@event4u/agent-config 9.26.0 → 9.28.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 (252) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +12 -11
  3. package/CHANGELOG.md +134 -207
  4. package/README.md +1 -1
  5. package/dist/agent-src/commands/analyze/conformance.md +43 -0
  6. package/dist/agent-src/commands/roadmap/next.md +65 -3
  7. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +10 -3
  8. package/dist/agent-src/rules/active-remediation.md +2 -0
  9. package/dist/agent-src/rules/analysis-skill-routing.md +2 -0
  10. package/dist/agent-src/rules/architecture.md +2 -0
  11. package/dist/agent-src/rules/artifact-drafting-protocol.md +2 -0
  12. package/dist/agent-src/rules/artifact-engagement-recording.md +6 -1
  13. package/dist/agent-src/rules/augment-edit-discipline.md +2 -0
  14. package/dist/agent-src/rules/autonomous-execution.md +3 -1
  15. package/dist/agent-src/rules/brand-consistency.md +2 -0
  16. package/dist/agent-src/rules/brand-source-of-truth.md +35 -6
  17. package/dist/agent-src/rules/broken-access-control.md +2 -0
  18. package/dist/agent-src/rules/cli-output-handling.md +2 -0
  19. package/dist/agent-src/rules/code-comment-discipline.md +2 -0
  20. package/dist/agent-src/rules/code-provenance.md +2 -0
  21. package/dist/agent-src/rules/command-suggestion-policy.md +2 -0
  22. package/dist/agent-src/rules/commit-conventions.md +2 -0
  23. package/dist/agent-src/rules/communication-through-line.md +2 -0
  24. package/dist/agent-src/rules/content-quoting-floor.md +2 -0
  25. package/dist/agent-src/rules/context-hygiene.md +13 -1
  26. package/dist/agent-src/rules/copilot-routing.md +2 -0
  27. package/dist/agent-src/rules/council-availability.md +93 -0
  28. package/dist/agent-src/rules/cross-source-consistency.md +5 -0
  29. package/dist/agent-src/rules/decision-revisit-gate.md +2 -0
  30. package/dist/agent-src/rules/delegation-policy.md +31 -5
  31. package/dist/agent-src/rules/design-fidelity.md +5 -0
  32. package/dist/agent-src/rules/design-review-after-ui-write.md +4 -0
  33. package/dist/agent-src/rules/devcontainer-routing.md +2 -0
  34. package/dist/agent-src/rules/doc-screenshot-hygiene.md +2 -0
  35. package/dist/agent-src/rules/docker-commands.md +2 -0
  36. package/dist/agent-src/rules/domain-adoption-policy.md +2 -0
  37. package/dist/agent-src/rules/domain-safety-disclaimer.md +2 -0
  38. package/dist/agent-src/rules/domain-safety-pii.md +2 -0
  39. package/dist/agent-src/rules/domain-safety-retention.md +2 -0
  40. package/dist/agent-src/rules/downstream-changes.md +2 -0
  41. package/dist/agent-src/rules/engineering-safety-floor.md +2 -0
  42. package/dist/agent-src/rules/evaluator-independence.md +14 -3
  43. package/dist/agent-src/rules/external-code-graph-interop.md +4 -1
  44. package/dist/agent-src/rules/external-reference-deep-dive.md +2 -0
  45. package/dist/agent-src/rules/fast-path-marker-visibility.md +2 -0
  46. package/dist/agent-src/rules/finance-safety-floor.md +3 -1
  47. package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +2 -0
  48. package/dist/agent-src/rules/git-history-discipline.md +3 -1
  49. package/dist/agent-src/rules/guidelines.md +2 -0
  50. package/dist/agent-src/rules/history-discipline.md +5 -1
  51. package/dist/agent-src/rules/icon-consistency.md +2 -0
  52. package/dist/agent-src/rules/image-likeness-and-rights.md +2 -0
  53. package/dist/agent-src/rules/improve-before-implement.md +2 -0
  54. package/dist/agent-src/rules/invite-challenge.md +2 -0
  55. package/dist/agent-src/rules/laravel-routing.md +2 -0
  56. package/dist/agent-src/rules/laravel-translations.md +2 -0
  57. package/dist/agent-src/rules/legal-safety-floor.md +3 -1
  58. package/dist/agent-src/rules/lethal-trifecta-guard.md +2 -0
  59. package/dist/agent-src/rules/linked-projects-onboarding-gate.md +2 -0
  60. package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +2 -0
  61. package/dist/agent-src/rules/markdown-safe-codeblocks.md +2 -0
  62. package/dist/agent-src/rules/media-governance-routing.md +2 -0
  63. package/dist/agent-src/rules/media-sync-ground-truth.md +2 -0
  64. package/dist/agent-src/rules/minimal-safe-diff.md +2 -0
  65. package/dist/agent-src/rules/missing-tool-handling.md +2 -0
  66. package/dist/agent-src/rules/model-recommendation.md +2 -0
  67. package/dist/agent-src/rules/no-attribution-footers.md +2 -0
  68. package/dist/agent-src/rules/no-decorative-emojis-in-git-surfaces.md +2 -0
  69. package/dist/agent-src/rules/no-pr-progress-comments.md +5 -1
  70. package/dist/agent-src/rules/no-roadmap-references.md +2 -0
  71. package/dist/agent-src/rules/notes-first-reasoning.md +2 -0
  72. package/dist/agent-src/rules/onboarding-gate.md +5 -1
  73. package/dist/agent-src/rules/output-discipline.md +2 -0
  74. package/dist/agent-src/rules/package-ci-checks.md +2 -0
  75. package/dist/agent-src/rules/persona-governance.md +2 -0
  76. package/dist/agent-src/rules/php-coding.md +2 -0
  77. package/dist/agent-src/rules/prefer-enums-over-literals.md +2 -0
  78. package/dist/agent-src/rules/preservation-guard.md +2 -0
  79. package/dist/agent-src/rules/provider-lifecycle-discipline.md +2 -0
  80. package/dist/agent-src/rules/question-not-instruction.md +2 -0
  81. package/dist/agent-src/rules/reviewer-awareness.md +2 -0
  82. package/dist/agent-src/rules/roadmap-ci-steps-policy.md +6 -1
  83. package/dist/agent-src/rules/roadmap-progress-sync.md +2 -0
  84. package/dist/agent-src/rules/role-mode-adherence.md +3 -1
  85. package/dist/agent-src/rules/rule-type-governance.md +2 -0
  86. package/dist/agent-src/rules/runtime-safety.md +2 -0
  87. package/dist/agent-src/rules/scale-discipline.md +5 -1
  88. package/dist/agent-src/rules/secret-vcs-guard.md +2 -0
  89. package/dist/agent-src/rules/security-sensitive-stop.md +2 -0
  90. package/dist/agent-src/rules/self-repair-loop.md +103 -0
  91. package/dist/agent-src/rules/senior-engineering-discipline.md +2 -0
  92. package/dist/agent-src/rules/session-canary.md +62 -19
  93. package/dist/agent-src/rules/settings-ask-protocol.md +4 -0
  94. package/dist/agent-src/rules/size-enforcement.md +2 -0
  95. package/dist/agent-src/rules/skill-improvement-trigger.md +2 -0
  96. package/dist/agent-src/rules/skill-quality.md +2 -0
  97. package/dist/agent-src/rules/slash-command-routing-policy.md +2 -0
  98. package/dist/agent-src/rules/source-confidentiality.md +2 -0
  99. package/dist/agent-src/rules/source-discovery-gate.md +2 -0
  100. package/dist/agent-src/rules/source-of-truth.md +2 -0
  101. package/dist/agent-src/rules/spreadsheet-source-quality.md +2 -0
  102. package/dist/agent-src/rules/strategy-safety-floor.md +3 -1
  103. package/dist/agent-src/rules/symfony-routing.md +2 -0
  104. package/dist/agent-src/rules/think-before-action.md +2 -0
  105. package/dist/agent-src/rules/token-budget-discipline.md +5 -1
  106. package/dist/agent-src/rules/token-efficiency.md +2 -0
  107. package/dist/agent-src/rules/token-optimizer-maintenance.md +2 -0
  108. package/dist/agent-src/rules/tool-safety.md +7 -1
  109. package/dist/agent-src/rules/ui-audit-gate.md +4 -0
  110. package/dist/agent-src/rules/untrusted-input-defense.md +2 -0
  111. package/dist/agent-src/rules/upstream-proposal.md +2 -0
  112. package/dist/agent-src/rules/user-interaction.md +2 -0
  113. package/dist/agent-src/rules/user-interrupt-priority.md +2 -0
  114. package/dist/agent-src/skills/adversarial-review/SKILL.md +1 -1
  115. package/dist/agent-src/skills/analysis-autonomous-mode/SKILL.md +1 -1
  116. package/dist/agent-src/skills/performance-analysis/SKILL.md +1 -1
  117. package/dist/agent-src/skills/persona-improvement/SKILL.md +1 -1
  118. package/dist/agent-src/skills/project-analyzer/SKILL.md +1 -1
  119. package/dist/agent-src/skills/security-audit/SKILL.md +1 -1
  120. package/dist/agent-src/skills/sequential-thinking/SKILL.md +1 -1
  121. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
  122. package/dist/agent-src/skills/universal-project-analysis/SKILL.md +1 -1
  123. package/dist/agent-src/templates/AGENTS.md +6 -10
  124. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  125. package/dist/cli/registry.js +9 -0
  126. package/dist/cli/registry.js.map +1 -1
  127. package/dist/cli-delegate/chunk-22GCIJ2O.js +270 -0
  128. package/dist/cli-delegate/{chunk-BSLUHV2F.js → chunk-2BE5I2MN.js} +156 -141
  129. package/dist/cli-delegate/{chunk-AIIUU73N.js → chunk-7OMQCH4V.js} +1 -1
  130. package/dist/cli-delegate/{chunk-HJ6DAJHR.js → chunk-GB2FSUEE.js} +57 -7
  131. package/dist/cli-delegate/{chunk-WR6GTRHJ.js → chunk-JXARCGVN.js} +3 -3
  132. package/dist/cli-delegate/{chunk-SJQMJI7M.js → chunk-PQSAQKHW.js} +467 -305
  133. package/dist/cli-delegate/chunk-REKCMYOU.js +2991 -0
  134. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  135. package/dist/cli-delegate/cmd_brand_status.js +134 -0
  136. package/dist/cli-delegate/cmd_conformance.js +7 -7
  137. package/dist/cli-delegate/cmd_converge.js +3 -3
  138. package/dist/cli-delegate/cmd_doctor.js +4 -4
  139. package/dist/cli-delegate/cmd_explain.js +80 -330
  140. package/dist/cli-delegate/cmd_export.js +5 -5
  141. package/dist/cli-delegate/cmd_fleet.js +3 -3
  142. package/dist/cli-delegate/cmd_handoff.js +2 -2
  143. package/dist/cli-delegate/cmd_mcp_available.js +433 -0
  144. package/dist/cli-delegate/cmd_migrate.js +3 -3
  145. package/dist/cli-delegate/cmd_packs_active.js +153 -0
  146. package/dist/cli-delegate/cmd_preflight.js +1 -1
  147. package/dist/cli-delegate/cmd_prune.js +5 -5
  148. package/dist/cli-delegate/cmd_route_audit.js +8 -8
  149. package/dist/cli-delegate/cmd_route_explain.js +4 -4
  150. package/dist/cli-delegate/cmd_settings_get.js +272 -0
  151. package/dist/cli-delegate/cmd_settings_migrate.js +5 -5
  152. package/dist/cli-delegate/cmd_settings_set.js +7 -2967
  153. package/dist/cli-delegate/cmd_sync.js +5 -5
  154. package/dist/cli-delegate/cmd_uninstall.js +5 -5
  155. package/dist/cli-delegate/cmd_update.js +4 -4
  156. package/dist/cli-delegate/cmd_upgrade.js +4 -4
  157. package/dist/cli-delegate/cmd_validate.js +5 -5
  158. package/dist/cli-delegate/cmd_versions.js +4 -4
  159. package/dist/discovery/deprecation-report.md +1 -1
  160. package/dist/discovery/discovery-manifest.json +184 -124
  161. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  162. package/dist/discovery/discovery-manifest.summary.md +3 -3
  163. package/dist/discovery/orphan-report.md +1 -1
  164. package/dist/discovery/packs.json +7 -5
  165. package/dist/discovery/trust-report.md +11 -11
  166. package/dist/discovery/workspaces.json +6 -4
  167. package/dist/hooks/dispatch.js +9080 -5701
  168. package/dist/install/install.mjs +698 -508
  169. package/dist/mcp/registry-manifest.json +3 -3
  170. package/dist/mcp/server.json +2 -2
  171. package/dist/mcp/server.mjs +698 -437
  172. package/dist/router.json +1 -1
  173. package/dist/scripts/_lib/agent_settings.js +80 -15
  174. package/dist/scripts/_lib/agent_settings.js.map +1 -1
  175. package/dist/scripts/condense.js +33 -2
  176. package/dist/scripts/condense.js.map +1 -1
  177. package/dist/server/io/yamlIO.js +85 -0
  178. package/dist/server/io/yamlIO.js.map +1 -1
  179. package/dist/server/routes/settings.js +1 -12
  180. package/dist/server/routes/settings.js.map +1 -1
  181. package/dist/server/routes/wizard.js +52 -2
  182. package/dist/server/routes/wizard.js.map +1 -1
  183. package/dist/shared/settingsCarveOut.js +118 -0
  184. package/dist/shared/settingsCarveOut.js.map +1 -0
  185. package/dist/shared/settingsSurface.js +7 -1
  186. package/dist/shared/settingsSurface.js.map +1 -1
  187. package/dist/ui/assets/index-DmR0mbk4.js.map +1 -1
  188. package/docs/contracts/settings-classes.md +29 -8
  189. package/docs/guidelines/agent-infra/memory-access.md +14 -3
  190. package/package.json +1 -1
  191. package/src/agent-src/templates/AGENTS.md +6 -10
  192. package/src/config/budgets.yml +29 -0
  193. package/src/config/evaluator-budgets.json +4 -4
  194. package/src/config/gate-coverage.yml +26 -0
  195. package/src/scripts/_cli/cmd_brand_status.ts +174 -0
  196. package/src/scripts/_cli/cmd_doctor.ts +6 -21
  197. package/src/scripts/_cli/cmd_mcp_available.ts +266 -0
  198. package/src/scripts/_cli/cmd_packs_active.ts +225 -0
  199. package/src/scripts/_cli/cmd_settings_get.ts +324 -0
  200. package/src/scripts/_dispatch.bash +75 -0
  201. package/src/scripts/_lib/agent_settings.ts +82 -16
  202. package/src/scripts/_lib/carrier_divergence.ts +135 -0
  203. package/src/scripts/_lib/git_common_dir.ts +128 -0
  204. package/src/scripts/_lib/host_capability.ts +84 -1
  205. package/src/scripts/_lib/obligation_frequency.ts +391 -0
  206. package/src/scripts/_lib/prompt_shape.ts +88 -0
  207. package/src/scripts/_lib/review_skipped_record.ts +102 -0
  208. package/src/scripts/_lib/rule_layer_overlap.ts +254 -0
  209. package/src/scripts/_lib/self_repair.ts +342 -0
  210. package/src/scripts/_lib/self_repair_store.ts +100 -0
  211. package/src/scripts/_lib/session_register.ts +361 -0
  212. package/src/scripts/_lib/transcript_entry.ts +80 -0
  213. package/src/scripts/check_branch_freshness.ts +255 -0
  214. package/src/scripts/check_council_config_location.ts +125 -1
  215. package/src/scripts/check_enforcement_coverage.ts +192 -7
  216. package/src/scripts/check_rule_projection_integrity.ts +364 -0
  217. package/src/scripts/check_standing_rule_delivery.ts +314 -0
  218. package/src/scripts/condense.ts +36 -2
  219. package/src/scripts/config/profiles.ts +13 -3
  220. package/src/scripts/conformance_scan.ts +307 -24
  221. package/src/scripts/council_availability_hook.ts +188 -0
  222. package/src/scripts/council_cli.ts +126 -7
  223. package/src/scripts/generate_settings_reference.ts +232 -0
  224. package/src/scripts/hook_manifest.yaml +155 -33
  225. package/src/scripts/hooks/block_no_verify.ts +95 -3
  226. package/src/scripts/hooks/block_unauthorized_git.ts +217 -2
  227. package/src/scripts/hooks/concern_registry.ts +12 -0
  228. package/src/scripts/hooks/delegation_nudge_hook.ts +450 -0
  229. package/src/scripts/hooks/end_review_nudge_hook.ts +726 -0
  230. package/src/scripts/hooks/orchestration_record_hook.ts +342 -0
  231. package/src/scripts/install.ts +151 -0
  232. package/src/scripts/language_mirror_hook.ts +49 -6
  233. package/src/scripts/measure_scope_dedup.ts +19 -13
  234. package/src/scripts/onboarding_gate_hook.ts +40 -1
  235. package/src/scripts/orchestration_backfill.ts +354 -0
  236. package/src/scripts/orchestration_record.ts +4 -1
  237. package/src/scripts/preamble_byte_census.ts +9 -20
  238. package/src/scripts/release.ts +142 -36
  239. package/src/scripts/report_carrier_divergence.ts +364 -0
  240. package/src/scripts/report_skill_activation.ts +257 -0
  241. package/src/scripts/report_skill_obligation_violations.ts +579 -0
  242. package/src/scripts/routing_doctor.ts +77 -6
  243. package/src/scripts/schemas/rule.schema.json +5 -0
  244. package/src/scripts/self_repair_cli.ts +240 -0
  245. package/src/scripts/self_repair_hook.ts +241 -0
  246. package/src/scripts/session_canary_hook.ts +35 -3
  247. package/src/scripts/session_register_hook.ts +288 -0
  248. package/src/scripts/sessions_cli.ts +140 -0
  249. package/src/scripts/validate_frontmatter.ts +68 -0
  250. package/src/server/io/yamlIO.ts +87 -0
  251. package/src/shared/settingsCarveOut.ts +143 -0
  252. package/src/shared/settingsSurface.ts +7 -1
