@event4u/agent-config 8.7.0 → 8.8.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 (83) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +52 -0
  3. package/CONTRIBUTING.md +25 -0
  4. package/README.md +7 -4
  5. package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +1 -1
  6. package/dist/agent-src/contexts/execution/orchestration-benchmark-gate.md +28 -16
  7. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +15 -0
  8. package/dist/agent-src/rules/active-remediation.md +92 -0
  9. package/dist/agent-src/rules/broken-access-control.md +106 -0
  10. package/dist/agent-src/rules/communication-through-line.md +42 -0
  11. package/dist/agent-src/rules/decision-revisit-gate.md +8 -0
  12. package/dist/agent-src/rules/minimal-safe-diff.md +18 -1
  13. package/dist/agent-src/rules/prefer-enums-over-literals.md +65 -0
  14. package/dist/agent-src/rules/senior-engineering-discipline.md +82 -0
  15. package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +105 -0
  16. package/dist/agent-src/skills/ai-code-blindspots/evals/triggers.json +52 -0
  17. package/dist/agent-src/skills/authz-review/SKILL.md +21 -4
  18. package/dist/agent-src/skills/dependency-upgrade/SKILL.md +20 -0
  19. package/dist/agent-src/skills/frontend-render-security/SKILL.md +104 -0
  20. package/dist/agent-src/skills/frontend-render-security/evals/triggers.json +52 -0
  21. package/dist/agent-src/skills/roadmap-writing/SKILL.md +7 -0
  22. package/dist/agent-src/skills/supply-chain-intake/SKILL.md +100 -0
  23. package/dist/agent-src/skills/supply-chain-intake/evals/triggers.json +52 -0
  24. package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +43 -2
  25. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  26. package/dist/agent-src/templates/roadmaps.md +24 -0
  27. package/dist/discovery/deprecation-report.md +1 -1
  28. package/dist/discovery/discovery-manifest.json +195 -15
  29. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  30. package/dist/discovery/discovery-manifest.summary.md +3 -3
  31. package/dist/discovery/orphan-report.md +1 -1
  32. package/dist/discovery/packs.json +13 -5
  33. package/dist/discovery/trust-report.md +11 -11
  34. package/dist/discovery/workspaces.json +12 -4
  35. package/dist/install/install.mjs +5 -2
  36. package/dist/mcp/awesome-mcp-servers.row.md +1 -1
  37. package/dist/mcp/mcp-cloudflare-catalogue.json +1 -1
  38. package/dist/mcp/registry-manifest.json +4 -4
  39. package/dist/router.json +1 -1
  40. package/dist/server/schemas/settings.js +2 -1
  41. package/dist/server/schemas/settings.js.map +1 -1
  42. package/docs/CLAIMS.md +30 -2
  43. package/docs/architecture.md +2 -2
  44. package/docs/catalog.md +14 -0
  45. package/docs/contracts/mcp-tool-inventory.md +31 -30
  46. package/docs/decisions/ADR-105-automatic-subagent-orchestration.md +6 -0
  47. package/docs/decisions/ADR-116-memory-tripwire-activation-path.md +2 -2
  48. package/docs/decisions/ADR-117-subagents-auto-default-on.md +88 -0
  49. package/docs/decisions/INDEX.md +1 -0
  50. package/docs/experiences/content_creator.md +2 -0
  51. package/docs/experiences/finance.md +2 -0
  52. package/docs/experiences/founder.md +2 -0
  53. package/docs/experiences/ops.md +2 -0
  54. package/docs/featured-skills.md +2 -2
  55. package/docs/getting-started-by-role.md +1 -1
  56. package/docs/getting-started.md +1 -1
  57. package/docs/governance-advantage.md +1 -1
  58. package/docs/guidelines/agent-infra/memory-access.md +12 -0
  59. package/docs/proof.md +45 -3
  60. package/docs/release-runbook.md +138 -0
  61. package/docs/second-brain-scope.md +73 -0
  62. package/docs/succession.md +80 -0
  63. package/package.json +2 -2
  64. package/src/config/agent-settings.template.yml +25 -6
  65. package/src/scripts/_lib/orchestration_gate.ts +12 -8
  66. package/src/scripts/_lib/orchestration_savings.ts +127 -0
  67. package/src/scripts/build_proof.ts +38 -0
  68. package/src/scripts/chat_history.ts +79 -7
  69. package/src/scripts/domain_soundness_status.ts +236 -0
  70. package/src/scripts/generate_index.ts +14 -0
  71. package/src/scripts/hot_context_hook.ts +47 -2
  72. package/src/scripts/lint_behavioural_eval_freshness.ts +140 -0
  73. package/src/scripts/mcp_server/consumer_tool_catalog.json +22 -3
  74. package/src/scripts/mcp_server/tools.ts +159 -1
  75. package/src/scripts/memory_lookup.ts +184 -6
  76. package/src/scripts/memory_replay.ts +285 -0
  77. package/src/scripts/orchestration_savings_report.ts +100 -0
  78. package/src/scripts/schemas/domain-truth.schema.json +94 -0
  79. package/src/scripts/schemas/evals.schema.json +113 -0
  80. package/src/scripts/second_brain_score.ts +186 -0
  81. package/src/scripts/session_memory_index.ts +94 -0
  82. package/src/scripts/skill_eval_coverage.ts +318 -0
  83. package/src/server/schemas/settings.ts +5 -2
@@ -0,0 +1,236 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Domain-soundness validation status + ratchet
4
+ * (road-to-domain-soundness Phase 1 + Phase 3).
5
+ *
6
+ * The four non-coding profiles (finance, founder, ops, content_creator) SELL
7
+ * concrete domain value (DCF, runway, RICE, incident command, messaging), but
8
+ * the skills are forged on TS/PHP — "promising, not proven" off those stacks.
9
+ * A disclaimer floor bounds liability, NOT correctness. This tool makes the
10
+ * provenance honest and machine-checkable: it enumerates the default-surface
11
+ * domain skills those profiles ship (from source — each profile's
12
+ * `skills_hint`, never hardcoded) and reports each as:
13
+ *
14
+ * - `validated` — ships `evals/domain-truth.json` (a sourced answer key,
15
+ * authored by domain competence, distinct from behavioural
16
+ * evals.json). Presence is the machine signal; a passing
17
+ * run against it is the maintainer's Phase-3 gate.
18
+ * - `unvalidated` — no domain-truth fixture: general-purpose scaffold, domain
19
+ * correctness not independently validated.
20
+ *
21
+ * A domain skill can pass a format/behavioural eval AND carry a disclaimer AND
22
+ * still embed a wrong domain assumption — this axis is the one that catches it.
23
+ *
24
+ * Modes:
25
+ * (default) human report.
26
+ * --json machine-readable.
27
+ * --check ratchet gate (CI): the count of `validated` skills may not
28
+ * drop below the pinned floor (internal/evals/domain-soundness-floor.json);
29
+ * a missing floor is an all-zero floor (bootstrap-inert). The
30
+ * floor only rises via --write-floor after a fixture lands and
31
+ * passes. This is the enforced-honesty gate: it makes a claimed
32
+ * validation impossible to assert without the fixture on disk.
33
+ * --write-floor pin the current validated count as the floor (maintainer
34
+ * action after authoring + running a domain-truth fixture).
35
+ *
36
+ * Exit codes: 0 ok / 1 ratchet regression / 2 usage error.
37
+ */
38
+ import * as fs from 'node:fs';
39
+ import * as path from 'node:path';
40
+ import { fileURLToPath, pathToFileURL } from 'node:url';
41
+
42
+ import { parse as parseYaml } from 'yaml';
43
+
44
+ const _HERE = fileURLToPath(import.meta.url);
45
+ export const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
46
+
47
+ /** The non-coding profiles whose default surface this tool scopes. */
48
+ export const NON_CODING_PROFILES: readonly string[] = [
49
+ 'finance',
50
+ 'founder',
51
+ 'ops',
52
+ 'content_creator',
53
+ ];
54
+
55
+ export interface StatusOptions {
56
+ skillsDir?: string;
57
+ profilesDir?: string;
58
+ floorPath?: string;
59
+ }
60
+
61
+ export interface SkillStatus {
62
+ skill: string;
63
+ status: 'validated' | 'unvalidated';
64
+ }
65
+
66
+ export interface SoundnessReport {
67
+ validationSet: SkillStatus[];
68
+ validated: number;
69
+ unvalidated: number;
70
+ total: number;
71
+ }
72
+
73
+ function _skillsDir(o: StatusOptions): string {
74
+ return o.skillsDir ?? path.join(REPO_ROOT, 'src', 'skills');
75
+ }
76
+ function _profilesDir(o: StatusOptions): string {
77
+ return o.profilesDir ?? path.join(REPO_ROOT, 'src', 'agent-src', 'profiles');
78
+ }
79
+ export function _floorPath(o: StatusOptions = {}): string {
80
+ return (
81
+ o.floorPath ??
82
+ path.join(REPO_ROOT, 'internal', 'evals', 'domain-soundness-floor.json')
83
+ );
84
+ }
85
+
86
+ /** Union of the non-coding profiles' `skills_hint`, restricted to real skills. */
87
+ export function validationSet(o: StatusOptions = {}): string[] {
88
+ const profilesDir = _profilesDir(o);
89
+ const skillsDir = _skillsDir(o);
90
+ const out = new Set<string>();
91
+ for (const p of NON_CODING_PROFILES) {
92
+ const f = path.join(profilesDir, `${p}.yml`);
93
+ let doc: unknown;
94
+ try {
95
+ doc = parseYaml(fs.readFileSync(f, 'utf-8'));
96
+ } catch {
97
+ continue;
98
+ }
99
+ const profile = (doc as { profile?: unknown } | null)?.profile;
100
+ const defaults = (profile as { defaults?: unknown } | null)?.defaults;
101
+ const hint = (defaults as { skills_hint?: unknown } | null)?.skills_hint;
102
+ if (Array.isArray(hint)) {
103
+ for (const s of hint) {
104
+ if (typeof s === 'string' && fs.existsSync(path.join(skillsDir, s, 'SKILL.md'))) {
105
+ out.add(s);
106
+ }
107
+ }
108
+ }
109
+ }
110
+ return [...out].sort();
111
+ }
112
+
113
+ function _isValidated(skillsDir: string, skill: string): boolean {
114
+ return fs.existsSync(path.join(skillsDir, skill, 'evals', 'domain-truth.json'));
115
+ }
116
+
117
+ export function computeStatus(o: StatusOptions = {}): SoundnessReport {
118
+ const skillsDir = _skillsDir(o);
119
+ const set = validationSet(o);
120
+ const validationSetStatus: SkillStatus[] = set.map((skill) => ({
121
+ skill,
122
+ status: _isValidated(skillsDir, skill) ? 'validated' : 'unvalidated',
123
+ }));
124
+ const validated = validationSetStatus.filter((s) => s.status === 'validated').length;
125
+ return {
126
+ validationSet: validationSetStatus,
127
+ validated,
128
+ unvalidated: validationSetStatus.length - validated,
129
+ total: validationSetStatus.length,
130
+ };
131
+ }
132
+
133
+ function _readFloor(floorPath: string): number {
134
+ try {
135
+ const raw = JSON.parse(fs.readFileSync(floorPath, 'utf-8')) as { validated?: number };
136
+ return raw.validated ?? 0;
137
+ } catch {
138
+ return 0;
139
+ }
140
+ }
141
+
142
+ export function writeFloor(o: StatusOptions = {}): SoundnessReport {
143
+ const report = computeStatus(o);
144
+ const p = _floorPath(o);
145
+ fs.mkdirSync(path.dirname(p), { recursive: true });
146
+ fs.writeFileSync(
147
+ p,
148
+ JSON.stringify(
149
+ {
150
+ _comment:
151
+ 'Domain-soundness ratchet floor (road-to-domain-soundness Phase 3). ' +
152
+ 'domain_soundness_status --check fails if the validated count drops below this. ' +
153
+ 'Regenerate with --write-floor ONLY after a domain-truth fixture lands and passes.',
154
+ validated: report.validated,
155
+ },
156
+ null,
157
+ 2,
158
+ ) + '\n',
159
+ );
160
+ return report;
161
+ }
162
+
163
+ export function checkRatchet(o: StatusOptions = {}): { ok: boolean; message: string } {
164
+ const report = computeStatus(o);
165
+ const floor = _readFloor(_floorPath(o));
166
+ if (report.validated < floor) {
167
+ return {
168
+ ok: false,
169
+ message: `validated domain skills ${report.validated} < floor ${floor}`,
170
+ };
171
+ }
172
+ return { ok: true, message: `validated ${report.validated}/${report.total} (floor ${floor})` };
173
+ }
174
+
175
+ function _renderHuman(r: SoundnessReport): string {
176
+ const lines = [
177
+ 'Domain-soundness status — non-coding default-surface skills',
178
+ ` validated: ${r.validated}/${r.total}`,
179
+ ` unvalidated: ${r.unvalidated}/${r.total}`,
180
+ '',
181
+ ];
182
+ for (const s of r.validationSet) {
183
+ const mark = s.status === 'validated' ? '✅' : '·';
184
+ lines.push(` ${mark} ${s.skill.padEnd(28)} ${s.status}`);
185
+ }
186
+ return lines.join('\n');
187
+ }
188
+
189
+ export function main(argv: readonly string[] = process.argv.slice(2)): number {
190
+ const known = new Set(['--json', '--check', '--write-floor']);
191
+ for (const a of argv) {
192
+ if (!known.has(a)) {
193
+ process.stderr.write('usage: domain_soundness_status [--json | --check | --write-floor]\n');
194
+ return 2;
195
+ }
196
+ }
197
+ if (argv.includes('--write-floor')) {
198
+ const r = writeFloor();
199
+ process.stdout.write(`✅ domain-soundness floor pinned: validated ${r.validated}/${r.total}\n`);
200
+ return 0;
201
+ }
202
+ if (argv.includes('--check')) {
203
+ const { ok, message } = checkRatchet();
204
+ if (!ok) {
205
+ process.stdout.write(`❌ domain-soundness ratchet regressed: ${message}\n`);
206
+ return 1;
207
+ }
208
+ process.stdout.write(`✅ domain-soundness ratchet: ${message}.\n`);
209
+ return 0;
210
+ }
211
+ const report = computeStatus();
212
+ if (argv.includes('--json')) {
213
+ process.stdout.write(JSON.stringify(report, null, 2) + '\n');
214
+ return 0;
215
+ }
216
+ process.stdout.write(_renderHuman(report) + '\n');
217
+ return 0;
218
+ }
219
+
220
+ function _isCliEntry(): boolean {
221
+ if (process.argv[1] === undefined) return false;
222
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
223
+ if (import.meta.url === argvUrl) return true;
224
+ try {
225
+ return (
226
+ fs.realpathSync(fileURLToPath(import.meta.url)) ===
227
+ fs.realpathSync(path.resolve(process.argv[1]))
228
+ );
229
+ } catch {
230
+ return false;
231
+ }
232
+ }
233
+
234
+ if (_isCliEntry()) {
235
+ process.exit(main());
236
+ }
@@ -410,6 +410,20 @@ export function _render_catalog(
410
410
  '',
411
411
  `## Skills (${skills.length})`,
412
412
  '',
413
+ '> **Behavioural-eval coverage is measured, not implied.** A skill is',
414
+ '> behaviourally evaluated only if it ships `evals/evals.json`; most do',
415
+ '> not yet. Coverage is reported per tier (`./scripts-run',
416
+ '> src/scripts/skill_eval_coverage`) and CI-ratcheted so it can only rise.',
417
+ '> Listing here never implies a skill is behaviourally tested — the',
418
+ '> current coverage and its gap are published on [the proof page](proof.md).',
419
+ '',
420
+ '> **Non-coding domain correctness is scoped, not proven.** The',
421
+ '> `finance` / `founder` / `ops` / `content` skills are forged on TS/PHP',
422
+ '> codebases; their embedded domain heuristics are labeled `unvalidated`',
423
+ '> until they pass a sourced `evals/domain-truth.json` fixture',
424
+ '> (`./scripts-run src/scripts/domain_soundness_status`). A disclaimer',
425
+ '> floor bounds liability, not correctness — see [the proof page](proof.md).',
426
+ '',
413
427
  _render_table(skills, ['kind', 'name', 'extra', 'description'], '../', _to_shipped_path),
414
428
  '',
415
429
  `## Rules (${publicRules.length})`,
@@ -30,6 +30,7 @@
30
30
 
31
31
  import { spawnSync } from 'node:child_process';
32
32
  import * as fs from 'node:fs';
33
+ import { createRequire } from 'node:module';
33
34
  import * as path from 'node:path';
34
35
  import process from 'node:process';
35
36
  import { fileURLToPath } from 'node:url';
@@ -309,6 +310,39 @@ export function restore_hot_context(
309
310
  return { action: 'inject', reason: `restored (source=${source || 'startup'})`, context: block };
310
311
  }
311
312
 
313
+ // ---------------------------------------------------------------------
314
+ // opt-in memory session index (road-to-memory-retrieval-economy P5)
315
+ // ---------------------------------------------------------------------
316
+
317
+ /**
318
+ * Build the compact memory index when `memory.session_index: on`; `null`
319
+ * on the default-off path, empty corpus, or any failure (never blocks).
320
+ * Memory roots are cwd-relative (same contract as the MCP tool handlers),
321
+ * so the build runs chdir-wrapped to the workspace root.
322
+ */
323
+ function _session_index_block_or_null(root: string): string | null {
324
+ try {
325
+ // Lazy require (ESM-safe via createRequire) keeps the default-off
326
+ // fast path free of the memory_lookup + settings-cascade import cost.
327
+ const req = createRequire(import.meta.url);
328
+ const mod = req('./session_memory_index.js') as {
329
+ session_index_enabled: (root: string) => boolean;
330
+ build_session_index_block: () => string | null;
331
+ };
332
+ if (!mod.session_index_enabled(root)) return null;
333
+ const prev = process.cwd();
334
+ try {
335
+ process.chdir(root);
336
+ return mod.build_session_index_block();
337
+ } finally {
338
+ process.chdir(prev);
339
+ }
340
+ } catch (exc) {
341
+ process.stderr.write(`hot-context-hook: session index skipped: ${String(exc)}\n`);
342
+ return null;
343
+ }
344
+ }
345
+
312
346
  // ---------------------------------------------------------------------
313
347
  // CLI — dispatcher concern entry point
314
348
  // ---------------------------------------------------------------------
@@ -351,12 +385,23 @@ export function main(): number {
351
385
  } else if (event === 'session_start') {
352
386
  const source = String(payload.source ?? '');
353
387
  const decision = restore_hot_context(root, source);
388
+ const blocks: string[] = [];
354
389
  if (decision.action === 'inject' && decision.context) {
390
+ blocks.push(decision.context);
391
+ }
392
+ // Opt-in compact memory index (road-to-memory-retrieval-economy
393
+ // P5) — default OFF; rides the same injection surface. Memory
394
+ // roots are cwd-relative, so resolve from the workspace root.
395
+ const indexBlock = _session_index_block_or_null(root);
396
+ if (indexBlock !== null) {
397
+ blocks.push(indexBlock);
398
+ }
399
+ if (blocks.length > 0) {
355
400
  process.stdout.write(
356
401
  JSON.stringify({
357
402
  decision: 'allow',
358
- reason: decision.reason,
359
- context: decision.context,
403
+ reason: decision.action === 'inject' ? decision.reason : 'memory session index',
404
+ context: blocks.join('\n\n'),
360
405
  }) + '\n',
361
406
  );
362
407
  }
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Lint behavioural-eval freshness — a behavioural `evals.json` that pins the
4
+ * skill body it was authored against must still match the current SKILL.md
5
+ * body, or it is flagged stale (road-to-skill-eval-coverage Phase 3).
6
+ *
7
+ * Sibling to `lint_eval_freshness.ts`, DIFFERENT axis: that gate keeps a
8
+ * corpus-backed skill's TRIGGER eval fresh against an upstream SHA; this gate
9
+ * keeps a skill's BEHAVIOURAL eval fresh against its own SKILL.md body. The
10
+ * two never conflate.
11
+ *
12
+ * A skill's `evals/evals.json` is in scope when it carries a `skill_body_sha`
13
+ * (SHA-256 of the SKILL.md body — post-frontmatter, trimmed — at authoring
14
+ * time). Absent = unpinned, OUT of scope (nothing to keep fresh), mirroring
15
+ * `lint_eval_freshness`'s `upstream: null` handling. So the currently-shipped
16
+ * evals (which carry no pin) never fail — the gate only bites once an author
17
+ * opts in by pinning.
18
+ *
19
+ * In scope, the gate fails when the pinned `skill_body_sha` differs from the
20
+ * current SKILL.md body sha: the body moved, so the eval may no longer
21
+ * exercise it — re-verify and re-pin (`--write-floor`-style human step) rather
22
+ * than let a stale eval pass silently.
23
+ *
24
+ * Deterministic, no token spend: reads on-disk files only. The live re-eval is
25
+ * a separate, spend-bearing step; this gate verifies the pin is current.
26
+ *
27
+ * Exit codes: 0 clean (or none in scope) / 1 at least one stale / 2 usage.
28
+ */
29
+ import * as crypto from 'node:crypto';
30
+ import * as fs from 'node:fs';
31
+ import * as path from 'node:path';
32
+ import { fileURLToPath, pathToFileURL } from 'node:url';
33
+
34
+ const _HERE = fileURLToPath(import.meta.url);
35
+ export const REPO = path.resolve(path.dirname(_HERE), '..', '..');
36
+ let SKILLS_DIR = path.join(REPO, 'src', 'skills');
37
+
38
+ export function _setSkillsDirForTest(p: string): void {
39
+ SKILLS_DIR = p;
40
+ }
41
+
42
+ type Json = string | number | boolean | null | Json[] | { [k: string]: Json };
43
+
44
+ function _splitBody(text: string): string {
45
+ if (!text.startsWith('---\n')) return text.replace(/\s+$/, '');
46
+ const end = text.indexOf('\n---', 4);
47
+ if (end === -1) return text.replace(/\s+$/, '');
48
+ return text.slice(end + 4).replace(/^\n+/, '').replace(/\s+$/, '');
49
+ }
50
+
51
+ /** SHA-256 of a skill's SKILL.md body (post-frontmatter, trailing-ws-trimmed). */
52
+ export function skillBodySha(skillMdText: string): string {
53
+ return crypto.createHash('sha256').update(_splitBody(skillMdText), 'utf-8').digest('hex');
54
+ }
55
+
56
+ export interface FreshnessResult {
57
+ ok: boolean;
58
+ stale: Array<{ skill: string; pinned: string; current: string }>;
59
+ inScope: number;
60
+ }
61
+
62
+ export function checkFreshness(): FreshnessResult {
63
+ const stale: FreshnessResult['stale'] = [];
64
+ let inScope = 0;
65
+ let entries: fs.Dirent[];
66
+ try {
67
+ entries = fs.readdirSync(SKILLS_DIR, { withFileTypes: true });
68
+ } catch {
69
+ return { ok: true, stale, inScope };
70
+ }
71
+ entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
72
+ for (const e of entries) {
73
+ if (!e.isDirectory()) continue;
74
+ const evalsPath = path.join(SKILLS_DIR, e.name, 'evals', 'evals.json');
75
+ const skillMd = path.join(SKILLS_DIR, e.name, 'SKILL.md');
76
+ if (!fs.existsSync(evalsPath) || !fs.existsSync(skillMd)) continue;
77
+ let spec: Json;
78
+ try {
79
+ spec = JSON.parse(fs.readFileSync(evalsPath, 'utf-8')) as Json;
80
+ } catch {
81
+ continue;
82
+ }
83
+ const pinned =
84
+ spec && typeof spec === 'object' && !Array.isArray(spec)
85
+ ? (spec['skill_body_sha'] as string | undefined)
86
+ : undefined;
87
+ if (typeof pinned !== 'string' || pinned === '') continue; // unpinned → out of scope
88
+ inScope += 1;
89
+ const current = skillBodySha(fs.readFileSync(skillMd, 'utf-8'));
90
+ if (current !== pinned) {
91
+ stale.push({ skill: e.name, pinned, current });
92
+ }
93
+ }
94
+ return { ok: stale.length === 0, stale, inScope };
95
+ }
96
+
97
+ export function main(argv: readonly string[] = process.argv.slice(2)): number {
98
+ let quiet = false;
99
+ for (const a of argv) {
100
+ if (a === '--quiet') quiet = true;
101
+ else {
102
+ process.stderr.write('usage: lint_behavioural_eval_freshness [--quiet]\n');
103
+ return 2;
104
+ }
105
+ }
106
+ const { ok, stale, inScope } = checkFreshness();
107
+ if (!ok) {
108
+ for (const s of stale) {
109
+ process.stderr.write(
110
+ `stale behavioural eval: ${s.skill}/evals/evals.json pinned ${s.pinned.slice(0, 12)}… ` +
111
+ `but SKILL.md body is ${s.current.slice(0, 12)}… — re-verify and re-pin skill_body_sha\n`,
112
+ );
113
+ }
114
+ return 1;
115
+ }
116
+ if (!quiet) {
117
+ process.stdout.write(
118
+ `✅ behavioural-eval freshness: ${inScope} pinned eval(s) current (unpinned evals out of scope).\n`,
119
+ );
120
+ }
121
+ return 0;
122
+ }
123
+
124
+ function _isCliEntry(): boolean {
125
+ if (process.argv[1] === undefined) return false;
126
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
127
+ if (import.meta.url === argvUrl) return true;
128
+ try {
129
+ return (
130
+ fs.realpathSync(fileURLToPath(import.meta.url)) ===
131
+ fs.realpathSync(path.resolve(process.argv[1]))
132
+ );
133
+ } catch {
134
+ return false;
135
+ }
136
+ }
137
+
138
+ if (_isCliEntry()) {
139
+ process.exit(main());
140
+ }
@@ -50,14 +50,18 @@
50
50
  "last": {"type": "integer", "description": "Return only the most recent N entries, after other filters apply.", "minimum": 1},
51
51
  "session": {"type": "string", "description": "Filter to a single 16-char session id."},
52
52
  "entry_type": {"type": "string", "description": "Filter by the `t` tag (e.g. note, decision, phase)."},
53
- "path": {"type": "string", "description": "Optional history-file path override; defaults to the standard chat-history location under the project root."}
53
+ "path": {"type": "string", "description": "Optional history-file path override; defaults to the standard chat-history location under the project root."},
54
+ "around": {"type": "integer", "minimum": 0, "description": "Timeline anchor: return the entries around this ref (from a detail:\"index\" row) instead of the filtered list — depth_before/depth_after neighbours plus the anchor. Refs are within-file ordinals; rotation invalidates them."},
55
+ "depth_before": {"type": "integer", "minimum": 0, "default": 3, "description": "Neighbours before the anchor (with `around`). Defaults to 3."},
56
+ "depth_after": {"type": "integer", "minimum": 0, "default": 3, "description": "Neighbours after the anchor (with `around`). Defaults to 3."},
57
+ "detail": {"type": "string", "enum": ["index", "full"], "default": "full", "description": "'index' returns compact rows (ref, t tag, ~100-char preview, tokens_estimate) instead of full entries — scan first, then re-read the refs you need via `around` or a narrowed filter. 'full' (default) returns complete entries."}
54
58
  },
55
59
  "additionalProperties": false
56
60
  }
