@event4u/agent-config 14.21.0 → 14.23.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 (235) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +164 -187
  5. package/CREDITS.md +37 -0
  6. package/README.md +1 -1
  7. package/dist/agent-src/commands/analyze/inbox.md +380 -5
  8. package/dist/agent-src/commands/analyze/repo.md +4 -2
  9. package/dist/agent-src/commands/pr/merge.md +47 -38
  10. package/dist/agent-src/commands/roadmap/next.md +8 -0
  11. package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +69 -0
  12. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +5 -5
  13. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +12 -0
  14. package/dist/agent-src/rules/external-code-graph-interop.md +19 -14
  15. package/dist/agent-src/rules/notes-first-reasoning.md +36 -25
  16. package/dist/agent-src/rules/token-efficiency.md +26 -3
  17. package/dist/agent-src/scripts/_cli_entry.ts +51 -0
  18. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +120 -27
  19. package/dist/agent-src/scripts/roadmap_gates.ts +4 -20
  20. package/dist/agent-src/scripts/roadmap_set_step.ts +5 -2
  21. package/dist/agent-src/scripts/stubs_due.ts +4 -18
  22. package/dist/agent-src/scripts/update_roadmap_progress.ts +7 -24
  23. package/dist/agent-src/skills/code-intelligence/SKILL.md +82 -1
  24. package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +9 -0
  25. package/dist/agent-src/skills/roadmap-writing/SKILL.md +17 -0
  26. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +52 -2
  27. package/dist/agent-src/templates/agent-settings.md +0 -1
  28. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  29. package/dist/cli/registry.js +1 -1
  30. package/dist/cli-delegate/_cli_entry.js +8 -0
  31. package/dist/cli-delegate/_lib/pin_resolver.js +170 -0
  32. package/dist/cli-delegate/archival_sweep.js +8 -0
  33. package/dist/cli-delegate/archive_completed_roadmaps.js +542 -0
  34. package/dist/cli-delegate/check_update_banner.js +923 -0
  35. package/dist/cli-delegate/{chunk-ZVBOHSVF.js → chunk-2KKXJW25.js} +23 -9
  36. package/dist/cli-delegate/chunk-5EK5WQN6.js +28 -0
  37. package/dist/cli-delegate/chunk-7XGD2OEU.js +96 -0
  38. package/dist/cli-delegate/chunk-DBH4AHDK.js +155 -0
  39. package/dist/cli-delegate/{chunk-4FS4WRXP.js → chunk-IAI3XL43.js} +15 -3
  40. package/dist/cli-delegate/chunk-JO25G5H7.js +20 -0
  41. package/dist/cli-delegate/{chunk-643Z7FIC.js → chunk-KCNTUSPI.js} +7 -1
  42. package/dist/cli-delegate/chunk-LTYVIY76.js +7961 -0
  43. package/dist/cli-delegate/chunk-QAJS5YWY.js +999 -0
  44. package/dist/cli-delegate/{chunk-YS6XTLEK.js → chunk-TYSY33MD.js} +56 -6
  45. package/dist/cli-delegate/chunk-ULET4BYS.js +328 -0
  46. package/dist/cli-delegate/chunk-YDQSHTD7.js +187 -0
  47. package/dist/cli-delegate/chunk-YWVXLR44.js +48 -0
  48. package/dist/cli-delegate/chunk-ZZQBUHFK.js +273 -0
  49. package/dist/cli-delegate/cmd_conformance.js +2 -2
  50. package/dist/cli-delegate/cmd_export.js +2 -2
  51. package/dist/cli-delegate/cmd_handoff.js +45 -13
  52. package/dist/cli-delegate/cmd_prune.js +2 -2
  53. package/dist/cli-delegate/cmd_session_recycle.js +4 -14
  54. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  55. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  56. package/dist/cli-delegate/cmd_sync.js +2 -2
  57. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  58. package/dist/cli-delegate/cmd_validate.js +2 -2
  59. package/dist/cli-delegate/dashboard_mode.js +16 -0
  60. package/dist/cli-delegate/gate_budget.js +24 -0
  61. package/dist/cli-delegate/gate_execute.js +25 -0
  62. package/dist/cli-delegate/guarded_baseline.js +18 -0
  63. package/dist/cli-delegate/resume_probe.js +22 -0
  64. package/dist/cli-delegate/roadmap_gates.js +638 -0
  65. package/dist/cli-delegate/roadmap_set_step.js +173 -0
  66. package/dist/cli-delegate/stub_queue.js +47 -0
  67. package/dist/cli-delegate/stubs_due.js +38 -0
  68. package/dist/cli-delegate/ticket_bundles.js +59 -0
  69. package/dist/cli-delegate/update_roadmap_progress.js +60 -0
  70. package/dist/discovery/deprecation-report.md +1 -1
  71. package/dist/discovery/discovery-manifest.json +13 -13
  72. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  73. package/dist/discovery/discovery-manifest.summary.md +1 -1
  74. package/dist/discovery/dormancy-report.md +1 -1
  75. package/dist/discovery/orphan-report.md +1 -1
  76. package/dist/discovery/packs.json +2 -2
  77. package/dist/discovery/trust-report.md +1 -1
  78. package/dist/discovery/workspaces.json +2 -2
  79. package/dist/hooks/dispatch.js +1813 -946
  80. package/dist/install/claudeLayerCarriage.js +237 -0
  81. package/dist/install/claudePathsPlan.js +79 -0
  82. package/dist/install/hostLayerFingerprint.js +1 -1
  83. package/dist/install/install.mjs +21 -3
  84. package/dist/install/partitionEligibility.js +231 -117
  85. package/dist/install/ruleLayerPartition.js +10 -7
  86. package/dist/mcp/registry-manifest.json +2 -2
  87. package/dist/mcp/server.json +2 -2
  88. package/dist/mcp/server.mjs +10870 -7641
  89. package/dist/scripts/_lib/lean_projection_mode.js +108 -0
  90. package/dist/scripts/_lib/router_match.js +243 -0
  91. package/dist/scripts/_lib/rule_injection.js +216 -0
  92. package/dist/scripts/condense.js +52 -45
  93. package/dist/scripts/project_thin_rules.js +139 -19
  94. package/dist/server/schemas/settings.js +16 -3
  95. package/docs/MIGRATION.md +105 -1
  96. package/docs/contracts/settings-classes.md +40 -8
  97. package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +73 -0
  98. package/docs/guidelines/agent-infra/notes-horizon-mechanics.md +90 -0
  99. package/llms.txt +1 -1
  100. package/package.json +7 -4
  101. package/src/agent-src/scripts/_cli_entry.ts +51 -0
  102. package/src/agent-src/scripts/archive_completed_roadmaps.ts +120 -27
  103. package/src/agent-src/scripts/roadmap_gates.ts +4 -20
  104. package/src/agent-src/scripts/roadmap_set_step.ts +5 -2
  105. package/src/agent-src/scripts/stubs_due.ts +4 -18
  106. package/src/agent-src/scripts/update_roadmap_progress.ts +7 -24
  107. package/src/config/agent-settings.template.yml +47 -1
  108. package/src/config/continuity-surface.json +516 -0
  109. package/src/config/estate-count-budget.json +1 -1
  110. package/src/config/evaluator-budgets.json +4 -4
  111. package/src/config/gate-coverage.yml +29 -8
  112. package/src/config/gate-violation-baselines.json +8 -8
  113. package/src/config/gitignore-block.txt +19 -0
  114. package/src/config/hook-token-budget.json +7 -6
  115. package/src/config/host-injection-effect.json +73 -0
  116. package/src/config/packed-binary-manifest.json +33 -0
  117. package/src/config/preamble-payload-budget.json +112 -105
  118. package/src/config/publish-surface.json +4 -2
  119. package/src/config/routing-coverage-seed.json +9 -2
  120. package/src/install/claudeLayerCarriage.ts +273 -0
  121. package/src/install/claudePathsPlan.ts +76 -0
  122. package/src/install/hostLayerFingerprint.ts +1 -1
  123. package/src/install/partitionEligibility.ts +261 -127
  124. package/src/install/ruleLayerPartition.ts +19 -23
  125. package/src/scripts/_cli/cmd_session_recycle.ts +2 -25
  126. package/src/scripts/_cli/handoff_sessions.ts +14 -44
  127. package/src/scripts/_dispatch.bash +77 -27
  128. package/src/scripts/_lib/auto_dispatch.ts +2 -2
  129. package/src/scripts/_lib/catalogue_layer_parity.ts +146 -0
  130. package/src/scripts/_lib/continuity_slot.ts +295 -0
  131. package/src/scripts/_lib/continuity_surface.ts +296 -0
  132. package/src/scripts/_lib/continuity_writer.ts +253 -0
  133. package/src/scripts/_lib/corpus_manifest.ts +26 -9
  134. package/src/scripts/_lib/hook_settings.ts +86 -1
  135. package/src/scripts/_lib/injection_effect.ts +151 -0
  136. package/src/scripts/_lib/layer_overlap_notice.ts +50 -6
  137. package/src/scripts/_lib/lean_projection_mode.ts +127 -0
  138. package/src/scripts/_lib/packed_binary_predicate.ts +262 -0
  139. package/src/scripts/_lib/pin_resolver.ts +13 -0
  140. package/src/scripts/_lib/recycle_envelope_paths.ts +109 -21
  141. package/src/scripts/_lib/regression_neighbourhood.ts +159 -22
  142. package/src/scripts/_lib/retired_status.ts +74 -0
  143. package/src/scripts/_lib/session_eol.ts +44 -0
  144. package/src/scripts/_lib/session_index_trust.ts +346 -0
  145. package/src/scripts/_lib/skill_catalogue.ts +156 -21
  146. package/src/scripts/_lib/standing_bound_ratchet.ts +154 -0
  147. package/src/scripts/_lib/subagent_capsule.ts +117 -8
  148. package/src/scripts/_lib/vendored_grammar_upstream.ts +280 -0
  149. package/src/scripts/ai_council/api_transport.ts +5 -0
  150. package/src/scripts/ai_council/deliberation_metrics.ts +353 -0
  151. package/src/scripts/ai_council/prompts.ts +9 -0
  152. package/src/scripts/ai_council/synthesis_disagreement.ts +206 -0
  153. package/src/scripts/audit_mcp_tools.ts +69 -25
  154. package/src/scripts/build_mcp_catalog.ts +24 -1
  155. package/src/scripts/capture_skill_catalogue.ts +24 -11
  156. package/src/scripts/check_candidate_lines.ts +499 -0
  157. package/src/scripts/check_continuity_surface.ts +475 -0
  158. package/src/scripts/check_dependency_floors.ts +13 -6
  159. package/src/scripts/check_estate_count.ts +40 -79
  160. package/src/scripts/check_host_tree_parity.ts +291 -0
  161. package/src/scripts/check_pack_size.ts +262 -23
  162. package/src/scripts/check_preamble_payload_budget.ts +136 -7
  163. package/src/scripts/check_roadmap_trackable.ts +19 -16
  164. package/src/scripts/check_rule_layer_partition.ts +54 -34
  165. package/src/scripts/check_rule_projection_integrity.ts +102 -2
  166. package/src/scripts/check_single_delivery.ts +97 -31
  167. package/src/scripts/check_update_banner.ts +13 -0
  168. package/src/scripts/ci_settle.ts +205 -5
  169. package/src/scripts/code_graph/build.ts +217 -15
  170. package/src/scripts/code_graph/cli.ts +334 -15
  171. package/src/scripts/code_graph/detect.ts +38 -0
  172. package/src/scripts/code_graph/extract.ts +11 -0
  173. package/src/scripts/code_graph/grammar_probe.ts +43 -0
  174. package/src/scripts/code_graph/loader.ts +88 -14
  175. package/src/scripts/code_graph/query.ts +144 -11
  176. package/src/scripts/code_graph/resolution_tiers.ts +238 -0
  177. package/src/scripts/code_graph/sqlite_store.ts +262 -6
  178. package/src/scripts/code_graph/types.ts +117 -3
  179. package/src/scripts/code_graph/validate.ts +26 -1
  180. package/src/scripts/code_graph/verbs.ts +590 -0
  181. package/src/scripts/condense.ts +54 -42
  182. package/src/scripts/corpus_manifest.ts +5 -5
  183. package/src/scripts/council_cli.ts +11 -12
  184. package/src/scripts/count_intent_disagreement.ts +426 -0
  185. package/src/scripts/handoff_context_hook.ts +118 -1
  186. package/src/scripts/hook_effect_doctor.ts +82 -1
  187. package/src/scripts/hook_manifest.json +1 -1
  188. package/src/scripts/hook_manifest.yaml +87 -12
  189. package/src/scripts/hooks/block_speaking_inbox_dir.ts +182 -16
  190. package/src/scripts/hooks/chain_nudge_hook.ts +282 -0
  191. package/src/scripts/hooks/code_graph_context_hook.ts +201 -0
  192. package/src/scripts/hooks/concern_registry.ts +4 -2
  193. package/src/scripts/hooks/host_semantics.ts +1 -1
  194. package/src/scripts/hooks/orchestration_record_hook.ts +2 -2
  195. package/src/scripts/hooks/push_settle_hook.ts +3 -2
  196. package/src/scripts/hooks/rule_inject_hook.ts +108 -21
  197. package/src/scripts/hooks/session_eol_hook.ts +120 -2
  198. package/src/scripts/hooks/skill_route_hook.ts +11 -4
  199. package/src/scripts/hot_context_hook.ts +62 -7
  200. package/src/scripts/inbox_source_census.ts +322 -0
  201. package/src/scripts/install-hooks.sh +82 -0
  202. package/src/scripts/{lint_carrier_integrity.ts → lint_deferral_integrity.ts} +60 -214
  203. package/src/scripts/lint_plan_risk_register.ts +13 -17
  204. package/src/scripts/lint_roadmap_blockers.ts +164 -1
  205. package/src/scripts/mcp_server/consumer_tool_catalog.json +152 -2
  206. package/src/scripts/mcp_server/graph_tools.ts +294 -0
  207. package/src/scripts/mcp_server/path_util.ts +31 -0
  208. package/src/scripts/mcp_server/suggest_skill.ts +72 -0
  209. package/src/scripts/mcp_server/tool_catalog_source.ts +7 -1
  210. package/src/scripts/mcp_server/tools.ts +5 -69
  211. package/src/scripts/media/lib/adapter-contract.md +7 -2
  212. package/src/scripts/media/lib/fixtures/flux/asset-0001.png +0 -0
  213. package/src/scripts/media/lib/fixtures/gemini-image/asset-0001.png +0 -0
  214. package/src/scripts/media/lib/fixtures/ideogram/asset-0001.png +0 -0
  215. package/src/scripts/model_rule_injection.ts +61 -4
  216. package/src/scripts/project_thin_rules.ts +149 -16
  217. package/src/scripts/rdp_candidates_delta.ts +276 -0
  218. package/src/scripts/rdp_quality_eval.ts +217 -19
  219. package/src/scripts/release_mix_taxonomy.json +3 -2
  220. package/src/scripts/report_host_injection_effect.ts +189 -0
  221. package/src/scripts/report_layer_overlap.ts +41 -0
  222. package/src/scripts/report_skill_menu_census.ts +341 -0
  223. package/src/scripts/report_standing_payload_by_host.ts +650 -0
  224. package/src/scripts/schemas/agent-settings.schema.json +7 -1
  225. package/src/scripts/session_memory_index.ts +81 -8
  226. package/src/scripts/skill_tools/score_skill_relevance.ts +70 -6
  227. package/src/scripts/skill_tools/suggest_skill_for_task.ts +37 -3
  228. package/src/server/schemas/settings.ts +23 -4
  229. package/src/vendor/grammars/README.md +86 -0
  230. package/src/vendor/grammars/tree-sitter-javascript.wasm +0 -0
  231. package/src/vendor/grammars/tree-sitter-php.wasm +0 -0
  232. package/src/vendor/grammars/tree-sitter-typescript.wasm +0 -0
  233. package/src/scripts/_lib/carrier_status.ts +0 -118
  234. package/src/scripts/_lib/estate_carrier_cases.ts +0 -72
  235. package/src/scripts/hooks/code_graph_nudge_hook.ts +0 -191
