@rune-kit/rune 2.28.0 → 2.29.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 (49) hide show
  1. package/.codex-plugin/plugin.json +14 -0
  2. package/README.md +72 -42
  3. package/compiler/__tests__/adapter-model-mapping.test.js +22 -24
  4. package/compiler/__tests__/adapters.test.js +4 -1
  5. package/compiler/__tests__/doctor-mesh.test.js +55 -1
  6. package/compiler/__tests__/governance-collector.test.js +1 -0
  7. package/compiler/__tests__/hooks-codex.test.js +85 -0
  8. package/compiler/__tests__/hooks-doctor-tier.test.js +3 -5
  9. package/compiler/__tests__/hooks-drift.test.js +2 -2
  10. package/compiler/__tests__/hooks-install.test.js +2 -2
  11. package/compiler/__tests__/hooks-tiers.test.js +3 -4
  12. package/compiler/__tests__/setup.test.js +59 -0
  13. package/compiler/__tests__/status.test.js +7 -1
  14. package/compiler/__tests__/tier-override.test.js +42 -1
  15. package/compiler/__tests__/transforms.test.js +15 -0
  16. package/compiler/adapters/codex.js +88 -11
  17. package/compiler/adapters/hooks/codex.js +178 -0
  18. package/compiler/adapters/hooks/index.js +10 -0
  19. package/compiler/adapters/openclaw.js +2 -2
  20. package/compiler/bin/rune.js +5 -4
  21. package/compiler/commands/hooks/install.js +2 -2
  22. package/compiler/commands/hooks/status.js +1 -1
  23. package/compiler/commands/setup.js +101 -28
  24. package/compiler/doctor.js +12 -6
  25. package/compiler/emitter.js +46 -3
  26. package/compiler/governance-collector.js +3 -2
  27. package/compiler/status.js +4 -7
  28. package/compiler/transforms/branding.js +2 -2
  29. package/compiler/transforms/subagents.js +3 -3
  30. package/hooks/codex-hooks.json +96 -0
  31. package/hooks/context-watch/index.cjs +8 -5
  32. package/hooks/intent-router/index.cjs +3 -0
  33. package/hooks/lib/hook-output.cjs +11 -3
  34. package/hooks/post-session-reflect/index.cjs +65 -24
  35. package/hooks/pre-compact/index.cjs +10 -4
  36. package/hooks/pre-tool-guard/index.cjs +38 -36
  37. package/hooks/run-hook +1 -1
  38. package/hooks/run-hook.cmd +1 -1
  39. package/hooks/secrets-scan/index.cjs +18 -2
  40. package/package.json +3 -2
  41. package/skills/browser-pilot/SKILL.md +1 -0
  42. package/skills/completion-gate/SKILL.md +5 -5
  43. package/skills/doc-processor/SKILL.md +2 -2
  44. package/skills/hallucination-guard/SKILL.md +1 -0
  45. package/skills/journal/SKILL.md +1 -0
  46. package/skills/retro/SKILL.md +2 -2
  47. package/skills/session-bridge/SKILL.md +1 -0
  48. package/skills/session-bridge/scripts/load-invariants.js +1 -1
  49. package/skills/video-creator/SKILL.md +1 -1
@@ -4,7 +4,7 @@
4
4
 
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
- const os = require('os');
7
+ const { stateFile } = require('../lib/context-key.cjs');
8
8
  const { captureConsole } = require('../lib/hook-output.cjs');
9
9
  // Hook stdout is a JSON contract, not free text (Codex rejects bare lines and
10
10
  // discards the output). Capture the prints below and emit one envelope on exit.
@@ -12,12 +12,17 @@ captureConsole('Stop');
12
12
 
13
13
 
14
14
  const cwd = process.cwd();
15
- const hash = Buffer.from(cwd).toString('base64url').slice(0, 16);
15
+ let sessionId;
16
+ try {
17
+ sessionId = JSON.parse(fs.readFileSync(0, 'utf-8')).session_id;
18
+ } catch {
19
+ // Older runtimes may not provide hook input.
20
+ }
16
21
 
17
22
  // === H3: Flush Session Metrics ===
18
23
 
19
- const metricsJsonl = path.join(os.tmpdir(), `rune-metrics-${hash}.jsonl`);
20
- const counterFile = path.join(os.tmpdir(), `rune-context-watch-${hash}.json`);
24
+ const metricsJsonl = stateFile('rune-metrics', sessionId, cwd).replace(/\.json$/, '.jsonl');
25
+ const counterFile = stateFile('rune-context-watch', sessionId, cwd);
21
26
  const runeMetricsDir = path.join(cwd, '.rune', 'metrics');
