agileflow 2.99.7 → 3.0.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 (65) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/lib/cache-provider.js +155 -0
  3. package/lib/codebase-indexer.js +1 -1
  4. package/lib/content-sanitizer.js +1 -0
  5. package/lib/dashboard-protocol.js +25 -0
  6. package/lib/dashboard-server.js +184 -133
  7. package/lib/errors.js +18 -0
  8. package/lib/file-cache.js +1 -1
  9. package/lib/flag-detection.js +11 -20
  10. package/lib/git-operations.js +15 -33
  11. package/lib/merge-operations.js +40 -34
  12. package/lib/process-executor.js +199 -0
  13. package/lib/registry-cache.js +13 -47
  14. package/lib/skill-loader.js +206 -0
  15. package/lib/smart-json-file.js +2 -4
  16. package/package.json +1 -1
  17. package/scripts/agileflow-configure.js +13 -12
  18. package/scripts/agileflow-statusline.sh +30 -0
  19. package/scripts/agileflow-welcome.js +181 -212
  20. package/scripts/auto-self-improve.js +3 -3
  21. package/scripts/claude-smart.sh +67 -0
  22. package/scripts/claude-tmux.sh +248 -170
  23. package/scripts/damage-control-multi-agent.js +227 -0
  24. package/scripts/lib/bus-utils.js +471 -0
  25. package/scripts/lib/configure-detect.js +5 -6
  26. package/scripts/lib/configure-features.js +44 -0
  27. package/scripts/lib/configure-repair.js +5 -6
  28. package/scripts/lib/configure-utils.js +2 -3
  29. package/scripts/lib/context-formatter.js +87 -8
  30. package/scripts/lib/damage-control-utils.js +37 -3
  31. package/scripts/lib/file-lock.js +392 -0
  32. package/scripts/lib/ideation-index.js +2 -5
  33. package/scripts/lib/lifecycle-detector.js +123 -0
  34. package/scripts/lib/process-cleanup.js +55 -81
  35. package/scripts/lib/scale-detector.js +357 -0
  36. package/scripts/lib/signal-detectors.js +779 -0
  37. package/scripts/lib/story-state-machine.js +1 -1
  38. package/scripts/lib/sync-ideation-status.js +2 -3
  39. package/scripts/lib/task-registry.js +7 -1
  40. package/scripts/lib/team-events.js +357 -0
  41. package/scripts/messaging-bridge.js +79 -36
  42. package/scripts/migrate-ideation-index.js +37 -14
  43. package/scripts/obtain-context.js +37 -19
  44. package/scripts/ralph-loop.js +3 -4
  45. package/scripts/smart-detect.js +390 -0
  46. package/scripts/team-manager.js +174 -30
  47. package/src/core/commands/audit.md +13 -11
  48. package/src/core/commands/babysit.md +162 -115
  49. package/src/core/commands/changelog.md +21 -4
  50. package/src/core/commands/configure.md +105 -2
  51. package/src/core/commands/debt.md +12 -2
  52. package/src/core/commands/feedback.md +7 -6
  53. package/src/core/commands/ideate/history.md +1 -1
  54. package/src/core/commands/ideate/new.md +5 -5
  55. package/src/core/commands/logic/audit.md +2 -2
  56. package/src/core/commands/pr.md +7 -6
  57. package/src/core/commands/research/analyze.md +28 -20
  58. package/src/core/commands/research/ask.md +43 -0
  59. package/src/core/commands/research/import.md +29 -21
  60. package/src/core/commands/research/list.md +8 -7
  61. package/src/core/commands/research/synthesize.md +356 -20
  62. package/src/core/commands/research/view.md +8 -5
  63. package/src/core/commands/review.md +24 -6
  64. package/src/core/commands/skill/create.md +34 -0
  65. package/tools/cli/lib/docs-setup.js +4 -0
@@ -8,6 +8,7 @@ const fs = require('fs');
8
8
  const path = require('path');
9
9
  const { execFileSync } = require('child_process');
10
10
  const { c, log, header, readJSON } = require('./configure-utils');
11
+ const { tryOptional } = require('../../lib/errors');
11
12
 
12
13
  // ============================================================================
13
14
  // DETECTION