@@ -0,0 +1,426 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * count_intent_disagreement.ts — the counter `mandated-lines.md` asks for.
4
+ *
5
+ * `src/agent-src/contexts/execution/mandated-lines.md` § Honest scope names one
6
+ * cheap observation and then names its own precondition:
7
+ *
8
+ * "track how often an emitted intent line has its three slots **disagree**.
9
+ * If disagreements are effectively never found, the line is decorating
10
+ * decisions already made and the set should shrink. [...] That observation
11
+ * costs one counter and is the first thing to look at before adding a sixth
12
+ * line."
13
+ *
14
+ * This is that counter. It answers a question no eval in this tree measures:
15
+ * whether the five EXISTING mandated lines earn their place — as opposed to
16
+ * whether a sixth would.
17
+ *
18
+ * WHAT IT CAN AND CANNOT DECIDE, stated here rather than discovered later.
19
+ * "Disagree" is a semantic relation between three claims, and no lexical pass
20
+ * decides it. So the counter reports two layers and never conflates them:
21
+ *
22
+ * Layer 1, deterministic. Population size, malformed lines (fewer than three
23
+ * slots), and RESTATEMENT lines — slots whose content-word sets overlap so
24
+ * heavily that the line reads the same three ways. That shape is the
25
+ * contract's own named failure ("A line that reads the same three ways is a
26
+ * line that was written after deciding"), so it is the one disagreement-
27
+ * adjacent signal a machine can own. `distinct` is NOT `disagree`: three
28
+ * slots can be phrased differently and still say the same thing.
29
+ *
30
+ * Layer 2, rated and opt-in. A model rater classifies each well-formed line
31
+ * as agree / disagree / undecidable. Only meaningful over a non-empty
32
+ * population, and deliberately not wired here — the rater path already
33
+ * exists in `rdp_quality_eval.ts --score-only` and duplicating its billing
34
+ * guard would be a second unguarded way to bill the same account.
35
+ *
36
+ * NO THRESHOLD IS SET IN ADVANCE. The split AI council of 2026-09-07 flagged a
37
+ * proposed "≥ 80 % zero disagreement = ceremony" figure as invented; what
38
+ * counts as ceremony is read off the distribution once one exists. This tool
39
+ * therefore reports a distribution and never a verdict.
40
+ *
41
+ * NOT WIRED INTO THE `ci` CORPUS SWEEP, on the same grounds
42
+ * `lint_mandated_lines.ts` states for itself: there is no standing tree of run
43
+ * reports to scan, and a gate pointed at a root that does not accumulate is
44
+ * the dead-scan-root shape this repository has already paid for. It is a
45
+ * callable counter, invoked when someone wants the reading.
46
+ *
47
+ * An EMPTY population exits 2, not 0. A rate over nothing is not a reading —
48
+ * the same call `rdp_quality_eval --score-only` makes when a results file
49
+ * carries no transcript text.
50
+ *
51
+ * Sources are supplied explicitly so the reading is reproducible and the tool
52
+ * stays hermetic under test:
53
+ *
54
+ * ./scripts-run src/scripts/count_intent_disagreement --git-log
55
+ * ./scripts-run src/scripts/count_intent_disagreement --pr-bodies prs.json
56
+ * ./scripts-run src/scripts/count_intent_disagreement --dir path/to/reports
57
+ * ./scripts-run src/scripts/count_intent_disagreement --stdin < report.md
58
+ *
59
+ * Gather the durable population for this repository with:
60
+ *
61
+ * gh pr list --state merged --limit 400 --json number,body > prs.json
62
+ *
63
+ * Exit: 0 population read and classified · 1 usage/IO error · 2 empty population.
64
+ */
65
+
66
+ import { execFileSync } from 'node:child_process';
67
+ import * as fs from 'node:fs';
68
+ import * as path from 'node:path';
69
+ import { fileURLToPath, pathToFileURL } from 'node:url';
70
+
71
+ import { stripFences, unwrapLines } from './lint_mandated_lines.js';
72
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
73
+
74
+ const _HERE = fileURLToPath(import.meta.url);
75
+ const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
76
+
77
+ /** The slot separator the contract specifies. */
78
+ const SLOT_SEPARATOR = '·';
79
+
80
+ /**
81
+ * `Intent: …` — EMPHASIS-TOLERANT, and deliberately NOT the shipped `INTENT_RE`.
82
+ *
83
+ * The first version of this counter imported `INTENT_RE` from
84
+ * `lint_mandated_lines.ts` on the reasoning that reusing the gate's own pattern
85
+ * makes the reading authoritative. That was backwards. The shipped pattern
86
+ * anchors the bare label at line start and matches no emphasis, so
87
+ * `**Intent:**` — the form an assistant actually writes for a labelled line —
88
+ * is invisible to it. Measured 2026-09-08: the paid treatment run emitted its
89
+ * one compliant candidates line as `**Candidates:**` and a pattern of the
90
+ * shipped shape scored it absent.
91
+ *
92
+ * A COUNTER MUST BE WIDER THAN THE GATE IT REPORTS ON. Sharing the gate's blind
93
+ * spot makes the null self-confirming: the gate cannot see the line, so the
94
+ * counter cannot see the line, so the counter reports that the line is not
95
+ * emitted — which is a fact about the regex, not about the tree.
96
+ *
97
+ * The shipped gap is REAL and is recorded as a finding rather than fixed here:
98
+ * `lint_mandated_lines.ts` is a shipped gate with its own 19 tests, sitting
99
+ * against the contract the 2026-09-07 council blocked from changing, so
100
+ * widening its discrimination is its own change with its own review.
101
+ *
102
+ * CASE-SENSITIVE, and that is the other half of the correction. Widening the
103
+ * pattern with an `i` flag took the count from 0 to 172, and essentially every
104
+ * new match was a lowercase `intent:` YAML key in a prompt-pattern or command
105
+ * config — a line anchor turned into a prose detector. The contract specifies
106
+ * the label `Intent:` capitalised, an emitted line carries it that way, and a
107
+ * lowercase `intent:` at line start is data. Widening in one direction and not
108
+ * checking the other would have replaced a false null with a false population,
109
+ * which is the worse error: it looks like evidence.
110
+ */
111
+ export const INTENT_LABEL_RE =
112
+ /^[ \t]*(?:>[ \t]*)?(?:[-*+][ \t]+)?(?:\*\*|__|\*|_)?Intent(?:\*\*|__|\*|_)?(?:[ \t]*\([^)]*\))?(?:\*\*|__|\*|_)?:(?:\*\*|__|\*|_)?[ \t]*(.+)$/gm;
113
+
114
+ /**
115
+ * Words carrying no discriminating content.
116
+ *
117
+ * Kept small on purpose. A large stop list makes two slots look identical
118
+ * because everything distinguishing them was filtered out, which would inflate
119
+ * the restatement count — the number this tool exists to report honestly.
120
+ */
121
+ const STOPWORDS = new Set([
122
+ 'a', 'an', 'and', 'as', 'at', 'be', 'but', 'by', 'for', 'from', 'in', 'is',
123
+ 'it', 'its', 'of', 'on', 'or', 'that', 'the', 'to', 'was', 'were', 'with',
124
+ 'says', 'say', 'expects', 'expect', 'does', 'do', 'should',
125
+ ]);
126
+
127
+ export type LineClass = 'malformed' | 'restatement' | 'distinct';
128
+
129
+ export interface ClassifiedLine {
130
+ /** Where it came from, for the report. */
131
+ source: string;
132
+ /** The raw text after the `Intent:` label. */
133
+ body: string;
134
+ slots: string[];
135
+ klass: LineClass;
136
+ /** Max pairwise Jaccard overlap across the three slots, for the distribution. */
137
+ maxOverlap: number;
138
+ }
139
+
140
+ export interface Population {
141
+ lines: ClassifiedLine[];
142
+ /** Per-source count, so a source contributing nothing is visible. */
143
+ bySource: Record<string, number>;
144
+ }
145
+
146
+ /** Content words of a slot, lower-cased, stop-words and punctuation dropped. */
147
+ export function contentWords(slot: string): Set<string> {
148
+ const words = slot
149
+ .toLowerCase()
150
+ .replace(/`[^`]*`/g, (m) => m.replace(/`/g, ''))
151
+ .split(/[^a-z0-9_]+/)
152
+ .filter((w) => w.length > 2 && !STOPWORDS.has(w));
153
+ return new Set(words);
154
+ }
155
+
156
+ /** Jaccard overlap of two content-word sets. Empty-vs-anything is 0. */
157
+ export function overlap(a: Set<string>, b: Set<string>): number {
158
+ if (a.size === 0 || b.size === 0) return 0;
159
+ let shared = 0;
160
+ for (const w of a) if (b.has(w)) shared += 1;
161
+ const union = a.size + b.size - shared;
162
+ return union === 0 ? 0 : shared / union;
163
+ }
164
+
165
+ /**
166
+ * The overlap above which two slots are read as restating each other.
167
+ *
168
+ * A STATED DEFAULT, not a measured optimum — said plainly rather than implying
169
+ * a derivation it does not have. It is reported alongside every reading so a
170
+ * later reader can recompute at a different value from the published
171
+ * `maxOverlap` per line, which is why the per-line number is in the output.
172
+ */
173
+ export const RESTATEMENT_OVERLAP = 0.6;
174
+
175
+ export function classify(body: string, source: string): ClassifiedLine {
176
+ const slots = body.split(SLOT_SEPARATOR).map((s) => s.trim()).filter((s) => s !== '');
177
+ if (slots.length < 3) {
178
+ return { source, body, slots, klass: 'malformed', maxOverlap: 0 };
179
+ }
180
+ const sets = slots.map(contentWords);
181
+ let maxOverlap = 0;
182
+ for (let i = 0; i < sets.length; i++) {
183
+ for (let j = i + 1; j < sets.length; j++) {
184
+ maxOverlap = Math.max(maxOverlap, overlap(sets[i] as Set<string>, sets[j] as Set<string>));
185
+ }
186
+ }
187
+ maxOverlap = Math.round(maxOverlap * 1000) / 1000;
188
+ return {
189
+ source,
190
+ body,
191
+ slots,
192
+ klass: maxOverlap >= RESTATEMENT_OVERLAP ? 'restatement' : 'distinct',
193
+ maxOverlap,
194
+ };
195
+ }
196
+
197
+ /**
198
+ * Extract emitted intent lines from one text.
199
+ *
200
+ * Reuses `lint_mandated_lines`' own regex and its two preparation passes
201
+ * verbatim. That matters more than it looks: `stripFences` is what stops the
202
+ * contract's own illustrative example from counting as an emitted line, and
203
+ * this counter's whole finding turns on that distinction.
204
+ */
205
+ export function extract(text: string, source: string): ClassifiedLine[] {
206
+ const prepared = unwrapLines(stripFences(text));
207
+ INTENT_LABEL_RE.lastIndex = 0;
208
+ return [...prepared.matchAll(INTENT_LABEL_RE)].map((m) =>
209
+ classify((m[1] ?? '').replace(/(?:\*\*|__|\*|_)+[ \t]*$/, '').trim(), source),
210
+ );
211
+ }
212
+
213
+ /**
214
+ * Decode a JSONL transcript into the prose it actually carries.
215
+ *
216
+ * WHY THIS IS NOT OPTIONAL. A session transcript is newline-delimited JSON, so
217
+ * the assistant prose inside it carries `\n` as a two-character ESCAPE, not as
218
+ * a newline. A line-anchored regex therefore matches nothing in a raw `.jsonl`
219
+ * and the source reads zero — a false null that looks exactly like a real one.
220
+ * The first version of this tool had that bug and reported `dir: 0` over 4,909
221
+ * transcript files while a plain grep found five candidate labels in them.
222
+ *
223
+ * Every string leaf is concatenated with a real newline between values, so a
224
+ * label that began a line in the original prose begins a line here too.
225
+ */
226
+ export function decodeJsonl(raw: string): string {
227
+ const chunks: string[] = [];
228
+ const visit = (v: unknown): void => {
229
+ if (typeof v === 'string') chunks.push(v);
230
+ else if (Array.isArray(v)) for (const x of v) visit(x);
231
+ else if (v !== null && typeof v === 'object') for (const x of Object.values(v)) visit(x);
232
+ };
233
+ for (const line of raw.split('\n')) {
234
+ if (line.trim() === '') continue;
235
+ try {
236
+ visit(JSON.parse(line));
237
+ } catch {
238
+ // Not a JSON record — treat the line as prose rather than dropping
239
+ // it, so a mixed or truncated file degrades to the plain-text read.
240
+ chunks.push(line);
241
+ }
242
+ }
243
+ return chunks.join('\n');
244
+ }
245
+
246
+ function walkFiles(root: string): string[] {
247
+ const found: string[] = [];
248
+ const stack = [root];
249
+ while (stack.length > 0) {
250
+ const dir = stack.pop() as string;
251
+ let entries: fs.Dirent[];
252
+ try {
253
+ entries = fs.readdirSync(dir, { withFileTypes: true });
254
+ } catch {
255
+ continue;
256
+ }
257
+ for (const e of entries) {
258
+ const p = path.join(dir, e.name);
259
+ if (e.isDirectory()) stack.push(p);
260
+ else if (e.isFile()) found.push(p);
261
+ }
262
+ }
263
+ return found.sort();
264
+ }
265
+
266
+ export function gather(argv: readonly string[]): Population | { error: string } {
267
+ const lines: ClassifiedLine[] = [];
268
+ const bySource: Record<string, number> = {};
269
+ const add = (source: string, text: string): void => {
270
+ const got = extract(text, source);
271
+ bySource[source] = (bySource[source] ?? 0) + got.length;
272
+ lines.push(...got);
273
+ };
274
+
275
+ if (argv.includes('--git-log')) {
276
+ try {
277
+ const body = execFileSync('git', ['log', '--all', '--format=%B'], {
278
+ cwd: REPO_ROOT,
279
+ encoding: 'utf-8',
280
+ maxBuffer: 256 * 1024 * 1024,
281
+ });
282
+ add('git-log', body);
283
+ } catch (e) {
284
+ return { error: `--git-log failed: ${String(e)}` };
285
+ }
286
+ }
287
+
288
+ const prIdx = argv.indexOf('--pr-bodies');
289
+ if (prIdx !== -1) {
290
+ const p = argv[prIdx + 1];
291
+ if (p === undefined) return { error: '--pr-bodies needs a path to a `gh pr list --json body` dump' };
292
+ try {
293
+ const raw = JSON.parse(fs.readFileSync(path.resolve(p), 'utf-8')) as Array<{ body?: string | null }>;
294
+ bySource['pr-bodies'] = 0;
295
+ for (const pr of raw) {
296
+ if (typeof pr.body === 'string' && pr.body !== '') add('pr-bodies', pr.body);
297
+ }
298
+ // A dump of N bodies that yields zero lines must still record that N
299
+ // bodies were read — otherwise an empty result is indistinguishable
300
+ // from a source that was never consulted.
301
+ process.stdout.write(` pr-bodies: read ${String(raw.length)} PR body/bodies\n`);
302
+ } catch (e) {
303
+ return { error: `cannot read ${p}: ${String(e)}` };
304
+ }
305
+ }
306
+
307
+ const dirIdx = argv.indexOf('--dir');
308
+ if (dirIdx !== -1) {
309
+ const d = argv[dirIdx + 1];
310
+ if (d === undefined) return { error: '--dir needs a path' };
311
+ const files = walkFiles(path.resolve(d));
312
+ bySource['dir'] = 0;
313
+ let jsonl = 0;
314
+ for (const f of files) {
315
+ try {
316
+ const raw = fs.readFileSync(f, 'utf-8');
317
+ if (f.endsWith('.jsonl') || f.endsWith('.json')) {
318
+ jsonl += 1;
319
+ add('dir', decodeJsonl(raw));
320
+ } else {
321
+ add('dir', raw);
322
+ }
323
+ } catch {
324
+ continue;
325
+ }
326
+ }
327
+ process.stdout.write(
328
+ ` dir: read ${String(files.length)} file(s) under ${d} ` +
329
+ `(${String(jsonl)} JSON/JSONL, string leaves decoded)\n`,
330
+ );
331
+ }
332
+
333
+ if (argv.includes('--stdin')) {
334
+ try {
335
+ add('stdin', fs.readFileSync(0, 'utf-8'));
336
+ } catch (e) {
337
+ return { error: `cannot read stdin: ${String(e)}` };
338
+ }
339
+ }
340
+
341
+ if (Object.keys(bySource).length === 0) {
342
+ return {
343
+ error:
344
+ 'no source selected. Pass at least one of --git-log, --pr-bodies <file>, --dir <path>, --stdin.\n' +
345
+ 'Gather this repository\'s durable population with:\n' +
346
+ ' gh pr list --state merged --limit 400 --json number,body > prs.json',
347
+ };
348
+ }
349
+ return { lines, bySource };
350
+ }
351
+
352
+ export function report(pop: Population, write: (s: string) => void): void {
353
+ const total = pop.lines.length;
354
+ const counts: Record<LineClass, number> = { malformed: 0, restatement: 0, distinct: 0 };
355
+ for (const l of pop.lines) counts[l.klass] += 1;
356
+
357
+ write('\nintent-line population\n');
358
+ for (const [src, n] of Object.entries(pop.bySource)) {
359
+ write(` ${src}: ${String(n)} emitted intent line(s)\n`);
360
+ }
361
+ write(` total: ${String(total)}\n`);
362
+ if (total === 0) return;
363
+
364
+ write('\nlayer 1 — deterministic classification\n');
365
+ write(` malformed (<3 slots): ${String(counts.malformed)}\n`);
366
+ write(` restatement (overlap >= ${RESTATEMENT_OVERLAP}): ${String(counts.restatement)}\n`);
367
+ write(` distinct: ${String(counts.distinct)}\n`);
368
+ const wellFormed = total - counts.malformed;
369
+ if (wellFormed > 0) {
370
+ const rate = Math.round((counts.restatement / wellFormed) * 1000) / 10;
371
+ write(` restatement rate over well-formed lines: ${String(rate)} %\n`);
372
+ }
373
+ write('\n per-line max pairwise slot overlap (recompute at another threshold from these)\n');
374
+ for (const [i, l] of pop.lines.entries()) {
375
+ write(` ${String(i + 1).padStart(3, ' ')}. [${l.klass}] overlap=${String(l.maxOverlap)} ${l.source} :: ${l.body.slice(0, 90)}\n`);
376
+ }
377
+ write(
378
+ '\nlayer 2 — semantic agree/disagree is NOT reported here. `distinct` is not ' +
379
+ '`disagree`: three slots can be worded differently and still say the same thing.\n',
380
+ );
381
+ }
382
+
383
+ export function main(argv: readonly string[] = process.argv.slice(2)): number {
384
+ if (argv.includes('--help') || argv.length === 0) {
385
+ process.stdout.write(
386
+ 'usage: count_intent_disagreement [--git-log] [--pr-bodies <file.json>] [--dir <path>] [--stdin]\n',
387
+ );
388
+ return argv.length === 0 ? 1 : 0;
389
+ }
390
+ const got = gather(argv);
391
+ if ('error' in got) {
392
+ process.stderr.write(`${got.error}\n`);
393
+ return 1;
394
+ }
395
+ report(got, (s) => process.stdout.write(s));
396
+
397
+ try {
398
+ assertScanned({
399
+ gate: 'count_intent_disagreement',
400
+ scanned: got.lines.length,
401
+ units: 'emitted intent line(s)',
402
+ roots: Object.keys(got.bySource),
403
+ });
404
+ } catch (e) {
405
+ if (e instanceof DeadScopeError) {
406
+ process.stderr.write(
407
+ `\n❌ ${e.message}\n\n` +
408
+ 'This is a FINDING, not a tool failure. Zero emitted intent lines means the\n' +
409
+ 'disagreement rate the `mandated-lines.md` lock asks for does not exist yet,\n' +
410
+ 'because the line whose slots would disagree is not being emitted at all.\n' +
411
+ 'Record the null. Do not read it as "no disagreements found".\n',
412
+ );
413
+ return 2;
414
+ }
415
+ throw e;
416
+ }
417
+
418
+ process.stdout.write(`\nscanned: ${String(got.lines.length)}\n`);
419
+ return 0;
420
+ }
421
+
422
+ if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
423
+ process.exit(main());
424
+ }
425
+
426
+ export { REPO_ROOT };
@@ -14,6 +14,11 @@
14
14
  * separate file.