@@ -25,6 +25,7 @@ import * as fs from 'node:fs';
25
25
  import * as os from 'node:os';
26
26
  import * as path from 'node:path';
27
27
 
28
+ import { comparePair } from './_lib/carrier_divergence.js';
28
29
  import { censusRuleDir } from './preamble_byte_census.js';
29
30
 
30
31
  const REPO_ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', '..');
@@ -91,17 +92,15 @@ function dedupableCount(userDir: string, sourceDir: string): { skipped: number;
91
92
  * perfect version alignment. Classifying the two apart is what turns "0/110"
92
93
  * from a puzzle into an answer: `provenanceOnly == total` means aligning
93
94
  * versions would buy exactly nothing.
95
+ *
96
+ * The three-way comparison now lives in `_lib/carrier_divergence.ts` because a
97
+ * second surface needs it (`report_carrier_divergence`, round-5 Phase 1.3). The
98
+ * DIRECTORY WALK stays here and stays anchored on `sourceDir`: this harness asks
99
+ * how many installed twins the dedup could skip, which is a question about the
100
+ * projection's own rules, not about the union of both carriers. Only the
101
+ * comparison is shared — that is the part that must not drift, and it is also
102
+ * the part `scope_dedup.test.ts` pins.
94
103
  */
95
- const OWNERSHIP_KEYS = ['package', 'source_path'] as const;
96
-
97
- function stripOwnershipKeys(buf: Buffer): string {
98
- return buf
99
- .toString('utf-8')
100
- .split('\n')
101
- .filter((line) => !OWNERSHIP_KEYS.some((k) => line.startsWith(`${k}:`)))
102
- .join('\n');
103
- }
104
-
105
104
  interface ReachabilitySplit {
106
105
  total: number;
107
106
  identical: number;
@@ -130,9 +129,16 @@ function classifyReachability(userDir: string, sourceDir: string): ReachabilityS
130
129
  continue;
131
130
  }
132
131
  const b = fs.readFileSync(path.join(sourceDir, entry));
133
- if (a.equals(b)) split.identical += 1;
134
- else if (stripOwnershipKeys(a) === stripOwnershipKeys(b)) split.provenanceOnly += 1;
135
- else split.bodyDiff += 1;
132
+ switch (comparePair(a, b)) {
133
+ case 'identical':
134
+ split.identical += 1;
135
+ break;
136
+ case 'provenance-only':
137
+ split.provenanceOnly += 1;
138
+ break;
139
+ default:
140
+ split.bodyDiff += 1;
141
+ }
136
142
  }
