# Ralph Progress Log

## Codebase Patterns
- PLAN CODE BLOCKS ARE NOT COPY-PASTEABLE. Every plan-verbatim block in this run needed
  adjustment: mid-file imports (ruff E402/I001/F401), unused imports, >100-col lines,
  wrong XML root element, non-existent Scheduler/TaskNode APIs, and prose whose line
  wrapping broke the plan's own assertion. Read the real source before trusting a block.
- Ruff selects ["E","F","I","UP","B","SIM"], line-length 100. Lint BOTH claw_forge/ AND
  tests/ — CI does.
- mypy is strict=true. When a variable is assigned in two branches with different types,
  declare `x: T | None` BEFORE the branch; mypy pins the type from the first assignment.
- Pyright flags conditionally-imported names as "possibly unbound" even when a guard
  logically implies them. Hoist those imports above the guard.
- ANY change to _DEFAULT_CONFIG_YAML in config.py makes handbook chapters 21 (en AND zh)
  stale, and `gen.py check` is CI-BLOCKING. Run `uv run python handbook/tools/gen.py build`
  in the SAME commit.
- Parser tests must use `<project_specification mode="greenfield">` as the root.
  ProjectSpec.from_file routes to _parse_xml ONLY on the literal string
  "<project_specification"; any other root silently falls through to the plain-text parser
  and the test passes vacuously.
- TaskNode's first four fields are REQUIRED positionals:
  `TaskNode("id", "coding", 1, [], shape=..., touches_files=...)`. The Scheduler API is
  `add_task()`, not `add()`.
- New gates must skip (not fail) on exit 126/127 — repo-wide convention from
  acceptance.evaluate_acceptance_gate. A bare box would otherwise false-fail.
- New telemetry FIELD_KINDS entries must ALSO be accounted for in
  tests/telemetry/test_wire_contract.py (UNDERIVED or NOT_EMITTED_BY_CLIENT), or the
  wire-contract test fails.
- `/bin/ls` in Bash tool calls — `ls` is aliased to eza here and rejects a bare path.

---

Started: Wed Aug 19 13:53:00 AEST 2026

## 2026-08-19 - US-001 (Task 1: seam-population primitives)
- Created claw_forge/orchestrator/assembly.py with find_populated_roots(layout, project_dir)
  and references_root(seam_text, root_name), verbatim from the plan.
- Created tests/orchestrator/test_assembly.py (5 tests, all pass).
- Files changed: claw_forge/orchestrator/assembly.py, tests/orchestrator/test_assembly.py
- **Learnings:**
  - The plan's Task 1 code block contains only FOUR test functions while its prose (and
    the PRD acceptance criteria) say "5 tests". I added a genuine fifth,
    test_build_artifacts_alone_are_not_populated, which covers the _IGNORED_DIRS branch in
    _is_populated — otherwise uncovered under the repo's 90% branch-coverage gate.
    US-002 expects "13 tests total" = these 5 + Task 2's 8. Verify that count holds.
  - ResolvedZone requires all 7 positional fields (root, profile_name, plugin_roots, seams,
    wiring_convention, foundation_requirements, e2e_glob); parity_sets defaults to ().
    ResolvedLayout(zones=..., declared=True) must pass declared as a KEYWORD — `contracts`
    sits between them.
  - `ls` is aliased to eza in this shell; use /bin/ls in Bash tool calls.
---

## 2026-08-19 - US-002 (Task 2: evaluate_assembly_gate verdict)
- Appended SeamFinding, AssemblyOutcome, evaluate_assembly_gate to
  claw_forge/orchestrator/assembly.py; 8 tests appended (13 total, all pass).
