@event4u/agent-config 9.31.0 → 9.33.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 (107) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +146 -0
  3. package/dist/agent-src/commands/review/routing.md +3 -4
  4. package/dist/agent-src/contexts/communication/rules-auto/reply-close-mechanics.md +65 -0
  5. package/dist/agent-src/contexts/execution/host-capability-manifest.md +32 -1
  6. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +3 -0
  7. package/dist/agent-src/rules/delegation-policy.md +17 -1
  8. package/dist/agent-src/scripts/roadmap_gates.ts +107 -3
  9. package/dist/agent-src/skills/docker/SKILL.md +11 -0
  10. package/dist/agent-src/skills/laravel/SKILL.md +11 -0
  11. package/dist/agent-src/skills/review-routing/SKILL.md +23 -6
  12. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  13. package/dist/agent-src/templates/scripts/ownership-map.example.yml +1 -1
  14. package/dist/agent-src/templates/scripts/work_engine/delivery_state.ts +6 -0
  15. package/dist/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +332 -0
  16. package/dist/agent-src/templates/scripts/work_engine/directives/backend/refine.ts +106 -1
  17. package/dist/agent-src/templates/scripts/work_engine/directives/backend/test.ts +50 -15
  18. package/dist/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +49 -15
  19. package/dist/agent-src/templates/scripts/work_engine/state.ts +74 -0
  20. package/dist/agent-src/templates/scripts/work_engine/state_io.ts +6 -0
  21. package/dist/cli/registry.js +4 -4
  22. package/dist/cli/registry.js.map +1 -1
  23. package/dist/cli-delegate/chunk-2WELHO3P.js +582 -0
  24. package/dist/cli-delegate/chunk-HTIU3W56.js +66 -0
  25. package/dist/cli-delegate/{chunk-ITIQSGOQ.js → chunk-MA4GFWDY.js} +7 -4
  26. package/dist/cli-delegate/{chunk-2WCLVDWC.js → chunk-PMQ6Z3T3.js} +1642 -165
  27. package/dist/cli-delegate/chunk-PZWRPRWP.js +38 -0
  28. package/dist/cli-delegate/{chunk-PFBV6XRT.js → chunk-Q26HT4QT.js} +1 -63
  29. package/dist/cli-delegate/{chunk-LVRYBKTI.js → chunk-YPAWXXXR.js} +158 -0
  30. package/dist/cli-delegate/cmd_conformance.js +308 -227
  31. package/dist/cli-delegate/cmd_doctor.js +4 -2
  32. package/dist/cli-delegate/cmd_export.js +1 -1
  33. package/dist/cli-delegate/cmd_handoff.js +160 -58
  34. package/dist/cli-delegate/cmd_prune.js +1 -1
  35. package/dist/cli-delegate/cmd_session_recycle.js +30 -5
  36. package/dist/cli-delegate/cmd_settings_get.js +4 -2
  37. package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
  38. package/dist/cli-delegate/cmd_settings_set.js +5 -3
  39. package/dist/cli-delegate/cmd_sync.js +1 -1
  40. package/dist/cli-delegate/cmd_uninstall.js +1 -1
  41. package/dist/cli-delegate/cmd_validate.js +1 -1
  42. package/dist/discovery/deprecation-report.md +1 -1
  43. package/dist/discovery/discovery-manifest.json +8 -8
  44. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  45. package/dist/discovery/discovery-manifest.summary.md +1 -1
  46. package/dist/discovery/orphan-report.md +1 -1
  47. package/dist/discovery/packs.json +2 -2
  48. package/dist/discovery/trust-report.md +1 -1
  49. package/dist/discovery/workspaces.json +2 -2
  50. package/dist/hooks/dispatch.js +1136 -690
  51. package/dist/install/install.mjs +7 -4
  52. package/dist/mcp/registry-manifest.json +2 -2
  53. package/dist/mcp/server.json +2 -2
  54. package/dist/mcp/server.mjs +3688 -1159
  55. package/package.json +1 -1
  56. package/src/agent-src/scripts/roadmap_gates.ts +107 -3
  57. package/src/agent-src/templates/scripts/ownership-map.example.yml +1 -1
  58. package/src/agent-src/templates/scripts/work_engine/delivery_state.ts +6 -0
  59. package/src/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +332 -0
  60. package/src/agent-src/templates/scripts/work_engine/directives/backend/refine.ts +106 -1
  61. package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +50 -15
  62. package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +49 -15
  63. package/src/agent-src/templates/scripts/work_engine/state.ts +74 -0
  64. package/src/agent-src/templates/scripts/work_engine/state_io.ts +6 -0
  65. package/src/config/cost-parity-budget.json +167 -0
  66. package/src/config/gate-coverage.yml +44 -5
  67. package/src/config/quorum-attendance-budget.json +39 -0
  68. package/src/scripts/_cli/cmd_doctor.ts +69 -1
  69. package/src/scripts/_cli/cmd_session_recycle.ts +44 -3
  70. package/src/scripts/_cli/handoff_generate.ts +36 -1
  71. package/src/scripts/_cli/handoff_sessions.ts +140 -9
  72. package/src/scripts/_dispatch.bash +5 -3
  73. package/src/scripts/_lib/carrier_divergence.ts +98 -27
  74. package/src/scripts/_lib/envelope_grounding.ts +206 -0
  75. package/src/scripts/_lib/host_capability.ts +79 -0
  76. package/src/scripts/_lib/orchestration_record.ts +45 -0
  77. package/src/scripts/_lib/session_eol.ts +85 -0
  78. package/src/scripts/_lib/subagent_capsule.ts +256 -1
  79. package/src/scripts/ai_council/clients.ts +31 -0
  80. package/src/scripts/ai_council/events_log.ts +128 -6
  81. package/src/scripts/ai_council/orchestrator.ts +12 -3
  82. package/src/scripts/ai_council/pricing.ts +48 -1
  83. package/src/scripts/ai_council/quorum.ts +20 -0
  84. package/src/scripts/ai_council/session.ts +11 -0
  85. package/src/scripts/ask_transport.ts +20 -1
  86. package/src/scripts/cache_realization_report.ts +38 -0
  87. package/src/scripts/check_release_highlights.ts +7 -1
  88. package/src/scripts/check_secret_leak.test.ts +43 -0
  89. package/src/scripts/check_secret_leak.ts +12 -0
  90. package/src/scripts/conformance_scan.ts +105 -0
  91. package/src/scripts/cost/track.mjs +39 -0
  92. package/src/scripts/cost_summary.ts +104 -1
  93. package/src/scripts/council_cli.ts +131 -13
  94. package/src/scripts/dispatch_economy_report.ts +5 -2
  95. package/src/scripts/handoff_context_hook.ts +66 -18
  96. package/src/scripts/hooks/session_eol_hook.ts +12 -12
  97. package/src/scripts/install.ts +20 -6
  98. package/src/scripts/lint_budget_ownership.ts +81 -0
  99. package/src/scripts/lint_skill_frontmatter_safety.ts +55 -1
  100. package/src/scripts/lint_workflow_paths.ts +384 -0
  101. package/src/scripts/preamble_byte_census.ts +9 -1
  102. package/src/scripts/report_carrier_divergence.ts +104 -29
  103. package/src/scripts/report_conformance_funnel.ts +51 -4
  104. package/src/scripts/routing_doctor.ts +25 -2
  105. package/src/scripts/schemas/dod.schema.json +45 -0
  106. package/src/scripts/schemas/subagent.schema.json +3 -2
  107. package/dist/cli-delegate/chunk-RVGBH4XK.js +0 -142