137
143
  return split;
138
144
  }
@@ -39,6 +39,45 @@ import { atomic_write_json } from "./hooks/state_io.js";
39
39
  import { readHookStdin } from "./hooks/hook_stdin.js";
40
40
 
41
41
  export const SETTINGS_FILE = ".agent-settings.yml";
42
+
43
+ /**
44
+ * Project-relative settings locations, canonical first.
45
+ *
46
+ * The canonical project settings file is `agents/settings/.agent-settings.yml`;
47
+ * the repo-root file is a documented legacy fallback
48
+ * (`agent_settings.ts::project_settings_path`). This hook read ONLY the legacy
49
+ * path, so on a canonically installed consumer the file was always "missing" and
50
+ * the gate resolved to `settings_file_missing` → do not block. An Iron-Law gate
51
+ * that cannot fire on a correct install is not a gate.
52
+ *
53
+ * Inlined rather than imported: this module is deliberately dependency-free so
54
+ * it can run as a hook on every platform. The list is two entries and its source
55
+ * of truth is named above.
56
+ */
57
+ export const SETTINGS_CANDIDATES: readonly string[] = [
58
+ path.join("agents", "settings", SETTINGS_FILE),
59
+ SETTINGS_FILE,
60
+ ];
61
+
62
+ /**
63
+ * First existing settings file under `consumer_root`, canonical before legacy.
64
+ *
65
+ * Falls back to the CANONICAL path when neither exists, so the "missing" branch
66
+ * reports the file a consumer should create rather than the one they should not.
67
+ */
68
+ export function resolve_settings_path(consumer_root: string): string {
69
+ for (const rel of SETTINGS_CANDIDATES) {
70
+ const candidate = path.join(consumer_root, rel);
71
+ try {
72
+ if (fs.statSync(candidate).isFile()) {
73
+ return candidate;
74
+ }
75
+ } catch {
76
+ continue;
77
+ }
78
+ }
79
+ return path.join(consumer_root, SETTINGS_CANDIDATES[0] as string);
80
+ }
42
81
  // NOTE: the Python docstring says `agents/runtime/state/`, but the code
