@event4u/agent-config 8.6.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 (118) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +81 -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/skill-writing/SKILL.md +4 -0
  23. package/dist/agent-src/skills/supply-chain-intake/SKILL.md +100 -0
  24. package/dist/agent-src/skills/supply-chain-intake/evals/triggers.json +52 -0
  25. package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +43 -2
  26. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  27. package/dist/agent-src/templates/roadmaps.md +24 -0
  28. package/dist/discovery/deprecation-report.md +1 -1
  29. package/dist/discovery/discovery-manifest.json +196 -16
  30. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  31. package/dist/discovery/discovery-manifest.summary.md +3 -3
  32. package/dist/discovery/orphan-report.md +1 -1
  33. package/dist/discovery/packs.json +13 -5
  34. package/dist/discovery/trust-report.md +11 -11
  35. package/dist/discovery/workspaces.json +12 -4
  36. package/dist/install/emit_host_rules_cli.js +185 -0
  37. package/dist/install/emit_host_rules_cli.js.map +1 -0
  38. package/dist/install/install.mjs +11 -2
  39. package/dist/install/plan.js +4 -1
  40. package/dist/install/plan.js.map +1 -1
  41. package/dist/install/rule_scope.js +109 -0
  42. package/dist/install/rule_scope.js.map +1 -0
  43. package/dist/install/rule_scope_cli.js +77 -0
  44. package/dist/install/rule_scope_cli.js.map +1 -0
  45. package/dist/install/wizard-plan.js +14 -1
  46. package/dist/install/wizard-plan.js.map +1 -1
  47. package/dist/mcp/awesome-mcp-servers.row.md +1 -1
  48. package/dist/mcp/mcp-cloudflare-catalogue.json +1 -1
  49. package/dist/mcp/registry-manifest.json +4 -4
  50. package/dist/router.json +1 -1
  51. package/dist/scripts/_lib/agent_settings.js +963 -0
  52. package/dist/scripts/_lib/agent_settings.js.map +1 -0
  53. package/dist/scripts/_lib/agent_src.js +607 -0
  54. package/dist/scripts/_lib/agent_src.js.map +1 -0
  55. package/dist/scripts/_lib/claude_settings_hooks.js +209 -0
  56. package/dist/scripts/_lib/claude_settings_hooks.js.map +1 -0
  57. package/dist/scripts/_lib/model_tier.js +64 -0
  58. package/dist/scripts/_lib/model_tier.js.map +1 -0
  59. package/dist/scripts/_lib/script_output.js +140 -0
  60. package/dist/scripts/_lib/script_output.js.map +1 -0
  61. package/dist/scripts/_lib/user_global_paths.js +297 -0
  62. package/dist/scripts/_lib/user_global_paths.js.map +1 -0
  63. package/dist/scripts/condense.js +2181 -0
  64. package/dist/scripts/condense.js.map +1 -0
  65. package/dist/server/routes/install.js +18 -2
  66. package/dist/server/routes/install.js.map +1 -1
  67. package/dist/server/schemas/settings.js +4 -1
  68. package/dist/server/schemas/settings.js.map +1 -1
  69. package/docs/CLAIMS.md +30 -2
  70. package/docs/architecture.md +2 -2
  71. package/docs/catalog.md +14 -0
  72. package/docs/contracts/mcp-tool-inventory.md +31 -30
  73. package/docs/decisions/ADR-105-automatic-subagent-orchestration.md +6 -0
  74. package/docs/decisions/ADR-116-memory-tripwire-activation-path.md +2 -2
  75. package/docs/decisions/ADR-117-subagents-auto-default-on.md +88 -0
  76. package/docs/decisions/INDEX.md +1 -0
  77. package/docs/experiences/content_creator.md +2 -0
  78. package/docs/experiences/finance.md +2 -0
  79. package/docs/experiences/founder.md +2 -0
  80. package/docs/experiences/ops.md +2 -0
  81. package/docs/featured-skills.md +2 -2
  82. package/docs/getting-started-by-role.md +1 -1
  83. package/docs/getting-started.md +1 -1
  84. package/docs/governance-advantage.md +1 -1
  85. package/docs/guidelines/agent-infra/memory-access.md +12 -0
  86. package/docs/proof.md +45 -3
  87. package/docs/release-runbook.md +138 -0
  88. package/docs/second-brain-scope.md +73 -0
  89. package/docs/succession.md +80 -0
  90. package/package.json +2 -2
  91. package/src/config/agent-settings.template.yml +29 -8
  92. package/src/install/emit_host_rules_cli.ts +190 -0
  93. package/src/install/plan.ts +11 -1
  94. package/src/install/rule_scope.ts +120 -0
  95. package/src/install/rule_scope_cli.ts +79 -0
  96. package/src/install/wizard-plan.ts +22 -1
  97. package/src/scripts/_lib/orchestration_gate.ts +12 -8
  98. package/src/scripts/_lib/orchestration_savings.ts +127 -0
  99. package/src/scripts/build_proof.ts +38 -0
  100. package/src/scripts/chat_history.ts +79 -7
  101. package/src/scripts/check_trigger_eval_presence.ts +171 -0
  102. package/src/scripts/domain_soundness_status.ts +236 -0
  103. package/src/scripts/generate_index.ts +14 -0
  104. package/src/scripts/hot_context_hook.ts +47 -2
  105. package/src/scripts/install.sh +118 -4
  106. package/src/scripts/lint_behavioural_eval_freshness.ts +140 -0
  107. package/src/scripts/mcp_server/consumer_tool_catalog.json +22 -3
  108. package/src/scripts/mcp_server/tools.ts +159 -1
  109. package/src/scripts/memory_lookup.ts +184 -6
  110. package/src/scripts/memory_replay.ts +285 -0
  111. package/src/scripts/orchestration_savings_report.ts +100 -0
  112. package/src/scripts/schemas/domain-truth.schema.json +94 -0
  113. package/src/scripts/schemas/evals.schema.json +113 -0
  114. package/src/scripts/second_brain_score.ts +186 -0
  115. package/src/scripts/session_memory_index.ts +94 -0
  116. package/src/scripts/skill_eval_coverage.ts +318 -0
  117. package/src/scripts/trigger_eval_grandfather.json +226 -0
  118. package/src/server/schemas/settings.ts +11 -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
  }
