Skip to content

Phase 26: Task Code Reorg & HTTP-Backed Agent Worker#57

Merged
SimplicityGuy merged 74 commits into
mainfrom
gsd/phase-26-task-code-reorg-http-backed-agent-worker
May 13, 2026
Merged

Phase 26: Task Code Reorg & HTTP-Backed Agent Worker#57
SimplicityGuy merged 74 commits into
mainfrom
gsd/phase-26-task-code-reorg-http-backed-agent-worker

Conversation

@SimplicityGuy
Copy link
Copy Markdown
Owner

Summary

Phase 26: Task Code Reorg & HTTP-Backed Agent Worker
Goal: SAQ task code is cleanly split between the application server (fileless phaze.tasks.controller) and agents (file-bound phaze.tasks.agent_worker), with role-driven startup and per-agent queues so the same Docker image runs both roles correctly. Three new internal-agent endpoints close the contract gap from Phase 25 so the full file-bound task surface can run on agents.
Status: Verified βœ“ (5/5 truths, 5/5 requirements, Nyquist 5/5 gaps resolved)

This phase converts the SAQ worker into a role-driven dual-mode service. PHAZE_ROLE=control boots a fileless application-server worker (generate_proposals, match_tracklist_to_discogs, search_tracklist, scrape_and_store_tracklist, refresh_tracklists cron) with Postgres access; PHAZE_ROLE=agent boots an agent worker (process_file, extract_file_metadata, fingerprint_file, scan_live_set, execute_approved_batch) with an HTTP client, no Postgres driver, and a per-agent phaze-agent-<agent_id> SAQ queue. Four new internal-agent endpoints (GET /whoami, PUT /analysis/{file_id}, POST /tracklists, PATCH /proposals/{id}/state) close the contract gap from Phase 25 so file-bound task bodies execute exclusively against the HTTP boundary with self-contained payloads. A subprocess import-boundary test enforces that phaze.database and sqlalchemy.ext.asyncio never enter the agent import graph.

Changes

Plan 01: Settings split + deps + enum extensions (Wave 1)

Adds tenacity runtime dep + respx dev dep + mypy strict overrides; full rewrite of phaze.config introducing Role / BaseSettings / ControlSettings / AgentSettings / get_settings() with fail-fast validation. Extends ProposalStatus (EXECUTED, FAILED) and FileState (MOVED, UNCHANGED).
Key files: pyproject.toml, src/phaze/config.py, src/phaze/models/proposal.py, src/phaze/models/file.py

Plan 02: PhazeAgentClient + error hierarchy + retries (Wave 2)

Single httpx.AsyncClient wrapper with 10 endpoint methods, a 4-class exception hierarchy (AgentApiError / AgentApiAuthError / AgentApiClientError / AgentApiServerError), tenacity AsyncRetrying retry funnel (4xx never retried; 5xx + ConnectError/Timeout retried up to 3 attempts), and respx contract tests. Bearer token lives only inside the client's headers, never as an instance attribute.
Key files: src/phaze/services/agent_client.py, tests/test_services/test_agent_client.py

Plan 03: Agent schemas package (Wave 2)

Five new Pydantic modules (agent_identity, agent_analysis, agent_tracklists, agent_proposals, agent_tasks) covering router request/response models and five SAQ-job payload models (extra="forbid", no current_path in any payload β€” enforced by test).
Key files: src/phaze/schemas/agent_*.py, tests/test_schemas/test_agent_*.py

Plan 04: AgentTaskRouter (Wave 3)

Per-agent SAQ Queue cache with lazy _queue_for(agent_id) lookup; two enqueue surfaces (by agent_id, by FileRecord); idempotent close() lifecycle.
Key files: src/phaze/services/agent_task_router.py, tests/test_services/test_agent_task_router.py

Plan 05: GET /api/internal/agent/whoami (Wave 3)

Returns AgentIdentity{agent_id, name, scan_roots, created_at} for the authenticated agent. Introduces the per-router smoke-app test pattern reused by Plans 06/07/08.
Key files: src/phaze/routers/agent_identity.py, tests/test_routers/test_agent_identity.py

Plan 06: PUT /api/internal/agent/analysis/{file_id} (Wave 3)

Idempotent upsert via pg_insert(...).on_conflict_do_update; deterministic _summarize_dict_to_string for top-3 mood/style compaction; overflow funnel to AnalysisResult.features JSONB for wire fields without dedicated columns.
Key files: src/phaze/routers/agent_analysis.py, tests/test_routers/test_agent_analysis.py

Plan 07: POST /api/internal/agent/tracklists (Wave 3)

Stripe-style request-id idempotency via Redis SET NX EX; three-path handling (fast-path cache / concurrent-writer poll β†’ 409 / owner DB path); atomic multi-row write of Tracklist + TracklistVersion + N TracklistTrack rows in a single commit.
Key files: src/phaze/routers/agent_tracklists.py, tests/test_routers/test_agent_tracklists.py

Plan 08: PATCH /api/internal/agent/proposals/{id}/state (Wave 3)

Table-driven state-machine transitions; cross-tenant guard returns 403 BEFORE state evaluation (prevents 409-vs-403 timing side-channel); joint Proposal + FileRecord mutation in a single await session.commit(); same-state PATCH is a no-op with zero DB writes.
Key files: src/phaze/routers/agent_proposals.py, tests/test_routers/test_agent_proposals.py

Plan 09: phaze.tasks.controller SAQ settings (Wave 4)

Fileless settings module: 4 tasks + refresh_tracklists cron; module-level Queue("controller"); startup banner logs role=control queue=controller (OPS-01 evidence); stashes ctx["queue"] for proposal/execution rate-limit readers.
Key files: src/phaze/tasks/controller.py, tests/test_tasks/test_controller_settings.py

Plan 10: phaze.tasks.agent_worker SAQ settings (Wave 5)

File-bound settings module: bounded exponential /whoami startup probe (1sβ†’32s, ≀63s budget); queue-name mismatch guard (token-derived agent_id vs PHAZE_AGENT_QUEUE); subprocess import-boundary test enforces phaze.database / sqlalchemy.ext.asyncio absence from the agent import graph (D-25).
Key files: src/phaze/tasks/agent_worker.py, tests/test_task_split.py, tests/test_tasks/test_agent_startup_banner.py

Plan 11: Rewrite 5 file-bound task bodies over HTTP (Wave 4)

process_file, extract_file_metadata, fingerprint_file, scan_live_set, execute_approved_batch now consume self-contained payloads via Payload.model_validate(kwargs) and call out via ctx["api_client"]. Extracts ExecutionStatus into a new DB-free phaze.enums package so agent schemas load without SQLAlchemy.
Key files: src/phaze/tasks/functions.py, src/phaze/tasks/metadata_extraction.py, src/phaze/services/fingerprint.py, src/phaze/services/scan.py, src/phaze/services/execution.py, src/phaze/enums/

