@event4u/agent-config 10.1.0 → 10.3.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 (158) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +110 -167
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/analyze/reference-repo.md +62 -0
  5. package/dist/agent-src/commands/roadmap/create.md +4 -1
  6. package/dist/agent-src/commands/roadmap/next.md +38 -0
  7. package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +22 -1
  8. package/dist/agent-src/contexts/contracts/frugality-charter.md +1 -1
  9. package/dist/agent-src/personas/advisors/first-principles.md +6 -0
  10. package/dist/agent-src/rules/active-remediation.md +53 -2
  11. package/dist/agent-src/rules/code-provenance.md +66 -4
  12. package/dist/agent-src/rules/content-quoting-floor.md +6 -0
  13. package/dist/agent-src/rules/context-hygiene.md +39 -0
  14. package/dist/agent-src/rules/design-fidelity.md +49 -3
  15. package/dist/agent-src/rules/token-budget-discipline.md +3 -2
  16. package/dist/agent-src/rules/token-efficiency.md +22 -0
  17. package/dist/agent-src/skills/ai-council/SKILL.md +28 -6
  18. package/dist/agent-src/skills/condense-memory/SKILL.md +1 -1
  19. package/dist/agent-src/skills/design-review/references/verification-automation.md +23 -1
  20. package/dist/agent-src/skills/fe-design/SKILL.md +27 -3
  21. package/dist/agent-src/skills/judge-synthesis/SKILL.md +30 -0
  22. package/dist/agent-src/skills/learning-to-rule-or-skill/SKILL.md +11 -0
  23. package/dist/agent-src/skills/roadmap-writing/SKILL.md +9 -9
  24. package/dist/agent-src/skills/skill-writing/SKILL.md +4 -0
  25. package/dist/agent-src/skills/token-optimizer/SKILL.md +5 -1
  26. package/dist/agent-src/templates/agent-settings.md +12 -28
  27. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +2 -2
  28. package/dist/agent-src/templates/persona.md +26 -0
  29. package/dist/agent-src/templates/roadmaps.md +27 -7
  30. package/dist/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +0 -1
  31. package/dist/agent-src/templates/skill.md +35 -1
  32. package/dist/cli/commands/commands.candidates.test.js +257 -0
  33. package/dist/cli/commands/commands.candidates.test.js.map +1 -0
  34. package/dist/cli/commands/commands.js +199 -0
  35. package/dist/cli/commands/commands.js.map +1 -1
  36. package/dist/cli/main.js +1 -0
  37. package/dist/cli/main.js.map +1 -1
  38. package/dist/cli/registry.js +2 -2
  39. package/dist/cli/registry.js.map +1 -1
  40. package/dist/cli-delegate/{chunk-3JUTT26I.js → chunk-6ENSXSBW.js} +1 -1
  41. package/dist/cli-delegate/{chunk-TVGXH7VA.js → chunk-6LOPHJLD.js} +1 -1
  42. package/dist/cli-delegate/{chunk-XT2YTJCZ.js → chunk-AYKQGIBG.js} +4 -0
  43. package/dist/cli-delegate/{chunk-HJSKTMIF.js → chunk-BV3WR67D.js} +3 -19
  44. package/dist/cli-delegate/{chunk-HTIU3W56.js → chunk-BWMXLD4B.js} +6 -2
  45. package/dist/cli-delegate/{chunk-2H5SE7DU.js → chunk-FV34EGEX.js} +166 -39
  46. package/dist/cli-delegate/{chunk-XTC5ARB6.js → chunk-IR3IVRVC.js} +7 -2
  47. package/dist/cli-delegate/{chunk-LRXJK6GN.js → chunk-JMQZ7MM5.js} +1 -1
  48. package/dist/cli-delegate/{chunk-NX5AXSLD.js → chunk-X3VOARZD.js} +3 -3
  49. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  50. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  51. package/dist/cli-delegate/cmd_conformance.js +881 -204
  52. package/dist/cli-delegate/cmd_doctor.js +3 -3
  53. package/dist/cli-delegate/cmd_explain.js +2 -2
  54. package/dist/cli-delegate/cmd_export.js +4 -4
  55. package/dist/cli-delegate/cmd_handoff.js +6 -5
  56. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  57. package/dist/cli-delegate/cmd_migrate.js +2 -2
  58. package/dist/cli-delegate/cmd_packs_active.js +2 -2
  59. package/dist/cli-delegate/cmd_prune.js +4 -4
  60. package/dist/cli-delegate/cmd_route_audit.js +2 -2
  61. package/dist/cli-delegate/cmd_route_explain.js +1 -1
  62. package/dist/cli-delegate/cmd_session_recycle.js +44 -9
  63. package/dist/cli-delegate/cmd_settings_get.js +2 -2
  64. package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
  65. package/dist/cli-delegate/cmd_settings_set.js +2 -2
  66. package/dist/cli-delegate/cmd_sync.js +4 -4
  67. package/dist/cli-delegate/cmd_uninstall.js +4 -4
  68. package/dist/cli-delegate/cmd_update.js +1 -1
  69. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  70. package/dist/cli-delegate/cmd_validate.js +4 -4
  71. package/dist/cli-delegate/cmd_versions.js +1 -1
  72. package/dist/discovery/deprecation-report.md +1 -1
  73. package/dist/discovery/discovery-manifest.json +21 -21
  74. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  75. package/dist/discovery/discovery-manifest.summary.md +1 -1
  76. package/dist/discovery/dormancy-report.md +1 -1
  77. package/dist/discovery/orphan-report.md +1 -1
  78. package/dist/discovery/packs.json +2 -2
  79. package/dist/discovery/trust-report.md +1 -1
  80. package/dist/discovery/workspaces.json +2 -2
  81. package/dist/hooks/dispatch.js +204 -44
  82. package/dist/install/install.mjs +9 -21
  83. package/dist/mcp/registry-manifest.json +2 -2
  84. package/dist/mcp/server.json +2 -2
  85. package/dist/mcp/server.mjs +1337 -490
  86. package/dist/router.json +1 -1
  87. package/dist/scripts/_lib/agent_settings.js +25 -2
  88. package/dist/scripts/_lib/agent_settings.js.map +1 -1
  89. package/dist/scripts/_lib/compile_time_toggles.js +4 -3
  90. package/dist/scripts/_lib/compile_time_toggles.js.map +1 -1
  91. package/dist/server/io/yamlIO.js +0 -2
  92. package/dist/server/io/yamlIO.js.map +1 -1
  93. package/dist/server/schemas/settings.js +3 -9
  94. package/dist/server/schemas/settings.js.map +1 -1
  95. package/dist/shared/settingsClasses.js +64 -2
  96. package/dist/shared/settingsClasses.js.map +1 -1
  97. package/dist/ui/assets/{index-B5qGYOJo.js → index-5fXOHoVh.js} +2 -2
  98. package/dist/ui/assets/index-5fXOHoVh.js.map +1 -0
  99. package/dist/ui/index.html +1 -1
  100. package/docs/contracts/settings-classes.md +307 -145
  101. package/docs/guidelines/agent-infra/active-remediation-mechanics.md +54 -0
  102. package/docs/guidelines/agent-infra/layered-settings.md +1 -2
  103. package/docs/guidelines/agent-infra/symptom-driven-harvest-loop.md +104 -0
  104. package/docs/guidelines/design-antipatterns.md +79 -10
  105. package/docs/guidelines/design-fidelity-mechanics.md +120 -2
  106. package/package.json +1 -1
  107. package/src/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +0 -1
  108. package/src/config/agent-settings.template.yml +12 -30
  109. package/src/config/gate-coverage.yml +42 -8
  110. package/src/config/gate-violation-baselines.json +15 -3
  111. package/src/config/profiles/balanced.ini +0 -2
  112. package/src/config/profiles/full.ini +1 -3
  113. package/src/config/profiles/minimal.ini +0 -2
  114. package/src/config/review-prompt-binding-baseline.json +19 -0
  115. package/src/config/rule-enforcement-baseline.json +0 -1
  116. package/src/scripts/_cli/cmd_session_recycle.ts +66 -9
  117. package/src/scripts/_cli/handoff_sessions.ts +3 -2
  118. package/src/scripts/_dispatch.bash +1 -0
  119. package/src/scripts/_lib/agent_settings.ts +25 -2
  120. package/src/scripts/_lib/bench_telegraph.ts +1 -1
  121. package/src/scripts/_lib/cc_transcript.ts +25 -0
  122. package/src/scripts/_lib/compile_time_toggles.ts +4 -3
  123. package/src/scripts/ai_council/clients.ts +243 -34
  124. package/src/scripts/cache_realization_report.ts +3 -2
  125. package/src/scripts/check_ci_local_parity.ts +60 -6
  126. package/src/scripts/check_review_prompt_binding.ts +553 -0
  127. package/src/scripts/check_rule_projection_integrity.ts +3 -1
  128. package/src/scripts/check_suppression_hygiene.ts +19 -3
  129. package/src/scripts/conformance_scan.ts +282 -9
  130. package/src/scripts/cost/track.mjs +10 -4
  131. package/src/scripts/council_cli.ts +33 -3
  132. package/src/scripts/design_slop_rules.test.ts +37 -6
  133. package/src/scripts/design_slop_rules.ts +150 -22
  134. package/src/scripts/dispatch_r2_reviewer.ts +15 -1
  135. package/src/scripts/external_sources_denylist.json +3 -1
  136. package/src/scripts/hook_manifest.yaml +17 -6
  137. package/src/scripts/hooks/block_unauthorized_git.ts +54 -8
  138. package/src/scripts/hooks/delegation_nudge_hook.ts +33 -8
  139. package/src/scripts/hooks/evidence_independence.ts +61 -9
  140. package/src/scripts/hooks/session_eol_hook.ts +123 -3
  141. package/src/scripts/hooks/turn_end_gate_hook.ts +31 -0
  142. package/src/scripts/hooks/ui_route_nudge_hook.ts +55 -2
  143. package/src/scripts/lint_design_antipattern_parity.ts +201 -0
  144. package/src/scripts/lint_harvest_provenance.ts +483 -0
  145. package/src/scripts/lint_settings_classes.ts +273 -11
  146. package/src/scripts/lint_skill_router_head.ts +254 -0
  147. package/src/scripts/lint_symptom_intake.ts +241 -0
  148. package/src/scripts/report_consultation_rate.ts +128 -2
  149. package/src/scripts/report_skill_activation.ts +6 -4
  150. package/src/scripts/report_skill_obligation_violations.ts +2 -1
  151. package/src/scripts/run_skill_evals.ts +10 -5
  152. package/src/scripts/schemas/persona.schema.json +8 -0
  153. package/src/scripts/schemas/provenance-harvest.schema.json +39 -0
  154. package/src/scripts/sweep_dead_scan_roots.ts +51 -0
  155. package/src/server/io/yamlIO.ts +0 -2
  156. package/src/server/schemas/settings.ts +3 -19
  157. package/src/shared/settingsClasses.ts +93 -2
  158. package/dist/ui/assets/index-B5qGYOJo.js.map +0 -1
@@ -0,0 +1,241 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Warn-only gate: operator symptom entries under `agents/tickets/symptoms/`
4
+ * must be resolved within 30 days of the reported date.
5
+ *
6
+ * "Resolved" means the entry carries exactly one of the two blocks the
7
+ * directory's `README.md` defines — `## confirmed:` (a defect with `file:line`
8
+ * at a pinned commit) or `## null:` (checked, not reproducible, with evidence).
9
+ * A `null:` is a first-class outcome: the point of the gate is that somebody
10
+ * looked, not that a defect was found.
11
+ *
12
+ * **Warn-only by design, and the design is the load-bearing part.** Intake has
13
+ * to stay cheaper than the work it triggers, so an unresolved report is a
14
+ * reminder, never a build failure — a gate that reds CI because a human has not
15
+ * yet investigated a symptom would train people to stop filing symptoms, which
16
+ * is the exact opposite of what the surface is for
17
+ * (`agents/roadmaps/archive/road-to-symptom-driven-harvest-loop.md` Phase 1 Step 2).
18
+ * The only exit-1 path is a dead scan scope.
19
+ *
20
+ * Scope: `agents/tickets/symptoms/*.md`, one level, no recursion.
21
+ * `README.md` and `_`-prefixed files (the template) are counted as scanned but
22
+ * never evaluated — so the gate keeps a non-zero denominator while the surface
23
+ * exists, and does not red the moment the last entry is resolved away.
24
+ *
25
+ * Exit codes: 0 = clean or warnings only, 1 = dead scan scope.
26
+ * `--quiet` is a bare argv membership check, matching the sibling roadmap gates.
27
+ * `--now=YYYY-MM-DD` overrides today (tests and reproducible runs only).
28
+ */
29
+ import * as fs from 'node:fs';
30
+ import * as path from 'node:path';
31
+ import { fileURLToPath, pathToFileURL } from 'node:url';
32
+
33
+ import { GateLedger } from './_lib/gate_ledger.js';
34
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
35
+
36
+ const QUIET = process.argv.slice(2).includes('--quiet');
37
+
38
+ const SYMPTOM_DIR = path.join('agents', 'tickets', 'symptoms');
39
+
40
+ /** Days after `reported:` at which an unresolved entry starts warning. */
41
+ export const STALE_AFTER_DAYS = 30;
42
+
43
+ const _HERE = path.resolve(fileURLToPath(import.meta.url));
44
+
45
+ const REPORTED_RE = /^reported:\s*(\d{4}-\d{2}-\d{2})\s*$/m;
46
+ const CONFIRMED_RE = /^##\s+confirmed:\s*$/m;
47
+ const NULL_RE = /^##\s+null:\s*$/m;
48
+
49
+ export interface SymptomFinding {
50
+ readonly file: string;
51
+ readonly kind: 'unresolved' | 'no-reported-date';
52
+ readonly ageDays: number | null;
53
+ }
54
+
55
+ /** Walk up from CWD until a dir containing `agents/tickets` is found. */
56
+ function _repoRoot(): string {
57
+ let cur = process.cwd();
58
+ const chain = [cur];
59
+ for (;;) {
60
+ const parent = path.dirname(cur);
61
+ if (parent === cur) break;
62
+ chain.push(parent);
63
+ cur = parent;
64
+ }
65
+ for (const candidate of chain) {
66
+ try {
67
+ if (fs.statSync(path.join(candidate, 'agents', 'tickets')).isDirectory()) {
68
+ return candidate;
69
+ }
70
+ } catch {
71
+ // keep walking
72
+ }
73
+ }
74
+ return process.cwd();
75
+ }
76
+
77
+ function _isDir(p: string): boolean {
78
+ try {
79
+ return fs.statSync(p).isDirectory();
80
+ } catch {
81
+ return false;
82
+ }
83
+ }
84
+
85
+ /** Every `*.md` directly under the symptoms dir, sorted. */
86
+ export function listMarkdown(base: string): string[] {
87
+ if (!_isDir(base)) return [];
88
+ let entries: fs.Dirent[];
89
+ try {
90
+ entries = fs.readdirSync(base, { withFileTypes: true });
91
+ } catch {
92
+ return [];
93
+ }
94
+ return entries
95
+ .filter((e) => e.isFile() && e.name.endsWith('.md'))
96
+ .map((e) => e.name)
97
+ .sort();
98
+ }
99
+
100
+ /** An entry is a report; `README.md` and `_`-prefixed files are not. */
101
+ export function isEntry(name: string): boolean {
102
+ return name.endsWith('.md') && name !== 'README.md' && !name.startsWith('_');
103
+ }
104
+
105
+ function _daysBetween(fromIso: string, now: Date): number | null {
106
+ const parsed = Date.parse(`${fromIso}T00:00:00Z`);
107
+ if (Number.isNaN(parsed)) return null;
108
+ return Math.floor((now.getTime() - parsed) / 86_400_000);
109
+ }
110
+
111
+ export function evaluate(text: string, name: string, now: Date): SymptomFinding | null {
112
+ const reported = REPORTED_RE.exec(text);
113
+ if (reported === null) {
114
+ return { file: name, kind: 'no-reported-date', ageDays: null };
115
+ }
116
+ if (CONFIRMED_RE.test(text) || NULL_RE.test(text)) {
117
+ return null;
118
+ }
119
+ const age = _daysBetween(reported[1] as string, now);
120
+ if (age === null) {
121
+ return { file: name, kind: 'no-reported-date', ageDays: null };
122
+ }
123
+ if (age <= STALE_AFTER_DAYS) {
124
+ return null;
125
+ }
126
+ return { file: name, kind: 'unresolved', ageDays: age };
127
+ }
128
+
129
+ function _now(): Date {
130
+ const flag = process.argv.slice(2).find((a) => a.startsWith('--now='));
131
+ if (flag !== undefined) {
132
+ const parsed = Date.parse(`${flag.slice('--now='.length)}T00:00:00Z`);
133
+ if (!Number.isNaN(parsed)) return new Date(parsed);
134
+ }
135
+ return new Date();
136
+ }
137
+
138
+ function main(): number {
139
+ const root = _repoRoot();
140
+ const base = path.join(root, SYMPTOM_DIR);
141
+ const names = listMarkdown(base);
142
+
143
+ try {
144
+ // The scanned unit counts README + template too, deliberately: this gate
145
+ // must not go dead the moment the last open entry is resolved, and the
146
+ // directory always carries its convention file while the surface exists.
147
+ assertScanned({
148
+ gate: 'lint_symptom_intake',
149
+ scanned: names.length,
150
+ units: 'symptom file(s)',
151
+ roots: [SYMPTOM_DIR],
152
+ });
153
+ } catch (exc) {
154
+ if (exc instanceof DeadScopeError) {
155
+ process.stderr.write(`❌ ${exc.message}\n`);
156
+ return 1;
157
+ }
158
+ throw exc;
159
+ }
160
+
161
+ const now = _now();
162
+ const findings: SymptomFinding[] = [];
163
+ let entryCount = 0;
164
+
165
+ // Per-target accounting: README.md and the template are planned too, so the
166
+ // ledger states out loud that they were seen and deliberately not evaluated
167
+ // — an unaccounted `continue` is the silent-skip defect this module catches.
168
+ const ledger = new GateLedger('lint_symptom_intake');
169
+ ledger.plan(names);
170
+
171
+ for (const name of names) {
172
+ if (!isEntry(name)) {
173
+ ledger.outOfScope(name, 'not_applicable_kind');
174
+ continue;
175
+ }
176
+ entryCount += 1;
177
+ let text: string;
178
+ try {
179
+ text = fs.readFileSync(path.join(base, name), 'utf-8');
180
+ } catch {
181
+ ledger.skip(name, 'binary_content');
182
+ continue;
183
+ }
184
+ const finding = evaluate(text, name, now);
185
+ if (finding !== null) {
186
+ findings.push(finding);
187
+ ledger.fail(name, finding.kind);
188
+ } else {
189
+ ledger.complete(name);
190
+ }
191
+ }
192
+
193
+ if (findings.length === 0) {
194
+ ledger.report();
195
+ if (!QUIET) {
196
+ process.stdout.write(
197
+ `✅ lint-symptom-intake: ${entryCount} entry(ies) resolved or within ` +
198
+ `${STALE_AFTER_DAYS} days.\n`,
199
+ );
200
+ }
201
+ return 0;
202
+ }
203
+
204
+ ledger.report();
205
+ process.stdout.write(`⚠️ lint-symptom-intake: ${findings.length} unresolved symptom entry(ies):\n`);
206
+ for (const f of findings) {
207
+ if (f.kind === 'no-reported-date') {
208
+ process.stdout.write(` ${SYMPTOM_DIR}/${f.file} — no valid \`reported: YYYY-MM-DD\` in frontmatter\n`);
209
+ } else {
210
+ process.stdout.write(
211
+ ` ${SYMPTOM_DIR}/${f.file} — reported ${String(f.ageDays)} days ago, ` +
212
+ 'no `## confirmed:` or `## null:` block\n',
213
+ );
214
+ }
215
+ }
216
+ process.stdout.write('\n');
217
+ process.stdout.write(' Add whichever block is true (see agents/tickets/symptoms/README.md):\n');
218
+ process.stdout.write(' • `## confirmed:` — defect + file:line at a pinned commit + owning roadmap\n');
219
+ process.stdout.write(' • `## null:` — what was checked, and why it does not reproduce\n');
220
+ process.stdout.write(' A null is a result, not a failure to act. This gate never fails the build.\n');
221
+ return 0;
222
+ }
223
+
224
+ function _isCliEntry(): boolean {
225
+ if (process.argv[1] === undefined) return false;
226
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
227
+ if (import.meta.url === argvUrl) return true;
228
+ try {
229
+ const here = fs.realpathSync(fileURLToPath(import.meta.url));
230
+ const argv = fs.realpathSync(path.resolve(process.argv[1]));
231
+ return here === argv;
232
+ } catch {
233
+ return false;
234
+ }
235
+ }
236
+
237
+ if (_isCliEntry() || process.argv[1] === _HERE) {
238
+ process.exitCode = main();
239
+ }
240
+
241
+ export { QUIET, SYMPTOM_DIR, main };
@@ -46,7 +46,9 @@ import * as os from 'node:os';
46
46
  import * as path from 'node:path';
