@event4u/agent-config 8.8.0 → 8.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +109 -274
  3. package/README.md +2 -2
  4. package/dist/agent-src/commands/agent-handoff.md +39 -2
  5. package/dist/agent-src/commands/research/deep.md +8 -0
  6. package/dist/agent-src/commands/research/report.md +7 -0
  7. package/dist/agent-src/commands/roadmap/create.md +12 -0
  8. package/dist/agent-src/commands/roadmap/process-full.md +31 -5
  9. package/dist/agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +30 -1
  10. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +21 -5
  11. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +37 -0
  12. package/dist/agent-src/contexts/execution/subagent-topologies.md +34 -0
  13. package/dist/agent-src/contexts/execution/verification-mechanics.md +9 -9
  14. package/dist/agent-src/rules/agent-authority.md +1 -1
  15. package/dist/agent-src/rules/communication-through-line.md +1 -1
  16. package/dist/agent-src/rules/delegation-policy.md +5 -0
  17. package/dist/agent-src/rules/direct-answers.md +1 -1
  18. package/dist/agent-src/rules/downstream-changes.md +1 -0
  19. package/dist/agent-src/rules/external-code-graph-interop.md +65 -0
  20. package/dist/agent-src/rules/git-history-discipline.md +18 -0
  21. package/dist/agent-src/rules/lethal-trifecta-guard.md +6 -1
  22. package/dist/agent-src/rules/minimal-safe-diff.md +58 -10
  23. package/dist/agent-src/rules/non-destructive-by-default.md +3 -0
  24. package/dist/agent-src/rules/question-not-instruction.md +76 -0
  25. package/dist/agent-src/rules/senior-engineering-discipline.md +1 -1
  26. package/dist/agent-src/rules/think-before-action.md +1 -0
  27. package/dist/agent-src/rules/tool-safety.md +8 -0
  28. package/dist/agent-src/rules/untrusted-input-defense.md +87 -22
  29. package/dist/agent-src/scripts/update_roadmap_progress.ts +2 -1
  30. package/dist/agent-src/skills/accessibility-auditor/SKILL.md +19 -0
  31. package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +10 -3
  32. package/dist/agent-src/skills/code-review/SKILL.md +2 -0
  33. package/dist/agent-src/skills/command-writing/SKILL.md +53 -40
  34. package/dist/agent-src/skills/dcf-modeling/evals/domain-truth.json +29 -0
  35. package/dist/agent-src/skills/deep-reading-analyst/SKILL.md +1 -0
  36. package/dist/agent-src/skills/design-intelligence/SKILL.md +49 -12
  37. package/dist/agent-src/skills/design-review/SKILL.md +29 -15
  38. package/dist/agent-src/skills/design-system-capture/SKILL.md +63 -24
  39. package/dist/agent-src/skills/forecasting/evals/domain-truth.json +29 -0
  40. package/dist/agent-src/skills/frontend-render-security/SKILL.md +2 -2
  41. package/dist/agent-src/skills/markitdown/SKILL.md +1 -0
  42. package/dist/agent-src/skills/mcp/SKILL.md +24 -0
  43. package/dist/agent-src/skills/mcp-builder/SKILL.md +9 -1
  44. package/dist/agent-src/skills/memory-consolidation/SKILL.md +55 -4
  45. package/dist/agent-src/skills/roadmap-writing/SKILL.md +39 -5
  46. package/dist/agent-src/skills/runway-cognition/evals/domain-truth.json +29 -0
  47. package/dist/agent-src/skills/scenario-modeling/evals/domain-truth.json +29 -0
  48. package/dist/agent-src/skills/secrets-management/SKILL.md +7 -0
  49. package/dist/agent-src/skills/skill-writing/SKILL.md +12 -3
  50. package/dist/agent-src/skills/subagent-orchestration/SKILL.md +27 -33
  51. package/dist/agent-src/skills/supply-chain-intake/SKILL.md +12 -0
  52. package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +29 -0
  53. package/dist/agent-src/skills/ui-component-architect/SKILL.md +9 -0
  54. package/dist/agent-src/skills/unit-economics-modeling/evals/domain-truth.json +40 -0
  55. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  56. package/dist/agent-src/templates/roadmaps.md +36 -0
  57. package/dist/cli/registry.js +3 -0
  58. package/dist/cli/registry.js.map +1 -1
  59. package/dist/discovery/deprecation-report.md +1 -1
  60. package/dist/discovery/discovery-manifest.json +90 -48
  61. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  62. package/dist/discovery/discovery-manifest.summary.md +4 -4
  63. package/dist/discovery/orphan-report.md +1 -1
  64. package/dist/discovery/packs.json +11 -9
  65. package/dist/discovery/trust-report.md +2 -2
  66. package/dist/discovery/workspaces.json +9 -7
  67. package/dist/install/install.mjs +136 -1
  68. package/dist/mcp/registry-manifest.json +3 -3
  69. package/dist/router.json +1 -1
  70. package/dist/scripts/_lib/claude_builtin_names.js +145 -0
  71. package/dist/scripts/_lib/claude_builtin_names.js.map +1 -0
  72. package/dist/scripts/condense.js +14 -0
  73. package/dist/scripts/condense.js.map +1 -1
  74. package/docs/CLAIMS.md +34 -6
  75. package/docs/architecture.md +2 -2
  76. package/docs/archive/CHANGELOG-pre-8.9.0.md +306 -0
  77. package/docs/benchmark.md +113 -0
  78. package/docs/catalog.md +20 -7
  79. package/docs/contracts/command-clusters.md +18 -0
  80. package/docs/contracts/design-artifact-verification.md +124 -0
  81. package/docs/contracts/injected-block-authenticity-model.md +66 -0
  82. package/docs/contracts/mcp-tool-inventory.md +28 -28
  83. package/docs/decisions/ADR-061-corpus-grounding-layer.md +13 -0
  84. package/docs/decisions/ADR-116-memory-tripwire-activation-path.md +9 -0
  85. package/docs/getting-started-by-role.md +1 -1
  86. package/docs/getting-started.md +1 -1
  87. package/docs/governance-advantage.md +1 -1
  88. package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +27 -0
  89. package/docs/guidelines/agent-infra/emphasis-budget.md +45 -0
  90. package/docs/guidelines/agent-infra/simplicity-and-goal-demos.md +151 -0
  91. package/docs/guidelines/agent-infra/tool-description-as-policy.md +35 -0
  92. package/docs/guidelines/design-canon.md +1 -0
  93. package/docs/proof.md +36 -21
  94. package/docs/second-brain-scope.md +69 -26
  95. package/package.json +1 -1
  96. package/src/agent-src/scripts/update_roadmap_progress.ts +2 -1
  97. package/src/scripts/_dispatch.bash +27 -0
  98. package/src/scripts/_lib/claude_builtin_names.ts +148 -0
  99. package/src/scripts/_lib/file_slicer.ts +117 -0
  100. package/src/scripts/_lib/lexical_index.ts +155 -0
  101. package/src/scripts/_lib/orchestration_record.ts +170 -0
  102. package/src/scripts/_lib/orchestration_savings.ts +62 -9
  103. package/src/scripts/_lib/retrieval_sanitize.ts +71 -0
  104. package/src/scripts/_lib/security_lint.ts +1 -0
  105. package/src/scripts/_lib/stat_index.ts +98 -0
  106. package/src/scripts/benchmark.ts +122 -0
  107. package/src/scripts/build_proof.ts +30 -17
  108. package/src/scripts/check_memory.ts +2 -0
  109. package/src/scripts/check_quality_regression.ts +51 -0
  110. package/src/scripts/check_roadmap_trackable.ts +3 -2
  111. package/src/scripts/condense.ts +14 -0
  112. package/src/scripts/discovery_graph.ts +0 -0
  113. package/src/scripts/external_sources_denylist.json +4 -1
  114. package/src/scripts/generate_index.ts +5 -2
  115. package/src/scripts/install-hooks.sh +25 -0
  116. package/src/scripts/install.ts +35 -2
  117. package/src/scripts/learning_sidecar.ts +0 -0
  118. package/src/scripts/lint_agent_skill_names.ts +29 -0
  119. package/src/scripts/lint_knowledge_scale.ts +4 -3
  120. package/src/scripts/lint_roadmap_complexity.ts +124 -0
  121. package/src/scripts/lint_versioned_cache.ts +195 -0
  122. package/src/scripts/mcp_server/consumer_tool_catalog.json +2 -1
  123. package/src/scripts/mcp_server/tools.ts +24 -1
  124. package/src/scripts/measure_lexical_ranking.ts +173 -0
  125. package/src/scripts/memory_lookup.ts +0 -0
  126. package/src/scripts/orchestration_record.ts +140 -0
  127. package/src/scripts/orchestration_savings_report.ts +29 -5
  128. package/src/scripts/schemas/skill.schema.json +4 -0
  129. package/src/scripts/score_domain_truth.ts +192 -0
  130. package/src/scripts/second_brain_retrieval.ts +278 -0
  131. package/src/scripts/second_brain_run.ts +341 -0
