@event4u/agent-config 9.30.0 → 9.32.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 (94) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +60 -292
  3. package/dist/agent-src/contexts/execution/host-capability-manifest.md +32 -1
  4. package/dist/agent-src/rules/delegation-policy.md +11 -1
  5. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  6. package/dist/agent-src/templates/scripts/work_engine/delivery_state.ts +6 -0
  7. package/dist/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +332 -0
  8. package/dist/agent-src/templates/scripts/work_engine/directives/backend/refine.ts +106 -1
  9. package/dist/agent-src/templates/scripts/work_engine/directives/backend/test.ts +50 -15
  10. package/dist/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +49 -15
  11. package/dist/agent-src/templates/scripts/work_engine/state.ts +74 -0
  12. package/dist/agent-src/templates/scripts/work_engine/state_io.ts +6 -0
  13. package/dist/cli-delegate/chunk-2WELHO3P.js +582 -0
  14. package/dist/cli-delegate/{chunk-IYTGIG2Y.js → chunk-542DZR4P.js} +118 -148
  15. package/dist/cli-delegate/{chunk-66OVWGB5.js → chunk-AG47LAGQ.js} +1 -1
  16. package/dist/cli-delegate/{chunk-MP3JT3IU.js → chunk-DS6BLDMG.js} +1 -1
  17. package/dist/cli-delegate/{chunk-O3CO5BFB.js → chunk-EJITJBJX.js} +12 -1
  18. package/dist/cli-delegate/{chunk-EQY4FLJB.js → chunk-ITIQSGOQ.js} +3 -3
  19. package/dist/cli-delegate/{chunk-LCFJIQ4J.js → chunk-JEJABGKB.js} +110 -6
  20. package/dist/cli-delegate/chunk-PZWRPRWP.js +38 -0
  21. package/dist/cli-delegate/{chunk-BTJNRUX3.js → chunk-QEEB4LHO.js} +1 -1
  22. package/dist/cli-delegate/{chunk-DDUNNFYX.js → chunk-UVKNIJH2.js} +2 -2
  23. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  24. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  25. package/dist/cli-delegate/cmd_conformance.js +154 -41
  26. package/dist/cli-delegate/cmd_doctor.js +4 -3
  27. package/dist/cli-delegate/cmd_explain.js +2 -2
  28. package/dist/cli-delegate/cmd_export.js +4 -4
  29. package/dist/cli-delegate/cmd_handoff.js +168 -59
  30. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  31. package/dist/cli-delegate/cmd_migrate.js +2 -2
  32. package/dist/cli-delegate/cmd_packs_active.js +2 -2
  33. package/dist/cli-delegate/cmd_prune.js +4 -4
  34. package/dist/cli-delegate/cmd_route_audit.js +2 -2
  35. package/dist/cli-delegate/cmd_route_explain.js +1 -1
  36. package/dist/cli-delegate/cmd_session_recycle.js +19 -4
  37. package/dist/cli-delegate/cmd_settings_get.js +2 -55
  38. package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
  39. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  40. package/dist/cli-delegate/cmd_sync.js +4 -4
  41. package/dist/cli-delegate/cmd_uninstall.js +4 -4
  42. package/dist/cli-delegate/cmd_update.js +1 -1
  43. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  44. package/dist/cli-delegate/cmd_validate.js +4 -4
  45. package/dist/cli-delegate/cmd_versions.js +1 -1
  46. package/dist/discovery/deprecation-report.md +1 -1
  47. package/dist/discovery/discovery-manifest.json +3 -3
  48. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  49. package/dist/discovery/discovery-manifest.summary.md +1 -1
  50. package/dist/discovery/orphan-report.md +1 -1
  51. package/dist/discovery/packs.json +2 -2
  52. package/dist/discovery/trust-report.md +1 -1
  53. package/dist/discovery/workspaces.json +2 -2
  54. package/dist/hooks/dispatch.js +1411 -810
  55. package/dist/install/install.mjs +121 -16
  56. package/dist/mcp/registry-manifest.json +1 -1
  57. package/dist/mcp/server.json +2 -2
  58. package/dist/mcp/server.mjs +365 -143
  59. package/dist/scripts/_lib/agent_settings.js +111 -2
  60. package/dist/scripts/_lib/agent_settings.js.map +1 -1
  61. package/dist/server/schemas/settings.js +2 -2
  62. package/dist/server/schemas/settings.js.map +1 -1
  63. package/package.json +1 -1
  64. package/src/agent-src/templates/scripts/work_engine/delivery_state.ts +6 -0
  65. package/src/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +332 -0
  66. package/src/agent-src/templates/scripts/work_engine/directives/backend/refine.ts +106 -1
  67. package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +50 -15
  68. package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +49 -15
  69. package/src/agent-src/templates/scripts/work_engine/state.ts +74 -0
  70. package/src/agent-src/templates/scripts/work_engine/state_io.ts +6 -0
  71. package/src/config/pack-size-budget.json +3 -2
  72. package/src/scripts/_cli/cmd_session_recycle.ts +25 -0
  73. package/src/scripts/_cli/explain_last/inputs.ts +13 -0
  74. package/src/scripts/_cli/handoff_generate.ts +36 -1
  75. package/src/scripts/_cli/handoff_sessions.ts +140 -9
  76. package/src/scripts/_lib/agent_settings.ts +119 -2
  77. package/src/scripts/_lib/carrier_divergence.ts +120 -2
  78. package/src/scripts/_lib/envelope_grounding.ts +206 -0
  79. package/src/scripts/_lib/host_capability.ts +79 -0
  80. package/src/scripts/_lib/session_eol.ts +85 -0
  81. package/src/scripts/_lib/subagent_capsule.ts +256 -1
  82. package/src/scripts/check_secret_leak.test.ts +43 -0
  83. package/src/scripts/check_secret_leak.ts +12 -0
  84. package/src/scripts/handoff_context_hook.ts +66 -18
  85. package/src/scripts/hooks/session_eol_hook.ts +12 -12
  86. package/src/scripts/hooks/turn_end_gate_hook.ts +296 -88
  87. package/src/scripts/language_mirror_hook.ts +69 -16
  88. package/src/scripts/measure_turn_end_gate.ts +129 -51
  89. package/src/scripts/report_carrier_divergence.ts +143 -30
  90. package/src/scripts/report_conformance_funnel.ts +18 -4
  91. package/src/scripts/routing_doctor.ts +25 -2
  92. package/src/scripts/schemas/dod.schema.json +45 -0
  93. package/src/server/schemas/settings.ts +2 -2
  94. package/dist/cli-delegate/chunk-RVGBH4XK.js +0 -142