57
61
  },
58
62
  {
59
63
  "name": "memory_lookup",
60
- "description": "Retrieve engineering-memory entries for one or more memory types, optionally narrowed to specific anchor paths. Use before editing a security-sensitive or historically buggy file to surface prior incidents, ownership, and patterns tied to it. Reads `agents/memory/<type>/*.yml` plus the `agents/memory/intake/*.jsonl` signal log. Read-only. Returns the v1 retrieval envelope: a `status` field plus per-type `slices` carrying the matched entries.",
64
+ "description": "Retrieve engineering-memory entries for one or more memory types, optionally narrowed to specific anchor paths. Use before editing a security-sensitive or historically buggy file to surface prior incidents, ownership, and patterns tied to it. WORKFLOW: call with detail:\"index\" FIRST — each row carries id, title and tokens_estimate (the cost of fetching it) — then fetch full bodies via memory_get ONLY for the ids you will actually use, batching multiple ids into one call. Reads `agents/memory/<type>/*.yml` plus the `agents/memory/intake/*.jsonl` signal log. Read-only. Returns the v1 retrieval envelope: a `status` field plus per-type `slices` carrying the matched entries.",
61
65
  "side_effect": "ro",
62
66
  "implemented_on": ["stdio"],
63
67
  "input_schema": {
@@ -65,12 +69,27 @@
65
69
  "properties": {
66
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."},
67
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)."},
68
- "limit": {"type": "integer", "minimum": 1, "default": 5, "description": "Maximum entries to return per type. Defaults to 5."}
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."}
69
74
  },
70
75
  "required": ["types"],
71
76
  "additionalProperties": false
72
77
  }
