@event4u/agent-config 8.7.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 (171) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +109 -222
  3. package/CONTRIBUTING.md +25 -0
  4. package/README.md +7 -4
  5. package/dist/agent-src/commands/agent-handoff.md +39 -2
  6. package/dist/agent-src/commands/research/deep.md +8 -0
  7. package/dist/agent-src/commands/research/report.md +7 -0
  8. package/dist/agent-src/commands/roadmap/create.md +12 -0
  9. package/dist/agent-src/commands/roadmap/process-full.md +31 -5
  10. package/dist/agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +30 -1
  11. package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +1 -1
  12. package/dist/agent-src/contexts/execution/orchestration-benchmark-gate.md +28 -16
  13. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +36 -5
  14. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +37 -0
  15. package/dist/agent-src/contexts/execution/subagent-topologies.md +34 -0
  16. package/dist/agent-src/contexts/execution/verification-mechanics.md +9 -9
  17. package/dist/agent-src/rules/active-remediation.md +92 -0
  18. package/dist/agent-src/rules/agent-authority.md +1 -1
  19. package/dist/agent-src/rules/broken-access-control.md +106 -0
  20. package/dist/agent-src/rules/communication-through-line.md +42 -0
  21. package/dist/agent-src/rules/decision-revisit-gate.md +8 -0
  22. package/dist/agent-src/rules/delegation-policy.md +5 -0
  23. package/dist/agent-src/rules/direct-answers.md +1 -1
  24. package/dist/agent-src/rules/downstream-changes.md +1 -0
  25. package/dist/agent-src/rules/external-code-graph-interop.md +65 -0
  26. package/dist/agent-src/rules/git-history-discipline.md +18 -0
  27. package/dist/agent-src/rules/lethal-trifecta-guard.md +6 -1
  28. package/dist/agent-src/rules/minimal-safe-diff.md +66 -1
  29. package/dist/agent-src/rules/non-destructive-by-default.md +3 -0
  30. package/dist/agent-src/rules/prefer-enums-over-literals.md +65 -0
  31. package/dist/agent-src/rules/question-not-instruction.md +76 -0
  32. package/dist/agent-src/rules/senior-engineering-discipline.md +82 -0
  33. package/dist/agent-src/rules/think-before-action.md +1 -0
  34. package/dist/agent-src/rules/tool-safety.md +8 -0
  35. package/dist/agent-src/rules/untrusted-input-defense.md +87 -22
  36. package/dist/agent-src/scripts/update_roadmap_progress.ts +2 -1
  37. package/dist/agent-src/skills/accessibility-auditor/SKILL.md +19 -0
  38. package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +112 -0
  39. package/dist/agent-src/skills/ai-code-blindspots/evals/triggers.json +52 -0
  40. package/dist/agent-src/skills/authz-review/SKILL.md +21 -4
  41. package/dist/agent-src/skills/code-review/SKILL.md +2 -0
  42. package/dist/agent-src/skills/command-writing/SKILL.md +53 -40
  43. package/dist/agent-src/skills/dcf-modeling/evals/domain-truth.json +29 -0
  44. package/dist/agent-src/skills/deep-reading-analyst/SKILL.md +1 -0
  45. package/dist/agent-src/skills/dependency-upgrade/SKILL.md +20 -0
  46. package/dist/agent-src/skills/design-intelligence/SKILL.md +49 -12
  47. package/dist/agent-src/skills/design-review/SKILL.md +29 -15
  48. package/dist/agent-src/skills/design-system-capture/SKILL.md +63 -24
  49. package/dist/agent-src/skills/forecasting/evals/domain-truth.json +29 -0
  50. package/dist/agent-src/skills/frontend-render-security/SKILL.md +104 -0
  51. package/dist/agent-src/skills/frontend-render-security/evals/triggers.json +52 -0
  52. package/dist/agent-src/skills/markitdown/SKILL.md +1 -0
  53. package/dist/agent-src/skills/mcp/SKILL.md +24 -0
  54. package/dist/agent-src/skills/mcp-builder/SKILL.md +9 -1
  55. package/dist/agent-src/skills/memory-consolidation/SKILL.md +55 -4
  56. package/dist/agent-src/skills/roadmap-writing/SKILL.md +46 -5
  57. package/dist/agent-src/skills/runway-cognition/evals/domain-truth.json +29 -0
  58. package/dist/agent-src/skills/scenario-modeling/evals/domain-truth.json +29 -0
  59. package/dist/agent-src/skills/secrets-management/SKILL.md +7 -0
  60. package/dist/agent-src/skills/skill-writing/SKILL.md +12 -3
  61. package/dist/agent-src/skills/subagent-orchestration/SKILL.md +27 -33
  62. package/dist/agent-src/skills/supply-chain-intake/SKILL.md +112 -0
  63. package/dist/agent-src/skills/supply-chain-intake/evals/triggers.json +52 -0
  64. package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +72 -2
  65. package/dist/agent-src/skills/ui-component-architect/SKILL.md +9 -0
  66. package/dist/agent-src/skills/unit-economics-modeling/evals/domain-truth.json +40 -0
  67. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  68. package/dist/agent-src/templates/roadmaps.md +60 -0
  69. package/dist/cli/registry.js +3 -0
  70. package/dist/cli/registry.js.map +1 -1
  71. package/dist/discovery/deprecation-report.md +1 -1
  72. package/dist/discovery/discovery-manifest.json +268 -46
  73. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  74. package/dist/discovery/discovery-manifest.summary.md +4 -4
  75. package/dist/discovery/orphan-report.md +1 -1
  76. package/dist/discovery/packs.json +18 -8
  77. package/dist/discovery/trust-report.md +11 -11
  78. package/dist/discovery/workspaces.json +16 -6
  79. package/dist/install/install.mjs +141 -3
  80. package/dist/mcp/awesome-mcp-servers.row.md +1 -1
  81. package/dist/mcp/mcp-cloudflare-catalogue.json +1 -1
  82. package/dist/mcp/registry-manifest.json +4 -4
  83. package/dist/router.json +1 -1
  84. package/dist/scripts/_lib/claude_builtin_names.js +145 -0
  85. package/dist/scripts/_lib/claude_builtin_names.js.map +1 -0
  86. package/dist/scripts/condense.js +14 -0
  87. package/dist/scripts/condense.js.map +1 -1
  88. package/dist/server/schemas/settings.js +2 -1
  89. package/dist/server/schemas/settings.js.map +1 -1
  90. package/docs/CLAIMS.md +58 -2
  91. package/docs/architecture.md +3 -3
  92. package/docs/archive/CHANGELOG-pre-8.9.0.md +306 -0
  93. package/docs/benchmark.md +113 -0
  94. package/docs/catalog.md +32 -5
  95. package/docs/contracts/command-clusters.md +18 -0
  96. package/docs/contracts/design-artifact-verification.md +124 -0
  97. package/docs/contracts/injected-block-authenticity-model.md +66 -0
  98. package/docs/contracts/mcp-tool-inventory.md +31 -30
  99. package/docs/decisions/ADR-061-corpus-grounding-layer.md +13 -0
  100. package/docs/decisions/ADR-105-automatic-subagent-orchestration.md +6 -0
  101. package/docs/decisions/ADR-116-memory-tripwire-activation-path.md +11 -2
  102. package/docs/decisions/ADR-117-subagents-auto-default-on.md +88 -0
  103. package/docs/decisions/INDEX.md +1 -0
  104. package/docs/experiences/content_creator.md +2 -0
  105. package/docs/experiences/finance.md +2 -0
  106. package/docs/experiences/founder.md +2 -0
  107. package/docs/experiences/ops.md +2 -0
  108. package/docs/featured-skills.md +2 -2
  109. package/docs/getting-started-by-role.md +1 -1
  110. package/docs/getting-started.md +1 -1
  111. package/docs/governance-advantage.md +1 -1
  112. package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +27 -0
  113. package/docs/guidelines/agent-infra/emphasis-budget.md +45 -0
  114. package/docs/guidelines/agent-infra/memory-access.md +12 -0
  115. package/docs/guidelines/agent-infra/simplicity-and-goal-demos.md +151 -0
  116. package/docs/guidelines/agent-infra/tool-description-as-policy.md +35 -0
  117. package/docs/guidelines/design-canon.md +1 -0
  118. package/docs/proof.md +60 -3
  119. package/docs/release-runbook.md +138 -0
  120. package/docs/second-brain-scope.md +116 -0
  121. package/docs/succession.md +80 -0
  122. package/package.json +2 -2
  123. package/src/agent-src/scripts/update_roadmap_progress.ts +2 -1
  124. package/src/config/agent-settings.template.yml +25 -6
  125. package/src/scripts/_dispatch.bash +27 -0
  126. package/src/scripts/_lib/claude_builtin_names.ts +148 -0
  127. package/src/scripts/_lib/file_slicer.ts +117 -0
  128. package/src/scripts/_lib/lexical_index.ts +155 -0
  129. package/src/scripts/_lib/orchestration_gate.ts +12 -8
  130. package/src/scripts/_lib/orchestration_record.ts +170 -0
  131. package/src/scripts/_lib/orchestration_savings.ts +180 -0
  132. package/src/scripts/_lib/retrieval_sanitize.ts +71 -0
  133. package/src/scripts/_lib/security_lint.ts +1 -0
  134. package/src/scripts/_lib/stat_index.ts +98 -0
  135. package/src/scripts/benchmark.ts +122 -0
  136. package/src/scripts/build_proof.ts +51 -0
  137. package/src/scripts/chat_history.ts +79 -7
  138. package/src/scripts/check_memory.ts +2 -0
  139. package/src/scripts/check_quality_regression.ts +51 -0
  140. package/src/scripts/check_roadmap_trackable.ts +3 -2
  141. package/src/scripts/condense.ts +14 -0
  142. package/src/scripts/discovery_graph.ts +0 -0
  143. package/src/scripts/domain_soundness_status.ts +236 -0
  144. package/src/scripts/external_sources_denylist.json +4 -1
  145. package/src/scripts/generate_index.ts +17 -0
  146. package/src/scripts/hot_context_hook.ts +47 -2
  147. package/src/scripts/install-hooks.sh +25 -0
  148. package/src/scripts/install.ts +35 -2
  149. package/src/scripts/learning_sidecar.ts +0 -0
  150. package/src/scripts/lint_agent_skill_names.ts +29 -0
  151. package/src/scripts/lint_behavioural_eval_freshness.ts +140 -0
  152. package/src/scripts/lint_knowledge_scale.ts +4 -3
  153. package/src/scripts/lint_roadmap_complexity.ts +124 -0
  154. package/src/scripts/lint_versioned_cache.ts +195 -0
  155. package/src/scripts/mcp_server/consumer_tool_catalog.json +23 -3
  156. package/src/scripts/mcp_server/tools.ts +182 -1
  157. package/src/scripts/measure_lexical_ranking.ts +173 -0
  158. package/src/scripts/memory_lookup.ts +0 -0
  159. package/src/scripts/memory_replay.ts +285 -0
  160. package/src/scripts/orchestration_record.ts +140 -0
  161. package/src/scripts/orchestration_savings_report.ts +124 -0
  162. package/src/scripts/schemas/domain-truth.schema.json +94 -0
  163. package/src/scripts/schemas/evals.schema.json +113 -0
  164. package/src/scripts/schemas/skill.schema.json +4 -0
  165. package/src/scripts/score_domain_truth.ts +192 -0
  166. package/src/scripts/second_brain_retrieval.ts +278 -0
  167. package/src/scripts/second_brain_run.ts +341 -0
  168. package/src/scripts/second_brain_score.ts +186 -0
  169. package/src/scripts/session_memory_index.ts +94 -0
  170. package/src/scripts/skill_eval_coverage.ts +318 -0
  171. package/src/server/schemas/settings.ts +5 -2
@@ -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
  }
@@ -42,6 +42,31 @@ if ! ./scripts-run src/scripts/check_no_conflict_markers --quiet; then
42
42
  fail=1
43
43
  fi
44
44
 
45
+ # Changed-files static pass (typecheck + lint) — the deterministic backstop to
46
+ # the behavioural verify-before-complete rule (#818). Catches the compile/lint
47
+ # errors that reach remote CI "Static Checks" (e.g. TS18048). Runs only when
48
+ # the push touches .ts, so docs-only pushes stay fast. Skip a genuine WIP push
49
+ # with AGENT_CONFIG_SKIP_PREPUSH_STATIC=1 (the agent cannot use --no-verify).
50
+ echo "🔍 Static check on changed TypeScript (typecheck + lint)..."
51
+ if [ "${AGENT_CONFIG_SKIP_PREPUSH_STATIC:-}" = "1" ]; then
52
+ echo "⏭️ skipped via AGENT_CONFIG_SKIP_PREPUSH_STATIC=1"
53
+ else
54
+ base="$(git merge-base HEAD origin/main 2>/dev/null || echo HEAD~1)"
55
+ changed_ts="$(git diff --name-only --diff-filter=d "$base"...HEAD -- '*.ts' 2>/dev/null || true)"
56
+ if [ -n "$changed_ts" ]; then
57
+ if ! npx eslint $changed_ts; then
58
+ echo "❌ ESLint failed on changed TypeScript. Fix before pushing (or AGENT_CONFIG_SKIP_PREPUSH_STATIC=1 for a WIP push)."
59
+ fail=1
60
+ fi
61
+ if ! npm run --silent typecheck; then
62
+ echo "❌ Typecheck (tsc) failed — this is the class of error remote CI 'Static Checks' catches (#818). Fix before pushing."
63
+ fail=1
64
+ fi
65
+ else
66
+ echo "⏭️ no changed .ts vs origin/main — skipping."
67
+ fi
68
+ fi
69
+
45
70
  if [ $fail -ne 0 ]; then
46
71
  echo ""
47
72
  echo " Push blocked — fix the failures above and re-push."
@@ -72,6 +72,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
72
72
  import type * as YamlModule from 'yaml';
73
73
 
74
74
  import { build_merge_entries } from './_lib/json_pointers.js';
75
+ import { is_claude_builtin_name } from './_lib/claude_builtin_names.js';
75
76
  import * as installed_lock from './_lib/installed_lock.js';
76
77
  import * as surface_tiers from './_lib/surface_tiers.js';
77
78
  import * as global_deploy_inventory from './_lib/global_deploy_inventory.js';
@@ -2847,15 +2848,40 @@ function _deploy_claude_desktop(
2847
2848
  * discovery (revert = one package release, no per-machine gating).
2848
2849
  * Tier-2/internal flat commands stay command files. A command whose name
2849
2850
  * collides with a real skill is skipped (the skill already owns `/name`).
2851
+ *
2852
+ * Reserved-name floor: a flat `/name` that equals a Claude Code BUILT-IN
2853
+ * command or bundled skill (see `_lib/claude_builtin_names.ts`) would shadow
2854
+ * the built-in — e.g. a `review` wrapper hides Claude Code's own `/review`.
2855
+ * Such slugs get NEITHER a skill wrapper NOR a flat command file on this
2856
+ * anchor (their nested `/cluster:sub` commands are untouched); the flat file
2857
+ * is removed so a future upstream flat-discovery fix cannot resurrect the
2858
+ * shadowing. The suite complements the host — it never overlays built-ins.
2850
2859
  */
2851
2860
  const _CLAUDE_FLAT_WRAPPER_EXTRA = new Set(['commit']); // essential; absent from the manifest
2852
2861
  export function _apply_claude_flat_command_wrappers(
2853
2862
  anchor: string,
2854
2863
  package_root: string,
2855
2864
  current_files: Set<string>,
2856
- ): { wrapped: string[]; collisions: string[] } {
2865
+ ): { wrapped: string[]; collisions: string[]; reserved: string[] } {
2857
2866
  const wrapped: string[] = [];
2858
2867
  const collisions: string[] = [];
2868
+ const reserved: string[] = [];
2869
+ // Reserved-name sweep over ALL flat command files (any tier): a name
2870
+ // shadowing a Claude Code built-in never ships on the claude-code anchor.
2871
+ const commands_dir = path.join(anchor, 'commands');
2872
+ let flat_entries: string[] = [];
2873
+ try {
2874
+ flat_entries = fs.readdirSync(commands_dir).filter((f) => f.endsWith('.md'));
2875
+ } catch {
2876
+ // No commands dir → nothing to sweep.
2877
+ }
2878
+ for (const fname of flat_entries.sort()) {
2879
+ const slug = fname.slice(0, -'.md'.length);
2880
+ if (!is_claude_builtin_name(slug)) continue;
2881
+ fs.rmSync(path.join(commands_dir, fname), { force: true });
2882
+ current_files.delete(`commands/${fname}`);
2883
+ reserved.push(slug);
2884
+ }
2859
2885
  // Tier-0/1 visible command slugs from the locked discovery manifest.
2860
2886
  const eligible = new Set<string>(_CLAUDE_FLAT_WRAPPER_EXTRA);
2861
2887
  try {
@@ -2892,7 +2918,7 @@ export function _apply_claude_flat_command_wrappers(
2892
2918
  current_files.add(`skills/${slug}/SKILL.md`);
2893
2919
  wrapped.push(slug);
2894
2920
  }
2895
- return { wrapped, collisions };
2921
+ return { wrapped, collisions, reserved };
2896
2922
  }
2897
2923
 
2898
2924
  function _deploy_global_content(
@@ -2949,6 +2975,13 @@ function _deploy_global_content(
2949
2975
  'skill wrappers (Claude Code flat-command discovery workaround)',
2950
2976
  );
2951
2977
  }
2978
+ if (res.reserved.length > 0 && !state.QUIET) {
2979
+ info(
2980
+ ` claude-code: ${res.reserved.length} flat command(s) withheld — name is a ` +
2981
+ `Claude Code built-in (${res.reserved.join(', ')}); nested /cluster:sub ` +
2982
+ 'commands remain available',
2983
+ );
2984
+ }
2952
2985
  }
2953
2986
 
2954
2987
  const missing_targets = _verify_deploy_targets(anchor, plan);
Binary file
@@ -14,6 +14,7 @@ import * as path from 'node:path';
14
14
  import { fileURLToPath, pathToFileURL } from 'node:url';
15
15
 
16
16
  import { command_slug } from './_lib/agent_src.js';
17
+ import { is_claude_builtin_name } from './_lib/claude_builtin_names.js';
17
18
 
18
19
  const SCRIPTS_DIR = path.dirname(fileURLToPath(import.meta.url));
19
20
  const ROOT = path.dirname(path.dirname(SCRIPTS_DIR));
@@ -97,6 +98,18 @@ function frontmatterNameFromFile(p: string): string | null {
97
98
  return _frontmatter_name(fs.readFileSync(p, 'utf-8'));
98
99
  }
99
100
 
101
+ const USER_INVOCABLE_FALSE_RE = /^user-invocable:\s*false\s*$/m;
102
+
103
+ /** True when the SKILL.md opts out of slash registration (`user-invocable: false`). */
104
+ export function _opts_out_of_slash(text: string): boolean {
105
+ if (!text.startsWith('---')) {
106
+ return false;
107
+ }
108
+ const end = text.indexOf('\n---', 3);
109
+ const block = end !== -1 ? text.slice(3, end) : text;
110
+ return USER_INVOCABLE_FALSE_RE.test(block);
111
+ }
112
+
100
113
  export function _spec_violation(name: string): string | null {
101
114
  if (name.length > MAX_NAME_LEN) {
102
115
  return `longer than ${MAX_NAME_LEN} chars`;
@@ -178,6 +191,22 @@ export function check_skills(): string[] {
178
191
  'requires the folder name to match the `name:` field',
179
192
  );
180
193
  }
194
+ // Reserved-name floor: a skill named after a Claude Code built-in
195
+ // command / bundled skill shadows the built-in when projected as
196
+ // `/name` (e.g. a `mcp` skill hides Claude Code's own /mcp dialog).
197
+ // Opting out of slash registration (`user-invocable: false`) keeps
198
+ // the skill model-loadable while the host retains the /name.
199
+ // Reserved set: src/scripts/_lib/claude_builtin_names.ts.
200
+ if (
201
+ is_claude_builtin_name(dirname) &&
202
+ !_opts_out_of_slash(fs.readFileSync(md, 'utf-8'))
203
+ ) {
204
+ violations.push(
205
+ `${rel}: \`${dirname}\` is a Claude Code built-in command name — ` +
206
+ 'add `user-invocable: false` (model-only invocation) or rename ' +
207
+ 'the skill; the suite must never shadow a host built-in',
208
+ );
209
+ }
181
210
  }
182
211
  return violations;
183
212
  }