@@ -190,6 +190,9 @@ function lint_roadmap(
190
190
  _check_execution_mode(fm, problems);
191
191
  if (warnings) {
192
192
  _check_autonomous_authoring(text, fm, warnings);
193
+ _check_human_gate_steps(text, warnings);
194
+ _check_human_gate_phase_headings(text, warnings);
195
+ _check_human_gate_exit_criteria(text, warnings);
193
196
  }
194
197
  return problems;
195
198
  }
@@ -236,6 +239,127 @@ function _check_autonomous_authoring(text: string, fm: string, warnings: string[
236
239
  }
237
240
  }
238
241
 
242
+ // Human-gate step patterns (templates/roadmaps.md rule 22) — checkbox
243
+ // steps that park work on a human instead of the agent. Steps are
244
+ // agent-executable by default; genuinely user-clearable gates belong in
245
+ // a structured `## Blockers` entry, agent-checkable verifications become
246
+ // commands / targeted tests. Warnings never fail the lint; they fire for
247
+ // every execution mode (an interactive roadmap with sprinkled human-check
248
+ // steps is the same authoring bug, just slower to hurt).
249
+ const HUMAN_GATE_STEP_PATS: Array<[RegExp, string]> = [
250
+ [
251
+ /\b(user|maintainer|human|operator|stakeholder|product owner) (verifies|reviews|approves|confirms|signs?[- ]off(?: on)?|inspects|validates)\b/i,
252
+ "'<human> verifies / reviews / approves …' step",
253
+ ],
254
+ [
255
+ /\bmanually (verify|review|check|test|confirm|inspect|validate)\b/i,
256
+ "'manually verify / check …' step",
257
+ ],
258
+ [
259
+ /\bwait for (the )?(user|maintainer|stakeholders?|approval|confirmation|review|feedback)\b/i,
260
+ "'wait for approval / user' step",
261
+ ],
262
+ [/\bask the (user|maintainer)\b/i, "'ask the user' step"],
263
+ [
264
+ /\b(get|obtain|request|await|collect) ((the|an?|final) )?((user|maintainer|stakeholder|product owner|team) )?(approval|confirmation|sign[- ]?off)\b/i,
265
+ "'obtain approval / sign-off' step",
266
+ ],
267
+ [/\bsign[- ]?off (from|by)\b/i, "'sign-off from <human>' step"],
268
+ [/\bhuman review (required|needed|gate)\b/i, "'human review required' step"],
269
+ [
270
+ /\b(confirm|check|verify|review|clarify) with the (user|maintainer)\b/i,
271
+ "'confirm with the user' step",
272
+ ],
273
+ ];
274
+
275
+ // Human-gate phase headings (template rule 22) — a phase whose TITLE is
276
+ // the human checkpoint ("## Phase 5 — Review / Sign-off", "## User
277
+ // Acceptance"). Whole-heading match after an optional "Phase N" prefix,
278
+ // so working headings ("## Review existing skills") never fire.
279
+ const HUMAN_GATE_HEADING_PAT =
280
+ /^#{2,3}\s+(?:phase\s+\d+\s*(?:[—–:.-])?\s*)?(?:review(?:\s*(?:[/&+]|and)\s*sign[- ]?off)?|sign[- ]?off|approval|user\s+acceptance(?:\s+testing)?|manual\s+(?:verification|review|testing|qa)|human\s+(?:validation|review))\s*$/i;
281
+
282
+ // Human-approval exit / acceptance criteria (template rule 22) — criteria
283
+ // must be agent-decidable (exit code, file exists, test passes), never
284
+ // "user approves" / "looks good". Applied inside Exit-criteria /
285
+ // Acceptance blocks only.
286
+ const HUMAN_GATE_CRITERIA_PAT =
287
+ /\b(user|maintainer|human|operator|stakeholder|product owner)\b[^\n]{0,60}\b(approv\w*|confirm\w*|sign[- ]?off|reviews)\b|\b(approval|confirmation|sign[- ]?off)\s+(from|by|received)\b|\blooks good\b/i;
288
+
289
+ const EXIT_CRITERIA_OPENER_PAT = /^\s*(?:[-*]\s+)?\*{0,2}(exit criteria|acceptance(?: criteria)?)\*{0,2}\s*:?/i;
290
+
291
+ const RULE22_HINT =
292
+ 'templates/roadmaps.md rule 22: steps and criteria are agent-decidable; ' +
293
+ 'replace with an agent-verifiable check (command / targeted test) or, ' +
294
+ 'if only a human can decide/authorize, a structured ## Blockers entry';
295
+
296
+ function _check_human_gate_steps(text: string, warnings: string[]): void {
297
+ for (const rawLine of text.split('\n')) {
298
+ if (!/^\s*-\s*\[ \]/.test(rawLine)) {
299
+ continue;
300
+ }
301
+ for (const [re, label] of HUMAN_GATE_STEP_PATS) {
302
+ if (re.test(rawLine)) {
303
+ warnings.push(
304
+ `human-gate step (${label}): ` +
305
+ `"${rawLine.trim().slice(0, 80)}" — ${RULE22_HINT}`,
306
+ );
307
+ break;
308
+ }
309
+ }
310
+ }
311
+ }
312
+
313
+ function _check_human_gate_phase_headings(text: string, warnings: string[]): void {
314
+ for (const rawLine of text.split('\n')) {
315
+ if (!rawLine.startsWith('#')) {
316
+ continue;
317
+ }
318
+ if (HUMAN_GATE_HEADING_PAT.test(rawLine)) {
319
+ warnings.push(
320
+ `human-gate phase heading: "${rawLine.trim().slice(0, 80)}" — ` +
321
+ 'a phase whose work only a human can finish blocks autonomous ' +
322
+ `execution; ${RULE22_HINT}`,
323
+ );
324
+ }
325
+ }
326
+ }
327
+
328
+ function _check_human_gate_exit_criteria(text: string, warnings: string[]): void {
329
+ let inHeadingBlock = false;
330
+ let inInlineBlock = false;
331
+ for (const rawLine of text.split('\n')) {
332
+ const heading = /^#{1,6}\s+(.*)$/.exec(rawLine);
333
+ if (heading) {
334
+ inHeadingBlock = /\b(acceptance|exit criteria)\b/i.test(heading[1]!);
335
+ inInlineBlock = false;
336
+ continue;
337
+ }
338
+ if (rawLine.trim() === '') {
339
+ inInlineBlock = false;
340
+ continue;
341
+ }
342
+ const opener = EXIT_CRITERIA_OPENER_PAT.test(rawLine);
343
+ if (opener) {
344
+ inInlineBlock = true;
345
+ }
346
+ if (!inHeadingBlock && !inInlineBlock) {
347
+ continue;
348
+ }
349
+ // Open-checkbox lines are covered by _check_human_gate_steps;
350
+ // done/deferred/cancelled lines record history, not future gates.
351
+ if (/^\s*-\s*\[[ xX~-]\]/.test(rawLine)) {
352
+ continue;
353
+ }
354
+ if (HUMAN_GATE_CRITERIA_PAT.test(rawLine)) {
355
+ warnings.push(
356
+ `human-approval exit criterion: "${rawLine.trim().slice(0, 80)}" — ` +
357
+ `criteria must be agent-decidable; ${RULE22_HINT}`,
358
+ );
359
+ }
360
+ }
361
+ }
362
+
239
363
  function _check_execution_mode(fm: string, problems: string[]): void {
240
364
  if (!fm || !/^execution:/m.test(fm)) {
241
365
  return; // absent field = interactive default — valid
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Versioned-cache lint (road-to-retrieval-substrate-hardening B5b).
4
+ *
5
+ * Every derived cache the suite writes MUST be safe to invalidate when its
6
+ * producer changes — otherwise a tool-version bump silently reads a
7
+ * stale-format cache and corrupts the read path. This lint makes that
8
+ * discipline structural: any cache-file path literal in `src/scripts/**`
9
+ * must EITHER carry a version namespace in the path (`v<N>`, a `${…version…}`
10
+ * interpolation, or a `_VERSION`/`schema_version` reference), OR carry an
11
+ * explicit invalidation justification comment (`cache-version:` /
12
+ * `cache-invalidation:`) within a few lines — an author's ack that the cache
13
+ * is invalidated by another mechanism (full overwrite, content hash, …).
14
+ *
15
+ * It is a FAILING gate (this roadmap introduces it), scoped tightly to
16
+ * cache-file SUFFIXES so it has near-zero false-positive surface: it fires on
17
+ * the derived caches later phases create (B2 index, B3 learning sidecar, B5a
18
+ * stat-index), not on directories or ordinary data files.
19
+ *
20
+ * Usage: lint_versioned_cache.ts [--dir <root>] [--format text|json] [--quiet]
21
+ * Exit codes: 0 = clean, 1 = usage error, 2 = violations found, 3 = internal.
22
+ */
23
+ import * as fs from 'node:fs';
24
+ import * as path from 'node:path';
25
+ import process from 'node:process';
26
+ import { fileURLToPath } from 'node:url';
27
+
28
+ const PROG = 'lint_versioned_cache.ts';
29
+
30
+ /** Window (lines) around a cache-path literal scanned for a justification. */
31
+ export const JUSTIFY_WINDOW = 6;
32
+
33
+ /**
34
+ * A path literal is a "derived cache file" when its basename matches one of
35
+ * these suffixes. Suffix-based (not dir-based) to keep the trigger precise —
36
+ * ordinary data/config JSON does not match.
37
+ */
38
+ export const CACHE_SUFFIX_RE =
39
+ /(?:^|[\/'"`\-.])(?:[a-z0-9_\-.]*)(?:-index|\.index|-cache|\.cache|\.stat-index|-stat-index|\.agent-learning)\.json(?:['"`]|$)/i;
40
+
41
+ /** A version namespace present in the path literal itself. */
42
+ const VERSION_IN_PATH_RE = /(?:[\/_\-.]v\d+|\$\{[^}]*[Vv]ersion[^}]*\}|\$\{[^}]*VERSION[^}]*\})/;
43
+
44
+ /** An explicit author justification comment near the literal. */
45
+ const JUSTIFY_RE = /(?:cache-version:|cache-invalidation:)/i;
46
+
47
+ /** A string / template literal containing a cache-file path. */
48
+ const LITERAL_RE = /(['"`])((?:\\.|(?!\1).)*?)\1/g;
49
+
50
+ export interface Violation {
51
+ file: string;
52
+ line: number; // 1-indexed
53
+ literal: string;
54
+ message: string;
55
+ }
56
+
57
+ function listTsFiles(root: string): string[] {
58
+ const out: string[] = [];
59
+ const skip = new Set([
60
+ 'node_modules',
61
+ '.git',
62
+ 'dist',
63
+ 'coverage',
64
+ '.turbo',
65
+ '.cache',
66
+ '__pycache__',
67
+ ]);
68
+ const walk = (dir: string): void => {
69
+ let entries: fs.Dirent[];
70
+ try {
71
+ entries = fs.readdirSync(dir, { withFileTypes: true });
72
+ } catch {
73
+ return;
74
+ }
75
+ for (const e of entries) {
76
+ if (e.isDirectory()) {
77
+ if (!skip.has(e.name)) walk(path.join(dir, e.name));
78
+ } else if (e.isFile() && e.name.endsWith('.ts') && !e.name.endsWith('.d.ts')) {
79
+ out.push(path.join(dir, e.name));
80
+ }
81
+ }
82
+ };
83
+ walk(root);
84
+ return out.sort();
85
+ }
86
+
87
+ function _isCachePathLiteral(value: string): boolean {
88
+ // Reset lastIndex — CACHE_SUFFIX_RE has no /g flag, so test() is safe.
89
+ return CACHE_SUFFIX_RE.test(value);
90
+ }
91
+
92
+ export function scanFile(file: string, source: string): Violation[] {
93
+ const lines = source.split('\n');
94
+ const violations: Violation[] = [];
95
+ for (let i = 0; i < lines.length; i++) {
96
+ const text = lines[i] as string;
97
+ LITERAL_RE.lastIndex = 0;
98
+ let m: RegExpExecArray | null;
99
+ while ((m = LITERAL_RE.exec(text)) !== null) {
100
+ const value = m[2] as string;
101
+ if (!_isCachePathLiteral(value)) continue;
102
+ if (VERSION_IN_PATH_RE.test(value)) continue; // versioned in the path itself
103
+
104
+ // Look for a justification comment in the window around the literal.
105
+ const from = Math.max(0, i - JUSTIFY_WINDOW);
106
+ const to = Math.min(lines.length - 1, i + JUSTIFY_WINDOW);
107
+ let justified = false;
108
+ for (let j = from; j <= to; j++) {
109
+ if (JUSTIFY_RE.test(lines[j] as string)) {
110
+ justified = true;
111
+ break;
112
+ }
113
+ }
114
+ if (justified) continue;
115
+
116
+ violations.push({
117
+ file,
118
+ line: i + 1,
119
+ literal: value,
120
+ message:
121
+ 'derived cache without a version namespace — add a `v<N>` (or ' +
122
+ '`${…version…}`) segment to the path, or an inline ' +
123
+ '`// cache-invalidation: <why no version is needed>` comment',
124
+ });
125
+ }
126
+ }
127
+ return violations;
128
+ }
129
+
130
+ export function runChecks(root: string): Violation[] {
131
+ const violations: Violation[] = [];
132
+ for (const file of listTsFiles(root)) {
133
+ // Never lint this file itself — its regexes contain the very suffixes
134
+ // it hunts for.
135
+ if (path.basename(file) === 'lint_versioned_cache.ts') continue;
136
+ let source: string;
137
+ try {
138
+ source = fs.readFileSync(file, 'utf-8');
139
+ } catch {
140
+ continue;
141
+ }
142
+ violations.push(...scanFile(file, source));
143
+ }
144
+ return violations;
145
+ }
146
+
147
+ export function main(argv: string[]): number {
148
+ let dir = 'src/scripts';
149
+ let format: 'text' | 'json' = 'text';
150
+ let quiet = false;
151
+ for (let i = 0; i < argv.length; i++) {
152
+ const a = argv[i] as string;
153
+ if (a === '--dir') {
154
+ dir = argv[++i] ?? dir;
155
+ } else if (a.startsWith('--dir=')) {
156
+ dir = a.slice('--dir='.length);
157
+ } else if (a === '--format') {
158
+ format = (argv[++i] as 'text' | 'json') ?? 'text';
159
+ } else if (a === '--quiet') {
160
+ quiet = true;
161
+ } else if (a === '-h' || a === '--help') {
162
+ process.stdout.write(`usage: ${PROG} [--dir <root>] [--format text|json] [--quiet]\n`);
163
+ return 0;
164
+ } else {
165
+ process.stderr.write(`${PROG}: error: unknown argument ${a}\n`);
166
+ return 1;
167
+ }
168
+ }
169
+
170
+ let violations: Violation[];
171
+ try {
172
+ violations = runChecks(path.resolve(dir));
173
+ } catch (exc) {
174
+ process.stderr.write(`${PROG}: internal error: ${String(exc)}\n`);
175
+ return 3;
176
+ }
177
+
178
+ if (format === 'json') {
179
+ process.stdout.write(JSON.stringify({ violations }, null, 2) + '\n');
180
+ return violations.length > 0 ? 2 : 0;
181
+ }
182
+ if (violations.length === 0) {
183
+ if (!quiet) process.stdout.write(`${PROG}: no unversioned derived caches\n`);
184
+ return 0;
185
+ }
186
+ for (const v of violations) {
187
+ process.stdout.write(`❌ ${v.file}:${v.line} — cache '${v.literal}': ${v.message}\n`);
188
+ }
189
+ process.stdout.write(`${PROG}: ${violations.length} unversioned derived cache(s)\n`);
190
+ return 2;
191
+ }
192
+
193
+ if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1] ?? '')) {
194
+ process.exit(main(process.argv.slice(2)));
195
+ }
@@ -70,7 +70,8 @@
70
70
  "types": {"type": "array", "items": {"type": "string"}, "minItems": 1, "description": "Memory types to scan, e.g. `historical-patterns`, `incident-learnings`, `ownership`. At least one required."},
