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.
Files changed (99) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +3 -3
  3. package/lib/README.md +178 -0
  4. package/lib/codebase-indexer.js +31 -23
  5. package/lib/colors.js +190 -12
  6. package/lib/consent.js +232 -0
  7. package/lib/correlation.js +277 -0
  8. package/lib/error-codes.js +46 -0
  9. package/lib/errors.js +48 -6
  10. package/lib/file-cache.js +182 -0
  11. package/lib/format-error.js +156 -0
  12. package/lib/path-resolver.js +155 -7
  13. package/lib/paths.js +212 -20
  14. package/lib/placeholder-registry.js +205 -0
  15. package/lib/registry-di.js +358 -0
  16. package/lib/result-schema.js +363 -0
  17. package/lib/result.js +210 -0
  18. package/lib/session-registry.js +13 -0
  19. package/lib/session-state-machine.js +465 -0
  20. package/lib/validate-commands.js +308 -0
  21. package/lib/validate.js +116 -52
  22. package/package.json +1 -1
  23. package/scripts/af +34 -0
  24. package/scripts/agent-loop.js +63 -9
  25. package/scripts/agileflow-configure.js +2 -2
  26. package/scripts/agileflow-welcome.js +435 -23
  27. package/scripts/archive-completed-stories.sh +57 -11
  28. package/scripts/claude-tmux.sh +102 -0
  29. package/scripts/damage-control-bash.js +3 -70
  30. package/scripts/damage-control-edit.js +3 -20
  31. package/scripts/damage-control-write.js +3 -20
  32. package/scripts/dependency-check.js +310 -0
  33. package/scripts/get-env.js +11 -4
  34. package/scripts/lib/configure-detect.js +23 -1
  35. package/scripts/lib/configure-features.js +43 -2
  36. package/scripts/lib/context-formatter.js +771 -0
  37. package/scripts/lib/context-loader.js +699 -0
  38. package/scripts/lib/damage-control-utils.js +107 -0
  39. package/scripts/lib/json-utils.sh +162 -0
  40. package/scripts/lib/state-migrator.js +353 -0
  41. package/scripts/lib/story-state-machine.js +437 -0
  42. package/scripts/obtain-context.js +80 -1248
  43. package/scripts/pre-push-check.sh +46 -0
  44. package/scripts/precompact-context.sh +23 -10
  45. package/scripts/query-codebase.js +122 -14
  46. package/scripts/ralph-loop.js +5 -5
  47. package/scripts/session-manager.js +220 -42
  48. package/scripts/spawn-parallel.js +651 -0
  49. package/scripts/tui/blessed/data/watcher.js +20 -15
  50. package/scripts/tui/blessed/index.js +2 -2
  51. package/scripts/tui/blessed/panels/output.js +14 -8
  52. package/scripts/tui/blessed/panels/sessions.js +22 -15
  53. package/scripts/tui/blessed/panels/trace.js +14 -8
  54. package/scripts/tui/blessed/ui/help.js +3 -3
  55. package/scripts/tui/blessed/ui/screen.js +4 -4
  56. package/scripts/tui/blessed/ui/statusbar.js +5 -9
  57. package/scripts/tui/blessed/ui/tabbar.js +11 -11
  58. package/scripts/validators/component-validator.js +41 -14
  59. package/scripts/validators/json-schema-validator.js +11 -4
  60. package/scripts/validators/markdown-validator.js +1 -2
  61. package/scripts/validators/migration-validator.js +17 -5
  62. package/scripts/validators/security-validator.js +137 -33
  63. package/scripts/validators/story-format-validator.js +31 -10
  64. package/scripts/validators/test-result-validator.js +19 -4
  65. package/scripts/validators/workflow-validator.js +12 -5
  66. package/src/core/agents/codebase-query.md +24 -0
  67. package/src/core/commands/adr.md +114 -0
  68. package/src/core/commands/agent.md +120 -0
  69. package/src/core/commands/assign.md +145 -0
  70. package/src/core/commands/babysit.md +32 -5
  71. package/src/core/commands/changelog.md +118 -0
  72. package/src/core/commands/configure.md +42 -6
  73. package/src/core/commands/diagnose.md +114 -0
  74. package/src/core/commands/epic.md +113 -0
  75. package/src/core/commands/handoff.md +128 -0
  76. package/src/core/commands/help.md +75 -0
  77. package/src/core/commands/pr.md +96 -0
  78. package/src/core/commands/roadmap/analyze.md +400 -0
  79. package/src/core/commands/session/new.md +113 -6
  80. package/src/core/commands/session/spawn.md +197 -0
  81. package/src/core/commands/sprint.md +22 -0
  82. package/src/core/commands/status.md +74 -0
  83. package/src/core/commands/story.md +143 -4
  84. package/src/core/templates/agileflow-metadata.json +55 -2
  85. package/src/core/templates/plan-template.md +125 -0
  86. package/src/core/templates/story-lifecycle.md +213 -0
  87. package/src/core/templates/story-template.md +4 -0
  88. package/src/core/templates/tdd-test-template.js +241 -0
  89. package/tools/cli/commands/setup.js +86 -0
  90. package/tools/cli/installers/core/installer.js +94 -0
  91. package/tools/cli/installers/ide/_base-ide.js +20 -11
  92. package/tools/cli/installers/ide/codex.js +29 -47
  93. package/tools/cli/lib/config-manager.js +17 -2
  94. package/tools/cli/lib/content-transformer.js +271 -0
  95. package/tools/cli/lib/error-handler.js +14 -22
  96. package/tools/cli/lib/ide-error-factory.js +421 -0
  97. package/tools/cli/lib/ide-health-monitor.js +364 -0
  98. package/tools/cli/lib/ide-registry.js +114 -1
  99. package/tools/cli/lib/ui.js +14 -25
