@event4u/agent-config 9.28.0 → 9.30.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 (209) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +6 -6
  3. package/CHANGELOG.md +172 -5
  4. package/README.md +2 -1
  5. package/agents/templates/.ai-council.yml.example +55 -24
  6. package/dist/agent-src/commands/analyze/inbox.md +22 -1
  7. package/dist/agent-src/commands/cost/report.md +2 -2
  8. package/dist/agent-src/commands/explain-run.md +31 -1
  9. package/dist/agent-src/commands/team/adversarial.md +8 -7
  10. package/dist/agent-src/commands/team/delegate.md +14 -12
  11. package/dist/agent-src/commands/team/review.md +9 -8
  12. package/dist/agent-src/commands/team/status.md +9 -8
  13. package/dist/agent-src/commands/team.md +29 -19
  14. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +88 -2
  15. package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +29 -21
  16. package/dist/agent-src/contexts/execution/host-capability-manifest.md +26 -15
  17. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +22 -1
  18. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +17 -0
  19. package/dist/agent-src/contexts/execution/subagent-response-contract.md +65 -1
  20. package/dist/agent-src/contexts/execution/subagent-routing.md +13 -11
  21. package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +25 -0
  22. package/dist/agent-src/contexts/execution/subagent-steering.md +6 -3
  23. package/dist/agent-src/rules/delegation-policy.md +11 -7
  24. package/dist/agent-src/rules/preservation-guard.md +9 -0
  25. package/dist/agent-src/rules/settings-ask-protocol.md +8 -7
  26. package/dist/agent-src/rules/token-budget-discipline.md +1 -1
  27. package/dist/agent-src/scripts/roadmap_gates.ts +312 -0
  28. package/dist/agent-src/scripts/update_roadmap_progress.ts +72 -4
  29. package/dist/agent-src/skills/forensics-report/SKILL.md +114 -0
  30. package/dist/agent-src/skills/forensics-report/evals/triggers.json +17 -0
  31. package/dist/agent-src/skills/premortem/SKILL.md +30 -1
  32. package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +2 -1
  33. package/dist/agent-src/skills/subagent-orchestration/SKILL.md +17 -5
  34. package/dist/agent-src/skills/subagent-orchestration/schemas/subagent-status.json +61 -3
  35. package/dist/agent-src/skills/test-case-discovery/SKILL.md +1 -1
  36. package/dist/agent-src/templates/AGENTS.md +4 -4
  37. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  38. package/dist/agent-src/templates/roadmaps.md +23 -0
  39. package/dist/cli/registry.js +2 -0
  40. package/dist/cli/registry.js.map +1 -1
  41. package/dist/cli-delegate/{chunk-22GCIJ2O.js → chunk-66OVWGB5.js} +1 -1
  42. package/dist/cli-delegate/{chunk-7OMQCH4V.js → chunk-BTJNRUX3.js} +22 -35
  43. package/dist/cli-delegate/{chunk-DVRS2KVR.js → chunk-DDUNNFYX.js} +24 -17
  44. package/dist/cli-delegate/{chunk-PQSAQKHW.js → chunk-EQY4FLJB.js} +3 -3
  45. package/dist/cli-delegate/{chunk-2BE5I2MN.js → chunk-IYTGIG2Y.js} +155 -91
  46. package/dist/cli-delegate/{chunk-GB2FSUEE.js → chunk-LCFJIQ4J.js} +32 -0
  47. package/dist/cli-delegate/{chunk-VSYT7OHO.js → chunk-LVRYBKTI.js} +31 -2
  48. package/dist/cli-delegate/chunk-MMJL4TIX.js +18 -0
  49. package/dist/cli-delegate/{chunk-JXARCGVN.js → chunk-MP3JT3IU.js} +1 -1
  50. package/dist/cli-delegate/chunk-O3CO5BFB.js +161 -0
  51. package/dist/cli-delegate/{chunk-REKCMYOU.js → chunk-PFBV6XRT.js} +36 -20
  52. package/dist/cli-delegate/chunk-RVGBH4XK.js +142 -0
  53. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  54. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  55. package/dist/cli-delegate/cmd_conformance.js +2803 -137
  56. package/dist/cli-delegate/cmd_doctor.js +10 -5
  57. package/dist/cli-delegate/cmd_explain.js +2 -2
  58. package/dist/cli-delegate/cmd_export.js +4 -4
  59. package/dist/cli-delegate/cmd_handoff.js +88 -8
  60. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  61. package/dist/cli-delegate/cmd_migrate.js +2 -2
  62. package/dist/cli-delegate/cmd_packs_active.js +2 -2
  63. package/dist/cli-delegate/cmd_prune.js +4 -4
  64. package/dist/cli-delegate/cmd_route_audit.js +3 -2
  65. package/dist/cli-delegate/cmd_route_explain.js +1 -1
  66. package/dist/cli-delegate/cmd_session_recycle.js +177 -0
  67. package/dist/cli-delegate/cmd_settings_get.js +2 -8
  68. package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
  69. package/dist/cli-delegate/cmd_settings_set.js +2 -2
  70. package/dist/cli-delegate/cmd_sync.js +4 -4
  71. package/dist/cli-delegate/cmd_uninstall.js +4 -4
  72. package/dist/cli-delegate/cmd_update.js +1 -1
  73. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  74. package/dist/cli-delegate/cmd_validate.js +4 -4
  75. package/dist/cli-delegate/cmd_versions.js +1 -1
  76. package/dist/discovery/deprecation-report.md +1 -1
  77. package/dist/discovery/discovery-manifest.json +72 -23
  78. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  79. package/dist/discovery/discovery-manifest.summary.md +3 -2
  80. package/dist/discovery/orphan-report.md +1 -1
  81. package/dist/discovery/packs.json +39 -2
  82. package/dist/discovery/trust-report.md +2 -2
  83. package/dist/discovery/workspaces.json +12 -4
  84. package/dist/hooks/dispatch.js +2278 -611
  85. package/dist/install/install.mjs +56 -17
  86. package/dist/mcp/registry-manifest.json +3 -3
  87. package/dist/mcp/server.json +2 -2
  88. package/dist/mcp/server.mjs +4122 -699
  89. package/dist/router.json +1 -1
  90. package/dist/scripts/_lib/agent_settings.js +54 -0
  91. package/dist/scripts/_lib/agent_settings.js.map +1 -1
  92. package/dist/scripts/condense.js +147 -2
  93. package/dist/scripts/condense.js.map +1 -1
  94. package/dist/server/schemas/settings.js +12 -7
  95. package/dist/server/schemas/settings.js.map +1 -1
  96. package/dist/shared/settingsCarveOut.js +0 -9
  97. package/dist/shared/settingsCarveOut.js.map +1 -1
  98. package/dist/shared/settingsClasses.js +11 -9
  99. package/dist/shared/settingsClasses.js.map +1 -1
  100. package/dist/ui/assets/index-B5qGYOJo.js +26 -0
  101. package/dist/ui/assets/index-B5qGYOJo.js.map +1 -0
  102. package/dist/ui/index.html +1 -1
  103. package/docs/contracts/settings-classes.md +58 -12
  104. package/package.json +1 -1
  105. package/src/agent-src/scripts/roadmap_gates.ts +312 -0
  106. package/src/agent-src/scripts/update_roadmap_progress.ts +72 -4
  107. package/src/agent-src/templates/AGENTS.md +4 -4
  108. package/src/config/agent-settings.template.yml +88 -66
  109. package/src/config/discovery/packs.yml +11 -0
  110. package/src/config/discovery/workspaces.yml +1 -1
  111. package/src/config/dispatch-economy-metrics.json +61 -0
  112. package/src/config/evaluator-budgets.json +4 -4
  113. package/src/config/gate-violation-baselines.json +3 -3
  114. package/src/config/hook-token-budget.json +91 -0
  115. package/src/config/recycle-threshold-budget.json +12 -0
  116. package/src/config/rule-enforcement-baseline.json +86 -0
  117. package/src/scripts/_cli/cmd_conformance.ts +31 -44
  118. package/src/scripts/_cli/cmd_doctor.ts +65 -48
  119. package/src/scripts/_cli/cmd_session_recycle.ts +226 -0
  120. package/src/scripts/_dispatch.bash +23 -0
  121. package/src/scripts/_lib/agent_settings.ts +58 -0
  122. package/src/scripts/_lib/auto_dispatch.corpus.test.ts +1 -1
  123. package/src/scripts/_lib/auto_dispatch.ts +23 -18
  124. package/src/scripts/_lib/capsule_trigger.ts +124 -0
  125. package/src/scripts/_lib/host_capability.ts +63 -6
  126. package/src/scripts/_lib/judgment_ladder.ts +634 -0
  127. package/src/scripts/_lib/orchestration_gate.ts +8 -0
  128. package/src/scripts/_lib/orchestration_record.ts +78 -10
  129. package/src/scripts/_lib/orchestration_savings.ts +1 -1
  130. package/src/scripts/_lib/recycle_envelope_paths.ts +35 -0
  131. package/src/scripts/_lib/self_repair.ts +354 -13
  132. package/src/scripts/_lib/self_repair_store.ts +28 -0
  133. package/src/scripts/_lib/session_eol.ts +230 -0
  134. package/src/scripts/_lib/session_role.ts +42 -0
  135. package/src/scripts/_lib/subagent_capsule.ts +341 -0
  136. package/src/scripts/_lib/subagent_response.ts +94 -0
  137. package/src/scripts/_lib/subagent_spawn.ts +13 -0
  138. package/src/scripts/_lib/subagent_steering.ts +9 -4
  139. package/src/scripts/_lib/tier_budget_routing.ts +6 -1
  140. package/src/scripts/_lib/worker_budget.ts +37 -2
  141. package/src/scripts/ai_council/clients.ts +126 -55
  142. package/src/scripts/ai_council/config.ts +94 -6
  143. package/src/scripts/ai_council/handoff.ts +113 -0
  144. package/src/scripts/ai_council/modes.ts +19 -4
  145. package/src/scripts/ai_council/orchestrator.ts +105 -0
  146. package/src/scripts/ai_council/quorum.ts +84 -0
  147. package/src/scripts/ai_council/session.ts +44 -0
  148. package/src/scripts/ai_council/transport_resolver.ts +127 -1
  149. package/src/scripts/ai_team/availability.ts +110 -0
  150. package/src/scripts/ai_team/config.ts +34 -14
  151. package/src/scripts/ai_team/team_dispatch.ts +71 -29
  152. package/src/scripts/ask_transport.ts +221 -0
  153. package/src/scripts/bench_critic_protocol.ts +396 -0
  154. package/src/scripts/bench_hook_injection.ts +234 -0
  155. package/src/scripts/check_adr_frontmatter.ts +84 -40
  156. package/src/scripts/check_branch_freshness.ts +1 -0
  157. package/src/scripts/check_corpus_staleness.ts +18 -4
  158. package/src/scripts/check_detector_corpus.ts +219 -0
  159. package/src/scripts/check_gitignore_freshness.ts +42 -8
  160. package/src/scripts/check_hook_bundle_freshness.ts +24 -1
  161. package/src/scripts/check_kernel_prefix_stability.ts +1 -0
  162. package/src/scripts/check_standing_rule_delivery.ts +73 -32
  163. package/src/scripts/check_static_layer_stability.ts +154 -0
  164. package/src/scripts/check_suppression_hygiene.ts +48 -0
  165. package/src/scripts/condense.ts +162 -2
  166. package/src/scripts/council_cli.ts +401 -25
  167. package/src/scripts/dispatch_economy_report.ts +448 -0
  168. package/src/scripts/dispatch_r2_reviewer.ts +15 -2
  169. package/src/scripts/explain_run.ts +206 -4
  170. package/src/scripts/forensics_report.ts +333 -0
  171. package/src/scripts/generate_settings_reference.ts +1 -0
  172. package/src/scripts/handoff_context_hook.ts +112 -4
  173. package/src/scripts/hook_manifest.yaml +117 -10
  174. package/src/scripts/hooks/concern_registry.ts +8 -0
  175. package/src/scripts/hooks/delegation_nudge_hook.ts +106 -35
  176. package/src/scripts/hooks/dispatch_hook.ts +39 -2
  177. package/src/scripts/hooks/edit_shape_hook.ts +303 -0
  178. package/src/scripts/hooks/orchestration_record_hook.ts +19 -1
  179. package/src/scripts/hooks/reread_guard_hook.ts +314 -0
  180. package/src/scripts/hooks/rtk_wrap_hook.ts +133 -14
  181. package/src/scripts/hooks/session_eol_hook.ts +268 -0
  182. package/src/scripts/hooks/turn_end_gate_hook.ts +507 -0
  183. package/src/scripts/install-hooks.sh +1 -1
  184. package/src/scripts/language_mirror_hook.ts +89 -0
  185. package/src/scripts/lint_decision_impact.ts +14 -1
  186. package/src/scripts/lint_discovery_vocabulary.ts +2 -0
  187. package/src/scripts/lint_hook_manifest.ts +82 -0
  188. package/src/scripts/lint_never_silent.ts +276 -0
  189. package/src/scripts/lint_no_activation_gates.ts +271 -0
  190. package/src/scripts/lint_readme_serial_comma.ts +1 -0
  191. package/src/scripts/lint_rule_enforcement_declaration.ts +347 -0
  192. package/src/scripts/measure_turn_end_gate.ts +194 -0
  193. package/src/scripts/orchestration_record.ts +12 -1
  194. package/src/scripts/rebuild_hook_bundle.ts +178 -0
  195. package/src/scripts/release.ts +74 -2
  196. package/src/scripts/report_conformance_funnel.ts +284 -0
  197. package/src/scripts/report_imperative_density.ts +1 -0
  198. package/src/scripts/routing_doctor.ts +118 -36
  199. package/src/scripts/score_skill_selection.ts +19 -5
  200. package/src/scripts/self_repair_cli.ts +337 -59
  201. package/src/scripts/session_eol_report.ts +261 -0
  202. package/src/scripts/team_events_spike.ts +271 -0
  203. package/src/scripts/team_review_gate_hook.ts +31 -9
  204. package/src/server/schemas/settings.ts +24 -17
  205. package/src/shared/settingsCarveOut.ts +0 -10
  206. package/src/shared/settingsClasses.ts +11 -9
  207. package/src/templates/compact-instructions.md +40 -0
  208. package/dist/ui/assets/index-DmR0mbk4.js +0 -26
  209. package/dist/ui/assets/index-DmR0mbk4.js.map +0 -1
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Safe self-heal for the self-hosted hook bundle — the builder that
4
+ * `check_hook_bundle_freshness` deliberately is not.
5
+ *
6
+ * WHY THIS EXISTS — measured, not hypothetical.
7
+ *
8
+ * The freshness gate reds after ANY hook-source merge from main, because
9
+ * `dist/hooks/dispatch.js` is untracked and nothing in the local chain
10
+ * rebuilds it. On 2026-08-09 that turned `task release` (9.28.0) into a
11
+ * blocked push with a remedy the operator had to run by hand — a routine
12
+ * pre-push chore promoted to a release incident. The gate is right to never
13
+ * build (its documented hazard: esbuild overwriting the dispatcher a running
14
+ * hook is executing can wedge the tool loop). The hazard is in HOW a naive
15
+ * rebuild writes, not in rebuilding: this script builds to a sibling `.new`
16
+ * file, proves the result actually dispatches, and only then renames it into
17
+ * place — an atomic same-directory rename that a running hook holding an open
18
+ * file descriptor never observes mid-write.
19
+ *
20
+ * The `.new` file MUST live in `dist/hooks/` (not a temp dir): the bundle
21
+ * resolves `src/scripts/hook_manifest.yaml` relative to its OWN location via
22
+ * the `__AGENT_CONFIG_BUNDLE__` path-depth switch. Built anywhere else, every
23
+ * probe answers "manifest missing" with exit 0 and the smoke test proves
24
+ * nothing (measured 2026-08-08).
25
+ *
26
+ * The esbuild invocation is NOT duplicated here — it is read from
27
+ * `package.json`'s `build:hooks` script and only the `--outfile=` is
28
+ * rewritten, so the flag set (banner, defines, target) has exactly one home.
29
+ *
30
+ * Behaviour:
31
+ * no self-hosted bundle → no-op (fresh checkout / CI — nothing is running
32
+ * stale code, and materialising a bundle here would
33
+ * change what other gates see)
34
+ * bundle fresh → no-op
35
+ * bundle stale → rebuild to `.new`, probe, rename into place
36
+ * build or probe fails → the `.new` file is removed and the OLD bundle
37
+ * keeps running (old-but-working beats broken);
38
+ * exit 1 so the freshness gate behind this still
39
+ * reds and the failure is loud.
40
+ *
41
+ * Exit codes: 0 = fresh / healed / nothing to heal · 1 = heal failed.
42
+ */
43
+ import { spawnSync } from "node:child_process";
44
+ import * as fs from "node:fs";
45
+ import * as path from "node:path";
46
+ import { fileURLToPath } from "node:url";
47
+
48
+ import { check } from "./check_hook_bundle_freshness.js";
49
+
50
+ // ledger-exempt: self-heal builder, not a checker — one build → probe → atomic-rename transaction whose outcome is a single aggregate heal verdict; the per-source staleness accounting lives in check_hook_bundle_freshness, which carries the ledger.
51
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
52
+ const REPO_ROOT = path.resolve(HERE, "..", "..");
53
+ const LIVE_REL = path.join("dist", "hooks", "dispatch.js");
54
+ const NEW_REL = path.join("dist", "hooks", "dispatch.new.js");
55
+
56
+ /**
57
+ * Rewrite the `--outfile=` of the canonical `build:hooks` script to the
58
+ * `.new` sibling. Returns null when the expected outfile token is absent —
59
+ * a changed `package.json` must fail loudly, never build to a guessed path.
60
+ */
61
+ export function rewriteOutfile(script: string, newOutfile: string): string | null {
62
+ const token = `--outfile=${LIVE_REL}`;
63
+ if (!script.includes(token)) return null;
64
+ return script.replace(token, `--outfile=${newOutfile}`);
65
+ }
66
+
67
+ /**
68
+ * Verdict on a dispatcher probe. The probe contract is pinned against the
69
+ * real dry-run output: exit 0 AND parseable JSON AND a non-empty `concerns`
70
+ * array. Exit 0 alone is NOT enough — a bundle that cannot find its manifest
71
+ * answers "manifest missing" with exit 0 (measured 2026-08-08), which is
72
+ * exactly the broken dispatcher this probe exists to keep out of the live
73
+ * path. Returns null on pass, a human-readable reason on failure.
74
+ */
75
+ export function probeVerdict(exitCode: number, stdout: string): string | null {
76
+ if (exitCode !== 0) return `probe exited ${String(exitCode)}`;
77
+ let parsed: unknown;
78
+ try {
79
+ parsed = JSON.parse(stdout);
80
+ } catch {
81
+ return `probe output is not JSON: ${stdout.slice(0, 120)}`;
82
+ }
83
+ const concerns = (parsed as { concerns?: unknown }).concerns;
84
+ if (!Array.isArray(concerns) || concerns.length === 0) {
85
+ return "probe JSON carries no concerns — the bundle dispatched nothing";
86
+ }
87
+ return null;
88
+ }
89
+
90
+ function cleanup(newAbs: string): void {
91
+ try {
92
+ fs.rmSync(newAbs, { force: true });
93
+ } catch {
94
+ // Best-effort: a leftover .new file is inert (nothing executes it).
95
+ }
96
+ }
97
+
98
+ export function main(): number {
99
+ const r = check(REPO_ROOT);
100
+
101
+ if (r.skipped) {
102
+ process.stdout.write(
103
+ `✅ OK rebuild_hook_bundle: no self-hosted ${LIVE_REL} — nothing runs stale code, nothing to heal\n`,
104
+ );
105
+ return 0;
106
+ }
107
+ if (r.stale.length === 0) {
108
+ process.stdout.write("✅ OK rebuild_hook_bundle: bundle already fresh — no rebuild needed\n");
109
+ return 0;
110
+ }
111
+
112
+ process.stdout.write(
113
+ `🔧 hook bundle stale (${String(r.stale.length)} newer source(s)) — rebuilding safely (build .new → probe → atomic rename)\n`,
114
+ );
115
+
116
+ const pkg = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, "package.json"), "utf-8")) as {
117
+ scripts?: Record<string, string>;
118
+ };
119
+ const buildScript = pkg.scripts?.["build:hooks"];
120
+ if (buildScript === undefined) {
121
+ console.error("❌ rebuild_hook_bundle: package.json has no build:hooks script");
122
+ return 1;
123
+ }
124
+ const cmd = rewriteOutfile(buildScript, NEW_REL);
125
+ if (cmd === null) {
126
+ console.error(
127
+ `❌ rebuild_hook_bundle: build:hooks no longer targets --outfile=${LIVE_REL} — ` +
128
+ "refusing to build to a guessed path; update this script alongside package.json",
129
+ );
130
+ return 1;
131
+ }
132
+
133
+ const newAbs = path.join(REPO_ROOT, NEW_REL);
134
+ const build = spawnSync("sh", ["-c", cmd], {
135
+ cwd: REPO_ROOT,
136
+ encoding: "utf-8",
137
+ env: { ...process.env, PATH: `${path.join(REPO_ROOT, "node_modules", ".bin")}:${process.env["PATH"] ?? ""}` },
138
+ });
139
+ if (build.status !== 0) {
140
+ cleanup(newAbs);
141
+ console.error(`❌ rebuild_hook_bundle: esbuild failed (${String(build.status)}):`);
142
+ console.error((build.stderr || build.stdout || "").trim());
143
+ return 1;
144
+ }
145
+
146
+ const probe = spawnSync(
147
+ "node",
148
+ [newAbs, "--platform", "claude", "--event", "pre_tool_use", "--dry-run"],
149
+ { cwd: REPO_ROOT, encoding: "utf-8", input: "{}" },
150
+ );
151
+ const verdict = probeVerdict(probe.status ?? 1, probe.stdout ?? "");
152
+ if (verdict !== null) {
153
+ cleanup(newAbs);
154
+ console.error(`❌ rebuild_hook_bundle: new bundle failed its probe — ${verdict}`);
155
+ console.error(" The old bundle stays in place (old-but-working beats broken).");
156
+ return 1;
157
+ }
158
+
159
+ fs.renameSync(newAbs, path.join(REPO_ROOT, LIVE_REL));
160
+ process.stdout.write(
161
+ `✅ OK rebuild_hook_bundle: rebuilt + probed + renamed into place (${String(r.checked)} bundled source(s))\n`,
162
+ );
163
+ return 0;
164
+ }
165
+
166
+ const invokedDirectly = ((): boolean => {
167
+ const entry = process.argv[1];
168
+ if (entry === undefined) return false;
169
+ try {
170
+ return fs.realpathSync(path.resolve(entry)) === fs.realpathSync(fileURLToPath(import.meta.url));
171
+ } catch {
172
+ return false;
173
+ }
174
+ })();
175
+
176
+ if (invokedDirectly) {
177
+ process.exit(main());
178
+ }
@@ -1073,6 +1073,78 @@ function _tag_exists_remote(tag: string): boolean {
1073
1073
  return r.returncode === 0;
1074
1074
  }