73
78
  },
79
+ {
80
+ "name": "memory_get",
81
+ "description": "Batch-fetch FULL memory entries by id — the second half of the index-first retrieval workflow. Call memory_lookup with detail:\"index\" first, pick the ids whose title/tokens_estimate justify the fetch, then fetch them here in ONE batched call. Unknown ids are reported per-id (ids[<id>]=\"unknown\"), never failing the batch. Read-only.",
82
+ "side_effect": "ro",
83
+ "implemented_on": ["stdio"],
84
+ "input_schema": {
85
+ "type": "object",
86
+ "properties": {
87
+ "ids": {"type": "array", "items": {"type": "string"}, "minItems": 1, "description": "Entry ids to fetch (from a detail:\"index\" lookup)."}
88
+ },
89
+ "required": ["ids"],
90
+ "additionalProperties": false
91
+ }
92
+ },
74
93
  {
75
94
  "name": "memory_signal",
76
95
  "description": "Record an engineering-memory signal — a short, anchored observation such as a recurring bug pattern or an ownership note — to the monthly intake log `agents/memory/intake/signals-YYYY-MM.jsonl`. Use to capture a learning tied to a specific file so future `memory_lookup` calls surface it. Appends to the filesystem and is rate-limited per `(type, path)` within a rolling window. Returns the recorded signal.",