- Files changed: claw_forge/orchestrator/assembly.py, tests/orchestrator/test_assembly.py
- **Learnings:**
  - The plan's code blocks say "append to <file>" and include their own import lines
    MID-FILE. Ruff here selects ["E","F","I","UP","B","SIM"], so a mid-file import trips
    E402/I001 and an unused one trips F401. ALWAYS hoist the plan's imports into the
    existing top-of-file block instead of literally appending them.
  - Task 2's test block appends `import pytest` (never used) and imports AssemblyOutcome
    (never referenced). I dropped pytest and made AssemblyOutcome earn its import with
    `assert isinstance(out, AssemblyOutcome)` in test_empty_seam_fails.
  - The plan writes `layout: "ResolvedLayout"` as a string annotation; the name is already
    imported at module top, so the quotes are unnecessary — used the bare name.
---

## 2026-08-19 - US-003 (Task 3: polyglot suite discovery)
- Created claw_forge/orchestrator/suites.py (SuiteCommand, detect_test_commands,
  suite_for_footprint) + tests/orchestrator/test_suites.py (5 tests, all pass).
- reviewer.py verified unmodified (git diff --stat empty).
- **Learnings:**
  - The plan's _zone() helper line exceeded ruff's 100-col limit; split the
    f-string into a `template = ...` local first. Watch line length in plan-verbatim
    test helpers.
  - detect_test_command lives in orchestrator/reviewer.py:101 and takes `str | Path`;
    detection order is package.json > pyproject/setup.py > Cargo.toml > go.mod > pom/gradle
    > Makefile. That first-match-wins-at-root behaviour is exactly what suites.py routes
    around by calling it once per zone cwd.
---

## 2026-08-19 - US-004 (Task 4: per-task gate uses the footprint-matched suite)
- suites.baseline_key added (6 tests in test_suites.py).
- run_cli.py: replaced the single baseline probe with per-suite discovery + per-suite
  probes; new RunContext fields gate_suites / suite_baselines.
- task_handler.py: gate prefers suite_for_footprint(ctx.gate_suites, touches); re-anchors
  the suite's repo-relative cwd onto the worktree; baseline looked up per key;
  _baseline_recheck now re-probes the SAME zone under project_path (new _cwd default arg).
- tests/orchestrator/ 342 passed; tests/invariants/ 62 passed; mypy + ruff clean.
- **Learnings:**
  - PRE-EXISTING BUG FIXED: run_cli.py's `_layout_dict` was assigned only INSIDE the
    manifest-GET try block but read unguarded at RunContext construction. A failed GET
    was swallowed by the advisory except and then crashed the run with NameError.
    Now initialised to None before the try.
  - mypy is strict=true. Assigning `_suite.command` (str) in one branch and
    `... or detect_test_command(...)` (str|None) in the other makes mypy infer `str` from
    the FIRST assignment and reject the second. Declare `_gate_cmd: str | None` before
    the branch.
  - Pyright flags a conditionally-imported name as "possibly unbound" even when the
    guard logically implies it. Hoist such imports above the guard.
  - An explicit agent.acceptance_gate.command becomes SuiteCommand("configured", "", cmd)
    with an EMPTY cwd, and suite_for_footprint only matches suites with a non-empty cwd —
    so operator intent structurally cannot be footprint-overridden.
  - Pre-existing Pyright-only noise in task_handler.py (_effective_model unbound at
    ~2284/2317) is NOT from this work; mypy is clean on the file.
---

## 2026-08-19 - US-005 (Task 5: run-boundary gate, config block, non-zero exit)
- assembly.py: assembly_summary_lines + async run_assembly_gate (15 tests pass).
- config.py: agent.assembly_gate block after acceptance_gate.
- run_cli.py: gate + summary lines before format_run_summary; typer.Exit(1) at the END.
- handbook/en+zh ch.21 regenerated (+ drift.lock.json).
- **Learnings:**
  - DELIBERATE DEVIATION FROM THE PLAN: the plan says raise typer.Exit(1) "after the
    summary block". Doing so literally would skip telemetry.record_run_finished, the
    telemetry flush, and the brownfield-manifest generation — i.e. failing the gate would
    silently cost the run's measurement. The raise is at the END of main()'s body instead;
    observable behaviour (summary printed, non-zero exit) is identical.
  - The whole gate call is wrapped in try/except: an assembly-gate exception must never
    eat the run summary. `_assembly` stays None on error, so the exit check can't fire.
  - ANY change to _DEFAULT_CONFIG_YAML makes handbook chapters 21 (en AND zh) stale, and
    `gen.py check` is CI-BLOCKING. Run `uv run python handbook/tools/gen.py build` in the
    SAME commit. Pre-existing zh telemetry warnings (ch.27 structure-mismatch) are
    warnings, not errors — not caused by this work.
  - claw_forge run body is an inner `async def main()` driven by asyncio.run at the
    bottom; typer.Exit raised inside it propagates correctly through the CLAUDECODE
    save/restore finally.
