agileflow 2.91.0 → 2.92.1
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/CHANGELOG.md +10 -0
- package/README.md +6 -6
- package/lib/README.md +178 -0
- package/lib/codebase-indexer.js +32 -23
- package/lib/colors.js +190 -12
- package/lib/consent.js +232 -0
- package/lib/correlation.js +277 -0
- package/lib/error-codes.js +46 -0
- package/lib/errors.js +48 -6
- package/lib/file-cache.js +182 -0
- package/lib/format-error.js +156 -0
- package/lib/path-resolver.js +155 -7
- package/lib/paths.js +212 -20
- package/lib/placeholder-registry.js +205 -0
- package/lib/registry-di.js +358 -0
- package/lib/result-schema.js +363 -0
- package/lib/result.js +210 -0
- package/lib/session-registry.js +13 -0
- package/lib/session-state-machine.js +465 -0
- package/lib/validate-commands.js +308 -0
- package/lib/validate.js +116 -52
- package/package.json +1 -1
- package/scripts/af +34 -0
- package/scripts/agent-loop.js +63 -9
- package/scripts/agileflow-configure.js +2 -2
- package/scripts/agileflow-welcome.js +491 -23
- package/scripts/archive-completed-stories.sh +57 -11
- package/scripts/claude-tmux.sh +102 -0
- package/scripts/damage-control-bash.js +3 -70
- package/scripts/damage-control-edit.js +3 -20
- package/scripts/damage-control-write.js +3 -20
- package/scripts/dependency-check.js +310 -0
- package/scripts/get-env.js +11 -4
- package/scripts/lib/configure-detect.js +23 -1
- package/scripts/lib/configure-features.js +50 -2
- package/scripts/lib/context-formatter.js +771 -0
- package/scripts/lib/context-loader.js +699 -0
- package/scripts/lib/damage-control-utils.js +107 -0
- package/scripts/lib/json-utils.sh +162 -0
- package/scripts/lib/state-migrator.js +353 -0
- package/scripts/lib/story-state-machine.js +437 -0
- package/scripts/obtain-context.js +80 -1248
- package/scripts/pre-push-check.sh +46 -0
- package/scripts/precompact-context.sh +23 -10
- package/scripts/query-codebase.js +127 -14
- package/scripts/ralph-loop.js +5 -5
- package/scripts/session-manager.js +408 -55
- package/scripts/spawn-parallel.js +666 -0
- package/scripts/tui/blessed/data/watcher.js +20 -15
- package/scripts/tui/blessed/index.js +2 -2
- package/scripts/tui/blessed/panels/output.js +14 -8
- package/scripts/tui/blessed/panels/sessions.js +22 -15
- package/scripts/tui/blessed/panels/trace.js +14 -8
- package/scripts/tui/blessed/ui/help.js +3 -3
- package/scripts/tui/blessed/ui/screen.js +4 -4
- package/scripts/tui/blessed/ui/statusbar.js +5 -9
- package/scripts/tui/blessed/ui/tabbar.js +11 -11
- package/scripts/validators/component-validator.js +41 -14
- package/scripts/validators/json-schema-validator.js +11 -4
- package/scripts/validators/markdown-validator.js +1 -2
- package/scripts/validators/migration-validator.js +17 -5
- package/scripts/validators/security-validator.js +137 -33
- package/scripts/validators/story-format-validator.js +31 -10
- package/scripts/validators/test-result-validator.js +19 -4
- package/scripts/validators/workflow-validator.js +12 -5
- package/src/core/agents/codebase-query.md +24 -0
- package/src/core/commands/adr.md +114 -0
- package/src/core/commands/agent.md +120 -0
- package/src/core/commands/assign.md +145 -0
- package/src/core/commands/babysit.md +32 -5
- package/src/core/commands/changelog.md +118 -0
- package/src/core/commands/configure.md +42 -6
- package/src/core/commands/diagnose.md +114 -0
- package/src/core/commands/epic.md +113 -0
- package/src/core/commands/handoff.md +128 -0
- package/src/core/commands/help.md +75 -0
- package/src/core/commands/pr.md +96 -0
- package/src/core/commands/roadmap/analyze.md +400 -0
- package/src/core/commands/session/new.md +132 -6
- package/src/core/commands/session/spawn.md +197 -0
- package/src/core/commands/sprint.md +22 -0
- package/src/core/commands/status.md +74 -0
- package/src/core/commands/story.md +143 -4
- package/src/core/templates/agileflow-metadata.json +55 -2
- package/src/core/templates/plan-template.md +125 -0
- package/src/core/templates/story-lifecycle.md +213 -0
- package/src/core/templates/story-template.md +4 -0
- package/src/core/templates/tdd-test-template.js +241 -0
- package/tools/cli/commands/setup.js +95 -0
- package/tools/cli/installers/core/installer.js +94 -0
- package/tools/cli/installers/ide/_base-ide.js +20 -11
- package/tools/cli/installers/ide/codex.js +29 -47
- package/tools/cli/installers/ide/windsurf.js +1 -1
- package/tools/cli/lib/config-manager.js +17 -2
- package/tools/cli/lib/content-transformer.js +271 -0
- package/tools/cli/lib/error-handler.js +14 -22
- package/tools/cli/lib/ide-error-factory.js +421 -0
- package/tools/cli/lib/ide-health-monitor.js +364 -0
- package/tools/cli/lib/ide-registry.js +113 -2
- package/tools/cli/lib/ui.js +15 -25
package/lib/result.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* result.js - Unified Result Schema for AgileFlow
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent result objects across modules with type helpers.
|
|
5
|
+
*
|
|
6
|
+
* Standard Result Schema:
|
|
7
|
+
* { ok: boolean, data?: any, error?: string|Error }
|
|
8
|
+
*
|
|
9
|
+
* Extended fields (context-specific):
|
|
10
|
+
* - found: boolean (for lookup operations)
|
|
11
|
+
* - applied: number (for batch operations)
|
|
12
|
+
* - cleaned: number (for cleanup operations)
|
|
13
|
+
* - path: string (for file operations)
|
|
14
|
+
* - status: string (for state operations)
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* const { ok, err, Result } = require('./result');
|
|
18
|
+
*
|
|
19
|
+
* // Success
|
|
20
|
+
* return ok({ path: '/saved/file.json' });
|
|
21
|
+
*
|
|
22
|
+
* // Failure
|
|
23
|
+
* return err('File not found');
|
|
24
|
+
*
|
|
25
|
+
* // With data
|
|
26
|
+
* return ok({ data: parsedConfig });
|
|
27
|
+
*
|
|
28
|
+
* // Type checking
|
|
29
|
+
* if (Result.isOk(result)) { ... }
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create a success result
|
|
34
|
+
*
|
|
35
|
+
* @param {Object} [extras={}] - Additional fields to include
|
|
36
|
+
* @returns {{ ok: true } & Object} Success result
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ok() // { ok: true }
|
|
40
|
+
* ok({ data: config }) // { ok: true, data: config }
|
|
41
|
+
* ok({ path: '/file.json', created: true }) // { ok: true, path: '/file.json', created: true }
|
|
42
|
+
*/
|
|
43
|
+
function ok(extras = {}) {
|
|
44
|
+
return { ok: true, ...extras };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Create a failure result
|
|
49
|
+
*
|
|
50
|
+
* @param {string|Error} error - Error message or Error object
|
|
51
|
+
* @param {Object} [extras={}] - Additional fields to include
|
|
52
|
+
* @returns {{ ok: false, error: string } & Object} Failure result
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* err('Not found') // { ok: false, error: 'Not found' }
|
|
56
|
+
* err(new Error('Failed')) // { ok: false, error: 'Failed' }
|
|
57
|
+
* err('Invalid', { code: 'EINVAL' }) // { ok: false, error: 'Invalid', code: 'EINVAL' }
|
|
58
|
+
*/
|
|
59
|
+
function err(error, extras = {}) {
|
|
60
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
61
|
+
return { ok: false, error: message, ...extras };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Result utilities for type checking and manipulation
|
|
66
|
+
*/
|
|
67
|
+
const Result = {
|
|
68
|
+
/**
|
|
69
|
+
* Check if result is success
|
|
70
|
+
* @param {Object} result - Result to check
|
|
71
|
+
* @returns {boolean}
|
|
72
|
+
*/
|
|
73
|
+
isOk(result) {
|
|
74
|
+
return Boolean(result && result.ok === true);
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check if result is failure
|
|
79
|
+
* @param {Object} result - Result to check
|
|
80
|
+
* @returns {boolean}
|
|
81
|
+
*/
|
|
82
|
+
isErr(result) {
|
|
83
|
+
return Boolean(result && result.ok === false);
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Unwrap result data or throw on error
|
|
88
|
+
* @param {Object} result - Result to unwrap
|
|
89
|
+
* @param {string} [context] - Context for error message
|
|
90
|
+
* @returns {any} The data field or the result without ok field
|
|
91
|
+
* @throws {Error} If result is not ok
|
|
92
|
+
*/
|
|
93
|
+
unwrap(result, context = '') {
|
|
94
|
+
if (!Result.isOk(result)) {
|
|
95
|
+
const prefix = context ? `${context}: ` : '';
|
|
96
|
+
throw new Error(`${prefix}${result.error || 'Unknown error'}`);
|
|
97
|
+
}
|
|
98
|
+
return result.data !== undefined ? result.data : result;
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Unwrap result data or return default value
|
|
103
|
+
* @param {Object} result - Result to unwrap
|
|
104
|
+
* @param {any} defaultValue - Value to return on error
|
|
105
|
+
* @returns {any}
|
|
106
|
+
*/
|
|
107
|
+
unwrapOr(result, defaultValue) {
|
|
108
|
+
if (!Result.isOk(result)) {
|
|
109
|
+
return defaultValue;
|
|
110
|
+
}
|
|
111
|
+
return result.data !== undefined ? result.data : result;
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Map over successful result
|
|
116
|
+
* @param {Object} result - Result to map
|
|
117
|
+
* @param {Function} fn - Function to apply to data
|
|
118
|
+
* @returns {Object} Mapped result or original error
|
|
119
|
+
*/
|
|
120
|
+
map(result, fn) {
|
|
121
|
+
if (!Result.isOk(result)) {
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
const data = result.data !== undefined ? result.data : result;
|
|
126
|
+
const mapped = fn(data);
|
|
127
|
+
return ok({ data: mapped });
|
|
128
|
+
} catch (e) {
|
|
129
|
+
return err(e);
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Convert legacy { success: true/false } to standard { ok: true/false }
|
|
135
|
+
* @param {Object} legacyResult - Legacy result object
|
|
136
|
+
* @returns {Object} Standardized result
|
|
137
|
+
*/
|
|
138
|
+
fromLegacy(legacyResult) {
|
|
139
|
+
if (legacyResult.success !== undefined) {
|
|
140
|
+
const { success, ...rest } = legacyResult;
|
|
141
|
+
return { ok: success, ...rest };
|
|
142
|
+
}
|
|
143
|
+
return legacyResult;
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Convert standard result to legacy format for backwards compatibility
|
|
148
|
+
* @param {Object} result - Standard result object
|
|
149
|
+
* @returns {Object} Legacy result with success field
|
|
150
|
+
*/
|
|
151
|
+
toLegacy(result) {
|
|
152
|
+
if (result.ok !== undefined) {
|
|
153
|
+
const { ok: isOk, ...rest } = result;
|
|
154
|
+
return { success: isOk, ...rest };
|
|
155
|
+
}
|
|
156
|
+
return result;
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Async result helpers
|
|
162
|
+
*/
|
|
163
|
+
const AsyncResult = {
|
|
164
|
+
/**
|
|
165
|
+
* Wrap an async function to return Result
|
|
166
|
+
* @param {Function} fn - Async function to wrap
|
|
167
|
+
* @returns {Function} Wrapped function returning Result
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* const safeRead = AsyncResult.wrap(fs.promises.readFile);
|
|
171
|
+
* const result = await safeRead('file.txt');
|
|
172
|
+
* if (Result.isOk(result)) { console.log(result.data); }
|
|
173
|
+
*/
|
|
174
|
+
wrap(fn) {
|
|
175
|
+
return async (...args) => {
|
|
176
|
+
try {
|
|
177
|
+
const data = await fn(...args);
|
|
178
|
+
return ok({ data });
|
|
179
|
+
} catch (e) {
|
|
180
|
+
return err(e);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Execute multiple async operations and collect results
|
|
187
|
+
* @param {Array<Promise>} promises - Promises to execute
|
|
188
|
+
* @returns {Promise<Object>} Result with all settled results
|
|
189
|
+
*/
|
|
190
|
+
async all(promises) {
|
|
191
|
+
try {
|
|
192
|
+
const results = await Promise.all(promises);
|
|
193
|
+
const allOk = results.every(r => Result.isOk(r));
|
|
194
|
+
if (allOk) {
|
|
195
|
+
return ok({ data: results });
|
|
196
|
+
}
|
|
197
|
+
const errors = results.filter(r => Result.isErr(r)).map(r => r.error);
|
|
198
|
+
return err(errors.join('; '), { partial: results });
|
|
199
|
+
} catch (e) {
|
|
200
|
+
return err(e);
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
module.exports = {
|
|
206
|
+
ok,
|
|
207
|
+
err,
|
|
208
|
+
Result,
|
|
209
|
+
AsyncResult,
|
|
210
|
+
};
|
package/lib/session-registry.js
CHANGED
|
@@ -155,6 +155,11 @@ class SessionRegistry extends EventEmitter {
|
|
|
155
155
|
* @returns {Object} Registry data
|
|
156
156
|
*/
|
|
157
157
|
loadSync() {
|
|
158
|
+
// Return cached data if within TTL (performance optimization)
|
|
159
|
+
if (this._cache && Date.now() - this._cacheTime < this.cacheTTL) {
|
|
160
|
+
return this._cache;
|
|
161
|
+
}
|
|
162
|
+
|
|
158
163
|
this._ensureDir();
|
|
159
164
|
const result = this._jsonFile.readSync();
|
|
160
165
|
|
|
@@ -167,6 +172,14 @@ class SessionRegistry extends EventEmitter {
|
|
|
167
172
|
return this._createDefaultRegistry();
|
|
168
173
|
}
|
|
169
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Invalidate cache (call after external modifications)
|
|
177
|
+
*/
|
|
178
|
+
invalidateCache() {
|
|
179
|
+
this._cache = null;
|
|
180
|
+
this._cacheTime = 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
170
183
|
/**
|
|
171
184
|
* Save registry
|
|
172
185
|
* @param {Object} registry - Registry data to save
|
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session State Machine
|
|
3
|
+
*
|
|
4
|
+
* Provides type-safe state transitions for AgileFlow sessions.
|
|
5
|
+
* Enforces valid state changes and emits events on transitions.
|
|
6
|
+
*
|
|
7
|
+
* States:
|
|
8
|
+
* - idle: Session created but not started
|
|
9
|
+
* - active: Session is running
|
|
10
|
+
* - paused: Session temporarily suspended
|
|
11
|
+
* - terminated: Session has ended
|
|
12
|
+
*
|
|
13
|
+
* Valid Transitions:
|
|
14
|
+
* - idle → active (start)
|
|
15
|
+
* - active → paused (pause)
|
|
16
|
+
* - active → terminated (stop)
|
|
17
|
+
* - paused → active (resume)
|
|
18
|
+
* - paused → terminated (stop)
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* const { SessionStateMachine, SessionState } = require('./session-state-machine');
|
|
22
|
+
*
|
|
23
|
+
* const sm = new SessionStateMachine('idle');
|
|
24
|
+
* sm.on('transition', ({ from, to, action }) => {
|
|
25
|
+
* console.log(`Transition: ${from} -[${action}]-> ${to}`);
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* sm.canTransition('start'); // true
|
|
29
|
+
* sm.transition('start'); // State is now 'active'
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
'use strict';
|
|
33
|
+
|
|
34
|
+
const EventEmitter = require('events');
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Valid session states
|
|
38
|
+
* @enum {string}
|
|
39
|
+
*/
|
|
40
|
+
const SessionState = Object.freeze({
|
|
41
|
+
IDLE: 'idle',
|
|
42
|
+
ACTIVE: 'active',
|
|
43
|
+
PAUSED: 'paused',
|
|
44
|
+
TERMINATED: 'terminated',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Valid state transition actions
|
|
49
|
+
* @enum {string}
|
|
50
|
+
*/
|
|
51
|
+
const SessionAction = Object.freeze({
|
|
52
|
+
START: 'start',
|
|
53
|
+
PAUSE: 'pause',
|
|
54
|
+
RESUME: 'resume',
|
|
55
|
+
STOP: 'stop',
|
|
56
|
+
RESTART: 'restart',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* State transition table
|
|
61
|
+
* Maps current state → action → next state
|
|
62
|
+
*/
|
|
63
|
+
const TRANSITIONS = Object.freeze({
|
|
64
|
+
[SessionState.IDLE]: {
|
|
65
|
+
[SessionAction.START]: SessionState.ACTIVE,
|
|
66
|
+
},
|
|
67
|
+
[SessionState.ACTIVE]: {
|
|
68
|
+
[SessionAction.PAUSE]: SessionState.PAUSED,
|
|
69
|
+
[SessionAction.STOP]: SessionState.TERMINATED,
|
|
70
|
+
[SessionAction.RESTART]: SessionState.ACTIVE, // Self-transition for restart
|
|
71
|
+
},
|
|
72
|
+
[SessionState.PAUSED]: {
|
|
73
|
+
[SessionAction.RESUME]: SessionState.ACTIVE,
|
|
74
|
+
[SessionAction.STOP]: SessionState.TERMINATED,
|
|
75
|
+
},
|
|
76
|
+
[SessionState.TERMINATED]: {
|
|
77
|
+
// No transitions out of terminated (final state)
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get all valid states
|
|
83
|
+
* @returns {string[]}
|
|
84
|
+
*/
|
|
85
|
+
function getValidStates() {
|
|
86
|
+
return Object.values(SessionState);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get all valid actions
|
|
91
|
+
* @returns {string[]}
|
|
92
|
+
*/
|
|
93
|
+
function getValidActions() {
|
|
94
|
+
return Object.values(SessionAction);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Check if a state is valid
|
|
99
|
+
* @param {string} state
|
|
100
|
+
* @returns {boolean}
|
|
101
|
+
*/
|
|
102
|
+
function isValidState(state) {
|
|
103
|
+
return getValidStates().includes(state);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check if an action is valid
|
|
108
|
+
* @param {string} action
|
|
109
|
+
* @returns {boolean}
|
|
110
|
+
*/
|
|
111
|
+
function isValidAction(action) {
|
|
112
|
+
return getValidActions().includes(action);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get valid transitions from a state
|
|
117
|
+
* @param {string} state - Current state
|
|
118
|
+
* @returns {Object} Map of action → next state
|
|
119
|
+
*/
|
|
120
|
+
function getTransitionsFromState(state) {
|
|
121
|
+
return TRANSITIONS[state] || {};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get available actions from a state
|
|
126
|
+
* @param {string} state - Current state
|
|
127
|
+
* @returns {string[]} Available actions
|
|
128
|
+
*/
|
|
129
|
+
function getAvailableActions(state) {
|
|
130
|
+
return Object.keys(getTransitionsFromState(state));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Check if a transition is valid
|
|
135
|
+
* @param {string} currentState - Current state
|
|
136
|
+
* @param {string} action - Action to perform
|
|
137
|
+
* @returns {boolean}
|
|
138
|
+
*/
|
|
139
|
+
function canTransition(currentState, action) {
|
|
140
|
+
const transitions = TRANSITIONS[currentState];
|
|
141
|
+
return !!(transitions && transitions[action]);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get the next state for a transition
|
|
146
|
+
* @param {string} currentState - Current state
|
|
147
|
+
* @param {string} action - Action to perform
|
|
148
|
+
* @returns {string|null} Next state or null if invalid
|
|
149
|
+
*/
|
|
150
|
+
function getNextState(currentState, action) {
|
|
151
|
+
const transitions = TRANSITIONS[currentState];
|
|
152
|
+
return (transitions && transitions[action]) || null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Session state machine error
|
|
157
|
+
*/
|
|
158
|
+
class StateMachineError extends Error {
|
|
159
|
+
constructor(message, details = {}) {
|
|
160
|
+
super(message);
|
|
161
|
+
this.name = 'StateMachineError';
|
|
162
|
+
this.currentState = details.currentState;
|
|
163
|
+
this.action = details.action;
|
|
164
|
+
this.code = details.code || 'INVALID_TRANSITION';
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Session State Machine
|
|
170
|
+
* @extends EventEmitter
|
|
171
|
+
*
|
|
172
|
+
* Events:
|
|
173
|
+
* - 'transition': { from, to, action, timestamp }
|
|
174
|
+
* - 'invalidTransition': { currentState, action, error }
|
|
175
|
+
* - 'stateChange': { state }
|
|
176
|
+
*/
|
|
177
|
+
class SessionStateMachine extends EventEmitter {
|
|
178
|
+
/**
|
|
179
|
+
* @param {string} [initialState='idle'] - Initial state
|
|
180
|
+
* @param {Object} [options={}] - Options
|
|
181
|
+
* @param {boolean} [options.strict=true] - Throw on invalid transitions
|
|
182
|
+
* @param {boolean} [options.historyEnabled=false] - Track transition history
|
|
183
|
+
*/
|
|
184
|
+
constructor(initialState = SessionState.IDLE, options = {}) {
|
|
185
|
+
super();
|
|
186
|
+
|
|
187
|
+
// Validate initial state
|
|
188
|
+
if (!isValidState(initialState)) {
|
|
189
|
+
throw new StateMachineError(`Invalid initial state: ${initialState}`, {
|
|
190
|
+
code: 'INVALID_STATE',
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
this._state = initialState;
|
|
195
|
+
this._strict = options.strict !== false;
|
|
196
|
+
this._historyEnabled = options.historyEnabled || false;
|
|
197
|
+
this._history = this._historyEnabled ? [{ state: initialState, timestamp: new Date() }] : [];
|
|
198
|
+
this._metadata = {};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Get current state
|
|
203
|
+
* @returns {string}
|
|
204
|
+
*/
|
|
205
|
+
get state() {
|
|
206
|
+
return this._state;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get transition history (if enabled)
|
|
211
|
+
* @returns {Array<{state: string, action?: string, timestamp: Date}>}
|
|
212
|
+
*/
|
|
213
|
+
get history() {
|
|
214
|
+
return [...this._history];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Get metadata
|
|
219
|
+
* @returns {Object}
|
|
220
|
+
*/
|
|
221
|
+
get metadata() {
|
|
222
|
+
return { ...this._metadata };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Set metadata
|
|
227
|
+
* @param {Object} meta - Metadata to merge
|
|
228
|
+
*/
|
|
229
|
+
setMetadata(meta) {
|
|
230
|
+
this._metadata = { ...this._metadata, ...meta };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Check if state machine is in a final state
|
|
235
|
+
* @returns {boolean}
|
|
236
|
+
*/
|
|
237
|
+
isFinal() {
|
|
238
|
+
return this._state === SessionState.TERMINATED;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Check if a transition can be performed
|
|
243
|
+
* @param {string} action - Action to check
|
|
244
|
+
* @returns {boolean}
|
|
245
|
+
*/
|
|
246
|
+
canTransition(action) {
|
|
247
|
+
return canTransition(this._state, action);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Get available actions from current state
|
|
252
|
+
* @returns {string[]}
|
|
253
|
+
*/
|
|
254
|
+
getAvailableActions() {
|
|
255
|
+
return getAvailableActions(this._state);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Perform a transition
|
|
260
|
+
* @param {string} action - Action to perform
|
|
261
|
+
* @returns {{ok: boolean, from?: string, to?: string, error?: Error}}
|
|
262
|
+
*/
|
|
263
|
+
transition(action) {
|
|
264
|
+
const from = this._state;
|
|
265
|
+
|
|
266
|
+
// Check if action is valid
|
|
267
|
+
if (!isValidAction(action)) {
|
|
268
|
+
const error = new StateMachineError(`Invalid action: ${action}`, {
|
|
269
|
+
currentState: from,
|
|
270
|
+
action,
|
|
271
|
+
code: 'INVALID_ACTION',
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
this.emit('invalidTransition', { currentState: from, action, error });
|
|
275
|
+
|
|
276
|
+
if (this._strict) {
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
return { ok: false, error };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Check if transition is valid
|
|
283
|
+
const to = getNextState(from, action);
|
|
284
|
+
|
|
285
|
+
if (!to) {
|
|
286
|
+
const error = new StateMachineError(
|
|
287
|
+
`Cannot perform '${action}' from state '${from}'. Available actions: ${this.getAvailableActions().join(', ') || 'none'}`,
|
|
288
|
+
{
|
|
289
|
+
currentState: from,
|
|
290
|
+
action,
|
|
291
|
+
code: 'INVALID_TRANSITION',
|
|
292
|
+
}
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
this.emit('invalidTransition', { currentState: from, action, error });
|
|
296
|
+
|
|
297
|
+
if (this._strict) {
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
return { ok: false, error };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Perform transition
|
|
304
|
+
this._state = to;
|
|
305
|
+
|
|
306
|
+
const timestamp = new Date();
|
|
307
|
+
|
|
308
|
+
if (this._historyEnabled) {
|
|
309
|
+
this._history.push({ state: to, action, from, timestamp });
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Emit events
|
|
313
|
+
this.emit('transition', { from, to, action, timestamp });
|
|
314
|
+
this.emit('stateChange', { state: to });
|
|
315
|
+
|
|
316
|
+
return { ok: true, from, to };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Attempt transition without throwing on failure
|
|
321
|
+
* @param {string} action - Action to perform
|
|
322
|
+
* @returns {boolean} True if transition succeeded
|
|
323
|
+
*/
|
|
324
|
+
tryTransition(action) {
|
|
325
|
+
try {
|
|
326
|
+
const result = this.transition(action);
|
|
327
|
+
return result.ok;
|
|
328
|
+
} catch {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Reset to initial state
|
|
335
|
+
* @param {string} [state='idle'] - State to reset to
|
|
336
|
+
*/
|
|
337
|
+
reset(state = SessionState.IDLE) {
|
|
338
|
+
if (!isValidState(state)) {
|
|
339
|
+
throw new StateMachineError(`Invalid state: ${state}`, {
|
|
340
|
+
code: 'INVALID_STATE',
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const from = this._state;
|
|
345
|
+
this._state = state;
|
|
346
|
+
|
|
347
|
+
if (this._historyEnabled) {
|
|
348
|
+
this._history.push({ state, action: 'reset', from, timestamp: new Date() });
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
this.emit('transition', { from, to: state, action: 'reset', timestamp: new Date() });
|
|
352
|
+
this.emit('stateChange', { state });
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Check if state matches
|
|
357
|
+
* @param {string} state - State to check
|
|
358
|
+
* @returns {boolean}
|
|
359
|
+
*/
|
|
360
|
+
is(state) {
|
|
361
|
+
return this._state === state;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Check if state is one of given states
|
|
366
|
+
* @param {...string} states - States to check
|
|
367
|
+
* @returns {boolean}
|
|
368
|
+
*/
|
|
369
|
+
isOneOf(...states) {
|
|
370
|
+
return states.includes(this._state);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Serialize state machine
|
|
375
|
+
* @returns {Object}
|
|
376
|
+
*/
|
|
377
|
+
serialize() {
|
|
378
|
+
return {
|
|
379
|
+
state: this._state,
|
|
380
|
+
history: this._historyEnabled ? this._history : undefined,
|
|
381
|
+
metadata: Object.keys(this._metadata).length > 0 ? this._metadata : undefined,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Deserialize state machine
|
|
387
|
+
* @param {Object} data - Serialized data
|
|
388
|
+
* @param {Object} [options={}] - Options
|
|
389
|
+
* @returns {SessionStateMachine}
|
|
390
|
+
*/
|
|
391
|
+
static deserialize(data, options = {}) {
|
|
392
|
+
const sm = new SessionStateMachine(data.state, {
|
|
393
|
+
...options,
|
|
394
|
+
historyEnabled: !!data.history,
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
if (data.history) {
|
|
398
|
+
sm._history = data.history.map(h => ({
|
|
399
|
+
...h,
|
|
400
|
+
timestamp: new Date(h.timestamp),
|
|
401
|
+
}));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (data.metadata) {
|
|
405
|
+
sm._metadata = data.metadata;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return sm;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Create state machine diagram (Mermaid format)
|
|
413
|
+
* @returns {string}
|
|
414
|
+
*/
|
|
415
|
+
static toMermaid() {
|
|
416
|
+
const lines = ['stateDiagram-v2'];
|
|
417
|
+
|
|
418
|
+
for (const [state, transitions] of Object.entries(TRANSITIONS)) {
|
|
419
|
+
for (const [action, nextState] of Object.entries(transitions)) {
|
|
420
|
+
lines.push(` ${state} --> ${nextState}: ${action}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Mark initial and final states
|
|
425
|
+
lines.push(' [*] --> idle');
|
|
426
|
+
lines.push(' terminated --> [*]');
|
|
427
|
+
|
|
428
|
+
return lines.join('\n');
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Create a pre-configured state machine for sessions
|
|
434
|
+
* @param {Object} [options={}] - Options
|
|
435
|
+
* @returns {SessionStateMachine}
|
|
436
|
+
*/
|
|
437
|
+
function createSessionStateMachine(options = {}) {
|
|
438
|
+
return new SessionStateMachine(SessionState.IDLE, options);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
module.exports = {
|
|
442
|
+
// Enums
|
|
443
|
+
SessionState,
|
|
444
|
+
SessionAction,
|
|
445
|
+
|
|
446
|
+
// Constants
|
|
447
|
+
TRANSITIONS,
|
|
448
|
+
|
|
449
|
+
// Utility functions
|
|
450
|
+
getValidStates,
|
|
451
|
+
getValidActions,
|
|
452
|
+
isValidState,
|
|
453
|
+
isValidAction,
|
|
454
|
+
getTransitionsFromState,
|
|
455
|
+
getAvailableActions,
|
|
456
|
+
canTransition,
|
|
457
|
+
getNextState,
|
|
458
|
+
|
|
459
|
+
// Classes
|
|
460
|
+
StateMachineError,
|
|
461
|
+
SessionStateMachine,
|
|
462
|
+
|
|
463
|
+
// Factory
|
|
464
|
+
createSessionStateMachine,
|
|
465
|
+
};
|