@@ -35,6 +35,16 @@
35
35
  * ignored, which is the failure `check-rule-invariants` already paid for once.
36
36
  * The point is that the condition becomes VISIBLE, not that it becomes illegal.
37
37
  *
38
+ * BEFORE QUOTING A COUNT FROM THIS REPORT
39
+ * ---------------------------------------
40
+ * Read `agents/settings/contexts/carrier-divergence-109-vs-24.md` first. The
41
+ * figure "109 divergent pairs, binding undefined" reached five independent
42
+ * release reviews as the tree's largest technical debt; all 109 carry
43
+ * byte-identical prose, and the real actionable set is the 24-pair `paths:`
44
+ * subset below, in the over-delivery direction. That correction had no stable
45
+ * surface, which is why every fresh reader re-derived the wrong number from
46
+ * this report's own output.
47
+ *
38
48
  * THE FOUR CLASSES, AND WHICH ONE A READER MUST ACT ON
39
49
  * ---------------------------------------------------
40
50
  * body-diff the two carriers deliver different PROSE. This is the class
@@ -52,13 +62,25 @@
52
62
  * reader acting on that would have believed the host resolves
53
63
  * something it does not.
54
64
  * frontmatter-only the prose is byte-identical and only the metadata block
55
- * differs. The host delivers the prose without that block, so
56
- * nothing a reader must act on reaches the model differently.
57
- * Split out because it was, measured, the ENTIRE content of
58
- * the `body-diff` class: 109 of 109 on 2026-08-10. Left inside
59
- * body-diff it manufactured the report's only actionable
60
- * class out of a metadata difference — the same mistake the
61
- * `unreadable` branch below already refuses to make.
65
+ * differs. Split out because it was, measured, the ENTIRE
66
+ * content of the `body-diff` class: 109 of 109 on 2026-08-10.
67
+ * Left inside body-diff it manufactured the report's only
68
+ * actionable class out of a metadata difference — the same
69
+ * mistake the `unreadable` branch below already refuses to
70
+ * make.
71
+ * NOT inert as a whole, and an earlier version of this entry
72
+ * wrongly said it was: the `paths:` SUBSET below is reported
73
+ * separately and IS actionable. Only the remainder — keys this
74
+ * host does not read — is safe to ignore.
75
+ * `paths:` subset a `frontmatter-only` pair whose two copies disagree about
76
+ * `paths`, the one frontmatter key the host reads. It decides
77
+ * WHEN a rule loads (matching-file read and no /compact
78
+ * re-injection, versus unconditionally at launch), so the copy
79
+ * without `paths` DEFEATS the other's scoping and an
80
+ * obligation someone deliberately scoped is silently unscoped.
81
+ * Measured 2026-08-10: 24 of the 109, all of them `paths` in
82
+ * the project copy and none in the global one. Identical prose
83
+ * does not make a schedule difference inert.
62
84
  * provenance-only identical after removing the two installer ownership keys.