15
15
  * - Staleness: a `Generated:` stamp older than 48 h (or unparseable)
16
16
  * discards without injecting.
17
+ * - Source-gated (road-to-continuity-retirement-sequencing 3.3): a
18
+ * `session_start` carries a `source`, and `resume` / `fork` / anything
19
+ * unrecognised inject NOTHING. The gate runs before either consumer, so a
20
+ * suppressed source never destroys the file it declined to read — see
21
+ * {@link sourceGate}.
17
22
  * - Never blocks: exit 0 on every path; failures are silent (stderr note).
18
23
  * - `AGENT_CONFIG_REPLAY=1` → no-op (replay fixtures never mutate state).
19
24
  *
@@ -152,6 +157,12 @@ export function consume_handoff_context(root: string, now: Date = new Date()): C
152
157
  * - drift (Phase 3.2): the envelope's recorded repo identity + branch +
153
158
  * HEAD are compared against the tree it lands in, and any mismatch LEADS
154
159
  * the injected block. Never a silent stale resume;
160
+ * - the BRANCH GATE (2026-09-09): a branch mismatch is a refusal rather than
161
+ * an annotation, and the refusal is `absent` so the record is left for a
162
+ * session on the right branch instead of consumed. It is the half of the
163
+ * resolution rule that keeps an unrelated session in the same checkout from
164
+ * resuming an ended peer, now that the resolver admits any record that is
165
+ * not the reader's own. Unknown on either side is not a mismatch;
155
166
  * - focus (Phase 3.4): `AGENT_RESUME_FOCUS` narrows what the successor
