@event4u/agent-config 14.21.0 → 14.22.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 (181) 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 +198 -0
  5. package/CREDITS.md +37 -0
  6. package/dist/agent-src/commands/analyze/inbox.md +380 -5
  7. package/dist/agent-src/commands/analyze/repo.md +4 -2
  8. package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +69 -0
  9. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +5 -5
  10. package/dist/agent-src/rules/external-code-graph-interop.md +19 -14
  11. package/dist/agent-src/rules/token-efficiency.md +26 -3
  12. package/dist/agent-src/scripts/_cli_entry.ts +51 -0
  13. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +14 -23
  14. package/dist/agent-src/scripts/roadmap_gates.ts +4 -20
  15. package/dist/agent-src/scripts/roadmap_set_step.ts +5 -2
  16. package/dist/agent-src/scripts/stubs_due.ts +4 -18
  17. package/dist/agent-src/scripts/update_roadmap_progress.ts +7 -24
  18. package/dist/agent-src/skills/code-intelligence/SKILL.md +25 -0
  19. package/dist/agent-src/skills/roadmap-writing/SKILL.md +17 -0
  20. package/dist/agent-src/templates/agent-settings.md +0 -1
  21. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  22. package/dist/cli-delegate/_cli_entry.js +8 -0
  23. package/dist/cli-delegate/_lib/pin_resolver.js +170 -0
  24. package/dist/cli-delegate/archival_sweep.js +8 -0
  25. package/dist/cli-delegate/archive_completed_roadmaps.js +494 -0
  26. package/dist/cli-delegate/check_update_banner.js +923 -0
  27. package/dist/cli-delegate/chunk-5EK5WQN6.js +28 -0
  28. package/dist/cli-delegate/chunk-7XGD2OEU.js +96 -0
  29. package/dist/cli-delegate/{chunk-643Z7FIC.js → chunk-BIXEDQPZ.js} +6 -0
  30. package/dist/cli-delegate/chunk-DBH4AHDK.js +155 -0
  31. package/dist/cli-delegate/chunk-JO25G5H7.js +20 -0
  32. package/dist/cli-delegate/chunk-LTYVIY76.js +7961 -0
  33. package/dist/cli-delegate/chunk-QAJS5YWY.js +999 -0
  34. package/dist/cli-delegate/{chunk-ZVBOHSVF.js → chunk-TGIPZBCW.js} +1 -1
  35. package/dist/cli-delegate/{chunk-YS6XTLEK.js → chunk-TYSY33MD.js} +56 -6
  36. package/dist/cli-delegate/chunk-ULET4BYS.js +328 -0
  37. package/dist/cli-delegate/chunk-YDQSHTD7.js +187 -0
  38. package/dist/cli-delegate/chunk-YWVXLR44.js +48 -0
  39. package/dist/cli-delegate/chunk-ZZQBUHFK.js +273 -0
  40. package/dist/cli-delegate/cmd_conformance.js +2 -2
  41. package/dist/cli-delegate/cmd_export.js +1 -1
  42. package/dist/cli-delegate/cmd_handoff.js +26 -2
  43. package/dist/cli-delegate/cmd_prune.js +1 -1
  44. package/dist/cli-delegate/cmd_session_recycle.js +2 -2
  45. package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
  46. package/dist/cli-delegate/cmd_sync.js +1 -1
  47. package/dist/cli-delegate/cmd_uninstall.js +1 -1
  48. package/dist/cli-delegate/cmd_validate.js +1 -1
  49. package/dist/cli-delegate/dashboard_mode.js +16 -0
  50. package/dist/cli-delegate/gate_budget.js +24 -0
  51. package/dist/cli-delegate/gate_execute.js +25 -0
  52. package/dist/cli-delegate/guarded_baseline.js +18 -0
  53. package/dist/cli-delegate/resume_probe.js +22 -0
  54. package/dist/cli-delegate/roadmap_gates.js +638 -0
  55. package/dist/cli-delegate/roadmap_set_step.js +173 -0
  56. package/dist/cli-delegate/stub_queue.js +47 -0
  57. package/dist/cli-delegate/stubs_due.js +38 -0
  58. package/dist/cli-delegate/ticket_bundles.js +59 -0
  59. package/dist/cli-delegate/update_roadmap_progress.js +60 -0
  60. package/dist/discovery/deprecation-report.md +1 -1
  61. package/dist/discovery/discovery-manifest.json +8 -8
  62. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  63. package/dist/discovery/discovery-manifest.summary.md +1 -1
  64. package/dist/discovery/dormancy-report.md +1 -1
  65. package/dist/discovery/orphan-report.md +1 -1
  66. package/dist/discovery/packs.json +2 -2
  67. package/dist/discovery/trust-report.md +1 -1
  68. package/dist/discovery/workspaces.json +2 -2
  69. package/dist/hooks/dispatch.js +1193 -917
  70. package/dist/install/claudeLayerCarriage.js +237 -0
  71. package/dist/install/claudePathsPlan.js +79 -0
  72. package/dist/install/hostLayerFingerprint.js +1 -1
  73. package/dist/install/install.mjs +6 -0
  74. package/dist/install/partitionEligibility.js +231 -117
  75. package/dist/install/ruleLayerPartition.js +10 -7
  76. package/dist/mcp/registry-manifest.json +2 -2
  77. package/dist/mcp/server.json +2 -2
  78. package/dist/mcp/server.mjs +39219 -38995
  79. package/dist/scripts/condense.js +32 -25
  80. package/docs/MIGRATION.md +49 -1
  81. package/docs/contracts/settings-classes.md +1 -1
  82. package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +73 -0
  83. package/package.json +7 -4
  84. package/src/agent-src/scripts/_cli_entry.ts +51 -0
  85. package/src/agent-src/scripts/archive_completed_roadmaps.ts +14 -23
  86. package/src/agent-src/scripts/roadmap_gates.ts +4 -20
  87. package/src/agent-src/scripts/roadmap_set_step.ts +5 -2
  88. package/src/agent-src/scripts/stubs_due.ts +4 -18
  89. package/src/agent-src/scripts/update_roadmap_progress.ts +7 -24
  90. package/src/config/continuity-surface.json +509 -0
  91. package/src/config/estate-count-budget.json +1 -1
  92. package/src/config/gate-coverage.yml +29 -8
  93. package/src/config/gate-violation-baselines.json +8 -8
  94. package/src/config/gitignore-block.txt +19 -0
  95. package/src/config/host-injection-effect.json +73 -0
  96. package/src/config/packed-binary-manifest.json +33 -0
  97. package/src/config/preamble-payload-budget.json +112 -105
  98. package/src/config/publish-surface.json +4 -2
  99. package/src/install/claudeLayerCarriage.ts +273 -0
  100. package/src/install/claudePathsPlan.ts +76 -0
  101. package/src/install/hostLayerFingerprint.ts +1 -1
  102. package/src/install/partitionEligibility.ts +261 -127
  103. package/src/install/ruleLayerPartition.ts +19 -23
  104. package/src/scripts/_dispatch.bash +77 -27
  105. package/src/scripts/_lib/auto_dispatch.ts +2 -2
  106. package/src/scripts/_lib/catalogue_layer_parity.ts +146 -0
  107. package/src/scripts/_lib/continuity_surface.ts +296 -0
  108. package/src/scripts/_lib/corpus_manifest.ts +26 -9
  109. package/src/scripts/_lib/hook_settings.ts +1 -1
  110. package/src/scripts/_lib/injection_effect.ts +151 -0
  111. package/src/scripts/_lib/layer_overlap_notice.ts +50 -6
  112. package/src/scripts/_lib/packed_binary_predicate.ts +225 -0
  113. package/src/scripts/_lib/pin_resolver.ts +13 -0
  114. package/src/scripts/_lib/regression_neighbourhood.ts +1 -1
  115. package/src/scripts/_lib/retired_status.ts +74 -0
  116. package/src/scripts/_lib/skill_catalogue.ts +156 -21
  117. package/src/scripts/_lib/subagent_capsule.ts +117 -8
  118. package/src/scripts/ai_council/api_transport.ts +5 -0
  119. package/src/scripts/ai_council/deliberation_metrics.ts +353 -0
  120. package/src/scripts/ai_council/prompts.ts +9 -0
  121. package/src/scripts/ai_council/synthesis_disagreement.ts +206 -0
  122. package/src/scripts/capture_skill_catalogue.ts +24 -11
  123. package/src/scripts/check_candidate_lines.ts +499 -0
  124. package/src/scripts/check_continuity_surface.ts +475 -0
  125. package/src/scripts/check_dependency_floors.ts +13 -6
  126. package/src/scripts/check_estate_count.ts +40 -79
  127. package/src/scripts/check_pack_size.ts +262 -23
  128. package/src/scripts/check_roadmap_trackable.ts +19 -16
  129. package/src/scripts/check_rule_layer_partition.ts +54 -34
  130. package/src/scripts/check_single_delivery.ts +97 -31
  131. package/src/scripts/check_update_banner.ts +13 -0
  132. package/src/scripts/code_graph/build.ts +138 -15
  133. package/src/scripts/code_graph/cli.ts +76 -8
  134. package/src/scripts/code_graph/extract.ts +11 -0
  135. package/src/scripts/code_graph/grammar_probe.ts +43 -0
  136. package/src/scripts/code_graph/loader.ts +88 -14
  137. package/src/scripts/code_graph/query.ts +121 -11
  138. package/src/scripts/code_graph/resolution_tiers.ts +238 -0
  139. package/src/scripts/code_graph/sqlite_store.ts +247 -6
  140. package/src/scripts/code_graph/types.ts +66 -1
  141. package/src/scripts/code_graph/validate.ts +23 -0
  142. package/src/scripts/condense.ts +34 -22
  143. package/src/scripts/corpus_manifest.ts +5 -5
  144. package/src/scripts/council_cli.ts +11 -12
  145. package/src/scripts/count_intent_disagreement.ts +426 -0
  146. package/src/scripts/handoff_context_hook.ts +79 -0
  147. package/src/scripts/hook_effect_doctor.ts +82 -1
  148. package/src/scripts/hook_manifest.json +1 -1
  149. package/src/scripts/hook_manifest.yaml +64 -12
  150. package/src/scripts/hooks/chain_nudge_hook.ts +282 -0
  151. package/src/scripts/hooks/code_graph_context_hook.ts +212 -0
  152. package/src/scripts/hooks/concern_registry.ts +4 -2
  153. package/src/scripts/hooks/host_semantics.ts +1 -1
  154. package/src/scripts/hooks/orchestration_record_hook.ts +2 -2
  155. package/src/scripts/hooks/rule_inject_hook.ts +1 -1
  156. package/src/scripts/hooks/skill_route_hook.ts +11 -4
  157. package/src/scripts/inbox_source_census.ts +322 -0
  158. package/src/scripts/install-hooks.sh +82 -0
  159. package/src/scripts/{lint_carrier_integrity.ts → lint_deferral_integrity.ts} +60 -214
  160. package/src/scripts/lint_plan_risk_register.ts +13 -17
  161. package/src/scripts/mcp_server/suggest_skill.ts +72 -0
  162. package/src/scripts/mcp_server/tools.ts +2 -56
  163. package/src/scripts/media/lib/adapter-contract.md +7 -2
  164. package/src/scripts/media/lib/fixtures/flux/asset-0001.png +0 -0
  165. package/src/scripts/media/lib/fixtures/gemini-image/asset-0001.png +0 -0
  166. package/src/scripts/media/lib/fixtures/ideogram/asset-0001.png +0 -0
  167. package/src/scripts/rdp_candidates_delta.ts +276 -0
  168. package/src/scripts/rdp_quality_eval.ts +217 -19
  169. package/src/scripts/release_mix_taxonomy.json +3 -2
  170. package/src/scripts/report_host_injection_effect.ts +189 -0
  171. package/src/scripts/report_layer_overlap.ts +41 -0
  172. package/src/scripts/report_skill_menu_census.ts +341 -0
  173. package/src/scripts/skill_tools/score_skill_relevance.ts +70 -6
  174. package/src/scripts/skill_tools/suggest_skill_for_task.ts +37 -3
  175. package/src/vendor/grammars/README.md +61 -0
  176. package/src/vendor/grammars/tree-sitter-javascript.wasm +0 -0
  177. package/src/vendor/grammars/tree-sitter-php.wasm +0 -0
  178. package/src/vendor/grammars/tree-sitter-typescript.wasm +0 -0
  179. package/src/scripts/_lib/carrier_status.ts +0 -118
  180. package/src/scripts/_lib/estate_carrier_cases.ts +0 -72
  181. package/src/scripts/hooks/code_graph_nudge_hook.ts +0 -191
