@event4u/agent-config 9.11.0 → 9.13.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 (183) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +80 -204
  3. package/README.md +8 -1
  4. package/dist/agent-src/commands/fix/ci.md +29 -0
  5. package/dist/agent-src/commands/pr/create.md +60 -0
  6. package/dist/agent-src/contexts/authority/kernel-rule-edits.md +18 -2
  7. package/dist/agent-src/rules/active-remediation.md +0 -2
  8. package/dist/agent-src/rules/architecture.md +0 -1
  9. package/dist/agent-src/rules/artifact-drafting-protocol.md +0 -4
  10. package/dist/agent-src/rules/autonomous-execution.md +1 -2
  11. package/dist/agent-src/rules/broken-access-control.md +0 -2
  12. package/dist/agent-src/rules/code-comment-discipline.md +0 -3
  13. package/dist/agent-src/rules/code-provenance.md +0 -3
  14. package/dist/agent-src/rules/communication-through-line.md +2 -3
  15. package/dist/agent-src/rules/content-quoting-floor.md +0 -2
  16. package/dist/agent-src/rules/context-hygiene.md +1 -3
  17. package/dist/agent-src/rules/decision-revisit-gate.md +0 -2
  18. package/dist/agent-src/rules/design-fidelity.md +30 -1
  19. package/dist/agent-src/rules/domain-adoption-policy.md +0 -3
  20. package/dist/agent-src/rules/downstream-changes.md +10 -1
  21. package/dist/agent-src/rules/external-reference-deep-dive.md +0 -7
  22. package/dist/agent-src/rules/fast-path-marker-visibility.md +0 -1
  23. package/dist/agent-src/rules/git-history-discipline.md +0 -7
  24. package/dist/agent-src/rules/guidelines.md +0 -2
  25. package/dist/agent-src/rules/improve-before-implement.md +4 -2
  26. package/dist/agent-src/rules/invite-challenge.md +0 -4
  27. package/dist/agent-src/rules/linked-projects-onboarding-gate.md +0 -2
  28. package/dist/agent-src/rules/markdown-safe-codeblocks.md +0 -1
  29. package/dist/agent-src/rules/minimal-safe-diff.md +2 -2
  30. package/dist/agent-src/rules/missing-tool-handling.md +0 -1
  31. package/dist/agent-src/rules/no-attribution-footers.md +0 -3
  32. package/dist/agent-src/rules/no-decorative-emojis-in-git-surfaces.md +8 -5
  33. package/dist/agent-src/rules/no-pr-progress-comments.md +1 -3
  34. package/dist/agent-src/rules/no-roadmap-references.md +0 -2
  35. package/dist/agent-src/rules/output-discipline.md +0 -1
  36. package/dist/agent-src/rules/prefer-enums-over-literals.md +0 -2
  37. package/dist/agent-src/rules/preservation-guard.md +0 -3
  38. package/dist/agent-src/rules/role-mode-adherence.md +0 -1
  39. package/dist/agent-src/rules/secret-vcs-guard.md +0 -2
  40. package/dist/agent-src/rules/senior-engineering-discipline.md +0 -2
  41. package/dist/agent-src/rules/session-canary.md +0 -2
  42. package/dist/agent-src/rules/size-enforcement.md +0 -5
  43. package/dist/agent-src/rules/source-confidentiality.md +0 -3
  44. package/dist/agent-src/rules/source-discovery-gate.md +0 -2
  45. package/dist/agent-src/rules/think-before-action.md +3 -3
  46. package/dist/agent-src/rules/token-budget-discipline.md +0 -1
  47. package/dist/agent-src/rules/token-efficiency.md +1 -2
  48. package/dist/agent-src/rules/tool-safety.md +0 -1
  49. package/dist/agent-src/rules/user-interaction.md +0 -3
  50. package/dist/agent-src/rules/user-interrupt-priority.md +0 -1
  51. package/dist/agent-src/skills/agent-docs-writing/SKILL.md +8 -0
  52. package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +1 -0
  53. package/dist/agent-src/skills/analysis-skill-router/SKILL.md +55 -86
  54. package/dist/agent-src/skills/character-consistency/SKILL.md +7 -4
  55. package/dist/agent-src/skills/decision-record/SKILL.md +8 -0
  56. package/dist/agent-src/skills/design-intelligence/SKILL.md +27 -1
  57. package/dist/agent-src/skills/design-intelligence/data/font-pairings-reference.csv +74 -74
  58. package/dist/agent-src/skills/design-intelligence/data/manifest.json +2 -0
  59. package/dist/agent-src/skills/design-review/SKILL.md +11 -0
  60. package/dist/agent-src/skills/design-system-capture/reference/design-system-json.md +8 -0
  61. package/dist/agent-src/skills/design-tokens/scripts/tokens.ts +0 -7
  62. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +34 -3
  63. package/dist/agent-src/skills/fe-design/SKILL.md +10 -0
  64. package/dist/agent-src/skills/motion-choreographer/SKILL.md +7 -4
  65. package/dist/agent-src/skills/overbuild-review-lens/SKILL.md +201 -0
  66. package/dist/agent-src/skills/performance/SKILL.md +14 -0
  67. package/dist/agent-src/skills/pixar-storyteller/SKILL.md +8 -5
  68. package/dist/agent-src/skills/scene-expander/SKILL.md +9 -6
  69. package/dist/agent-src/skills/supply-chain-intake/SKILL.md +10 -0
  70. package/dist/agent-src/skills/typography-system/SKILL.md +55 -11
  71. package/dist/agent-src/skills/ui-apply-generic/SKILL.md +180 -0
  72. package/dist/agent-src/skills/universal-project-analysis/SKILL.md +14 -37
  73. package/dist/agent-src/skills/video-director/SKILL.md +9 -6
  74. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  75. package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +162 -17
  76. package/dist/agent-src/templates/scripts/work_engine/directives/ui/design.ts +118 -0
  77. package/dist/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +49 -6
  78. package/dist/agent-src/templates/scripts/work_engine/directives/ui/review.ts +9 -5
  79. package/dist/agent-src/templates/scripts/work_engine/directives/ui/stack_bundles.ts +182 -29
  80. package/dist/agent-src/templates/scripts/work_engine/stack/detect.ts +311 -24
  81. package/dist/agent-src/templates/scripts/work_engine/state.ts +55 -0
  82. package/dist/cli-delegate/chunk-523HCFYO.js +90 -0
  83. package/dist/cli-delegate/{chunk-IR7P7JWA.js → chunk-A3HJ6NTI.js} +271 -59
  84. package/dist/cli-delegate/chunk-EJYIGZ3T.js +125 -0
  85. package/dist/cli-delegate/{chunk-RDGRHAAB.js → chunk-HYWSRNUX.js} +6 -86
  86. package/dist/cli-delegate/{chunk-YMFO6FDN.js → chunk-QFKQLQ7X.js} +313 -81
  87. package/dist/cli-delegate/{chunk-4SV3XGFA.js → chunk-TEGJU2JX.js} +7 -9
  88. package/dist/cli-delegate/chunk-TJIAM5FW.js +133 -0
  89. package/dist/cli-delegate/cmd_conformance.js +4 -7
  90. package/dist/cli-delegate/cmd_doctor.js +3 -5
  91. package/dist/cli-delegate/cmd_explain.js +153 -85
  92. package/dist/cli-delegate/cmd_export.js +5 -3
  93. package/dist/cli-delegate/cmd_fleet.js +2 -1
  94. package/dist/cli-delegate/cmd_preflight.js +6 -123
  95. package/dist/cli-delegate/cmd_prune.js +5 -3
  96. package/dist/cli-delegate/cmd_settings_migrate.js +5 -3
  97. package/dist/cli-delegate/cmd_sync.js +5 -3
  98. package/dist/cli-delegate/cmd_uninstall.js +6 -5
  99. package/dist/cli-delegate/cmd_validate.js +5 -3
  100. package/dist/discovery/deprecation-report.md +1 -1
  101. package/dist/discovery/discovery-manifest.json +116 -72
  102. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  103. package/dist/discovery/discovery-manifest.summary.md +3 -3
  104. package/dist/discovery/orphan-report.md +1 -1
  105. package/dist/discovery/packs.json +7 -5
  106. package/dist/discovery/trust-report.md +2 -2
  107. package/dist/discovery/workspaces.json +6 -4
  108. package/dist/hooks/dispatch.js +174 -13
  109. package/dist/install/install.mjs +605 -412
  110. package/dist/install/ruleInScope.js +123 -0
  111. package/dist/install/ruleInScope.js.map +1 -0
  112. package/dist/install/rule_scope.js +2 -2
  113. package/dist/install/rule_scope.js.map +1 -1
  114. package/dist/install/wizard-plan.js +10 -1
  115. package/dist/install/wizard-plan.js.map +1 -1
  116. package/dist/mcp/registry-manifest.json +3 -3
  117. package/dist/mcp/server.json +2 -2
  118. package/dist/mcp/server.mjs +710 -2491
  119. package/dist/router.json +1 -1
  120. package/dist/scripts/condense.js +8 -24
  121. package/dist/scripts/condense.js.map +1 -1
  122. package/docs/MIGRATION.md +1 -1
  123. package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +71 -0
  124. package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +24 -1
  125. package/docs/guidelines/code-clarity.md +9 -2
  126. package/docs/guidelines/design-fidelity-mechanics.md +58 -0
  127. package/llms.txt +63 -27
  128. package/package.json +4 -4
  129. package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +162 -17
  130. package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +118 -0
  131. package/src/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +49 -6
  132. package/src/agent-src/templates/scripts/work_engine/directives/ui/review.ts +9 -5
  133. package/src/agent-src/templates/scripts/work_engine/directives/ui/stack_bundles.ts +182 -29
  134. package/src/agent-src/templates/scripts/work_engine/stack/detect.ts +311 -24
  135. package/src/agent-src/templates/scripts/work_engine/state.ts +55 -0
  136. package/src/config/agent-settings.template.yml +8 -0
  137. package/src/install/ruleInScope.ts +133 -0
  138. package/src/install/rule_scope.ts +2 -2
  139. package/src/install/wizard-plan.ts +11 -1
  140. package/src/scripts/_lib/counted_probe.ts +142 -0
  141. package/src/scripts/_lib/global_deploy_inventory.ts +38 -2
  142. package/src/scripts/_lib/overbuild_lens_contract.ts +196 -0
  143. package/src/scripts/_lib/release_scope.ts +207 -0
  144. package/src/scripts/ai_council/stance_tally.ts +37 -2
  145. package/src/scripts/audit_auto_rules.ts +9 -9
  146. package/src/scripts/audit_likelihood.ts +3 -3
  147. package/src/scripts/audit_overlap.ts +2 -2
  148. package/src/scripts/audit_skill_overlap.ts +233 -95
  149. package/src/scripts/audit_skill_overlap_allowlist.json +4 -0
  150. package/src/scripts/build_proof.ts +15 -1
  151. package/src/scripts/check_dependency_floors.ts +16 -6
  152. package/src/scripts/check_gate_coverage.ts +6 -6
  153. package/src/scripts/check_installer_import_purity.ts +295 -0
  154. package/src/scripts/check_kernel_rule_bundle.ts +5 -4
  155. package/src/scripts/check_token_quality_golden.ts +0 -18
  156. package/src/scripts/code_graph/loader.ts +35 -6
  157. package/src/scripts/compile_router.ts +3 -8
  158. package/src/scripts/condense.ts +9 -30
  159. package/src/scripts/explain_run.ts +0 -1
  160. package/src/scripts/generate_pack_manifests.ts +3 -20
  161. package/src/scripts/hooks/block_kernel_rule_writes.ts +145 -10
  162. package/src/scripts/hooks/block_no_verify.ts +101 -0
  163. package/src/scripts/install-hooks.sh +36 -4
  164. package/src/scripts/install.ts +210 -5
  165. package/src/scripts/lint_agent_security.ts +5 -4
  166. package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
  167. package/src/scripts/lint_regression.ts +92 -1
  168. package/src/scripts/lint_skill_descriptions.ts +93 -5
  169. package/src/scripts/lint_ui_stack_bundles.ts +31 -0
  170. package/src/scripts/project_thin_rules.ts +2 -2
  171. package/src/scripts/release.ts +107 -3
  172. package/src/scripts/resolve_lint_scope.ts +53 -0
  173. package/src/scripts/router_telemetry.ts +0 -5
  174. package/src/scripts/runtime_dispatcher.ts +40 -0
  175. package/src/scripts/schemas/pack.schema.json +1 -5
  176. package/src/scripts/schemas/rule.schema.json +1 -2
  177. package/src/scripts/self_review_gate.ts +15 -73
  178. package/src/scripts/skill_linter.ts +93 -11
  179. package/src/scripts/sweep_dead_scan_roots.ts +550 -0
  180. package/src/scripts/trigger_coverage.ts +11 -37
  181. package/dist/cli-delegate/chunk-7DSAMZFU.js +0 -356
  182. package/dist/cli-delegate/chunk-BJGXZNPM.js +0 -266
  183. package/dist/cli-delegate/chunk-TFG57JJM.js +0 -1969
