@event4u/agent-config 9.27.0 → 9.29.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 (220) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +13 -12
  3. package/CHANGELOG.md +165 -0
  4. package/README.md +1 -1
  5. package/agents/templates/.ai-council.yml.example +55 -24
  6. package/dist/agent-src/commands/analyze/conformance.md +24 -0
  7. package/dist/agent-src/commands/cost/report.md +2 -2
  8. package/dist/agent-src/commands/team/adversarial.md +8 -7
  9. package/dist/agent-src/commands/team/delegate.md +13 -12
  10. package/dist/agent-src/commands/team/review.md +9 -8
  11. package/dist/agent-src/commands/team/status.md +9 -8
  12. package/dist/agent-src/commands/team.md +29 -19
  13. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +88 -2
  14. package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +29 -21
  15. package/dist/agent-src/contexts/execution/host-capability-manifest.md +26 -15
  16. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +28 -3
  17. package/dist/agent-src/contexts/execution/subagent-response-contract.md +54 -0
  18. package/dist/agent-src/contexts/execution/subagent-routing.md +13 -11
  19. package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +25 -0
  20. package/dist/agent-src/contexts/execution/subagent-steering.md +6 -3
  21. package/dist/agent-src/rules/artifact-engagement-recording.md +4 -1
  22. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  23. package/dist/agent-src/rules/brand-source-of-truth.md +33 -6
  24. package/dist/agent-src/rules/context-hygiene.md +3 -1
  25. package/dist/agent-src/rules/council-availability.md +93 -0
  26. package/dist/agent-src/rules/cross-source-consistency.md +3 -0
  27. package/dist/agent-src/rules/delegation-policy.md +38 -10
  28. package/dist/agent-src/rules/design-fidelity.md +3 -0
  29. package/dist/agent-src/rules/evaluator-independence.md +2 -1
  30. package/dist/agent-src/rules/external-code-graph-interop.md +2 -1
  31. package/dist/agent-src/rules/finance-safety-floor.md +1 -1
  32. package/dist/agent-src/rules/git-history-discipline.md +1 -1
  33. package/dist/agent-src/rules/history-discipline.md +3 -1
  34. package/dist/agent-src/rules/legal-safety-floor.md +1 -1
  35. package/dist/agent-src/rules/no-pr-progress-comments.md +3 -1
  36. package/dist/agent-src/rules/onboarding-gate.md +3 -1
  37. package/dist/agent-src/rules/roadmap-ci-steps-policy.md +4 -1
  38. package/dist/agent-src/rules/role-mode-adherence.md +1 -1
  39. package/dist/agent-src/rules/scale-discipline.md +3 -1
  40. package/dist/agent-src/rules/self-repair-loop.md +103 -0
  41. package/dist/agent-src/rules/session-canary.md +2 -1
  42. package/dist/agent-src/rules/settings-ask-protocol.md +8 -7
  43. package/dist/agent-src/rules/strategy-safety-floor.md +1 -1
  44. package/dist/agent-src/rules/token-budget-discipline.md +4 -2
  45. package/dist/agent-src/rules/tool-safety.md +5 -1
  46. package/dist/agent-src/skills/adversarial-review/SKILL.md +1 -1
  47. package/dist/agent-src/skills/analysis-autonomous-mode/SKILL.md +1 -1
  48. package/dist/agent-src/skills/forensics-report/SKILL.md +114 -0
  49. package/dist/agent-src/skills/forensics-report/evals/triggers.json +17 -0
  50. package/dist/agent-src/skills/performance-analysis/SKILL.md +1 -1
  51. package/dist/agent-src/skills/persona-improvement/SKILL.md +1 -1
  52. package/dist/agent-src/skills/premortem/SKILL.md +30 -1
  53. package/dist/agent-src/skills/project-analyzer/SKILL.md +1 -1
  54. package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +2 -1
  55. package/dist/agent-src/skills/security-audit/SKILL.md +1 -1
  56. package/dist/agent-src/skills/sequential-thinking/SKILL.md +1 -1
  57. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
  58. package/dist/agent-src/skills/subagent-orchestration/SKILL.md +7 -5
  59. package/dist/agent-src/skills/subagent-orchestration/schemas/subagent-status.json +61 -3
  60. package/dist/agent-src/skills/test-case-discovery/SKILL.md +1 -1
  61. package/dist/agent-src/skills/universal-project-analysis/SKILL.md +1 -1
  62. package/dist/agent-src/templates/AGENTS.md +8 -12
  63. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  64. package/dist/agent-src/templates/roadmaps.md +23 -0
  65. package/dist/cli/registry.js +7 -0
  66. package/dist/cli/registry.js.map +1 -1
  67. package/dist/cli-delegate/chunk-66OVWGB5.js +270 -0
  68. package/dist/cli-delegate/{chunk-HJ6DAJHR.js → chunk-LCFJIQ4J.js} +89 -7
  69. package/dist/cli-delegate/{chunk-VSYT7OHO.js → chunk-LVRYBKTI.js} +31 -2
  70. package/dist/cli-delegate/{chunk-DVRS2KVR.js → chunk-MMXA5GY7.js} +10 -17
  71. package/dist/cli-delegate/{chunk-WR6GTRHJ.js → chunk-MP3JT3IU.js} +3 -3
  72. package/dist/cli-delegate/{chunk-3KJIMFTB.js → chunk-NZ5N2XQL.js} +467 -305
  73. package/dist/cli-delegate/{chunk-AIIUU73N.js → chunk-OJM3MBJM.js} +1 -1
  74. package/dist/cli-delegate/{chunk-BCPIOAGV.js → chunk-RCIA67XN.js} +117 -96
  75. package/dist/cli-delegate/chunk-REKCMYOU.js +2991 -0
  76. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  77. package/dist/cli-delegate/cmd_brand_status.js +134 -0
  78. package/dist/cli-delegate/cmd_conformance.js +10 -47
  79. package/dist/cli-delegate/cmd_converge.js +3 -3
  80. package/dist/cli-delegate/cmd_doctor.js +10 -6
  81. package/dist/cli-delegate/cmd_explain.js +80 -330
  82. package/dist/cli-delegate/cmd_export.js +6 -6
  83. package/dist/cli-delegate/cmd_fleet.js +3 -3
  84. package/dist/cli-delegate/cmd_handoff.js +3 -3
  85. package/dist/cli-delegate/cmd_mcp_available.js +433 -0
  86. package/dist/cli-delegate/cmd_migrate.js +3 -3
  87. package/dist/cli-delegate/cmd_packs_active.js +153 -0
  88. package/dist/cli-delegate/cmd_preflight.js +1 -1
  89. package/dist/cli-delegate/cmd_prune.js +6 -6
  90. package/dist/cli-delegate/cmd_route_audit.js +8 -8
  91. package/dist/cli-delegate/cmd_route_explain.js +4 -4
  92. package/dist/cli-delegate/cmd_settings_get.js +266 -0
  93. package/dist/cli-delegate/cmd_settings_migrate.js +6 -6
  94. package/dist/cli-delegate/cmd_settings_set.js +8 -2968
  95. package/dist/cli-delegate/cmd_sync.js +6 -6
  96. package/dist/cli-delegate/cmd_uninstall.js +6 -6
  97. package/dist/cli-delegate/cmd_update.js +4 -4
  98. package/dist/cli-delegate/cmd_upgrade.js +4 -4
  99. package/dist/cli-delegate/cmd_validate.js +6 -6
  100. package/dist/cli-delegate/cmd_versions.js +4 -4
  101. package/dist/discovery/deprecation-report.md +1 -1
  102. package/dist/discovery/discovery-manifest.json +164 -55
  103. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  104. package/dist/discovery/discovery-manifest.summary.md +4 -3
  105. package/dist/discovery/orphan-report.md +1 -1
  106. package/dist/discovery/packs.json +44 -5
  107. package/dist/discovery/trust-report.md +11 -11
  108. package/dist/discovery/workspaces.json +16 -6
  109. package/dist/hooks/dispatch.js +9341 -6131
  110. package/dist/install/install.mjs +739 -524
  111. package/dist/mcp/registry-manifest.json +3 -3
  112. package/dist/mcp/server.json +2 -2
  113. package/dist/mcp/server.mjs +777 -281
  114. package/dist/router.json +1 -1
  115. package/dist/scripts/_lib/agent_settings.js +134 -15
  116. package/dist/scripts/_lib/agent_settings.js.map +1 -1
  117. package/dist/scripts/condense.js +147 -2
  118. package/dist/scripts/condense.js.map +1 -1
  119. package/dist/server/schemas/settings.js +6 -7
  120. package/dist/server/schemas/settings.js.map +1 -1
  121. package/dist/shared/settingsCarveOut.js +0 -9
  122. package/dist/shared/settingsCarveOut.js.map +1 -1
  123. package/dist/shared/settingsClasses.js +11 -9
  124. package/dist/shared/settingsClasses.js.map +1 -1
  125. package/dist/ui/assets/{index-DmR0mbk4.js → index-JDAF00bC.js} +2 -2
  126. package/dist/ui/assets/{index-DmR0mbk4.js.map → index-JDAF00bC.js.map} +1 -1
  127. package/dist/ui/index.html +1 -1
  128. package/docs/contracts/settings-classes.md +68 -17
  129. package/docs/guidelines/agent-infra/memory-access.md +14 -3
  130. package/package.json +1 -1
  131. package/src/agent-src/templates/AGENTS.md +8 -12
  132. package/src/config/agent-settings.template.yml +52 -66
  133. package/src/config/budgets.yml +29 -0
  134. package/src/config/discovery/packs.yml +11 -0
  135. package/src/config/discovery/workspaces.yml +1 -1
  136. package/src/config/evaluator-budgets.json +4 -4
  137. package/src/config/gate-coverage.yml +26 -0
  138. package/src/config/gate-violation-baselines.json +3 -3
  139. package/src/scripts/_cli/cmd_brand_status.ts +174 -0
  140. package/src/scripts/_cli/cmd_conformance.ts +14 -42
  141. package/src/scripts/_cli/cmd_doctor.ts +65 -48
  142. package/src/scripts/_cli/cmd_mcp_available.ts +266 -0
  143. package/src/scripts/_cli/cmd_packs_active.ts +225 -0
  144. package/src/scripts/_cli/cmd_settings_get.ts +324 -0
  145. package/src/scripts/_dispatch.bash +57 -0
  146. package/src/scripts/_lib/agent_settings.ts +140 -16
  147. package/src/scripts/_lib/auto_dispatch.corpus.test.ts +1 -1
  148. package/src/scripts/_lib/auto_dispatch.ts +23 -18
  149. package/src/scripts/_lib/capsule_trigger.ts +124 -0
  150. package/src/scripts/_lib/carrier_divergence.ts +135 -0
  151. package/src/scripts/_lib/host_capability.ts +144 -4
  152. package/src/scripts/_lib/judgment_ladder.ts +382 -0
  153. package/src/scripts/_lib/orchestration_gate.ts +8 -0
  154. package/src/scripts/_lib/orchestration_record.ts +37 -0
  155. package/src/scripts/_lib/orchestration_savings.ts +1 -1
  156. package/src/scripts/_lib/prompt_shape.ts +88 -0
  157. package/src/scripts/_lib/review_skipped_record.ts +102 -0
  158. package/src/scripts/_lib/rule_layer_overlap.ts +254 -0
  159. package/src/scripts/_lib/self_repair.ts +489 -0
  160. package/src/scripts/_lib/self_repair_store.ts +128 -0
  161. package/src/scripts/_lib/subagent_capsule.ts +182 -0
  162. package/src/scripts/_lib/subagent_response.ts +19 -0
  163. package/src/scripts/_lib/subagent_spawn.ts +13 -0
  164. package/src/scripts/_lib/subagent_steering.ts +9 -4
  165. package/src/scripts/_lib/tier_budget_routing.ts +6 -1
  166. package/src/scripts/_lib/transcript_entry.ts +80 -0
  167. package/src/scripts/_lib/worker_budget.ts +37 -2
  168. package/src/scripts/ai_council/clients.ts +76 -54
  169. package/src/scripts/ai_council/config.ts +94 -6
  170. package/src/scripts/ai_council/handoff.ts +113 -0
  171. package/src/scripts/ai_council/modes.ts +19 -4
  172. package/src/scripts/ai_council/orchestrator.ts +105 -0
  173. package/src/scripts/ai_council/quorum.ts +84 -0
  174. package/src/scripts/ai_council/session.ts +44 -0
  175. package/src/scripts/ai_council/transport_resolver.ts +127 -1
  176. package/src/scripts/ai_team/availability.ts +110 -0
  177. package/src/scripts/ai_team/config.ts +34 -14
  178. package/src/scripts/ai_team/team_dispatch.ts +71 -29
  179. package/src/scripts/bench_critic_protocol.ts +396 -0
  180. package/src/scripts/check_branch_freshness.ts +255 -0
  181. package/src/scripts/check_council_config_location.ts +125 -1
  182. package/src/scripts/check_detector_corpus.ts +202 -0
  183. package/src/scripts/check_hook_bundle_freshness.ts +4 -1
  184. package/src/scripts/check_standing_rule_delivery.ts +314 -0
  185. package/src/scripts/condense.ts +162 -2
  186. package/src/scripts/config/profiles.ts +13 -3
  187. package/src/scripts/conformance_scan.ts +272 -24
  188. package/src/scripts/council_availability_hook.ts +188 -0
  189. package/src/scripts/council_cli.ts +527 -32
  190. package/src/scripts/forensics_report.ts +333 -0
  191. package/src/scripts/hook_manifest.yaml +107 -26
  192. package/src/scripts/hooks/block_no_verify.ts +39 -2
  193. package/src/scripts/hooks/block_unauthorized_git.ts +156 -7
  194. package/src/scripts/hooks/concern_registry.ts +10 -0
  195. package/src/scripts/hooks/delegation_nudge_hook.ts +484 -0
  196. package/src/scripts/hooks/end_review_nudge_hook.ts +726 -0
  197. package/src/scripts/hooks/orchestration_record_hook.ts +342 -0
  198. package/src/scripts/install.ts +151 -0
  199. package/src/scripts/language_mirror_hook.ts +30 -37
  200. package/src/scripts/lint_discovery_vocabulary.ts +2 -0
  201. package/src/scripts/lint_never_silent.ts +276 -0
  202. package/src/scripts/lint_no_activation_gates.ts +271 -0
  203. package/src/scripts/measure_scope_dedup.ts +19 -13
  204. package/src/scripts/onboarding_gate_hook.ts +40 -1
  205. package/src/scripts/orchestration_record.ts +12 -1
  206. package/src/scripts/preamble_byte_census.ts +9 -20
  207. package/src/scripts/rebuild_hook_bundle.ts +177 -0
  208. package/src/scripts/release.ts +74 -2
  209. package/src/scripts/report_carrier_divergence.ts +364 -0
  210. package/src/scripts/report_skill_activation.ts +257 -0
  211. package/src/scripts/report_skill_obligation_violations.ts +579 -0
  212. package/src/scripts/routing_doctor.ts +95 -39
  213. package/src/scripts/score_skill_selection.ts +19 -5
  214. package/src/scripts/self_repair_cli.ts +470 -0
  215. package/src/scripts/self_repair_hook.ts +241 -0
  216. package/src/scripts/team_events_spike.ts +271 -0
  217. package/src/scripts/team_review_gate_hook.ts +31 -9
  218. package/src/server/schemas/settings.ts +10 -17
  219. package/src/shared/settingsCarveOut.ts +0 -10
  220. package/src/shared/settingsClasses.ts +11 -9