Plan 12: FastAPI lifespan wiring (Wave 5)

Includes 4 new agent routers in create_app(); lifespan constructs app.state.task_router = AgentTaskRouter(redis_url=...) and app.state.redis (decode_responses=True) and closes both on shutdown. Refactors agent_files.upsert_files to use app.state.task_router.enqueue_for_agent (no more inline Queue construction).
Key files: src/phaze/main.py, src/phaze/routers/agent_files.py

Plan 13: Closing housekeeping β€” delete legacy modules (Wave 6)

Deletes src/phaze/tasks/worker.py (115 LOC legacy combined settings) and src/phaze/tasks/session.py (5 LOC deprecated stub). Rewires docker-compose.yml worker service to uv run saq phaze.tasks.controller.settings + PHAZE_ROLE=control; drops audfprint/panako from controller depends_on. Doc sweep replaces lux_worker hostname leak with controller across PROJECT.md + ROADMAP.md (D-33).
Key files: docker-compose.yml, PROJECT.md, ROADMAP.md, deletions of src/phaze/tasks/worker.py + src/phaze/tasks/session.py

Requirements Addressed

  • DIST-03 β€” Each agent pulls jobs from a per-agent SAQ queue named phaze-agent-<agent_id>; enqueuer routes via FileRecord.agent_id
  • TASK-01 β€” File-bound SAQ tasks run only on agents; bodies use HTTP client instead of async_session
  • TASK-02 β€” Fileless SAQ tasks run only on the application-server worker with direct Postgres access
  • TASK-03 β€” Agent task payloads are self-contained snapshots; no read-back from the application server during execution
  • OPS-01 β€” Same Docker image for both roles; PHAZE_ROLE={control,agent} selects SAQ settings module and startup resources

Verification

  • Automated verification: PASSED (5/5 truths, all required artifacts present, all key links wired, all data flows verified)
  • Behavioral spot-checks: 101 passed, 0 failed across schemas/routers/tasks/services
  • Subprocess import-boundary test (tests/test_task_split.py): 1 passed β€” confirms phaze.database / sqlalchemy.ext.asyncio absent from agent import graph
  • Nyquist validation audit: 5/5 gaps resolved (config role-split unit tests, PhazeAgentClient per-method respx tests, bearer-token-absent-from-WARNING-logs, original_path escape in execute_approved_batch, queue/token mismatch raises at agent startup)
  • Anti-patterns scan: no TODO/FIXME/PLACEHOLDER; no async_session reachable from agent code paths
  • Live-Redis tests (test_agent_task_router.py, test_agent_tracklists.py) skip β€” pre-existing infra gap (D-3), planned for CI Redis sidecar per D-30

Key Decisions

  • Role dispatch via PHAZE_ROLE (D-14): get_settings() returns ControlSettings() or AgentSettings() at construction time; back-compat alias Settings = ControlSettings preserved so existing from phaze.config import Settings call sites keep type-checking until migrated
  • Bearer token never stored as instance attribute (T-26-02-I): lives only inside httpx.AsyncClient.headers
  • Tenacity AsyncRetrying async-iterator (not @retry decorator): cleaner integration for post-loop 4xx/5xx status-code mapping
  • dict[str, Queue] cache, not functools.cache or LRU: LRU eviction without .disconnect() would leak Redis connections; bounded growth not needed at v4.0's 1–5 agent scale
  • Stripe-style request-id idempotency via Redis SET NX EX with 1h TTL; concurrent-writer poll bounded at 10Γ—50ms then 409
  • Cross-tenant guard BEFORE state-machine (T-26-08-S2): prevents timing side-channel via 409 vs 403
  • Overflow funnel pattern (Plan 06): wire-format fields without dedicated columns (danceability, energy) merge into the row's features JSONB instead of being dropped β€” preserves D-26 wire contract without an Alembic migration
  • phaze.enums package: ExecutionStatus extracted DB-free so agent schemas load without sqlalchemy / phaze.database β€” the D-03 import boundary holds for the agent worker
  • Bounded exponential /whoami startup probe (1sβ†’32s, ≀63s wall-clock): RuntimeError on exhaustion; queue-name mismatch guard catches PHAZE_AGENT_QUEUE vs token-derived agent_id misconfig

πŸ€– Generated with Claude Code

SimplicityGuy and others added 30 commits May 12, 2026 13:53
Pulls the Phase 26 planning files (PLAN, CONTEXT, RESEARCH, PATTERNS,
VALIDATION, DISCUSSION-LOG) plus updated STATE/ROADMAP/REQUIREMENTS from
main into this worktree so plan execution has the canonical references.
Wave 0 foundation (Task 1 of Plan 26-01, D-11 + D-31 + D-33):

- Add tenacity>=8.5.0 to [project].dependencies (runtime retry decorator
  for PhazeAgentClient HTTP methods in Plan 02).
- Add respx>=0.21.1 to [dependency-groups].dev (httpx mock library for
  PhazeAgentClient contract tests in Plan 02).
- Add [[tool.mypy.overrides]] blocks for phaze.services.agent_client and
  phaze.services.agent_task_router that opt them into strict checking
  (disallow_untyped_defs, check_untyped_defs, warn_return_any,
  strict_equality) despite the global services/ exclude. New code in
  these two modules will be fully type-checked.
- Regenerate uv.lock with the new constraints.

The global mypy exclude = "^(tests/|prototype/|services/)" is unchanged
so the rest of services/ continues to iterate without strict gates.
Wave 0 foundation (Task 2 of Plan 26-01):

- Add `Role` StrEnum with values "control" and "agent".
- Split the legacy `Settings` class into:
  * `BaseSettings(PydanticBaseSettings)` β€” shared fields (database_url,
    redis_url, debug, scan_path, models_path, output_path, worker_*,
    audfprint_url, panako_url, discogsography_url, api_host/port,
    agent_token_prefix, agent_file_chunk_max).
  * `ControlSettings(BaseSettings)` β€” application-server role: Discogs
    concurrency + LLM proposal generation fields.
  * `AgentSettings(BaseSettings)` β€” file-server role: `agent_api_url`,
    `agent_token: SecretStr`, `scan_roots: list[str]`. A
    `model_validator(mode="after")` raises ValueError at construction
    time if any is missing/empty so the agent worker fails fast rather
    than silently producing 401s or path-traversal rejections at
    runtime (D-14 + threat model T-26-01-T2 mitigation).
- Add `@lru_cache(maxsize=1) def get_settings() -> BaseSettings`: reads
  `PHAZE_ROLE` env (default "control") and dispatches to the right
  subclass.