@@ -23,8 +23,14 @@ set -euo pipefail
23
23
  COPY_DIRS="rules" # Subdirectories where files must be real copies (space-separated)
24
24
 
25
25
  # Rules that are internal to the agent-config package and should NOT be shipped to consumers.
26
- # These are only relevant when developing the agent-config package itself.
27
- EXCLUDE_RULES="source-of-truth.md augment-portability.md docs-sync.md"
26
+ # Static COMPAT floor only (see src/install/rule_scope.ts for the recorded
27
+ # source-of-truth.md decision — same treatment on project AND global paths).
28
+ # The full workspace/pack scoping list is resolved at runtime by
29
+ # resolve_excluded_rules() via src/install/rule_scope_cli.ts, which re-uses
30
+ # the projection predicate (rule_in_scope) so install semantics can never
31
+ # drift from projection semantics. This variable is the fail-safe fallback
32
+ # when no node/tsx runtime is available (over-ship, never under-ship).
33
+ EXCLUDE_RULES="source-of-truth.md"
28
34
 
29
35
  # Files inside target/.augment/ that are NOT managed by sync (created by the bridge installer).
30
36
  # Never remove them in clean_stale even though they are absent in the source manifest.
@@ -284,6 +290,47 @@ create_symlink() {
284
290
 
285
291
  # --- Core functions ---
286
292
 
293
+ # Resolve the full excluded-rule list for this install via the shared
294
+ # scoping CLI (workspace/pack scope from <TARGET>/.agent-settings.yml,
295
+ # router/frontmatter truth, kernel-safe). Overwrites EXCLUDE_RULES on
296
+ # success; keeps the static compat fallback on any failure
297
+ # (road-to-request-scoped-rule-load Phase 1b).
298
+ resolve_excluded_rules() {
299
+ local rules_src="$SOURCE_PAYLOAD/rules"
300
+ local settings_file="$TARGET_DIR/.agent-settings.yml"
301
+ local cli="$SOURCE_DIR/src/install/rule_scope_cli.ts"
302
+ [[ -f "$cli" && -d "$rules_src" ]] || { log_verbose "rule-scope CLI unavailable — static exclude fallback"; return 0; }
303
+
304
+ local tsx_bin=""
305
+ if [[ -x "$SOURCE_DIR/node_modules/.bin/tsx" ]]; then
306
+ tsx_bin="$SOURCE_DIR/node_modules/.bin/tsx"
307
+ elif command -v npx >/dev/null 2>&1; then
308
+ tsx_bin="npx --yes tsx"
309
+ else
310
+ log_verbose "no tsx/npx runtime — static exclude fallback"
311
+ return 0
312
+ fi
313
+
314
+ local resolved
315
+ local -a cli_args=(--rules-dir "$rules_src")
316
+ [[ -f "$settings_file" ]] && cli_args+=(--settings "$settings_file")
317
+ # TMPDIR pinned for the child: tsx writes its transform cache under
318
+ # $TMPDIR — on macOS TMPDIR is exported, and callers (tests, wrappers)
319
+ # may point it INTO the install target, leaking tsx-cache files into a
320
+ # consumer tree / breaking dry-run's zero-file guarantee.
321
+ # shellcheck disable=SC2086
322
+ if resolved=$(TMPDIR=/tmp $tsx_bin "$cli" "${cli_args[@]}" 2>/dev/null); then
323
+ # Newline list → space list (is_excluded_rule iterates words).
324
+ EXCLUDE_RULES="$(echo "$resolved" | tr '\n' ' ')"
325
+ local n
326
+ n="$(echo "$resolved" | grep -c . || true)"
327
+ log_verbose "rule scope resolved: $n excluded rule(s)"
328
+ else
329
+ log_verbose "rule-scope CLI failed — static exclude fallback"
330
+ fi
331
+ return 0
332
+ }
333
+
287
334
  # Check if a relative path matches an excluded rule
288
335
  is_excluded_rule() {
289
336
  local rel_path="$1"
@@ -470,6 +517,60 @@ clean_stale() {
470
517
  }
471
518
 
472
519
 
520
+ # Emit host-NATIVE rule surfaces for consumer Cursor (.mdc with globs) and
521
+ # Windsurf (.windsurf/rules + .windsurfrules) via the exported condense
522
+ # emitters — so consumer installs carry the same trigger signal as the
523
+ # maintainer projection (road-to-request-scoped-rule-load Phase 1b Step 3;
524
+ # the recorded install-time-vs-dist decision lives in
525
+ # src/install/emit_host_rules_cli.ts). Sets CURSOR_NATIVE_EMITTED /
526
+ # WINDSURF_NATIVE_EMITTED so the legacy surfaces (raw-md symlink farm,
527
+ # bash frontmatter-stripping concatenator) run only as no-node fallbacks.
528
+ CURSOR_NATIVE_EMITTED=false
529
+ WINDSURF_NATIVE_EMITTED=false
530
+ emit_host_rules() {
531
+ local project_root="$1"
532
+ local rules_dir="$project_root/.augment/rules"
533
+ local cli="$SOURCE_DIR/src/install/emit_host_rules_cli.ts"
534
+ [[ -d "$rules_dir" && -f "$cli" ]] || return 0
535
+
536
+ local -a tools=()
537
+ is_tool_enabled "cursor" && tools+=("cursor")
538
+ is_tool_enabled "windsurf" && tools+=("windsurf")
539
+ [[ ${#tools[@]} -gt 0 ]] || return 0
540
+
541
+ if $DRY_RUN; then
542
+ log_verbose "emit host-native rules (${tools[*]})"
543
+ return 0
544
+ fi
545
+
546
+ local tsx_bin=""
547
+ if [[ -x "$SOURCE_DIR/node_modules/.bin/tsx" ]]; then
548
+ tsx_bin="$SOURCE_DIR/node_modules/.bin/tsx"
549
+ elif command -v npx >/dev/null 2>&1; then
550
+ tsx_bin="npx --yes tsx"
551
+ else
552
+ log_verbose "no tsx/npx runtime — legacy host-rule surfaces"
553
+ return 0
554
+ fi
555
+
556
+ local tools_csv
557
+ tools_csv="$(IFS=,; echo "${tools[*]}")"
558
+ # TMPDIR pinned: tsx cache must never land in the install target.
559
+ # shellcheck disable=SC2086
560
+ if TMPDIR=/tmp $tsx_bin "$cli" --rules-dir "$rules_dir" \
561
+ --project-root "$project_root" --tools "$tools_csv" >/dev/null 2>&1; then
562
+ for tool in "${tools[@]}"; do
563
+ case "$tool" in
564
+ cursor) CURSOR_NATIVE_EMITTED=true; log_info "Emitted native .cursor/rules/*.mdc" ;;
565
+ windsurf) WINDSURF_NATIVE_EMITTED=true; log_info "Emitted native .windsurf/rules + .windsurfrules" ;;
566
+ esac
567
+ done
568
+ else
569
+ log_verbose "host-rule emission failed — legacy fallback surfaces"
570
+ fi
571
+ return 0
572
+ }
573
+
473
574
  # Create tool-specific rule symlinks (filtered by --tools selection).
474
575
  # Map: tool ID → (target dir, relative prefix from target → .augment/rules).
475
576
  create_tool_symlinks() {
@@ -482,7 +583,11 @@ create_tool_symlinks() {
482
583
  local -a tool_dirs=()
483
584
  local -a rel_prefixes=()
484
585
  is_tool_enabled "claude-code" && { tool_ids+=("claude-code"); tool_dirs+=(".claude/rules"); rel_prefixes+=("../../.augment/rules"); }
485
- is_tool_enabled "cursor" && { tool_ids+=("cursor"); tool_dirs+=(".cursor/rules"); rel_prefixes+=("../../.augment/rules"); }
586
+ # Cursor gets NATIVE .mdc files when emit_host_rules succeeded — the
587
+ # raw-md symlink farm is the no-node fallback only (Phase 1b Step 3).
588
+ if ! $CURSOR_NATIVE_EMITTED; then
589
+ is_tool_enabled "cursor" && { tool_ids+=("cursor"); tool_dirs+=(".cursor/rules"); rel_prefixes+=("../../.augment/rules"); }
590
+ fi
486
591
  is_tool_enabled "cline" && { tool_ids+=("cline"); tool_dirs+=(".clinerules"); rel_prefixes+=("../.augment/rules"); }
487
592
 
488
593
  if [[ ${#tool_dirs[@]} -eq 0 ]]; then
@@ -596,6 +701,9 @@ generate_windsurfrules() {
596
701
  local project_root="$1"
597
702
  local rules_dir="$project_root/.augment/rules"
598
703
 
704
+ # Native emission (frontmatter-aware, trigger-preserving) already wrote
705
+ # .windsurfrules — this bash concatenator is the no-node fallback only.
706
+ $WINDSURF_NATIVE_EMITTED && { log_verbose "skip bash .windsurfrules (native emitted)"; return 0; }
599
707
  [[ -d "$rules_dir" ]] || return 0
600
708
  is_tool_enabled "windsurf" || { log_verbose "skip .windsurfrules (windsurf not selected)"; return 0; }
601
709
 
@@ -1107,6 +1215,10 @@ main() {
1107
1215
  # install the file does not exist yet → defaults preserved.
1108
1216
  resolve_settings
1109
1217
 
1218
+ # 0c. Resolve the excluded-rule set for this install (workspace/pack
1219
+ # scope via the shared rule_scope CLI; static compat fallback).
1220
+ resolve_excluded_rules
1221
+
1110
1222
  # 1. Hybrid sync payload → target/.augment/
1111
1223
  sync_hybrid "$SOURCE_PAYLOAD" "$TARGET_DIR/.augment"
1112
1224
  if $USE_RULES_SYMLINKS; then
@@ -1130,7 +1242,9 @@ main() {
1130
1242
  log_verbose "skip .github/copilot-instructions.md (copilot not selected)"
1131
1243
  fi
1132
1244
 
1133
- # 3. Create tool-specific symlinks
1245
+ # 3. Emit host-native rule surfaces (cursor .mdc / windsurf), then
1246
+ # create tool-specific symlinks for the remaining tools.
1247
+ emit_host_rules "$TARGET_DIR"
1134
1248
  create_tool_symlinks "$TARGET_DIR"
1135
1249
  create_skill_symlinks "$TARGET_DIR"
1136
1250
 
@@ -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
+ }