agileflow 2.94.1 → 2.95.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 +20 -0
- package/README.md +3 -3
- package/lib/colors.generated.js +117 -0
- package/lib/colors.js +59 -109
- package/lib/generator-factory.js +333 -0
- package/lib/path-utils.js +49 -0
- package/lib/session-registry.js +25 -15
- package/lib/smart-json-file.js +40 -32
- package/lib/state-machine.js +286 -0
- package/package.json +1 -1
- package/scripts/agileflow-configure.js +7 -6
- package/scripts/archive-completed-stories.sh +86 -11
- package/scripts/babysit-context-restore.js +89 -0
- package/scripts/claude-tmux.sh +111 -5
- package/scripts/damage-control/bash-tool-damage-control.js +11 -247
- package/scripts/damage-control/edit-tool-damage-control.js +9 -249
- package/scripts/damage-control/write-tool-damage-control.js +9 -244
- package/scripts/generate-colors.js +314 -0
- package/scripts/lib/colors.generated.sh +82 -0
- package/scripts/lib/colors.sh +10 -70
- package/scripts/lib/configure-features.js +401 -0
- package/scripts/lib/context-loader.js +181 -52
- package/scripts/precompact-context.sh +54 -17
- package/scripts/session-coordinator.sh +2 -2
- package/scripts/session-manager.js +653 -10
- package/src/core/commands/audit.md +93 -0
- package/src/core/commands/auto.md +73 -0
- package/src/core/commands/babysit.md +169 -13
- package/src/core/commands/baseline.md +73 -0
- package/src/core/commands/batch.md +64 -0
- package/src/core/commands/blockers.md +60 -0
- package/src/core/commands/board.md +66 -0
- package/src/core/commands/choose.md +77 -0
- package/src/core/commands/ci.md +77 -0
- package/src/core/commands/compress.md +27 -1
- package/src/core/commands/configure.md +126 -10
- package/src/core/commands/council.md +74 -0
- package/src/core/commands/debt.md +72 -0
- package/src/core/commands/deploy.md +73 -0
- package/src/core/commands/deps.md +68 -0
- package/src/core/commands/docs.md +60 -0
- package/src/core/commands/feedback.md +68 -0
- package/src/core/commands/ideate.md +74 -0
- package/src/core/commands/impact.md +74 -0
- package/src/core/commands/install.md +529 -0
- package/src/core/commands/maintain.md +558 -0
- package/src/core/commands/metrics.md +75 -0
- package/src/core/commands/multi-expert.md +74 -0
- package/src/core/commands/packages.md +69 -0
- package/src/core/commands/readme-sync.md +64 -0
- package/src/core/commands/research/analyze.md +285 -121
- package/src/core/commands/research/import.md +281 -109
- package/src/core/commands/retro.md +76 -0
- package/src/core/commands/review.md +72 -0
- package/src/core/commands/rlm.md +83 -0
- package/src/core/commands/rpi.md +90 -0
- package/src/core/commands/session/cleanup.md +214 -12
- package/src/core/commands/session/end.md +155 -17
- package/src/core/commands/sprint.md +72 -0
- package/src/core/commands/story-validate.md +68 -0
- package/src/core/commands/template.md +69 -0
- package/src/core/commands/tests.md +83 -0
- package/src/core/commands/update.md +59 -0
- package/src/core/commands/validate-expertise.md +76 -0
- package/src/core/commands/velocity.md +74 -0
- package/src/core/commands/verify.md +91 -0
- package/src/core/commands/whats-new.md +69 -0
- package/src/core/commands/workflow.md +88 -0
- package/src/core/templates/command-documentation.md +187 -0
- package/tools/cli/commands/session.js +1171 -0
- package/tools/cli/commands/setup.js +2 -81
- package/tools/cli/installers/core/installer.js +0 -5
- package/tools/cli/installers/ide/claude-code.js +6 -0
- package/tools/cli/lib/config-manager.js +42 -5
package/lib/smart-json-file.js
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
const fs = require('fs');
|
|
32
32
|
const path = require('path');
|
|
33
33
|
const { createTypedError, getErrorCodeFromError, ErrorCodes } = require('./error-codes');
|
|
34
|
+
const { success, failure, failureFromError } = require('./result-schema');
|
|
34
35
|
|
|
35
36
|
// Debug logging
|
|
36
37
|
const DEBUG = process.env.AGILEFLOW_DEBUG === '1';
|
|
@@ -54,7 +55,7 @@ const SECURE_FILE_MODE = 0o600;
|
|
|
54
55
|
function checkFilePermissions(mode) {
|
|
55
56
|
// Skip permission checks on Windows (different permission model)
|
|
56
57
|
if (process.platform === 'win32') {
|
|
57
|
-
return
|
|
58
|
+
return success(undefined);
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
// Extract permission bits (last 9 bits)
|
|
@@ -106,7 +107,7 @@ function checkFilePermissions(mode) {
|
|
|
106
107
|
};
|
|
107
108
|
}
|
|
108
109
|
|
|
109
|
-
return
|
|
110
|
+
return success(undefined);
|
|
110
111
|
}
|
|
111
112
|
|
|
112
113
|
/**
|
|
@@ -117,13 +118,13 @@ function checkFilePermissions(mode) {
|
|
|
117
118
|
function setSecurePermissions(filePath) {
|
|
118
119
|
// Skip on Windows
|
|
119
120
|
if (process.platform === 'win32') {
|
|
120
|
-
return
|
|
121
|
+
return success(undefined);
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
try {
|
|
124
125
|
fs.chmodSync(filePath, SECURE_FILE_MODE);
|
|
125
126
|
debugLog('setSecurePermissions', { filePath, mode: SECURE_FILE_MODE.toString(8) });
|
|
126
|
-
return
|
|
127
|
+
return success(undefined);
|
|
127
128
|
} catch (err) {
|
|
128
129
|
const error = createTypedError(
|
|
129
130
|
`Failed to set secure permissions on ${filePath}: ${err.message}`,
|
|
@@ -133,7 +134,7 @@ function setSecurePermissions(filePath) {
|
|
|
133
134
|
context: { filePath, mode: SECURE_FILE_MODE },
|
|
134
135
|
}
|
|
135
136
|
);
|
|
136
|
-
return
|
|
137
|
+
return failureFromError(error);
|
|
137
138
|
}
|
|
138
139
|
}
|
|
139
140
|
|
|
@@ -221,12 +222,12 @@ class SmartJsonFile {
|
|
|
221
222
|
if (!fs.existsSync(this.filePath)) {
|
|
222
223
|
if (this.defaultValue !== undefined) {
|
|
223
224
|
debugLog('read', { status: 'using default value' });
|
|
224
|
-
return
|
|
225
|
+
return success(this.defaultValue);
|
|
225
226
|
}
|
|
226
227
|
const error = createTypedError(`File not found: ${this.filePath}`, 'ENOENT', {
|
|
227
228
|
context: { filePath: this.filePath },
|
|
228
229
|
});
|
|
229
|
-
return
|
|
230
|
+
return failureFromError(error);
|
|
230
231
|
}
|
|
231
232
|
|
|
232
233
|
// Read file
|
|
@@ -257,7 +258,7 @@ class SmartJsonFile {
|
|
|
257
258
|
'EPARSE',
|
|
258
259
|
{ cause: parseError, context: { filePath: this.filePath } }
|
|
259
260
|
);
|
|
260
|
-
return
|
|
261
|
+
return failureFromError(error);
|
|
261
262
|
}
|
|
262
263
|
|
|
263
264
|
// Validate schema if provided
|
|
@@ -270,12 +271,12 @@ class SmartJsonFile {
|
|
|
270
271
|
'ESCHEMA',
|
|
271
272
|
{ cause: schemaError, context: { filePath: this.filePath } }
|
|
272
273
|
);
|
|
273
|
-
return
|
|
274
|
+
return failureFromError(error);
|
|
274
275
|
}
|
|
275
276
|
}
|
|
276
277
|
|
|
277
278
|
debugLog('read', { status: 'success' });
|
|
278
|
-
return
|
|
279
|
+
return success(data);
|
|
279
280
|
} catch (err) {
|
|
280
281
|
lastError = err;
|
|
281
282
|
debugLog('read', { status: 'error', error: err.message, attempt });
|
|
@@ -288,7 +289,7 @@ class SmartJsonFile {
|
|
|
288
289
|
cause: err,
|
|
289
290
|
context: { filePath: this.filePath },
|
|
290
291
|
});
|
|
291
|
-
return
|
|
292
|
+
return failureFromError(error);
|
|
292
293
|
}
|
|
293
294
|
|
|
294
295
|
// Wait before retrying
|
|
@@ -308,7 +309,7 @@ class SmartJsonFile {
|
|
|
308
309
|
'EUNKNOWN',
|
|
309
310
|
{ cause: lastError, context: { filePath: this.filePath, attempts: this.retries + 1 } }
|
|
310
311
|
);
|
|
311
|
-
return
|
|
312
|
+
return failureFromError(error);
|
|
312
313
|
}
|
|
313
314
|
|
|
314
315
|
/**
|
|
@@ -330,7 +331,7 @@ class SmartJsonFile {
|
|
|
330
331
|
'ESCHEMA',
|
|
331
332
|
{ cause: schemaError, context: { filePath: this.filePath } }
|
|
332
333
|
);
|
|
333
|
-
return
|
|
334
|
+
return failureFromError(error);
|
|
334
335
|
}
|
|
335
336
|
}
|
|
336
337
|
|
|
@@ -368,7 +369,7 @@ class SmartJsonFile {
|
|
|
368
369
|
|
|
369
370
|
debugLog('write', { status: 'success' });
|
|
370
371
|
|
|
371
|
-
return
|
|
372
|
+
return success(undefined);
|
|
372
373
|
} catch (err) {
|
|
373
374
|
lastError = err;
|
|
374
375
|
debugLog('write', { status: 'error', error: err.message, attempt });
|
|
@@ -394,7 +395,7 @@ class SmartJsonFile {
|
|
|
394
395
|
errorCode.code,
|
|
395
396
|
{ cause: err, context: { filePath: this.filePath } }
|
|
396
397
|
);
|
|
397
|
-
return
|
|
398
|
+
return failureFromError(error);
|
|
398
399
|
}
|
|
399
400
|
|
|
400
401
|
// Wait before retrying
|
|
@@ -414,7 +415,7 @@ class SmartJsonFile {
|
|
|
414
415
|
'EUNKNOWN',
|
|
415
416
|
{ cause: lastError, context: { filePath: this.filePath, attempts: this.retries + 1 } }
|
|
416
417
|
);
|
|
417
|
-
return
|
|
418
|
+
return failureFromError(error);
|
|
418
419
|
}
|
|
419
420
|
|
|
420
421
|
/**
|
|
@@ -447,7 +448,7 @@ class SmartJsonFile {
|
|
|
447
448
|
cause: modifyError,
|
|
448
449
|
context: { filePath: this.filePath },
|
|
449
450
|
});
|
|
450
|
-
return
|
|
451
|
+
return failureFromError(error);
|
|
451
452
|
}
|
|
452
453
|
|
|
453
454
|
// Write modified data
|
|
@@ -456,7 +457,7 @@ class SmartJsonFile {
|
|
|
456
457
|
return writeResult;
|
|
457
458
|
}
|
|
458
459
|
|
|
459
|
-
return
|
|
460
|
+
return success(newData);
|
|
460
461
|
}
|
|
461
462
|
|
|
462
463
|
/**
|
|
@@ -474,12 +475,12 @@ class SmartJsonFile {
|
|
|
474
475
|
async delete() {
|
|
475
476
|
try {
|
|
476
477
|
if (!fs.existsSync(this.filePath)) {
|
|
477
|
-
return
|
|
478
|
+
return success(undefined); // Already doesn't exist
|
|
478
479
|
}
|
|
479
480
|
|
|
480
481
|
fs.unlinkSync(this.filePath);
|
|
481
482
|
debugLog('delete', { filePath: this.filePath, status: 'success' });
|
|
482
|
-
return
|
|
483
|
+
return success(undefined);
|
|
483
484
|
} catch (err) {
|
|
484
485
|
const errorCode = getErrorCodeFromError(err);
|
|
485
486
|
const error = createTypedError(
|
|
@@ -487,7 +488,7 @@ class SmartJsonFile {
|
|
|
487
488
|
errorCode.code,
|
|
488
489
|
{ cause: err, context: { filePath: this.filePath } }
|
|
489
490
|
);
|
|
490
|
-
return
|
|
491
|
+
return failureFromError(error);
|
|
491
492
|
}
|
|
492
493
|
}
|
|
493
494
|
|
|
@@ -499,12 +500,12 @@ class SmartJsonFile {
|
|
|
499
500
|
try {
|
|
500
501
|
if (!fs.existsSync(this.filePath)) {
|
|
501
502
|
if (this.defaultValue !== undefined) {
|
|
502
|
-
return
|
|
503
|
+
return success(this.defaultValue);
|
|
503
504
|
}
|
|
504
505
|
const error = createTypedError(`File not found: ${this.filePath}`, 'ENOENT', {
|
|
505
506
|
context: { filePath: this.filePath },
|
|
506
507
|
});
|
|
507
|
-
return
|
|
508
|
+
return failureFromError(error);
|
|
508
509
|
}
|
|
509
510
|
|
|
510
511
|
const content = fs.readFileSync(this.filePath, 'utf8');
|
|
@@ -514,7 +515,7 @@ class SmartJsonFile {
|
|
|
514
515
|
this.schema(data);
|
|
515
516
|
}
|
|
516
517
|
|
|
517
|
-
return
|
|
518
|
+
return success(data);
|
|
518
519
|
} catch (err) {
|
|
519
520
|
const errorCode = getErrorCodeFromError(err);
|
|
520
521
|
const error = createTypedError(
|
|
@@ -522,7 +523,7 @@ class SmartJsonFile {
|
|
|
522
523
|
errorCode.code,
|
|
523
524
|
{ cause: err, context: { filePath: this.filePath } }
|
|
524
525
|
);
|
|
525
|
-
return
|
|
526
|
+
return failureFromError(error);
|
|
526
527
|
}
|
|
527
528
|
}
|
|
528
529
|
|
|
@@ -559,7 +560,7 @@ class SmartJsonFile {
|
|
|
559
560
|
}
|
|
560
561
|
}
|
|
561
562
|
|
|
562
|
-
return
|
|
563
|
+
return success(undefined);
|
|
563
564
|
} catch (err) {
|
|
564
565
|
// Clean up temp file
|
|
565
566
|
try {
|
|
@@ -576,7 +577,7 @@ class SmartJsonFile {
|
|
|
576
577
|
errorCode.code,
|
|
577
578
|
{ cause: err, context: { filePath: this.filePath } }
|
|
578
579
|
);
|
|
579
|
-
return
|
|
580
|
+
return failureFromError(error);
|
|
580
581
|
}
|
|
581
582
|
}
|
|
582
583
|
}
|
|
@@ -594,7 +595,7 @@ const DEFAULT_TEMP_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
|
594
595
|
* @param {Object} [options={}] - Cleanup options
|
|
595
596
|
* @param {number} [options.maxAgeMs=86400000] - Max age in ms (default: 24 hours)
|
|
596
597
|
* @param {boolean} [options.dryRun=false] - Don't delete, just report
|
|
597
|
-
* @returns {{
|
|
598
|
+
* @returns {Result<{cleaned: string[], errors: string[]}>}
|
|
598
599
|
*/
|
|
599
600
|
function cleanupTempFiles(directory, options = {}) {
|
|
600
601
|
const { maxAgeMs = DEFAULT_TEMP_MAX_AGE_MS, dryRun = false } = options;
|
|
@@ -604,7 +605,7 @@ function cleanupTempFiles(directory, options = {}) {
|
|
|
604
605
|
|
|
605
606
|
try {
|
|
606
607
|
if (!fs.existsSync(directory)) {
|
|
607
|
-
return {
|
|
608
|
+
return success({ cleaned, errors });
|
|
608
609
|
}
|
|
609
610
|
|
|
610
611
|
const now = Date.now();
|
|
@@ -646,10 +647,17 @@ function cleanupTempFiles(directory, options = {}) {
|
|
|
646
647
|
}
|
|
647
648
|
}
|
|
648
649
|
|
|
649
|
-
|
|
650
|
+
if (errors.length > 0) {
|
|
651
|
+
return failure('EUNKNOWN', 'Some temp files could not be cleaned', {
|
|
652
|
+
context: { cleaned, errors },
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
return success({ cleaned, errors });
|
|
650
656
|
} catch (err) {
|
|
651
657
|
errors.push(`Directory read error: ${err.message}`);
|
|
652
|
-
return
|
|
658
|
+
return failure('EUNKNOWN', `Directory read error: ${err.message}`, {
|
|
659
|
+
context: { cleaned, errors },
|
|
660
|
+
});
|
|
653
661
|
}
|
|
654
662
|
}
|
|
655
663
|
|
|
@@ -658,7 +666,7 @@ function cleanupTempFiles(directory, options = {}) {
|
|
|
658
666
|
*
|
|
659
667
|
* @param {string} filePath - Path to the JSON file
|
|
660
668
|
* @param {Object} [options={}] - Cleanup options
|
|
661
|
-
* @returns {{
|
|
669
|
+
* @returns {Result<{cleaned: string[], errors: string[]}>}
|
|
662
670
|
*/
|
|
663
671
|
function cleanupTempFilesFor(filePath, options = {}) {
|
|
664
672
|
const directory = path.dirname(filePath);
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* state-machine.js - Generic State Machine Base Class
|
|
3
|
+
*
|
|
4
|
+
* Provides a reusable state machine pattern for:
|
|
5
|
+
* - Story status transitions (ready → in_progress → completed)
|
|
6
|
+
* - Session thread type transitions (base → parallel → fusion)
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Configurable states and transitions
|
|
10
|
+
* - Transition validation with clear error messages
|
|
11
|
+
* - Audit trail support
|
|
12
|
+
* - Force mode for admin overrides
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* const { StateMachine } = require('./state-machine');
|
|
16
|
+
*
|
|
17
|
+
* const storyMachine = new StateMachine({
|
|
18
|
+
* states: ['ready', 'in_progress', 'completed'],
|
|
19
|
+
* transitions: {
|
|
20
|
+
* ready: ['in_progress'],
|
|
21
|
+
* in_progress: ['completed', 'ready'],
|
|
22
|
+
* completed: [],
|
|
23
|
+
* },
|
|
24
|
+
* initial: 'ready',
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* const result = storyMachine.transition('ready', 'in_progress');
|
|
28
|
+
* // { success: true, from: 'ready', to: 'in_progress' }
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Generic State Machine
|
|
33
|
+
*/
|
|
34
|
+
class StateMachine {
|
|
35
|
+
/**
|
|
36
|
+
* @param {Object} config - State machine configuration
|
|
37
|
+
* @param {string[]} config.states - Valid state values
|
|
38
|
+
* @param {Object<string, string[]>} config.transitions - Map of state -> allowed next states
|
|
39
|
+
* @param {string} [config.initial] - Initial state (first in states array if not specified)
|
|
40
|
+
* @param {string} [config.name='state'] - Name for error messages (e.g., 'status', 'thread_type')
|
|
41
|
+
*/
|
|
42
|
+
constructor(config) {
|
|
43
|
+
if (!config.states || !Array.isArray(config.states) || config.states.length === 0) {
|
|
44
|
+
throw new Error('StateMachine requires non-empty states array');
|
|
45
|
+
}
|
|
46
|
+
if (!config.transitions || typeof config.transitions !== 'object') {
|
|
47
|
+
throw new Error('StateMachine requires transitions object');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.states = config.states;
|
|
51
|
+
this.transitions = config.transitions;
|
|
52
|
+
this.initial = config.initial || config.states[0];
|
|
53
|
+
this.name = config.name || 'state';
|
|
54
|
+
|
|
55
|
+
// Validate that all transition targets are valid states
|
|
56
|
+
for (const [from, targets] of Object.entries(this.transitions)) {
|
|
57
|
+
if (!this.states.includes(from)) {
|
|
58
|
+
throw new Error(`Invalid transition source state: ${from}`);
|
|
59
|
+
}
|
|
60
|
+
for (const to of targets) {
|
|
61
|
+
if (!this.states.includes(to)) {
|
|
62
|
+
throw new Error(`Invalid transition target state: ${to} (from ${from})`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if a state is valid
|
|
70
|
+
* @param {string} state - State to check
|
|
71
|
+
* @returns {boolean}
|
|
72
|
+
*/
|
|
73
|
+
isValidState(state) {
|
|
74
|
+
return this.states.includes(state);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check if a transition is valid
|
|
79
|
+
* @param {string} from - Current state
|
|
80
|
+
* @param {string} to - Target state
|
|
81
|
+
* @returns {boolean}
|
|
82
|
+
*/
|
|
83
|
+
isValidTransition(from, to) {
|
|
84
|
+
// Same state is always valid (no-op)
|
|
85
|
+
if (from === to) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Check if from state has defined transitions
|
|
90
|
+
const allowed = this.transitions[from];
|
|
91
|
+
if (!allowed) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return allowed.includes(to);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get valid transitions from a state
|
|
100
|
+
* @param {string} from - Current state
|
|
101
|
+
* @returns {string[]}
|
|
102
|
+
*/
|
|
103
|
+
getValidTransitions(from) {
|
|
104
|
+
return this.transitions[from] || [];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Validate and perform a transition
|
|
109
|
+
* @param {string} from - Current state
|
|
110
|
+
* @param {string} to - Target state
|
|
111
|
+
* @param {Object} [options={}] - Transition options
|
|
112
|
+
* @param {boolean} [options.force=false] - Force transition even if invalid
|
|
113
|
+
* @returns {{success: boolean, from: string, to: string, error?: string, forced?: boolean}}
|
|
114
|
+
*/
|
|
115
|
+
transition(from, to, options = {}) {
|
|
116
|
+
const { force = false } = options;
|
|
117
|
+
|
|
118
|
+
// Validate target state
|
|
119
|
+
if (!this.isValidState(to)) {
|
|
120
|
+
return {
|
|
121
|
+
success: false,
|
|
122
|
+
from,
|
|
123
|
+
to,
|
|
124
|
+
error: `Invalid ${this.name}: "${to}". Valid values: ${this.states.join(', ')}`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Validate source state
|
|
129
|
+
if (!this.isValidState(from)) {
|
|
130
|
+
return {
|
|
131
|
+
success: false,
|
|
132
|
+
from,
|
|
133
|
+
to,
|
|
134
|
+
error: `Invalid source ${this.name}: "${from}". Valid values: ${this.states.join(', ')}`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Same state is a no-op
|
|
139
|
+
if (from === to) {
|
|
140
|
+
return {
|
|
141
|
+
success: true,
|
|
142
|
+
from,
|
|
143
|
+
to,
|
|
144
|
+
noop: true,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Check transition validity
|
|
149
|
+
if (!force && !this.isValidTransition(from, to)) {
|
|
150
|
+
const validTargets = this.getValidTransitions(from);
|
|
151
|
+
return {
|
|
152
|
+
success: false,
|
|
153
|
+
from,
|
|
154
|
+
to,
|
|
155
|
+
error: `Invalid transition: ${from} → ${to}. Valid transitions from "${from}": ${validTargets.join(', ') || 'none'}`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
success: true,
|
|
161
|
+
from,
|
|
162
|
+
to,
|
|
163
|
+
forced: force && !this.isValidTransition(from, to),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Get the initial state
|
|
169
|
+
* @returns {string}
|
|
170
|
+
*/
|
|
171
|
+
getInitialState() {
|
|
172
|
+
return this.initial;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Get all valid states
|
|
177
|
+
* @returns {string[]}
|
|
178
|
+
*/
|
|
179
|
+
getStates() {
|
|
180
|
+
return [...this.states];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get all transitions as a map
|
|
185
|
+
* @returns {Object<string, string[]>}
|
|
186
|
+
*/
|
|
187
|
+
getTransitionsMap() {
|
|
188
|
+
return { ...this.transitions };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Generate a Mermaid state diagram
|
|
193
|
+
* @returns {string}
|
|
194
|
+
*/
|
|
195
|
+
toMermaidDiagram() {
|
|
196
|
+
const lines = ['stateDiagram-v2'];
|
|
197
|
+
|
|
198
|
+
// Add initial state arrow
|
|
199
|
+
lines.push(` [*] --> ${this.initial}`);
|
|
200
|
+
|
|
201
|
+
// Add transitions
|
|
202
|
+
for (const [from, targets] of Object.entries(this.transitions)) {
|
|
203
|
+
for (const to of targets) {
|
|
204
|
+
lines.push(` ${from} --> ${to}`);
|
|
205
|
+
}
|
|
206
|
+
// Mark terminal states
|
|
207
|
+
if (targets.length === 0) {
|
|
208
|
+
lines.push(` ${from} --> [*]`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return lines.join('\n');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ============================================================================
|
|
217
|
+
// Pre-configured State Machines
|
|
218
|
+
// ============================================================================
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Story status state machine
|
|
222
|
+
*
|
|
223
|
+
* States: ready, in_progress, in_review, blocked, completed, archived
|
|
224
|
+
*
|
|
225
|
+
* Transitions:
|
|
226
|
+
* - ready → in_progress, blocked
|
|
227
|
+
* - in_progress → in_review, blocked, ready
|
|
228
|
+
* - in_review → completed, in_progress, blocked
|
|
229
|
+
* - blocked → ready, in_progress, in_review
|
|
230
|
+
* - completed → archived, in_progress (reopened)
|
|
231
|
+
* - archived → (terminal)
|
|
232
|
+
*/
|
|
233
|
+
const storyStatusMachine = new StateMachine({
|
|
234
|
+
name: 'status',
|
|
235
|
+
states: ['ready', 'in_progress', 'in_review', 'blocked', 'completed', 'archived'],
|
|
236
|
+
transitions: {
|
|
237
|
+
ready: ['in_progress', 'blocked'],
|
|
238
|
+
in_progress: ['in_review', 'blocked', 'ready'],
|
|
239
|
+
in_review: ['completed', 'in_progress', 'blocked'],
|
|
240
|
+
blocked: ['ready', 'in_progress', 'in_review'],
|
|
241
|
+
completed: ['archived', 'in_progress'],
|
|
242
|
+
archived: [], // Terminal state
|
|
243
|
+
},
|
|
244
|
+
initial: 'ready',
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Session thread type state machine
|
|
249
|
+
*
|
|
250
|
+
* States: base, parallel, chained, fusion, big, long
|
|
251
|
+
*
|
|
252
|
+
* Thread Type Semantics:
|
|
253
|
+
* - base: Main session in project root (default)
|
|
254
|
+
* - parallel: Independent worktree session
|
|
255
|
+
* - chained: Sequential dependency on another session
|
|
256
|
+
* - fusion: Merged work from multiple sessions
|
|
257
|
+
* - big: Large task spanning multiple sessions
|
|
258
|
+
* - long: Extended session with context preservation
|
|
259
|
+
*
|
|
260
|
+
* Transitions:
|
|
261
|
+
* - base → parallel (spawn worktree)
|
|
262
|
+
* - parallel → base (merge to main), fusion (merge multiple), chained (add dependency)
|
|
263
|
+
* - chained → parallel (remove dependency), fusion (complete chain)
|
|
264
|
+
* - fusion → base (merge to main)
|
|
265
|
+
* - big → parallel (split), fusion (consolidate)
|
|
266
|
+
* - long → base (complete), parallel (split)
|
|
267
|
+
*/
|
|
268
|
+
const sessionThreadMachine = new StateMachine({
|
|
269
|
+
name: 'thread_type',
|
|
270
|
+
states: ['base', 'parallel', 'chained', 'fusion', 'big', 'long'],
|
|
271
|
+
transitions: {
|
|
272
|
+
base: ['parallel', 'big', 'long'],
|
|
273
|
+
parallel: ['base', 'fusion', 'chained'],
|
|
274
|
+
chained: ['parallel', 'fusion'],
|
|
275
|
+
fusion: ['base'],
|
|
276
|
+
big: ['parallel', 'fusion'],
|
|
277
|
+
long: ['base', 'parallel'],
|
|
278
|
+
},
|
|
279
|
+
initial: 'base',
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
module.exports = {
|
|
283
|
+
StateMachine,
|
|
284
|
+
storyStatusMachine,
|
|
285
|
+
sessionThreadMachine,
|
|
286
|
+
};
|
package/package.json
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* --detect Show current status
|
|
24
24
|
* --help Show help
|
|
25
25
|
*
|
|
26
|
-
* Features: sessionstart, precompact, ralphloop, selfimprove, archival, statusline, autoupdate, damagecontrol, askuserquestion, tmuxautospawn
|
|
26
|
+
* Features: sessionstart, precompact, ralphloop, selfimprove, archival, statusline, autoupdate, damagecontrol, askuserquestion, tmuxautospawn, shellaliases, claudemdreinforcement
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
29
|
const fs = require('fs');
|
|
@@ -121,16 +121,17 @@ ${c.cyan}Usage:${c.reset}
|
|
|
121
121
|
node .agileflow/scripts/agileflow-configure.js [options]
|
|
122
122
|
|
|
123
123
|
${c.cyan}Profiles:${c.reset}
|
|
124
|
-
--profile=full
|
|
125
|
-
--profile=basic
|
|
126
|
-
--profile=minimal
|
|
127
|
-
--profile=
|
|
124
|
+
--profile=full All features (hooks, Stop hooks, archival, statusline)
|
|
125
|
+
--profile=basic SessionStart + PreCompact + archival (no Stop hooks)
|
|
126
|
+
--profile=minimal SessionStart + archival only
|
|
127
|
+
--profile=experimental ⚠️ All features + FULL FILE injection during compact (CONTEXT HEAVY)
|
|
128
|
+
--profile=none Disable all AgileFlow features
|
|
128
129
|
|
|
129
130
|
${c.cyan}Feature Control:${c.reset}
|
|
130
131
|
--enable=<list> Enable features (comma-separated)
|
|
131
132
|
--disable=<list> Disable features (comma-separated)
|
|
132
133
|
|
|
133
|
-
Features: sessionstart, precompact, ralphloop, selfimprove, archival, statusline, damagecontrol, askuserquestion, tmuxautospawn
|
|
134
|
+
Features: sessionstart, precompact, ralphloop, selfimprove, archival, statusline, damagecontrol, askuserquestion, tmuxautospawn, shellaliases, claudemdreinforcement
|
|
134
135
|
|
|
135
136
|
${c.cyan}Statusline Components:${c.reset}
|
|
136
137
|
--show=<list> Show statusline components (comma-separated)
|