@@ -35,16 +35,42 @@
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
- * THE THREE CLASSES, AND WHICH ONE A READER MUST ACT ON
39
- * ----------------------------------------------------
40
- * body-diff the two carriers deliver different TEXT. This is the class
38
+ * THE FOUR CLASSES, AND WHICH ONE A READER MUST ACT ON
39
+ * ---------------------------------------------------
40
+ * body-diff the two carriers deliver different PROSE. This is the class
41
41
  * round 5's contradiction lived in, and the only one that
42
- * needs a decision. Precedence when it appears: the project
43
- * projection is generated from `src/` at the current commit,
44
- * so it is the newer copy and it wins; the global install is
45
- * a release snapshot. Printed with the report so the
46
- * precedence is read at the moment it matters rather than
47
- * looked up.
42
+ * needs a decision. What the host does about it: NOTHING —
43
+ * both copies load at launch with the same priority and no
44
+ * precedence marker between them, so the divergence is
45
+ * binding-undefined rather than resolved in either
46
+ * direction. Cited, not inferred:
47
+ * `agents/evidence/analysis/claude-code-rules-dir-contract.md`
48
+ * (host 2.1.226, first-party observation plus the host's own
49
+ * documentation). The project copy is the NEWER text, which
50
+ * is a fact about recency and not a precedence rule — the
51
+ * earlier wording of this header called it a win, and a
52
+ * reader acting on that would have believed the host resolves
53
+ * something it does not.
54
+ * frontmatter-only the prose is byte-identical and only the metadata block
55
+ * differs. Split out because it was, measured, the ENTIRE
56
+ * content of the `body-diff` class: 109 of 109 on 2026-08-10.
57
+ * Left inside body-diff it manufactured the report's only
58
+ * actionable class out of a metadata difference — the same
59
+ * mistake the `unreadable` branch below already refuses to
60
+ * make.
61
+ * NOT inert as a whole, and an earlier version of this entry
62
+ * wrongly said it was: the `paths:` SUBSET below is reported
63
+ * separately and IS actionable. Only the remainder — keys this
64
+ * host does not read — is safe to ignore.
65
+ * `paths:` subset a `frontmatter-only` pair whose two copies disagree about
66
+ * `paths`, the one frontmatter key the host reads. It decides
67
+ * WHEN a rule loads (matching-file read and no /compact
68
+ * re-injection, versus unconditionally at launch), so the copy
69
+ * without `paths` DEFEATS the other's scoping and an
70
+ * obligation someone deliberately scoped is silently unscoped.
71
+ * Measured 2026-08-10: 24 of the 109, all of them `paths` in
72
+ * the project copy and none in the global one. Identical prose
73
+ * does not make a schedule difference inert.
48
74
  * provenance-only identical after removing the two installer ownership keys.