71
71
  "keys": {"type": "array", "items": {"type": "string"}, "description": "Optional anchor paths or globs to match entries against (e.g. a file you are about to edit)."},
72
72
  "limit": {"type": "integer", "minimum": 1, "default": 5, "description": "Maximum entries to return per type. Defaults to 5."},
73
- "detail": {"type": "string", "enum": ["index", "full"], "default": "full", "description": "'index' returns compact priced rows (id, title, tokens_estimate) instead of full bodies — call this first, then memory_get the ids you need. 'full' (default) returns complete entries."}
73
+ "detail": {"type": "string", "enum": ["index", "full"], "default": "full", "description": "'index' returns compact priced rows (id, title, tokens_estimate) instead of full bodies — call this first, then memory_get the ids you need. 'full' (default) returns complete entries."},
74
+ "token_budget": {"type": "integer", "minimum": 1, "description": "Optional token budget. When set, entries are rendered as one-line compact rows (id, type, confidence, `line`) and the row set is hard-cut at token_budget × 4 chars; omitted hits appear as a top-level `truncation` hint naming a concrete next step. Absent → the envelope is unchanged."}
74
75
  },
75
76
  "required": ["types"],
76
77
  "additionalProperties": false
@@ -477,6 +477,18 @@ async function _memoryLookupHandler(
477
477
  throw new Error("'detail' must be 'index' or 'full'");
478
478
  }
