@event4u/agent-config 14.6.0 → 14.8.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 (258) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +255 -145
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/analyze/inbox.md +264 -10
  5. package/dist/agent-src/commands/analyze.md +1 -1
  6. package/dist/agent-src/commands/evals/git-pr-merge.json +13 -0
  7. package/dist/agent-src/commands/evals/roadmap.json +17 -1
  8. package/dist/agent-src/commands/pr/create.md +14 -2
  9. package/dist/agent-src/commands/pr/merge.md +379 -0
  10. package/dist/agent-src/commands/roadmap/process-full.md +272 -32
  11. package/dist/agent-src/commands/roadmap.md +1 -1
  12. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +46 -36
  13. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +37 -2
  14. package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +95 -2
  15. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +289 -106
  16. package/dist/agent-src/rules/artifact-drafting-protocol.md +5 -1
  17. package/dist/agent-src/rules/context-hygiene.md +2 -0
  18. package/dist/agent-src/rules/decision-revisit-gate.md +67 -32
  19. package/dist/agent-src/rules/fix-what-you-see.md +108 -0
  20. package/dist/agent-src/rules/no-roadmap-references.md +0 -5
  21. package/dist/agent-src/rules/recurring-criticism.md +110 -0
  22. package/dist/agent-src/rules/roadmap-progress-sync.md +20 -1
  23. package/dist/agent-src/rules/rule-type-governance.md +0 -2
  24. package/dist/agent-src/rules/self-repair-loop.md +17 -0
  25. package/dist/agent-src/rules/skill-quality.md +0 -2
  26. package/dist/agent-src/rules/source-confidentiality.md +37 -7
  27. package/dist/agent-src/scripts/archival_sweep.ts +117 -0
  28. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +26 -7
  29. package/dist/agent-src/scripts/dashboard_mode.ts +212 -0
  30. package/dist/agent-src/scripts/gate_budget.ts +282 -0
  31. package/dist/agent-src/scripts/gate_execute.ts +72 -31
  32. package/dist/agent-src/scripts/roadmap_gates.ts +74 -3
  33. package/dist/agent-src/scripts/update_roadmap_progress.ts +296 -53
  34. package/dist/agent-src/skills/adr-create/SKILL.md +124 -5
  35. package/dist/agent-src/skills/ai-council/SKILL.md +48 -807
  36. package/dist/agent-src/skills/ai-council/references/advanced-modes.md +320 -0
  37. package/dist/agent-src/skills/ai-council/references/cost-and-redaction.md +99 -0
  38. package/dist/agent-src/skills/ai-council/references/execution-modes.md +162 -0
  39. package/dist/agent-src/skills/ai-council/references/output-and-synthesis.md +125 -0
  40. package/dist/agent-src/skills/ai-council/references/procedure.md +143 -0
  41. package/dist/agent-src/skills/decision-record/SKILL.md +61 -3
  42. package/dist/agent-src/skills/decision-review/SKILL.md +65 -0
  43. package/dist/agent-src/skills/merge-conflicts/SKILL.md +26 -0
  44. package/dist/agent-src/skills/quality-tools/SKILL.md +34 -326
  45. package/dist/agent-src/skills/quality-tools/references/js-ts-tools.md +113 -0
  46. package/dist/agent-src/skills/quality-tools/references/php-tools.md +222 -0
  47. package/dist/agent-src/skills/roadmap-management/SKILL.md +15 -409
  48. package/dist/agent-src/skills/roadmap-management/references/archival.md +226 -0
  49. package/dist/agent-src/skills/roadmap-management/references/authoring.md +107 -0
  50. package/dist/agent-src/skills/roadmap-management/references/dashboard.md +47 -0
  51. package/dist/agent-src/skills/roadmap-management/references/lifecycle.md +58 -0
  52. package/dist/agent-src/skills/security/SKILL.md +8 -0
  53. package/dist/agent-src/skills/security-audit/SKILL.md +12 -1
  54. package/dist/agent-src/skills/skill-writing/SKILL.md +34 -570
  55. package/dist/agent-src/skills/skill-writing/references/procedure.md +275 -0
  56. package/dist/agent-src/skills/skill-writing/references/read-only-scripts.md +18 -0
  57. package/dist/agent-src/skills/skill-writing/references/section-patterns.md +298 -0
  58. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  59. package/dist/agent-src/templates/roadmaps.md +65 -4
  60. package/dist/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
  61. package/dist/agent-src/templates/scripts/telemetry/remote.ts +160 -7
  62. package/dist/agent-src/templates/scripts/telemetry/transport.ts +169 -0
  63. package/dist/cli/registry.js +2 -1
  64. package/dist/cli/registry.js.map +1 -1
  65. package/dist/cli-delegate/chunk-2GNWGMAL.js +172 -0
  66. package/dist/cli-delegate/{chunk-P6LQDIWK.js → chunk-7PRE6ZV2.js} +338 -0
  67. package/dist/cli-delegate/{chunk-BKO2AZ7Z.js → chunk-B2ERU2VA.js} +1 -1
  68. package/dist/cli-delegate/chunk-E4XIZDIB.js +402 -0
  69. package/dist/cli-delegate/{chunk-3UCJWMMP.js → chunk-EEJUMCDZ.js} +16 -16
  70. package/dist/cli-delegate/{chunk-NXFWFAYV.js → chunk-G23AT6GD.js} +1 -0
  71. package/dist/cli-delegate/{chunk-XURJ4AQC.js → chunk-H4VIVD4C.js} +385 -284
  72. package/dist/cli-delegate/chunk-I3EIN2YG.js +36 -0
  73. package/dist/cli-delegate/{chunk-RLN73PR5.js → chunk-LPPWO5CE.js} +12 -8
  74. package/dist/cli-delegate/chunk-MWRD5YOH.js +100 -0
  75. package/dist/cli-delegate/{chunk-UEZ7D35A.js → chunk-N7CDDGI3.js} +4 -23
  76. package/dist/cli-delegate/{chunk-63WSP6YZ.js → chunk-PMEKGDED.js} +74 -167
  77. package/dist/cli-delegate/{chunk-GRRPEPRN.js → chunk-T7L2QK3K.js} +24 -5
  78. package/dist/cli-delegate/{chunk-EVTY7ISR.js → chunk-YM4GZDJG.js} +9 -1
  79. package/dist/cli-delegate/cmd_adr_effective.js +1162 -0
  80. package/dist/cli-delegate/cmd_conformance.js +466 -716
  81. package/dist/cli-delegate/cmd_converge.js +3 -3
  82. package/dist/cli-delegate/cmd_doctor.js +10 -10
  83. package/dist/cli-delegate/cmd_explain.js +1 -1
  84. package/dist/cli-delegate/cmd_export.js +10 -9
  85. package/dist/cli-delegate/cmd_handoff.js +10 -9
  86. package/dist/cli-delegate/cmd_memory_get.js +2 -1
  87. package/dist/cli-delegate/cmd_migrate.js +2 -2
  88. package/dist/cli-delegate/cmd_packs_active.js +1 -1
  89. package/dist/cli-delegate/cmd_preflight.js +5 -5
  90. package/dist/cli-delegate/cmd_prune.js +11 -10
  91. package/dist/cli-delegate/cmd_route_audit.js +6 -5
  92. package/dist/cli-delegate/cmd_route_explain.js +3 -3
  93. package/dist/cli-delegate/cmd_session_recycle.js +840 -30
  94. package/dist/cli-delegate/cmd_settings_get.js +3 -3
  95. package/dist/cli-delegate/cmd_settings_migrate.js +10 -9
  96. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  97. package/dist/cli-delegate/cmd_sync.js +11 -10
  98. package/dist/cli-delegate/cmd_uninstall.js +13 -11
  99. package/dist/cli-delegate/cmd_update.js +4 -4
  100. package/dist/cli-delegate/cmd_upgrade.js +4 -4
  101. package/dist/cli-delegate/cmd_validate.js +11 -10
  102. package/dist/cli-delegate/cmd_versions.js +3 -3
  103. package/dist/discovery/deprecation-report.md +1 -1
  104. package/dist/discovery/discovery-manifest.json +111 -37
  105. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  106. package/dist/discovery/discovery-manifest.summary.md +5 -5
  107. package/dist/discovery/dormancy-report.md +1 -1
  108. package/dist/discovery/orphan-report.md +1 -1
  109. package/dist/discovery/packs.json +15 -12
  110. package/dist/discovery/trust-report.md +3 -3
  111. package/dist/discovery/workspaces.json +11 -8
  112. package/dist/hooks/dispatch.js +3165 -2192
  113. package/dist/install/claudePathsPlan.js +187 -0
  114. package/dist/install/claudePathsPlan.js.map +1 -0
  115. package/dist/install/globalRuleLayers.js +169 -0
  116. package/dist/install/globalRuleLayers.js.map +1 -0
  117. package/dist/install/hostLayerFingerprint.js +136 -0
  118. package/dist/install/hostLayerFingerprint.js.map +1 -0
  119. package/dist/install/install.mjs +553 -425
  120. package/dist/install/partitionEligibility.js +432 -0
  121. package/dist/install/partitionEligibility.js.map +1 -0
  122. package/dist/install/ruleLayerPartition.js +157 -0
  123. package/dist/install/ruleLayerPartition.js.map +1 -0
  124. package/dist/mcp/registry-manifest.json +3 -3
  125. package/dist/mcp/server.json +2 -2
  126. package/dist/mcp/server.mjs +1863 -1234
  127. package/dist/router.json +1 -1
  128. package/dist/scripts/_lib/agent_src.js +41 -0
  129. package/dist/scripts/_lib/agent_src.js.map +1 -1
  130. package/dist/scripts/_lib/install_layout.js +83 -0
  131. package/dist/scripts/_lib/install_layout.js.map +1 -0
  132. package/dist/scripts/_lib/installed_lock.js +444 -0
  133. package/dist/scripts/_lib/installed_lock.js.map +1 -0
  134. package/dist/scripts/_lib/prune_empty_dirs.js +85 -0
  135. package/dist/scripts/_lib/prune_empty_dirs.js.map +1 -0
  136. package/dist/scripts/condense.js +63 -251
  137. package/dist/scripts/condense.js.map +1 -1
  138. package/dist/server/schemas/settings.js +4 -0
  139. package/dist/server/schemas/settings.js.map +1 -1
  140. package/docs/contracts/settings-classes.md +12 -6
  141. package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +8 -3
  142. package/docs/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +45 -1
  143. package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +26 -0
  144. package/docs/guidelines/agent-infra/false-green.md +47 -1
  145. package/docs/guidelines/agent-infra/memory-access.md +17 -0
  146. package/docs/guidelines/agent-infra/recurring-criticism-mechanics.md +96 -0
  147. package/package.json +1 -1
  148. package/src/agent-src/scripts/archival_sweep.ts +117 -0
  149. package/src/agent-src/scripts/archive_completed_roadmaps.ts +26 -7
  150. package/src/agent-src/scripts/dashboard_mode.ts +212 -0
  151. package/src/agent-src/scripts/gate_budget.ts +282 -0
  152. package/src/agent-src/scripts/gate_execute.ts +72 -31
  153. package/src/agent-src/scripts/roadmap_gates.ts +74 -3
  154. package/src/agent-src/scripts/update_roadmap_progress.ts +296 -53
  155. package/src/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
  156. package/src/agent-src/templates/scripts/telemetry/remote.ts +160 -7
  157. package/src/agent-src/templates/scripts/telemetry/transport.ts +169 -0
  158. package/src/config/agent-settings.template.yml +27 -2
  159. package/src/config/ci-local-parity.yml +39 -0
  160. package/src/config/estate-count-budget.json +612 -3
  161. package/src/config/evaluator-budgets.json +3 -3
  162. package/src/config/gate-coverage.yml +220 -12
  163. package/src/config/gate-violation-baselines.json +73 -63
  164. package/src/config/gitignore-block.txt +15 -2
  165. package/src/config/hook-latency-budget.json +5 -0
  166. package/src/config/rule-activation-census.json +14 -6
  167. package/src/config/rule-stub-ceilings.json +34 -7
  168. package/src/install/claudePathsPlan.ts +196 -0
  169. package/src/install/globalRuleLayers.ts +197 -0
  170. package/src/install/hostLayerFingerprint.ts +145 -0
  171. package/src/install/partitionEligibility.ts +502 -0
  172. package/src/install/ruleLayerPartition.ts +212 -0
  173. package/src/scripts/_cli/cmd_adr_effective.ts +484 -0
  174. package/src/scripts/_cli/cmd_session_recycle.ts +9 -1
  175. package/src/scripts/_dispatch.bash +17 -1
  176. package/src/scripts/_lib/ac_heading.ts +30 -0
  177. package/src/scripts/_lib/adr_frontmatter.test.ts +365 -0
  178. package/src/scripts/_lib/adr_frontmatter.ts +459 -0
  179. package/src/scripts/_lib/agent_src.ts +42 -0
  180. package/src/scripts/_lib/bench_ab_pinned_repo.ts +162 -0
  181. package/src/scripts/_lib/bench_ab_workspace.ts +10 -1
  182. package/src/scripts/_lib/envelope_grounding.ts +49 -9
  183. package/src/scripts/_lib/installed_lock.ts +50 -5
  184. package/src/scripts/_lib/layer_overlap_notice.ts +11 -2
  185. package/src/scripts/_lib/md_section.ts +129 -0
  186. package/src/scripts/_lib/prune_empty_dirs.ts +80 -0
  187. package/src/scripts/_lib/scope_guard.sh +26 -3
  188. package/src/scripts/_lib/self_repair.ts +19 -0
  189. package/src/scripts/_lib/self_repair_class_b.ts +171 -0
  190. package/src/scripts/adr/evidence_census.ts +899 -0
  191. package/src/scripts/adr/regenerate_index.ts +12 -32
  192. package/src/scripts/adr_cite_check.ts +540 -39
  193. package/src/scripts/ai_council/cli_help.ts +55 -0
  194. package/src/scripts/audit_adr_coverage.ts +103 -17
  195. package/src/scripts/before_complete_hook.ts +153 -28
  196. package/src/scripts/bench_ab_v2_complexity.ts +27 -7
  197. package/src/scripts/bench_hook_latency.ts +204 -0
  198. package/src/scripts/build_archive_index.ts +34 -0
  199. package/src/scripts/check_adr_frontmatter.ts +574 -39
  200. package/src/scripts/check_hook_bundle_content.ts +219 -0
  201. package/src/scripts/check_new_adr_evidence.ts +529 -0
  202. package/src/scripts/check_references.ts +78 -0
  203. package/src/scripts/check_release_published.ts +32 -11
  204. package/src/scripts/check_rule_layer_partition.ts +531 -0
  205. package/src/scripts/check_single_delivery.ts +194 -17
  206. package/src/scripts/check_standing_rule_delivery.ts +8 -2
  207. package/src/scripts/ci_settle.ts +170 -0
  208. package/src/scripts/condense.ts +79 -259
  209. package/src/scripts/council_cli.ts +3 -3
  210. package/src/scripts/dispatch_r2_reviewer.ts +6 -14
  211. package/src/scripts/external_sources_denylist.json +4 -1
  212. package/src/scripts/hook_manifest.json +1 -1
  213. package/src/scripts/hook_manifest.yaml +77 -14
  214. package/src/scripts/hooks/block_no_verify.ts +61 -10
  215. package/src/scripts/hooks/block_unauthorized_git.ts +21 -1
  216. package/src/scripts/hooks/concern_registry.ts +4 -0
  217. package/src/scripts/hooks/dispatch_hook.ts +154 -187
  218. package/src/scripts/hooks/dispatch_issues.ts +38 -19
  219. package/src/scripts/hooks/fallback_yaml.ts +66 -0
  220. package/src/scripts/hooks/host_semantics.ts +21 -0
  221. package/src/scripts/hooks/injection_budget.ts +13 -3
  222. package/src/scripts/hooks/py_json_dumps.ts +103 -0
  223. package/src/scripts/hooks/rtk_wrap_hook.ts +8 -3
  224. package/src/scripts/hooks/run_continuation_hook.ts +8 -5
  225. package/src/scripts/hooks/ship_diff_volume_hook.ts +81 -15
  226. package/src/scripts/hooks/source_first_gate_hook.ts +464 -0
  227. package/src/scripts/hooks/state_io.ts +870 -21
  228. package/src/scripts/hooks/stdin_failure_policy.ts +96 -0
  229. package/src/scripts/hooks/subagent_ledger_hook.ts +44 -15
  230. package/src/scripts/hooks/telemetry_flush_hook.ts +139 -0
  231. package/src/scripts/hooks/telemetry_self_repair.ts +128 -0
  232. package/src/scripts/hooks/telemetry_usage_hook.ts +22 -4
  233. package/src/scripts/hooks/turn_end_gate_hook.ts +81 -15
  234. package/src/scripts/injection_scan_hook.ts +137 -5
  235. package/src/scripts/install.ts +16 -11
  236. package/src/scripts/language_mirror_hook.ts +561 -25
  237. package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
  238. package/src/scripts/lint_hook_manifest.ts +49 -0
  239. package/src/scripts/lint_plan_risk_register.ts +4 -3
  240. package/src/scripts/lint_provenance_vocabulary.ts +472 -4
  241. package/src/scripts/lint_skill_router_head.ts +4 -4
  242. package/src/scripts/mcp_server/tools.ts +2 -2
  243. package/src/scripts/new_roadmap.ts +136 -0
  244. package/src/scripts/pack_mcp_content.ts +20 -5
  245. package/src/scripts/release.ts +37 -38
  246. package/src/scripts/report_layer_overlap.ts +6 -0
  247. package/src/scripts/routing_doctor.ts +5 -1
  248. package/src/scripts/self_repair_hook.ts +37 -5
  249. package/src/scripts/session_register_hook.ts +11 -0
  250. package/src/scripts/skill_usage_collect.ts +11 -1
  251. package/src/scripts/skill_usage_report.ts +278 -8
  252. package/src/scripts/sync_pr_branch.ts +170 -20
  253. package/src/server/schemas/settings.ts +8 -0
  254. package/dist/cli-delegate/chunk-EJITJBJX.js +0 -172
  255. package/dist/cli-delegate/chunk-PZWRPRWP.js +0 -38
  256. package/dist/cli-delegate/{chunk-PPQO4NGR.js → chunk-6RJA2F5A.js} +3 -3
  257. package/dist/cli-delegate/{chunk-RYNADUFG.js → chunk-LWTYTX6F.js} +0 -0
  258. package/dist/cli-delegate/{chunk-FBKL6JWT.js → chunk-MMSGVZKV.js} +3 -3