- Replace `settings = Settings()` with `settings: ControlSettings = ...`
  module-level singleton (annotated as ControlSettings so existing
  call sites that read `settings.llm_*` / `settings.discogs_match_concurrency`
  still type-check). Module-level singleton stays Control-typed; the
  agent worker (Plan 10) calls `get_settings()` / `AgentSettings()`
  directly and stashes the instance at ctx["agent_settings"].
- Add `Settings = ControlSettings` back-compat alias for test files
  that still import the legacy class name.

Deviations from the literal plan text (Rule 2 β€” missing critical
functionality):

- pydantic-settings v2 does NOT natively comma-split `PHAZE_AGENT_SCAN_ROOTS
  =/a,/b` into `["/a", "/b"]`. It expects a JSON-encoded list and raises
  JSONDecodeError on bare comma-strings. Fix: annotate the field as
  `Annotated[list[str], NoDecode]` so pydantic-settings skips the JSON
  decode step, then add a `@field_validator("scan_roots", mode="before")`
  classmethod `_split_scan_roots` that comma-splits string input while
  passing native list input through unchanged.
- pydantic-settings reads env vars by *field name* (case-insensitive)
  absent an `env_prefix`. The documented env-var names `PHAZE_AGENT_*`
  required explicit `validation_alias=AliasChoices(...)` per field so
  both the documented env-var form and direct kwargs work.

Module-level `settings = get_settings()` keeps every existing
`from phaze.config import settings` call site (37+ across src/ and
tests/) working unchanged. Tests pass: 593 routers+services, 102
models+config-worker+phase01-gaps. Full repo mypy clean.
Wave 0 foundation (Task 3 of Plan 26-01):

- ProposalStatus gains EXECUTED + FAILED. These are the terminal targets
  of the PATCH /api/internal/agent/proposals/{id}/state router (Plan 08).
  State machine: APPROVED -> EXECUTED or APPROVED -> FAILED. Re-PATCHing
  the same terminal value is a 200 idempotent no-op; any other transition
  returns 409.
- FileState gains MOVED + UNCHANGED. MOVED pairs with ProposalStatus.
  EXECUTED (file successfully copy-verified-deleted at new path);
  UNCHANGED pairs with ProposalStatus.FAILED (file stays at original
  path). The pre-existing FileState.EXECUTED + FAILED values are
  retained for Phase 25-era execution-log emit paths; Phase 28's batch
  execution will adopt MOVED/UNCHANGED when wiring the PATCH endpoint
  into the live dispatch loop.

No alembic migration needed:
- Proposal.status uses String(20): "executed" (8) and "failed" (6) fit.
- FileRecord.state uses String(30): "moved" (5) and "unchanged" (9) fit.

StrEnum values store as plain strings β€” adding values widens the
accepted value set without altering DDL. Existing 83 model tests pass.
Captures the outcome of Plan 26-01 (tenacity+respx deps, settings
split into BaseSettings + ControlSettings + AgentSettings with
fail-fast scan_roots validator, enum extensions ProposalStatus.
EXECUTED|FAILED + FileState.MOVED|UNCHANGED) and advances STATE.md:

- Current Position now points to Phase 26 / Plan 01 complete, ready
  for Plan 02 (PhazeAgentClient + AgentApiError).
- Resume file updated to 26-02-PLAN.md.
- Four [Phase 26-01]: decisions appended to Accumulated Context
  documenting the pydantic-settings v2 env-var quirks discovered
  during execution (no native comma-split for list[str]; no automatic
  PHAZE_AGENT_* prefix mapping) and the back-compat alias choices.
- Progress recalculated to 14/26 plans (54%).

The SUMMARY.md records all four deviations (two Rule-2 fixes for
pydantic-settings quirks, two Rule-1 fixes for type-system and
test-import regressions) with their exact rationale.
- 9 respx-mocked async tests covering D-09..D-13 invariants
- Asserts 4xx never retried (call_count == 1) for 401/403/404/422
- Asserts 5xx retries 3 times (call_count == 3) on persistent 500
- Asserts 5xx-then-200 recovery (call_count == 2)
- Asserts ConnectError retries 3 times (call_count == 3)
- Asserts Authorization: Bearer <token> header injected
- whoami parses AgentIdentity model from response JSON

Tests fail at import (ModuleNotFoundError) until Task 2 ships
phaze.services.agent_client and Plan 03 ships agent_analysis +
agent_identity schemas.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
RED phase for Task 1 of Plan 26-03. Tests target modules that don't exist yet:
- tests/test_schemas/test_agent_identity.py       (AgentIdentity β€” D-15)
- tests/test_schemas/test_agent_analysis.py       (AnalysisWritePayload/Response β€” D-26)
- tests/test_schemas/test_agent_tracklists.py     (TracklistCreatePayload + nested β€” D-27, T-26-07-DoS)
- tests/test_schemas/test_agent_proposals.py      (ProposalStatePatch/Response β€” D-28)

Covers:
- extra='forbid' on request bodies and nested items
- Literal value validation against ProposalStatus/FileState enum string values
- _require_path_when_moved conditional validator
- max_length=2000 cap on tracks (T-26-07-DoS)
- Optional-everything semantics on AnalysisWritePayload
- bpm >= 0, danceability/energy in [0, 1] bounds
…-26, D-27, D-28)

GREEN for Task 1 of Plan 26-03. Implements the wire-level Pydantic models
consumed by Wave 2 routers (Plans 05-08) and the PhazeAgentClient (Plan 02):

- agent_identity.py: AgentIdentity (response-only, loose schema for /whoami)
- agent_analysis.py: AnalysisWritePayload + AnalysisWriteResponse (PUT /analysis,
  all fields optional for partial-PUT, extra='forbid' + ge/le bounds)
- agent_tracklists.py: TracklistTrackPayload + TracklistCreatePayload
  (POST /tracklists, request_id idempotency, tracks min=1/max=2000 per
  T-26-07-DoS) + TracklistCreateResponse
- agent_proposals.py: ProposalStatePatch with _require_path_when_moved
  model_validator (current_path mandatory when file_state='moved') +
  ProposalStateResponse. Literal values mirror ProposalStatus.EXECUTED/FAILED
  + FileState.MOVED/UNCHANGED enum string values from Plan 26-01.

Every request/nested schema sets ConfigDict(extra='forbid') per Phase 25 D-16
(unknown body keys -> 422). Response schemas stay loose for forward-compat.

All 34 unit tests pass; mypy strict + ruff clean.
RED for Task 2 of Plan 26-03. Tests the 6 models that will land in
src/phaze/schemas/agent_tasks.py: ProcessFilePayload, ExtractMetadataPayload,
FingerprintFilePayload, ScanLiveSetPayload, ExecuteApprovedBatchPayload, and
the nested ExecuteBatchProposalItem.

