@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
@@ -22,10 +22,18 @@
22
22
  * default is off, per the roadmap's "off unless proven".
23
23
  */
24
24
 
25
- import { CURATED_TYPES, retrieve_v1 } from './memory_lookup.js';
25
+ import { CURATED_TYPES, MEMORY_ROOT, retrieve_v1 } from './memory_lookup.js';
26
26
  import { load_agent_settings } from './_lib/agent_settings.js';
27
+ import {
28
+ capRows,
29
+ orderRows,
30
+ SESSION_INDEX_ROW_CAP,
31
+ verifyMemoryRoot,
32
+ type TrustGrant,
33
+ type TrustVerdict,
34
+ } from './_lib/session_index_trust.js';
27
35
 
28
- export const SESSION_INDEX_ROW_CAP = 30;
36
+ export { SESSION_INDEX_ROW_CAP };
29
37
 
30
38
  interface IndexRow {
31
39
  id: string;
@@ -54,21 +62,72 @@ export function session_index_enabled(root: string): boolean {
54
62
  * Expects the caller to have chdir'd to the workspace root — memory roots
55
63
  * are cwd-relative, same contract as the MCP tool handlers.
56
64
  */
65
+ /**
66
+ * How many entries retrieval is asked for before the declared order picks.
67
+ *
68
+ * R2 finding 2: asking retrieval for the CAP made P4 decorative. Every curated
69
+ * hit scores an identical 0.1 with an empty key set, so `retrieve` sliced the
70
+ * first 30 in STORE order at `memory_lookup.ts:995` and `orderRows` then sorted
71
+ * a set the cap had already chosen. Measured by the reviewer: a 40-entry corpus
72
+ * with the 5 cheapest last emitted zero cheap rows.
73
+ *
74
+ * The number must therefore exceed any realistic curated corpus, not merely the
75
+ * cap. 500 is a STATED DEFAULT, not a measured optimum — the cost of asking for
76
+ * more than exists is bounded by what exists, since `detail: 'index'` returns a
77
+ * row rather than a body. Its falsifier is recorded in the roadmap: a corpus
78
+ * larger than this bound makes store order decide again, silently.
79
+ */
80
+ export const SESSION_INDEX_RETRIEVAL_LIMIT = 500;
81
+
57
82
  export function session_index_rows(cap: number = SESSION_INDEX_ROW_CAP): IndexRow[] {
58
- const envelope = retrieve_v1([...CURATED_TYPES], [], cap, { detail: 'index' });
83
+ // Retrieval is asked for far more than the cap, so the declared order below
84
+ // is what chooses which rows survive truncation. P4 and P6 are one
85
+ // decision: retrieve wide, order, THEN cap — and never the reverse.
86
+ const envelope = retrieve_v1([...CURATED_TYPES], [], SESSION_INDEX_RETRIEVAL_LIMIT, { detail: 'index' });
59
87
  const entries = (envelope['entries'] as Array<Record<string, unknown>>) ?? [];
60
- return entries.slice(0, cap).map((e) => ({
88
+ const rows: IndexRow[] = entries.map((e) => ({
61
89
  id: String(e['id'] ?? ''),
62
90
  title: String(e['title'] ?? ''),
63
91
  tokens_estimate: Number(e['tokens_estimate'] ?? 0),
64
92
  }));
93
+ return capRows(orderRows(rows), cap);
94
+ }
95
+
96
+ /**
97
+ * The trust gate, separated from the render so a caller can log WHY nothing
98
+ * was injected. `null` from {@link build_session_index_block} is ambiguous by
99
+ * construction — empty corpus and refused root look identical — and the
100
+ * distinction is the point of the contract, so it is available here.
101
+ *
102
+ * The workspace root is REQUIRED. `MEMORY_ROOT` is relative, so without a root
103
+ * to resolve it against there is nothing to check identity or containment
104
+ * against, and a default would be a bypass wearing a convenience.
105
+ */
106
+ export function session_index_trust(workspaceRoot: string, now?: Date): TrustVerdict {
107
+ // `exactOptionalPropertyTypes` refuses `now: undefined` on an optional
108
+ // field, so the key is omitted rather than passed as undefined.
109
+ return verifyMemoryRoot({
110
+ workspaceRoot,
111
+ relativeMemoryRoot: MEMORY_ROOT,
112
+ ...(now === undefined ? {} : { now }),
113
+ });
65
114
  }
66
115
 
67
116
  /**
68
- * Render the injectable block, or `null` when the corpus is empty (no
69
- * block beats an empty scaffold).
117
+ * RENDER a block from whatever `MEMORY_ROOT` currently resolves to. Checks
118
+ * nothing, serves nothing.
119
+ *
120
+ * Split out on R2 findings 6 and 7. The trust check used to be an OPTIONAL
121
+ * parameter, which made the serving path opt-in — and `session_index_cost()`
122
+ * omitted it, so the one function whose name suggests it only measures was also
123
+ * the one that could render unchecked. Now the two operations have two names:
124
+ * this one renders (for cost measurement and for the unit seam that injects an
125
+ * absolute fixture root via `_setMemoryRoot`, which by construction sits
126
+ * outside any workspace), and {@link serve_session_index_block} is the only way
127
+ * to obtain a block for INJECTION — it takes a granted verdict, so there is no
128
+ * unchecked serving path to forget.
70
129
  */
71
- export function build_session_index_block(cap: number = SESSION_INDEX_ROW_CAP): string | null {
130
+ export function render_session_index_block(cap: number = SESSION_INDEX_ROW_CAP): string | null {
72
131
  const rows = session_index_rows(cap);
73
132
  if (rows.length === 0) return null;
74
133
  const lines = [
@@ -81,13 +140,27 @@ export function build_session_index_block(cap: number = SESSION_INDEX_ROW_CAP):
81
140
  return lines.join('\n');
82
141
  }
83
142
 
143
+ /**
144
+ * The only path to a block that will be INJECTED.
145
+ *
146
+ * Takes a granted verdict rather than a root, so the check happens once, at the
147
+ * caller, and cannot be skipped here — R2 finding 7 was that the hook and the
148
+ * builder each computed it, for a diagnostic the dispatcher does not forward.
149
+ * The type is `TrustGrant`, not `TrustVerdict`: a refusal cannot be passed in,
150
+ * so "did you check?" is answered by the signature instead of by a convention.
151
+ */
152
+ export function serve_session_index_block(_grant: TrustGrant, cap: number = SESSION_INDEX_ROW_CAP): string | null {
153
+ return render_session_index_block(cap);
154
+ }
155
+
84
156
  /**
85
157
  * Deterministic fixed cost of the block in real tokens (cl100k_base) —
86
158
  * the measurable arm of the roadmap's ship-criterion. Lazy-loads the
87
159
  * tokenizer so the enabled=off fast path never pays it.
88
160
  */
89
161
  export async function session_index_cost(cap: number = SESSION_INDEX_ROW_CAP): Promise<number> {
90
- const block = build_session_index_block(cap);
162
+ // Renders rather than serves: this measures a cost, it injects nothing.
163
+ const block = render_session_index_block(cap);
91
164
  if (block === null) return 0;
92
165
  const { gpt_tokens } = await import('./_lib/token_count.js');
93
166
  return gpt_tokens(block).tokens;
@@ -66,9 +66,16 @@ export const ROOT = path.resolve(path.dirname(_HERE), '..', '..', '..');
66
66
  * start ranking different trees. `null` when no candidate root exists, and the
67
67
  * CLI turns that into a distinct exit code rather than an empty list.
68
68
  */
69
- import { DEFAULT_CATALOGUE_ROOTS, resolveSkillsRoot } from '../_lib/skill_catalogue.js';
69
+ import { DEFAULT_CATALOGUE_ROOTS, resolveSkillCatalogueRoots } from '../_lib/skill_catalogue.js';
70
70
 
71
- export const DEFAULT_SKILLS_DIR: string | null = resolveSkillsRoot(ROOT);
71
+ /**
72
+ * Every readable catalogue root, in precedence order — possibly empty.
73
+ *
74
+ * The default the CLI and the hook rank across. `DEFAULT_SKILLS_DIR` below is
75
+ * the first of them and stays exported for the surfaces that name one directory.
76
+ */
77
+ export const DEFAULT_SKILLS_ROOTS: readonly string[] = resolveSkillCatalogueRoots(ROOT);
78
+ export const DEFAULT_SKILLS_DIR: string | null = DEFAULT_SKILLS_ROOTS[0] ?? null;
72
79
 
73
80
  /** Mirror Python len(str) — count Unicode code points, not UTF-16 units. */
74
81
  function pyLen(s: string): number {
@@ -164,6 +171,27 @@ export interface Skill {
164
171
  triggerText: string[];
165
172
  }
166
173
 
174
+ /**
175
+ * Load one root, or several with the FIRST occurrence of a name winning.
176
+ *
177
+ * Precedence by name, never by root: every root is read, and a collision
178
+ * resolves to the earliest root that carries it — `resolveSkillCatalogueRoots`
179
+ * owns what that order means. Reading only the first root is the defect this
180
+ * replaced; silently ranking one name twice would be the next one.
181
+ */
182
+ function _load_skills_across(roots: readonly string[]): Skill[] {
183
+ const out: Skill[] = [];
184
+ const seen = new Set<string>();
185
+ for (const root of roots) {
186
+ for (const s of _load_skills(root)) {
187
+ if (seen.has(s.name)) continue;
188
+ seen.add(s.name);
189
+ out.push(s);
190
+ }
191
+ }
192
+ return out;
193
+ }
194
+
167
195
  function _load_skills(skillsDir: string): Skill[] {
168
196
  const skills: Skill[] = [];
169
197
  for (const skillMd of _globSkillMd(skillsDir)) {
@@ -246,9 +274,16 @@ function _score(taskTerms: Set<string>, skill: Skill, opts: RankOptions = {}): n
246
274
 
247
275
  export type RankRow = [string, number, string[]];
248
276
 
249
- export function rank(task: string, skillsDir: string, opts: RankOptions = {}): RankRow[] {
277
+ /**
278
+ * @param skillsDir one root, or several to rank across (see `_load_skills_across`).
279
+ */
280
+ export function rank(
281
+ task: string,
282
+ skillsDir: string | readonly string[],
283
+ opts: RankOptions = {},
284
+ ): RankRow[] {
250
285
  const taskTerms = _tokenize(task);
251
- const skills = _load_skills(skillsDir);
286
+ const skills = _load_skills_across(typeof skillsDir === 'string' ? [skillsDir] : skillsDir);
252
287
  const rows: RankRow[] = [];
253
288
  for (const s of skills) {
254
289
  const score = _score(taskTerms, s, opts);
@@ -384,6 +419,15 @@ interface Args {
384
419
  task: string;
385
420
  /** `null` when no catalogue root exists — a DIFFERENT answer from an empty ranking. */
386
421
  skills_dir: string | null;
422
+ /**
423
+ * Did the operator NAME the directory?
424
+ *
425
+ * An explicit `--skills-dir` means one root and only that root: an operator
426
+ * pointing at a tree is asking about that tree, and quietly unioning the
427
+ * host catalogue into their answer would make the flag mean something else.
428
+ * Absent, the default ranks across every readable root.
429
+ */
430
+ skills_dir_explicit: boolean;
387
431
  top: number;
388
432
  json: boolean;
389
433
  sample: boolean;
@@ -407,6 +451,7 @@ export function parse_args(argv: string[]): Args {
407
451
  const args: Args = {
408
452
  task: '',
409
453
  skills_dir: DEFAULT_SKILLS_DIR,
454
+ skills_dir_explicit: false,
410
455
  top: 0,
411
456
  json: false,
412
457
  sample: false,
@@ -431,8 +476,10 @@ export function parse_args(argv: string[]): Args {
431
476
  _argError('argument --skills-dir: expected one argument');
432
477
  }
433
478
  args.skills_dir = v;
479
+ args.skills_dir_explicit = true;
434
480
  } else if (a.startsWith('--skills-dir=')) {
435
481
  args.skills_dir = a.slice('--skills-dir='.length);
482
+ args.skills_dir_explicit = true;
436
483
  } else if (a === '--top') {
437
484
  const v = argv[++i];
438
485
  if (v === undefined) {
@@ -497,7 +544,24 @@ export function main(argv: string[] | null = null): number {
497
544
  // operator who passes a wrong path is in exactly the position the silent
498
545
  // failure was about, and answering them with "(no relevant skills found)"
499
546
  // is the same wrong answer with a different cause.
500
- const unreadable = catalogueDeficit(args.skills_dir);
547
+ // One root when the operator named it; otherwise every readable root, so the
548
+ // ranking covers what the session can actually reach.
549
+ const roots: readonly string[] = args.skills_dir_explicit
550
+ ? args.skills_dir === null
551
+ ? []
552
+ : [args.skills_dir]
553
+ : DEFAULT_SKILLS_ROOTS;
554
+ // An EXPLICIT directory is always deficit-checked, even when it resolved to a
555
+ // one-element list: a wrong path must not degrade to "(no relevant skills
556
+ // found)", which is the silent failure the check exists for and which the
557
+ // first draft of this branch reintroduced. For the DEFAULT, the deficit is
558
+ // "not one readable root anywhere" — a single empty candidate is no longer a
559
+ // deficit, because the others may still carry the catalogue.
560
+ const unreadable = args.skills_dir_explicit
561
+ ? catalogueDeficit(args.skills_dir)
562
+ : roots.length === 0
563
+ ? catalogueDeficit(null)
564
+ : null;
501
565
  if (unreadable !== null) {
502
566
  if (args.json) {
503
567
  process.stdout.write(
@@ -520,7 +584,7 @@ export function main(argv: string[] | null = null): number {
520
584
  }
521
585
  // Non-null past the deficit check above, which returns before this line
522
586
  // whenever the root cannot be ranked.
523
- const rows = rank(task, args.skills_dir as string);
587
+ const rows = rank(task, roots);
524
588
  if (args.json) {
525
589
  const sliced = args.top ? rows.slice(0, args.top) : rows;
526
590
  const payload = sliced.map(([n, s, p]) => ({ name: n, score: s, personas: p }));
@@ -32,7 +32,7 @@ import { audit, type PersonaRow } from './audit_persona_coverage.js';
32
32
  /** Sentinel for "no catalogue root resolved" — never a readable path. */
33
33
  const NO_CATALOGUE = '<no-skills-catalogue>';
34
34
 
35
- import { DEFAULT_SKILLS_DIR, rank } from './score_skill_relevance.js';
35
+ import { DEFAULT_SKILLS_DIR, DEFAULT_SKILLS_ROOTS, rank } from './score_skill_relevance.js';
36
36
 
37
37
  const _HERE = fileURLToPath(import.meta.url);
38
38
  // src/scripts/skill_tools/suggest_skill_for_task.ts → parents[3] of the .py
@@ -78,13 +78,27 @@ export interface Combo {
78
78
  why: string;
79
79
  }
80
80
 
81
+ /**
82
+ * @param skillsDir the root the PERSONA audit reads — one directory by necessity.
83
+ * @param rankRoots roots to RANK across; defaults to `[skillsDir]`.
84
+ *
85
+ * The two parameters are separate because the two questions are. Ranking wants
86
+ * every catalogue the session can reach (`resolveSkillCatalogueRoots`); the
87
+ * persona-coverage audit walks one tree and pairs it with one persona directory.
88
+ * Collapsing them was the state a neutral review found on 2026-09-07: the MCP
89
+ * handler ranked the union while this CLI ranked the first root, so a consumer
90
+ * carrying its own project skills got a different answer from each — exactly the
91
+ * two-resolvers-over-one-catalogue split the resolver's docstring warns about,
92
+ * reproduced by the change that claimed to fix it.
93
+ */
81
94
  export function suggest(
82
95
  task: string,
83
96
  skillsDir: string,
84
97
  personasDir: string,
85
98
  top = 3,
99
+ rankRoots: readonly string[] = [skillsDir],
86
100
  ): Combo[] {
87
- const ranked = rank(task, skillsDir).slice(0, top);
101
+ const ranked = rank(task, rankRoots).slice(0, top);
88
102
  const personaRows = audit(skillsDir, personasDir);
89
103
  const status = _persona_status(personaRows);
90
104
  return ranked.map(([name, score, personas]) => ({
@@ -193,6 +207,8 @@ const PROG = 'suggest_skill_for_task.py';
193
207
  interface Args {
194
208
  task: string;
195
209
  skills_dir: string;
210
+ /** Did the operator NAME the directory? One tree then, and only that tree. */
211
+ skills_dir_explicit: boolean;
196
212
  personas_dir: string;
197
213
  top: number;
198
214
  json: boolean;
@@ -219,6 +235,7 @@ export function parse_args(argv: string[]): Args {
219
235
  // DEFAULT_SKILLS_DIR removed. When no catalogue resolves, this reads
220
236
  // as the literal marker below and the caller's own check reports it.
221
237
  skills_dir: DEFAULT_SKILLS_DIR ?? NO_CATALOGUE,
238
+ skills_dir_explicit: false,
222
239
  personas_dir: DEFAULT_PERSONAS,
223
240
  top: 3,
224
241
  json: false,
@@ -244,8 +261,10 @@ export function parse_args(argv: string[]): Args {
244
261
  _argError('argument --skills-dir: expected one argument');
245
262
  }
246
263
  args.skills_dir = v;
264
+ args.skills_dir_explicit = true;
247
265
  } else if (a.startsWith('--skills-dir=')) {
248
266
  args.skills_dir = a.slice('--skills-dir='.length);
267
+ args.skills_dir_explicit = true;
249
268
  } else if (a === '--personas-dir') {
250
269
  const v = argv[++i];
251
270
  if (v === undefined) {
@@ -279,7 +298,22 @@ export function main(argv: string[] | null = null): number {
279
298
  if (!task) {
280
299
  _argError('--task is required (or pass --sample)');
281
300
  }
282
- const combos = suggest(task, args.skills_dir, args.personas_dir, args.top);
301
+ // Rank across every readable root; audit personas against the first. An
302
+ // explicit --skills-dir names one tree and is honoured as one tree.
303
+ //
304
+ // Keyed on the FLAG, not on value-equality against the default. Corrected
305
+ // after a second neutral review: comparing paths meant
306
+ // `--skills-dir <the resolved default>` silently ranked the union including
307
+ // `~/.claude/skills`, so the comment above was false for exactly the operator
308
+ // who spelled the default out. And when no catalogue resolves at all,
309
+ // `skills_dir` is the `NO_CATALOGUE` sentinel while `DEFAULT_SKILLS_DIR` is
310
+ // null, so the old comparison was false there too — the branch was dead in
311
+ // the state it was written for.
312
+ const rankRoots =
313
+ !args.skills_dir_explicit && DEFAULT_SKILLS_ROOTS.length > 0
314
+ ? DEFAULT_SKILLS_ROOTS
315
+ : [args.skills_dir];
316
+ const combos = suggest(task, args.skills_dir, args.personas_dir, args.top, rankRoots);
283
317
  if (args.json) {
284
318
  process.stdout.write(pyJsonDumpsIndent2({ task, suggestions: combos }));
285
319
  process.stdout.write('\n');
@@ -31,7 +31,8 @@ const confidenceBand = z.enum(['off', 'low', 'medium', 'high']);
31
31
  const onBlock = z.enum(['stop', 'ask', 'warn']);
32
32
  const onBlockFallback = z.enum(['stop', 'warn']);
33
33
  const modelAutoSwitch = z.enum(['auto', 'suggest', 'off']);
34
- const leanProjectionMode = z.enum(['eager-all', 'thin']);
34
+ const leanProjectionMode = z.enum(['eager-all', 'thin', 'delivery']);
35
+ const leanProjectionHost = z.enum(['claude-code', 'cursor', 'cline']);
35
36
  const projectionMode = z.enum(['legacy-all', 'scoped']);
36
37
  const memoryCadence = z.enum(['auto', 'always', 'never']);
37
38
  const projectAudience = z.enum(['self', 'internal', 'client', 'public']);
@@ -64,9 +65,19 @@ export const settingsSchema = z.object({
64
65
  ),
65
66
  lean_projection: z.object({
66
67
  mode: leanProjectionMode.default('eager-all').describe(
67
- 'How the per-tool projector emits the rule layer. eager-all = every rule body inlined into every projection (default, safe). thin = kernel rules full-bodied + non-kernel rules as router-resolved pointers (~45k GPT-tok lighter per session). EXPERIMENTAL: validate with the live A/B before flipping; one-flip revert to eager-all.',
68
- ),
69
- }).default({ mode: 'eager-all' }),
68
+ 'How the per-tool projector emits the rule layer. eager-all = every rule body inlined into every projection (safe; what an ABSENT key still resolves to, per ADR-267 decision point 4). thin = kernel rules full-bodied + non-kernel rules as router-resolved pointers (~45k GPT-tok lighter per session), with no delivery mechanism. delivery = thin stubs PLUS the rule-inject hook concern, which delivers a body back on a trigger match; this is the value the SHIPPED TEMPLATE carries for Claude Code since ADR-267, and the activation charge for it is paid in src/config/hook-token-budget.json (user_prompt_submit slot sum 4,096 -> 16,384).',
69
+ ),
70
+ hosts: z.array(leanProjectionHost).default(['claude-code']).describe(
71
+ 'Which hosts a thinning mode may thin (ADR-267). Absent resolves to [claude-code]. '
72
+ + 'Only the three hosts with a per-rule rule tree are accepted, and this enum is the '
73
+ + 'AUTHORING layer: an id outside it is a hard validation FAILURE here and under '
74
+ + 'validate_agent_settings, never a warning. resolveLeanProjectionHosts is the '
75
+ + 'READ-TIME backstop for a hand-edited .agent-settings.yml that passed neither, and '
76
+ + 'THERE the id is dropped with a warning (R2 finding 6 — the two layers answer '
77
+ + 'differently and neither said so). Every host outside this list receives exactly '
78
+ + 'what eager-all writes, asserted byte-for-byte by check_host_tree_parity.',
79
+ ),
80
+ }).default({ mode: 'eager-all', hosts: ['claude-code'] }),
70
81
  cost: z.object({
71
82
  budgets: z.object({
72
83
  daily: z.number().min(0).default(0).describe(
@@ -438,6 +449,14 @@ export const settingsSchema = z.object({
438
449
  ),
439
450
  }),
440
451
  }),
452
+ continuity: z.object({
453
+ auto_record: z.enum(['on', 'off']).default('off').describe(
454
+ 'Deterministic continuity-record writer at session end (road-to-continuity-writer-activation Phase 1). on = the session-eol concern writes the continuity_record capsule variant on Stop for a substantive session that has claimed a roadmap; every field is computed from on-disk state, with no model spend and no subprocess. off (default) = no automatic record — while session:recycle is still the normal path, a second producer on it before the parity evidence is in would be unverified.',
455
+ ),
456
+ run_checkpoints: z.enum(['on', 'off']).default('on').describe(
457
+ 'Deterministic run-checkpoint production at session end (road-to-continuity-writer-activation Phase 1.4). on (default) = a session above the recycle threshold and inside a running roadmap contract leaves agents/runtime/state/checkpoints/<run>.json, so a killed run resumes from a derived checkpoint rather than from bookkeeping. off = no checkpoint; continuity writing, the recycle advisory and the context-fill surface are unaffected. Default ON because it is the behaviour the tree already had — the switch exists to make the three session-end handlers independently disableable, not to change what ships.',
458
+ ),
459
+ }),
441
460
  memory: z.object({
442
461
  cadence: memoryCadence.default('always').describe(
443
462
  'Cadence of the 🧠 memory-visibility line after a memory-consulting step. always (default) = show whenever a memory type was asked; auto = show only when 3+ types were consulted (less noise); never = suppress. Distinct from rule_loading_tier — owns its own key since the 2026-06-01 untangle.',
@@ -0,0 +1,86 @@
1
+ # Vendored tree-sitter grammars
2
+
3
+ Compiled WebAssembly grammars for the native code-graph engine
4
+ (`src/scripts/code_graph/`). They are vendored — carried in this repository and
5
+ published inside the npm tarball — so that a consumer who runs
6
+ `npm install @event4u/agent-config` can build a code graph on the next command
7
+ with no manual parser install (ADR-259).
8
+
9
+ ## Why vendored rather than a dependency
10
+
11
+ `tree-sitter-wasms` carries 36 grammars (49 MiB on disk). Depending on it at
12
+ runtime would hand every consumer all 36, which is the option ADR-259 §
13
+ Alternatives rejects on exactly that measurement. Only the three grammars below
14
+ are loadable by the engine — `GRAMMAR_WASM` in `src/scripts/code_graph/types.ts`
15
+ has three entries — so the other 33 would be payload with no reader.
16
+
17
+ **No grammar is ever fetched at runtime** (Kill register K1). These files are in
18
+ the tarball; the loader reads them from disk.
19
+
20
+ ## The set
21
+
22
+ | Language | File | SHA-256 |
23
+ |---|---|---|
24
+ | php | `tree-sitter-php.wasm` | `55bb617b6f01e14bab997861f0b20a2420cf6ba3199ffeb295b9ec398966d8a3` |
25
+ | typescript (also serves `.tsx`) | `tree-sitter-typescript.wasm` | `8515404dceed38e1ed86aa34b09fcf3379fff1b4ff9dd3967bcd6d1eb5ac3d8f` |
26
+ | javascript | `tree-sitter-javascript.wasm` | `63812b9e275d26851264734868d27a1656bd44a2ef6eb3e85e6b03728c595ab5` |
27
+
28
+ All three are built against **grammar ABI 14** (`EXPECTED_GRAMMAR_ABI` in
29
+ `src/scripts/code_graph/types.ts`). The loader asserts the ABI on every load, so
30
+ a drifted grammar fails loudly rather than parsing wrongly.
31
+
32
+ ## Provenance
33
+
34
+ Copied byte-for-byte from `tree-sitter-wasms@0.1.13`
35
+ (<https://github.com/Gregoor/tree-sitter-wasms>, Unlicense), which compiles them
36
+ from the upstream `tree-sitter/tree-sitter-{php,typescript,javascript}` grammars
37
+ (MIT). The compiled artifacts are derivatives of the MIT grammar sources, so
38
+ they are annotated MIT in `REUSE.toml` and credited in `CREDITS.md`.
39
+
40
+ `tree-sitter-wasms` remains a **devDependency**: it is the source these copies
41
+ are refreshed from, and the ABI smoke test in `tests/scripts/code_graph.test.ts`
42
+ runs against it. It is deliberately not a runtime dependency.
43
+
44
+ ## Verification
45
+
46
+ Two checks, and they answer different questions.
47
+
48
+ `src/config/packed-binary-manifest.json` plus `_lib/packed_binary_predicate.ts`
49
+ verify that these bytes are what the manifest says they are — eleven conditions,
50
+ all of them re-derived from the bytes. Every one of them reads the manifest, so
51
+ a commit that edits a row and its file together satisfies all eleven.
52
+
53
+ `_lib/vendored_grammar_upstream.ts` verifies that these bytes are what the
54
+ **locked upstream** says they are: it byte-compares each file against
55
+ `node_modules/tree-sitter-wasms/out/`, after asserting that the installed
56
+ version is the one `package-lock.json` pins. An absent or unlocked upstream is a
57
+ refusal, never a pass. It is exercised by
58
+ `tests/scripts/vendored_grammar_upstream.test.ts`, which also drives every
59
+ refusal path and a real mutated grammar to failure.
60
+
61
+ What neither check covers is a `package-lock.json` row pointing at a hostile
62
+ package; that is review's job, and `packed_binary_predicate.ts`'s docblock says
63
+ so in those words.
64
+
65
+ ## Refreshing
66
+
67
+ The pair is ABI-coupled and must move together:
68
+
69
+ ```bash
70
+ npm i -D web-tree-sitter@<v> tree-sitter-wasms@<v>
71
+ for g in php typescript javascript; do
72
+ cp node_modules/tree-sitter-wasms/out/tree-sitter-$g.wasm src/vendor/grammars/
73
+ done
74
+ shasum -a 256 src/vendor/grammars/*.wasm # update the table above
75
+ ```
76
+
77
+ Then update, in the same change:
78
+
79
+ - the three entries in `src/config/packed-binary-manifest.json` (they are
80
+ path-, sha256- and size-bound, so any refresh invalidates them by design).
81
+ This used to name `BINARY_PAYLOAD_EXCEPTIONS` in `src/scripts/check_pack_size.ts`,
82
+ a constant that no longer exists — `grep -rn BINARY_PAYLOAD_EXCEPTIONS` returned
83
+ this line and nothing else, so a refresher following it would have skipped the
84
+ manifest and reddened the pack-size gate with no pointer to why;
85
+ - `EXPECTED_GRAMMAR_ABI` in `src/scripts/code_graph/types.ts` if the ABI moved;
86
+ - the install-size line in `MIGRATION.md`.
@@ -1,118 +0,0 @@
1
- /**
2
- * The `status: carrier` predicate, and the reasoning five readers share.
3
- *
4
- * A carrier is a roadmap whose job is to hold obligations deferred out of a
5
- * parent that is already archived. Every item in it has an unmet resumption
6
- * trigger, and it has no `## Phase` headings, so it is skipped wherever a draft
7
- * is: the dashboard, `/roadmap:process-*`, the trackability check and the plan
8
- * risk register. It differs from a draft in what it costs to remove — a draft
9
- * may be deleted freely, a carrier may not.
10
- *
11
- * This module exists because the predicate had two implementations within one
12
- * change and five callers within one week. Two implementations of one frontmatter
13
- * test drift, and the one nobody runs locally drifts first — the same
14
- * memory-twin failure this repository has already recorded against a fixture
15
- * corpus. One test, one place, and the reasoning above lives beside it rather
16
- * than being restated in each caller.
17
- *
18
- * The read is frontmatter-scoped on purpose. A fenced `status: carrier` inside a
19
- * roadmap that documents the syntax is not a declaration, and a body-wide match
20
- * turned exactly that into a hard failure before a review caught it.
21
- */
22
-
23
- /** The frontmatter values that mean "not schedulable work, and not disposable". */
24
- export const CARRIER_VALUES: ReadonlySet<string> = new Set(['carrier']);
25
-
26
- /** The leading `---\n…\n---` block, or null when the text carries none. */
27
- export function frontmatterBlock(text: string): string | null {
28
- const m = /^---\n([\s\S]*?)\n---/.exec(text);
29
- return m === null ? null : (m[1] ?? '');
30
- }
31
-
32
- /** True when `text`'s FRONTMATTER declares one of {@link CARRIER_VALUES}. */
33
- export function declaresCarrier(text: string | null | undefined): boolean {
34
- if (text === null || text === undefined) {
35
- return false;
36
- }
37
- const fm = frontmatterBlock(text);
38
- if (fm === null) {
39
- return false;
40
- }
41
- const m = /^status:[ \t]*([A-Za-z-]+)[ \t]*$/m.exec(fm);
42
- return m !== null && CARRIER_VALUES.has((m[1] ?? '').toLowerCase());
43
- }
44
-
45
- /**
46
- * Count the top-level roadmaps in `roadmapRoot` that declare themselves carriers.
47
- *
48
- * Lives here rather than in the estate gate because it is the same fact as the
49
- * predicate above, read over a directory instead of a string, and the gate that
50
- * needs it is already at its source-size ceiling.
51
- *
52
- * @param isCandidate the caller's own roadmap-filename test, so this module does
53
- * not acquire a second opinion about what counts as a roadmap.
54
- */
55
- export function countDeclaredCarriers(
56
- roadmapRoot: string,
57
- isCandidate: (name: string) => boolean,
58
- fsLike: {
59
- readdirSync: (p: string) => string[];
60
- statSync: (p: string) => { isFile: () => boolean };
61
- readFileSync: (p: string, enc: 'utf-8') => string;
62
- },
63
- join: (...parts: string[]) => string,
64
- ): number {
65
- let names: string[];
66
- try {
67
- names = fsLike.readdirSync(roadmapRoot);
68
- } catch {
69
- return 0;
70
- }
71
- let n = 0;
72
- for (const name of names) {
73
- if (!name.endsWith('.md') || !isCandidate(name)) {
74
- continue;
75
- }
76
- const abs = join(roadmapRoot, name);
77
- try {
78
- if (!fsLike.statSync(abs).isFile()) {
79
- continue;
80
- }
81
- if (declaresCarrier(fsLike.readFileSync(abs, 'utf-8'))) {
82
- n += 1;
83
- }
84
- } catch {
85
- continue;
86
- }
87
- }
88
- return n;
89
- }
90
-
91
- /**
92
- * Fixture design — why two of the estate self-test cases are shaped as they are.
93
- *
94
- * The reasoning behind two of `check_estate_count`'s self-test cases, kept here
95
- * because the gate that holds them is at its source-size ceiling and because
96
- * both facts are about the carrier status rather than about that gate.
97
- *
98
- * **The deletion case's addition is a DRAFT, and that is the whole design.**
99
- * A carrier holds obligations lifted out of an already-archived parent, so
100
- * paying an offset for removing one rewards the loss: the deletion buys nothing
101
- * and the addition stays unpaid. With an *ordinary* addition the growth half and
102
- * the unpaid half coincide — growth iff `n > a`, unpaid iff `n > a` — so the
103
- * case rejected on two independent grounds and stayed red with the carrier
104
- * branch reverted, asserting nothing about the feature it named. A draft is
105
- * invisible to `collect()`, so the count half cannot move and only the offset
106
- * scoring decides.
107
- *
108
- * **The count case asserts the flip BACK, not the flip TO.** A reclassification
109
- * is not a disposal in either direction, but the flip *to* carrier cannot be
110
- * asserted by exit code: it lowers the count, a count below the floor is a
111
- * drawdown, and a drawdown is free whichever way carriers are counted. Its
112
- * consequence is assertable — with carriers uncounted the flip lowers the floor,
113
- * so flipping the same file back reads as growth and must be paid for. That is
114
- * the fixture's state, with the carrier already in the base tree, and it rejects
115
- * with the carrier term removed from the count. The neutrality of the forward
116
- * flip is asserted directly in `check_estate_count.test.ts`, where a count is
117
- * readable.
118
- */