@event4u/agent-config 14.7.0 → 14.9.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 (230) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +175 -390
  3. package/README.md +1 -1
  4. package/agents/templates/.ai-council.yml.example +34 -4
  5. package/dist/agent-src/commands/evals/git-pr-merge.json +13 -0
  6. package/dist/agent-src/commands/evals/roadmap.json +17 -1
  7. package/dist/agent-src/commands/pr/create.md +14 -2
  8. package/dist/agent-src/commands/pr/merge.md +383 -0
  9. package/dist/agent-src/commands/review/changes.md +95 -14
  10. package/dist/agent-src/commands/roadmap/process-full.md +157 -3
  11. package/dist/agent-src/commands/roadmap.md +1 -1
  12. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +46 -36
  13. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +37 -2
  14. package/dist/agent-src/contexts/execution/host-capability-manifest.md +77 -0
  15. package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +100 -2
  16. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +154 -2
  17. package/dist/agent-src/contexts/execution/subagent-response-contract.md +80 -0
  18. package/dist/agent-src/rules/artifact-drafting-protocol.md +5 -1
  19. package/dist/agent-src/rules/decision-revisit-gate.md +65 -33
  20. package/dist/agent-src/rules/recurring-criticism.md +1 -1
  21. package/dist/agent-src/rules/roadmap-progress-sync.md +19 -0
  22. package/dist/agent-src/rules/source-confidentiality.md +1 -1
  23. package/dist/agent-src/rules/untrusted-input-defense.md +9 -3
  24. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +7 -4
  25. package/dist/agent-src/scripts/dashboard_mode.ts +212 -0
  26. package/dist/agent-src/scripts/update_roadmap_progress.ts +47 -48
  27. package/dist/agent-src/skills/adr-create/SKILL.md +124 -5
  28. package/dist/agent-src/skills/code-review/evals/evals.json +87 -46
  29. package/dist/agent-src/skills/decision-record/SKILL.md +61 -3
  30. package/dist/agent-src/skills/decision-review/SKILL.md +65 -0
  31. package/dist/agent-src/skills/humanizer/SKILL.md +39 -0
  32. package/dist/agent-src/skills/humanizer/evals/strip_fixtures.json +57 -0
  33. package/dist/agent-src/skills/humanizer/references/fixtures.md +54 -0
  34. package/dist/agent-src/skills/judge-spec-compliance/SKILL.md +154 -0
  35. package/dist/agent-src/skills/judge-synthesis/SKILL.md +45 -0
  36. package/dist/agent-src/skills/merge-conflicts/SKILL.md +26 -0
  37. package/dist/agent-src/skills/roadmap-writing/SKILL.md +18 -18
  38. package/dist/agent-src/skills/security/SKILL.md +8 -0
  39. package/dist/agent-src/skills/security-audit/SKILL.md +12 -1
  40. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  41. package/dist/agent-src/templates/roadmaps.md +28 -2
  42. package/dist/cli/registry.js +3 -0
  43. package/dist/cli/registry.js.map +1 -1
  44. package/dist/cli-delegate/{chunk-FBKL6JWT.js → chunk-77GR3VD4.js} +68 -43
  45. package/dist/cli-delegate/{chunk-JZ4NR2TW.js → chunk-7PRE6ZV2.js} +1 -1
  46. package/dist/cli-delegate/{chunk-UEZ7D35A.js → chunk-CYH5SP6M.js} +4 -23
  47. package/dist/cli-delegate/{chunk-Q26HT4QT.js → chunk-E46KPN6K.js} +251 -150
  48. package/dist/cli-delegate/{chunk-74E3TKWV.js → chunk-H4VIVD4C.js} +5 -5
  49. package/dist/cli-delegate/chunk-I3EIN2YG.js +36 -0
  50. package/dist/cli-delegate/{chunk-4VR4ORQ6.js → chunk-LPPWO5CE.js} +4 -3
  51. package/dist/cli-delegate/{chunk-7F3VYSM2.js → chunk-PLSG6UL5.js} +14 -14
  52. package/dist/cli-delegate/cmd_adr_effective.js +1162 -0
  53. package/dist/cli-delegate/cmd_conformance.js +101 -25
  54. package/dist/cli-delegate/cmd_converge.js +3 -3
  55. package/dist/cli-delegate/cmd_doctor.js +7 -7
  56. package/dist/cli-delegate/cmd_export.js +6 -6
  57. package/dist/cli-delegate/cmd_handoff.js +7 -6
  58. package/dist/cli-delegate/cmd_memory_get.js +2 -1
  59. package/dist/cli-delegate/cmd_migrate.js +2 -2
  60. package/dist/cli-delegate/cmd_preflight.js +5 -5
  61. package/dist/cli-delegate/cmd_prune.js +6 -6
  62. package/dist/cli-delegate/cmd_route_audit.js +6 -5
  63. package/dist/cli-delegate/cmd_route_explain.js +3 -3
  64. package/dist/cli-delegate/cmd_session_recycle.js +3 -3
  65. package/dist/cli-delegate/cmd_settings_get.js +4 -4
  66. package/dist/cli-delegate/cmd_settings_migrate.js +6 -6
  67. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  68. package/dist/cli-delegate/cmd_sync.js +6 -6
  69. package/dist/cli-delegate/cmd_uninstall.js +6 -6
  70. package/dist/cli-delegate/cmd_update.js +3 -3
  71. package/dist/cli-delegate/cmd_upgrade.js +3 -3
  72. package/dist/cli-delegate/cmd_validate.js +6 -6
  73. package/dist/cli-delegate/cmd_versions.js +3 -3
  74. package/dist/discovery/deprecation-report.md +1 -1
  75. package/dist/discovery/discovery-manifest.json +83 -30
  76. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  77. package/dist/discovery/discovery-manifest.summary.md +4 -4
  78. package/dist/discovery/dormancy-report.md +1 -1
  79. package/dist/discovery/orphan-report.md +1 -1
  80. package/dist/discovery/packs.json +11 -9
  81. package/dist/discovery/trust-report.md +2 -2
  82. package/dist/discovery/workspaces.json +9 -7
  83. package/dist/hooks/dispatch.js +1295 -812
  84. package/dist/install/globalRuleLayers.js +169 -0
  85. package/dist/install/globalRuleLayers.js.map +1 -0
  86. package/dist/install/partitionEligibility.js +156 -0
  87. package/dist/install/partitionEligibility.js.map +1 -1
  88. package/dist/install/ruleLayerPartition.js +157 -0
  89. package/dist/install/ruleLayerPartition.js.map +1 -0
  90. package/dist/mcp/registry-manifest.json +3 -3
  91. package/dist/mcp/server.json +2 -2
  92. package/dist/mcp/server.mjs +1358 -805
  93. package/dist/scripts/_lib/claude_plugin.js +162 -0
  94. package/dist/scripts/_lib/claude_plugin.js.map +1 -0
  95. package/dist/scripts/_lib/environment_detector.js +397 -0
  96. package/dist/scripts/_lib/environment_detector.js.map +1 -0
  97. package/dist/scripts/_lib/prune_empty_dirs.js +85 -0
  98. package/dist/scripts/_lib/prune_empty_dirs.js.map +1 -0
  99. package/dist/scripts/_lib/spawn_env.js +129 -0
  100. package/dist/scripts/_lib/spawn_env.js.map +1 -0
  101. package/dist/scripts/ai_council/modes.js +199 -0
  102. package/dist/scripts/ai_council/modes.js.map +1 -0
  103. package/dist/scripts/ai_council/transport_resolver.js +418 -0
  104. package/dist/scripts/ai_council/transport_resolver.js.map +1 -0
  105. package/dist/scripts/condense.js +52 -89
  106. package/dist/scripts/condense.js.map +1 -1
  107. package/dist/server/routes/wizard.js +3 -2
  108. package/dist/server/routes/wizard.js.map +1 -1
  109. package/dist/ui/assets/index-Cxgy63kz.js +26 -0
  110. package/dist/ui/assets/index-Cxgy63kz.js.map +1 -0
  111. package/dist/ui/index.html +1 -1
  112. package/docs/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +45 -1
  113. package/docs/guidelines/agent-infra/false-green.md +47 -1
  114. package/docs/guidelines/agent-infra/traceability-field-mechanics.md +50 -0
  115. package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +17 -0
  116. package/package.json +7 -3
  117. package/src/agent-src/scripts/archive_completed_roadmaps.ts +7 -4
  118. package/src/agent-src/scripts/dashboard_mode.ts +212 -0
  119. package/src/agent-src/scripts/update_roadmap_progress.ts +47 -48
  120. package/src/config/ci-local-parity.yml +21 -0
  121. package/src/config/estate-count-budget.json +13 -471
  122. package/src/config/evaluator-budgets.json +4 -4
  123. package/src/config/gate-coverage.yml +458 -12
  124. package/src/config/gate-violation-baselines.json +18 -8
  125. package/src/config/gitignore-block.txt +15 -2
  126. package/src/config/hook-latency-budget.json +5 -0
  127. package/src/config/publish-surface.json +60 -0
  128. package/src/config/requirements-trace-budget.json +32 -0
  129. package/src/config/rule-stub-ceilings.json +25 -4
  130. package/src/install/globalRuleLayers.ts +197 -0
  131. package/src/install/partitionEligibility.ts +177 -0
  132. package/src/install/ruleLayerPartition.ts +212 -0
  133. package/src/scripts/_cli/cmd_adr_effective.ts +484 -0
  134. package/src/scripts/_dispatch.bash +18 -0
  135. package/src/scripts/_lib/adr_frontmatter.test.ts +365 -0
  136. package/src/scripts/_lib/adr_frontmatter.ts +459 -0
  137. package/src/scripts/_lib/base_tree.ts +175 -0
  138. package/src/scripts/_lib/billing_grant.ts +165 -0
  139. package/src/scripts/_lib/billing_grant_cli.ts +123 -0
  140. package/src/scripts/_lib/council_fallback_posture.ts +53 -6
  141. package/src/scripts/_lib/council_fallback_wiring.ts +61 -1
  142. package/src/scripts/_lib/envelope_grounding.ts +11 -1
  143. package/src/scripts/_lib/host_capability.ts +59 -5
  144. package/src/scripts/_lib/layer_overlap_notice.ts +11 -2
  145. package/src/scripts/_lib/prune_empty_dirs.ts +80 -0
  146. package/src/scripts/_lib/quota_parked.ts +133 -0
  147. package/src/scripts/_lib/retrieval_sanitize.ts +62 -0
  148. package/src/scripts/_lib/review_telemetry.ts +196 -0
  149. package/src/scripts/_lib/subagent_response.ts +92 -0
  150. package/src/scripts/adr/evidence_census.ts +899 -0
  151. package/src/scripts/adr/regenerate_index.ts +25 -38
  152. package/src/scripts/adr_cite_check.ts +540 -39
  153. package/src/scripts/ai_council/clients.ts +12 -1
  154. package/src/scripts/ai_council/config.ts +72 -115
  155. package/src/scripts/ai_council/fallback_config.ts +19 -7
  156. package/src/scripts/ai_council/mid_flight_fallback.ts +88 -7
  157. package/src/scripts/ai_council/orchestrator.ts +2 -2
  158. package/src/scripts/ai_council/py_format.ts +124 -0
  159. package/src/scripts/ai_council/transport_resolver.ts +65 -2
  160. package/src/scripts/audit_adr_coverage.ts +134 -21
  161. package/src/scripts/bench_hook_latency.ts +204 -0
  162. package/src/scripts/build_archive_index.ts +34 -0
  163. package/src/scripts/check_adr_frontmatter.ts +574 -39
  164. package/src/scripts/check_council_pin_staleness.ts +300 -0
  165. package/src/scripts/check_estate_count.ts +420 -293
  166. package/src/scripts/check_gate_coverage.ts +91 -0
  167. package/src/scripts/check_hook_bundle_content.ts +219 -0
  168. package/src/scripts/check_new_adr_evidence.ts +529 -0
  169. package/src/scripts/check_no_stub_inventory_table.ts +292 -0
  170. package/src/scripts/check_pack_size.ts +234 -1
  171. package/src/scripts/check_publish_surface.ts +297 -0
  172. package/src/scripts/check_references.ts +18 -0
  173. package/src/scripts/check_requirements_trace.ts +378 -0
  174. package/src/scripts/check_rule_layer_partition.ts +531 -0
  175. package/src/scripts/check_secret_leak.ts +166 -12
  176. package/src/scripts/check_single_delivery.ts +137 -4
  177. package/src/scripts/check_standing_rule_delivery.ts +8 -2
  178. package/src/scripts/code_graph/cli.ts +44 -5
  179. package/src/scripts/code_graph/extract.ts +87 -0
  180. package/src/scripts/condense.ts +61 -91
  181. package/src/scripts/council_cli.ts +12 -16
  182. package/src/scripts/detect_ai_tells.ts +87 -0
  183. package/src/scripts/dispatch_r2_reviewer.ts +42 -0
  184. package/src/scripts/external_sources_denylist.json +4 -1
  185. package/src/scripts/generate_capabilities_index.ts +41 -0
  186. package/src/scripts/git_authorization_hook.ts +54 -4
  187. package/src/scripts/hooks/block_unauthorized_git.ts +41 -3
  188. package/src/scripts/hooks/dispatch_hook.ts +154 -187
  189. package/src/scripts/hooks/dispatch_issues.ts +38 -19
  190. package/src/scripts/hooks/fallback_yaml.ts +66 -0
  191. package/src/scripts/hooks/host_semantics.ts +21 -0
  192. package/src/scripts/hooks/injection_budget.ts +13 -3
  193. package/src/scripts/hooks/py_json_dumps.ts +103 -0
  194. package/src/scripts/hooks/run_continuation_hook.ts +3 -1
  195. package/src/scripts/hooks/state_io.ts +88 -1
  196. package/src/scripts/hooks/stdin_failure_policy.ts +96 -0
  197. package/src/scripts/hooks/subagent_ledger_hook.ts +76 -11
  198. package/src/scripts/injection_scan_hook.ts +257 -15
  199. package/src/scripts/lint_adr_sweep_routing.ts +365 -0
  200. package/src/scripts/lint_hook_manifest.ts +49 -0
  201. package/src/scripts/lint_provenance_vocabulary.ts +472 -4
  202. package/src/scripts/lint_workflow_security.ts +53 -10
  203. package/src/scripts/lint_workflow_security_allowlist.json +2 -18
  204. package/src/scripts/report_envelope_rate.ts +166 -0
  205. package/src/scripts/review_axis_report.ts +218 -0
  206. package/src/scripts/routing_doctor.ts +5 -1
  207. package/src/scripts/run_supervise.ts +46 -1
  208. package/src/scripts/schemas/evals.schema.json +5 -0
  209. package/src/scripts/sync_pr_branch.ts +170 -20
  210. package/dist/cli/commands/commands.candidates.test.js +0 -257
  211. package/dist/cli/commands/commands.candidates.test.js.map +0 -1
  212. package/dist/cli/commands/profiles.test.js +0 -88
  213. package/dist/cli/commands/profiles.test.js.map +0 -1
  214. package/dist/cli/commands/recordTriggerEval.test.js +0 -113
  215. package/dist/cli/commands/recordTriggerEval.test.js.map +0 -1
  216. package/dist/cli/configRoot.test.js +0 -97
  217. package/dist/cli/configRoot.test.js.map +0 -1
  218. package/dist/cli/mcp/content.test.js +0 -54
  219. package/dist/cli/mcp/content.test.js.map +0 -1
  220. package/dist/cli/mcp/dispatch.test.js +0 -108
  221. package/dist/cli/mcp/dispatch.test.js.map +0 -1
  222. package/dist/server/writeRoot.test.js +0 -91
  223. package/dist/server/writeRoot.test.js.map +0 -1
  224. package/dist/shared/capabilities.test.js +0 -39
  225. package/dist/shared/capabilities.test.js.map +0 -1
  226. package/dist/ui/assets/index-C1pBRm1f.js +0 -26
  227. package/dist/ui/assets/index-C1pBRm1f.js.map +0 -1
  228. package/dist/cli-delegate/{chunk-PPQO4NGR.js → chunk-6RJA2F5A.js} +3 -3
  229. package/dist/cli-delegate/{chunk-6EOCNQAM.js → chunk-E4XIZDIB.js} +0 -0
  230. package/dist/cli-delegate/{chunk-GJ5BVHXM.js → chunk-PMEKGDED.js} +3 -3
