From 4596c0074892fb950d53188123d6afb64ff00879 Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Wed, 30 Sep 2026 01:25:30 +0100 Subject: [PATCH 01/91] docs(intents): qualify the predecessor-store spec ids five intents cite itd-4, itd-6, itd-50, itd-65 and itd-66 cite spec ids of the retired predecessor store without the specs charter's qualifier, and each id is at or below spc-70, so it resolves in the live store to a spec on another subject. Each site was read against the live spec it collides with: - itd-6: spc-2, spc-4 and spc-5 are the earlier Python lineage's RP MCP specs (live: lifecycle automation, the transcript clock, folder classification); lines that already said "earlier Python lineage" in words now also carry the parenthetical. - itd-50: spc-52 is the audit-loop spec (live: dangling supersedes). - itd-65 and itd-66: spc-64 is the gitleaks and pii.py secret/PII gate (live: the read-block eval) and spc-27 the oracle (live: the surface-coverage registry). - itd-4: spc-20 to spc-23 are the ledger specs of the superseded record system (live: banlist, fresh install, stale-binary warning, tier placement). Two shipped acceptance criteria gain the qualifier (itd-4's drift criterion and itd-65's fail-closed criterion). Each names a precedent or an owner, not what is promised, so the qualifier restores the authored reading and changes no promise: a wording clarification, with no Audit Notes line. Refs: iss-2609290448510918 Assisted-by: Claude:claude-opus-5-5 --- .../planned/itd-50-loop-toward-acceptance.md | 4 ++-- .../planned/itd-6-rp-mcp-only-integration.md | 14 +++++++------- .../intents/shipped/itd-4-issue-capture.md | 10 +++++----- .../itd-65-launch-preflight-gate-suite.md | 16 ++++++++-------- .../itd-66-launch-payload-render-parity.md | 2 +- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.abcd/development/intents/planned/itd-50-loop-toward-acceptance.md b/.abcd/development/intents/planned/itd-50-loop-toward-acceptance.md index 32b7cfe8a..b816b7b1c 100644 --- a/.abcd/development/intents/planned/itd-50-loop-toward-acceptance.md +++ b/.abcd/development/intents/planned/itd-50-loop-toward-acceptance.md @@ -115,9 +115,9 @@ _None open; decisions 2 to 4 settle the four this record carried._ ## Prior Art -- `spc-52-audit-loop-to-acceptance-modes` — the predecessor implementation delivered this intent (tasks .1–.3); its AC reconciliation below is carried as design input per the brief's delivery-state provenance note. In this repo itd-50 is undelivered (nothing in the Go tree implements the audit loop) and ships only when this intent reaches `shipped/` with its own audit notes. +- `spc-52-audit-loop-to-acceptance-modes` (predecessor store) — the predecessor implementation delivered this intent (tasks .1–.3); its AC reconciliation below is carried as design input per the brief's delivery-state provenance note. In this repo itd-50 is undelivered (nothing in the Go tree implements the audit loop) and ships only when this intent reaches `shipped/` with its own audit notes. -### Predecessor AC reconciliation (spc-52) +### Predecessor AC reconciliation (spc-52, predecessor store) In the predecessor implementation each acceptance criterion above is satisfied and the open questions are resolved at its plan + build time; the table attributes which spc-52 (predecessor store) task owns which behaviour. diff --git a/.abcd/development/intents/planned/itd-6-rp-mcp-only-integration.md b/.abcd/development/intents/planned/itd-6-rp-mcp-only-integration.md index 00e6a810b..74e7b6946 100644 --- a/.abcd/development/intents/planned/itd-6-rp-mcp-only-integration.md +++ b/.abcd/development/intents/planned/itd-6-rp-mcp-only-integration.md @@ -21,7 +21,7 @@ impact: additive > **abcd has exactly one integration with RepoPrompt: the MCP API.** abcd never picks an `oracle`, never reads RP's preset selection, never spawns its own subprocess for code review. It calls RP via MCP and RP uses whatever `oracle` the persona has configured for whatever task — Claude via the persona's subscription, Codex via the persona's subscription, Gemini, any preset RP knows. The persona configures `oracle` backends inside RP once; abcd uses them forever. Zero abcd-side `oracle` logic, zero "which preset?" prompts, zero hard-coded routing. > -> **Status: no part of the RP MCP route is built.** The `RPUnavailable` error, the `MCPBridge` and the `oracle.py` audit-fix loop this record names belong to an earlier Python lineage: its spec `spc-5-rp-mcp-integration-declare` (not the `spc-5` in this repository's spec store) and its ADR-02 and ADR-03 (not this repository's adr-2 and adr-3). None of them is in this binary, and `go.mod` carries no MCP dependency. The re-filed scope, [spc-2609211950427074](../../specs/open/spc-2609211950427074-rp-mcp-only-integration.md), builds the route from nothing. See the Implementation status section. +> **Status: no part of the RP MCP route is built.** The `RPUnavailable` error, the `MCPBridge` and the `oracle.py` audit-fix loop this record names belong to an earlier Python lineage: its spec `spc-5-rp-mcp-integration-declare` (predecessor store; not the `spc-5` in this repository's spec store) and its ADR-02 and ADR-03 (not this repository's adr-2 and adr-3). None of them is in this binary, and `go.mod` carries no MCP dependency. The re-filed scope, [spc-2609211950427074](../../specs/open/spc-2609211950427074-rp-mcp-only-integration.md), builds the route from nothing. See the Implementation status section. > > "I had wired up Claude, Codex, and Gemini in RP with task-specific presets," said Bob, staff engineer. "I'd worried abcd would keep asking me which to use. The RP MCP bridge just calls RP; when RP is not reachable it raises a typed `RPUnavailable` so the tooling can react cleanly instead of guessing. RP picks the `oracle`. I don't think about it." @@ -42,7 +42,7 @@ This intent re-frames the brief's RP integration: drop "select RP backend with p - **Failure mode**: if RP MCP is unreachable, abcd falls through to Codex if configured, then in-session subagent. Three-step cascade. - **One-time RP setup discovery**: ahoy detects the RP MCP server config (in `~/Library/Application Support/RepoPrompt/MCP/` or `.mcp.json`), notes it in `.abcd/config.json` → `oracle.rp.mcp_config_path`, and tests reachability. If reachable: lock `oracle.backend = "rp"`. If not: lock `oracle.backend = "codex"` (if Codex CLI present) or `"in-session"` (final fallback) and surface a one-time hint about how to enable RP later. -- **Same-chat re-review semantics** (codified abcd rule, narrowed by ADR-02 § 3): when abcd re-runs an oracle/review/audit after applying fixes (plan-review → fix → re-review; impl-review → fix → re-review; lifeboat-oracle → fix → re-audit), the re-call MUST stay in the **same RP chat** — never `--new-chat`, never fresh `rp builder`. RP chats accumulate context (original artefact + first review + fix summary); same-chat re-runs let the model do incremental "are these fixes correct?" checks instead of starting from scratch. The harness `mcp_call` for an RP audit MUST return `chat_id` in `McpResult`; the audit-fix loop in abcd's `oracle.py` MUST thread that ID back as the `chat_id` arg on the next call. **Narrowed (ADR-02 Criterion 3b):** "same chat" means within one `abcd-cli` command invocation's stdio session. Cross-invocation chat continuation requires fresh GUI approval and is out of scope for autonomous operation. Same rule applies whether the backend is RP, Codex, or in-session subagent (in-session uses `Task` with continuation prompts). **Verdict direction across iterations**: the verdict can change in EITHER direction across audit-fix iterations — a fix can resolve issues (NEEDS_WORK→SHIP) AND a fix can introduce regressions (SHIP→NEEDS_WORK). Both are valid signal; abcd's `re_audit` MUST NOT reject downgrades (mirroring spc-2 spec's anti-pattern list). **Lifecycle narrowing (added post-spc-5, per ADR-02 § 3):** "same chat" now narrows further — it means **same-MCP-session / same-`MCPBridge`-instance only**. In spawn mode the session is per `abcd-cli` invocation; in host-reuse mode (ADR-03) the session lives for the lifetime of the injected host harness. A `chat_id` is only meaningful within the `MCPBridge` instance that produced it — cross-bridge `chat_id` reuse is undefined behaviour, not a supported continuation path. +- **Same-chat re-review semantics** (codified abcd rule, narrowed by ADR-02 § 3): when abcd re-runs an oracle/review/audit after applying fixes (plan-review → fix → re-review; impl-review → fix → re-review; lifeboat-oracle → fix → re-audit), the re-call MUST stay in the **same RP chat** — never `--new-chat`, never fresh `rp builder`. RP chats accumulate context (original artefact + first review + fix summary); same-chat re-runs let the model do incremental "are these fixes correct?" checks instead of starting from scratch. The harness `mcp_call` for an RP audit MUST return `chat_id` in `McpResult`; the audit-fix loop in abcd's `oracle.py` MUST thread that ID back as the `chat_id` arg on the next call. **Narrowed (ADR-02 Criterion 3b):** "same chat" means within one `abcd-cli` command invocation's stdio session. Cross-invocation chat continuation requires fresh GUI approval and is out of scope for autonomous operation. Same rule applies whether the backend is RP, Codex, or in-session subagent (in-session uses `Task` with continuation prompts). **Verdict direction across iterations**: the verdict can change in EITHER direction across audit-fix iterations — a fix can resolve issues (NEEDS_WORK→SHIP) AND a fix can introduce regressions (SHIP→NEEDS_WORK). Both are valid signal; abcd's `re_audit` MUST NOT reject downgrades (mirroring the anti-pattern list of spc-2, predecessor store). **Lifecycle narrowing (added after spc-5 (predecessor store), per ADR-02 § 3):** "same chat" now narrows further — it means **same-MCP-session / same-`MCPBridge`-instance only**. In spawn mode the session is per `abcd-cli` invocation; in host-reuse mode (ADR-03) the session lives for the lifetime of the injected host harness. A `chat_id` is only meaningful within the `MCPBridge` instance that produced it — cross-bridge `chat_id` reuse is undefined behaviour, not a supported continuation path. ## What's Out of Scope @@ -84,19 +84,19 @@ _None open._ ## Resolved (post-spc-5) -These questions were settled against the earlier Python lineage's design — its spc-5 spec and its ADR-02 and ADR-03, not this repository's spc-5, adr-2 and adr-3 — and the Phase 0 harness-interface research note ([`01-harness-interface.md`](../../research/notes/01-harness-interface.md)). The answers stand as design input for the re-filed adapter; the `MCPBridge`, `McpResult`, `RPUnavailable` and `oracle.py` they name are that lineage's, and none of them is in this binary. +These questions were settled against the earlier Python lineage's design — its spc-5 spec (predecessor store) and its ADR-02 and ADR-03, not this repository's spc-5, adr-2 and adr-3 — and the Phase 0 harness-interface research note ([`01-harness-interface.md`](../../research/notes/01-harness-interface.md)). The answers stand as design input for the re-filed adapter; the `MCPBridge`, `McpResult`, `RPUnavailable` and `oracle.py` they name are that lineage's, and none of them is in this binary. - **Does RP MCP support the long-running, async-result pattern abcd needs (e.g., a 5-minute Carmack review)? Or is it strictly synchronous within an MCP call lifetime?** Resolved by ADR-02 § 4: the `MCPBridge` contract is synchronous within an MCP call lifetime — `mcp_call` blocks for the call's duration. There is no async-result handle. The long-running case is handled by a generous per-tool `call_timeout_s` budget (`oracle_send` / `context_builder` get 600 s) inside one held-warm stdio session, not by an async poll. - **If RP MCP returns a chat ID for long-running work, how does abcd poll/listen for completion?** - Resolved by ADR-02 §§ 3–4: there is no polling. The call is synchronous; `mcp_call` returns when the tool call returns. The `chat_id` on `McpResult` is for *same-session re-review threading*, not completion polling. The async-vs-sync decision referenced for "Task 5's harness.py" is settled — the harness method stays synchronous (ADR-01 § 3 lock), and the concrete sync↔async bridge is internal to spc-5's `MCPBridge`. + Resolved by ADR-02 §§ 3–4: there is no polling. The call is synchronous; `mcp_call` returns when the tool call returns. The `chat_id` on `McpResult` is for *same-session re-review threading*, not completion polling. The async-vs-sync decision referenced for "Task 5's harness.py" is settled — the harness method stays synchronous (ADR-01 § 3 lock), and the concrete sync↔async bridge is internal to the `MCPBridge` of spc-5 (predecessor store). - **Chat identity and continuation — what does a `chat_id` mean, and can a chat be resumed across `abcd-cli` invocations?** - Resolved by ADR-02 § 3 and the spc-5 `.6` exception mapping: a `chat_id` is meaningful only within the `MCPBridge` instance / MCP session that produced it. Cross-invocation (and cross-bridge) chat continuation is **not supported** — RP's GUI approval gate forecloses it, and any RP-infrastructure failure surfaces as the typed `RPUnavailable` (`OSError` subclass) declared by spc-5. "Same chat" therefore means same-MCP-session only; the spc-5 `.6` failure-path mapping routes every unreachable-RP path through `RPUnavailable` so callers cascade cleanly rather than relying on a stale `chat_id`. + Resolved by ADR-02 § 3 and the spc-5 (predecessor store) `.6` exception mapping: a `chat_id` is meaningful only within the `MCPBridge` instance / MCP session that produced it. Cross-invocation (and cross-bridge) chat continuation is **not supported** — RP's GUI approval gate forecloses it, and any RP-infrastructure failure surfaces as the typed `RPUnavailable` (`OSError` subclass) declared by spc-5 (predecessor store). "Same chat" therefore means same-MCP-session only; the spc-5 (predecessor store) `.6` failure-path mapping routes every unreachable-RP path through `RPUnavailable` so callers cascade cleanly rather than relying on a stale `chat_id`. ## Resolved Questions - **Failure semantics: if an MCP call to RP times out, does abcd retry, fall through to in-session, or both?** - Resolved by ADR-02 (spc-4-phase-0-p1-patch-viability-framing-mcp.4): + Resolved by ADR-02 (spc-4-phase-0-p1-patch-viability-framing-mcp.4, predecessor store): On any RP-infrastructure failure (`RPUnavailable` — subprocess spawn fail, `startup_timeout_s` expiry, RP approval denial via `McpError: Connection closed`, `call_timeout_s` expiry, or mid-call transport failure), `oracle.py` routes to `dispatch_agent(agent_name="codex", ...)`. @@ -134,7 +134,7 @@ over `internal/` and `cmd/` finds only the scanner's RepoPrompt session-key patt (`internal/adapter/scanner/patterns.go`), a guard corpus line, and the doc comment of the configuration layer (`internal/core/layered`) naming `oracle.review` as a consumer it serves; `go.mod` carries no MCP dependency. The bridge, the typed error -and the host-reuse path an earlier Python lineage's `spc-5-rp-mcp-integration-declare` describes +and the host-reuse path an earlier Python lineage's `spc-5-rp-mcp-integration-declare` (predecessor store) describes belong to that lineage, not to this binary, so no part of the route is a foundation to build on. The four acceptance criteria above are the re-filed set, and [spc-2609211950427074](../../specs/open/spc-2609211950427074-rp-mcp-only-integration.md) diff --git a/.abcd/development/intents/shipped/itd-4-issue-capture.md b/.abcd/development/intents/shipped/itd-4-issue-capture.md index 39e04b931..9102eb629 100644 --- a/.abcd/development/intents/shipped/itd-4-issue-capture.md +++ b/.abcd/development/intents/shipped/itd-4-issue-capture.md @@ -66,7 +66,7 @@ None stated. - **Given** an abcd-installed repo, **when** the persona runs `/abcd:capture "review nitpick: T7 cache_ttl_days dead-config alternative"`, **then** a new file `.abcd/work/issues/open/iss-N-.md` exists with frontmatter populated (id, severity, category, source, found_during) and the captured text in the body. - **Given** an existing `iss-N` entry at `.abcd/work/issues/open/iss-3-foo.md`, **when** the persona runs `/abcd:capture resolve iss-3 "fixed in spc-7 task 4"`, **then** the file moves to `.abcd/work/issues/resolved/iss-3-foo.md` with the resolution note appended to the body. -- **Given** an existing `iss-N` entry, **when** the persona runs `/abcd:capture promote iss-N`, **then** `/abcd:intent new` is invoked with the entry's content as the seed; the resulting intent's frontmatter has `related_issues: [iss-N]`; the `iss-N` entry's frontmatter has `related_intents: [itd-M]` (the new intent's ID). Drift detection enforced by spc-23 (intent-fidelity-reviewer `--issue-drift`). +- **Given** an existing `iss-N` entry, **when** the persona runs `/abcd:capture promote iss-N`, **then** `/abcd:intent new` is invoked with the entry's content as the seed; the resulting intent's frontmatter has `related_issues: [iss-N]`; the `iss-N` entry's frontmatter has `related_intents: [itd-M]` (the new intent's ID). Drift detection enforced by spc-23 (predecessor store; intent-fidelity-reviewer `--issue-drift`). - **Given** a fresh `/abcd:ahoy` upgrade with an existing `.abcd/.work.local/issues.md`, **when** `dev-sync` runs, **then** every entry in `.abcd/.work.local/issues.md` is promoted to a corresponding `.abcd/work/issues/open/iss-N-.md` with provenance noting "migrated from .abcd/.work.local/issues.md". - **Given** the persona runs `/abcd:capture list --open`, **when** there are 5 open `iss-N` entries, **then** the output lists all 5 with id, slug, severity, and one-line summary. @@ -85,10 +85,10 @@ native spec store). The frontmatter `spec_id` records the **native** spec, **spc-6**, the record catch-up that verifies the shipped engine against the Acceptance Criteria and carries the open AC3 (promote) gap. Historical index: -- **spc-20** (primary) — `iss-N`-ledger primitives (`iss-N` allocator, schema, capture/resolve/wontfix/update_field workflow, structure under `.abcd/work/issues/`). -- **spc-21** — `/abcd:capture` command surface (flow-text ingest into the ledger). -- **spc-22** — `.abcd/.work.local/issues.md` migration to the structured ledger (`dev-sync work` orchestrator, regex-extracted intent linkage on migrated issues). -- **spc-23** — `intent-fidelity-reviewer --issue-drift` mode (bidirectional cross-reference walk; reader half of the bidirectional contract). +- **spc-20** (predecessor store; primary) — `iss-N`-ledger primitives (`iss-N` allocator, schema, capture/resolve/wontfix/update_field workflow, structure under `.abcd/work/issues/`). +- **spc-21** (predecessor store) — `/abcd:capture` command surface (flow-text ingest into the ledger). +- **spc-22** (predecessor store) — `.abcd/.work.local/issues.md` migration to the structured ledger (`dev-sync work` orchestrator, regex-extracted intent linkage on migrated issues). +- **spc-23** (predecessor store) — `intent-fidelity-reviewer --issue-drift` mode (bidirectional cross-reference walk; reader half of the bidirectional contract). ## Audit Notes diff --git a/.abcd/development/intents/shipped/itd-65-launch-preflight-gate-suite.md b/.abcd/development/intents/shipped/itd-65-launch-preflight-gate-suite.md index 176657a9f..b84b9cd99 100644 --- a/.abcd/development/intents/shipped/itd-65-launch-preflight-gate-suite.md +++ b/.abcd/development/intents/shipped/itd-65-launch-preflight-gate-suite.md @@ -31,13 +31,13 @@ impact: additive ## Press Release -> **`/abcd:launch ship` gains the complete Phase-5 pre-flight gate suite the brief specifies: on top of the spc-64 secret + PII scan, it adds the custom-regex identity layer (home-dir paths, real emails, GitHub usernames), marker-block sanity, `plugin.json` + `marketplace.json` validation, dirty-tree refusal, and the warn-fail documentation and hook-compliance checks — each hard-failing (or warn-failing) exactly as the brief's § 1 pins.** Today `launch` is a dry-run/render-only stub: it runs the spc-64 gate for real but renders every other gate as "(not yet implemented)". That means a real promotion would ship with those gates inert — precisely the invisible risks abcd exists to catch. This intent graduates the dry-run's "not yet implemented" lines into a real, runnable, fail-closed gate suite so a publish is blocked on a finding, not merely previewed. +> **`/abcd:launch ship` gains the complete Phase-5 pre-flight gate suite the brief specifies: on top of the spc-64 (predecessor store) secret + PII scan, it adds the custom-regex identity layer (home-dir paths, real emails, GitHub usernames), marker-block sanity, `plugin.json` + `marketplace.json` validation, dirty-tree refusal, and the warn-fail documentation and hook-compliance checks — each hard-failing (or warn-failing) exactly as the brief's § 1 pins.** Today `launch` is a dry-run/render-only stub: it runs the spc-64 (predecessor store) gate for real but renders every other gate as "(not yet implemented)". That means a real promotion would ship with those gates inert — precisely the invisible risks abcd exists to catch. This intent graduates the dry-run's "not yet implemented" lines into a real, runnable, fail-closed gate suite so a publish is blocked on a finding, not merely previewed. > "The dry-run already tells me a home-directory path or a broken plugin.json *would* be a problem," said a maintainer. "But 'would' isn't 'does' — ship has to actually hard-fail on it. I don't want to hand-audit the payload before every snapshot; the gate suite should." ## Why This Matters -abcd's whole thesis is routing the risks a non-expert cannot see to a fail-closed gate ([[itd-62-pluggable-safety-gate]]). Its OWN publish path is the highest-stakes instance of that: a launch cuts a curated release from the single repo — packaging that excludes `.abcd/**` ([adr-28](../../decisions/adrs/0028-single-repo-curated-release.md)) — and publishes it, where a leaked home-dir path, real email, or committed secret is irreversible. The canonical launch brief (`04-surfaces/04-launch.md` § 1) already specifies the full gate suite; spc-64 built the secret/PII floor; but the custom-regex identity layer, marker-block sanity, plugin/marketplace validation, and dirty-tree refusal are still stubs. Until they are real, `launch ship` cannot honestly claim to gate a promotion — and the project standards (no home-dir paths, no real emails, no usernames in file content) have no enforcement at the one moment they matter most. This closes that honesty gap the same way spc-74 closed the doc-fidelity one: make the built reality match what the surface implies. +abcd's whole thesis is routing the risks a non-expert cannot see to a fail-closed gate ([[itd-62-pluggable-safety-gate]]). Its OWN publish path is the highest-stakes instance of that: a launch cuts a curated release from the single repo — packaging that excludes `.abcd/**` ([adr-28](../../decisions/adrs/0028-single-repo-curated-release.md)) — and publishes it, where a leaked home-dir path, real email, or committed secret is irreversible. The canonical launch brief (`04-surfaces/04-launch.md` § 1) already specifies the full gate suite; spc-64 (predecessor store) built the secret/PII floor; but the custom-regex identity layer, marker-block sanity, plugin/marketplace validation, and dirty-tree refusal are still stubs. Until they are real, `launch ship` cannot honestly claim to gate a promotion — and the project standards (no home-dir paths, no real emails, no usernames in file content) have no enforcement at the one moment they matter most. This closes that honesty gap the same way spc-74 closed the doc-fidelity one: make the built reality match what the surface implies. ## What's In Scope @@ -54,14 +54,14 @@ abcd's whole thesis is routing the risks a non-expert cannot see to a fail-close - Dirty-tree refusal unless `--allow-dirty`; git-inferable-metadata scan (dates/authors/versions in file content, per project standards). - Warn-fail gates: hook-compliance, documentation auditor over `docs/`. - A single fail-closed orchestrator that runs the suite against the § 2 payload include-manifest and writes the pre-flight report (`.abcd/logbook/launch//preflight.{json,md}`), returning non-zero on any hard-fail — the Phase-5 `ship` behaviour, distinct from `dry-run`'s always-exit-0 preview. The orchestrator RUNS ALL gates and collects ALL findings before returning a verdict (report-everything, one fix pass), with a single ordering constraint: doc-history reroute runs before the dirty-tree gate (see below). -- Gate composition + tiering (grill Q2/Q4/Q6): the custom-regex identity layer is a SIBLING gate the orchestrator composes (spc-64 keeps its pinned gitleaks+pii.py engines); the identity layer flags only leaks of the LOCAL git identity (user.name/user.email, dev-repo remote URLs) with the public org handle allowlisted, never arbitrary handles. The suite is built as a callable unit so CI and pre-commit can invoke the SAME checks earlier (advisory/blocking per tier), while `launch ship` holds the authoritative hard-fail. -- Doc-history detector (grill Q3/Q5): a LAYERED detector — deterministic narrow patterns (past→present transitions: "used to X", "changed from X to Y", "no longer X", "migrated from", "renamed X to Y") run always, local-first, never hard-failing on bare present-tense "now"/"previously"; an OPTIONAL oracle/LLM pass (reusing the spc-27 oracle + spc-64 fail-closed-on-unavailable precedent) adjudicates ambiguous hits when a backend is available, falling back to surface-for-confirmation when not. Auto-reroute stages its own changelog + doc edits as one fix transaction; the dirty-tree gate runs AFTER reroute resolution so a clean staged fix is not mistaken for unexpected dirt. -- Reuse of the unmodified upstream scanners (gitleaks ≥ 8.18.0 pinned per spc-64; wrap, never fork) per the wrap-only rule. +- Gate composition + tiering (grill Q2/Q4/Q6): the custom-regex identity layer is a SIBLING gate the orchestrator composes (spc-64, predecessor store, keeps its pinned gitleaks+pii.py engines); the identity layer flags only leaks of the LOCAL git identity (user.name/user.email, dev-repo remote URLs) with the public org handle allowlisted, never arbitrary handles. The suite is built as a callable unit so CI and pre-commit can invoke the SAME checks earlier (advisory/blocking per tier), while `launch ship` holds the authoritative hard-fail. +- Doc-history detector (grill Q3/Q5): a LAYERED detector — deterministic narrow patterns (past→present transitions: "used to X", "changed from X to Y", "no longer X", "migrated from", "renamed X to Y") run always, local-first, never hard-failing on bare present-tense "now"/"previously"; an OPTIONAL oracle/LLM pass (reusing the spc-27 oracle + spc-64 fail-closed-on-unavailable precedent, both predecessor store) adjudicates ambiguous hits when a backend is available, falling back to surface-for-confirmation when not. Auto-reroute stages its own changelog + doc edits as one fix transaction; the dirty-tree gate runs AFTER reroute resolution so a clean staged fix is not mistaken for unexpected dirt. +- Reuse of the unmodified upstream scanners (gitleaks ≥ 8.18.0 pinned per spc-64 (predecessor store); wrap, never fork) per the wrap-only rule. ## What's Out of Scope - The payload render / mirror-mode / versioning machinery (that is the sibling launch work [[itd-66-launch-payload-render-parity]] — this intent is the GATES only). -- Replacing the spc-64 secret/PII engine or adopting Presidio as the wired engine (a separate recorded decision per brief § 1 / spc-64 C1a). +- Replacing the spc-64 (predecessor store) secret/PII engine or adopting Presidio as the wired engine (a separate recorded decision per brief § 1 / spc-64 (predecessor store) C1a). - Forking or reimplementing any scanner — configure and wrap the trusted ones. - Publishing anything: this intent decides go/no-go; it never pushes. @@ -137,7 +137,7 @@ test (the evidence for each is under `## Decisions`). - **Given** a shipped doc body containing change-history or rationale-for-change narration ("previously X, now Y", migration notes), **when** the doc-history gate runs during ship, **then** it HARD-FAILS and offers to auto-append the flagged passage to the [[itd-67-installable-versioned-plugin]] changelog; the ship proceeds only once the doc describes present state and the change is recorded in the changelog. - **Given** a dirty working tree, **when** ship runs without `--allow-dirty`, **then** it refuses; **with** `--allow-dirty` it proceeds and records the override. - **Given** a doc-auditor or hook-compliance concern, **when** the suite runs, **then** it WARN-fails (surfaced, non-blocking unless configured strict). -- **Given** gitleaks is absent or older than the pinned floor, **when** the suite runs, **then** it fails closed (never a regex fallback), consistent with spc-64. +- **Given** gitleaks is absent or older than the pinned floor, **when** the suite runs, **then** it fails closed (never a regex fallback), consistent with spc-64 (predecessor store). - **Given** a fully clean payload, **when** the suite runs, **then** it exits 0 and writes the pre-flight report. - **Given** a payload with multiple independent findings across different gates, **when** the suite runs, **then** it reports ALL of them in one pass (run-all-collect-all), not just the first hard-fail. - **Given** the identity gate, **when** it scans, **then** it flags a leaked LOCAL git identity (maintainer's personal name/email/handle) but NOT the allowlisted public org handle appearing in install docs. @@ -146,7 +146,7 @@ test (the evidence for each is under `## Decisions`). ## Open Questions -- ~~Should the custom-regex identity layer live inside the spc-64 gate module (extending its config) or as a sibling gate the orchestrator composes? (Reuse vs separation.)~~ Answered by this intent's own scope (grill Q2/Q4/Q6, "Gate composition + tiering"): a sibling gate the orchestrator composes, with spc-64 keeping its pinned engines; delivered as `internal/adapter/scanner/identity.go` (Delivery Status). +- ~~Should the custom-regex identity layer live inside the spc-64 (predecessor store) gate module (extending its config) or as a sibling gate the orchestrator composes? (Reuse vs separation.)~~ Answered by this intent's own scope (grill Q2/Q4/Q6, "Gate composition + tiering"): a sibling gate the orchestrator composes, with spc-64 (predecessor store) keeping its pinned engines; delivered as `internal/adapter/scanner/identity.go` (Delivery Status). - ~~What is the exact GitHub-username source — git config `user.name`/`user.email`, remote URLs, or a maintained denylist — and how are legitimate org handles in docs distinguished from leaked personal ones?~~ Answered by the same scope bullet: the LOCAL git identity (`user.name`/`user.email`, the dev-repo remote URL), never a denylist of arbitrary handles, with the public org handle allowlisted; delivered and pinned by `TestOtherIdentitiesArmMatchersAndHandleStaysPublic`. - Does the documentation auditor reuse the existing doc-scout machinery, or is it a launch-specific pass? Open. The `documentation-auditor` row runs the deterministic docs-lint engine today; whether the host-delegated `documentation-auditor` agent (`brief/05-internals/01-agents.md`) joins it is not ruled. - Where does `--allow-doc-warnings` sit relative to a strict CI invocation of the same suite? Open. No such flag ships: a warning refuses nothing unless the repository sets `"strict_warnings": true`, and whether a per-run override of that setting is wanted is not ruled. diff --git a/.abcd/development/intents/shipped/itd-66-launch-payload-render-parity.md b/.abcd/development/intents/shipped/itd-66-launch-payload-render-parity.md index 4a534360e..6d7398a8c 100644 --- a/.abcd/development/intents/shipped/itd-66-launch-payload-render-parity.md +++ b/.abcd/development/intents/shipped/itd-66-launch-payload-render-parity.md @@ -41,7 +41,7 @@ The pre-flight gate suite ([[itd-65-launch-preflight-gate-suite]]) decides wheth - A leak-proof assertion: the rendered tree contains ZERO `.abcd/**` paths, and honours the `.abcd/launch.allow` allowlist contract (never promotes any `.abcd/**` line, per adr-28). - A parity diff between the rendered payload and the previously published release: added / changed / removed files, so the operator previews the exact snapshot delta before promotion. - An installed-surface smoke test: from the rendered snapshot, load `plugin.json` + `marketplace.json` and assert every declared `/abcd:*` command, skill, and hook resolves, and every shipped Python entrypoint imports. -- All read-only w.r.t. the dev repo (temp-tree writes only, removed after) — matches the side-effect-free posture of the spc-64 gate. +- All read-only w.r.t. the dev repo (temp-tree writes only, removed after) — matches the side-effect-free posture of the spc-64 (predecessor store) gate. - Canonical payload resolution (grill Q3): itd-66's render is the SINGLE resolver of "the payload" (include-manifest + default-deny + `.gitignore` + symlink-resolve). [[itd-65-launch-preflight-gate-suite]]'s gate suite scans exactly this resolved output and never re-resolves — so render and gate can never disagree on what is being shipped. This makes itd-66 (render) a dependency of itd-65 (gate): render → gate. - Layered leak defense (grill Q2): the render asserts no excluded PATH in the tree AND resolves symlinks (a payload symlink targeting `.abcd/` fails the assertion); embedded `.abcd`/`.flow` CONTENT that rode along inside a shipped file is caught by itd-65's secret/PII/identity content scan. Structural exclusion here; content cleanliness there. - Parity baseline (grill Q1): the diff targets the previously published release at a configured ref (default: the latest release tag). An absent/empty prior release yields an all-added diff (valid first-launch); a wrong/missing configured baseline is a hard error, never a silent empty diff. From 542737b2123f16eb5bd14941b7bcccab3c8cb2cd Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Wed, 30 Sep 2026 01:25:57 +0100 Subject: [PATCH 02/91] docs(intents): correct itd-36 and itd-4 on where their predecessor specs stand itd-36's Implementing specs section said the frontmatter spec_id records spc-38 as the primary delivering spec; the frontmatter records spc-2609211905174684, and live spc-38 and spc-39 are itd-136's record explorer and itd-137's relationship chart. itd-4's said its spc-20 to spc-23 do not exist in the native spec store; the live store holds all four as other specs. Both sections now name the live spec_id, say the native store reuses each number, and qualify every predecessor id, the shape itd-4's own spc-6 catch-up paragraph already had. Neither section is an acceptance criterion. The finding is captured in this change and resolved in the next. Refs: iss-2609300025372991, iss-2609290448510918 Assisted-by: Claude:claude-opus-5-5 --- .../shipped/itd-36-memory-unification.md | 17 ++++++++++------- .../intents/shipped/itd-4-issue-capture.md | 5 +++-- ...ents-implementing-specs-sections-misstate.md | 15 +++++++++++++++ 3 files changed, 28 insertions(+), 9 deletions(-) create mode 100644 .abcd/work/issues/open/iss-2609300025372991-two-shipped-intents-implementing-specs-sections-misstate.md diff --git a/.abcd/development/intents/shipped/itd-36-memory-unification.md b/.abcd/development/intents/shipped/itd-36-memory-unification.md index 6b214befe..493833104 100644 --- a/.abcd/development/intents/shipped/itd-36-memory-unification.md +++ b/.abcd/development/intents/shipped/itd-36-memory-unification.md @@ -87,13 +87,16 @@ None stated. ## Implementing specs -itd-36 is implemented across multiple specs. The single-valued frontmatter -`spec_id` records the **primary** delivering spec (spc-38); the remaining spec is -recorded here because `spec_id` holds one value and would understate scope. -This section is the canonical multi-spec implementation index: - -- **spc-38** (primary) — `/abcd:memory` write core (the memory substrate, ingest, registry). -- **spc-39** — `/abcd:memory lint` quality gate (quotation-budget / licence / provenance lint). +itd-36 was implemented across two specs of the predecessor store; those ids are +preserved below as history. The native spec store reuses both numbers for other +specs (live spc-38 is itd-136's record explorer, live spc-39 itd-137's +relationship chart), so each carries the predecessor-store qualifier. The +frontmatter `spec_id` records the **native** spec, **spc-2609211905174684**, the +record catch-up that covers the write core and the quality gate together. +Historical index: + +- **spc-38** (predecessor store; primary) — `/abcd:memory` write core (the memory substrate, ingest, registry). +- **spc-39** (predecessor store) — `/abcd:memory lint` quality gate (quotation-budget / licence / provenance lint). ## Ship gate — adversarial worked examples diff --git a/.abcd/development/intents/shipped/itd-4-issue-capture.md b/.abcd/development/intents/shipped/itd-4-issue-capture.md index 9102eb629..f139aa355 100644 --- a/.abcd/development/intents/shipped/itd-4-issue-capture.md +++ b/.abcd/development/intents/shipped/itd-4-issue-capture.md @@ -80,8 +80,9 @@ None stated. ## Implementing specs itd-4 was implemented across multiple specs of the superseded pre-Go record -system; those ids are preserved below as history (they do not exist in the -native spec store). The frontmatter `spec_id` records the **native** spec, +system; those ids are preserved below as history. The native spec store +reuses each number for another spec, so each carries the predecessor-store +qualifier. The frontmatter `spec_id` records the **native** spec, **spc-6**, the record catch-up that verifies the shipped engine against the Acceptance Criteria and carries the open AC3 (promote) gap. Historical index: diff --git a/.abcd/work/issues/open/iss-2609300025372991-two-shipped-intents-implementing-specs-sections-misstate.md b/.abcd/work/issues/open/iss-2609300025372991-two-shipped-intents-implementing-specs-sections-misstate.md new file mode 100644 index 000000000..6d648be40 --- /dev/null +++ b/.abcd/work/issues/open/iss-2609300025372991-two-shipped-intents-implementing-specs-sections-misstate.md @@ -0,0 +1,15 @@ +--- +schema_version: 1 +id: "iss-2609300025372991" +slug: "two-shipped-intents-implementing-specs-sections-misstate" +severity: "minor" +category: "drift" +source: "agent-finding" +found_during: "autonomous run A resumed 2026-09-25" +found_at: ".abcd/development/intents/shipped" +origin: researcher-authored +production_mode: hand-written +remedy: "Rewrite both sections to name the live spec_id and to say the native store reuses each number for another spec, qualifying every predecessor id '(predecessor store)' per the specs charter's Two spc-N Namespaces rule; grounds: the frontmatter spec_id and the files under specs/closed/ are the primary record, and itd-4's own spc-6 catch-up section is the shape to follow." +--- + +Two shipped intents' Implementing specs sections misstate where their predecessor-store spec ids stand: itd-36 says its frontmatter spec_id records spc-38 as the primary delivering spec, while the frontmatter records spc-2609211905174684 and live spc-38 and spc-39 are itd-136's record explorer and itd-137's relationship chart; itd-4 says its spc-20 to spc-23 do not exist in the native spec store, while the live store holds all four as other specs (banlist, fresh install, stale-binary warning, tier placement). Found while qualifying predecessor-store citations for iss-2609290448510918. From 3d188458aec5187b8e5ab35e4ba2ddde2c73b3a1 Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Wed, 30 Sep 2026 01:29:43 +0100 Subject: [PATCH 03/91] =?UTF-8?q?chore:=20resolve=20iss-2609300025372991?= =?UTF-8?q?=20=E2=80=94=20itd-36=20and=20itd-4=20name=20their=20live=20spe?= =?UTF-8?q?cs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fix landed in 542737b21: both Implementing specs sections name the live spec_id and qualify every predecessor-store id. Resolves: iss-2609300025372991 Assisted-by: Claude:claude-opus-5-5 --- ...hipped-intents-implementing-specs-sections-misstate.md | 8 ++++++++ 1 file changed, 8 insertions(+) rename .abcd/work/issues/{open => resolved}/iss-2609300025372991-two-shipped-intents-implementing-specs-sections-misstate.md (66%) diff --git a/.abcd/work/issues/open/iss-2609300025372991-two-shipped-intents-implementing-specs-sections-misstate.md b/.abcd/work/issues/resolved/iss-2609300025372991-two-shipped-intents-implementing-specs-sections-misstate.md similarity index 66% rename from .abcd/work/issues/open/iss-2609300025372991-two-shipped-intents-implementing-specs-sections-misstate.md rename to .abcd/work/issues/resolved/iss-2609300025372991-two-shipped-intents-implementing-specs-sections-misstate.md index 6d648be40..7c616e036 100644 --- a/.abcd/work/issues/open/iss-2609300025372991-two-shipped-intents-implementing-specs-sections-misstate.md +++ b/.abcd/work/issues/resolved/iss-2609300025372991-two-shipped-intents-implementing-specs-sections-misstate.md @@ -10,6 +10,14 @@ found_at: ".abcd/development/intents/shipped" origin: researcher-authored production_mode: hand-written remedy: "Rewrite both sections to name the live spec_id and to say the native store reuses each number for another spec, qualifying every predecessor id '(predecessor store)' per the specs charter's Two spc-N Namespaces rule; grounds: the frontmatter spec_id and the files under specs/closed/ are the primary record, and itd-4's own spc-6 catch-up section is the shape to follow." +resolution: "itd-36's and itd-4's Implementing specs sections now name the live spec_id (spc-2609211905174684 and spc-6), say the native store reuses each predecessor number for another spec, and qualify every predecessor id '(predecessor store)'. Neither section is an acceptance criterion, so no shipped promise moved." +impact: internal +resolved_by: + commit: "542737b21" --- Two shipped intents' Implementing specs sections misstate where their predecessor-store spec ids stand: itd-36 says its frontmatter spec_id records spc-38 as the primary delivering spec, while the frontmatter records spc-2609211905174684 and live spc-38 and spc-39 are itd-136's record explorer and itd-137's relationship chart; itd-4 says its spc-20 to spc-23 do not exist in the native spec store, while the live store holds all four as other specs (banlist, fresh install, stale-binary warning, tier placement). Found while qualifying predecessor-store citations for iss-2609290448510918. + +## Grounds + +- pursued: neither section claims a predecessor id is the frontmatter spec_id or is absent from the live store; a grep of either section finding such a claim, or an unqualified spc-20 to spc-23, spc-38 or spc-39 cited as a delivering spec, would show it wrong From d381395995770968766cdcba189908d037e9d85f Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Wed, 30 Sep 2026 01:32:09 +0100 Subject: [PATCH 04/91] docs(intents): qualify the predecessor-store spec ids six superseded intents cite itd-17, itd-20, itd-27, itd-29, itd-47 and itd-49 sit in superseded/ and describe pre-rebuild work in the predecessor store's terms: the flow-next checkpoints, the Python oracle and Codex leg, the Ralph quota work and the grill skill's first spec. Every spc-N they cite at or below spc-70 was read against the live spec it collides with, and none names the live one (live spc-3 is the lifeboat coverage experiment, spc-6 issue capture, spc-12 the disembark grounding, spc-41 the banner), so each carries the specs charter's qualifier. Where one id repeats within one line of prose, the first mention carries it. Left as written: itd-27's reclassification_history reason (a dated reason keeps the words it was written with) and itd-47's Implementing specs section, whose false spec_id claim is corrected in its own change. Refs: iss-2609290448510918 Assisted-by: Claude:claude-opus-5-5 --- .../itd-17-model-effectiveness-tracking.md | 4 +-- .../itd-20-top-level-abcd-dispatcher.md | 2 +- .../itd-27-grill-skill-and-glossary.md | 8 ++--- .../itd-29-autonomous-run-resilience.md | 18 +++++----- .../itd-47-oracle-gates-autonomous-mode.md | 22 ++++++------ .../itd-49-flow-state-drift-detector.md | 34 +++++++++---------- 6 files changed, 44 insertions(+), 44 deletions(-) diff --git a/.abcd/development/intents/superseded/itd-17-model-effectiveness-tracking.md b/.abcd/development/intents/superseded/itd-17-model-effectiveness-tracking.md index 0e2c2a24a..003abbb2a 100644 --- a/.abcd/development/intents/superseded/itd-17-model-effectiveness-tracking.md +++ b/.abcd/development/intents/superseded/itd-17-model-effectiveness-tracking.md @@ -69,9 +69,9 @@ The fix: track per-target statistics (ship_count, revise_count, false_revise_cou > Empirical observations to feed the reframed plan-review (per the reframe blockquote at the top of this file). Not yet structured into the `{task_class, agent, backend, model_id, outcome, failure_mode_tag}` schema — recorded here as raw input the reframe should consume. -### 2026-05-16 — review-backend frontier, observed over the `spc-5` dual-backend plan-review loop (12 rounds, 24 reviews) +### 2026-05-16 — review-backend frontier, observed over the `spc-5` (predecessor store) dual-backend plan-review loop (12 rounds, 24 reviews) -A new **`task_class: review_backend`** worth tracking once itd-17 reframes. The `spc-5` spec plan review ran RepoPrompt and Codex CLI in parallel for rounds 16–27; their behaviour was asymmetric and consistent: +A new **`task_class: review_backend`** worth tracking once itd-17 reframes. The `spc-5` (predecessor store) spec plan review ran RepoPrompt and Codex CLI in parallel for rounds 16–27; their behaviour was asymmetric and consistent: | Backend | Strength (high accuracy) | Weakness (failure mode) | |---|---|---| diff --git a/.abcd/development/intents/superseded/itd-20-top-level-abcd-dispatcher.md b/.abcd/development/intents/superseded/itd-20-top-level-abcd-dispatcher.md index 9383717af..66ab3f0cd 100644 --- a/.abcd/development/intents/superseded/itd-20-top-level-abcd-dispatcher.md +++ b/.abcd/development/intents/superseded/itd-20-top-level-abcd-dispatcher.md @@ -91,7 +91,7 @@ _Populated by intent-fidelity-reviewer when intent moves to shipped/._ state substrate exists. This is a recorded terminal state, not a shipped capability. Adding the substrate is out of scope. Full record in the surface doc: [`../../brief/04-surfaces/08-abcd.md`](../../brief/04-surfaces/08-abcd.md). -- **No spc-17 stub to replace.** spc-17 shipped bare/probe renders for the +- **No spc-17 (predecessor store) stub to replace.** That spc-17 shipped bare/probe renders for the *sub-verb* surfaces only; the top-level `commands/abcd.md` never existed. This task creates it fresh — the "stub replacement" premise is not-applicable (verified against `git log`). diff --git a/.abcd/development/intents/superseded/itd-27-grill-skill-and-glossary.md b/.abcd/development/intents/superseded/itd-27-grill-skill-and-glossary.md index a5e8dae2f..7c71ef087 100644 --- a/.abcd/development/intents/superseded/itd-27-grill-skill-and-glossary.md +++ b/.abcd/development/intents/superseded/itd-27-grill-skill-and-glossary.md @@ -48,7 +48,7 @@ Three forces in this intent work together to make the glossary *emerge* from the 2. **Cite-or-fail enforcement at lint time.** `internal/core/lint` blocks promotion on (a) non-canonical synonym in body (`GL002`, blocker) and (b) draft term in promoted intent (`GL004`, blocker); warns on (c) undefined term (`GL001`, warn) and cross-context collision without `contexts:` declared (`GL003`, warn). 3. **Seed sparingly, tag bounded contexts from day one.** Ship 8–12 abcd-canonical seed terms with explicit `bounded_context:` so the pattern is in place before the second context arrives. -This intent is the **`press-release`-shaped commitment** behind spec `spc-3-strengthen-intent-stage-abcdgrill-skill` (already specced), which decomposes into **6 implementation tasks** (tasks .1–.5 for core implementation + glossary lint + freeze; task .6 for ADR, fixtures, reviewer spec uplift, README updates, and end-to-end smoke). +This intent is the **`press-release`-shaped commitment** behind spec `spc-3-strengthen-intent-stage-abcdgrill-skill` (predecessor store; already specced), which decomposes into **6 implementation tasks** (tasks .1–.5 for core implementation + glossary lint + freeze; task .6 for ADR, fixtures, reviewer spec uplift, README updates, and end-to-end smoke). ## What's In Scope @@ -109,9 +109,9 @@ None stated. - ~~**Glossary location**~~ — **DECIDED post-audit (2026-05-07)**: source at `.abcd/development/foundation/terminology//.md` (one-file-per-term, RAG-friendly, consistent with brief's `.abcd/development/` source-side convention). Lifeboat OUTPUT is `docs/terminology.md`, rendered from source at disembark time. - ~~**Verb canonical form**~~ — **DECIDED post-round-2-review (2026-05-07)**: `/abcd:intent grill` (sub-verb of `/abcd:intent`, sibling of `refine`). Top-level `/abcd:grill` and `/abcd:grill-me` aliases dropped. -- ~~**Glossary-aware mode trigger**~~ — **DECIDED (spc-3, 2026-05-11)**: presence of `.abcd/development/foundation/terminology/` directory triggers glossary-aware mode. No explicit config flag needed. +- ~~**Glossary-aware mode trigger**~~ — **DECIDED (spc-3, predecessor store, 2026-05-11)**: presence of `.abcd/development/foundation/terminology/` directory triggers glossary-aware mode. No explicit config flag needed. - **Cross-context term canonicalisation**: require explicit `contexts: [list]` in intent frontmatter when ANY cited term has cross-context collision (recommended) vs always require. -- ~~**PRD location**~~ — **DECIDED (spc-3, 2026-05-11)**: `.abcd/intents//prd.md` (per-intent, `prd-archive` subdirectory `.abcd/intents//prd-archive/.md` for regrills). Colocating with the intent file was rejected because it breaks the "intent file is one file" invariant and mixes lifecycle artefacts. +- ~~**PRD location**~~ — **DECIDED (spc-3, predecessor store, 2026-05-11)**: `.abcd/intents//prd.md` (per-intent, `prd-archive` subdirectory `.abcd/intents//prd-archive/.md` for regrills). Colocating with the intent file was rejected because it breaks the "intent file is one file" invariant and mixes lifecycle artefacts. - **Resynthesise path**: when an intent's glossary citations drift post-promotion (e.g. a glossary term gets renamed), is `/abcd:intent grill --resynthesise itd-N` (skip Phase 1, rerun Phase 2 only) the right surface, or is regrill always Phase-1-then-2? Current draft keeps Phase 2 inseparable from Phase 1; resynthesise-only is a candidate follow-up if drift becomes common. - **PRD `Further Notes` semantics**: Pocock's template uses this as a catch-all. Should abcd's adaptation pin specific things into it (e.g. links to the grill report, related intents, ADR cross-references) or keep it free-form? Pocock keeps it free-form; abcd defaulting to the same unless friction emerges. @@ -121,7 +121,7 @@ _Empty. Populated by intent-fidelity-reviewer when intent moves to shipped/._ ## References -- Linked spec: `spc-3-strengthen-intent-stage-abcdgrill-skill` (in this repo). The spec slug retains the `abcdgrill-skill` form for now; rename to `intent-grill-skill` is queued as a follow-up wave (tracked in a local working note, unmigrated). +- Linked spec: `spc-3-strengthen-intent-stage-abcdgrill-skill` (predecessor store). The spec slug retains the `abcdgrill-skill` form for now; rename to `intent-grill-skill` is queued as a follow-up wave (tracked in a local working note, unmigrated). - Depends on: `itd-1` (acceptance gates) — this intent eats its own dog food. - Coordinates with: `itd-24` (reflect command) — different register (post-completion learning vs pre-promotion adversarial). - Extended by: `itd-42` (coherence-aware grill) — adds a brief- and sibling-coherence tier to the grill this intent built; glossary-aware mode is kept verbatim. diff --git a/.abcd/development/intents/superseded/itd-29-autonomous-run-resilience.md b/.abcd/development/intents/superseded/itd-29-autonomous-run-resilience.md index 9b6515a25..0395df2de 100644 --- a/.abcd/development/intents/superseded/itd-29-autonomous-run-resilience.md +++ b/.abcd/development/intents/superseded/itd-29-autonomous-run-resilience.md @@ -18,16 +18,16 @@ severity: major ## Press Release -> **abcd collapses autonomous-run safety to a six-verb operating surface a domain expert can use without ever opening git.** Before `abcd spec start spc-X` kicks off the autonomous run through the pluggable run seam, a pre-flight budget check estimates token cost against remaining quota and refuses to start if the math doesn't add up. Mid-run telemetry surfaces "X% of daily budget consumed, Y tasks remaining" via spc-29-42i (telemetry spec). On a 429 rate-limit response the run catches it cleanly, writes a `RESUME-FROM` checkpoint to the native spec store, and exits. `abcd spec resume spc-X` picks up exactly where it stopped. `abcd spec rewind spc-X --to-task 3` undoes a wrong turn — soft by default (lets the user edit the task spec and re-run; keeps reviews visible for context), hard with `--hard` (full discard). Completed specs auto-merge to a `dev` trunk branch only when all reviews verdict `SHIP` and lint/smoke pass; promotion to `main` requires explicit `abcd spec ship spc-X`. When auto-rebase hits a conflict, the spec suspends and emits a single concrete next-step: `abcd spec resolve spc-X` walks the user through resolution. The domain expert never types `git reset`, never sees a branch name, never decides whether a 429 means "retry" or "give up." +> **abcd collapses autonomous-run safety to a six-verb operating surface a domain expert can use without ever opening git.** Before `abcd spec start spc-X` kicks off the autonomous run through the pluggable run seam, a pre-flight budget check estimates token cost against remaining quota and refuses to start if the math doesn't add up. Mid-run telemetry surfaces "X% of daily budget consumed, Y tasks remaining" via spc-29-42i (telemetry spec, predecessor store). On a 429 rate-limit response the run catches it cleanly, writes a `RESUME-FROM` checkpoint to the native spec store, and exits. `abcd spec resume spc-X` picks up exactly where it stopped. `abcd spec rewind spc-X --to-task 3` undoes a wrong turn — soft by default (lets the user edit the task spec and re-run; keeps reviews visible for context), hard with `--hard` (full discard). Completed specs auto-merge to a `dev` trunk branch only when all reviews verdict `SHIP` and lint/smoke pass; promotion to `main` requires explicit `abcd spec ship spc-X`. When auto-rebase hits a conflict, the spec suspends and emits a single concrete next-step: `abcd spec resolve spc-X` walks the user through resolution. The domain expert never types `git reset`, never sees a branch name, never decides whether a 429 means "retry" or "give up." > > "I'd kicked off an autonomous run, gone to lunch, come back to find it had burned through my Opus budget on iteration 7 of a task that was already wrong," said Iris, product lead. "abcd's pre-flight check would have flagged the budget; the rewind would have undone iteration 6; resume would have picked up Sonnet for the rest. Instead I spent two hours with git and a model bill. Never again." ## Scope Reconciliation Parts of this intent's scope overlap with adjacent run-seam specs: -- **Graceful 429/quota handling, checkpoint, clean exit, resume-on-reset** — spc-19 (infra-failure classification, no-burn backoff) + spc-35 (quota-window sleep-until-reset, cross-run markers, clean weekly exits). The run-seam side of scope items "Graceful 429 handling" and the checkpoint substrate exists. -- **Budget spreading** — spc-47 (iteration pacing, model-aware) covers part of the budget concern from the proactive side. -- **Checkpoints per spec** — the native spec-store checkpoint records exist (spc-41 consumes them). +- **Graceful 429/quota handling, checkpoint, clean exit, resume-on-reset** — spc-19 (predecessor store; infra-failure classification, no-burn backoff) + spc-35 (predecessor store; quota-window sleep-until-reset, cross-run markers, clean weekly exits). The run-seam side of scope items "Graceful 429 handling" and the checkpoint substrate exists. +- **Budget spreading** — spc-47 (predecessor store; iteration pacing, model-aware) covers part of the budget concern from the proactive side. +- **Checkpoints per spec** — the native spec-store checkpoint records exist (spc-41, predecessor store, consumes them). The residual scope this intent owns is the OPERATOR SURFACE: the verbs, the pre-flight budget estimate, model auto-downgrade, rewind, and the trunk/auto-merge pattern. A **v1 cut** — `status` / `pause` / `resume` / standalone `preflight` riding the existing sentinels + checkpoints + markers — comes first; `rewind`, `ship`, `resolve`, auto-merge, and auto-downgrade stay in this intent for a later cut (the v2 trigger: first real demand for rewind or trunk promotion). Terminology note: the surface uses run/spec vocabulary, not `epic` (the itd-43 direction). The autonomous engine underneath is the **pluggable run seam** (Workflows / the companion harness / native loop), not a fixed loop ([adr-27](../../decisions/adrs/0027-autonomous-run-pluggable-seam.md)); checkpoints and reviews live in the **native spec store** ([adr-26](../../decisions/adrs/0026-native-spec-layer-ccpm-backend.md)). @@ -39,7 +39,7 @@ abcd's near-term value is autonomous execution: a domain expert hands an intent 2. **A spec completes wrong but later tasks built on the wrong one.** The domain expert wants to "go back" — currently means knowing about `git reset`, branches, low-level task-uncomplete commands, manual cleanup, possibly cross-branch reverts. 3. **Branch lifecycle stranding.** The autonomous run creates a `spc-X-foo` branch, completes it, and then nothing. Branch stays open, work is invisible to the rest of the repo, lifeboat doesn't see it. Domain expert never merges → branches pile up → confusion about "is this done?" -These are **failure modes of a system that doesn't yet exist.** The substrate (`/abcd:intent` + sub-verbs including `/abcd:intent grill`, lifeboat, spc-1 reviews, spc-2 review artefacts → the native review store) must ship first. The first time a domain expert runs an autonomous loop end-to-end and hits any of these, the texture becomes clear and this intent can be designed against real evidence — not guesses. +These are **failure modes of a system that doesn't yet exist.** The substrate (`/abcd:intent` + sub-verbs including `/abcd:intent grill`, lifeboat, spc-1 reviews, spc-2 review artefacts (both predecessor store) → the native review store) must ship first. The first time a domain expert runs an autonomous loop end-to-end and hits any of these, the texture becomes clear and this intent can be designed against real evidence — not guesses. This intent **captures the concern now** so the project memory holds it. **Implementation depends on the substrate shipping first.** The triggers to revisit are listed below. @@ -54,7 +54,7 @@ This intent **captures the concern now** so the project memory holds it. **Imple - `abcd spec ship ` — explicit promotion from `dev` trunk to `main` after user confirmation - `abcd spec resolve ` — guided conflict resolution when auto-rebase fails - **Pre-flight budget check**: estimate token cost (tasks × ~80k tokens × iteration count) against remaining quota; refuse to start if the math doesn't add up. -- **Mid-run budget telemetry** via spc-29-42i: surface "X% of daily budget consumed, Y tasks remaining" in `abcd spec status`. +- **Mid-run budget telemetry** via spc-29-42i (predecessor store): surface "X% of daily budget consumed, Y tasks remaining" in `abcd spec status`. - **Graceful 429 handling**: catch rate-limit response, write `RESUME-FROM` checkpoint, exit cleanly. No retry loops that burn remaining budget. - **Optional model auto-downgrade**: domain-expert opt-in: "if Opus runs out, fall back to Sonnet for remaining tasks." Configurable per-project. - **Checkpoint-per-task**: each task ending creates a tagged restore point; `rewind --to-task ` reverts via the tag, not raw git. @@ -133,7 +133,7 @@ The first user to hit (1)–(4) is also asked to record the texture in the `.abc - **Trunk branch name**: `dev` vs `staging` vs `main-staging` vs project-configurable. Recommend project-configurable with `dev` default. - **Rewind semantics for `--hard`**: discard branch entirely vs keep branch but tag a "rewound-from" marker. Recommend tag-and-keep so the user can `git reflog` if they realise they wanted the work back. - **Auto-downgrade decision logic**: Opus → Sonnet → Haiku on budget exhaustion vs single-step Opus → Sonnet only. Probably single-step initially; multi-step if proven needed. -- **Telemetry / multi-model substrate**: this intent assumes a telemetry surface (cost/budget visibility) and a multi-model orchestration surface (for auto-downgrade). Earlier drafts referenced legacy abcd-repo IDs (`spc-29-42i`, `spc-9-kbe`) which do NOT exist in `abcd-cli` — those are speculative substrate from the legacy roadmap, not declared in this brief. **Action for plan-review:** identify or create the actual specs (in the native spec store) that deliver the telemetry + multi-model substrate, then list them as hard dependencies here. +- **Telemetry / multi-model substrate**: this intent assumes a telemetry surface (cost/budget visibility) and a multi-model orchestration surface (for auto-downgrade). Earlier drafts referenced legacy abcd-repo IDs (`spc-29-42i`, `spc-9-kbe`, predecessor store) which do NOT exist in `abcd-cli` — those are speculative substrate from the legacy roadmap, not declared in this brief. **Action for plan-review:** identify or create the actual specs (in the native spec store) that deliver the telemetry + multi-model substrate, then list them as hard dependencies here. ## Audit Notes @@ -141,8 +141,8 @@ _Empty. Populated by intent-fidelity-reviewer when intent moves to shipped/._ ## References -- Coordinates with: `itd-1` (acceptance gates), `itd-3` (modular rules loader, may govern budget rules), `itd-9` (schema migration, for checkpoint format), `itd-28` (RP reviews → the native review store, audit-trail integration target via `spc-2-move-repoprompt-review-artifacts-into`). +- Coordinates with: `itd-1` (acceptance gates), `itd-3` (modular rules loader, may govern budget rules), `itd-9` (schema migration, for checkpoint format), `itd-28` (RP reviews → the native review store, audit-trail integration target via `spc-2-move-repoprompt-review-artifacts-into`, predecessor store). - Builds on: a future native spec for budget/cost surfacing (likely a `spc-N-budget` spec); the pluggable run seam. -- Implemented by: `spc-35-ralph-quota-window-resilience` — the 429/quota implementation. spc-35 delivers the window-aware quota classifier (`QUOTA_WEEKLY`/`QUOTA_FIVE_HOUR`), the `RATE_LIMITED` completion marker, and the window-aware sleep that this intent's "graceful 429 handling" acceptance describes. spc-35 is the concrete substrate behind this intent's rate-limit scope. +- Implemented by: `spc-35-ralph-quota-window-resilience` (predecessor store) — the 429/quota implementation. spc-35 delivers the window-aware quota classifier (`QUOTA_WEEKLY`/`QUOTA_FIVE_HOUR`), the `RATE_LIMITED` completion marker, and the window-aware sleep that this intent's "graceful 429 handling" acceptance describes. spc-35 is the concrete substrate behind this intent's rate-limit scope. - The out-of-band-merge-pickup scope (the added scope bullet + AC + SOTA) was surfaced during the itd-80 intent-lifecycle build; the design rationale (host-owns-git MVP → a read-only `abcd run reconcile --json` advisory verb, conflicts/force-push excluded) is recorded in the itd-80 run-learnings note under `research/notes/` (2026-07-11). - The **auto-merge guardrails** in this intent's scope (trunk-only, SHIP-verdict-gated not CI-gated, audit entry, conflict→human, `ship` promotes to `main`) are recorded as a standing decision — `.abcd/work/DECISIONS.md` (2026-07-13) — so the trust-boundary rule outlives this intent's own lifecycle; it graduates to a brief invariant + an ADR when the v2 cut is built. The **presentation** of this and every run surface follows the [`facilitator-default-thinker-optional`](../../principles/facilitator-default-thinker-optional.md) principle. diff --git a/.abcd/development/intents/superseded/itd-47-oracle-gates-autonomous-mode.md b/.abcd/development/intents/superseded/itd-47-oracle-gates-autonomous-mode.md index 561ef5754..939a26724 100644 --- a/.abcd/development/intents/superseded/itd-47-oracle-gates-autonomous-mode.md +++ b/.abcd/development/intents/superseded/itd-47-oracle-gates-autonomous-mode.md @@ -19,19 +19,19 @@ severity: nitpick > **abcd's `intent-fidelity-reviewer` agent runs its three oracle-backed quality gates (itd-5 self-improvement pre-flight, research-file review, live injection-canary execution) in an autonomous Ralph session without a human in the loop.** Today those gates are structurally un-completable in headless mode: `_build_cli_oracle()` returns `Oracle(MCPBridge())` — the RP MCP leg only, which requires a running RepoPrompt GUI. Ralph running overnight on a server has no GUI. So the three gates either stay `deferred` (honest but blocking spec completion) or get rubber-stamped with fabricated outcomes. Once this intent lands, `_build_cli_oracle()` returns an `Oracle` with both an RP leg and a Codex leg; the Codex leg is reachable headlessly (the `codex` CLI is on PATH), and the gates complete with real outcomes against real oracle round-trips. > -> "The spc-12 spec completion review used to stall every time it hit R6," said Ethan, autonomous-loop operator. "I'd come back to a Ralph run that had spent its budget thrashing on a gate it couldn't reach. With the Codex leg wired into the CLI oracle, the same gate runs in the next loop pass and either passes or fails honestly. The run completes — or doesn't — for real reasons." +> "The spc-12 (predecessor store) spec completion review used to stall every time it hit R6," said Ethan, autonomous-loop operator. "I'd come back to a Ralph run that had spent its budget thrashing on a gate it couldn't reach. With the Codex leg wired into the CLI oracle, the same gate runs in the next loop pass and either passes or fails honestly. The run completes — or doesn't — for real reasons." ## Why This Matters -`.work/issues.md` 2026-05-19 line 360 records the blocker: spc-12 (`intent-fidelity-reviewer` agent) ships Role 1 — per-criterion `MET`/`NOT_MET` verdicts on shipped intents — but three of its acceptance gates require a genuine `Oracle.ask()` round-trip that the autonomous run environment cannot satisfy. The three gates are: +`.work/issues.md` 2026-05-19 line 360 records the blocker: spc-12 (predecessor store; `intent-fidelity-reviewer` agent) ships Role 1 — per-criterion `MET`/`NOT_MET` verdicts on shipped intents — but three of its acceptance gates require a genuine `Oracle.ask()` round-trip that the autonomous run environment cannot satisfy. The three gates are: - **R6 — itd-5 self-improvement pre-flight.** The candidate prompt is submitted to `lifeboat-oracle` for a clarity rewrite; the rewritten variant must pass the same goldens and be shorter by >10% to be accepted. Decision logged in the CHANGELOG. - **T1 — research-file oracle review.** The reviewer agent's research artefact (`.abcd/development/research/prompting/agents/intent-fidelity-reviewer.md §7`) is reviewed against oracle judgement. - **R7 — live injection-canary execution.** The reviewer agent's injection-canary fixture is executed end-to-end through the oracle to demonstrate the injection is ignored. -All three need `_build_cli_oracle()` to reach a real oracle backend. The current implementation returns `Oracle(MCPBridge())`, which is RP-MCP-only and requires an active RepoPrompt GUI — not available in headless mode. spc-13 wired the Codex CLI as a *backend* (`oracle_codex.py`), but `_build_cli_oracle()` does not consume it. So the gates are reachable only when a human is sitting in front of an RP GUI; the autonomous loop reaches them via the run, can't complete them, and either defers (per spc-12's `deferred` permission, extended in `.work/issues.md` 2026-05-19 line 393) or stalls. +All three need `_build_cli_oracle()` to reach a real oracle backend. The current implementation returns `Oracle(MCPBridge())`, which is RP-MCP-only and requires an active RepoPrompt GUI — not available in headless mode. spc-13 (predecessor store) wired the Codex CLI as a *backend* (`oracle_codex.py`), but `_build_cli_oracle()` does not consume it. So the gates are reachable only when a human is sitting in front of an RP GUI; the autonomous loop reaches them via the run, can't complete them, and either defers (per the `deferred` permission of spc-12 (predecessor store), extended in `.work/issues.md` 2026-05-19 line 393) or stalls. -The deferral is *honest* — substituting a different reviewer changes the judgement substrate and fails itd-5 honestly — but it leaves spc-12 in a state where every Ralph completion-review run on the spec produces the same deferral, no progress is made on the gates, and the spec is structurally un-shippable in the loop. The fix is mechanical: extend `_build_cli_oracle()` to wire the Codex leg through a `CodexAgentDispatch` (which spc-11 / itd-6 work already provides primitives for). +The deferral is *honest* — substituting a different reviewer changes the judgement substrate and fails itd-5 honestly — but it leaves spc-12 (predecessor store) in a state where every Ralph completion-review run on the spec produces the same deferral, no progress is made on the gates, and the spec is structurally un-shippable in the loop. The fix is mechanical: extend `_build_cli_oracle()` to wire the Codex leg through a `CodexAgentDispatch` (which spc-11 (predecessor store) / itd-6 work already provides primitives for). This intent is **a precondition for several downstream specs**: any agent spec that depends on `intent-fidelity-reviewer`'s discipline-checking roles (Roles 2 and 3 — itd-31, itd-34) and any future `lifeboat-oracle` work (itd-5's named reviewer) hits the same gate. Fixing it here unblocks the chain. @@ -42,9 +42,9 @@ This intent is **a precondition for several downstream specs**: any agent spec t `CodexAgentDispatch` (Codex leg). The selection logic follows the itd-6 cascade contract: prefer RP if reachable, fall back to Codex, fall back to in-session subagent. -- **Confirm spc-13's `oracle_codex.py` integration** is reachable from this - call site (the Codex CLI is on PATH per spc-13's wiring). -- **Re-run spc-12's three oracle-backed gates** (R6, R7, T1) under the +- **Confirm the `oracle_codex.py` integration of spc-13 (predecessor store)** is reachable from this + call site (the Codex CLI is on PATH per the wiring of spc-13, predecessor store). +- **Re-run the three oracle-backed gates of spc-12 (predecessor store)** (R6, R7, T1) under the extended `_build_cli_oracle()` and rewrite the CHANGELOG / research §7 with real outcomes — replacing the current `deferred` markers. @@ -63,7 +63,7 @@ This intent is **a precondition for several downstream specs**: any agent spec t (depends on itd-2). - **`_build_cli_oracle()` callers other than `intent_fidelity_reviewer.py`.** If other call sites construct the CLI oracle, they likely have the same - problem, but the fix surface here is spc-12's specific call site. Wider + problem, but the fix surface here is the specific call site of spc-12 (predecessor store). Wider audit deferred to itd-6 cascade epic. ## Acceptance Criteria @@ -96,7 +96,7 @@ This section is the canonical multi-spec implementation index: (RP → Codex → in-session). This intent ships the first two. The in-session leg depends on itd-2 (which has no spec yet). Document the partial as an explicit deferral, plumb the third leg later. -- **Test surface for the Codex leg.** spc-13 tested `oracle_codex.py` in +- **Test surface for the Codex leg.** spc-13 (predecessor store) tested `oracle_codex.py` in isolation; this intent needs at least one integration test that exercises the extended `_build_cli_oracle()` end-to-end against a real Codex CLI invocation in a Ralph-like environment. Where does that test live — @@ -105,9 +105,9 @@ This section is the canonical multi-spec implementation index: ## Related -- **spc-12** (`intent-fidelity-reviewer` agent) — the spec whose oracle gates +- **spc-12** (predecessor store; `intent-fidelity-reviewer` agent) — the spec whose oracle gates this intent unblocks. -- **spc-13** (headless Codex CLI oracle wiring) — provides `oracle_codex.py`, +- **spc-13** (predecessor store; headless Codex CLI oracle wiring) — provides `oracle_codex.py`, the Codex leg this intent's `_build_cli_oracle()` consumes. - **itd-6** (RP-MCP-only integration / oracle cascade) — the broader cascade work; this intent ships the first two legs. diff --git a/.abcd/development/intents/superseded/itd-49-flow-state-drift-detector.md b/.abcd/development/intents/superseded/itd-49-flow-state-drift-detector.md index 8fb21e1d1..b795e334c 100644 --- a/.abcd/development/intents/superseded/itd-49-flow-state-drift-detector.md +++ b/.abcd/development/intents/superseded/itd-49-flow-state-drift-detector.md @@ -17,19 +17,19 @@ severity: nitpick ## Press Release -> **abcd ships a standing flow-state drift detector — a read-only checker that compares the `.flow/.checkpoint-spc-*.json` runtime blocks against the `/flow-state/` store and exits non-zero on divergence.** Wired as a local pre-commit hook (pre-commit/local only — see the corrected framing under What's In Scope: both inputs are git-untracked, so a CI gate is infeasible), the detector catches the exact desync that spc-6 (Phase 1 reconciliation) had to repair by hand: completed specs reporting `0/N tasks done`, `flowctl validate --all` flagging done-specs INVALID because the runtime state store was never persisted. Once drift surfaces in a pre-commit run, it gets fixed in the moment instead of accumulating until the next reconciliation pass. +> **abcd ships a standing flow-state drift detector — a read-only checker that compares the `.flow/.checkpoint-spc-*.json` runtime blocks against the `/flow-state/` store and exits non-zero on divergence.** Wired as a local pre-commit hook (pre-commit/local only — see the corrected framing under What's In Scope: both inputs are git-untracked, so a CI gate is infeasible), the detector catches the exact desync that spc-6 (predecessor store; Phase 1 reconciliation) had to repair by hand: completed specs reporting `0/N tasks done`, `flowctl validate --all` flagging done-specs INVALID because the runtime state store was never persisted. Once drift surfaces in a pre-commit run, it gets fixed in the moment instead of accumulating until the next reconciliation pass. > -> "I committed a task closure and the pre-commit hook told me my flow-state store and my `checkpoint` file disagreed about three tasks — including one I hadn't actually finished yet," said Diana, contributor. "Five minutes of investigation showed the runtime store had stale rows from a Ralph run that crashed partway through a loop pass. I fixed it, re-committed, and moved on. The alternative would have been: don't notice, accumulate three more drifts over the next month, then spend half a day reconciling everything in a spc-6-style sweep." +> "I committed a task closure and the pre-commit hook told me my flow-state store and my `checkpoint` file disagreed about three tasks — including one I hadn't actually finished yet," said Diana, contributor. "Five minutes of investigation showed the runtime store had stale rows from a Ralph run that crashed partway through a loop pass. I fixed it, re-committed, and moved on. The alternative would have been: don't notice, accumulate three more drifts over the next month, then spend half a day reconciling everything in a sweep like the predecessor store's spc-6." ## Why This Matters -spc-6 fixed the flow-state desync that abcd-cli had accumulated: `.git/flow-state/` was never persisted (deliverables squashed into the initial commit), `flowctl validate --all` defaulted every task's status to `todo`, and three closed specs (spc-1, spc-2, spc-4) reported `0/N` because the runtime state store and the committed task JSONs disagreed silently. spc-6 reconciled by hand. spc-6's `## Boundaries / non-goals` explicitly deferred the **standing drift detector** — a read-only checker that would catch the desync from recurring — as a new feature with its own intent. +spc-6 (predecessor store) fixed the flow-state desync that abcd-cli had accumulated: `.git/flow-state/` was never persisted (deliverables squashed into the initial commit), `flowctl validate --all` defaulted every task's status to `todo`, and three closed specs (spc-1, spc-2 and spc-4, all predecessor store) reported `0/N` because the runtime state store and the committed task JSONs disagreed silently. spc-6 reconciled by hand. spc-6's `## Boundaries / non-goals` explicitly deferred the **standing drift detector** — a read-only checker that would catch the desync from recurring — as a new feature with its own intent. That intent is this one. The status quo is fragile in a specific way. Before the 0.20.21 → 1.1.1 flow-next migration, abcd had a stricter-than-flow-next `flow-task-definition-drift` pre-commit hook (`check_task_drift.py`) that covered exactly this surface. The 1.1.1 migration removed it, and flow-next 1.x ships no equivalent. So the exact desync this spec repaired by hand has **no automated guard against silently recurring** (per `.work/issues.md` 2026-05-17 line 240). Until a drift detector exists, periodic manual re-verification (`flowctl validate --all` + `flowctl specs`) is the only safety net — and "periodic" means "when someone remembers", which means "after the drift has already cost someone an afternoon". -The drift detector is **project-agnostic** in the same sense spc-6 was: every abcd project that uses flow-next accumulates flow-state, every one of those projects can desync the runtime store from the checkpoint files, and every one of those projects benefits from the same read-only check. +The drift detector is **project-agnostic** in the same sense spc-6 (predecessor store) was: every abcd project that uses flow-next accumulates flow-state, every one of those projects can desync the runtime store from the checkpoint files, and every one of those projects benefits from the same read-only check. ## What's In Scope @@ -44,10 +44,10 @@ The drift detector is **project-agnostic** in the same sense spc-6 was: every ab - Supports `--json` for machine consumption, `--codes` for code-contract discovery, and a default human-readable text output. - A pre-commit hook wired in `.pre-commit-config.yaml`. (Corrected at plan, - spc-41: NOT scoped to staged `.flow/` paths — the hook runs on every commit, + spc-41, predecessor store: NOT scoped to staged `.flow/` paths — the hook runs on every commit, because the store changes independent of staged files and a staged-file - filter would blind the detector. See spc-41 R5.) -- ~~A CI workflow~~ — **corrected at plan (spc-41): pre-commit/local only, NO + filter would blind the detector. See spc-41 (predecessor store) R5.) +- ~~A CI workflow~~ — **corrected at plan (spc-41, predecessor store): pre-commit/local only, NO CI gate.** Both inputs are git-untracked (`.flow/.checkpoint-*` is gitignored; the store lives under `.git/flow-state/`, which git never tracks), so a CI checkout has nothing to scan — a CI job would be @@ -59,10 +59,10 @@ The drift detector is **project-agnostic** in the same sense spc-6 was: every ab - **Auto-repair.** The detector is read-only. Repair tooling (`flowctl checkpoint restore` is the existing flow-next-side option, but - it has the overwrite-defs problem spc-6's T1 documented) is a separate + it has the overwrite-defs problem that T1 of spc-6 (predecessor store) documented) is a separate intent if it turns out the hand-fix path is too cumbersome. - **Coverage beyond task `status`.** The detector flags status divergence - (which was spc-6's specific drift). Per-task `claim_note` or `claimed_at` + (which was the specific drift of spc-6, predecessor store). Per-task `claim_note` or `claimed_at` drift, spec-level `next_task` drift, or dependency-graph drift are out-of-scope for v1. - **flow-next-side fix.** If flow-next 1.x re-introduces an equivalent hook, @@ -75,7 +75,7 @@ The drift detector is **project-agnostic** in the same sense spc-6 was: every ab - *Given* a fresh repo where `.git/flow-state/` matches `.flow/.checkpoint-*.json` exactly, *when* `check_flow_state_drift.py` runs, *then* exit code is 0 and no findings are emitted. - *Given* a repo where one task's runtime state differs from its checkpoint (e.g. checkpoint says `done`, state store says `todo`), *when* the detector runs, *then* exit code is non-zero and the finding names the task id, the divergent field, the checkpoint value, and the state-store value. - *Given* a contributor staging a change that introduces flow-state drift, *when* they commit, *then* the pre-commit hook fires and blocks the commit with the same finding output the detector emits standalone. -- ~~*Given* a PR whose head carries flow-state drift its base does not, *when* CI runs, *then* the drift-check workflow fails the PR check.~~ (Corrected at plan, spc-41: no CI gate exists or can exist — both inputs are git-untracked, so a CI checkout carries no drift to detect. The pre-commit hook is the only automated trigger.) +- ~~*Given* a PR whose head carries flow-state drift its base does not, *when* CI runs, *then* the drift-check workflow fails the PR check.~~ (Corrected at plan, spc-41 (predecessor store): no CI gate exists or can exist — both inputs are git-untracked, so a CI checkout carries no drift to detect. The pre-commit hook is the only automated trigger.) - *Given* the detector emits a finding, *when* a contributor reads `05-internals/06-lint.md`, *then* the finding's code is registered there (provisional code: `FS001` — exact allocation at plan). @@ -83,7 +83,7 @@ The drift detector is **project-agnostic** in the same sense spc-6 was: every ab ## Open Questions - **Strict mode vs. recoverable mode.** Should the detector also flag the - *absence* of `/flow-state/` (which is the spc-6 starting + *absence* of `/flow-state/` (which is the spc-6 (predecessor store) starting state — the store had been wiped), or only divergence between an existing store and the checkpoints? Lean: missing-store is itself a finding (an empty store is worse than a wrong one), but a `--allow-empty-store` flag @@ -91,10 +91,10 @@ The drift detector is **project-agnostic** in the same sense spc-6 was: every ab - **Pre-commit scope.** Run on every commit, or only when `.flow/` paths are staged? Tighter scope (only `.flow/`) is faster; looser scope catches drift introduced by non-`.flow/` work (which shouldn't happen but - occasionally does). Lean tight. (Resolved at plan, spc-41: LOOSE — the hook + occasionally does). Lean tight. (Resolved at plan, spc-41 (predecessor store): LOOSE — the hook runs on every commit. The store is git-untracked and changes independent of staged files, so the tight scope would miss exactly the drift the detector - exists to catch. Cost is measured and bounded instead — spc-41 M5.) + exists to catch. Cost is measured and bounded instead — spc-41 (predecessor store) M5.) - **Finding code allocation.** `FS001` for status divergence; reserve `FS002`–`FS005` for the deferred coverage extensions (claim_note, claimed_at, next_task, dependency-graph). Decide if the reservation is @@ -109,13 +109,13 @@ The drift detector is **project-agnostic** in the same sense spc-6 was: every ab ## Related -- **spc-6** (Phase 1 reconciliation) — the spec that surfaced the gap and +- **spc-6** (predecessor store; Phase 1 reconciliation) — the spec that surfaced the gap and deferred this intent. Frame this work as "the standing version of what - spc-6 did by hand". + spc-6 (predecessor store) did by hand". - **`.work/issues.md` 2026-05-17 line 240** — the canonical entry recording the gap. -- **spc-18 T2** — the pre-commit-vs-CI parity work; this intent's hook - should adopt the same scoping pattern spc-18 T2 settles. +- **spc-18 T2** (predecessor store) — the pre-commit-vs-CI parity work; this intent's hook + should adopt the same scoping pattern spc-18 T2 (predecessor store) settles. - **`05-internals/06-lint.md`** — the lint-code contract; this detector registers there alongside the existing IL/MG/PQ/TM families. - **flow-next 0.20.21** `check_task_drift.py` — the historical reference for From 8f5b7260a770ee1393ea001a11d4a833ae3f282a Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Wed, 30 Sep 2026 01:32:39 +0100 Subject: [PATCH 05/91] docs(intents): correct itd-47 on where its predecessor specs stand Superseded itd-47's Implementing specs section said its frontmatter spec_id records spc-27 as the primary delivering spec. The frontmatter holds spec_id: null, and spc-27 and spc-32 are predecessor-store ids that the live store reuses for the surface-coverage registry and abcd update. The section now says the ids are history, that no native spec delivers the intent (adr-22 supersedes it), and qualifies each id. This is the same defect as iss-2609300025372991 in a third intent. The finding is captured in this change and resolved in the next. Refs: iss-2609300032219282, iss-2609300025372991, iss-2609290448510918 Assisted-by: Claude:claude-opus-5-5 --- .../itd-47-oracle-gates-autonomous-mode.md | 15 ++++++++------- ...d-47-s-implementing-specs-section-says-its.md | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 7 deletions(-) create mode 100644 .abcd/work/issues/open/iss-2609300032219282-superseded-itd-47-s-implementing-specs-section-says-its.md diff --git a/.abcd/development/intents/superseded/itd-47-oracle-gates-autonomous-mode.md b/.abcd/development/intents/superseded/itd-47-oracle-gates-autonomous-mode.md index 939a26724..68918fe66 100644 --- a/.abcd/development/intents/superseded/itd-47-oracle-gates-autonomous-mode.md +++ b/.abcd/development/intents/superseded/itd-47-oracle-gates-autonomous-mode.md @@ -76,13 +76,14 @@ This intent is **a precondition for several downstream specs**: any agent spec t ## Implementing specs -itd-47 is implemented across multiple specs. The single-valued frontmatter -`spec_id` records the **primary** delivering spec (spc-27); the remaining spec is -recorded here because `spec_id` holds one value and would understate scope. -This section is the canonical multi-spec implementation index: - -- **spc-27** (primary) — oracle CLI Codex leg for autonomous mode (the `_build_cli_oracle()` extension + Codex-leg gates). -- **spc-32** — Phase-3 closeout sweep (the remaining oracle-gate hardening delivered under the closeout). +itd-47 was implemented across two specs of the predecessor store; those ids are +preserved below as history. The native spec store reuses both numbers for other +specs, so each carries the predecessor-store qualifier. The frontmatter +`spec_id` is null: adr-22 supersedes this intent, and no native spec delivers +it. Historical index: + +- **spc-27** (predecessor store; primary) — oracle CLI Codex leg for autonomous mode (the `_build_cli_oracle()` extension + Codex-leg gates). +- **spc-32** (predecessor store) — Phase-3 closeout sweep (the remaining oracle-gate hardening delivered under the closeout). ## Open Questions diff --git a/.abcd/work/issues/open/iss-2609300032219282-superseded-itd-47-s-implementing-specs-section-says-its.md b/.abcd/work/issues/open/iss-2609300032219282-superseded-itd-47-s-implementing-specs-section-says-its.md new file mode 100644 index 000000000..cec8f1151 --- /dev/null +++ b/.abcd/work/issues/open/iss-2609300032219282-superseded-itd-47-s-implementing-specs-section-says-its.md @@ -0,0 +1,16 @@ +--- +schema_version: 1 +id: "iss-2609300032219282" +slug: "superseded-itd-47-s-implementing-specs-section-says-its" +severity: "minor" +category: "drift" +source: "agent-finding" +found_during: "autonomous run A resumed 2026-09-25" +origin: researcher-authored +production_mode: hand-written +found_at: ".abcd/development/intents/superseded/itd-47-oracle-gates-autonomous-mode.md" +remedy: "Rewrite the section to say the ids are the predecessor store's, preserved as history, that the native store reuses both numbers, and that the frontmatter spec_id is null because the intent was superseded by adr-22 without a native spec; qualify each id '(predecessor store)'. Grounds: the frontmatter and superseded_by are the primary record, and iss-2609300025372991's fix is the shape." +refines: [iss-2609300025372991] +--- + +Superseded itd-47's Implementing specs section says its frontmatter spec_id records spc-27 as the primary delivering spec, while the frontmatter holds spec_id: null, and spc-27 and spc-32 are predecessor-store ids that the live store reuses for the surface-coverage registry and abcd update. The same defect iss-2609300025372991 fixed in itd-36 and itd-4, found in a third intent while qualifying predecessor-store citations for iss-2609290448510918. From 855ebc9e04882d64486f9a83478a7b8363fe57e5 Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Wed, 30 Sep 2026 01:32:51 +0100 Subject: [PATCH 06/91] =?UTF-8?q?chore:=20resolve=20iss-2609300032219282?= =?UTF-8?q?=20=E2=80=94=20itd-47=20names=20no=20spec=5Fid=20it=20does=20no?= =?UTF-8?q?t=20hold?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fix landed in 8f5b7260a: the Implementing specs section of superseded itd-47 keeps its predecessor-store ids as qualified history and says no native spec delivers it. Resolves: iss-2609300032219282 Assisted-by: Claude:claude-opus-5-5 --- ...rseded-itd-47-s-implementing-specs-section-says-its.md | 8 ++++++++ 1 file changed, 8 insertions(+) rename .abcd/work/issues/{open => resolved}/iss-2609300032219282-superseded-itd-47-s-implementing-specs-section-says-its.md (71%) diff --git a/.abcd/work/issues/open/iss-2609300032219282-superseded-itd-47-s-implementing-specs-section-says-its.md b/.abcd/work/issues/resolved/iss-2609300032219282-superseded-itd-47-s-implementing-specs-section-says-its.md similarity index 71% rename from .abcd/work/issues/open/iss-2609300032219282-superseded-itd-47-s-implementing-specs-section-says-its.md rename to .abcd/work/issues/resolved/iss-2609300032219282-superseded-itd-47-s-implementing-specs-section-says-its.md index cec8f1151..03381f99c 100644 --- a/.abcd/work/issues/open/iss-2609300032219282-superseded-itd-47-s-implementing-specs-section-says-its.md +++ b/.abcd/work/issues/resolved/iss-2609300032219282-superseded-itd-47-s-implementing-specs-section-says-its.md @@ -11,6 +11,14 @@ production_mode: hand-written found_at: ".abcd/development/intents/superseded/itd-47-oracle-gates-autonomous-mode.md" remedy: "Rewrite the section to say the ids are the predecessor store's, preserved as history, that the native store reuses both numbers, and that the frontmatter spec_id is null because the intent was superseded by adr-22 without a native spec; qualify each id '(predecessor store)'. Grounds: the frontmatter and superseded_by are the primary record, and iss-2609300025372991's fix is the shape." refines: [iss-2609300025372991] +resolution: "itd-47's Implementing specs section now says spc-27 and spc-32 are the predecessor store's ids kept as history, that the native store reuses both numbers, and that no native spec delivers the intent (spec_id null, superseded by adr-22)." +impact: internal +resolved_by: + commit: "8f5b7260a" --- Superseded itd-47's Implementing specs section says its frontmatter spec_id records spc-27 as the primary delivering spec, while the frontmatter holds spec_id: null, and spc-27 and spc-32 are predecessor-store ids that the live store reuses for the surface-coverage registry and abcd update. The same defect iss-2609300025372991 fixed in itd-36 and itd-4, found in a third intent while qualifying predecessor-store citations for iss-2609290448510918. + +## Grounds + +- pursued: the section no longer claims a spec_id the frontmatter does not hold; a grep of it finding 'spec_id' records spc-27, or an unqualified spc-27 or spc-32, would show it wrong From 058c392dd1cc625c12da55b0cd0c0f44b0c96bea Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Wed, 30 Sep 2026 01:33:13 +0100 Subject: [PATCH 07/91] chore(issues): record the predecessor-qualifier sweep's progress iss-2609290448510918 gains a dated progress section and a remedy line. The census over the four intent folders counts 186 sites; 94 were the predecessor store's and carry the qualifier, 72 cite live specs and 15 are data. The five sites in itd-82 and itd-130 were read but not edited, because other branches carry those intents, so the record stays open until they are confirmed at the merged tip. Refs: iss-2609290448510918 Assisted-by: Claude:claude-opus-5-5 --- ...ssor-qualifier-sweep-past-the-six-named-intents.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.abcd/work/issues/open/iss-2609290448510918-predecessor-qualifier-sweep-past-the-six-named-intents.md b/.abcd/work/issues/open/iss-2609290448510918-predecessor-qualifier-sweep-past-the-six-named-intents.md index 129d34307..b5d710b9e 100644 --- a/.abcd/work/issues/open/iss-2609290448510918-predecessor-qualifier-sweep-past-the-six-named-intents.md +++ b/.abcd/work/issues/open/iss-2609290448510918-predecessor-qualifier-sweep-past-the-six-named-intents.md @@ -11,6 +11,17 @@ production_mode: hand-written found_at: ".abcd/development/intents" deferred_after: "v0.11.1" deferral_reason: "no ruling owed; carried past v0.11.1 by lane drainDrift3 (run A 2026-09-29) on its size: the 192 sites each need a reading against the live spec their id collides with, which the lane's time box did not hold after qualifying the six intents iss-2609261536147903 named." +remedy: "Read each remaining site against the live spec its id collides with and qualify the predecessor-store ones '(predecessor store)' per the specs charter's Two spc-N Namespaces rule, after the branches carrying itd-82 and itd-130 land (the five sites named in the progress section below); grounds: the charter is the rule, and the census below (ordinal ids at or below spc-70, not the intent's own spec, no qualifier on the line) reproduces the set." --- The predecessor-store qualifier sweep reaches past the six intents iss-2609261536147903 named: outside drafts/ and those six, 192 citation sites across about forty intents name a spc-N at or below spc-70 that is not the citing intent's own spec, with no '(predecessor store)' on the line. Some are live cross-references (the cold-reading family's intents cite each other's live specs), and some are the predecessor store's (itd-4, itd-6, itd-29, itd-47 and itd-49 describe pre-rebuild work in its terms), so each site needs the same reading against the live spec it collides with; the specs charter's Two spc-N Namespaces section is the rule. Found while sweeping the pattern of iss-2609261536147903 in lane drainDrift3; a census script over intents/{planned,shipped,disciplines} reproduces the count. + +## Progress 2026-09-30 (lane drainCitations, run A) + +A census at 2b9d52fbb over `intents/planned`, `intents/shipped`, `intents/disciplines` and `intents/superseded` (an ordinal `spc-N` at or below spc-70, not the citing intent's own spec, on a line without the qualifier) counts 186 sites across 42 intents: 125 outside superseded/ and 61 inside it, where itd-29, itd-47 and itd-49 now sit. The first count of 192 was not scripted in the record, so the six-site difference is not traced. Every site was read against the live spec it collides with. + +- Predecessor store, now qualified (94 sites, 12 intents): itd-4 (spc-20 to spc-23), itd-6 (spc-2, spc-4, spc-5), itd-36 (spc-38, spc-39), itd-50 (spc-52), itd-65 and itd-66 (spc-64, spc-27), and superseded itd-17, itd-20, itd-27, itd-29, itd-47 and itd-49. Two shipped acceptance criteria gained the qualifier, itd-4's drift criterion and itd-65's fail-closed criterion; each names an owner or a precedent, not a promise, so both are wording clarifications. The Implementing specs sections of itd-36, itd-4 and itd-47 also misstated where those ids stand, captured and fixed as iss-2609300025372991 and iss-2609300032219282. +- Live cross-references, correct as written (72 sites): the cold-reading family's citations of spc-55 to spc-69 (itd-177 to itd-189, itd-2609020625400194, itd-2609020625400445), itd-4's audit notes on spc-24, and itd-3, itd-80, itd-94, itd-101, itd-121, itd-132, itd-133, itd-147, itd-160, itd-161, itd-2609091416295622, itd-2609111003026787 and itd-2609231013154443. +- Data, left as written (15 sites): the `routed_from` frontmatter of itd-48, itd-50 and itd-53; itd-27's dated reclassification_history reason; fixture strings quoted as evidence in itd-4, itd-28, itd-186 and itd-2609111003026787; the example resolution note inside itd-4's resolve criterion; and itd-4's audit-note line that already says "of the retired record system". + +Skipped, because other branches carry these intents: itd-82 (one site, spc-24) and itd-130 (four sites, spc-35). Read in place, all five cite live specs (itd-119's promote and itd-132's data directory), so none looks owed a qualifier. The other intents named for skipping (itd-111, itd-148, itd-24, itd-103, itd-2609081951381895, itd-2609211116005482, itd-2609212103568351 and itd-2609212103572513) hold no site in the census. The record stays open until those five sites are confirmed at the merged tip. From 4c2c1634d9d83d40c751f35dcf719073931ef4d2 Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Wed, 30 Sep 2026 01:55:03 +0100 Subject: [PATCH 08/91] refactor(changelog): unexport LatestVersionIn and MaxImpact Both are used only inside the changelog package, so they leave the exported surface and the reach-audit baseline. DeriveNext stays baselined: launch/semver.go cites it by its qualified name, and launch is being edited elsewhere. Refs: iss-2609252211487887 Assisted-by: Claude:claude-opus-5-5 --- internal/core/changelog/anchor.go | 12 ++++++------ internal/core/changelog/impact.go | 4 ++-- internal/core/changelog/impact_test.go | 4 ++-- internal/core/changelog/shipped.go | 2 +- internal/core/changelog/version_test.go | 2 +- internal/reachaudit/testdata/core-unreached.txt | 2 -- 6 files changed, 12 insertions(+), 14 deletions(-) diff --git a/internal/core/changelog/anchor.go b/internal/core/changelog/anchor.go index 2732d129a..d999969fb 100644 --- a/internal/core/changelog/anchor.go +++ b/internal/core/changelog/anchor.go @@ -134,14 +134,14 @@ func LatestChangelogVersion(root string) (launch.Semver, bool, error) { } return launch.Semver{}, false, err } - return LatestVersionIn(data) + return latestVersionIn(data) } -// LatestVersionIn is LatestChangelogVersion over CHANGELOG bytes the caller +// latestVersionIn is LatestChangelogVersion over CHANGELOG bytes the caller // already holds — a blob read out of a commit rather than the working tree, // which is how the release gate compares the version a receipt's commit carries // with the version being released. -func LatestVersionIn(data []byte) (launch.Semver, bool, error) { +func latestVersionIn(data []byte) (launch.Semver, bool, error) { for _, line := range strings.Split(string(data), "\n") { m := datedHeadingRe.FindStringSubmatch(strings.TrimRight(line, "\r")) if m == nil { @@ -173,13 +173,13 @@ var ErrUnreadableReleaseHeading = errors.New("the newest CHANGELOG release headi // ReleasedVersionIn is the strict reading of the version a released tree names, // for a caller that BINDS something to that version rather than merely reports -// it. LatestVersionIn skips every heading datedHeadingRe does not match, which is +// it. latestVersionIn skips every heading datedHeadingRe does not match, which is // right for a preview but wrong for a binding: a pre-release head ("## [1.0.0-rc.1] // - …"), a build-metadata head or an undated version head would be skipped, and // the reader would answer with the PREVIOUS release's version. // // So this reader takes the newest "## [" heading other than "## [Unreleased]" -// and requires it to be a dated heading LatestVersionIn parses; anything else is +// and requires it to be a dated heading latestVersionIn parses; anything else is // ErrUnreadableReleaseHeading, naming the line. found=false means the file names // no release heading at all, which the caller decides about. func ReleasedVersionIn(data []byte) (launch.Semver, bool, error) { @@ -191,7 +191,7 @@ func ReleasedVersionIn(data []byte) (launch.Semver, bool, error) { if !datedHeadingRe.MatchString(line) { return launch.Semver{}, false, fmt.Errorf("%w: %q", ErrUnreadableReleaseHeading, line) } - return LatestVersionIn([]byte(line)) + return latestVersionIn([]byte(line)) } return launch.Semver{}, false, nil } diff --git a/internal/core/changelog/impact.go b/internal/core/changelog/impact.go index e59d03349..03a06903a 100644 --- a/internal/core/changelog/impact.go +++ b/internal/core/changelog/impact.go @@ -100,13 +100,13 @@ func (i Impact) rank() int { } } -// MaxImpact returns the strongest impact in a set — the single comparison in +// maxImpact returns the strongest impact in a set — the single comparison in // this package, so callers never re-derive the ordering. A set with nothing // user-facing in it (empty, all-internal, or all-unrecognised) yields // ImpactInternal, which reads correctly at the call site as "nothing to // release": the result drives no bump and belongs in no changelog. The input is // not modified. -func MaxImpact(impacts []Impact) Impact { +func maxImpact(impacts []Impact) Impact { best := ImpactInternal for _, i := range impacts { if i.rank() > best.rank() { diff --git a/internal/core/changelog/impact_test.go b/internal/core/changelog/impact_test.go index 966dd4c98..20b014faa 100644 --- a/internal/core/changelog/impact_test.go +++ b/internal/core/changelog/impact_test.go @@ -126,7 +126,7 @@ func TestMaxImpactOrdering(t *testing.T) { } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - if got := MaxImpact(tc.in); got != tc.want { + if got := maxImpact(tc.in); got != tc.want { t.Errorf("MaxImpact(%v) = %q, want %q", tc.in, got, tc.want) } }) @@ -137,7 +137,7 @@ func TestMaxImpactOrdering(t *testing.T) { // passes the cut's impacts around and must not find them reordered. func TestMaxImpactDoesNotMutateInput(t *testing.T) { in := []Impact{ImpactFix, ImpactBreaking, ImpactInternal} - _ = MaxImpact(in) + _ = maxImpact(in) want := []Impact{ImpactFix, ImpactBreaking, ImpactInternal} for i := range want { if in[i] != want[i] { diff --git a/internal/core/changelog/shipped.go b/internal/core/changelog/shipped.go index f8b20139f..ec96b1b0c 100644 --- a/internal/core/changelog/shipped.go +++ b/internal/core/changelog/shipped.go @@ -249,7 +249,7 @@ func maxImpactOf(records []Record) Impact { for _, r := range records { impacts = append(impacts, r.Impact) } - return MaxImpact(impacts) + return maxImpact(impacts) } // ShippedSince computes the release cut between baseRef (the anchor tag) and diff --git a/internal/core/changelog/version_test.go b/internal/core/changelog/version_test.go index ee08592f7..5f1697a6b 100644 --- a/internal/core/changelog/version_test.go +++ b/internal/core/changelog/version_test.go @@ -98,7 +98,7 @@ func TestDeriveNextDropsPrereleaseMetadata(t *testing.T) { // the caller would write as a duplicate heading). func TestDeriveNextEmptySetDoesNotBump(t *testing.T) { prev := mustSemver(t, "0.3.0") - if _, bumped := DeriveNext(prev, MaxImpact(nil)); bumped { + if _, bumped := DeriveNext(prev, maxImpact(nil)); bumped { t.Error("an empty record set must not bump") } } diff --git a/internal/reachaudit/testdata/core-unreached.txt b/internal/reachaudit/testdata/core-unreached.txt index 7f9916520..70a32054d 100644 --- a/internal/reachaudit/testdata/core-unreached.txt +++ b/internal/reachaudit/testdata/core-unreached.txt @@ -20,8 +20,6 @@ internal/core/capture.ConstrualFingerprint internal/core/capture.GlossaryFingerprint internal/core/capture.ScopeFingerprint internal/core/changelog.DeriveNext -internal/core/changelog.LatestVersionIn -internal/core/changelog.MaxImpact internal/core/cite.NewHTTPChecker internal/core/cite.ParseReceipt internal/core/credential.KeychainItem From 0963b1ad803f2d4a7064a10b267af2cb013ea393 Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Wed, 30 Sep 2026 01:55:47 +0100 Subject: [PATCH 09/91] refactor(glossary,grounds,identity): unexport in-package-only functions glossary.RenderIndex and RenderLayout are reached only by the package's own README anti-drift tests; grounds.ParseToken and identity.EffectiveCommitter only by their own packages. All four leave the exported surface and the reach-audit baseline; the fence-writer reason in mdrecord follows the rename. glossary.Scan stays baselined (site cites it by qualified name, and site is being edited elsewhere), as does identity.LoadPin (the ahoy tests call it across packages). Refs: iss-2609252211487887 Assisted-by: Claude:claude-opus-5-5 --- internal/core/glossary/index.go | 8 ++++---- internal/core/glossary/index_test.go | 12 ++++++------ internal/core/grounds/grounds.go | 12 ++++++------ internal/core/identity/committer_test.go | 4 ++-- internal/core/identity/identity.go | 8 ++++---- internal/core/mdrecord/fence_canonical_test.go | 2 +- internal/reachaudit/testdata/core-unreached.txt | 4 ---- 7 files changed, 23 insertions(+), 27 deletions(-) diff --git a/internal/core/glossary/index.go b/internal/core/glossary/index.go index 129c5b238..8db3e8c2d 100644 --- a/internal/core/glossary/index.go +++ b/internal/core/glossary/index.go @@ -246,9 +246,9 @@ func frontmatterOpen(lines []string) int { return -1 } -// RenderLayout returns the glossary's directory layout as a fenced tree, exactly +// renderLayout returns the glossary's directory layout as a fenced tree, exactly // as it appears between the layout markers in the glossary README. -func RenderLayout(g Glossary) string { +func renderLayout(g Glossary) string { var b strings.Builder b.WriteString("```\n") b.WriteString("glossary/\n") @@ -276,9 +276,9 @@ func RenderLayout(g Glossary) string { return b.String() } -// RenderIndex returns the term index — one subsection and table per bounded +// renderIndex returns the term index — one subsection and table per bounded // context — exactly as it appears between the index markers in the README. -func RenderIndex(g Glossary) string { +func renderIndex(g Glossary) string { var b strings.Builder for i, ctx := range g.Contexts { if i > 0 { diff --git a/internal/core/glossary/index_test.go b/internal/core/glossary/index_test.go index 9ecae94f1..08bd4ae10 100644 --- a/internal/core/glossary/index_test.go +++ b/internal/core/glossary/index_test.go @@ -68,9 +68,9 @@ func scan(t *testing.T) Glossary { // regenerating the index fails here. func TestGlossaryREADMECarriesTheRenderedIndex(t *testing.T) { got := between(t, readREADME(t), IndexMarkerBegin, IndexMarkerEnd) - want := strings.TrimSpace(RenderIndex(scan(t))) + want := strings.TrimSpace(renderIndex(scan(t))) if got != want { - t.Errorf("%s term index has drifted from the term files.\n\n--- README has ---\n%s\n\n--- RenderIndex wants ---\n%s", + t.Errorf("%s term index has drifted from the term files.\n\n--- README has ---\n%s\n\n--- renderIndex wants ---\n%s", READMERelPath, got, want) } } @@ -80,9 +80,9 @@ func TestGlossaryREADMECarriesTheRenderedIndex(t *testing.T) { // omit a whole bounded context. func TestGlossaryREADMECarriesTheRenderedLayout(t *testing.T) { got := between(t, readREADME(t), LayoutMarkerBegin, LayoutMarkerEnd) - want := strings.TrimSpace(RenderLayout(scan(t))) + want := strings.TrimSpace(renderLayout(scan(t))) if got != want { - t.Errorf("%s directory layout has drifted from the glossary directory.\n\n--- README has ---\n%s\n\n--- RenderLayout wants ---\n%s", + t.Errorf("%s directory layout has drifted from the glossary directory.\n\n--- README has ---\n%s\n\n--- renderLayout wants ---\n%s", READMERelPath, got, want) } } @@ -134,9 +134,9 @@ func TestRenderIndexEscapesTableCells(t *testing.T) { Files: []string{"widget.md"}, Terms: []Term{{Name: "widget", File: "widget.md", Status: "draft", Definition: "a | b"}}, }}} - row := RenderIndex(g) + row := renderIndex(g) if !strings.Contains(row, `a \| b`) { - t.Errorf("RenderIndex did not escape the pipe in a definition:\n%s", row) + t.Errorf("renderIndex did not escape the pipe in a definition:\n%s", row) } } diff --git a/internal/core/grounds/grounds.go b/internal/core/grounds/grounds.go index 4e43905e4..5ccb6f4ad 100644 --- a/internal/core/grounds/grounds.go +++ b/internal/core/grounds/grounds.go @@ -55,7 +55,7 @@ const ( ) // Vocabulary is the closed set, in the order a surface should offer it. The two -// refusals that reject a TOKEN -- Parse's grammar refusal and ParseToken's -- +// refusals that reject a TOKEN -- Parse's grammar refusal and parseToken's -- // render it (vocabularyList), so a caller told their token is wrong is told // which tokens are right. The package's other refusals render nothing of // the kind -- and that is all this comment claims about them. Two earlier @@ -214,7 +214,7 @@ func Parse(s string) (Grounds, error) { return Grounds{}, fmt.Errorf( "grounds %q is not `: ` (want one of %s followed by the conjecture)", s, vocabularyList()) } - t, err := ParseToken(tok) + t, err := parseToken(tok) if err != nil { return Grounds{}, err } @@ -225,10 +225,10 @@ func Parse(s string) (Grounds, error) { return Grounds{Token: t, Text: folded}, nil } -// ParseToken validates one vocabulary value. Surrounding whitespace and case are +// parseToken validates one vocabulary value. Surrounding whitespace and case are // forgiven — the value is stored canonically either way, and refusing `Pursued:` // would spend a refusal on nothing. -func ParseToken(s string) (Token, error) { +func parseToken(s string) (Token, error) { t := Token(strings.ToLower(strings.TrimSpace(s))) for _, v := range Vocabulary { if t == v { @@ -244,7 +244,7 @@ func ParseToken(s string) (Token, error) { // folded to one line first — both carriers hold a single line — so a text that // is only line breaks is refused as the empty text it is. func New(tok Token, text string) (Grounds, error) { - t, err := ParseToken(string(tok)) + t, err := parseToken(string(tok)) if err != nil { return Grounds{}, err } @@ -264,7 +264,7 @@ func New(tok Token, text string) (Grounds, error) { // supplies, because the value it is derived from has its own contract and a // terse reason is a legal one (iss-2608301244450106). func NewDerived(tok Token, text string) (Grounds, error) { - t, err := ParseToken(string(tok)) + t, err := parseToken(string(tok)) if err != nil { return Grounds{}, err } diff --git a/internal/core/identity/committer_test.go b/internal/core/identity/committer_test.go index a05b79c5d..a92829779 100644 --- a/internal/core/identity/committer_test.go +++ b/internal/core/identity/committer_test.go @@ -30,7 +30,7 @@ func TestEffectiveCommitter_EnvFirst(t *testing.T) { dir := gitRepo(t, "Alex Reppel", "alex@example.com") t.Setenv("GIT_COMMITTER_NAME", "Test User") t.Setenv("GIT_COMMITTER_EMAIL", "test@example.com") - eff, err := EffectiveCommitter(dir) + eff, err := effectiveCommitter(dir) if err != nil { t.Fatal(err) } @@ -45,7 +45,7 @@ func TestEffectiveCommitter_RoleConfigThenUser(t *testing.T) { isolateCommitter(t) dir := gitRepo(t, "Alex Reppel", "alex@example.com") runGitT(t, dir, "config", "committer.email", "ci@example.com") - eff, err := EffectiveCommitter(dir) + eff, err := effectiveCommitter(dir) if err != nil { t.Fatal(err) } diff --git a/internal/core/identity/identity.go b/internal/core/identity/identity.go index e57505181..7ecaa7dc1 100644 --- a/internal/core/identity/identity.go +++ b/internal/core/identity/identity.go @@ -93,7 +93,7 @@ type Result struct { Effective Effective Reason string - // Committer is the committer identity git would stamp (EffectiveCommitter). + // Committer is the committer identity git would stamp (effectiveCommitter). Committer Effective // CommitterDiverges reports a committer that differs from the author and is // not the pinned identity either: a GIT_COMMITTER_* override, a committer.* @@ -270,12 +270,12 @@ func EffectiveIdentity(root string) (Effective, error) { return effective(root, RoleAuthor) } -// EffectiveCommitter returns the committer identity git would stamp on a commit +// effectiveCommitter returns the committer identity git would stamp on a commit // in root, resolved exactly as EffectiveIdentity resolves the author: // GIT_COMMITTER_NAME / GIT_COMMITTER_EMAIL first, then committer.name / // committer.email, then user.name / user.email. An unset field is empty, never // fabricated. -func EffectiveCommitter(root string) (Effective, error) { +func effectiveCommitter(root string) (Effective, error) { return effective(root, RoleCommitter) } @@ -368,7 +368,7 @@ func Check(root string) (Result, error) { if err != nil { return Result{}, err } - committer, err := EffectiveCommitter(root) + committer, err := effectiveCommitter(root) if err != nil { return Result{}, err } diff --git a/internal/core/mdrecord/fence_canonical_test.go b/internal/core/mdrecord/fence_canonical_test.go index ed8d9ed27..ccef5b58a 100644 --- a/internal/core/mdrecord/fence_canonical_test.go +++ b/internal/core/mdrecord/fence_canonical_test.go @@ -32,7 +32,7 @@ type fenceWriter struct { var fenceWriters = map[string]fenceWriter{ "internal/adapter/openaiapi/client.go": {3, "judges one model answer whole: unfence strips a single fence wrapping the entire answer, and refuses to when another delimiter sits inside; it reads no document and tracks no lines"}, "internal/adapter/scanner/scanner.go": {1, "a comment quoting a regexp quantifier (`{36,}`); no delimiter is written or read"}, - "internal/core/glossary/index.go": {2, "a WRITER: RenderLayout wraps the generated layout tree in one fence; it reads no fences"}, + "internal/core/glossary/index.go": {2, "a WRITER: renderLayout wraps the generated layout tree in one fence; it reads no fences"}, "internal/core/history/reconstruct_render.go": {1, "a WRITER: writeFenced opens a fence longer than any backtick run in the body, the floor of three; it reads no fences"}, "internal/core/implement/loop/brief.go": {2, "a WRITER: the lane brief shows the receipt's shape inside one json fence, before any record body it quotes; it reads no fences"}, "internal/core/lifeboat/sources_conventions.go": {3, "judges one line or the whole text: a README prose measure skips a delimiter line, and a presence test asks whether any fence exists; neither tracks which lines a fence covers"}, diff --git a/internal/reachaudit/testdata/core-unreached.txt b/internal/reachaudit/testdata/core-unreached.txt index 70a32054d..375544294 100644 --- a/internal/reachaudit/testdata/core-unreached.txt +++ b/internal/reachaudit/testdata/core-unreached.txt @@ -26,16 +26,12 @@ internal/core/credential.KeychainItem internal/core/credential.Machine internal/core/credential.Set internal/core/credential.SetMachine -internal/core/glossary.RenderIndex -internal/core/glossary.RenderLayout internal/core/glossary.Scan -internal/core/grounds.ParseToken internal/core/guard.Defaults internal/core/guard.Load internal/core/guard.Merge internal/core/guard.Validate internal/core/history.DefaultConfig -internal/core/identity.EffectiveCommitter internal/core/identity.LoadPin internal/core/implement.LoadResultFromEvent internal/core/implement.ReadingCorpus From 83e558cdd83792dd2fa55ac77c7a51a5d0affdb7 Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Wed, 30 Sep 2026 01:57:22 +0100 Subject: [PATCH 10/91] refactor(lifeboat): unexport four in-package-only functions Render (now renderMapping, the table the brief's anti-drift test pins), Tiers (now allTiers), VerifyManifest and RecordManifestSHA256 are reached only from inside the lifeboat package. They leave the exported surface and the reach-audit baseline. ManifestSHA256 stays baselined: a cli test calls it across packages. Refs: iss-2609252211487887 Assisted-by: Claude:claude-opus-5-5 --- internal/core/lifeboat/embark.go | 8 +++--- internal/core/lifeboat/embark_test.go | 26 +++++++++---------- internal/core/lifeboat/embark_types.go | 10 +++---- internal/core/lifeboat/mapping.go | 8 +++--- internal/core/lifeboat/mapping_test.go | 8 +++--- internal/core/lifeboat/operand_test.go | 2 +- internal/core/lifeboat/plan.go | 16 ++++++------ internal/core/lifeboat/plan_test.go | 20 +++++++------- .../core/lifeboat/synthesis_manifest_test.go | 4 +-- .../core/lifeboat/synthesis_principles.go | 2 +- internal/core/lifeboat/synthesis_review.go | 6 ++--- .../core/lifeboat/synthesis_review_test.go | 8 +++--- internal/core/lifeboat/synthesis_types.go | 4 +-- .../reachaudit/testdata/core-unreached.txt | 4 --- 14 files changed, 61 insertions(+), 65 deletions(-) diff --git a/internal/core/lifeboat/embark.go b/internal/core/lifeboat/embark.go index c9ae2068d..adb26d884 100644 --- a/internal/core/lifeboat/embark.go +++ b/internal/core/lifeboat/embark.go @@ -178,7 +178,7 @@ func rejudgeEmbark(targetAbs string, planned []PlannedEmbark) ([]PlannedEmbark, return out, conflicts } -// VerifyManifest re-hashes every non-excluded file in the lifeboat and compares +// verifyManifest re-hashes every non-excluded file in the lifeboat and compares // the result to _provenance.json's manifest_sha256. It enforces the trust // boundary during the walk: it refuses a symlink anywhere in the tree, a path // that fails validRelPath, a file over maxEmbarkFileBytes, a tree over @@ -187,7 +187,7 @@ func rejudgeEmbark(targetAbs string, planned []PlannedEmbark) ([]PlannedEmbark, // nil iff the lifeboat is intact. The excluded set (_provenance.json, the // post-pack layer-3 graveyard/lessons.json and graveyard/low-confidence/**) is // the same set the packer left out of manifest_sha256. -func VerifyManifest(dir string) error { +func verifyManifest(dir string) error { abs, err := filepath.Abs(dir) if err != nil { return err @@ -288,7 +288,7 @@ func runPlanner(lifeboatDir, targetDir string) (plannerResult, error) { return plannerResult{}, update.TooNew("lifeboat", prov.SchemaVersion, SchemaVersion) } - if err := VerifyManifest(lifeboatAbs); err != nil { + if err := verifyManifest(lifeboatAbs); err != nil { return plannerResult{}, err } // Gate the target: a real directory (a symlinked or absent target is @@ -793,7 +793,7 @@ func readProvenance(abs string) (Provenance, error) { // isManifestExcluded reports whether a lifeboat path was left out of // manifest_sha256 (the header and the post-pack layer-3 interpretation), so -// VerifyManifest reproduces the pinned hash exactly. +// verifyManifest reproduces the pinned hash exactly. func isManifestExcluded(rel string) bool { for _, e := range manifestExcludedExact { if rel == e { diff --git a/internal/core/lifeboat/embark_test.go b/internal/core/lifeboat/embark_test.go index 894202fa9..5e12da6c1 100644 --- a/internal/core/lifeboat/embark_test.go +++ b/internal/core/lifeboat/embark_test.go @@ -350,13 +350,13 @@ func mustWrite(t *testing.T, path string, data []byte) { } // --------------------------------------------------------------------------- -// VerifyManifest +// verifyManifest // --------------------------------------------------------------------------- func TestVerifyManifestIntactLifeboat(t *testing.T) { repo := packFixture(t) dest, _ := packInto(t, repo, okScan) - if err := VerifyManifest(dest); err != nil { + if err := verifyManifest(dest); err != nil { t.Errorf("intact lifeboat failed verification: %v", err) } } @@ -370,7 +370,7 @@ func TestVerifyManifestCatchesTampering(t *testing.T) { if err := os.WriteFile(adr, append(data, '!'), 0o644); err != nil { t.Fatal(err) } - if err := VerifyManifest(dest); err == nil { + if err := verifyManifest(dest); err == nil { t.Error("a flipped record byte must fail verification") } }) @@ -381,7 +381,7 @@ func TestVerifyManifestCatchesTampering(t *testing.T) { if err := os.Remove(filepath.Join(dest, "docs/adrs/0001-example.md")); err != nil { t.Fatal(err) } - if err := VerifyManifest(dest); err == nil { + if err := verifyManifest(dest); err == nil { t.Error("a missing manifest file must fail verification") } }) @@ -392,7 +392,7 @@ func TestVerifyManifestCatchesTampering(t *testing.T) { if err := os.WriteFile(filepath.Join(dest, "docs/adrs/planted.md"), []byte("foreign\n"), 0o644); err != nil { t.Fatal(err) } - if err := VerifyManifest(dest); err == nil { + if err := verifyManifest(dest); err == nil { t.Error("an extra manifest-relevant file must fail verification") } }) @@ -403,7 +403,7 @@ func TestVerifyManifestCatchesTampering(t *testing.T) { if err := os.Symlink(filepath.Join(dest, "coverage.json"), filepath.Join(dest, "link.json")); err != nil { t.Skipf("cannot symlink: %v", err) } - if err := VerifyManifest(dest); err == nil { + if err := verifyManifest(dest); err == nil { t.Error("a symlink inside the lifeboat must fail verification") } }) @@ -415,7 +415,7 @@ func TestVerifyManifestCatchesTampering(t *testing.T) { if err := os.WriteFile(filepath.Join(dest, "docs/adrs/huge.md"), big, 0o644); err != nil { t.Fatal(err) } - if err := VerifyManifest(dest); err == nil { + if err := verifyManifest(dest); err == nil { t.Error("an oversize file must fail verification") } }) @@ -428,7 +428,7 @@ func TestVerifyManifestToleratesLayer3(t *testing.T) { // manifest; its presence must NOT break verification. mustWrite(t, filepath.Join(dest, "graveyard/lessons.json"), []byte(`{"schema_version":1,"lessons":[]}`+"\n")) mustWrite(t, filepath.Join(dest, "graveyard/low-confidence/x.json"), []byte(`{"schema_version":1,"lessons":[]}`+"\n")) - if err := VerifyManifest(dest); err != nil { + if err := verifyManifest(dest); err != nil { t.Errorf("layer-3 files broke verification: %v", err) } } @@ -639,7 +639,7 @@ func TestEmbarkProbeIgnoredClassification(t *testing.T) { source := embarkableSourceFixture(t) dest := packSource(t, source) // Plant an unknown foreign file and an unknown-bucket issue INTO the lifeboat, - // then re-seal so VerifyManifest still passes. + // then re-seal so verifyManifest still passes. mustWrite(t, filepath.Join(dest, "foo/bar.md"), []byte("foreign\n")) mustWrite(t, filepath.Join(dest, "activity/issues/bogus/iss-9-x.md"), []byte("bad bucket\n")) reseal(t, dest) @@ -673,7 +673,7 @@ func TestEmbarkProbeIgnoredClassification(t *testing.T) { func TestEmbarkRefusesSymlinkedLifeboatFile(t *testing.T) { source := embarkableSourceFixture(t) dest := packSource(t, source) - // Replace a record with a symlink; VerifyManifest (and the walk) must refuse it. + // Replace a record with a symlink; verifyManifest (and the walk) must refuse it. adr := filepath.Join(dest, "docs/adrs/0001-record-architecture-decisions.md") if err := os.Remove(adr); err != nil { t.Fatal(err) @@ -753,8 +753,8 @@ func TestP1RecordManifestClosure(t *testing.T) { t.Fatal(err) } - h1 := RecordManifestSHA256(l1.Files) - h2 := RecordManifestSHA256(l2.Files) + h1 := recordManifestSHA256(l1.Files) + h2 := recordManifestSHA256(l2.Files) if h1 != h2 { t.Errorf("record manifest hash not closed: L1=%s L2=%s", h1, h2) // Diagnose which record family diverged. @@ -894,7 +894,7 @@ func bumpProvenanceSchema(t *testing.T, dest string, v int) { // reseal recomputes manifest_sha256 over the current on-disk (non-excluded) tree // and rewrites _provenance.json, so a test that plants files into a packed -// lifeboat keeps VerifyManifest passing. It mirrors the manifest construction. +// lifeboat keeps verifyManifest passing. It mirrors the manifest construction. func reseal(t *testing.T, dest string) { t.Helper() root, err := os.OpenRoot(dest) diff --git a/internal/core/lifeboat/embark_types.go b/internal/core/lifeboat/embark_types.go index 2bc6bd5a9..8b2bebe4d 100644 --- a/internal/core/lifeboat/embark_types.go +++ b/internal/core/lifeboat/embark_types.go @@ -6,10 +6,10 @@ package lifeboat // it without diverging: // // - Agent A (plan.go): teaches the packer to copy .abcd/development/specs/** -// into rescue/specs//, adds RecordManifestSHA256 over the +// into rescue/specs//, adds recordManifestSHA256 over the // record-derived families (isRecordDerived below), and records it in // Provenance as record_manifest_sha256. -// - Agent B (embark.go): EmbarkProbe / EmbarkFrom / VerifyManifest and the +// - Agent B (embark.go): EmbarkProbe / EmbarkFrom / verifyManifest and the // conflict/marker/coverage machinery, plus ahoy.EnsureMarker in package ahoy. // - Agent C (surface/cli): the `abcd embark probe|from` command tree, // commands/embark.md, and the surface-registry row-3 flip. @@ -319,7 +319,7 @@ var embarkFamilies = []embarkFamily{ // recordDerivedPrefixes are the lifeboat path prefixes whose bytes derive purely // from the repo's RECORD (never from git or the operator's identity), so they // must round-trip byte-identically through pack -> embark -> re-pack (closure -// property P1, decision 1). RecordManifestSHA256 (Agent A) hashes exactly the +// property P1, decision 1). recordManifestSHA256 (Agent A) hashes exactly the // files matching one of these prefixes. A slash-terminated entry matches a whole // family; "graveyard/abandoned.json" is deliberately slash-LESS so it matches only // itself (the deterministic layer-2 record extraction), not a family. @@ -358,7 +358,7 @@ var reportOnlyPrefixes = []string{ } // manifestExcludedExact / manifestExcludedPrefixes name the on-disk lifeboat -// files that are NOT part of manifest_sha256, so VerifyManifest (Agent B) can walk +// files that are NOT part of manifest_sha256, so verifyManifest (Agent B) can walk // the tree and reproduce the pinned hash exactly. _provenance.json cannot hash // itself; graveyard/lessons.json and graveyard/low-confidence/** are the mutable, // post-pack, host-delegated layer-3 interpretation that IngestLessons writes into @@ -367,7 +367,7 @@ var reportOnlyPrefixes = []string{ // review/** verdict artefact) is the same kind of post-pack mutable artifact — written // into an already-sealed lifeboat, its integrity the per-entry cite-or-be-dropped // rule and the registered-verdict gate, not the manifest seal — so it is excluded -// here too and VerifyManifest still reproduces the pinned hash after synthesis. +// here too and verifyManifest still reproduces the pinned hash after synthesis. var ( manifestExcludedExact = []string{ProvenanceName, "graveyard/lessons.json", "principles.json", "principles.md", "press-release.json", "press-release.md"} manifestExcludedPrefixes = []string{"graveyard/low-confidence/", "review/", "audit/"} diff --git a/internal/core/lifeboat/mapping.go b/internal/core/lifeboat/mapping.go index 353ee87d8..9c94b92ed 100644 --- a/internal/core/lifeboat/mapping.go +++ b/internal/core/lifeboat/mapping.go @@ -41,8 +41,8 @@ const ( TierNative Tier = "abcd-native" ) -// Tiers lists every tier from poorest to richest. -func Tiers() []Tier { return []Tier{TierGit, TierConventions, TierNative} } +// allTiers lists every tier from poorest to richest. +func allTiers() []Tier { return []Tier{TierGit, TierConventions, TierNative} } // Status is the three-valued coverage result for one brief section. A blank is // a first-class result — it names a question a human must answer — not a @@ -234,9 +234,9 @@ const ( MarkerEnd = "" ) -// Render returns the mapping table as a Markdown table, exactly as it appears +// renderMapping returns the mapping table as a Markdown table, exactly as it appears // between the markers in the brief's 00-meta.md. -func Render() string { +func renderMapping() string { var b strings.Builder b.WriteString("| Brief section | Lifeboat path | Tier 0 git | Tier 1 conventions | Tier 2 abcd-native | Reads |\n") b.WriteString("|---|---|---|---|---|---|\n") diff --git a/internal/core/lifeboat/mapping_test.go b/internal/core/lifeboat/mapping_test.go index 44946e75f..baeada337 100644 --- a/internal/core/lifeboat/mapping_test.go +++ b/internal/core/lifeboat/mapping_test.go @@ -8,7 +8,7 @@ import ( ) // briefMetaRelPath is the brief file that calls the mapping table "the -// contract". The table rendered there must equal Render(). +// contract". The table rendered there must equal renderMapping(). const briefMetaRelPath = ".abcd/development/brief/00-meta.md" // repoRoot walks up from the test's working directory to the directory holding @@ -54,9 +54,9 @@ func TestBriefCarriesTheRenderedMappingTable(t *testing.T) { } got := strings.TrimSpace(doc[begin+len(MarkerBegin) : end]) - want := strings.TrimSpace(Render()) + want := strings.TrimSpace(renderMapping()) if got != want { - t.Errorf("%s has drifted from lifeboat.Table.\n\n--- brief has ---\n%s\n\n--- Render() wants ---\n%s", + t.Errorf("%s has drifted from lifeboat.Table.\n\n--- brief has ---\n%s\n\n--- renderMapping() wants ---\n%s", briefMetaRelPath, got, want) } } @@ -67,7 +67,7 @@ func TestBriefCarriesTheRenderedMappingTable(t *testing.T) { func TestTiersDegradeMonotonically(t *testing.T) { for _, m := range Table { prev := Status("") - for _, tier := range Tiers() { + for _, tier := range allTiers() { s := m.StatusAt(tier) if !s.Valid() { t.Errorf("%s at tier %s: %q is not a member of the status enum", m.Section, tier, s) diff --git a/internal/core/lifeboat/operand_test.go b/internal/core/lifeboat/operand_test.go index 7a4ef24c2..83ce9fb9a 100644 --- a/internal/core/lifeboat/operand_test.go +++ b/internal/core/lifeboat/operand_test.go @@ -78,7 +78,7 @@ var lifeboatOperandGates = map[string]func(dir string) error{ _, err := IngestLessons(dir, []byte(`{"schema_version":1,"lessons":[]}`)) return err }, - "manifest verification": VerifyManifest, + "manifest verification": verifyManifest, "embark (lifeboat operand)": func(dir string) error { _, err := runPlanner(dir, nestedTarget()) return err diff --git a/internal/core/lifeboat/plan.go b/internal/core/lifeboat/plan.go index 981633dbd..57f1d32d0 100644 --- a/internal/core/lifeboat/plan.go +++ b/internal/core/lifeboat/plan.go @@ -69,7 +69,7 @@ type Provenance struct { SourceRootSHA string `json:"source_root_sha,omitempty"` TiersPresent []Tier `json:"tiers_present"` ManifestSHA256 string `json:"manifest_sha256"` - // RecordManifestSHA256 is the pinned hash over ONLY the record-derived + // recordManifestSHA256 is the pinned hash over ONLY the record-derived // families (docs/adrs/**, activity/issues/**, rescue/intents/**, // rescue/specs/**, graveyard/abandoned.json) — the P1 closure seal. It is // byte-identical across pack -> embark -> re-pack of the same records, and is @@ -103,10 +103,10 @@ const passBReason = "no transcript source was read for this package, so the rati // transcriptTiers names the source tiers that carry the chat transcripts Pass B // mines. It is EMPTY, and that is the fact the exemption rests on: the probe's -// tiers are git, conventions and abcd-native (Tiers()), none of them a +// tiers are git, conventions and abcd-native (allTiers()), none of them a // transcript store, so no lifeboat this build packs was grounded by a pass that // read one. Registering a transcript adapter means adding its tier here as well -// as to Tiers() and tiersPresent — and if it is added here, the declaration +// as to allTiers() and tiersPresent — and if it is added here, the declaration // stops being written for a pack that tier grounded. var transcriptTiers = map[Tier]bool{} @@ -300,7 +300,7 @@ func Plan(repoRoot string, opts ...ProbeOption) (Lifeboat, error) { SourceRootSHA: cov.Repo.RootSHA, TiersPresent: cov.TiersPresent, ManifestSHA256: ManifestSHA256(files), - RecordManifestSHA256: RecordManifestSHA256(files), + RecordManifestSHA256: recordManifestSHA256(files), Omissions: pb.omissions, PassBExemption: passBExemption(cov.TiersPresent, transcriptTiers), } @@ -318,7 +318,7 @@ func Plan(repoRoot string, opts ...ProbeOption) (Lifeboat, error) { // concatenation of " \n" for every file the keep predicate admits, // sorted lexicographically BY PATH — not by the assembled line, whose leading hash // would otherwise dominate the ordering. It is deterministic for a given file set -// and predicate. ManifestSHA256 and RecordManifestSHA256 differ only in which +// and predicate. ManifestSHA256 and recordManifestSHA256 differ only in which // files they keep, so the two hashes cannot drift in their line construction. func manifestSHA256Over(files []PlannedFile, keep func(PlannedFile) bool) string { type entry struct { @@ -348,18 +348,18 @@ func ManifestSHA256(files []PlannedFile) string { return manifestSHA256Over(files, func(f PlannedFile) bool { return f.Path != ProvenanceName }) } -// RecordManifestSHA256 is the pinned hash over ONLY the record-derived families +// recordManifestSHA256 is the pinned hash over ONLY the record-derived families // (docs/adrs/**, activity/issues/**, rescue/intents/**, rescue/specs/**, // graveyard/abandoned.json) — the same construction as ManifestSHA256, restricted // to isRecordDerived paths. It is the closure seal (P1): byte-identical across // pack -> embark -> re-pack, because those families derive purely from the repo's // record and never from git or the operator's identity. -func RecordManifestSHA256(files []PlannedFile) string { +func recordManifestSHA256(files []PlannedFile) string { return manifestSHA256Over(files, func(f PlannedFile) bool { return isRecordDerived(f.Path) }) } // isRecordDerived reports whether a lifeboat-relative path is one of the -// record-derived families sealed by RecordManifestSHA256. The set is +// record-derived families sealed by recordManifestSHA256. The set is // recordDerivedPrefixes (embark_types.go), the single source of truth shared with // the embarker, so the pack side and the embark side cannot disagree about which // bytes must round-trip. diff --git a/internal/core/lifeboat/plan_test.go b/internal/core/lifeboat/plan_test.go index 484ed4fc7..fb0f9599e 100644 --- a/internal/core/lifeboat/plan_test.go +++ b/internal/core/lifeboat/plan_test.go @@ -256,7 +256,7 @@ func bumpRecordFile(t *testing.T, files []PlannedFile, p string) { } // TestRecordManifestSHA256CoversRecordFamiliesOnly pins the P1 closure boundary: -// RecordManifestSHA256 hashes exactly the record-derived families +// recordManifestSHA256 hashes exactly the record-derived families // (docs/adrs/**, activity/issues/**, rescue/intents/**, rescue/specs/**, // graveyard/abandoned.json) and NOTHING else. Changing any record byte moves the // hash; changing an identity/git-derived file (coverage.*, brief/**, @@ -277,9 +277,9 @@ func TestRecordManifestSHA256CoversRecordFamiliesOnly(t *testing.T) { {Path: "rescue/spine.md", Content: []byte("spine")}, {Path: ProvenanceName, Content: []byte("prov")}, } - baseHash := RecordManifestSHA256(base) + baseHash := recordManifestSHA256(base) if baseHash == "" { - t.Fatal("RecordManifestSHA256 over a record-bearing set is empty") + t.Fatal("recordManifestSHA256 over a record-bearing set is empty") } records := []string{ @@ -292,8 +292,8 @@ func TestRecordManifestSHA256CoversRecordFamiliesOnly(t *testing.T) { for _, p := range records { m := cloneRecordFiles(base) bumpRecordFile(t, m, p) - if RecordManifestSHA256(m) == baseHash { - t.Errorf("RecordManifestSHA256 did not move when record %q changed", p) + if recordManifestSHA256(m) == baseHash { + t.Errorf("recordManifestSHA256 did not move when record %q changed", p) } } @@ -308,14 +308,14 @@ func TestRecordManifestSHA256CoversRecordFamiliesOnly(t *testing.T) { for _, p := range identity { m := cloneRecordFiles(base) bumpRecordFile(t, m, p) - if RecordManifestSHA256(m) != baseHash { - t.Errorf("RecordManifestSHA256 moved when identity-derived %q changed", p) + if recordManifestSHA256(m) != baseHash { + t.Errorf("recordManifestSHA256 moved when identity-derived %q changed", p) } } } // TestPlanProvenanceRecordsRecordManifestHash checks the plan writes -// record_manifest_sha256 into _provenance.json, equal to RecordManifestSHA256 over +// record_manifest_sha256 into _provenance.json, equal to recordManifestSHA256 over // the file set; that adding the field left manifest_sha256 untouched; that // isAbcdLifeboat still parses the provenance; and that a re-plan of an unchanged // source reproduces the provenance byte-for-byte (no timestamp crept in). @@ -333,9 +333,9 @@ func TestPlanProvenanceRecordsRecordManifestHash(t *testing.T) { if prov.RecordManifestSHA256 == "" { t.Fatal("provenance carries no record_manifest_sha256") } - // _provenance.json is not record-derived, so RecordManifestSHA256(lb.Files) + // _provenance.json is not record-derived, so recordManifestSHA256(lb.Files) // equals the value Plan computed over the pre-provenance slice. - if want := RecordManifestSHA256(lb.Files); prov.RecordManifestSHA256 != want { + if want := recordManifestSHA256(lb.Files); prov.RecordManifestSHA256 != want { t.Errorf("record_manifest_sha256 = %s, recomputed = %s", prov.RecordManifestSHA256, want) } if want := ManifestSHA256(lb.Files); prov.ManifestSHA256 != want { diff --git a/internal/core/lifeboat/synthesis_manifest_test.go b/internal/core/lifeboat/synthesis_manifest_test.go index d5924eac8..4c31ec61b 100644 --- a/internal/core/lifeboat/synthesis_manifest_test.go +++ b/internal/core/lifeboat/synthesis_manifest_test.go @@ -12,7 +12,7 @@ func TestSynthesisDoesNotPerturbManifest(t *testing.T) { src := embarkableSourceFixture(t) lb := packSource(t, src) - if err := VerifyManifest(lb); err != nil { + if err := verifyManifest(lb); err != nil { t.Fatalf("freshly packed lifeboat must verify: %v", err) } before := readProvenanceFile(t, lb).ManifestSHA256 @@ -24,7 +24,7 @@ func TestSynthesisDoesNotPerturbManifest(t *testing.T) { t.Fatalf("ComposePressRelease: %v", err) } - if err := VerifyManifest(lb); err != nil { + if err := verifyManifest(lb); err != nil { t.Fatalf("manifest must still verify after synthesis writes: %v", err) } if after := readProvenanceFile(t, lb).ManifestSHA256; after != before { diff --git a/internal/core/lifeboat/synthesis_principles.go b/internal/core/lifeboat/synthesis_principles.go index 88e43ef73..4a727489b 100644 --- a/internal/core/lifeboat/synthesis_principles.go +++ b/internal/core/lifeboat/synthesis_principles.go @@ -462,7 +462,7 @@ func gateSynthLifeboat(lifeboatDir string) (string, Provenance, error) { // buildLifeboatPathSet is the packed-path membership set P: every regular file's // lifeboat-relative POSIX path, from the same sorted, symlink-refusing, bounded -// walk VerifyManifest uses. A delegated ref that names a packed path is a valid +// walk verifyManifest uses. A delegated ref that names a packed path is a valid // citation. func buildLifeboatPathSet(root *os.Root, ownOutput func(string) bool) (map[string]bool, error) { rels, err := walkLifeboatFiles(root) diff --git a/internal/core/lifeboat/synthesis_review.go b/internal/core/lifeboat/synthesis_review.go index 1f3a5b11e..7ea4e42d1 100644 --- a/internal/core/lifeboat/synthesis_review.go +++ b/internal/core/lifeboat/synthesis_review.go @@ -9,7 +9,7 @@ package lifeboat // Dual-mode single entrypoint (mirroring IngestLessons): // // - DETERMINISTIC (raw == nil): the verdict is a mechanical, pure mapping over -// VerifyManifest + the packed coverage summary — no model, no wall-clock, and +// verifyManifest + the packed coverage summary — no model, no wall-clock, and // the source repo's CONTENT is never read (it is gated as a real dir only, so // the audit stays deterministic and safe even when the source is gone). The // inputs are the lifeboat's own sealed files. @@ -106,9 +106,9 @@ func ReviewLifeboat(lifeboatDir, sourceRepo string, raw []byte) (ReviewResult, e sourceName := sanitize(filepath.Base(srcAbs)) // 2. Manifest attestation and packed coverage summary — the trusted inputs the - // core owns in BOTH modes. VerifyManifest is the seal check; a false result + // core owns in BOTH modes. verifyManifest is the seal check; a false result // is a verdict input, never fatal. - manifestVerified := VerifyManifest(abs) == nil + manifestVerified := verifyManifest(abs) == nil cov := readPackedCoverage(abs) coveragePresent := cov.Present && !cov.Degraded diff --git a/internal/core/lifeboat/synthesis_review_test.go b/internal/core/lifeboat/synthesis_review_test.go index 2a2f64321..7c24670f9 100644 --- a/internal/core/lifeboat/synthesis_review_test.go +++ b/internal/core/lifeboat/synthesis_review_test.go @@ -11,14 +11,14 @@ import ( // --------------------------------------------------------------------------- // Fixtures — a REAL-manifest packed lifeboat: _provenance.json's manifest_sha256 -// actually hashes the packed tree, so VerifyManifest passes and a tampered byte +// actually hashes the packed tree, so verifyManifest passes and a tampered byte // makes it fail (unlike the layer-3 hand fixture, which pins a placeholder hash). // marshalIndent, writeFile, stdArch, stdAband live in graveyard_lessons_test.go // (same package). // --------------------------------------------------------------------------- // sealLifeboat writes _provenance.json with a manifest_sha256 reproduced exactly -// the way VerifyManifest reproduces it (walk, exclude the header + layer-3, hash), +// the way verifyManifest reproduces it (walk, exclude the header + layer-3, hash), // so the sealed tree verifies. It writes the header LAST so it is never in its own // hash. sourceName lets a test drive the identity-drift finding. func sealLifeboat(t *testing.T, dir, sourceName string) string { @@ -168,7 +168,7 @@ func TestReviewLifeboatDeterministicNoCoverage(t *testing.T) { } // TestReviewLifeboatDeterministicMajorRethink: a flipped sealed byte fails -// VerifyManifest -> MAJOR_RETHINK + fnd-manifest, and it is a VERDICT INPUT, not a +// verifyManifest -> MAJOR_RETHINK + fnd-manifest, and it is a VERDICT INPUT, not a // fatal error (err is nil, the audit is written). func TestReviewLifeboatDeterministicMajorRethink(t *testing.T) { dir := reviewFixture(t, "abc", &Summary{Grounded: 7, Blank: 3}) @@ -623,7 +623,7 @@ func TestReviewReplacesPreRenameArtefact(t *testing.T) { if _, err := os.Stat(filepath.Join(dir, "review", "review-"+m12+".json")); err != nil { t.Fatalf("expected the review artefact: %v", err) } - // The legacy pair is on disk when VerifyManifest runs, so a SHIP verdict + // The legacy pair is on disk when verifyManifest runs, so a SHIP verdict // pins audit/ staying in manifestExcludedPrefixes. Without that entry this // migration run would falsely accuse an untampered lifeboat // (MAJOR_RETHINK), which is the one run this feature exists to serve. diff --git a/internal/core/lifeboat/synthesis_types.go b/internal/core/lifeboat/synthesis_types.go index 01da015b5..be863251a 100644 --- a/internal/core/lifeboat/synthesis_types.go +++ b/internal/core/lifeboat/synthesis_types.go @@ -201,7 +201,7 @@ type PressReleaseResult struct { // verdict, the manifest attestation, the packed coverage summary, and findings that // each cite packed lifeboat paths (cite-or-be-dropped). Coverage reuses the coverage // Summary shape. In deterministic mode the verdict is a mechanical mapping over -// VerifyManifest + the packed coverage summary; in delegated mode the model's +// verifyManifest + the packed coverage summary; in delegated mode the model's // verdict is membership-validated and its findings are cite-or-dropped. // --------------------------------------------------------------------------- @@ -222,7 +222,7 @@ type ReviewFindingDrop struct { } // ReviewArtefact is the on-disk shape of review/review-.json. PromptVersion -// is omitted in deterministic mode. ManifestVerified is the VerifyManifest outcome — +// is omitted in deterministic mode. ManifestVerified is the verifyManifest outcome — // a false value is a MAJOR_RETHINK verdict input, NOT a fatal error. type ReviewArtefact struct { SchemaVersion int `json:"schema_version"` diff --git a/internal/reachaudit/testdata/core-unreached.txt b/internal/reachaudit/testdata/core-unreached.txt index 375544294..cfe2e2640 100644 --- a/internal/reachaudit/testdata/core-unreached.txt +++ b/internal/reachaudit/testdata/core-unreached.txt @@ -80,10 +80,6 @@ internal/core/launch/scaffold.DeriveCIChecks internal/core/launch/scaffold.GateSubstitutions internal/core/launch/scaffold.Render internal/core/lifeboat.ManifestSHA256 -internal/core/lifeboat.RecordManifestSHA256 -internal/core/lifeboat.Render -internal/core/lifeboat.Tiers -internal/core/lifeboat.VerifyManifest internal/core/lint.CitationAgeSummaryAt internal/core/lint.LintAt internal/core/lint.PrincipleEvidence From 142ee51813411b7449e5378c8535622bd0399306 Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Wed, 30 Sep 2026 01:57:42 +0100 Subject: [PATCH 11/91] refactor(cite,machineload): unexport in-package-only functions cite.NewHTTPChecker (now newShippedHTTPChecker, beside the existing newHTTPChecker it wraps), cite.ParseReceipt and machineload.ParseLoadavgSysctl are reached only from inside their own packages. They leave the exported surface and the reach-audit baseline. Refs: iss-2609252211487887 Assisted-by: Claude:claude-opus-5-5 --- internal/core/cite/confirm.go | 6 +++--- internal/core/cite/confirm_test.go | 6 +++--- internal/core/cite/fetch.go | 6 +++--- internal/core/cite/fetch_test.go | 4 ++-- internal/core/cite/refresh.go | 2 +- internal/core/machineload/parse.go | 4 ++-- internal/core/machineload/parse_test.go | 6 +++--- internal/core/machineload/read_darwin.go | 2 +- internal/reachaudit/testdata/core-unreached.txt | 3 --- 9 files changed, 18 insertions(+), 21 deletions(-) diff --git a/internal/core/cite/confirm.go b/internal/core/cite/confirm.go index 33890995e..fc6d826d4 100644 --- a/internal/core/cite/confirm.go +++ b/internal/core/cite/confirm.go @@ -78,9 +78,9 @@ type ConfirmError struct{ msg string } func (e *ConfirmError) Error() string { return e.msg } -// ParseReceipt decodes a receipt file, refusing anything it does not fully +// parseReceipt decodes a receipt file, refusing anything it does not fully // understand. -func ParseReceipt(data []byte) (Receipt, error) { +func parseReceipt(data []byte) (Receipt, error) { dec := json.NewDecoder(bytes.NewReader(data)) dec.DisallowUnknownFields() var r Receipt @@ -101,7 +101,7 @@ func LoadReceipt(path string) (Receipt, error) { if err != nil { return Receipt{}, err } - return ParseReceipt(data) + return parseReceipt(data) } // Confirm records a human's confirmations in the baseline. diff --git a/internal/core/cite/confirm_test.go b/internal/core/cite/confirm_test.go index ecdc69b64..806d9593e 100644 --- a/internal/core/cite/confirm_test.go +++ b/internal/core/cite/confirm_test.go @@ -142,7 +142,7 @@ func TestParseReceiptRoundTrip(t *testing.T) { {"url": "https://b.example.org/y"} ] }`) - got, err := ParseReceipt(raw) + got, err := parseReceipt(raw) if err != nil { t.Fatalf("ParseReceipt: %v", err) } @@ -161,7 +161,7 @@ func TestParseReceiptRoundTrip(t *testing.T) { // than having the key quietly dropped. func TestParseReceiptRefusesAMethodField(t *testing.T) { raw := []byte(`{"schema_version": 1, "confirmed": [{"url": "https://a.example.org/x", "method": "logged in via the library proxy"}]}`) - if _, err := ParseReceipt(raw); err == nil { + if _, err := parseReceipt(raw); err == nil { t.Fatal("ParseReceipt accepted a method field") } } @@ -169,7 +169,7 @@ func TestParseReceiptRefusesAMethodField(t *testing.T) { // TestParseReceiptRefusesAnUnknownSchemaVersion keeps a future producer's record // from being best-effort parsed by a build that does not understand it. func TestParseReceiptRefusesAnUnknownSchemaVersion(t *testing.T) { - if _, err := ParseReceipt([]byte(`{"schema_version": 99, "confirmed": []}`)); err == nil { + if _, err := parseReceipt([]byte(`{"schema_version": 99, "confirmed": []}`)); err == nil { t.Fatal("ParseReceipt accepted an unknown schema version") } } diff --git a/internal/core/cite/fetch.go b/internal/core/cite/fetch.go index 8fdb07282..da6ac4ee9 100644 --- a/internal/core/cite/fetch.go +++ b/internal/core/cite/fetch.go @@ -117,14 +117,14 @@ type HTTPChecker struct { // they relax (loopback, for httptest) and inherit the rest unchanged. var shippedBlocked = urlguard.BlockedIP -// NewHTTPChecker returns the checker as it ships: the full SSRF policy and the +// newShippedHTTPChecker returns the checker as it ships: the full SSRF policy and the // default timeout. -func NewHTTPChecker() *HTTPChecker { return newHTTPChecker(shippedBlocked, DefaultTimeout) } +func newShippedHTTPChecker() *HTTPChecker { return newHTTPChecker(shippedBlocked, DefaultTimeout) } // newHTTPChecker builds a checker with an explicit address policy and timeout. // The policy is a parameter for exactly one reason: it lets the fetch paths be // exercised against an httptest server, which binds loopback, without ever -// relaxing what NewHTTPChecker ships. +// relaxing what newShippedHTTPChecker ships. func newHTTPChecker(blocked func(net.IP) bool, timeout time.Duration) *HTTPChecker { dialer := &net.Dialer{ Timeout: connectTimeout, diff --git a/internal/core/cite/fetch_test.go b/internal/core/cite/fetch_test.go index 008ee8eba..cd214a3c6 100644 --- a/internal/core/cite/fetch_test.go +++ b/internal/core/cite/fetch_test.go @@ -119,7 +119,7 @@ func TestCheckAnsweredSeparatesADeadLinkFromADeadNetwork(t *testing.T) { t.Errorf("%s: Answered = true, but nothing was listening", deadURL) } // An SSRF refusal never reaches a host at all. - if got := NewHTTPChecker().Check("http://169.254.169.254/x"); got.Answered { + if got := newShippedHTTPChecker().Check("http://169.254.169.254/x"); got.Answered { t.Error("a guard refusal reported that a host answered") } } @@ -212,7 +212,7 @@ func TestCheckRefusesInternalAddressesUnderTheShippedPolicy(t *testing.T) { defer srv.Close() for _, target := range []string{srv.URL, "http://169.254.169.254/latest/meta-data/", "http://svc.internal/x"} { - got := NewHTTPChecker().Check(target) + got := newShippedHTTPChecker().Check(target) if got.Status != StatusBroken { t.Errorf("%s: status = %q, want %q", target, got.Status, StatusBroken) } diff --git a/internal/core/cite/refresh.go b/internal/core/cite/refresh.go index 311f23708..7cad3ec41 100644 --- a/internal/core/cite/refresh.go +++ b/internal/core/cite/refresh.go @@ -143,7 +143,7 @@ func Refresh(req RefreshRequest) (RefreshResult, error) { checker := req.Checker if checker == nil { - checker = NewHTTPChecker() + checker = newShippedHTTPChecker() } parallel := req.Parallel if parallel <= 0 { diff --git a/internal/core/machineload/parse.go b/internal/core/machineload/parse.go index 0ebc8dcac..937b2f515 100644 --- a/internal/core/machineload/parse.go +++ b/internal/core/machineload/parse.go @@ -17,12 +17,12 @@ import ( // tag, so both platforms' parsers are exercised on every platform the tests run // on. Only read_darwin.go and read_linux.go touch the real machine. -// ParseLoadavgSysctl reads macOS's `vm.loadavg` sysctl value: the kernel's +// parseLoadavgSysctl reads macOS's `vm.loadavg` sysctl value: the kernel's // struct loadavg, three uint32 fixed-point averages, four bytes of padding, then // an int64 scale, little-endian. syscall.Sysctl returns it as a string with one // trailing NUL stripped (23 bytes where the struct is 24), so the stripped bytes // are restored as zeros before the scale is read. -func ParseLoadavgSysctl(raw []byte) (l1, l5, l15 float64, err error) { +func parseLoadavgSysctl(raw []byte) (l1, l5, l15 float64, err error) { const size = 24 if len(raw) < 20 || len(raw) > size { return 0, 0, 0, fmt.Errorf("vm.loadavg is %d bytes, not the %d-byte loadavg struct", len(raw), size) diff --git a/internal/core/machineload/parse_test.go b/internal/core/machineload/parse_test.go index dfa5a4eda..1566d02c2 100644 --- a/internal/core/machineload/parse_test.go +++ b/internal/core/machineload/parse_test.go @@ -21,7 +21,7 @@ func TestParseLoadavgSysctlBytes(t *testing.T) { stripped := full[:23] // syscall.Sysctl drops the trailing NUL for name, raw := range map[string][]byte{"stripped": stripped, "full": full} { - l1, l5, l15, err := ParseLoadavgSysctl(raw) + l1, l5, l15, err := parseLoadavgSysctl(raw) if err != nil { t.Fatalf("%s: %v", name, err) } @@ -30,11 +30,11 @@ func TestParseLoadavgSysctlBytes(t *testing.T) { t.Fatalf("%s: loads = %v %v %v, want 13.79 18.40 18.80", name, l1, l5, l15) } } - if _, _, _, err := ParseLoadavgSysctl(full[:12]); err == nil { + if _, _, _, err := parseLoadavgSysctl(full[:12]); err == nil { t.Fatal("a 12-byte value parsed as a loadavg struct") } zeroScale := make([]byte, 24) - if _, _, _, err := ParseLoadavgSysctl(zeroScale); err == nil { + if _, _, _, err := parseLoadavgSysctl(zeroScale); err == nil { t.Fatal("a zero scale parsed") } } diff --git a/internal/core/machineload/read_darwin.go b/internal/core/machineload/read_darwin.go index 72379e60c..031ffe3e6 100644 --- a/internal/core/machineload/read_darwin.go +++ b/internal/core/machineload/read_darwin.go @@ -31,7 +31,7 @@ func Read() (Snapshot, error) { if err != nil { return snap, fmt.Errorf("could not read the load average (sysctl vm.loadavg: %w)", err) } - if snap.Load1, snap.Load5, snap.Load15, err = ParseLoadavgSysctl([]byte(raw)); err != nil { + if snap.Load1, snap.Load5, snap.Load15, err = parseLoadavgSysctl([]byte(raw)); err != nil { return snap, fmt.Errorf("could not read the load average (%w)", err) } snap.HasLoad = true diff --git a/internal/reachaudit/testdata/core-unreached.txt b/internal/reachaudit/testdata/core-unreached.txt index cfe2e2640..50178bb93 100644 --- a/internal/reachaudit/testdata/core-unreached.txt +++ b/internal/reachaudit/testdata/core-unreached.txt @@ -20,8 +20,6 @@ internal/core/capture.ConstrualFingerprint internal/core/capture.GlossaryFingerprint internal/core/capture.ScopeFingerprint internal/core/changelog.DeriveNext -internal/core/cite.NewHTTPChecker -internal/core/cite.ParseReceipt internal/core/credential.KeychainItem internal/core/credential.Machine internal/core/credential.Set @@ -84,7 +82,6 @@ internal/core/lint.CitationAgeSummaryAt internal/core/lint.LintAt internal/core/lint.PrincipleEvidence internal/core/lint.UnresolvedProseCitationsInText -internal/core/machineload.ParseLoadavgSysctl internal/core/mdrecord.IsHeading internal/core/memory.BuildCitation internal/core/memory.CountSourceTokens From dfa9e987fd8cda7a026f8ba777df2e87f5705fbf Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Wed, 30 Sep 2026 01:58:36 +0100 Subject: [PATCH 12/91] refactor(memory): unexport eleven in-package-only functions BuildCitation, CountSourceTokens, CoverageIndexPath, DetectLicence, NormaliseSourceText, QueryPages, RenderCitedMatches, RenderNoMatches, ResolveDistilledPages, SourceContentHash and ValidateDistilledPage are reached only from inside the memory package, and none is named by writer.go. They leave the exported surface and the reach-audit baseline. The ten names writer.go calls (Dir, IsMemoryPageName, LoadRegistry, ParsePageFilename, RenderContradictions, RenderIndex, SerializeRegistry, SourceClasses, SourceHashes, SourcesIndexPath), MergeIngest (writer.go names it in a comment) and writer.go's own two stay baselined: that file is reserved to another lane. Refs: iss-2609252211487887 Assisted-by: Claude:claude-opus-5-5 --- internal/core/memory/ancestor_symlink_test.go | 4 +-- internal/core/memory/ask.go | 32 +++++++++---------- .../ask_empty_question_termsafe_test.go | 6 ++-- internal/core/memory/ask_termsafe_test.go | 6 ++-- internal/core/memory/codespan_test.go | 2 +- internal/core/memory/ingest.go | 14 ++++---- internal/core/memory/ingest_boundary_test.go | 6 ++-- internal/core/memory/lint.go | 2 +- internal/core/memory/memory_test.go | 4 +-- internal/core/memory/provenance.go | 28 ++++++++-------- internal/core/memory/querypages_bench_test.go | 4 +-- internal/core/memory/schema.go | 12 +++---- .../core/memory/schema_glued_token_test.go | 2 +- .../core/memory/schema_refusal_echo_test.go | 2 +- .../single_source_required_fields_test.go | 6 ++-- internal/core/memory/store_handle_test.go | 2 +- .../reachaudit/testdata/core-unreached.txt | 11 ------- 17 files changed, 66 insertions(+), 77 deletions(-) diff --git a/internal/core/memory/ancestor_symlink_test.go b/internal/core/memory/ancestor_symlink_test.go index 0189dc7fa..d7effa543 100644 --- a/internal/core/memory/ancestor_symlink_test.go +++ b/internal/core/memory/ancestor_symlink_test.go @@ -54,8 +54,8 @@ func TestMemoryStoreDirSymlinkRefused(t *testing.T) { t.Errorf("Lint wrote %s INTO the symlink target — a write escaped the repo", coverageInTarget) } - // (b) A read (QueryPages / Ask) must not disclose the out-of-repo page. - matches, err := QueryPages(repoRoot, "secret leak", 5) + // (b) A read (queryPages / Ask) must not disclose the out-of-repo page. + matches, err := queryPages(repoRoot, "secret leak", 5) if err == nil { t.Error("QueryPages followed a symlinked .abcd/memory store; the directory symlink must be refused") } diff --git a/internal/core/memory/ask.go b/internal/core/memory/ask.go index de6701840..1d8f79304 100644 --- a/internal/core/memory/ask.go +++ b/internal/core/memory/ask.go @@ -13,8 +13,8 @@ import ( "github.com/intentdriven/abcd/internal/termsafe" ) -// ask.go — deterministic native recall (fn-38 .6). Retrieval (QueryPages) is -// read-only token-overlap ranking; synthesis defaults to RenderCitedMatches (no +// ask.go — deterministic native recall (fn-38 .6). Retrieval (queryPages) is +// read-only token-overlap ranking; synthesis defaults to renderCitedMatches (no // LLM). The optional file-back (default OFF) routes through the SAME dedup + // WritePages seams as ingest. @@ -46,7 +46,7 @@ type MatchedPage struct { Citations []AskCitation `json:"citations"` } -// Synthesizer turns matches into answer prose; nil uses RenderCitedMatches. +// Synthesizer turns matches into answer prose; nil uses renderCitedMatches. type Synthesizer func(question string, matches []MatchedPage) string // FileBackDecision is consulted after validation and before any write; false @@ -92,7 +92,7 @@ func Ask(req AskRequest) (AskResult, error) { if topN == 0 { topN = AskTopN } - matches, err := QueryPages(root, req.Question, topN) + matches, err := queryPages(root, req.Question, topN) if err != nil { return AskResult{}, err } @@ -100,7 +100,7 @@ func Ask(req AskRequest) (AskResult, error) { // every render and carried in AskResult.Question, the --json field. It is // sanitised ONCE here and that one value feeds both return paths, so the // empty-store branch cannot drift from the matched branch again: the - // no-matches render used to take the raw question while RenderCitedMatches + // no-matches render used to take the raw question while renderCitedMatches // sanitised its own copy, and a raw ESC/C1/bidi rune reached stdout from // exactly the branch a first-time user hits (GHSA-4fmm-95pf-32c6). // Retrieval above still tokenises the raw question — masking runes to '?' @@ -110,11 +110,11 @@ func Ask(req AskRequest) (AskResult, error) { if req.FileBackPage != nil { return AskResult{}, newAskError("no matching memory pages — a file-back without cited matches would write an unattributable page; nothing was written") } - return AskResult{Question: question, Matches: nil, Answer: RenderNoMatches(question)}, nil + return AskResult{Question: question, Matches: nil, Answer: renderNoMatches(question)}, nil } synth := req.Synthesizer if synth == nil { - synth = RenderCitedMatches + synth = renderCitedMatches } answer := synth(question, matches) if strings.TrimSpace(answer) == "" { @@ -190,10 +190,10 @@ func citationsFromSource(source map[string]any) []AskCitation { return []AskCitation{one(source)} } -// QueryPages is the read-only deterministic retrieval: tokenise the question, +// queryPages is the read-only deterministic retrieval: tokenise the question, // score by token overlap against each page's index-line facts, apply optional // class:/domain: filters, rank by overlap (filename tie-break), take top-N. -func QueryPages(repoRoot, question string, topN int) ([]MatchedPage, error) { +func queryPages(repoRoot, question string, topN int) ([]MatchedPage, error) { tokens, classFilter, domainFilter := parseQuestion(question) tokenSet := map[string]bool{} for _, t := range tokens { @@ -297,9 +297,9 @@ func cleanCitationJSON(raw string) string { return termsafe.CleanProse(raw, maxPageValueBytes-len(citationTruncatedMarker)) + citationTruncatedMarker } -// RenderCitedMatches is the default deterministic synthesizer — a +// renderCitedMatches is the default deterministic synthesizer — a // citation-renderer, not an LLM. Missing provenance renders as explicit (none). -func RenderCitedMatches(question string, matches []MatchedPage) string { +func renderCitedMatches(question string, matches []MatchedPage) string { lines := []string{ // Every untrusted field on the answer's markdown lines goes through // CleanProse, not Sanitize alone, which leaves an HTML opener and link @@ -347,10 +347,10 @@ func RenderCitedMatches(question string, matches []MatchedPage) string { return strings.Join(lines, "\n") + "\n" } -// RenderNoMatches is the explicit empty-result render. It cleans the question -// itself, as RenderCitedMatches does, so a direct caller is covered and the two +// renderNoMatches is the explicit empty-result render. It cleans the question +// itself, as renderCitedMatches does, so a direct caller is covered and the two // renders cannot disagree on what reaches the terminal. -func RenderNoMatches(question string) string { +func renderNoMatches(question string) string { return "# " + AskReportHeading + " — " + cleanPageField(question) + "\n\n" + "No matching memory pages (token overlap found nothing; an empty or absent store matches nothing).\n" + "Try different terms, an explicit class: / domain: filter, or ingest a source first.\n" @@ -407,7 +407,7 @@ func fileBack(root string, matches []MatchedPage, rawPage map[string]any, decide merged["source"] = src rawPage = merged } - page, err := ValidateDistilledPage(root, rawPage) + page, err := validateDistilledPage(root, rawPage) if err != nil { return FileBackResult{}, err } @@ -429,7 +429,7 @@ func fileBack(root string, matches []MatchedPage, rawPage map[string]any, decide if err != nil { return FileBackResult{}, err } - plan, err := ResolveDistilledPages(existing, []DistilledPage{page}) + plan, err := resolveDistilledPages(existing, []DistilledPage{page}) if err != nil { return FileBackResult{}, err } diff --git a/internal/core/memory/ask_empty_question_termsafe_test.go b/internal/core/memory/ask_empty_question_termsafe_test.go index ed08982e8..62ecc4f61 100644 --- a/internal/core/memory/ask_empty_question_termsafe_test.go +++ b/internal/core/memory/ask_empty_question_termsafe_test.go @@ -7,7 +7,7 @@ import ( // TestAskEmptyStoreSanitisesQuestion is the GHSA-4fmm-95pf-32c6 detector. On // an empty store Ask took the no-matches branch and handed the raw argv -// question to RenderNoMatches, while RenderCitedMatches on the matched branch +// question to renderNoMatches, while renderCitedMatches on the matched branch // sanitised it; both branches also put the raw question into AskResult.Question, // the --json field. An ESC, a C1 control, a bidi override or a zero-width rune // in the question therefore reached the terminal raw from exactly the branch a @@ -48,9 +48,9 @@ func TestAskEmptyStoreSanitisesQuestion(t *testing.T) { // TestRenderNoMatchesSanitisesDirectly pins the exported render on its own, so // a caller that reaches it without going through Ask is covered the way -// RenderCitedMatches already is. +// renderCitedMatches already is. func TestRenderNoMatchesSanitisesDirectly(t *testing.T) { - out := RenderNoMatches("q" + string(rune(0x1b)) + string(rune(0x202e))) + out := renderNoMatches("q" + string(rune(0x1b)) + string(rune(0x202e))) if strings.ContainsRune(out, 0x1b) || strings.ContainsRune(out, 0x202e) { t.Fatalf("RenderNoMatches echoes attack runes raw: %q", out) } diff --git a/internal/core/memory/ask_termsafe_test.go b/internal/core/memory/ask_termsafe_test.go index 31b6d1e39..1d01f0a62 100644 --- a/internal/core/memory/ask_termsafe_test.go +++ b/internal/core/memory/ask_termsafe_test.go @@ -6,7 +6,7 @@ import ( ) // TestRenderCitedMatchesSanitizesCitationFields is the gh-250 detector: the -// per-citation fields printed by RenderCitedMatches are page-derived content +// per-citation fields printed by renderCitedMatches are page-derived content // from the same untrusted ingest boundary as Summary/Filename (which ARE // sanitised), so a citation carrying an ESC/C1/bidi/zero-width rune must reach // the terminal defanged, not raw. encoding/json escapes only C0 and a couple of @@ -40,7 +40,7 @@ func TestRenderCitedMatchesSanitizesCitationFields(t *testing.T) { }}, }} - out := RenderCitedMatches("what tokens", matches) + out := renderCitedMatches("what tokens", matches) for name, r := range attacks { if strings.ContainsRune(out, r) { @@ -62,7 +62,7 @@ func TestRenderCitedMatchesSanitizesCitationFields(t *testing.T) { func TestRenderCitedMatchesMarksATruncatedCitation(t *testing.T) { render := func(title string) string { t.Helper() - out := RenderCitedMatches("what tokens", []MatchedPage{{ + out := renderCitedMatches("what tokens", []MatchedPage{{ Filename: "topic_auth_tokens.md", Score: 1, Summary: "summary", diff --git a/internal/core/memory/codespan_test.go b/internal/core/memory/codespan_test.go index 20f2a5be7..364a3e08e 100644 --- a/internal/core/memory/codespan_test.go +++ b/internal/core/memory/codespan_test.go @@ -174,7 +174,7 @@ func TestCodeSpanRoundTripsEveryShape(t *testing.T) { // link syntax live), and the filename's code span is CodeSpan's. func TestRenderCitedMatchesCleansEveryFieldAndOwnsNoDelimiter(t *testing.T) { const payload = "