@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.
- package/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +198 -0
- package/CREDITS.md +37 -0
- package/dist/agent-src/commands/analyze/inbox.md +380 -5
- package/dist/agent-src/commands/analyze/repo.md +4 -2
- package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +69 -0
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +5 -5
- package/dist/agent-src/rules/external-code-graph-interop.md +19 -14
- package/dist/agent-src/rules/token-efficiency.md +26 -3
- package/dist/agent-src/scripts/_cli_entry.ts +51 -0
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +14 -23
- package/dist/agent-src/scripts/roadmap_gates.ts +4 -20
- package/dist/agent-src/scripts/roadmap_set_step.ts +5 -2
- package/dist/agent-src/scripts/stubs_due.ts +4 -18
- package/dist/agent-src/scripts/update_roadmap_progress.ts +7 -24
- package/dist/agent-src/skills/code-intelligence/SKILL.md +25 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +17 -0
- package/dist/agent-src/templates/agent-settings.md +0 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli-delegate/_cli_entry.js +8 -0
- package/dist/cli-delegate/_lib/pin_resolver.js +170 -0
- package/dist/cli-delegate/archival_sweep.js +8 -0
- package/dist/cli-delegate/archive_completed_roadmaps.js +494 -0
- package/dist/cli-delegate/check_update_banner.js +923 -0
- package/dist/cli-delegate/chunk-5EK5WQN6.js +28 -0
- package/dist/cli-delegate/chunk-7XGD2OEU.js +96 -0
- package/dist/cli-delegate/{chunk-643Z7FIC.js → chunk-BIXEDQPZ.js} +6 -0
- package/dist/cli-delegate/chunk-DBH4AHDK.js +155 -0
- package/dist/cli-delegate/chunk-JO25G5H7.js +20 -0
- package/dist/cli-delegate/chunk-LTYVIY76.js +7961 -0
- package/dist/cli-delegate/chunk-QAJS5YWY.js +999 -0
- package/dist/cli-delegate/{chunk-ZVBOHSVF.js → chunk-TGIPZBCW.js} +1 -1
- package/dist/cli-delegate/{chunk-YS6XTLEK.js → chunk-TYSY33MD.js} +56 -6
- package/dist/cli-delegate/chunk-ULET4BYS.js +328 -0
- package/dist/cli-delegate/chunk-YDQSHTD7.js +187 -0
- package/dist/cli-delegate/chunk-YWVXLR44.js +48 -0
- package/dist/cli-delegate/chunk-ZZQBUHFK.js +273 -0
- package/dist/cli-delegate/cmd_conformance.js +2 -2
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_handoff.js +26 -2
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +1 -1
- package/dist/cli-delegate/cmd_uninstall.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +1 -1
- package/dist/cli-delegate/dashboard_mode.js +16 -0
- package/dist/cli-delegate/gate_budget.js +24 -0
- package/dist/cli-delegate/gate_execute.js +25 -0
- package/dist/cli-delegate/guarded_baseline.js +18 -0
- package/dist/cli-delegate/resume_probe.js +22 -0
- package/dist/cli-delegate/roadmap_gates.js +638 -0
- package/dist/cli-delegate/roadmap_set_step.js +173 -0
- package/dist/cli-delegate/stub_queue.js +47 -0
- package/dist/cli-delegate/stubs_due.js +38 -0
- package/dist/cli-delegate/ticket_bundles.js +59 -0
- package/dist/cli-delegate/update_roadmap_progress.js +60 -0
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +8 -8
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +1193 -917
- package/dist/install/claudeLayerCarriage.js +237 -0
- package/dist/install/claudePathsPlan.js +79 -0
- package/dist/install/hostLayerFingerprint.js +1 -1
- package/dist/install/install.mjs +6 -0
- package/dist/install/partitionEligibility.js +231 -117
- package/dist/install/ruleLayerPartition.js +10 -7
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +39219 -38995
- package/dist/scripts/condense.js +32 -25
- package/docs/MIGRATION.md +49 -1
- package/docs/contracts/settings-classes.md +1 -1
- package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +73 -0
- package/package.json +7 -4
- package/src/agent-src/scripts/_cli_entry.ts +51 -0
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +14 -23
- package/src/agent-src/scripts/roadmap_gates.ts +4 -20
- package/src/agent-src/scripts/roadmap_set_step.ts +5 -2
- package/src/agent-src/scripts/stubs_due.ts +4 -18
- package/src/agent-src/scripts/update_roadmap_progress.ts +7 -24
- package/src/config/continuity-surface.json +509 -0
- package/src/config/estate-count-budget.json +1 -1
- package/src/config/gate-coverage.yml +29 -8
- package/src/config/gate-violation-baselines.json +8 -8
- package/src/config/gitignore-block.txt +19 -0
- package/src/config/host-injection-effect.json +73 -0
- package/src/config/packed-binary-manifest.json +33 -0
- package/src/config/preamble-payload-budget.json +112 -105
- package/src/config/publish-surface.json +4 -2
- package/src/install/claudeLayerCarriage.ts +273 -0
- package/src/install/claudePathsPlan.ts +76 -0
- package/src/install/hostLayerFingerprint.ts +1 -1
- package/src/install/partitionEligibility.ts +261 -127
- package/src/install/ruleLayerPartition.ts +19 -23
- package/src/scripts/_dispatch.bash +77 -27
- package/src/scripts/_lib/auto_dispatch.ts +2 -2
- package/src/scripts/_lib/catalogue_layer_parity.ts +146 -0
- package/src/scripts/_lib/continuity_surface.ts +296 -0
- package/src/scripts/_lib/corpus_manifest.ts +26 -9
- package/src/scripts/_lib/hook_settings.ts +1 -1
- package/src/scripts/_lib/injection_effect.ts +151 -0
- package/src/scripts/_lib/layer_overlap_notice.ts +50 -6
- package/src/scripts/_lib/packed_binary_predicate.ts +225 -0
- package/src/scripts/_lib/pin_resolver.ts +13 -0
- package/src/scripts/_lib/regression_neighbourhood.ts +1 -1
- package/src/scripts/_lib/retired_status.ts +74 -0
- package/src/scripts/_lib/skill_catalogue.ts +156 -21
- package/src/scripts/_lib/subagent_capsule.ts +117 -8
- package/src/scripts/ai_council/api_transport.ts +5 -0
- package/src/scripts/ai_council/deliberation_metrics.ts +353 -0
- package/src/scripts/ai_council/prompts.ts +9 -0
- package/src/scripts/ai_council/synthesis_disagreement.ts +206 -0
- package/src/scripts/capture_skill_catalogue.ts +24 -11
- package/src/scripts/check_candidate_lines.ts +499 -0
- package/src/scripts/check_continuity_surface.ts +475 -0
- package/src/scripts/check_dependency_floors.ts +13 -6
- package/src/scripts/check_estate_count.ts +40 -79
- package/src/scripts/check_pack_size.ts +262 -23
- package/src/scripts/check_roadmap_trackable.ts +19 -16
- package/src/scripts/check_rule_layer_partition.ts +54 -34
- package/src/scripts/check_single_delivery.ts +97 -31
- package/src/scripts/check_update_banner.ts +13 -0
- package/src/scripts/code_graph/build.ts +138 -15
- package/src/scripts/code_graph/cli.ts +76 -8
- package/src/scripts/code_graph/extract.ts +11 -0
- package/src/scripts/code_graph/grammar_probe.ts +43 -0
- package/src/scripts/code_graph/loader.ts +88 -14
- package/src/scripts/code_graph/query.ts +121 -11
- package/src/scripts/code_graph/resolution_tiers.ts +238 -0
- package/src/scripts/code_graph/sqlite_store.ts +247 -6
- package/src/scripts/code_graph/types.ts +66 -1
- package/src/scripts/code_graph/validate.ts +23 -0
- package/src/scripts/condense.ts +34 -22
- package/src/scripts/corpus_manifest.ts +5 -5
- package/src/scripts/council_cli.ts +11 -12
- package/src/scripts/count_intent_disagreement.ts +426 -0
- package/src/scripts/handoff_context_hook.ts +79 -0
- package/src/scripts/hook_effect_doctor.ts +82 -1
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +64 -12
- package/src/scripts/hooks/chain_nudge_hook.ts +282 -0
- package/src/scripts/hooks/code_graph_context_hook.ts +212 -0
- package/src/scripts/hooks/concern_registry.ts +4 -2
- package/src/scripts/hooks/host_semantics.ts +1 -1
- package/src/scripts/hooks/orchestration_record_hook.ts +2 -2
- package/src/scripts/hooks/rule_inject_hook.ts +1 -1
- package/src/scripts/hooks/skill_route_hook.ts +11 -4
- package/src/scripts/inbox_source_census.ts +322 -0
- package/src/scripts/install-hooks.sh +82 -0
- package/src/scripts/{lint_carrier_integrity.ts → lint_deferral_integrity.ts} +60 -214
- package/src/scripts/lint_plan_risk_register.ts +13 -17
- package/src/scripts/mcp_server/suggest_skill.ts +72 -0
- package/src/scripts/mcp_server/tools.ts +2 -56
- package/src/scripts/media/lib/adapter-contract.md +7 -2
- package/src/scripts/media/lib/fixtures/flux/asset-0001.png +0 -0
- package/src/scripts/media/lib/fixtures/gemini-image/asset-0001.png +0 -0
- package/src/scripts/media/lib/fixtures/ideogram/asset-0001.png +0 -0
- package/src/scripts/rdp_candidates_delta.ts +276 -0
- package/src/scripts/rdp_quality_eval.ts +217 -19
- package/src/scripts/release_mix_taxonomy.json +3 -2
- package/src/scripts/report_host_injection_effect.ts +189 -0
- package/src/scripts/report_layer_overlap.ts +41 -0
- package/src/scripts/report_skill_menu_census.ts +341 -0
- package/src/scripts/skill_tools/score_skill_relevance.ts +70 -6
- package/src/scripts/skill_tools/suggest_skill_for_task.ts +37 -3
- package/src/vendor/grammars/README.md +61 -0
- package/src/vendor/grammars/tree-sitter-javascript.wasm +0 -0
- package/src/vendor/grammars/tree-sitter-php.wasm +0 -0
- package/src/vendor/grammars/tree-sitter-typescript.wasm +0 -0
- package/src/scripts/_lib/carrier_status.ts +0 -118
- package/src/scripts/_lib/estate_carrier_cases.ts +0 -72
- 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
|
-
|
|
133
|
-
|
|
134
|
-
[
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
3
|
-
"updated": "2026-09-
|
|
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
|
}
|