Skip to content

docker(deps): bump python from 3.13-slim to 3.14-slim in the docker-images group across 1 directory#1

Open
dependabot[bot] wants to merge 1 commit into
mainfrom
dependabot/docker/docker-images-6dafb4a59b
Open

docker(deps): bump python from 3.13-slim to 3.14-slim in the docker-images group across 1 directory#1
dependabot[bot] wants to merge 1 commit into
mainfrom
dependabot/docker/docker-images-6dafb4a59b

Conversation

@dependabot
Copy link
Copy Markdown
Contributor

@dependabot dependabot Bot commented on behalf of github Mar 28, 2026

Bumps the docker-images group with 1 update in the / directory: python.

Updates python from 3.13-slim to 3.14-slim

@dependabot @github
Copy link
Copy Markdown
Contributor Author

dependabot Bot commented on behalf of github Mar 28, 2026

Labels

The following labels could not be found: dependencies, docker. Please create them before Dependabot can add them to a pull request.

Please fix the above issues or remove invalid values from dependabot.yml.

@dependabot dependabot Bot changed the title docker(deps): bump python from 3.13-slim to 3.14-slim in the docker-images group docker(deps): bump python from 3.13-slim to 3.14-slim in the docker-images group across 1 directory Mar 28, 2026
@dependabot dependabot Bot force-pushed the dependabot/docker/docker-images-6dafb4a59b branch 2 times, most recently from e6677eb to 1bcc779 Compare March 30, 2026 17:43
@dependabot dependabot Bot force-pushed the dependabot/docker/docker-images-6dafb4a59b branch from 1bcc779 to 53a6c6b Compare April 6, 2026 16:14
@dependabot dependabot Bot force-pushed the dependabot/docker/docker-images-6dafb4a59b branch from 53a6c6b to bb3b2e3 Compare April 13, 2026 17:29
@dependabot dependabot Bot force-pushed the dependabot/docker/docker-images-6dafb4a59b branch from bb3b2e3 to c284f72 Compare April 20, 2026 18:41
@dependabot dependabot Bot force-pushed the dependabot/docker/docker-images-6dafb4a59b branch from c284f72 to e89ad13 Compare April 27, 2026 20:05
@dependabot dependabot Bot force-pushed the dependabot/docker/docker-images-6dafb4a59b branch from e89ad13 to 5f752b4 Compare May 4, 2026 20:39
Bumps the docker-images group with 1 update in the / directory: python.


Updates `python` from 3.13-slim to 3.14-slim

---
updated-dependencies:
- dependency-name: python
  dependency-version: 3.14-slim
  dependency-type: direct:production
  dependency-group: docker-images
...

Signed-off-by: dependabot[bot] <support@github.com>
@dependabot dependabot Bot force-pushed the dependabot/docker/docker-images-6dafb4a59b branch from 5f752b4 to 1c00293 Compare May 11, 2026 23:46
SimplicityGuy added a commit that referenced this pull request May 13, 2026
* chore(26): sync phase 26 planning artifacts into worktree

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.

* chore(26): add tenacity + respx deps and mypy strict overrides

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.

* feat(26): split phaze.config into Base + Control + Agent settings (D-14)

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.

* feat(26): extend ProposalStatus + FileState with D-28 transition targets

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.

* docs(26): finalize Phase 26 Plan 01 -- Wave 0 foundation summary

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.

* test(26-02): add respx contract tests for PhazeAgentClient (RED)

- 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>

* test(26-03): add failing tests for 4 HTTP-boundary schema modules

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