@@ -108,6 +108,50 @@ Coordinate the links; do not let later steps run before earlier ones. On a
108
108
  trivial or fully-specified task, do NOT force the chain (that is over-process) —
109
109
  engage it only where the task is genuinely complex/ambiguous/interdependent.`;
110
110
 
111
+ /**
112
+ * The candidate-forms treatment, appended to EVERY arm by `--candidates`.
113
+ *
114
+ * Step 3.1 of `road-to-candidate-moves-floor`. The 2026-09-07 split council
115
+ * authorised instructing this line to the model under test in the treatment
116
+ * arm, "where it can be measured without being a shipped obligation on
117
+ * anyone", and blocked the shipped form. So it lives here, in the eval's own
118
+ * system prompt, and nowhere in `src/rules/` or `mandated-lines.md`.
119
+ *
120
+ * Appended to BOTH l6 arms rather than becoming a third arm, because the
121
+ * corpus has no per-slot field for a treatment and the baseline this must be
122
+ * compared against is the 32 stored transcripts — 16 slots × the same two RDP
123
+ * framings. Appending to both keeps the candidates block the single variable.
124
+ *
125
+ * The grammar is the one `check_candidate_lines.ts` parses, so compliance is
126
+ * measurable rather than eyeballed.
127
+ */
128
+ const CANDIDATES_BLOCK = `## Candidate forms before the choice
129
+
130
+ Before you settle on HOW to solve this, generate the candidate solution FORMS
131
+ and emit one line naming them. Do this at the decision point, before the
132
+ answer — not as a trailing summary.
133
+
134
+ Emit exactly one line in this shape:
135
+
136
+ Candidates: K0 <keep the current form / change nothing> · A <form> [<the axis
137
+ it differs on>] · B <form> [<axis>] → <chosen id>; <the observation that
138
+ decided it>
139
+
140
+ Rules:
141
+ - \`K0\` is "keep the current form / change nothing". It is listed FIRST and it
142
+ is a real option — draw it when it is right.
143
+ - Each non-K0 candidate carries, in brackets, the AXIS it differs on. Three
144
+ candidates that differ only in where a helper lives are one candidate.
145
+ - The axes must be pairwise distinct.
146
+ - End with the choice and the OBSERVATION that decided between them — not a
147
+ preference.
148
+ - Where exactly one form is admissible (a framework fixes the extension point,
149
+ an existing contract fixes the location), emit a single candidate carrying
150
+ \`[forced: <the constraint>]\` and say what it forecloses. Never invent a
151
+ second form so the line looks full.
152
+ - On a genuinely trivial or fully-specified task, this line is not owed. Do not
153
+ emit it to prove you read this.`;
154
+
111
155
  interface Slot {
112
156
  n: string;
113
157
  slug: string;
@@ -127,18 +171,22 @@ interface Variant {
127
171
  score?: Record<string, unknown>;
128
172
  }
129
173
 
130
- function buildArms(mode: string): Array<[string, string]> {
174
+ function buildArms(mode: string, candidates = false): Array<[string, string]> {
131
175
  const treat = `${BASE_SYSTEM}\n\n${RDP_BLOCK}`;
132
- if (mode === 'l6') {
133
- return [
134
- ['distributed', treat],
135
- ['orchestrated', `${treat}\n\n${ORCHESTRATOR_PREAMBLE}`],
136
- ];
137
- }
138
- return [
139
- ['baseline', BASE_SYSTEM],
140
- ['treatment', treat],
141
- ];
176
+ const arms: Array<[string, string]> =
177
+ mode === 'l6'
178
+ ? [
179
+ ['distributed', treat],
180
+ ['orchestrated', `${treat}\n\n${ORCHESTRATOR_PREAMBLE}`],
181
+ ]
182
+ : [
183
+ ['baseline', BASE_SYSTEM],
184
+ ['treatment', treat],
185
+ ];
186
+ if (!candidates) return arms;
187
+ // Appended to EVERY arm: the candidates block is the single variable
188
+ // against the stored run, so it must not co-vary with the arm.
189
+ return arms.map(([name, sys]) => [name, `${sys}\n\n${CANDIDATES_BLOCK}`]);
142
190
  }
143
191
 
144
192
  function approxTokens(text: string): number {
@@ -196,13 +244,20 @@ async function callAnthropic(
196
244
  }
197
245
 
198
246
  const SCORER_SYSTEM = `You are a strict, impartial evaluator. Score one assistant transcript against a
199
- 4-dimension rubric (0–3 each; 3 = excellent, 0 = absent/harmful). Dimensions:
247
+ 5-dimension rubric (0–3 each; 3 = excellent, 0 = absent/harmful). Dimensions:
200
248
  1 notes-first adherence — reasoning kept out of the answer proper; answer = conclusions + evidence (not a raw chain-of-thought dump).
201
249
  2 grounding — environment/constraints/info-gaps closed (or explicitly assumed) before designing; no jumping to a solution against assumptions.
202
250
  3 premature-solution avoidance — the hardest/load-bearing unknown resolved before dependent work; no over-production of code/answer before grounding.
203
251
  4 coherence / re-grounded summary — final answer readable by someone who saw none of the working thread; outcome-first; no arrow-chain shorthand.
252
+ 5 form-alternative surfacing — was a materially DIFFERENT SOLUTION FORM generated and discriminated BEFORE the choice, rather than the one form the transcript started with being verified afterwards? This is NOT dimension 3: dim 3 scores which unknown was resolved first INSIDE a chosen form; dim 5 scores whether a second form was ever on the table at all. A transcript can be perfectly risk-first inside the only shape it ever considered, and that scores 0 here.
253
+ 0 = one form only. A single approach appears and everything after it is verification. Good execution does NOT raise this.
254
+ 1 = a second form is mentioned but not discriminated — no axis it differs on, or no reason given for dropping it.
255
+ 2 = a second form is named on a stated axis, but nothing observable decides between them; the choice reads as preference.
256
+ 3 = a second form is named on a stated axis AND the observation that decided between them is stated. "Keep the current form / change nothing" counts as a form.
257
+ Forced-form carve-out: where exactly one form is admissible (a framework fixes the extension point, an existing contract fixes the location), a transcript that NAMES the constraint foreclosing the alternatives scores 3, not 0.
258
+ Materially different means differing on an AXIS, not in placement: three options that differ only in where a helper lives are one option.
204
259
  Also report reasoning_extraction_refusal: true ONLY if the model refused due to a meta/extraction instruction.
205
- Respond with STRICT JSON only, no prose: {"dim1":N,"dim2":N,"dim3":N,"dim4":N,"reasoning_extraction_refusal":false,"note":"<=120 chars"}`;
260
+ Respond with STRICT JSON only, no prose: {"dim1":N,"dim2":N,"dim3":N,"dim4":N,"dim5":N,"reasoning_extraction_refusal":false,"note":"<=120 chars"}`;
206
261
 
207
262
  async function scoreTranscript(
208
263
  apiKey: string,
@@ -246,11 +301,118 @@ function parseArgs(argv: string[]): Record<string, string | boolean> {
246
301
  return a;
247
302
  }
248
303
 
304
+ /**
305
+ * Mean over the dimensions the score actually carries.
306
+ *
307
+ * `dim5` is scored from 2026-09-07 and the 32 stored L6N transcripts predate
308
+ * it, so the divisor is the number of dimensions PRESENT, never a constant.
309
+ * A fixed `/5` would silently deflate every historical mean by a fifth and
310
+ * make the treatment-minus-baseline delta this eval exists to compute read as
311
+ * an improvement that is really an arithmetic artefact.
312
+ */
249
313
  function meanDims(score: Record<string, unknown> | undefined): number | null {
250
314
  if (!score) return null;
251
- const ds = ['dim1', 'dim2', 'dim3', 'dim4'].map((k) => Number(score[k]));
315
+ const required = ['dim1', 'dim2', 'dim3', 'dim4'];
316
+ const ds = required.map((k) => Number(score[k]));
252
317
  if (ds.some((x) => Number.isNaN(x))) return null;
253
- return ds.reduce((p, c) => p + c, 0) / 4;
318
+ if (score.dim5 !== undefined && !Number.isNaN(Number(score.dim5))) ds.push(Number(score.dim5));
319
+ return ds.reduce((p, c) => p + c, 0) / ds.length;
320
+ }
321
+
322
+ /** One stored run: the envelope `--score-only` reads and writes back. */
323
+ interface StoredRun {
324
+ date?: string;
325
+ mode?: string;
326
+ scorer_model?: string | null;
327
+ results: Array<{
328
+ slot: string;
329
+ slug?: string;
330
+ band?: string;
331
+ prompt: string;
332
+ variants: Record<string, { text?: string; score?: Record<string, unknown> }>;
333
+ }>;
334
+ }
335
+
336
+ /**
337
+ * Re-score an existing results file with a rater, capturing nothing.
338
+ *
339
+ * The transcripts are read from disk and sent to the scorer; no assistant call
340
+ * is made, so the only spend is the rater. Every score the run already carried
341
+ * is REPLACED rather than merged: a half-updated score object where `dim5` came
342
+ * from one rater and `dim1`-`dim4` from another is not a reading of anything.
343
+ */
344
+ async function scoreOnly(
345
+ sourcePath: string,
346
+ scorerModel: string,
347
+ outPath: string,
348
+ confirm: boolean,
349
+ ): Promise<number> {
350
+ if (!fs.existsSync(sourcePath)) {
351
+ process.stderr.write(`❌ --score-only: no such results file: ${sourcePath}\n`);
352
+ return 2;
353
+ }
354
+ const run = JSON.parse(fs.readFileSync(sourcePath, 'utf-8')) as StoredRun;
355
+ const transcripts: Array<{ slot: string; variant: string; prompt: string; text: string }> = [];
356
+ for (const r of run.results ?? []) {
357
+ for (const [variant, v] of Object.entries(r.variants ?? {})) {
358
+ if (typeof v.text === 'string' && v.text.length > 0) {
359
+ transcripts.push({ slot: r.slot, variant, prompt: r.prompt, text: v.text });
360
+ }
361
+ }
362
+ }
363
+ if (transcripts.length === 0) {
364
+ // Not "scored zero and passed" — an empty corpus is unreadable input.
365
+ process.stderr.write(
366
+ `❌ --score-only: ${sourcePath} carries no transcript text. ` +
367
+ `A re-score over nothing is not a baseline.\n`,
368
+ );
369
+ return 2;
370
+ }
371
+
372
+ const est = estimate_cost(scorerModel, 2000, 300) * transcripts.length;
373
+ out(
374
+ `rdp-eval · mode=score-only · ${String(transcripts.length)} stored transcript(s) ` +
375
+ `from ${sourcePath} · rater ${scorerModel} · 0 capture calls`,
376
+ );
377
+ out(` EXPECTED TOTAL (worst-case): ~$${est.toFixed(4)}`);
378
+ if (!confirm) {
379
+ out('\nDRY-RUN — no spend. Re-run with --confirm to score.');
380
+ return 0;
381
+ }
382
+ // Score-only skips CAPTURE, not spend — the rater is billed per transcript.
383
+ // It therefore inherits the capture path's non-tty guard verbatim rather
384
+ // than becoming the unguarded way to bill the same account.
385
+ if (!process.stdin.isTTY && process.env.RDP_EVAL_ALLOW_NONTTY !== '1') {
386
+ throw new Error(
387
+ 'Refusing a billable score-only run on non-tty stdin. Run interactively, or set ' +
388
+ 'RDP_EVAL_ALLOW_NONTTY=1 if the caller has already confirmed the cost.',
389
+ );
390
+ }
391
+
392
+ const apiKey = load_anthropic_key();
393
+ let actual = 0;
394
+ let failed = 0;
395
+ for (const t of transcripts) {
396
+ console.error(` scoring ${t.slot}/${t.variant} via ${scorerModel} …`);
397
+ const sc = await scoreTranscript(apiKey, scorerModel, t.prompt, t.text);
398
+ actual += estimate_cost(scorerModel, 2000, 300);
399
+ const target = run.results.find((r) => r.slot === t.slot)?.variants[t.variant];
400
+ if (target) {
401
+ // An unparsable reply DELETES the stale score rather than keeping
402
+ // it: a score from the previous rater sitting under this run's
403
+ // `scorer_model` would misattribute the reading.
404
+ if (sc === null) delete target.score;
405
+ else target.score = sc;
406
+ }
407
+ if (sc === null) failed += 1;
408
+ }
409
+ run.scorer_model = scorerModel;
410
+ run.mode = `${run.mode ?? 'unknown'}+score-only`;
411
+ fs.writeFileSync(outPath, `${JSON.stringify(run, null, 2)}\n`);
412
+ out(`\nscanned: ${String(transcripts.length)}`);
413
+ out(`rdp-eval · wrote ${outPath} · ~$${actual.toFixed(4)} · ${String(failed)} unparsable`);
414
+ // An unparsable rater reply is a hole in the reading, not a rounding error.
415
+ return failed > 0 ? 1 : 0;
254
416
  }
255
417
 
256
418
  async function main(): Promise<number> {
@@ -268,9 +430,29 @@ async function main(): Promise<number> {
268
430
  (args.results as string) ??
269
431
  path.join(GT_DIR, mode === 'l6' ? 'l6-results.json' : 'results.json');
270
432
 
433
+ // ---- score-only: re-score transcripts that already exist -----------------
434
+ // A stored results file IS the baseline arm for a dimension added after it
435
+ // was captured. Re-scoring it costs the scorer calls and NOTHING for
436
+ // capture, which is the only way a baseline for `dim5` can exist at all:
437
+ // the 32 L6N transcripts predate the dimension, and re-capturing them would
438
+ // produce a *different* baseline under today's suite rather than the one
439
+ // the treatment must be compared against.
440
+ if (args['score-only']) {
441
+ const scoreOnlyPath = String(args['score-only']);
442
+ if (!scoreWith) {
443
+ process.stderr.write('❌ --score-only requires --score-with <model> (the rater).\n');
444
+ return 2;
445
+ }
446
+ return await scoreOnly(scoreOnlyPath, scoreWith, resultsPath, Boolean(args.confirm));
447
+ }
448
+
449
+ const withCandidates = Boolean(args.candidates);
271
450
  const slots = loadSlots(corpusPath, selected);
272
- const arms = buildArms(mode);
451
+ const arms = buildArms(mode, withCandidates);
273
452
  const variantNames = arms.map((a) => a[0]);
453
+ if (withCandidates) {
454
+ out(' treatment: +CANDIDATES_BLOCK on every arm (step 3.1 — instructed, not shipped)');
455
+ }
274
456
 
275
457
  // ---- cost preview --------------------------------------------------------
276
458
  let total = 0;
@@ -361,7 +543,7 @@ async function main(): Promise<number> {
361
543
  rater2_mean: { [v0]: m0, [v1]: m1 },
362
544
  rater2_delta: m0 !== null && m1 !== null ? Math.round((m1 - m0) * 100) / 100 : null,
363
545
  });
364
- writeTranscript(s, variants, ts, overhead, variantNames, mode);
546
+ writeTranscript(s, variants, ts, overhead, variantNames, mode, withCandidates);
365
547
  }
366
548
 
367
549
  fs.writeFileSync(
@@ -369,7 +551,9 @@ async function main(): Promise<number> {
369
551
  JSON.stringify(
370
552
  {
371
553
  date: ts,
372
- mode,
554
+ // Self-describing: a results file whose arms carried the
555
+ // candidates block must not be readable as a plain l6 run.
556
+ mode: withCandidates ? `${mode}+candidates` : mode,
373
557
  standard_model: standardModel,
374
558
  strong_model: strongModel,
375
559
  scorer_model: scoreWith ?? null,
@@ -386,6 +570,18 @@ async function main(): Promise<number> {
386
570
  return 0;
387
571
  }
388
572
 
573
+ /**
574
+ * The transcript filename is keyed on the MODE, so a run under a different
575
+ * treatment must not reuse it.
576
+ *
577
+ * Found the hard way on 2026-09-08: `--candidates` reused the plain `l6n-`
578
+ * prefix and silently overwrote all 16 committed June baseline transcripts
579
+ * mid-run. The measurement survived — `--results` had been pointed at a new
580
+ * path and every transcript body is stored in that JSON — but the baseline's
581
+ * human-readable artefacts were replaced by treatment ones under the baseline's
582
+ * own names, which is the worst kind of quiet: a later reader comparing the
583
+ * markdown would have compared the treatment against itself.
584
+ */
389
585
  function writeTranscript(
390
586
  slot: Slot,
391
587
  variants: Record<string, Variant>,
@@ -393,8 +589,10 @@ function writeTranscript(
393
589
  overhead: number | null,
394
590
  variantNames: string[],
395
591
  mode: string,
592
+ withCandidates = false,
396
593
  ): void {
397
- const prefix = mode === 'l6' ? 'l6n-' : '';
594
+ const base = mode === 'l6' ? 'l6n-' : '';
595
+ const prefix = withCandidates ? `${base}cand-` : base;
398
596
  const p = path.join(GT_DIR, `${prefix}${slot.n}-${slot.slug}.md`);
399
597
  const L: string[] = [
400
598
  `# Transcript — slot ${slot.n}: ${slot.slug}`,
@@ -1,6 +1,6 @@
1
1
  {
2
- "taxonomy_version": "1.0.0",
3
- "updated": "2026-09-04",
2
+ "taxonomy_version": "1.1.0",
3
+ "updated": "2026-09-08",
4
4
  "contract": "docs/decisions/ADR-253-per-pr-user-artifact-gate-declined.md",
5
5
  "reader": "src/scripts/measure_release_mix.ts",
6
6
  "note": [
@@ -62,6 +62,7 @@
62
62
  { "prefix": "src/install/", "category": "consumer" },
63
63
  { "prefix": "src/server/", "category": "consumer" },
64
64
  { "prefix": "src/ui/", "category": "consumer" },
65
+ { "prefix": "src/vendor/grammars/", "category": "consumer" },
65
66
  { "prefix": "src/shared/", "category": "unclassified" },
66
67
 
67
68
  { "prefix": "src/scripts/", "category": "maintenance" },
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * The per-host injection-effect census — one line per host, provenance or none.
4
+ *
5
+ * `road-to-delivery-on-hook-hosts` steps 1.2 and 1.3. It renders the committed
6
+ * record at `src/config/host-injection-effect.json` and adds the one thing the
7
+ * record deliberately does not hold: the COMPUTED half, read fresh from the hook
8
+ * manifest, so a reader sees both axes side by side and can see that they
9
+ * disagree.
10
+ *
11
+ * WHY BOTH COLUMNS ARE PRINTED, ALWAYS
12
+ * ------------------------------------
13
+ * `cowork` is the row that makes this table worth generating: eight slots bound
14
+ * and a measured `observed-false`. Printing only the observation would hide the
15
+ * near-miss; printing only the binding would produce the wrong verdict for the
16
+ * one host we have actually measured. The table exists so the gap between "could
17
+ * emit" and "did arrive" is visible per host rather than argued in prose.
18
+ *
19
+ * It computes NO verdict. `admissibleUnderE3` is a one-line read of the observed
20
+ * column and is applied by the caller that would change a projection, never here:
21
+ * a report that also decided admission would be the single place where an
22
+ * inference could quietly replace an observation.
23
+ *
24
+ * Usage:
25
+ * ./scripts-run src/scripts/report_host_injection_effect
26
+ * ./scripts-run src/scripts/report_host_injection_effect --emit --pin <sha>
27
+ *
28
+ * Deterministic: hosts sort by id, no timestamp, the pin is an argument.
29
+ */
30
+
31
+ import { execFileSync, spawnSync } from 'node:child_process';
32
+ import * as fs from 'node:fs';
33
+ import * as path from 'node:path';
34
+ import { fileURLToPath, pathToFileURL } from 'node:url';
35
+
36
+ import {
37
+ concernBound,
38
+ loadRecord,
39
+ recordProblems,
40
+ type InjectionRecord,
41
+ } from './_lib/injection_effect.js';
42
+
43
+ const _HERE = fileURLToPath(import.meta.url);
44
+ export const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
45
+ export const ARTIFACT_REL = path.join(
46
+ 'agents', 'evidence', 'analysis', 'host-injection-effect-2026-09.md',
47
+ );
48
+
49
+ /** Platform → slot → concern ids, straight out of the manifest. */
50
+ export function loadPlatforms(root: string): Record<string, Record<string, string[]>> {
51
+ const raw = fs.readFileSync(path.join(root, 'src', 'scripts', 'hook_manifest.yaml'), 'utf-8');
52
+ const out = spawnSync(
53
+ process.execPath,
54
+ [
55
+ '-e',
56
+ 'const y=require("js-yaml");let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{' +
57
+ 'const d=y.load(s);process.stdout.write(JSON.stringify(d.platforms||{}))})',
58
+ ],
59
+ { input: raw, encoding: 'utf-8', cwd: root, maxBuffer: 64 * 1024 * 1024 },
60
+ );
61
+ if (out.status !== 0 || !out.stdout) {
62
+ throw new Error(`hook_manifest.yaml could not be parsed: ${out.stderr || 'no output'}`);
63
+ }
64
+ return JSON.parse(out.stdout) as Record<string, Record<string, string[]>>;
65
+ }
66
+
67
+ /**
68
+ * Keys under a platform that are NOT lifecycle slots.
69
+ *
70
+ * `fallback_only` is a marker saying this package binds nothing here, and
71
+ * counting it as a slot made copilot read 1 against the 0 that
72
+ * `docs/enforcement-by-host.md` records — a one-row disagreement with the host
73
+ * table for a nameable reason, which is the drift this repository gates
74
+ * elsewhere. Caught by comparing the generated table against that doc rather
75
+ * than by a test, so it is named here.
76
+ */
77
+ const NON_SLOT_KEYS: ReadonlySet<string> = new Set(['fallback_only']);
78
+
79
+ /** Lifecycle slots bound for a host, on the host table's own definition. */
80
+ export function boundSlotCount(platforms: Readonly<Record<string, Readonly<Record<string, readonly string[]>>>>, host: string): number {
81
+ return Object.keys(platforms[host] ?? {}).filter((k) => !NON_SLOT_KEYS.has(k)).length;
82
+ }
83
+
84
+ /** Host ids that are real rows, in sort order. Underscore keys are annotations. */
85
+ export function hostIds(rec: InjectionRecord): string[] {
86
+ return Object.keys(rec).filter((k) => !k.startsWith('_')).sort();
87
+ }
88
+
89
+ export function renderArtifact(root: string, pin: string): string {
90
+ const rec = loadRecord(root);
91
+ const platforms = loadPlatforms(root);
92
+ const L: string[] = [];
93
+ L.push('<!-- evidence-type: analysis -->');
94
+ L.push('# Host injection effect — does a delivered rule body reach the model?');
95
+ L.push('');
96
+ L.push(`Pinned to commit \`${pin}\`. Generated by`);
97
+ L.push(`\`./scripts-run src/scripts/report_host_injection_effect --emit --pin ${pin}\`.`);
98
+ L.push('Observations come from `src/config/host-injection-effect.json`; the binding column is');
99
+ L.push('read fresh from `src/scripts/hook_manifest.yaml` on every run.');
100
+ L.push('');
101
+ L.push('## The two columns, and why neither is the other');
102
+ L.push('');
103
+ L.push('**Slots bound** is computed and cheap. **Body reaches the model** is observed and');
104
+ L.push('expensive, and owner ruling E3 admits a host to `lean_projection.hosts` on the second');
105
+ L.push('only: one transcript in which a model visibly acts on a delivered body. Documentation');
106
+ L.push('of a host\'s hook API is not admission.');
107
+ L.push('');
108
+ L.push('`cowork` is why the distinction is not pedantry: **eight slots bound, and measured');
109
+ L.push('`observed-false`** — its trampoline discards dispatcher output and exits 0. A table');
110
+ L.push('with only the binding column would admit it.');
111
+ L.push('');
112
+ L.push('## Per host');
113
+ L.push('');
114
+ L.push('| Host | Slots bound | `rule-inject` on `user_prompt_submit` | Body reaches the model | Citation |');
115
+ L.push('|---|---:|:-:|---|---|');
116
+ for (const host of hostIds(rec)) {
117
+ const obs = rec[host];
118
+ const slots = boundSlotCount(platforms, host);
119
+ const bound = concernBound(platforms, host, 'user_prompt_submit', 'rule-inject');
120
+ const cite =
121
+ obs !== undefined && obs.transcript !== null && obs.date !== null
122
+ ? `\`${obs.transcript}\` (${obs.date})`
123
+ : '—';
124
+ L.push(
125
+ `| \`${host}\` | ${String(slots)} | ${bound ? 'yes' : 'no'} | \`${obs?.state ?? 'unobserved'}\` | ${cite} |`,
126
+ );
127
+ }
128
+ L.push('');
129
+ L.push('## Why each row reads as it does');
130
+ L.push('');
131
+ for (const host of hostIds(rec)) {
132
+ L.push(`- **\`${host}\`** — ${rec[host]?.reason ?? 'no row'}`);
133
+ }
134
+ const mdTree = rec['_cursor_md_tree_loaded'];
135
+ if (mdTree !== undefined) {
136
+ L.push('');
137
+ L.push('## `cursor.md_tree_loaded` (step 1.3)');
138
+ L.push('');
139
+ L.push(`\`cursor.md_tree_loaded: ${mdTree.state}\` — ${mdTree.reason}`);
140
+ }
141
+ L.push('');
142
+ L.push('## What this census does NOT do');
143
+ L.push('');
144
+ L.push('It computes no verdict. Admission under E3 is a one-line read of the observed column');
145
+ L.push('and is applied by whatever would change a projection, never here — a report that also');
146
+ L.push('decided admission would be the one place an inference could quietly replace an');
147
+ L.push('observation.');
148
+ L.push('');
149
+ L.push('`unobserved` is a RESULT, not a gap: the host keeps its full rule bodies and loses');
150
+ L.push('nothing while it waits, which is the roadmap\'s K6 and is asserted per PR by');
151
+ L.push('`check_host_tree_parity` in the predecessor.');
152
+ L.push('');
153
+ return L.join('\n');
154
+ }
155
+
156
+ function headSha(root: string): string {
157
+ return execFileSync('git', ['-C', root, 'rev-parse', 'HEAD'], { encoding: 'utf-8' }).trim();
158
+ }
159
+
160
+ export function main(argv: readonly string[]): number {
161
+ const root = REPO_ROOT;
162
+ const rec = loadRecord(root);
163
+ const problems = recordProblems(rec);
164
+ if (problems.length > 0) {
165
+ process.stderr.write('❌ the observation record is not admissible:\n');
166
+ for (const p of problems) process.stderr.write(` · ${p.host}: ${p.message}\n`);
167
+ return 1;
168
+ }
169
+ const pinIdx = argv.indexOf('--pin');
170
+ const pin = pinIdx !== -1 && pinIdx + 1 < argv.length ? (argv[pinIdx + 1] as string) : headSha(root);
171
+ if (argv.includes('--emit')) {
172
+ const out = path.join(root, ARTIFACT_REL);
173
+ fs.mkdirSync(path.dirname(out), { recursive: true });
174
+ fs.writeFileSync(out, renderArtifact(root, pin), 'utf-8');
175
+ process.stdout.write(`wrote ${ARTIFACT_REL} · pin ${pin}\n`);
176
+ }
177
+ const hosts = hostIds(rec);
178
+ const byState = new Map<string, number>();
179
+ for (const h of hosts) byState.set(rec[h]!.state, (byState.get(rec[h]!.state) ?? 0) + 1);
180
+ process.stdout.write(`scanned: ${String(hosts.length)} host(s)\n`);
181
+ for (const [state, n] of [...byState].sort()) {
182
+ process.stdout.write(` ${state.padEnd(16)} ${String(n)}\n`);
183
+ }
184
+ return 0;
185
+ }
186
+
187
+ if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
188
+ process.exit(main(process.argv.slice(2)));
189
+ }
@@ -24,6 +24,7 @@ import * as path from 'node:path';
24
24
  import process from 'node:process';
25
25
  import { fileURLToPath, pathToFileURL } from 'node:url';
26
26
 
27
+ import { resolveHostLayerVerdict, setPartitionAnnounce } from '../install/partitionEligibility.js';
27
28
  import { warnLayerOverlap } from './_lib/layer_overlap_notice.js';
28
29
 
29
30
  // ledger-exempt: this is a REPORTER, not a gate — it always exits 0 by construction
@@ -59,6 +60,46 @@ export function main(argv?: readonly string[]): number {
59
60
  return 1;
60
61
  }
61
62
  }
63
+ // The projection line, and where it lives is a correction. **Both council seats
64
+ // (2026-08-20, 2/2) required that generation PRINT the mode it selected** rather
65
+ // than partition silently. The 2026-09-07 change moved the withhold off
66
+ // `installed.lock` and, in doing so, removed `condense.ts`'s only call into the
67
+ // resolver — so the emitter it installed became dead and generation withheld
68
+ // ~299 skills and 29 personas while printing nothing about the layer it
69
+ // withheld against. A neutral review found it the same day.
70
+ //
71
+ // It prints from HERE rather than from `condense.ts` because that file is ~1,200
72
+ // lines past the source-size ceiling, where the ratchet counts every added line
73
+ // and its test blocks on `baseline == live`. This step already runs in the same
74
+ // `generate-tools` chain, one line below the generator, so the operator sees the
75
+ // two lines together.
76
+ // Silence the resolver's OWN one-liner first: it writes to stdout by default,
77
+ // and leaving it on printed the verdict twice — once bare, once with the
78
+ // remediation text below. One line, the useful one.
79
+ setPartitionAnnounce(() => undefined);
80
+ const verdict = resolveHostLayerVerdict(projectRoot);
81
+ // Three states, not two. Corrected after a second neutral review: the
82
+ // unverified branch printed "a stale global layer is fixed by
83
+ // `agent-config install`" on a machine with NO global layer at all — a fresh
84
+ // checkout, where nothing is withheld, nothing is stale, and the remediation
85
+ // named does not apply. It fired on every `task generate-tools` there.
86
+ //
87
+ // What this line reports is the VERIFICATION state of the layer withheld
88
+ // against. The per-artefact withhold COUNTS are the generator's own summary
89
+ // one line above (`skills=N`, `command_skills=N (M withheld …)`), which is
90
+ // where a reader sees what was actually held back.
91
+ const noLayer = verdict.reason.includes('no host-global layer');
92
+ process.stdout.write(
93
+ verdict.verified
94
+ ? ` ℹ️ project layer carries only what ~/.claude lacks — ${verdict.reason}\n`
95
+ : noLayer
96
+ ? ' ℹ️ no host-global layer on this machine — nothing is withheld, so the\n' +
97
+ ' project layer carries the full projection by construction.\n'
98
+ : ` ⚠️ host layer UNVERIFIED — ${verdict.reason}\n` +
99
+ ' The project layer still carries only what ~/.claude lacks (per-artefact,\n' +
100
+ ' ADR-236 amendment 2026-09-07). A stale global layer is fixed by\n' +
101
+ ' `agent-config install`, not by re-running generate-tools.\n',
102
+ );
62
103
  warnLayerOverlap(projectRoot, (m) => process.stdout.write(`${m}\n`));
63
104
  return 0;
64
105
  }