@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,358 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * report_consultation_rate — the consultation half of the pre-registered
4
+ * frontend-skill metrics, computed from transcripts.
5
+ *
6
+ * WHAT IT MEASURES, AND WHY ONLY HALF.
7
+ * `agents/settings/contexts/skill-catalogue-baseline.md` pre-registers two
8
+ * rates. Exactly one of them is mechanically computable, and the reason the
9
+ * other is not is a roadmap non-goal rather than an oversight:
10
+ *
11
+ * CONSULTATION RATE — share of UI-write turns in a session that happened
12
+ * AFTER the session opened a design surface. Every input is a tool call:
13
+ * the write, its path, the prior read. Computable, and computed here.
14
+ *
15
+ * DISCHARGE RATE — share of UI-write turns followed by a review verdict
16
+ * that is "render-scoped or explicitly static-scoped". A verdict is PROSE.
17
+ * Deciding whether a paragraph is a scoped verdict means matching prose,
18
+ * which the roadmap's non-goals forbid ("no prose-matching missed-activation
19
+ * detector", the FC-8 boundary the round-6 phase drew). So this script does
20
+ * NOT compute it, and does not approximate it under its name.
21
+ *
22
+ * What it offers instead is a labelled PROXY: how often a UI-write turn is
23
+ * followed, in the same session, by opening `design-review`. That is a
24
+ * consultation-shaped fact about the review skill, not a fact about a verdict —
25
+ * an agent can open the skill and still write "looks good". Reported under its
26
+ * own name so it can never be quoted as the discharge rate.
27
+ *
28
+ * ONE EVENT STREAM, NOT TWO. The UI-write and consultation predicates are
29
+ * imported from the `ui-route-nudge` concern rather than restated. A metric
30
+ * whose denominator drifted from the nudge's trigger would make any later A/B
31
+ * uninterpretable — the two would be measuring different populations while
32
+ * appearing to measure one.
33
+ *
34
+ * PRIVACY, stated at the precision the code actually holds. The text report is
35
+ * counts and rates only. The `--json` form additionally echoes the SCAN ROOT
36
+ * (`store`) — the directory the caller pointed at, which is a local path and is
37
+ * the one thing a reader needs to know what was measured. Nothing else crosses:
38
+ * no transcript filename, no session id, no path from inside a session, no
39
+ * prompt or file content. The measurement reads those and keeps none of them.
40
+ *
41
+ * Exit: 0 always, except a usage/IO error (1). Advisory — it gates nothing.
42
+ */
43
+
44
+ import * as fs from 'node:fs';
45
+ import * as os from 'node:os';
46
+ import * as path from 'node:path';
47
+ import { fileURLToPath } from 'node:url';
48
+
49
+ import {
50
+ isConsultation,
51
+ isUiWrite,
52
+ type ToolEvent,
53
+ } from './hooks/ui_route_nudge_hook.js';
54
+
55
+ const _HERE = fileURLToPath(import.meta.url);
56
+
57
+ /** Tool names that propose new file content on this host. */
58
+ const WRITE_TOOLS = new Set(['Write', 'Edit', 'NotebookEdit', 'MultiEdit']);
59
+
60
+ /** The skill whose opening the discharge PROXY counts. */
61
+ const REVIEW_SURFACE = 'design-review';
62
+
63
+ /**
64
+ * One transcript tool call, tagged with the assistant turn it belongs to.
65
+ *
66
+ * The turn index is what makes the published unit honest. The pre-registered
67
+ * metric is a share of UI *turns*; an earlier version counted `tool_use` parts
68
+ * and published the count as turns, which inflates any turn that writes two
69
+ * files and mixes a per-session numerator with a per-call denominator.
70
+ */
71
+ export interface TimedEvent {
72
+ event: ToolEvent;
73
+ turn: number;
74
+ }
75
+
76
+ export interface SessionMeasurement {
77
+ /** Assistant turns containing at least one UI write. */
78
+ uiWriteTurns: number;
79
+ /** Those turns that followed a consultation in the same session. */
80
+ consulted: number;
81
+ /** Those turns followed later by opening the review skill. */
82
+ reviewOpenedAfter: number;
83
+ }
84
+
85
+ export interface RateReport {
86
+ store: string;
87
+ /** False when the store directory does not exist — never the same as empty. */
88
+ storeExists: boolean;
89
+ /** Transcripts read. Capped by --limit; the cap is reported when it bites. */
90
+ sessions: number;
91
+ /** True when --limit truncated the store rather than reading all of it. */
92
+ truncated: boolean;
93
+ sessionsWithUiWrite: number;
94
+ /** Assistant turns containing at least one UI write — the denominator. */
95
+ uiWriteTurns: number;
96
+ consulted: number;
97
+ reviewOpenedAfter: number;
98
+ }
99
+
100
+ /**
101
+ * Default transcript store for a project directory.
102
+ *
103
+ * The host flattens the path by replacing BOTH separators and dots — a worktree
104
+ * at `<repo>/.claude/worktrees/x` lands in `…-agent-config--claude-worktrees-x`,
105
+ * with two dashes where `/.` was. Replacing only `/` (the shape the older census
106
+ * helper carries) produces a directory that does not exist, and the caller then
107
+ * reads a clean zero out of a store it never found.
108
+ */
109
+ export function defaultStore(cwd: string): string {
110
+ return path.join(os.homedir(), '.claude', 'projects', cwd.replace(/[/.]/g, '-'));
111
+ }
112
+
113
+ /**
114
+ * Map one transcript `tool_use` part onto the event shape the nudge decides
115
+ * over. Returns null for parts that carry no path — they cannot be either a
116
+ * UI write or a consultation.
117
+ */
118
+ export function toolUseToEvent(part: Record<string, unknown>): ToolEvent | null {
119
+ const name = part['name'];
120
+ const input = part['input'];
121
+ if (typeof name !== 'string' || input === null || typeof input !== 'object') return null;
122
+ const ti = input as Record<string, unknown>;
123
+ const fileVal = ti['file_path'] ?? ti['path'] ?? ti['filePath'] ?? ti['notebook_path'];
124
+ if (typeof fileVal !== 'string' || fileVal === '') return null;
125
+ return { file: fileVal, isWrite: WRITE_TOOLS.has(name) };
126
+ }
127
+
128
+ /**
129
+ * Walk one session's tool events in order and measure it.
130
+ *
131
+ * Shares the nudge's PREDICATES (`isUiWrite`, `isConsultation`) but not its
132
+ * `decide`, deliberately: `decide` stops warning after `MAX_NUDGES`, which is
133
+ * correct for a reminder and wrong for a denominator — measuring through it
134
+ * would silently drop every UI write past the second one in a session.
135
+ *
136
+ * Ordering is load-bearing. A consultation counts only for writes that come
137
+ * after it; a session that opens `fe-design` at the end does not retroactively
138
+ * consult for the file it already wrote.
139
+ */
140
+ export function measureSession(timed: readonly TimedEvent[]): SessionMeasurement {
141
+ const out: SessionMeasurement = { uiWriteTurns: 0, consulted: 0, reviewOpenedAfter: 0 };
142
+ let consulted = false;
143
+ /** turn -> was the session already consulted when that turn first wrote UI */
144
+ const writeTurns = new Map<number, boolean>();
145
+ let lastReviewOpenAt = -1;
146
+ const reviewOpenPositions: number[] = [];
147
+ const writePositions: Array<{ position: number; turn: number }> = [];
148
+
149
+ timed.forEach(({ event, turn }, position) => {
150
+ if (isUiWrite(event)) {
151
+ if (!writeTurns.has(turn)) {
152
+ writeTurns.set(turn, consulted);
153
+ writePositions.push({ position, turn });
154
+ }
155
+ return;
156
+ }
157
+ if (isConsultation(event)) {
158
+ consulted = true;
159
+ if (event.file.replace(/\\/g, '/').toLowerCase().includes(`skills/${REVIEW_SURFACE}/`)) {
160
+ reviewOpenPositions.push(position);
161
+ lastReviewOpenAt = position;
162
+ }
163
+ }
164
+ });
165
+
166
+ out.uiWriteTurns = writeTurns.size;
167
+ for (const wasConsulted of writeTurns.values()) if (wasConsulted) out.consulted += 1;
168
+ // A single max comparison, not a scan per write: the proxy asks only whether
169
+ // ANY review-open follows, so the last one decides every earlier write.
170
+ void reviewOpenPositions;
171
+ for (const { position } of writePositions) {
172
+ if (lastReviewOpenAt > position) out.reviewOpenedAfter += 1;
173
+ }
174
+ return out;
175
+ }
176
+
177
+ /** Read every `tool_use` part of one transcript file, in order. */
178
+ export function readSessionEvents(file: string): TimedEvent[] {
179
+ const events: TimedEvent[] = [];
180
+ let turn = -1;
181
+ for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
182
+ if (!line.trim()) continue;
183
+ let entry: Record<string, unknown>;
184
+ try {
185
+ entry = JSON.parse(line) as Record<string, unknown>;
186
+ } catch {
187
+ continue;
188
+ }
189
+ if (entry['type'] !== 'assistant') continue;
190
+ turn += 1;
191
+ const msg = entry['message'];
192
+ const content =
193
+ msg !== null && typeof msg === 'object'
194
+ ? (msg as Record<string, unknown>)['content']
195
+ : null;
196
+ if (!Array.isArray(content)) continue;
197
+ for (const part of content) {
198
+ if (part === null || typeof part !== 'object') continue;
199
+ const p = part as Record<string, unknown>;
200
+ if (p['type'] !== 'tool_use') continue;
201
+ const event = toolUseToEvent(p);
202
+ if (event) events.push({ event, turn });
203
+ }
204
+ }
205
+ return events;
206
+ }
207
+
208
+ export function measureStore(store: string, limit: number): RateReport {
209
+ const report: RateReport = {
210
+ store,
211
+ storeExists: fs.existsSync(store),
212
+ sessions: 0,
213
+ truncated: false,
214
+ sessionsWithUiWrite: 0,
215
+ uiWriteTurns: 0,
216
+ consulted: 0,
217
+ reviewOpenedAfter: 0,
218
+ };
219
+ // A store that is not there and a store with nothing in it are different
220
+ // findings, and only one of them is about the agent. Collapsing them into
221
+ // one clean zero is the failure this repo has recorded four times.
222
+ if (!report.storeExists) return report;
223
+
224
+ // mtime is read ONCE per file, not inside the comparator: a comparator that
225
+ // stats is O(n log n) syscalls and throws ENOENT if a live store rotates a
226
+ // transcript mid-sort — a measurement must not crash on the store being used.
227
+ const all = fs
228
+ .readdirSync(store)
229
+ .filter((f) => f.endsWith('.jsonl'))
230
+ .map((f) => path.join(store, f))
231
+ .map((file) => {
232
+ let mtimeMs = 0;
233
+ try {
234
+ mtimeMs = fs.statSync(file).mtimeMs;
235
+ } catch {
236
+ /* vanished between readdir and stat — sort it last, skip it below */
237
+ }
238
+ return { file, mtimeMs };
239
+ })
240
+ .sort((a, b) => b.mtimeMs - a.mtimeMs);
241
+
242
+ const files = all.slice(0, limit).map((f) => f.file);
243
+ report.truncated = all.length > files.length;
244
+ report.sessions = files.length;
245
+ for (const file of files) {
246
+ let measured: SessionMeasurement;
247
+ try {
248
+ measured = measureSession(readSessionEvents(file));
249
+ } catch {
250
+ continue; // rotated or unreadable transcript — skip, never crash
251
+ }
252
+ if (measured.uiWriteTurns === 0) continue;
253
+ report.sessionsWithUiWrite += 1;
254
+ report.uiWriteTurns += measured.uiWriteTurns;
255
+ report.consulted += measured.consulted;
256
+ report.reviewOpenedAfter += measured.reviewOpenedAfter;
257
+ }
258
+ return report;
259
+ }
260
+
261
+ function pct(n: number, d: number): string {
262
+ return d === 0 ? 'n/a' : `${((100 * n) / d).toFixed(1)}%`;
263
+ }
264
+
265
+ export function render(report: RateReport, threshold: number): string {
266
+ const lines: string[] = [];
267
+ lines.push('consultation rate — advisory, gates on nothing');
268
+ lines.push('');
269
+ if (!report.storeExists) {
270
+ lines.push(' ❌ transcript store not found — nothing was measured.');
271
+ lines.push(' This is not a rate of zero. Point --store at a real store;');
272
+ lines.push(' `ls ~/.claude/projects` lists them.');
273
+ return lines.join('\n');
274
+ }
275
+ lines.push(
276
+ ` sessions scanned ${report.sessions}${report.truncated ? ' (TRUNCATED by --limit)' : ''}`,
277
+ );
278
+ lines.push(` sessions with a UI write ${report.sessionsWithUiWrite}`);
279
+ lines.push(` UI-write turns ${report.uiWriteTurns}`);
280
+ lines.push('');
281
+ lines.push(
282
+ ` CONSULTATION RATE ${pct(report.consulted, report.uiWriteTurns)} (${report.consulted}/${report.uiWriteTurns})`,
283
+ );
284
+ lines.push(
285
+ ` discharge PROXY (review opened) ${pct(report.reviewOpenedAfter, report.uiWriteTurns)} (${report.reviewOpenedAfter}/${report.uiWriteTurns})`,
286
+ );
287
+ lines.push('');
288
+
289
+ if (report.sessionsWithUiWrite < threshold) {
290
+ lines.push(
291
+ ` ⚠ ${report.sessionsWithUiWrite} session(s) with a UI write — below the pre-registered floor of ${threshold}.`,
292
+ );
293
+ lines.push(' Report the rate as provisional; it is not yet a baseline.');
294
+ lines.push('');
295
+ }
296
+
297
+ lines.push(' The PROXY is not the discharge rate. It counts a UI-write turn followed by');
298
+ lines.push(' opening `design-review` in the same session. A verdict is prose, and matching');
299
+ lines.push(' prose is a roadmap non-goal, so the discharge rate stays unmeasured rather');
300
+ lines.push(' than approximated under its own name.');
301
+ return lines.join('\n');
302
+ }
303
+
304
+ /**
305
+ * Value of `flag`, or `fallback` when it is absent.
306
+ *
307
+ * Throws when the flag is present with no value after it. The earlier version
308
+ * returned the fallback there, so `--store` as the last argv element silently
309
+ * measured the DEFAULT store — and since the text report never prints the scan
310
+ * root, the substitution was invisible in exactly the output a reader would
311
+ * publish. A usage error is the only honest answer to "you asked for a store
312
+ * and named none".
313
+ */
314
+ function argValue(flag: string, fallback: string): string {
315
+ const index = process.argv.indexOf(flag);
316
+ if (index === -1) return fallback;
317
+ const value = process.argv[index + 1];
318
+ if (value === undefined || value.startsWith('--')) {
319
+ throw new Error(`${flag} requires a value`);
320
+ }
321
+ return value;
322
+ }
323
+
324
+ /** Parse a positive-integer flag, or fail loudly. Never silently NaN. */
325
+ function intArg(flag: string, fallback: string): number {
326
+ const raw = argValue(flag, fallback);
327
+ const parsed = Number.parseInt(raw, 10);
328
+ if (!Number.isFinite(parsed) || parsed <= 0) {
329
+ throw new Error(`${flag} must be a positive integer (got ${JSON.stringify(raw)})`);
330
+ }
331
+ return parsed;
332
+ }
333
+
334
+ function main(): number {
335
+ const store = argValue('--store', defaultStore(process.cwd()));
336
+ const limit = intArg('--limit', '200');
337
+ // Validated for the same reason --limit is, and it was not: a NaN threshold
338
+ // makes `sessionsWithUiWrite < NaN` false, which SUPPRESSES the provisional
339
+ // warning — the run then reads as a settled baseline over any corpus.
340
+ const threshold = intArg('--min-sessions', '20');
341
+
342
+ const report = measureStore(store, limit);
343
+ if (process.argv.includes('--json')) {
344
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
345
+ } else {
346
+ process.stdout.write(`${render(report, threshold)}\n`);
347
+ }
348
+ return 0;
349
+ }
350
+
351
+ if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(_HERE)) {
352
+ try {
353
+ process.exit(main());
354
+ } catch (error) {
355
+ process.stderr.write(`❌ ${(error as Error).message}\n`);
356
+ process.exit(1);
357
+ }
358
+ }
@@ -114,7 +114,11 @@ export interface UsageReport {
114
114
 
115
115
  /** Default store for the current working directory, mangled the way the host does. */
116
116
  export function defaultStore(cwd: string): string {
117
- return path.join(os.homedir(), '.claude', 'projects', cwd.replace(/\//g, '-'));
117
+ // Dots are flattened too, not only separators: a worktree at
118
+ // `<repo>/.claude/worktrees/x` lands in `…-agent-config--claude-worktrees-x`.
119
+ // Replacing only `/` returns a directory that does not exist, and the caller
120
+ // then reads a clean zero out of a store it never found.
121
+ return path.join(os.homedir(), '.claude', 'projects', cwd.replace(/[/.]/g, '-'));
118
122
  }
119
123
 
120
124
  export function measureUsage(store: string, limit: number): UsageReport {
@@ -36,6 +36,7 @@ const modelAutoSwitch = z.enum(['auto', 'suggest', 'off']);
36
36
  const leanProjectionMode = z.enum(['eager-all', 'thin']);
37
37
  const projectionMode = z.enum(['legacy-all', 'scoped']);
38
38
  const memoryCadence = z.enum(['auto', 'always', 'never']);
39
+ const projectAudience = z.enum(['self', 'internal', 'client', 'public']);
39
40
 
40
41
  export const settingsSchema = z.object({
41
42
  agent_config_version: z.string().default('').describe(
@@ -142,6 +143,9 @@ export const settingsSchema = z.object({
142
143
  improvement_pr_branch_prefix: z.string().default('improve/agent-').describe(
143
144
  'Branch-name prefix for improvement PRs the agent opens against project.upstream_repo (e.g. "improve/agent-add-react-skill"). Pick a prefix your repo conventions allow.',
144
145
  ),
146
+ audience: projectAudience.default('public').describe(
147
+ 'Who this project is built for — read by the demand gate (§ 8-pre of docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md), whose L0-L4 ladder measures MARKET demand and is meaningless where no market is intended. self = a tool its maintainer builds for themselves; the gate is inert and work is classified L-self (build) instead of being deferred for lack of a user population nobody wants. internal = a team tool; only "what breaks without it?" survives. client = built for a named client, who is the requester rather than a market segment. public (default) = a product with an intended market; full three-question gate, behaviour unchanged from before this key existed.',
148
+ ),
145
149
  }),
146
150
  github: z.object({
147
151
  pr_reply_method: replyMethod.default('create_review_comment').describe(
@@ -515,25 +519,22 @@ export const settingsSchema = z.object({
515
519
  'PreToolUse anti-slop nudge (road-to-anti-slop-detector Phase 3). Default off. When on, runs the lint_design_slop registry against about-to-be-written UI content and WARNS (never blocks) on P0/P1 aesthetic tells (side-stripe, gradient-text, magic z-index, …). Flags are rebuttable via DESIGN.md / design-slop-disable. Anti-loop: a file::rule signature surfaced 3x goes silent. Host-limited convenience layer; the universal gate is the lint_design_slop linter/CI.',
516
520
  ),
517
521
  }).default({}),
518
- code_graph: z.object({
522
+ ui_route_nudge: z.object({
519
523
  enabled: z.boolean().default(false).describe(
520
- 'PreToolUse code-graph nudge (ADR-124 Phase 4). Default off. When on AND a native code-graph cache or a consumer-shipped graph.json/SCIP index is present, warns once per session (never blocks) as the agent is about to Grep/Glob or Read a source file — query the graph first for who-calls/where-used/impact questions (or rebuild if stale, build if absent). Source G’s strict block-first-read mode is deliberately un-ported.',
524
+ 'PreToolUse UI-route nudge (road-to-frontend-skill-application Phase 4). Default off. When on, a Write/Edit to a UI surface with no design consultation latched this session WARNS (never blocks) naming the route — run existing-ui-audit, then the fe-design loop. A read or search touching fe-design / existing-ui-audit / design-review / design-intelligence latches consultation and silences it for the session. Anti-loop: at most 2 nudges per session. It does not read the rules: the UI-surface decision comes from _lib/ui_surface.ts and no code parses rule frontmatter, so this runs parallel to the two UI rules rather than consuming their triggers, and a test keeps the sets from drifting. It is a nudge, so their enforced_by: none stays accurate.',
521
525
  ),
522
526
  }).default({}),
523
- turn_end_gate: z.object({
527
+ code_graph: z.object({
524
528
  enabled: z.boolean().default(false).describe(
525
- 'Stop-slot turn-end gate (road-to-conformance-round5 Phase 3). Default off, and it is the only concern that can REFUSE a turn-end rather than a tool call — so it soaks behind an explicit opt-in before it binds. Round 5 measured the reason: both blocking carriers reached zero violations, neither advisory carrier did, and 19 wrong-language replies survived a pin that had fired 26-35 seconds earlier. A refused turn continues in the SAME turn and is never refused twice.',
526
- ),
527
- promissory: z.boolean().default(true).describe(
528
- 'Detector A of the turn-end gate: refuse a turn whose closing paragraph promises work not yet performed while asking the user nothing (20 measured occurrences). No fire rate is quoted here on purpose: the measured rate moved twice while the instrument itself was being corrected, so a number in a shipped description would be a moving target. Re-derive it with `measure-turn-end-gate --store <transcript dir>` against your own corpus before arming the gate. Inert while hooks.turn_end_gate.enabled is false.',
529
- ),
530
- language: z.boolean().default(true).describe(
531
- 'Detector B of the turn-end gate: refuse a turn whose reply is not in the pinned language (19 measured occurrences). Fenced code, inline code, quoted tool output, tables, URLs and paths are stripped first, and the remainder is then scored LEAD-FIRST by the same classifier that sets the pin — so the verdict usually comes from the reply opening, not from all of it. Sharing that classifier is deliberate: it stops the pin and the detector disagreeing about a turn. Accepted opt-in spellings for the switches below include YAML true/false and yes/no/on/off. Inert while hooks.turn_end_gate.enabled is false.',
532
- ),
533
- verification: z.boolean().default(true).describe(
534
- 'Detector C of the turn-end gate: refuse a turn that changed a file and then ran nothing that could have checked it — no test, type-check, lint or build call follows the LAST edit. Ordering is the point: a run before the final edit did not exercise it, which is the freshness clause verify-before-complete states. Read off the turn tool calls, not the prose, because a reply cannot say whether an edit was verified. Deliberately narrow on what counts as verification — a missed detection costs one unguarded turn, a false refusal teaches you to switch the gate off. No fire rate is quoted: none has been measured yet. Inert while hooks.turn_end_gate.enabled is false.',
529
+ 'PreToolUse code-graph nudge (ADR-124 Phase 4). Default off. When on AND a native code-graph cache or a consumer-shipped graph.json/SCIP index is present, warns once per session (never blocks) as the agent is about to Grep/Glob or Read a source file — query the graph first for who-calls/where-used/impact questions (or rebuild if stale, build if absent). Source G’s strict block-first-read mode is deliberately un-ported.',
535
530
  ),
536
531
  }).default({}),
532
+ // `turn_end_gate` is deliberately ABSENT. The stop-slot turn-end gate is
533
+ // always armed (2026-08-12) and has no settings surface: whether it
534
+ // fires is decided by each detector's own trigger conditions, not by a
535
+ // flag. A leftover `hooks.turn_end_gate.*` block from an older install
536
+ // warns once on stderr and is ignored — see REMOVED_KEYS in
537
+ // `src/scripts/_lib/agent_settings.ts`.
537
538
  }),
538
539
  decision_engine: z.object({
539
540
  surface_traces: z.boolean().default(false).describe(