Covers D-22..D-24 invariants:
- D-22: only ProcessFilePayload carries models_path
- D-23: ExecuteApprovedBatchPayload is fully self-contained
        (no DB read-back mid-job; B2 Option A)
- D-24: NO payload has a current_path field (agents work off original_path)
- min/max_length=500 on proposals list (DoS hardening)
- JSON round-trip equality preserved
- extra='forbid' on all 6 classes
…4-class error hierarchy (GREEN)

Implements D-09..D-13 + D-31/32/33:

- PhazeAgentClient mirrors DiscogsographyClient pattern (services/discogs_matcher.py).
- Constructor injects bearer token as default Authorization header on the
  underlying httpx.AsyncClient -- token is NEVER stored as an instance
  attribute (T-26-02-I mitigation, D-13).
- _request funnel applies tenacity AsyncRetrying with stop_after_attempt(3),
  wait_exponential_jitter(initial=0.5, max=4.0), retry_if_exception(_should_retry).
- _should_retry returns True for ConnectError, ReadTimeout, WriteTimeout, and
  5xx HTTPStatusError. NEVER returns True for 4xx (D-11, D-32).
- 4-class exception hierarchy (D-12): AgentApiError base + AgentApiAuthError
  (401/403, no retry) + AgentApiClientError (other 4xx, no retry) +
  AgentApiServerError (5xx + persistent network, after retry exhaustion).
- DEBUG on success, WARNING on failure; log format strings exclude the token
  (T-26-02-I, D-13).
- 10 endpoint methods (D-10): whoami, upsert_files, put_metadata,
  put_fingerprint, put_analysis, create_tracklist, post_execution_log,
  patch_execution_log, patch_proposal_state, heartbeat.
- PUT/PATCH methods use exclude_unset=True per Phase 25 CR-01 partial-update
  semantics. POST + heartbeat use full bodies.
- _client kwarg supports respx test injection (leading underscore = private).
- Plan 03 schema imports gated behind TYPE_CHECKING + lazy method-body imports
  so the module loads independent of merge order; type: ignore[import-not-found]
  markers self-delete via warn_unused_ignores once Plan 03 lands.

Verified:
- uv run ruff check src/phaze/services/agent_client.py exits 0
- uv run mypy src/phaze/services/agent_client.py exits 0 (strict per
  pyproject.toml [[tool.mypy.overrides]] for the agent_client module)
- Contract tests fail at import for Plan 03 schemas (agent_analysis,
  agent_identity, agent_proposals, agent_tracklists) -- GREEN gate satisfied
  at Wave 2 merge per plan body acceptance criteria.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-24)

GREEN for Task 2 of Plan 26-03. Ships the typed payload models used by every
file-bound SAQ task in Plan 11. Task bodies will call <Payload>.model_validate
(ctx kwargs) at entry, applying the same extra='forbid' strictness as HTTP
request bodies (D-16).

Payloads:
- ProcessFilePayload         (essentia analysis; only payload with models_path)
- ExtractMetadataPayload     (mutagen tag extraction)
- FingerprintFilePayload     (audfprint + panako submission)
- ScanLiveSetPayload         (live-set fingerprint resolution)
- ExecuteApprovedBatchPayload (per-agent execution sub-batch β€” B2 Option A)
- ExecuteBatchProposalItem    (nested: per-proposal copy+verify+delete details)

Invariants enforced:
- D-22: only ProcessFilePayload carries models_path
- D-23: ExecuteApprovedBatchPayload is fully self-contained (proposals carry
        original_path + proposed_path + optional sha256_hash so the agent
        never reads DB state mid-job)
- D-24: NO payload carries current_path anywhere
- Field(min_length=1, max_length=500) caps per-job batch size
- All 6 classes set ConfigDict(extra='forbid')

`from __future__ import annotations` omitted because Pydantic resolves the
uuid.UUID field annotation at runtime; deferred annotations would trigger
ruff TC003 on the runtime-needed uuid import (mirrors agent_metadata.py pattern).

22 unit tests pass; mypy strict + ruff clean.
- 26-02-SUMMARY.md captures contract-test invariants, retry policy values,
  mypy-strict-override confirmation (RESEARCH A8 / A12), and the 3 auto-fix
  deviations (PLC0415 noqa, import-not-found ignore, uuid TC003 hoist).
- STATE.md advances Current Plan to 02 (complete), records the 3
  Plan-26-02 decisions, and bumps progress to 15/26 plans (58%).
- ROADMAP.md updated for Phase 26 plan-progress row (2/13 plans complete).
- REQUIREMENTS.md marks TASK-02 + TASK-03 complete with traceability.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… + 5 SAQ schemas

Plan 26-03 ships 5 new Pydantic schema modules (4 HTTP-boundary + 1 SAQ-payload bundle)
with 56 unit tests, all green. mypy strict + ruff clean across all modules.
Pre-commit hooks pass on all files. 582 non-integration tests pass overall.

