@event4u/agent-config 9.35.0 → 10.0.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 (117) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +95 -142
  3. package/agents/templates/.ai-council.yml.example +31 -51
  4. package/dist/agent-src/commands/analyze/conformance.md +15 -4
  5. package/dist/agent-src/commands/analyze/reference-repo.md +135 -42
  6. package/dist/agent-src/contexts/augment-infrastructure.md +1 -1
  7. package/dist/agent-src/rules/design-review-after-ui-write.md +53 -6
  8. package/dist/agent-src/rules/improve-before-implement.md +1 -1
  9. package/dist/agent-src/rules/session-canary.md +28 -0
  10. package/dist/agent-src/rules/ui-audit-gate.md +35 -0
  11. package/dist/agent-src/skills/design-system-capture/SKILL.md +2 -2
  12. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
  13. package/dist/agent-src/skills/fe-design/SKILL.md +84 -12
  14. package/dist/agent-src/skills/fe-design/evals/triggers.json +11 -3
  15. package/dist/agent-src/skills/prediction-pool-optimizer/SKILL.md +5 -5
  16. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
  17. package/dist/agent-src/skills/skill-writing/SKILL.md +24 -0
  18. package/dist/agent-src/skills/subagent-orchestration/prompts/README.md +29 -0
  19. package/dist/agent-src/templates/AGENTS.md +1 -1
  20. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  21. package/dist/cli-delegate/{chunk-2TGMGLBL.js → chunk-2H5SE7DU.js} +35 -5
  22. package/dist/cli-delegate/{chunk-DS6BLDMG.js → chunk-3JUTT26I.js} +8 -3
  23. package/dist/cli-delegate/{chunk-ZAXH3ZWU.js → chunk-HJSKTMIF.js} +14 -13
  24. package/dist/cli-delegate/{chunk-QEEB4LHO.js → chunk-LRXJK6GN.js} +1 -1
  25. package/dist/cli-delegate/{chunk-736JCWLM.js → chunk-M2DPGFZ4.js} +10 -0
  26. package/dist/cli-delegate/{chunk-H6F2CQFI.js → chunk-NX5AXSLD.js} +3 -3
  27. package/dist/cli-delegate/{chunk-AG47LAGQ.js → chunk-TVGXH7VA.js} +1 -1
  28. package/dist/cli-delegate/{chunk-55RI5UXK.js → chunk-XT2YTJCZ.js} +22 -18
  29. package/dist/cli-delegate/{chunk-JEJABGKB.js → chunk-XTC5ARB6.js} +16 -12
  30. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  31. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  32. package/dist/cli-delegate/cmd_conformance.js +1038 -197
  33. package/dist/cli-delegate/cmd_doctor.js +4 -4
  34. package/dist/cli-delegate/cmd_explain.js +2 -2
  35. package/dist/cli-delegate/cmd_export.js +4 -4
  36. package/dist/cli-delegate/cmd_handoff.js +3 -3
  37. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  38. package/dist/cli-delegate/cmd_migrate.js +2 -2
  39. package/dist/cli-delegate/cmd_packs_active.js +2 -2
  40. package/dist/cli-delegate/cmd_prune.js +4 -4
  41. package/dist/cli-delegate/cmd_refresh.js +1 -1
  42. package/dist/cli-delegate/cmd_route_audit.js +2 -2
  43. package/dist/cli-delegate/cmd_route_explain.js +1 -1
  44. package/dist/cli-delegate/cmd_session_recycle.js +9 -2
  45. package/dist/cli-delegate/cmd_settings_get.js +1 -1
  46. package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
  47. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  48. package/dist/cli-delegate/cmd_sync.js +4 -4
  49. package/dist/cli-delegate/cmd_uninstall.js +4 -4
  50. package/dist/cli-delegate/cmd_update.js +1 -1
  51. package/dist/cli-delegate/cmd_upgrade.js +2 -2
  52. package/dist/cli-delegate/cmd_validate.js +4 -4
  53. package/dist/cli-delegate/cmd_versions.js +1 -1
  54. package/dist/discovery/deprecation-report.md +1 -1
  55. package/dist/discovery/discovery-manifest.json +14 -14
  56. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  57. package/dist/discovery/discovery-manifest.summary.md +1 -1
  58. package/dist/discovery/dormancy-report.md +1 -1
  59. package/dist/discovery/orphan-report.md +1 -1
  60. package/dist/discovery/packs.json +2 -2
  61. package/dist/discovery/trust-report.md +1 -1
  62. package/dist/discovery/workspaces.json +2 -2
  63. package/dist/hooks/dispatch.js +654 -411
  64. package/dist/install/install.mjs +39 -29
  65. package/dist/mcp/registry-manifest.json +1 -1
  66. package/dist/mcp/server.json +2 -2
  67. package/dist/mcp/server.mjs +1580 -617
  68. package/dist/router.json +1 -1
  69. package/dist/scripts/_lib/agent_settings.js +39 -25
  70. package/dist/scripts/_lib/agent_settings.js.map +1 -1
  71. package/dist/scripts/condense.js +15 -2
  72. package/dist/scripts/condense.js.map +1 -1
  73. package/dist/server/routes/wizard.js +23 -3
  74. package/dist/server/routes/wizard.js.map +1 -1
  75. package/dist/server/schemas/settings.js +11 -6
  76. package/dist/server/schemas/settings.js.map +1 -1
  77. package/docs/contracts/settings-classes.md +16 -9
  78. package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +46 -4
  79. package/package.json +2 -2
  80. package/src/agent-src/templates/AGENTS.md +1 -1
  81. package/src/config/agent-settings.template.yml +38 -36
  82. package/src/config/ci-local-parity.yml +44 -0
  83. package/src/config/gate-coverage.yml +12 -0
  84. package/src/scripts/_cli/cmd_doctor.ts +32 -0
  85. package/src/scripts/_cli/cmd_migrate.ts +26 -3
  86. package/src/scripts/_cli/cmd_refresh.ts +29 -0
  87. package/src/scripts/_cli/cmd_session_recycle.ts +40 -2
  88. package/src/scripts/_lib/agent_settings.ts +39 -25
  89. package/src/scripts/_lib/environment_detector.ts +52 -1
  90. package/src/scripts/_lib/hook_settings.ts +79 -0
  91. package/src/scripts/_lib/ui_surface.ts +97 -0
  92. package/src/scripts/ai_council/config.ts +62 -24
  93. package/src/scripts/before_complete_hook.ts +39 -0
  94. package/src/scripts/capture_skill_catalogue.ts +483 -0
  95. package/src/scripts/check_bridge_derivation.ts +89 -3
  96. package/src/scripts/check_ci_local_parity.ts +74 -0
  97. package/src/scripts/check_references.ts +15 -1
  98. package/src/scripts/condense.ts +16 -2
  99. package/src/scripts/conformance_scan.ts +200 -9
  100. package/src/scripts/council_cli.ts +95 -3
  101. package/src/scripts/hook_manifest.yaml +30 -9
  102. package/src/scripts/hooks/block_no_verify.ts +82 -9
  103. package/src/scripts/hooks/concern_registry.ts +2 -0
  104. package/src/scripts/hooks/design_slop_hook.ts +1 -1
  105. package/src/scripts/hooks/turn_end_gate_hook.ts +138 -105
  106. package/src/scripts/hooks/ui_route_nudge_hook.ts +250 -0
  107. package/src/scripts/lint_roadmap_complexity.ts +60 -0
  108. package/src/scripts/lint_rule_skill_pack_reach.ts +263 -0
  109. package/src/scripts/probe_promissory_closing.ts +180 -0
  110. package/src/scripts/probe_session_canary.ts +180 -0
  111. package/src/scripts/report_consultation_rate.ts +358 -0
  112. package/src/scripts/report_skill_activation.ts +5 -1
  113. package/src/server/schemas/settings.ts +14 -13
  114. /package/dist/agent-src/skills/design-system-capture/{reference → references}/design-system-json.md +0 -0
  115. /package/dist/agent-src/skills/prediction-pool-optimizer/{reference → references}/ev-fixtures.md +0 -0
  116. /package/dist/agent-src/skills/prediction-pool-optimizer/{reference → references}/odds-and-bonus.md +0 -0
  117. /package/dist/agent-src/skills/react-shadcn-ui/{reference → references}/registry.md +0 -0
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Round 7 § 6.3 — the promissory-closing rate, as a script instead of an ad-hoc probe.
4
+ *
5
+ * `verify-before-complete` § Turn-completion forbids ending a turn on a promise of
6
+ * unexecuted work ("I'll…", "als nächstes mache ich…"). Round 7 measured the rate
7
+ * to decide whether to build a mechanism, and the answer was: don't — a blocking
8
+ * `detectPromissory` ALREADY ships in `turn-end-gate`.
9
+ *
10
+ * That inverts the reading of a low number, and the inversion is the reason this
11
+ * script exists. Round 7's first draft declined the class as "below any threshold
12
+ * worth a gate"; with a gate already in place, 0.6 % is plausibly the mechanism
13
+ * WORKING, not the class being rare. A rate over a gated class measures residual
14
+ * leakage. Read it that way or not at all.
15
+ *
16
+ * WHAT IS COUNTED: only a HAND-BACK turn — an assistant turn immediately followed
17
+ * by a genuine user turn, i.e. where the agent actually stopped. A promise in a
18
+ * mid-turn message is not a violation; the agent kept going. A turn that ASKS is
19
+ * also excluded: a question is a legitimate stop condition.
20
+ *
21
+ * HONEST BOUND: the predicate is deliberately narrow (first-person + future,
22
+ * anchored in the closing lines), so the figure is a FLOOR. A looser predicate
23
+ * over the same corpus read 1.2 % against this one's 0.6 %; both are printed so a
24
+ * reader can see the width of the bracket rather than trust one edge of it.
25
+ *
26
+ * Usage:
27
+ * ./scripts-run src/scripts/probe_promissory_closing [--limit N] [--store PATH]
28
+ *
29
+ * Exit codes: 0 always — a measurement, not a gate.
30
+ */
31
+ import fs from "node:fs";
32
+ import path from "node:path";
33
+
34
+ import { defaultStore, HARNESS_TEXT, isInjectedBody } from "./conformance_scan.js";
35
+ import { isSyntheticPrompt } from "./_lib/prompt_shape.js";
36
+
37
+ /** First person + intent, at the start of a closing sentence. Narrow on purpose. */
38
+ export const PROMISE_NARROW: readonly RegExp[] = [
39
+ /\bich (mache|baue|schreibe|starte|prüfe|räume|setze|lege|fixe|committe|pushe|nehme|ziehe|hole|führe|erstelle|implementiere)\b[^.?!]{0,80}\b(jetzt|als nächstes|gleich|danach|anschließend|im nächsten schritt)\b/i,
40
+ /\b(als nächstes|im nächsten schritt|danach) (mache|baue|schreibe|starte|prüfe|nehme|committe|pushe|erstelle|implementiere) ich\b/i,
41
+ /\bi'?(ll| will) (now |next |then )?(start|build|write|check|run|fix|commit|push|create|implement|add|wire|measure)\b/i,
42
+ /\bnext,? i'?(ll| will)\b/i,
43
+ /\bi'?m going to (start|build|write|check|run|fix|commit|push|create|implement)\b/i,
44
+ ];
45
+
46
+ /** The bracket's other edge — any future-intent marker at all. */
47
+ export const PROMISE_LOOSE =
48
+ /\b(ich werde|ich mache|ich baue|ich starte|ich prüfe|als nächstes|next up|i'll |i will |i'm going to|nun mache ich|jetzt mache ich)/i;
49
+
50
+ /** A reply that asks is a legitimate stop condition, never a promissory closing. */
51
+ const ASKS = /(\?\s*$)|(^\s*\d\.\s)/m;
52
+
53
+ interface Turn {
54
+ role: "user" | "assistant";
55
+ text: string;
56
+ at: string;
57
+ }
58
+
59
+ function turns(lines: string[]): Turn[] {
60
+ const out: Turn[] = [];
61
+ for (const line of lines) {
62
+ if (!line.trim()) continue;
63
+ let e: Record<string, unknown>;
64
+ try {
65
+ e = JSON.parse(line) as Record<string, unknown>;
66
+ } catch {
67
+ continue;
68
+ }
69
+ if (e["isSidechain"] === true) continue;
70
+ if (e["type"] === "user" && e["toolUseResult"] !== undefined) continue;
71
+ if (e["isCompactSummary"] === true) continue;
72
+ const msg = e["message"] as { role?: string; content?: unknown } | undefined;
73
+ const role = msg?.role;
74
+ if (role !== "user" && role !== "assistant") continue;
75
+ const c = msg?.content;
76
+ const text =
77
+ typeof c === "string"
78
+ ? c
79
+ : Array.isArray(c)
80
+ ? c
81
+ .filter((b: { type?: string }) => b?.type === "text")
82
+ .map((b: { text?: string }) => b.text ?? "")
83
+ .join("\n")
84
+ : "";
85
+ if (!text.trim()) continue;
86
+ if (role === "assistant" && HARNESS_TEXT.test(text.trim())) continue;
87
+ // A skill / slash-command body arrives in the user role and is not a chat
88
+ // message, so it must not close a hand-back.
89
+ if (role === "user" && (isSyntheticPrompt(text) || isInjectedBody(text))) continue;
90
+ out.push({ role, text, at: String(e["timestamp"] ?? "") });
91
+ }
92
+ return out;
93
+ }
94
+
95
+ export interface PromissoryResult {
96
+ /** Hand-back turns that could carry a promissory closing — asking ones excluded. */
97
+ handbacks: number;
98
+ /** Asking hand-backs, excluded from the population. R2 finding 6. */
99
+ asked: number;
100
+ narrow: { session: string; at: string; span: string }[];
101
+ loose: number;
102
+ }
103
+
104
+ export function measure(store: string, limit: number): PromissoryResult {
105
+ const result: PromissoryResult = { handbacks: 0, asked: 0, narrow: [], loose: 0 };
106
+ const files = fs
107
+ .readdirSync(store)
108
+ .filter((f) => f.endsWith(".jsonl"))
109
+ .map((f) => ({ f, m: fs.statSync(path.join(store, f)).mtimeMs }))
110
+ .sort((a, b) => b.m - a.m)
111
+ .slice(0, limit);
112
+
113
+ for (const { f } of files) {
114
+ const session = f.replace(/\.jsonl$/, "").slice(0, 8);
115
+ const ts = turns(fs.readFileSync(path.join(store, f), "utf8").split("\n"));
116
+ for (let i = 0; i < ts.length; i += 1) {
117
+ const t = ts[i] as Turn;
118
+ if (t.role !== "assistant") continue;
119
+ const next = ts[i + 1];
120
+ // A hand-back: the next turn is the user's, or the session ends here.
121
+ if (next !== undefined && next.role !== "user") continue;
122
+ const tail = t.text.trimEnd().split("\n").slice(-4).join("\n");
123
+ // R2 finding 6: `handbacks` was incremented BEFORE this guard, so every
124
+ // asking hand-back sat in the denominator while being excluded from both
125
+ // numerators — understating both rates by the ask share. An asking closing
126
+ // is a legitimate stop condition and cannot be a promissory closing, so it
127
+ // is not in the population at all. Counted separately, because a reader who
128
+ // cannot see the ask share cannot judge the denominator.
129
+ if (ASKS.test(tail)) {
130
+ result.asked += 1;
131
+ continue;
132
+ }
133
+ result.handbacks += 1;
134
+ if (PROMISE_LOOSE.test(tail)) result.loose += 1;
135
+ const hit = PROMISE_NARROW.find((re) => re.test(tail));
136
+ if (hit) {
137
+ result.narrow.push({ session, at: t.at, span: (tail.match(hit)?.[0] ?? "").slice(0, 110) });
138
+ }
139
+ }
140
+ }
141
+ return result;
142
+ }
143
+
144
+ export function main(argv?: string[]): number {
145
+ const args = argv ?? process.argv.slice(2);
146
+ const arg = (flag: string, dflt: string): string => {
147
+ const i = args.indexOf(flag);
148
+ return i >= 0 && args[i + 1] !== undefined ? (args[i + 1] as string) : dflt;
149
+ };
150
+ const store = arg("--store", defaultStore(process.cwd()));
151
+ const limit = Number(arg("--limit", "30"));
152
+
153
+ if (!fs.existsSync(store)) {
154
+ process.stdout.write(`probe:promissory-closing · store not found: ${store}\n`);
155
+ return 0;
156
+ }
157
+ const r = measure(store, limit);
158
+ process.stdout.write(
159
+ `probe:promissory-closing · ${r.handbacks} eligible hand-back turn(s) ` +
160
+ `(+${r.asked} that ASK, excluded — a question is a legitimate stop condition) in ${store}\n`,
161
+ );
162
+ for (const h of r.narrow) {
163
+ process.stdout.write(` ${h.session} ${h.at.slice(0, 16)} ${JSON.stringify(h.span)}\n`);
164
+ }
165
+ const pct = (n: number): string => (r.handbacks === 0 ? "—" : `${((100 * n) / r.handbacks).toFixed(1)}%`);
166
+ process.stdout.write(
167
+ ` narrow predicate: ${r.narrow.length} (${pct(r.narrow.length)}) · ` +
168
+ `loose predicate: ${r.loose} (${pct(r.loose)})\n`,
169
+ );
170
+ process.stdout.write(
171
+ " The class is ALREADY GATED — `turn-end-gate` ships a blocking `detectPromissory`.\n" +
172
+ " So a low figure here is residual leakage past a working mechanism, not evidence\n" +
173
+ " the class is rare. The narrow figure is a floor; the loose one is the other edge.\n",
174
+ );
175
+ return 0;
176
+ }
177
+
178
+ if (process.argv[1] !== undefined && import.meta.url.endsWith(path.basename(process.argv[1]))) {
179
+ process.exit(main());
180
+ }
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Round 7 § 6.3 — the opening-canary rate, as a script instead of an ad-hoc probe.
4
+ *
5
+ * `session-canary` obliges the agent to address the user by name in the first
6
+ * reply of a session AND of each new task within it. The rule's own text records
7
+ * two audits reading it as broadly missed (~13/15, then 24/29 task starts) and
8
+ * concludes the reminder carrier does not move it.
9
+ *
10
+ * This measures the half that is DECIDABLE from a transcript: the per-SESSION
11
+ * instance. The per-TASK instance is not measurable at all — no task boundary is
12
+ * recorded anywhere in a transcript — which is why the mechanism the rule
13
+ * pre-registers ("a check at delivery that rejects a task-start reply carrying no
14
+ * greeting") cannot be built as specified. Round 7 states that as an honest
15
+ * downgrade rather than an unbuilt promise.
16
+ *
17
+ * Two instrument defects this probe was born with, fixed and recorded:
18
+ * 1. Its first version omitted `You've hit your` from the harness predicate, so
19
+ * a spend-limit banner counted as a missed greeting: 24/28 instead of 25/28.
20
+ * The predicate is now the same one `conformance_scan` uses.
21
+ * 2. It reads the first assistant turn carrying PROSE, not the first assistant
22
+ * entry: a session opening with 30 tool calls has no prose to greet in.
23
+ *
24
+ * Usage:
25
+ * ./scripts-run src/scripts/probe_session_canary [--limit N] [--store PATH] [--name NAME]
26
+ *
27
+ * Exit codes: 0 always — this is a measurement, not a gate. A rate is not a
28
+ * verdict, and the round-7 reading (96 % post-carrier) is not a number anyone
29
+ * should gate on before it has a second window to compare against.
30
+ */
31
+ import fs from "node:fs";
32
+ import path from "node:path";
33
+
34
+ import { load_agent_settings } from "./_lib/agent_settings.js";
35
+ import { defaultStore, HARNESS_TEXT } from "./conformance_scan.js";
36
+
37
+ /** The first assistant turn that carries prose, or null. */
38
+ function firstProse(lines: string[]): { text: string; at: string } | null {
39
+ for (const line of lines) {
40
+ if (!line.trim()) continue;
41
+ let e: Record<string, unknown>;
42
+ try {
43
+ e = JSON.parse(line) as Record<string, unknown>;
44
+ } catch {
45
+ continue;
46
+ }
47
+ if (e["isSidechain"] === true) continue;
48
+ const msg = e["message"] as { role?: string; content?: unknown } | undefined;
49
+ if (msg?.role !== "assistant") continue;
50
+ const c = msg.content;
51
+ if (!Array.isArray(c)) continue;
52
+ const text = c
53
+ .filter((b: { type?: string }) => b?.type === "text")
54
+ .map((b: { text?: string }) => b.text ?? "")
55
+ .join("\n")
56
+ .trim();
57
+ if (!text) continue;
58
+ // A harness banner occupies an assistant turn without being the assistant
59
+ // writing. Same predicate as the scan's, imported rather than re-typed.
60
+ if (HARNESS_TEXT.test(text)) continue;
61
+ return { text, at: String(e["timestamp"] ?? "") };
62
+ }
63
+ return null;
64
+ }
65
+
66
+ /**
67
+ * The name the obligation actually uses, read through the same three layers the
68
+ * rule names: project `personal.canary_name` → user-global `personal.canary_name`
69
+ * → user-global `identity.name`. Empty string when no layer carries one, which is
70
+ * exactly when the rule is inert.
71
+ *
72
+ * Deliberately tolerant of every read failure: a probe that throws because a
73
+ * settings file is malformed measures the file, not the behaviour.
74
+ */
75
+ export function resolveCanaryName(): string {
76
+ try {
77
+ const s = load_agent_settings({ cwd: process.cwd() }) as Record<string, unknown>;
78
+ const personal = s["personal"];
79
+ if (typeof personal === "object" && personal !== null && !Array.isArray(personal)) {
80
+ const n = (personal as Record<string, unknown>)["canary_name"];
81
+ if (typeof n === "string" && n.trim() !== "") return n.trim();
82
+ }
83
+ const identity = s["identity"];
84
+ if (typeof identity === "object" && identity !== null && !Array.isArray(identity)) {
85
+ const n = (identity as Record<string, unknown>)["name"];
86
+ if (typeof n === "string" && n.trim() !== "") return n.trim();
87
+ }
88
+ } catch {
89
+ // no settings, unreadable settings, malformed settings — all mean "no name".
90
+ }
91
+ return "";
92
+ }
93
+
94
+ export interface CanaryRow {
95
+ session: string;
96
+ at: string;
97
+ greeted: boolean;
98
+ opener: string;
99
+ }
100
+
101
+ export function measure(store: string, limit: number, name: string): CanaryRow[] {
102
+ const re = new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i");
103
+ return fs
104
+ .readdirSync(store)
105
+ .filter((f) => f.endsWith(".jsonl"))
106
+ .map((f) => ({ f, m: fs.statSync(path.join(store, f)).mtimeMs }))
107
+ .sort((a, b) => b.m - a.m)
108
+ .slice(0, limit)
109
+ .flatMap(({ f }) => {
110
+ const lines = fs.readFileSync(path.join(store, f), "utf8").split("\n");
111
+ const first = firstProse(lines);
112
+ if (first === null) return [];
113
+ // The name has to be in the OPENING, not anywhere in a long first reply —
114
+ // a greeting is a greeting only where the reader looks for it.
115
+ const head = first.text.split("\n").slice(0, 3).join(" ");
116
+ return [
117
+ {
118
+ session: f.replace(/\.jsonl$/, "").slice(0, 8),
119
+ at: first.at,
120
+ greeted: re.test(head),
121
+ opener: first.text.split("\n")[0]?.slice(0, 70).replace(/\s+/g, " ") ?? "",
122
+ },
123
+ ];
124
+ });
125
+ }
126
+
127
+ export function main(argv?: string[]): number {
128
+ const args = argv ?? process.argv.slice(2);
129
+ const arg = (flag: string, dflt: string): string => {
130
+ const i = args.indexOf(flag);
131
+ return i >= 0 && args[i + 1] !== undefined ? (args[i + 1] as string) : dflt;
132
+ };
133
+ const store = arg("--store", defaultStore(process.cwd()));
134
+ const limit = Number(arg("--limit", "30"));
135
+ // R2 finding 7: the default used to be a maintainer's own nickname, hardcoded
136
+ // in `src/` — which breaks the portability Iron Law and made the figure
137
+ // irreproducible for anyone else (a name that never appears greps to a silent
138
+ // 0 %). The rule this probe measures resolves the name through the settings
139
+ // cascade, so the probe resolves it the same way and REFUSES rather than
140
+ // guessing when no layer carries one.
141
+ const name = arg("--name", resolveCanaryName());
142
+
143
+ if (name === "") {
144
+ process.stdout.write(
145
+ "probe:session-canary · no canary name configured — nothing to measure.\n" +
146
+ " The obligation itself is inert without one (`session-canary`: no name on any\n" +
147
+ " layer ⇒ the rule does not fire), so a rate here would be a rate over an\n" +
148
+ " obligation that does not exist. Pass --name <name> to measure anyway.\n",
149
+ );
150
+ return 0;
151
+ }
152
+ if (!fs.existsSync(store)) {
153
+ process.stdout.write(`probe:session-canary · store not found: ${store}\n`);
154
+ return 0;
155
+ }
156
+ const rows = measure(store, limit, name);
157
+ if (rows.length === 0) {
158
+ process.stdout.write(`probe:session-canary · 0 session(s) with assistant prose in ${store}\n`);
159
+ return 0;
160
+ }
161
+ const hit = rows.filter((r) => r.greeted).length;
162
+ process.stdout.write(`probe:session-canary · ${rows.length} session(s) · name=${name}\n`);
163
+ for (const r of rows.filter((r) => !r.greeted).sort((a, b) => a.at.localeCompare(b.at))) {
164
+ process.stdout.write(` MISS ${r.session} ${r.at.slice(0, 16)} "${r.opener}"\n`);
165
+ }
166
+ process.stdout.write(
167
+ ` opening canary present in ${hit}/${rows.length} sessions ` +
168
+ `(${((100 * hit) / rows.length).toFixed(1)}%)\n`,
169
+ );
170
+ process.stdout.write(
171
+ " Per-SESSION only. The per-TASK instance the rule also obliges is NOT decidable\n" +
172
+ " from a transcript — no task boundary is recorded — so this figure is a floor on\n" +
173
+ " compliance, never a verdict on the obligation as a whole.\n",
174
+ );
175
+ return 0;
176
+ }
177
+
178
+ if (process.argv[1] !== undefined && import.meta.url.endsWith(path.basename(process.argv[1]))) {
179
+ process.exit(main());
180
+ }