@aperant/framework 0.8.7 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/agents/apt-pr-review-fixer.md +9 -4
  3. package/agents/apt-verifier.md +6 -3
  4. package/dist/cli/commands/init.d.mts.map +1 -1
  5. package/dist/cli/commands/init.mjs +157 -23
  6. package/dist/cli/commands/init.mjs.map +1 -1
  7. package/dist/cli/commands/toolchain-detect.d.mts.map +1 -1
  8. package/dist/cli/commands/toolchain-detect.mjs +44 -0
  9. package/dist/cli/commands/toolchain-detect.mjs.map +1 -1
  10. package/dist/cli/commands/triage.d.mts.map +1 -1
  11. package/dist/cli/commands/triage.mjs +346 -13
  12. package/dist/cli/commands/triage.mjs.map +1 -1
  13. package/dist/cli/commands/uninstall.d.mts.map +1 -1
  14. package/dist/cli/commands/uninstall.mjs +11 -8
  15. package/dist/cli/commands/uninstall.mjs.map +1 -1
  16. package/dist/cli/consistency/parse-review.mjs +5 -3
  17. package/dist/cli/consistency/parse-review.mjs.map +1 -1
  18. package/dist/cli/consistency/rules/r5-verdict-consistency.d.mts.map +1 -1
  19. package/dist/cli/consistency/rules/r5-verdict-consistency.mjs +10 -8
  20. package/dist/cli/consistency/rules/r5-verdict-consistency.mjs.map +1 -1
  21. package/dist/cli/gate/gates/review-clean.d.mts.map +1 -1
  22. package/dist/cli/gate/gates/review-clean.mjs +4 -2
  23. package/dist/cli/gate/gates/review-clean.mjs.map +1 -1
  24. package/dist/cli/install/mcp-provision.d.mts +26 -0
  25. package/dist/cli/install/mcp-provision.d.mts.map +1 -0
  26. package/dist/cli/install/mcp-provision.mjs +281 -0
  27. package/dist/cli/install/mcp-provision.mjs.map +1 -0
  28. package/dist/cli/install/mcp-server-specs.d.mts +80 -0
  29. package/dist/cli/install/mcp-server-specs.d.mts.map +1 -0
  30. package/dist/cli/install/mcp-server-specs.mjs +102 -0
  31. package/dist/cli/install/mcp-server-specs.mjs.map +1 -0
  32. package/dist/cli/install/runtime-detect.d.mts +27 -0
  33. package/dist/cli/install/runtime-detect.d.mts.map +1 -1
  34. package/dist/cli/install/runtime-detect.mjs +32 -0
  35. package/dist/cli/install/runtime-detect.mjs.map +1 -1
  36. package/dist/cli/install/toml-merge.d.mts +40 -0
  37. package/dist/cli/install/toml-merge.d.mts.map +1 -0
  38. package/dist/cli/install/toml-merge.mjs +134 -0
  39. package/dist/cli/install/toml-merge.mjs.map +1 -0
  40. package/dist/cli/util/aperant-section.d.mts +12 -0
  41. package/dist/cli/util/aperant-section.d.mts.map +1 -1
  42. package/dist/cli/util/aperant-section.mjs +12 -0
  43. package/dist/cli/util/aperant-section.mjs.map +1 -1
  44. package/dist/cli/util/copy.d.mts +27 -0
  45. package/dist/cli/util/copy.d.mts.map +1 -1
  46. package/dist/cli/util/copy.mjs +49 -5
  47. package/dist/cli/util/copy.mjs.map +1 -1
  48. package/dist/cli/util/events-path.d.mts +8 -0
  49. package/dist/cli/util/events-path.d.mts.map +1 -0
  50. package/dist/cli/util/events-path.mjs +23 -0
  51. package/dist/cli/util/events-path.mjs.map +1 -0
  52. package/dist/cli/util/runtime-capabilities.d.mts +30 -0
  53. package/dist/cli/util/runtime-capabilities.d.mts.map +1 -0
  54. package/dist/cli/util/runtime-capabilities.mjs +214 -0
  55. package/dist/cli/util/runtime-capabilities.mjs.map +1 -0
  56. package/dist/cli/verify-proof/audit.d.mts +2 -7
  57. package/dist/cli/verify-proof/audit.d.mts.map +1 -1
  58. package/dist/cli/verify-proof/audit.mjs +11 -17
  59. package/dist/cli/verify-proof/audit.mjs.map +1 -1
  60. package/dist/cli/verify-proof/idl/index.d.mts +12 -1
  61. package/dist/cli/verify-proof/idl/index.d.mts.map +1 -1
  62. package/dist/cli/verify-proof/idl/index.mjs +50 -1
  63. package/dist/cli/verify-proof/idl/index.mjs.map +1 -1
  64. package/dist/cli/verify-proof/resolver.d.mts.map +1 -1
  65. package/dist/cli/verify-proof/resolver.mjs +51 -1
  66. package/dist/cli/verify-proof/resolver.mjs.map +1 -1
  67. package/dist/cli/verify-proof/runtime-detect.d.mts +2 -29
  68. package/dist/cli/verify-proof/runtime-detect.d.mts.map +1 -1
  69. package/dist/cli/verify-proof/runtime-detect.mjs +8 -203
  70. package/dist/cli/verify-proof/runtime-detect.mjs.map +1 -1
  71. package/dist/plugin/.claude-plugin/plugin.json +1 -1
  72. package/dist/plugin/agents/apt-pr-review-fixer.md +9 -4
  73. package/dist/plugin/agents/apt-verifier.md +6 -3
  74. package/dist/plugin/skills/apt-pr-review/SKILL.md +106 -15
  75. package/dist/plugin/skills/apt-setup/SKILL.md +7 -6
  76. package/dist/plugin/skills/apt-triage/SKILL.md +8 -6
  77. package/dist/plugin/skills/apt-update/SKILL.md +1 -1
  78. package/dist/plugin/skills/apt-verify/SKILL.md +1 -1
  79. package/dist/plugin/skills/apt-verify-proof/SKILL.md +25 -1
  80. package/drivers/browser/skill/agent-browser/SKILL.md +51 -0
  81. package/drivers/tauri/README.md +107 -0
  82. package/drivers/tauri/driver.mjs +88 -0
  83. package/drivers/tauri/manifest.json +37 -0
  84. package/drivers/tauri/scripts/tauri-mcp.sh +41 -0
  85. package/package.json +1 -1
  86. package/skills/apt-pr-review/SKILL.md +106 -15
  87. package/skills/apt-setup/SKILL.md +7 -6
  88. package/skills/apt-triage/SKILL.md +8 -6
  89. package/skills/apt-update/SKILL.md +1 -1
  90. package/skills/apt-verify/SKILL.md +1 -1
  91. package/skills/apt-verify-proof/SKILL.md +25 -1
  92. package/src/cli/commands/init.mjs +163 -19
  93. package/src/cli/commands/toolchain-detect.mjs +44 -0
  94. package/src/cli/commands/triage.mjs +352 -11
  95. package/src/cli/commands/uninstall.mjs +11 -8
  96. package/src/cli/consistency/parse-review.mjs +5 -3
  97. package/src/cli/consistency/rules/r5-verdict-consistency.mjs +14 -9
  98. package/src/cli/gate/gates/review-clean.mjs +4 -2
  99. package/src/cli/install/mcp-provision.mjs +293 -0
  100. package/src/cli/install/mcp-server-specs.mjs +101 -0
  101. package/src/cli/install/runtime-detect.mjs +36 -0
  102. package/src/cli/install/toml-merge.mjs +139 -0
  103. package/src/cli/util/aperant-section.mjs +14 -0
  104. package/src/cli/util/copy.mjs +53 -8
  105. package/src/cli/util/events-path.mjs +24 -0
  106. package/src/cli/util/runtime-capabilities.mjs +216 -0
  107. package/src/cli/verify-proof/audit.mjs +11 -17
  108. package/src/cli/verify-proof/idl/index.mjs +49 -11
  109. package/src/cli/verify-proof/resolver.mjs +49 -2
  110. package/src/cli/verify-proof/runtime-detect.mjs +11 -204
  111. package/templates/config.json +2 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,148 @@