@@ -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;
@@ -136,14 +160,30 @@ export function readLastVerify(root: string): string | null {
136
160
  const command = typeof rec['command'] === 'string' ? rec['command'] : null;
137
161
  if (!command) return null;
138
162
  const at = typeof rec['at'] === 'string' ? rec['at'] : 'unknown time';
139
- return `${command} @ ${at} (no exit status is recorded anywhere)`.slice(0, MAX_FIELD_CHARS);
163
+ // Collapse whitespace BEFORE slicing. A recorded command is whatever
164
+ // the operator ran, and a heredoc (`git commit -F - <<'MSG'`) records a
165
+ // multi-line string — so slicing to 200 chars kept the newlines, and
166
+ // `validateRecycleEnvelope`'s `isShortLine` rejects any value containing
167
+ // one. The result was that `session:recycle` refused its OWN
168
+ // machine-collected field: composing a valid envelope was impossible
169
+ // whenever the previous verification command spanned more than one line,
170
+ // and the error named `last_verify` without hinting that the composer
171
+ // had not supplied it. Reproduced 2026-08-21 on a 512-char heredoc.
172
+ const flat = `${command} @ ${at} (no exit status is recorded anywhere)`.replace(/\s+/g, ' ').trim();
173
+ return flat.slice(0, MAX_FIELD_CHARS);
140
174
  } catch {
141
175
  return null;
142
176
  }