156
167
  * attacks first — the consumer-side mirror of `next_task`.
157
168
  */
@@ -257,7 +268,39 @@ export function consume_recycle_envelope(
257
268
 
258
269
  // Drift first: a stale resume against the wrong tree is the failure the
259
270
  // reader must see before anything else in the block.
260
- const drift = describeDrift(envelope, collectRepoAnchor(root));
271
+ const anchor = collectRepoAnchor(root);
272
+
273
+ // The branch gate — an ADMISSION check, not an annotation, and the half of
274
+ // the 2026-09-09 resolution rule that lives outside the resolver.
275
+ //
276
+ // Why it is needed at all: the resolver now admits the unique record that is
277
+ // not the reader's own, so peer isolation no longer comes from the filename.
278
+ // Without this, an unrelated session in the same checkout would resume an
279
+ // ended peer's work simply by being the next one to start. AI council
280
+ // 2026-09-09 was explicit that (d) is acceptable WITH this gate and not
281
+ // without it.
282
+ //
283
+ // `absent`, deliberately, and this is the one place the invariant matters:
284
+ // every other outcome consumes the record (moved, not copied), and consuming
285
+ // here would destroy state the rightful successor could still use. `absent`
286
+ // is the only outcome that leaves the file alone, and the 48-hour staleness
287
+ // guard bounds how long it can sit unclaimed.
288
+ //
289
+ // Unknown on either side is NOT a mismatch. An envelope written before the
290
+ // field existed, or a tree whose branch cannot be read, must not be refused
291
+ // on a comparison neither side can make — that would turn a missing field
292
+ // into a broken resume. `describeDrift` still annotates those.
293
+ const envelopeBranch = String((envelope as Record<string, unknown>)['branch'] ?? '').trim();
294
+ if (envelopeBranch !== '' && anchor.branch !== null && envelopeBranch !== anchor.branch) {
295
+ return {
296
+ action: 'absent',
297
+ reason:
298
+ `recycle envelope is for branch "${envelopeBranch}" and this tree is on ` +
299
+ `"${anchor.branch}" — left unclaimed for a session on that branch rather than consumed`,
300
+ };
301
+ }
302
+
303
+ const drift = describeDrift(envelope, anchor);
261
304
  // Proposal fields carrying an imperative LEAD the block as a stop
262
305
  // notice — surfaced, never executed, never silently stripped.
263
306
  const warnings = scanEnvelopeDirectives(envelope);
@@ -281,6 +324,67 @@ export function consume_recycle_envelope(
281
324
  );
282
325
  }