49
75
  * Structural, expected, and decided against closing —
50
76
  * `agents/settings/contexts/dedup-reachability-refusal.md`.
@@ -69,7 +95,13 @@ import * as path from 'node:path';
69
95
  import process from 'node:process';
70
96
  import { fileURLToPath, pathToFileURL } from 'node:url';
71
97
 
72
- import { comparePair, type PairVerdict } from './_lib/carrier_divergence.js';
98
+ import {
99
+ comparePair,
100
+ frontmatterPaths,
101
+ type PairVerdict,
102
+ proseEqual,
103
+ splitFrontmatter,
104
+ } from './_lib/carrier_divergence.js';
73
105
 
74
106
  const _HERE = fileURLToPath(import.meta.url);
75
107
  const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
@@ -103,6 +135,20 @@ export interface CarrierDivergence {
103
135
  shared: number;
104
136
  identical: string[];
105
137
  provenanceOnly: string[];
138
+ /**
139
+ * Prose byte-identical, metadata block different. A real byte difference — so
140
+ * `comparePair` still calls it `body-diff` and the dedup predicate is
141
+ * unchanged — but not a difference in what the host delivers, so it is not
142
+ * something a reader has to act on.
143
+ */
144
+ frontmatterOnly: string[];
145
+ /**
146
+ * A SUBSET of `frontmatterOnly` whose two copies disagree about `paths:` —
147
+ * the one frontmatter key this host reads. Same prose, different load
148
+ * schedule, so this one IS actionable and must not be reported as inert
149
+ * metadata.
150
+ */
151
+ pathsScopeDiff: string[];
106
152
  bodyDiff: string[];
107
153
  projectOnly: string[];
108
154
  globalOnly: string[];
@@ -131,7 +177,17 @@ function _listRules(dir: string): string[] {
131
177
  }
132
178
  }
133
179
 