@@ -5,12 +5,26 @@
5
5
 
6
6
  set -e
7
7
 
8
- # Colors for output
9
- RED='\033[0;31m'
10
- GREEN='\033[0;32m'
11
- YELLOW='\033[1;33m'
12
- BLUE='\033[0;34m'
13
- NC='\033[0m' # No Color
8
+ # Source shared utilities
9
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10
+
11
+ # Source colors from canonical source (lib/colors.sh)
12
+ if [[ -f "$SCRIPT_DIR/lib/colors.sh" ]]; then
13
+ source "$SCRIPT_DIR/lib/colors.sh"
14
+ NC="$RESET" # Alias for backwards compatibility
15
+ else
16
+ # Fallback colors if colors.sh not available
17
+ RED='\033[0;31m'
18
+ GREEN='\033[0;32m'
19
+ YELLOW='\033[1;33m'
20
+ BLUE='\033[0;34m'
21
+ NC='\033[0m'
22
+ fi
23
+
24
+ # Source JSON utilities if available
25
+ if [[ -f "$SCRIPT_DIR/lib/json-utils.sh" ]]; then
26
+ source "$SCRIPT_DIR/lib/json-utils.sh"
27
+ fi
14
28
 
15
29
  # Default paths (relative to project root)
16
30
  DOCS_DIR="docs"
@@ -45,12 +59,17 @@ THRESHOLD_DAYS=7
45
59
  ENABLED=true
46
60
 
47
61
  if [[ -f "$METADATA_FILE" ]]; then
48
- if command -v jq &> /dev/null; then
62
+ # Use safeJsonParse if available (from json-utils.sh), otherwise fallback
63
+ if declare -f safeJsonParse > /dev/null; then
64
+ ENABLED=$(safeJsonParse "$METADATA_FILE" ".archival.enabled" "true")
65
+ THRESHOLD_DAYS=$(safeJsonParse "$METADATA_FILE" ".archival.threshold_days" "7")
66
+ elif command -v jq &> /dev/null; then
49
67
  ENABLED=$(jq -r '.archival.enabled // true' "$METADATA_FILE")
50
68
  THRESHOLD_DAYS=$(jq -r '.archival.threshold_days // 7' "$METADATA_FILE")
51
69
  elif command -v node &> /dev/null; then
52
- ENABLED=$(node -pe "JSON.parse(require('fs').readFileSync('$METADATA_FILE', 'utf8')).archival?.enabled ?? true")
53
- THRESHOLD_DAYS=$(node -pe "JSON.parse(require('fs').readFileSync('$METADATA_FILE', 'utf8')).archival?.threshold_days ?? 7")
70
+ # Security: Pass file path via environment variable, not string interpolation
71
+ ENABLED=$(METADATA_PATH="$METADATA_FILE" node -pe "JSON.parse(require('fs').readFileSync(process.env.METADATA_PATH, 'utf8')).archival?.enabled ?? true" 2>/dev/null || echo "true")
72
+ THRESHOLD_DAYS=$(METADATA_PATH="$METADATA_FILE" node -pe "JSON.parse(require('fs').readFileSync(process.env.METADATA_PATH, 'utf8')).archival?.threshold_days ?? 7" 2>/dev/null || echo "7")
54
73
  fi