3
3
  All notable changes to `@aperant/framework` are documented here. See
4
4
  [`VERSIONING.md`](VERSIONING.md) for the per-tier contract.
5
5
 
6
+ ## Unreleased
7
+
8
+ ## 0.10.0 — 2026-05-26 — Tauri driver + MCP auto-provision
9
+
10
+ Adds a bundled Tauri verify-proof driver and install-time auto-provisioning
11
+ of the MCP transport each driver needs, so `/apt:verify-proof` works out of
12
+ the box for Tauri (and web) projects with no manual MCP setup.
13
+
14
+ ### Added
15
+
16
+ - **Bundled `tauri` verify-proof driver** (`drivers/tauri/`). Same 10-verb
17
+ IDL adapter-shim surface as the electron driver, `transport.mcp_server_id
18
+ "tauri"`, `priority 85` (below electron's 90 — Tauri is rarer), `ga`. The
19
+ verb→tool mapping (the `tauri-plugin-mcp-server` tool surface) lives ONLY
20
+ in the driver + its README (ID-01) — zero workflow-prose edits; a grep
21
+ guard pins the no-`mcp__tauri__*`-in-workflow invariant. The resolver
22
+ already understood `is_tauri`, so it selects the new driver with zero
23
+ resolver edits. The app-side `tauri-plugin-mcp` Rust plugin + `tauri dev
24
+ --features mcp-debug` is a documented PREREQUISITE, never installed by the
25
+ framework.
26
+ - **Install-time MCP/skill auto-provisioning** (`apt-tools init`, and
27
+ `/apt:update` via the same path). Detects the project runtime and, per
28
+ SELECTED host CLI, writes the matching transport TOFU/non-clobbering:
29
+ - claude → JSON-merge the MCP server into project-root `.mcp.json`
30
+ (`mcpServers.<id>`).
31
+ - codex → upsert a `[mcp_servers.<id>]` table into project-root
32
+ `.codex/config.toml` (dependency-free minimal TOML writer).
33
+ - electron / tauri projects get the matching MCP server (tauri via a
34
+ generated `scripts/tauri-mcp.sh` launcher, since its npm bin ships
35
+ without a shebang); web frontends get the browser driver's declared
36
+ `agent-browser` skill copied into each host skill dir
37
+ (`.claude/skills/`, `.agents/skills/`) — NOT a puppeteer MCP server.
38
+ A pre-existing server id, config table, skill file, or launcher is never
39
+ overwritten (protects a hand-authored `SCREENSHOT_ENCRYPTION_KEY` and a
40
+ user-edited skill). Each action is audited to
41
+ `.aperant/events/{date}.jsonl`. Opt out with `verification.provision_mcp:
42
+ false` (default `true`). v1 host writers: claude + codex.
43
+
44
+ ### Out of scope (documented)
45
+
46
+ - The Rust/Tauri app-side plugin (`tauri-plugin-mcp`) — a project prereq.
47
+ - Puppeteer MCP server provisioning — web projects get the `agent-browser`
48
+ skill instead; the browser driver manifest's `mcp_server_id` is untouched.
49
+ - The `agent-browser` CLI binary, and generating/persisting
50
+ `SCREENSHOT_ENCRYPTION_KEY` — both documented prereqs, not auto-provisioned.
51
+
52
+ ## 0.9.1 — 2026-05-25 — G42 Phase 1: close deferred ship/merge bypass
53
+
54
+ **Breaking (behavior):** Blocking review findings tagged `deferred` no longer
55
+ silently clear the ship gate. Only `status: "fixed"` clears a `critical` or
56
+ `major` finding. The `--force --reason "<why>"` override (audit-logged to
57
+ `.aperant/events/`) remains the sole intentional bypass.
58
+
59
+ ### Fixed
60
+
61
+ - **`review-clean` gate now blocks `critical`/`major` findings tagged `deferred`.** Previously,
62
+ tagging a blocking finding `deferred` (any variant: `deferred`, `deferred-per-spec`,
63
+ `deferred-followup`, `deferred-out-of-scope`) silently cleared the ship gate, allowing a
64
+ critical safety defect to ship as ✓MERGED. The gate now only accepts `status: "fixed"` as a
65
+ clearing condition for blocking severities. A deliberate override is still possible via
66
+ `--force --reason "<why>"` (audit-logged). (G42 Phase 1)
67
+ - **`r5-verdict-consistency` rule no longer exempts `approved-with-notes` verdict from blocking
68
+ finding check.** `approved-with-notes` was previously an explicit escape hatch that passed R5
69
+ regardless of unresolved criticals. It now fails R5 when any unresolved `critical` or `major`
70
+ finding exists. The check also extends to `major` severity (previously only `critical` was
71
+ checked). (G42 Phase 1)
72
+ - **`apt-verify` completeness carve-out adds severity floor.** The verifier no longer exempts a
73
+ `critical` or `major` deferred behavior from Completeness scoring — such findings are now
74
+ treated as a Completeness gap. (G42 Phase 1)
75
+ - **`apt-verifier` agent instruction updated.** The verifier agent doc no longer instructs agents
76
+ to use `approved-with-notes` as an escape hatch for unresolved critical/major review findings;
77
+ the sole bypass is the audited `--force --reason` gate override. (G42 Phase 1)
78
+ - **`init` stamps the APT block into every selected runtime's instruction
79
+ file.** Previously init picked exactly ONE instruction file (CLAUDE.md if
80
+ present, else AGENTS.md) regardless of which runtimes were selected, so a
81
+ `--claude --codex` user got the skill catalog in CLAUDE.md only and native
82
+ Codex (which reads AGENTS.md) never saw it; `--gemini` users never got a
83
+ GEMINI.md block at all. `RuntimeDescriptor` now carries a nullable
84
+ `nativeInstructionFile` (claude → CLAUDE.md, codex/opencode → AGENTS.md,
85
+ gemini → GEMINI.md; the other 11 runtimes install to their own surface and
86
+ map to `null`). init resolves the deduplicated target set from the selected
87
+ runtimes, runs an all-target foreign-row preflight (any offending target →
88
+ exit 2, NOTHING written), then writes every target. The result envelope gains
89
+ a plural `instructionFiles[]` array; the singular `instructionFile` stays as a
90
+ backwards-compat alias = first entry. The single-runtime fallback is
91
+ preserved: `--claude` against a project with a pre-existing AGENTS.md (and no
92
+ CLAUDE.md) still writes AGENTS.md.
93
+ - **`uninstall` strips the APT block from GEMINI.md too.** `INSTRUCTION_FILES`
94
+ expanded to `['CLAUDE.md', 'AGENTS.md', 'GEMINI.md']` so a `--gemini` install
95
+ no longer leaves a stale managed block behind.
96
+ - **The chat-agent instruction loader dedupes identical APT blocks.**
97
+ `loadInstructionFiles` (packages/core) now loads a given verbatim
98
+ `<!-- APT:framework-* -->` block once even when it appears in multiple walk-up
99
+ files (the post-fix normal state for multi-runtime users) — preventing the
100
+ skill catalog from rendering twice in the system prompt. Narrow: only
101
+ identical managed blocks are deduped; non-managed content and distinct blocks
102
+ are preserved.
103
+
104
+ ## 0.9.0 — 2026-05-25 — github-issues triage backend
105
+
106
+ Replaces the v1 `github-issues` triage stub with a working `gh`-CLI adapter,
107
+ so projects on `task_tracking.backend: "github-issues"` get real GitHub
108
+ issues synced to triage state.
109
+
110
+ ### Added
111
+
112
+ - **`github-issues` triage backend is now implemented.** The first triage
113
+ mirror CREATES a GitHub issue via `gh issue create` (`triage: <task-id>`,
114
+ labelled `<category>` + `<vocab>:<state>`); subsequent transitions UPDATE
115
+ the SAME issue (`gh issue edit` label swap); a transition to `wontfix`
116
+ CLOSES it as "not planned" (`gh issue close --reason "not planned"`). The
117
+ issue pointer (`github.{issue_number, issue_url, repo}`) persists on
118
+ `triage.json`, so transitions are idempotent — no duplicate issues. Honors
119
+ `task_tracking.tracker_label_vocabulary` (absent/`"default"` →
120
+ `triage:<state>`; otherwise `<vocab>:<state>`). Transport is the `gh` CLI
121
+ via `execFileSync` arg-arrays (no shell-string interpolation), inheriting
122
+ the user's existing `gh auth` — zero new credential plumbing. When `gh` is
123
+ missing / unauthenticated / there is no resolvable git remote, the adapter
124
+ returns a structured error (`reason: gh-unavailable | gh-unauthenticated |
125
+ no-remote | gh-command-failed`) and exits non-zero — never a crash or
126
+ stacktrace. Label operations are best-effort: an unknown label degrades to
127
+ a `warnings[]` entry while the open/closed state change still lands. A
128
+ deterministic `APT_TRIAGE_GH_FIXTURE` test hook (mirroring `pr-review.mjs`'s
129
+ `APT_GH_FIXTURE_JSON`) keeps the unit tests hermetic — no real network.
130
+ The `app-inbox` backend remains a not-implemented stub (out of scope,
131
+ post-app-launch).
132
+
133
+ ### Changed
134
+
135
+ - **`## User Outcomes` coverage gate is now a HARD BLOCK (cutover from the
136
+ 0.8.x warn-only grace window).** As telegraphed in the 0.8.6 release notes
137
+ ("v0.9.0 will **HARD BLOCK** specs that [lack `## User Outcomes`]"), the
138
+ `coverage-check` gate flips from `warn` to `block` for STANDARD/DEEP plans
139
+ whose spec is missing the `## User Outcomes` section, starting this 0.9.0
140
+ release. The version-gated logic already lived in
141
+ `userOutcomesGateResult` (warn in 0.8.x, block in 0.9.0+); bumping the
142
+ kernel to 0.9.0 activates it. Refactor-only specs use the
143
+ `_No user-observable changes — pure refactor._` empty-with-note escape to
144
+ pass. QUICK-track tasks remain exempt (Fast Path Guarantee). The
145
+ `migration_deadline` advisory note (only emitted during the warn window) is
146
+ no longer surfaced — the deadline has passed.
147
+
6
148
  ## 0.8.7 — 2026-05-24 — Codex parity: drift false-positive fix + agent inlining
7
149
 
8
150
  Brings the Codex runtime to parity with Claude Code on two fronts.
@@ -71,9 +71,14 @@ The only valid reasons to skip a finding:
71
71
 
72
72
  ## Coordination Protocol
73
73
 
74
- When `[PARALLEL_FIXERS]` is true, multiple fix agents work in parallel on different file groups.
75
-
76
- 1. **Only edit files in your `[FIX_ASSIGNMENT]`** do NOT touch files assigned to other fixers
74
+ **Fixers run SEQUENTIALLY, never concurrently (FRAMEWORK-BUG-006).** At most one fixer touches the
75
+ shared worktree at any moment — the orchestrator spawns fixer-1, waits for it to return and audits it,
76
+ THEN spawns fixer-2, and so on. You are the ONLY fixer touching this worktree right now. `[PARALLEL_FIXERS]`
77
+ is always `false`; do NOT assume a sibling fixer is editing, staging, or committing at the same time.
78
+ Concurrent `git add`/`git commit` against a shared index either sweeps another fixer's staged files
79
+ into your commit or clobbers their working-tree edits — which is exactly the race this rule prevents.
80
+
81
+ 1. **Only edit files in your `[FIX_ASSIGNMENT]`** — do NOT touch files outside your assigned group (the grouping keeps fixes scoped and the history clean)
77
82
  2. **If a finding spans files across groups** — fix only the parts in your group, note the cross-group dependency in your report
78
83
  3. **If you discover NEW issues** while fixing — note them in your report but do NOT fix them (they aren't validated)
79
84
 
@@ -89,7 +94,7 @@ Findings with severity `critical`:
89
94
 
90
95
  ## Report Format
91
96
 
92
- Write to `[REVIEW_DIR]/iterations/[N]/fixes-applied-[FIXER_NAME].md` (BUG-030 canonical naming — the audit gate resolves `fixes-applied-${FIXER_NAME}.md` ahead of the legacy `fixes-applied.md`, so parallel fixers no longer collide on a shared report path):
97
+ Write to `[REVIEW_DIR]/iterations/[N]/fixes-applied-[FIXER_NAME].md` (BUG-030 canonical naming — the audit gate resolves `fixes-applied-${FIXER_NAME}.md` ahead of the legacy `fixes-applied.md`. Each fixer owns its own report file; this per-fixer path stays correct now that fixers run sequentially — FRAMEWORK-BUG-006):
93
98
 
94
99
  ```markdown
95
100
  # Fix Report — Iteration [N]
@@ -105,9 +105,12 @@ Either shape is accepted:
105
105
 
106
106
  Gate G8 (`consistency-check`) enforces this on ship via rules R4
107
107
  (finding-propagation) and R5 (verdict-consistency):
108
- - `verdict: "approved"` is incompatible with any unresolved `critical`
109
- review finding use `approved-with-notes` as the escape hatch when you
110
- intentionally ship over a flagged finding, and cite it in `findings.notes`.
108
+ - `verdict: "approved"` or `verdict: "approved-with-notes"` is incompatible
109
+ with any unresolved `critical` or `major` review finding (G42 Phase 1).
110
+ Neither verdict escapes R5 for blocking-severity findings. The sole bypass
111
+ is `--force --reason "<why>"` on the gate CLI (audit-logged). For
112
+ non-blocking findings (suggestion/note/info/nitpick), cite them in
113
+ `findings.notes` and use `approved-with-notes`.
111
114
  - `verdict: "rejected"` requires at least one cited finding (rejected
112
115
  without evidence is treated as drift).
113
116
 
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.mts","sourceRoot":"","sources":["../../../src/cli/commands/init.mjs"],"names":[],"mappings":"AA4EA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wCAHW,MAAM,EAAE,GACN;IAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;IAAC,iBAAiB,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,GAAC,IAAI,CAAC;IAAC,WAAW,EAAE,OAAO,CAAC;IAAC,aAAa,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,GAAC,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,CAAC,GAAC,CAAC,GAAC,CAAC,GAAC,CAAC,CAAC;IAAC,mBAAmB,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,OAAO,CAAC;IAAC,aAAa,EAAE,OAAO,CAAC;IAAC,qBAAqB,EAAE,OAAO,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,CAmE1S;AAwzBD;;;;;;;GAOG;AACH,8CAHW,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,WACnB,cAAc,0BAoDxB;AAoED;;;;;;GAMG;AACH,oCAJW,MAAM,cACN,MAAM,EAAE,GACN,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CA+L3D;;;;;cAtnCa,MAAM,EAAE;;;;qBACR,CAAC,GAAC,CAAC,GAAC,CAAC,GAAC,CAAC;;;;gBACP,MAAM,GAAC,MAAM;;;;4BACb,MAAM,EAAE;;;;aACR,OAAO;;;;mBACP,OAAO;eACP,OAAO,0BAA0B,EAAE,eAAe;;;;;;;;;aAErC,MAAM;eAAS,OAAO;cAAQ,MAAM;iBAAW,MAAM;;;;;;aAErD,MAAM;eAAS,OAAO;gBAAU,MAAM;;;;;;kBAEvC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;0BAAoB,OAAO,EAAE;2BAAqB,OAAO,EAAE;oBAAc,OAAO,EAAE"}
1
+ {"version":3,"file":"init.d.mts","sourceRoot":"","sources":["../../../src/cli/commands/init.mjs"],"names":[],"mappings":"AA8EA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wCAHW,MAAM,EAAE,GACN;IAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;IAAC,iBAAiB,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,GAAC,IAAI,CAAC;IAAC,WAAW,EAAE,OAAO,CAAC;IAAC,aAAa,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,GAAC,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,CAAC,GAAC,CAAC,GAAC,CAAC,GAAC,CAAC,CAAC;IAAC,mBAAmB,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,OAAO,CAAC;IAAC,aAAa,EAAE,OAAO,CAAC;IAAC,qBAAqB,EAAE,OAAO,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,CAmE1S;AAwzBD;;;;;;;GAOG;AACH,8CAHW,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,WACnB,cAAc,0BAoDxB;AAoED;;;;;;GAMG;AACH,oCAJW,MAAM,cACN,MAAM,EAAE,GACN,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAgN3D;;;;;cAvoCa,MAAM,EAAE;;;;qBACR,CAAC,GAAC,CAAC,GAAC,CAAC,GAAC,CAAC;;;;gBACP,MAAM,GAAC,MAAM;;;;4BACb,MAAM,EAAE;;;;aACR,OAAO;;;;mBACP,OAAO;eACP,OAAO,0BAA0B,EAAE,eAAe;;;;;;;;;aAErC,MAAM;eAAS,OAAO;cAAQ,MAAM;iBAAW,MAAM;;;;;;aAErD,MAAM;eAAS,OAAO;gBAAU,MAAM;;;;;;kBAEvC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;0BAAoB,OAAO,EAAE;2BAAqB,OAAO,EAAE;oBAAc,OAAO,EAAE"}
@@ -22,7 +22,7 @@ import { fileURLToPath } from 'node:url';
22
22
  import { detectMode } from '../bootstrap/detect-mode.mjs';
23
23
  import { deriveSidecar } from '../bootstrap/product-context-sidecar.mjs';
24
24
  import { assertConfigWriteAllowed } from '../config/guard.mjs';
25
- import { AUDIT_001_LOCAL_FIELDS, migrateDeferredFieldsToLocalConfig, writeLocalConfigField, } from '../config/load.mjs';
25
+ import { AUDIT_001_LOCAL_FIELDS, loadMergedProjectConfig, migrateDeferredFieldsToLocalConfig, writeLocalConfigField, } from '../config/load.mjs';
26
26
  import { migrateConfigSchema, setAt } from '../config/schema-migrate.mjs';
27
27
  import { resolveSharePolicy } from '../config/share-policy.mjs';
28
28
  import { buildAperantGitignoreTemplate } from '../config/upgrade-gitignore.mjs';
@@ -30,6 +30,7 @@ import { detectPublicRemote } from '../git/remote.mjs';
30
30
  import { detectArchetype } from '../install/archetype.mjs';
31
31
  import { installKernel } from '../install/install-kernel.mjs';
32
32
  import { MANIFEST_FILENAME } from '../install/manifest.mjs';
33
+ import { provisionMcpConfig } from '../install/mcp-provision.mjs';
33
34
  import { discoverCanonicalFiles, runInstall, writeLastInstallSummary, } from '../install/pipeline.mjs';
34
35
  import { getRuntime, RUNTIMES, suggestRuntimes } from '../install/runtime-detect.mjs';
35
36
  import { runLegacyCleanup } from '../install/runtime-migrate.mjs';
@@ -38,7 +39,7 @@ import { detectAgentRuntime } from '../util/agent-runtime.mjs';
38
39
  import { buildAptSection } from '../util/aperant-section.mjs';
39
40
  import { atomicWriteJson } from '../util/atomic-write.mjs';
40
41
  import { aperantBanner } from '../util/banner.mjs';
41
- import { copyDirRecursive, injectInstructionFile } from '../util/copy.mjs';
42
+ import { checkInstructionFile, copyDirRecursive, injectInstructionFile } from '../util/copy.mjs';
42
43
  import { withFileLock } from '../util/fs-lock.mjs';
43
44
  import { emitInitConfigHint } from '../util/init-hints.mjs';
44
45
  import { err, exitWith, ok, rawText } from '../util/result.mjs';
@@ -1183,6 +1184,21 @@ export async function cmdInit(projectDir, extraArgs = []) {
1183
1184
  if (envelope?.__earlyExit) {
1184
1185
  return envelope.__earlyExit;
1185
1186
  }
1187
+ // Auto-provision host MCP-server config + the agent-browser skill for
1188
+ // the detected runtime (C38). Runs on BOTH the fresh install AND the
1189
+ // no-drift /apt:update upgrade path (both reach finishInit). finishInit
1190
+ // has already written .aperant/config.json from the template, so the
1191
+ // merged config carries verification.provision_mcp (default true) — the
1192
+ // opt-out gate. TOFU/non-clobbering; never throws.
1193
+ const mergedConfig = loadMergedProjectConfig(installTarget);
1194
+ const mcpProvision = provisionMcpConfig({
1195
+ targetDir: installTarget,
1196
+ selectedRuntimes: effectiveAnswers.runtimes,
1197
+ config: mergedConfig,
1198
+ frameworkRoot: canonicalRoot,
1199
+ version,
1200
+ });
1201
+ envelope.mcp_provision = mcpProvision;
1186
1202
  if (interactiveTty) {
1187
1203
  await renderOutro(effectiveAnswers, results, version, kernelResult);
1188
1204
  // TTY mode: envelope was persisted to disk + suppressed from stdout
@@ -1325,6 +1341,69 @@ async function renderOutro(answers, results, version, kernelResult) {
1325
1341
  clack.note('Relaunch Claude Code / Codex / your AI CLI in this directory\nso the new .claude/ + .codex/ surfaces are picked up.', 'Activate');
1326
1342
  clack.outro(`Installed ${fileCount} files · v${version} · ${xverifyCopy}`);
1327
1343
  }
1344
+ /**
1345
+ * Resolve the deduplicated set of root instruction-file paths the selected
1346
+ * runtimes read. Each chosen runtime contributes its `nativeInstructionFile`
1347
+ * (claude→CLAUDE.md, codex/opencode→AGENTS.md, gemini→GEMINI.md); runtimes
1348
+ * mapped to `null` install to their own surface and contribute nothing.
1349
+ * Filenames are deduplicated (codex + opencode both map to AGENTS.md → one
1350
+ * target).
1351
+ *
1352
+ * Resolution rules:
1353
+ * - Multiple distinct targets (e.g. `--claude --codex`): write all of them
1354
+ * verbatim — that is the whole point of the fix (AC1, AC2, AC5).
1355
+ * - Exactly one resolved target that does NOT exist AND it is CLAUDE.md, but
1356
+ * AGENTS.md already exists (the `--claude` with a pre-seeded AGENTS.md and
1357
+ * no CLAUDE.md case, Risk 4 / AC9): redirect to the existing AGENTS.md
1358
+ * rather than creating an orphan CLAUDE.md. This is safe ONLY for the
1359
+ * claude runtime because native Claude Code reads AGENTS.md as well as
1360
+ * CLAUDE.md. The redirect is NOT applied for gemini (reads only GEMINI.md)
1361
+ * or codex/opencode (read only AGENTS.md) — redirecting those to an
1362
+ * unrelated existing file would reintroduce the very visibility gap this
1363
+ * fix closes, so their absent target is simply created.
1364
+ * - Zero resolved targets (no selected runtime maps to an instruction file):
1365
+ * fall back to the legacy heuristic — CLAUDE.md if present, else AGENTS.md
1366
+ * if present, else a new CLAUDE.md.
1367
+ *
1368
+ * @param {string[]} chosenIds
1369
+ * @param {string} targetDir
1370
+ * @returns {string[]} absolute instruction-file paths, deduplicated
1371
+ */
1372
+ function resolveInstructionTargets(chosenIds, targetDir) {
1373
+ const filenames = [];
1374
+ for (const id of chosenIds) {
1375
+ const runtime = getRuntime(id);
1376
+ const file = runtime?.nativeInstructionFile;
1377
+ if (file && !filenames.includes(file))
1378
+ filenames.push(file);
1379
+ }
1380
+ const claudeMdPath = join(targetDir, 'CLAUDE.md');
1381
+ const agentsMdPath = join(targetDir, 'AGENTS.md');
1382
+ const resolved = filenames.map((name) => join(targetDir, name));
1383
+ if (resolved.length === 0) {
1384
+ // No selected runtime maps to an instruction file — legacy heuristic.
1385
+ if (existsSync(claudeMdPath))
1386
+ return [claudeMdPath];
1387
+ if (existsSync(agentsMdPath))
1388
+ return [agentsMdPath];
1389
+ return [claudeMdPath];
1390
+ }
1391
+ if (resolved.length === 1 && !existsSync(resolved[0])) {
1392
+ // Single registry target that doesn't exist yet. The ONLY safe redirect
1393
+ // is claude→AGENTS.md (Risk 4 / AC9): native Claude Code reads AGENTS.md
1394
+ // too, so when CLAUDE.md is absent but AGENTS.md exists (e.g. a prior
1395
+ // codex install), writing AGENTS.md still serves claude and avoids an
1396
+ // orphan CLAUDE.md. gemini/codex/opencode read only their own native
1397
+ // file — redirecting them to an unrelated existing file would hide the
1398
+ // catalog from their CLI, so we just create their target.
1399
+ if (resolved[0] === claudeMdPath && existsSync(agentsMdPath))
1400
+ return [agentsMdPath];
1401
+ return resolved;
1402
+ }
1403
+ // One or more registry targets, at least one exists OR multiple distinct
1404
+ // targets — write the registry-resolved set verbatim.
1405
+ return resolved;
1406
+ }
1328
1407
  /**
1329
1408
  * Synchronous tail of init — runs after the runtime-install promise
1330
1409
  * resolves. Performs all .aperant/ + Claude-hook + remote-warning work
@@ -1488,18 +1567,19 @@ function finishInit(targetDir, installTarget, canonicalRoot, version, chosenIds,
1488
1567
  if (existsSync(installedContextSrc)) {
1489
1568
  copyDirRecursive(installedContextSrc, join(aperantDir, 'context', 'installed'));
1490
1569
  }
1491
- const claudeMdPath = join(targetDir, 'CLAUDE.md');
1492
- const agentsMdPath = join(targetDir, 'AGENTS.md');
1493
- let instructionFile;
1494
- if (existsSync(claudeMdPath))
1495
- instructionFile = claudeMdPath;
1496
- else if (existsSync(agentsMdPath))
1497
- instructionFile = agentsMdPath;
1498
- else
1499
- instructionFile = claudeMdPath;
1570
+ // Resolve every distinct instruction file the selected runtimes read, then
1571
+ // preflight ALL of them before writing ANY — so a single foreign-row abort
1572
+ // leaves no file partially written (US-03 / AC3). Each chosen runtime maps
1573
+ // to its `nativeInstructionFile` (claude→CLAUDE.md, codex/opencode→AGENTS.md,
1574
+ // gemini→GEMINI.md); null-mapped runtimes install to their own surface and
1575
+ // contribute no target. When the selected runtimes resolve to zero targets,
1576
+ // or none of the resolved targets exist on disk, fall back to the legacy
1577
+ // "pick the existing file" heuristic so e.g. `--claude` with a pre-seeded
1578
+ // AGENTS.md (and no CLAUDE.md) still writes AGENTS.md (Risk 4 / AC9).
1579
+ const instructionTargets = resolveInstructionTargets(chosenIds, targetDir);
1500
1580
  // FRAMEWORK-BUG-023 — generate the section from discoverSkills before
1501
1581
  // handing it to the writer. The writer also runs a foreign-row defensive
1502
- // check; if rows present in the file aren't in the generated set we
1582
+ // check; if rows present in any file aren't in the generated set we
1503
1583
  // signal exit-2 via the __earlyExit sentinel so cmdInit can route the
1504
1584
  // envelope through exitWith() without writing a malformed install.
1505
1585
  const aptSection = buildAptSection({
@@ -1507,26 +1587,75 @@ function finishInit(targetDir, installTarget, canonicalRoot, version, chosenIds,
1507
1587
  targetDir,
1508
1588
  cli: detectAgentRuntime(),
1509
1589
  });
1510
- const injectionOutcome = injectInstructionFile(instructionFile, {
1511
- aptSection,
1512
- force: forceOverwriteCatalog,
1590
+ // Preflight pass check every target with the write-free helper; collect
1591
+ // all aborting targets so the error lists every offender, not just the first.
1592
+ const abortingTargets = [];
1593
+ for (const filePath of instructionTargets) {
1594
+ const check = checkInstructionFile(filePath, { aptSection, force: forceOverwriteCatalog });
1595
+ if (check.status === 'aborted-foreign-rows') {
1596
+ abortingTargets.push({ path: basename(filePath), foreignRowIds: check.foreignRowIds });
1597
+ }
1598
+ }
1599
+ if (abortingTargets.length > 0) {
1600
+ const allForeignRowIds = [...new Set(abortingTargets.flatMap((t) => t.foreignRowIds))];
1601
+ const fileList = abortingTargets.map((t) => t.path).join(', ');
1602
+ return {
1603
+ __earlyExit: exitWith({
1604
+ ok: false,
1605
+ status: 'error',
1606
+ command: 'init',
1607
+ code: 'instruction-file.foreign-rows',
1608
+ message: `${fileList} contains skill rows not present in the installed framework: ` +
1609
+ `${allForeignRowIds.join(', ')}. ` +
1610
+ 'Re-run with --force-overwrite-catalog to regenerate anyway, or remove the foreign rows manually.',
1611
+ foreignRowIds: allForeignRowIds,
1612
+ instructionFile: { path: abortingTargets[0].path, status: 'aborted-foreign-rows' },
1613
+ instructionFiles: abortingTargets.map((t) => ({
1614
+ path: t.path,
1615
+ status: 'aborted-foreign-rows',
1616
+ })),
1617
+ }, 2),
1618
+ };
1619
+ }
1620
+ // Write pass — no target aborted in the preflight, so write all.
1621
+ const instructionFiles = instructionTargets.map((filePath) => {
1622
+ const outcome = injectInstructionFile(filePath, {
1623
+ aptSection,
1624
+ force: forceOverwriteCatalog,
1625
+ });
1626
+ return {
1627
+ path: basename(filePath),
1628
+ status: outcome.status,
1629
+ foreignRowIds: outcome.foreignRowIds,
1630
+ };
1513
1631
  });
1514
- if (injectionOutcome.status === 'aborted-foreign-rows') {
1632
+ // TOCTOU guard: if a file gained foreign rows BETWEEN the preflight check and
1633
+ // the write (e.g. another process wrote to it), surface the same error envelope
1634
+ // the preflight uses rather than silently accepting partial writes.
1635
+ const writtenAborts = instructionFiles.filter((f) => f.status === 'aborted-foreign-rows');
1636
+ if (writtenAborts.length > 0) {
1637
+ const allForeignRowIds = [...new Set(writtenAborts.flatMap((f) => f.foreignRowIds ?? []))];
1638
+ const fileList = writtenAborts.map((f) => f.path).join(', ');
1515
1639
  return {
1516
1640
  __earlyExit: exitWith({
1517
1641
  ok: false,
1518
1642
  status: 'error',
1519
1643
  command: 'init',
1520
1644
  code: 'instruction-file.foreign-rows',
1521
- message: `${basename(instructionFile)} contains skill rows not present in the installed framework: ` +
1522
- `${injectionOutcome.foreignRowIds.join(', ')}. ` +
1645
+ message: `${fileList} contains skill rows not present in the installed framework: ` +
1646
+ `${allForeignRowIds.join(', ')}. ` +
1523
1647
  'Re-run with --force-overwrite-catalog to regenerate anyway, or remove the foreign rows manually.',
1524
- foreignRowIds: injectionOutcome.foreignRowIds,
1525
- instructionFile: basename(instructionFile),
1648
+ foreignRowIds: allForeignRowIds,
1649
+ instructionFile: { path: writtenAborts[0].path, status: 'aborted-foreign-rows' },
1650
+ instructionFiles: writtenAborts.map((f) => ({
1651
+ path: f.path,
1652
+ status: 'aborted-foreign-rows',
1653
+ })),
1526
1654
  }, 2),
1527
1655
  };
1528
1656
  }
1529
- const injectionResult = injectionOutcome.status;
1657
+ // Primary entry drives the backwards-compat singular alias + structure key.
1658
+ const primaryInstructionFile = instructionFiles[0];
1530
1659
  const installedClaude = chosenIds.includes('claude') || existsSync(join(targetDir, '.claude'));
1531
1660
  const precompactResult = installedClaude
1532
1661
  ? installPrecompactHook(targetDir)
@@ -1560,9 +1689,11 @@ function finishInit(targetDir, installTarget, canonicalRoot, version, chosenIds,
1560
1689
  '.aperant/events/': 'event log (git-ignored)',
1561
1690
  '.aperant/prompts/': 'agent prompt templates',
1562
1691
  '.aperant/install/last-install.json': 'cross-runtime install summary',
1563
- [basename(instructionFile)]: `APT section ${injectionResult}`,
1564
1692
  '.claude/settings.local.json': `PreCompact hook ${precompactResult}; SessionStart hook ${sessionStartResult}`,
1565
1693
  };
1694
+ for (const f of instructionFiles) {
1695
+ structure[f.path] = `APT section ${f.status}`;
1696
+ }
1566
1697
  if (kernelResult) {
1567
1698
  structure['.aperant/deps/'] =
1568
1699
  kernelResult.status === 'installed'
@@ -1601,7 +1732,10 @@ function finishInit(targetDir, installTarget, canonicalRoot, version, chosenIds,
1601
1732
  framework_version: version,
1602
1733
  configCreated,
1603
1734
  configMigrated,
1604
- instructionFile: { path: basename(instructionFile), status: injectionResult },
1735
+ // Plural contract (ID-03): one entry per instruction file written.
1736
+ // `instructionFile` (singular) is a backwards-compat alias = first entry.
1737
+ instructionFiles,
1738
+ instructionFile: primaryInstructionFile,
1605
1739
  stateCreated,
1606
1740
  seedsCreated,
1607
1741
  publicRemoteWarning,