134
- /** `type:` from a rule's frontmatter, or `''` when unreadable / absent. */
180
+ /**
181
+ * `type:` from a rule's frontmatter, or `''` when unreadable / absent.
182
+ *
183
+ * Reads the shared fence parser rather than re-implementing one. It used to
184
+ * carry its own, and the two disagreed on an unterminated fence: this one
185
+ * treated the WHOLE file as frontmatter, so a `type:` line anywhere in the prose
186
+ * could be read as the rule's type. Now an unterminated fence yields no
187
+ * frontmatter and therefore no type — the conservative answer, since attributing
188
+ * a rule to the ADR-004 manual filter on a malformed read would hide it from the
189
+ * drift list.
190
+ */
135
191
  function _ruleType(file: string): string {
136
192
  let text: string;
137
193
  try {
@@ -139,10 +195,7 @@ function _ruleType(file: string): string {
139
195
  } catch {
140
196
  return '';
141
197
  }
142
- if (!text.startsWith('---')) return '';
143
- const end = text.indexOf('\n---', 3);
144
- const fm = end === -1 ? text : text.slice(0, end);
145
- const m = /^type:\s*"?([a-z]+)"?/m.exec(fm);
198
+ const m = /^type:\s*"?([a-z]+)"?/m.exec(splitFrontmatter(text).frontmatter);
146
199
  return m === null ? '' : (m[1] as string);
147
200
  }
148
201
 
@@ -171,6 +224,8 @@ export function compareCarriers(
171
224
  shared: 0,
172
225
  identical: [],
173
226
  provenanceOnly: [],
227
+ frontmatterOnly: [],
228
+ pathsScopeDiff: [],
174
229
  bodyDiff: [],
175
230
  projectOnly: [],
176
231
  globalOnly: [],
@@ -196,11 +251,12 @@ export function compareCarriers(
196
251
  }
197
252
  out.shared += 1;
198
253
  let verdict: PairVerdict;
254
+ let projectCopy: Buffer;
255
+ let globalCopy: Buffer;
199
256
  try {
200
- verdict = comparePair(
201
- fs.readFileSync(path.join(projectDir, rule)),
202
- fs.readFileSync(path.join(globalDir, rule)),
203
- );
257
+ projectCopy = fs.readFileSync(path.join(projectDir, rule));
258
+ globalCopy = fs.readFileSync(path.join(globalDir, rule));
259
+ verdict = comparePair(projectCopy, globalCopy);
204
260
  } catch {
205
261
  // An unreadable copy is not a body difference and must not be
206
262
  // reported as one — a permission error would otherwise manufacture
@@ -213,7 +269,23 @@ export function compareCarriers(
213
269
  }
214
270
  if (verdict === 'identical') out.identical.push(rule);
215
271
  else if (verdict === 'provenance-only') out.provenanceOnly.push(rule);
216
- else out.bodyDiff.push(rule);
272
+ else {
273
+ // Only this branch consumes the prose comparison, so it is computed
274
+ // here rather than beside `comparePair`: on an `identical` or
275
+ // `provenance-only` pair the two `toString` conversions and two fence
276
+ // scans are work whose result is never read.
277
+ const projectText = projectCopy.toString('utf-8');
278
+ const globalText = globalCopy.toString('utf-8');
279
+ if (proseEqual(projectText, globalText)) {
280
+ out.frontmatterOnly.push(rule);
281
+ // `paths:` is the one key the host reads, so a disagreement here
282
+ // is a load-schedule difference, not inert metadata. Recorded as
283
+ // a subset so the count stays reconcilable with `frontmatterOnly`.
284
+ if (frontmatterPaths(projectText) !== frontmatterPaths(globalText)) {
285
+ out.pathsScopeDiff.push(rule);
286
+ }
287
+ } else out.bodyDiff.push(rule);
288
+ }
217
289
  }
218
290
  return out;
219
291
  }
@@ -244,7 +316,9 @@ export function render(d: CarrierDivergence): string {
244
316
  lines.push(` shared rule names ${d.shared}`);
245
317
  lines.push(` byte-identical ${d.identical.length}`);
246
318
  lines.push(` differ ONLY in the install stamp ${d.provenanceOnly.length}`);
247
- lines.push(` differ in BODY ${d.bodyDiff.length}`);
319
+ lines.push(` differ ONLY in frontmatter ${d.frontmatterOnly.length}`);
320
+ lines.push(` of which disagree on \`paths:\` ${d.pathsScopeDiff.length} ← ACTIONABLE`);
321
+ lines.push(` differ in PROSE ${d.bodyDiff.length}`);
248
322
  if (d.unreadable.length > 0) {
249
323
  lines.push(` unreadable on one side ${d.unreadable.length} (${d.unreadable.join(', ')})`);
250
324
  lines.push(' Not counted as shared and NOT a body difference — a dangling entry is');
@@ -255,16 +329,46 @@ export function render(d: CarrierDivergence): string {
255
329
  lines.push('');
256
330
 
257
331
  if (d.bodyDiff.length > 0) {
258
- lines.push(' BODY DIVERGENCE — the two carriers deliver different text for these rules.');
259
- lines.push(' Both copies reach the model, with no precedence marker between them, so a');
260
- lines.push(' claim one copy retracts can be re-asserted by the other. PRECEDENCE: the');
261
- lines.push(' project projection is generated from src/ at this commit and wins; the');
262
- lines.push(' global install is a release snapshot. Close it by reinstalling the global');
263
- lines.push(' copy, or by reading the project copy as authoritative.');
332
+ lines.push(' PROSE DIVERGENCE — the two carriers deliver different governed text for');
333
+ lines.push(' these rules. Both copies reach the model, and the host resolves NOTHING:');
334
+ lines.push(' rules without a `paths` key load at launch with the same priority as');
335
+ lines.push(' CLAUDE.md, with no precedence marker between the layers, so a claim one');
336
+ lines.push(' copy retracts can be re-asserted by the other and which text binds is');
337
+ lines.push(' UNDEFINED. Cited, not inferred — agents/evidence/analysis/');
338
+ lines.push(' claude-code-rules-dir-contract.md (host 2.1.226, the host\'s own docs plus');
339
+ lines.push(' a first-party observation). The project copy is the NEWER text, which is a');
340
+ lines.push(' fact about recency and NOT a host precedence rule: close the divergence by');
341
+ lines.push(' reinstalling the global copy, or decide which text binds — do not assume');
342
+ lines.push(' the newer one already won.');
264
343
  for (const r of d.bodyDiff) lines.push(` - ${r}`);
265
344
  lines.push('');
266
345
  }
267
346
 
347
+ if (d.pathsScopeDiff.length > 0) {
348
+ lines.push(` \`paths:\` SCOPE DISAGREEMENT (${String(d.pathsScopeDiff.length)}) — same prose, different load schedule.`);
349
+ lines.push(' ACT ON THESE. `paths` is the one frontmatter key this host reads: with it, a');
350
+ lines.push(' rule fires when a matching file is read and is NOT re-injected after');
351
+ lines.push(' /compact; without it, the rule loads unconditionally at launch. So the copy');
352
+ lines.push(' that lacks `paths` DEFEATS the other copy\'s scoping — the rule the project');
353
+ lines.push(' layer meant to deliver conditionally is delivered always, and an obligation');
354
+ lines.push(' a maintainer deliberately scoped is silently unscoped on any machine that');
355
+ lines.push(' carries both. Identical prose does not make this inert.');
356
+ for (const r of d.pathsScopeDiff) lines.push(` - ${r}`);
357
+ lines.push('');
358
+ }
359
+
360
+ const inert = d.frontmatterOnly.length - d.pathsScopeDiff.length;
361
+ if (inert > 0) {
362
+ lines.push(` ${String(inert)} further pair(s) differ ONLY in frontmatter the host does not read —`);
363
+ lines.push(' prose byte-identical, `paths` in agreement, nothing to act on. Two writers,');
364
+ lines.push(' two policies: `generate-tools` emits `paths` where a rule is path-scoped and');
365
+ lines.push(' nothing otherwise; `install.ts` writes agent-config\'s own vocabulary plus its');
366
+ lines.push(' ownership stamp, and none of those keys is one this host reads');
367
+ lines.push(' (claude-code-rules-dir-contract.md). Reported as a count on purpose: naming');
368
+ lines.push(' every pair would bury the two classes above, which are the actionable ones.');
369
+ lines.push('');
370
+ }
371
+
268
372
  if (d.provenanceOnly.length > 0) {
269
373
  lines.push(` ${String(d.provenanceOnly.length)} pair(s) differ only by the installer's package:/source_path: stamp.`);
270
374
  lines.push(' Not a content difference and NOT a defect: install.ts stamps every installed');
@@ -302,9 +406,18 @@ export function render(d: CarrierDivergence): string {
302
406
 
303
407
  if (d.bodyDiff.length === 0) {
304
408
  lines.push('');
305
- lines.push(' No body divergence. The condition is transient — it returns whenever the');
306
- lines.push(' checkout moves ahead of the installed release — so this is a reading of');
307
- lines.push(' right now, not a property of the repo. Re-run it, do not cite it.');
409
+ lines.push(' No prose divergence — no rule\'s governed text differs between the carriers.');
410
+ if (d.pathsScopeDiff.length > 0) {
411
+ lines.push(' NOT an all-clear: the `paths:` disagreements above are still open, and they');
412
+ lines.push(' change when a rule loads rather than what it says.');
413
+ }
414
+ lines.push(' The condition is transient — it returns whenever the checkout moves ahead of');
415
+ lines.push(' the installed release — so this is a reading of right now, not a property of');
416
+ lines.push(' the repo. Re-run it, do not cite it. That holds doubly here: the reading also');
417
+ lines.push(' depends on WHICH GENERATION of the emitter produced the project tree (a');
418
+ lines.push(' frontmatter-carrying symlink tree and a frontmatter-less real-file tree');
419
+ lines.push(' classify the same commit differently), so a number from another checkout is');
420
+ lines.push(' not comparable to this one.');
308
421
  }
309
422
  return lines.join('\n');
310
423
  }
@@ -146,15 +146,29 @@ export function render(r: FunnelReport): string {
146
146
  lines.push(' substituted with dist/, which would answer a different question.');
147
147
  } else {
148
148
  lines.push(
149
- ` Divergence: ${d.shared} shared · ${d.bodyDiff.length} body-diff · ` +
149
+ ` Divergence: ${d.shared} shared · ${d.bodyDiff.length} prose-diff · ` +
150
+ `${d.frontmatterOnly.length} frontmatter-only (${d.pathsScopeDiff.length} on \`paths:\`) · ` +
150
151
  `${d.provenanceOnly.length} provenance-only · ${d.projectOnly.length} project-only · ` +
151
152
  `${d.globalOnly.length} global-only` +
152
153
  (d.manualOnlyGlobal.length > 0 ? ` (${d.manualOnlyGlobal.length} by design, ADR-004 manual)` : ''),
153
154
  );
154
155
  if (d.bodyDiff.length > 0) {
155
- lines.push(` BODY DIVERGENCE — both copies reach the model: ${d.bodyDiff.join(', ')}`);
156
- lines.push(' Precedence: the project projection is generated from src/ at this commit');
157
- lines.push(' and wins; the global install is a release snapshot.');
156
+ lines.push(` PROSE DIVERGENCE — both copies reach the model: ${d.bodyDiff.join(', ')}`);
157
+ lines.push(' The host resolves nothing: both layers load at launch at the same');
158
+ lines.push(' priority with no precedence marker, so which text binds is UNDEFINED');
159
+ lines.push(' (claude-code-rules-dir-contract.md). The project copy is merely the');
160
+ lines.push(' newer one — recency, not precedence.');
161
+ }
162
+ if (d.pathsScopeDiff.length > 0) {
163
+ lines.push(` \`paths:\` SCOPE DISAGREEMENT (${String(d.pathsScopeDiff.length)}) — same prose, different load`);
164
+ lines.push(' schedule. `paths` is the one frontmatter key the host reads, so the copy');
165
+ lines.push(' without it defeats the other copy\'s scoping: a deliberately scoped');
166
+ lines.push(' obligation is delivered unconditionally. Act on these.');
167
+ }
168
+ const inertFm = d.frontmatterOnly.length - d.pathsScopeDiff.length;
169
+ if (inertFm > 0) {
170
+ lines.push(` ${String(inertFm)} further pair(s) differ only in frontmatter the host does not read —`);
171
+ lines.push(' prose byte-identical, `paths` in agreement, nothing to act on.');
158
172
  }
159
173
  if (d.unreadable.length > 0) {
160
174
  lines.push(` Unreadable on one side (${d.unreadable.length}) — a broken install, not a disagreement.`);
@@ -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
+ }
@@ -525,10 +525,10 @@ export const settingsSchema = z.object({
525
525
  'Stop-slot turn-end gate (road-to-conformance-round5 Phase 3). Default off, and it is the only concern that can REFUSE a turn-end rather than a tool call — so it soaks behind an explicit opt-in before it binds. Round 5 measured the reason: both blocking carriers reached zero violations, neither advisory carrier did, and 19 wrong-language replies survived a pin that had fired 26-35 seconds earlier. A refused turn continues in the SAME turn and is never refused twice.',
526
526
  ),
527
527
  promissory: z.boolean().default(true).describe(
528
- 'Detector A of the turn-end gate: refuse a turn whose closing paragraph promises work not yet performed while asking the user nothing (20 measured occurrences). Inert while hooks.turn_end_gate.enabled is false.',
528
+ 'Detector A of the turn-end gate: refuse a turn whose closing paragraph promises work not yet performed while asking the user nothing (20 measured occurrences). No fire rate is quoted here on purpose: the measured rate moved twice while the instrument itself was being corrected, so a number in a shipped description would be a moving target. Re-derive it with `measure-turn-end-gate --store <transcript dir>` against your own corpus before arming the gate. Inert while hooks.turn_end_gate.enabled is false.',
529
529
  ),
530
530
  language: z.boolean().default(true).describe(
531
- 'Detector B of the turn-end gate: refuse a turn whose user-visible prose is not in the pinned language, with fenced code, inline code, quoted tool output, tables, URLs, paths and identifiers excluded first (19 measured occurrences). Fires 18.0% of turns on the measured corpus, which is why the master switch is off by default. Inert while hooks.turn_end_gate.enabled is false.',
531
+ 'Detector B of the turn-end gate: refuse a turn whose reply is not in the pinned language (19 measured occurrences). Fenced code, inline code, quoted tool output, tables, URLs and paths are stripped first, and the remainder is then scored LEAD-FIRST by the same classifier that sets the pin — so the verdict usually comes from the reply opening, not from all of it. Sharing that classifier is deliberate: it stops the pin and the detector disagreeing about a turn. Accepted opt-in spellings for the switches below include YAML true/false and yes/no/on/off. Inert while hooks.turn_end_gate.enabled is false.',
532
532
  ),
533
533
  }).default({}),
534
534
  }),
@@ -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
- };