Two auto-fix deviations folded into GREEN commits (both Rule 1 β€” ruff lint
fixes to the plan's verbatim source): removed unused `# noqa: TC003` in
agent_identity.py; dropped `from __future__ import annotations` in
agent_tasks.py to avoid TC003 firing on runtime-needed `import uuid`.

Closes: Plan 26-03 of Phase 26 (TASK-02, TASK-03, DIST-03 β€” partial; full
completion deferred to phase-end verifier).
Plan 03 has merged, so the parallelization-debt `# type: ignore[import-not-found]`
comments on phase 26 schema imports are now unused. `warn_unused_ignores` flags
them as errors and blocks every commit on this branch. Removing per the original
comment intent ("removed once Plan 03 merges").

Rule 3 (blocking issue) deviation -- minimal, surgical cleanup required to
proceed with Plan 26-04.
- 4 integration tests covering per-agent queue isolation, lazy cache
  identity, close() drain, and enqueue_for_file delegation
- Uses real Redis via PHAZE_REDIS_URL env (default redis://localhost:6379/0)
- Marked @pytest.mark.integration per D-30; skips cleanly when Redis is down
- Module import currently fails (ModuleNotFoundError) -- RED confirmed
- 8 contract tests covering happy path, replay idempotence, partial-PUT
  field-level LWW (CR-01 invariant), empty-body no-op, first-PUT-with-
  empty-body row creation, 422 on extra fields (AUTH-01 spoof block),
  401 missing auth, 403 unknown token.
- Mirrors test_agent_metadata.py smoke-app pattern with _seed_file FK
  helper (AnalysisResult.file_id FKs files.id).
- Asserts mood/style dict -> summary string conversion at storage
  boundary (per D-26 storage discretion area).
- Collection fails with ModuleNotFoundError until Task 2 ships the
  router module (RED state confirmed).

Deviation (Rule 3 -- auto-fix blocking issue): removed four
`# type: ignore[import-not-found]` tripwires in services/agent_client.py.
These were intentional self-deleting markers placed by Plan 03's author;
now that Plan 03 has merged the schema modules they reference, mypy with
`warn_unused_ignores=true` correctly errors on the now-resolvable imports.
Removing them unblocks the pre-commit mypy gate for this and all
subsequent commits in Phase 26 Wave 3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e (GREEN)

Controller-side service that enqueues file-bound SAQ jobs onto the right
per-agent queue. Replaces the inline Queue.from_url + try/finally pattern
at agent_files.py:99-117 with a reusable service whose Queue instances are
cached per-agent (one Redis connection pool per agent, reused across enqueues).

API:
- AgentTaskRouter(redis_url) -- stores URL + initializes empty cache
- _queue_for(agent_id) -- lazy Queue.from_url with name=phaze-agent-<id> (D-18)
- enqueue_for_agent(*, agent_id, task_name, payload) -- enqueue via model_dump
- enqueue_for_file(*, file_record, task_name, payload) -- delegate via FileRecord.agent_id
- close() -- disconnect every cached Queue and clear cache (idempotent)

Decisions: D-19 (class shape), D-20 (lifespan-wired in Plan 12),
D-21 (replaces inline enqueue in agent_files.py in Plan 12),
D-30 (real-Redis integration tests), D-33 (mypy strict opt-in).

All 4 integration tests pass against a real Redis instance.
…s in agent_client.py

Plan 26-02's PhazeAgentClient annotated 4 Plan 03 schema imports with
'# type: ignore[import-not-found]' parallelization debt, with a comment
explicitly declaring them a self-deleting tripwire that would fire once
Plan 03 schemas landed.

Plan 03 merged via 6ae8a49 (5 schema modules including agent_analysis,
agent_identity, agent_proposals, agent_tracklists). The four 'type: ignore'
comments are now unused -- mypy's 'warn_unused_ignores' flag turns this
into a hard failure (Rule 3 blocker preventing Plan 08 commits).

Removed all four ignore directives. Mypy now exits clean. No runtime change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nt (Rule 3)

Plan 26-02 (PhazeAgentClient) added `# type: ignore[import-not-found]`
to five schema imports as self-deleting tripwires, intended to fire
unused-ignore errors once Plan 26-03 (schemas) merged so the cleanup
would be obvious.

Plan 26-03 merged at 6ae8a49 but the cleanup did not happen, so every
subsequent commit fails the local mypy pre-commit hook with 4
`Unused "type: ignore" comment [unused-ignore]` errors -- blocking
Wave 3 plans 26-04..26-08.

Removing the ignores (and their stale explanatory comments) restores
mypy green and unblocks Plan 26-05 (this plan).

Rule 3 deviation: scope-adjacent to Plan 26-05's `files_modified`
but required to commit at all (pre-commit mypy is mandatory).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s/{id}/state (RED)

11 contract tests for the joint Proposal+FileRecord state-transition endpoint
(D-28). Covers:

- APPROVED -> EXECUTED joint update (proposal_state + file_state=moved + current_path)
- APPROVED -> FAILED joint update (proposal_state + file_state=unchanged)
- Same-state PATCH idempotent no-op (EXECUTED -> EXECUTED returns 200)
- Illegal transitions (EXECUTED -> FAILED, PENDING -> EXECUTED -> 409)
- 404 on unknown proposal_id
- 422 on extra field (extra='forbid' from ProposalStatePatch)
- 422 on moved-without-current_path (_require_path_when_moved validator)
- 401 missing bearer, 403 unknown token
- Cross-agent 403 (W1 / T-26-08-S2): agent B cannot mutate agent A's proposal

Uses smoke-app pattern (mirrors test_agent_execution.py) so tests don't depend
on Plan 12 wiring the router into main.py. Confirms RED: ImportError because
phaze.routers.agent_proposals does not exist yet (implemented in Task 2).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 4 contract tests: happy path (200 + AgentIdentity body),
  missing header -> 401 (with WWW-Authenticate: Bearer per RFC 6750),
  unknown token -> 403, revoked-mid-session -> 403 (AUTH-04).
- Uses Phase 25 per-router smoke-app pattern
  (mirrors tests/test_routers/test_agent_metadata.py:30-38) so the
  suite is independent of Plan 12's main.py wiring.
- Re-uses session + seed_test_agent fixtures from tests/conftest.py.

Currently RED: import of phaze.routers.agent_identity fails because
the router module has not been created yet (implemented in Task 2
of this plan).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The four ``# type: ignore[import-not-found]`` comments in agent_client.py
were placed by Plan 26-02 as a deliberate self-deleting tripwire (see
the inline comment) -- once Plan 26-03 lands the schema modules,
mypy's ``warn_unused_ignores`` flags the comments as unused and fails
the local hook. Plan 26-03 merged into the phase branch in Wave 2;
removing the ignores unblocks Wave 3 plans (including this one, 26-07)
from running their pre-commit suite.

Rule 3 scope-blocker fix: pre-existing mypy failure in a file not
authored by Plan 26-07. Surgical removal of dead comments only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 7 integration tests covering happy path, idempotent replay, multi-version
  promotion via new request_id, extra-field 422, too-many-tracks 422
  (T-26-07-DoS), 401, 403
- Smoke-app fixture pattern + real-Redis fixture with scan_iter cleanup
- Tests fail RED until router lands (ImportError: phaze.routers.agent_tracklists)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…EEN)

Mirror of agent_metadata.py for AnalysisResult. Key behaviors:

- pg_insert + on_conflict_do_update on AnalysisResult.file_id (UQ from
  models/analysis.py:18) for idempotent upsert.
- body.model_dump(exclude_unset=True) so only fields the caller set land
  in the UPDATE SET clause (Phase 25 CR-01 field-level LWW invariant).
- Empty body -> on_conflict_do_nothing branch (avoids Postgres "empty
  SET" syntax error; new rows still get an INSERT with all NULL fields).
- payload["id"] = uuid.uuid4() stamped explicitly because
  AnalysisResult.id has a Python-only default that pg_insert bypasses.
- agent_id sourced from auth dep, never from body (AUTH-01).
- Depends(get_authenticated_agent) -> 401/403 surface honored.

Storage representation:
- mood/style: dict[str, float] -> top-3 "k=v,k=v" summary string via
  _summarize_dict_to_string (bounded 50 chars to fit existing String(50)
  columns; deterministic alphabetical tiebreak on equal scores).
- Overflow funnel (Rule 1 + Rule 3 -- plan-discretion area): wire-format
  fields without a dedicated column (`danceability`, `energy`) are
  merged into the existing `features` JSONB column rather than dropped,
  preserving D-26's wire contract without an Alembic migration. Plan
  task originally asserted `row.danceability == 0.8` directly; the
  model has no such column, so RED-tests were updated to assert against
  `row.features["danceability"]` to match the storage funnel.

Tests: 8/8 contract tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… (GREEN)

Joint Proposal + FileRecord state-transition endpoint per D-28. Mirrors the
agent_execution.py PATCH handler (Phase 25) for structural symmetry:

- Table-driven transitions via _PROPOSAL_TRANSITIONS (not monotonic ladder --
  D-28 has only one allowed from-state, APPROVED -> {EXECUTED, FAILED})
- Idempotent same-state PATCH: returns 200 echoing current row state with NO
  DB writes (canonical SAQ retry case for terminal states)
- Joint Proposal + FileRecord update in ONE session.commit() (Pitfall 6)
- 409 with detail 'illegal transition {cur} -> {new}' for any other transition
- 404 when proposal_id not found
- W1 / T-26-08-S2: cross-tenant guard via FileRecord.agent_id check; returns
  403 BEFORE state-machine logic so a leaked proposal_id cannot be timing-probed
  via 409 vs 403
- Auth gated by Depends(get_authenticated_agent) -- 401/403 inherited from
  HTTPBearer + agent_auth.get_authenticated_agent

Also fixes a Rule 1 bug in test_agent_proposals.py: AsyncSession.expire_all()
is a sync method (not async); the original 'await session.expire_all()' raised
TypeError. Stripped 'await' from all three callsites.

All 11 contract tests pass:
- test_executed_joint_update
- test_failed_joint_update
- test_same_state_idempotent_no_op
- test_illegal_transition_409
- test_pending_to_executed_409
- test_proposal_not_found_404
- test_proposal_extra_field_422
- test_moved_without_current_path_422
- test_proposal_cross_agent_403 (W1)
- test_proposal_missing_auth_returns_401
- test_proposal_unknown_token_returns_403

Router NOT wired into main.py here -- that lands in Plan 26-12.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six test invocations exercising every documented edge case of the
helper that converts dict[str, float] -> "k=v,k=v,k=v" summary string
for mood/style storage in AnalysisResult's String(50) columns:

- Empty dict -> ""
- Single-key dict -> one entry, no comma
- 3-key dict -> top-3 sorted by score descending
- 10-key dict -> top-3 only (truncation contract)
- Identical scores -> alphabetical tiebreak (determinism invariant
  pinned to the `(-score, key)` two-key sort)
- Long keys -> hard 50-char cap fires

The alphabetical-tiebreak case prevents regression to the older
`reverse=True` single-key sort that would tiebreak by insertion order.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 26-04-SUMMARY.md recording the lazy-cache implementation choice
  (plain dict over functools.cache / LRU), real-Redis integration test
  fixture pattern, and the 1 deviation (Rule 3 unblocker for stale
  type:ignore tripwires in agent_client.py)
- STATE.md advances Current Plan to 04 (complete); 4/26 plans of v4.0
  done -> 19/26 plans overall (73%)
- ROADMAP.md flips 26-03 + 26-04 checkboxes (26-03 SUMMARY already on disk
  from prior merge); progress 4/13 plans in phase 26
… idempotency (GREEN)

POST /api/internal/agent/tracklists -- idempotent atomic create of
Tracklist + new TracklistVersion + N TracklistTrack rows in a single
transaction. Three-path Stripe-style idempotency keyed on body.request_id:

  1. Fast path: tracklist_resp:{request_id} cached JSON -> return without DB work
  2. Concurrent-writer path: SET NX lost -> poll resp_key 10x50ms -> 409 on timeout
  3. Owner path: SET NX won -> UPSERT Tracklist + version_number+1 + INSERT tracks
     + UPDATE latest_version_id pointer + commit + cache response

Key choices documented in 26-07-SUMMARY (to follow):
- No payload-hash check on cached replays (T-26-07-T accept, single-operator trust)
- max_length=2000 on tracks lives in schemas/agent_tracklists.py (Plan 26-03)
- Redis client pulled via `request.app.state.redis` (Plan 26-12 wires the lifespan)

Implements: D-27, TASK-03
Resolves RED: tests/test_routers/test_agent_tracklists.py (7 integration tests GREEN)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SimplicityGuy and others added 25 commits May 12, 2026 15:37
…FastAPI lifespan

- Add include_router calls for agent_identity, agent_analysis, agent_tracklists,
  agent_proposals (Phase 26 D-15, D-26, D-27, D-28). All 10 /api/internal/agent/*
  endpoints are now reachable from the production create_app() factory.
- Wire AgentTaskRouter at app.state.task_router (D-20); used by agent_files.py's
  auto-enqueue path in Task 2.
- Wire shared async Redis client at app.state.redis with decode_responses=True
  for the tracklists idempotency cache (D-27); agent_tracklists.py reads it via
  request.app.state.redis.
- Lifespan shutdown closes both new resources before the existing default queue
  (reverse construction order).

Verifies: 10 unique agent route paths enumerated from create_app(); all 31 Phase 25
router tests still green; mypy + ruff clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Launches a clean Python subprocess to import phaze.tasks.agent_worker.
- Asserts phaze.database, phaze.tasks.session, sqlalchemy.ext.asyncio
  are absent from sys.modules.
- Runs on every CI build (no marks, no skips) β€” Phase 26 structural invariant.
- Currently RED: ModuleNotFoundError until Task 2 creates agent_worker.py.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… role (D-01..D-04, D-16)

Companion to phaze.tasks.controller (Plan 09). Boots the file-server role
under PHAZE_ROLE=agent. Six-step startup:
1. Models check (essentia .pb files).
2. Construct PhazeAgentClient(base_url, token, timeout=30.0).
3. /whoami probe with bounded exponential backoff (1sβ†’32s, ≀63s total).
4. Queue-name mismatch guard β€” token-derived agent_id MUST match
   operator-supplied PHAZE_AGENT_QUEUE env (anti-misconfig probe).
5. FingerprintOrchestrator(AudfprintAdapter + PanakoAdapter) stashed at
   ctx["fingerprint_orchestrator"] (B1 β€” Plan 11 readers).
6. CPU-bound essentia process pool.

functions list contains exactly the 5 file-bound tasks:
  process_file, extract_file_metadata, fingerprint_file, scan_live_set,
  execute_approved_batch.

Module-level Queue.from_url(redis_url, name=PHAZE_AGENT_QUEUE) β€” env-driven
at import time per RESEARCH Pitfall 7. Module exits non-zero if
PHAZE_AGENT_QUEUE is unset.

D-13 invariant: bearer never logged. Banner emits a 12-char preview
(`phaze_agent_...`) under a non-secret format key (`auth_id_prefix=`).
The variable is named `token_preview` for grep-ability of the D-13
intent across the codebase.

D-25 import-boundary test (tests/test_task_split.py from Task 1) now
passes β€” no phaze.database / phaze.tasks.session / sqlalchemy.ext.asyncio
in sys.modules after `import phaze.tasks.agent_worker`. Structural
invariant of Phase 26 is enforced.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ent_files.upsert_files

Phase 26 D-20 / D-21 -- the auto-enqueue path in agent_files.upsert_files no
longer constructs a SAQ Queue per request. Instead it reads the lifespan-wired
AgentTaskRouter at request.app.state.task_router and calls
enqueue_for_agent(agent_id=..., task_name="extract_file_metadata", payload=...).

- Handler signature gains `request: Request` (positional, before `body`).
- `from saq import Queue` import removed; `from fastapi import Request` and
  `from phaze.schemas.agent_tasks import ExtractMetadataPayload` added.
- UPSERT RETURNING extended with `FileRecord.original_path` so the payload
  can be built without a re-query.
- The handler no longer owns the Queue lifecycle -- close() runs once in the
  FastAPI lifespan shutdown via app.state.task_router.close().

Test fixture (tests/test_routers/test_agent_files.py) migrates from
`patch("phaze.routers.agent_files.Queue")` to installing an AsyncMock at
`app.state.task_router` on the smoke-app. Assertions now inspect
`mock_router.enqueue_for_agent.await_args_list` and verify the
ExtractMetadataPayload's typed fields (file_id, original_path, file_type,
agent_id). All 9 existing tests continue to pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Asserts the startup banner contains module name, role=agent,
  agent_id=<value>, and the 12-char token preview `phaze_agent_...`.
- Asserts the full bearer-token secret bytes never appear in the log
  output (D-13 invariant).
- Heavy constructors (PhazeAgentClient, process pool, fingerprint
  adapters/orchestrator, models check) are monkeypatched so the test
  runs in-memory with no Postgres/Redis/.pb files required.
- Mirrors tests/test_tasks/test_controller_startup_banner.py (Plan 09)
  with agent-specific assertions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…P + REQUIREMENTS

Plan 26-12 wired the four Phase 26 agent routers (whoami, analysis, tracklists,
proposals) into create_app(), installed AgentTaskRouter + async Redis client at
app.state.task_router / app.state.redis in the FastAPI lifespan, and refactored
agent_files.upsert_files off the inline Queue.from_url pattern. Marks DIST-03
requirement complete.

- SUMMARY: 26-12-SUMMARY.md (created)
- STATE: current_plan advanced 11 -> 12; progress 92% (24/26); metric recorded
- ROADMAP: phase 26 progress 11/13 -> in progress (Plan 13 outstanding)
- REQUIREMENTS: DIST-03 marked complete

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add 26-10-SUMMARY.md documenting agent_worker.py + import-boundary test (D-25)
  + banner test (D-13/OPS-01).
- Advance STATE.md plan counter (10 of 13 done; in-progress).
- Mark TASK-01, DIST-03 complete in REQUIREMENTS.md (OPS-01 already marked).
- Update ROADMAP.md plan progress (11/13 summaries).
- Record three key decisions: import-boundary structural invariant,
  format-key rename to avoid secret-detector false-positive, and
  /whoami retry budget + queue-name mismatch guard semantics.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…or (Wave 5)

# Conflicts:
#	.planning/ROADMAP.md
#	.planning/STATE.md
…e to controller.settings

Closes Phase 26 D-04 + D-06 + D-08:

- DELETED src/phaze/tasks/worker.py (115 lines) -- replaced by phaze.tasks.controller
  (fileless, control role) and phaze.tasks.agent_worker (file-bound, agent role).
- DELETED src/phaze/tasks/session.py (5 lines) -- the legacy v1.0 session-helper stub;
  both new SAQ settings modules build their own session pool in their startup hooks.
- Updated docker-compose.yml worker service:
    command: uv run saq phaze.tasks.controller.settings   (was: phaze.tasks.worker.settings)
    environment: + PHAZE_ROLE=control
    depends_on: dropped audfprint + panako (controller is fileless per D-04)
- Updated test references that imported from phaze.tasks.worker / phaze.tasks.session:
    - tests/test_tasks/test_worker.py        DELETED (covered by test_controller_startup_banner.py)
    - tests/test_tasks/test_session.py       DELETED (covered by test_task_split.py D-25 invariant)
    - tests/test_tasks/test_pool.py          stripped worker.startup/shutdown tests (3); kept
                                             pool-helper tests (2) which still exercise
                                             phaze.tasks.pool (used by agent_worker).
    - tests/test_tasks/test_proposal.py      retargeted test_worker_* -> test_controller_*
    - tests/test_tasks/test_tracklist.py     retargeted test_worker_* -> test_controller_*
    - tests/test_phase04_gaps.py             models-dir checks retargeted to agent_worker
                                             (the new owner per D-04); docker-compose-command
                                             assertion now expects controller.settings.
- Logged D-3 in deferred-items.md: agent_task_router + agent_tracklists tests need a live
  Redis; pre-existing flakiness independent of Plan 26-13.

Acceptance: `uv run pytest tests/test_task_split.py tests/test_tasks/ tests/test_phase04_gaps.py
-x --no-cov` => 62 passed; `uv run mypy src/` clean; `uv run ruff check .` clean;
`docker compose config -q` exits 0.
…roller

Per Phase 26 D-02 / D-33, replaces every forward-looking reference to
`phaze.tasks.lux_worker` (the original roadmap name that leaked the
application-server hostname "lux") with `phaze.tasks.controller`. The new
name pairs cleanly with `PHAZE_ROLE=control` and reads naturally without
betraying physical topology.

Sweep targets defined by D-33 (5 files):
- .planning/PROJECT.md           β€” milestone v4.0 task-code-reorg bullet (1 hit)
- .planning/ROADMAP.md           β€” Phase 26 plan-13 line + Phase 29 success-criterion #1
                                   (2 hits)
- .planning/REQUIREMENTS.md      β€” clean (already used `controller` / `agent_worker`)
- .planning/STATE.md             β€” clean (no historical lux_worker references)
- .planning/phases/25-internal-agent-http-api-bearer-auth/25-CONTEXT.md
                                   β€” clean

Historical records preserved (intentionally untouched):
- .planning/phases/26-…/26-13-PLAN.md
- .planning/phases/26-…/26-10-SUMMARY.md
- .planning/phases/26-…/26-CONTEXT.md
- .planning/phases/26-…/26-DISCUSSION-LOG.md

These document what was planned and decided at the time and are part of the
audit trail; per Plan 26-13 Task 2 explicit guidance, historical SUMMARY /
CONTEXT / DISCUSSION-LOG files are not rewritten.

Verification:
  $ grep -rn 'lux_worker' .planning/ROADMAP.md .planning/REQUIREMENTS.md \
      .planning/STATE.md .planning/PROJECT.md \
      .planning/phases/25-internal-agent-http-api-bearer-auth/25-CONTEXT.md
  # β†’ 0 matches
…ROADMAP

Plan 26-13 final metadata commit:

- Adds .planning/phases/26-…/26-13-SUMMARY.md (closing housekeeping plan summary;
  deletions + docker-compose + doc sweep details; deviations + threat-flag scan)
- STATE.md advanced to Plan 13/13 (100% v4.0-phase-26 progress); decision recorded
- ROADMAP.md updated by roadmap.update-plan-progress: Phase 26 marked complete
  (13/13 plans), aligned with the SUMMARY artefacts on disk

Phase 26 (Task Code Reorg & HTTP-Backed Agent Worker) is ready for verifier
sweep + merge. The next milestone-v4.0 phase (Phase 27: Watcher Service &
User-Initiated Scan) can plan against a clean controller/agent_worker split.
…es, lux_workerβ†’controller doc sweep (Wave 6)
…rage gaps

Verification: All 5 success criteria PASS via goal-backward analysis (task
split, role-driven startup, HTTP-only file-bound tasks, per-agent queue
isolation, self-contained payloads). 26-VERIFICATION.md records evidence.

Nyquist audit: NEEDS GAPS FILLED β€” 26-NYQUIST.md identifies:
- GAP-1 (critical): tests/test_config_role_split.py missing β€”
  AgentSettings validators + get_settings() PHAZE_ROLE dispatch untested
- GAP-2 (high): tests/test_services/test_agent_client_endpoints.py missing β€”
  5 client methods (create_tracklist, patch_proposal_state, execution_log
  POST+PATCH, heartbeat) have no respx tests
- GAP-3 (medium): bearer-token-never-logged invariant has no caplog
  assertion in PhazeAgentClient WARNING path

Recommendation: address via /gsd:validate-phase before shipping. Pre-existing
infrastructure issues (D-1 DB fixture flakiness, D-2 enum-type race, D-3
Redis daemon requirement) are logged in deferred-items.md and out of scope.
Cover 7 behaviors of get_settings() dispatch and AgentSettings fail-fast
validators: role dispatch to AgentSettings/ControlSettings, missing
PHAZE_AGENT_API_URL/TOKEN/SCAN_ROOTS each raise, and comma-split produces
correct list. No DB or Redis required.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Five respx happy-path tests covering Phase-26-new methods: create_tracklist,
patch_proposal_state, post_execution_log, patch_execution_log, and heartbeat.
Verifies URL construction, payload serialization (including exclude_unset=True
on PATCH methods), and response model type for each endpoint.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds one caplog test to test_agent_client.py that mocks a 500 response,
captures WARNING-level logs from PhazeAgentClient._request(), and asserts
the token string does not appear β€” enforcing the D-13 invariant on the
HTTP-client warning path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…AP-4)

Adds one test where original_path="/etc/shadow" (outside scan_root) while
proposed_path is inside the scan root. Verifies that _resolve_and_check_containment
rejects the operation (error_count==1) and leaves the proposed destination
uncreated β€” confirming the guard covers both paths, not just proposed_path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tup (GAP-5)

Adds a second test to test_agent_startup_banner.py where PHAZE_AGENT_QUEUE
is set to 'phaze-agent-wrong-id' while whoami returns agent_id='correct-id'.
Asserts startup() raises RuntimeError matching 'queue/token mismatch',
exercising the Pitfall 1 anti-misconfiguration guard at agent_worker.py:133-142.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Audit trail appended to 26-VALIDATION.md documenting the 5 gap closures
(GAP-1 critical through GAP-5 low) added in commits ac6e3b0..9179907.
15 new test assertions across 2 new files (test_config_role_split.py,
test_agent_client_endpoints.py) + 3 existing-file edits.

Phase 26 is now nyquist_compliant. All DIST-03, TASK-01/02/03, OPS-01
requirements have automated verification.
Phase 26 introduced 11 integration tests (test_agent_task_router.py,
test_agent_tracklists.py) that require live Redis at PHAZE_REDIS_URL.
SAQ Queue.from_url is not compatible with fakeredis at saq>=0.26.3, so
these tests have no fakeredis fallback and were failing in CI.

Adds redis:7-alpine service container and PHAZE_REDIS_URL env, mirroring
the existing postgres service pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented May 13, 2026

Codecov Report

❌ Patch coverage is 99.17808% with 6 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/phaze/services/agent_client.py 97.53% 2 Missing ⚠️
src/phaze/tasks/agent_worker.py 97.56% 2 Missing ⚠️
src/phaze/config.py 98.24% 1 Missing ⚠️
src/phaze/routers/agent_tracklists.py 98.03% 1 Missing ⚠️

πŸ“’ Thoughts on this report? Let us know!

Codecov flagged 68 lines missing in the Phase 26 patch. Adds 19 targeted tests
across 8 files; per-file patch coverage rises sharply:

- agent_worker.py: 68.29% β†’ 97.56% (whoami retry exhaustion, role mismatch,
  shutdown cleanup, module-import RuntimeError on missing PHAZE_AGENT_QUEUE)
- agent_client.py: 86.41% β†’ 97.53% (upsert_files / put_metadata /
  put_fingerprint happy paths)
- execution.py:    89.74% β†’ 100%   (4 best-effort log/PATCH failure paths)
- controller.py:   78.12% β†’ 100%   (shutdown disposes engine + closes client)
- agent_tracklists.py: 88.24% β†’ 100% (409 concurrent-writer poll exhaustion)
- functions.py:    88.46% β†’ 100%   (malformed mood/style prediction skips)
- agent_files.py:  78.57% β†’ 94.12% (non-music skip + enqueue failure swallow)

Two log-content assertions replaced with mock-call assertions because caplog
record propagation is fragile when other tests in the suite reconfigure root
logger handlers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@SimplicityGuy SimplicityGuy merged commit 599f2bf into main May 13, 2026
34 checks passed
@SimplicityGuy SimplicityGuy deleted the gsd/phase-26-task-code-reorg-http-backed-agent-worker branch May 13, 2026 04:33
SimplicityGuy added a commit that referenced this pull request May 13, 2026
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant