@event4u/agent-config 9.12.0 → 9.14.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 (146) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +110 -238
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/optimize/deep.md +147 -0
  5. package/dist/agent-src/commands/optimize.md +12 -6
  6. package/dist/agent-src/rules/active-remediation.md +0 -2
  7. package/dist/agent-src/rules/architecture.md +0 -1
  8. package/dist/agent-src/rules/artifact-drafting-protocol.md +0 -4
  9. package/dist/agent-src/rules/autonomous-execution.md +1 -2
  10. package/dist/agent-src/rules/broken-access-control.md +0 -2
  11. package/dist/agent-src/rules/code-comment-discipline.md +0 -3
  12. package/dist/agent-src/rules/code-provenance.md +0 -3
  13. package/dist/agent-src/rules/communication-through-line.md +2 -3
  14. package/dist/agent-src/rules/content-quoting-floor.md +0 -2
  15. package/dist/agent-src/rules/context-hygiene.md +1 -3
  16. package/dist/agent-src/rules/decision-revisit-gate.md +0 -2
  17. package/dist/agent-src/rules/design-fidelity.md +29 -0
  18. package/dist/agent-src/rules/domain-adoption-policy.md +0 -3
  19. package/dist/agent-src/rules/downstream-changes.md +10 -1
  20. package/dist/agent-src/rules/external-reference-deep-dive.md +0 -7
  21. package/dist/agent-src/rules/fast-path-marker-visibility.md +0 -1
  22. package/dist/agent-src/rules/git-history-discipline.md +0 -7
  23. package/dist/agent-src/rules/guidelines.md +0 -2
  24. package/dist/agent-src/rules/improve-before-implement.md +4 -2
  25. package/dist/agent-src/rules/invite-challenge.md +0 -4
  26. package/dist/agent-src/rules/linked-projects-onboarding-gate.md +0 -2
  27. package/dist/agent-src/rules/markdown-safe-codeblocks.md +0 -1
  28. package/dist/agent-src/rules/minimal-safe-diff.md +2 -2
  29. package/dist/agent-src/rules/missing-tool-handling.md +0 -1
  30. package/dist/agent-src/rules/no-attribution-footers.md +0 -3
  31. package/dist/agent-src/rules/no-decorative-emojis-in-git-surfaces.md +8 -5
  32. package/dist/agent-src/rules/no-pr-progress-comments.md +1 -3
  33. package/dist/agent-src/rules/no-roadmap-references.md +0 -2
  34. package/dist/agent-src/rules/output-discipline.md +0 -1
  35. package/dist/agent-src/rules/prefer-enums-over-literals.md +0 -2
  36. package/dist/agent-src/rules/preservation-guard.md +0 -3
  37. package/dist/agent-src/rules/role-mode-adherence.md +0 -1
  38. package/dist/agent-src/rules/secret-vcs-guard.md +0 -2
  39. package/dist/agent-src/rules/senior-engineering-discipline.md +0 -2
  40. package/dist/agent-src/rules/session-canary.md +0 -2
  41. package/dist/agent-src/rules/size-enforcement.md +0 -5
  42. package/dist/agent-src/rules/source-confidentiality.md +0 -3
  43. package/dist/agent-src/rules/source-discovery-gate.md +0 -2
  44. package/dist/agent-src/rules/think-before-action.md +3 -3
  45. package/dist/agent-src/rules/token-budget-discipline.md +0 -1
  46. package/dist/agent-src/rules/token-efficiency.md +1 -2
  47. package/dist/agent-src/rules/tool-safety.md +0 -1
  48. package/dist/agent-src/rules/user-interaction.md +0 -3
  49. package/dist/agent-src/rules/user-interrupt-priority.md +0 -1
  50. package/dist/agent-src/skills/agent-docs-writing/SKILL.md +8 -0
  51. package/dist/agent-src/skills/analysis-skill-router/SKILL.md +55 -86
  52. package/dist/agent-src/skills/character-consistency/SKILL.md +7 -4
  53. package/dist/agent-src/skills/decision-record/SKILL.md +8 -0
  54. package/dist/agent-src/skills/design-review/SKILL.md +11 -0
  55. package/dist/agent-src/skills/design-system-capture/reference/design-system-json.md +8 -0
  56. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +34 -3
  57. package/dist/agent-src/skills/fe-design/SKILL.md +10 -0
  58. package/dist/agent-src/skills/motion-choreographer/SKILL.md +7 -4
  59. package/dist/agent-src/skills/overbuild-review-lens/SKILL.md +201 -0
  60. package/dist/agent-src/skills/performance/SKILL.md +14 -0
  61. package/dist/agent-src/skills/pixar-storyteller/SKILL.md +8 -5
  62. package/dist/agent-src/skills/scene-expander/SKILL.md +9 -6
  63. package/dist/agent-src/skills/supply-chain-intake/SKILL.md +10 -0
  64. package/dist/agent-src/skills/universal-project-analysis/SKILL.md +14 -37
  65. package/dist/agent-src/skills/video-director/SKILL.md +9 -6
  66. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  67. package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +154 -13
  68. package/dist/agent-src/templates/scripts/work_engine/directives/ui/design.ts +118 -0
  69. package/dist/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +40 -0
  70. package/dist/agent-src/templates/scripts/work_engine/state.ts +55 -0
  71. package/dist/cli-delegate/{chunk-QFKQLQ7X.js → chunk-GCVFLBP2.js} +399 -365
  72. package/dist/cli-delegate/cmd_export.js +1 -1
  73. package/dist/cli-delegate/cmd_prune.js +1 -1
  74. package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
  75. package/dist/cli-delegate/cmd_sync.js +1 -1
  76. package/dist/cli-delegate/cmd_uninstall.js +1 -1
  77. package/dist/cli-delegate/cmd_validate.js +1 -1
  78. package/dist/discovery/deprecation-report.md +1 -1
  79. package/dist/discovery/discovery-manifest.json +123 -73
  80. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  81. package/dist/discovery/discovery-manifest.summary.md +4 -4
  82. package/dist/discovery/orphan-report.md +1 -1
  83. package/dist/discovery/packs.json +10 -8
  84. package/dist/discovery/trust-report.md +3 -3
  85. package/dist/discovery/workspaces.json +8 -6
  86. package/dist/hooks/dispatch.js +174 -13
  87. package/dist/install/install.mjs +648 -614
  88. package/dist/mcp/registry-manifest.json +3 -3
  89. package/dist/mcp/server.json +2 -2
  90. package/dist/mcp/server.mjs +783 -507
  91. package/dist/router.json +1 -1
  92. package/dist/scripts/condense.js +2 -2
  93. package/docs/MIGRATION.md +1 -1
  94. package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +71 -0
  95. package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +24 -1
  96. package/docs/guidelines/code-clarity.md +9 -2
  97. package/docs/guidelines/design-fidelity-mechanics.md +44 -0
  98. package/llms.txt +63 -27
  99. package/package.json +4 -4
  100. package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +154 -13
  101. package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +118 -0
  102. package/src/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +40 -0
  103. package/src/agent-src/templates/scripts/work_engine/state.ts +55 -0
  104. package/src/config/agent-settings.template.yml +8 -0
  105. package/src/scripts/_lib/counted_probe.ts +142 -0
  106. package/src/scripts/_lib/exec_evidence.ts +5 -0
  107. package/src/scripts/_lib/overbuild_lens_contract.ts +196 -0
  108. package/src/scripts/_lib/release_scope.ts +207 -0
  109. package/src/scripts/_lib/scoped_projection.ts +212 -0
  110. package/src/scripts/ai_council/stance_tally.ts +37 -2
  111. package/src/scripts/audit_auto_rules.ts +9 -9
  112. package/src/scripts/audit_likelihood.ts +3 -3
  113. package/src/scripts/audit_overlap.ts +2 -2
  114. package/src/scripts/audit_skill_overlap.ts +233 -95
  115. package/src/scripts/audit_skill_overlap_allowlist.json +4 -0
  116. package/src/scripts/build_proof.ts +15 -1
  117. package/src/scripts/check_artefact_count_messaging.ts +128 -15
  118. package/src/scripts/check_dependency_floors.ts +16 -6
  119. package/src/scripts/check_gate_coverage.ts +6 -6
  120. package/src/scripts/check_kernel_rule_bundle.ts +5 -4
  121. package/src/scripts/check_token_quality_golden.ts +0 -18
  122. package/src/scripts/code_graph/loader.ts +35 -6
  123. package/src/scripts/compile_router.ts +3 -8
  124. package/src/scripts/condense.ts +2 -2
  125. package/src/scripts/count_scoped_projection.ts +104 -0
  126. package/src/scripts/explain_run.ts +0 -1
  127. package/src/scripts/generate_pack_manifests.ts +3 -20
  128. package/src/scripts/hooks/block_kernel_rule_writes.ts +145 -10
  129. package/src/scripts/hooks/block_no_verify.ts +101 -0
  130. package/src/scripts/install.ts +13 -90
  131. package/src/scripts/lint_agent_security.ts +5 -4
  132. package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
  133. package/src/scripts/lint_regression.ts +92 -1
  134. package/src/scripts/lint_skill_descriptions.ts +93 -5
  135. package/src/scripts/project_thin_rules.ts +2 -2
  136. package/src/scripts/release.ts +107 -3
  137. package/src/scripts/resolve_lint_scope.ts +53 -0
  138. package/src/scripts/router_telemetry.ts +0 -5
  139. package/src/scripts/runtime_dispatcher.ts +40 -0
  140. package/src/scripts/schemas/pack.schema.json +1 -5
  141. package/src/scripts/schemas/rule.schema.json +1 -2
  142. package/src/scripts/self_review_gate.ts +15 -73
  143. package/src/scripts/skill_linter.ts +160 -11
  144. package/src/scripts/sweep_dead_scan_roots.ts +550 -0
  145. package/src/scripts/trigger_coverage.ts +11 -37
  146. package/src/scripts/update_counts.ts +90 -11