@@ -0,0 +1,133 @@
1
+ /**
2
+ * quota_parked — the marker that says a run stopped because plan quota ran out,
3
+ * rather than because it crashed.
4
+ *
5
+ * `run_supervise` can already tell a dead session from a live one, and a
6
+ * finished run from an unfinished one. What it cannot tell is WHY a run
7
+ * stopped, and for one cause that distinction decides the whole response: a run
8
+ * held back by an exhausted plan quota is not broken, it is early. Reported as
9
+ * `relaunchable` with "the session is gone" it reads as a crash, and the one
10
+ * fact an operator needs — that waiting is a strategy here and is not a
11
+ * strategy for a crash — is the fact that gets lost.
12
+ *
13
+ * **The trigger is ours, and that is the point.** Detecting the billing cliff
14
+ * inside Claude Code needs a host signal nobody has shown exists
15
+ * (`agents/roadmaps/later/road-to-billing-cliff-detection.md`). But the council
16
+ * establishes the same fact under its own roof: when `api_on_quota: 'ask'`
17
+ * parks a seat, plan quota for that provider is exhausted, measured by us, with
18
+ * no host surface involved. This marker is written from there.
19
+ *
20
+ * PII-exclusion-by-construction, the same shape as `billing_grant`: a record
21
+ * holds a run id, provider names, and an ISO stamp. There is no field capable
22
+ * of carrying a prompt, a path, or an error body, so there is no scrubbing pass
23
+ * here that could fail.
24
+ *
25
+ * **It records, it does not act.** Nothing here sleeps, probes, or relaunches.
26
+ * Unattended relaunch is a published refusal
27
+ * (road-to-long-horizon-execution 4.0, AI council 2026-08-19) whose reopen
28
+ * condition has since fired — see
29
+ * `agents/evidence/billing-cliff/spawn-reopen-condition.md` — and a fired
30
+ * condition reopens the question in the venue that closed it, never the
31
+ * capability by itself.
32
+ */
33
+
34
+ import * as fs from 'node:fs';
35
+ import * as path from 'node:path';
36
+
37
+ /** Directory, relative to the repo root, holding one marker per parked run. */
38
+ export const QUOTA_PARKED_DIR_REL = path.join('agents', 'runtime', 'state', 'quota-parked');
39
+
40
+ export interface QuotaParkedMarker {
41
+ readonly run_id: string;
42
+ /** Providers whose plan quota was exhausted, sorted and deduplicated. */
43
+ readonly providers: string[];
44
+ readonly parked_at: string;
45
+ }
46
+
47
+ /** Filesystem-safe path for a run's marker. Mirrors `billing_grant.grantFile`. */
48
+ export function markerFile(repoRoot: string, runId: string): string {
49
+ return path.join(
50
+ repoRoot,
51
+ QUOTA_PARKED_DIR_REL,
52
+ `${runId.replace(/[^A-Za-z0-9_-]/g, '_')}.json`,
53
+ );
54
+ }
55
+
56
+ /**
57
+ * Record that `runId` is held back by exhausted plan quota on `providers`.
58
+ *
59
+ * Idempotent on the timestamp: a second park inside one run keeps the original
60
+ * `parked_at` and merges the provider set, so "when did this run first hit the
61
+ * cliff" survives a round that parks twice.
62
+ */
63
+ export function writeQuotaParked(
64
+ repoRoot: string,
65
+ runId: string,
66
+ providers: readonly string[],
67
+ now: () => Date = () => new Date(),
68
+ ): QuotaParkedMarker {
69
+ const file = markerFile(repoRoot, runId);
70
+ const existing = readMarker(file);
71
+ const merged = [...new Set([...(existing?.providers ?? []), ...providers])].sort();
72
+ const marker: QuotaParkedMarker = {
73
+ run_id: runId,
74
+ providers: merged,
75
+ parked_at: existing?.parked_at ?? now().toISOString(),
76
+ };
77
+ fs.mkdirSync(path.dirname(file), { recursive: true });
78
+ fs.writeFileSync(file, `${JSON.stringify(marker, null, 2)}\n`, 'utf8');
79
+ return marker;
80
+ }
81
+
82
+ /** Remove a run's marker. Safe to call when none exists. */
83
+ export function clearQuotaParked(repoRoot: string, runId: string): void {
84
+ try {
85
+ fs.unlinkSync(markerFile(repoRoot, runId));
86
+ } catch {
87
+ // Already gone is the state the caller asked for.
88
+ }
89
+ }
90
+
91
+ /**
92
+ * The marker for a session, or `null`.
93
+ *
94
+ * Looks up by run id first, then scans. The scan is not redundant: the run id
95
+ * the council writes comes from `currentRunId`, whose chain is
96
+ * `AC_BILLING_GRANT` → `AC_RUN_ID` → `CLAUDE_CODE_SESSION_ID`, while the
97
+ * register's `session_id` comes from `AGENT_CONFIG_SESSION_ID` →
98
+ * `CLAUDE_CODE_SESSION_ID`. The two coincide in an ordinary Claude Code session
99
+ * and diverge the moment an orchestrator sets `AC_RUN_ID` — so a direct hit is
100
+ * the common case and the scan is what stops the uncommon one from silently
101
+ * reporting a parked run as a crashed one. The directory holds one small file
102
+ * per parked run, so the scan is cheap by construction.
103
+ */
104
+ export function findQuotaParked(repoRoot: string, sessionId: string): QuotaParkedMarker | null {
105
+ const direct = readMarker(markerFile(repoRoot, sessionId));
106
+ if (direct !== null) return direct;
107
+ let names: string[];
108
+ try {
109
+ names = fs.readdirSync(path.join(repoRoot, QUOTA_PARKED_DIR_REL));
110
+ } catch {
111
+ return null;
112
+ }
113
+ for (const name of names) {
114
+ if (!name.endsWith('.json')) continue;
115
+ const m = readMarker(path.join(repoRoot, QUOTA_PARKED_DIR_REL, name));
116
+ if (m !== null && m.run_id === sessionId) return m;
117
+ }
118
+ return null;
119
+ }
120
+
121
+ function readMarker(file: string): QuotaParkedMarker | null {
122
+ try {
123
+ const raw: unknown = JSON.parse(fs.readFileSync(file, 'utf8'));
124
+ if (raw === null || typeof raw !== 'object') return null;
125
+ const o = raw as Record<string, unknown>;
126
+ if (typeof o['run_id'] !== 'string' || typeof o['parked_at'] !== 'string') return null;
127
+ if (!Array.isArray(o['providers'])) return null;
128
+ const providers = o['providers'].filter((p): p is string => typeof p === 'string');
129
+ return { run_id: o['run_id'], providers, parked_at: o['parked_at'] };
130
+ } catch {
131
+ return null;
132
+ }
133
+ }
@@ -85,6 +85,68 @@ export function sanitize_text(s: string): string {
85
85
  return out.length > MAX_FIELD_CHARS ? out.slice(0, MAX_FIELD_CHARS) : out;
86
86
  }