1075
1075
 
1076
+ /**
1077
+ * True when git rejected a tag push because the ref already exists on the
1078
+ * remote. Wording pinned from the real failure (2026-08-09, 9.28.0 — two
1079
+ * concurrent resume runs, the loser crashed):
1080
+ *
1081
+ * ! [remote rejected] 9.28.0 -> 9.28.0 (cannot lock ref 'refs/tags/9.28.0': reference already exists)
1082
+ *
1083
+ * plus the `[rejected] … (already exists)` variant git emits when the local
1084
+ * and remote tag objects differ.
1085
+ */
1086
+ export function _is_tag_already_exists(stderr: string, stdout: string): boolean {
1087
+ const text = `${stderr}\n${stdout}`;
1088
+ return /\[(?:remote )?rejected\]/i.test(text) && /already exists/i.test(text);
1089
+ }
1090
+
1091
+ /** Peeled commit the remote tag points at, or null when the tag is absent. */
1092
+ function _remote_tag_commit(tag: string): string | null {
1093
+ const r = run(['git', 'ls-remote', REMOTE, `refs/tags/${tag}`, `refs/tags/${tag}^{}`], {
1094
+ check: false,
1095
+ capture: true,
1096
+ });
1097
+ if (r.returncode !== 0) return null;
1098
+ const lines = r.stdout.trim().split('\n').filter(Boolean);
1099
+ if (lines.length === 0) return null;
1100
+ // An annotated tag lists the tag object AND the peeled `^{}` commit — the
1101
+ // peeled line wins so the comparison is commit-vs-commit regardless of
1102
+ // whether the two runs minted distinct tag objects for the same commit.
1103
+ const peeled = lines.find((l) => l.trimEnd().endsWith('^{}'));
1104
+ return (peeled ?? (lines[0] as string)).split('\t')[0] ?? null;
1105
+ }
1106
+
1107
+ /**
1108
+ * Push a tag, tolerating exactly ONE failure: the concurrent-release race.
1109
+ * `_tag_exists_remote` in step 8 is a live check, but between it and this
1110
+ * push a parallel `task release` run can land the same tag (measured
1111
+ * 2026-08-09, 9.28.0). When the remote tag already points at the same commit
1112
+ * as the local one, the repository IS in the desired state — continue. Any
1113
+ * other rejection, and a same-name tag on a DIFFERENT commit, stays fatal
1114
+ * with the push output as the error.
1115
+ */
1116
+ function _push_tag(tag: string): void {
1117
+ const first = run(['git', 'push', REMOTE, tag], { check: false, capture: true });
1118
+ if (first.returncode === 0) {
1119
+ process.stdout.write(first.stdout);
1120
+ process.stderr.write(first.stderr);
1121
+ return;
1122
+ }
1123
+ if (_is_tag_already_exists(first.stderr, first.stdout)) {
1124
+ const remote = _remote_tag_commit(tag);
1125
+ const local = git(['rev-list', '-n', '1', tag], { capture: true });
1126
+ if (remote !== null && remote === local) {
1127
+ process.stdout.write(
1128
+ `↻ tag ${tag} already on ${REMOTE} at the same commit — a parallel run pushed it; continuing\n`,
1129
+ );
1130
+ return;
1131
+ }
1132
+ process.stdout.write(first.stdout);
1133
+ process.stderr.write(first.stderr);
1134
+ die(
1135
+ `tag ${tag} exists on ${REMOTE} pointing at ${remote ?? '<unreadable>'} while the local tag ` +
1136
+ `points at ${local} — refusing to overwrite a published tag; ` +
1137
+ 'delete the wrong one deliberately and re-run',
1138
+ );
1139
+ }
1140
+ process.stdout.write(first.stdout);
1141
+ process.stderr.write(first.stderr);
1142
+ die(
1143
+ `push of tag ${tag} failed (exit ${first.returncode}) — ` +
1144
+ 'the push output above is the real error (a pre-push gate, credentials, or protection)',
1145
+ );
1146
+ }
1147
+
1076
1148
  /** Most recent PR (any state) with `release/X.Y.Z` as head, or null. */