* feat(26-03): add 4 HTTP-boundary schemas for agent endpoints (D-15, D-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.

* test(26-03): add failing tests for agent_tasks SAQ payload schemas

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

* feat(26-02): implement PhazeAgentClient with tenacity retry funnel + 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>

* feat(26-03): add 5 SAQ-job payload schemas in agent_tasks.py (D-22..D-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.

* docs(26-02): complete PhazeAgentClient plan -- summary + state + roadmap

- 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>

* docs(26-03): complete schema modules plan -- SUMMARY recording 4 HTTP + 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).

* chore(26-04): retire self-deleting type:ignore tripwires in agent_client

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.

* test(26-04): add failing tests for AgentTaskRouter (RED)

- 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

* test(26-06): add contract tests for PUT /agent/analysis (RED)

- 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>

* feat(26-04): implement AgentTaskRouter with lazy per-agent Queue cache (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.

* fix(26-08): drop now-unused 'type: ignore[import-not-found]' tripwires 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>

* chore(26-05): clean self-deleting type-ignore tripwires in agent_client (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>

* test(26-08): add failing tests for PATCH /api/internal/agent/proposals/{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>

* test(26-05): add failing contract tests for GET /whoami (RED)

- 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>

* chore(26-07): remove dead type-ignore on Plan 03 schema imports

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>

* test(26-07): add failing integration tests for POST /tracklists (RED)

- 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>

* feat(26-06): implement PUT /agent/analysis with idempotent upsert (GREEN)

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>

* feat(26-08): implement PATCH /api/internal/agent/proposals/{id}/state (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>

* test(26-06): parametrized unit tests for _summarize_dict_to_string (W6)

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>

* docs(26-04): complete AgentTaskRouter plan -- SUMMARY + state + roadmap

- 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

* feat(26-07): implement POST /api/internal/agent/tracklists with Redis 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>

* feat(26-05): implement GET /api/internal/agent/whoami router (GREEN)

Implements D-15..D-17 with the established Phase 25 router pattern:
- APIRouter(prefix="/api/internal/agent/whoami", tags=["agent-internal"])
- Single GET handler depending on get_authenticated_agent
  (401 missing/malformed header, 403 unknown/revoked token)
- Returns AgentIdentity{agent_id, name, scan_roots, created_at}
  projected directly from the auth dep's Agent row

Used at agent worker startup (Plan 10) to verify the bearer token
is valid AND the token-derived agent_id matches the operator-supplied
PHAZE_AGENT_QUEUE env var (anti-misconfiguration probe per
RESEARCH Pitfall 1), and by Phase 29's Agents admin reachability probe.

Plan 12 will wire this router into create_app(); this plan keeps the
router module + tests parallel-safe via the smoke-app fixture pattern.

Test-side adjustment (Rule 1 fix):
- Relaxed `assert parsed.tzinfo is not None` on the happy-path test:
  TimestampMixin in src/phaze/models/base.py uses `Mapped[datetime]`
  WITHOUT `DateTime(timezone=True)`, so the server-side timestamp is
  naive UTC -- matching the project-wide convention asserted by
  tests/test_routers/test_execution.py:70's `.replace(tzinfo=None)`.
  The plan's verbatim test asserted a tzinfo guarantee that the
  established ORM convention does not (and should not) provide.
  Replaced with `assert isinstance(parsed, datetime)` to keep the
  ISO-8601 round-trip check intact.

Verification:
  uv run pytest tests/test_routers/test_agent_identity.py -x -q --no-cov   # 4 pass
  uv run mypy src/phaze/routers/agent_identity.py                          # clean
  uv run ruff check src/phaze/routers/agent_identity.py                    # clean
  coverage of agent_identity.py: 100.00% (9/9 lines)

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

* docs(26-05): complete plan — SUMMARY + STATE/ROADMAP/REQUIREMENTS update

- SUMMARY records 2 task commits (RED + GREEN) + 1 Rule 3 deviation
  commit (agent_client.py tripwire cleanup) + 1 Rule 1 deviation
  (timezone-aware assertion relaxed to match naive-UTC convention).
- 100% coverage on src/phaze/routers/agent_identity.py (9/9 lines).
- 4 contract tests pass: 200 happy path, 401 missing header,
  403 unknown token, 403 revoked-mid-session.
- deferred-items.md logs out-of-scope full-suite integration-test
  flakiness (pre-existing on the inherited Phase 26 base).
- ROADMAP marks 26-05 + 26-03 complete (4/13 plans for Phase 26).
- REQUIREMENTS marks OPS-01 complete (TASK-02/TASK-03 already
  completed by earlier plans).

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

* docs(26-06): complete Plan 06 -- agent_analysis router + helper unit tests

- 26-06-SUMMARY.md documenting:
  * Overflow funnel decision: D-26 wire fields without dedicated columns
    (danceability, energy) land in `features` JSONB column (no migration).
  * mood/style top-3 string summarization with deterministic
    `(-score, key)` two-key tiebreak.
  * Two auto-fix deviations: (1) removed self-deleting tripwires in
    agent_client.py that fired as designed when Plan 03 merged in,
    (2) updated test assertions to match the overflow funnel storage.
- STATE.md updated: Phase 26 Plan 06 complete, 18/26 plans done (69%),
  +3 accumulated-context entries for the overflow-funnel pattern, the
  two-key sort canonical pattern, and the tripwire-fire-on-merge note.
- ROADMAP.md: mark 26-03 (merged-in dependency) and 26-06 complete.

Verified: 14/14 tests pass (8 contract + 6 helper), mypy clean across
102 source files, ruff/ruff-format/all pre-commit hooks green.

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

* docs(26-07): complete POST /tracklists plan -- SUMMARY + STATE + ROADMAP

- 26-07-SUMMARY.md records: three-path idempotency design, discretion
  decisions (no payload-hash check, accept T-26-07-T), test fixture
  insights (PHAZE_REDIS_URL, scan_iter cleanup, decode_responses=True),
  coverage (88.24%), deviations (chore commit + sqlalchemy.update fix)
- STATE.md: advance to Plan 07 complete (18/26 plans, 69%); record
  Phase 26 P07 in metrics table (14m 31s); add 6 [Phase 26-07] decisions
- ROADMAP.md: tick the 26-07-PLAN.md checkbox
- deferred-items.md: log pre-existing test-isolation regression that
  surfaces under full-suite test_routers runs (unrelated to Plan 07)

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

* docs(26-08): complete agent_proposals router plan -- SUMMARY + STATE + ROADMAP

Plan 26-08 (Wave 3) complete in 14 min. Two TDD tasks (RED + GREEN) committed
atomically:
- e2e35e0 test(26-08): 11 failing contract tests
- 8c94069 feat(26-08): PATCH router implementation + test sync-method bug fix

Plus one Rule 3 blocker fix commit (03b3d28) that cleared now-unused
'type: ignore[import-not-found]' parallelization tripwires in agent_client.py
which were blocking the pre-commit mypy hook.

SUMMARY documents:
- _PROPOSAL_TRANSITIONS table for state-machine validation (D-28)
- W1 / T-26-08-S2 cross-tenant guard placement (BEFORE state-machine logic)
- Pitfall 6 invariant: single await session.commit() for joint mutation
- Idempotent same-state PATCH semantics (no DB writes on retry)
- Deferred-items.md: pre-existing test-fixture flakiness in conftest.py
  (DEF-26-08-01) -- async_engine teardown silently fails on prior-test errors,
  affects all agent tests (Phase 25 + Phase 26). Out of scope for Plan 08.

STATE.md updated: progress 18/26 (69%), last_activity=Phase 26 Plan 08 complete,
4 new decisions appended to Accumulated Context.

ROADMAP.md updated: 26-08-PLAN.md marked complete with 11 tests (incl. W1).

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

* feat(26-09): create phaze.tasks.controller -- SAQ settings for control role

- Fileless-only subset of legacy worker.py (D-01..D-04)
- Functions list: generate_proposals, match_tracklist_to_discogs,
  search_tracklist, scrape_and_store_tracklist
- Cron list: refresh_tracklists (1st of each month at 03:00)
- Module-level Queue.from_url(name="controller") for saq CLI consumption
- Startup hook stashes ctx["async_session"], ctx["task_engine"],
  ctx["discogs_client"], ctx["proposal_service"], ctx["queue"] (W4)
- Startup banner logs role=control queue=controller (OPS-01)
- Imports ZERO file-bound modules (no functions/execution/fingerprint/
  metadata_extraction/scan/pool, no services.fingerprint or agent_client)
- Legacy worker.py stays in place this plan (deleted by Plan 26-13)

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

* test(26-09): controller startup banner test (W2 / OPS-01 coverage)

- Asserts controller.startup() emits "role=control queue=controller" log line
- Verifies W4 invariant: ctx["queue"] = queue is stashed by startup
- Monkeypatches create_async_engine/DiscogsographyClient/ProposalService
  so the test runs without Postgres/HTTP connections (<1s)
- Underscore-prefixes unused lambda args to satisfy ruff ARG005
  (Rule 3 deviation -- plan body had bare *a/**kw which trips ruff;
  semantically equivalent fix)

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

* docs(26-09): complete controller SAQ settings module plan

SUMMARY records:
- 2 commits: 555a718 (feat controller.py), 8cffb47 (test banner)
- 1 deviation: ARG005 lambda-arg fix in test (mechanical, no semantic change)
- Plan-level success criteria all green: mypy, ruff, format, pre-commit, smoke
- Banner test: 1 passed in 1.07s
- Acceptance grep matrix: all 12 checks pass

Closes Plan 26-09 (Wave 4). Plan 26-10 will mirror this module under
phaze.tasks.agent_worker for the file-bound role.

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

* refactor(26-11): rewrite process_file + extract_file_metadata over HTTP (Task 1)

- Drop all phaze.database / phaze.models / sqlalchemy imports from both tasks
- Validate kwargs via ProcessFilePayload / ExtractMetadataPayload (extra='forbid')
- Call PhazeAgentClient.put_analysis / put_metadata via ctx["api_client"]
- Add _features_to_mood_dict + _features_to_style_dict helpers to convert
  essentia's str outputs to D-26 wire-format dict[str, float] for AnalysisWritePayload
- Rewrite test_functions.py + test_metadata_extraction.py to mock api_client
  instead of async_session (no new pytest.skip markers)

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

* refactor(26-11): rewrite fingerprint_file + scan_live_set over HTTP (Task 2)

- Drop all phaze.database / phaze.models / sqlalchemy imports from both tasks
- Validate kwargs via FingerprintFilePayload / ScanLiveSetPayload (extra='forbid')
- fingerprint_file calls api.put_fingerprint(file_id, engine, body) per engine
- scan_live_set uses stable uuid5(NAMESPACE_URL, "phaze-scan-{file_id}") request_id
  so SAQ retries collapse via server-side Redis idempotency cache
- scan_live_set drops the in-process FileMetadata join (W5 Option (b)):
  artist/title now None on fingerprint tracks; documented as a known v3.0 UI
  regression for future controller-side enrichment (Phase 27/28)
- Refactor src/phaze/services/fingerprint.py: move sqlalchemy / phaze.models
  imports inside get_fingerprint_progress() so the agent worker can import the
  orchestrator without pulling in the Postgres driver (Rule 3 -- structural
  fix required to satisfy DIST-03 import boundary)
- Rewrite test_fingerprint.py + test_scan.py to mock api_client + orchestrator
  (no new pytest.skip markers; preserves coverage)

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

* feat(26-11): rewrite execute_approved_batch over HTTP (B2 Option A) (Task 3)

- Implement full per-proposal copy + verify + delete + HTTP state reporting
  (no NotImplementedError stub -- B2 Option A per CONTEXT.md revision iter 2)
- Per-proposal lifecycle: POST execution-log (in_progress) -> file op ->
  PATCH execution-log (completed|failed) -> PATCH proposal-state (executed|failed)
- Failure isolation: bad file (IO error, path traversal, sha256 mismatch) gets
  state=failed; siblings succeed; batch status='completed_with_errors' iff any failed
- Path-traversal guard (T-26-11-S1): _resolve_and_check_containment rejects
  proposed_path that escapes scan_roots via Path.resolve() + relative_to() check
- Streaming sha256 verify (avoid loading huge files into memory)
- Refuse to execute when scan_roots is empty (mis-deployment fail-fast)

Schema-extraction (Rule 3 fix for D-03 import boundary):
- Move ExecutionStatus enum to new phaze.enums.execution module (DB-free)
- src/phaze/models/execution.py re-exports for backward compatibility
- src/phaze/schemas/agent_execution.py now imports from phaze.enums (clean boundary)
- src/phaze/tasks/execution.py imports from phaze.enums

Tests:
- New tests/test_tasks/test_execute_approved_batch.py: 5 contract tests (happy, partial,
  path-escape, sha256-mismatch, requires_scan_roots)
- Rewrite tests/test_tasks/test_execution.py to 2 smoke tests covering aggregate
  status='completed' and status='completed_with_errors'

Verified: import phaze.tasks.execution does NOT load phaze.database / sqlalchemy /
phaze.models -- the D-03 import boundary holds for all 5 rewritten task modules.

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

* docs(26-11): complete task-body HTTP rewrite plan

- Create 26-11-SUMMARY.md documenting the 5 rewritten task bodies, the B2
  Option A implementation of execute_approved_batch, the ExecutionStatus
  enum extraction to phaze.enums (Rule 3 fix), the function-local DB import
  refactor in services/fingerprint.py, and the v3.0 UI regression note
  for scan_live_set artist/title resolution (W5 Option (b))
- Mark ROADMAP.md Phase 26 Plan 11 entry as COMPLETE
- Update STATE.md Current Position, Decisions, Quick Tasks Completed,
  Session Continuity

Plan 11 is the mechanical core of Phase 26 -- the agent worker can now
boot without Postgres and run the full file-bound pipeline once Plans 10
(agent_worker SAQ settings) and 12 (main.py wiring) land.

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

* feat(26-12): wire 4 new agent routers + AgentTaskRouter + Redis into 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>

* test(26-10): add subprocess import-boundary test for agent_worker (D-25)

- 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>

* feat(26-10): create phaze.tasks.agent_worker — SAQ settings for agent 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>

* refactor(26-12): swap inline Queue.from_url for AgentTaskRouter in agent_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>

* test(26-10): add agent_worker startup-banner test (W2 / D-13 / OPS-01)

- 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>

* docs(26-12): complete FastAPI wiring plan -- SUMMARY + STATE + ROADMAP + 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>

* docs(26-10): complete HTTP-backed agent worker plan

- 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>

* chore(26-13): delete legacy tasks.worker + tasks.session; wire compose 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.

* docs(v4.0): replace hostname-leaked lux_worker with role-neutral controller

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

* docs(26-13): complete closing-housekeeping plan -- SUMMARY + STATE + 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.

* docs(26): finalize phase 26 — verification PASS, Nyquist flags 3 coverage 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.

* test(26-val): add config role-split unit tests (GAP-1)

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>

* test(26-val): add PhazeAgentClient per-method respx tests (GAP-2)

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>

* test(26-val): assert bearer token absent from WARNING logs (GAP-3)

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>

* test(26-val): cover original_path escape in execute_approved_batch (GAP-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>

* test(26-val): verify queue/token mismatch raises at agent_worker startup (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>

* docs(26-val): record Nyquist validation audit — 5/5 gaps resolved

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.

* docs(26): ship phase 26 — PR #57

* ci: add Redis sidecar to tests workflow (D-30)

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>

* test(26): cover Phase 26 patch coverage gaps (94.92% → 96.36%)

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>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SimplicityGuy added a commit that referenced this pull request May 17, 2026
* docs(29): capture phase context

* docs(state): record phase 29 context session

* docs(29): UI design contract for Agents admin page

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

* docs(29): mark UI-SPEC approved after checker verification

Checker verdict: PASS on 5 dimensions; FLAG on Dimension 5 (spacing) for
two pre-existing project invariants inherited from Phases 27/28
(py-0.5 pill padding, py-3 table cell padding). Both are documented
in the spec's Spacing Exceptions section. Non-blocking.

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

* docs(29): research deployment hardening + agents admin

Verified against installed binaries:
- SAQ 0.26.3 + croniter 6.2.2 — 6-field trailing-seconds cron
- httpx 0.28.1 supports verify=<path>
- cryptography NOT a transitive dep — must be added as new runtime dep
- agent_worker is single .py file (not a package) — no refactor needed

* docs(29): add validation strategy

* docs(29): pattern mapping for deployment hardening + agents admin

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

* docs(29): create phase plan

8 plans across 4 waves covering DIST-01, AUTH-02, AUTH-03, OPS-02, OPS-03, OPS-04. Plan-checker passed after one revision round (3 BLOCKERs + 8 WARNINGs addressed). Decision coverage 23/23.

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

* test(29-01): add failing tests for cert_bootstrap + Postgres-free guard

RED phase of Plan 29-01 Task 1. Adds:
- cryptography>=46.0.0,<49 runtime dep (NOT transitive, per RESEARCH
  Critical Discovery #1; verified via uv pip list)
- tests/test_cert_bootstrap.py with 7 LOCKED cases (D-22):
    1. 4-file generation + x509 parseability
    2. idempotency (mtimes unchanged on second call)
    3. banner-via-stdout (capsys) + Pitfall-4 no-secret-leak guard
    4. file modes (0o644 certs, 0o600 keys)
    5. SubjectAlternativeName entries match input
    6. _parse_san_entries DNS vs IP dispatch
    7. WARNING-8 banner-via-logger.warning (caplog) +
       Pitfall-4 no-secret-leak parity for logger channel
- tests/test_task_split.py::test_cert_bootstrap_stays_postgres_free
  (Phase 29 D-22 extension of D-25)

All 7 cert_bootstrap tests + the new task_split case currently fail
with ModuleNotFoundError -- this is the expected RED state.

Refs: D-01, D-02, D-22; AUTH-02

* feat(29-01): implement cert_bootstrap + entrypoint shim

GREEN phase of Plan 29-01 Task 1. Implements:

src/phaze/cert_bootstrap.py
    - ensure_certs_present(certs_dir, cn, sans_csv): idempotent CA + leaf
      generation via cryptography.x509. ECDSA P-256, 10-year CA, 2-year
      leaf. Writes phaze-ca.{crt,key} + phaze-server.{crt,key} with
      0o644 / 0o600 modes.
    - _parse_san_entries(): DNSName for hostnames, IPAddress for IPs
      via ipaddress.ip_address dispatch.
    - _generate_ca / _generate_leaf: x509.CertificateBuilder with
      BasicConstraints + KeyUsage extensions per RESEARCH Pattern 1.
    - Loud banner on actual generation via BOTH print() AND
      logger.warning() (CONTEXT D-02 D-discretion "Both", WARNING-8).
      Banner is a LITERAL CONSTANT referencing only phaze-ca.crt --
      never the private key (Pitfall 4).
    - IMPORT-BOUNDARY INVARIANT: no phaze.database / sqlalchemy.ext.asyncio
      imports (extends Phase 26 D-25; verified by test_task_split).

src/phaze/entrypoint.py
    - main(): reads PHAZE_CERTS_DIR / PHAZE_API_HOST / PHAZE_API_TLS_SANS,
      calls ensure_certs_present, then os.execvp uvicorn with
      --ssl-keyfile / --ssl-certfile flags pointing at the generated
      leaf cert. Process replacement so signals + PID-1 propagate
      cleanly (RESEARCH Pattern 2).
    - Invoked from compose as `uv run python -m phaze.entrypoint`.

All 7 cert_bootstrap tests + the new test_task_split case pass.
ruff + mypy + bandit clean on both new modules.

Refs: D-01, D-02, D-22; AUTH-02

* test(29-01): add TLS integration tests + fix CA chain extensions

RED phase of Plan 29-01 Task 2. Adds:

tests/test_services/test_agent_client_tls.py
    - Real-TLS smoke server fixture (uvicorn in background asyncio task,
      two independent CA bundles in tmp_path/{server,wrong}_certs).
    - test_wrong_ca_raises_connect_error: D-04 success criterion --
      httpx.AsyncClient(verify=wrong_ca) against a server presenting
      the correct cert raises httpx.ConnectError.
    - test_correct_ca_succeeds: same setup with verify=correct_ca
      returns 200 OK.
    - test_construct_agent_client_missing_ca_raises +
      test_construct_agent_client_empty_ca_raises: D-03 fail-fast --
      RuntimeError("CA file empty or unreadable: ...") when the CA path
      is non-existent or zero-byte. Currently RED -- AgentSettings does
      not yet expose agent_ca_file, construct_agent_client does not yet
      validate.

[Rule 1 - Bug] src/phaze/cert_bootstrap.py:
    - Add AuthorityKeyIdentifier + SubjectKeyIdentifier extensions on
      the leaf cert; SubjectKeyIdentifier on the CA cert. Python 3.13's
      ssl module rejects the validation chain with "Missing Authority
      Key Identifier" without these (discovered while running
      test_correct_ca_succeeds against the real cert chain).
    - Add ExtendedKeyUsage(SERVER_AUTH) on the leaf cert; required by
      Python 3.13's strict TLS validation path -- otherwise the leaf
      is rejected when presented to a TLS client expecting a server cert.
    All 7 cert_bootstrap unit tests still pass; the chain now validates
    end-to-end (test_correct_ca_succeeds passes).

Refs: D-03, D-04, D-22; AUTH-02

* feat(29-01): wire verify= through PhazeAgentClient + AgentSettings

GREEN phase of Plan 29-01 Task 2. Implements:

src/phaze/config.py
    - BaseSettings.api_tls_sans (D-02): comma-separated SAN list for the
      auto-generated leaf cert. Default "localhost,127.0.0.1,api" covers
      single-host dev (loopback) + docker-compose service-name DNS.
      Env alias PHAZE_API_TLS_SANS.
    - AgentSettings.agent_ca_file (D-03): path to the operator-distributed
      CA cert. Default "/certs/phaze-ca.crt" matches the in-container
      bind-mount path on the agent side. Env alias PHAZE_AGENT_CA_FILE.

src/phaze/services/agent_client.py
    - PhazeAgentClient.__init__ accepts keyword-only `verify` parameter
      (type: ssl.SSLContext | str | bool, default True). Threaded through
      to httpx.AsyncClient(verify=...). Default True preserves backwards
      compat with all existing respx-based tests (Pitfall 10) -- respx
      mocks below the TLS layer.
    - `ssl` moved to TYPE_CHECKING block (annotation-only usage).

src/phaze/tasks/_shared/agent_bootstrap.py
    - construct_agent_client(cfg) now validates cfg.agent_ca_file at
      construction time: if the path does not exist OR is zero-byte,
      raises RuntimeError("CA file empty or unreadable: ...") so
      misconfiguration surfaces fast (D-03 fail-fast).
    - Passes verify=cfg.agent_ca_file through to PhazeAgentClient.

All 4 TLS integration tests pass. 26 existing respx-based
test_agent_client*.py cases still pass (Pitfall 10 confirmed:
default verify=True preserves the transport-level mock behavior).
Postgres-free import boundary still holds (test_task_split: 5/5).
ruff + mypy clean on all modified modules.

Refs: D-02, D-03, D-04; AUTH-02

* docs(29-01): complete cert-bootstrap + agent-TLS plan

Adds 29-01-SUMMARY.md documenting the 4 plan commits:
- ffdbf5f (test RED): 7 cert_bootstrap cases + task_split extension
- 5840bfe (feat GREEN): cert_bootstrap + entrypoint shim
- 57d9843 (test RED): TLS integration tests + Rule 1 bug fix
  (CA chain extensions: AKI, SKI, EKU(SERVER_AUTH))
- 25c4ca4 (feat GREEN): verify= plumbing through PhazeAgentClient
  + AgentSettings.agent_ca_file + BaseSettings.api_tls_sans

12 net new tests passing. AUTH-02 partially closed (full closure in
Plan 03 once docker-compose api command switches to phaze.entrypoint).

Refs: D-01..D-04, D-22; AUTH-02

* test(29-02): add failing tests for AgentSettings agent_env + redis-password validator

- New tests/test_config/__init__.py marker so pytest discovers the sub-package
- 4 RED cases covering Phase 29 D-06:
  1. agent_env=production + passwordless redis_url -> ValidationError with
     "requires a password in redis_url" substring
  2. agent_env=production + redis://default:<pw>@host:6379/0 constructs OK
  3. agent_env=dev + passwordless redis_url constructs OK (Pitfall 7)
  4. Default agent_env is "dev" when omitted
- Tests pass kwargs directly (cleaner than env-var monkeypatching for model contract)

RED state confirmed: first test fails with "DID NOT RAISE ValidationError"
because the agent_env field + model_validator do not exist yet.

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

* feat(29-02): enforce passworded Redis URL on AgentSettings in production mode

Phase 29 D-06 / AUTH-03 (agent-side half):

- Add `Literal` to typing imports
- Add AgentSettings.agent_env: Literal["dev", "production"] field
  (default "dev"; env alias PHAZE_AGENT_ENV)
- Add AgentSettings._enforce_redis_password_in_production model_validator:
  when agent_env=="production", `urlparse(self.redis_url).password` must be
  set; otherwise raise ValueError("agent_env=production requires a password
  in redis_url (Phase 29 D-06)").
- Field placed adjacent to other PHAZE_AGENT_* fields for grouping.
- model_validator placed after _enforce_required_agent_fields so the
  redis-url check runs after the required-field check.

The pairing server-side hardening (Redis `requirepass` + LAN-bound port)
lands in Plan 03 alongside the docker-compose rewrite; together they fully
close AUTH-03. Dev mode preserves Pitfall 7: fresh clones do `docker compose
up` with no Redis password ceremony.

Verification:
- 4/4 tests in tests/test_config/test_agent_settings_redis_password.py pass
- 22 existing tests in tests/test_config_role_split.py + test_config_worker.py
  pass (no regression)
- uv run mypy src/phaze/config.py: clean
- uv run ruff check + ruff format --check: clean

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

* docs(29-02): complete agent-side redis password validator plan

Phase 29 Plan 02 SUMMARY. Documents:
- Files created (tests/test_config/__init__.py, tests/test_config/test_agent_settings_redis_password.py)
- File modified (src/phaze/config.py: Literal import + agent_env field + _enforce_redis_password_in_production model_validator)
- 2 commits (4b95029 RED, a7741ff GREEN; no REFACTOR needed)
- 4 new tests; 0 regressions in 22 existing config tests
- D-06 fully implemented; AUTH-03 partial (server-side half lands in Plan 03)
- TDD gate compliance verified; self-check passed

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

* test(29-03): add failing YAML-parse tests for app-server compose isolation

- tests/test_deployment/__init__.py (pytest sub-package marker)
- tests/test_deployment/test_api_filesystem_isolation.py with 4 tests:
  * test_api_service_has_no_file_mounts (DIST-01)
  * test_controller_worker_has_no_file_mounts (DIST-01)
  * test_no_watcher_or_agent_worker_in_root_compose (D-15 / D-17;
    also asserts audfprint + panako absent)
  * test_redis_hardened (D-05 / AUTH-03; requirepass + LAN bind +
    --no-auth-warning healthcheck)
- All 4 tests FAIL against the current docker-compose.yml — RED step
  of TDD. Task 2 lands the compose rewrite that turns them GREEN.

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

* feat(29-03): harden app-server compose — strip file mounts, lock down redis

Rewrite root docker-compose.yml as the application-server-only compose
(DIST-01, D-05, D-17, D-19; AUTH-03 server-side). End state: services
block is exactly {api, worker, postgres, redis}.

- api: swap `command:` to `uv run python -m phaze.entrypoint` (Plan 01
  cert-bootstrap shim) and replace the SCAN_PATH mount with a single
  bind volume `${CA_PATH:-./certs}:/certs:rw`.
- worker (controller): drop `MODELS_PATH=/models` from environment and
  remove all three file mounts (SCAN_PATH, MODELS_PATH, OUTPUT_PATH).
  Controller is now fileless.
- DELETE the watcher, agent-worker, audfprint, panako service blocks —
  they live in docker-compose.agent.yml on the file server (Plan 04+).
- DELETE the unused audfprint_data and panako_data named volumes.
- redis: list-form command with `--requirepass ${REDIS_PASSWORD:?...}`
  (fail-fast at compose-parse time); ports bound via
  `${REDIS_BIND_IP:-127.0.0.1}:6379:6379` (loopback default, prod sets
  LAN IP); healthcheck uses
  `redis-cli --no-auth-warning -a ${REDIS_PASSWORD} ping`.

.env.example: add the three Phase-29 variables with comment blocks:
REDIS_PASSWORD=changeme (dev placeholder; Pitfall-7 mitigation),
REDIS_BIND_IP=127.0.0.1, PHAZE_API_TLS_SANS=localhost,127.0.0.1,api.

Dockerfile audit: no MODELS_PATH/SCAN_PATH/OUTPUT_PATH ENV defaults
were present — verify step only, no changes needed.

All 4 tests in tests/test_deployment/test_api_filesystem_isolation.py
now pass (GREEN step of TDD).

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

* docs(29-03): complete app-server compose hardening plan

- 4 YAML-parse structural tests (D-19) in tests/test_deployment/
- docker-compose.yml rewritten: services = {api, worker, postgres, redis}
- api volumes stripped to /certs:rw only; controller worker is fileless
- watcher, agent-worker, audfprint, panako removed (move to agent.yml)
- redis hardened: --requirepass, LAN-bound port, authenticated healthcheck
- .env.example documents REDIS_PASSWORD, REDIS_BIND_IP, PHAZE_API_TLS_SANS
- Dockerfile audited — no MODELS_PATH/SCAN_PATH/OUTPUT_PATH ENV defaults

Closes DIST-01 (app server has no file mounts) and the server-side half
of AUTH-03 (Redis requirepass + LAN binding). Decision IDs D-05, D-17,
D-19 fully implemented; D-15 partial pending Plan 04's agent.yml.

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

* feat(29-05): add phaze.scripts.download_models helper + bash shim (D-21)

Extract the essentia model URL list (33 classifier paths + 1 genre model)
from scripts/download-models.sh into a Python helper so both bash and the
agent bootstrap can drive the download from a single source of truth.

- src/phaze/scripts/__init__.py: new package marker
- src/phaze/scripts/download_models.py: download_to(target_dir) public
  entry; _download_one uses .part atomic-rename pattern (T-29-05-03);
  CLI entry via `python -m phaze.scripts.download_models <dir>`
- scripts/download-models.sh: rewritten as a 6-line bash shim that execs
  the Python module (signals + exit code pass through cleanly)
- tests/test_services/test_model_bootstrap.py: scaffold with three
  ensure_models_present cases (RED until Task 2 lands) plus three
  download_to/_download_one cases (GREEN now)

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

* feat(29-05): wire ensure_models_present into agent_worker startup (D-21)

Phase 29 D-21 completes OPS-03's auto-download path. A new Postgres-free
shared module owns the .pb-glob + download orchestration; agent_worker
calls it AFTER /whoami succeeds so a bad token / unreachable app server
fails fast in ~60s instead of after a 5-minute 150MB download.

- src/phaze/tasks/_shared/model_bootstrap.py: ensure_models_present
  module (Postgres-free; stdlib + phaze.scripts.download_models only)
- src/phaze/tasks/agent_worker.py: drop the in-place RuntimeError checks;
  call ensure_models_present(Path(cfg.models_path)) as Step 3a after
  whoami_with_retry
- src/phaze/agent_watcher/__main__.py: add WARNING-7 documentation
  comment explaining why the watcher intentionally does NOT auto-download
- tests/test_task_split.py: add test_model_bootstrap_stays_postgres_free
  subprocess case (BLOCKER-1 resolution; parallel to the existing
  agent_bootstrap case)
- tests/test_phase04_gaps.py: replace the two old fail-fast model-dir
  RuntimeError tests with ordering + propagation tests that match the
  new auto-download semantics

Deferred (out of scope, pre-existing from Plan 29-03 compose hardening):
test_docker_compose_has_agent_worker_consuming_agent_queue -- Plan 29-04
moves the agent-worker block into docker-compose.agent.yml, and the
test must be updated to scan both compose files there.

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

* docs(29-05): complete models auto-download plan

OPS-03 + D-21 fully implemented. SUMMARY records the 33+1 URL migration
from bash to Python, the WARNING-7 watcher-no-download choice, and the
BLOCKER-1 subprocess import-boundary test (test_model_bootstrap_stays_postgres_free).

deferred-items.md notes the pre-existing
test_docker_compose_has_agent_worker_consuming_agent_queue failure from
Plan 29-03 (compose hardening removed the agent-worker block); the test
will be updated by Plan 29-04 (parallel wave) which lands docker-compose.agent.yml.

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

* test(29-07): add failing tests for agent_liveness + humanize (RED)

- tests/test_services/test_agent_liveness.py: 5-state classify matrix
  (12 boundary cases) + sort_key ordering invariants per UI-SPEC.
- tests/test_utils/test_humanize.py: relative_time output table (UI-SPEC
  LOCKED) covering all bucket boundaries, the 89.7s → "89s ago" truncation
  case, and format invariants (no plural-s suffix, single-letter unit).

Both modules ImportError today; GREEN commit follows.

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

* feat(29-07): implement agent liveness classifier + humanize helper (GREEN)

Wave 0 of plan 29-07 (OPS-04 UI half — pure-function tier):

- src/phaze/constants.py: add AGENT_LIVENESS_ALIVE_SECONDS=90 +
  AGENT_LIVENESS_STALE_SECONDS=300 (Phase 29 D-12 LOCKED thresholds).
- src/phaze/services/agent_liveness.py: pure-function classify(agent, now)
  → AgentStatus literal in {alive, stale, dead, revoked, never} with
  precedence revoked → never → alive/stale/dead. sort_key returns
  (revoked_int, status_rank, neg_last_seen) so revoked agents land last,
  non-revoked sort alive→stale→dead→never, ties break by last_seen DESC.
- src/phaze/utils/__init__.py + src/phaze/utils/humanize.py: relative_time
  helper producing "never" / "just now" / "Ns ago" / "Nm ago" / "Nh ago" /
  "Nd ago" with int-truncate semantics (UI-SPEC LOCKED bucket table).

Reconciled UI-SPEC documentation defect (line 248 prose example "89.7s →
89s ago" is inconsistent with its own bucket table lines 232-241; the
table is authoritative — see test docstring for Rule-1 fix rationale).

All 51 tests pass; mypy + ruff clean.

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

* test(29-07): add failing tests for /admin/agents router (RED)

Wave 1 RED gate for plan 29-07. 10 tests covering:
- Full page render with base.html chrome
- HX-Request: true returns partial only
- Dedicated /_table partial route (always partial, never halts polling)
- 5-state status pill rendering with LOCKED Tailwind classes
- Empty state UI-SPEC §Empty State LOCKED copy
- Sort order alive → stale → dead → never → revoked
- 3 BLOCKER-2 tests: htmx event listener + role=alert failure footer +
  localStorage `phaze:agents:lastError` plumbing
- Production-wiring smoke (router registered in main.create_app)

Currently fails at import — phaze.routers.admin_agents does not exist yet.

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

* feat(29-07): admin/agents router + templates + nav link (GREEN)

Wave 1 of plan 29-07 (OPS-04 UI half — Wave-1 deliverables):

- src/phaze/routers/admin_agents.py: APIRouter(prefix="/admin/agents"); two
  handlers — `page` (HX-Request-aware, full page OR partial) and
  `table_partial` (always partial, the canonical 5s polling target).
  `_load_agents` injects transient `agent._status` via classify(a, now) and
  sorts via sort_key (Phase 27 transient-attr pattern). No
  get_authenticated_agent dep (operator-facing on private LAN, consistent
  with pipeline.py / pipeline_scans.py precedent).

- src/phaze/templates/admin/agents.html: page shell extending base.html;
  current_page="admin_agents"; hosts the MANDATORY htmx:responseError +
  htmx:sendError + htmx:afterSwap listener writing/clearing
  `phaze:agents:lastError` localStorage (BLOCKER-2 UI-SPEC §Error /
  Failure-Tolerant Refresh LOCKED).

- src/phaze/templates/admin/partials/agents_table.html: HTMX
  self-replacing <section> (hx-get/hx-trigger/hx-swap=outerHTML, never
  halts — UI-SPEC §Polling LOCKED). Empty state + 6-column table + happy-
  path "Last refreshed Ns ago" Alpine footer + MANDATORY red role=alert
  "Refresh failed at HH:MM:SS" footer driven by localStorage (BLOCKER-2).

- src/phaze/templates/admin/partials/_status_pill.html: 5-state liveness
  pill with LOCKED Tailwind palette (alive=green-100/950,
  stale=amber-100/950, dead=red-100/950, revoked/never=gray-100/800) +
  redundant aria-label="Status: <state>" for screen readers.

- src/phaze/templates/base.html: new "Agents" nav link inserted between
  Audit Log and the theme toggle. Uses short-slug
  `current_page == 'admin_agents'` per WARNING-1 (matches live convention
  where Audit Log uses 'audit' not 'audit_log'). aria-current="page" is
  a forward-looking a11y upgrade applied only to this new link.

- src/phaze/main.py: register admin_agents.router alongside Phase 27/28
  routers.

All BLOCKER-2 grep gates pass:
  agents.html: htmx:responseError × 2, htmx:sendError × 2, htmx:afterSwap × 2,
              phaze:agents:lastError × 2, localStorage.setItem × 1,
              localStorage.removeItem × 1.
  agents_table.html: localStorage.getItem × 2, phaze:agents:lastError × 4,
                     "Refresh failed" × 2, role="alert" × 1.

Test status:
- test_router_registered_in_main_app passes (non-DB, structural).
- 9 DB-backed tests collect cleanly; cannot execute locally (no Postgres
  on the executor host). Logic verified via direct Jinja render smoke
  (positions, pill classes, BLOCKER-2 markup, relative_time output).
- mypy + ruff clean on all new files.

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

* docs(29-07): complete admin/agents UI plan

Plan 07 of Phase 29 (OPS-04 UI half) complete. Summary documents:

- Closes OPS-04 (combined with Plan 06 heartbeat caller half: full OPS-04
  closure).
- D-11..D-14 LOCKED contracts implemented.
- BLOCKER-2 (UI-SPEC §Error / Failure-Tolerant Refresh, status: approved)
  DELIVERED in v1 — htmx event listener + localStorage `phaze:agents:
  lastError` + red role=alert banner all shipped; 3 dedicated tests +
  10 grep gates verify presence.
- WARNING-1 RESOLVED: short-slug `current_page == 'admin_agents'` nav
  convention adopted (matches live base.html where Audit Log uses
  'audit', not 'audit_log').
- 1 deviation auto-fixed (Rule 1): UI-SPEC line 248 prose example
  "89.7s → 89s ago" is internally inconsistent with its own bucket
  table; truncation rule verified with non-conflicting 59.7s test
  instead. Bucket table is the authoritative spec.

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

* fix(29-w1): update test_shared_agent_bootstrap for D-03 CA-file gate

Plan 29-01 added a CA-file fail-fast in construct_agent_client. Two pre-
existing tests in tests/test_tasks/test_shared_agent_bootstrap.py called
the function without providing PHAZE_AGENT_CA_FILE and broke once Wave 1
merged. Use cert_bootstrap.ensure_certs_present in the test fixture to
generate a real CA that httpx can load via verify=path.

Caught by the post-merge test gate after Wave 1 of phase 29.

* docs(phase-29): update tracking after wave 1

* test(29-04): add failing YAML-parse tests for docker-compose.agent.yml (RED)

Four LOCKED structural assertions for the file-server compose:
  1. test_agent_compose_service_list — exactly {worker, watcher, audfprint, panako}
  2. test_agent_compose_has_no_postgres_env — DIST-04 invariant
  3. test_worker_service_has_phaze_role_agent — D-17
  4. test_all_scan_path_mounts_use_failfast_syntax — WARNING-3

All four currently fail because docker-compose.agent.yml does not yet
exist. The GREEN commit will create the compose file + env template.

Per WARNING-3: the fail-fast regex test rejects future drift to
${SCAN_PATH:-/data/music} loose-default form that would let
docker compose up succeed on a misconfigured file-server host.

Refs phase 29-04 plan, D-15..D-17, D-22 test surface.

* test(29-06): add failing tests for SAQ heartbeat cron handler

- 4 happy-path tests in test_heartbeat_cron.py:
  * success: heartbeat_tick POSTs HeartbeatRequest with correct payload
  * ctx-missing: missing api_client/agent_identity -> WARNING + return
  * queue.info-fail: queue_depth defaults to 0; heartbeat still POSTs
  * importlib metadata: agent_version sourced from importlib.metadata
- 1 failure test in test_heartbeat_failure.py:
  * AgentApiServerError -> WARNING logged; no exception escapes (D-09)
- Tests use ctx["worker"].queue (NOT ctx["queue"]) per RESEARCH Pitfall 8
- AgentApiServerError constructed positional-only (no status_code= kwarg)
- RED step: tests FAIL with ModuleNotFoundError until heartbeat.py lands

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

* feat(29-04): create docker-compose.agent.yml + .env.example.agent (GREEN)

The file-server-host compose file (D-15) declares exactly 4 services:
worker, watcher, audfprint, panako. Worker + watcher pull from
ghcr.io/simplicityguy/phaze (D-16 with `${PHAZE_IMAGE_TAG:-latest}`);
sidecars retain `build:` (not yet on GHCR per D-15).

All 4 services use `${SCAN_PATH:?SCAN_PATH required}` fail-fast
interpolation (WARNING-2 unified explicit-message form; WARNING-3
test enforces). MODELS_PATH bind mount is rw on worker + watcher for
D-21 auto-download; CA_PATH bind mount is ro everywhere.

Adds `.env.example.agent` with every variable a file-server host
needs (D-23 portion): PHAZE_IMAGE_TAG, PHAZE_AGENT_API_URL,
PHAZE_REDIS_URL, PHAZE_AGENT_{ID,TOKEN,QUEUE}, PHAZE_AGENT_CA_FILE,
PHAZE_AGENT_ENV, SCAN_PATH, MODELS_PATH, CA_PATH,
PHAZE_AGENT_SCAN_ROOTS. Production-pin guidance lives in inline
comments.

Also resolves the Plan 29-05 deferred test failure: updates
`tests/test_phase04_gaps.py::test_docker_compose_has_agent_worker_consuming_agent_queue`
to scan BOTH `docker-compose.yml` and `docker-compose.agent.yml`. The
agent-worker now lives in `docker-compose.agent.yml::worker`, so the
Phase 27 UAT gap-13 invariant is again codified across the split
compose surface. Marks the deferred-items.md entry resolved.

All 4 RED tests now pass.

Refs phase 29-04 plan, D-15, D-16, D-17, D-22 (agent-compose portion),
D-23 (.env.example.agent portion).

* test(29-04): add failing workflow-tag check (WARNING-4 RED)

Adds test_docker_publish_workflow_tags_both_latest_and_version — a 5th
test in test_agent_compose.py. Replaces the original checkpoint:human-verify
task with an automated YAML-parse check that .github/workflows/docker-publish.yml
emits BOTH a `:latest` tag and a `:v<version>` tag (D-16).

Currently fails because docker-publish.yml's docker/metadata-action step
only declares `type=raw,value=latest`, `type=ref,event=branch`, `type=ref,event=pr`,
and `type=schedule,pattern=...` — no `type=semver,pattern={{version}}` and
no `type=ref,event=tag`. The GREEN commit will extend the workflow.

The plan stays autonomous (no human checkpoint); the regression-detection
is now in CI permanently.

Refs phase 29-04 plan WARNING-4 resolution.

* feat(29-04): extend docker-publish.yml tag strategy + realign api URL (GREEN)

Two coupled changes to satisfy the WARNING-4 automated test and align
the published api image URL with docker-compose.agent.yml:

1. Tag strategy (D-16 + WARNING-4): the docker/metadata-action step now
   emits `type=raw,value=latest`, `type=semver,pattern={{version}}`,
   `type=semver,pattern={{major}}.{{minor}}`, `type=ref,event=tag`,
   `type=ref,event=branch`, `type=ref,event=pr`, and the schedule tag.
   On a tagged release `v4.0.0`, this produces `:latest`, `:v4.0.0` (via
   ref,event=tag), `:4.0.0` and `:4.0` (via semver). Operators get the
   full set of stability rungs the .env.example.agent comments
   reference (PHAZE_IMAGE_TAG=v4.0.0 production pin).

2. Image URL realignment (D-15): the matrix entry for `api` now sets
   `image_suffix: ""`, pushing to the BARE-repo URL
   `ghcr.io/simplicityguy/phaze:<tag>` — the exact URL
   docker-compose.agent.yml's worker + watcher pull from. The sidecars
   keep their `/audfprint` and `/panako` sub-paths because agent.yml
   builds them locally (D-15) and does not pull them from GHCR.

This makes `test_docker_publish_workflow_tags_both_latest_and_version`
pass, closing WARNING-4 with an autonomous test instead of a
checkpoint:human-verify task.

Verification result: `fixed` + `url-realigned` (both the tag pattern
and the image URL needed adjustment).

Refs phase 29-04 plan WARNING-4 resolution.

* docs(29-04): complete docker-compose.agent.yml + GHCR-tag verification plan

OPS-02 fully closed. Lands the file-server-host compose surface
(docker-compose.agent.yml + .env.example.agent) with exactly 4
services and replaces the original GHCR-tag human-verify checkpoint
with an automated YAML-parse test (WARNING-4 resolution). Workflow
extended to emit :v<version> tags and api image realigned to the
bare-repo URL ghcr.io/simplicityguy/phaze.

Also resolves the Plan 29-05 deferred test (gap-13 invariant now
codified across the split compose surface).

Plan stays autonomous: true. 5 new tests, all green, no regressions
across deployment + adjacent suites (22 tests).

* feat(29-06): wire SAQ 30s heartbeat cron handler (OPS-04 caller)

Land the agent-side half of OPS-04 (D-07..D-10):

- New src/phaze/tasks/heartbeat.py with heartbeat_tick(ctx) async cron
  handler. Reads ctx["api_client"], ctx["agent_identity"],
  ctx["worker"].queue (NOT ctx["queue"] per RESEARCH Pitfall 8); builds
  HeartbeatRequest(agent_version=importlib.metadata.version("phaze"),
  worker_pid=os.getpid(), queue_depth=Queue.info()["queued"]) and POSTs
  it via PhazeAgentClient.heartbeat.
- Defensive: ctx not initialized -> WARNING + return; queue.info()
  failure -> default queue_depth=0 + still POST; AgentApiError -> WARNING
  + swallow (D-09 fire-and-forget; SAQ retries on next tick).
- agent_worker.py: import CronJob + heartbeat_tick; add heartbeat_tick
  to settings.functions; add cron_jobs=[CronJob(heartbeat_tick,
  cron="* * * * * */30", unique=True, timeout=10)]. Trailing-seconds
  6-field form per RESEARCH Critical Discovery #2 (the CONTEXT.md D-08
  leading-seconds example would fire every second; verified empirically
  with croniter -- gaps are 30s vs 1s).
- agent_worker.py stays a single .py file (Pitfall 9 avoided).
- heartbeat.py is Postgres-free (banner documents the invariant).

Tests: all 5 heartbeat tests pass (GREEN); test_task_split still passes;
tests/test_tasks/ suite passes (excluding the pre-existing Plan 29-03/04
deferred test_docker_compose_has_agent_worker_consuming_agent_queue).

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

* docs(29-06): summary for OPS-04 heartbeat caller plan

Captures Task 1 (RED tests) + Task 2 (GREEN implementation) outcomes,
threat-model mitigations, and the one notable RESEARCH Critical
Discovery #2 fact: the cron string is the trailing-seconds 6-field form
`* * * * * */30` (NOT the leading-seconds form from CONTEXT.md D-08).

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

* docs(phase-29): update tracking after wave 2

* docs(29-08): justfile recipes, deployment.md, PROJECT.md, update-project.sh (Task 1)

- justfile: add up-agent + up-all recipes under [group('dev')]; existing `up` unchanged
- docs/deployment.md (new, 230 lines): 6-step two-host operator walkthrough,
  D-20 filesystem-isolation smoke, CA rotation guidance, production checklist;
  required strings present: phaze-ca.crt (9), just up-agent (5), REDIS_PASSWORD (3),
  /admin/agents (2), PHAZE_AGENT_TOKEN (2)
- .planning/PROJECT.md: new "### Deployment (v4.0 — Distributed Agents)" subsection
  under Constraints; documents two-compose-file invariant, HTTPS internal CA,
  Redis password-bound LAN, zero-new-pip-deps beyond cryptography
- scripts/update-project.sh: audited (pure dependency/version orchestrator with no
  Python module enumeration); left untouched per plan rule

Closes D-18 (justfile recipes), D-20 (filesystem-isolation smoke documented),
D-23 (operator workflow + doc sweep).

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

* docs(29-08): close Task 2 with verified-docs-only signal (SUMMARY)

Operator reviewed docs/deployment.md end-to-end against the live codebase
and confirmed all commands, env vars, routes (/admin/agents,
/api/internal/agent/heartbeat), and paths (/data/music, /certs/phaze-ca.crt)
match the compose mounts and router prefixes. Cert-bootstrap banner text
matches src/phaze/cert_bootstrap.py verbatim.

Resume signal: verified-docs-only (Option C from the checkpoint).

Follow-up: real-deployment smoke deferred until file-server hardware is
available — tracked as a v4.0 outstanding UAT item in the SUMMARY's
"Outstanding Items" section. Structural CI tests under tests/test_deployment/
cover compose-file invariants in the meantime.

Plan 29-08 complete — Phase 29 (deployment-hardening-agents-admin) closed.

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

* docs(phase-29): update tracking after wave 3

* test(29): persist human verification items as UAT

* fix(29-cr-01,cr-02): production https:// guard + bind PHAZE_REDIS_URL env-var

Two critical issues from gsd-code-reviewer's REVIEW.md, both in
src/phaze/config.py:

CR-01: .env.example.agent:22 references AgentSettings._enforce_https_in_production
as a guard that refuses http:// agent_api_url in production. The guard did
not exist — operators following the docs could ship plaintext bearer tokens.
Adds the validator (and three test cases covering https-ok / http-blocked /
dev-permits-http).

CR-02: BaseSettings.redis_url and database_url had no validation_alias, so
pydantic-settings only accepted bare REDIS_URL / DATABASE_URL env-var names.
Operators using PHAZE_REDIS_URL (as documented in .env.example.agent) hit
the default passwordless URL and tripped _enforce_redis_password_in_production
with a misleading "requires a password" error — preventing the production
agent from starting. Adds AliasChoices on both fields and an env-var
integration test that exercises the binding via AgentSettings() with no
kwargs (the 29-02 tests pass kwargs directly and never hit the env path).

* fix(29-cr-03): model_bootstrap rejects partial-download state

ensure_models_present previously short-circuited whenever *any* .pb file
existed in the models directory. An interrupted first download (e.g., 1/34
files written before SIGTERM) permanently left every subsequent agent
start skipping re-download — the agent would silently break at analysis
time when essentia tried to load the 2-33 missing weights.

Compares the observed .pb count against
len(CLASSIFIER_MODELS) + len(GENRE_MODELS). Partial state logs WARNING
with the observed/expected counts and re-invokes download_to, which is
idempotent at the per-file level (_download_one skips existing dests).

Two test updates:
- tests/test_services/test_model_bootstrap.py: populated-no-op test now
  writes all 34 expected files; new partial-triggers-redownload test
  pins the WARNING path.
- tests/test_tasks/test_agent_startup_banner.py: two pre-existing tests
  patched pathlib.Path.glob to return a single fake .pb and relied on
  the old loose check. Patch ensure_models_present directly so the
  banner / queue-mismatch logic under test is not coupled to the
  completeness rule.

Caught by the gsd-code-reviewer agent (Phase 29 REVIEW.md CR-03).

* docs(phase-29): complete phase execution

* docs(milestone-v4.0): audit report — passed with documentation drift

3-source requirements cross-reference: all 26 requirements satisfied
in code (verification + summary + wiring). 22/22 cross-phase exports
wired, 12/12 internal API routes consumed, all 5 E2E flows traced.

Documentation drift surfaced (does not affect runtime):
- 13 REQUIREMENTS.md traceability entries stale at Pending despite
  verified-passed phases (DIST-04/05, DATA-01..04, AUTH-01/04,
  TASK-04, EXEC-01..04)
- ROADMAP.md Phase 24 checkbox still [ ]
- Phase 24 VERIFICATION.md filename unprefixed

Tech debt carried into post-milestone backlog: P28-WR-03, P28-RACE-01,
P29-WR-01..04, P29-IN-01..03 (all advisory; none block archive).

* docs(milestone-v4.0): close documentation drift surfaced by audit

1. REQUIREMENTS.md: 13 stale `[ ]` checkboxes → `[x]` and 13 `| Pending |`
   traceability rows → `| Complete |` (DIST-04, DIST-05, DATA-01..04,
   AUTH-01, AUTH-04, TASK-04, EXEC-01..04). Traceability table footer
   bumped to today with the v4.0 completion note. The integration
   checker confirmed all 26 requirements are wired in code; the rows
   were already satisfied — the doc just hadn't been touched since
   2026-05-11.

2. ROADMAP.md: flip Phase 24 checkbox `[ ]` → `[x] (completed 2026-05-11)`.
   Phase 24 VERIFICATION.md `status: passed, score 4/4` since 2026-05-11.

3. Rename `phases/24-schema-foundation-agent-registry/VERIFICATION.md`
   → `24-VERIFICATION.md` to match the v4.0 convention used by every
   other phase (`{phase_num}-VERIFICATION.md`). The unprefixed name
   broke `gsd-sdk query find-phase` discovery.

* docs(phase-29): add REVIEW.md from gsd-code-reviewer audit

* chore: archive v4.0 milestone files

Snapshot v4.0 Distributed Agents milestone:
- .planning/milestones/v4.0-ROADMAP.md (full phase details)
- .planning/milestones/v4.0-REQUIREMENTS.md (26/26 satisfied)
- .planning/milestones/v4.0-MILESTONE-AUDIT.md (moved from .planning/)
- MILESTONES.md prepended with v4.0 entry + delivered summary
- PROJECT.md full evolution review (Current State, Validated, Key Decisions outcomes)
- STATE.md status -> milestone_complete; v4.0 velocity recorded
- ROADMAP.md collapses v4.0 Phases 24-29 into <details>

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

* chore: remove REQUIREMENTS.md for v4.0 milestone close

Archived to .planning/milestones/v4.0-REQUIREMENTS.md (all 26 reqs
satisfied). Next milestone will define fresh requirements via
/gsd:new-milestone.

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

* docs: update retrospective for v4.0 milestone

Append v4.0 Distributed Agents section: what was built, what worked,
what was inefficient, patterns established (settings split factory,
subprocess import-boundary tests, 403-before-state-machine guard,
pre-uvicorn entrypoint shim, etc.), key lessons (8), cost
observations.

Update Cross-Milestone Trends: process evolution row for v3.0 + v4.0,
cumulative quality table, top lessons (7) verified across milestones.

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

* fix(ci): unbreak Phase 29 CI — 3 root causes

1. **`admin_agents` router 422s on all 9 tests + production** —
   `AsyncSession` was imported under `if TYPE_CHECKING:` with
   `from __future__ import annotations`, so FastAPI's `get_type_hints`
   could not resolve the runtime annotation in
   `Annotated[AsyncSession, Depends(get_session)]` and treated
   `session` as a query parameter (`422 {"detail":[{"type":"missing","loc":["query","session"]}]}`).
   Move `from sqlalchemy.ext.asyncio import AsyncSession` to a
   runtime import (matches `agent_files.py` pattern, noqa TC002 since
   FastAPI requires runtime resolution). Fixes all 9
   `test_admin_agents.py` failures and the production route.

2. **`test_settings_redis_url_default` env leak** — Phase 29-02 added
   `PHAZE_REDIS_URL` as a pydantic `AliasChoices` alias for the
   `redis_url` field, and CI sets `PHAZE_REDIS_URL=redis://localhost:6379/0`
   for the test Redis service. The default-value test only deleted
   `REDIS_URL`, not the new alias. Delete all three spellings
   (`PHAZE_REDIS_URL`, `REDIS_URL`, `redis_url`) before asserting on
   the default.

3. **`validate-docker-compose` job parse-fail** — Phase 29-03 added
   `${REDIS_PASSWORD:?REDIS_PASSWORD required}` to the application-server
   `docker-compose.yml`, and Phase 29-04 added `${SCAN_PATH:?...}` on
   four services in the new `docker-compose.agent.yml`. The CI job
   only did `touch .env`, so compose-parse fail-fast tripped before
   anything could be validated. Supply placeholders for both compose
   files and validate the new agent compose alongside the existing
   app-server compose.

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

* test(29): close codecov patch-coverage gaps on PR #63

Codecov flagged 26 lines missing across 3 Phase 29 modules
(patch coverage 90.33%). This commit takes all three files to 100%.

- tests/test_entrypoint.py (new, 14 lines covered)
  Monkeypatches ensure_certs_present + os.execvp; verifies
  env-var defaults, env-var overrides, and the ensure→execvp
  sequencing invariant (RESEARCH Pattern 2: cert files must
  exist before uvicorn boots against --ssl-keyfile/--ssl-certfile).

- tests/test_scripts/test_download_models.py (new, 10 lines covered)
  respx-mocked tests for _download_one (idempotent skip on existing
  dest, atomic .part-then-rename on success, 4xx leaves dest absent
  so model_bootstrap's *.pb glob retries) and download_to (walks
  CLASSIFIER_MODELS + GENRE_MODELS, no-ops on a populated dir).

- tests/test_cert_bootstrap.py — added test_unparseable_existing_
  certs_trigger_regeneration to cover lines 202-203 (the WARNING
  + regeneration branch when all 4 files exist but parse as garbage).

- Tagged the two `if __name__ == "__main__":` CLI invocation guards
  in entrypoint.py and download_models.py with `# pragma: no cover`
  so coverage reflects what is reachable from `python -m`.

Coverage after this commit:
  cert_bootstrap.py:    96.77% -> 100.00%
  entrypoint.py:         0.00% -> 100.00%
  download_models.py:   67.74% -> 100.00%

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

---------

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