283
326
 
327
+ /**
328
+ * What a `session_start` `source` means for the continuity record
329
+ * (road-to-continuity-retirement-sequencing 3.3).
330
+ *
331
+ * A `session_start` is not one event. The host says WHY the session started,
332
+ * and reading the wrong record for the wrong reason is how a continuation
333
+ * acquires a stranger's state — the failure this step exists to prevent.
334
+ *
335
+ * - `startup` (and an ABSENT source) — a fresh session. Inject: this is the
336
+ * ordinary case and the one every existing fixture exercises.
337
+ * - `clear` — the operator wrote the record and cleared. Inject. This is the
338
+ * recycle path the record was built for, and suppressing it would defeat
339
+ * the mechanism. Note that hot-context DISCARDS on the same source, and the
340
+ * two are right for opposite reasons: hot-context is a cache of a session
341
+ * that was deliberately thrown away, while the record is the thing the
342
+ * operator wrote in order to survive throwing it away.
343
+ * - `compact` — the SAME session continuing past a compaction. Inject: the
344
+ * resolver keys on session id, so what comes back is this session's own
345
+ * record rather than a predecessor's. That identity is the reason this is
346
+ * an inject rather than a suppression.
347
+ * - `resume` / `fork` — the host has already restored the conversation.
348
+ * Inject NOTHING. On `resume` an injection would duplicate context the host
349
+ * just replayed; on `fork` it is worse than duplication, because the fork
350
+ * would consume a record belonging to a session that is still alive.
351
+ * - anything else — inject nothing. An unrecognised source is a host this
352
+ * code has not been taught, and guessing is the failure mode, not the
353
+ * conservative choice.
354
+ *
355
+ * SUPPRESSION MUST NOT CONSUME. The gate runs BEFORE
356
+ * {@link consume_recycle_envelope}, never inside it, because that function
357
+ * moves the file aside on every outcome except `absent`. Calling it and then
358
+ * discarding the result would destroy the record without anyone reading it —
359
+ * the operator's recycle would silently evaporate on a `resume`.
360
+ */
361
+ export const INJECTING_SOURCES: ReadonlySet<string> = new Set(['', 'startup', 'clear', 'compact']);
362
+
363
+ export interface SourceGate {
364
+ inject: boolean;
365
+ reason: string;
366
+ }
367
+
368
+ export function sourceGate(source: string): SourceGate {
369
+ const s = source.trim();
370
+ if (INJECTING_SOURCES.has(s)) {
371
+ return { inject: true, reason: `source=${s === '' ? 'absent' : s}` };
372
+ }
373
+ if (s === 'resume' || s === 'fork') {
374
+ return {
375
+ inject: false,
376
+ reason: `source=${s} — the host already restored this conversation; injecting would ` +
377
+ (s === 'fork'
378
+ ? "consume a record belonging to a session that is still running"
379
+ : 'duplicate context the host just replayed'),
380
+ };
381
+ }
382
+ return {
383
+ inject: false,
384
+ reason: `source=${s} is not a source this reader knows — injecting nothing rather than guessing`,
385
+ };
386
+ }
387
+
284
388
  // ---------------------------------------------------------------------
285
389
  // CLI — dispatcher concern entry point
286
390
  // ---------------------------------------------------------------------
@@ -307,6 +411,19 @@ export function main(): number {
307
411
  return 0; // replay fixtures: read-only, no state mutation
308
412
  }
309
413
  if (event === 'session_start') {
414
+ const payload =
415
+ envelope.payload && typeof envelope.payload === 'object' && !Array.isArray(envelope.payload)
416
+ ? (envelope.payload as Record<string, unknown>)
417
+ : {};
418
+ // 3.3. The gate is evaluated FIRST and short-circuits, because both
419
+ // consumers below move their file aside on every non-absent
420
+ // outcome. Consuming and then dropping the result would delete the
421
+ // record a suppressed source was supposed to leave alone.
422
+ const gate = sourceGate(String(payload['source'] ?? envelope['source'] ?? ''));
423
+ if (!gate.inject) {
424
+ process.stderr.write(`handoff-context-hook: no injection (${gate.reason})\n`);
425
+ return 0;
426
+ }
310
427
  const handoff = consume_handoff_context(root);
311
428
  const recycle = consume_recycle_envelope(
312
429
  root,