143
177
  }
144
178
 
145
- /** Collect every scripted field. Never throws — an unreadable fact is `null`. */
146
- export function collectGrounding(root: string): Grounding {
179
+ /**
180
+ * Collect every scripted field. Never throws — an unreadable fact is `null`.
181
+ *
182
+ * `session_id` is required and forwarded to `readLastVerify` — see the reason
183
+ * stated there. A caller with no id in hand passes `null` and gets a `null`
184
+ * `last_verify`, which is the honest answer rather than a neighbour's.
185
+ */
186
+ export function collectGrounding(root: string, session_id: string | null): Grounding {
147
187
  const anchor = collectRepoAnchor(root);
148
188
  const porcelain = git(root, ['status', '--porcelain']);
149
189
  if (porcelain === null) {
@@ -151,7 +191,7 @@ export function collectGrounding(root: string): Grounding {
151
191
  // this the one field in the module that asserts something it never
152
192
  // read — and it would assert it exactly when every sibling field is
153
193
  // null, so `describeDrift` is silent and nothing challenges it.
154
- return { ...anchor, uncommitted_paths: [], status_summary: null, last_verify: readLastVerify(root) };
194
+ return { ...anchor, uncommitted_paths: [], status_summary: null, last_verify: readLastVerify(root, session_id) };
155
195
  }
156
196
  const lines = porcelain.split('\n').filter((l) => l.trim().length > 0);
157
197
  const paths = lines
@@ -165,7 +205,7 @@ export function collectGrounding(root: string): Grounding {
165
205
  lines.length === 0
166
206
  ? 'clean working tree'
167
207
  : `${lines.length} uncommitted path(s)${lines.length > MAX_PATHS ? `, first ${MAX_PATHS} listed` : ''}`,
168
- last_verify: readLastVerify(root),
208
+ last_verify: readLastVerify(root, session_id),
169
209
  };
170
210
  }
171
211
 
@@ -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,
@@ -28,12 +28,21 @@ import * as path from 'node:path';
28
28
  /**
29
29
  * Artefact types both host layers can carry.
30
30
  *
31
- * All four, matching `check_single_delivery` (R2 finding): this notice read three
31
+ * All five, matching `check_single_delivery` (R2 finding): this notice read three
32
32
  * where the gate reads four, so a subagent duplicated across layers was invisible
33
33
  * on one surface and reported on the other — two surfaces disagreeing about the
34
34
  * same question is worse than one surface being silent.
35
+ *
36
+ * `personas` was added 2026-08-21. Both lists had read four types while the
37
+ * installer's `_CLAUDE_SKILL_BUNDLE` (`install.ts:1916-1921`) ships
38
+ * rules · skills · commands · **personas** — so the one family this repository
39
+ * actually delivered from both layers was the one neither surface looked at
40
+ * (29 shared names on a freshly regenerated tree). The two lists stay
41
+ * hand-written and independent of the installer's on purpose: deriving the
42
+ * verifier's scope from the producer would make the check inherit the producer's
43
+ * omissions, which is the defect being closed here.
35
44
  */
36
- const TYPES = ['rules', 'skills', 'commands', 'agents'] as const;
45
+ const TYPES = ['rules', 'skills', 'commands', 'personas', 'user-types', 'agents'] as const;
37
46
 
38
47
  /**
39
48
  * Count shared entry names per artefact type between the two layers.
@@ -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
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Remove empty directories under a root, deepest first.
3
+ *
4
+ * ## Why this exists as a shared helper
5
+ *
6
+ * A generator that stops writing into a tree leaves the tree's DIRECTORIES
7
+ * behind. Measured 2026-08-21 while extending the ADR-236 partition to the
8
+ * colon-form commands: after the generator was gated, `.claude/commands/`
9
+ * held **0 `.md` files and 40 empty cluster directories** — and
10
+ * `check_single_delivery` counts directory NAMES, so it still reported 40
11
+ * overlapping commands against a layer that delivered none. A partition that
12
+ * stops the writes and leaves the shape is invisible to the gate that is
13
+ * supposed to confirm it.
14
+ *
15
+ * `condense.ts` already carried this loop inline for the skills tree. It is
16
+ * lifted here rather than copied because the second caller is what turns an
17
+ * inline block into a shape two producers must agree on — and because
18
+ * `condense.ts` sits above `check_source_size_budget`'s ceiling, where a
19
+ * shrink-only ratchet refuses even a small addition.
20
+ *
21
+ * Best-effort by construction: a race or a permission error on one directory is
22
+ * swallowed, matching the inline original. Pruning is cleanup, never a gate.
23
+ *
24
+ * ## Symlinks: MEASURED, because a neutral review split on it
25
+ *
26
+ * One seat called an unguarded recursion a merge blocker (a directory symlink
27
+ * pointing at an ancestor would loop forever); the other called the finding
28
+ * false, on the grounds that `Dirent.isDirectory()` does not follow the target.
29
+ * A vote cannot settle that, so it was probed — `readdirSync(dir, {withFileTypes:
30
+ * true})` over a real symlink-to-directory on darwin, Node 26:
31
+ *
32
+ * link isDirectory=false isSymbolicLink=true
33
+ * real isDirectory=true isSymbolicLink=false
34
+ *
35
+ * `Dirent` carries `lstat` semantics, so the walk below cannot descend into a
36
+ * symlink at all and no loop is reachable — and no inode set is needed. The
37
+ * second seat was right. Recorded rather than fixed, so the next reader does not
38
+ * re-raise it: a defensive `!e.isSymbolicLink()` here would be dead code
39
+ * asserting something the type already guarantees.
40
+ *
41
+ * The same property is why this is safe on a tree of symlinks: `.claude/commands`
42
+ * holds symlinks into `src/domains/**`, and pruning must not follow one out of
43
+ * the tree it was pointed at.
44
+ */
45
+ import * as fs from 'node:fs';
46
+ import * as path from 'node:path';
47
+
48
+ /** @returns the number of directories removed. */
49
+ export function pruneEmptyDirs(root: string): number {
50
+ let removed = 0;
51
+ const walk = (dir: string): void => {
52
+ let entries: fs.Dirent[];
53
+ try {
54
+ entries = fs.readdirSync(dir, { withFileTypes: true });
55
+ } catch {
56
+ return;
57
+ }
58
+ for (const e of entries) {
59
+ if (e.isDirectory()) walk(path.join(dir, e.name));
60
+ }
61
+ try {
62
+ if (fs.readdirSync(dir).length === 0) {
63
+ fs.rmdirSync(dir);
64
+ removed += 1;
65
+ }
66
+ } catch {
67
+ /* race or permission — cleanup is best-effort, mirrors the inline original */
68
+ }
69
+ };
70
+ let top: fs.Dirent[];
71
+ try {
72
+ top = fs.readdirSync(root, { withFileTypes: true });
73
+ } catch {
74
+ return 0;
75
+ }
76
+ for (const e of top) {
77
+ if (e.isDirectory()) walk(path.join(root, e.name));
78
+ }
79
+ return removed;
80
+ }
@@ -165,14 +165,37 @@ probe_tool() {
165
165
  local this_ver
166
166
  this_ver="$(this_version)"
167
167
 
168
+ # Field 6 is the duplicate-registration count, and it is printed on BOTH
169
+ # verdicts. It used to appear only on WARN, which required the two scopes to
170
+ # report the SAME version — and `installed_version_at` returns `unknown` for
171
+ # `~/.claude/…` because a host directory has no `package.json` beside it. So
172
+ # every real cross-scope install classified as DRIFT and the count never
173
+ # printed at all: the instrumentation was live and unreachable, which reads as
174
+ # coverage in a diff and is strictly worse than absent.
175
+ #
176
+ # Resolved 2026-08-20, road-to-single-delivery Phase 5, blocker
177
+ # `warn-path-unreachable-without-version-marker`, option (b). The council had
178
+ # already put the reasoning on record in the round that decided the partition:
179
+ # *"overlap reporting should not depend on version equality in the first
180
+ # place. Duplicate files can and should be reported whether versions match,
181
+ # drift, or are unknown."* A duplicate is a duplicate at any version pair.
182
+ #
183
+ # (a) — having the installer stamp a version beside every host layer — was the
184
+ # larger change and is NOT what shipped: it alters a consumer-facing surface to
185
+ # repair a reporting line. (c) — treating `unknown` as "same version" for the
186
+ # overlap half — was rejected because it weakens the DRIFT block itself.
187
+ #
188
+ # Wire compatibility: the DRIFT line gains a SIXTH field. The only structured
189
+ # consumer (`src/server/routes/wizard.ts`) destructures the first five and
190
+ # ignores the rest, so this is additive.
191
+ local overlap
192
+ overlap="$(count_overlap "$other_path" "$this_scope_path")"
168
193
  if [[ "$other_ver" == "unknown" ]] || [[ "$this_ver" == "unknown" ]] || [[ "$other_ver" != "$this_ver" ]]; then
169
- printf 'DRIFT\t%s\t%s\t%s\t%s\n' "$tool" "$other_path" "$other_ver" "$this_ver"
194
+ printf 'DRIFT\t%s\t%s\t%s\t%s\t%s\n' "$tool" "$other_path" "$other_ver" "$this_ver" "$overlap"
170
195
  else
171
196
  # Same version, so no drift — but the duplicate registration is delivered
172
197
  # twice, and field 6 is what makes that cost visible at the moment of the
173
198
  # decision instead of only in a token census nobody runs.
174
- local overlap
175
- overlap="$(count_overlap "$other_path" "$this_scope_path")"
176
199
  printf 'WARN\t%s\t%s\t%s\t%s\t%s\n' "$tool" "$other_path" "$other_ver" "$this_ver" "$overlap"
177
200
  fi
178
201
  }
@@ -338,6 +338,25 @@ const COMPLAINT_PATTERNS: readonly RegExp[] = [
338
338
  /\byou (didn'?t|did not|failed to|ignored|forgot)\b/i,
339
339
  /\bthat'?s (wrong|not right|incorrect)\b/i,
340
340
  /\byou (worked|did (it|that)) (wrong|incorrectly)\b/i,
341
+ // Recurrence phrasings. A restated complaint often carries none of the fault
342
+ // words above — "das habe ich dir schon dreimal gesagt" names no fault at
343
+ // all — so without these the SECOND and THIRD report of a defect were
344
+ // silently not records, which is exactly when the occurrences counter
345
+ // carries the most signal.
346
+ //
347
+ // Every branch requires an explicit COUNT word next to a saying-verb, and
348
+ // that is the precision knob: "nochmal" / "wieder" / "again" are deliberately
349
+ // NOT counts, because a bare repetition word fires on a large share of
350
+ // ordinary prompts ("mach das nochmal") and a spurious record becomes a
351
+ // spurious PR. Ordinals and numerals only.
352
+ /\b(schon|bereits)[^.!?]{0,30}(zum (zweiten|dritten|vierten|fünften) mal|(zwei|drei|vier|fünf)mal|mehrmals|mehrfach|\d+ ?mal)[^.!?]{0,20}(gesagt|erwähnt|geschrieben|angesprochen)\b/i,
353
+ /\b(sage|sag|sagte)[^.!?]{0,30}(zum (zweiten|dritten|vierten|fünften) mal|(zwei|drei|vier|fünf)mal|mehrmals|mehrfach|\d+ ?mal)\b/i,
354
+ /\bwie oft (soll|muss|müssen)\b/i,
355
+ // Typographic AND ASCII apostrophe: the curly form is what a real prompt
356
+ // carries, and matching only the ASCII one is a dead branch on live input.
357
+ /\bi ?(have|['’]ve)? ?already (told|said)\b/i,
358
+ /\b(told|said) (you )?(this|that|it)? ?(\w+|\d+) times?\b/i,
359
+ /\bhow many times (do|must|should) i\b/i,
341
360
  ];
342
361
 
343
362
  // Phrasings that MATCH a complaint pattern but are not complaints — a curious