@rune-kit/rune 2.10.0 → 2.12.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 (240) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +65 -6
  3. package/commands/rune.md +168 -168
  4. package/compiler/__tests__/detect-invariants.test.js +136 -0
  5. package/compiler/__tests__/doctor-mesh.test.js +229 -0
  6. package/compiler/__tests__/hook-dispatch.test.js +91 -0
  7. package/compiler/__tests__/hooks-antigravity.test.js +118 -0
  8. package/compiler/__tests__/hooks-cursor.test.js +139 -0
  9. package/compiler/__tests__/hooks-install.test.js +305 -0
  10. package/compiler/__tests__/hooks-merge.test.js +204 -0
  11. package/compiler/__tests__/hooks-tiers.test.js +519 -0
  12. package/compiler/__tests__/hooks-windsurf.test.js +115 -0
  13. package/compiler/__tests__/inject-claude-md.test.js +152 -0
  14. package/compiler/__tests__/load-invariants.test.js +408 -0
  15. package/compiler/__tests__/onboard-invariants.test.js +240 -0
  16. package/compiler/adapters/hooks/antigravity.js +140 -0
  17. package/compiler/adapters/hooks/claude.js +166 -0
  18. package/compiler/adapters/hooks/cursor.js +191 -0
  19. package/compiler/adapters/hooks/index.js +82 -0
  20. package/compiler/adapters/hooks/tier-emitter.js +182 -0
  21. package/compiler/adapters/hooks/windsurf.js +202 -0
  22. package/compiler/bin/rune.js +196 -6
  23. package/compiler/commands/hook-dispatch.js +87 -0
  24. package/compiler/commands/hooks/install.js +120 -0
  25. package/compiler/commands/hooks/merge.js +211 -0
  26. package/compiler/commands/hooks/presets.js +116 -0
  27. package/compiler/commands/hooks/status.js +112 -0
  28. package/compiler/commands/hooks/tiers.js +221 -0
  29. package/compiler/commands/hooks/uninstall.js +94 -0
  30. package/compiler/doctor.js +236 -0
  31. package/contexts/dev.md +34 -34
  32. package/contexts/research.md +43 -43
  33. package/contexts/review.md +55 -55
  34. package/extensions/ai-ml/PACK.md +88 -88
  35. package/extensions/ai-ml/skills/ai-agents.md +172 -172
  36. package/extensions/ai-ml/skills/code-sandbox.md +187 -187
  37. package/extensions/ai-ml/skills/deep-research.md +146 -146
  38. package/extensions/ai-ml/skills/embedding-search.md +66 -66
  39. package/extensions/ai-ml/skills/fine-tuning-guide.md +74 -74
  40. package/extensions/ai-ml/skills/llm-architect.md +125 -125
  41. package/extensions/ai-ml/skills/llm-integration.md +64 -64
  42. package/extensions/ai-ml/skills/prompt-patterns.md +72 -72
  43. package/extensions/ai-ml/skills/rag-patterns.md +66 -66
  44. package/extensions/ai-ml/skills/web-extraction.md +114 -114
  45. package/extensions/analytics/PACK.md +92 -92
  46. package/extensions/analytics/skills/ab-testing.md +72 -72
  47. package/extensions/analytics/skills/dashboard-patterns.md +83 -83
  48. package/extensions/analytics/skills/data-validation.md +68 -68
  49. package/extensions/analytics/skills/funnel-analysis.md +81 -81
  50. package/extensions/analytics/skills/sql-patterns.md +57 -57
  51. package/extensions/analytics/skills/statistical-analysis.md +79 -79
  52. package/extensions/analytics/skills/tracking-setup.md +71 -71
  53. package/extensions/backend/PACK.md +104 -104
  54. package/extensions/backend/skills/api-patterns.md +84 -84
  55. package/extensions/backend/skills/async-pipeline.md +193 -193
  56. package/extensions/backend/skills/auth-patterns.md +97 -97
  57. package/extensions/backend/skills/background-jobs.md +133 -133
  58. package/extensions/backend/skills/caching-patterns.md +108 -108
  59. package/extensions/backend/skills/cli-generation.md +133 -133
  60. package/extensions/backend/skills/database-patterns.md +87 -87
  61. package/extensions/backend/skills/middleware-patterns.md +104 -104
  62. package/extensions/chrome-ext/PACK.md +93 -93
  63. package/extensions/chrome-ext/skills/cws-preflight.md +143 -143
  64. package/extensions/chrome-ext/skills/cws-publish.md +104 -104
  65. package/extensions/chrome-ext/skills/ext-ai-integration.md +251 -251
  66. package/extensions/chrome-ext/skills/ext-messaging.md +139 -139
  67. package/extensions/chrome-ext/skills/ext-storage.md +133 -133
  68. package/extensions/chrome-ext/skills/mv3-scaffold.md +164 -164
  69. package/extensions/content/PACK.md +96 -96
  70. package/extensions/content/skills/blog-patterns.md +88 -88
  71. package/extensions/content/skills/cms-integration.md +131 -131
  72. package/extensions/content/skills/content-scoring.md +107 -107
  73. package/extensions/content/skills/i18n.md +83 -83
  74. package/extensions/content/skills/mdx-authoring.md +137 -137
  75. package/extensions/content/skills/reference.md +1014 -1014
  76. package/extensions/content/skills/seo-patterns.md +67 -67
  77. package/extensions/content/skills/video-repurpose.md +153 -153
  78. package/extensions/devops/PACK.md +101 -101
  79. package/extensions/devops/skills/chaos-testing.md +67 -67
  80. package/extensions/devops/skills/ci-cd.md +75 -75
  81. package/extensions/devops/skills/docker.md +58 -58
  82. package/extensions/devops/skills/edge-serverless.md +163 -163
  83. package/extensions/devops/skills/infra-as-code.md +158 -158
  84. package/extensions/devops/skills/kubernetes.md +110 -110
  85. package/extensions/devops/skills/monitoring.md +57 -57
  86. package/extensions/devops/skills/server-setup.md +64 -64
  87. package/extensions/devops/skills/ssl-domain.md +42 -42
  88. package/extensions/ecommerce/PACK.md +116 -116
  89. package/extensions/ecommerce/skills/cart-system.md +79 -79
  90. package/extensions/ecommerce/skills/inventory-mgmt.md +102 -102
  91. package/extensions/ecommerce/skills/order-management.md +126 -126
  92. package/extensions/ecommerce/skills/payment-integration.md +472 -472
  93. package/extensions/ecommerce/skills/shopify-dev.md +69 -69
  94. package/extensions/ecommerce/skills/subscription-billing.md +93 -93
  95. package/extensions/ecommerce/skills/tax-compliance.md +117 -117
  96. package/extensions/gamedev/PACK.md +142 -142
  97. package/extensions/gamedev/skills/asset-pipeline.md +74 -74
  98. package/extensions/gamedev/skills/audio-system.md +129 -129
  99. package/extensions/gamedev/skills/camera-system.md +87 -87
  100. package/extensions/gamedev/skills/ecs.md +98 -98
  101. package/extensions/gamedev/skills/game-loops.md +72 -72
  102. package/extensions/gamedev/skills/input-system.md +199 -199
  103. package/extensions/gamedev/skills/multiplayer.md +180 -180
  104. package/extensions/gamedev/skills/particles.md +105 -105
  105. package/extensions/gamedev/skills/physics-engine.md +89 -89
  106. package/extensions/gamedev/skills/scene-management.md +146 -146
  107. package/extensions/gamedev/skills/threejs-patterns.md +90 -90
  108. package/extensions/gamedev/skills/webgl.md +71 -71
  109. package/extensions/mobile/PACK.md +106 -106
  110. package/extensions/mobile/skills/app-store-connect.md +152 -152
  111. package/extensions/mobile/skills/app-store-prep.md +66 -66
  112. package/extensions/mobile/skills/deep-linking.md +109 -109
  113. package/extensions/mobile/skills/flutter.md +60 -60
  114. package/extensions/mobile/skills/ios-build-pipeline.md +142 -142
  115. package/extensions/mobile/skills/native-bridge.md +66 -66
  116. package/extensions/mobile/skills/ota-updates.md +97 -97
  117. package/extensions/mobile/skills/push-notifications.md +111 -111
  118. package/extensions/mobile/skills/react-native.md +82 -82
  119. package/extensions/saas/PACK.md +116 -116
  120. package/extensions/saas/skills/billing-integration.md +200 -200
  121. package/extensions/saas/skills/feature-flags.md +130 -130
  122. package/extensions/saas/skills/multi-tenant.md +103 -103
  123. package/extensions/saas/skills/onboarding-flow.md +139 -139
  124. package/extensions/saas/skills/subscription-flow.md +95 -95
  125. package/extensions/saas/skills/team-management.md +144 -144
  126. package/extensions/security/PACK.md +99 -99
  127. package/extensions/security/skills/api-security.md +140 -140
  128. package/extensions/security/skills/compliance.md +68 -68
  129. package/extensions/security/skills/owasp-audit.md +64 -64
  130. package/extensions/security/skills/pentest-patterns.md +77 -77
  131. package/extensions/security/skills/secret-mgmt.md +65 -65
  132. package/extensions/security/skills/supply-chain.md +65 -65
  133. package/extensions/trading/PACK.md +80 -80
  134. package/extensions/trading/skills/chart-components.md +55 -55
  135. package/extensions/trading/skills/experiment-loop.md +125 -125
  136. package/extensions/trading/skills/fintech-patterns.md +47 -47
  137. package/extensions/trading/skills/indicator-library.md +58 -58
  138. package/extensions/trading/skills/quant-analysis.md +111 -111
  139. package/extensions/trading/skills/realtime-data.md +58 -58
  140. package/extensions/trading/skills/trade-logic.md +104 -104
  141. package/extensions/ui/PACK.md +130 -130
  142. package/extensions/ui/skills/a11y-audit.md +91 -91
  143. package/extensions/ui/skills/animation-patterns.md +127 -127
  144. package/extensions/ui/skills/component-patterns.md +100 -100
  145. package/extensions/ui/skills/design-decision.md +108 -108
  146. package/extensions/ui/skills/design-system.md +68 -68
  147. package/extensions/ui/skills/landing-patterns.md +155 -155
  148. package/extensions/ui/skills/palette-picker.md +173 -173
  149. package/extensions/ui/skills/react-health.md +90 -90
  150. package/extensions/ui/skills/type-system.md +125 -125
  151. package/extensions/ui/skills/web-vitals.md +153 -153
  152. package/extensions/zalo/PACK.md +145 -145
  153. package/extensions/zalo/skills/zalo-oa-mcp.md +317 -317
  154. package/extensions/zalo/skills/zalo-oa-messaging.md +429 -429
  155. package/extensions/zalo/skills/zalo-oa-setup.md +236 -236
  156. package/extensions/zalo/skills/zalo-oa-webhook.md +189 -189
  157. package/extensions/zalo/skills/zalo-personal-messaging.md +194 -194
  158. package/extensions/zalo/skills/zalo-personal-setup.md +153 -153
  159. package/extensions/zalo/skills/zalo-rate-guard.md +219 -219
  160. package/hooks/auto-format/index.cjs +48 -48
  161. package/hooks/hooks.json +111 -111
  162. package/hooks/post-session-reflect/index.cjs +189 -189
  163. package/hooks/pre-compact/index.cjs +95 -95
  164. package/hooks/run-hook.cmd +1 -1
  165. package/hooks/secrets-scan/index.cjs +100 -100
  166. package/hooks/session-start/index.cjs +71 -71
  167. package/hooks/typecheck/index.cjs +65 -65
  168. package/package.json +63 -63
  169. package/references/ui-pro-max-data/LICENSE-UI-PRO-MAX +21 -21
  170. package/references/ui-pro-max-data/charts.csv +26 -26
  171. package/references/ui-pro-max-data/colors.csv +161 -161
  172. package/references/ui-pro-max-data/styles.csv +68 -68
  173. package/references/ui-pro-max-data/typography.csv +74 -74
  174. package/references/ui-pro-max-data/ui-reasoning.csv +162 -162
  175. package/references/ui-pro-max-data/ux-guidelines.csv +99 -99
  176. package/skills/adversary/SKILL.md +283 -283
  177. package/skills/asset-creator/SKILL.md +157 -157
  178. package/skills/audit/SKILL.md +147 -2
  179. package/skills/autopsy/SKILL.md +335 -335
  180. package/skills/ba/SKILL.md +85 -1
  181. package/skills/brainstorm/SKILL.md +380 -342
  182. package/skills/browser-pilot/SKILL.md +169 -168
  183. package/skills/constraint-check/SKILL.md +165 -165
  184. package/skills/context-engine/SKILL.md +408 -404
  185. package/skills/cook/SKILL.md +917 -863
  186. package/skills/db/SKILL.md +273 -273
  187. package/skills/debug/SKILL.md +465 -465
  188. package/skills/dependency-doctor/SKILL.md +265 -235
  189. package/skills/deploy/SKILL.md +274 -231
  190. package/skills/design/DESIGN-REFERENCE.md +365 -365
  191. package/skills/design/SKILL.md +590 -589
  192. package/skills/doc-processor/SKILL.md +254 -254
  193. package/skills/docs/SKILL.md +374 -374
  194. package/skills/docs-seeker/SKILL.md +178 -177
  195. package/skills/fix/SKILL.md +332 -330
  196. package/skills/git/SKILL.md +339 -339
  197. package/skills/hallucination-guard/SKILL.md +220 -219
  198. package/skills/incident/SKILL.md +254 -253
  199. package/skills/integrity-check/SKILL.md +169 -169
  200. package/skills/journal/SKILL.md +241 -240
  201. package/skills/launch/SKILL.md +344 -344
  202. package/skills/logic-guardian/SKILL.md +269 -251
  203. package/skills/marketing/SKILL.md +351 -289
  204. package/skills/mcp-builder/SKILL.md +425 -425
  205. package/skills/neural-memory/SKILL.md +359 -362
  206. package/skills/onboard/SKILL.md +432 -403
  207. package/skills/onboard/references/invariants-template.md +76 -0
  208. package/skills/onboard/scripts/detect-invariants.js +439 -0
  209. package/skills/onboard/scripts/inject-claude-md.js +150 -0
  210. package/skills/onboard/scripts/onboard-invariants.js +194 -0
  211. package/skills/perf/SKILL.md +347 -346
  212. package/skills/plan/SKILL.md +435 -428
  213. package/skills/preflight/SKILL.md +415 -415
  214. package/skills/problem-solver/SKILL.md +380 -284
  215. package/skills/rescue/SKILL.md +474 -474
  216. package/skills/research/SKILL.md +4 -0
  217. package/skills/retro/SKILL.md +3 -1
  218. package/skills/review/SKILL.md +614 -588
  219. package/skills/review-intake/SKILL.md +249 -249
  220. package/skills/safeguard/SKILL.md +200 -200
  221. package/skills/sast/SKILL.md +190 -190
  222. package/skills/scaffold/SKILL.md +328 -287
  223. package/skills/scope-guard/SKILL.md +183 -180
  224. package/skills/scout/SKILL.md +269 -263
  225. package/skills/sentinel/SKILL.md +384 -381
  226. package/skills/sentinel-env/SKILL.md +254 -254
  227. package/skills/sequential-thinking/SKILL.md +234 -234
  228. package/skills/session-bridge/SKILL.md +595 -543
  229. package/skills/session-bridge/scripts/load-invariants.js +397 -0
  230. package/skills/skill-forge/SKILL.md +581 -581
  231. package/skills/skill-router/SKILL.md +3 -0
  232. package/skills/slides/SKILL.md +19 -0
  233. package/skills/surgeon/SKILL.md +215 -215
  234. package/skills/team/SKILL.md +557 -537
  235. package/skills/test/SKILL.md +620 -614
  236. package/skills/trend-scout/SKILL.md +145 -145
  237. package/skills/verification/SKILL.md +334 -326
  238. package/skills/video-creator/SKILL.md +201 -201
  239. package/skills/watchdog/SKILL.md +168 -168
  240. package/skills/worktree/SKILL.md +140 -140
