agileflow 2.94.1 → 2.95.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/CHANGELOG.md +15 -0
- 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
|
@@ -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)
|
|
@@ -105,13 +105,87 @@ echo -e "${BLUE}Cutoff date: $CUTOFF_DATE${NC}"
|
|
|
105
105
|
|
|
106
106
|
# Archive using Node.js (more reliable for JSON manipulation)
|
|
107
107
|
if command -v node &> /dev/null; then
|
|
108
|
-
STATUS_FILE="$STATUS_FILE" ARCHIVE_DIR="$ARCHIVE_DIR" CUTOFF_DATE="$CUTOFF_DATE" node <<'EOF'
|
|
108
|
+
STATUS_FILE="$STATUS_FILE" ARCHIVE_DIR="$ARCHIVE_DIR" CUTOFF_DATE="$CUTOFF_DATE" PROJECT_ROOT="$PROJECT_ROOT" node <<'EOF'
|
|
109
109
|
const fs = require('fs');
|
|
110
110
|
const path = require('path');
|
|
111
111
|
|
|
112
112
|
const statusFile = process.env.STATUS_FILE;
|
|
113
113
|
const archiveDir = process.env.ARCHIVE_DIR;
|
|
114
114
|
const cutoffDate = process.env.CUTOFF_DATE;
|
|
115
|
+
const projectRoot = process.env.PROJECT_ROOT;
|
|
116
|
+
|
|
117
|
+
// =============================================================================
|
|
118
|
+
// Security: Inline validatePath equivalent (US-0188)
|
|
119
|
+
// =============================================================================
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Validate a path is safe and within the base directory.
|
|
123
|
+
* Rejects direct symlinks within the path but allows symlinked parent directories
|
|
124
|
+
* (needed for git worktrees where docs/ is often symlinked).
|
|
125
|
+
* @param {string} inputPath - Path to validate
|
|
126
|
+
* @param {string} baseDir - Allowed base directory
|
|
127
|
+
* @returns {{ ok: boolean, resolvedPath?: string, realPath?: string, error?: string }}
|
|
128
|
+
*/
|
|
129
|
+
function validatePath(inputPath, baseDir) {
|
|
130
|
+
if (!inputPath || typeof inputPath !== 'string') {
|
|
131
|
+
return { ok: false, error: 'Path is required and must be a string' };
|
|
132
|
+
}
|
|
133
|
+
if (!baseDir || typeof baseDir !== 'string') {
|
|
134
|
+
return { ok: false, error: 'Base directory is required' };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Resolve to absolute path
|
|
138
|
+
const resolvedPath = path.resolve(baseDir, inputPath);
|
|
139
|
+
const resolvedBase = path.resolve(baseDir);
|
|
140
|
+
|
|
141
|
+
// Check path stays within base directory (path traversal prevention)
|
|
142
|
+
if (!resolvedPath.startsWith(resolvedBase + path.sep) && resolvedPath !== resolvedBase) {
|
|
143
|
+
return { ok: false, error: `Path traversal detected: ${inputPath} escapes ${baseDir}` };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Check if the final target path itself is a symlink (allowSymlinks: false for target)
|
|
147
|
+
// Note: We allow parent directories to be symlinks (needed for git worktrees)
|
|
148
|
+
try {
|
|
149
|
+
const stats = fs.lstatSync(resolvedPath);
|
|
150
|
+
if (stats.isSymbolicLink()) {
|
|
151
|
+
// The actual file/directory we're writing to is a symlink - reject
|
|
152
|
+
return { ok: false, error: `Target path is a symlink: ${resolvedPath}` };
|
|
153
|
+
}
|
|
154
|
+
} catch (e) {
|
|
155
|
+
// Path doesn't exist yet, that's OK for new files
|
|
156
|
+
if (e.code !== 'ENOENT') {
|
|
157
|
+
return { ok: false, error: `Cannot stat path: ${e.message}` };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Use fs.realpathSync() to get the actual path after symlink resolution
|
|
162
|
+
let realPath = resolvedPath;
|
|
163
|
+
try {
|
|
164
|
+
realPath = fs.realpathSync(resolvedPath);
|
|
165
|
+
// We don't restrict realPath to baseDir because parent directories may be
|
|
166
|
+
// symlinked (e.g., git worktrees). The key protection is:
|
|
167
|
+
// 1. path.resolve() prevents ../../ traversal in the input
|
|
168
|
+
// 2. lstatSync() above prevents the target itself from being a symlink
|
|
169
|
+
} catch (e) {
|
|
170
|
+
// Path doesn't exist yet, use resolved path
|
|
171
|
+
if (e.code !== 'ENOENT') {
|
|
172
|
+
return { ok: false, error: `Cannot resolve real path: ${e.message}` };
|
|
173
|
+
}
|
|
174
|
+
realPath = resolvedPath;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return { ok: true, resolvedPath, realPath };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// =============================================================================
|
|
181
|
+
// Validate archive directory (US-0188)
|
|
182
|
+
// =============================================================================
|
|
183
|
+
|
|
184
|
+
const archiveDirValidation = validatePath(archiveDir, projectRoot);
|
|
185
|
+
if (!archiveDirValidation.ok) {
|
|
186
|
+
console.error(`\x1b[31mSecurity: ${archiveDirValidation.error}. Aborting.\x1b[0m`);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
115
189
|
|
|
116
190
|
// Read status.json
|
|
117
191
|
const status = JSON.parse(fs.readFileSync(statusFile, 'utf8'));
|
|
@@ -146,7 +220,7 @@ for (const [storyId, story] of Object.entries(toArchive)) {
|
|
|
146
220
|
const date = new Date(story.completed_at);
|
|
147
221
|
const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
|
148
222
|
|
|
149
|
-
// Security: Validate monthKey matches expected format (YYYY-MM) to prevent path traversal
|
|
223
|
+
// Security: Validate monthKey matches expected format (YYYY-MM) to prevent path traversal (US-0188 AC)
|
|
150
224
|
if (!/^\d{4}-\d{2}$/.test(monthKey)) {
|
|
151
225
|
console.error(`\x1b[31mSkipping story ${storyId}: invalid date format\x1b[0m`);
|
|
152
226
|
continue;
|
|
@@ -165,27 +239,28 @@ for (const [storyId, story] of Object.entries(toArchive)) {
|
|
|
165
239
|
|
|
166
240
|
// Write archive files
|
|
167
241
|
for (const [monthKey, archiveData] of Object.entries(byMonth)) {
|
|
168
|
-
const archiveFile =
|
|
242
|
+
const archiveFile = `${monthKey}.json`;
|
|
169
243
|
|
|
170
|
-
// Security:
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
console.error(`\x1b[31mSecurity: Archive path ${archiveFile} escapes archive directory. Skipping.\x1b[0m`);
|
|
244
|
+
// Security: Use validatePath() with allowSymlinks: false (US-0188 AC)
|
|
245
|
+
const validation = validatePath(archiveFile, archiveDir);
|
|
246
|
+
if (!validation.ok) {
|
|
247
|
+
console.error(`\x1b[31mSecurity: ${validation.error}. Skipping ${monthKey}.\x1b[0m`);
|
|
175
248
|
continue;
|
|
176
249
|
}
|
|
177
250
|
|
|
251
|
+
const finalPath = validation.resolvedPath;
|
|
252
|
+
|
|
178
253
|
// Merge with existing archive if it exists
|
|
179
|
-
if (fs.existsSync(
|
|
254
|
+
if (fs.existsSync(finalPath)) {
|
|
180
255
|
try {
|
|
181
|
-
const existing = JSON.parse(fs.readFileSync(
|
|
256
|
+
const existing = JSON.parse(fs.readFileSync(finalPath, 'utf8'));
|
|
182
257
|
archiveData.stories = { ...existing.stories, ...archiveData.stories };
|
|
183
258
|
} catch (e) {
|
|
184
259
|
console.error(`\x1b[31mWarning: Could not parse existing ${monthKey}.json, will overwrite\x1b[0m`);
|
|
185
260
|
}
|
|
186
261
|
}
|
|
187
262
|
|
|
188
|
-
fs.writeFileSync(
|
|
263
|
+
fs.writeFileSync(finalPath, JSON.stringify(archiveData, null, 2));
|
|
189
264
|
const count = Object.keys(archiveData.stories).length;
|
|
190
265
|
console.log(`\x1b[32m✓ Archived ${count} stories to ${monthKey}.json\x1b[0m`);
|
|
191
266
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* babysit-context-restore.js - UserPromptSubmit hook
|
|
4
|
+
*
|
|
5
|
+
* Backup mechanism to restore babysit context after plan mode clears context.
|
|
6
|
+
* When user selects "Clear context and bypass permissions" after ExitPlanMode,
|
|
7
|
+
* this hook fires on the next user prompt and reminds Claude of babysit rules.
|
|
8
|
+
*
|
|
9
|
+
* The primary mechanism is embedding rules in the plan file (Rule #6).
|
|
10
|
+
* This hook is a backup for edge cases where plan file approach might miss.
|
|
11
|
+
*
|
|
12
|
+
* Usage: Called automatically as UserPromptSubmit hook
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
// Find session-state.json - try multiple locations
|
|
19
|
+
function findSessionState() {
|
|
20
|
+
const locations = [
|
|
21
|
+
'docs/09-agents/session-state.json',
|
|
22
|
+
path.join(process.env.CLAUDE_PROJECT_DIR || '.', 'docs/09-agents/session-state.json'),
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
for (const loc of locations) {
|
|
26
|
+
if (fs.existsSync(loc)) {
|
|
27
|
+
return loc;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function main() {
|
|
34
|
+
const sessionStatePath = findSessionState();
|
|
35
|
+
if (!sessionStatePath) return;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const state = JSON.parse(fs.readFileSync(sessionStatePath, 'utf8'));
|
|
39
|
+
|
|
40
|
+
// Check if restoration is pending
|
|
41
|
+
if (!state.babysit_pending_restore) return;
|
|
42
|
+
|
|
43
|
+
// Output restoration context
|
|
44
|
+
console.log('');
|
|
45
|
+
console.log('\x1b[36m╔══════════════════════════════════════════════════════════════╗\x1b[0m');
|
|
46
|
+
console.log(
|
|
47
|
+
'\x1b[36m║\x1b[0m \x1b[1m\x1b[33m/babysit CONTEXT RESTORED\x1b[0m \x1b[36m║\x1b[0m'
|
|
48
|
+
);
|
|
49
|
+
console.log('\x1b[36m╠══════════════════════════════════════════════════════════════╣\x1b[0m');
|
|
50
|
+
console.log(
|
|
51
|
+
'\x1b[36m║\x1b[0m /agileflow:babysit was active before context clear. \x1b[36m║\x1b[0m'
|
|
52
|
+
);
|
|
53
|
+
console.log(
|
|
54
|
+
'\x1b[36m║\x1b[0m These rules are MANDATORY: \x1b[36m║\x1b[0m'
|
|
55
|
+
);
|
|
56
|
+
console.log(
|
|
57
|
+
'\x1b[36m║\x1b[0m \x1b[36m║\x1b[0m'
|
|
58
|
+
);
|
|
59
|
+
console.log(
|
|
60
|
+
'\x1b[36m║\x1b[0m 1. ALWAYS end responses with AskUserQuestion tool \x1b[36m║\x1b[0m'
|
|
61
|
+
);
|
|
62
|
+
console.log(
|
|
63
|
+
'\x1b[36m║\x1b[0m 2. Use EnterPlanMode before non-trivial tasks \x1b[36m║\x1b[0m'
|
|
64
|
+
);
|
|
65
|
+
console.log(
|
|
66
|
+
'\x1b[36m║\x1b[0m 3. Delegate complex work to domain experts \x1b[36m║\x1b[0m'
|
|
67
|
+
);
|
|
68
|
+
console.log(
|
|
69
|
+
'\x1b[36m║\x1b[0m 4. Track progress with TodoWrite for 3+ step tasks \x1b[36m║\x1b[0m'
|
|
70
|
+
);
|
|
71
|
+
console.log(
|
|
72
|
+
'\x1b[36m║\x1b[0m \x1b[36m║\x1b[0m'
|
|
73
|
+
);
|
|
74
|
+
console.log(
|
|
75
|
+
'\x1b[36m║\x1b[0m For full context: /agileflow:babysit \x1b[36m║\x1b[0m'
|
|
76
|
+
);
|
|
77
|
+
console.log('\x1b[36m╚══════════════════════════════════════════════════════════════╝\x1b[0m');
|
|
78
|
+
console.log('');
|
|
79
|
+
|
|
80
|
+
// Clear the flag (one-time restoration)
|
|
81
|
+
state.babysit_pending_restore = false;
|
|
82
|
+
state.babysit_restored_at = new Date().toISOString();
|
|
83
|
+
fs.writeFileSync(sessionStatePath, JSON.stringify(state, null, 2) + '\n');
|
|
84
|
+
} catch (e) {
|
|
85
|
+
// Silently fail - don't break user's workflow
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
main();
|