87
87
 
88
+ /**
89
+ * Report the INVISIBLE layer without removing it.
90
+ *
91
+ * `sanitize_text` above strips four channels — hidden-instruction vectors
92
+ * (bidi, zero-width, tag, deprecated-format, private-use-area, per `_classify`),
93
+ * strippable control chars, and invisible fillers. Stripping is the right
94
+ * disposition for a retrieval path that is about to hand text to a model.
95
+ *
96
+ * It is the WRONG disposition for a reporting surface. A warn-only PostToolUse
97
+ * hook must not rewrite what the agent already read, so it cannot call
98
+ * `sanitize_text` at all — and `scan_encoding_findings` is the VISIBLE-layer
99
+ * twin and deliberately says nothing about these four. Measured over the frozen
100
+ * corpus before this function existed: `deprecated-format`, `private-use-area`,
101
+ * `control-char` and `invisible-filler` reported **0 of 20 each** through the
102
+ * reporting API while `sanitize_text` changed **20 of 20** for every one of
103
+ * them. So the layer had a reporting blind spot exactly where its stripping was
104
+ * perfect, and a consumer that may not strip was blind to a quarter of the
105
+ * channel set.
106
+ *
107
+ * The published 99.00 % recall figure is the STRIPPING pipeline's. The reporting
108
+ * half alone measured 72.33 %. Both numbers are correct about different things,
109
+ * and conflating them is what made "import the measured layer and get 15
110
+ * channels" read as true.
111
+ *
112
+ * Additive by construction: nothing that exists changes behaviour, and the
113
+ * predicates are the SAME ones `sanitize_text` uses — not copies — so a channel
114
+ * cannot become strippable without becoming reportable in the same edit.
115
+ */
116
+ export function scan_invisible_findings(s: string): EncodingFinding[] {
117
+ const out: EncodingFinding[] = [];
118
+ const seen = new Set<string>();
119
+ const add = (channel: string, cp: number): void => {
120
+ const key = `${channel}:${cp}`;
121
+ if (seen.has(key)) return;
122
+ seen.add(key);
123
+ out.push({
124
+ channel,
125
+ detail: `U+${cp.toString(16).toUpperCase().padStart(4, '0')} in the invisible layer`,
126
+ });
127
+ };
128
+ for (const ch of s) {
129
+ const cp = ch.codePointAt(0);
130
+ if (cp === undefined) continue;
131
+ const classified = _classify(cp);
132
+ if (classified !== null) {
133
+ // `_classify` already names the channel; reuse its label rather than
134
+ // re-deriving one, so the hook's output and the authoring-time
135
+ // linter's speak the same vocabulary.
136
+ add(typeof classified === 'string' ? classified : 'hidden-instruction', cp);
137
+ continue;
138
+ }
139
+ if (_isStrippableControl(cp)) {
140
+ add('control-char', cp);
141
+ continue;
142
+ }
143
+ if (_INVISIBLE_FILLERS.has(cp)) {
144
+ add('invisible-filler', cp);
145
+ }
146
+ }
147
+ return out;
148
+ }
149
+
88
150
  // ---------------------------------------------------------------------------