@@ -0,0 +1,76 @@
1
+ # Project Invariants
2
+
3
+ > Auto-seeded by `rune onboard`. Edit freely — your changes survive re-runs.
4
+ > Append-only: new auto-detections land under `## Auto-detected (new)` and never
5
+ > overwrite existing rules.
6
+
7
+ This file is the project's truth-source for rules that code changes must respect.
8
+ `logic-guardian` loads it as a pre-edit gate, and `session-bridge` surfaces the
9
+ most critical rules at session start so agents know the danger zones immediately.
10
+
11
+ ## How to read an entry
12
+
13
+ ```
14
+ ### <short rule title>
15
+ - **WHAT**: one-sentence statement of the rule (what must hold)
16
+ - **WHERE**: glob patterns for files this rule applies to
17
+ - **WHY**: why this rule exists — past incident, cross-file coupling,
18
+ compliance, external contract, etc.
19
+ ```
20
+
21
+ If an entry becomes stale, move it to `## Archived` with a one-line note.
22
+ Never delete — the archive is evidence of past decisions.
23
+
24
+ ---
25
+
26
+ ## Danger Zones
27
+
28
+ Paths that disproportionately affect the rest of the codebase. Test coverage,
29
+ review, and change-review should be stricter here.
30
+
31
+ <!-- auto-generated entries appear below -->
32
+
33
+ ---
34
+
35
+ ## Critical Invariants
36
+
37
+ Rules that cross file boundaries — shared constants, contract values, protocol
38
+ fields. Breaking any one silently breaks downstream code.
39
+
40
+ <!-- auto-generated entries appear below -->
41
+
42
+ ---
43
+
44
+ ## State Machine Rules
45
+
46
+ Legal transitions for state machines in the project. Illegal transitions are
47
+ often the root cause of hard-to-reproduce bugs.
48
+
49
+ <!-- auto-generated entries appear below -->
50
+
51
+ ---
52
+
53
+ ## Cross-File Consistency
54
+
55
+ Fields, enums, or schemas mirrored in multiple places. Updates must propagate
56
+ together or the project enters an inconsistent state.
57
+
58
+ <!-- auto-generated entries appear below -->
59
+
60
+ ---
61
+
62
+ ## Auto-detected (new)
63
+
64
+ New detections from the most recent `rune onboard` run. Review these, promote
65
+ the real ones into the sections above, and drop the noise.
66
+
67
+ <!-- placeholder — each run appends or replaces this block -->
68
+
69
+ ---
70
+
71
+ ## Archived
72
+
73
+ Rules that were once invariants but are no longer — kept here as history. Do
74
+ not delete; their absence later makes reasoning harder.
75
+
76
+ <!-- empty on first run -->
@@ -0,0 +1,439 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * detect-invariants.js — Scan a project and emit candidate invariant rules
5
+ * for `.rune/INVARIANTS.md`.
6
+ *
7
+ * Detection buckets (ordered by specificity, not confidence):
8
+ * 1. Danger zones — directories with high churn signals (deep + many files)
9
+ * 2. Critical — shared constants exported and imported in ≥ 3 places
10
+ * 3. State machines — reducer shapes, switch(state), enum-driven transitions
11
+ * 4. Cross-file — duplicated literal tuples (suggests mirrored schemas)
12
+ *
13
+ * Heuristics favor recall over precision for danger zones (let the user prune),
14
+ * precision over recall for critical invariants (don't cry wolf).
15
+ *
16
+ * Usage as CLI:
17
+ * node detect-invariants.js --root <project-root> [--json]
18
+ *
19
+ * Usage as module:
20
+ * import { detectInvariants } from './detect-invariants.js';
21
+ * const rules = await detectInvariants({ root });
22
+ */
23
+
24
+ import { readdir, readFile, stat } from 'node:fs/promises';
25
+ import path from 'node:path';
26
+ import { parseArgs } from 'node:util';
27
+
28
+ const IGNORED_DIRS = new Set([
29
+ 'node_modules',
30
+ '.git',
31
+ 'dist',
32
+ 'build',
33
+ 'coverage',
34
+ '.next',
35
+ '.nuxt',
36
+ '.svelte-kit',
37
+ '.turbo',
38
+ '.cache',
39
+ '__pycache__',
40
+ '.venv',
41
+ 'venv',
42
+ 'target',
43
+ '.rune',
44
+ ]);
45
+
46
+ const SOURCE_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.go', '.rs']);
47
+
48
+ /**
49
+ * Files whose presence marks a directory as a first-class artifact (AI skills,
50
+ * extension packs, plugin manifests). These directories become danger-zone
51
+ * candidates even if they contain no SOURCE_EXTS files — a SKILL.md edit can
52
+ * reshape runtime behavior for every consumer.
53
+ */
54
+ const SIGNAL_FILES = new Set(['SKILL.md', 'PACK.md', 'plugin.json']);
55
+
56
+ const MAX_FILES = 2000;
57
+ const MAX_FILE_BYTES = 200_000;
58
+
59
+ /**
60
+ * Main entry — returns structured rule objects.
61
+ *
62
+ * @param {{root: string, maxFiles?: number}} opts
63
+ * @returns {Promise<{danger: Rule[], critical: Rule[], state: Rule[], cross: Rule[], stats: Object}>}
64
+ */
65
+ export async function detectInvariants(opts) {
66
+ const { root } = opts;
67
+ const maxFiles = opts.maxFiles ?? MAX_FILES;
68
+ if (!root) throw new Error('detectInvariants: root is required');
69
+
70
+ const index = await buildIndex(root, maxFiles);
71
+
72
+ const danger = detectDangerZones(index);
73
+ const critical = await detectCriticalConstants(root, index);
74
+ const state = await detectStateMachines(root, index);
75
+ const cross = await detectCrossFileConsistency(root, index);
76
+
77
+ return {
78
+ danger,
79
+ critical,
80
+ state,
81
+ cross,
82
+ stats: {
83
+ filesScanned: index.files.length,
84
+ directoriesSeen: index.dirs.size,
85
+ truncated: index.truncated,
86
+ },
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Walk the tree, collecting source files with size + path.
92
+ */
93
+ async function buildIndex(root, maxFiles) {
94
+ const files = [];
95
+ const dirs = new Set();
96
+ const signalDirs = new Map(); // rel-dir -> count of signal files
97
+ let truncated = false;
98
+
99
+ async function walk(dir, depth) {
100
+ if (files.length >= maxFiles) {
101
+ truncated = true;
102
+ return;
103
+ }
104
+ let entries;
105
+ try {
106
+ entries = await readdir(dir, { withFileTypes: true });
107
+ } catch {
108
+ return;
109
+ }
110
+ dirs.add(path.relative(root, dir) || '.');
111
+ for (const entry of entries) {
112
+ if (files.length >= maxFiles) {
113
+ truncated = true;
114
+ return;
115
+ }
116
+ if (IGNORED_DIRS.has(entry.name)) continue;
117
+ if (entry.name.startsWith('.') && depth === 0) continue;
118
+ const full = path.join(dir, entry.name);
119
+ if (entry.isDirectory()) {
120
+ await walk(full, depth + 1);
121
+ } else if (entry.isFile() && SIGNAL_FILES.has(entry.name)) {
122
+ const relDir = path.relative(root, dir).replace(/\\/g, '/') || '.';
123
+ signalDirs.set(relDir, (signalDirs.get(relDir) || 0) + 1);
124
+ }
125
+ if (entry.isFile() && SOURCE_EXTS.has(path.extname(entry.name))) {
126
+ try {
127
+ const s = await stat(full);
128
+ if (s.size <= MAX_FILE_BYTES) {
129
+ files.push({
130
+ abs: full,
131
+ rel: path.relative(root, full).replace(/\\/g, '/'),
132
+ size: s.size,
133
+ });
134
+ }
135
+ } catch {
136
+ /* skip unreadable */
137
+ }
138
+ }
139
+ }
140
+ }
141
+
142
+ await walk(root, 0);
143
+ return { files, dirs, signalDirs, truncated };
144
+ }
145
+
146
+ /**
147
+ * Danger-zone heuristic: directories with ≥ 5 source files AND depth ≥ 2
148
+ * OR directories whose relative path contains high-risk keywords.
149
+ * Sort by file count desc, take top 5.
150
+ */
151
+ function detectDangerZones(index) {
152
+ const HIGH_RISK_KEYWORDS = [
153
+ 'auth',
154
+ 'payment',
155
+ 'billing',
156
+ 'router',
157
+ 'session',
158
+ 'security',
159
+ 'crypto',
160
+ 'migration',
161
+ 'compiler',
162
+ 'parser',
163
+ 'state',
164
+ ];
165
+
166
+ const byDir = new Map();
167
+ for (const f of index.files) {
168
+ const dir = path.posix.dirname(f.rel);
169
+ byDir.set(dir, (byDir.get(dir) || 0) + 1);
170
+ }
171
+
172
+ // Fold signal-file directories (SKILL.md / PACK.md / plugin.json) into the
173
+ // scoring pool so AI-skill repos surface their orchestrators as danger zones
174
+ // even when the directory contains no SOURCE_EXTS files.
175
+ const signalDirs = index.signalDirs ?? new Map();
176
+ const scored = [];
177
+ const consideredDirs = new Set([...byDir.keys(), ...signalDirs.keys()]);
178
+ for (const dir of consideredDirs) {
179
+ if (dir === '.' || dir === '') continue;
180
+ const count = byDir.get(dir) ?? 0;
181
+ const signalCount = signalDirs.get(dir) ?? 0;
182
+ const depth = dir.split('/').length;
183
+ const keywordHit = HIGH_RISK_KEYWORDS.find((kw) => dir.toLowerCase().includes(kw));
184
+ let score = 0;
185
+ if (count >= 5 && depth >= 2) score += count;
186
+ if (keywordHit) score += 20;
187
+ if (signalCount > 0) score += 15 + signalCount;
188
+ if (score === 0) continue;
189
+ scored.push({ dir, count: count + signalCount, keyword: keywordHit, signal: signalCount > 0, score });
190
+ }
191
+
192
+ scored.sort((a, b) => b.score - a.score);
193
+ const top = scored.slice(0, 5);
194
+
195
+ return top.map((s) => {
196
+ const artifactLabel = s.signal ? 'skill / pack artifacts' : 'source files';
197
+ const why = s.keyword
198
+ ? `High-risk keyword "${s.keyword}" in path + ${s.count} ${artifactLabel}. Cross-cutting concerns amplify blast radius.`
199
+ : s.signal
200
+ ? `${s.count} skill/pack artifact(s) under \`${s.dir}/\` — SKILL.md / PACK.md edits reshape runtime behavior for every consumer.`
201
+ : `${s.count} source files in a directory ≥ depth 2 — concentrated surface area.`;
202
+ return {
203
+ section: 'danger',
204
+ title: `${s.dir} — ${s.count} ${artifactLabel}`,
205
+ what: `Changes under \`${s.dir}/\` touch core logic; require tests before merge.`,
206
+ where: [`${s.dir}/**`],
207
+ why,
208
+ };
209
+ });
210
+ }
211
+
212
+ /**
213
+ * Shared constant detection:
214
+ * Find `export const FOO = "literal"` (or UPPER_CASE) in ≤ 50 files,
215
+ * then count imports of FOO across the index. If imported ≥ 3 times, flag it.
216
+ */
217
+ async function detectCriticalConstants(_root, index) {
218
+ const CONST_REGEX = /export\s+const\s+([A-Z][A-Z0-9_]+)\s*=\s*(?:['"`]|\d|\{|\[)/g;
219
+ const candidates = new Map(); // name -> { file, value-snippet }
220
+ let examined = 0;
221
+
222
+ for (const f of index.files) {
223
+ if (examined >= 50) break;
224
+ if (!/\.(ts|tsx|js|jsx|mjs)$/.test(f.rel)) continue;
225
+ if (f.size > 80_000) continue;
226
+ examined += 1;
227
+ let content;
228
+ try {
229
+ content = await readFile(f.abs, 'utf-8');
230
+ } catch {
231
+ continue;
232
+ }
233
+ let match;
234
+ CONST_REGEX.lastIndex = 0;
235
+ while ((match = CONST_REGEX.exec(content))) {
236
+ const name = match[1];
237
+ if (!candidates.has(name)) {
238
+ candidates.set(name, { file: f.rel, hits: 0 });
239
+ }
240
+ }
241
+ }
242
+
243
+ if (candidates.size === 0) return [];
244
+
245
+ // Count usages for each candidate across all indexed source files
246
+ const importRegexes = new Map();
247
+ for (const name of candidates.keys()) {
248
+ importRegexes.set(name, new RegExp(`\\b${escapeRegex(name)}\\b`, 'g'));
249
+ }
250
+
251
+ let usageScanned = 0;
252
+ for (const f of index.files) {
253
+ if (usageScanned >= 200) break;
254
+ if (!/\.(ts|tsx|js|jsx|mjs)$/.test(f.rel)) continue;
255
+ if (f.size > 80_000) continue;
256
+ usageScanned += 1;
257
+ let content;
258
+ try {
259
+ content = await readFile(f.abs, 'utf-8');
260
+ } catch {
261
+ continue;
262
+ }
263
+ for (const [name, re] of importRegexes) {
264
+ const matches = content.match(re);
265
+ if (matches && matches.length > 0) {
266
+ const c = candidates.get(name);
267
+ if (f.rel !== c.file) c.hits += matches.length;
268
+ }
269
+ }
270
+ }
271
+
272
+ const rules = [];
273
+ for (const [name, { file, hits }] of candidates) {
274
+ if (hits < 3) continue;
275
+ rules.push({
276
+ section: 'critical',
277
+ title: `Shared constant ${name}`,
278
+ what: `\`${name}\` (defined in \`${file}\`) is referenced ${hits} times across the codebase. Changing its value affects every consumer.`,
279
+ where: [file, `**/*.{ts,tsx,js,jsx,mjs}`],
280
+ why: `Widely imported constants are effectively part of the project's contract. Renames or value changes must propagate atomically.`,
281
+ });
282
+ }
283
+ rules.sort((a, b) => (b.what.match(/\d+/) || 0) - (a.what.match(/\d+/) || 0));
284
+ return rules.slice(0, 10);
285
+ }
286
+
287
+ /**
288
+ * State-machine detection: files that contain both a state enum/union AND
289
+ * a switch or reducer acting on it. Surface the pair as a single rule.
290
+ */
291
+ async function detectStateMachines(_root, index) {
292
+ const rules = [];
293
+ const STATE_HINT = /\b(state|status|phase|stage)\s*[:=]\s*(?:['"])([a-z_]+)(?:['"])/i;
294
+ const SWITCH_HINT = /switch\s*\(\s*(?:\w+\.)?(?:state|status|phase|stage)\s*\)/;
295
+ const REDUCER_HINT = /case\s+['"`][A-Z_][A-Z0-9_]+['"`]\s*:/;
296
+
297
+ let scanned = 0;
298
+ for (const f of index.files) {
299
+ if (scanned >= 150) break;
300
+ if (!/\.(ts|tsx|js|jsx|mjs|py)$/.test(f.rel)) continue;
301
+ if (f.size > 80_000) continue;
302
+ scanned += 1;
303
+ let content;
304
+ try {
305
+ content = await readFile(f.abs, 'utf-8');
306
+ } catch {
307
+ continue;
308
+ }
309
+
310
+ const hasSwitch = SWITCH_HINT.test(content);
311
+ const hasReducer = REDUCER_HINT.test(content);
312
+ const stateMatch = STATE_HINT.exec(content);
313
+ if (!(hasSwitch || hasReducer) || !stateMatch) continue;
314
+
315
+ rules.push({
316
+ section: 'state',
317
+ title: `State machine in ${f.rel}`,
318
+ what: `Transitions in \`${f.rel}\` must respect declared states. New states require updating every switch/case site.`,
319
+ where: [f.rel],
320
+ why: `Detected ${hasSwitch ? 'switch(state)' : 'reducer'} + state literal (e.g. "${stateMatch[2]}"). Missing-case bugs are a classic regression vector.`,
321
+ });
322
+ if (rules.length >= 5) break;
323
+ }
324
+ return rules;
325
+ }
326
+
327
+ /**
328
+ * Cross-file consistency heuristic: find string literal tuples that appear
329
+ * verbatim in ≥ 3 files (e.g. `["pending", "active", "closed"]`). Often these
330
+ * are mirrored schemas/enums that must stay in lock-step.
331
+ */
332
+ async function detectCrossFileConsistency(_root, index) {
333
+ const TUPLE_REGEX = /\[\s*(['"][a-z_-]{2,}['"](?:\s*,\s*['"][a-z_-]{2,}['"]){2,})\s*\]/gi;
334
+ const byTuple = new Map();
335
+
336
+ let scanned = 0;
337
+ for (const f of index.files) {
338
+ if (scanned >= 150) break;
339
+ if (!/\.(ts|tsx|js|jsx|mjs|py)$/.test(f.rel)) continue;
340
+ if (f.size > 80_000) continue;
341
+ scanned += 1;
342
+ let content;
343
+ try {
344
+ content = await readFile(f.abs, 'utf-8');
345
+ } catch {
346
+ continue;
347
+ }
348
+ const seen = new Set();
349
+ TUPLE_REGEX.lastIndex = 0;
350
+ let match;
351
+ while ((match = TUPLE_REGEX.exec(content))) {
352
+ const normalized = match[1].replace(/\s+/g, '').toLowerCase();
353
+ if (seen.has(normalized)) continue;
354
+ seen.add(normalized);
355
+ if (!byTuple.has(normalized)) byTuple.set(normalized, new Set());
356
+ byTuple.get(normalized).add(f.rel);
357
+ }
358
+ }
359
+
360
+ const rules = [];
361
+ for (const [tuple, fileSet] of byTuple) {
362
+ if (fileSet.size < 3) continue;
363
+ const sample = tuple.split(',').slice(0, 4).join(', ');
364
+ rules.push({
365
+ section: 'cross',
366
+ title: `Mirrored literal tuple (${sample}${tuple.includes(',') ? '…' : ''})`,
367
+ what: `Identical literal list appears in ${fileSet.size} files; updates must land in every location.`,
368
+ where: Array.from(fileSet).sort().slice(0, 10),
369
+ why: `Duplicated tuples are typically mirrored schemas (DB columns, API fields, state enums). Drift between copies causes subtle bugs.`,
370
+ });
371
+ }
372
+ rules.sort((a, b) => b.where.length - a.where.length);
373
+ return rules.slice(0, 5);
374
+ }
375
+
376
+ function escapeRegex(s) {
377
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
378
+ }
379
+
380
+ /**
381
+ * Render a detection result into the markdown block that gets injected under
382
+ * `## Auto-detected (new)` in INVARIANTS.md.
383
+ */
384
+ export function renderInvariants(result) {
385
+ const lines = [];
386
+ const sections = [
387
+ ['Danger Zones', result.danger],
388
+ ['Critical Invariants', result.critical],
389
+ ['State Machine Rules', result.state],
390
+ ['Cross-File Consistency', result.cross],
391
+ ];
392
+ for (const [title, rules] of sections) {
393
+ if (!rules || rules.length === 0) continue;
394
+ lines.push(`### ${title}`);
395
+ lines.push('');
396
+ for (const rule of rules) {
397
+ lines.push(`#### ${rule.title}`);
398
+ lines.push(`- **WHAT**: ${rule.what}`);
399
+ lines.push(`- **WHERE**: ${rule.where.map((w) => `\`${w}\``).join(', ')}`);
400
+ lines.push(`- **WHY**: ${rule.why}`);
401
+ lines.push('');
402
+ }
403
+ }
404
+ if (lines.length === 0) return '_No invariants detected in this run._\n';
405
+ return `${lines.join('\n')}`;
406
+ }
407
+
408
+ // ─── CLI ───
409
+
410
+ async function main() {
411
+ const { values } = parseArgs({
412
+ options: {
413
+ root: { type: 'string', default: process.cwd() },
414
+ json: { type: 'boolean', default: false },
415
+ },
416
+ });
417
+ const result = await detectInvariants({ root: values.root });
418
+ if (values.json) {
419
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
420
+ return;
421
+ }
422
+ process.stdout.write(renderInvariants(result));
423
+ process.stdout.write(`\n_Scanned ${result.stats.filesScanned} files._\n`);
424
+ }
425
+
426
+ // Only run main() when invoked directly as a CLI
427
+ const isMain = (() => {
428
+ try {
429
+ return import.meta.url === `file://${process.argv[1]}` || import.meta.url.endsWith(path.basename(process.argv[1]));
430
+ } catch {
431
+ return false;
432
+ }
433
+ })();
434
+ if (isMain) {
435
+ main().catch((err) => {
436
+ process.stderr.write(`detect-invariants: ${err.message}\n`);
437
+ process.exit(1);
438
+ });
439
+ }
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * inject-claude-md.js — Idempotent editor for CLAUDE.md that maintains an
5
+ * auto-generated "Invariants (auto-detected)" pointer block.
6
+ *
7
+ * The block is delimited by HTML comment markers that are valid in Markdown:
8
+ *
9
+ * <!-- @rune-invariants-pointer:start -->
10
+ * ...
11
+ * <!-- @rune-invariants-pointer:end -->
12
+ *
13
+ * Content outside these markers is NEVER touched. Re-running replaces only the
14
+ * content between the markers. Users can delete the markers to opt out — a
15
+ * subsequent run will re-inject them once and respect a user-placed
16
+ * `<!-- @rune-invariants-pointer:skip -->` directive indefinitely.
17
+ *
18
+ * Usage as CLI:
19
+ * node inject-claude-md.js --claude-md <path> --invariants <path>
20
+ *
21
+ * Usage as module:
22
+ * import { injectInvariantsPointer, buildPointerBlock } from './inject-claude-md.js';
23
+ * const { action, content } = injectInvariantsPointer({ claudeMd, globs });
24
+ */
25
+
26
+ import { existsSync } from 'node:fs';
27
+ import { readFile, writeFile } from 'node:fs/promises';
28
+ import { parseArgs } from 'node:util';
29
+
30
+ export const MARKER_START = '<!-- @rune-invariants-pointer:start -->';
31
+ export const MARKER_END = '<!-- @rune-invariants-pointer:end -->';
32
+ export const SKIP_DIRECTIVE = '<!-- @rune-invariants-pointer:skip -->';
33
+
34
+ const DEFAULT_INVARIANTS_PATH = '.rune/INVARIANTS.md';
35
+ const MAX_GLOBS_IN_POINTER = 8;
36
+
37
+ export function buildPointerBlock({ globs = [], invariantsPath = DEFAULT_INVARIANTS_PATH } = {}) {
38
+ const unique = Array.from(new Set(globs.filter((g) => typeof g === 'string' && g.trim())));
39
+ const shown = unique.slice(0, MAX_GLOBS_IN_POINTER);
40
+ const overflow = unique.length - shown.length;
41
+
42
+ const lines = [
43
+ MARKER_START,
44
+ '## Invariants (auto-detected)',
45
+ '',
46
+ `Before editing these paths, read [\`${invariantsPath}\`](${invariantsPath}) —`,
47
+ 'it lists danger zones and cross-file invariants this project enforces.',
48
+ '',
49
+ ];
50
+
51
+ if (shown.length === 0) {
52
+ lines.push('_No danger zones detected yet. Re-run `rune onboard` after the codebase grows._');
53
+ } else {
54
+ for (const glob of shown) {
55
+ lines.push(`- \`${glob}\``);
56
+ }
57
+ if (overflow > 0) {
58
+ lines.push(`- _…and ${overflow} more — see \`${invariantsPath}\`_`);
59
+ }
60
+ }
61
+
62
+ lines.push('', MARKER_END);
63
+ return lines.join('\n');
64
+ }
65
+
66
+ export function injectInvariantsPointer({ claudeMd = '', globs = [], invariantsPath = DEFAULT_INVARIANTS_PATH } = {}) {
67
+ if (claudeMd.includes(SKIP_DIRECTIVE)) {
68
+ return { action: 'skipped', reason: 'skip-directive', content: claudeMd };
69
+ }
70
+
71
+ const block = buildPointerBlock({ globs, invariantsPath });
72
+
73
+ const startIdx = claudeMd.indexOf(MARKER_START);
74
+ const endIdx = claudeMd.indexOf(MARKER_END);
75
+
76
+ if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
77
+ const before = claudeMd.slice(0, startIdx);
78
+ const after = claudeMd.slice(endIdx + MARKER_END.length);
79
+ const next = `${before}${block}${after}`;
80
+ if (next === claudeMd) {
81
+ return { action: 'unchanged', content: claudeMd };
82
+ }
83
+ return { action: 'updated', content: next };
84
+ }
85
+
86
+ if (startIdx !== -1 || endIdx !== -1) {
87
+ return {
88
+ action: 'error',
89
+ reason: 'marker-mismatch',
90
+ content: claudeMd,
91
+ };
92
+ }
93
+
94
+ const separator = claudeMd.length === 0 || claudeMd.endsWith('\n\n') ? '' : claudeMd.endsWith('\n') ? '\n' : '\n\n';
95
+ const next = `${claudeMd}${separator}${block}\n`;
96
+ return { action: 'created', content: next };
97
+ }
98
+
99
+ export async function applyInvariantsPointer({
100
+ claudeMdPath,
101
+ globs = [],
102
+ invariantsPath = DEFAULT_INVARIANTS_PATH,
103
+ dryRun = false,
104
+ } = {}) {
105
+ if (!claudeMdPath) {
106
+ throw new Error('claudeMdPath is required');
107
+ }
108
+
109
+ const existing = existsSync(claudeMdPath) ? await readFile(claudeMdPath, 'utf8') : '';
110
+ const result = injectInvariantsPointer({ claudeMd: existing, globs, invariantsPath });
111
+
112
+ if (!dryRun && (result.action === 'created' || result.action === 'updated')) {
113
+ await writeFile(claudeMdPath, result.content, 'utf8');
114
+ }
115
+
116
+ return { ...result, path: claudeMdPath, existed: existing.length > 0 };
117
+ }
118
+
119
+ async function main() {
120
+ const { values } = parseArgs({
121
+ options: {
122
+ 'claude-md': { type: 'string' },
123
+ invariants: { type: 'string', default: DEFAULT_INVARIANTS_PATH },
124
+ globs: { type: 'string', multiple: true, default: [] },
125
+ dry: { type: 'boolean', default: false },
126
+ },
127
+ });
128
+
129
+ const claudeMdPath = values['claude-md'];
130
+ if (!claudeMdPath) {
131
+ console.error('Usage: inject-claude-md.js --claude-md <path> [--invariants <path>] [--globs glob ...] [--dry]');
132
+ process.exit(2);
133
+ }
134
+
135
+ const result = await applyInvariantsPointer({
136
+ claudeMdPath,
137
+ globs: values.globs,
138
+ invariantsPath: values.invariants,
139
+ dryRun: values.dry,
140
+ });
141
+
142
+ console.log(JSON.stringify({ path: result.path, action: result.action, reason: result.reason ?? null }));
143
+ }
144
+
145
+ if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith('inject-claude-md.js')) {
146
+ main().catch((err) => {
147
+ console.error(err.message);
148
+ process.exit(1);
149
+ });
150
+ }