@@ -64,12 +65,10 @@ function detectConfig(version) {
64
65
  // Git detection
65
66
  if (fs.existsSync('.git')) {
66
67
  status.git.initialized = true;
67
- try {
68
- status.git.remote = execFileSync('git', ['remote', 'get-url', 'origin'], {
69
- encoding: 'utf8',
70
- stdio: ['pipe', 'pipe', 'pipe'],
71
- }).trim();
72
- } catch {}
68
+ status.git.remote = tryOptional(() => execFileSync('git', ['remote', 'get-url', 'origin'], {
69
+ encoding: 'utf8',
70
+ stdio: ['pipe', 'pipe', 'pipe'],
71
+ }).trim(), 'git remote') ?? null;
73
72
  }
74
73
 
75
74
  // Settings file detection
@@ -56,6 +56,10 @@ const FEATURES = {
56
56
  metadataOnly: true,
57
57
  description: 'Default flags for Claude CLI (e.g., --dangerously-skip-permissions)',
58
58
  },
59
+ agentteams: {
60
+ metadataOnly: true,
61
+ description: 'Enable Claude Code native Agent Teams (experimental multi-agent orchestration)',
62
+ },
59
63
  };
60
64
 
61
65
  const PROFILES = {
@@ -324,6 +328,27 @@ function enableFeature(feature, options = {}, version) {
324
328
  return true;
325
329
  }
326
330
 
331
+ // Handle agent teams (metadata only)
332
+ if (feature === 'agentteams') {
333
+ updateMetadata(
334
+ {
335
+ features: {
336
+ agentTeams: {
337
+ enabled: true,
338
+ version,
339
+ at: new Date().toISOString(),
340
+ },
341
+ },
342
+ },
343
+ version
344
+ );
345
+ success('Native Agent Teams enabled');
346
+ info('Claude Code will use native TeamCreate/SendMessage tools when available');
347
+ info('Requires: CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 (set by Claude Code)');
348
+ info('Fallback: subagent mode (Task/TaskOutput) when native is unavailable');
349
+ return true;
350
+ }
351
+
327
352
  // Handle shell aliases
328
353
  if (feature === 'shellaliases') {
329
354
  const result = enableShellAliases();
@@ -730,6 +755,25 @@ function disableFeature(feature, version) {
730
755
  return true;
731
756
  }
732
757
 
758
+ // Disable agent teams
759
+ if (feature === 'agentteams') {
760
+ updateMetadata(
761
+ {
762
+ features: {
763
+ agentTeams: {
764
+ enabled: false,
765
+ version,
766
+ at: new Date().toISOString(),
767
+ },
768
+ },
769
+ },
770
+ version
771
+ );
772
+ success('Native Agent Teams disabled');
773
+ info('AgileFlow will use subagent mode (Task/TaskOutput) for multi-agent orchestration');
774
+ return true;
775
+ }
776
+
733
777
  // Disable shell aliases
734
778
  if (feature === 'shellaliases') {
735
779
  const result = disableShellAliases();
@@ -9,6 +9,7 @@ const path = require('path');
9
9
  const crypto = require('crypto');
10
10
  const { execFileSync } = require('child_process');
11
11
  const { feedback } = require('../../lib/feedback');
12
+ const { tryOptional } = require('../../lib/errors');
12
13
  const {
13
14
  c,
14
15
  log,
@@ -115,11 +116,11 @@ function listScripts() {
115
116
  // Check if modified
116
117
  let isModified = false;
117
118
  if (exists && fileIndex?.files?.[`scripts/${script}`]) {
118
- try {
119
+ isModified = tryOptional(() => {
119
120
  const currentHash = sha256(fs.readFileSync(scriptPath));
120
121
  const indexHash = fileIndex.files[`scripts/${script}`].sha256;
121
- isModified = currentHash !== indexHash;
122
- } catch {}
122
+ return currentHash !== indexHash;
123
+ }, 'hash check') ?? false;
123
124
  }
124
125
 
125
126
  // Print status
@@ -279,9 +280,7 @@ function repairScripts(targetFeature = null) {
279
280
  if (fs.existsSync(srcPath)) {
280
281
  try {
281
282
  fs.copyFileSync(srcPath, destPath);
282
- try {
283
- fs.chmodSync(destPath, 0o755);
284
- } catch {}
283
+ tryOptional(() => fs.chmodSync(destPath, 0o755), 'chmod');
285
284
  success(`Restored ${script}`);
286
285
  repaired++;
287
286
  } catch (err) {
@@ -6,6 +6,7 @@
6
6
 
7
7
  const fs = require('fs');
8
8
  const path = require('path');
9
+ const { tryOptional } = require('../../lib/errors');
9
10
 
10
11
  // ============================================================================
11
12
  // COLORS & LOGGING
@@ -58,9 +59,7 @@ const copyTemplate = (templateName, destPath) => {
58
59
  for (const src of sources) {
59
60
  if (fs.existsSync(src)) {
60
61
  fs.copyFileSync(src, destPath);
61
- try {
62
- fs.chmodSync(destPath, '755');
63
- } catch {}
62
+ tryOptional(() => fs.chmodSync(destPath, '755'), 'chmod');
64
63
  return true;
65
64
  }
66
65
  }
@@ -321,6 +321,23 @@ function generateSummary(prefetched = null, options = {}) {
321
321
 
322
322
  summary += divider();
323
323
 
324
+ // Scale indicator (EP-0033)
325
+ let scaleDetectorSummary;
326
+ try { scaleDetectorSummary = require('./scale-detector'); } catch { /* not available */ }
327
+ if (scaleDetectorSummary) {
328
+ try {
329
+ const scaleResult = scaleDetectorSummary.detectScale({
330
+ rootDir: process.cwd(),
331
+ statusJson: prefetched?.json?.statusJson,
332
+ sessionState: prefetched?.json?.sessionState,
333
+ });
334
+ const label = scaleDetectorSummary.getScaleLabel(scaleResult.scale);
335
+ summary += row('Scale', label, C.lavender, C.cyan);
336
+ } catch {
337
+ // Silently ignore
338
+ }
339
+ }
340
+
324
341
  // Key files
325
342
  const keyFileChecks = [
326
343
  { path: 'CLAUDE.md', label: 'CLAUDE' },
@@ -378,7 +395,7 @@ function generateSummary(prefetched = null, options = {}) {
378
395
  * @returns {string} Full content
379
396
  */
380
397
  function generateFullContent(prefetched = null, options = {}) {
381
- const { commandName = null, activeSections = [] } = options;
398
+ const { commandName = null, activeSections = [], smartDetectResults = null } = options;
382
399
 
383
400
  let content = '';
384
401
 
@@ -437,12 +454,17 @@ function generateFullContent(prefetched = null, options = {}) {
437
454
 
438
455
  if (askUserQuestionConfig?.enabled) {
439
456
  content += `${C.coral}${C.bold}┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓${C.reset}\n`;
440
- content += `${C.coral}${C.bold}┃ 🔔 MANDATORY: AskUserQuestion After EVERY Response ┃${C.reset}\n`;
457
+ content += `${C.coral}${C.bold}┃ 🔔 SMART AskUserQuestion After EVERY Response ┃${C.reset}\n`;
441
458
  content += `${C.coral}${C.bold}┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛${C.reset}\n`;
442
459
  content += `${C.bold}After completing ANY task${C.reset} (implementation, fix, etc.):\n`;
443
- content += `${C.mintGreen}→ ALWAYS${C.reset} call ${C.skyBlue}AskUserQuestion${C.reset} tool to offer next steps\n`;
444
- content += `${C.coral}→ NEVER${C.reset} end with text like "Done!" or "What's next?"\n\n`;
445
- content += `${C.dim}Balance: Use at natural pause points. Don't ask permission for routine work.${C.reset}\n\n`;
460
+ content += `${C.mintGreen}→ ALWAYS${C.reset} call ${C.skyBlue}AskUserQuestion${C.reset} tool with ${C.bold}SMART, contextual${C.reset} options\n`;
461
+ content += `${C.coral}→ NEVER${C.reset} generic options like "Continue" or "What next?"\n\n`;
462
+ content += `${C.bold}Smart Suggestion Principles:${C.reset}\n`;
463
+ content += `${C.mintGreen}→${C.reset} Always mark one option ${C.bold}"(Recommended)"${C.reset} based on logical next step\n`;
464
+ content += `${C.mintGreen}→${C.reset} Be specific: ${C.dim}"Run npm test for auth changes"${C.reset} not ${C.dim}"Run tests"${C.reset}\n`;
465
+ content += `${C.mintGreen}→${C.reset} Include context: ${C.dim}"3 files changed"${C.reset}, story IDs, test results\n`;
466
+ content += `${C.mintGreen}→${C.reset} Suggest workflow progression: plan → implement → test → commit\n`;
467
+ content += `${C.mintGreen}→${C.reset} After errors: suggest specific alternative, not ${C.dim}"fix it"${C.reset}\n\n`;
446
468
  }
447
469
 
448
470
  // CONTEXT BUDGET WARNING
@@ -479,6 +501,27 @@ function generateFullContent(prefetched = null, options = {}) {
479
501
  content += '\n';
480
502
  }
481
503
 
504
+ // SCALE DETECTION (EP-0033)
505
+ let scaleDetector;
506
+ try { scaleDetector = require('./scale-detector'); } catch { /* not available */ }
507
+ if (scaleDetector) {
508
+ try {
509
+ const scaleResult = scaleDetector.detectScale({
510
+ rootDir: process.cwd(),
511
+ statusJson: prefetched?.json?.statusJson,
512
+ sessionState: prefetched?.json?.sessionState,
513
+ });
514
+ const recs = scaleDetector.getScaleRecommendations(scaleResult.scale);
515
+ const label = scaleDetector.getScaleLabel(scaleResult.scale);
516
+ content += `\n${C.cyan}${C.bold}═══ Project Scale: ${label} ═══${C.reset}\n`;
517
+ content += `${C.dim}Detected: ${scaleResult.metrics.files} files, ${scaleResult.metrics.stories} stories, ${scaleResult.metrics.commits} commits (6mo)${C.reset}\n`;
518
+ content += `Planning depth: ${C.mintGreen}${recs.planningDepth}${C.reset} | Experts: ${C.mintGreen}${recs.expertCount}${C.reset}\n`;
519
+ content += `${C.dim}${recs.description}${C.reset}\n`;
520
+ } catch {
521
+ // Silently ignore scale detection errors
522
+ }
523
+ }
524
+
482
525
  // GIT STATUS
483
526
  content += `\n${C.skyBlue}${C.bold}═══ Git Status ═══${C.reset}\n`;
484
527
  const branch = prefetched?.git?.branch ?? safeExec('git branch --show-current') ?? 'unknown';
@@ -533,7 +576,7 @@ function generateFullContent(prefetched = null, options = {}) {
533
576
  if (Array.isArray(sessionState.active_commands) && sessionState.active_commands.length > 0) {
534
577
  const cmdNames = sessionState.active_commands.map(c => c.name).join(', ');
535
578
  content += `Active commands: ${C.skyBlue}${cmdNames}${C.reset}\n`;
536
- } else if (sessionState.active_command) {
579
+ } else if (sessionState.active_command && sessionState.active_command.name) {
537
580
  content += `Active command: ${C.skyBlue}${sessionState.active_command.name}${C.reset}\n`;
538
581
  }
539
582
 
@@ -592,7 +635,7 @@ function generateRemainingContent(prefetched, options = {}) {
592
635
  const sessionDir = story.claimedBy?.path
593
636
  ? path.basename(story.claimedBy.path)
594
637
  : 'unknown';
595
- content += ` ${C.coral}🔒${C.reset} ${C.lavender}${story.id}${C.reset} "${story.title}" ${C.dim}→ Session ${story.claimedBy?.session_id || '?'} (${sessionDir})${C.reset}\n`;
638
+ content += ` ${C.coral}🔒${C.reset} ${C.lavender}${story.id ?? '?'}${C.reset} "${story.title ?? 'untitled'}" ${C.dim}→ Session ${story.claimedBy?.session_id ?? '?'} (${sessionDir})${C.reset}\n`;
596
639
  });
597
640
  content += '\n';
598
641
  }
@@ -601,7 +644,7 @@ function generateRemainingContent(prefetched, options = {}) {
601
644
  if (myResult.ok && myResult.stories && myResult.stories.length > 0) {
602
645
  content += `\n${C.mintGreen}${C.bold}═══ ✓ Your Claimed Stories ═══${C.reset}\n`;
603
646
  myResult.stories.forEach(story => {
604
- content += ` ${C.mintGreen}✓${C.reset} ${C.lavender}${story.id}${C.reset} "${story.title}"\n`;
647
+ content += ` ${C.mintGreen}✓${C.reset} ${C.lavender}${story.id ?? '?'}${C.reset} "${story.title ?? 'untitled'}"\n`;
605
648
  });
606
649
  content += '\n';
607
650
  }
@@ -813,6 +856,42 @@ function generateRemainingContent(prefetched, options = {}) {
813
856
  }
814
857
  }
815
858
 
859
+ // SMART RECOMMENDATIONS (Contextual Feature Router)
860
+ const smartDetectResults = options.smartDetectResults;
861
+ if (smartDetectResults && !smartDetectResults.disabled) {
862
+ const { immediate, available, auto_enabled } = smartDetectResults.recommendations;
863
+ const hasRecommendations = immediate.length > 0 || available.length > 0;
864
+
865
+ if (hasRecommendations) {
866
+ content += `\n${C.brand}${C.bold}═══ Smart Recommendations ═══${C.reset}\n`;
867
+ content += `${C.dim}Phase: ${smartDetectResults.lifecycle_phase} (${smartDetectResults.phase_reason})${C.reset}\n`;
868
+
869
+ if (immediate.length > 0) {
870
+ content += `\n${C.coral}${C.bold}Immediate:${C.reset}\n`;
871
+ immediate.forEach(r => {
872
+ content += ` ${C.coral}!${C.reset} ${C.bold}${r.feature}${C.reset}: ${r.trigger} ${C.dim}(${r.command})${C.reset}\n`;
873
+ });
874
+ }
875
+
876
+ if (available.length > 0) {
877
+ content += `\n${C.skyBlue}Available:${C.reset}\n`;
878
+ available.slice(0, 5).forEach(r => {
879
+ content += ` ${C.skyBlue}>${C.reset} ${r.feature}: ${r.trigger} ${C.dim}(${r.command})${C.reset}\n`;
880
+ });
881
+ if (available.length > 5) {
882
+ content += ` ${C.dim}... and ${available.length - 5} more${C.reset}\n`;
883
+ }
884
+ }
885
+ }
886
+
887
+ // Show auto-enabled modes
888
+ const modes = auto_enabled || {};
889
+ const enabledModes = Object.entries(modes).filter(([, v]) => v).map(([k]) => k.replace('_', ' '));
890
+ if (enabledModes.length > 0) {
891
+ content += `\n${C.mintGreen}Auto-enabled:${C.reset} ${enabledModes.join(', ')}\n`;
892
+ }
893
+ }
894
+
816
895
  // FOOTER
817
896
  content += `\n${C.dim}─────────────────────────────────────────${C.reset}\n`;
818
897
  content += `${C.dim}Context gathered in single execution. Claude has full context.${C.reset}\n`;
@@ -33,6 +33,14 @@ const c = {
33
33
  reset: '\x1b[0m',
34
34
  };
35
35
 
36
+ // Pattern cache: avoids re-reading and re-parsing YAML on every hook invocation.
37
+ // Invalidated when the config file's mtime changes.
38
+ const _patternCache = {
39
+ /** @type {string|null} */ filePath: null,
40
+ /** @type {number} */ mtime: 0,
41
+ /** @type {object|null} */ config: null,
42
+ };
43
+
36
44
  // Shared constants
37
45
  const CONFIG_PATHS = [
38
46
  '.agileflow/config/damage-control-patterns.yaml',
@@ -70,8 +78,9 @@ function expandPath(p) {
70
78
  }
71
79
 
72
80
  /**
73
- * Load patterns configuration from YAML file
74
- * Returns empty config if not found (fail-open)
81
+ * Load patterns configuration from YAML file with caching.
82
+ * Returns cached config when the file hasn't changed (mtime check).
83
+ * Returns empty config if not found (fail-open).
75
84
  *
76
85
  * @param {string} projectRoot - Project root directory
77
86
  * @param {function} parseYAML - Function to parse YAML content
@@ -83,8 +92,23 @@ function loadPatterns(projectRoot, parseYAML, defaultConfig = {}) {
83
92
  const fullPath = path.join(projectRoot, configPath);
84
93
  if (fs.existsSync(fullPath)) {
85
94
  try {
95
+ // Check mtime for cache invalidation
96
+ const stat = fs.statSync(fullPath);
97
+ const mtime = stat.mtimeMs;
98
+
99
+ if (_patternCache.filePath === fullPath && _patternCache.mtime === mtime && _patternCache.config) {
100
+ return _patternCache.config;
101
+ }
102
+
86
103
  const content = fs.readFileSync(fullPath, 'utf8');
87
- return parseYAML(content);
104
+ const config = parseYAML(content);
105
+
106
+ // Store in cache
107
+ _patternCache.filePath = fullPath;
108
+ _patternCache.mtime = mtime;
109
+ _patternCache.config = config;
110
+
111
+ return config;
88
112
  } catch (e) {
89
113
  // Continue to next path
90
114
  }
@@ -95,6 +119,15 @@ function loadPatterns(projectRoot, parseYAML, defaultConfig = {}) {
95
119
  return defaultConfig;
96
120
  }
97
121
 
122
+ /**
123
+ * Clear the pattern cache (for testing or forced reload).
124
+ */
125
+ function clearPatternCache() {
126
+ _patternCache.filePath = null;
127
+ _patternCache.mtime = 0;
128
+ _patternCache.config = null;
129
+ }
130
+
98
131
  /**
99
132
  * Check if a file path matches any of the protected patterns
100
133
  *
@@ -559,6 +592,7 @@ module.exports = {
559
592
  findProjectRoot,
560
593
  expandPath,
561
594
  loadPatterns,
595
+ clearPatternCache,
562
596
  pathMatches,
563
597
  outputBlocked,
564
598
  runDamageControlHook,