89
151
  // Visible-layer scanner — FLAG ONLY, never rewrites
90
152
  // ---------------------------------------------------------------------------
@@ -0,0 +1,196 @@
1
+ /**
2
+ * One structural line per review run — the review surface's only telemetry.
3
+ *
4
+ * Why it exists: before this file the review surface emitted nothing. Nothing
5
+ * recorded how often a review ran, which judges fired, or whether the spec
6
+ * axis ever changed a recommendation — so the value of adding a sixth judge
7
+ * was unmeasurable in either direction, and the cheapest response to a slower
8
+ * default path is to switch the new judge off. A number is the only defence
9
+ * against that, and the honest number includes "it changed nothing".
10
+ *
11
+ * PII-exclusion by CONSTRUCTION, not by scrubbing. `ReviewLine` has no field
12
+ * able to hold free-form content: every member is an enum, a boolean, a
13
+ * non-negative integer, or an ISO timestamp. There is no `payload`, `notes`,
14
+ * `extra`, `prompt`, `diff` or `path` member, and `buildReviewLine` REJECTS an
15
+ * input object carrying one rather than dropping it silently — a scrubber can
16
+ * fail, an absent field cannot. Same principle the artifact-engagement event
17
+ * and `orchestration_record`'s hex-only `payload_hash` apply.
18
+ *
19
+ * Deliberately NOT reused from `_lib/orchestration_record.ts`: that line
20
+ * describes a subagent DISPATCH (tier, route, token deltas, cache hits). A
21
+ * review is not a dispatch, and widening `RecordInput` with review-shaped
22
+ * optionals would make both types describe neither thing precisely.
23
+ */
24
+
25
+ /** Was the spec axis able to run at all, and on what basis? */
26
+ export type SpecAxisReach = 'reachable_with_criteria' | 'reachable_no_criteria' | 'unreachable';
27
+
28
+ /**
29
+ * Did the spec axis change the recommendation the craft judges would have
30
+ * produced on their own?
31
+ *
32
+ * `null` is NOT "no" — it means the comparison never happened (the axis did
33
+ * not run, so there is no counterfactual). A `false` here would claim a
34
+ * comparison that was never made, which is the mis-attribution this field
35
+ * exists to expose. Same reasoning as `modelDivergent` in
36
+ * `_lib/orchestration_record.ts`.
37
+ */
38
+ export type SpecAxisEffect = 'changed' | 'unchanged' | null;
39
+
40
+ /** The recommendation word the synthesis emitted. */
41
+ export type ReviewRecommendation = 'block' | 'revise' | 'proceed';
42
+
43
+ /** The criteria-source state, mirroring `judge-spec-compliance`'s three states. */
44
+ export type CriteriaSource = 'supplied' | 'not_provided' | 'supplied_unparseable';
45
+
46
+ /** The closed field set. A member not on this list cannot be written. */
47
+ export const REVIEW_LINE_FIELDS = [
48
+ 'schema',
49
+ 'at',
50
+ 'judges_declared',
51
+ 'judges_ran',
52
+ 'spec_axis_reach',
53
+ 'criteria_source',
54
+ 'criteria_count',
55
+ 'spec_missing',
56
+ 'spec_partial',
57
+ 'recommendation',
58
+ 'spec_axis_effect',
59
+ ] as const;
60
+
61
+ export type ReviewLineField = (typeof REVIEW_LINE_FIELDS)[number];
62
+
63
+ /**
64
+ * Field names that must never appear on this line — the free-form shapes a
65
+ * reviewer would reach for first. Checked as an INPUT rejection, so adding one
66
+ * fails loudly at the call site instead of being stripped downstream.
67
+ */
68
+ export const FORBIDDEN_FREEFORM_FIELDS = [
69
+ 'payload',
70
+ 'notes',
71
+ 'extra',
72
+ 'prompt',
73
+ 'diff',
74
+ 'path',
75
+ 'file',
76
+ 'criteria',
77
+ 'findings',
78
+ 'message',
79
+ 'body',
80
+ 'content',
81
+ 'text',
82
+ ] as const;
83
+
84
+ export interface ReviewLine {
85
+ readonly schema: 'review-axis-v1';
86
+ /** ISO-8601 UTC. Caller-supplied so the line is reproducible in a test. */
87
+ readonly at: string;
88
+ readonly judges_declared: number;
89
+ readonly judges_ran: number;
90
+ readonly spec_axis_reach: SpecAxisReach;
91
+ readonly criteria_source: CriteriaSource;
92
+ readonly criteria_count: number;
93
+ readonly spec_missing: number;
94
+ readonly spec_partial: number;
95
+ readonly recommendation: ReviewRecommendation;
96
+ readonly spec_axis_effect: SpecAxisEffect;
97
+ }
98
+
99
+ export interface ReviewLineInput {
100
+ at: string;
101
+ judges_declared: number;
102
+ judges_ran: number;
103
+ spec_axis_reach: SpecAxisReach;
104
+ criteria_source: CriteriaSource;
105
+ criteria_count: number;
106
+ spec_missing: number;
107
+ spec_partial: number;
108
+ recommendation: ReviewRecommendation;
109
+ spec_axis_effect?: SpecAxisEffect;
110
+ }
111
+
112
+ export interface BuiltReviewLine {
113
+ line: ReviewLine | null;
114
+ errors: string[];
115
+ }
116
+
117
+ const ISO_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?Z$/;
118
+ const REACH: readonly SpecAxisReach[] = ['reachable_with_criteria', 'reachable_no_criteria', 'unreachable'];
119
+ const SOURCE: readonly CriteriaSource[] = ['supplied', 'not_provided', 'supplied_unparseable'];
120
+ const RECO: readonly ReviewRecommendation[] = ['block', 'revise', 'proceed'];
121
+
122
+ function isCount(n: unknown): n is number {
123
+ return typeof n === 'number' && Number.isInteger(n) && n >= 0;
124
+ }
125
+
126
+ /**
127
+ * Build + validate one `review-axis-v1` line.
128
+ *
129
+ * Returns `{line, errors}`; when `errors` is non-empty, `line` is null and the
130
+ * caller must NOT write. Never throws, never partially writes, never silently
131
+ * repairs an invalid input — the same contract as `buildOrchestrationLine`.
132
+ */
133
+ export function buildReviewLine(input: ReviewLineInput): BuiltReviewLine {
134
+ const errors: string[] = [];
135
+ const raw = input as unknown as Record<string, unknown>;
136
+
137
+ for (const banned of FORBIDDEN_FREEFORM_FIELDS) {
138
+ if (Object.prototype.hasOwnProperty.call(raw, banned)) {
139
+ errors.push(
140
+ `'${banned}' is a free-form field and review-axis-v1 carries none — ` +
141
+ 'privacy here is a property of the schema shape, not of a scrubbing pass',
142
+ );
143
+ }
144
+ }
145
+
146
+ if (typeof input.at !== 'string' || !ISO_RE.test(input.at)) {
147
+ errors.push('at must be an ISO-8601 UTC timestamp (…Z)');
148
+ }
149
+ for (const k of ['judges_declared', 'judges_ran', 'criteria_count', 'spec_missing', 'spec_partial'] as const) {
150
+ if (!isCount(input[k])) errors.push(`${k} must be a non-negative integer`);
151
+ }
152
+ if (isCount(input.judges_ran) && isCount(input.judges_declared) && input.judges_ran > input.judges_declared) {
153
+ errors.push('judges_ran cannot exceed judges_declared');
154
+ }
155
+ if (!REACH.includes(input.spec_axis_reach)) errors.push(`spec_axis_reach must be one of ${REACH.join(' | ')}`);
156
+ if (!SOURCE.includes(input.criteria_source)) errors.push(`criteria_source must be one of ${SOURCE.join(' | ')}`);
157
+ if (!RECO.includes(input.recommendation)) errors.push(`recommendation must be one of ${RECO.join(' | ')}`);
158
+
159
+ const effect = input.spec_axis_effect ?? null;
160
+ if (effect !== null && effect !== 'changed' && effect !== 'unchanged') {
161
+ errors.push('spec_axis_effect must be changed | unchanged | null');
162
+ }
163
+ // A counterfactual that never ran is `null`, never `unchanged`.
164
+ if (input.spec_axis_reach === 'unreachable' && effect !== null) {
165
+ errors.push('spec_axis_effect must be null when the axis was unreachable — there was no comparison to report');
166
+ }
167
+ // The two honest pairings of reach and source.
168
+ if (input.spec_axis_reach === 'reachable_with_criteria' && input.criteria_source !== 'supplied') {
169
+ errors.push('reachable_with_criteria requires criteria_source=supplied');
170
+ }
171
+ if (input.spec_axis_reach === 'reachable_no_criteria' && input.criteria_source === 'supplied') {
172
+ errors.push('reachable_no_criteria contradicts criteria_source=supplied');
173
+ }
174
+ if (input.criteria_source !== 'supplied' && isCount(input.criteria_count) && input.criteria_count > 0) {
175
+ errors.push('criteria_count must be 0 unless criteria were supplied');
176
+ }
177
+
178
+ if (errors.length > 0) return { line: null, errors };
179
+
180
+ return {
181
+ line: {
182
+ schema: 'review-axis-v1',
183
+ at: input.at,
184
+ judges_declared: input.judges_declared,
185
+ judges_ran: input.judges_ran,
186
+ spec_axis_reach: input.spec_axis_reach,
187
+ criteria_source: input.criteria_source,
188
+ criteria_count: input.criteria_count,
189
+ spec_missing: input.spec_missing,
190
+ spec_partial: input.spec_partial,
191
+ recommendation: input.recommendation,
192
+ spec_axis_effect: effect,
193
+ },
194
+ errors: [],
195
+ };
196
+ }
@@ -61,10 +61,102 @@ export interface SubagentResponse {
61
61
  * the same claim as "no assumptions were made".
62
62
  */
63
63
  assumptions?: CapsuleAssumption[];
64
+ /**
65
+ * Where the tests in this change were authored FROM.
66
+ *
67
+ * `from-spec` — written against the acceptance criteria, before the diff
68
+ * existed. `from-diff` — written after reading the implementation.
69
+ * `unknown` — not recorded.
70
+ *
71
+ * **`unknown` is the default and an ABSENT field resolves to it, never to
72
+ * `from-spec`.** That asymmetry is the whole point of the field. The claim
73
+ * this metric exists to keep measurable is that a suite written by the same
74
+ * context as the implementation inherits its blind spots — so the valuable
75
+ * state is `from-spec`, and a default that let absence read as the valuable
76
+ * state would report the claim as satisfied by silence.
77
+ *
78
+ * Structural enum only: three string values and nothing that can hold
79
+ * free-form content. Same exclusion-by-construction the rest of this
80
+ * envelope uses — a field that cannot carry a prompt or a diff has no
81
+ * scrubber to fail.
82
+ *
83
+ * OPTIONAL, and deliberately not in `RESPONSE_REQUIRED_FIELDS`: that list
84
+ * is calibrated against a recorded ledger equivalence (`error_count: 5`),
85
+ * and adding a sixth required field would break it. A metric is not worth
86
+ * invalidating a measurement for.
87
+ */
88
+ test_authorship?: TestAuthorship;
89
+ }
90
+
91
+ /**
92
+ * Three states. `unknown` is one of them, not the absence of the other two.
93
+ *
94
+ * Ordered by what they license rather than alphabetically: `from-spec` is the
95
+ * only value that supports the independence claim, `from-diff` is the weaker
96
+ * form the claim says is worthless, and `unknown` licenses nothing.
97
+ */
98
+ export type TestAuthorship = 'from-spec' | 'from-diff' | 'unknown';
99
+
100
+ export const TEST_AUTHORSHIP: ReadonlySet<string> = new Set<TestAuthorship>([
101
+ 'from-spec',
102
+ 'from-diff',
103
+ 'unknown',
104
+ ]);
105
+
106
+ /**
107
+ * Resolve the field from a decoded envelope. ABSENT and UNRECOGNISED both
108
+ * resolve to `unknown`.
109
+ *
110
+ * An unrecognised value resolving to `unknown` rather than throwing is
111
+ * deliberate: this is a metric, and a producer that emits `from_spec` with an
112
+ * underscore should show up in the distribution as unrecorded, not stop a
113
+ * dispatch. The report is what surfaces it — a spike of `unknown` IS the
114
+ * finding that the field is not reaching its producers.
115
+ */
116
+ export function resolveTestAuthorship(input: unknown): TestAuthorship {
117
+ if (input === null || typeof input !== 'object') return 'unknown';
118
+ const v = (input as Record<string, unknown>)['test_authorship'];
119
+ return typeof v === 'string' && TEST_AUTHORSHIP.has(v) ? (v as TestAuthorship) : 'unknown';
64
120
  }