@@ -0,0 +1,382 @@
1
+ /**
2
+ * Judgment ladder (Phase 2, road-to-always-on-orchestration) — the ONE
3
+ * resolver that decides which of the five dispatch rungs (0-4), or the
4
+ * silent ∅, a task resolves to. Committed table + rationale:
5
+ * `src/agent-src/contexts/execution/auto-dispatch-classification.md`
6
+ * § Judgment ladder.
7
+ *
8
+ * This WRAPS `classifyTask` rather than replacing it — rungs 1 and 2 ARE
9
+ * `classifyTask`'s existing dispatch verdict (single-slice vs multi-slice);
10
+ * rungs 0, 3, and 4 are new signal layers checked around it, in a FIXED
11
+ * priority order (documented on {@link classifyLadder} itself, because the
12
+ * order IS the contract: two signals matching the same text must resolve
13
+ * deterministically, never on evaluation-order accident).
14
+ *
15
+ * Three previously-scattered classification surfaces (the delegable-signal
16
+ * rules in `auto_dispatch.ts`, ad-hoc "should this be a team" judgment
17
+ * calls, and the absence of any council-routing signal on the TASK side)
18
+ * get ONE committed name and ONE resolver here — never a fourth parallel
19
+ * classifier bolted on beside it.
20
+ *
21
+ * Deliberately independent of `ai_council/necessity.ts`: that module is the
22
+ * council's OWN necessity gate (its `NECESSARY_TRIGGERS` vocabulary and its
23
+ * `off|educate|block|warn-only` modes stay the council-side surface, per the
24
+ * roadmap's 2.2 note). This resolver's rung-4 signals are a NEW, narrower
25
+ * set — the three shapes the roadmap names explicitly (design decision,
26
+ * security downgrade, release-gate escalation) — kept as separate,
27
+ * regex-only constants rather than an import so this resolver carries no
28
+ * runtime dependency on the council module.
29
+ */
30
+ import {
31
+ classifyLookup,
32
+ classifyTask,
33
+ SIZE_FLOOR,
34
+ type ActivationInputs,
35
+ type DispatchMode,
36
+ type TaskSignals,
37
+ } from './auto_dispatch.js';
38
+
39
+ /** The five dispatch rungs, or `null` for the silent ∅ (in-session / ask). */
40
+ export type LadderRung = 0 | 1 | 2 | 3 | 4 | null;
41
+
42
+ export type LadderVerdict =
43
+ | 'script' // rung 0 — deterministic transform/primitive, no spawn at all
44
+ | 'subagent' // rung 1/2 — one, or several downshifted, subagent dispatch(es)
45
+ | 'team' // rung 3 — communicating slices, dispatched via the host teams primitive
46
+ | 'council' // rung 4 — contested judgment
47
+ | 'ask' // ∅ — ambiguous; a VERDICT to the user, never a speculative spawn
48
+ | 'in-session'; // ∅ — halted, no host primitive, trivial, approval-required, or recursive guard
49
+
50
+ export interface LadderResult {
51
+ rung: LadderRung;
52
+ verdict: LadderVerdict;
53
+ reason: string;
54
+ /**
55
+ * Present only when a HIGHER rung's signal fired but its precondition
56
+ * failed (today: rung 3's `agentTeams` host-capability check) and
57
+ * resolution fell back to a lower rung/verdict. Names the rung the
58
+ * signal originally pointed at, never the resolved one.
59
+ */
60
+ degraded_from?: LadderRung;
61
+ /**
62
+ * Carried through from `classifyTask` when the resolved rung is 1 or 2
63
+ * — the concrete `subagent-orchestration` mode a caller dispatches on.
64
+ * `null` for rung 1 (a single bounded slice has no do-in-steps /
65
+ * do-in-parallel shape of its own) and for every non-subagent verdict.
66
+ * Not part of the roadmap's minimal `{rung, verdict, reason,
67
+ * degraded_from?}` shape — added because the delegation-nudge hook
68
+ * (2.4) needs the concrete mode to build its injected line.
69
+ */
70
+ mode?: DispatchMode | null;
71
+ }
72
+
73
+ export interface LadderInputs {
74
+ /**
75
+ * Raw task/prompt text — regex-only signal extraction for rungs 0, 1, 3,
76
+ * and 4, in the same discipline as `delegation_nudge_hook.ts`'s
77
+ * extractors (cheap, no LLM call; false-positive/negative trade-offs
78
+ * documented per pattern below).
79
+ */
80
+ taskText: string;
81
+ /** Structural signals `classifyTask` already consumes — rungs 1/2. */
82
+ signals: TaskSignals;
83
+ /** Emergency halt + subagent_spawn primitive — gates every rung above 0. */
84
+ activation: ActivationInputs;
85
+ /**
86
+ * Host-capability manifest's `agent_teams` field
87
+ * (`host_capability.ts::probeHostCapabilities`) — rung 3's precondition.
88
+ * Absent/`false` degrades a matched rung-3 signal to rung 2.
89
+ */
90
+ agentTeams: boolean;
91
+ /**
92
+ * Caller states this step needs an explicit human decision before
93
+ * anything may run (an approval gate, a Hard-Floor action, …) — NEVER
94
+ * inferred from `taskText`. Checked first; overrides every other signal.
95
+ */
96
+ interactiveApprovalRequired?: boolean;
97
+ /**
98
+ * Recursive-dispatch guard (2.3). See {@link classifyLadder}'s doc
99
+ * comment for the honest scope of what this suite can and cannot
100
+ * detect on its own — this field is a CALLER-SUPPLIED fact, never
101
+ * something this resolver probes for itself.
102
+ */
103
+ insideSubagentSession?: boolean;
104
+ }
105
+
106
+ // ── Rung 0 — mechanical-transform signals (regex-only) ──────────────────
107
+ //
108
+ // `classifyLookup` (auto_dispatch.ts, road-to-lean-agent-init L0) already
109
+ // covers the READ-ONLY half of "deterministic script, no spawn" (definition/
110
+ // references/string-existence/report-run lookups). This adds the WRITE half
111
+ // the roadmap names explicitly: mechanical transforms with no semantics to
112
+ // judge — a rename, a codemod, a formatter run, a bulk search-and-replace.
113
+ // Each pattern requires the verb AND its object shape (e.g. "rename X to Y",
114
+ // not bare "rename") so a sentence that merely MENTIONS renaming in passing
115
+ // ("we renamed this last week") does not false-positive into rung 0.
116
+ const RENAME_RE = /\brename\s+\S+\s+to\s+\S+\b/i;
117
+ const CODEMOD_RE = /\bcodemod\b/i;
118
+ const FORMATTER_RUN_RE =
119
+ /\bauto-?format(?:ting)?\b|\breformat(?:ting)?\b|\brun(?:ning)?\s+prettier\b|\brun(?:ning)?\s+eslint\s*--fix\b|\bapply\s+eslint\s*--fix\b/i;
120
+ const SEARCH_REPLACE_RE = /\bsearch-and-replace\b|\bfind-and-replace\b|\bbulk\s+rename\b/i;
121
+
122
+ export function detectMechanicalTransform(text: string): { matched: boolean; reason: string } {
123
+ const rename = RENAME_RE.exec(text);
124
+ if (rename) {
125
+ return { matched: true, reason: `rename signal ("${rename[0]}") — deterministic script, no spawn` };
126
+ }
127
+ const codemod = CODEMOD_RE.exec(text);
128
+ if (codemod) {
129
+ return { matched: true, reason: `codemod signal ("${codemod[0]}") — deterministic script, no spawn` };
130
+ }
131
+ const formatter = FORMATTER_RUN_RE.exec(text);
132
+ if (formatter) {
133
+ return { matched: true, reason: `formatter-run signal ("${formatter[0]}") — deterministic script, no spawn` };
134
+ }
135
+ const searchReplace = SEARCH_REPLACE_RE.exec(text);
136
+ if (searchReplace) {
137
+ return {
138
+ matched: true,
139
+ reason: `search-and-replace/bulk-rename signal ("${searchReplace[0]}") — deterministic script, no spawn`,
140
+ };
141
+ }
142
+ return { matched: false, reason: 'no mechanical-transform signal' };
143
+ }
144
+
145
+ // ── Rung 1 — single bounded read-heavy slice (regex-only) ───────────────
146
+ //
147
+ // The one rung `classifyTask` structurally cannot see: its delegable rules
148
+ // all require >=2 slices, an ordered plan, or a declared `parallelizable`
149
+ // frontmatter value. A single "read this whole file and summarize it" task
150
+ // is real, non-trivial work that still benefits from a lite-tier subagent
151
+ // (session-context savings) but has no multi-slice shape at all. Requires a
152
+ // read-heavy VERB plus a bounded, singular TARGET phrase — a bare "read the
153
+ // code" without a stated target is too weak a signal to spawn on.
154
+ const SINGLE_SLICE_READHEAVY_RE =
155
+ /\b(?:read|review|summarize|summarise|inventory|audit|investigate|research|inspect|dig into|look into)\b[^.!?\n]{0,80}\b(?:this file|the file|this codebase|the codebase|this transcript|the transcript|this log|the log|this document|the document|this diff|the diff)\b/i;
156
+
157
+ export function detectBoundedReadHeavySlice(text: string): { matched: boolean; reason: string } {
158
+ const m = SINGLE_SLICE_READHEAVY_RE.exec(text);
159
+ return m
160
+ ? {
161
+ matched: true,
162
+ reason: `single bounded read-heavy slice ("${m[0]}") — one lite-tier subagent instead of session-context burn`,
163
+ }
164
+ : { matched: false, reason: 'no single-slice read-heavy signal' };
165
+ }
166
+
167
+ // ── Rung 3 — communication-need signals (team) ───────────────────────────
168
+ //
169
+ // "Slices that must communicate" per the committed table: cross-layer work,
170
+ // an in-flight review-with-challenge shape, or an explicit shared work
171
+ // queue — signals a single implementer-then-judge handoff, or a set of
172
+ // independent parallel slices, cannot express, because the workers need to
173
+ // see each other's state DURING the run, not just at handoff.
174
+ const CROSS_LAYER_RE =
175
+ /\bcross[- ]layer\b|\bcross[- ]cutting\b|\bback[- ]?end\s+and\s+front[- ]?end\b|\bfront[- ]?end\s+and\s+back[- ]?end\b/i;
176
+ const REVIEW_WITH_CHALLENGE_RE =
177
+ /\breview[- ]with[- ]challenge\b|\bchallenge[- ]and[- ]implement\b|\bimplementer\s+and\s+(?:a\s+)?(?:judge|reviewer)\s+(?:working\s+)?together\b|\bsecond\s+opinion\s+while\s+(?:building|implementing)\b/i;
178
+ const SHARED_TASK_LIST_RE =
179
+ /\bshared\s+(?:task\s+list|todo\s+list|backlog)\b|\bcoordinat(?:e|ed|ing)\s+(?:with|between|across)\s+(?:multiple\s+|several\s+)?(?:agents|workers|subagents|teammates)\b/i;
180
+
181
+ export function detectCommunicationNeed(text: string): { matched: boolean; reason: string } {
182
+ const crossLayer = CROSS_LAYER_RE.exec(text);
183
+ if (crossLayer) {
184
+ return { matched: true, reason: `cross-layer communication signal ("${crossLayer[0]}")` };
185
+ }
186
+ const reviewChallenge = REVIEW_WITH_CHALLENGE_RE.exec(text);
187
+ if (reviewChallenge) {
188
+ return { matched: true, reason: `review-with-challenge signal ("${reviewChallenge[0]}")` };
189
+ }
190
+ const sharedList = SHARED_TASK_LIST_RE.exec(text);
191
+ if (sharedList) {
192
+ return { matched: true, reason: `shared-task-list signal ("${sharedList[0]}")` };
193
+ }
194
+ return { matched: false, reason: 'no communication-need signal' };
195
+ }
196
+
197
+ // ── Rung 4 — contested-judgment signals (council) ────────────────────────
198
+ //
199
+ // "Judgment under disagreement" per the committed table: a design decision,
200
+ // a proposed security downgrade, or an escalation at a release gate — the
201
+ // three shapes the roadmap names explicitly. Deliberately its OWN, narrower
202
+ // vocabulary rather than an import of `ai_council/necessity.ts`'s broader
203
+ // `NECESSARY_TRIGGERS` (architecture/tradeoff/ambiguity/strategic): that
204
+ // module stays the council's own necessity gate; this resolver only needs
205
+ // the narrow signal that should route a TASK to rung 4 in the first place.
206
+ // A bare `\badr\b` (no decision-context anchor) matched "update the ADR
207
+ // index" and "fix the release-gate CI script typo" — routine maintenance
208
+ // prose that names the artefact/gate but decides nothing — and suppressed
209
+ // the correct rung-2 nudge on both. Both alternatives below now require an
210
+ // explicit decision/escalation VERB within 40 chars of the artefact/gate
211
+ // token, on either side, so the bare noun alone never fires.
212
+ const DESIGN_DECISION_RE =
213
+ /\bdesign\s+decision\b|\barchitectur(?:e|al)\s+decision\b|\b(?:record(?:ing)?|decide|deciding|decision|accept(?:ed|ing)?|approv(?:e|ed|ing|al)|challeng(?:e|ed|ing)|choos(?:e|ing)|reject(?:ed|ing)?)\b[^.!?\n]{0,40}\bADR\b|\bADR\b[^.!?\n]{0,40}\b(?:record(?:ing)?|decide|deciding|decision|accept(?:ed|ing)?|approv(?:e|ed|ing|al)|challeng(?:e|ed|ing)|choos(?:e|ing)|reject(?:ed|ing)?)\b/i;
214
+ const SECURITY_DOWNGRADE_RE = /\bsecurity\s+downgrade\b|\bdowngrade\s+(?:the\s+)?security\b|\bsecurity\s+exception\b/i;
215
+ const RELEASE_GATE_ESCALATION_RE =
216
+ /\bgate\s+escalation\b|\bescalat(?:e|ion)\s+to\s+(?:the\s+)?council\b|\b(?:escalat(?:e|ing|ion)|contest(?:ed|ing)?|dispute|disputing|override|overriding|waiv(?:e|ing)|bypass(?:ed|ing)?|challeng(?:e|ed|ing))\b[^.!?\n]{0,40}\brelease[- ]gate\b|\brelease[- ]gate\b[^.!?\n]{0,40}\b(?:escalat(?:e|ing|ion)|contest(?:ed|ing)?|dispute|disputing|override|overriding|waiv(?:e|ing)|bypass(?:ed|ing)?|challeng(?:e|ed|ing))\b/i;
217
+
218
+ export function detectContestedJudgment(text: string): { matched: boolean; reason: string } {
219
+ const design = DESIGN_DECISION_RE.exec(text);
220
+ if (design) {
221
+ return { matched: true, reason: `design-decision signal ("${design[0]}")` };
222
+ }
223
+ const security = SECURITY_DOWNGRADE_RE.exec(text);
224
+ if (security) {
225
+ return { matched: true, reason: `security-downgrade signal ("${security[0]}")` };
226
+ }
227
+ const releaseGate = RELEASE_GATE_ESCALATION_RE.exec(text);
228
+ if (releaseGate) {
229
+ return { matched: true, reason: `release-gate-escalation signal ("${releaseGate[0]}")` };
230
+ }
231
+ return { matched: false, reason: 'no contested-judgment signal' };
232
+ }
233
+
234
+ /**
235
+ * Resolve the judgment-ladder verdict for one task.
236
+ *
237
+ * FIXED priority order (the order is the contract):
238
+ *
239
+ * 1. `interactiveApprovalRequired` — caller-stated, overrides everything.
240
+ * 2. Rung 0 (`classifyLookup` primitive match OR a mechanical-transform
241
+ * signal) — never spawns, so neither the recursive guard nor the
242
+ * activation gate applies to it.
243
+ * 3. Recursive-dispatch guard (2.3) — `insideSubagentSession` resolves ∅
244
+ * for every rung below this point.
245
+ * 4. Activation gate (`halted` / no `subagent_spawn`) — nothing below this
246
+ * point can spawn without it.
247
+ * 5. Rung 4 (contested-judgment signal) — council.
248
+ * 6. Rung 3 (communication-need signal) — team when `agentTeams`; otherwise
249
+ * degrades to whatever `classifyTask` resolves on the SAME signals
250
+ * (usually rung 2 — slices that must communicate are usually plural
251
+ * slices — but this honestly follows `classifyTask`'s real verdict
252
+ * rather than fabricating a slice count nothing confirmed).
253
+ * 7. Rungs 1/2 — `classifyTask`'s dispatch verdict. `do-in-steps` and
254
+ * `do-in-parallel` both resolve rung 2 in this v1: the roadmap's rung
255
+ * 1-vs-2 split is single-slice vs multi-slice, and `classifyTask`
256
+ * structurally never dispatches on a single slice (its rules require
257
+ * >=2 slices, an ordered plan, or a declared `parallelizable` value) —
258
+ * so every `classifyTask` dispatch verdict is inherently multi-slice.
259
+ * Its `ask` / size-floor `in-session` outcomes get one more check
260
+ * first: rung 1's single-bounded-slice signal, the one shape
261
+ * `classifyTask`'s own rules cannot see.
262
+ *
263
+ * ## Recursive-dispatch guard — honest scope (2.3)
264
+ *
265
+ * No verified host discriminator exists for "is this classification running
266
+ * inside a subagent/teammate session". This roadmap's own
267
+ * `point-of-action-carrier` blocker records that the upstream PreToolUse
268
+ * agent-identity request is closed NOT_PLANNED, and nothing in this repo's
269
+ * hook envelope (`event` / `payload` / `platform` / `workspace_root` —
270
+ * `delegation_nudge_hook.ts`'s own `main()`) carries a session-lineage
271
+ * field. `insideSubagentSession` is therefore a CALLER-SUPPLIED fact: the
272
+ * code path that dispatched the subagent/teammate loop is the one place
273
+ * that genuinely knows. This resolver does NOT probe `process.env` for
274
+ * it — inventing an unverified `CLAUDE_AGENT_*`-shaped variable name here
275
+ * would be exactly the hallucinated-field failure `source-discovery-gate`
276
+ * exists to stop. Documented as a gap, not silently assumed away: on a
277
+ * host/caller that never sets this field, the guard is a no-op and rungs
278
+ * 1-4 resolve normally even inside a subagent — the same gap the
279
+ * `point-of-action-carrier` blocker already names as its pre-registered
280
+ * null ("no discriminator" is publishable and does not block this roadmap).
281
+ */
282
+ export function classifyLadder(inp: LadderInputs): LadderResult {
283
+ if (inp.interactiveApprovalRequired === true) {
284
+ return {
285
+ rung: null,
286
+ verdict: 'in-session',
287
+ reason: 'interactive-approval-required — caller-stated, needs a human decision before anything may run',
288
+ };
289
+ }
290
+
291
+ // Rung 0 — never spawns, so checked before the recursive guard and the
292
+ // activation gate: neither one is a precondition for a script that
293
+ // never dispatches anything.
294
+ const lookup = classifyLookup(inp.taskText);
295
+ if (lookup.route === 'primitive') {
296
+ return { rung: 0, verdict: 'script', reason: `lookup-class ${lookup.lookup_class} — ${lookup.reason}` };
297
+ }
298
+ const mechanical = detectMechanicalTransform(inp.taskText);
299
+ if (mechanical.matched) {
300
+ return { rung: 0, verdict: 'script', reason: mechanical.reason };
301
+ }
302
+
303
+ if (inp.insideSubagentSession === true) {
304
+ return {
305
+ rung: null,
306
+ verdict: 'in-session',
307
+ reason:
308
+ 'recursive-dispatch guard: this classification runs inside a subagent/teammate session — ' +
309
+ 'rungs 1-4 resolve in-session',
310
+ };
311
+ }
312
+
313
+ if (inp.activation.halted) {
314
+ return { rung: null, verdict: 'in-session', reason: 'emergency.orchestration_halt is set' };
315
+ }
316
+ if (!inp.activation.subagent_spawn) {
317
+ return { rung: null, verdict: 'in-session', reason: 'host has no subagent_spawn primitive' };
318
+ }
319
+
320
+ const contested = detectContestedJudgment(inp.taskText);
321
+ if (contested.matched) {
322
+ return { rung: 4, verdict: 'council', reason: contested.reason };
323
+ }
324
+
325
+ const communication = detectCommunicationNeed(inp.taskText);
326
+ if (communication.matched) {
327
+ if (inp.agentTeams) {
328
+ return { rung: 3, verdict: 'team', reason: communication.reason };
329
+ }
330
+ const fallback = classifyTask(inp.signals, inp.activation);
331
+ if (fallback.action === 'dispatch' && fallback.mode !== null) {
332
+ return {
333
+ rung: 2,
334
+ verdict: 'subagent',
335
+ mode: fallback.mode,
336
+ degraded_from: 3,
337
+ reason: `${communication.reason} — no agent_teams host capability, degraded to rung 2 (${fallback.reason})`,
338
+ };
339
+ }
340
+ return {
341
+ rung: null,
342
+ verdict: fallback.action === 'ask' ? 'ask' : 'in-session',
343
+ degraded_from: 3,
344
+ reason: `${communication.reason} — no agent_teams host capability, degraded below rung 2 (${fallback.reason})`,
345
+ };
346
+ }
347
+
348
+ const classification = classifyTask(inp.signals, inp.activation);
349
+ if (classification.action === 'dispatch' && classification.mode !== null) {
350
+ return { rung: 2, verdict: 'subagent', mode: classification.mode, reason: classification.reason };
351
+ }
352
+
353
+ // A task below the size floor never delegates — not even via rung 1's
354
+ // narrower single-slice regex. `classifyTask` already enforces this
355
+ // floor for rung 2 (its own `inSession('task below size floor …')`
356
+ // verdict); checked directly on `size_estimate` here — rather than by
357
+ // string-matching `classification.reason` — so this guard survives a
358
+ // future reword of that message, and so it fires regardless of WHICH of
359
+ // classifyTask's `in-session` branches produced the non-dispatch verdict.
360
+ // Before this check, a floor-worthy prompt like "review this diff" (no
361
+ // enumerated slices → size_estimate 0) still matched rung 1's read-heavy
362
+ // regex and dispatched a subagent anyway, silently bypassing the floor
363
+ // `classifyTask` had just enforced one line above.
364
+ if (inp.signals.size_estimate <= SIZE_FLOOR) {
365
+ return {
366
+ rung: null,
367
+ verdict: classification.action === 'ask' ? 'ask' : 'in-session',
368
+ reason: classification.reason,
369
+ };
370
+ }
371
+
372
+ const single = detectBoundedReadHeavySlice(inp.taskText);
373
+ if (single.matched) {
374
+ return { rung: 1, verdict: 'subagent', mode: null, reason: single.reason };
375
+ }
376
+
377
+ return {
378
+ rung: null,
379
+ verdict: classification.action === 'ask' ? 'ask' : 'in-session',
380
+ reason: classification.reason,
381
+ };
382
+ }
@@ -14,6 +14,14 @@
14
14
  *