1077
1149
  function _pr_for_branch(branch: string): Record<string, unknown> | null {
1078
1150
  const r = run(
@@ -2029,7 +2101,7 @@ function execute(
2029
2101
  _step(8, total, `Tag ${plan.target} already on ${REMOTE} — skip`);
2030
2102
  } else {
2031
2103
  _step(8, total, `Tag ${plan.target} exists locally — push only`);
2032
- run(['git', 'push', REMOTE, plan.target]);
2104
+ _push_tag(plan.target);
2033
2105
  }
2034
2106
  } else {
2035
2107
  // Sequencing is load-bearing (release-truth Phase 1, council
@@ -2047,7 +2119,7 @@ function execute(
2047
2119
  );
2048
2120
  }
2049
2121
  run(['git', 'tag', '-a', plan.target, '-m', tag_message_from_section(merged!.body, plan.target)]);
2050
- run(['git', 'push', REMOTE, plan.target]);
2122
+ _push_tag(plan.target);
2051
2123
  }
2052
2124
 
2053
2125
  // ─── 9. GitHub Release ──────────────────────────────────────────────────
@@ -0,0 +1,284 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * report_conformance_funnel.ts — delivery → activation → compliance, one view.
4
+ *
5
+ * ⚠️ REPORT ONLY — THIS SCRIPT MUST NEVER BE WIRED INTO A CI WORKFLOW WITHOUT
6
+ * A MEASURED FALSE-POSITIVE RATE FIRST (locked policy; council requirement,
7
+ * road-to-feedback-9-29 Phase 4.2). Every source it joins is itself advisory
8
+ * and says so; a join of advisories does not become a gate by aggregation.
9
+ *
10
+ * Executes road-to-feedback-9-29 Phase 4.2: one report join over the existing
11
+ * conformance sources, so the three questions that were answered in three
12
+ * places read as the funnel they are:
13
+ *
14
+ * DELIVERY what rule text the carriers put in front of the model, and
15
+ * whether the two carriers agree —
16
+ * `conformance_scan.measureDelivered` (carrier census) +
17
+ * `report_carrier_divergence.compareCarriers` (divergence).
18
+ * ACTIVATION whether the shipped skills are invoked at all —
19
+ * `report_skill_activation.censusSkills` + `measureUsage`.
20
+ * COMPLIANCE whether a loaded skill's obligations were violated (SK-2) —
21
+ * `report_skill_obligation_violations.scanStore`.
22
+ *
23
+ * JOINS, NEVER RE-DERIVES. Every number below comes from an EXPORTED function
24
+ * of the source that owns it. A second copy of a classifier is the "second
25
+ * artefact to keep in sync" this repo forbids — it is how the 8 → 30 spread
26
+ * and the 303-vs-626 detector defect happened, and both are cited in the
27
+ * sources this file imports.
28
+ *
29
+ * Each axis also carries the honest-scope statement its source already
30
+ * carries (SK-2's mechanisable-subset ratio, activation's catalogue-not-
31
+ * persisted caveat, delivery's not-per-session caveat). Where a source has no
32
+ * local data, the axis prints its honest "no data" line rather than
33
+ * disappearing — an absent store is an answer, not a zero.
34
+ *
35
+ * Reached via `agent-config conformance --funnel` (no new top-level verb —
36
+ * the flag hangs off the existing `conformance` verb per the dispatcher-verb
37
+ * registry trap named in the roadmap's risk table).
38
+ *
39
+ * Exit: 0 always, except a usage error (1). Deliberate — see the header.
40
+ */
41
+ import * as fs from 'node:fs';
42
+ import * as os from 'node:os';
43
+ import * as path from 'node:path';
44
+ import process from 'node:process';
45
+ import { fileURLToPath, pathToFileURL } from 'node:url';
46
+
47
+ import {
48
+ measureDelivered,
49
+ defaultStore,
50
+ type DeliveredPayload,
51
+ } from './conformance_scan.js';
52
+ import {
53
+ compareCarriers,
54
+ GLOBAL_RULES,
55
+ PROJECT_RULES,
56
+ PROJECTION_SOURCE,
57
+ type CarrierDivergence,
58
+ } from './report_carrier_divergence.js';
59
+ import {
60
+ censusSkills,
61
+ measureUsage,
62
+ SKILLS_ROOT,
63
+ type SkillCensus,
64
+ type UsageReport,
65
+ } from './report_skill_activation.js';
66
+ import { scanStore as scanObligationStore, type Sk2Report } from './report_skill_obligation_violations.js';
67
+
68
+ const _HERE = fileURLToPath(import.meta.url);
69
+ const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
70
+
71
+ export interface FunnelOptions {
72
+ /** The checkout whose skills and projection source are measured. */
73
+ repoRoot?: string;
74
+ /** The project rule carrier — what the host reads at project scope. */
75
+ projectRulesDir?: string;
76
+ /** The machine-global rule carrier. */
77
+ globalRulesDir?: string;
78
+ /** The projection source, read only to attribute manual-only absences. */
79
+ sourceDir?: string;
80
+ /** Transcript store for the activation and compliance axes. */
81
+ store?: string;
82
+ /** Most-recent sessions to scan. */
83
+ limit?: number;
84
+ }
85
+
86
+ export interface FunnelReport {
87
+ /** DELIVERY — carrier census (conformance_scan) + divergence (report_carrier_divergence). */
88
+ delivered: DeliveredPayload;
89
+ divergence: CarrierDivergence;
90
+ /** ACTIVATION — skill census + usage (report_skill_activation). */
91
+ skills: SkillCensus;
92
+ usage: UsageReport;
93
+ /** `false` is the no-data branch for both transcript-fed axes. */
94
+ storePresent: boolean;
95
+ /** COMPLIANCE — SK-2 loaded-but-violated (report_skill_obligation_violations). */
96
+ sk2: Sk2Report;
97
+ }
98
+
99
+ export function buildFunnel(opts: FunnelOptions = {}): FunnelReport {
100
+ const repoRoot = opts.repoRoot ?? REPO_ROOT;
101
+ const projectRulesDir = opts.projectRulesDir ?? path.join(repoRoot, PROJECT_RULES);
102
+ const globalRulesDir = opts.globalRulesDir ?? path.join(os.homedir(), GLOBAL_RULES);
103
+ const sourceDir = opts.sourceDir ?? path.join(repoRoot, PROJECTION_SOURCE);
104
+ const store = opts.store ?? defaultStore(repoRoot);
105
+ const limit = opts.limit ?? 30;
106
+ return {
107
+ delivered: measureDelivered(projectRulesDir, globalRulesDir),
108
+ divergence: compareCarriers(projectRulesDir, globalRulesDir, sourceDir),
109
+ skills: censusSkills(path.join(repoRoot, SKILLS_ROOT)),
110
+ usage: measureUsage(store, limit),
111
+ storePresent: fs.existsSync(store),
112
+ sk2: scanObligationStore(repoRoot, store, limit),
113
+ };
114
+ }
115
+
116
+ function _pct(n: number, d: number): string {
117
+ return d === 0 ? 'n/a' : `${((100 * n) / d).toFixed(1)}%`;
118
+ }
119
+
120
+ function _carrierLine(label: string, c: { present: boolean; files: number; tokens: number }): string {
121
+ return c.present
122
+ ? ` ${label} ${String(c.files).padStart(4)} rules ${String(c.tokens).padStart(7)} tok`
123
+ : ` ${label} ABSENT — not a measured zero, this carrier does not exist here`;
124
+ }
125
+
126
+ export function render(r: FunnelReport): string {
127
+ const lines: string[] = [];
128
+ lines.push('conformance funnel — delivery → activation → compliance (report, not a gate)');
129
+ lines.push('');
130
+
131
+ // ── DELIVERY ───────────────────────────────────────────────────────────
132
+ lines.push('DELIVERY — rule text the carriers put in front of the model');
133
+ lines.push(_carrierLine('project', r.delivered.project));
134
+ lines.push(_carrierLine('global ', r.delivered.global));
135
+ lines.push(` union ${String(r.delivered.union_tokens).padStart(7)} tok`);
136
+ lines.push(' Carriers as they stand NOW — a session\'s own payload is NOT recoverable:');
137
+ lines.push(' the transcript records no system or tools field (conformance_scan).');
138
+ const d = r.divergence;
139
+ if (!d.globalPresent) {
140
+ lines.push(' Divergence: the global carrier is absent — this machine loads the project');
141
+ lines.push(' projection only, so there is no second copy to diverge from. A real');
142
+ lines.push(' answer, not a skipped check.');
143
+ } else if (!d.projectPresent) {
144
+ lines.push(' Divergence: the project rule tree does not exist in this checkout — it is');
145
+ lines.push(' generated and gitignored. Run `task generate-tools`, then re-run. Not');
146
+ lines.push(' substituted with dist/, which would answer a different question.');
147
+ } else {
148
+ lines.push(
149
+ ` Divergence: ${d.shared} shared · ${d.bodyDiff.length} body-diff · ` +
150
+ `${d.provenanceOnly.length} provenance-only · ${d.projectOnly.length} project-only · ` +
151
+ `${d.globalOnly.length} global-only` +
152
+ (d.manualOnlyGlobal.length > 0 ? ` (${d.manualOnlyGlobal.length} by design, ADR-004 manual)` : ''),
153
+ );
154
+ if (d.bodyDiff.length > 0) {
155
+ lines.push(` BODY DIVERGENCE — both copies reach the model: ${d.bodyDiff.join(', ')}`);
156
+ lines.push(' Precedence: the project projection is generated from src/ at this commit');
157
+ lines.push(' and wins; the global install is a release snapshot.');
158
+ }
159
+ if (d.unreadable.length > 0) {
160
+ lines.push(` Unreadable on one side (${d.unreadable.length}) — a broken install, not a disagreement.`);
161
+ }
162
+ }
163
+ lines.push('');
164
+
165
+ // ── ACTIVATION ─────────────────────────────────────────────────────────
166
+ lines.push('ACTIVATION — are the delivered skills invoked?');
167
+ lines.push(` skills shipped ${r.skills.total}`);
168
+ lines.push(
169
+ ` with a machine-matchable trigger key ${r.skills.withTriggerKey.length} ` +
170
+ `(${_pct(r.skills.withTriggerKey.length, r.skills.total)})`,
171
+ );
172
+ if (!r.storePresent) {
173
+ lines.push(` NO DATA: no transcript store at ${r.usage.store} — usage is not`);
174
+ lines.push(' measurable here. An absent store is an answer, not a zero.');
175
+ } else {
176
+ const distinct = Object.keys(r.usage.bySkill).length;
177
+ lines.push(
178
+ ` sessions=${r.usage.sessions} assistant turns=${r.usage.assistantTurns} ` +
179
+ `Skill invocations=${r.usage.invocations} distinct skills=${distinct} of ${r.skills.total} ` +
180
+ `(${_pct(distinct, r.skills.total)})`,
181
+ );
182
+ }
183
+ if (r.skills.withTriggerKey.length === 0) {
184
+ lines.push(' Honest scope: activation is NOT measurable as a rate — no skill declares');
185
+ lines.push(' a machine-matchable trigger, and matching description prose is the class');
186
+ lines.push(' this suite excludes (report_skill_activation).');
187
+ }
188
+ lines.push(' NOT MEASURED: whether each skill reached the model WITH its description —');
189
+ lines.push(' the host\'s injected catalogue is not persisted in the transcript, so the');
190
+ lines.push(' bare-name (truncation) hypothesis stays a single-session observation.');
191
+ lines.push('');
192
+
193
+ // ── COMPLIANCE ─────────────────────────────────────────────────────────
194
+ const c = r.sk2.census;
195
+ lines.push('COMPLIANCE — SK-2 loaded-but-violated (coverage is the headline)');
196
+ lines.push(` skills with a deterministic obligation ${c.skills.length}`);
197
+ lines.push(
198
+ ` testable without judgement ${c.forbidden.length} artefact(s) over ` +
199
+ `${c.totalLines} obligation line(s)`,
200
+ );
201
+ lines.push(' The rest are deterministic in WORDING and need a reading to test — the');
202
+ lines.push(' FC-8 class this suite excludes, reported as uncovered, not approximated');
203
+ lines.push(' (report_skill_obligation_violations).');
204
+ if (!r.storePresent) {
205
+ lines.push(' NO DATA: no transcript store — loaded-but-violated cannot be observed');
206
+ lines.push(' here. The coverage census above is static and still holds.');
207
+ } else {
208
+ lines.push(
209
+ ` sessions scanned=${r.sk2.sessions} with a skill in context=${r.sk2.sessionsWithASkill} ` +
210
+ `flags=${r.sk2.flags.length}`,
211
+ );
212
+ for (const f of r.sk2.flags) {
213
+ lines.push(` ${f.session} [${f.skill}] ${f.artefact} via ${f.tool}`);
214
+ }
215
+ }
216
+ lines.push(' PRECISION: every flag is hand-read before any number here is cited — the');
217
+ lines.push(' detector cannot state its false-positive rate, so it ships detection-only.');
218
+ lines.push('');
219
+
220
+ lines.push('This is a REPORT. It gates on nothing and must never be wired into a CI');
221
+ lines.push('workflow without a measured false-positive rate first (locked policy).');
222
+ return lines.join('\n');
223
+ }
224
+
225
+ export function main(argv?: readonly string[]): number {
226
+ const args = argv ?? process.argv.slice(2);
227
+ const opts: FunnelOptions = {};
228
+ // A value must not itself be a flag — the same hole the sibling reports
229
+ // close on both the flag-name and flag-value halves.
230
+ const value = (i: number): string | null => {
231
+ const v = args[i + 1];
232
+ return v === undefined || v.startsWith('-') ? null : v;
233
+ };
234
+ for (let i = 0; i < args.length; i += 1) {
235
+ const a = args[i];
236
+ if (a === '--store' || a === '--project' || a === '--global' || a === '--source') {
237
+ const v = value(i);
238
+ if (v === null) {
239
+ process.stderr.write(`report_conformance_funnel: ${a} needs a value\n`);
240
+ return 1;
241
+ }
242
+ const resolved = path.resolve(v);
243
+ if (a === '--store') opts.store = resolved;
244
+ else if (a === '--project') opts.projectRulesDir = resolved;
245
+ else if (a === '--global') opts.globalRulesDir = resolved;
246
+ else opts.sourceDir = resolved;
247
+ i += 1;
248
+ } else if (a === '--limit') {
249
+ const v = value(i);
250
+ const n = v === null ? Number.NaN : Number.parseInt(v, 10);
251
+ if (!Number.isFinite(n) || n <= 0) {
252
+ process.stderr.write('report_conformance_funnel: --limit must be a positive integer\n');
253
+ return 1;
254
+ }
255
+ opts.limit = n;
256
+ i += 1;
257
+ } else if (a === '--help' || a === '-h') {
258
+ process.stdout.write(
259
+ 'usage: report_conformance_funnel [--store PATH] [--limit N]\n' +
260
+ ' [--project DIR] [--global DIR] [--source DIR]\n',
261
+ );
262
+ return 0;
263
+ } else if (a !== undefined && a.startsWith('--') && a !== '--quiet') {
264
+ process.stderr.write(`report_conformance_funnel: unknown flag ${a}\n`);
265
+ return 1;
266
+ }
267
+ }
268
+ process.stdout.write(`${render(buildFunnel(opts))}\n`);
269
+ return 0;
270
+ }
271
+
272
+ function _isCliEntry(): boolean {
273
+ if (process.argv[1] === undefined) return false;
274
+ if (import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) return true;
275
+ try {
276
+ return fs.realpathSync(_HERE) === fs.realpathSync(path.resolve(process.argv[1]));
277
+ } catch {
278
+ return false;
279
+ }
280
+ }
281
+
282
+ if (_isCliEntry()) {
283
+ process.exit(main());
284
+ }
@@ -32,6 +32,7 @@ import * as fs from 'node:fs';
32
32
  import * as path from 'node:path';
33
33
  import { fileURLToPath, pathToFileURL } from 'node:url';
34
34
 
35
+ // ledger-exempt: advisory report that gates on nothing BY DESIGN (exit 0 always, no threshold, no baseline entry, no gate-coverage row — see the header); there is no pass/fail outcome per target to account, and acquiring one would be the promotion this report must not undergo.
35
36
  const _HERE = fileURLToPath(import.meta.url);
36
37
  const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
37
38