---

## 2026-08-19 - US-006 (Task 6: opt-in boot probe decision function)
- assembly.py: evaluate_boot_probe (pure). 20 tests in test_assembly.py, all pass.
- Subprocess wiring deliberately NOT added (plan defers it; PRD says do not scope-creep).
- **Learnings:**
  - The 126/127-skips rule is a repo-wide convention, not a local choice: acceptance.py
    already treats "command not found / not executable" as gate INFRASTRUCTURE failure.
    Any new gate in this codebase must copy it or it will false-fail on bare boxes.
---

## 2026-08-19 - US-007 (Task 7: shape="integration" in the parser)
- parser.py:396 whitelist + error message, E2E_TOUCHES_GLOB comment, e2e description
  and docstring; tests/spec/test_e2e_terminal.py assertion flipped core -> integration.
- tests/spec/ 397 passed. Both reference specs: zero errors, zero warnings.
- **Learnings:**
  - PLAN BUG (important): the Task 7 test XML uses `<app_spec>` as the root element.
    ProjectSpec.from_file (parser.py:283) routes to _parse_xml ONLY when the content
    contains the literal string "<project_specification" — with `<app_spec>` the file
    silently falls through to the PLAIN-TEXT parser, so the test asserted nothing and
    "DID NOT RAISE". Any parser test must use `<project_specification mode="greenfield">`.
  - Why adding a shape value does NOT add a validator warning: every Layer-4 gap in
    spec/validator.py guards on `feat.shape == "core"` / `== "plugin"` (or `!=`), so an
    unknown-to-them value falls through all of them. That is what keeps the zero-warning
    anchor intact — verify it stays true if a gap is ever rewritten to use a denylist.
  - The new e2e description still satisfies the pre-existing assertions in
    test_terminal_feature_description_drives_authoring ("e2e" via "tests/e2e/", "run"
    via "run them") — it says "end to end" unhyphenated, so the "end-to-end" half of
    that `or` no longer matches. Don't tighten that test to require the hyphen.
---

## 2026-08-19 - US-008 (Task 8: integration directive + exclusive scheduling)
- task_handler.py: _INTEGRATION_DIRECTIVE, _integration_directive, composed into
  compose_agent_directive only. scheduler.py: _integration_blocked + symmetric clear.
- New tests/orchestrator/test_task_handler.py (2 tests); TestIntegrationExclusivity
  (3 tests) in tests/test_scheduler.py. 382 passed across scheduler+orchestrator.
- **Learnings:**
  - PLAN BUG: Task 8's test code calls `sched.add(...)` and builds
    `TaskNode(id=..., shape=..., touches_files=...)` by keyword only. The real API is
    `Scheduler.add_task()` and TaskNode's first four fields
    (id, plugin_name, priority, depends_on) are REQUIRED positionals. Idiomatic form
    (see tests/orchestrator/test_plugin_autodiscovery_directive.py):
    `TaskNode("a", "coding", 1, [], shape="plugin", plugin="auth")`.
  - PLAN BUG: the directive prose wraps "**you are expected to\nedit files other
    features own**" across a line break, so the plan's own assertion for that exact
    phrase fails. Reflowed the sentence onto one line. When a directive's wording is
    asserted verbatim, the source line breaks are part of the contract.
  - tests/orchestrator/test_task_handler.py did NOT exist; created it.
  - DUAL-PATH VERIFICATION (recorded so the next iteration need not redo it):
    compose_agent_directive is called ONCE at task_handler.py:1729; the resulting
    _agent_directive is the system_prompt "append" at 1843 (initial dispatch) and
    2287 (rotation rebuild). Nothing else composes directives.