47
47
  import { fileURLToPath } from 'node:url';
48
48
 
49
+ import { projectStoreSlug } from './_lib/cc_transcript.js';
49
50
  import {
51
+ isArtifactRead,
50
52
  isConsultation,
51
53
  isUiWrite,
52
54
  type ToolEvent,
@@ -80,6 +82,24 @@ export interface SessionMeasurement {
80
82
  consulted: number;
81
83
  /** Those turns followed later by opening the review skill. */
82
84
  reviewOpenedAfter: number;
85
+ /**
86
+ * Those turns that followed reading a provided design artifact.
87
+ *
88
+ * Shares this function's denominator on purpose — the two rates answer
89
+ * different questions over the SAME population ("did it consult the design
90
+ * surface" vs "did it read the handed-over source"), and measuring them on
91
+ * separate denominators is how two instruments about one behaviour stop
92
+ * being comparable.
93
+ */
94
+ artifactRead: number;
95
+ /** True when this session read a provided artifact at any point. */
96
+ sawArtifact: boolean;
97
+ /**
98
+ * True when this session's FIRST UI write came after an artifact read —
99
+ * the metric named "artifact read before first UI write". Meaningless
100
+ * unless `sawArtifact`, which is why it is reported over that denominator.
101
+ */
102
+ readBeforeFirstWrite: boolean;
83
103
  }
84
104
 
85
105
  export interface RateReport {
@@ -95,6 +115,16 @@ export interface RateReport {
95
115
  uiWriteTurns: number;
96
116
  consulted: number;
97
117
  reviewOpenedAfter: number;
118
+ /** UI-write turns preceded by reading a provided design artifact. */
119
+ artifactRead: number;
120
+ /**
121
+ * Sessions that read a provided artifact at all — the ONLY honest
122
+ * denominator for the read-before-write rate. A session with no handover
123
+ * cannot fail to read one.
124
+ */
125
+ handoverSessions: number;
126
+ /** Of those, the ones whose first UI write came after the read. */
127
+ handoverReadFirst: number;
98
128
  }
99
129
 
100
130
  /**
@@ -107,7 +137,7 @@ export interface RateReport {
107
137
  * reads a clean zero out of a store it never found.
108
138
  */
109
139
  export function defaultStore(cwd: string): string {
110
- return path.join(os.homedir(), '.claude', 'projects', cwd.replace(/[/.]/g, '-'));
140
+ return path.join(os.homedir(), '.claude', 'projects', projectStoreSlug(cwd));
111
141
  }
112
142
 
113
143
  /**
@@ -138,10 +168,20 @@ export function toolUseToEvent(part: Record<string, unknown>): ToolEvent | null
138
168
  * consult for the file it already wrote.
139
169
  */
140
170
  export function measureSession(timed: readonly TimedEvent[]): SessionMeasurement {
141
- const out: SessionMeasurement = { uiWriteTurns: 0, consulted: 0, reviewOpenedAfter: 0 };
171
+ const out: SessionMeasurement = {
172
+ uiWriteTurns: 0,
173
+ consulted: 0,
174
+ reviewOpenedAfter: 0,
175
+ artifactRead: 0,
176
+ sawArtifact: false,
177
+ readBeforeFirstWrite: false,
178
+ };
142
179
  let consulted = false;
180
+ let artifactRead = false;
143
181
  /** turn -> was the session already consulted when that turn first wrote UI */
144
182
  const writeTurns = new Map<number, boolean>();
183
+ /** turn -> had an artifact been read when that turn first wrote UI */
184
+ const artifactTurns = new Map<number, boolean>();
145
185
  let lastReviewOpenAt = -1;
146
186
  const reviewOpenPositions: number[] = [];
147
187
  const writePositions: Array<{ position: number; turn: number }> = [];
@@ -150,10 +190,18 @@ export function measureSession(timed: readonly TimedEvent[]): SessionMeasurement
150
190
  if (isUiWrite(event)) {
151
191
  if (!writeTurns.has(turn)) {
152
192
  writeTurns.set(turn, consulted);
193
+ artifactTurns.set(turn, artifactRead);
153
194
  writePositions.push({ position, turn });
154
195
  }
155
196
  return;
156
197
  }
198
+ // Evaluated INDEPENDENTLY of the consultation branch, never as an
199
+ // early return before it. The two predicates are not disjoint —
200
+ // `skills/design-review/references/design.html` satisfies both — so a
201
+ // `return` here would remove such an event from the consultation
202
+ // numerator and from the discharge-proxy latch. An earlier revision did
203
+ // exactly that while claiming in its comment to prevent it.
204
+ if (isArtifactRead(event)) artifactRead = true;
157
205
  if (isConsultation(event)) {
158
206
  consulted = true;
159
207
  if (event.file.replace(/\\/g, '/').toLowerCase().includes(`skills/${REVIEW_SURFACE}/`)) {
@@ -165,6 +213,20 @@ export function measureSession(timed: readonly TimedEvent[]): SessionMeasurement
165
213
 
166
214
  out.uiWriteTurns = writeTurns.size;
167
215
  for (const wasConsulted of writeTurns.values()) if (wasConsulted) out.consulted += 1;
216
+ for (const wasRead of artifactTurns.values()) if (wasRead) out.artifactRead += 1;
217
+ out.sawArtifact = artifactRead;
218
+ // KNOWN IMPRECISION, accepted: ordering inside ONE assistant message comes
219
+ // from the `tool_use` part index, and parallel tool calls in a single
220
+ // message carry no real order. A read and a write issued together can
221
+ // therefore score either way. Not repaired here — a transcript carries no
222
+ // per-call timestamp, so the fix is a different data source rather than a
223
+ // different loop, and the error is bounded to same-message pairs. It is a
224
+ // third reason the published rate is a ceiling.
225
+ //
226
+ // The FIRST write's outcome — `artifactTurns` is insertion-ordered on the
227
+ // first UI write of each turn, so its first value is that write's state.
228
+ const firstWrite = artifactTurns.values().next();
229
+ out.readBeforeFirstWrite = firstWrite.done !== true && firstWrite.value === true;
168
230
  // A single max comparison, not a scan per write: the proxy asks only whether
169
231
  // ANY review-open follows, so the last one decides every earlier write.
170
232
  void reviewOpenPositions;
@@ -215,6 +277,9 @@ export function measureStore(store: string, limit: number): RateReport {
215
277
  uiWriteTurns: 0,
216
278
  consulted: 0,
217
279
  reviewOpenedAfter: 0,
280
+ artifactRead: 0,
281
+ handoverSessions: 0,
282
+ handoverReadFirst: 0,
218
283
  };
219
284
  // A store that is not there and a store with nothing in it are different
220
285
  // findings, and only one of them is about the agent. Collapsing them into
@@ -254,6 +319,11 @@ export function measureStore(store: string, limit: number): RateReport {
254
319
  report.uiWriteTurns += measured.uiWriteTurns;
255
320
  report.consulted += measured.consulted;
256
321
  report.reviewOpenedAfter += measured.reviewOpenedAfter;
322
+ report.artifactRead += measured.artifactRead;
323
+ if (measured.sawArtifact) {
324
+ report.handoverSessions += 1;
325
+ if (measured.readBeforeFirstWrite) report.handoverReadFirst += 1;
326
+ }
257
327
  }
258
328
  return report;
259
329
  }
@@ -284,6 +354,62 @@ export function render(report: RateReport, threshold: number): string {
284
354
  lines.push(
285
355
  ` discharge PROXY (review opened) ${pct(report.reviewOpenedAfter, report.uiWriteTurns)} (${report.reviewOpenedAfter}/${report.uiWriteTurns})`,
286
356
  );
357
+ lines.push(
358
+ ` artifact read (all UI turns) ${pct(report.artifactRead, report.uiWriteTurns)} (${report.artifactRead}/${report.uiWriteTurns})`,
359
+ );
360
+ lines.push(
361
+ ` READ BEFORE FIRST WRITE ${pct(report.handoverReadFirst, report.handoverSessions)} (${report.handoverReadFirst}/${report.handoverSessions} handover session(s))`,
362
+ );
363
+ lines.push('');
364
+ lines.push(
365
+ ' READ BEFORE FIRST WRITE is the rate to quote. Its denominator is',
366
+ );
367
+ lines.push(
368
+ ' sessions that read a provided artifact at all — a session with no',
369
+ );
370
+ lines.push(
371
+ ' handover cannot fail to read one, so including it would dilute the',
372
+ );
373
+ lines.push(
374
+ ' rate toward the size of the estate rather than the behaviour.',
375
+ );
376
+ lines.push(
377
+ ' That denominator is narrower than it sounds, twice: "artifact" means',
378
+ );
379
+ lines.push(
380
+ ' a path ending design.html or under .claude/design-system/ — a handover',
381
+ );
382
+ lines.push(
383
+ ' under any other name is not counted — and a session that read one but',
384
+ );
385
+ lines.push(
386
+ ' wrote no UI is excluded, since it had nothing to be late for.',
387
+ );
388
+ lines.push(
389
+ ' The line above it shares the consultation denominator and is kept for',
390
+ );
391
+ lines.push(
392
+ ' comparability with that rate only.',
393
+ );
394
+ lines.push('');
395
+ lines.push(
396
+ ' A CEILING, NOT A MEASUREMENT, for two reasons. A session handed an',
397
+ );
398
+ lines.push(
399
+ ' artifact that NEVER reads it is invisible — nothing in a transcript',
400
+ );
401
+ lines.push(
402
+ ' marks a handover that was ignored, and that case IS the defect. And a',
403
+ );
404
+ lines.push(
405
+ ' search whose path argument happens to be the artifact counts as a read',
406
+ );
407
+ lines.push(
408
+ ' although nothing was read. Both errors point the same way: the true',
409
+ );
410
+ lines.push(
411
+ ' read-before-write rate is at most what is printed here.',
412
+ );
287
413
  lines.push('');
288
414
 
289
415
  if (report.sessionsWithUiWrite < threshold) {
@@ -52,6 +52,8 @@ import * as os from 'node:os';
52
52
  import * as path from 'node:path';
53
53
  import { fileURLToPath, pathToFileURL } from 'node:url';
54
54
 
55
+ import { projectStoreSlug } from './_lib/cc_transcript.js';
56
+
55
57
  const _HERE = fileURLToPath(import.meta.url);
56
58
  const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
57
59
 
@@ -114,11 +116,11 @@ export interface UsageReport {
114
116
 
115
117
  /** Default store for the current working directory, mangled the way the host does. */
116
118
  export function defaultStore(cwd: string): string {
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
119
+ // Every non-alphanumeric is flattened, not only separators: a worktree at
120
+ // `<repo>/.claude/worktrees/x+y` lands in `…-agent-config--claude-worktrees-x-y`.
121
+ // A narrower class returns a directory that does not exist, and the caller
120
122
  // then reads a clean zero out of a store it never found.
121
- return path.join(os.homedir(), '.claude', 'projects', cwd.replace(/[/.]/g, '-'));
123
+ return path.join(os.homedir(), '.claude', 'projects', projectStoreSlug(cwd));
122
124
  }
123
125
 
124
126
  export function measureUsage(store: string, limit: number): UsageReport {
@@ -59,6 +59,7 @@ import * as path from 'node:path';
59
59
  import process from 'node:process';
60
60
  import { fileURLToPath, pathToFileURL } from 'node:url';
61
61
 
62
+ import { projectStoreSlug } from './_lib/cc_transcript.js';
62
63
  import { entryText, isSidechain, toolUses as _toolUses } from './_lib/transcript_entry.js';
63
64
  import { censusSkills, DETERMINISTIC_RE, SKILLS_ROOT } from './report_skill_activation.js';
64
65
 
@@ -408,7 +409,7 @@ export interface Sk2Report {
408
409
  }
409
410
 
410
411
  export function defaultStore(cwd: string): string {
411
- return path.join(os.homedir(), '.claude', 'projects', cwd.replace(/[/.]/g, '-'));
412
+ return path.join(os.homedir(), '.claude', 'projects', projectStoreSlug(cwd));
412
413
  }
413
414
 
414
415
  export function scanStore(repoRoot: string, store: string, limit: number): Sk2Report {
@@ -10,10 +10,13 @@
10
10
  * JSON files (json.dumps indent=2 + trailing newline). No behaviour
11
11
  * changes.
12
12
  *
13
- * The `.agent-src.uncondensed/skills/` literal below is part of the
14
- * mirrored .py source — this is a faithful twin, so the legacy path is
15
- * replicated verbatim, not modernized (ADR-051 carve-out for faithful
16
- * twins).
13
+ * SKILLS_ROOT diverges from the mirrored .py source: it resolves the live
14
+ * skills tree through the shared resolver instead of replicating the retired
15
+ * container literal. Replicating it made every subcommand fail in
16
+ * `_skill_dir()` before doing any work — the twin was byte-faithful to a
17
+ * source that no longer exists and runnable against nothing. Documented per
18
+ * ADR-200 § 6 in `docs/migration/divergences/src-scripts-run_skill_evals.md`;
19
+ * an undocumented difference would be a regression by definition.
17
20
  *
18
21
  * Sub-agent SPAWNING is per-environment (Claude Code, Augment Code,
19
22
  * council) and is left as a stub `_spawn_subagent(...)` that authors
@@ -25,11 +28,13 @@ import * as fs from 'node:fs';
25
28
  import * as path from 'node:path';
26
29
  import { fileURLToPath, pathToFileURL } from 'node:url';
27
30
 
31
+ import { SRC_SKILLS } from './_lib/agent_src.js';
32
+
28
33
  const _HERE = fileURLToPath(import.meta.url);
29
34
  // src/scripts/run_skill_evals.ts → parents[2] is the repo root (mirrors
30
35
  // `Path(__file__).resolve().parent.parent.parent` in the .py).
31
36
  export const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
32
- export const SKILLS_ROOT = path.join(REPO_ROOT, '.agent-src.uncondensed', 'skills');
37
+ export const SKILLS_ROOT = SRC_SKILLS();
33
38
 
34
39
  /** JSON value type for eval specs / grades. */
35
40
  type JsonValue =
@@ -44,6 +44,14 @@
44
44
  "type": "string",
45
45
  "enum": ["package", "project"],
46
46
  "default": "package"
47
+ },
48
+ "sources": {
49
+ "type": "array",
50
+ "items": {
51
+ "type": "string",
52
+ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$"
53
+ },
54
+ "description": "Optional harvest_id list (provenance/harvests.jsonl) this persona may cite as external doctrine. Three distinct states, and the difference is the point: ABSENT = unscoped, today's behaviour for every persona that does not opt in; [] = explicitly declares it asserts only from its own reasoning, nothing harvested; a non-empty list = scoped to exactly those ids. Every declared id must resolve to a ledger row — checked by lint_harvest_provenance.ts, which treats a persona entry as a citation exactly like an inline harvest marker. Not to be confused with the singular `source` field above, which records package-vs-project origin."
47
55
  }
48
56
  }
49
57
  }
@@ -0,0 +1,39 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "provenance-harvest.schema.json",
4
+ "title": "Provenance ledger harvest record",
5
+ "description": "One line of provenance/harvests.jsonl — a single knowledge claim adopted from an external source into this repo's own artefacts. Sibling of provenance-borrow.schema.json, which covers borrowed CODE; this one covers borrowed IDEAS. Contract: provenance/README.md. Enforced by src/scripts/lint_harvest_provenance.ts (hand-rolled validator, following the provenance-borrow precedent — this file documents the contract; it is not loaded at runtime).",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["harvest_id", "stated_in", "source_ref", "evidence_locator", "harvested_at", "verdict"],
9
+ "properties": {
10
+ "harvest_id": {
11
+ "type": "string",
12
+ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
13
+ "description": "Kebab-case slug naming the harvested mechanism. This is the id an artefact cites; it must be unique across the ledger."
14
+ },
15
+ "stated_in": {
16
+ "type": "string",
17
+ "description": "Repo-relative path of the artefact asserting the claim, optionally suffixed ':<line>'. The path must exist."
18
+ },
19
+ "source_ref": {
20
+ "type": "string",
21
+ "description": "Either a public source pinned to a revision ('<url>@<sha>'), or an opaque token ('opaque:<id>' / 'ENC1:<payload>') when source-confidentiality keeps the source name out of the tracked tree. A bare URL with no revision is rejected — an unpinned reference cannot be re-verified."
22
+ },
23
+ "evidence_locator": {
24
+ "type": "string",
25
+ "minLength": 3,
26
+ "description": "Where inside the source the claim was read — a file:line, a section anchor, or an equivalent locator. Free-form because sources differ, but never empty: a claim whose evidence cannot be located again is not a citation."
27
+ },
28
+ "harvested_at": {
29
+ "type": "string",
30
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
31
+ "description": "ISO-8601 date (YYYY-MM-DD) the harvest landed."
32
+ },
33
+ "verdict": {
34
+ "type": "string",
35
+ "enum": ["adopt", "adapt"],
36
+ "description": "How the mechanism entered this repo. Only adopt and adapt produce a row — a rejected, already-present, or unclear finding is recorded in the analysis document, never in the ledger, because nothing in the tree cites it."
37
+ }
38
+ }
39
+ }
@@ -148,6 +148,36 @@ export function classify(rel: string): TriageClass {
148
148
  return 'C';
149
149
  }
150
150
 
151
+ /**
152
+ * Non-gate scripts eligible for the Class-A advisory pass.
153
+ *
154
+ * WHY A SECOND POPULATION EXISTS AT ALL. The gate population above is
155
+ * deliberately shared with the ratchet and the registration test, because three
156
+ * sites once carried their own regex and disagreed. That sharing is worth
157
+ * keeping — but it means the sweep's reach is a FILENAME PREFIX, and a script
158
+ * reading a retired container is dead whether or not its name starts with one
159
+ * of five words. Measured: `run_skill_evals.ts` roots its `SKILLS_ROOT` at the
160
+ * skills subtree of the container ADR-051 retired (see `CLASS_A_RETIRED`), and
161
+ * every subcommand fails in `_skill_dir()` before doing any work — while its
162
+ * sibling `skill_trigger_eval.ts`, same retired root and a `skill_` prefix, is
163
+ * reported. One visible, one not, on the prefix alone.
164
+ *
165
+ * So this pass widens the READING, never the shared population: it is
166
+ * Class-A-only (a closed set of retired containers, needing no allowlist to stay
167
+ * honest), it is advisory, and it feeds neither the ratchet count nor the exit
168
+ * code. Widening `matchesGatePattern` instead would move the ratchet base for a
169
+ * reason unrelated to any gate.
170
+ */
171
+ export function isNonGateScript(filename: string): boolean {
172
+ if (!filename.endsWith('.ts')) return false;
173
+ if (filename.endsWith('.d.ts') || filename.endsWith('.test.ts')) return false;
174
+ // This file declares the retired containers as a classification table
175
+ // (`CLASS_A_RETIRED`); reading its own constant back as a dead root is a
176
+ // self-report, not a finding.
177
+ if (filename === 'sweep_dead_scan_roots.ts') return false;
178
+ return !isGateFile(filename);
179
+ }
180
+
151
181
  export interface Finding {
152
182
  rel: string;
153
183
  names: string;
@@ -827,6 +857,20 @@ export function main(argv: string[]): number {
827
857
  if (r.confirmed.length || r.unproven.length) results.set(f, r);
828
858
  }
829
859
 
860
+ // Class-A advisory over the scripts the shared gate population cannot reach.
861
+ // Reuses the same extractor, so a prose mention or a report string is not a
862
+ // finding — only a literal join over a recognised root base is.
863
+ const advisory: string[] = [];
864
+ const advisoryPop = fs.existsSync(args.scripts)
865
+ ? fs.readdirSync(args.scripts).filter(isNonGateScript).sort()
866
+ : [];
867
+ for (const f of advisoryPop) {
868
+ const r = analyze(fs.readFileSync(path.join(args.scripts, f), 'utf-8'), repoRoot);
869
+ for (const h of [...r.confirmed, ...r.unproven]) {
870
+ if (classify(h.rel) === 'A') advisory.push(` advisory [A] ${f}: ${h.rel} (${h.names})`);
871
+ }
872
+ }
873
+
830
874
  const used = new Set<Disposition>();
831
875
  const disposed = (script: string, rel: string): Disposition | undefined => {
832
876
  const hit = ledger.find((l) => l.script === script && l.rel === rel);
@@ -899,6 +943,13 @@ export function main(argv: string[]): number {
899
943
  if (!args.quiet) {
900
944
  process.stdout.write(`sweep_dead_scan_roots: ${pop.length} gate script(s) scanned\n`);
901
945
  for (const l of lines) process.stdout.write(`${l}\n`);
946
+ if (advisory.length) {
947
+ process.stdout.write(
948
+ `sweep_dead_scan_roots: ${advisoryPop.length} non-gate script(s) scanned ` +
949
+ '(Class-A advisory — not counted, not gated)\n',
950
+ );
951
+ for (const l of advisory) process.stdout.write(`${l}\n`);
952
+ }
902
953
  }
903
954
 
904
955
  if (stale.length) {
@@ -144,8 +144,6 @@ export const TEMPLATE_PLACEHOLDER_DEFAULTS: Readonly<Record<string, string>> = {
144
144
  __DISCIPLINE_PROFILE__: 'auto',
145
145
  __USER_TYPE__: '',
146
146
  __CHAT_HISTORY_FREQUENCY__: 'per_turn',
147
- __CHAT_HISTORY_MAX_SIZE_KB__: '2048',
148
- __CHAT_HISTORY_ON_OVERFLOW__: 'rotate',
149
147
  };
150
148
 
151
149
  /** Replace every known template placeholder with its default value. */