@@ -44,11 +44,14 @@ const REPO_ROOT = path.resolve(fileURLToPath(import.meta.url), '..', '..', '..')
44
44
  * exact pin cannot suffer the freshest-patch failure (the version is either
45
45
  * cached or it is not), but it also forfeits patch fixes — so each one is
46
46
  * named here rather than silently tolerated.
47
+ *
48
+ * Empty since the code-graph parser pair (`web-tree-sitter` /
49
+ * `tree-sitter-wasms`) left `dependencies` — it was the only entry. The pair's
50
+ * ABI pin now lives in `code_graph/loader.ts`'s install hint, because that is
51
+ * the only place a re-enabler reads it; this gate scans `dependencies` and so
52
+ * no longer sees the pair at all.
47
53
  */
48
- export const EXACT_PIN_EXCEPTIONS: Readonly<Record<string, string>> = {
49
- 'web-tree-sitter': 'ABI-locked against tree-sitter-wasms; see code-graph engine pin',
50
- 'tree-sitter-wasms': 'ABI-locked against web-tree-sitter; see code-graph engine pin',
51
- };
54
+ export const EXACT_PIN_EXCEPTIONS: Readonly<Record<string, string>> = {};
52
55
 
53
56
  /**
54
57
  * Floors that are non-settled ON PURPOSE, because every version below them is
@@ -74,8 +77,15 @@ const EXACT_VERSION = /^\d+\.\d+\.\d+$/;
74
77
  /**
75
78
  * Every violation in `dependencies`, as human-readable lines. Empty array
76
79
  * means the manifest cannot produce a freshest-patch `ETARGET` for a consumer.
80
+ *
81
+ * `exactPinExceptions` defaults to the module's list and is injectable so the
82
+ * allow-an-exact-pin branch stays covered now that the real list is empty —
83
+ * otherwise the only way to test it would be to add a fake production entry.
77
84
  */