@@ -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 };
@@ -0,0 +1,295 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Guard against a maintainer-only CLI being bundled into the consumer installer.
4
+ *
5
+ * ## The incident this exists to prevent (road-to-consistent-rule-scoping, 2026-07-31)
6
+ *
7
+ * `src/install/rule_scope.ts` imported the rule-scoping predicate from
8
+ * `src/scripts/condense.ts` — deliberately, so install semantics could not drift
9
+ * from projection semantics. Correct intent. But `condense.ts` is maintainer CLI
10
+ * tooling that ends in a module-level self-invoke:
11
+ *
12
+ * const isMain = _isCliEntry();
13
+ * if (isMain) { process.exit(main()); }
14
+ *
15
+ * `_isCliEntry()` compares `import.meta.url` against `process.argv[1]`. That is
16
+ * right for a file run directly — and wrong once esbuild BUNDLES the module,
17
+ * because `import.meta.url` then resolves to the BUNDLE's own URL, which *is*
18
+ * `argv[1]` when a consumer runs `node dist/install/install.mjs`. The guard
19
+ * fired inside the installer, `condense.main()` ran, found no maintainer source
20
+ * tree in a consumer checkout, printed "No source directory found" and called
21
+ * `process.exit(1)` — killing the install AFTER the payload had already synced.
22
+ *
23
+ * All four `smoke-public-install` legs went red. Nothing caught it earlier:
24
+ * `prepack-check` verifies that imports resolve to SHIPPED paths (they did),
25
+ * the bundle-freshness gate only byte-diffs the rebuilt output, and the
26
+ * behavioural smoke test only runs post-push in its own workflow.
27
+ *
28
+ * ## What this checks
29
+ *
30
+ * The transitive relative-import closure of the installer bundle entry must
31
+ * contain NO module-level `process.exit(` outside the entry itself.
32
+ *
33
+ * "Module-level" is decided structurally, not textually: the AST walk descends
34
+ * through top-level statements and nested blocks/ifs/try but STOPS at any
35
+ * function, method, class, arrow or accessor body. So
36
+ * `function main() { process.exit(1) }` is fine — it only runs when called —
37
+ * while `if (isMain) { process.exit(main()) }` is not, because importing the
38
+ * module runs it.
39
+ *
40
+ * The entry module is exempt: it IS the CLI, and its own exit is the point.
41
+ *
42
+ * ## Why this shape and not the alternatives
43
+ *
44
+ * - **Not a string scan of the built bundle.** That reports a mystery literal
45
+ * with no cause. This reports the import CHAIN
46
+ * (`install.ts → rule_scope.ts → condense.ts`), which is the actionable fact.
47
+ * - **Not "forbid importing condense.ts".** A denylist of one file rots the
48
+ * moment a second CLI module grows an entry guard. The hazard is the
49
+ * module-level exit, so that is what is detected.
50
+ * - **Runs on SOURCE, so it needs no build.** It fails in `task ci` before a
51
+ * push instead of on four CI legs after one.
52
+ *
53
+ * Exit 0 clean, 1 on any finding. No network, no build, no spend.
54
+ */
55
+
56
+ import * as fs from 'node:fs';
57
+ import * as path from 'node:path';
58
+ import { fileURLToPath } from 'node:url';
59
+
60
+ import ts from 'typescript';
61
+
62
+ const _HERE = path.dirname(fileURLToPath(import.meta.url));
63
+ const REPO_ROOT = path.resolve(_HERE, '..', '..');
64
+
65
+ /**
66
+ * Bundle entry points that ship to consumers as a single file.
67
+ *
68
+ * Keep in sync with the `build:*bundle` scripts in package.json. Each entry is
69
+ * exempt from the check for its own module-level exit — it is the CLI.
70
+ */
71
+ const BUNDLE_ENTRIES: readonly string[] = [
72
+ 'src/scripts/install.ts', // → dist/install/install.mjs (build:install-bundle)
73
+ ];
74
+
75
+ interface Finding {
76
+ /** The offending module, repo-relative POSIX. */
77
+ readonly module: string;
78
+ /** 1-indexed line of the offending call. */
79
+ readonly line: number;
80
+ /** How the entry reaches it, entry first. */
81
+ readonly chain: readonly string[];
82
+ }
83
+
84
+ function toPosix(p: string): string {
85
+ return p.split(path.sep).join('/');
86
+ }
87
+
88
+ function relToRepo(abs: string, root: string = REPO_ROOT): string {
89
+ return toPosix(path.relative(root, abs));
90
+ }
91
+
92
+ /**
93
+ * Resolve a relative import specifier to a source file on disk.
94
+ *
95
+ * The codebase imports with `.js` extensions (NodeNext) while the sources are
96
+ * `.ts`, so try the TypeScript twin first, then the literal path, then an
97
+ * `index` file. Returns `null` for anything unresolvable — a bare package
98
+ * specifier, a type-only path that no longer exists, or a `.json` asset.
99
+ */
100
+ function resolveImport(fromFile: string, specifier: string): string | null {
101
+ if (!specifier.startsWith('.')) return null;
102
+ const base = path.resolve(path.dirname(fromFile), specifier);
103
+ const candidates = [
104
+ base.replace(/\.js$/, '.ts'),
105
+ base.replace(/\.js$/, '.tsx'),
106
+ base,
107
+ `${base}.ts`,
108
+ path.join(base, 'index.ts'),
109
+ ];
110
+ for (const c of candidates) {
111
+ try {
112
+ if (fs.statSync(c).isFile()) return c;
113
+ } catch {
114
+ // next candidate
115
+ }
116
+ }
117
+ return null;
118
+ }
119
+
120
+ /** Relative import specifiers declared by `source`, in source order. */
121
+ function importSpecifiers(source: ts.SourceFile): string[] {
122
+ const out: string[] = [];
123
+ const visit = (node: ts.Node): void => {
124
+ if (
125
+ (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) &&
126
+ node.moduleSpecifier !== undefined &&
127
+ ts.isStringLiteral(node.moduleSpecifier)
128
+ ) {
129
+ out.push(node.moduleSpecifier.text);
130
+ } else if (
131
+ ts.isCallExpression(node) &&
132
+ node.expression.kind === ts.SyntaxKind.ImportKeyword &&
133
+ node.arguments.length > 0 &&
134
+ ts.isStringLiteral(node.arguments[0] as ts.Node)
135
+ ) {
136
+ // A dynamic import() is still bundled by esbuild unless code-split,
137
+ // so it carries the same hazard as a static one.
138
+ out.push((node.arguments[0] as ts.StringLiteral).text);
139
+ }
140
+ ts.forEachChild(node, visit);
141
+ };
142
+ ts.forEachChild(source, visit);
143
+ return out;
144
+ }
145
+
146
+ /** True for nodes whose body only runs when called — the walk stops here. */
147
+ function isDeferredScope(node: ts.Node): boolean {
148
+ return (
149
+ ts.isFunctionDeclaration(node) ||
150
+ ts.isFunctionExpression(node) ||
151
+ ts.isArrowFunction(node) ||
152
+ ts.isMethodDeclaration(node) ||
153
+ ts.isConstructorDeclaration(node) ||
154
+ ts.isGetAccessorDeclaration(node) ||
155
+ ts.isSetAccessorDeclaration(node) ||
156
+ ts.isClassDeclaration(node) ||
157
+ ts.isClassExpression(node)
158
+ );
159
+ }
160
+
161
+ /** Is `node` a `process.exit(...)` call? */
162
+ function isProcessExitCall(node: ts.Node): boolean {
163
+ if (!ts.isCallExpression(node)) return false;
164
+ const callee = node.expression;
165
+ if (!ts.isPropertyAccessExpression(callee)) return false;
166
+ return (
167
+ ts.isIdentifier(callee.expression) &&
168
+ callee.expression.text === 'process' &&
169
+ callee.name.text === 'exit'
170
+ );
171
+ }
172
+
173
+ /**
174
+ * 1-indexed lines of every `process.exit(` that executes at MODULE LOAD.
175
+ *
176
+ * Descends through statement-level structure (blocks, if/else, try/catch/finally,
177
+ * loops, labels) and stops at any deferred scope, so a call inside a function is
178
+ * not reported while one inside a top-level `if` is.
179
+ */
180
+ function moduleLevelExitLines(source: ts.SourceFile): number[] {
181
+ const lines: number[] = [];
182
+ const visit = (node: ts.Node): void => {
183
+ if (isDeferredScope(node)) return;
184
+ if (isProcessExitCall(node)) {
185
+ lines.push(source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1);
186
+ }
187
+ ts.forEachChild(node, visit);
188
+ };
189
+ ts.forEachChild(source, visit);
190
+ return lines;
191
+ }
192
+
193
+ function parse(file: string): ts.SourceFile | null {
194
+ let text: string;
195
+ try {
196
+ text = fs.readFileSync(file, 'utf-8');
197
+ } catch {
198
+ return null;
199
+ }
200
+ return ts.createSourceFile(file, text, ts.ScriptTarget.ESNext, true);
201
+ }
202
+
203
+ /**
204
+ * Walk one entry's closure; return every module-level exit found below it.
205
+ *
206
+ * `root` defaults to the repo; tests pass a fixture tree so the chain-reporting
207
+ * itself is covered, not only the per-file detector.
208
+ */
209
+ function auditEntry(entryRel: string, root: string = REPO_ROOT): Finding[] {
210
+ const entryAbs = path.join(root, entryRel);
211
+ if (!fs.existsSync(entryAbs)) {
212
+ process.stderr.write(
213
+ `check-installer-import-purity: entry ${entryRel} does not exist — ` +
214
+ 'update BUNDLE_ENTRIES\n',
215
+ );
216
+ process.exitCode = 1;
217
+ return [];
218
+ }
219
+ const findings: Finding[] = [];
220
+ const seen = new Set<string>([entryAbs]);
221
+ // BFS so the reported chain is a shortest path — the least confusing one.
222
+ const queue: Array<{ file: string; chain: string[] }> = [
223
+ { file: entryAbs, chain: [entryRel] },
224
+ ];
225
+ while (queue.length > 0) {
226
+ const { file, chain } = queue.shift() as { file: string; chain: string[] };
227
+ const source = parse(file);
228
+ if (source === null) continue;
229
+
230
+ // The entry is the CLI; its own module-level exit is intended.
231
+ if (file !== entryAbs) {
232
+ for (const line of moduleLevelExitLines(source)) {
233
+ findings.push({ module: relToRepo(file, root), line, chain });
234
+ }
235
+ }
236
+
237
+ for (const spec of importSpecifiers(source)) {
238
+ const target = resolveImport(file, spec);
239
+ if (target === null || seen.has(target)) continue;
240
+ seen.add(target);
241
+ queue.push({ file: target, chain: [...chain, relToRepo(target, root)] });
242
+ }
243
+ }
244
+ return findings;
245
+ }
246
+
247
+ function main(): number {
248
+ const all: Finding[] = [];
249
+ for (const entry of BUNDLE_ENTRIES) {
250
+ all.push(...auditEntry(entry));
251
+ }
252
+ if (all.length === 0) {
253
+ process.stdout.write(
254
+ `✅ installer import purity: ${BUNDLE_ENTRIES.length} bundle entr` +
255
+ `${BUNDLE_ENTRIES.length === 1 ? 'y' : 'ies'} reach no module-level ` +
256
+ 'process.exit()\n',
257
+ );
258
+ return 0;
259
+ }
260
+ process.stdout.write('❌ installer import purity — module-level process.exit() reachable:\n');
261
+ for (const f of all) {
262
+ process.stdout.write(`\n ${f.module}:${f.line}\n`);
263
+ process.stdout.write(` reached via: ${f.chain.join(' → ')}\n`);
264
+ }
265
+ process.stdout.write(
266
+ '\n Why this fails the build: esbuild bundles the whole closure into\n' +
267
+ ' dist/install/install.mjs. A module-level process.exit() then runs when a\n' +
268
+ ' CONSUMER loads the installer — and a CLI entry guard comparing\n' +
269
+ ' import.meta.url to process.argv[1] evaluates TRUE inside a bundle, because\n' +
270
+ ' the bundle IS argv[1].\n\n' +
271
+ ' Fix: move the code the installer actually needs into a module with no CLI\n' +
272
+ ' entry and no top-level exit, and let the CLI re-export it. Do NOT relax\n' +
273
+ ' this gate — the failure it prevents kills a consumer install after the\n' +
274
+ ' payload has already been written.\n',
275
+ );
276
+ return 1;
277
+ }
278
+
279
+ const _isCliEntry = (): boolean => {
280
+ if (process.argv[1] === undefined) return false;
281
+ try {
282
+ return (
283
+ fs.realpathSync(fileURLToPath(import.meta.url)) ===
284
+ fs.realpathSync(path.resolve(process.argv[1]))
285
+ );
286
+ } catch {
287
+ return false;
288
+ }
289
+ };
290
+
291
+ if (_isCliEntry()) {
292
+ process.exit(main());
293
+ }
294
+
295
+ export { auditEntry, moduleLevelExitLines, BUNDLE_ENTRIES };
@@ -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')) {
@@ -238,6 +238,13 @@ function _stripChars(s: string, chars: string): string {
238
238
  // the repo root.
239
239
  // =============================================================================
240
240
 
241
+ // road-to-consistent-rule-scoping: the predicate moved to a side-effect-free
242
+ // module so the consumer installer can import it WITHOUT pulling this file's
243
+ // module-level CLI self-invoke into its bundle. Re-exported here because four
244
+ // in-repo consumers import it from `condense.js`.
245
+ export { rule_in_scope } from '../install/ruleInScope.js';
246
+ import { rule_in_scope } from '../install/ruleInScope.js';
247
+
241
248
  const _HERE = path.dirname(fileURLToPath(import.meta.url)); // <repo>/src/scripts
242
249
  const _DEFAULT_PROJECT_ROOT = path.resolve(_HERE, '..', '..');
243
250
 
@@ -1014,34 +1021,6 @@ function _read_rule_packs(): string[] | null {
1014
1021
  * `null` (no role filtering), so every existing 2- and 3-arg call site
1015
1022
  * compiles and behaves unchanged.
1016
1023
  */
1017
- export function rule_in_scope(
1018
- source_path: string,
1019
- scope: readonly string[] | null,
1020
- pack_scope: readonly string[] | null = null,
1021
- role_scope: readonly string[] | null = null,
1022
- ): boolean {
1023
- if (scope === null && pack_scope === null && role_scope === null) {
1024
- return true;
1025
- }
1026
- const [meta] = _parse_frontmatter(_readText(source_path));
1027
- if (meta['type'] === 'always' || meta['alwaysApply'] === true) {
1028
- return true; // kernel always projects
1029
- }
1030
- const axis = (key: string, configured: readonly string[] | null): boolean => {
1031
- if (configured === null) {
1032
- return true;
1033
- }
1034
- const values = Array.isArray(meta[key])
1035
- ? (meta[key] as unknown[]).map((w) => String(w))
1036
- : [];
1037
- if (values.length === 0) {
1038
- return true; // untagged → fail safe: ship it
1039
- }
1040
- return values.some((v) => configured.includes(v));
1041
- };
1042
- return axis('workspaces', scope) && axis('packs', pack_scope) && axis('roles', role_scope);
1043
- }
1044
-
1045
1024
  /** List rule basenames under RULES_SOURCE, workspace/pack scope applied. */
1046
1025
  function _scoped_rule_basenames(): string[] {
1047
1026
  const scope = _read_rule_workspaces();
@@ -1197,7 +1176,7 @@ function _yaml_scalar(value: string): string {
1197
1176
  * Derive host-native activation globs from a rule's `triggers:` frontmatter
1198
1177
  * (road-to-request-scoped-rule-load Phase 2). `file_pattern` triggers map
1199
1178
  * verbatim; `path_prefix` triggers map to `<prefix>**`. Keyword / phrase /
1200
- * intent / command triggers produce no glob — those rules stay
1179
+ * command triggers produce no glob — those rules stay
1201
1180
  * description-activated (Cursor Agent-Requested / Windsurf model_decision).
1202
1181
  */
1203
1182
  export function derive_trigger_globs(meta: Record<string, unknown>): string[] {
@@ -1245,7 +1224,7 @@ export function _emit_windsurf_rule(source: string, target: string): void {
1245
1224
  const description = _strip(String(meta['description'] ?? '').replace(/\n/g, ' '));
1246
1225
  const always_apply = Boolean(meta['alwaysApply'] || meta['type'] === 'always');
1247
1226
  // Path-shaped triggers activate host-natively via Windsurf's `glob`
1248
- // trigger; keyword/intent-only rules keep `model_decision`.
1227
+ // trigger; keyword/phrase-only rules keep `model_decision`.
1249
1228
  const globs = always_apply ? [] : derive_trigger_globs(meta);
1250
1229
  const trigger = always_apply ? 'always_on' : globs.length > 0 ? 'glob' : 'model_decision';
1251
1230
  const lines = [
@@ -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);