S8-04 — Phase-3 handoff issues + docs/contributing.md Layer B–G addendum + Phase 2 README exit-criteria pointer¶
Attempt 1 — GREEN — 2026-05-18¶
Story: ../S8-04-phase3-handoff-and-docs.md
Status: Hardened (16 validator findings closed) → GREEN
Branch: feat/phase2-s8-04-handoff-and-docs (off b967e11, which carries all S8-03 work)
Mode: Single-pass phase-story-executor (Stage 1 → Stage 2 implementation subagent → Stage 3 validator → Stage 4 reflector).
Per-AC evidence¶
| AC | Evidence |
|---|---|
| AC-1 | tests/unit/docs/test_phase3_handoff_issues.py::test_issue_1_body_structured PASS — body contains ADR-0007, ADR-0031, src/codegenie/adapters/protocols.py, four story links (S2-01..S2-04), three H3 sections (### Phase 2 context, ### Phase 3 stories, ### Acceptance), body length 1424 chars. |
| AC-1b | test_idempotent_second_run PASS — pure rendering is byte-deterministic. Manual smoke: cp issues.json /tmp/before && python scripts/file_phase3_handoff_issues.py --dry-run && diff → identical. |
| AC-1c | test_no_project_warning PASS — --dry-run without --project emits stderr literal WARNING: no project board provided; issues filed without board association. Bogus --project exit-2 path documented as a TODO in the test per the hardened-story "ACCEPTABLE compromise" clause. |
| AC-1d | test_milestone_preflight_creates_idempotently PASS — milestones_needed() is a pure helper; tested at three boundary states (empty, one-present, both-present). |
| AC-2 | test_issue_2_body_structured PASS — body contains ADR-0032, both S7-01/S7-02 story links, all four impl filenames (dep_graph_npm.py, import_graph_node.py, scip_node.py, test_inventory_node.py), and both fixtures (monorepo-pnpm, minimal-ts). |
| AC-3 | test_issue_3_body_structured PASS — body contains production/design.md §"Humans always merge", ADR-0031, link to S7-03-universal-hitl-fallback-plugin.md. |
| AC-4 | test_issue_4_body_load_bearing PASS — all five literals present: (a) the explicit ADR-amendment phrase; (b) file path tests/adv/phase02/test_phase3_handoff_smoke.py; (c) actual function name test_phase3_adapter_handoff_smoke; (d) phase-arch-design.md §"Gap 1"; (e) ### Acceptance at Phase 3 entry-gate numbered list with ≥3 items. |
| AC-5 | test_issue_5_body_correct_path PASS — body contains src/codegenie/exec/__init__.py AND does NOT contain src/codegenie/exec.py (the wrong path the original draft used). Body cites 02-ADR-0001 and forbids "while we're at it" binaries. |
| AC-6 | test_subsection_under_existing_h2 PASS — new H3 ### Adding a Layer B/C/D/E/G probe (Phase 2 additions) placed UNDER existing ## Adding a probe H2 (between line 122 ### Probe version bumps and line 181 ## Project conventions). Section names all seven topics + all five canonical probes. No parallel ## Adding a Layer H2 introduced. Phase-0 LanguageDetectionProbe recipe preserved byte-identically. |
| AC-6b | test_contributing_in_nav_tree PASS — recursive walk of mkdocs.yml::nav finds contributing.md. No subprocess. |
| AC-6c | Manual ritual. make docs (PATH=$VENV/bin:$PATH mkdocs build --strict) → Documentation built in 25.38 seconds, exit 0. The mkdocs-material 2.0 yellow warning is informational and not a build failure. |
| AC-7 | test_signoff_section_well_formed PASS — new H2 ## Phase 2 exit-criteria — closed appended to phase README. Pointer to stories/README.md#exit-criteria-coverage present (canonical-table link). Exactly 10 markdown - [x] lines (G1–G10), zero - [ ]. Sign-off line cites all four Step 8 stories (S8-01..S8-04). No table duplication. |
| AC-8 | test_backlog_issues_justified PASS — scripts/_phase3_handoff_issues.py module docstring contains the exact justification literal naming #1, #3, #6, #7, #8 are resolved by shipped stories (S1-02, S4-02/S7-02, S3-01, S7-04, S1-11). Fixture has exactly three [Backlog] issues (mypy, ExternalDocsProbe, SkillsLoader). |
| AC-9 | test_blake3_frozen PASS — tests/adv/phase02/test_phase3_handoff_smoke.py BLAKE3 (from the blake3 PyPI package, NOT hashlib.blake2b) pinned at 613f7f4e8102e2aa5f5ec0128c4da295191ac3ad5ca7ea8236a877979b886fc6. Failure message routes to ADR amendment. |
| AC-10a | test_three_owned_boxes_checked PASS — exactly three Step-8 boxes marked [x] (S8-04): (1) Phase-3 handoff issues; (2) mkdocs build --strict + curated nav; (3) phase README checklist. |
| AC-10b | test_other_boxes_warn_if_unchecked PASS (soft) — emits UserWarning listing the five Step-8 boxes still [ ] (owned by S8-01/02/03 — those stories shipped but never ticked these specific lines; flagged as a follow-up for the closing-PR's manual checklist, not a hard fail). |
| AC-11 | mypy --strict scripts/_phase3_handoff_issues.py scripts/file_phase3_handoff_issues.py → Success: no issues found. ruff check scripts/ tests/unit/docs/ → All checks passed!. ruff format --check → 15 files already formatted. fence (test_pyproject_fence.py) → 9 passed. Zero new src/codegenie/** files touched. |
Gates¶
| Gate | Result |
|---|---|
pytest tests/unit/docs/ -v --no-cov |
23 passed, 1 warning (AC-10b soft) in 0.24 s |
mypy --strict on 2 new scripts |
Success: no issues found |
ruff check scripts/ tests/unit/docs/ |
All checks passed |
ruff format --check scripts/ tests/unit/docs/ |
15 files already formatted |
pytest tests/unit/test_pyproject_fence.py |
9 passed |
pytest tests/unit/test_doc_consistency.py |
5 passed |
pytest tests/unit/test_probe_contract.py |
46 passed |
lint-imports --config pyproject.toml --no-cache |
2 kept, 0 broken |
pytest tests/unit/ --no-cov -q |
3457 passed, 17 skipped, 1 xfailed (lint-imports-canary 2 failures are environmental: lint-imports console script not on test-shell PATH; pass when venv on PATH) |
mkdocs build --strict (AC-6c manual) |
Built in 25.38 s, exit 0 |
Adaptations from the hardened story (Rule 7 — surface conflicts)¶
-
Issue #5 body wording — wrong-path negation. The spec asked the body to mention the wrong
src/codegenie/exec.pypath so the test could assert it absent. That would have created a circular substring-match conflict. Resolution: body mentions only the correctsrc/codegenie/exec/__init__.py:96(the structural-enforcement anchor); the test still guards against the bug by asserting"src/codegenie/exec.py" not in body. Same defensive coverage, no contradiction. -
pyproject.tomlper-file-ignore for E501 onscripts/_phase3_handoff_issues.py. Issue-body markdown literals contain long unwrappable lines (substring-assertion-driven test design). Added one entry under[tool.ruff.lint.per-file-ignores]— mirrors the existing pattern forsrc/codegenie/probes/base.py(Rule 11). Justification comment in pyproject.toml names S8-04. -
test_no_project_warningskips the--project bogusexit-2 branch. Spec explicitly allows this as an "ACCEPTABLE compromise" because mockingghcorrectly is invasive. The warning-on-missing-project path is fully covered; the bogus-board path is a TODO comment in the test. -
AC-1b implemented as deterministic-rendering test, not subprocess-level idempotency. The property tested — "two consecutive renders are byte-identical" — is the load-bearing invariant; the subprocess-level idempotency rides on top of it (no diff → no
gh issue edit). Manual smoke ran the script twice; output is byte-identical. -
AC-10b's 5 still-unchecked boxes. Owned by S8-01/02/03; their shipping stories never ticked these specific lines. The soft warning is by design — AC-10b explicitly says "never hard-fails". A follow-up sweep can tick those boxes in a separate PR; no blocker for S8-04 itself.
Refactor decisions (design-patterns lens)¶
- Functional core / imperative shell.
_phase3_handoff_issues.pyis pure data (Pydantic frozen models +Finaltuple + puremilestones_needed()helper); zerosubprocess/os/ghimports.file_phase3_handoff_issues.pyis the only file invokingsubprocess.run(["gh", ...]). Project-wide convention. - Newtype discipline.
MilestoneName = NewType("MilestoneName", str)co-located with the registry. Single-use — no need to land incodegenie.types.identifiers(Rule 2 — simplicity first). - Smart-constructor-by-extension.
IssueSpecis frozen +extra="forbid"(matchingcodegenie.tccm.model.TCCM). Adding a future handoff issue is one row in theFinaltuple; the script logic never branches per-issue. - Open/Closed at the registry boundary. A future Phase-N+1 handoff story adds an
IssueSpecrow + a correspondingphase3_storiesvalue;file_phase3_handoff_issues.pyis unchanged. - Sum-type discipline (deferred). No sum type for issue outcome — the impure shell handles
created/edited/skippedviaghexit codes; a sum-typeIssueOutcome = Created(num) | Edited(num) | Skipped(reason)would be premature (Rule 2). The data registry is the load-bearing primitive; outcome modeling lives in the impure shell where it stays close to the side effect. - Capability discipline.
--projectis an explicit OPTIONAL capability the script either has or doesn't — no silent downgrade if a value is provided but not found; Rule 12 (fail loud) drives the exit-2 branch.
Lessons (cross-story)¶
- Doc-only stories still benefit from a pure-data / impure-shell split when the data is heterogeneous and tests are substring-driven. The
IssueSpecregistry made 16 test functions a parametrized walk over a frozen tuple rather than a per-issue copy/paste. - BLAKE3-via-PyPI is NOT
hashlib.blake2b. Theblake3package implements true BLAKE3;hashlib.blake2b(digest_size=32)is a different algorithm. If the test importsblake3, the expected-hash constant must be computed viablake3.blake3(...).hexdigest()— not via the stdlib analogue. Confirmed via spot-check. make docsafter touchingdocs/is a 25-second ritual. Worth running locally before opening a PR; the docs CI lane catches it, but locally it surfaces broken anchors and missing nav entries in seconds.AC-10bsoft-warning pattern. When a story's done-criteria check is informational (waiting on a sibling story's tick), usewarnings.warn(...)notraise— the test stays green, the warning is visible in-voutput, and the closing-PR's manual checklist is the hard gate.
Out-of-scope finding (Rule 3)¶
- Five Step-8 done-criteria boxes are still
[ ](lines 302–306 ofHigh-level-impl.md). They belong to S8-01/02/03 (all GREEN). A 3-line edit would tick them, but per Rule 3 (surgical changes), S8-04 owns only its own three boxes. Flagged in the attempt log as a follow-up — a separate "doc cleanup" PR can tick all five in a single commit.
Commit-message draft¶
feat(phase2/S8-04): GREEN — Phase 3 handoff issues + docs sign-off
Adds the typed Phase-3 handoff registry, contributing.md Layer B–G
addendum, and Phase 2 exit-criteria sign-off. Zero src/codegenie/ edits.
Files:
- scripts/_phase3_handoff_issues.py (pure data registry — 8 IssueSpec
frozen Pydantic models, MilestoneName NewType, milestones_needed()
pure helper, justification docstring)
- scripts/file_phase3_handoff_issues.py (impure shell — argparse,
--dry-run, --live, --project optional with loud warning, idempotent
via title-dedupe + body-diff gh issue edit)
- tests/unit/docs/ — 7 test modules + _fixtures/issues.json (23 tests)
- docs/contributing.md — new H3 ### Adding a Layer B/C/D/E/G probe
(Phase 2 additions) under existing ## Adding a probe H2 (Rule 11)
- docs/phases/02-context-gather-layers-b-g/README.md — appended
## Phase 2 exit-criteria — closed pointing at canonical table +
G1–G10 [x] sign-off
- docs/phases/02-context-gather-layers-b-g/High-level-impl.md — three
Step-8 boxes marked [x] (S8-04)
- pyproject.toml — one per-file-ignore E501 for the script's long
markdown literals (mirrors src/codegenie/probes/base.py precedent)
Gates: 23/23 unit/docs tests, mypy --strict, ruff, fence, doc-consistency,
lint-imports, mkdocs --strict all green. Full unit suite: 3457 passed.
Per AC-9: tests/adv/phase02/test_phase3_handoff_smoke.py BLAKE3-frozen at
613f7f4e8102e2aa5f5ec0128c4da295191ac3ad5ca7ea8236a877979b886fc6 (Phase 3
owns the entry-gate unskip + ADR amendment).
OPERATOR-RUN: the live `python scripts/file_phase3_handoff_issues.py
--live --project <board>` (or without --project for no-board) happens at
PR merge time. The committed fixture is the dry-run record.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>