78
- export function evaluate(dependencies: Readonly<Record<string, string>>): string[] {
85
+ export function evaluate(
86
+ dependencies: Readonly<Record<string, string>>,
87
+ exactPinExceptions: Readonly<Record<string, string>> = EXACT_PIN_EXCEPTIONS,
88
+ ): string[] {
79
89
  const errors: string[] = [];
80
90
  for (const [name, range] of Object.entries(dependencies)) {
81
91
  const security = SECURITY_FLOOR_EXCEPTIONS[name];
@@ -89,7 +99,7 @@ export function evaluate(dependencies: Readonly<Record<string, string>>): string
89
99
  continue;
90
100
  }
91
101
  if (EXACT_VERSION.test(range)) {
92
- if (!(name in EXACT_PIN_EXCEPTIONS)) {
102
+ if (!(name in exactPinExceptions)) {
93
103
  errors.push(
94
104
  `${name}@${range}: exact pin without an entry in EXACT_PIN_EXCEPTIONS — ` +
95
105
  'add the reason there, or use a settled caret floor (^X.Y.0)',
@@ -31,13 +31,13 @@
31
31
  * Exit codes: 0 all enforced gates cleared their floor · 1 a gate is blind,
32
32
  * collapsed, or silent · 2 the manifest itself is missing/empty/malformed.
33
33
  */
34
- import { spawnSync } from 'node:child_process';
35
34
  import * as fs from 'node:fs';
36
35
  import * as path from 'node:path';
37
36
  import process from 'node:process';
38
37
  import { fileURLToPath, pathToFileURL } from 'node:url';
39
38
 
40
39
  import yaml from 'js-yaml';
40
+ import { runCountedProbe } from './_lib/counted_probe.js';
41
41
 
42
42
  const _HERE = fileURLToPath(import.meta.url);
43
43
  const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
@@ -184,11 +184,11 @@ export function classify(
184
184
 
185
185
  function run_gate(spec: GateSpec): { scanned: number | null; crashed: boolean; exit_code: number | null } {
186
186
  const runner = path.join(REPO_ROOT, 'scripts-run');
187
- const r = spawnSync(runner, [`src/scripts/${spec.id}`, ...spec.argv], {
188
- cwd: REPO_ROOT,
189
- encoding: 'utf8',
190
- });
191
- if (r.error !== undefined || r.status === null) return { scanned: null, crashed: true, exit_code: null };
187
+ // Bounded read: a gate whose output overflowed would lose its `scanned:`
188
+ // line and be classified `silent` — the scan-scope guard reporting a dead
189
+ // scope that is merely truncated. `runCountedProbe` throws instead.
190
+ const r = runCountedProbe(runner, [`src/scripts/${spec.id}`, ...spec.argv], { cwd: REPO_ROOT });
191
+ if (r.failure !== null && r.status === null) return { scanned: null, crashed: true, exit_code: null };
192
192
  // A gate may legitimately exit non-zero (it found real violations) and still
193
193
  // have scanned plenty — coverage and verdict are different questions.
194
194
  return { scanned: parse_scanned(`${r.stdout}\n${r.stderr}`), crashed: false, exit_code: r.status };
@@ -36,6 +36,7 @@ import * as fs from 'node:fs';
36
36
  import * as path from 'node:path';
37
37
  import { fileURLToPath, pathToFileURL } from 'node:url';
38
38
  import { KERNEL_RULE_FILENAMES } from './_lib/kernel_rules.js';
39
+ import { runCountedProbe } from './_lib/counted_probe.js';
39
40
 
40
41
  const KERNEL_RULES: ReadonlySet<string> = KERNEL_RULE_FILENAMES;
41
42
 
@@ -56,13 +57,13 @@ const KERNEL_DIR = 'src/rules';
56
57
  const DEFAULT_LABEL = 'bundled-always-rules-acknowledged';
57
58
 
58
59
  function _git_changed_files(base_ref: string): string[] {
59
- const res = spawnSync('git', ['diff', '--name-only', `${base_ref}...HEAD`], {
60
- encoding: 'utf8',
61
- });
60
+ // Bounded read: a truncated diff would shrink the change set and let the
61
+ // bundle guard pass on files it never saw.
62
+ const res = runCountedProbe('git', ['diff', '--name-only', `${base_ref}...HEAD`]);
62
63
  // Python uses check_output(stderr=STDOUT, text=True): on non-zero it raises
63
64
  // CalledProcessError and the handler prints the combined output. spawnSync
64
65
  // does not raise; mirror by checking status and combining stdout+stderr.
65
- if (res.status !== 0) {
66
+ if (!res.ok) {
66
67
  const combined = `${res.stdout ?? ''}${res.stderr ?? ''}`;
67
68
  process.stderr.write(`❌ git diff failed: ${combined.trim()}\n`);
68
69
  return [];
@@ -113,20 +113,6 @@ export function router_triggers(router: Json): Map<string, Array<Record<string,
113
113
  return map;
114
114
  }
115
115
 
116
- // trigger_coverage.ts word semantics: lowercase alpha-led tokens, len > 2.
117
- const _WORD = /[a-z][a-z0-9_]+/g;
118
-
119
- function _tokens(text: string): Set<string> {
120
- const out = new Set<string>();
121
- const low = text.toLowerCase();
122
- _WORD.lastIndex = 0;
123
- let m: RegExpExecArray | null;
124
- while ((m = _WORD.exec(low)) !== null) {
125
- if (m[0].length > 2) out.add(m[0]);
126
- }
127
- return out;
128
- }
129
-
130
116
  /**
131
117
  * Whether a task exercises ≥1 trigger of the rule (prompt↔trigger
132
118
  * falsifiability, Phase 3). Kernel rules always fire.
@@ -140,16 +126,12 @@ export function task_fires_rule(
140
126
  if (kernel.has(ruleId)) return true;
141
127
  const triggers = router_triggers(router).get(ruleId) ?? [];
142
128
  const low = task.prompt.toLowerCase();
143
- const toks = _tokens(task.prompt);
144
129
  const files = Array.isArray(task.context_files) ? task.context_files : [];
145
130
  const cmd = typeof task.command === 'string' ? task.command : '';
146
131
  for (const trig of triggers) {
147
132
  if ('keyword' in trig || 'phrase' in trig) {
148
133
  const needle = String(trig.keyword ?? trig.phrase).toLowerCase();
149
134
  if (needle && low.includes(needle)) return true;
150
- } else if ('intent' in trig) {
151
- const words = _tokens(String(trig.intent));
152
- if (words.size > 0 && [...words].every((w) => toks.has(w))) return true;
153
135
  } else if ('path_prefix' in trig) {
154
136
  const prefix = String(trig.path_prefix);
155
137
  if (files.some((f) => f.startsWith(prefix))) return true;
@@ -2,10 +2,11 @@
2
2
  * WASM tree-sitter loader for the code-graph engine.
3
3
  *
4
4
  * Pinned pair (ABI-verified 2026-07-23): `web-tree-sitter@0.24.7` +
5
- * `tree-sitter-wasms@0.1.13` → grammar ABI 14. The ABI smoke test
6
- * (`tests/scripts/code_graph_abi.test.ts`) asserts this pairing loads and
7
- * parses before merge; a bump to either dependency re-runs it. No network,
8
- * no node-gyp — grammars load from `node_modules` via the resolved path.
5
+ * `tree-sitter-wasms@0.1.13` → grammar ABI 14. The ABI smoke test lives in
6
+ * `tests/scripts/code_graph.test.ts`; it runs in this repo because the pair is
7
+ * a devDependency. Consumers install neither (see `INSTALL_HINT` below). No
8
+ * network, no node-gyp — grammars load from `node_modules` via the resolved
9
+ * path.
9
10
  *
10
11
  * web-tree-sitter 0.24.x is CJS with a single default export (the Parser
11
12
  * class) and the classic `Parser.Language.load()` API; we reach it through
@@ -51,10 +52,32 @@ interface ParserStatic {
51
52
  Language: { load(wasmPath: string): Promise<TsLanguage> };
52
53
  }
53
54
 
55
+ /**
56
+ * The parser pair is a devDependency, not a runtime one: this repo's tests still
57
+ * exercise the loader, while a consumer install resolves neither. The engine is
58
+ * permanently `enabled: false` (measured recall 0.365 vs grep 0.797), so
59
+ * shipping ~51 MB of WASM to every consumer for a path none of them can reach
60
+ * was pure install cost. A consumer re-enabling the engine installs the
61
+ * ABI-locked pair themselves — this message carries the pin, because
62
+ * `check_dependency_floors` only scans the `dependencies` block.
63
+ */
64
+ const INSTALL_HINT =
65
+ 'the code-graph engine needs its parser pair, which this package no longer ships to consumers ' +
66
+ '(the engine is disabled by a recorded null result). Install the ABI-locked pair:\n' +
67
+ ' npm i web-tree-sitter@0.24.7 tree-sitter-wasms@0.1.13\n' +
68
+ 'Both versions are exact on purpose — the pair is ABI-coupled (grammar ABI 14) ' +
69
+ 'and must move together.';
70
+
54
71
  let _wasmDir: string | null = null;
55
72
  function wasmDir(): string {
56
73
  if (_wasmDir === null) {
57
- _wasmDir = path.join(path.dirname(require.resolve('tree-sitter-wasms/package.json')), 'out');
74
+ let pkgJson: string;
75
+ try {
76
+ pkgJson = require.resolve('tree-sitter-wasms/package.json');
77
+ } catch {
78
+ throw new Error(`tree-sitter-wasms is not installed — ${INSTALL_HINT}`);
79
+ }
80
+ _wasmDir = path.join(path.dirname(pkgJson), 'out');
58
81
  }
59
82
  return _wasmDir;
60
83
  }
@@ -72,7 +95,13 @@ function _Parser(): ParserStatic {
72
95
  // Require exactly once and cache the class reference — repeated
73
96
  // createRequire calls under tsx can hand back a module view whose static
74
97
  // `Language` is not yet populated, which breaks the second grammar load.
75
- if (_mod === null) _mod = require('web-tree-sitter') as ParserStatic;
98
+ if (_mod === null) {
99
+ try {
100
+ _mod = require('web-tree-sitter') as ParserStatic;
101
+ } catch {
102
+ throw new Error(`web-tree-sitter is not installed — ${INSTALL_HINT}`);
103
+ }
104
+ }
76
105
  return _mod;
77
106
  }
78
107
 
@@ -73,14 +73,9 @@ const LEGACY_TIER_MAP: Record<string, string> = {
73
73
  };
74
74
 
75
75
  const ALLOWED_TIERS = new Set(['kernel', 'tier-1', 'tier-2']);
76
- const ALLOWED_TRIGGER_KEYS = new Set([
77
- 'keyword',
78
- 'phrase',
79
- 'intent',
80
- 'file_pattern',
81
- 'path_prefix',
82
- 'command',
83
- ]);
76
+ // `intent` was removed: it never auto-matched at runtime, so declaring one gave
77
+ // the author a false activation path. Compiling one is now a hard error.
78
+ const ALLOWED_TRIGGER_KEYS = new Set(['keyword', 'phrase', 'file_pattern', 'path_prefix', 'command']);
84
79
 
85
80
  function _parse_frontmatter(text: string): JsonObject {
86
81
  if (!text.startsWith('---\n')) {
@@ -1176,7 +1176,7 @@ function _yaml_scalar(value: string): string {
1176
1176
  * Derive host-native activation globs from a rule's `triggers:` frontmatter
1177
1177
  * (road-to-request-scoped-rule-load Phase 2). `file_pattern` triggers map
1178
1178
  * verbatim; `path_prefix` triggers map to `<prefix>**`. Keyword / phrase /
1179
- * intent / command triggers produce no glob — those rules stay
1179
+ * command triggers produce no glob — those rules stay
1180
1180
  * description-activated (Cursor Agent-Requested / Windsurf model_decision).
1181
1181
  */
1182
1182
  export function derive_trigger_globs(meta: Record<string, unknown>): string[] {
@@ -1224,7 +1224,7 @@ export function _emit_windsurf_rule(source: string, target: string): void {
1224
1224
  const description = _strip(String(meta['description'] ?? '').replace(/\n/g, ' '));
1225
1225
  const always_apply = Boolean(meta['alwaysApply'] || meta['type'] === 'always');
1226
1226
  // Path-shaped triggers activate host-natively via Windsurf's `glob`
1227
- // trigger; keyword/intent-only rules keep `model_decision`.
1227
+ // trigger; keyword/phrase-only rules keep `model_decision`.
1228
1228
  const globs = always_apply ? [] : derive_trigger_globs(meta);
1229
1229
  const trigger = always_apply ? 'always_on' : globs.length > 0 ? 'glob' : 'model_decision';
1230
1230
  const lines = [
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Emit the scoped-projection skill count — the number `docs/CLAIMS.md`
4
+ * publishes for the default install
5
+ * (road-to-reproducible-artefact-counts).
6
+ *
7
+ * The claim used to read "217 of 288 skills … with the counting method
8
+ * pinned in the benchmark doc", while that benchmark doc said 215 of 286.
9
+ * Both numbers were hand-typed, only one of them tracked the catalog, and
10
+ * the gap grew by one on every skill added because no gate could compare
11
+ * them. This script is the single derivation both now come from.
12
+ *
13
+ * It adds no counting mechanism of its own — that would be a second path
14
+ * free to disagree with the first, which is the defect, not the fix:
15
+ *
16
+ * - the file set is `update_counts.iter_skills()`, the exact walk behind
17
+ * the canonical `count('skills')`, so `projected + pruned === total`
18
+ * holds by construction;
19
+ * - the in/out predicate is `_lib/scoped_projection.is_pruned_under_scoped`,
20
+ * the exact function `install.ts` applies when it prunes a real user's
21
+ * tree, so the published number cannot describe a projection the
22
+ * installer does not perform.
23
+ *
24
+ * `runtime.active_packs` is deliberately empty here: the claim is about the
25
+ * DEFAULT for a new install, and a fresh machine has no overlay. An operator
26
+ * who activates extra packs gets more skills than the claim states — the
27
+ * claim is a floor, and `--json` exposes `active_packs` so a reader can see
28
+ * the assumption rather than infer it.
29
+ *
30
+ * Usage:
31
+ * ./scripts-run src/scripts/count_scoped_projection # "217 of 288"
32
+ * ./scripts-run src/scripts/count_scoped_projection --json
33
+ * ./scripts-run src/scripts/count_scoped_projection --projected
34
+ *
35
+ * Exit codes: 0 ok · 1 the pack registry could not be read.
36
+ */
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 {
43
+ scoped_projection_stats,
44
+ type ScopedProjectionStats,
45
+ } from './_lib/scoped_projection.js';
46
+ import { iter_skills, REPO_ROOT } from './update_counts.js';
47
+
48
+ const _HERE = fileURLToPath(import.meta.url);
49
+
50
+ export type { ScopedProjectionStats };
51
+
52
+ /** Partition the canonical skill catalog with the installer's own predicate. */
53
+ export function stats(package_root: string = REPO_ROOT): ScopedProjectionStats {
54
+ return scoped_projection_stats(package_root, iter_skills());
55
+ }
56
+
57
+ export function main(argv: readonly string[] = process.argv.slice(2)): number {
58
+ let s: ScopedProjectionStats;
59
+ try {
60
+ s = stats();
61
+ } catch (err) {
62
+ process.stderr.write(
63
+ `❌ scoped-projection count unavailable: ${(err as Error).message}\n` +
64
+ ' (src/config/discovery/packs.yml must exist and parse to a list)\n',
65
+ );
66
+ return 1;
67
+ }
68
+
69
+ if (argv.includes('--json')) {
70
+ process.stdout.write(`${JSON.stringify(s, null, 2)}\n`);
71
+ return 0;
72
+ }
73
+ if (argv.includes('--projected')) {
74
+ process.stdout.write(`${s.projected}\n`);
75
+ return 0;
76
+ }
77
+ if (argv.includes('--total')) {
78
+ process.stdout.write(`${s.total}\n`);
79
+ return 0;
80
+ }
81
+ process.stdout.write(`${s.projected} of ${s.total}\n`);
82
+ return 0;
83
+ }
84
+
85
+ function _isCliEntry(): boolean {
86
+ if (process.argv[1] === undefined) {
87
+ return false;
88
+ }
89
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
90
+ if (import.meta.url === argvUrl) {
91
+ return true;
92
+ }
93
+ try {
94
+ const here = fs.realpathSync(fileURLToPath(import.meta.url));
95
+ const argv = fs.realpathSync(path.resolve(process.argv[1]));
96
+ return here === argv;
97
+ } catch {
98
+ return false;
99
+ }
100
+ }
101
+
102
+ if (_isCliEntry() || process.argv[1] === _HERE) {
103
+ process.exit(main(process.argv.slice(2)));
104
+ }
@@ -130,7 +130,6 @@ function parseSince(since: string | null): number | null {
130
130
  export interface TriggerSpec {
131
131
  keyword?: string;
132
132
  phrase?: string;
133
- intent?: string;
134
133
  file_pattern?: string;
135
134
  path_prefix?: string;
136
135
  command?: string;
@@ -17,7 +17,7 @@
17
17
  * regenerated on every `task sync` and must never be hand-edited.
18
18
  *
19
19
  * For each pack home we emit:
20
- * - `<home>/pack.yaml` — id, label, owner, requires, version, artefact_count, …
20
+ * - `<home>/pack.yaml` — id, label, owner, requires, artefact_count, …
21
21
  * - `<home>/README.md` — pack description + per-artefact description table
22
22
  *
23
23
  * CLI:
@@ -40,7 +40,6 @@ const PACKAGES = path.join(ROOT, 'packages');
40
40
  const SRC_DOMAINS = path.join(ROOT, 'src', 'domains');
41
41
  const SRC_PACKS = path.join(ROOT, 'src', 'packs');
42
42
  const PACKS_VOCAB = path.join(ROOT, 'src', 'config', 'discovery', 'packs.yml');
43
- const PACKAGE_JSON = path.join(ROOT, 'package.json');
44
43
  const GENERATED_HEADER =
45
44
  '# Generated by scripts/generate_pack_manifests.ts — DO NOT EDIT BY HAND\n';
46
45
 
@@ -138,14 +137,6 @@ function _vocab_lookup(packsVocab: Dict[]): Record<string, Dict> {
138
137
  return out;
139
138
  }
140
139
 
141
- function _package_version(): string {
142
- if (!_exists(PACKAGE_JSON)) {
143
- return '0.0.0';
144
- }
145
- const pkg = JSON.parse(fs.readFileSync(PACKAGE_JSON, 'utf-8')) as Dict;
146
- return _pyTruthy(pkg.version) ? String(pkg.version) : '0.0.0';
147
- }
148
-
149
140
  function _artefact_record(p: string, logicalRel: string, fm: Dict): Artefact {
150
141
  const name = _pyTruthy(fm.name)
151
142
  ? String(fm.name)
@@ -256,12 +247,7 @@ function _category_from_logical(logicalRel: string): string {
256
247
  * Build the pack.yaml meta dict. Mirrors `_build_pack_yaml`. Insertion
257
248
  * order here is irrelevant — `_py_safe_dump` sorts keys (sort_keys=True).
258
249
  */
259
- function _build_pack_yaml(
260
- pid: string,
261
- vocab: Record<string, Dict>,
262
- artefacts: Artefact[],
263
- version: string,
264
- ): Dict {
250
+ function _build_pack_yaml(pid: string, vocab: Record<string, Dict>, artefacts: Artefact[]): Dict {
265
251
  const meta = vocab[pid] ?? {};
266
252
  const label = _pyTruthy(meta.label)
267
253
  ? String(meta.label)
@@ -289,7 +275,6 @@ function _build_pack_yaml(
289
275
  trust_level_default: _pyTruthy(meta.trust_level_default)
290
276
  ? (meta.trust_level_default as unknown)
291
277
  : 'core',
292
- version,
293
278
  artefact_count: artefacts.length,
294
279
  };
295
280
  if (_isPlainObject(meta.onboarding)) {
@@ -330,7 +315,6 @@ function _render_readme(packMeta: Dict, artefacts: Artefact[]): string {
330
315
  const requires = (packMeta.requires as unknown[]) ?? [];
331
316
  lines.push(
332
317
  `- **id**: \`${String(packMeta.id)}\``,
333
- `- **version**: \`${String(packMeta.version)}\``,
334
318
  `- **owner**: ${owner.map(String).join(', ') || '—'}`,
335
319
  `- **requires**: ${requires.map(String).join(', ') || '—'}`,
336
320
  `- **artefacts**: ${String(packMeta.artefact_count)}`,
@@ -369,7 +353,6 @@ function _write_if_changed(p: string, content: string, check: boolean): boolean
369
353
  export function main(argv: string[] = process.argv.slice(2)): number {
370
354
  const check = argv.includes('--check');
371
355
  const vocab = _vocab_lookup((_load_yaml(PACKS_VOCAB) as Dict[]) ?? []);
372
- const version = _package_version();
373
356
  const homes = _pack_homes();
374
357
  if (homes.length === 0) {
375
358
  process.stderr.write(
@@ -380,7 +363,7 @@ export function main(argv: string[] = process.argv.slice(2)): number {
380
363
  let drift = 0;
381
364
  for (const [pid, homeDir, mode] of homes) {
382
365
  const artefacts = _collect_artefacts(pid, homeDir, mode);
383
- const meta = _build_pack_yaml(pid, vocab, artefacts, version);
366
+ const meta = _build_pack_yaml(pid, vocab, artefacts);
384
367
  const yamlBody = GENERATED_HEADER + _py_safe_dump(meta);
385
368
  const readmeBody = _render_readme(meta, artefacts);
386
369
  const label = _relativeToPosix(homeDir, ROOT);
@@ -40,6 +40,7 @@ import * as path from 'node:path';
40
40
 
41
41
  import { is_kernel_rule } from '../_lib/kernel_rules.js';
42
42
  import { EDIT_TOOLS } from '../minimal_safe_diff_hook.js';
43
+ import { COMMAND_TOOLS } from '../verify_before_complete_hook.js';
43
44
  import { readHookStdin } from './hook_stdin.js';
44
45
 
45
46
  const _HERE = fileURLToPath(import.meta.url);
@@ -122,25 +123,159 @@ export function targets_kernel_rule(filePath: string): string | null {
122
123
  return basename.replace(/\.md$/, '');
123
124
  }
124
125
 
125
- /** Return (blocked, reason) for one PreToolUse envelope. Pure — no I/O beyond the arg. */
126
+ /** In-place mutators that name their target as a plain argument. */
127
+ const _MUTATOR_VERBS: ReadonlySet<string> = new Set([
128
+ 'sed',
129
+ 'tee',
130
+ 'truncate',
131
+ 'rm',
132
+ 'shred',
133
+ ]);
134
+ /** Verbs whose LAST positional argument is the destination. */
135
+ const _DEST_LAST_VERBS: ReadonlySet<string> = new Set(['mv', 'cp', 'install', 'rsync']);
136
+
137
+ /**
138
+ * Shell tokens that, in a command naming a kernel rule path, mean the path is
139
+ * being WRITTEN rather than read. Reads (`cat`, `grep`, `head`, `diff`,
140
+ * `git show`) carry none of these and stay allowed — a kernel rule is
141
+ * immutable, not secret.
142
+ */
143
+ function _bash_targets_kernel_rule(command: string): string | null {
144
+ // Cheap reject first: no kernel-rule path in the string at all.
145
+ const tokens = command.split(/\s+/).filter((t) => t.length > 0);
146
+ const stripped = tokens.map((t) => t.replace(/^['"]|['"]$/g, ''));
147
+ const ruleOf = (t: string): string | null => targets_kernel_rule(t);
148
+ if (!stripped.some((t) => ruleOf(t) !== null)) {
149
+ return null;
150
+ }
151
+
152
+ // 1. Redirection into the path: `> p`, `>> p`, `>p`, `>>p`.
153
+ for (let i = 0; i < stripped.length; i += 1) {
154
+ const tok = stripped[i] as string;
155
+ const inline = /^>{1,2}(.+)$/.exec(tok);
156
+ if (inline) {
157
+ const r = ruleOf(inline[1] as string);
158
+ if (r !== null) {
159
+ return r;
160
+ }
161
+ }
162
+ if (tok === '>' || tok === '>>') {
163
+ const next = stripped[i + 1];
164
+ if (typeof next === 'string') {
165
+ const r = ruleOf(next);
166
+ if (r !== null) {
167
+ return r;
168
+ }
169
+ }
170
+ }
171
+ }
172
+
173
+ // 2. A mutator verb anywhere in the pipeline, with the path as an argument.
174
+ // `sed` only counts in its in-place form — `sed 's/x/y/' file` prints.
175
+ const verbs = new Set<string>();
176
+ let prevWasSeparator = true;
177
+ for (const tok of stripped) {
178
+ if (['&&', '||', ';', '|'].includes(tok)) {
179
+ prevWasSeparator = true;
180
+ continue;
181
+ }
182
+ if (prevWasSeparator && !tok.startsWith('-')) {
183
+ verbs.add(path.basename(tok));
184
+ prevWasSeparator = false;
185
+ }
186
+ }
187
+ const sedInPlace = verbs.has('sed') && stripped.some((t) => /^-i/.test(t));
188
+ for (const verb of verbs) {
189
+ if (verb === 'sed' && !sedInPlace) {
190
+ continue;
191
+ }
192
+ if (_MUTATOR_VERBS.has(verb)) {
193
+ const hit = stripped.map(ruleOf).find((r) => r !== null);
194
+ if (hit !== undefined && hit !== null) {
195
+ return hit;
196
+ }
197
+ }
198
+ if (_DEST_LAST_VERBS.has(verb)) {
199
+ const last = stripped[stripped.length - 1] as string;
200
+ const r = ruleOf(last);
201
+ if (r !== null) {
202
+ return r;
203
+ }
204
+ }
205
+ }
206
+ return null;
207
+ }
208
+
209
+ /**
210
+ * Return (blocked, reason) for one PreToolUse envelope. Pure — no I/O beyond
211
+ * the arg.
212
+ *
213
+ * Two surfaces, one effect. The Write/Edit branch is the original Layer-1
214
+ * guard. The Bash branch was added by road-to-governance-invariants Phase 1
215
+ * after the S0.2 spike measured the gap: this gate keyed on the TOOL NAME, so
216
+ * `Bash sed -i … src/rules/commit-policy.md` reached the immutable-rule
217
+ * outcome the Write branch refuses, and a two-step
218
+ * `Write docs/staging/<kernel>.md` → `Bash mv … src/rules/<kernel>.md`
219
+ * sequence did the same with every step individually allowed.
220
+ *
221
+ * The Bash branch is deliberately narrow (2026-08-02 council cut, option ii):
222
+ * only redirection into the path, an in-place `sed`, a `tee`/`truncate`/`rm`
223
+ * naming it, or a `mv`/`cp` whose DESTINATION it is. Reads stay allowed — a
224
+ * kernel rule is immutable, not secret — and no attempt is made to understand
225
+ * arbitrary shell. Recognising every conceivable write verb would make this a
226
+ * shell sandbox, which is the failure mode the council named.
227
+ */
126
228
  export function check_envelope(envelope: JsonObject): [boolean, string] {
127
229
  const { tool, paths } = _extract(envelope);
128
- if (!tool || !EDIT_TOOLS.has(tool)) {
230
+ if (!tool) {
129
231
  return [false, ''];
130
232
  }
131
- for (const p of paths) {
132
- const ruleName = targets_kernel_rule(p);
133
- if (ruleName !== null) {
134
- return [
135
- true,
136
- `kernel rule ${ruleName} is immutable — tighten-only via the override ` +
137
- 'exception registry; see docs/contracts/kernel-membership.md',
138
- ];
233
+ const deny = (ruleName: string): [boolean, string] => [
234
+ true,
235
+ `kernel rule ${ruleName} is immutable — tighten-only via the override ` +
236
+ 'exception registry; see docs/contracts/kernel-membership.md',
237
+ ];
238
+ if (EDIT_TOOLS.has(tool)) {
239
+ for (const p of paths) {
240
+ const ruleName = targets_kernel_rule(p);
241
+ if (ruleName !== null) {
242
+ return deny(ruleName);
243
+ }
244
+ }
245
+ return [false, ''];
246
+ }
247
+ if (COMMAND_TOOLS.has(tool)) {
248
+ const cmd = _extract_command(envelope);
249
+ if (cmd !== null) {
250
+ const ruleName = _bash_targets_kernel_rule(cmd);
251
+ if (ruleName !== null) {
252
+ return deny(ruleName);
253
+ }
139
254
  }
140
255
  }
141
256
  return [false, ''];
142
257
  }
143
258
 
259
+ /** Best-effort read of a shell command off a PreToolUse envelope. */
260
+ function _extract_command(envelope: JsonObject): string | null {
261
+ const payload = _isObject(envelope['payload']) ? envelope['payload'] : envelope;
262
+ const ti = _isObject(payload['tool_input'])
263
+ ? payload['tool_input']
264
+ : _isObject(envelope['tool_input'])
265
+ ? envelope['tool_input']
266
+ : null;
267
+ for (const src of [ti, payload]) {
268
+ if (src === null) {
269
+ continue;
270
+ }
271
+ const v = src['command'];
272
+ if (typeof v === 'string' && v) {
273
+ return v;
274
+ }
275
+ }
276
+ return null;
277
+ }
278
+
144
279
  function _asObject(v: JsonValue | undefined): JsonObject | null {
145
280
  return _isObject(v) ? v : null;
146
281
  }