479
479
 
480
+ // B1 (road-to-retrieval-substrate-hardening): optional token budget →
481
+ // compact one-line rows, hard-cut with a truncation hint. Absent → the
482
+ // envelope is unchanged (v1 default-full contract holds).
483
+ const opts: { detail: 'full' | 'index'; token_budget?: number } = { detail: detailRaw };
484
+ if (args.token_budget !== undefined) {
485
+ const tb = args.token_budget;
486
+ if (typeof tb !== 'number' || !Number.isInteger(tb) || typeof tb === 'boolean' || tb < 1) {
487
+ throw new Error("'token_budget' must be a positive integer");
488
+ }
489
+ opts.token_budget = tb;
490
+ }
491
+
480
492
  const prevCwd = process.cwd();
481
493
  let envelope: Record<string, unknown>;
482
494
  try {
@@ -485,7 +497,7 @@ async function _memoryLookupHandler(
485
497
  [...(types as string[])],
486
498
  [...(keys as string[])],
487
499
  limitRaw,
488
- { detail: detailRaw },
500
+ opts,
489
501
  );
490
502
  } finally {
491
503
  process.chdir(prevCwd);
@@ -1400,6 +1412,17 @@ export const ALLOWLIST: Record<string, BuiltinTool> = {
1400
1412
  "this first, then memory_get the ids you need. " +
1401
1413
  "'full' (default) returns complete entries.",
1402
1414
  },
1415
+ token_budget: {
1416
+ type: 'integer',
1417
+ minimum: 1,
1418
+ description:
1419
+ 'Optional token budget. When set, entries are ' +
1420
+ 'rendered as one-line compact rows (id, type, ' +
1421
+ 'confidence, `line`) and the row set is hard-cut at ' +
1422
+ 'token_budget × 4 chars; omitted hits appear as a ' +
1423
+ 'top-level `truncation` hint naming a concrete next ' +
1424
+ 'step. Absent → the envelope is unchanged.',
1425
+ },
1403
1426
  },