55
74
  fi
56
75
 
@@ -64,6 +83,15 @@ echo -e "${BLUE}Starting auto-archival (threshold: $THRESHOLD_DAYS days)...${NC}
64
83
  # Create archive directory if needed
65
84
  mkdir -p "$ARCHIVE_DIR"
66
85
 
86
+ # Security: Validate archive directory is not a symlink pointing outside project
87
+ if [[ -L "$ARCHIVE_DIR" ]]; then
88
+ RESOLVED_ARCHIVE=$(readlink -f "$ARCHIVE_DIR" 2>/dev/null || realpath "$ARCHIVE_DIR" 2>/dev/null)
89
+ if [[ ! "$RESOLVED_ARCHIVE" == "$PROJECT_ROOT"* ]]; then
90
+ echo -e "${RED}Error: Archive directory symlink points outside project. Aborting.${NC}"
91
+ exit 1
92
+ fi
93
+ fi
94
+
67
95
  # Calculate cutoff date (threshold days ago)
68
96
  if [[ "$OSTYPE" == "darwin"* ]]; then
69
97
  # macOS
@@ -118,6 +146,12 @@ for (const [storyId, story] of Object.entries(toArchive)) {
118
146
  const date = new Date(story.completed_at);
119
147
  const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
120
148
 
149
+ // Security: Validate monthKey matches expected format (YYYY-MM) to prevent path traversal
150
+ if (!/^\d{4}-\d{2}$/.test(monthKey)) {
151
+ console.error(`\x1b[31mSkipping story ${storyId}: invalid date format\x1b[0m`);
152
+ continue;
153
+ }
154
+
121
155
  if (!byMonth[monthKey]) {
122
156
  byMonth[monthKey] = {
123
157
  month: monthKey,
@@ -133,10 +167,22 @@ for (const [storyId, story] of Object.entries(toArchive)) {
133
167
  for (const [monthKey, archiveData] of Object.entries(byMonth)) {
134
168
  const archiveFile = path.join(archiveDir, `${monthKey}.json`);
135
169
 
170
+ // Security: Verify resolved path stays within archive directory
171
+ const resolvedPath = path.resolve(archiveFile);
172
+ const resolvedArchiveDir = path.resolve(archiveDir);
173
+ if (!resolvedPath.startsWith(resolvedArchiveDir + path.sep) && resolvedPath !== resolvedArchiveDir) {
174
+ console.error(`\x1b[31mSecurity: Archive path ${archiveFile} escapes archive directory. Skipping.\x1b[0m`);
175
+ continue;
176
+ }
177
+
136
178
  // Merge with existing archive if it exists
137
179
  if (fs.existsSync(archiveFile)) {
138
- const existing = JSON.parse(fs.readFileSync(archiveFile, 'utf8'));
139
- archiveData.stories = { ...existing.stories, ...archiveData.stories };
180
+ try {
181
+ const existing = JSON.parse(fs.readFileSync(archiveFile, 'utf8'));
182
+ archiveData.stories = { ...existing.stories, ...archiveData.stories };
183
+ } catch (e) {
184
+ console.error(`\x1b[31mWarning: Could not parse existing ${monthKey}.json, will overwrite\x1b[0m`);
185
+ }
140
186
  }
141
187
 
142
188
  fs.writeFileSync(archiveFile, JSON.stringify(archiveData, null, 2));
@@ -0,0 +1,102 @@
1
+ #!/bin/bash
2
+ # claude-tmux.sh - Wrapper script that auto-starts Claude Code in a tmux session
3
+ #
4
+ # Usage:
5
+ # ./claude-tmux.sh # Start in tmux with default session
6
+ # ./claude-tmux.sh --no-tmux # Start without tmux (regular claude)
7
+ # ./claude-tmux.sh -n # Same as --no-tmux
8
+ #
9
+ # When already in tmux: Just runs claude normally
10
+ # When not in tmux: Creates a tmux session and runs claude inside it
11
+
12
+ set -e
13
+
14
+ # Check for --no-tmux flag
15
+ NO_TMUX=false
16
+ for arg in "$@"; do
17
+ case $arg in
18
+ --no-tmux|-n)
19
+ NO_TMUX=true
20
+ shift
21
+ ;;
22
+ esac
23
+ done
24
+
25
+ # If --no-tmux was specified, just run claude directly
26
+ if [ "$NO_TMUX" = true ]; then
27
+ exec claude "$@"
28
+ fi
29
+
30
+ # Check if tmux auto-spawn is disabled in config
31
+ METADATA_FILE="docs/00-meta/agileflow-metadata.json"
32
+ if [ -f "$METADATA_FILE" ]; then
33
+ # Use node to parse JSON (more reliable than jq which may not be installed)
34
+ TMUX_ENABLED=$(node -e "
35
+ try {
36
+ const meta = JSON.parse(require('fs').readFileSync('$METADATA_FILE', 'utf8'));
37
+ // Default to true (enabled) if not explicitly set to false
38
+ console.log(meta.features?.tmuxAutoSpawn?.enabled !== false ? 'true' : 'false');
39
+ } catch (e) {
40
+ console.log('true'); // Default to enabled on error
41
+ }
42
+ " 2>/dev/null || echo "true")
43
+
44
+ if [ "$TMUX_ENABLED" = "false" ]; then
45
+ exec claude "$@"
46
+ fi
47
+ fi
48
+
49
+ # Check if we're already inside tmux
50
+ if [ -n "$TMUX" ]; then
51
+ # Already in tmux, just run claude
52
+ exec claude "$@"
53
+ fi
54
+
55
+ # Check if tmux is available
56
+ if ! command -v tmux &> /dev/null; then
57
+ echo "tmux not found. Running claude without tmux."
58
+ echo "Install tmux for parallel session support:"
59
+ echo " macOS: brew install tmux"
60
+ echo " Ubuntu/Debian: sudo apt install tmux"
61
+ echo ""
62
+ exec claude "$@"
63
+ fi
64
+
65
+ # Generate session name based on current directory
66
+ DIR_NAME=$(basename "$(pwd)")
67
+ SESSION_NAME="claude-${DIR_NAME}"
68
+
69
+ # Check if session already exists
70
+ if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
71
+ echo "Attaching to existing session: $SESSION_NAME"
72
+ exec tmux attach-session -t "$SESSION_NAME"
73
+ fi
74
+
75
+ # Create new tmux session with Claude
76
+ echo "Starting Claude in tmux session: $SESSION_NAME"
77
+
78
+ # Create session in detached mode first
79
+ tmux new-session -d -s "$SESSION_NAME" -n "main"
80
+
81
+ # Minimal config - mouse and scrolling only, no fancy styling
82
+ tmux set-option -t "$SESSION_NAME" mouse on
83
+
84
+ # Fix colors - proper terminal support
85
+ tmux set-option -t "$SESSION_NAME" default-terminal "xterm-256color"
86
+
87
+ # Keybindings: Alt+number to switch windows, Alt+q to detach
88
+ for i in 1 2 3 4 5 6 7 8 9; do
89
+ tmux bind-key -n "M-$i" select-window -t ":$((i-1))"
90
+ done
91
+ tmux bind-key -n M-q detach-client
92
+
93
+ # Send the claude command to the first window
94
+ CLAUDE_CMD="claude"
95
+ if [ $# -gt 0 ]; then
96
+ # Pass any remaining arguments to claude
97
+ CLAUDE_CMD="claude $*"
98
+ fi
99
+ tmux send-keys -t "$SESSION_NAME" "$CLAUDE_CMD" Enter
100
+
101
+ # Attach to the session
102
+ exec tmux attach-session -t "$SESSION_NAME"
@@ -15,74 +15,7 @@
15
15
  * Usage: Configured as PreToolUse hook in .claude/settings.json
16
16
  */
17
17
 
18
- const {
19
- findProjectRoot,
20
- loadPatterns,
21
- outputBlocked,
22
- runDamageControlHook,
23
- parseBashPatterns,
24
- c,
25
- } = require('./lib/damage-control-utils');
18
+ const { createBashHook } = require('./lib/damage-control-utils');
26
19
 
27
- /**
28
- * Test command against a single pattern rule
29
- */
30
- function matchesPattern(command, rule) {
31
- try {
32
- const flags = rule.flags || '';
33
- const regex = new RegExp(rule.pattern, flags);
34
- return regex.test(command);
35
- } catch (e) {
36
- // Invalid regex - skip this pattern
37
- return false;
38
- }
39
- }
40
-
41
- /**
42
- * Validate command against all patterns
43
- */
44
- function validateCommand(command, config) {
45
- // Check blocked patterns (bashToolPatterns + agileflowProtections)
46
- const blockedPatterns = [
47
- ...(config.bashToolPatterns || []),
48
- ...(config.agileflowProtections || []),
49
- ];
50
-
51
- for (const rule of blockedPatterns) {
52
- if (matchesPattern(command, rule)) {
53
- return {
54
- action: 'block',
55
- reason: rule.reason || 'Command blocked by damage control',
56
- };
57
- }
58
- }
59
-
60
- // Check ask patterns
61
- for (const rule of config.askPatterns || []) {
62
- if (matchesPattern(command, rule)) {
63
- return {
64
- action: 'ask',
65
- reason: rule.reason || 'Please confirm this command',
66
- };
67
- }
68
- }
69
-
70
- // Allow by default
71
- return { action: 'allow' };
72
- }
73
-
74
- // Run the hook
75
- const projectRoot = findProjectRoot();
76
- const defaultConfig = { bashToolPatterns: [], askPatterns: [], agileflowProtections: [] };
77
-
78
- runDamageControlHook({
79
- getInputValue: input => input.command || input.tool_input?.command,
80
- loadConfig: () => loadPatterns(projectRoot, parseBashPatterns, defaultConfig),
81
- validate: validateCommand,
82
- onBlock: (result, command) => {
83
- outputBlocked(
84
- result.reason,
85
- `Command: ${command.substring(0, 100)}${command.length > 100 ? '...' : ''}`
86
- );
87
- },
88
- });
20
+ // Run the hook using factory
21
+ createBashHook()();
@@ -12,24 +12,7 @@
12
12
  * Usage: Configured as PreToolUse hook in .claude/settings.json
13
13
  */
14
14
 
15
- const {
16
- findProjectRoot,
17
- loadPatterns,
18
- outputBlocked,
19
- runDamageControlHook,
20
- parsePathPatterns,
21
- validatePathAgainstPatterns,
22
- } = require('./lib/damage-control-utils');
15
+ const { createPathHook } = require('./lib/damage-control-utils');
23
16
 
24
- // Run the hook
25
- const projectRoot = findProjectRoot();
26
- const defaultConfig = { zeroAccessPaths: [], readOnlyPaths: [], noDeletePaths: [] };
27
-
28
- runDamageControlHook({
29
- getInputValue: input => input.file_path || input.tool_input?.file_path,
30
- loadConfig: () => loadPatterns(projectRoot, parsePathPatterns, defaultConfig),
31
- validate: (filePath, config) => validatePathAgainstPatterns(filePath, config, 'edit'),
32
- onBlock: (result, filePath) => {
33
- outputBlocked(result.reason, result.detail, `File: ${filePath}`);
34
- },
35
- });
17
+ // Run the hook using factory
18
+ createPathHook('edit')();
@@ -12,24 +12,7 @@
12
12
  * Usage: Configured as PreToolUse hook in .claude/settings.json
13
13
  */
14
14
 
15
- const {
16
- findProjectRoot,
17
- loadPatterns,
18
- outputBlocked,
19
- runDamageControlHook,
20
- parsePathPatterns,
21
- validatePathAgainstPatterns,
22
- } = require('./lib/damage-control-utils');
15
+ const { createPathHook } = require('./lib/damage-control-utils');
23
16
 
24
- // Run the hook
25
- const projectRoot = findProjectRoot();
26
- const defaultConfig = { zeroAccessPaths: [], readOnlyPaths: [], noDeletePaths: [] };
27
-
28
- runDamageControlHook({
29
- getInputValue: input => input.file_path || input.tool_input?.file_path,
30
- loadConfig: () => loadPatterns(projectRoot, parsePathPatterns, defaultConfig),
31
- validate: (filePath, config) => validatePathAgainstPatterns(filePath, config, 'write'),
32
- onBlock: (result, filePath) => {
33
- outputBlocked(result.reason, result.detail, `File: ${filePath}`);
34
- },
35
- });
17
+ // Run the hook using factory
18
+ createPathHook('write')();
@@ -0,0 +1,310 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * AgileFlow CLI - Dependency Check Script
5
+ *
6
+ * Local script for checking dependency vulnerabilities and generating reports.
7
+ * Can be run manually or integrated into CI/CD pipelines.
8
+ *
9
+ * Usage:
10
+ * node scripts/dependency-check.js [options]
11
+ *
12
+ * Options:
13
+ * --json Output results as JSON
14
+ * --fix Attempt to auto-fix vulnerabilities
15
+ * --force Force fixes (may include breaking changes)
16
+ * --severity=X Minimum severity to report (low, moderate, high, critical)
17
+ * --quiet Only show errors
18
+ * --help Show help
19
+ */
20
+
21
+ const { execSync } = require('child_process');
22
+ const fs = require('fs');
23
+ const path = require('path');
24
+
25
+ // ANSI colors
26
+ const c = {
27
+ reset: '\x1b[0m',
28
+ bold: '\x1b[1m',
29
+ dim: '\x1b[2m',
30
+ red: '\x1b[31m',
31
+ green: '\x1b[32m',
32
+ yellow: '\x1b[33m',
33
+ cyan: '\x1b[36m',
34
+ };
35
+
36
+ const SEVERITY_LEVELS = ['low', 'moderate', 'high', 'critical'];
37
+
38
+ /**
39
+ * Parse command line arguments
40
+ */
41
+ function parseArgs(args) {
42
+ const options = {
43
+ json: false,
44
+ fix: false,
45
+ force: false,
46
+ severity: 'low',
47
+ quiet: false,
48
+ help: false,
49
+ };
50
+
51
+ for (const arg of args) {
52
+ if (arg === '--json') options.json = true;
53
+ else if (arg === '--fix') options.fix = true;
54
+ else if (arg === '--force') options.force = true;
55
+ else if (arg === '--quiet') options.quiet = true;
56
+ else if (arg === '--help' || arg === '-h') options.help = true;
57
+ else if (arg.startsWith('--severity=')) {
58
+ const level = arg.split('=')[1].toLowerCase();
59
+ if (SEVERITY_LEVELS.includes(level)) {
60
+ options.severity = level;
61
+ }
62
+ }
63
+ }
64
+
65
+ return options;
66
+ }
67
+
68
+ /**
69
+ * Show help message
70
+ */
71
+ function showHelp() {
72
+ console.log(`
73
+ ${c.bold}AgileFlow Dependency Check${c.reset}
74
+
75
+ ${c.cyan}Usage:${c.reset}
76
+ node scripts/dependency-check.js [options]
77
+
78
+ ${c.cyan}Options:${c.reset}
79
+ --json Output results as JSON
80
+ --fix Attempt to auto-fix vulnerabilities
81
+ --force Force fixes (may include breaking changes)
82
+ --severity=X Minimum severity to report (low, moderate, high, critical)
83
+ --quiet Only show errors
84
+ --help, -h Show this help message
85
+
86
+ ${c.cyan}Examples:${c.reset}
87
+ # Check for all vulnerabilities
88
+ node scripts/dependency-check.js
89
+
90
+ # Only report high and critical
91
+ node scripts/dependency-check.js --severity=high
92
+
93
+ # Auto-fix and output JSON
94
+ node scripts/dependency-check.js --fix --json
95
+
96
+ # Force all fixes
97
+ node scripts/dependency-check.js --fix --force
98
+ `);
99
+ }
100
+
101
+ /**
102
+ * Run npm audit and parse results
103
+ */
104
+ function runAudit() {
105
+ try {
106
+ const output = execSync('npm audit --json 2>&1', {
107
+ encoding: 'utf8',
108
+ maxBuffer: 10 * 1024 * 1024, // 10MB buffer for large outputs
109
+ });
110
+ return JSON.parse(output);
111
+ } catch (error) {
112
+ // npm audit exits with non-zero if vulnerabilities found
113
+ if (error.stdout) {
114
+ try {
115
+ return JSON.parse(error.stdout);
116
+ } catch {
117
+ return { error: error.message, metadata: { vulnerabilities: {} } };
118
+ }
119
+ }
120
+ return { error: error.message, metadata: { vulnerabilities: {} } };
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Apply npm audit fix
126
+ */
127
+ function runFix(force = false) {
128
+ const cmd = force
129
+ ? 'npm audit fix --force --legacy-peer-deps'
130
+ : 'npm audit fix --legacy-peer-deps';
131
+ try {
132
+ const output = execSync(cmd, { encoding: 'utf8', stdio: 'pipe' });
133
+ return { success: true, output };
134
+ } catch (error) {
135
+ return { success: false, error: error.message, output: error.stdout || '' };
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Filter vulnerabilities by severity
141
+ */
142
+ function filterBySeverity(audit, minSeverity) {
143
+ const minIndex = SEVERITY_LEVELS.indexOf(minSeverity);
144
+ const filtered = { ...audit };
145
+
146
+ if (audit.vulnerabilities) {
147
+ filtered.vulnerabilities = Object.fromEntries(
148
+ Object.entries(audit.vulnerabilities).filter(([, vuln]) => {
149
+ const vulnIndex = SEVERITY_LEVELS.indexOf(vuln.severity);
150
+ return vulnIndex >= minIndex;
151
+ })
152
+ );
153
+ }
154
+
155
+ return filtered;
156
+ }
157
+
158
+ /**
159
+ * Format audit results for console output
160
+ */
161
+ function formatResults(audit, options) {
162
+ const { metadata = {}, vulnerabilities = {} } = audit;
163
+ const vulnCounts = metadata.vulnerabilities || {};
164
+
165
+ const lines = [];
166
+
167
+ // Header
168
+ lines.push(`\n${c.bold}Dependency Audit Report${c.reset}`);
169
+ lines.push(`${c.dim}${'─'.repeat(40)}${c.reset}\n`);
170
+
171
+ // Summary
172
+ const total = vulnCounts.total || 0;
173
+ if (total === 0) {
174
+ lines.push(`${c.green}✓ No vulnerabilities found!${c.reset}\n`);
175
+ return lines.join('\n');
176
+ }
177
+
178
+ lines.push(`${c.bold}Vulnerabilities Found: ${total}${c.reset}`);
179
+ lines.push('');
180
+
181
+ // By severity
182
+ if (vulnCounts.critical > 0) {
183
+ lines.push(` ${c.red}● Critical: ${vulnCounts.critical}${c.reset}`);
184
+ }
185
+ if (vulnCounts.high > 0) {
186
+ lines.push(` ${c.yellow}● High: ${vulnCounts.high}${c.reset}`);
187
+ }
188
+ if (vulnCounts.moderate > 0) {
189
+ lines.push(` ${c.cyan}● Moderate: ${vulnCounts.moderate}${c.reset}`);
190
+ }
191
+ if (vulnCounts.low > 0) {
192
+ lines.push(` ${c.dim}● Low: ${vulnCounts.low}${c.reset}`);
193
+ }
194
+ lines.push('');
195
+
196
+ // Details (if not quiet)
197
+ if (!options.quiet && Object.keys(vulnerabilities).length > 0) {
198
+ lines.push(`${c.bold}Details:${c.reset}`);
199
+ for (const [name, vuln] of Object.entries(vulnerabilities)) {
200
+ const severityColor =
201
+ vuln.severity === 'critical'
202
+ ? c.red
203
+ : vuln.severity === 'high'
204
+ ? c.yellow
205
+ : vuln.severity === 'moderate'
206
+ ? c.cyan
207
+ : c.dim;
208
+ lines.push(` ${severityColor}[${vuln.severity.toUpperCase()}]${c.reset} ${name}`);
209
+ if (vuln.via && Array.isArray(vuln.via)) {
210
+ const vias = vuln.via.filter(v => typeof v === 'object').slice(0, 2);
211
+ for (const via of vias) {
212
+ if (via.title) {
213
+ lines.push(` └─ ${via.title}`);
214
+ }
215
+ }
216
+ }
217
+ }
218
+ lines.push('');
219
+ }
220
+
221
+ // Recommendations
222
+ lines.push(`${c.bold}Recommendations:${c.reset}`);
223
+ lines.push(' Run: npm audit fix');
224
+ if (vulnCounts.critical > 0 || vulnCounts.high > 0) {
225
+ lines.push(' Or force: npm audit fix --force (may include breaking changes)');
226
+ }
227
+ lines.push('');
228
+
229
+ return lines.join('\n');
230
+ }
231
+
232
+ /**
233
+ * Main function
234
+ */
235
+ async function main() {
236
+ const options = parseArgs(process.argv.slice(2));
237
+
238
+ if (options.help) {
239
+ showHelp();
240
+ process.exit(0);
241
+ }
242
+
243
+ if (!options.quiet && !options.json) {
244
+ console.log(`${c.cyan}Running dependency audit...${c.reset}`);
245
+ }
246
+
247
+ // Run audit
248
+ let audit = runAudit();
249
+
250
+ // Filter by severity
251
+ audit = filterBySeverity(audit, options.severity);
252
+
253
+ // Apply fix if requested
254
+ let fixResult = null;
255
+ if (options.fix) {
256
+ if (!options.quiet && !options.json) {
257
+ console.log(`${c.cyan}Applying fixes...${c.reset}`);
258
+ }
259
+ fixResult = runFix(options.force);
260
+ }
261
+
262
+ // Output results
263
+ if (options.json) {
264
+ const result = {
265
+ audit,
266
+ fix: fixResult,
267
+ timestamp: new Date().toISOString(),
268
+ severity_filter: options.severity,
269
+ };
270
+ console.log(JSON.stringify(result, null, 2));
271
+ } else {
272
+ console.log(formatResults(audit, options));
273
+ if (fixResult) {
274
+ if (fixResult.success) {
275
+ console.log(`${c.green}✓ Fixes applied successfully${c.reset}`);
276
+ } else {
277
+ console.log(`${c.yellow}⚠ Some fixes could not be applied${c.reset}`);
278
+ }
279
+ }
280
+ }
281
+
282
+ // Exit with appropriate code
283
+ const vulnCounts = audit.metadata?.vulnerabilities || {};
284
+ const total = vulnCounts.total || 0;
285
+ const hasHighSeverity = (vulnCounts.critical || 0) + (vulnCounts.high || 0) > 0;
286
+
287
+ if (hasHighSeverity) {
288
+ process.exit(2); // High/critical vulnerabilities
289
+ } else if (total > 0) {
290
+ process.exit(1); // Some vulnerabilities
291
+ }
292
+ process.exit(0); // All clear
293
+ }
294
+
295
+ // Run if called directly
296
+ if (require.main === module) {
297
+ main().catch(error => {
298
+ console.error(`${c.red}Error: ${error.message}${c.reset}`);
299
+ process.exit(1);
300
+ });
301
+ }
302
+
303
+ module.exports = {
304
+ parseArgs,
305
+ runAudit,
306
+ runFix,
307
+ filterBySeverity,
308
+ formatResults,
309
+ SEVERITY_LEVELS,
310
+ };
@@ -19,6 +19,12 @@ const path = require('path');
19
19
  const os = require('os');
20
20
  const { execSync } = require('child_process');
21
21
 
22
+ // Import centralized path utilities
23
+ const { getStatusPath } = require('../lib/paths');
24
+
25
+ // Import centralized file cache (US-0176: deduplicated status.json access)
26
+ const { readJSONCached } = require('../lib/file-cache');
27
+
22
28
  function getProjectInfo() {
23
29
  const rootDir = path.resolve(__dirname, '..');
24
30
 
@@ -72,14 +78,15 @@ function getProjectInfo() {
72
78
  const activeEpics = [];
73
79
 
74
80
  try {
75
- const statusPath = path.join(rootDir, 'docs/09-agents/status.json');
76
- if (fs.existsSync(statusPath)) {
77
- const status = JSON.parse(fs.readFileSync(statusPath, 'utf8'));
81
+ const statusPath = getStatusPath(rootDir);
82
+ // Use centralized file cache (US-0176: 60s TTL, shared across all consumers)
83
+ const status = readJSONCached(statusPath, { ttlMs: 60000 });
78
84
 
85
+ if (status) {
79
86
  // Get active stories
80
87
  if (status.stories) {
81
88
  Object.entries(status.stories).forEach(([id, story]) => {
82
- if (story.status === 'in_progress') {
89
+ if (story.status === 'in-progress') {
83
90
  activeStories.push({ id, title: story.title, owner: story.owner });
84
91
  wipCount++;
85
92
  }