43
82
  // constant is `agents/state/`. Replicated verbatim — this is a latent
44
83
  // docstring/code divergence in the retired Python implementation (ADR-200 § replicate
@@ -141,7 +180,7 @@ export function run(options: {
141
180
  }): number {
142
181
  const { consumer_root } = options;
143
182
  const verbose = options.verbose ?? false;
144
- const settings_path = path.join(consumer_root, SETTINGS_FILE);
183
+ const settings_path = resolve_settings_path(consumer_root);
145
184
  let settings_present = false;
146
185
  try {
147
186
  settings_present = fs.statSync(settings_path).isFile();
@@ -0,0 +1,354 @@
1
+ #!/usr/bin/env -S npx tsx
2
+ /**
3
+ * Backfill orchestration telemetry from the host transcript corpus.
4
+ *
5
+ * Why this exists (road-to-orchestrator-first-execution Phase 1). Two roadmaps
6
+ * are blocked on "≥20 orchestration lines in the current month", with the
7
+ * stated remedy "use the agent on real delegable work". Measured 2026-08-07:
8
+ * the agent was used on real delegable work **370 times** in this repo over one
9
+ * month, and `orchestration_record` — a model-carried emit step — captured
10
+ * exactly **one** of them. The blocker was never a usage problem; it is an
11
+ * instrumentation defect, and the data it was waiting for is already on disk.
12
+ *
13
+ * What this reads. The host writes one JSONL transcript per session under
14
+ * `~/.claude/projects/<slugified-project-path>/`. An `Agent` dispatch appears
15
+ * as a `tool_use` block; its result appears as a `toolUseResult` object.
16
+ *
17
+ * The coverage limit, which is the point rather than a footnote. Only
18
+ * SYNCHRONOUS dispatches carry cost: `totalTokens`, `totalDurationMs`,
19
+ * `usage`. An ASYNCHRONOUS dispatch returns a launch acknowledgement
20
+ * (`isAsync: true`, `status: "async_launched"`) and its completion never
21
+ * writes cost back into the parent transcript. So the metric-bearing
22
+ * population is a strict subset, and this script reports both counts rather
23
+ * than the flattering one. Emitting 370 lines of which 331 carry a null cost
24
+ * would inflate n against a gate that counts lines.
25
+ *
26
+ * What this deliberately does NOT do. It does not compute a `token_delta`
27
+ * against an in-session baseline. That counterfactual is not on disk and is
28
+ * not measured by anything, so synthesising one here and stamping it
29
+ * `measured` is the exact laundering `orchestration-observed-dispatch-cost`
30
+ * criterion (2) forbids. The orchestrated side is measured; the baseline is a
31
+ * downstream decision that must name its own method.
32
+ *
33
+ * Read-only. Never mutates a transcript.
34
+ */
35
+
36
+ import { readFileSync, readdirSync, existsSync, writeFileSync } from 'node:fs';
37
+ import { join, basename } from 'node:path';
38
+ import { homedir } from 'node:os';
39
+
40
+ /** One dispatch-family. `unclassified` is a real answer, never a guess. */
41
+ export type DispatchFamily =
42
+ | 'read-only-fanout'
43
+ | 'ordered-steps'
44
+ | 'competitive'
45
+ | 'verdict-judge'
46
+ | 'unclassified';
47
+
48
+ /**
49
+ * One backfilled dispatch.
50
+ *
51
+ * PII/leak-exclusion by construction: the host's free-text `description` and
52
+ * `prompt` are read to CLASSIFY and are then dropped — no field here can carry
53
+ * free-form content. This is not fastidiousness: the first real run emitted a
54
+ * historical task description carrying a denylisted external-source name and
55
+ * tripped `check_no_external_sources`. A type that cannot hold arbitrary text
56
+ * has no scrubber to fail, which is the same principle
57
+ * `artifact-engagement-recording` states for telemetry events. Never widen
58
+ * this with a `description` / `notes` / `payload` field.
59
+ */
60
+ export interface BackfilledDispatch {
61
+ session_id: string;
62
+ tool_use_id: string;
63
+ timestamp: string | null;
64
+ subagent_type: string | null;
65
+ family: DispatchFamily;
66
+ family_signal: string;
67
+ resolved_model: string | null;
68
+ /** Measured orchestrated-side cost. `null` on an async dispatch. */
69
+ total_tokens: number | null;
70
+ wall_clock_ms: number | null;
71
+ tool_use_count: number | null;
72
+ /** `measured` only when the host recorded the cost; never inferred. */
73
+ cost_provenance: 'measured' | 'absent';
74
+ async_launch: boolean;
75
+ }
76
+
77
+ /**
78
+ * Slugify a project path the way the host names its transcript directory:
79
+ * every character outside `[A-Za-z0-9]` becomes `-`.
80
+ */
81
+ export function transcriptDirFor(projectPath: string, home = homedir()): string {
82
+ return join(home, '.claude', 'projects', projectPath.replace(/[^A-Za-z0-9]/g, '-'));
83
+ }
84
+
85
+ /**
86
+ * Classify a dispatch into a family from OBSERVABLE fields only.
87
+ *
88
+ * The council's round-2 finding was that eight dispatch templates were being
89
+ * treated as one hypothesis. Family is therefore carried per line. Where no
90
+ * enumerated signal matches, the answer is `unclassified` — the same
91
+ * discipline `classifyTask` applies when no delegable signal matches. A
92
+ * classifier that always produces a family would make the per-family split
93
+ * meaningless.
94
+ */
95
+ export function classifyFamily(
96
+ subagentType: string | null,
97
+ description: string | null,
98
+ prompt: string | null,
99
+ ): { family: DispatchFamily; signal: string } {
100
+ const hay = `${description ?? ''}\n${prompt ?? ''}`.toLowerCase();
101
+
102
+ // Verdict/judge first — it is the narrowest and can otherwise be swallowed
103
+ // by the read-only signal, since most verdict passes are also read-only.
104
+ if (subagentType === 'production-validator') {
105
+ return { family: 'verdict-judge', signal: 'subagent_type=production-validator' };
106
+ }
107
+ if (/\b(verify|verdict|judge|adjudicat|audit this|review this|acceptance criterion)\b/.test(hay)) {
108
+ return { family: 'verdict-judge', signal: 'verdict-shaped verb in description/prompt' };
109
+ }
110
+
111
+ if (subagentType === 'Explore') {
112
+ return { family: 'read-only-fanout', signal: 'subagent_type=Explore' };
113
+ }
114
+ if (/\b(read-only|do not edit|never edit|investigation only|map |inventory|locate)\b/.test(hay)) {
115
+ return { family: 'read-only-fanout', signal: 'read-only declaration in prompt' };
116
+ }
117
+
118
+ if (/\b(step \d|then |after (the )?previous|in order|sequentially|depends on step)\b/.test(hay)) {
119
+ return { family: 'ordered-steps', signal: 'ordering language in prompt' };
120
+ }
121
+
122
+ if (/\b(variant|competing|alternative approach|candidate [ab]|independently propose)\b/.test(hay)) {
123
+ return { family: 'competitive', signal: 'competitive framing in prompt' };
124
+ }
125
+
126
+ return { family: 'unclassified', signal: 'no enumerated family signal matched' };
127
+ }
128
+
129
+ interface Extraction {
130
+ dispatches: BackfilledDispatch[];
131
+ sessions_scanned: number;
132
+ unparseable_lines: number;
133
+ }
134
+
135
+ /** Walk one project's transcript corpus and pair each `Agent` tool_use with its result. */
136
+ export function extract(dir: string): Extraction {
137
+ if (!existsSync(dir)) {
138
+ return { dispatches: [], sessions_scanned: 0, unparseable_lines: 0 };
139
+ }
140
+ const files = readdirSync(dir).filter((f) => f.endsWith('.jsonl'));
141
+ const dispatches: BackfilledDispatch[] = [];
142
+ let unparseable = 0;
143
+
144
+ for (const file of files) {
145
+ const sessionId = file.replace(/\.jsonl$/, '');
146
+ // tool_use_id → the dispatch we are still waiting on a result for.
147
+ const pending = new Map<string, Partial<BackfilledDispatch> & { prompt: string | null; description: string | null }>();
148
+ const emitted = new Map<string, BackfilledDispatch>();
149
+
150
+ for (const line of readFileSync(join(dir, file), 'utf8').split('\n')) {
151
+ if (line.trim() === '') continue;
152
+ let obj: Record<string, unknown>;
153
+ try {
154
+ obj = JSON.parse(line) as Record<string, unknown>;
155
+ } catch {
156
+ unparseable += 1;
157
+ continue;
158
+ }
159
+
160
+ const message = obj.message as Record<string, unknown> | undefined;
161
+ const content = message?.content;
162
+ if (Array.isArray(content)) {
163
+ for (const block of content) {
164
+ const b = block as Record<string, unknown>;
165
+ if (b.type !== 'tool_use' || b.name !== 'Agent') continue;
166
+ const input = (b.input ?? {}) as Record<string, unknown>;
167
+ const id = String(b.id ?? '');
168
+ if (id === '') continue;
169
+ pending.set(id, {
170
+ session_id: sessionId,
171
+ tool_use_id: id,
172
+ timestamp: typeof obj.timestamp === 'string' ? obj.timestamp : null,
173
+ subagent_type: typeof input.subagent_type === 'string' ? input.subagent_type : null,
174
+ description: typeof input.description === 'string' ? input.description : null,
175
+ prompt: typeof input.prompt === 'string' ? input.prompt : null,
176
+ });
177
+ }
178
+ }
179
+
180
+ const result = obj.toolUseResult as Record<string, unknown> | undefined;
181
+ if (!result || typeof result !== 'object') continue;
182
+ // The result carries no tool_use_id of its own; the host pairs it via
183
+ // the enclosing user message's tool_result block.
184
+ let resultId: string | null = null;
185
+ if (Array.isArray(content)) {
186
+ for (const block of content) {
187
+ const b = block as Record<string, unknown>;
188
+ if (b.type === 'tool_result' && typeof b.tool_use_id === 'string') {
189
+ resultId = b.tool_use_id;
190
+ }
191
+ }
192
+ }
193
+ if (resultId === null || !pending.has(resultId)) continue;
194
+ const head = pending.get(resultId)!;
195
+ pending.delete(resultId);
196
+
197
+ const totalTokens = typeof result.totalTokens === 'number' ? result.totalTokens : null;
198
+ const { family, signal } = classifyFamily(
199
+ head.subagent_type ?? null,
200
+ head.description ?? null,
201
+ head.prompt,
202
+ );
203
+
204
+ emitted.set(resultId, {
205
+ session_id: sessionId,
206
+ tool_use_id: resultId,
207
+ timestamp: head.timestamp ?? null,
208
+ subagent_type: head.subagent_type ?? null,
209
+ family,
210
+ family_signal: signal,
211
+ resolved_model: typeof result.resolvedModel === 'string' ? result.resolvedModel : null,
212
+ total_tokens: totalTokens,
213
+ wall_clock_ms: typeof result.totalDurationMs === 'number' ? result.totalDurationMs : null,
214
+ tool_use_count: typeof result.totalToolUseCount === 'number' ? result.totalToolUseCount : null,
215
+ cost_provenance: totalTokens === null ? 'absent' : 'measured',
216
+ async_launch: result.isAsync === true,
217
+ });
218
+ }
219
+
220
+ dispatches.push(...emitted.values());
221
+ // A dispatch whose result never landed in this transcript (session ended
222
+ // mid-flight) is reported as a gap rather than dropped silently.
223
+ for (const [id, head] of pending) {
224
+ const { family, signal } = classifyFamily(
225
+ head.subagent_type ?? null,
226
+ head.description ?? null,
227
+ head.prompt,
228
+ );
229
+ dispatches.push({
230
+ session_id: sessionId,
231
+ tool_use_id: id,
232
+ timestamp: head.timestamp ?? null,
233
+ subagent_type: head.subagent_type ?? null,
234
+ family,
235
+ family_signal: signal,
236
+ resolved_model: null,
237
+ total_tokens: null,
238
+ wall_clock_ms: null,
239
+ tool_use_count: null,
240
+ cost_provenance: 'absent',
241
+ async_launch: false,
242
+ });
243
+ }
244
+ }
245
+
246
+ return { dispatches, sessions_scanned: files.length, unparseable_lines: unparseable };
247
+ }
248
+
249
+ /**
250
+ * The shape that actually gets written to disk.
251
+ *
252
+ * `tool_use_id` is pairing plumbing: it exists to match a dispatch to its
253
+ * result during extraction and has no analytical role afterwards. It is
254
+ * dropped at the TYPE level rather than scrubbed at write time, because the
255
+ * host ids are high-entropy opaque tokens and `check_secret_leak` flags them
256
+ * as candidate credentials — correctly, in the sense that it cannot know they
257
+ * are not. Emitting a field only to allow-list it downstream is the worse
258
+ * trade: the artifact does not need it.
259
+ */
260
+ export type EmittedDispatch = Omit<BackfilledDispatch, 'tool_use_id'>;
261
+
262
+ /** Project an extracted dispatch onto the emitted shape. */
263
+ export function toEmitted(d: BackfilledDispatch): EmittedDispatch {
264
+ const { tool_use_id: _pairingKey, ...rest } = d;
265
+ return rest;
266
+ }
267
+
268
+ /** Per-family roll-up. Medians, never means — one 530k outlier should not carry a family. */
269
+ export function summarize(dispatches: BackfilledDispatch[]): Record<string, unknown> {
270
+ const byFamily = new Map<string, number[]>();
271
+ for (const d of dispatches) {
272
+ if (d.cost_provenance !== 'measured' || d.total_tokens === null) continue;
273
+ const bucket = byFamily.get(d.family) ?? [];
274
+ bucket.push(d.total_tokens);
275
+ byFamily.set(d.family, bucket);
276
+ }
277
+ const families: Record<string, unknown> = {};
278
+ for (const [family, tokens] of byFamily) {
279
+ tokens.sort((a, b) => a - b);
280
+ families[family] = {
281
+ n: tokens.length,
282
+ // criterion (1): n<5 is UNDERPOWERED and is never merged to reach n.
283
+ power: tokens.length < 5 ? 'UNDERPOWERED' : 'reportable',
284
+ median_tokens: tokens[Math.floor(tokens.length / 2)],
285
+ min_tokens: tokens[0],
286
+ max_tokens: tokens[tokens.length - 1],
287
+ sum_tokens: tokens.reduce((a, b) => a + b, 0),
288
+ };
289
+ }
290
+ const measured = dispatches.filter((d) => d.cost_provenance === 'measured').length;
291
+ return {
292
+ dispatches_total: dispatches.length,
293
+ dispatches_with_measured_cost: measured,
294
+ dispatches_without_cost: dispatches.length - measured,
295
+ async_launches: dispatches.filter((d) => d.async_launch).length,
296
+ cost_coverage_ratio: dispatches.length === 0 ? 0 : Number((measured / dispatches.length).toFixed(4)),
297
+ families,
298
+ models: dispatches
299
+ .filter((d) => d.cost_provenance === 'measured')
300
+ .reduce<Record<string, number>>((acc, d) => {
301
+ const key = d.resolved_model ?? 'unknown';
302
+ acc[key] = (acc[key] ?? 0) + 1;
303
+ return acc;
304
+ }, {}),
305
+ baseline_note:
306
+ 'No in-session counterfactual exists on disk. token_delta is NOT computed here; ' +
307
+ 'any downstream comparison must name its own baseline method and stamp it estimated.',
308
+ };
309
+ }
310
+
311
+ function main(argv: string[]): number {
312
+ const projectArg = argv.indexOf('--project');
313
+ const project =
314
+ projectArg >= 0 && argv[projectArg + 1] !== undefined ? argv[projectArg + 1]! : process.cwd();
315
+ const outArg = argv.indexOf('--out');
316
+ const dir = transcriptDirFor(project);
317
+
318
+ if (!existsSync(dir)) {
319
+ process.stderr.write(`orchestration_backfill: no transcript corpus at ${dir}\n`);
320
+ return 1;
321
+ }
322
+
323
+ const { dispatches, sessions_scanned, unparseable_lines } = extract(dir);
324
+ const summary = summarize(dispatches);
325
+ // Emit the corpus identity, never the absolute path: the transcript
326
+ // directory is rooted at the operator's home and a committed report
327
+ // carrying it leaks an identity-revealing local path for no analytical
328
+ // gain (the basename already identifies which corpus was read).
329
+ const report = {
330
+ schema_version: 1,
331
+ source_corpus: basename(dir),
332
+ sessions_scanned,
333
+ unparseable_lines,
334
+ ...summary,
335
+ };
336
+
337
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
338
+
339
+ if (outArg >= 0 && argv[outArg + 1] !== undefined) {
340
+ const lines = dispatches
341
+ .filter((d) => d.cost_provenance === 'measured')
342
+ .map((d) => JSON.stringify({ input_kind: 'orchestration', orchestration: toEmitted(d) }))
343
+ .join('\n');
344
+ writeFileSync(argv[outArg + 1]!, lines === '' ? '' : `${lines}\n`, 'utf8');
345
+ process.stderr.write(
346
+ `orchestration_backfill: wrote ${summary.dispatches_with_measured_cost} measured line(s) to ${argv[outArg + 1]}\n`,
347
+ );
348
+ }
349
+ return 0;
350
+ }
351
+
352
+ if (process.argv[1] !== undefined && process.argv[1].includes('orchestration_backfill')) {
353
+ process.exit(main(process.argv.slice(2)));
354
+ }
@@ -41,7 +41,10 @@ import {
41
41
  type RouteTaken,
42
42
  } from './_lib/orchestration_record.js';
43
43
 
44
- const DEFAULT_DIR = 'agents/runtime/state/audit';
44
+ // Exported so other writers of the same audit-log-v1 orchestration line
45
+ // (e.g. orchestration_record_hook.ts, the deterministic PostToolUse capture)
46
+ // target the exact same directory instead of duplicating the literal.
47
+ export const DEFAULT_DIR = 'agents/runtime/state/audit';
45
48
 
46
49
  type Flags = Record<string, string | boolean>;
47
50
 
@@ -44,6 +44,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
44
44
  import * as YAML from 'yaml';
45
45
 
46
46
  import { resolveGlobalProfilePath } from './_lib/agent_user_profile.js';
47
+ import { censusRuleDir, type RuleDirCensus } from './_lib/carrier_divergence.js';
47
48
  import { DEFAULT_PROJECTS_ROOT, scanTranscripts } from './_lib/cc_transcript.js';
48
49
  import { censusDuplicateScope } from './_lib/duplicate_scope_census.js';
49
50
  import { computeColdStarts } from './cache_realization_report.js';
@@ -71,26 +72,14 @@ function readCharsIfExists(p: string): number {
71
72
 
72
73
  // ── source 1/2: rule directories ────────────────────────────────────────
73
74
 
74
- export interface RuleDirCensus {
75
- files: number;
76
- chars: number;
77
- }
78
-
79
- export function censusRuleDir(dir: string): RuleDirCensus {
80
- if (!fs.existsSync(dir)) return { files: 0, chars: 0 };
81
- let files = 0;
82
- let chars = 0;
83
- for (const name of fs.readdirSync(dir)) {
84
- if (!name.endsWith('.md')) continue;
85
- try {
86
- chars += fs.statSync(path.join(dir, name)).size;
87
- files += 1;
88
- } catch {
89
- // Unreadable entry — skip rather than fail the whole census.
90
- }
91
- }
92
- return { files, chars };
93
- }
75
+ // Moved to `_lib/carrier_divergence.ts` when `conformance_scan` needed the same
76
+ // count (round-6 Phase 4.3): that module is on a bundled CLI path and this one
77
+ // pulls `yaml` plus the cold-start report, so importing this file to reach fifteen
78
+ // lines of `statSync` was the wrong direction. Re-exported so every existing
79
+ // caller keeps its import and there is still one definition. Imported as well as
80
+ // re-exported: a bare `export … from` does not bind the name in this module's own
81
+ // scope, and this file uses both below.
82
+ export { censusRuleDir, type RuleDirCensus };
94
83
 
95
84
  export interface PerRuleCost {
96
85
  file: string;