1404
1427
  required: ['types'],
1405
1428
  additionalProperties: false,
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Ranking-lift measurement for the hand-rolled lexical index
4
+ * (road-to-retrieval-substrate-hardening B2, deterministic + free).
5
+ *
6
+ * The gate on shipping the index (roadmap Phase 2): it must measurably rank
7
+ * BETTER than the current `_score` bucket scorer, else honest-null. This
8
+ * script runs both scorers over the SAME retrieval-precision corpus + store
9
+ * (internal/bench/second-brain/retrieval-store/) and reports, per task and in
10
+ * aggregate:
11
+ *
12
+ * - mean top tie-set size — how many entries share the TOP score. `_score`
13
+ * gives 0.8 to any keyword match, so the needed entry ties with its
14
+ * confusers (the "recalls but does not rank" finding, mean 3.3). A good
15
+ * ranker drives this toward 1.
16
+ * - precision@1 / @k — was the NEEDED entry actually first / in the top-k.
17
+ *
18
+ * No model, no spend: pure retrieval scoring. Exit 0 ok / 1 error / 2 usage.
19
+ *
20
+ * Usage: measure_lexical_ranking.ts [--write] [--format text|json]
21
+ * --write persist the report to internal/bench/reports/lexical-ranking.json
22
+ */
23
+ import * as fs from 'node:fs';
24
+ import * as path from 'node:path';
25
+ import { fileURLToPath } from 'node:url';
26
+
27
+ import * as YAML from 'yaml';
28
+
29
+ import { LexicalIndex } from './_lib/lexical_index.js';
30
+ import { _score, _setIntakeRoot, _setKnowledgeRoot, _setMemoryRoot, retrieve } from './memory_lookup.js';
31
+
32
+ const _HERE = fileURLToPath(import.meta.url);
33
+ export const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
34
+ export const STORE_DIR = path.join(REPO_ROOT, 'internal', 'bench', 'second-brain', 'retrieval-store');
35
+ export const CORPUS = path.join(STORE_DIR, 'retrieval-corpus.yml');
36
+ export const REPORT = path.join(REPO_ROOT, 'internal', 'bench', 'reports', 'lexical-ranking.json');
37
+
38
+ interface Task {
39
+ id: string;
40
+ query: string[];
41
+ needed: string;
42
+ }
43
+
44
+ interface TaskResult {
45
+ id: string;
46
+ needed: string;
47
+ baseline: { rank: number; tieSet: number };
48
+ index: { rank: number; tieSet: number };
49
+ }
50
+
51
+ interface RankingSummary {
52
+ tasks: number;
53
+ k: number;
54
+ baseline_mean_tie_set: number;
55
+ index_mean_tie_set: number;
56
+ baseline_precision_at_1: number;
57
+ index_precision_at_1: number;
58
+ baseline_precision_at_k: number;
59
+ index_precision_at_k: number;
60
+ }
61
+
62
+ /** Text an entry contributes to the index — the fields `_score` also reads. */
63
+ function entryText(entry: Record<string, unknown>): string {
64
+ const parts: string[] = [];
65
+ for (const f of ['path', 'key', 'symptom', 'feature', 'rule', 'body']) {
66
+ const v = entry[f];
67
+ if (typeof v === 'string') parts.push(v);
68
+ else if (Array.isArray(v)) for (const x of v) parts.push(String(x));
69
+ }
70
+ return parts.join(' | ');
71
+ }
72
+
73
+ /** Top tie-set size + rank of the needed id in a sorted (id, score) list. */
74
+ function diagnose(ranked: Array<{ id: string; score: number }>, needed: string): { rank: number; tieSet: number } {
75
+ if (ranked.length === 0) return { rank: -1, tieSet: 0 };
76
+ const top = ranked[0]?.score ?? 0;
77
+ const tieSet = ranked.filter((r) => r.score === top).length;
78
+ const rank = ranked.findIndex((r) => r.id === needed);
79
+ return { rank, tieSet };
80
+ }
81
+
82
+ export function measure(): { tasks: TaskResult[]; summary: RankingSummary } {
83
+ _setMemoryRoot(STORE_DIR);
84
+ _setKnowledgeRoot(path.join(STORE_DIR, 'knowledge-none'));
85
+ _setIntakeRoot(path.join(STORE_DIR, 'intake-none'));
86
+
87
+ const doc = YAML.parse(fs.readFileSync(CORPUS, 'utf-8')) as { type: string; k: number; tasks: Task[] };
88
+ const type = doc.type;
89
+ const k = doc.k;
90
+ const results: TaskResult[] = [];
91
+
92
+ for (const task of doc.tasks) {
93
+ // Shared universe: every store entry `_score` considers relevant.
94
+ const hits = retrieve([type], task.query, 100);
95
+
96
+ // Baseline — rank by the current bucket scorer, ties broken by the
97
+ // order `retrieve` already returns (store order), exactly as today.
98
+ const baselineRanked = hits.map((h) => ({ id: h.id, score: h.score }));
99
+ const baseline = diagnose(baselineRanked, task.needed);
100
+
101
+ // Index — BM25 over the same entries' text.
102
+ const index = new LexicalIndex(hits.map((h) => ({ id: h.id, text: entryText(h.entry) })));
103
+ const indexRanked = index.rank(task.query);
104
+ const idx = diagnose(indexRanked, task.needed);
105
+
106
+ results.push({ id: task.id, needed: task.needed, baseline, index: idx });
107
+ void _score; // referenced for provenance — retrieve() applies it internally
108
+ }
109
+
110
+ const n = results.length;
111
+ const mean = (sel: (r: TaskResult) => number): number =>
112
+ n === 0 ? 0 : Math.round((results.reduce((a, r) => a + sel(r), 0) / n) * 1000) / 1000;
113
+ const precAt = (sel: (r: TaskResult) => number, kk: number): number =>
114
+ n === 0 ? 0 : Math.round((results.filter((r) => sel(r) >= 0 && sel(r) < kk).length / n) * 1000) / 1000;
115
+
116
+ const summary = {
117
+ tasks: n,
118
+ k,
119
+ baseline_mean_tie_set: mean((r) => r.baseline.tieSet),
120
+ index_mean_tie_set: mean((r) => r.index.tieSet),
121
+ baseline_precision_at_1: precAt((r) => r.baseline.rank, 1),
122
+ index_precision_at_1: precAt((r) => r.index.rank, 1),
123
+ baseline_precision_at_k: precAt((r) => r.baseline.rank, k),
124
+ index_precision_at_k: precAt((r) => r.index.rank, k),
125
+ };
126
+ return { tasks: results, summary };
127
+ }
128
+
129
+ export function main(argv: string[]): number {
130
+ let write = false;
131
+ let format: 'text' | 'json' = 'text';
132
+ for (let i = 0; i < argv.length; i++) {
133
+ const a = argv[i] as string;
134
+ if (a === '--write') write = true;
135
+ else if (a === '--format') format = (argv[++i] as 'text' | 'json') ?? 'text';
136
+ else if (a === '-h' || a === '--help') {
137
+ process.stdout.write('usage: measure_lexical_ranking.ts [--write] [--format text|json]\n');
138
+ return 0;
139
+ } else {
140
+ process.stderr.write(`measure_lexical_ranking: unknown argument ${a}\n`);
141
+ return 2;
142
+ }
143
+ }
144
+
145
+ let out: { tasks: TaskResult[]; summary: RankingSummary };
146
+ try {
147
+ out = measure();
148
+ } catch (exc) {
149
+ process.stderr.write(`measure_lexical_ranking: error: ${String(exc)}\n`);
150
+ return 1;
151
+ }
152
+
153
+ if (write) {
154
+ fs.mkdirSync(path.dirname(REPORT), { recursive: true });
155
+ fs.writeFileSync(REPORT, JSON.stringify({ schema_version: 1, ...out }, null, 2) + '\n');
156
+ }
157
+ if (format === 'json') {
158
+ process.stdout.write(JSON.stringify(out, null, 2) + '\n');
159
+ return 0;
160
+ }
161
+ const s = out.summary;
162
+ process.stdout.write(
163
+ `lexical-ranking (${s.tasks} tasks, k=${s.k}):\n` +
164
+ ` mean top tie-set baseline=${s.baseline_mean_tie_set} index=${s.index_mean_tie_set}\n` +
165
+ ` precision@1 baseline=${s.baseline_precision_at_1} index=${s.index_precision_at_1}\n` +
166
+ ` precision@${s.k} baseline=${s.baseline_precision_at_k} index=${s.index_precision_at_k}\n`,
167
+ );
168
+ return 0;
169
+ }
170
+
171
+ if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1] ?? '')) {
172
+ process.exit(main(process.argv.slice(2)));
173
+ }
Binary file