@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,397 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * load-invariants.js — Parse `.rune/INVARIANTS.md` at session start.
5
+ *
6
+ * Responsibilities:
7
+ * - Read the file if it exists (silent no-op otherwise).
8
+ * - Skip the `## Archived` section entirely (rules there are intentionally
9
+ * retired; including them would re-activate historical noise).
10
+ * - Extract active rules with WHAT / WHERE / WHY + section label.
11
+ * - Produce a token-budgeted preview string (≤ ~500 tokens by default) that
12
+ * session-bridge injects verbatim into the agent's session-start summary.
13
+ * - Detect staleness: if mtime > 30 days old, flag so session-bridge can warn
14
+ * once per session.
15
+ *
16
+ * Returned shape:
17
+ * {
18
+ * loaded: boolean,
19
+ * path: string,
20
+ * stale: boolean, // mtime > 30 days old
21
+ * stats: { danger, critical, state, cross, archivedSkipped, total },
22
+ * rules: Rule[], // [{ section, title, what, where: string[], why }]
23
+ * preview: string, // agent-visible bullet list, capped by budget
24
+ * overflow: number, // rules present but NOT in preview (budget overflow)
25
+ * }
26
+ *
27
+ * Usage as library:
28
+ * import {
29
+ * loadInvariants,
30
+ * matchesInvariant,
31
+ * findMatchingInvariants,
32
+ * } from './load-invariants.js';
33
+ *
34
+ * const result = await loadInvariants({ root, budgetTokens: 500 });
35
+ * // Blast-radius check (used by logic-guardian, Pro autopilot Step 0.5):
36
+ * const hits = findMatchingInvariants('skills/cook/foo.js', result.rules);
37
+ *
38
+ * Usage as CLI:
39
+ * node load-invariants.js --root <project-root> [--json]
40
+ */
41
+
42
+ import { existsSync } from 'node:fs';
43
+ import { readFile, stat } from 'node:fs/promises';
44
+ import path from 'node:path';
45
+ import { parseArgs } from 'node:util';
46
+
47
+ const DEFAULT_BUDGET_TOKENS = 500;
48
+ const STALE_AGE_DAYS = 30;
49
+
50
+ /**
51
+ * Rough token estimator. Uses Unicode scalar count (`[...str].length`) rather
52
+ * than `str.length`, because:
53
+ * - `str.length` in JS = UTF-16 code units → emoji outside the BMP count as 2
54
+ * (e.g. `🔒`.length === 2) even though tokenizers treat them as ~1 token.
55
+ * - CJK / Vietnamese text tokenizes closer to 1 char ≈ 1 token, not 1/4 —
56
+ * scalar count gives a safer lower-bound denominator.
57
+ * Not exact tokenization — just a deterministic ceiling that doesn't drift
58
+ * wildly on non-ASCII content.
59
+ */
60
+ function estimateTokens(str) {
61
+ let scalars = 0;
62
+ for (const _ of str) scalars += 1;
63
+ return Math.ceil(scalars / 3);
64
+ }
65
+
66
+ /**
67
+ * Walk `text` line-by-line, tracking triple-backtick / triple-tilde fence
68
+ * state. Yields `{ line, inFence, offset }` for each line. Shared by
69
+ * `stripArchived` and `parseInvariants` so markdown constructs inside fenced
70
+ * code blocks (e.g. a `## Archived` line inside a ```markdown example) never
71
+ * trigger false-positive section breaks or rule headers.
72
+ */
73
+ function* walkLines(text) {
74
+ let offset = 0;
75
+ let inFence = false;
76
+ let fenceDelim = null;
77
+ while (offset <= text.length) {
78
+ const eol = text.indexOf('\n', offset);
79
+ const lineEnd = eol === -1 ? text.length : eol;
80
+ const line = text.slice(offset, lineEnd);
81
+ const trimmed = line.trimStart();
82
+ if (inFence) {
83
+ if (trimmed.startsWith(fenceDelim)) {
84
+ inFence = false;
85
+ fenceDelim = null;
86
+ }
87
+ yield { line, inFence: true, offset };
88
+ } else {
89
+ if (trimmed.startsWith('```') || trimmed.startsWith('~~~')) {
90
+ inFence = true;
91
+ fenceDelim = trimmed.startsWith('```') ? '```' : '~~~';
92
+ yield { line, inFence: true, offset };
93
+ } else {
94
+ yield { line, inFence: false, offset };
95
+ }
96
+ }
97
+ if (eol === -1) break;
98
+ offset = eol + 1;
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Strip the `## Archived` section (and anything below it) from `text`.
104
+ * Archived rules are intentionally retired — loading them would re-surface
105
+ * historical noise. Fence-aware: `## Archived` inside a code block does NOT
106
+ * terminate active rules.
107
+ */
108
+ export function stripArchived(text) {
109
+ for (const { line, inFence, offset } of walkLines(text)) {
110
+ if (inFence) continue;
111
+ if (/^## Archived\s*$/.test(line)) {
112
+ return text.slice(0, offset);
113
+ }
114
+ }
115
+ return text;
116
+ }
117
+
118
+ /**
119
+ * Parse an INVARIANTS.md body (with archived already stripped) into rule
120
+ * objects. Walks `### SectionTitle` and `#### RuleTitle` blocks; expects each
121
+ * rule block to carry `- **WHAT**:`, `- **WHERE**:`, `- **WHY**:` lines.
122
+ * Missing lines are tolerated — they become empty strings / empty arrays.
123
+ */
124
+ export function parseInvariants(text) {
125
+ const rules = [];
126
+ const SECTION_RE = /^##\s+(Danger Zones|Critical Invariants|State Machine Rules|Cross-File Consistency)\b/;
127
+ const SUBSECTION_RE = /^###\s+(Danger Zones|Critical Invariants|State Machine Rules|Cross-File Consistency)\b/;
128
+ const RULE_HEADER_RE = /^####\s+(.+?)\s*$/;
129
+ const FIELD_RE = /^-\s+\*\*(WHAT|WHERE|WHY)\*\*:\s*(.+)$/;
130
+
131
+ let currentSection = null;
132
+ let current = null;
133
+
134
+ const flush = () => {
135
+ if (current && current.title) rules.push(current);
136
+ current = null;
137
+ };
138
+
139
+ for (const { line: raw, inFence } of walkLines(text)) {
140
+ // Fenced code blocks can legitimately contain text that looks like
141
+ // section headers or field lines (e.g. a markdown example inside a WHY).
142
+ // Ignore them while inside a fence.
143
+ if (inFence) continue;
144
+ const line = raw.trimEnd();
145
+ const sectionMatch = SECTION_RE.exec(line) || SUBSECTION_RE.exec(line);
146
+ if (sectionMatch) {
147
+ flush();
148
+ currentSection = sectionLabelToKey(sectionMatch[1]);
149
+ continue;
150
+ }
151
+ const ruleMatch = RULE_HEADER_RE.exec(line);
152
+ if (ruleMatch && currentSection) {
153
+ flush();
154
+ current = {
155
+ section: currentSection,
156
+ title: ruleMatch[1].trim(),
157
+ what: '',
158
+ where: [],
159
+ why: '',
160
+ };
161
+ continue;
162
+ }
163
+ if (!current) continue;
164
+ const fieldMatch = FIELD_RE.exec(line);
165
+ if (!fieldMatch) continue;
166
+ const [, key, value] = fieldMatch;
167
+ if (key === 'WHAT') current.what = value.trim();
168
+ else if (key === 'WHY') current.why = value.trim();
169
+ else if (key === 'WHERE') current.where = extractGlobs(value);
170
+ }
171
+ flush();
172
+ return rules;
173
+ }
174
+
175
+ function sectionLabelToKey(label) {
176
+ switch (label) {
177
+ case 'Danger Zones':
178
+ return 'danger';
179
+ case 'Critical Invariants':
180
+ return 'critical';
181
+ case 'State Machine Rules':
182
+ return 'state';
183
+ case 'Cross-File Consistency':
184
+ return 'cross';
185
+ default:
186
+ return 'other';
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Convert a minimal glob pattern to a RegExp. Supports the subset of globs
192
+ * seen in INVARIANTS.md `WHERE` entries:
193
+ * - `**` matches any number of path segments (including zero)
194
+ * - `*` matches anything except `/`
195
+ * - `?` matches a single non-`/` character
196
+ * - All other regex metacharacters are escaped literally
197
+ * Paths are normalized to forward slashes before matching.
198
+ */
199
+ function globToRegExp(glob) {
200
+ const normalized = glob.replace(/\\/g, '/');
201
+ let re = '';
202
+ let i = 0;
203
+ while (i < normalized.length) {
204
+ const c = normalized[i];
205
+ if (c === '*' && normalized[i + 1] === '*') {
206
+ // `**/` = zero or more segments
207
+ if (normalized[i + 2] === '/') {
208
+ re += '(?:.*/)?';
209
+ i += 3;
210
+ } else {
211
+ re += '.*';
212
+ i += 2;
213
+ }
214
+ } else if (c === '*') {
215
+ re += '[^/]*';
216
+ i += 1;
217
+ } else if (c === '?') {
218
+ re += '[^/]';
219
+ i += 1;
220
+ } else if ('.+^$()[]{}|\\'.includes(c)) {
221
+ re += `\\${c}`;
222
+ i += 1;
223
+ } else {
224
+ re += c;
225
+ i += 1;
226
+ }
227
+ }
228
+ return new RegExp(`^${re}$`);
229
+ }
230
+
231
+ /**
232
+ * Return true if `filePath` matches any glob in a rule's `where[]`.
233
+ * Normalizes paths to forward slashes first so Windows and POSIX callers get
234
+ * identical results. Consumers (logic-guardian pre-edit gate, autopilot
235
+ * pre-flight gate) use this to decide whether a planned edit touches an
236
+ * invariant-protected path.
237
+ */
238
+ export function matchesInvariant(filePath, rule) {
239
+ if (!rule || !Array.isArray(rule.where) || rule.where.length === 0) return false;
240
+ const normalized = filePath.replace(/\\/g, '/').replace(/^\.\//, '');
241
+ for (const glob of rule.where) {
242
+ if (globToRegExp(glob).test(normalized)) return true;
243
+ }
244
+ return false;
245
+ }
246
+
247
+ /**
248
+ * Return every rule whose WHERE globs match `filePath`. Order preserved from
249
+ * the loaded rules array. Use this when an edit blast radius needs to surface
250
+ * every active invariant that applies.
251
+ */
252
+ export function findMatchingInvariants(filePath, rules) {
253
+ return rules.filter((rule) => matchesInvariant(filePath, rule));
254
+ }
255
+
256
+ function extractGlobs(raw) {
257
+ const BACKTICK_RE = /`([^`]+)`/g;
258
+ const globs = [];
259
+ let match;
260
+ BACKTICK_RE.lastIndex = 0;
261
+ while ((match = BACKTICK_RE.exec(raw))) {
262
+ globs.push(match[1].trim());
263
+ }
264
+ // Fallback: if no backticks, split on commas/semicolons.
265
+ if (globs.length === 0) {
266
+ return raw
267
+ .split(/[,;]/)
268
+ .map((s) => s.trim())
269
+ .filter(Boolean);
270
+ }
271
+ return globs;
272
+ }
273
+
274
+ /**
275
+ * Render an agent-visible preview. Caps at ~budgetTokens by dropping tail rules
276
+ * and appending an overflow marker. Priority order: danger → critical → state → cross.
277
+ */
278
+ export function renderPreview(rules, { budgetTokens = DEFAULT_BUDGET_TOKENS } = {}) {
279
+ if (rules.length === 0) {
280
+ return { preview: '', overflow: 0 };
281
+ }
282
+ const priority = { danger: 0, critical: 1, state: 2, cross: 3, other: 4 };
283
+ const ordered = [...rules].sort((a, b) => (priority[a.section] ?? 9) - (priority[b.section] ?? 9));
284
+
285
+ const ICON = { danger: '⚠', critical: '🔒', state: '🔁', cross: '🔗', other: '•' };
286
+ const header = '📎 Active Invariants (.rune/INVARIANTS.md)';
287
+ const lines = [header];
288
+ let tokens = estimateTokens(header);
289
+ let shown = 0;
290
+
291
+ for (const rule of ordered) {
292
+ const icon = ICON[rule.section] ?? '•';
293
+ const wherePreview = rule.where.length > 0 ? rule.where.slice(0, 2).join(', ') : rule.title;
294
+ const line = `${icon} ${wherePreview} — ${rule.what || rule.title}`;
295
+ const cost = estimateTokens(line);
296
+ if (tokens + cost > budgetTokens) break;
297
+ lines.push(line);
298
+ tokens += cost;
299
+ shown += 1;
300
+ }
301
+
302
+ const overflow = ordered.length - shown;
303
+ if (overflow > 0) {
304
+ lines.push(`…+${overflow} more rule${overflow === 1 ? '' : 's'} in .rune/INVARIANTS.md`);
305
+ }
306
+ return { preview: lines.join('\n'), overflow };
307
+ }
308
+
309
+ export async function loadInvariants({ root, budgetTokens = DEFAULT_BUDGET_TOKENS } = {}) {
310
+ if (!root) throw new Error('loadInvariants: root is required');
311
+
312
+ const invariantsPath = path.join(root, '.rune', 'INVARIANTS.md');
313
+ const empty = {
314
+ loaded: false,
315
+ count: 0,
316
+ path: invariantsPath,
317
+ stale: false,
318
+ stats: { danger: 0, critical: 0, state: 0, cross: 0, total: 0, archivedSkipped: false },
319
+ rules: [],
320
+ preview: '',
321
+ overflow: 0,
322
+ };
323
+ if (!existsSync(invariantsPath)) return empty;
324
+
325
+ let raw;
326
+ try {
327
+ raw = await readFile(invariantsPath, 'utf8');
328
+ } catch {
329
+ return empty;
330
+ }
331
+
332
+ const beforeArchive = stripArchived(raw);
333
+ const archivedSkipped = beforeArchive.length !== raw.length;
334
+ const rules = parseInvariants(beforeArchive);
335
+
336
+ const stats = {
337
+ danger: rules.filter((r) => r.section === 'danger').length,
338
+ critical: rules.filter((r) => r.section === 'critical').length,
339
+ state: rules.filter((r) => r.section === 'state').length,
340
+ cross: rules.filter((r) => r.section === 'cross').length,
341
+ total: rules.length,
342
+ archivedSkipped,
343
+ };
344
+
345
+ let stale = false;
346
+ try {
347
+ const st = await stat(invariantsPath);
348
+ const ageDays = (Date.now() - st.mtimeMs) / (1000 * 60 * 60 * 24);
349
+ stale = ageDays > STALE_AGE_DAYS;
350
+ } catch {
351
+ /* stat failure is non-fatal */
352
+ }
353
+
354
+ const { preview, overflow } = renderPreview(rules, { budgetTokens });
355
+
356
+ return {
357
+ loaded: rules.length > 0,
358
+ count: rules.length,
359
+ path: invariantsPath,
360
+ stale,
361
+ stats,
362
+ rules,
363
+ preview,
364
+ overflow,
365
+ };
366
+ }
367
+
368
+ async function main() {
369
+ const { values } = parseArgs({
370
+ options: {
371
+ root: { type: 'string' },
372
+ json: { type: 'boolean', default: false },
373
+ budget: { type: 'string', default: String(DEFAULT_BUDGET_TOKENS) },
374
+ },
375
+ });
376
+ const root = values.root ?? process.cwd();
377
+ const parsedBudget = Number.parseInt(values.budget, 10);
378
+ const budgetTokens = Number.isFinite(parsedBudget) && parsedBudget > 0 ? parsedBudget : DEFAULT_BUDGET_TOKENS;
379
+ const result = await loadInvariants({ root, budgetTokens });
380
+ if (values.json) {
381
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
382
+ return;
383
+ }
384
+ if (!result.loaded) {
385
+ process.stdout.write('No invariants loaded — .rune/INVARIANTS.md missing or empty.\n');
386
+ return;
387
+ }
388
+ process.stdout.write(`${result.preview}\n`);
389
+ if (result.stale) process.stdout.write('\n⚠ Invariants file is stale (>30 days). Run `rune onboard --refresh`.\n');
390
+ }
391
+
392
+ if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith('load-invariants.js')) {
393
+ main().catch((err) => {
394
+ process.stderr.write(`load-invariants: ${err.message}\n`);
395
+ process.exit(1);
396
+ });
397
+ }