agileflow 2.91.0 → 2.92.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 +5 -0
- package/README.md +3 -3
- package/lib/README.md +178 -0
- package/lib/codebase-indexer.js +31 -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 +435 -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 +43 -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 +122 -14
- package/scripts/ralph-loop.js +5 -5
- package/scripts/session-manager.js +220 -42
- package/scripts/spawn-parallel.js +651 -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 +113 -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 +86 -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/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 +114 -1
- package/tools/cli/lib/ui.js +14 -25
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* validate-commands.js - Command Validation for Shell Execution
|
|
3
|
+
*
|
|
4
|
+
* Validates and sanitizes commands before shell execution to prevent
|
|
5
|
+
* command injection attacks. Uses allowlist approach for safe commands
|
|
6
|
+
* and rejects dangerous shell metacharacters.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const { validateCommand, ALLOWED_COMMANDS } = require('./validate-commands');
|
|
10
|
+
*
|
|
11
|
+
* const result = validateCommand('npm test');
|
|
12
|
+
* if (result.ok) {
|
|
13
|
+
* // Safe to execute
|
|
14
|
+
* const { command, args } = result.data;
|
|
15
|
+
* } else {
|
|
16
|
+
* console.error(result.error);
|
|
17
|
+
* }
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const { debugLog, sanitizeForShell } = require('./errors');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Allowed command prefixes for agent-loop execution
|
|
24
|
+
* These are the only commands that can be executed
|
|
25
|
+
*/
|
|
26
|
+
const ALLOWED_COMMANDS = {
|
|
27
|
+
// Package managers
|
|
28
|
+
npm: ['test', 'run', 'run-script'],
|
|
29
|
+
npx: ['jest', 'tsc', 'eslint', 'prettier', 'playwright', 'vitest'],
|
|
30
|
+
yarn: ['test', 'run'],
|
|
31
|
+
pnpm: ['test', 'run'],
|
|
32
|
+
bun: ['test', 'run'],
|
|
33
|
+
|
|
34
|
+
// Direct test runners (for projects not using npm scripts)
|
|
35
|
+
jest: true, // Allow all jest args
|
|
36
|
+
vitest: true,
|
|
37
|
+
mocha: true,
|
|
38
|
+
playwright: ['test'],
|
|
39
|
+
|
|
40
|
+
// Build tools
|
|
41
|
+
tsc: ['--noEmit', '--build', '-b'],
|
|
42
|
+
|
|
43
|
+
// Linters
|
|
44
|
+
eslint: true,
|
|
45
|
+
prettier: ['--check', '--write'],
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Dangerous shell metacharacters that could enable injection
|
|
50
|
+
*/
|
|
51
|
+
const DANGEROUS_PATTERNS = [
|
|
52
|
+
/[;&|`$(){}]/g, // Shell operators, substitution
|
|
53
|
+
/\$\{/g, // Variable expansion
|
|
54
|
+
/\$\(/g, // Command substitution
|
|
55
|
+
/`[^`]*`/g, // Backtick substitution
|
|
56
|
+
/>\s/g, // Redirection
|
|
57
|
+
/<\s/g, // Input redirection
|
|
58
|
+
/\n/g, // Newlines (could chain commands)
|
|
59
|
+
/\r/g, // Carriage return
|
|
60
|
+
/\\$/g, // Line continuation
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Parse a command string into executable and arguments
|
|
65
|
+
* @param {string} cmdString - Full command string
|
|
66
|
+
* @returns {{ executable: string, args: string[] }}
|
|
67
|
+
*/
|
|
68
|
+
function parseCommand(cmdString) {
|
|
69
|
+
// Handle quoted arguments properly
|
|
70
|
+
const parts = [];
|
|
71
|
+
let current = '';
|
|
72
|
+
let inQuote = null;
|
|
73
|
+
|
|
74
|
+
for (let i = 0; i < cmdString.length; i++) {
|
|
75
|
+
const char = cmdString[i];
|
|
76
|
+
|
|
77
|
+
if (inQuote) {
|
|
78
|
+
if (char === inQuote) {
|
|
79
|
+
inQuote = null;
|
|
80
|
+
} else {
|
|
81
|
+
current += char;
|
|
82
|
+
}
|
|
83
|
+
} else if (char === '"' || char === "'") {
|
|
84
|
+
inQuote = char;
|
|
85
|
+
} else if (char === ' ' || char === '\t') {
|
|
86
|
+
if (current) {
|
|
87
|
+
parts.push(current);
|
|
88
|
+
current = '';
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
current += char;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (current) {
|
|
96
|
+
parts.push(current);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
executable: parts[0] || '',
|
|
101
|
+
args: parts.slice(1),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check if an argument contains dangerous patterns
|
|
107
|
+
* @param {string} arg - Argument to check
|
|
108
|
+
* @returns {{ safe: boolean, pattern?: string }}
|
|
109
|
+
*/
|
|
110
|
+
function checkArgSafety(arg) {
|
|
111
|
+
for (const pattern of DANGEROUS_PATTERNS) {
|
|
112
|
+
if (pattern.test(arg)) {
|
|
113
|
+
return { safe: false, pattern: pattern.source };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { safe: true };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Validate a command against the allowlist
|
|
121
|
+
* @param {string} cmdString - Full command string to validate
|
|
122
|
+
* @param {Object} [options={}] - Validation options
|
|
123
|
+
* @param {boolean} [options.strict=true] - Require command in allowlist
|
|
124
|
+
* @param {boolean} [options.logBlocked=true] - Log blocked commands
|
|
125
|
+
* @returns {{ ok: boolean, data?: { command: string, args: string[] }, error?: string, severity?: string }}
|
|
126
|
+
*/
|
|
127
|
+
function validateCommand(cmdString, options = {}) {
|
|
128
|
+
const { strict = true, logBlocked = true } = options;
|
|
129
|
+
|
|
130
|
+
// Must be a string
|
|
131
|
+
if (typeof cmdString !== 'string') {
|
|
132
|
+
return {
|
|
133
|
+
ok: false,
|
|
134
|
+
error: `Command must be a string, got ${typeof cmdString}`,
|
|
135
|
+
severity: 'high',
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Trim and check for empty
|
|
140
|
+
const trimmed = cmdString.trim();
|
|
141
|
+
if (!trimmed) {
|
|
142
|
+
return {
|
|
143
|
+
ok: false,
|
|
144
|
+
error: 'Command cannot be empty',
|
|
145
|
+
severity: 'medium',
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Parse the command
|
|
150
|
+
const { executable, args } = parseCommand(trimmed);
|
|
151
|
+
|
|
152
|
+
if (!executable) {
|
|
153
|
+
return {
|
|
154
|
+
ok: false,
|
|
155
|
+
error: 'No executable found in command',
|
|
156
|
+
severity: 'medium',
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Check executable against allowlist
|
|
161
|
+
const allowedSubcommands = ALLOWED_COMMANDS[executable];
|
|
162
|
+
|
|
163
|
+
if (strict && !allowedSubcommands) {
|
|
164
|
+
const error = `Command '${executable}' not in allowlist. Allowed: ${Object.keys(ALLOWED_COMMANDS).join(', ')}`;
|
|
165
|
+
if (logBlocked) {
|
|
166
|
+
debugLog('validateCommand', {
|
|
167
|
+
blocked: true,
|
|
168
|
+
command: executable,
|
|
169
|
+
reason: 'not_in_allowlist',
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
ok: false,
|
|
174
|
+
error,
|
|
175
|
+
severity: 'high',
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// If allowedSubcommands is an array, check the first argument
|
|
180
|
+
if (Array.isArray(allowedSubcommands) && args.length > 0) {
|
|
181
|
+
const subcommand = args[0];
|
|
182
|
+
if (!allowedSubcommands.includes(subcommand)) {
|
|
183
|
+
const error = `Subcommand '${subcommand}' not allowed for '${executable}'. Allowed: ${allowedSubcommands.join(', ')}`;
|
|
184
|
+
if (logBlocked) {
|
|
185
|
+
debugLog('validateCommand', {
|
|
186
|
+
blocked: true,
|
|
187
|
+
command: `${executable} ${subcommand}`,
|
|
188
|
+
reason: 'subcommand_not_allowed',
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
ok: false,
|
|
193
|
+
error,
|
|
194
|
+
severity: 'high',
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Check all arguments for dangerous patterns
|
|
200
|
+
for (const arg of args) {
|
|
201
|
+
const argCheck = checkArgSafety(arg);
|
|
202
|
+
if (!argCheck.safe) {
|
|
203
|
+
const error = `Dangerous pattern in argument: '${arg}' matches ${argCheck.pattern}`;
|
|
204
|
+
if (logBlocked) {
|
|
205
|
+
debugLog('validateCommand', {
|
|
206
|
+
blocked: true,
|
|
207
|
+
command: executable,
|
|
208
|
+
arg,
|
|
209
|
+
pattern: argCheck.pattern,
|
|
210
|
+
reason: 'dangerous_pattern',
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
ok: false,
|
|
215
|
+
error,
|
|
216
|
+
severity: 'critical',
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Additional check with sanitizeForShell for the full command
|
|
222
|
+
const fullCmdCheck = sanitizeForShell(trimmed, { context: 'command' });
|
|
223
|
+
if (!fullCmdCheck.ok) {
|
|
224
|
+
if (logBlocked) {
|
|
225
|
+
debugLog('validateCommand', {
|
|
226
|
+
blocked: true,
|
|
227
|
+
command: trimmed.slice(0, 50),
|
|
228
|
+
reason: 'shell_unsafe',
|
|
229
|
+
detected: fullCmdCheck.detected,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
ok: false,
|
|
234
|
+
error: fullCmdCheck.error,
|
|
235
|
+
severity: 'critical',
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
ok: true,
|
|
241
|
+
data: {
|
|
242
|
+
command: executable,
|
|
243
|
+
args,
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Build a safe command array for spawn()
|
|
250
|
+
* @param {string} cmdString - Command string
|
|
251
|
+
* @param {Object} [options={}] - Options
|
|
252
|
+
* @returns {{ ok: boolean, data?: { file: string, args: string[] }, error?: string }}
|
|
253
|
+
*/
|
|
254
|
+
function buildSpawnArgs(cmdString, options = {}) {
|
|
255
|
+
const validation = validateCommand(cmdString, options);
|
|
256
|
+
|
|
257
|
+
if (!validation.ok) {
|
|
258
|
+
return validation;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
ok: true,
|
|
263
|
+
data: {
|
|
264
|
+
file: validation.data.command,
|
|
265
|
+
args: validation.data.args,
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Check if a command would be allowed (without modifying)
|
|
272
|
+
* @param {string} cmdString - Command to check
|
|
273
|
+
* @returns {boolean}
|
|
274
|
+
*/
|
|
275
|
+
function isAllowedCommand(cmdString) {
|
|
276
|
+
return validateCommand(cmdString, { logBlocked: false }).ok;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Get a list of all allowed command patterns
|
|
281
|
+
* @returns {string[]}
|
|
282
|
+
*/
|
|
283
|
+
function getAllowedCommandList() {
|
|
284
|
+
const list = [];
|
|
285
|
+
|
|
286
|
+
for (const [exe, subcommands] of Object.entries(ALLOWED_COMMANDS)) {
|
|
287
|
+
if (subcommands === true) {
|
|
288
|
+
list.push(`${exe} *`);
|
|
289
|
+
} else if (Array.isArray(subcommands)) {
|
|
290
|
+
for (const sub of subcommands) {
|
|
291
|
+
list.push(`${exe} ${sub}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return list;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
module.exports = {
|
|
300
|
+
ALLOWED_COMMANDS,
|
|
301
|
+
DANGEROUS_PATTERNS,
|
|
302
|
+
validateCommand,
|
|
303
|
+
buildSpawnArgs,
|
|
304
|
+
isAllowedCommand,
|
|
305
|
+
getAllowedCommandList,
|
|
306
|
+
parseCommand,
|
|
307
|
+
checkArgSafety,
|
|
308
|
+
};
|
package/lib/validate.js
CHANGED
|
@@ -4,67 +4,131 @@
|
|
|
4
4
|
* Centralized validation patterns and helpers to prevent
|
|
5
5
|
* command injection, path traversal, and invalid input handling.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* Usage patterns:
|
|
8
|
+
*
|
|
9
|
+
* 1. Namespace import (recommended - clear, discoverable):
|
|
10
|
+
* const { names, args, paths, commands } = require('./validate');
|
|
11
|
+
* names.isValidStoryId('US-0001')
|
|
12
|
+
* paths.validatePath('/some/path')
|
|
13
|
+
* args.validateArgs(schema, input)
|
|
14
|
+
*
|
|
15
|
+
* 2. Flat import (backwards compatible):
|
|
16
|
+
* const { isValidStoryId, validatePath } = require('./validate');
|
|
17
|
+
*
|
|
18
|
+
* 3. Direct import (best performance):
|
|
19
|
+
* const { isValidStoryId } = require('./validate-names');
|
|
20
|
+
* const { validatePath } = require('./validate-paths');
|
|
12
21
|
*/
|
|
13
22
|
|
|
14
|
-
//
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
isValidEpicId,
|
|
20
|
-
isValidFeatureName,
|
|
21
|
-
isValidProfileName,
|
|
22
|
-
isValidCommandName,
|
|
23
|
-
isValidSessionNickname,
|
|
24
|
-
isValidMergeStrategy,
|
|
25
|
-
} = require('./validate-names');
|
|
23
|
+
// Import all validators
|
|
24
|
+
const validateNames = require('./validate-names');
|
|
25
|
+
const validateArgs = require('./validate-args');
|
|
26
|
+
const validatePaths = require('./validate-paths');
|
|
27
|
+
const validateCommands = require('./validate-commands');
|
|
26
28
|
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
parseIntBounded,
|
|
31
|
-
isValidOption,
|
|
32
|
-
validateArgs,
|
|
33
|
-
} = require('./validate-args');
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Namespace exports (recommended for new code)
|
|
31
|
+
// ============================================================================
|
|
34
32
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Name/ID validation namespace
|
|
35
|
+
* @namespace names
|
|
36
|
+
*/
|
|
37
|
+
const names = {
|
|
38
|
+
PATTERNS: validateNames.PATTERNS,
|
|
39
|
+
isValidBranchName: validateNames.isValidBranchName,
|
|
40
|
+
isValidStoryId: validateNames.isValidStoryId,
|
|
41
|
+
isValidEpicId: validateNames.isValidEpicId,
|
|
42
|
+
isValidFeatureName: validateNames.isValidFeatureName,
|
|
43
|
+
isValidProfileName: validateNames.isValidProfileName,
|
|
44
|
+
isValidCommandName: validateNames.isValidCommandName,
|
|
45
|
+
isValidSessionNickname: validateNames.isValidSessionNickname,
|
|
46
|
+
isValidMergeStrategy: validateNames.isValidMergeStrategy,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* CLI argument validation namespace
|
|
51
|
+
* @namespace args
|
|
52
|
+
*/
|
|
53
|
+
const args = {
|
|
54
|
+
isPositiveInteger: validateArgs.isPositiveInteger,
|
|
55
|
+
parseIntBounded: validateArgs.parseIntBounded,
|
|
56
|
+
isValidOption: validateArgs.isValidOption,
|
|
57
|
+
validateArgs: validateArgs.validateArgs,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Path traversal protection namespace
|
|
62
|
+
* @namespace paths
|
|
63
|
+
*/
|
|
64
|
+
const paths = {
|
|
65
|
+
PathValidationError: validatePaths.PathValidationError,
|
|
66
|
+
checkSymlinkChainDepth: validatePaths.checkSymlinkChainDepth,
|
|
67
|
+
validatePath: validatePaths.validatePath,
|
|
68
|
+
validatePathSync: validatePaths.validatePathSync,
|
|
69
|
+
hasUnsafePathPatterns: validatePaths.hasUnsafePathPatterns,
|
|
70
|
+
sanitizeFilename: validatePaths.sanitizeFilename,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Command validation namespace
|
|
75
|
+
* @namespace commands
|
|
76
|
+
*/
|
|
77
|
+
const commands = {
|
|
78
|
+
ALLOWED_COMMANDS: validateCommands.ALLOWED_COMMANDS,
|
|
79
|
+
DANGEROUS_PATTERNS: validateCommands.DANGEROUS_PATTERNS,
|
|
80
|
+
validateCommand: validateCommands.validateCommand,
|
|
81
|
+
buildSpawnArgs: validateCommands.buildSpawnArgs,
|
|
82
|
+
isAllowedCommand: validateCommands.isAllowedCommand,
|
|
83
|
+
getAllowedCommandList: validateCommands.getAllowedCommandList,
|
|
84
|
+
parseCommand: validateCommands.parseCommand,
|
|
85
|
+
checkArgSafety: validateCommands.checkArgSafety,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// ============================================================================
|
|
89
|
+
// Flat exports (backwards compatible)
|
|
90
|
+
// ============================================================================
|
|
44
91
|
|
|
45
92
|
module.exports = {
|
|
93
|
+
// Namespaces (recommended for new code)
|
|
94
|
+
names,
|
|
95
|
+
args,
|
|
96
|
+
paths,
|
|
97
|
+
commands,
|
|
98
|
+
|
|
99
|
+
// Flat exports (backwards compatible)
|
|
46
100
|
// Patterns and basic validators (from validate-names.js)
|
|
47
|
-
PATTERNS,
|
|
48
|
-
isValidBranchName,
|
|
49
|
-
isValidStoryId,
|
|
50
|
-
isValidEpicId,
|
|
51
|
-
isValidFeatureName,
|
|
52
|
-
isValidProfileName,
|
|
53
|
-
isValidCommandName,
|
|
54
|
-
isValidSessionNickname,
|
|
55
|
-
isValidMergeStrategy,
|
|
101
|
+
PATTERNS: validateNames.PATTERNS,
|
|
102
|
+
isValidBranchName: validateNames.isValidBranchName,
|
|
103
|
+
isValidStoryId: validateNames.isValidStoryId,
|
|
104
|
+
isValidEpicId: validateNames.isValidEpicId,
|
|
105
|
+
isValidFeatureName: validateNames.isValidFeatureName,
|
|
106
|
+
isValidProfileName: validateNames.isValidProfileName,
|
|
107
|
+
isValidCommandName: validateNames.isValidCommandName,
|
|
108
|
+
isValidSessionNickname: validateNames.isValidSessionNickname,
|
|
109
|
+
isValidMergeStrategy: validateNames.isValidMergeStrategy,
|
|
56
110
|
|
|
57
111
|
// Argument validators (from validate-args.js)
|
|
58
|
-
isPositiveInteger,
|
|
59
|
-
parseIntBounded,
|
|
60
|
-
isValidOption,
|
|
61
|
-
validateArgs,
|
|
112
|
+
isPositiveInteger: validateArgs.isPositiveInteger,
|
|
113
|
+
parseIntBounded: validateArgs.parseIntBounded,
|
|
114
|
+
isValidOption: validateArgs.isValidOption,
|
|
115
|
+
validateArgs: validateArgs.validateArgs,
|
|
62
116
|
|
|
63
117
|
// Path traversal protection (from validate-paths.js)
|
|
64
|
-
PathValidationError,
|
|
65
|
-
validatePath,
|
|
66
|
-
validatePathSync,
|
|
67
|
-
hasUnsafePathPatterns,
|
|
68
|
-
sanitizeFilename,
|
|
69
|
-
checkSymlinkChainDepth,
|
|
118
|
+
PathValidationError: validatePaths.PathValidationError,
|
|
119
|
+
validatePath: validatePaths.validatePath,
|
|
120
|
+
validatePathSync: validatePaths.validatePathSync,
|
|
121
|
+
hasUnsafePathPatterns: validatePaths.hasUnsafePathPatterns,
|
|
122
|
+
sanitizeFilename: validatePaths.sanitizeFilename,
|
|
123
|
+
checkSymlinkChainDepth: validatePaths.checkSymlinkChainDepth,
|
|
124
|
+
|
|
125
|
+
// Command validation (from validate-commands.js)
|
|
126
|
+
ALLOWED_COMMANDS: validateCommands.ALLOWED_COMMANDS,
|
|
127
|
+
DANGEROUS_PATTERNS: validateCommands.DANGEROUS_PATTERNS,
|
|
128
|
+
validateCommand: validateCommands.validateCommand,
|
|
129
|
+
buildSpawnArgs: validateCommands.buildSpawnArgs,
|
|
130
|
+
isAllowedCommand: validateCommands.isAllowedCommand,
|
|
131
|
+
getAllowedCommandList: validateCommands.getAllowedCommandList,
|
|
132
|
+
parseCommand: validateCommands.parseCommand,
|
|
133
|
+
checkArgSafety: validateCommands.checkArgSafety,
|
|
70
134
|
};
|
package/package.json
CHANGED
package/scripts/af
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# af - Short alias for Claude Code with tmux integration
|
|
3
|
+
#
|
|
4
|
+
# This is a convenience wrapper that:
|
|
5
|
+
# 1. Auto-starts Claude in a tmux session (for parallel sessions support)
|
|
6
|
+
# 2. Attaches to existing session if one exists for this directory
|
|
7
|
+
# 3. Falls back to regular claude if tmux isn't available
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# af # Start Claude in tmux
|
|
11
|
+
# af --no-tmux # Start without tmux
|
|
12
|
+
# af -h # Show help
|
|
13
|
+
#
|
|
14
|
+
# To install globally:
|
|
15
|
+
# sudo ln -s $(pwd)/.agileflow/scripts/af /usr/local/bin/af
|
|
16
|
+
# # Or add to your PATH
|
|
17
|
+
|
|
18
|
+
set -e
|
|
19
|
+
|
|
20
|
+
# Get the directory where this script is located
|
|
21
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
22
|
+
|
|
23
|
+
# Check if claude-tmux.sh exists in the same directory
|
|
24
|
+
if [ -f "$SCRIPT_DIR/claude-tmux.sh" ]; then
|
|
25
|
+
exec "$SCRIPT_DIR/claude-tmux.sh" "$@"
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# Fallback: try to find claude-tmux.sh in .agileflow/scripts
|
|
29
|
+
if [ -f ".agileflow/scripts/claude-tmux.sh" ]; then
|
|
30
|
+
exec ".agileflow/scripts/claude-tmux.sh" "$@"
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
# Last resort: just run claude directly
|
|
34
|
+
exec claude "$@"
|
package/scripts/agent-loop.js
CHANGED
|
@@ -25,7 +25,14 @@ const crypto = require('crypto');
|
|
|
25
25
|
// Shared utilities
|
|
26
26
|
const { c } = require('../lib/colors');
|
|
27
27
|
const { getProjectRoot } = require('../lib/paths');
|
|
28
|
-
const { safeReadJSON, safeWriteJSON } = require('../lib/errors');
|
|
28
|
+
const { safeReadJSON, safeWriteJSON, debugLog } = require('../lib/errors');
|
|
29
|
+
const { validateCommand, buildSpawnArgs } = require('../lib/validate-commands');
|
|
30
|
+
const {
|
|
31
|
+
initializeForProject,
|
|
32
|
+
injectCorrelation,
|
|
33
|
+
startSpan,
|
|
34
|
+
getContext,
|
|
35
|
+
} = require('../lib/correlation');
|
|
29
36
|
|
|
30
37
|
const ROOT = getProjectRoot();
|
|
31
38
|
const LOOPS_DIR = path.join(ROOT, '.agileflow', 'sessions', 'agent-loops');
|
|
@@ -85,11 +92,13 @@ function emitEvent(event) {
|
|
|
85
92
|
fs.mkdirSync(busDir, { recursive: true });
|
|
86
93
|
}
|
|
87
94
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
95
|
+
// Inject correlation IDs (trace_id, session_id, span_id)
|
|
96
|
+
const correlatedEvent = injectCorrelation({
|
|
97
|
+
...event,
|
|
98
|
+
timestamp: new Date().toISOString(),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const line = JSON.stringify(correlatedEvent) + '\n';
|
|
93
102
|
|
|
94
103
|
fs.appendFileSync(BUS_PATH, line);
|
|
95
104
|
}
|
|
@@ -128,12 +137,51 @@ function getCoverageReportPath() {
|
|
|
128
137
|
return 'coverage/coverage-summary.json';
|
|
129
138
|
}
|
|
130
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Run a command safely using spawn with validated arguments
|
|
142
|
+
* @param {string} cmd - Command string to run
|
|
143
|
+
* @returns {{ passed: boolean, exitCode: number, error?: string, blocked?: boolean }}
|
|
144
|
+
*/
|
|
131
145
|
function runCommand(cmd) {
|
|
146
|
+
// Validate command against allowlist
|
|
147
|
+
const validation = buildSpawnArgs(cmd, { strict: true, logBlocked: true });
|
|
148
|
+
|
|
149
|
+
if (!validation.ok) {
|
|
150
|
+
console.error(
|
|
151
|
+
`${c.red}Command blocked: ${validation.error}${c.reset}` +
|
|
152
|
+
(validation.severity ? ` [${validation.severity}]` : '')
|
|
153
|
+
);
|
|
154
|
+
debugLog('runCommand', {
|
|
155
|
+
blocked: true,
|
|
156
|
+
command: cmd.slice(0, 50),
|
|
157
|
+
error: validation.error,
|
|
158
|
+
severity: validation.severity,
|
|
159
|
+
});
|
|
160
|
+
return { passed: false, exitCode: 1, error: validation.error, blocked: true };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const { file, args } = validation.data;
|
|
164
|
+
|
|
132
165
|
try {
|
|
133
|
-
|
|
134
|
-
|
|
166
|
+
// Use spawnSync with array arguments (no shell injection possible)
|
|
167
|
+
const result = spawnSync(file, args, {
|
|
168
|
+
cwd: ROOT,
|
|
169
|
+
stdio: 'inherit',
|
|
170
|
+
shell: false, // CRITICAL: Do not use shell to prevent injection
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
if (result.error) {
|
|
174
|
+
// spawn itself failed (e.g., command not found)
|
|
175
|
+
console.error(`${c.red}Spawn error: ${result.error.message}${c.reset}`);
|
|
176
|
+
return { passed: false, exitCode: 1, error: result.error.message };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
passed: result.status === 0,
|
|
181
|
+
exitCode: result.status || 0,
|
|
182
|
+
};
|
|
135
183
|
} catch (error) {
|
|
136
|
-
return { passed: false, exitCode: error.status || 1 };
|
|
184
|
+
return { passed: false, exitCode: error.status || 1, error: error.message };
|
|
137
185
|
}
|
|
138
186
|
}
|
|
139
187
|
|
|
@@ -265,6 +313,9 @@ function initLoop(options) {
|
|
|
265
313
|
parentId = null,
|
|
266
314
|
} = options;
|
|
267
315
|
|
|
316
|
+
// Initialize correlation context (trace_id, session_id)
|
|
317
|
+
const { traceId, sessionId } = initializeForProject(ROOT);
|
|
318
|
+
|
|
268
319
|
// Validate gate
|
|
269
320
|
if (!GATES[gate]) {
|
|
270
321
|
console.error(`${c.red}Invalid gate: ${gate}${c.reset}`);
|
|
@@ -295,6 +346,8 @@ function initLoop(options) {
|
|
|
295
346
|
|
|
296
347
|
const state = {
|
|
297
348
|
loop_id: loopId,
|
|
349
|
+
trace_id: traceId,
|
|
350
|
+
session_id: sessionId,
|
|
298
351
|
agent_type: agentType,
|
|
299
352
|
parent_orchestration: parentId,
|
|
300
353
|
quality_gate: gate,
|
|
@@ -324,6 +377,7 @@ function initLoop(options) {
|
|
|
324
377
|
console.log(`${c.green}${c.bold}Agent Loop Initialized${c.reset}`);
|
|
325
378
|
console.log(`${c.dim}${'─'.repeat(40)}${c.reset}`);
|
|
326
379
|
console.log(` Loop ID: ${c.cyan}${loopId}${c.reset}`);
|
|
380
|
+
console.log(` Trace ID: ${c.dim}${traceId}${c.reset}`);
|
|
327
381
|
console.log(` Gate: ${c.magenta}${GATES[gate].name}${c.reset}`);
|
|
328
382
|
console.log(` Threshold: ${threshold > 0 ? threshold + '%' : 'pass/fail'}`);
|
|
329
383
|
console.log(` Max Iterations: ${maxIter}`);
|
|
@@ -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
|
|
26
|
+
* Features: sessionstart, precompact, ralphloop, selfimprove, archival, statusline, autoupdate, damagecontrol, askuserquestion, tmuxautospawn
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
29
|
const fs = require('fs');
|
|
@@ -130,7 +130,7 @@ ${c.cyan}Feature Control:${c.reset}
|
|
|
130
130
|
--enable=<list> Enable features (comma-separated)
|
|
131
131
|
--disable=<list> Disable features (comma-separated)
|
|
132
132
|
|
|
133
|
-
Features: sessionstart, precompact, ralphloop, selfimprove, archival, statusline, damagecontrol, askuserquestion
|
|
133
|
+
Features: sessionstart, precompact, ralphloop, selfimprove, archival, statusline, damagecontrol, askuserquestion, tmuxautospawn
|
|
134
134
|
|
|
135
135
|
${c.cyan}Statusline Components:${c.reset}
|
|
136
136
|
--show=<list> Show statusline components (comma-separated)
|