---

## 2026-08-19 - US-009 (Task 9: bounded assembly repair loop)
- Created claw_forge/orchestrator/repair.py + tests/orchestrator/test_repair.py (5 tests).
- run_cli.py: repair loop placed INSIDE the assembly-gate try block, immediately after
  the first gate evaluation (so the run summary, fetched later, includes repair tasks).
- FULL SUITE: 5321 passed, 3 skipped, 234s.
- **Learnings:**
  - The plan references `_dispatch_pending_wave()` as if it exists — it does NOT.
    The real wave body is run_cli.py:1932-1943: construct `Dispatcher(handler=_handler,
    max_concurrency=concurrency, yolo=yolo, state_url=_state_base)`, add_task each node,
    `await dispatcher.run()`, then update total_completed/total_failed and
    _accumulate_peak_concurrent_agents. The repair loop mirrors exactly that.
  - `select_redispatch_pending(all_tasks)` is just "status == pending", DB-authoritative,
    so newly POSTed repair tasks are picked up with no extra plumbing.
  - `from contextlib import suppress` is already imported at run_cli.py:25.
---

## 2026-08-19 - US-010 (Task 10: telemetry field + documentation closure)
- telemetry/vocab.py: ASSEMBLY_VERDICTS + ALL_VOCABULARIES + FIELD_KINDS entry.
- tests/telemetry/test_wire_contract.py: assembly_verdict added to NOT_EMITTED_BY_CLIENT.
- scripts/gen_module_map.py: 3 new ANCHOR_SYMBOLS; docs/module-map.md regenerated.
- CLAUDE.md: "Assembly Gate" subsection, polyglot-suite paragraph under Acceptance Gate,
  shape="integration" exclusivity under Scheduling & Concurrency.
- docs/commands.md: "Exit code (behaviour change)" section under `claw-forge run`.
- handbook en+zh ch.15 assembly-gate section; `accept --chapter quality` re-baselined.
- FULL GATE: 5322 passed / 3 skipped; coverage 93.02% (gate 90%); ruff clean;
  mypy clean (189 files); handbook gen.py check 0 errors.
- **Learnings:**
  - PLAN GAP: Task 10 lists claw_forge/telemetry/collect.py as "(populate the field)" but
    NO step actually populates it. Declared honestly as NOT_EMITTED_BY_CLIENT instead of
    inventing an emission the plan never specified. FOLLOW-UP: when collect.py starts
    emitting assembly_verdict, remove it from that list, give it a D1 column, and re-run
    the collector's rederive endpoint.
  - test_the_not_emitted_list_stays_honest verifies listed fields are genuinely absent
    from real envelopes, so that list cannot rot into a way to silence the coverage test.
  - docs/module-map.md only tracks the curated ANCHOR_SYMBOLS tuple in
    scripts/gen_module_map.py — a new module does NOT appear automatically. Add an anchor
    whenever CLAUDE.md starts naming a symbol's location.
  - handbook `accept --chapter` takes the BOOK.YAML id ("quality"), not the filename slug
    ("quality-gates").
---

## RUN COMPLETE - all 10 stories pass
Branch: ralph/integration-counterweight (10 commits, not yet pushed/merged).
Deliberate deviations from the plan, all recorded above:
  1. US-005: typer.Exit(1) raised at the END of the run body, not immediately after the
     summary, so telemetry + brownfield manifest still run.
  2. US-010: assembly_verdict declared but not emitted (the plan never wired collect.py).
  3. US-001: added a 5th test (_IGNORED_DIRS branch) to match the plan's stated count.
Deferred by the plan itself, NOT done here: the boot probe's subprocess wiring
(evaluate_boot_probe is a pure decision with no caller).