65
121
 
66
122
  const CONFIDENCE: ReadonlySet<string> = new Set<Confidence>(['low', 'medium', 'high']);
67
123
 
124
+ /**
125
+ * The five fields `validateResponse` REQUIRES. Exported so the ledger's
126
+ * classifier counts contract fields against the validator's own list rather
127
+ * than a second copy that can drift from it.
128
+ *
129
+ * The optional fields (`artifact_paths`, `assumptions`) are deliberately NOT
130
+ * here. The boundary this list defines is "did the author aim at the envelope
131
+ * at all", and it is calibrated against a recorded measurement: every `fail`
132
+ * in the live ledger carries `error_count: 5`, which is exactly the count of
133
+ * required-field errors an object with none of them produces. Counting an
134
+ * optional field as a hit would break that equivalence, so an object carrying
135
+ * only `assumptions` reads as foreign — recorded here as a known edge, not as
136
+ * an oversight.
137
+ */
138
+ export const RESPONSE_REQUIRED_FIELDS: readonly string[] = [
139
+ 'summary',
140
+ 'handoff',
141
+ 'confidence',
142
+ 'findings',
143
+ 'risks',
144
+ ];
145
+
146
+ /**
147
+ * How many required contract fields are PRESENT on a decoded object.
148
+ *
149
+ * Presence, never validity: a `summary: 123` is a contract attempt with a
150
+ * wrong type, which is a different defect from a fenced tool call that never
151
+ * aimed at the envelope. That distinction is the whole point of the count —
152
+ * `validateResponse` already reports the type errors.
153
+ */
154
+ export function countContractFields(input: unknown): number {
155
+ if (input === null || typeof input !== 'object') return 0;
156
+ const r = input as Record<string, unknown>;
157
+ return RESPONSE_REQUIRED_FIELDS.filter((k) => Object.hasOwn(r, k)).length;
158
+ }
159
+
68
160
  export interface ValidationResult {
69
161
  valid: boolean;
70
162
  errors: string[];