agileflow 2.80.0 → 2.81.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/README.md +6 -6
- package/package.json +1 -1
- package/scripts/agent-loop.js +765 -0
- package/scripts/agileflow-configure.js +3 -1
- package/scripts/agileflow-welcome.js +65 -0
- package/scripts/damage-control-bash.js +22 -115
- package/scripts/damage-control-edit.js +19 -156
- package/scripts/damage-control-write.js +19 -156
- package/scripts/lib/damage-control-utils.js +251 -0
- package/scripts/obtain-context.js +57 -2
- package/scripts/ralph-loop.js +230 -26
- package/scripts/session-manager.js +434 -20
- package/src/core/agents/configuration-visual-e2e.md +300 -0
- package/src/core/agents/orchestrator.md +166 -0
- package/src/core/commands/babysit.md +61 -15
- package/src/core/commands/configure.md +372 -100
- package/src/core/commands/session/end.md +332 -103
- package/src/core/commands/setup/visual-e2e.md +0 -462
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* damage-control-utils.js - Shared utilities for damage-control hooks
|
|
3
|
+
*
|
|
4
|
+
* IMPORTANT: These scripts must FAIL OPEN (exit 0 on error)
|
|
5
|
+
* to avoid blocking users when config is broken.
|
|
6
|
+
*
|
|
7
|
+
* This module is copied to .agileflow/scripts/lib/ at install time
|
|
8
|
+
* and used by damage-control-bash.js, damage-control-edit.js, damage-control-write.js
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
|
|
15
|
+
// Inline colors (no external dependency - keeps scripts standalone)
|
|
16
|
+
const c = {
|
|
17
|
+
coral: '\x1b[38;5;203m',
|
|
18
|
+
dim: '\x1b[2m',
|
|
19
|
+
reset: '\x1b[0m',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Shared constants
|
|
23
|
+
const CONFIG_PATHS = [
|
|
24
|
+
'.agileflow/config/damage-control-patterns.yaml',
|
|
25
|
+
'.agileflow/config/damage-control-patterns.yml',
|
|
26
|
+
'.agileflow/templates/damage-control-patterns.yaml',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const STDIN_TIMEOUT_MS = 4000;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Find project root by looking for .agileflow directory
|
|
33
|
+
* @returns {string} Project root path or current working directory
|
|
34
|
+
*/
|
|
35
|
+
function findProjectRoot() {
|
|
36
|
+
let dir = process.cwd();
|
|
37
|
+
while (dir !== '/') {
|
|
38
|
+
if (fs.existsSync(path.join(dir, '.agileflow'))) {
|
|
39
|
+
return dir;
|
|
40
|
+
}
|
|
41
|
+
dir = path.dirname(dir);
|
|
42
|
+
}
|
|
43
|
+
return process.cwd();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Expand ~ to home directory
|
|
48
|
+
* @param {string} p - Path that may start with ~/
|
|
49
|
+
* @returns {string} Expanded path
|
|
50
|
+
*/
|
|
51
|
+
function expandPath(p) {
|
|
52
|
+
if (p.startsWith('~/')) {
|
|
53
|
+
return path.join(os.homedir(), p.slice(2));
|
|
54
|
+
}
|
|
55
|
+
return p;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Load patterns configuration from YAML file
|
|
60
|
+
* Returns empty config if not found (fail-open)
|
|
61
|
+
*
|
|
62
|
+
* @param {string} projectRoot - Project root directory
|
|
63
|
+
* @param {function} parseYAML - Function to parse YAML content
|
|
64
|
+
* @param {object} defaultConfig - Default config if no file found
|
|
65
|
+
* @returns {object} Parsed configuration
|
|
66
|
+
*/
|
|
67
|
+
function loadPatterns(projectRoot, parseYAML, defaultConfig = {}) {
|
|
68
|
+
for (const configPath of CONFIG_PATHS) {
|
|
69
|
+
const fullPath = path.join(projectRoot, configPath);
|
|
70
|
+
if (fs.existsSync(fullPath)) {
|
|
71
|
+
try {
|
|
72
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
73
|
+
return parseYAML(content);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
// Continue to next path
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Return empty config if no file found (fail-open)
|
|
81
|
+
return defaultConfig;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Check if a file path matches any of the protected patterns
|
|
86
|
+
*
|
|
87
|
+
* @param {string} filePath - File path to check
|
|
88
|
+
* @param {string[]} patterns - Array of patterns to match against
|
|
89
|
+
* @returns {string|null} Matched pattern or null
|
|
90
|
+
*/
|
|
91
|
+
function pathMatches(filePath, patterns) {
|
|
92
|
+
if (!filePath) return null;
|
|
93
|
+
|
|
94
|
+
const normalizedPath = path.resolve(filePath);
|
|
95
|
+
const relativePath = path.relative(process.cwd(), normalizedPath);
|
|
96
|
+
|
|
97
|
+
for (const pattern of patterns) {
|
|
98
|
+
const expandedPattern = expandPath(pattern);
|
|
99
|
+
|
|
100
|
+
// Check if pattern is a directory prefix
|
|
101
|
+
if (pattern.endsWith('/')) {
|
|
102
|
+
const patternDir = expandedPattern.slice(0, -1);
|
|
103
|
+
if (normalizedPath.startsWith(patternDir)) {
|
|
104
|
+
return pattern;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Check exact match
|
|
109
|
+
if (normalizedPath === expandedPattern) {
|
|
110
|
+
return pattern;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Check if normalized path ends with pattern (for filenames like "id_rsa")
|
|
114
|
+
if (normalizedPath.endsWith(pattern) || relativePath.endsWith(pattern)) {
|
|
115
|
+
return pattern;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Check if pattern appears in path (for patterns like "*.pem")
|
|
119
|
+
if (pattern.startsWith('*')) {
|
|
120
|
+
const ext = pattern.slice(1);
|
|
121
|
+
if (normalizedPath.endsWith(ext) || relativePath.endsWith(ext)) {
|
|
122
|
+
return pattern;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Check if path contains pattern (for things like ".env.production")
|
|
127
|
+
const patternBase = path.basename(pattern);
|
|
128
|
+
if (path.basename(normalizedPath) === patternBase) {
|
|
129
|
+
return pattern;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Output blocked message to stderr
|
|
138
|
+
*
|
|
139
|
+
* @param {string} reason - Main reason for blocking
|
|
140
|
+
* @param {string} [detail] - Additional detail
|
|
141
|
+
* @param {string} [context] - Context info (file path or command)
|
|
142
|
+
*/
|
|
143
|
+
function outputBlocked(reason, detail, context) {
|
|
144
|
+
console.error(`${c.coral}[BLOCKED]${c.reset} ${reason}`);
|
|
145
|
+
if (detail) {
|
|
146
|
+
console.error(`${c.dim}${detail}${c.reset}`);
|
|
147
|
+
}
|
|
148
|
+
if (context) {
|
|
149
|
+
console.error(`${c.dim}${context}${c.reset}`);
|
|
150
|
+
}
|
|
151
|
+
// Help message for AI and user
|
|
152
|
+
console.error('');
|
|
153
|
+
console.error(
|
|
154
|
+
`${c.dim}This is intentional - AgileFlow Damage Control blocked a potentially dangerous operation.${c.reset}`
|
|
155
|
+
);
|
|
156
|
+
console.error(
|
|
157
|
+
`${c.dim}DO NOT retry this command. Ask the user if they want to proceed manually.${c.reset}`
|
|
158
|
+
);
|
|
159
|
+
console.error(`${c.dim}To disable: run /configure → Infrastructure → Damage Control${c.reset}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Run damage control hook with stdin parsing
|
|
164
|
+
* Handles common error cases and timeout
|
|
165
|
+
*
|
|
166
|
+
* @param {object} options - Hook options
|
|
167
|
+
* @param {function} options.getInputValue - Extract value from parsed input (input) => value
|
|
168
|
+
* @param {function} options.loadConfig - Load configuration () => config
|
|
169
|
+
* @param {function} options.validate - Validate input (value, config) => { action, reason, detail? }
|
|
170
|
+
* @param {function} options.onBlock - Handle blocked result (result, value) => void
|
|
171
|
+
* @param {function} [options.onAsk] - Handle ask result (result, value) => void (optional)
|
|
172
|
+
*/
|
|
173
|
+
function runDamageControlHook(options) {
|
|
174
|
+
const { getInputValue, loadConfig, validate, onBlock, onAsk } = options;
|
|
175
|
+
|
|
176
|
+
let inputData = '';
|
|
177
|
+
|
|
178
|
+
process.stdin.setEncoding('utf8');
|
|
179
|
+
|
|
180
|
+
process.stdin.on('data', chunk => {
|
|
181
|
+
inputData += chunk;
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
process.stdin.on('end', () => {
|
|
185
|
+
try {
|
|
186
|
+
// Parse tool input from Claude Code
|
|
187
|
+
const input = JSON.parse(inputData);
|
|
188
|
+
const value = getInputValue(input);
|
|
189
|
+
|
|
190
|
+
if (!value) {
|
|
191
|
+
// No value to validate - allow
|
|
192
|
+
process.exit(0);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Load patterns and validate
|
|
196
|
+
const config = loadConfig();
|
|
197
|
+
const result = validate(value, config);
|
|
198
|
+
|
|
199
|
+
switch (result.action) {
|
|
200
|
+
case 'block':
|
|
201
|
+
onBlock(result, value);
|
|
202
|
+
process.exit(2);
|
|
203
|
+
break;
|
|
204
|
+
|
|
205
|
+
case 'ask':
|
|
206
|
+
if (onAsk) {
|
|
207
|
+
onAsk(result, value);
|
|
208
|
+
} else {
|
|
209
|
+
// Default ask behavior - output JSON
|
|
210
|
+
console.log(
|
|
211
|
+
JSON.stringify({
|
|
212
|
+
result: 'ask',
|
|
213
|
+
message: result.reason,
|
|
214
|
+
})
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
process.exit(0);
|
|
218
|
+
break;
|
|
219
|
+
|
|
220
|
+
case 'allow':
|
|
221
|
+
default:
|
|
222
|
+
process.exit(0);
|
|
223
|
+
}
|
|
224
|
+
} catch (e) {
|
|
225
|
+
// Parse error or other issue - fail open
|
|
226
|
+
process.exit(0);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Handle no stdin (direct invocation)
|
|
231
|
+
process.stdin.on('error', () => {
|
|
232
|
+
process.exit(0);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Set timeout to prevent hanging
|
|
236
|
+
setTimeout(() => {
|
|
237
|
+
process.exit(0);
|
|
238
|
+
}, STDIN_TIMEOUT_MS);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
module.exports = {
|
|
242
|
+
c,
|
|
243
|
+
findProjectRoot,
|
|
244
|
+
expandPath,
|
|
245
|
+
loadPatterns,
|
|
246
|
+
pathMatches,
|
|
247
|
+
outputBlocked,
|
|
248
|
+
runDamageControlHook,
|
|
249
|
+
CONFIG_PATHS,
|
|
250
|
+
STDIN_TIMEOUT_MS,
|
|
251
|
+
};
|
|
@@ -19,12 +19,13 @@ const fs = require('fs');
|
|
|
19
19
|
const path = require('path');
|
|
20
20
|
const { execSync } = require('child_process');
|
|
21
21
|
const { c: C, box } = require('../lib/colors');
|
|
22
|
+
const { isValidCommandName } = require('../lib/validate');
|
|
22
23
|
|
|
23
24
|
const DISPLAY_LIMIT = 30000; // Claude Code's Bash tool display limit
|
|
24
25
|
|
|
25
26
|
// Optional: Register command for PreCompact context preservation
|
|
26
27
|
const commandName = process.argv[2];
|
|
27
|
-
if (commandName) {
|
|
28
|
+
if (commandName && isValidCommandName(commandName)) {
|
|
28
29
|
const sessionStatePath = 'docs/09-agents/session-state.json';
|
|
29
30
|
if (fs.existsSync(sessionStatePath)) {
|
|
30
31
|
try {
|
|
@@ -378,7 +379,61 @@ function generateFullContent() {
|
|
|
378
379
|
content += `${C.dim}No session-state.json found${C.reset}\n`;
|
|
379
380
|
}
|
|
380
381
|
|
|
381
|
-
// 4.
|
|
382
|
+
// 4. SESSION CONTEXT (multi-session awareness)
|
|
383
|
+
content += `\n${C.skyBlue}${C.bold}═══ Session Context ═══${C.reset}\n`;
|
|
384
|
+
const sessionManagerPath = path.join(__dirname, 'session-manager.js');
|
|
385
|
+
const altSessionManagerPath = '.agileflow/scripts/session-manager.js';
|
|
386
|
+
|
|
387
|
+
if (fs.existsSync(sessionManagerPath) || fs.existsSync(altSessionManagerPath)) {
|
|
388
|
+
const managerPath = fs.existsSync(sessionManagerPath)
|
|
389
|
+
? sessionManagerPath
|
|
390
|
+
: altSessionManagerPath;
|
|
391
|
+
const sessionStatus = safeExec(`node "${managerPath}" status`);
|
|
392
|
+
|
|
393
|
+
if (sessionStatus) {
|
|
394
|
+
try {
|
|
395
|
+
const statusData = JSON.parse(sessionStatus);
|
|
396
|
+
if (statusData.current) {
|
|
397
|
+
const session = statusData.current;
|
|
398
|
+
const isMain = session.is_main === true;
|
|
399
|
+
|
|
400
|
+
if (isMain) {
|
|
401
|
+
content += `Session: ${C.mintGreen}Main project${C.reset} (Session ${session.id || 1})\n`;
|
|
402
|
+
} else {
|
|
403
|
+
// NON-MAIN SESSION - Show prominent banner
|
|
404
|
+
const sessionName = session.nickname
|
|
405
|
+
? `${session.id} "${session.nickname}"`
|
|
406
|
+
: `${session.id}`;
|
|
407
|
+
content += `${C.teal}${C.bold}🔀 SESSION ${sessionName} (worktree)${C.reset}\n`;
|
|
408
|
+
content += `Branch: ${C.skyBlue}${session.branch || 'unknown'}${C.reset}\n`;
|
|
409
|
+
content += `Path: ${C.dim}${session.path || process.cwd()}${C.reset}\n`;
|
|
410
|
+
|
|
411
|
+
// Calculate relative path to main
|
|
412
|
+
const mainPath = process.cwd().replace(/-[^/]+$/, ''); // Heuristic: strip session suffix
|
|
413
|
+
content += `Main project: ${C.dim}${mainPath}${C.reset}\n`;
|
|
414
|
+
|
|
415
|
+
// Remind about merge flow
|
|
416
|
+
content += `${C.lavender}💡 When done: /agileflow:session:end → merge to main${C.reset}\n`;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Show other active sessions
|
|
420
|
+
if (statusData.otherActive > 0) {
|
|
421
|
+
content += `${C.peach}⚠️ ${statusData.otherActive} other session(s) active${C.reset}\n`;
|
|
422
|
+
}
|
|
423
|
+
} else {
|
|
424
|
+
content += `${C.dim}No session registered${C.reset}\n`;
|
|
425
|
+
}
|
|
426
|
+
} catch (e) {
|
|
427
|
+
content += `${C.dim}Session manager available but status parse failed${C.reset}\n`;
|
|
428
|
+
}
|
|
429
|
+
} else {
|
|
430
|
+
content += `${C.dim}Session manager available${C.reset}\n`;
|
|
431
|
+
}
|
|
432
|
+
} else {
|
|
433
|
+
content += `${C.dim}Multi-session not configured${C.reset}\n`;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// 5. INTERACTION MODE (AskUserQuestion guidance)
|
|
382
437
|
const metadata = safeReadJSON('docs/00-meta/agileflow-metadata.json');
|
|
383
438
|
const askUserQuestionConfig = metadata?.features?.askUserQuestion;
|
|
384
439
|
|