63
85
  * Structural, expected, and decided against closing —
64
86
  * `agents/settings/contexts/dedup-reachability-refusal.md`.
@@ -83,7 +105,13 @@ import * as path from 'node:path';
83
105
  import process from 'node:process';
84
106
  import { fileURLToPath, pathToFileURL } from 'node:url';
85
107
 
86
- import { comparePair, type PairVerdict, proseEqual } from './_lib/carrier_divergence.js';
108
+ import {
109
+ comparePair,
110
+ frontmatterPaths,
111
+ type PairVerdict,
112
+ proseEqual,
113
+ splitFrontmatter,
114
+ } from './_lib/carrier_divergence.js';
87
115
 
88
116
  const _HERE = fileURLToPath(import.meta.url);
89
117
  const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
@@ -124,6 +152,13 @@ export interface CarrierDivergence {
124
152
  * something a reader has to act on.
125
153
  */
126
154
  frontmatterOnly: string[];
155
+ /**
156
+ * A SUBSET of `frontmatterOnly` whose two copies disagree about `paths:` —
157
+ * the one frontmatter key this host reads. Same prose, different load
158
+ * schedule, so this one IS actionable and must not be reported as inert
159
+ * metadata.
160
+ */
161
+ pathsScopeDiff: string[];
127
162
  bodyDiff: string[];
128
163
  projectOnly: string[];
129
164
  globalOnly: string[];
@@ -152,7 +187,17 @@ function _listRules(dir: string): string[] {
152
187
  }
153
188
  }
154
189
 