22
27
 
23
28
  // Resolve skill names → expected model from agent frontmatter
@@ -101,33 +106,69 @@ function flushMetrics() {
101
106
  const modelsUsed = resolveSkillModels(skillCounts);
102
107
 
103
108
  // Use session ID from context-watch (shared) or generate new
104
- const sessionId = watchState.sessionId
109
+ const metricSessionId = sessionId
110
+ || watchState.sessionId
105
111
  || `s-${now.slice(0, 10).replace(/-/g, '')}-${now.slice(11, 19).replace(/:/g, '')}`;
106
112
 
107
- // 1. Append to sessions.jsonl
113
+ // 1. Upsert sessions.jsonl. Stop can fire more than once in one session;
114
+ // appending every time inflated session counts and duration metrics.
115
+ let previousSession = null;
116
+ const sessionsFile = path.join(runeMetricsDir, 'sessions.jsonl');
117
+ let sessionEntries = [];
118
+ if (fs.existsSync(sessionsFile)) {
119
+ sessionEntries = fs
120
+ .readFileSync(sessionsFile, 'utf-8')
121
+ .trim()
122
+ .split('\n')
123
+ .filter(Boolean)
124
+ .flatMap((line) => {
125
+ try {
126
+ return [JSON.parse(line)];
127
+ } catch {
128
+ return [];
129
+ }
130
+ });
131
+ previousSession = [...sessionEntries].reverse().find((entry) => entry.id === metricSessionId) || null;
132
+ }
133
+
134
+ const mergedSkillCounts = { ...(previousSession?.skill_counts || {}) };
135
+ for (const [skill, count] of Object.entries(skillCounts)) {
136
+ mergedSkillCounts[skill] = (mergedSkillCounts[skill] || 0) + count;
137
+ }
138
+ const mergedSkillDurations = { ...(previousSession?.skill_durations || {}) };
139
+ for (const [skill, duration] of Object.entries(skillDurations)) {
140
+ mergedSkillDurations[skill] = (mergedSkillDurations[skill] || 0) + duration;
141
+ }
142
+ const mergedModels = { ...(previousSession?.models_used || {}) };
143
+ for (const [model, count] of Object.entries(modelsUsed)) {
144
+ mergedModels[model] = (mergedModels[model] || 0) + count;
145
+ }
146
+ const mergedSkills = Object.keys(mergedSkillCounts);
147
+ const mergedPrimarySkill = Object.entries(mergedSkillCounts)
148
+ .sort((a, b) => b[1] - a[1])[0]?.[0]
149
+ || previousSession?.primary_skill
150
+ || primarySkill;
151
+
108
152
  const sessionEntry = {
109
- id: sessionId,
153
+ id: metricSessionId,
110
154
  date: now.slice(0, 10),
111
155
  duration_min: durationMin,
112
156
  tool_calls: watchState.count,
113
157
  tool_distribution: watchState.toolCounts,
114
- skill_invocations: skillEvents.length,
115
- skills_used: Object.keys(skillCounts),
116
- primary_skill: primarySkill,
117
- models_used: modelsUsed,
118
- skill_durations: Object.keys(skillDurations).length > 0 ? skillDurations : undefined
158
+ skill_invocations: Object.values(mergedSkillCounts).reduce((sum, count) => sum + count, 0),
159
+ skills_used: mergedSkills,
160
+ primary_skill: mergedPrimarySkill,
161
+ models_used: mergedModels,
162
+ skill_counts: Object.keys(mergedSkillCounts).length > 0 ? mergedSkillCounts : undefined,
163
+ skill_durations: Object.keys(mergedSkillDurations).length > 0 ? mergedSkillDurations : undefined
119
164
  };
120
165
 
121
- const sessionsFile = path.join(runeMetricsDir, 'sessions.jsonl');
122
- fs.appendFileSync(sessionsFile, JSON.stringify(sessionEntry) + '\n');
123
-
124
- // Cap at 100 sessions (rotate oldest)
125
- try {
126
- const allLines = fs.readFileSync(sessionsFile, 'utf-8').trim().split('\n').filter(Boolean);
127
- if (allLines.length > 100) {
128
- fs.writeFileSync(sessionsFile, allLines.slice(-100).join('\n') + '\n');
129
- }
130
- } catch { /* cap is best-effort */ }
166
+ sessionEntries = sessionEntries.filter((entry) => entry.id !== metricSessionId);
167
+ sessionEntries.push(sessionEntry);
168
+ fs.writeFileSync(
169
+ sessionsFile,
170
+ `${sessionEntries.slice(-100).map((entry) => JSON.stringify(entry)).join('\n')}\n`,
171
+ );
131
172
 
132
173
  // 2. Merge into skills.json (running totals)
133
174
  const skillsFile = path.join(runeMetricsDir, 'skills.json');
@@ -153,7 +194,7 @@ function flushMetrics() {
153
194
  if (skillChain.length > 0) {
154
195
  const chainsFile = path.join(runeMetricsDir, 'chains.jsonl');
155
196
  const chainEntry = {
156
- session: sessionId,
197
+ session: metricSessionId,
157
198
  chain: skillChain,
158
199
  depth: skillChain.length
159
200
  };
@@ -170,7 +211,7 @@ function flushMetrics() {
170
211
  .map(([s, c]) => `${s}(${c})`)
171
212
  .join(', ');
172
213
 
173
- console.log(`\n📊 [Rune metrics] Session ${sessionId} — ${durationMin}min, ${watchState.count} tool calls, ${skillEvents.length} skill invocations`);
214
+ console.log(`\n📊 [Rune metrics] Session ${metricSessionId} — ${durationMin}min, ${watchState.count} tool calls, ${skillEvents.length} new skill invocations`);
174
215
  if (skillList) console.log(` Skills: ${skillList}`);
175
216
  console.log(` Saved to .rune/metrics/\n`);
176
217
  }
@@ -7,20 +7,26 @@
7
7
 
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
- const os = require('os');
10
+ const { stateFile } = require('../lib/context-key.cjs');
11
11
 
12
12
  const { captureConsole } = require('../lib/hook-output.cjs');
13
13
  // Hook stdout is a JSON contract, not free text (Codex rejects bare lines and
14
14
  // discards the output). Capture the prints below and emit one envelope on exit.
15
- captureConsole('PreCompact');
15
+ captureConsole('PreCompact', { captureError: true });
16
16
 
17
17
 
18
18
  const cwd = process.cwd();
19
19
  const runeDir = path.join(cwd, '.rune');
20
20
 
21
+ let sessionId;
22
+ try {
23
+ sessionId = JSON.parse(fs.readFileSync(0, 'utf-8')).session_id;
24
+ } catch {
25
+ // Older runtimes may not provide hook input.
26
+ }
27
+
21
28
  // Read context-watch state (tool counts, session timing)
22
- const hash = Buffer.from(cwd).toString('base64url').slice(0, 16);
23
- const counterFile = path.join(os.tmpdir(), `rune-context-watch-${hash}.json`);
29
+ const counterFile = stateFile('rune-context-watch', sessionId, cwd);
24
30
 
25
31
  let watchState = null;
26
32
  try {
@@ -66,14 +66,14 @@ function appendGateOutcome(gate, outcome, detail) {
66
66
  *
67
67
  * @param {string} toolName
68
68
  * @param {object} toolInput
69
- * @returns {string} target path, or '' when this is not a patch we understand
69
+ * @returns {string[]} target paths
70
70
  */
71
- function patchTargetPath(toolName, toolInput) {
72
- if (toolName !== 'apply_patch') return '';
73
- const patch = typeof toolInput === 'string' ? toolInput : toolInput.input || toolInput.patch || '';
74
- if (typeof patch !== 'string') return '';
75
- const match = patch.match(/^\*\*\* (?:Update|Add|Delete) File:\s*(.+)$/m);
76
- return match ? match[1].trim() : '';
71
+ function patchTargetPaths(toolName, toolInput) {
72
+ if (toolName !== 'apply_patch') return [];
73
+ const patch =
74
+ typeof toolInput === 'string' ? toolInput : toolInput.command || toolInput.input || toolInput.patch || '';
75
+ if (typeof patch !== 'string') return [];
76
+ return Array.from(patch.matchAll(/^\*\*\* (?:Update|Add|Delete) File:\s*(.+)$/gm), (match) => match[1].trim());
77
77
  }
78
78
 
79
79
  // Read tool_input from Claude Code hook stdin
@@ -92,11 +92,9 @@ process.stdin.on('end', () => {
92
92
  process.exit(0);
93
93
  }
94
94
 
95
- const filePath = toolInput.file_path || toolInput.path || patchTargetPath(toolName, toolInput);
96
- if (!filePath) process.exit(0);
97
-
98
- const basename = path.basename(filePath);
99
- const normalized = filePath.replace(/\\/g, '/');
95
+ const directPath = toolInput.file_path || toolInput.path;
96
+ const filePaths = directPath ? [directPath] : patchTargetPaths(toolName, toolInput);
97
+ if (filePaths.length === 0) process.exit(0);
100
98
 
101
99
  // Load project-specific privacy config
102
100
  const privacyConfig = loadPrivacyConfig();
@@ -147,33 +145,37 @@ process.stdin.on('end', () => {
147
145
  const activeSkill = process.env.RUNE_ACTIVE_SKILL || '';
148
146
  const isElevated = elevatedSkills.has(activeSkill);
149
147
 
150
- const isSafe = safeExceptions.some((p) => p.test(basename) || p.test(normalized));
151
- if (isSafe) process.exit(0);
152
-
153
- const isBlocked = blockPatterns.some((p) => p.test(basename) || p.test(normalized));
154
- if (isBlocked) {
155
- // Append block outcome BEFORE printing/exiting — fail-safe, never affects exit code.
156
- // Only "blocked" is captured here; "passed" and "bypassed" are not observable
157
- // at this layer and intentionally remain uncaptured (see GAP-1 in governance-collector.js).
158
- appendGateOutcome('privacy-mesh', 'blocked', `file matched BLOCK-tier pattern: ${basename}`);
159
-
160
- console.log(`\n🚫 [Rune privacy-mesh] BLOCKED: ${filePath}`);
161
- console.log(' This file matches a BLOCK-tier pattern (private keys, certificates).');
162
- console.log(' Override: add path to .rune/privacy.json "allow" list if intentional.\n');
163
- process.exit(2); // Exit code 2 = BLOCK
164
- }
148
+ for (const filePath of filePaths) {
149
+ const basename = path.basename(filePath);
150
+ const normalized = filePath.replace(/\\/g, '/');
151
+ const isSafe = safeExceptions.some((p) => p.test(basename) || p.test(normalized));
152
+ if (isSafe) continue;
153
+
154
+ const isBlocked = blockPatterns.some((p) => p.test(basename) || p.test(normalized));
155
+ if (isBlocked) {
156
+ // Append block outcome BEFORE printing/exiting — fail-safe, never affects exit code.
157
+ // Only "blocked" is captured here; "passed" and "bypassed" are not observable
158
+ // at this layer and intentionally remain uncaptured (see GAP-1 in governance-collector.js).
159
+ appendGateOutcome('privacy-mesh', 'blocked', `file matched BLOCK-tier pattern: ${basename}`);
160
+
161
+ console.log(`\n🚫 [Rune privacy-mesh] BLOCKED: ${filePath}`);
162
+ console.log(' This file matches a BLOCK-tier pattern (private keys, certificates).');
163
+ console.log(' Override: add path to .rune/privacy.json "allow" list if intentional.\n');
164
+ process.exit(2); // Exit code 2 = BLOCK
165
+ }
165
166
 
166
- const isWarned = warnPatterns.some((p) => p.test(basename) || p.test(normalized));
167
- if (isWarned && !isElevated) {
168
- // Content-aware check: scan first 4KB for secret patterns
169
- const contentWarning = scanContentForSecrets(filePath);
167
+ const isWarned = warnPatterns.some((p) => p.test(basename) || p.test(normalized));
168
+ if (isWarned && !isElevated) {
169
+ // Content-aware check: scan first 4KB for secret patterns
170
+ const contentWarning = scanContentForSecrets(filePath);
170
171
 
171
- console.log(`\n⚠ [Rune privacy-mesh] Sensitive file: ${filePath}`);
172
- if (contentWarning) {
173
- console.log(` Content scan: ${contentWarning}`);
172
+ console.log(`\n⚠ [Rune privacy-mesh] Sensitive file: ${filePath}`);
173
+ if (contentWarning) {
174
+ console.log(` Content scan: ${contentWarning}`);
175
+ }
176
+ console.log(' This file may contain secrets. Confirm access is intentional.');
177
+ console.log(' Elevated skills (sentinel, review, audit) bypass this warning.\n');
174
178
  }
175
- console.log(' This file may contain secrets. Confirm access is intentional.');
176
- console.log(' Elevated skills (sentinel, review, audit) bypass this warning.\n');
177
179
  }
178
180
 
179
181
  process.exit(0);
package/hooks/run-hook CHANGED
@@ -8,7 +8,7 @@ if (!hookName) {
8
8
  process.exit(1);
9
9
  }
10
10
 
11
- const hookPath = path.join(__dirname, hookName, 'index.js');
11
+ const hookPath = path.join(__dirname, hookName, 'index.cjs');
12
12
  try {
13
13
  require(hookPath);
14
14
  } catch (e) {
@@ -1 +1 @@
1
- @node "%~dp0run-hook.js" %*
1
+ @node "%~dp0run-hook.cjs" %*
@@ -6,10 +6,26 @@
6
6
  // Zero false-positive tolerance on BLOCK patterns.
7
7
 
8
8
  const { execSync } = require('child_process');
9
+ const fs = require('fs');
10
+ const { captureConsole } = require('../lib/hook-output.cjs');
11
+
12
+ captureConsole('PreToolUse', { captureError: true });
9
13
 
10
14
  // Only intercept git commit commands
11
- const input = JSON.parse(process.env.CLAUDE_TOOL_INPUT || '{}');
12
- const command = (input.command || '').trim();
15
+ let rawInput = '';
16
+ try {
17
+ rawInput = fs.readFileSync(0, 'utf-8');
18
+ } catch {
19
+ // Claude compatibility fallback below.
20
+ }
21
+ let event = {};
22
+ try {
23
+ event = JSON.parse(rawInput.trim() || process.env.CLAUDE_TOOL_INPUT || '{}');
24
+ } catch {
25
+ process.exit(0);
26
+ }
27
+ const toolInput = event.tool_input || event;
28
+ const command = (toolInput.command || '').trim();
13
29
 
14
30
  // Check if this is a git commit command
15
31
  if (!/^git\s+commit\b/.test(command)) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rune-kit/rune",
3
- "version": "2.28.0",
4
- "description": "65-skill mesh for AI coding assistants — runtime auto-discipline via native hooks (Claude/Cursor/Windsurf/Antigravity), 5-layer architecture, 204 connections + 43 signals, multi-platform compiler. converge (L3) scans spec vs code so dead-button/frontend-only implementations can't ship.",
3
+ "version": "2.29.0",
4
+ "description": "66-skill mesh for AI coding assistants — native lifecycle hooks for Claude Code and Codex, 5-layer architecture, 248 connections + 45 signals, and a 13-platform compiler.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "rune": "./compiler/bin/rune.js"
@@ -52,6 +52,7 @@
52
52
  "commands/",
53
53
  "agents/",
54
54
  "hooks/",
55
+ ".codex-plugin/",
55
56
  "references/"
56
57
  ],
57
58
  "homepage": "https://rune-kit.github.io/rune",
@@ -25,6 +25,7 @@ Browser automation for testing and verification using MCP Playwright tools. Navi
25
25
  - `launch` (L1): verify live site after deployment
26
26
  - `perf` (L2): Lighthouse / Core Web Vitals measurement
27
27
  - `audit` (L2): visual verification during quality assessment
28
+ - `design` (L2): render the surface before any visual property is claimed (design Step 5.4 — render blindness)
28
29
 
29
30
  ## Calls (outbound)
30
31
 
@@ -148,11 +148,11 @@ For each claim, look for corresponding evidence in the conversation context:
148
148
 
149
149
  | Claim Type | Required Evidence | Where to Find |
150
150
  |---|---|---|
151
- | "tests pass" | Test runner stdout with pass count | Bash output from test command |
152
- | "build succeeds" | Build command stdout showing success | Bash output from build command |
153
- | "lint clean" | Linter stdout (even if empty = 0 errors) | Bash output from lint command |
154
- | "fixed" | Git diff showing the change + test proving fix | Edit/Write tool calls + test output |
155
- | "implemented" | Files created/modified matching the plan | Write/Edit tool calls vs plan |
151
+ | "tests pass" | Test runner stdout with pass count | Shell output from test command |
152
+ | "build succeeds" | Build command stdout showing success | Shell output from build command |
153
+ | "lint clean" | Linter stdout (even if empty = 0 errors) | Shell output from lint command |
154
+ | "fixed" | Git diff showing the change + test proving fix | File-edit evidence + test output |
155
+ | "implemented" | Files created/modified matching the plan | File changes compared with the plan |
156
156
  | "no security issues" | Sentinel report with PASS verdict | Sentinel skill output |
157
157
  | "coverage ≥ X%" | Coverage tool output with actual percentage | Test runner with coverage flag |
158
158
 
@@ -41,7 +41,7 @@ None — pure L3 utility. Receives content, produces formatted output.
41
41
 
42
42
  | Format | Generate | Parse | Node.js Library | Python Library |
43
43
  |--------|----------|-------|-----------------|----------------|
44
- | PDF | Yes | Yes (via Read tool) | jsPDF, Puppeteer (HTML→PDF) | reportlab, weasyprint |
44
+ | PDF | Yes | Yes (via the runtime's document/PDF reader) | jsPDF, Puppeteer (HTML→PDF) | reportlab, weasyprint |
45
45
  | DOCX | Yes | Yes | docx (officegen) | python-docx |
46
46
  | XLSX | Yes | Yes | ExcelJS | openpyxl |
47
47
  | PPTX | Yes | Yes | pptxgenjs | python-pptx |
@@ -157,7 +157,7 @@ Identify file format from extension and MIME type.
157
157
 
158
158
  | Format | Extraction Strategy |
159
159
  |--------|-------------------|
160
- | PDF | Use Read tool (Claude can read PDFs natively) |
160
+ | PDF | Use the runtime's native document/PDF reader when available; otherwise use a supported PDF parsing library |
161
161
  | DOCX | docx library → extract text, tables, images |
162
162
  | XLSX | ExcelJS → extract sheets, rows, formulas |
163
163
  | PPTX | pptxgenjs → extract slides, text, notes |
@@ -39,6 +39,7 @@ Post-generation validation that verifies AI-generated code references actually e
39
39
  - `review-intake` (L2): verify imports in code submitted for review
40
40
  - `skill-forge` (L2): verify imports in newly generated skill code
41
41
  - `adversary` (L2): verify APIs/packages in plan actually exist
42
+ - `logic-guardian` (L2): verify referenced functions and imports after guarded logic edits
42
43
 
43
44
  ## Execution
44
45
 
@@ -39,6 +39,7 @@ None — pure L3 state management utility.
39
39
  - `graft` (L2): auto-log graft operations — source URL, mode, challenge score, files changed
40
40
  - `retro` (L2): record retrospective insights and decisions
41
41
  - `improve-architecture` (L2): record an ADR when the user rejects a deepening candidate with a load-bearing reason
42
+ - `logic-guardian` (L2): record guarded logic changes for cross-session persistence
42
43
 
43
44
  ## Files Managed
44
45
 
@@ -316,8 +316,8 @@ Pull from all installed domain packs:
316
316
  - **Engineering**: git history (commits, velocity, test ratio, fix ratio, hotspots)
317
317
  - **Revenue** (@rune-pro/sales): pipeline metrics, deal velocity, churn risk
318
318
  - **Support** (@rune-pro/support): ticket volume, SLA compliance, CSAT
319
- - **Finance** (@rune-business/finance): burn rate, runway, budget variance
320
- - **Compliance** (@rune-business/legal): framework status, audit dates, open items
319
+ - **Finance** (@rune-pro/finance): burn rate, runway, budget variance
320
+ - **Compliance** (@rune-pro/legal): framework status, audit dates, open items
321
321
 
322
322
  ### Business Execution Steps
323
323
 
@@ -41,6 +41,7 @@ Solve the #1 developer complaint: context loss across sessions. Session-bridge a
41
41
  - `context-pack` (L3): coordinate state for sub-agent handoff
42
42
  - `neural-memory` (L3): sync key decisions back to `.rune/` files after Capture Mode
43
43
  - `adversary` (L2): (oracle-mode) detach protocol when target model is opus-class for non-blocking dispatch
44
+ - `logic-guardian` (L2): preserve logic-manifest state across sessions
44
45
 
45
46
  ## State Files Managed
46
47
 
@@ -132,7 +132,7 @@ export function parseInvariants(text) {
132
132
  let current = null;
133
133
 
134
134
  const flush = () => {
135
- if (current && current.title) rules.push(current);
135
+ if (current?.title) rules.push(current);
136
136
  current = null;
137
137
  };
138
138
 
@@ -265,7 +265,7 @@ Known failure modes for this skill. Check these before declaring done.
265
265
  - Storyboard created scene-by-scene with transitions
266
266
  - Shot list categorized by type (screen recording, terminal, code, diagram)
267
267
  - Assets needed checklist generated
268
- - video-plan.md written to output_path via Write tool
268
+ - `video-plan.md` written to `output_path` with the runtime's file-edit capability
269
269
  - Video Plan Created report emitted with scene count, shot count, and asset count
270
270
 
271
271
  ## Cost Profile