@@ -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
+ }
@@ -14,9 +14,10 @@
14
14
  * - sessions scale — `agents/knowledge/sessions/` > 50 pages
15
15
  * → design fold-through-consolidate-gate for tracked pages
16
16
  * - type scale — any single memory/knowledge type > 200 files
17
- * → build the pre-decided file-first in-memory BM25 CLI
17
+ * → rank via `_lib/lexical_index.ts` (hand-rolled BM25 +
18
+ * trigram, no engine fork / no FTS5; ADR-061 honoured)
18
19
  * - corpus scale — > 500 files across all types
19
- * → same BM25 activation path
20
+ * → same lexical-index activation path
20
21
  * - hot-context size — `agents/runtime/state/hot-context.md` > 600 tokens
21
22
  * (estimated at 4 chars/token) → trim schema / fix the
22
23
  * deterministic writer
@@ -151,7 +152,7 @@ export function runChecks(root: string): Warning[] {
151
152
  metric: `${n}/${TYPE_FILES_MAX}`,
152
153
  message:
153
154
  `type '${type}' holds ${n} files (> ${TYPE_FILES_MAX}). ` +
154
- 'Activation path (pre-decided): build the file-first in-memory BM25 CLI (re-index at session start, no vectors, no service).',
155
+ 'Activation path (resolved, road-to-retrieval-substrate-hardening B2): rank via the hand-rolled BM25 + trigram index in `_lib/lexical_index.ts` (pure stdlib, NO engine fork / NO SQLite-FTS5, ADR-061 honoured). Measured lift: mean tie-set 3.333 → 1.0 (internal/bench/reports/lexical-ranking.json). Wire it lazily at first lookup + a stat-index, no vectors, no service.',
155
156
  });
156
157
  }
157
158
  }
@@ -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