15
15
  * This helper decides; it never runs the measurement. The signal is realized
16
16
  * orchestration telemetry, aggregated + evaluated out of band.
17
+ *
18
+ * **Amendment (road-to-always-on-orchestration Phase 1).** `subagents.auto`
19
+ * was DELETED as a settings key — always-on orchestration carries no
20
+ * per-layer on/off setting to demote. `resolveShippedDefault` (the
21
+ * `subagents.auto`-specific half below) has no production caller today and
22
+ * is kept as a historical record of the demotion mechanism, not a live
23
+ * decision path; `resolveRecursiveDefault` (the `verification.recursive`
24
+ * half) is unaffected by this deletion.
17
25
  */
18
26
 
19
27
  export interface BenchmarkResult {
@@ -28,6 +28,8 @@ export type TierSource = 'static' | 'inferred' | 'inherit';
28
28
  export type Band = 'low' | 'medium' | 'high';
29
29
  export type LinePhase = 'refine' | 'memory' | 'analyze' | 'plan' | 'implement' | 'test' | 'verify' | 'report';
30
30
  export type LineOutcome = 'success' | 'blocked' | 'skipped' | 'error';
31
+ /** Which capsule-emission trigger arm fired first (Phase 1 shadow comparison). */
32
+ export type TriggerArm = 'watermark' | 'saturation' | 'tie';
31
33
  /** The orchestration form the form-gate selected (road-to-opt-subagent-harvest P2). */
32
34
  export type DispatchModeId =
33
35
  | 'do-and-judge'
@@ -96,6 +98,20 @@ export interface RecordInput {
96
98
  /** Rules the worker actually applied/cited (count of the envelope's
97
99
  * `rules_applied`-equivalent on the worker side). Counts only. */
98
100
  rules_used?: number | null | undefined;
101
+ // ── capsule shadow-measurement fields (road-to-worker-generation-recycling
102
+ // Phase 1.2). SHADOW ONLY: the worker still runs to stop-loss, nothing
103
+ // reads a capsule. All counts / enums — a capsule's CONTENT never
104
+ // reaches telemetry, only its shape. ──
105
+ /** A CHECKPOINT capsule was emitted at the watermark. */
106
+ capsule_emitted?: boolean | undefined;
107
+ /** Total entries across the capsule's arrays — a size proxy, never content. */
108
+ capsule_entries?: number | null | undefined;
109
+ /** 1-based step at which the token-watermark arm fired (null = never). */
110
+ watermark_step?: number | null | undefined;
111
+ /** 1-based step at which the novelty-saturation arm fired (null = never). */
112
+ saturation_step?: number | null | undefined;
113
+ /** Which arm fired first on this dispatch — the paired-comparison datum. */
114
+ trigger_arm_earlier?: TriggerArm | null | undefined;
99
115
  // Audit-log envelope (sensible defaults for a dispatch record)
100
116
  phase?: LinePhase | undefined;
101
117
  outcome?: LineOutcome | undefined;
@@ -123,6 +139,7 @@ const BANDS: readonly Band[] = ['low', 'medium', 'high'];
123
139
  const PHASES: readonly LinePhase[] = ['refine', 'memory', 'analyze', 'plan', 'implement', 'test', 'verify', 'report'];
124
140
  const LOOKUP_CLASSES: readonly LookupClass[] = ['definition', 'references', 'string-existence', 'report-run'];
125
141
  const ROUTES_TAKEN: readonly RouteTaken[] = ['primitive', 'subagent'];
142
+ const TRIGGER_ARMS: readonly TriggerArm[] = ['watermark', 'saturation', 'tie'];
126
143
  /** Hex-only payload hash — a hash can never smuggle content (privacy by construction). */
127
144
  const PAYLOAD_HASH_RE = /^[a-f0-9]{8,64}$/i;
128
145
  /** Id-shaped origin tag — enum-ish, never free-form prose. */
@@ -233,6 +250,20 @@ export function buildOrchestrationLine(input: RecordInput): BuiltLine {
233
250
  errors.push('rules_used cannot exceed rules_carried (a worker cannot apply a rule it was not given)');
234
251
  }
235
252
 
253
+ if (input.capsule_emitted !== undefined && typeof input.capsule_emitted !== 'boolean') {
254
+ errors.push('capsule_emitted must be a boolean or omitted');
255
+ }
256
+ for (const [key, v] of [
257
+ ['capsule_entries', input.capsule_entries],
258
+ ['watermark_step', input.watermark_step],
259
+ ['saturation_step', input.saturation_step],
260
+ ] as const) {
261
+ if (v != null && (!isInt(v) || v < 0)) errors.push(`${key} must be a non-negative integer count`);
262
+ }
263
+ if (input.trigger_arm_earlier != null && !TRIGGER_ARMS.includes(input.trigger_arm_earlier)) {
264
+ errors.push(`trigger_arm_earlier must be one of ${TRIGGER_ARMS.join(' | ')} (or omitted / null when neither arm fired)`);
265
+ }
266
+
236
267
  if (!input.ts) errors.push('ts (ISO-8601 UTC) is required');
237
268
  if (!input.id) errors.push('id (ULID or content hash) is required');
238
269
 
@@ -267,6 +298,12 @@ export function buildOrchestrationLine(input: RecordInput): BuiltLine {
267
298
  origin: input.origin ?? null,
268
299
  rules_carried: input.rules_carried ?? null,
269
300
  rules_used: input.rules_used ?? null,
301
+ // capsule shadow-measurement — readers ignore unknowns per audit-log-v1
302
+ capsule_emitted: input.capsule_emitted ?? null,
303
+ capsule_entries: input.capsule_entries ?? null,
304
+ watermark_step: input.watermark_step ?? null,
305
+ saturation_step: input.saturation_step ?? null,
306
+ trigger_arm_earlier: input.trigger_arm_earlier ?? null,
270
307
  };
271
308
 
272
309
  const line: Record<string, unknown> = {
@@ -212,7 +212,7 @@ export function aggregateOrchestrationSavings(
212
212
 
213
213
  // Honest caveats — always attached so a consumer never over-reads the number.
214
214
  if (report.dispatches === 0) {
215
- report.notes.push('No orchestration telemetry yet (0 dispatches). Data accrues as `subagents.auto: on` delegates real slices (recorded via orchestration_record).');
215
+ report.notes.push('No orchestration telemetry yet (0 dispatches). Data accrues as delegable slices dispatch (always-on orchestration; recorded via orchestration_record).');
216
216
  } else {
217
217
  report.notes.push('Token counts are MEASURED but blind to the downshift rate win (same tokens on a cheaper tier ≈ same count). The MODELED cost-% below captures that axis.');
218
218
  if (report.by_provenance.estimated.dispatches > 0) {
@@ -0,0 +1,88 @@
1
+ /**
2
+ * What a "prompt" actually is — shared by the language hook and the conformance
3
+ * scanner so the two cannot classify the same entry differently.
4
+ *
5
+ * They did. Round 6 measured it: `isSyntheticPrompt` lived in
6
+ * `language_mirror_hook` and was never referenced from `conformance_scan`, whose
7
+ * only general net was `length > 2500 && english`. So the hook skipped
8
+ * harness-generated turns and the scanner counted them, and every era split
9
+ * argued about two different populations without knowing it.
10
+ *
11
+ * The asymmetry ran the other way too, and that half had no net on either side:
12
+ * a prompt whose bulk is pasted content in another language pinned to the paste.
13
+ * That fired on the very session that received the round-6 review — the pin read
14
+ * English because an English draft was pasted below German prose.
15
+ *
16
+ * Nothing here is language-specific. A rule that names a language cannot answer
17
+ * a bidirectional question.
18
+ */
19
+
20
+ /**
21
+ * Is this "prompt" a harness-generated turn rather than something a human typed?
22
+ *
23
+ * MEASURED, IN THE LANGUAGE HOOK'S OWN AUDIT (round 5, 2026-08-07). The state
24
+ * file read mid-session:
25
+ *
26
+ * { "language": "en", "source": "prompt", "prompt_chars": 6627,
27
+ * "de_markers": 0, "en_markers": 63 }
28
+ *
29
+ * No human wrote those 6,627 characters. They were a background-task completion
30
+ * notification, injected as a user turn, which `user_prompt_submit` sees and the
31
+ * hook classified as the trigger — flipping a German session to `en` for every
32
+ * later turn, and recording `source: "prompt"`, a false provenance claim.
33
+ *
34
+ * The markers are structural, not linguistic: the harness stamps these turns
35
+ * with fixed envelope tags. Matching prose would misfire on a human quoting a
36
+ * notification; matching the tags cannot, because a human typing
37
+ * `<task-notification>` is quoting, and a quote does not open at character zero.
38
+ *
39
+ * Deliberately conservative: an unrecognised synthetic shape falls through and
40
+ * is treated as a chat message. Under-filtering keeps prior behaviour;
41
+ * over-filtering would silently stop pinning real prompts, which is worse.
42
+ * Measured across 1 540 user-role entries: 509 open at character zero with a
43
+ * marker, and in 0 of them does human text survive wrapper removal.
44
+ */
45
+ export function isSyntheticPrompt(prompt: string): boolean {
46
+ const head = prompt.trimStart().slice(0, 400);
47
+ return (
48
+ head.startsWith("[SYSTEM NOTIFICATION - NOT USER INPUT]") ||
49
+ head.startsWith("<task-notification>") ||
50
+ head.startsWith("<system-reminder>") ||
51
+ head.startsWith("<local-command-caveat>") ||
52
+ /^\[SYSTEM NOTIFICATION\b/.test(head)
53
+ );
54
+ }
55
+
56
+ /**
57
+ * A line that starts pasted DOCUMENT content rather than typed instruction.
58
+ *
59
+ * Distinct from the output-shaped heads `instructionText` already removes (stack
60
+ * traces, diffs, `remote:` lines). Those look like machine output. This catches
61
+ * the other paste shape, which looks like prose: a roadmap, a review, a spec —
62
+ * pasted whole, under its own headings, below one typed sentence.
63
+ *
64
+ * The markers are structural for the same reason as above: a heading, a
65
+ * frontmatter fence, or a bare tag line is document furniture. A typed chat turn
66
+ * rarely carries one; a pasted document essentially always does.
67
+ */
68
+ const DOCUMENT_HEAD = /^\s*(#{1,6}\s|-{3,}\s*$|={3,}\s*$|<[A-Za-z][\w-]*>\s*$)/;
69
+
70
+ /**
71
+ * The part of a prompt a human plausibly typed: everything before the first
72
+ * pasted document begins.
73
+ *
74
+ * Callers pass text that has already had output-shaped pastes removed. Returning
75
+ * the LEAD rather than a filtered whole is deliberate — a paste can be
76
+ * interleaved, and reassembling the human fragments would invent a text nobody
77
+ * wrote. The lead is the one span whose authorship is not in question.
78
+ */
79
+ export function humanAuthoredLead(text: string): string {
80
+ const kept: string[] = [];
81
+ for (const line of text.split("\n")) {
82
+ if (DOCUMENT_HEAD.test(line)) {
83
+ break;
84
+ }
85
+ kept.push(line);
86
+ }
87
+ return kept.join("\n");
88
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * `review_skipped` telemetry line builder (pure, no I/O).
3
+ *
4
+ * Purpose-built, MINIMAL extension of the audit-log-v1 envelope
5
+ * (`docs/contracts/audit-log-v1.md`) for the F4-lite end-review-nudge concern
6
+ * (road-to-orchestrator-discipline-carriers Phase 5). It reuses the same
7
+ * envelope shape and the same file location
8
+ * (`agents/runtime/state/audit/<YYYY-MM>.jsonl`) as
9
+ * `_lib/orchestration_record.ts`'s `orchestration` object, but does not
10
+ * import that module: `buildOrchestrationLine` requires `spawn_count`, which
11
+ * has no meaning for a review-skip event, and its enum-constant tables are
12
+ * private to that file. Adding a second additive top-level object
13
+ * (`review_skipped`) is exactly the pattern the contract already uses for
14
+ * `orchestration` — "Unknown trailing fields are forward-compat extensions;
15
+ * readers MUST NOT raise on them."
16
+ *
17
+ * PRIVACY BY CONSTRUCTION: the `review_skipped` object carries `diff_lines`
18
+ * (a non-negative integer count) and `mutation_measure` (a closed 2-value
19
+ * enum — never free text). There is no field capable of holding a file
20
+ * path, a prompt, or any free-form content — never add one (mirrors
21
+ * `domain-safety-pii` § Surface 2 / the `artifact-engagement` event shape).
22
+ */
23
+
24
+ /**
25
+ * Whether `diff_lines` is an exact sum or a guaranteed-over-threshold
26
+ * approximation (`end_review_nudge_hook.ts`'s `UNTRACKED_FILE_CAP` path —
27
+ * past that many untracked non-doc files, the hook refuses to spawn one
28
+ * `git diff --no-index` subprocess per file and reports a value merely
29
+ * guaranteed to exceed the fire threshold, not an exact count). Mixing
30
+ * exact and approximated counts in this telemetry stream, unlabeled, would
31
+ * corrupt any future blocking-threshold calibration that reads it — hence
32
+ * this field, rather than folding the approximation silently into
33
+ * `diff_lines`.
34
+ */
35
+ export type MutationMeasure = 'exact' | 'capped_approximation';
36
+
37
+ export interface ReviewSkippedInput {
38
+ /** Non-doc tracked-file mutation size that triggered the nudge (count only). */
39
+ diff_lines: number;
40
+ /** Whether `diff_lines` is exact or a capped approximation — see above. */
41
+ mutation_measure: MutationMeasure;
42
+ /** ISO-8601 UTC timestamp; caller supplies (keeps this fn pure/deterministic). */
43
+ ts: string;
44
+ /** Stable id (ULID, UUID, or content hash); caller supplies. */
45
+ id: string;
46
+ work_id?: string | undefined;
47
+ }
48
+
49
+ export interface BuiltReviewSkippedLine {
50
+ line: Record<string, unknown> | null;
51
+ errors: string[];
52
+ }
53
+
54
+ function isNonNegInt(n: unknown): n is number {
55
+ return typeof n === 'number' && Number.isFinite(n) && Number.isInteger(n) && n >= 0;
56
+ }
57
+
58
+ /**
59
+ * High-risk threshold for the line's own `risk_class` (audit-log-v1 field,
60
+ * not the hook's fire threshold): a skipped review over a large diff carries
61
+ * more residual risk than one just past the fire threshold.
62
+ */
63
+ const HIGH_RISK_DIFF_LINES = 200;
64
+
65
+ /**
66
+ * Build + validate ONE audit-log-v1 line carrying a `review_skipped` object.
67
+ * Returns `{line, errors}`; when `errors` is non-empty, `line` is null and
68
+ * the caller must NOT write.
69
+ */
70
+ export function buildReviewSkippedLine(input: ReviewSkippedInput): BuiltReviewSkippedLine {
71
+ const errors: string[] = [];
72
+ if (!isNonNegInt(input.diff_lines)) {
73
+ errors.push('diff_lines must be a non-negative integer');
74
+ }
75
+ if (input.mutation_measure !== 'exact' && input.mutation_measure !== 'capped_approximation') {
76
+ errors.push('mutation_measure must be "exact" or "capped_approximation"');
77
+ }
78
+ if (!input.ts) errors.push('ts (ISO-8601 UTC) is required');
79
+ if (!input.id) errors.push('id (ULID, UUID, or content hash) is required');
80
+
81
+ if (errors.length) return { line: null, errors };
82
+
83
+ const line: Record<string, unknown> = {
84
+ schema_version: 1,
85
+ id: input.id,
86
+ ts: input.ts,
87
+ work_id: input.work_id ?? `review-skipped-${input.ts}`,
88
+ phase: 'report',
89
+ outcome: 'skipped',
90
+ confidence_band: 'medium',
91
+ risk_class: input.diff_lines >= HIGH_RISK_DIFF_LINES ? 'high' : 'medium',
92
+ memory: { asks: 0, hits: 0 },
93
+ verify: { claims: 0, first_try_passes: 0 },
94
+ rules_applied: ['delegation-policy'],
95
+ persona: null,
96
+ input_kind: 'prompt',
97
+ type: 'note',
98
+ review_skipped: { diff_lines: input.diff_lines, mutation_measure: input.mutation_measure },
99
+ };
100
+
101
+ return { line, errors: [] };
102
+ }