@event4u/agent-config 14.5.0 → 14.7.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 (203) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +384 -3
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/analyze/decision.md +7 -0
  5. package/dist/agent-src/commands/analyze/inbox.md +264 -10
  6. package/dist/agent-src/commands/analyze.md +1 -1
  7. package/dist/agent-src/commands/optimize/project.md +10 -0
  8. package/dist/agent-src/commands/roadmap/process-full.md +132 -4
  9. package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +31 -0
  10. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +161 -1
  11. package/dist/agent-src/rules/context-hygiene.md +2 -0
  12. package/dist/agent-src/rules/decision-revisit-gate.md +111 -7
  13. package/dist/agent-src/rules/fix-what-you-see.md +108 -0
  14. package/dist/agent-src/rules/no-roadmap-references.md +0 -5
  15. package/dist/agent-src/rules/recurring-criticism.md +110 -0
  16. package/dist/agent-src/rules/roadmap-progress-sync.md +1 -1
  17. package/dist/agent-src/rules/rule-type-governance.md +0 -2
  18. package/dist/agent-src/rules/self-repair-loop.md +17 -0
  19. package/dist/agent-src/rules/skill-quality.md +0 -2
  20. package/dist/agent-src/rules/source-confidentiality.md +37 -7
  21. package/dist/agent-src/scripts/archival_sweep.ts +117 -0
  22. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +19 -3
  23. package/dist/agent-src/scripts/gate_budget.ts +282 -0
  24. package/dist/agent-src/scripts/gate_execute.ts +72 -31
  25. package/dist/agent-src/scripts/roadmap_gates.ts +74 -3
  26. package/dist/agent-src/scripts/update_roadmap_progress.ts +251 -7
  27. package/dist/agent-src/skills/adr-create/SKILL.md +25 -3
  28. package/dist/agent-src/skills/ai-council/SKILL.md +48 -807
  29. package/dist/agent-src/skills/ai-council/references/advanced-modes.md +320 -0
  30. package/dist/agent-src/skills/ai-council/references/cost-and-redaction.md +99 -0
  31. package/dist/agent-src/skills/ai-council/references/execution-modes.md +162 -0
  32. package/dist/agent-src/skills/ai-council/references/output-and-synthesis.md +125 -0
  33. package/dist/agent-src/skills/ai-council/references/procedure.md +143 -0
  34. package/dist/agent-src/skills/decision-review/SKILL.md +18 -0
  35. package/dist/agent-src/skills/quality-tools/SKILL.md +34 -326
  36. package/dist/agent-src/skills/quality-tools/references/js-ts-tools.md +113 -0
  37. package/dist/agent-src/skills/quality-tools/references/php-tools.md +222 -0
  38. package/dist/agent-src/skills/roadmap-management/SKILL.md +15 -409
  39. package/dist/agent-src/skills/roadmap-management/references/archival.md +226 -0
  40. package/dist/agent-src/skills/roadmap-management/references/authoring.md +107 -0
  41. package/dist/agent-src/skills/roadmap-management/references/dashboard.md +47 -0
  42. package/dist/agent-src/skills/roadmap-management/references/lifecycle.md +58 -0
  43. package/dist/agent-src/skills/skill-writing/SKILL.md +34 -570
  44. package/dist/agent-src/skills/skill-writing/references/procedure.md +275 -0
  45. package/dist/agent-src/skills/skill-writing/references/read-only-scripts.md +18 -0
  46. package/dist/agent-src/skills/skill-writing/references/section-patterns.md +298 -0
  47. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  48. package/dist/agent-src/templates/roadmaps.md +45 -2
  49. package/dist/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
  50. package/dist/agent-src/templates/scripts/telemetry/remote.ts +160 -7
  51. package/dist/agent-src/templates/scripts/telemetry/transport.ts +169 -0
  52. package/dist/cli/registry.js +1 -1
  53. package/dist/cli/registry.js.map +1 -1
  54. package/dist/cli-delegate/chunk-2GNWGMAL.js +172 -0
  55. package/dist/cli-delegate/{chunk-RLN73PR5.js → chunk-4VR4ORQ6.js} +10 -7
  56. package/dist/cli-delegate/chunk-6EOCNQAM.js +402 -0
  57. package/dist/cli-delegate/{chunk-XURJ4AQC.js → chunk-74E3TKWV.js} +381 -280
  58. package/dist/cli-delegate/{chunk-3UCJWMMP.js → chunk-7F3VYSM2.js} +4 -4
  59. package/dist/cli-delegate/{chunk-BKO2AZ7Z.js → chunk-B2ERU2VA.js} +1 -1
  60. package/dist/cli-delegate/{chunk-NXFWFAYV.js → chunk-G23AT6GD.js} +1 -0
  61. package/dist/cli-delegate/{chunk-63WSP6YZ.js → chunk-GJ5BVHXM.js} +72 -165
  62. package/dist/cli-delegate/{chunk-ZPSXJTLI.js → chunk-JZ4NR2TW.js} +353 -1
  63. package/dist/cli-delegate/chunk-MWRD5YOH.js +100 -0
  64. package/dist/cli-delegate/{chunk-GRRPEPRN.js → chunk-T7L2QK3K.js} +24 -5
  65. package/dist/cli-delegate/{chunk-EVTY7ISR.js → chunk-YM4GZDJG.js} +9 -1
  66. package/dist/cli-delegate/cmd_conformance.js +459 -709
  67. package/dist/cli-delegate/cmd_doctor.js +6 -6
  68. package/dist/cli-delegate/cmd_explain.js +1 -1
  69. package/dist/cli-delegate/cmd_export.js +7 -6
  70. package/dist/cli-delegate/cmd_handoff.js +6 -6
  71. package/dist/cli-delegate/cmd_packs_active.js +1 -1
  72. package/dist/cli-delegate/cmd_preflight.js +2 -2
  73. package/dist/cli-delegate/cmd_prune.js +7 -6
  74. package/dist/cli-delegate/cmd_session_recycle.js +840 -30
  75. package/dist/cli-delegate/cmd_settings_migrate.js +7 -6
  76. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  77. package/dist/cli-delegate/cmd_sync.js +7 -6
  78. package/dist/cli-delegate/cmd_uninstall.js +9 -7
  79. package/dist/cli-delegate/cmd_update.js +1 -1
  80. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  81. package/dist/cli-delegate/cmd_validate.js +7 -6
  82. package/dist/discovery/deprecation-report.md +1 -1
  83. package/dist/discovery/discovery-manifest.json +72 -29
  84. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  85. package/dist/discovery/discovery-manifest.summary.md +4 -4
  86. package/dist/discovery/dormancy-report.md +1 -1
  87. package/dist/discovery/orphan-report.md +1 -1
  88. package/dist/discovery/packs.json +10 -8
  89. package/dist/discovery/trust-report.md +3 -3
  90. package/dist/discovery/workspaces.json +8 -6
  91. package/dist/hooks/dispatch.js +3127 -2022
  92. package/dist/install/claudePathsPlan.js +187 -0
  93. package/dist/install/claudePathsPlan.js.map +1 -0
  94. package/dist/install/hostLayerFingerprint.js +136 -0
  95. package/dist/install/hostLayerFingerprint.js.map +1 -0
  96. package/dist/install/install.mjs +553 -425
  97. package/dist/install/partitionEligibility.js +276 -0
  98. package/dist/install/partitionEligibility.js.map +1 -0
  99. package/dist/mcp/registry-manifest.json +3 -3
  100. package/dist/mcp/server.json +2 -2
  101. package/dist/mcp/server.mjs +1744 -1191
  102. package/dist/router.json +1 -1
  103. package/dist/scripts/_lib/agent_src.js +41 -0
  104. package/dist/scripts/_lib/agent_src.js.map +1 -1
  105. package/dist/scripts/_lib/install_layout.js +83 -0
  106. package/dist/scripts/_lib/install_layout.js.map +1 -0
  107. package/dist/scripts/_lib/installed_lock.js +444 -0
  108. package/dist/scripts/_lib/installed_lock.js.map +1 -0
  109. package/dist/scripts/condense.js +13 -164
  110. package/dist/scripts/condense.js.map +1 -1
  111. package/dist/server/schemas/settings.js +4 -0
  112. package/dist/server/schemas/settings.js.map +1 -1
  113. package/docs/contracts/settings-classes.md +12 -6
  114. package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +8 -3
  115. package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +26 -0
  116. package/docs/guidelines/agent-infra/engineering-memory-data-format.md +49 -0
  117. package/docs/guidelines/agent-infra/memory-access.md +17 -0
  118. package/docs/guidelines/agent-infra/recurring-criticism-mechanics.md +96 -0
  119. package/package.json +1 -1
  120. package/src/agent-src/scripts/archival_sweep.ts +117 -0
  121. package/src/agent-src/scripts/archive_completed_roadmaps.ts +19 -3
  122. package/src/agent-src/scripts/gate_budget.ts +282 -0
  123. package/src/agent-src/scripts/gate_execute.ts +72 -31
  124. package/src/agent-src/scripts/roadmap_gates.ts +74 -3
  125. package/src/agent-src/scripts/update_roadmap_progress.ts +251 -7
  126. package/src/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
  127. package/src/agent-src/templates/scripts/telemetry/remote.ts +160 -7
  128. package/src/agent-src/templates/scripts/telemetry/transport.ts +169 -0
  129. package/src/config/agent-settings.template.yml +27 -2
  130. package/src/config/ci-local-parity.yml +18 -0
  131. package/src/config/estate-count-budget.json +367 -3
  132. package/src/config/gate-violation-baselines.json +41 -41
  133. package/src/config/pack-size-budget.json +3 -2
  134. package/src/config/rule-activation-census.json +14 -6
  135. package/src/config/rule-stub-ceilings.json +19 -6
  136. package/src/install/claudePathsPlan.ts +196 -0
  137. package/src/install/hostLayerFingerprint.ts +145 -0
  138. package/src/install/partitionEligibility.ts +325 -0
  139. package/src/scripts/_cli/cmd_session_recycle.ts +9 -1
  140. package/src/scripts/_dispatch.bash +6 -1
  141. package/src/scripts/_lib/ac_heading.ts +30 -0
  142. package/src/scripts/_lib/agent_src.ts +42 -0
  143. package/src/scripts/_lib/bench_ab_pinned_repo.ts +162 -0
  144. package/src/scripts/_lib/bench_ab_workspace.ts +10 -1
  145. package/src/scripts/_lib/envelope_grounding.ts +38 -8
  146. package/src/scripts/_lib/installed_lock.ts +50 -5
  147. package/src/scripts/_lib/layer_overlap_notice.ts +85 -0
  148. package/src/scripts/_lib/md_section.ts +129 -0
  149. package/src/scripts/_lib/scope_guard.sh +86 -4
  150. package/src/scripts/_lib/self_repair.ts +19 -0
  151. package/src/scripts/_lib/self_repair_class_b.ts +171 -0
  152. package/src/scripts/_lib/subagent_capsule.ts +81 -1
  153. package/src/scripts/adr/regenerate_index.ts +13 -2
  154. package/src/scripts/adr_cite_check.ts +424 -0
  155. package/src/scripts/ai_council/cli_help.ts +55 -0
  156. package/src/scripts/before_complete_hook.ts +153 -28
  157. package/src/scripts/bench_ab_v2_complexity.ts +27 -7
  158. package/src/scripts/check_adr_frontmatter.ts +187 -1
  159. package/src/scripts/check_references.ts +60 -0
  160. package/src/scripts/check_release_published.ts +32 -11
  161. package/src/scripts/check_single_delivery.ts +517 -0
  162. package/src/scripts/ci_settle.ts +170 -0
  163. package/src/scripts/condense.ts +20 -170
  164. package/src/scripts/council_cli.ts +3 -3
  165. package/src/scripts/dispatch_r2_reviewer.ts +6 -14
  166. package/src/scripts/hook_manifest.json +1 -1
  167. package/src/scripts/hook_manifest.yaml +77 -14
  168. package/src/scripts/hooks/block_no_verify.ts +61 -10
  169. package/src/scripts/hooks/block_unauthorized_git.ts +1 -1
  170. package/src/scripts/hooks/concern_registry.ts +4 -0
  171. package/src/scripts/hooks/rtk_wrap_hook.ts +8 -3
  172. package/src/scripts/hooks/run_continuation_hook.ts +887 -68
  173. package/src/scripts/hooks/ship_diff_volume_hook.ts +81 -15
  174. package/src/scripts/hooks/source_first_gate_hook.ts +464 -0
  175. package/src/scripts/hooks/state_io.ts +782 -20
  176. package/src/scripts/hooks/subagent_ledger_hook.ts +44 -15
  177. package/src/scripts/hooks/telemetry_flush_hook.ts +139 -0
  178. package/src/scripts/hooks/telemetry_self_repair.ts +128 -0
  179. package/src/scripts/hooks/telemetry_usage_hook.ts +22 -4
  180. package/src/scripts/hooks/turn_end_gate_hook.ts +81 -15
  181. package/src/scripts/install.sh +12 -2
  182. package/src/scripts/install.ts +16 -11
  183. package/src/scripts/language_mirror_hook.ts +561 -25
  184. package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
  185. package/src/scripts/lint_plan_risk_register.ts +4 -3
  186. package/src/scripts/lint_skill_router_head.ts +4 -4
  187. package/src/scripts/mcp_server/tools.ts +2 -2
  188. package/src/scripts/memory_consolidation.ts +189 -0
  189. package/src/scripts/memory_eviction.ts +384 -0
  190. package/src/scripts/new_roadmap.ts +136 -0
  191. package/src/scripts/pack_mcp_content.ts +20 -5
  192. package/src/scripts/release.ts +37 -38
  193. package/src/scripts/report_layer_overlap.ts +78 -0
  194. package/src/scripts/report_memory_pointers.ts +622 -0
  195. package/src/scripts/self_repair_hook.ts +37 -5
  196. package/src/scripts/session_register_hook.ts +115 -15
  197. package/src/scripts/skill_usage_collect.ts +11 -1
  198. package/src/scripts/skill_usage_report.ts +278 -8
  199. package/src/scripts/sync_pr_branch.ts +7 -0
  200. package/src/server/schemas/settings.ts +8 -0
  201. package/dist/cli-delegate/chunk-EJITJBJX.js +0 -172
  202. package/dist/cli-delegate/chunk-PZWRPRWP.js +0 -38
  203. /package/dist/cli-delegate/{chunk-RYNADUFG.js → chunk-LWTYTX6F.js} +0 -0
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Delta #9 — a corpus task may name an EXTERNAL repository pinned at a SHA.
3
+ *
4
+ * Until this module, every v2 task resolved to a self-contained fixture tree
5
+ * under `internal/bench/ab/`, so the harness could only measure trap tasks this
6
+ * repository authored. Phase 3 of `road-to-solution-minimalism` asks for the
7
+ * opposite: a public repo nobody chose for the purpose, so a size effect cannot
8
+ * be an artefact of fixtures written by the same hand as the rule.
9
+ *
10
+ * ## Why a SHA and not a ref
11
+ *
12
+ * `sha` is a full 40-hex commit id, and a branch or tag name is REFUSED rather
13
+ * than resolved. A ref moves, so a report pinned to one records which repository
14
+ * answered but not which tree — and F7 of that roadmap forbids exactly that
15
+ * class of unreproducible number. The refusal is loud (a throw) because the
16
+ * quiet alternative, resolving the ref at run time, produces a report that looks
17
+ * pinned and is not.
18
+ *
19
+ * ## Materialisation, and what needs the network
20
+ *
21
+ * `ensure_pinned_tree` warms a cache ONCE per SHA — `git init` + a `--depth 1`
22
+ * fetch of that exact commit + `git archive` into a `.git`-free tree. Every
23
+ * per-trial workspace is then a local copy of that tree, so a sweep touches the
24
+ * network once per pinned task and never on the metered path. `is_materialised`
25
+ * answers the warm/cold question without touching the network, which is what
26
+ * lets `--mode selftest` and the offline complexity re-scorer report an honest
27
+ * "not materialised locally" instead of either hanging or scoring zero.
28
+ *
29
+ * The tree is `.git`-free on purpose. `changed_files` already skips `.git`, so
30
+ * carrying it would not corrupt a diff — it would just copy a repository per
31
+ * trial, and a sweep is 5 arms x 3 seeds per task.
32
+ *
33
+ * ## What this module deliberately does NOT do
34
+ *
35
+ * No sub-directory scoping, no submodules, no LFS, no authenticated remotes:
36
+ * `repo` must be an `https://` URL. Each of those is a real feature and none is
37
+ * needed to pin one task to one commit, which is the whole of delta #9.
38
+ */
39
+ import { execFileSync } from 'node:child_process';
40
+ import * as fs from 'node:fs';
41
+ import * as os from 'node:os';
42
+ import * as path from 'node:path';
43
+
44
+ /** A task's pinned-repo coordinates, already validated. */
45
+ export interface PinnedSpec {
46
+ repo: string;
47
+ sha: string;
48
+ }
49
+
50
+ /** A malformed or contradictory pin. Never swallowed into a fixture fallback. */
51
+ export class PinnedRepoError extends Error {}
52
+
53
+ const SHA_RE = /^[0-9a-f]{40}$/;
54
+ const REPO_URL_RE = /^https:\/\/[A-Za-z0-9._~:/?#@!$&'()*+,;=%-]+$/;
55
+ const CACHE_ROOT = path.join(os.tmpdir(), 'agent-config-bench-v2-repos');
56
+ /** Written into a warm cache entry so a half-finished fetch never reads as warm. */
57
+ const STAMP = '.pinned-repo.json';
58
+
59
+ type Dict = Record<string, unknown>;
60
+
61
+ /**
62
+ * The pin declared by `task`, or `null` when it is an ordinary fixture task.
63
+ *
64
+ * Throws rather than returning `null` on a task that is *trying* to be pinned
65
+ * and failing: a half-declared pin silently treated as a fixture task is how a
66
+ * sweep would run the wrong tree and report it as the right one.
67
+ */
68
+ export function pinnedSpecFor(task: Dict): PinnedSpec | null {
69
+ const repoRaw = task['repo'];
70
+ const shaRaw = task['sha'];
71
+ const id = task['id'] === undefined ? '<unknown>' : String(task['id']);
72
+ if (repoRaw === undefined && shaRaw === undefined) return null;
73
+ if (repoRaw === undefined || shaRaw === undefined) {
74
+ throw new PinnedRepoError(`task ${id}: \`repo\` and \`sha\` must be declared together`);
75
+ }
76
+ if (task['fixture'] !== undefined) {
77
+ throw new PinnedRepoError(`task ${id}: a pinned task must not also declare \`fixture\``);
78
+ }
79
+ const repo = String(repoRaw);
80
+ const sha = String(shaRaw);
81
+ if (!REPO_URL_RE.test(repo)) {
82
+ throw new PinnedRepoError(`task ${id}: \`repo\` must be an https:// URL, got ${repo}`);
83
+ }
84
+ if (!SHA_RE.test(sha)) {
85
+ throw new PinnedRepoError(
86
+ `task ${id}: \`sha\` must be a full 40-hex commit id, got ${sha} — ` +
87
+ 'a branch or tag is refused because it moves and the report would not be reproducible',
88
+ );
89
+ }
90
+ return { repo, sha };
91
+ }
92
+
93
+ /** Cache directory for one pin. Keyed by the SHA — a commit id is content. */
94
+ export function pinned_tree_dir(spec: PinnedSpec): string {
95
+ return path.join(CACHE_ROOT, spec.sha, 'tree');
96
+ }
97
+
98
+ /** True when this pin's tree is already on disk and complete. */
99
+ export function is_materialised(spec: PinnedSpec): boolean {
100
+ const stampPath = path.join(CACHE_ROOT, spec.sha, STAMP);
101
+ if (!fs.existsSync(stampPath) || !fs.existsSync(pinned_tree_dir(spec))) return false;
102
+ try {
103
+ const stamp = JSON.parse(fs.readFileSync(stampPath, 'utf-8')) as Dict;
104
+ return String(stamp['repo']) === spec.repo && String(stamp['sha']) === spec.sha;
105
+ } catch {
106
+ return false;
107
+ }
108
+ }
109
+
110
+ function _git(cwd: string, args: readonly string[]): void {
111
+ execFileSync('git', [...args], { cwd, stdio: 'pipe' });
112
+ }
113
+
114
+ /**
115
+ * Materialise the pinned tree, fetching over the network only on a cold cache.
116
+ *
117
+ * Returns the tree path. A failed fetch leaves NO stamp, so the next call
118
+ * retries from scratch rather than reading a partial checkout as warm.
119
+ */
120
+ export function ensure_pinned_tree(spec: PinnedSpec): string {
121
+ const tree = pinned_tree_dir(spec);
122
+ if (is_materialised(spec)) return tree;
123
+ const base = path.join(CACHE_ROOT, spec.sha);
124
+ fs.rmSync(base, { recursive: true, force: true });
125
+ const gitdir = path.join(base, 'src');
126
+ fs.mkdirSync(gitdir, { recursive: true });
127
+ fs.mkdirSync(tree, { recursive: true });
128
+ _git(gitdir, ['init', '-q', '.']);
129
+ _git(gitdir, ['remote', 'add', 'origin', spec.repo]);
130
+ _git(gitdir, ['fetch', '-q', '--depth', '1', 'origin', spec.sha]);
131
+ const tar = execFileSync('git', ['archive', '--format=tar', 'FETCH_HEAD'], {
132
+ cwd: gitdir,
133
+ maxBuffer: 512 * 1024 * 1024,
134
+ });
135
+ const tarPath = path.join(base, 'tree.tar');
136
+ fs.writeFileSync(tarPath, tar);
137
+ execFileSync('tar', ['-x', '-f', tarPath, '-C', tree], { stdio: 'pipe' });
138
+ fs.rmSync(tarPath, { force: true });
139
+ fs.rmSync(gitdir, { recursive: true, force: true });
140
+ fs.writeFileSync(path.join(base, STAMP), JSON.stringify({ repo: spec.repo, sha: spec.sha }), 'utf-8');
141
+ return tree;
142
+ }
143
+
144
+ /**
145
+ * Absolute path to a task's pristine tree — fixture or pinned repo.
146
+ *
147
+ * `offlineOnly` is for readers that must not reach the network (the offline
148
+ * re-scorer, `--mode selftest`): a cold cache returns `null` there, which every
149
+ * caller reports as "not measured" rather than as a zero.
150
+ */
151
+ export function pristine_tree_for(
152
+ task: Dict,
153
+ opts: { fixturesRoot: string; offlineOnly?: boolean },
154
+ ): string | null {
155
+ const spec = pinnedSpecFor(task);
156
+ if (spec !== null) {
157
+ if (opts.offlineOnly === true) return is_materialised(spec) ? pinned_tree_dir(spec) : null;
158
+ return ensure_pinned_tree(spec);
159
+ }
160
+ if (task['fixture'] === undefined) return null;
161
+ return path.join(opts.fixturesRoot, String(task['fixture']));
162
+ }
@@ -35,6 +35,8 @@
35
35
  import * as fs from 'node:fs';
36
36
  import * as os from 'node:os';
37
37
  import * as path from 'node:path';
38
+
39
+ import { pristine_tree_for } from './bench_ab_pinned_repo.js';
38
40
  import { fileURLToPath } from 'node:url';
39
41
 
40
42
  const _HERE = fileURLToPath(import.meta.url);
@@ -99,7 +101,14 @@ export function workspace_dir(task_id: string, arm: string, seed: number): strin
99
101
  * directory that is, never whether it is clean.
100
102
  */
101
103
  export function reset_fixture(task: Dict, arm: string, seed: number): [string, string] {
102
- const fixture = path.join(FIXTURES_ROOT, String(task['fixture']));
104
+ // Delta #9: a task may pin an external repo at a SHA instead of naming an
105
+ // in-repo fixture. `pristine_tree_for` returns the tree either way, so the
106
+ // per-trial copy below — and every endpoint that diffs against the pristine
107
+ // tree — is unchanged by which kind of task this is.
108
+ const fixture = pristine_tree_for(task, { fixturesRoot: FIXTURES_ROOT });
109
+ if (fixture === null) {
110
+ throw new Error(`task ${String(task['id'])} declares neither a \`fixture\` nor a pinned \`repo\`/\`sha\``);
111
+ }
103
112
  const dest = workspace_dir(String(task['id']), arm, seed);
104
113
  if (fs.existsSync(dest)) {
105
114
  fs.rmSync(dest, { recursive: true, force: true });
@@ -18,6 +18,7 @@ import { execFileSync } from 'node:child_process';
18
18
  import * as fs from 'node:fs';
19
19
  import * as path from 'node:path';
20
20
 
21
+ import { statePathFor } from '../before_complete_hook.js';
21
22
  import { git_common_dir } from './git_common_dir.js';
22
23
  import { gitEnv } from './git_env.js';
23
24
 
@@ -114,10 +115,9 @@ export interface Grounding extends RepoAnchor {
114
115
  last_verify: string | null;
115
116
  }
116
117
 
117
- const VERIFY_STATE_REL = path.join('agents', 'runtime', 'state', 'verify-before-complete.json');
118
118
 
119
119
  /**
120
- * The last verification this workspace recorded, as one line.
120
+ * The last verification THIS SESSION recorded, as one line.
121
121
  *
122
122
  * **What this is not:** an exit code. The roadmap step asked for "last verify
123
123
  * exit", and nothing in the tree records one — `before_complete_hook` stores
@@ -125,10 +125,34 @@ const VERIFY_STATE_REL = path.join('agents', 'runtime', 'state', 'verify-before-
125
125
  * worse than emitting less, so the field carries the command and its
126
126
  * timestamp and says so, rather than implying a pass/fail the source never
127
127
  * held.
128
+ *
129
+ * **Why the session id is a required parameter, not an optional one.** This
130
+ * function resolved `agents/runtime/state/verify-before-complete.json` for its
131
+ * whole life and returned `null` every time: the producer never wrote that
132
+ * path — pre-split it wrote `agents/state/verify-before-complete.json`, and
133
+ * after the per-session split it writes
134
+ * `agents/state/verify-before-complete/<digest>.json`. The `runtime/` segment
135
+ * was never a directory this state used, so the reader was dead on arrival
136
+ * rather than made dead by the split.
137
+ *
138
+ * The producer's state is now per-session, so a reader without a session id
139
+ * cannot address it — and there is no defensible fallback. Reading "whatever
140
+ * file is newest in the directory" would attribute a NEIGHBOURING session's
141
+ * verification to this envelope, which is the exact cross-session read the
142
+ * split exists to stop, and it would do it on the field a successor session
143
+ * uses to decide whether work was verified. So an absent id yields `null`, and
144
+ * the parameter is required so no caller can reach the dead behaviour by
145
+ * omission.
146
+ *
147
+ * The path comes from the producer's own builder (`statePathFor`), never from
148
+ * a literal repeated here: a copied path constant is what went stale, and
149
+ * copying it again with the correct value would only reset the clock on the
150
+ * same failure.
128
151
  */
129
- export function readLastVerify(root: string): string | null {
152
+ export function readLastVerify(root: string, session_id: string | null): string | null {
153
+ if (typeof session_id !== 'string' || session_id.trim() === '') return null;
130
154
  try {
131
- const raw = fs.readFileSync(path.join(root, VERIFY_STATE_REL), 'utf-8');
155
+ const raw = fs.readFileSync(path.join(root, statePathFor(session_id)), 'utf-8');
132
156
  const state = JSON.parse(raw) as Record<string, unknown>;
133
157
  const lv = state['last_verification'];
134
158
  if (typeof lv !== 'object' || lv === null || Array.isArray(lv)) return null;
@@ -142,8 +166,14 @@ export function readLastVerify(root: string): string | null {
142
166
  }
143
167
  }
144
168
 
145
- /** Collect every scripted field. Never throws — an unreadable fact is `null`. */
146
- export function collectGrounding(root: string): Grounding {
169
+ /**
170
+ * Collect every scripted field. Never throws — an unreadable fact is `null`.
171
+ *
172
+ * `session_id` is required and forwarded to `readLastVerify` — see the reason
173
+ * stated there. A caller with no id in hand passes `null` and gets a `null`
174
+ * `last_verify`, which is the honest answer rather than a neighbour's.
175
+ */
176
+ export function collectGrounding(root: string, session_id: string | null): Grounding {
147
177
  const anchor = collectRepoAnchor(root);
148
178
  const porcelain = git(root, ['status', '--porcelain']);
149
179
  if (porcelain === null) {
@@ -151,7 +181,7 @@ export function collectGrounding(root: string): Grounding {
151
181
  // this the one field in the module that asserts something it never
152
182
  // read — and it would assert it exactly when every sibling field is
153
183
  // null, so `describeDrift` is silent and nothing challenges it.
154
- return { ...anchor, uncommitted_paths: [], status_summary: null, last_verify: readLastVerify(root) };
184
+ return { ...anchor, uncommitted_paths: [], status_summary: null, last_verify: readLastVerify(root, session_id) };
155
185
  }
156
186
  const lines = porcelain.split('\n').filter((l) => l.trim().length > 0);
157
187
  const paths = lines
@@ -165,7 +195,7 @@ export function collectGrounding(root: string): Grounding {
165
195
  lines.length === 0
166
196
  ? 'clean working tree'
167
197
  : `${lines.length} uncommitted path(s)${lines.length > MAX_PATHS ? `, first ${MAX_PATHS} listed` : ''}`,
168
- last_verify: readLastVerify(root),
198
+ last_verify: readLastVerify(root, session_id),
169
199
  };
170
200
  }
171
201
 
@@ -44,6 +44,13 @@ export interface LockfileData {
44
44
  agent_config_version?: string;
45
45
  installed_at?: string;
46
46
  tools: string[];
47
+ /**
48
+ * Content fingerprint of the host artefact layers this install wrote
49
+ * (`hostLayerFingerprint.fingerprintLayers`). Absent on every lockfile
50
+ * written before the single-delivery partition, which is why the partition
51
+ * predicate treats absence as "not verified" and keeps the full projection.
52
+ */
53
+ host_layer_fingerprint?: string;
47
54
  }
48
55
 
49
56
  export const LOCKFILE_ENV = "AGENT_CONFIG_INSTALLED_LOCK";
@@ -65,6 +72,7 @@ const _VERSION_RE = /^\s*agent_config_version\s*:\s*"?([^"\s]+)"?\s*$/;
65
72
  const _SCHEMA_RE = /^\s*schema_version\s*:\s*(\d+)\s*$/;
66
73
  const _LAYOUT_RE = /^\s*install_layout_version\s*:\s*(\d+)\s*$/;
67
74
  const _INSTALLED_AT_RE = /^\s*installed_at\s*:\s*"?([^"\s]+)"?\s*$/;
75
+ const _FINGERPRINT_RE = /^\s*host_layer_fingerprint\s*:\s*"?([0-9a-f]{64})"?\s*$/;
68
76
  const _TOOL_RE = /^\s*-\s*([A-Za-z0-9_\-.]+)\s*$/;
69
77
 
70
78
  /** Expand a leading `~` like Python's `Path.expanduser()`. */
@@ -158,6 +166,12 @@ export function read_lockfile(path?: string | null): LockfileData | null {
158
166
  in_tools = false;
159
167
  continue;
160
168
  }
169
+ const fp_m = _FINGERPRINT_RE.exec(raw_line);
170
+ if (fp_m) {
171
+ data.host_layer_fingerprint = fp_m[1]!;
172
+ in_tools = false;
173
+ continue;
174
+ }
161
175
  if (raw_line.trim().startsWith("tools:")) {
162
176
  in_tools = true;
163
177
  continue;
@@ -186,14 +200,24 @@ function splitlines(text: string): string[] {
186
200
  return parts;
187
201
  }
188
202
 
189
- function _render(version: string, tools: string[], installed_at: string): string {
203
+ function _render(
204
+ version: string,
205
+ tools: string[],
206
+ installed_at: string,
207
+ fingerprint?: string | null,
208
+ ): string {
190
209
  const lines = [
191
210
  `schema_version: ${SCHEMA_VERSION}`,
192
211
  `install_layout_version: ${INSTALL_LAYOUT_VERSION}`,
193
212
  `agent_config_version: "${version}"`,
194
213
  `installed_at: "${installed_at}"`,
195
- "tools:",
196
214
  ];
215
+ // Written BEFORE `tools:` so the sequence block stays last and the parser's
216
+ // in_tools state machine needs no extra exit condition.
217
+ if (fingerprint) {
218
+ lines.push(`host_layer_fingerprint: "${fingerprint}"`);
219
+ }
220
+ lines.push("tools:");
197
221
  for (const tool of tools) {
198
222
  lines.push(` - ${tool}`);
199
223
  }
@@ -204,12 +228,26 @@ function _render(version: string, tools: string[], installed_at: string): string
204
228
  export function write_lockfile(
205
229
  version: string,
206
230
  tools: string[],
207
- options: { path?: string | null; now?: Date | null } = {},
231
+ options: {
232
+ path?: string | null;
233
+ now?: Date | null;
234
+ /**
235
+ * Host-layer content fingerprint. Omit to write no fingerprint line at
236
+ * all — never write a placeholder: absence means "not verified" to the
237
+ * partition predicate, and a placeholder would mean "verified, wrongly".
238
+ */
239
+ host_layer_fingerprint?: string | null;
240
+ } = {},
208
241
  ): string {
209
242
  const target = options.path ?? lockfile_path();
210
243
  fs.mkdirSync(path.dirname(target), { recursive: true });
211
244
  const stamp = strftime_iso_z(options.now ?? new Date());
212
- const rendered = _render(version, sorted_unique(tools), stamp);
245
+ const rendered = _render(
246
+ version,
247
+ sorted_unique(tools),
248
+ stamp,
249
+ options.host_layer_fingerprint ?? null,
250
+ );
213
251
  // Atomic write: tempfile in the same dir + rename. The same-dir constraint
214
252
  // keeps the rename atomic across all POSIX filesystems and Windows when the
215
253
  // file already exists.
@@ -316,7 +354,14 @@ export function migrate_layout(
316
354
  const version = existing.agent_config_version || current_package_version();
317
355
  const tools = [...(existing.tools ?? [])];
318
356
  const when = options.now ?? _parse_installed_at(existing.installed_at);
319
- write_lockfile(version, tools, { path: target, now: when });
357
+ // Carry the fingerprint across the migration. Dropping it would silently
358
+ // demote a verified host layer to "not verified" and turn the partition off
359
+ // on the next build — a layout migration must not change delivery.
360
+ write_lockfile(version, tools, {
361
+ path: target,
362
+ now: when,
363
+ host_layer_fingerprint: existing.host_layer_fingerprint ?? null,
364
+ });
320
365
  return {
321
366
  from: from_v,
322
367
  to: INSTALL_LAYOUT_VERSION,
@@ -0,0 +1,85 @@
1
+ /**
2
+ * The overlap notice `task generate-tools` prints after writing the project layer.
3
+ *
4
+ * Extracted from `condense.ts` rather than left inline: it added exactly 60 lines
5
+ * there and `check_source_size_budget` is a **shrink-only** ratchet, so the legal
6
+ * repair is extraction and never a re-pinned baseline. It also belongs here on its
7
+ * own terms — the notice is about the RELATIONSHIP between two host layers, not
8
+ * about condensation.
9
+ *
10
+ * WHY THE NOTICE EXISTS. `generate-tools` writes ONE of the two layers Claude Code
11
+ * loads and was silent about the other existing, while the installer's overlap gate
12
+ * runs at install time and cannot see a layer written afterwards. So the overlap is
13
+ * created by whichever producer runs LAST, and neither said so. Measured 2026-08-19
14
+ * on a freshly regenerated maintainer projection: 110 rules delivered twice, plus
15
+ * 290 skills and 40 commands sharing a name across layers of different payload
16
+ * shape. Invariant: ADR-236. Full census:
17
+ * `agents/evidence/analysis/single-delivery-partition-census.md`.
18
+ *
19
+ * ADVISORY, NEVER FAILING. `generate-tools` is the normal build step. Failing it on
20
+ * a topology the operator may not be able to change today would make the build
21
+ * unusable rather than the duplication visible. The check that can refuse is
22
+ * `check_single_delivery --enforce`.
23
+ */
24
+
25
+ import * as fs from 'node:fs';
26
+ import * as path from 'node:path';
27
+
28
+ /**
29
+ * Artefact types both host layers can carry.
30
+ *
31
+ * All four, matching `check_single_delivery` (R2 finding): this notice read three
32
+ * where the gate reads four, so a subagent duplicated across layers was invisible
33
+ * on one surface and reported on the other — two surfaces disagreeing about the
34
+ * same question is worse than one surface being silent.
35
+ */
36
+ const TYPES = ['rules', 'skills', 'commands', 'agents'] as const;
37
+
38
+ /**
39
+ * Count shared entry names per artefact type between the two layers.
40
+ *
41
+ * A type whose layers are not both readable is SKIPPED rather than reported as
42
+ * zero: one layer absent means nothing is doubled there, and that is the topology
43
+ * this notice wants, so a warning would be noise on the success case.
44
+ */
45
+ export function overlapFindings(homeDir: string, projectRoot: string): string[] {
46
+ const findings: string[] = [];
47
+ for (const type of TYPES) {
48
+ let g: string[];
49
+ let p: string[];
50
+ try {
51
+ g = fs.readdirSync(path.join(homeDir, '.claude', type));
52
+ p = fs.readdirSync(path.join(projectRoot, '.claude', type));
53
+ } catch {
54
+ continue;
55
+ }
56
+ const gset = new Set(g);
57
+ const both = p.filter((n) => gset.has(n)).length;
58
+ if (both > 0) findings.push(`${type}=${both}`);
59
+ }
60
+ return findings;
61
+ }
62
+
63
+ /** Render the notice, or `null` when there is nothing to say. */
64
+ export function overlapNotice(homeDir: string, projectRoot: string): string | null {
65
+ const findings = overlapFindings(homeDir, projectRoot);
66
+ if (findings.length === 0) return null;
67
+ return (
68
+ ` ⚠️ a global layer holds the same names (${findings.join(' ')}) — the host loads both` +
69
+ ' with no dedup, so these are delivered twice per session.' +
70
+ '\n Detail: ./scripts-run src/scripts/check_single_delivery · invariant: ADR-236'
71
+ );
72
+ }
73
+
74
+ /**
75
+ * Print the notice through the caller's own writer.
76
+ *
77
+ * The writer is injected so this stays testable and so it uses `condense.ts`'s
78
+ * summary channel rather than reaching for `process.stdout` behind its back.
79
+ */
80
+ export function warnLayerOverlap(projectRoot: string, write: (s: string) => void): void {
81
+ const home = process.env['HOME'];
82
+ if (home === undefined || home === '') return;
83
+ const notice = overlapNotice(home, projectRoot);
84
+ if (notice !== null) write(notice);
85
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Structural markdown section edits — anchored on a LINE, never on a substring.
3
+ *
4
+ * WHY THIS EXISTS. Measured 2026-08-20, and it was the single costliest error
5
+ * of that session. A script located a section with
6
+ * `text.index('## Acceptance criteria')` and sliced there. The first match was
7
+ * not the heading: it was a **quotation of that heading inside a step's prose**,
8
+ * one paragraph earlier. The slice cut mid-step, the document silently lost its
9
+ * risk register, and it took two calls to notice and two more to rebuild.
10
+ *
11
+ * Three properties made it expensive, and all three are generic:
12
+ *
13
+ * 1. the anchor was a substring, so content that *mentions* a heading is
14
+ * indistinguishable from the heading;
15
+ * 2. the edit was destructive rather than checked — one match was assumed;
16
+ * 3. nothing verified the document's shape afterwards, so the loss was silent.
17
+ *
18
+ * This module closes all three. It refuses on zero matches and on more than
19
+ * one, because "take the first" is exactly the behaviour that failed. And
20
+ * `headings()` plus `assertShapePreserved()` make a dropped section loud.
21
+ */
22
+
23
+ export class SectionError extends Error {}
24
+
25
+ /** Every ATX heading in the document, in order, as `## Title` including hashes. */
26
+ export function headings(text: string): string[] {
27
+ return text.split('\n').filter((l) => /^#{1,6}\s+\S/.test(l));
28
+ }
29
+
30
+ /** Line indices of a heading matched at LINE START and exactly. */
31
+ function headingLines(lines: readonly string[], heading: string): number[] {
32
+ const want = heading.trim();
33
+ const out: number[] = [];
34
+ lines.forEach((l, i) => {
35
+ if (l.trim() === want) {
36
+ out.push(i);
37
+ }
38
+ });
39
+ return out;
40
+ }
41
+
42
+ /** The heading's own level, e.g. 2 for `## X`. */
43
+ function levelOf(heading: string): number {
44
+ const m = /^(#{1,6})\s/.exec(heading.trim());
45
+ if (m === null) {
46
+ throw new SectionError(`not an ATX heading: ${JSON.stringify(heading)}`);
47
+ }
48
+ return (m[1] as string).length;
49
+ }
50
+
51
+ /**
52
+ * `[start, end)` line range of the section, from its heading up to the next
53
+ * heading of the same or a higher level (or EOF).
54
+ *
55
+ * Throws when the heading appears zero times or more than once — the caller
56
+ * decides what to do, and neither case may be silently resolved by taking the
57
+ * first hit.
58
+ */
59
+ export function sectionRange(text: string, heading: string): [number, number] {
60
+ const lines = text.split('\n');
61
+ const hits = headingLines(lines, heading);
62
+ if (hits.length === 0) {
63
+ throw new SectionError(`heading not found at line start: ${JSON.stringify(heading)}`);
64
+ }
65
+ if (hits.length > 1) {
66
+ throw new SectionError(
67
+ `heading is ambiguous — ${String(hits.length)} occurrences at lines ` +
68
+ `${hits.map((h) => String(h + 1)).join(', ')}: ${JSON.stringify(heading)}`,
69
+ );
70
+ }
71
+ const start = hits[0] as number;
72
+ const level = levelOf(heading);
73
+ let end = lines.length;
74
+ for (let i = start + 1; i < lines.length; i++) {
75
+ const l = lines[i] as string;
76
+ const m = /^(#{1,6})\s+\S/.exec(l);
77
+ if (m !== null && (m[1] as string).length <= level) {
78
+ end = i;
79
+ break;
80
+ }
81
+ }
82
+ return [start, end];
83
+ }
84
+
85
+ /** The section's text, heading line included. */
86
+ export function extractSection(text: string, heading: string): string {
87
+ const [s, e] = sectionRange(text, heading);
88
+ return text.split('\n').slice(s, e).join('\n');
89
+ }
90
+
91
+ /** Replace the section's body, heading line included, with `replacement`. */
92
+ export function replaceSection(text: string, heading: string, replacement: string): string {
93
+ const lines = text.split('\n');
94
+ const [s, e] = sectionRange(text, heading);
95
+ return [...lines.slice(0, s), ...replacement.split('\n'), ...lines.slice(e)].join('\n');
96
+ }
97
+
98
+ /**
99
+ * Assert that an edit did not lose a section it was not meant to touch.
100
+ *
101
+ * `allowedRemoved` / `allowedAdded` name the headings the edit intends to
102
+ * change; anything else disappearing or appearing throws. This is the check
103
+ * that would have caught the measured failure at the moment it happened rather
104
+ * than two calls later.
105
+ */
106
+ export function assertShapePreserved(
107
+ before: string,
108
+ after: string,
109
+ opts: { allowedRemoved?: readonly string[]; allowedAdded?: readonly string[] } = {},
110
+ ): void {
111
+ const norm = (h: string): string => h.trim();
112
+ const b = headings(before).map(norm);
113
+ const a = headings(after).map(norm);
114
+ const allowedRemoved = new Set((opts.allowedRemoved ?? []).map(norm));
115
+ const allowedAdded = new Set((opts.allowedAdded ?? []).map(norm));
116
+
117
+ const removed = b.filter((h) => !a.includes(h) && !allowedRemoved.has(h));
118
+ const added = a.filter((h) => !b.includes(h) && !allowedAdded.has(h));
119
+ if (removed.length > 0 || added.length > 0) {
120
+ const parts: string[] = [];
121
+ if (removed.length > 0) {
122
+ parts.push(`lost ${String(removed.length)}: ${removed.join(' · ')}`);
123
+ }
124
+ if (added.length > 0) {
125
+ parts.push(`gained ${String(added.length)}: ${added.join(' · ')}`);
126
+ }
127
+ throw new SectionError(`document shape changed unexpectedly — ${parts.join('; ')}`);
128
+ }
129
+ }