@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.
- package/LICENSE +21 -0
- package/README.md +71 -0
- package/dist/cli/dev.d.ts +2 -0
- package/dist/cli/dev.js +114 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +215 -0
- package/dist/core/event-store.d.ts +182 -0
- package/dist/core/event-store.js +762 -0
- package/dist/core/fsm-engine.d.ts +94 -0
- package/dist/core/fsm-engine.js +648 -0
- package/dist/core/guard-evaluator.d.ts +19 -0
- package/dist/core/guard-evaluator.js +103 -0
- package/dist/core/legacy-adapter.d.ts +27 -0
- package/dist/core/legacy-adapter.js +126 -0
- package/dist/core/migration.d.ts +18 -0
- package/dist/core/migration.js +256 -0
- package/dist/core/projection-engine.d.ts +43 -0
- package/dist/core/projection-engine.js +167 -0
- package/dist/core/runtime-hooks.d.ts +51 -0
- package/dist/core/runtime-hooks.js +195 -0
- package/dist/core/types.d.ts +238 -0
- package/dist/core/types.js +55 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.js +453 -0
- package/dist/sync/cli.d.ts +1 -0
- package/dist/sync/cli.js +186 -0
- package/dist/sync/engine.d.ts +2 -0
- package/dist/sync/engine.js +366 -0
- package/dist/sync/types.d.ts +35 -0
- package/dist/sync/types.js +1 -0
- package/package.json +75 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Reactive Skills Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# @reactive-skills/runtime
|
|
2
|
+
|
|
3
|
+
Reactive Skills Architecture (RSA) core runtime - FSM engine, event store, guard evaluator, projection engine, and MCP server.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @reactive-skills/runtime
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { FSMEngine, EventStore, LegacySkillAdapter, ProjectionEngine } from '@reactive-skills/runtime';
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Core Modules
|
|
18
|
+
|
|
19
|
+
- `FSMEngine` - Hierarchical State Machine loader, state path resolver, and signal-driven transition engine
|
|
20
|
+
- `EventStore` - Immutable append-only event ledger (JSONL + SQLite)
|
|
21
|
+
- `GuardEvaluator` - Sandbox evaluator for transition guard expressions
|
|
22
|
+
- `ProjectionEngine` - Handlebars deliverable generator for read-model projections
|
|
23
|
+
- `LegacySkillAdapter` - Backward compatibility wrapper and converter for SKILL.md
|
|
24
|
+
- `McpServer` - Stdio Model Context Protocol (MCP) server integration
|
|
25
|
+
|
|
26
|
+
## Skill Manifest Schema
|
|
27
|
+
|
|
28
|
+
Reactive skills use `skill.yaml` with `schema_version: "2.0.0"`:
|
|
29
|
+
|
|
30
|
+
```yaml
|
|
31
|
+
schema_version: "2.0.0"
|
|
32
|
+
name: "my-skill"
|
|
33
|
+
description: "Skill description"
|
|
34
|
+
initial_state: "START"
|
|
35
|
+
|
|
36
|
+
context_keys:
|
|
37
|
+
- "project_type"
|
|
38
|
+
- "mission"
|
|
39
|
+
|
|
40
|
+
states:
|
|
41
|
+
START:
|
|
42
|
+
description: "Initial state"
|
|
43
|
+
prompt_template: "states/start.md"
|
|
44
|
+
on_enter:
|
|
45
|
+
- set_context:
|
|
46
|
+
active_phase: "start"
|
|
47
|
+
transitions:
|
|
48
|
+
DONE:
|
|
49
|
+
target: "DONE"
|
|
50
|
+
guard: "event.payload.completed === true"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Integration Modes
|
|
54
|
+
|
|
55
|
+
The runtime has no dependency on any specific integration mode:
|
|
56
|
+
|
|
57
|
+
1. **Direct API** - Import and use `FSMEngine` directly in your application
|
|
58
|
+
2. **MCP stdio** - Use the `McpServer` class to expose tools over stdio
|
|
59
|
+
3. **Legacy hooks (removed)** - `runtime-hooks.ts` was removed in v2.0. Use `on_enter`/`on_exit` lifecycle hooks and explicit `engine.handleSignal()` calls instead
|
|
60
|
+
|
|
61
|
+
## Event Store
|
|
62
|
+
|
|
63
|
+
Dual-mode event sourcing:
|
|
64
|
+
|
|
65
|
+
1. **JSONL** (`.reactive/skills/<skill>/events.jsonl`) - Human-readable append-only log
|
|
66
|
+
2. **SQLite** (`.reactive/skills/<skill>/events.db`) - Indexed relational database
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
MIT
|
|
71
|
+
|
package/dist/cli/dev.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
const args = process.argv.slice(2);
|
|
5
|
+
const command = args[0];
|
|
6
|
+
function printHelp() {
|
|
7
|
+
console.log(`
|
|
8
|
+
reactive-skills-dev: Developer CLI for Reactive Skills Architecture
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
reactive-skills-dev init <name> Scaffold new reactive skill in skills/<name>/
|
|
12
|
+
reactive-skills-dev upgrade <path> Convert legacy SKILL.md to reactive format
|
|
13
|
+
reactive-skills-dev inspect <skill> Print statechart, transitions, and guards
|
|
14
|
+
reactive-skills-dev migrate [path] Retroactively migrate project to latest schema
|
|
15
|
+
reactive-skills-dev sync Sync skills to all agent config directories
|
|
16
|
+
`);
|
|
17
|
+
}
|
|
18
|
+
async function main() {
|
|
19
|
+
if (!command || command === 'help' || command === '--help') {
|
|
20
|
+
printHelp();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
// INIT
|
|
24
|
+
if (command === 'init') {
|
|
25
|
+
const name = args[1];
|
|
26
|
+
if (!name) {
|
|
27
|
+
console.error(JSON.stringify({ error: 'Skill name required. Usage: reactive-skills-dev init <name>' }));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
const skillDir = path.resolve(process.cwd(), 'skills', name);
|
|
31
|
+
fs.mkdirSync(path.join(skillDir, 'states'), { recursive: true });
|
|
32
|
+
fs.mkdirSync(path.join(skillDir, 'guards'), { recursive: true });
|
|
33
|
+
fs.mkdirSync(path.join(skillDir, 'templates'), { recursive: true });
|
|
34
|
+
const manifest = `name: ${name}\nversion: "1.0.0"\ndescription: "${name} reactive skill"\ninitial_state: INITIAL\nstates:\n INITIAL:\n description: "Starting state"\n transitions: {}\n`;
|
|
35
|
+
fs.writeFileSync(path.join(skillDir, 'skill.yaml'), manifest, 'utf8');
|
|
36
|
+
fs.writeFileSync(path.join(skillDir, 'states', 'INITIAL.md'), `# State: INITIAL\n\nDescribe what the agent should do in this state.\n`, 'utf8');
|
|
37
|
+
console.log(`Scaffolded reactive skill '${name}' at skills/${name}/`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
// UPGRADE
|
|
41
|
+
if (command === 'upgrade') {
|
|
42
|
+
const skillPath = args[1];
|
|
43
|
+
if (!skillPath) {
|
|
44
|
+
console.error(JSON.stringify({ error: 'Path required. Usage: reactive-skills-dev upgrade <path>' }));
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
const { LegacySkillAdapter } = await import('../core/legacy-adapter.js');
|
|
48
|
+
const targetDir = path.resolve(process.cwd(), skillPath);
|
|
49
|
+
const result = LegacySkillAdapter.upgradeToModular(targetDir);
|
|
50
|
+
console.log(`Upgraded legacy SKILL.md at ${targetDir}`);
|
|
51
|
+
console.log(JSON.stringify(result, null, 2));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// INSPECT
|
|
55
|
+
if (command === 'inspect') {
|
|
56
|
+
const skillPath = args[1] && !args[1].startsWith('--') ? args[1] : 'skills/synthesis';
|
|
57
|
+
const targetDir = path.resolve(process.cwd(), skillPath);
|
|
58
|
+
const { FSMEngine } = await import('../core/fsm-engine.js');
|
|
59
|
+
const engine = new FSMEngine({ skillDir: targetDir });
|
|
60
|
+
const manifest = engine.getManifest();
|
|
61
|
+
const isJson = args.includes('--json');
|
|
62
|
+
if (isJson) {
|
|
63
|
+
console.log(JSON.stringify(manifest, null, 2));
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
console.log(`\nReactive Skill: ${manifest.name} (v${manifest.version || '1.0.0'})`);
|
|
67
|
+
console.log(`Description: ${manifest.description}`);
|
|
68
|
+
console.log(`Initial State: ${manifest.initial_state}`);
|
|
69
|
+
console.log(`\nStates & Transitions:`);
|
|
70
|
+
for (const [sName, sDef] of Object.entries(manifest.states)) {
|
|
71
|
+
console.log(` [${sName}]`);
|
|
72
|
+
if (sDef.transitions) {
|
|
73
|
+
for (const [sig, trans] of Object.entries(sDef.transitions)) {
|
|
74
|
+
const t = typeof trans === 'string' ? { target: trans } : trans;
|
|
75
|
+
console.log(` on '${sig}' -> ${t.target}${t.guard ? ` [guard: ${t.guard}]` : ''}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// MIGRATE
|
|
83
|
+
if (command === 'migrate') {
|
|
84
|
+
const targetPath = args[1] || process.cwd();
|
|
85
|
+
const { ProjectMigrator } = await import('../core/migration.js');
|
|
86
|
+
const result = ProjectMigrator.migrate(targetPath);
|
|
87
|
+
const isJson = args.includes('--json');
|
|
88
|
+
if (isJson) {
|
|
89
|
+
console.log(JSON.stringify(result, null, 2));
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
console.log(`\nProject Migrated to ${result.schemaVersion}:`);
|
|
93
|
+
console.log(`Directory: ${result.projectDir}`);
|
|
94
|
+
for (const note of result.notes) {
|
|
95
|
+
console.log(` - ${note}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// SYNC (delegated to canonical sync engine)
|
|
101
|
+
if (command === 'sync') {
|
|
102
|
+
const { syncEngineCommand } = await import('../sync/cli.js');
|
|
103
|
+
const output = await syncEngineCommand(args.slice(1));
|
|
104
|
+
console.log(output);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
console.error(JSON.stringify({ error: `Unknown command: ${command}` }));
|
|
108
|
+
printHelp();
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
main().catch(err => {
|
|
112
|
+
console.error(JSON.stringify({ error: err.message }));
|
|
113
|
+
process.exit(1);
|
|
114
|
+
});
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { FSMEngine } from '../core/fsm-engine.js';
|
|
6
|
+
import { EventStore } from '../core/event-store.js';
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const command = args[0];
|
|
9
|
+
const isJson = args.includes('--json');
|
|
10
|
+
function printHelp() {
|
|
11
|
+
console.log(`
|
|
12
|
+
⚡ reactive-skills: Agent Experience Interface for Reactive Skills
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
reactive-skills mcp Run the stdio Model Context Protocol (MCP) server
|
|
16
|
+
reactive-skills install <skill> Install skill globally and sync
|
|
17
|
+
reactive-skills migrate [path] Retroactively migrate project to latest schema
|
|
18
|
+
reactive-skills state [skill] Get active state slice and prompt
|
|
19
|
+
reactive-skills emit <signal> Emit signal and step state machine
|
|
20
|
+
reactive-skills query "<sql>" Query SQLite event store
|
|
21
|
+
reactive-skills inspect <skill> Inspect statechart and transitions
|
|
22
|
+
reactive-skills events [limit] Tail event store ledger
|
|
23
|
+
reactive-skills sync Sync skills across agent platforms
|
|
24
|
+
reactive-skills init <name> Scaffold new reactive skill
|
|
25
|
+
reactive-skills upgrade <path> Upgrade legacy SKILL.md
|
|
26
|
+
`);
|
|
27
|
+
}
|
|
28
|
+
async function main() {
|
|
29
|
+
if (!command || command === 'help' || command === '--help') {
|
|
30
|
+
printHelp();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
// 0. RUN MCP SERVER
|
|
34
|
+
if (command === 'mcp') {
|
|
35
|
+
const { runMcpServer } = await import('../mcp/server.js');
|
|
36
|
+
await runMcpServer();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
// 0.5 MIGRATE PROJECT RETROACTIVELY
|
|
40
|
+
if (command === 'migrate') {
|
|
41
|
+
const targetPath = args[1] || process.cwd();
|
|
42
|
+
const { ProjectMigrator } = await import('../core/migration.js');
|
|
43
|
+
const result = ProjectMigrator.migrate(targetPath);
|
|
44
|
+
if (isJson) {
|
|
45
|
+
console.log(JSON.stringify(result, null, 2));
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
console.log(`\n🔄 Project Migrated to ${result.schemaVersion}:`);
|
|
49
|
+
console.log(`📁 Directory: ${result.projectDir}`);
|
|
50
|
+
for (const note of result.notes) {
|
|
51
|
+
console.log(` • ${note}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// 1. INSTALL SKILL GLOBALLY (delegated to sync engine for the sync step)
|
|
57
|
+
if (command === 'install') {
|
|
58
|
+
const skillName = args[1];
|
|
59
|
+
if (!skillName) {
|
|
60
|
+
console.error(JSON.stringify({ error: 'Skill name required. Usage: reactive-skills install <skill-name>' }));
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
const homeDir = os.homedir();
|
|
64
|
+
const sourceCandidates = [
|
|
65
|
+
path.resolve(process.cwd(), 'skills', skillName),
|
|
66
|
+
path.resolve(process.cwd(), skillName),
|
|
67
|
+
path.resolve(import.meta.dirname, '..', '..', 'skills', skillName),
|
|
68
|
+
];
|
|
69
|
+
const sourceDir = sourceCandidates.find(d => fs.existsSync(d));
|
|
70
|
+
if (!sourceDir) {
|
|
71
|
+
console.error(JSON.stringify({ error: `Skill '${skillName}' not found in local workspace.` }));
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
const destAgent = path.join(homeDir, '.agents', 'skills', skillName);
|
|
75
|
+
fs.mkdirSync(path.dirname(destAgent), { recursive: true });
|
|
76
|
+
fs.mkdirSync(destAgent, { recursive: true });
|
|
77
|
+
// Copy skill using the canonical engine's copySkillDir (via runSync with single target)
|
|
78
|
+
const { runSync } = await import('../sync/engine.js');
|
|
79
|
+
const report = runSync({
|
|
80
|
+
sourceDir: path.dirname(sourceDir),
|
|
81
|
+
targetDirs: [path.join(homeDir, '.agents', 'skills')],
|
|
82
|
+
targetSkill: skillName,
|
|
83
|
+
dryRun: false,
|
|
84
|
+
backup: false,
|
|
85
|
+
});
|
|
86
|
+
if (report.errors.length > 0) {
|
|
87
|
+
console.error(JSON.stringify({ error: report.errors.join('; ') }));
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
if (isJson) {
|
|
91
|
+
console.log(JSON.stringify({ ok: true, skill: skillName, installedTo: [destAgent], synced: true }));
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
console.log(`✅ Skill '${skillName}' installed to ${destAgent} and synced globally.`);
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
// 2. GET ACTIVE STATE SLICE
|
|
99
|
+
if (command === 'state') {
|
|
100
|
+
const skillPath = args[1] && !args[1].startsWith('--') ? args[1] : path.resolve(process.cwd(), 'skills', 'synthesis');
|
|
101
|
+
const targetDir = path.resolve(process.cwd(), skillPath);
|
|
102
|
+
if (!fs.existsSync(targetDir)) {
|
|
103
|
+
console.error(JSON.stringify({ error: `Skill path not found: ${targetDir}` }));
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
const engine = new FSMEngine({ skillDir: targetDir });
|
|
107
|
+
if (engine.isStrictExecution()) {
|
|
108
|
+
engine.recordTurnStart();
|
|
109
|
+
}
|
|
110
|
+
const slice = engine.generatePromptSlice();
|
|
111
|
+
if (isJson) {
|
|
112
|
+
console.log(JSON.stringify(slice, null, 2));
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
console.log(slice.formattedXml);
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// 3. EMIT SIGNAL
|
|
120
|
+
if (command === 'emit') {
|
|
121
|
+
const signalName = args[1];
|
|
122
|
+
if (!signalName) {
|
|
123
|
+
console.error(JSON.stringify({ error: 'Signal name required. Usage: reactive-skills emit <SIGNAL> [--payload JSON]' }));
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
let payload = {};
|
|
127
|
+
const payloadIdx = args.indexOf('--payload');
|
|
128
|
+
if (payloadIdx !== -1 && args[payloadIdx + 1]) {
|
|
129
|
+
try {
|
|
130
|
+
payload = JSON.parse(args[payloadIdx + 1]);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
payload = { raw: args[payloadIdx + 1] };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const skillPath = path.resolve(process.cwd(), 'skills', 'synthesis');
|
|
137
|
+
const store = new EventStore({ enableSqlite: true, skillId: 'synthesis' });
|
|
138
|
+
const engine = new FSMEngine({ skillDir: skillPath, eventStore: store });
|
|
139
|
+
const result = await engine.handleSignal(signalName, payload);
|
|
140
|
+
console.log(JSON.stringify(result, null, 2));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
// 4. SQL QUERY ON EVENTS.DB
|
|
144
|
+
if (command === 'query') {
|
|
145
|
+
const sql = args[1];
|
|
146
|
+
if (!sql) {
|
|
147
|
+
console.error(JSON.stringify({ error: 'SQL query required. Usage: reactive-skills query "SELECT * FROM events"' }));
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
const store = new EventStore({ enableSqlite: true });
|
|
151
|
+
const driver = store.getSqliteDriver();
|
|
152
|
+
if (!driver) {
|
|
153
|
+
console.error(JSON.stringify({ error: 'SQLite driver not enabled in active event store.' }));
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
const rows = driver.querySql(sql);
|
|
157
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
// 5. INSPECT SKILL
|
|
161
|
+
if (command === 'inspect') {
|
|
162
|
+
const skillPath = args[1] && !args[1].startsWith('--') ? args[1] : 'skills/synthesis';
|
|
163
|
+
const targetDir = path.resolve(process.cwd(), skillPath);
|
|
164
|
+
const engine = new FSMEngine({ skillDir: targetDir });
|
|
165
|
+
const manifest = engine.getManifest();
|
|
166
|
+
if (isJson) {
|
|
167
|
+
console.log(JSON.stringify(manifest, null, 2));
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
console.log(`\n⚡ Reactive Skill: ${manifest.name} (v${manifest.version || '1.0.0'})`);
|
|
171
|
+
console.log(`📖 Description: ${manifest.description}`);
|
|
172
|
+
console.log(`🏁 Initial State: ${manifest.initial_state}`);
|
|
173
|
+
console.log(`\n📋 States & Transitions:`);
|
|
174
|
+
for (const [sName, sDef] of Object.entries(manifest.states)) {
|
|
175
|
+
console.log(` [State: ${sName}]`);
|
|
176
|
+
if (sDef.transitions) {
|
|
177
|
+
for (const [sig, trans] of Object.entries(sDef.transitions)) {
|
|
178
|
+
const t = typeof trans === 'string' ? { target: trans } : trans;
|
|
179
|
+
console.log(` • on '${sig}' -> ${t.target}${t.guard ? ` [guard: ${t.guard}]` : ''}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
// 6. SYNC SKILLS (delegated to canonical sync engine)
|
|
187
|
+
if (command === 'sync') {
|
|
188
|
+
const { syncEngineCommand } = await import('../sync/cli.js');
|
|
189
|
+
const output = await syncEngineCommand(args.slice(1));
|
|
190
|
+
console.log(output);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
// 7. EVENTS
|
|
194
|
+
if (command === 'events') {
|
|
195
|
+
const limit = parseInt(args[1] || '20', 10);
|
|
196
|
+
const store = new EventStore();
|
|
197
|
+
const events = store.getAll();
|
|
198
|
+
const slice = events.slice(-limit);
|
|
199
|
+
if (isJson) {
|
|
200
|
+
console.log(JSON.stringify(slice, null, 2));
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
console.log(`\n📜 Event Store Ledger (.reactive/events.jsonl) - Showing last ${slice.length} events:`);
|
|
204
|
+
for (const e of slice) {
|
|
205
|
+
console.log(`[#${e.seq}] ${e.timestamp} | ${e.type.padEnd(18)} | State: ${(e.state || '-').padEnd(14)} | ${JSON.stringify(e.payload)}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
printHelp();
|
|
211
|
+
}
|
|
212
|
+
main().catch(err => {
|
|
213
|
+
console.error('Error running reactive-skills:', err);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { EventContext, SignalEvent } from './types.js';
|
|
2
|
+
export declare const EVENT_STORE_SCHEMA_VERSION = 2;
|
|
3
|
+
export type EventListener = (event: SignalEvent) => void;
|
|
4
|
+
export declare function createSortableId(): string;
|
|
5
|
+
export interface EventStoreOptions {
|
|
6
|
+
storagePath?: string;
|
|
7
|
+
sqlitePath?: string;
|
|
8
|
+
workspaceDir?: string;
|
|
9
|
+
skillId?: string;
|
|
10
|
+
runId?: string;
|
|
11
|
+
run_id?: string;
|
|
12
|
+
correlationId?: string;
|
|
13
|
+
correlation_id?: string;
|
|
14
|
+
requestId?: string;
|
|
15
|
+
request_id?: string;
|
|
16
|
+
traceParent?: string;
|
|
17
|
+
trace_parent?: string;
|
|
18
|
+
parentRunId?: string;
|
|
19
|
+
parent_run_id?: string;
|
|
20
|
+
schemaVersion?: string;
|
|
21
|
+
schema_version?: string;
|
|
22
|
+
inMemory?: boolean;
|
|
23
|
+
enableSqlite?: boolean;
|
|
24
|
+
maxInMemoryEvents?: number;
|
|
25
|
+
maxJsonlBytes?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface EventQueryOptions {
|
|
28
|
+
type?: string;
|
|
29
|
+
state?: string;
|
|
30
|
+
sinceSeq?: number;
|
|
31
|
+
limit?: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* SQLite Storage Driver for EventStore
|
|
35
|
+
* Provides ACID relational storage, indexing, and direct SQL querying
|
|
36
|
+
*/
|
|
37
|
+
export declare class SQLiteStorageDriver {
|
|
38
|
+
private db;
|
|
39
|
+
constructor(dbPath: string);
|
|
40
|
+
private initTables;
|
|
41
|
+
private ensureSchemaVersion;
|
|
42
|
+
private runMigrations;
|
|
43
|
+
insertEvent(event: SignalEvent): void;
|
|
44
|
+
beginTransaction(): void;
|
|
45
|
+
commitTransaction(): void;
|
|
46
|
+
queryEvents(options?: EventQueryOptions): SignalEvent[];
|
|
47
|
+
getLatestSequence(): number;
|
|
48
|
+
nextSequence(): number;
|
|
49
|
+
getRecentEvents(limit: number): SignalEvent[];
|
|
50
|
+
querySql(sql: string, params?: any[]): any[];
|
|
51
|
+
saveSnapshot(seq: number, state: string, context: Record<string, any>): void;
|
|
52
|
+
getLatestSnapshot(): {
|
|
53
|
+
seq: number;
|
|
54
|
+
state: string;
|
|
55
|
+
context: Record<string, any>;
|
|
56
|
+
} | null;
|
|
57
|
+
saveProjectionWatermark(name: string, eventSeq: number, projectionVersion: string): void;
|
|
58
|
+
getProjectionWatermark(name: string): {
|
|
59
|
+
eventSeq: number;
|
|
60
|
+
projectionVersion: string;
|
|
61
|
+
} | null;
|
|
62
|
+
saveProjection(name: string, content: string): void;
|
|
63
|
+
getProjection(name: string): string | null;
|
|
64
|
+
close(): void;
|
|
65
|
+
static getSchemaVersion(dbPath: string): number;
|
|
66
|
+
private rowToEvent;
|
|
67
|
+
clear(): void;
|
|
68
|
+
/**
|
|
69
|
+
* Purge every table including seq_counter so a rebuilt store starts from a
|
|
70
|
+
* clean slate. Used by EventStore.rebuildFromJsonl().
|
|
71
|
+
*/
|
|
72
|
+
clearAll(): void;
|
|
73
|
+
/**
|
|
74
|
+
* Replace the entire events table with the supplied ordered list.
|
|
75
|
+
* Preserves caller-supplied seq numbers (JSONL heritage). seq_counter is
|
|
76
|
+
* reset to the max seq so future appends continue from the correct point.
|
|
77
|
+
*/
|
|
78
|
+
rebuildEvents(orderedEvents: SignalEvent[]): void;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Immutable Append-Only Event Store
|
|
82
|
+
* All signals, tool results, guard evaluations, and state transitions are recorded here.
|
|
83
|
+
*/
|
|
84
|
+
export declare class EventStore {
|
|
85
|
+
private events;
|
|
86
|
+
private listeners;
|
|
87
|
+
private storagePath;
|
|
88
|
+
private sqliteDriver;
|
|
89
|
+
private sqlitePath;
|
|
90
|
+
private seqCounter;
|
|
91
|
+
private maxInMemoryEvents;
|
|
92
|
+
private eventContext;
|
|
93
|
+
private projectionWatermarks;
|
|
94
|
+
private latestSnapshot;
|
|
95
|
+
private maxJsonlBytes;
|
|
96
|
+
constructor(options?: EventStoreOptions);
|
|
97
|
+
private initializeStorage;
|
|
98
|
+
/**
|
|
99
|
+
* Scan all JSONL files for the authoritative max seq and event count.
|
|
100
|
+
* Returns { count: 0, maxSeq: 0 } when no JSONL is present.
|
|
101
|
+
*/
|
|
102
|
+
private readJsonlStats;
|
|
103
|
+
/**
|
|
104
|
+
* Rebuild SQLite from JSONL. Reads every JSONL file, deduplicates by event
|
|
105
|
+
* id (keeping the highest seq on conflict), clears SQLite, and re-inserts
|
|
106
|
+
* in seq order preserving original seq numbers. seq_counter is reset to the
|
|
107
|
+
* max seq. In-memory state is left untouched; callers must refresh it.
|
|
108
|
+
*/
|
|
109
|
+
rebuildFromJsonl(): number;
|
|
110
|
+
/**
|
|
111
|
+
* Read every event from every JSONL file (active + archived segments).
|
|
112
|
+
*/
|
|
113
|
+
private readAllJsonlEvents;
|
|
114
|
+
/**
|
|
115
|
+
* Deduplicate events by id, keeping the entry with the highest seq.
|
|
116
|
+
*/
|
|
117
|
+
private deduplicateById;
|
|
118
|
+
/**
|
|
119
|
+
* Backfill JSONL with events SQLite has that JSONL is missing. SQLite is
|
|
120
|
+
* authoritative; this keeps the audit log complete when a JSONL write
|
|
121
|
+
* failed mid-write. Best-effort: failures are logged, not thrown.
|
|
122
|
+
*/
|
|
123
|
+
syncJsonlToSqlite(): number;
|
|
124
|
+
private getJsonlPaths;
|
|
125
|
+
private rotateJsonlIfNeeded;
|
|
126
|
+
/**
|
|
127
|
+
* Append a new event to the immutable log.
|
|
128
|
+
*
|
|
129
|
+
* SQLite is the authoritative store when enabled; JSONL is an append-only
|
|
130
|
+
* audit mirror that can optionally be checked into git. If JSONL mirroring
|
|
131
|
+
* fails the run stays live because SQLite already persisted the event.
|
|
132
|
+
*/
|
|
133
|
+
append<T = Record<string, any>>(type: string, payload: T, metadata?: {
|
|
134
|
+
source?: string;
|
|
135
|
+
causationId?: string;
|
|
136
|
+
correlationId?: string;
|
|
137
|
+
requestId?: string;
|
|
138
|
+
traceParent?: string;
|
|
139
|
+
parentRunId?: string;
|
|
140
|
+
skillId?: string;
|
|
141
|
+
runId?: string;
|
|
142
|
+
state?: string;
|
|
143
|
+
}): SignalEvent<T>;
|
|
144
|
+
/**
|
|
145
|
+
* Subscribe to new events
|
|
146
|
+
*/
|
|
147
|
+
subscribe(listener: EventListener): () => void;
|
|
148
|
+
/**
|
|
149
|
+
* Get all recorded events (delegates to SQLite if available, otherwise in-memory buffer)
|
|
150
|
+
*/
|
|
151
|
+
getAll(): SignalEvent[];
|
|
152
|
+
getSince(seq: number): SignalEvent[];
|
|
153
|
+
getLatestSequence(): number;
|
|
154
|
+
getEventContext(): EventContext;
|
|
155
|
+
saveProjectionWatermark(name: string, eventSeq: number, projectionVersion: string): void;
|
|
156
|
+
getProjectionWatermark(name: string): {
|
|
157
|
+
eventSeq: number;
|
|
158
|
+
projectionVersion: string;
|
|
159
|
+
} | null;
|
|
160
|
+
saveSnapshot(seq: number, state: string, context: Record<string, any>): void;
|
|
161
|
+
getLatestSnapshot(): {
|
|
162
|
+
seq: number;
|
|
163
|
+
state: string;
|
|
164
|
+
context: Record<string, any>;
|
|
165
|
+
} | null;
|
|
166
|
+
/**
|
|
167
|
+
* Query recorded events (relational index in SQLite, or in-memory filter)
|
|
168
|
+
*/
|
|
169
|
+
query(filter?: EventQueryOptions): SignalEvent[];
|
|
170
|
+
/**
|
|
171
|
+
* Close storage drivers and release file handles
|
|
172
|
+
*/
|
|
173
|
+
close(): void;
|
|
174
|
+
/**
|
|
175
|
+
* Get SQLite driver instance if enabled
|
|
176
|
+
*/
|
|
177
|
+
getSqliteDriver(): SQLiteStorageDriver | null;
|
|
178
|
+
/**
|
|
179
|
+
* Clear in-memory and file events (for test isolation)
|
|
180
|
+
*/
|
|
181
|
+
clear(): void;
|
|
182
|
+
}
|