@reactive-skills/runtime 0.1.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.
@@ -0,0 +1,103 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+ import vm from 'node:vm';
4
+ import { pathToFileURL } from 'node:url';
5
+ const ALLOWED_GUARD_EXTENSIONS = ['.js', '.mjs', '.cjs'];
6
+ /**
7
+ * Guard Evaluator: Safely checks transition guards and domain invariants inside an isolated sandbox
8
+ */
9
+ export class GuardEvaluator {
10
+ /**
11
+ * Evaluate a transition guard expression or custom JS function file
12
+ */
13
+ static async evaluate(guardExpr, guardFunctionPath, evalContext) {
14
+ // If no guard defined, it unconditionally passes
15
+ if (!guardExpr && !guardFunctionPath) {
16
+ return { passed: true };
17
+ }
18
+ try {
19
+ // 1. Evaluate custom guard function file if specified (SEC-01)
20
+ if (guardFunctionPath && evalContext.skillDir) {
21
+ const ext = path.extname(guardFunctionPath).toLowerCase();
22
+ if (!ALLOWED_GUARD_EXTENSIONS.includes(ext)) {
23
+ return {
24
+ passed: false,
25
+ error: `Guard function must be a JavaScript file (.js, .mjs, .cjs): ${guardFunctionPath}`,
26
+ };
27
+ }
28
+ const skillRoot = path.resolve(evalContext.skillDir);
29
+ const fullPath = path.resolve(skillRoot, guardFunctionPath);
30
+ const relativePath = path.relative(skillRoot, fullPath);
31
+ if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
32
+ return {
33
+ passed: false,
34
+ error: `Guard function path escapes skill directory: ${guardFunctionPath}`,
35
+ };
36
+ }
37
+ if (fs.existsSync(fullPath)) {
38
+ const realSkillRoot = fs.existsSync(skillRoot) ? fs.realpathSync(skillRoot) : skillRoot;
39
+ const realFullPath = fs.realpathSync(fullPath);
40
+ const realRel = path.relative(realSkillRoot, realFullPath);
41
+ if (realRel.startsWith('..') || path.isAbsolute(realRel)) {
42
+ return {
43
+ passed: false,
44
+ error: `Guard function path escapes skill directory: ${guardFunctionPath}`,
45
+ };
46
+ }
47
+ const fileUrl = pathToFileURL(realFullPath).href;
48
+ const module = await import(fileUrl);
49
+ const fn = module.default || module.guard || module.check;
50
+ if (typeof fn === 'function') {
51
+ const result = await fn(evalContext);
52
+ return { passed: Boolean(result) };
53
+ }
54
+ return {
55
+ passed: false,
56
+ error: `Guard function file does not export a valid function (default, guard, check): ${guardFunctionPath}`,
57
+ };
58
+ }
59
+ else {
60
+ return {
61
+ passed: false,
62
+ error: `Guard function file not found: ${guardFunctionPath}`,
63
+ };
64
+ }
65
+ }
66
+ // 2. Evaluate inline expression using hardened node:vm sandbox
67
+ if (guardExpr) {
68
+ // Deep clone / sanitize sandbox variables to prevent prototype pollution or escape
69
+ const sandbox = {
70
+ event: JSON.parse(JSON.stringify(evalContext.event)),
71
+ context: JSON.parse(JSON.stringify(evalContext.context || {})),
72
+ currentState: String(evalContext.currentState),
73
+ state: String(evalContext.currentState),
74
+ payload: JSON.parse(JSON.stringify(evalContext.event.payload || {})),
75
+ Boolean,
76
+ Number,
77
+ String,
78
+ Array,
79
+ Object,
80
+ Math,
81
+ JSON,
82
+ };
83
+ const vmContext = vm.createContext(sandbox, {
84
+ codeGeneration: {
85
+ strings: false, // Disable eval / new Function inside guard
86
+ wasm: false,
87
+ },
88
+ });
89
+ // Wrap expression safely with 100ms timeout
90
+ const script = new vm.Script(`"use strict"; Boolean(${guardExpr})`);
91
+ const result = script.runInContext(vmContext, { timeout: 100 });
92
+ return { passed: Boolean(result) };
93
+ }
94
+ return { passed: true };
95
+ }
96
+ catch (err) {
97
+ return {
98
+ passed: false,
99
+ error: `Guard evaluation failed: ${err.message}`,
100
+ };
101
+ }
102
+ }
103
+ }
@@ -0,0 +1,27 @@
1
+ import { SkillManifest } from './types.js';
2
+ export interface LegacySkillMetadata {
3
+ name: string;
4
+ description: string;
5
+ rawMarkdown: string;
6
+ frontmatter?: Record<string, any>;
7
+ }
8
+ /**
9
+ * Legacy Skill Adapter
10
+ * Enables backward compatibility with existing static SKILL.md files.
11
+ * Automatically wraps plain markdown skills into reactive state machines or upgrades them.
12
+ */
13
+ export declare class LegacySkillAdapter {
14
+ /**
15
+ * Parse frontmatter and markdown body from SKILL.md
16
+ */
17
+ static parseSkillMd(skillMdPath: string): LegacySkillMetadata;
18
+ /**
19
+ * Wraps a legacy SKILL.md in-memory into a 3-stage reactive state machine
20
+ * (DISCOVERY -> EXECUTION -> VERIFICATION -> COMPLETED)
21
+ */
22
+ static wrapAsReactiveManifest(skillMdPath: string): SkillManifest;
23
+ /**
24
+ * Upgrades a legacy SKILL.md directory into a full modular Reactive Skill package
25
+ */
26
+ static upgradeToModular(skillDir: string, outDir?: string): string;
27
+ }
@@ -0,0 +1,126 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import yaml from 'js-yaml';
4
+ /**
5
+ * Legacy Skill Adapter
6
+ * Enables backward compatibility with existing static SKILL.md files.
7
+ * Automatically wraps plain markdown skills into reactive state machines or upgrades them.
8
+ */
9
+ export class LegacySkillAdapter {
10
+ /**
11
+ * Parse frontmatter and markdown body from SKILL.md
12
+ */
13
+ static parseSkillMd(skillMdPath) {
14
+ if (!fs.existsSync(skillMdPath)) {
15
+ throw new Error(`SKILL.md not found at ${skillMdPath}`);
16
+ }
17
+ const content = fs.readFileSync(skillMdPath, 'utf8');
18
+ const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
19
+ let frontmatter = {};
20
+ let rawMarkdown = content;
21
+ if (frontmatterMatch) {
22
+ try {
23
+ frontmatter = yaml.load(frontmatterMatch[1]) || {};
24
+ rawMarkdown = frontmatterMatch[2];
25
+ }
26
+ catch {
27
+ // Fallback if frontmatter is plain text
28
+ }
29
+ }
30
+ const name = frontmatter.name || path.basename(path.dirname(skillMdPath));
31
+ const description = frontmatter.description || `Skill for ${name}`;
32
+ return {
33
+ name,
34
+ description,
35
+ rawMarkdown,
36
+ frontmatter,
37
+ };
38
+ }
39
+ /**
40
+ * Wraps a legacy SKILL.md in-memory into a 3-stage reactive state machine
41
+ * (DISCOVERY -> EXECUTION -> VERIFICATION -> COMPLETED)
42
+ */
43
+ static wrapAsReactiveManifest(skillMdPath) {
44
+ const meta = this.parseSkillMd(skillMdPath);
45
+ return {
46
+ schema_version: 'reactive/v1',
47
+ name: meta.name,
48
+ description: meta.description,
49
+ initial_state: 'EXECUTION',
50
+ states: {
51
+ DISCOVERY: {
52
+ description: 'Analyze requirements and verify workspace prerequisites',
53
+ prompt_template: 'states/01_discovery.md',
54
+ tools: ['view_file', 'grep_search', 'find_by_name'],
55
+ transitions: {
56
+ READY_TO_EXECUTE: {
57
+ target: 'EXECUTION',
58
+ },
59
+ },
60
+ },
61
+ EXECUTION: {
62
+ description: `Execute instructions according to ${meta.name} skill`,
63
+ prompt_template: 'SKILL.md', // Direct reference to original markdown body
64
+ transitions: {
65
+ TASK_COMPLETED: {
66
+ target: 'VERIFICATION',
67
+ },
68
+ TEST_RAN: {
69
+ target: 'VERIFICATION',
70
+ guard: 'event.payload.exit_code === 0',
71
+ },
72
+ },
73
+ },
74
+ VERIFICATION: {
75
+ description: 'Validate quality gates, linting, or tests',
76
+ prompt_template: 'states/03_verification.md',
77
+ transitions: {
78
+ VERIFIED: {
79
+ target: 'COMPLETED',
80
+ },
81
+ REGRESSION_FOUND: {
82
+ target: 'EXECUTION',
83
+ },
84
+ },
85
+ },
86
+ COMPLETED: {
87
+ description: 'Skill workflow finalized',
88
+ prompt_template: 'states/04_completed.md',
89
+ },
90
+ },
91
+ deliverable_projections: [
92
+ {
93
+ template: 'templates/summary.md.hbs',
94
+ output: `.docs/${meta.name}-execution-summary.md`,
95
+ },
96
+ ],
97
+ };
98
+ }
99
+ /**
100
+ * Upgrades a legacy SKILL.md directory into a full modular Reactive Skill package
101
+ */
102
+ static upgradeToModular(skillDir, outDir) {
103
+ const skillMdPath = path.join(skillDir, 'SKILL.md');
104
+ const meta = this.parseSkillMd(skillMdPath);
105
+ const targetDir = outDir ? path.resolve(outDir) : skillDir;
106
+ const statesDir = path.join(targetDir, 'states');
107
+ const templatesDir = path.join(targetDir, 'templates');
108
+ const guardsDir = path.join(targetDir, 'guards');
109
+ fs.mkdirSync(statesDir, { recursive: true });
110
+ fs.mkdirSync(templatesDir, { recursive: true });
111
+ fs.mkdirSync(guardsDir, { recursive: true });
112
+ // 1. Write skill.yaml
113
+ const manifest = this.wrapAsReactiveManifest(skillMdPath);
114
+ // Point EXECUTION prompt to the modular state file
115
+ manifest.states.EXECUTION.prompt_template = 'states/02_execution.md';
116
+ fs.writeFileSync(path.join(targetDir, 'skill.yaml'), yaml.dump(manifest), 'utf8');
117
+ // 2. Write state slices
118
+ fs.writeFileSync(path.join(statesDir, '01_discovery.md'), `# Discovery Phase: ${meta.name}\n\nReview requirements and inspect existing codebase context before making changes.`, 'utf8');
119
+ fs.writeFileSync(path.join(statesDir, '02_execution.md'), `# Execution Phase: ${meta.name}\n\n${meta.rawMarkdown}`, 'utf8');
120
+ fs.writeFileSync(path.join(statesDir, '03_verification.md'), `# Verification Phase: ${meta.name}\n\nRun verification suites and ensure zero regressions before completing.`, 'utf8');
121
+ fs.writeFileSync(path.join(statesDir, '04_completed.md'), `# Completed: ${meta.name}\n\nExecution finished. Deliverables projected.`, 'utf8');
122
+ // 3. Write summary projection template
123
+ fs.writeFileSync(path.join(templatesDir, 'summary.md.hbs'), `# {{skillName}} Execution Audit\n\n- State: **{{currentState}}**\n- Synchronized: {{lastUpdated}}\n\n## State Transitions\n{{#each transitions}}\n- {{timestamp}}: \`{{from}}\` -> \`{{to}}\` (via {{signal}})\n{{/each}}\n`, 'utf8');
124
+ return targetDir;
125
+ }
126
+ }
@@ -0,0 +1,18 @@
1
+ export interface MigrationResult {
2
+ migrated: boolean;
3
+ projectDir: string;
4
+ filesUpdated: string[];
5
+ schemaVersion: string;
6
+ notes: string[];
7
+ }
8
+ /**
9
+ * Retroactive Migration Engine for Reactive Projects and Skills
10
+ * Upgrades existing projects and skills to latest standards (bootloader, INIT states, event store).
11
+ */
12
+ export declare class ProjectMigrator {
13
+ static migrate(targetDir: string): MigrationResult;
14
+ /**
15
+ * Migrate a single skill package: inject bootloader in SKILL.md, INIT state in skill.yaml, and init.md / setup_mcp.md.
16
+ */
17
+ static migrateSkill(skillDir: string): MigrationResult;
18
+ }
@@ -0,0 +1,256 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import yaml from 'js-yaml';
4
+ /**
5
+ * Retroactive Migration Engine for Reactive Projects and Skills
6
+ * Upgrades existing projects and skills to latest standards (bootloader, INIT states, event store).
7
+ */
8
+ export class ProjectMigrator {
9
+ static migrate(targetDir) {
10
+ const absDir = path.resolve(targetDir);
11
+ const filesUpdated = [];
12
+ const notes = [];
13
+ if (!fs.existsSync(absDir)) {
14
+ throw new Error(`Target project directory does not exist: ${absDir}`);
15
+ }
16
+ const docsDir = path.join(absDir, '.docs', 'synthesis');
17
+ if (!fs.existsSync(docsDir)) {
18
+ fs.mkdirSync(docsDir, { recursive: true });
19
+ }
20
+ // 1. Generate GLOSSARY.md if missing
21
+ const glossaryPath = path.join(docsDir, 'GLOSSARY.md');
22
+ if (!fs.existsSync(glossaryPath)) {
23
+ const initialGlossary = `# Ubiquitous Domain Glossary\n\n> Authoritative definitions for core business concepts and domain vocabulary.\n\n| Term | Definition & Context |\n| :--- | :--- |\n| **Session** | An active execution or domain workflow lifecycle. |\n| **Decider** | Pure business logic function validating commands and emitting events with zero I/O. |\n| **Projection** | Materialized read-model view updated asynchronously from domain events. |\n| **Command** | Intent to mutate state, validated by pure deciders. |\n`;
24
+ fs.writeFileSync(glossaryPath, initialGlossary, 'utf8');
25
+ filesUpdated.push(glossaryPath);
26
+ notes.push('Generated missing GLOSSARY.md');
27
+ }
28
+ // 2. Generate PROGRESS.md if missing
29
+ const progressPath = path.join(docsDir, 'PROGRESS.md');
30
+ if (!fs.existsSync(progressPath)) {
31
+ const initialProgress = `# Project Progress & MVP Status Tracker\n\n- **Project Status:** Active\n- **Last Migrated:** ${new Date().toISOString()}\n\n## Slice Completion by Priority (Eisenhower Matrix)\n\n### 🚀 Q1 / P0 (MVP / Walking Skeleton Slices)\n- [x] **[State Change] Core Domain Slices:** Verified\n\n### 🛠️ Q2 / P1 (Core Quality & Secondary Views)\n- [ ] Queued for next iteration\n\n### ⚡ Q3 / P2 (Ops, Metrics & Enhancements)\n- [ ] Queued for next iteration\n`;
32
+ fs.writeFileSync(progressPath, initialProgress, 'utf8');
33
+ filesUpdated.push(progressPath);
34
+ notes.push('Generated missing PROGRESS.md');
35
+ }
36
+ // 3. Detect legacy workspace storage without mutating or guessing its skill ownership
37
+ const reactiveDir = path.join(absDir, '.reactive');
38
+ const legacyJsonlPath = path.join(reactiveDir, 'events.jsonl');
39
+ const legacySqlitePath = path.join(reactiveDir, 'events.db');
40
+ if (fs.existsSync(legacyJsonlPath) || fs.existsSync(legacySqlitePath)) {
41
+ notes.push('Legacy workspace event store detected and preserved; skill ownership was not inferred.');
42
+ notes.push('Create a skill-scoped store explicitly before resuming any skill run.');
43
+ }
44
+ // 4. Auto-migrate child skills if skills/ folder exists
45
+ const skillsFolder = path.join(absDir, 'skills');
46
+ if (fs.existsSync(skillsFolder)) {
47
+ const entries = fs.readdirSync(skillsFolder, { withFileTypes: true });
48
+ for (const entry of entries) {
49
+ if (entry.isDirectory() && !entry.name.startsWith('.')) {
50
+ const childDir = path.join(skillsFolder, entry.name);
51
+ if (fs.existsSync(path.join(childDir, 'skill.yaml'))) {
52
+ const skillResult = ProjectMigrator.migrateSkill(childDir);
53
+ if (skillResult.migrated) {
54
+ filesUpdated.push(...skillResult.filesUpdated);
55
+ notes.push(`Migrated skill '${entry.name}' with bootloader and INIT state`);
56
+ }
57
+ }
58
+ }
59
+ }
60
+ }
61
+ return {
62
+ migrated: true,
63
+ projectDir: absDir,
64
+ filesUpdated,
65
+ schemaVersion: 'reactive/v2',
66
+ notes,
67
+ };
68
+ }
69
+ /**
70
+ * Migrate a single skill package: inject bootloader in SKILL.md, INIT state in skill.yaml, and init.md / setup_mcp.md.
71
+ */
72
+ static migrateSkill(skillDir) {
73
+ const absDir = path.resolve(skillDir);
74
+ const filesUpdated = [];
75
+ const notes = [];
76
+ if (!fs.existsSync(absDir)) {
77
+ throw new Error(`Target skill directory does not exist: ${absDir}`);
78
+ }
79
+ const skillYamlPath = path.join(absDir, 'skill.yaml');
80
+ const skillMdPath = path.join(absDir, 'SKILL.md');
81
+ if (!fs.existsSync(skillYamlPath)) {
82
+ throw new Error(`skill.yaml not found in ${absDir}`);
83
+ }
84
+ const skillName = path.basename(absDir);
85
+ // 1. Update SKILL.md with bootloader if missing
86
+ if (fs.existsSync(skillMdPath)) {
87
+ const rawMd = fs.readFileSync(skillMdPath, 'utf8');
88
+ if (!rawMd.includes('REACTIVE BOOTLOADER')) {
89
+ const bootloaderBlock = `<!-- REACTIVE BOOTLOADER -->\n> **REACTIVE SKILL — STRICT RUNTIME EXECUTION**\n> This skill is an event-driven state machine. Manual execution without the runtime is FORBIDDEN.\n>\n> **PRIMARY EXECUTION (AXI CLI — Shell):**\n> 1. Run \`reactive-skills-axi state ${skillName}\` (or \`invoke ${skillName}\`) to read your current instructions.\n> 2. Complete the tasks described in the state prompt.\n> 3. Run \`reactive-skills-axi emit ${skillName} <signal>\` to advance to the next state.\n>\n> **ALTERNATIVE (MCP Mode):**\n> If the \`reactive_state\` MCP tool is present in your tool list, you may use \`reactive_state\` and \`reactive_emit_signal\`.\n>\n> **STRICT INVARIANT:**\n> Never manually author \`.docs/\` deliverables or guess next states. The runtime governs all transitions and projections.\n<!-- END REACTIVE BOOTLOADER -->\n\n`;
90
+ let updatedMd;
91
+ if (rawMd.startsWith('---')) {
92
+ const secondYamlMarker = rawMd.indexOf('---', 3);
93
+ if (secondYamlMarker !== -1) {
94
+ const frontmatter = rawMd.slice(0, secondYamlMarker + 3);
95
+ const rest = rawMd.slice(secondYamlMarker + 3).trimStart();
96
+ updatedMd = `${frontmatter}\n\n${bootloaderBlock}${rest}`;
97
+ }
98
+ else {
99
+ updatedMd = `${bootloaderBlock}${rawMd}`;
100
+ }
101
+ }
102
+ else {
103
+ updatedMd = `${bootloaderBlock}${rawMd}`;
104
+ }
105
+ fs.writeFileSync(skillMdPath, updatedMd, 'utf8');
106
+ filesUpdated.push(skillMdPath);
107
+ notes.push('Injected reactive runtime bootloader into SKILL.md');
108
+ }
109
+ }
110
+ else {
111
+ const bootloaderBlock = `<!-- REACTIVE BOOTLOADER -->\n> **REACTIVE SKILL — STRICT RUNTIME EXECUTION**\n> This skill is an event-driven state machine. Manual execution without the runtime is FORBIDDEN.\n>\n> **PRIMARY EXECUTION (AXI CLI — Shell):**\n> 1. Run \`reactive-skills-axi state ${skillName}\` (or \`invoke ${skillName}\`) to read your current instructions.\n> 2. Complete the tasks described in the state prompt.\n> 3. Run \`reactive-skills-axi emit ${skillName} <signal>\` to advance to the next state.\n>\n> **ALTERNATIVE (MCP Mode):**\n> If the \`reactive_state\` MCP tool is present in your tool list, you may use \`reactive_state\` and \`reactive_emit_signal\`.\n>\n> **STRICT INVARIANT:**\n> Never manually author \`.docs/\` deliverables or guess next states. The runtime governs all transitions and projections.\n<!-- END REACTIVE BOOTLOADER -->\n\n`;
112
+ const initialMd = `---\nname: ${skillName}\ndescription: Skill: ${skillName}\ntype: reactive\n---\n\n${bootloaderBlock}# ${skillName}\n\nSkill: ${skillName} governed by \`skill.yaml\`.\n`;
113
+ fs.writeFileSync(skillMdPath, initialMd, 'utf8');
114
+ filesUpdated.push(skillMdPath);
115
+ notes.push('Created SKILL.md with reactive runtime bootloader');
116
+ }
117
+ // 2. Update skill.yaml with INIT and SETUP_MCP states and bump schema to 2.1.0
118
+ const rawYaml = fs.readFileSync(skillYamlPath, 'utf8');
119
+ const manifest = yaml.load(rawYaml);
120
+ let yamlModified = false;
121
+ if (manifest && typeof manifest === 'object') {
122
+ manifest.states = manifest.states || {};
123
+ if (manifest.schema_version !== '2.1.0') {
124
+ manifest.schema_version = '2.1.0';
125
+ yamlModified = true;
126
+ }
127
+ if (!manifest.states.INIT || !manifest.states.SETUP_MCP) {
128
+ const previousInitial = manifest.initial_state || Object.keys(manifest.states)[0] || 'READY';
129
+ manifest.initial_state = 'INIT';
130
+ manifest.states = {
131
+ INIT: {
132
+ description: 'Bootloader: Verify reactive runtime environment',
133
+ prompt_template: 'states/init.md',
134
+ transitions: {
135
+ RUNTIME_READY: { target: previousInitial },
136
+ SETUP_REQUIRED: { target: 'SETUP_MCP' },
137
+ },
138
+ },
139
+ SETUP_MCP: {
140
+ description: 'Auto-configure harness MCP server',
141
+ prompt_template: 'states/setup_mcp.md',
142
+ tools: ['run_command'],
143
+ transitions: {
144
+ SETUP_COMPLETE: { target: previousInitial, guard: 'payload.exit_code == 0' },
145
+ SETUP_FAILED: { target: 'ERROR', guard: 'payload.exit_code != 0' },
146
+ },
147
+ },
148
+ ...manifest.states,
149
+ };
150
+ if (!manifest.states.ERROR) {
151
+ manifest.states.ERROR = {
152
+ description: 'Runtime setup failed',
153
+ };
154
+ }
155
+ if (!manifest.states.BYPASS_DETECTED) {
156
+ manifest.states.BYPASS_DETECTED = {
157
+ description: 'Bypass detected: Agent operated outside the signal contract',
158
+ prompt_template: 'states/bypass_detected.md',
159
+ };
160
+ }
161
+ yamlModified = true;
162
+ notes.push('Injected INIT and SETUP_MCP states into skill.yaml');
163
+ }
164
+ if (yamlModified) {
165
+ if (manifest.strict_execution !== true) {
166
+ manifest.strict_execution = true;
167
+ }
168
+ fs.writeFileSync(skillYamlPath, yaml.dump(manifest, { indent: 2 }), 'utf8');
169
+ filesUpdated.push(skillYamlPath);
170
+ }
171
+ else {
172
+ // INIT/SETUP_MCP already exist; still enforce strict_execution
173
+ if (manifest.strict_execution !== true) {
174
+ manifest.strict_execution = true;
175
+ fs.writeFileSync(skillYamlPath, yaml.dump(manifest, { indent: 2 }), 'utf8');
176
+ filesUpdated.push(skillYamlPath);
177
+ notes.push('Set strict_execution: true on existing manifest');
178
+ }
179
+ }
180
+ }
181
+ // 2.5 Generate skill-release.json if missing
182
+ const releaseJsonPath = path.join(absDir, 'skill-release.json');
183
+ if (!fs.existsSync(releaseJsonPath)) {
184
+ const releaseData = {
185
+ schemaVersion: 1,
186
+ skillId: skillName,
187
+ channel: 'stable',
188
+ version: manifest?.version || '2.1.0',
189
+ type: 'reactive',
190
+ reactiveSchemaVersion: '2.1.0',
191
+ };
192
+ fs.writeFileSync(releaseJsonPath, JSON.stringify(releaseData, null, 2) + '\n', 'utf8');
193
+ filesUpdated.push(releaseJsonPath);
194
+ notes.push('Created skill-release.json (v2.1.0)');
195
+ }
196
+ // 3. Ensure states/init.md and states/setup_mcp.md exist
197
+ const statesDir = path.join(absDir, 'states');
198
+ if (!fs.existsSync(statesDir)) {
199
+ fs.mkdirSync(statesDir, { recursive: true });
200
+ }
201
+ const initMdPath = path.join(statesDir, 'init.md');
202
+ if (!fs.existsSync(initMdPath)) {
203
+ const initContent = `---\nname: ${skillName}\ndescription: Bootloader - Verify reactive runtime\ntype: reactive\n---\n\n# ${skillName} - INIT\n\nVerify agent harness has access to reactive runtime.\n\n## Instructions\n1. Check if \`reactive_state\` MCP tool is available in active tool whitelist.\n2. If \`reactive_state\` tool is present, emit signal \`RUNTIME_READY\`.\n3. If \`reactive_state\` tool is absent, emit signal \`SETUP_REQUIRED\`.\n`;
204
+ fs.writeFileSync(initMdPath, initContent, 'utf8');
205
+ filesUpdated.push(initMdPath);
206
+ notes.push('Created states/init.md');
207
+ }
208
+ const setupMcpMdPath = path.join(statesDir, 'setup_mcp.md');
209
+ if (!fs.existsSync(setupMcpMdPath)) {
210
+ const setupMcpContent = `---\nname: ${skillName}\ndescription: Auto-configure harness MCP server\ntype: reactive\n---\n\n# ${skillName} - SETUP_MCP\n\nConfigure host harness with \`reactive-skills-axi\` MCP server.\n\n## Instructions\n1. Run shell command via \`run_command\`:\n \`npx -y reactive-skills-axi setup\`\n2. When command completes:\n - If exit code 0, emit signal \`SETUP_COMPLETE\` with payload \`{\"exit_code\": 0}\`.\n - If non-zero exit code, emit signal \`SETUP_FAILED\` with payload \`{\"exit_code\": 1}\`.\n`;
211
+ fs.writeFileSync(setupMcpMdPath, setupMcpContent, 'utf8');
212
+ filesUpdated.push(setupMcpMdPath);
213
+ notes.push('Created states/setup_mcp.md');
214
+ }
215
+ // Create BYPASS_DETECTED template if missing
216
+ const bypassDetectedMdPath = path.join(statesDir, 'bypass_detected.md');
217
+ if (!fs.existsSync(bypassDetectedMdPath)) {
218
+ const bypassContent = [
219
+ '---',
220
+ 'name: ' + skillName,
221
+ 'description: Bypass detected - Agent operated outside signal contract',
222
+ 'type: reactive',
223
+ '---',
224
+ '',
225
+ '# ' + skillName + ' - BYPASS_DETECTED',
226
+ '',
227
+ '**STRICT EXECUTION BYPASS DETECTED**',
228
+ '',
229
+ 'The runtime detected that the agent operated outside the signal contract:',
230
+ '- Fetched state without emitting a signal within the allowed turn budget.',
231
+ '- Used tools not in the allowed_tools list (interceptor mode).',
232
+ '- Attempted to read skill files directly instead of going through the runtime.',
233
+ '',
234
+ '## Recovery',
235
+ '1. Run: `reactive-skills-axi reset ' + skillName + '`',
236
+ '2. Then: `reactive-skills-axi invoke ' + skillName + '`',
237
+ '3. Or re-invoke the skill through the MCP server.',
238
+ '',
239
+ '## Prevention',
240
+ '- Use ONLY `reactive_state` to load your TODO card.',
241
+ '- After each turn, emit a signal via `reactive_emit_signal`.',
242
+ '- Do NOT read skill files directly.',
243
+ ].join('\n') + '\n';
244
+ fs.writeFileSync(bypassDetectedMdPath, bypassContent, 'utf8');
245
+ filesUpdated.push(bypassDetectedMdPath);
246
+ notes.push('Created states/bypass_detected.md');
247
+ }
248
+ return {
249
+ migrated: filesUpdated.length > 0,
250
+ projectDir: absDir,
251
+ filesUpdated,
252
+ schemaVersion: 'reactive/v2.0.0',
253
+ notes,
254
+ };
255
+ }
256
+ }
@@ -0,0 +1,43 @@
1
+ import { DeliverableProjection, SignalEvent } from './types.js';
2
+ import { EventStore } from './event-store.js';
3
+ export interface ProjectionContext {
4
+ skillName: string;
5
+ currentState: string;
6
+ context: Record<string, any>;
7
+ events: SignalEvent[];
8
+ transitions: {
9
+ from: string;
10
+ to: string;
11
+ timestamp: string;
12
+ signal: string;
13
+ }[];
14
+ lastUpdated: string;
15
+ }
16
+ export interface ProjectionResult {
17
+ writtenFiles: string[];
18
+ errors: Array<{
19
+ template: string;
20
+ error: string;
21
+ }>;
22
+ }
23
+ /**
24
+ * Deliverable Projection Engine (Event-Sourced Read Models)
25
+ * Synthesizes persistent deliverables continuously from the event log.
26
+ */
27
+ export declare class ProjectionEngine {
28
+ private skillDir;
29
+ private workspaceDir;
30
+ private projections;
31
+ private compiledTemplates;
32
+ private eventCaches;
33
+ constructor(skillDir: string, projections?: DeliverableProjection[], workspaceDir?: string);
34
+ private resolveOutputPath;
35
+ private getProjectionEvents;
36
+ private matchesTrigger;
37
+ private registerHelpers;
38
+ private compileTemplates;
39
+ /**
40
+ * Render all deliverables matching the given event trigger safely with an error boundary
41
+ */
42
+ project(eventStore: EventStore, currentState: string, skillName: string, context: Record<string, any>, triggerSignal?: string): string[];
43
+ }