155
- /** `type:` from a rule's frontmatter, or `''` when unreadable / absent. */
190
+ /**
191
+ * `type:` from a rule's frontmatter, or `''` when unreadable / absent.
192
+ *
193
+ * Reads the shared fence parser rather than re-implementing one. It used to
194
+ * carry its own, and the two disagreed on an unterminated fence: this one
195
+ * treated the WHOLE file as frontmatter, so a `type:` line anywhere in the prose
196
+ * could be read as the rule's type. Now an unterminated fence yields no
197
+ * frontmatter and therefore no type — the conservative answer, since attributing
198
+ * a rule to the ADR-004 manual filter on a malformed read would hide it from the
199
+ * drift list.
200
+ */
156
201
  function _ruleType(file: string): string {
157
202
  let text: string;
158
203
  try {
@@ -160,10 +205,7 @@ function _ruleType(file: string): string {
160
205
  } catch {
161
206
  return '';
162
207
  }
163
- if (!text.startsWith('---')) return '';
164
- const end = text.indexOf('\n---', 3);
165
- const fm = end === -1 ? text : text.slice(0, end);
166
- const m = /^type:\s*"?([a-z]+)"?/m.exec(fm);
208
+ const m = /^type:\s*"?([a-z]+)"?/m.exec(splitFrontmatter(text).frontmatter);
167
209
  return m === null ? '' : (m[1] as string);
168
210
  }
169
211
 
@@ -193,6 +235,7 @@ export function compareCarriers(
193
235
  identical: [],
194
236
  provenanceOnly: [],
195
237
  frontmatterOnly: [],
238
+ pathsScopeDiff: [],
196
239
  bodyDiff: [],
197
240
  projectOnly: [],
198
241
  globalOnly: [],
@@ -218,12 +261,12 @@ export function compareCarriers(
218
261
  }
219
262
  out.shared += 1;
220
263
  let verdict: PairVerdict;
221
- let proseSame: boolean;
264
+ let projectCopy: Buffer;
265
+ let globalCopy: Buffer;
222
266
  try {
223
- const projectCopy = fs.readFileSync(path.join(projectDir, rule));
224
- const globalCopy = fs.readFileSync(path.join(globalDir, rule));
267
+ projectCopy = fs.readFileSync(path.join(projectDir, rule));
268
+ globalCopy = fs.readFileSync(path.join(globalDir, rule));
225
269
  verdict = comparePair(projectCopy, globalCopy);
226
- proseSame = proseEqual(projectCopy.toString('utf-8'), globalCopy.toString('utf-8'));
227
270
  } catch {
228
271
  // An unreadable copy is not a body difference and must not be
229
272
  // reported as one — a permission error would otherwise manufacture
@@ -236,8 +279,23 @@ export function compareCarriers(
236
279
  }
237
280
  if (verdict === 'identical') out.identical.push(rule);
238
281
  else if (verdict === 'provenance-only') out.provenanceOnly.push(rule);
239
- else if (proseSame) out.frontmatterOnly.push(rule);
240
- else out.bodyDiff.push(rule);
282
+ else {
283
+ // Only this branch consumes the prose comparison, so it is computed
284
+ // here rather than beside `comparePair`: on an `identical` or
285
+ // `provenance-only` pair the two `toString` conversions and two fence
286
+ // scans are work whose result is never read.
287
+ const projectText = projectCopy.toString('utf-8');
288
+ const globalText = globalCopy.toString('utf-8');
289
+ if (proseEqual(projectText, globalText)) {
290
+ out.frontmatterOnly.push(rule);
291
+ // `paths:` is the one key the host reads, so a disagreement here
292
+ // is a load-schedule difference, not inert metadata. Recorded as
293
+ // a subset so the count stays reconcilable with `frontmatterOnly`.
294
+ if (frontmatterPaths(projectText) !== frontmatterPaths(globalText)) {
295
+ out.pathsScopeDiff.push(rule);
296
+ }
297
+ } else out.bodyDiff.push(rule);
298
+ }
241
299
  }
242
300
  return out;
243
301
  }
@@ -269,6 +327,7 @@ export function render(d: CarrierDivergence): string {
269
327
  lines.push(` byte-identical ${d.identical.length}`);
270
328
  lines.push(` differ ONLY in the install stamp ${d.provenanceOnly.length}`);
271
329
  lines.push(` differ ONLY in frontmatter ${d.frontmatterOnly.length}`);
330
+ lines.push(` of which disagree on \`paths:\` ${d.pathsScopeDiff.length} ← ACTIONABLE`);
272
331
  lines.push(` differ in PROSE ${d.bodyDiff.length}`);
273
332
  if (d.unreadable.length > 0) {
274
333
  lines.push(` unreadable on one side ${d.unreadable.length} (${d.unreadable.join(', ')})`);
@@ -295,16 +354,28 @@ export function render(d: CarrierDivergence): string {
295
354
  lines.push('');
296
355
  }
297
356
 
298
- if (d.frontmatterOnly.length > 0) {
299
- lines.push(` ${String(d.frontmatterOnly.length)} pair(s) differ ONLY in the frontmatter block — prose byte-identical.`);
300
- lines.push(' Not a governed-text difference and nothing to act on: the host delivers the');
301
- lines.push(' prose without that block, and none of the keys in it is one this host reads');
302
- lines.push(' (claude-code-rules-dir-contract.md: it reads `paths`, and agent-config\'s own');
303
- lines.push(' vocabulary is not read at all). Two writers, two frontmatter policies —');
304
- lines.push(' `generate-tools` emits `paths` where a rule is path-scoped and nothing');
305
- lines.push(' otherwise; `install.ts` writes the full vocabulary plus its ownership stamp.');
306
- lines.push(' Reported as a count on purpose: naming every pair would bury the class');
307
- lines.push(' above, which is the one a reader must act on.');
357
+ if (d.pathsScopeDiff.length > 0) {
358
+ lines.push(` \`paths:\` SCOPE DISAGREEMENT (${String(d.pathsScopeDiff.length)}) — same prose, different load schedule.`);
359
+ lines.push(' ACT ON THESE. `paths` is the one frontmatter key this host reads: with it, a');
360
+ lines.push(' rule fires when a matching file is read and is NOT re-injected after');
361
+ lines.push(' /compact; without it, the rule loads unconditionally at launch. So the copy');
362
+ lines.push(' that lacks `paths` DEFEATS the other copy\'s scoping — the rule the project');
363
+ lines.push(' layer meant to deliver conditionally is delivered always, and an obligation');
364
+ lines.push(' a maintainer deliberately scoped is silently unscoped on any machine that');
365
+ lines.push(' carries both. Identical prose does not make this inert.');
366
+ for (const r of d.pathsScopeDiff) lines.push(` - ${r}`);
367
+ lines.push('');
368
+ }
369
+
370
+ const inert = d.frontmatterOnly.length - d.pathsScopeDiff.length;
371
+ if (inert > 0) {
372
+ lines.push(` ${String(inert)} further pair(s) differ ONLY in frontmatter the host does not read —`);
373
+ lines.push(' prose byte-identical, `paths` in agreement, nothing to act on. Two writers,');
374
+ lines.push(' two policies: `generate-tools` emits `paths` where a rule is path-scoped and');
375
+ lines.push(' nothing otherwise; `install.ts` writes agent-config\'s own vocabulary plus its');
376
+ lines.push(' ownership stamp, and none of those keys is one this host reads');
377
+ lines.push(' (claude-code-rules-dir-contract.md). Reported as a count on purpose: naming');
378
+ lines.push(' every pair would bury the two classes above, which are the actionable ones.');
308
379
  lines.push('');
309
380
  }
310
381
 
@@ -346,6 +417,10 @@ export function render(d: CarrierDivergence): string {
346
417
  if (d.bodyDiff.length === 0) {
347
418
  lines.push('');
348
419
  lines.push(' No prose divergence — no rule\'s governed text differs between the carriers.');
420
+ if (d.pathsScopeDiff.length > 0) {
421
+ lines.push(' NOT an all-clear: the `paths:` disagreements above are still open, and they');
422
+ lines.push(' change when a rule loads rather than what it says.');
423
+ }
349
424
  lines.push(' The condition is transient — it returns whenever the checkout moves ahead of');
350
425
  lines.push(' the installed release — so this is a reading of right now, not a property of');
351
426
  lines.push(' the repo. Re-run it, do not cite it. That holds doubly here: the reading also');
@@ -11,6 +11,11 @@
11
11
  * conformance sources, so the three questions that were answered in three
12
12
  * places read as the funnel they are:
13
13
  *
14
+ * OPPORTUNITY in how many scanned sessions a skill was in context at all —
15
+ * derived from `report_skill_obligation_violations.scanStore`'s
16
+ * own `sessionsWithASkill`, so it adds no classifier. It is the
17
+ * denominator ACTIVATION was missing: `skills.total` is supply,
18
+ * not opportunity.
14
19
  * DELIVERY what rule text the carriers put in front of the model, and
15
20
  * whether the two carriers agree —
16
21
  * `conformance_scan.measureDelivered` (carrier census) +
@@ -147,7 +152,7 @@ export function render(r: FunnelReport): string {
147
152
  } else {
148
153
  lines.push(
149
154
  ` Divergence: ${d.shared} shared · ${d.bodyDiff.length} prose-diff · ` +
150
- `${d.frontmatterOnly.length} frontmatter-only · ` +
155
+ `${d.frontmatterOnly.length} frontmatter-only (${d.pathsScopeDiff.length} on \`paths:\`) · ` +
151
156
  `${d.provenanceOnly.length} provenance-only · ${d.projectOnly.length} project-only · ` +
152
157
  `${d.globalOnly.length} global-only` +
153
158
  (d.manualOnlyGlobal.length > 0 ? ` (${d.manualOnlyGlobal.length} by design, ADR-004 manual)` : ''),
@@ -159,9 +164,16 @@ export function render(r: FunnelReport): string {
159
164
  lines.push(' (claude-code-rules-dir-contract.md). The project copy is merely the');
160
165
  lines.push(' newer one — recency, not precedence.');
161
166
  }
162
- if (d.frontmatterOnly.length > 0) {
163
- lines.push(` ${String(d.frontmatterOnly.length)} pair(s) differ only in frontmatter — prose byte-identical, nothing`);
164
- lines.push(' to act on; the host delivers the prose without that block.');
167
+ if (d.pathsScopeDiff.length > 0) {
168
+ lines.push(` \`paths:\` SCOPE DISAGREEMENT (${String(d.pathsScopeDiff.length)}) — same prose, different load`);
169
+ lines.push(' schedule. `paths` is the one frontmatter key the host reads, so the copy');
170
+ lines.push(' without it defeats the other copy\'s scoping: a deliberately scoped');
171
+ lines.push(' obligation is delivered unconditionally. Act on these.');
172
+ }
173
+ const inertFm = d.frontmatterOnly.length - d.pathsScopeDiff.length;
174
+ if (inertFm > 0) {
175
+ lines.push(` ${String(inertFm)} further pair(s) differ only in frontmatter the host does not read —`);
176
+ lines.push(' prose byte-identical, `paths` in agreement, nothing to act on.');
165
177
  }
166
178
  if (d.unreadable.length > 0) {
167
179
  lines.push(` Unreadable on one side (${d.unreadable.length}) — a broken install, not a disagreement.`);
@@ -169,6 +181,41 @@ export function render(r: FunnelReport): string {
169
181
  }
170
182
  lines.push('');
171
183
 
184
+ // ── OPPORTUNITY ────────────────────────────────────────────────────────
185
+ // The stage the funnel was missing. ACTIVATION below reports against
186
+ // `skills.total`, which is a SUPPLY number: how many skills exist. That
187
+ // denominator can only fall as the catalogue grows, and it says nothing
188
+ // about whether any skill had a chance to fire. Opportunity is the
189
+ // denominator that belongs between delivery and activation — sessions in
190
+ // which a skill was actually in context.
191
+ //
192
+ // DERIVED, never re-measured: `sessionsWithASkill` is the SK-2 scan's own
193
+ // field, so this stage adds no classifier and cannot disagree with the
194
+ // COMPLIANCE stage that already prints it.
195
+ lines.push('OPPORTUNITY — in how many sessions could a skill fire at all?');
196
+ if (!r.storePresent) {
197
+ lines.push(' NO DATA: no transcript store — opportunity is not measurable here.');
198
+ lines.push(' An absent store is an answer, not a zero.');
199
+ } else {
200
+ lines.push(` sessions scanned ${r.sk2.sessions}`);
201
+ lines.push(
202
+ ` with a skill in context ${r.sk2.sessionsWithASkill} ` +
203
+ `(${_pct(r.sk2.sessionsWithASkill, r.sk2.sessions)})`,
204
+ );
205
+ lines.push(
206
+ ' Skill invocations per such session ' +
207
+ (r.sk2.sessionsWithASkill === 0
208
+ ? 'n/a'
209
+ : (r.usage.invocations / r.sk2.sessionsWithASkill).toFixed(2)),
210
+ );
211
+ }
212
+ lines.push(" Honest scope: \"in context\" is the SK-2 scan's reading — a skill was");
213
+ lines.push(' LOADED, not that the task needed it. So this bounds activation from');
214
+ lines.push(' above; it never claims a missed invocation was a defect. Report-only and');
215
+ lines.push(' deliberately without a threshold: a joined funnel with a denominator');
216
+ lines.push(' invites one, and the activation red-baseline null is terminal.');
217
+ lines.push('');
218
+
172
219
  // ── ACTIVATION ─────────────────────────────────────────────────────────
173
220
  lines.push('ACTIVATION — are the delivered skills invoked?');
174
221
  lines.push(` skills shipped ${r.skills.total}`);
@@ -54,8 +54,9 @@ import {
54
54
  import * as hooks_status from "./hooks_status.js";
55
55
  import { load_agent_settings } from "./_lib/agent_settings.js";
56
56
  import {
57
- probeHostCapabilities,
57
+ describeHostCapabilities,
58
58
  type HostCapabilityManifest,
59
+ type HostCapabilitySources,
59
60
  } from "./_lib/host_capability.js";
60
61
  import {
61
62
  classifyLookup,
@@ -124,6 +125,17 @@ export interface OrchestrationReport {
124
125
  */
125
126
  host_platform: string;
126
127
  host_platform_assumed: boolean;
128
+ /**
129
+ * Per-field provenance for `host_manifest` — `registry` (a committed
130
+ * observation about this host), `live-probe` (established in this process),
131
+ * or `default` (nothing answered; the all-false safe default applied).
132
+ *
133
+ * Without it the six booleans read alike, and `false` cannot be told apart
134
+ * from "no answer". That distinction is the whole difference between "this
135
+ * host cannot spawn subagents" and "nobody has ever checked this host" —
136
+ * and this command is the one a user runs precisely to find out which.
137
+ */
138
+ host_manifest_sources: HostCapabilitySources;
127
139
  /** Verdict of the real activation gate for a canonical delegable probe. */
128
140
  activation: { action: string; reason: string };
129
141
  cost_budgets: Record<string, unknown>;
@@ -326,7 +338,8 @@ export function collect_orchestration(
326
338
  const emergency = (settings["emergency"] ?? {}) as Record<string, unknown>;
327
339
  const halted = emergency["orchestration_halt"] === true;
328
340
  const downshift = sub["downshift"] !== false;
329
- const host_manifest = probeHostCapabilities(platform);
341
+ const { manifest: host_manifest, sources: host_manifest_sources } =
342
+ describeHostCapabilities(platform);
330
343
  const activationInputs: ActivationInputs = {
331
344
  halted,
332
345
  subagent_spawn: host_manifest.subagent_spawn,
@@ -372,6 +385,7 @@ export function collect_orchestration(
372
385
  host_manifest,
373
386
  host_platform: platform,
374
387
  host_platform_assumed: platform_assumed,
388
+ host_manifest_sources,
375
389
  activation: { action: probe.action, reason: probe.reason },
376
390
  cost_budgets,
377
391
  ledger_present,
@@ -611,6 +625,15 @@ function _render(report: DoctorReport): string {
611
625
  `and the capability registry is keyed on it. Pass --platform <host> to check another.`,
612
626
  );
613
627
  }
628
+ const provenanceBits = Object.entries(o.host_manifest_sources).map(
629
+ ([field, src]) =>
630
+ `${field}=${o.host_manifest[field as keyof HostCapabilityManifest]}(${src})`,
631
+ );
632
+ lines.push(` capability provenance: ${provenanceBits.join(" · ")}`);
633
+ lines.push(
634
+ ` registry = committed observation about this host, not a live check · ` +
635
+ `default = nobody answered, rendered as false`,
636
+ );
614
637
  const tierBits = Object.entries(o.tier_budgets).map(([t, s]) => {
615
638
  const cap = s.ceiling_usd === null ? "no cap" : `$${s.ceiling_usd}`;
616
639
  const cool = s.cooldown_until_ms > 0 ? " COOLING" : "";
@@ -0,0 +1,45 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "dod.schema.json",
4
+ "title": "Executable definition of done",
5
+ "description": "The `dod[]` slot a refined ticket or prompt envelope may carry. `dod` is EXECUTABLE by construction: every item names the command that proves it, so the engine can report which items are still unproven without asking a human to interpret prose. Prose acceptance criteria stay where they are (`acceptance_criteria`) — this is the machine-checkable subset, not a replacement. The slot is OPTIONAL: a ticket without `dod` behaves exactly as before, which is why the `refine` gate validates the shape only when the key is present. Consumed by `work_engine/directives/backend/refine.ts` (shape gate) and reported by `work_engine/directives/backend/_self_fix.ts` at the PARTIAL exit.",
6
+ "type": "object",
7
+ "required": [
8
+ "dod"
9
+ ],
10
+ "additionalProperties": true,
11
+ "properties": {
12
+ "dod": {
13
+ "type": "array",
14
+ "description": "May be empty. An empty array means the author declared no machine-checkable done-condition, which is different from omitting the key: omission means the question was never asked. Both are legal and neither blocks.",
15
+ "items": {
16
+ "type": "object",
17
+ "required": [
18
+ "id",
19
+ "check"
20
+ ],
21
+ "additionalProperties": true,
22
+ "properties": {
23
+ "id": {
24
+ "type": "string",
25
+ "minLength": 1,
26
+ "description": "Stable identifier, unique within the array. Used verbatim in the self-fix PARTIAL exit so a reader can match an unproven item back to the ticket without re-reading the whole envelope."
27
+ },
28
+ "check": {
29
+ "type": "string",
30
+ "minLength": 1,
31
+ "description": "The command that proves this item — a test filter, a linter invocation, a curl probe. Prose here defeats the point of the slot: the engine never runs it (the dispatcher spawns no subprocesses, per the delegation contract), it hands it to the agent, and an agent cannot run a sentence."
32
+ },
33
+ "description": {
34
+ "type": "string",
35
+ "description": "Optional human sentence for the halt surface. Absent is fine — `check` is already the falsifiable half."
36
+ },
37
+ "proven": {
38
+ "type": "boolean",
39
+ "description": "Set by the agent once `check` has actually been run green. Absent or `false` both mean unproven; the engine never infers `true` from a passing suite, because a suite passing is not evidence that THIS check ran."
40
+ }
41
+ }
42
+ }
43
+ }
44
+ }
45
+ }
@@ -49,9 +49,10 @@
49
49
  "uniqueItems": true,
50
50
  "items": {
51
51
  "type": "string",
52
- "enum": ["Read", "Write", "Edit", "Grep", "Glob", "Bash", "WebFetch", "WebSearch", "NotebookEdit"]
52
+ "pattern": "^(Read|Write|Edit|Grep|Glob|Bash|WebFetch|WebSearch|NotebookEdit)(\\([^()]+\\))?$"
53
53
  },
54
- "description": "Enum-validated at build time against the Claude Code tool set, so a bad value fails CI rather than producing broken `.claude/agents/` YAML in the user's IDE."
54
+ "description": "A tool grant: one of the Claude Code tool names, optionally scoped — `Bash(npm test:*)` grants one command family where a bare `Bash` grants a shell. The base name stays closed, so a typo still fails CI rather than producing broken `.claude/agents/` YAML in the user's IDE; only the optional `(...)` suffix is new. Prefer the narrowest grant the task actually needs, per tool-safety Least Agency.",
55
+ "$comment": "Was a bare enum, which made the scoped-grant syntax `tool-safety` prescribes literally inexpressible here — a schema forbidding what the rule asks for. skill.schema.json's allowed_tools permits that syntax by validating nothing at all; this pattern permits it while keeping the base-name guarantee that made the enum worth having."
55
56
  },
56
57
  "trust": {
57
58
  "type": "object",
@@ -1,142 +0,0 @@
1
- import { createRequire as __acCreateRequire } from 'node:module'; const require = globalThis.require ?? __acCreateRequire(import.meta.url);
2
-
3
- // src/scripts/_lib/recycle_envelope_paths.ts
4
- import * as path from "node:path";
5
- var RECYCLE_ENVELOPE_REL = path.join("agents", "runtime", "state", "recycle-envelope.json");
6
- var RECYCLE_CONSUMED_REL = path.join(
7
- "agents",
8
- "runtime",
9
- "state",
10
- "recycle-envelope.consumed.json"
11
- );
12
- var RECYCLE_MAX_AGE_HOURS = 48;
13
- var RECYCLE_ENVELOPE_MAX_BYTES = 6144;
14
-
15
- // src/scripts/_lib/subagent_capsule.ts
16
- var EPISTEMIC_STATES = ["verified", "assumed", "gap"];
17
- var EPISTEMIC = new Set(EPISTEMIC_STATES);
18
- var MAX_REF_CHARS = 200;
19
- var MAX_LINE_CHARS = 240;
20
- var MAX_ENTRIES = 40;
21
- var CAPSULE_SCHEMA_VERSION = 2;
22
- function isShortLine(value, max) {
23
- return typeof value === "string" && value.length > 0 && value.length <= max && !value.includes("\n");
24
- }
25
- function checkList(errors, field, value, max, required) {
26
- if (value === void 0) {
27
- if (required) errors.push(`${field} is required`);
28
- return;
29
- }
30
- if (!Array.isArray(value)) {
31
- errors.push(`${field} must be an array`);
32
- return;
33
- }
34
- if (value.length > MAX_ENTRIES) {
35
- errors.push(`${field} carries ${value.length} entries (max ${MAX_ENTRIES}) \u2014 a capsule is a handoff, not a transcript`);
36
- }
37
- const bad = value.filter((x) => !isShortLine(x, max));
38
- if (bad.length > 0) {
39
- errors.push(
40
- `${field} carries ${bad.length} entr${bad.length === 1 ? "y" : "ies"} that are not single lines of \u2264 ${max} chars (refs and short lines only, never bodies)`
41
- );
42
- }
43
- }
44
- var RECYCLE_ENVELOPE_KEYS = /* @__PURE__ */ new Set([
45
- "capsule_version",
46
- "variant",
47
- "summary",
48
- "task",
49
- "workspace",
50
- "written_at",
51
- "acceptance_criteria",
52
- "remaining",
53
- "not_carried_forward",
54
- "decisions",
55
- "constraints",
56
- "open_worker_envelopes",
57
- "artifact_paths",
58
- "assumptions"
59
- ]);
60
- function validateRecycleEnvelope(input) {
61
- const errors = [];
62
- if (typeof input !== "object" || input === null || Array.isArray(input)) {
63
- return ["not an object"];
64
- }
65
- const e = input;
66
- for (const key of Object.keys(e)) {
67
- if (!RECYCLE_ENVELOPE_KEYS.has(key)) {
68
- errors.push(
69
- `unknown field "${key}" \u2014 a recycle envelope carries selection and pointers only; free-form additions (prose summaries included) are schema-invalid by construction`
70
- );
71
- }
72
- }
73
- if (e["capsule_version"] !== CAPSULE_SCHEMA_VERSION) {
74
- errors.push(`capsule_version must be ${CAPSULE_SCHEMA_VERSION}`);
75
- }
76
- if (e["variant"] !== "main_session") {
77
- errors.push("variant must be 'main_session'");
78
- }
79
- if (!isShortLine(e["summary"], MAX_LINE_CHARS)) {
80
- errors.push(`summary must be a single line of 1\u2013${MAX_LINE_CHARS} chars`);
81
- }
82
- if (!isShortLine(e["task"], MAX_LINE_CHARS)) {
83
- errors.push(`task must be a single line of 1\u2013${MAX_LINE_CHARS} chars`);
84
- }
85
- if (!isShortLine(e["workspace"], MAX_REF_CHARS)) {
86
- errors.push(`workspace must be a path ref of \u2264 ${MAX_REF_CHARS} chars`);
87
- }
88
- const writtenAt = e["written_at"];
89
- if (typeof writtenAt !== "string" || Number.isNaN(Date.parse(writtenAt))) {
90
- errors.push("written_at must be a parseable ISO-8601 timestamp");
91
- }
92
- checkList(errors, "acceptance_criteria", e["acceptance_criteria"], MAX_LINE_CHARS, true);
93
- if (Array.isArray(e["acceptance_criteria"]) && e["acceptance_criteria"].length === 0) {
94
- errors.push('acceptance_criteria must carry at least one entry \u2014 the successor cannot know "done" without it');
95
- }
96
- checkList(errors, "remaining", e["remaining"], MAX_LINE_CHARS, true);
97
- checkList(errors, "not_carried_forward", e["not_carried_forward"], MAX_LINE_CHARS, true);
98
- checkList(errors, "decisions", e["decisions"], MAX_LINE_CHARS, false);
99
- checkList(errors, "constraints", e["constraints"], MAX_LINE_CHARS, false);
100
- checkList(errors, "open_worker_envelopes", e["open_worker_envelopes"], MAX_REF_CHARS, false);
101
- checkList(errors, "artifact_paths", e["artifact_paths"], MAX_REF_CHARS, false);
102
- const assumptions = e["assumptions"];
103
- if (assumptions !== void 0) {
104
- if (!Array.isArray(assumptions)) {
105
- errors.push("assumptions must be an array");
106
- } else {
107
- if (assumptions.length > MAX_ENTRIES) {
108
- errors.push(`assumptions carries ${assumptions.length} entries (max ${MAX_ENTRIES})`);
109
- }
110
- for (const [i, raw] of assumptions.entries()) {
111
- errors.push(...validateAssumption(raw, `assumptions[${i}]`));
112
- }
113
- }
114
- }
115
- return errors;
116
- }
117
- function validateAssumption(input, label = "assumption") {
118
- const errors = [];
119
- if (typeof input !== "object" || input === null || Array.isArray(input)) {
120
- return [`${label} is not an object`];
121
- }
122
- const a = input;
123
- if (!isShortLine(a["statement"], MAX_LINE_CHARS)) {
124
- errors.push(`${label}.statement must be a single line of 1\u2013${MAX_LINE_CHARS} chars`);
125
- }
126
- if (!isShortLine(a["basis"], MAX_REF_CHARS)) {
127
- errors.push(`${label}.basis must be a ref token (file:line, id, path, command) of \u2264 ${MAX_REF_CHARS} chars`);
128
- }
129
- if (typeof a["epistemic_state"] !== "string" || !EPISTEMIC.has(a["epistemic_state"])) {
130
- errors.push(`${label}.epistemic_state must be one of ${EPISTEMIC_STATES.join(" | ")}`);
131
- }
132
- return errors;
133
- }
134
-
135
- export {
136
- RECYCLE_ENVELOPE_REL,
137
- RECYCLE_CONSUMED_REL,
138
- RECYCLE_MAX_AGE_HOURS,
139
- RECYCLE_ENVELOPE_MAX_BYTES,
140
- CAPSULE_SCHEMA_VERSION,
141
- validateRecycleEnvelope
142
- };