agileflow 3.4.0 → 3.4.1

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 (112) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +4 -4
  3. package/package.json +1 -1
  4. package/scripts/agileflow-welcome.js +79 -0
  5. package/scripts/claude-tmux.sh +12 -36
  6. package/scripts/lib/ac-test-matcher.js +452 -0
  7. package/scripts/lib/audit-registry.js +58 -2
  8. package/scripts/lib/configure-features.js +35 -0
  9. package/scripts/lib/model-profiles.js +25 -5
  10. package/scripts/lib/quality-gates.js +163 -0
  11. package/scripts/lib/signal-detectors.js +43 -0
  12. package/scripts/lib/status-writer.js +255 -0
  13. package/scripts/lib/story-claiming.js +128 -45
  14. package/scripts/lib/task-sync.js +32 -38
  15. package/scripts/lib/tmux-audit-monitor.js +611 -0
  16. package/scripts/lib/tool-registry.yaml +241 -0
  17. package/scripts/lib/tool-shed.js +441 -0
  18. package/scripts/native-team-observer.js +219 -0
  19. package/scripts/obtain-context.js +14 -0
  20. package/scripts/ralph-loop.js +30 -5
  21. package/scripts/smart-detect.js +21 -0
  22. package/scripts/spawn-audit-sessions.js +372 -44
  23. package/scripts/team-manager.js +19 -0
  24. package/src/core/agents/a11y-analyzer-aria.md +155 -0
  25. package/src/core/agents/a11y-analyzer-forms.md +162 -0
  26. package/src/core/agents/a11y-analyzer-keyboard.md +175 -0
  27. package/src/core/agents/a11y-analyzer-semantic.md +153 -0
  28. package/src/core/agents/a11y-analyzer-visual.md +158 -0
  29. package/src/core/agents/a11y-consensus.md +248 -0
  30. package/src/core/agents/ads-consensus.md +74 -0
  31. package/src/core/agents/ads-generate.md +145 -0
  32. package/src/core/agents/ads-performance-tracker.md +197 -0
  33. package/src/core/agents/api-quality-analyzer-conventions.md +148 -0
  34. package/src/core/agents/api-quality-analyzer-docs.md +176 -0
  35. package/src/core/agents/api-quality-analyzer-errors.md +183 -0
  36. package/src/core/agents/api-quality-analyzer-pagination.md +171 -0
  37. package/src/core/agents/api-quality-analyzer-versioning.md +143 -0
  38. package/src/core/agents/api-quality-consensus.md +214 -0
  39. package/src/core/agents/arch-analyzer-circular.md +148 -0
  40. package/src/core/agents/arch-analyzer-complexity.md +171 -0
  41. package/src/core/agents/arch-analyzer-coupling.md +146 -0
  42. package/src/core/agents/arch-analyzer-layering.md +151 -0
  43. package/src/core/agents/arch-analyzer-patterns.md +162 -0
  44. package/src/core/agents/arch-consensus.md +227 -0
  45. package/src/core/commands/adr.md +1 -0
  46. package/src/core/commands/ads/generate.md +238 -0
  47. package/src/core/commands/ads/health.md +327 -0
  48. package/src/core/commands/ads/test-plan.md +317 -0
  49. package/src/core/commands/ads/track.md +288 -0
  50. package/src/core/commands/ads.md +28 -16
  51. package/src/core/commands/assign.md +1 -0
  52. package/src/core/commands/audit.md +43 -6
  53. package/src/core/commands/babysit.md +90 -6
  54. package/src/core/commands/baseline.md +1 -0
  55. package/src/core/commands/blockers.md +1 -0
  56. package/src/core/commands/board.md +1 -0
  57. package/src/core/commands/changelog.md +1 -0
  58. package/src/core/commands/choose.md +1 -0
  59. package/src/core/commands/ci.md +1 -0
  60. package/src/core/commands/code/accessibility.md +347 -0
  61. package/src/core/commands/code/api.md +297 -0
  62. package/src/core/commands/code/architecture.md +297 -0
  63. package/src/core/commands/code/completeness.md +43 -6
  64. package/src/core/commands/code/legal.md +43 -6
  65. package/src/core/commands/code/logic.md +43 -6
  66. package/src/core/commands/code/performance.md +43 -6
  67. package/src/core/commands/code/security.md +43 -6
  68. package/src/core/commands/code/test.md +43 -6
  69. package/src/core/commands/configure.md +1 -0
  70. package/src/core/commands/council.md +1 -0
  71. package/src/core/commands/deploy.md +1 -0
  72. package/src/core/commands/diagnose.md +1 -0
  73. package/src/core/commands/docs.md +1 -0
  74. package/src/core/commands/epic/edit.md +213 -0
  75. package/src/core/commands/epic.md +1 -0
  76. package/src/core/commands/export.md +238 -0
  77. package/src/core/commands/help.md +16 -1
  78. package/src/core/commands/ideate/discover.md +7 -3
  79. package/src/core/commands/ideate/features.md +65 -4
  80. package/src/core/commands/ideate/new.md +158 -124
  81. package/src/core/commands/impact.md +1 -0
  82. package/src/core/commands/learn/explain.md +118 -0
  83. package/src/core/commands/learn/glossary.md +135 -0
  84. package/src/core/commands/learn/patterns.md +138 -0
  85. package/src/core/commands/learn/tour.md +126 -0
  86. package/src/core/commands/migrate/codemods.md +151 -0
  87. package/src/core/commands/migrate/plan.md +131 -0
  88. package/src/core/commands/migrate/scan.md +114 -0
  89. package/src/core/commands/migrate/validate.md +119 -0
  90. package/src/core/commands/multi-expert.md +1 -0
  91. package/src/core/commands/pr.md +1 -0
  92. package/src/core/commands/review.md +1 -0
  93. package/src/core/commands/sprint.md +1 -0
  94. package/src/core/commands/status/undo.md +191 -0
  95. package/src/core/commands/status.md +1 -0
  96. package/src/core/commands/story/edit.md +204 -0
  97. package/src/core/commands/story/view.md +29 -7
  98. package/src/core/commands/story-validate.md +1 -0
  99. package/src/core/commands/story.md +1 -0
  100. package/src/core/commands/tdd.md +1 -0
  101. package/src/core/commands/team/start.md +10 -6
  102. package/src/core/commands/tests.md +1 -0
  103. package/src/core/commands/verify.md +27 -1
  104. package/src/core/commands/workflow.md +2 -0
  105. package/src/core/teams/backend.json +41 -0
  106. package/src/core/teams/frontend.json +41 -0
  107. package/src/core/teams/qa.json +41 -0
  108. package/src/core/teams/solo.json +35 -0
  109. package/src/core/templates/agileflow-metadata.json +5 -0
  110. package/tools/cli/commands/setup.js +85 -3
  111. package/tools/cli/commands/update.js +42 -0
  112. package/tools/cli/installers/ide/claude-code.js +68 -0
@@ -132,6 +132,62 @@ const AUDIT_TYPES = {
132
132
  deep_analyzers: ['handlers', 'routes', 'api', 'stubs', 'state', 'imports', 'conditional'],
133
133
  },
134
134
 
135
+ brainstorm: {
136
+ name: 'Feature Brainstorm',
137
+ prefix: 'Brain',
138
+ color: '#c0caf5', // lavender
139
+ command: 'ideate/features',
140
+ analyzers: {
141
+ features: { subagent_type: 'brainstorm-analyzer-features', label: 'Feature Gaps' },
142
+ ux: { subagent_type: 'brainstorm-analyzer-ux', label: 'UX Improvements' },
143
+ market: { subagent_type: 'brainstorm-analyzer-market', label: 'Market Features' },
144
+ growth: { subagent_type: 'brainstorm-analyzer-growth', label: 'Growth & Engagement' },
145
+ integration: { subagent_type: 'brainstorm-analyzer-integration', label: 'Integrations' },
146
+ },
147
+ consensus: { subagent_type: 'brainstorm-consensus', label: 'Brainstorm Consensus' },
148
+ quick_analyzers: ['features', 'ux', 'market'],
149
+ deep_analyzers: ['features', 'ux', 'market', 'growth', 'integration'],
150
+ },
151
+
152
+ ideate: {
153
+ name: 'Ideation',
154
+ prefix: 'Idea',
155
+ color: '#ff9e64', // orange
156
+ command: 'ideate/new',
157
+ analyzers: {
158
+ security: { subagent_type: 'agileflow-security', label: 'Security' },
159
+ performance: { subagent_type: 'agileflow-performance', label: 'Performance' },
160
+ refactor: { subagent_type: 'agileflow-refactor', label: 'Code Quality' },
161
+ ui: { subagent_type: 'agileflow-ui', label: 'UX/Design' },
162
+ testing: { subagent_type: 'agileflow-testing', label: 'Testing' },
163
+ api: { subagent_type: 'agileflow-api', label: 'API/Architecture' },
164
+ accessibility: { subagent_type: 'agileflow-accessibility', label: 'Accessibility' },
165
+ compliance: { subagent_type: 'agileflow-compliance', label: 'Compliance' },
166
+ database: { subagent_type: 'agileflow-database', label: 'Database' },
167
+ monitoring: { subagent_type: 'agileflow-monitoring', label: 'Monitoring' },
168
+ qa: { subagent_type: 'agileflow-qa', label: 'QA' },
169
+ analytics: { subagent_type: 'agileflow-analytics', label: 'Analytics' },
170
+ documentation: { subagent_type: 'agileflow-documentation', label: 'Documentation' },
171
+ },
172
+ consensus: null, // ideation does its own synthesis (no consensus coordinator)
173
+ quick_analyzers: ['security', 'performance', 'refactor', 'ui', 'testing', 'api'],
174
+ deep_analyzers: [
175
+ 'security',
176
+ 'performance',
177
+ 'refactor',
178
+ 'ui',
179
+ 'testing',
180
+ 'api',
181
+ 'accessibility',
182
+ 'compliance',
183
+ 'database',
184
+ 'monitoring',
185
+ 'qa',
186
+ 'analytics',
187
+ 'documentation',
188
+ ],
189
+ },
190
+
135
191
  legal: {
136
192
  name: 'Legal Risk',
137
193
  prefix: 'Legal',
@@ -167,7 +223,7 @@ const AUDIT_TYPES = {
167
223
  /**
168
224
  * Get audit type configuration.
169
225
  *
170
- * @param {string} type - Audit type key (logic, security, performance, test, completeness, legal)
226
+ * @param {string} type - Audit type key (logic, security, performance, test, completeness, legal, ideate)
171
227
  * @returns {object|null} Audit type config or null if invalid
172
228
  */
173
229
  function getAuditType(type) {
@@ -195,7 +251,7 @@ function getAnalyzersForAudit(type, depth, focus) {
195
251
  const audit = AUDIT_TYPES[type];
196
252
  if (!audit) return null;
197
253
 
198
- const effectiveDepth = depth === 'ultradeep' ? 'deep' : depth || 'quick';
254
+ const effectiveDepth = depth === 'ultradeep' || depth === 'extreme' ? 'deep' : depth || 'quick';
199
255
  const analyzerKeys = effectiveDepth === 'deep' ? audit.deep_analyzers : audit.quick_analyzers;
200
256
 
201
257
  // Filter by focus if specified
@@ -393,6 +393,25 @@ function enableFeature(feature, options = {}, version) {
393
393
  if (feature === 'agentteams') {
394
394
  settings.env = settings.env || {};
395
395
  settings.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = '1';
396
+
397
+ // Register PostToolUse hooks for native team observability
398
+ if (!settings.hooks) settings.hooks = {};
399
+ if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
400
+ const observerCmd = 'node $CLAUDE_PROJECT_DIR/.agileflow/scripts/native-team-observer.js';
401
+ for (const matcher of ['TeamCreate', 'SendMessage', 'ListTeams']) {
402
+ const exists = settings.hooks.PostToolUse.some(
403
+ h =>
404
+ h.matcher === matcher &&
405
+ h.hooks?.some(hk => hk.command && hk.command.includes('native-team-observer'))
406
+ );
407
+ if (!exists) {
408
+ settings.hooks.PostToolUse.push({
409
+ matcher,
410
+ hooks: [{ type: 'command', command: observerCmd, timeout: 5000 }],
411
+ });
412
+ }
413
+ }
414
+
396
415
  writeJSON('.claude/settings.json', settings);
397
416
  updateMetadata(
398
417
  {
@@ -408,6 +427,7 @@ function enableFeature(feature, options = {}, version) {
408
427
  );
409
428
  success('Native Agent Teams enabled');
410
429
  info('Set CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 in .claude/settings.json');
430
+ info('Registered PostToolUse hooks for native team observability');
411
431
  info('Claude Code will use native TeamCreate/SendMessage tools');
412
432
  info('Fallback: subagent mode (Task/TaskOutput) when native is unavailable');
413
433
  return true;
@@ -954,6 +974,20 @@ function disableFeature(feature, version) {
954
974
  delete settings.env;
955
975
  }
956
976
  }
977
+
978
+ // Remove PostToolUse hooks for native team observer
979
+ if (settings.hooks?.PostToolUse) {
980
+ settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(
981
+ h => !h.hooks?.some(hk => hk.command && hk.command.includes('native-team-observer'))
982
+ );
983
+ if (settings.hooks.PostToolUse.length === 0) {
984
+ delete settings.hooks.PostToolUse;
985
+ }
986
+ if (Object.keys(settings.hooks).length === 0) {
987
+ delete settings.hooks;
988
+ }
989
+ }
990
+
957
991
  writeJSON('.claude/settings.json', settings);
958
992
  updateMetadata(
959
993
  {
@@ -969,6 +1003,7 @@ function disableFeature(feature, version) {
969
1003
  );
970
1004
  success('Native Agent Teams disabled');
971
1005
  info('Removed CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS from .claude/settings.json');
1006
+ info('Removed PostToolUse hooks for native team observer');
972
1007
  info('AgileFlow will use subagent mode (Task/TaskOutput) for multi-agent orchestration');
973
1008
  return true;
974
1009
  }
@@ -60,9 +60,10 @@ function isValidModel(model) {
60
60
  *
61
61
  * @param {string} model - Model name
62
62
  * @param {number} [analyzerCount=5] - Number of analyzers
63
- * @returns {{ multiplier: number, model: string, perAnalyzerCost: string }}
63
+ * @param {number} [partitions=1] - Number of partitions (extreme mode)
64
+ * @returns {{ multiplier: number, model: string, perAnalyzerCost: string, totalEstimate: string, partitions?: number, totalSessions?: number }}
64
65
  */
65
- function estimateCost(model, analyzerCount) {
66
+ function estimateCost(model, analyzerCount, partitions) {
66
67
  let MODEL_PRICING;
67
68
  try {
68
69
  MODEL_PRICING = require('./team-events').MODEL_PRICING;
@@ -75,19 +76,38 @@ function estimateCost(model, analyzerCount) {
75
76
  }
76
77
 
77
78
  const count = analyzerCount || 5;
79
+ const partCount = typeof partitions === 'number' && partitions > 1 ? partitions : 1;
78
80
  const resolved = resolveModel(model);
79
81
  const pricing = MODEL_PRICING[resolved] || MODEL_PRICING.haiku;
80
82
  const haikuPricing = MODEL_PRICING.haiku;
81
83
 
82
84
  const multiplier = pricing.output / haikuPricing.output;
83
- const perAnalyzer = `$${((pricing.input * 50000) / 1_000_000 + (pricing.output * 10000) / 1_000_000).toFixed(3)}`;
85
+ const perAnalyzerCostNum =
86
+ (pricing.input * 50000) / 1_000_000 + (pricing.output * 10000) / 1_000_000;
87
+ const perAnalyzer = `$${perAnalyzerCostNum.toFixed(3)}`;
84
88
 
85
- return {
89
+ // For extreme mode: each partition has a coordinator + all analyzers as sub-agents
90
+ // Estimated cost per partition coordinator session in USD (~10k input + 2k output at haiku rates)
91
+ const coordinatorCostUSD = 0.05;
92
+ const totalSessions = partCount * count;
93
+ const totalCost =
94
+ partCount > 1
95
+ ? partCount * coordinatorCostUSD + totalSessions * perAnalyzerCostNum
96
+ : count * perAnalyzerCostNum;
97
+
98
+ const result = {
86
99
  multiplier: Math.round(multiplier * 100) / 100,
87
100
  model: resolved,
88
101
  perAnalyzerCost: perAnalyzer,
89
- totalEstimate: `~$${(count * ((pricing.input * 50000) / 1_000_000 + (pricing.output * 10000) / 1_000_000)).toFixed(2)}`,
102
+ totalEstimate: `~$${totalCost.toFixed(2)}`,
90
103
  };
104
+
105
+ if (partCount > 1) {
106
+ result.partitions = partCount;
107
+ result.totalSessions = totalSessions;
108
+ }
109
+
110
+ return result;
91
111
  }
92
112
 
93
113
  module.exports = {
@@ -561,6 +561,164 @@ function createValidationReport(gateResults, options = {}) {
561
561
  return lines.join('\n');
562
562
  }
563
563
 
564
+ // ============================================================================
565
+ // CI Feedback Loop
566
+ // ============================================================================
567
+
568
+ /**
569
+ * Default CI feedback loop configuration
570
+ */
571
+ const CI_FEEDBACK_DEFAULTS = {
572
+ enabled: true,
573
+ max_rounds: 3,
574
+ };
575
+
576
+ /**
577
+ * Load CI feedback loop config from agileflow-metadata.json
578
+ * @param {string} projectRoot - Project root directory
579
+ * @returns {Object} CI feedback loop config
580
+ */
581
+ function loadCIFeedbackConfig(projectRoot) {
582
+ const metadataPath = path.join(projectRoot, 'docs', '00-meta', 'agileflow-metadata.json');
583
+ try {
584
+ if (fs.existsSync(metadataPath)) {
585
+ const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
586
+ if (metadata.ci_feedback_loops) {
587
+ return {
588
+ ...CI_FEEDBACK_DEFAULTS,
589
+ ...metadata.ci_feedback_loops,
590
+ };
591
+ }
592
+ }
593
+ } catch {
594
+ // Fall through to defaults
595
+ }
596
+ return { ...CI_FEEDBACK_DEFAULTS };
597
+ }
598
+
599
+ /**
600
+ * Execute a CI feedback loop - runs gates, and if they fail, returns
601
+ * structured feedback for the agent to retry (up to max_rounds).
602
+ *
603
+ * This implements the Stripe "Minions" pattern: deterministic CI check
604
+ * followed by agent retry, with a hard iteration limit.
605
+ *
606
+ * @param {Object[]} gates - Quality gate definitions to check
607
+ * @param {Object} options - Loop options
608
+ * @param {string} [options.projectRoot] - Project root directory
609
+ * @param {number} [options.maxRounds] - Override max retry rounds (default: from config)
610
+ * @param {number} [options.currentRound] - Current round number (1-based, default: 1)
611
+ * @param {string} [options.cwd] - Working directory for gate execution
612
+ * @returns {Object} Loop result with status and agent feedback
613
+ */
614
+ function executeCIFeedbackLoop(gates, options = {}) {
615
+ const { projectRoot = process.cwd(), maxRounds, currentRound = 1, cwd } = options;
616
+
617
+ const config = loadCIFeedbackConfig(projectRoot);
618
+
619
+ if (!config.enabled) {
620
+ return {
621
+ status: 'disabled',
622
+ message: 'CI feedback loops are disabled in agileflow-metadata.json',
623
+ should_retry: false,
624
+ round: currentRound,
625
+ max_rounds: 0,
626
+ };
627
+ }
628
+
629
+ const effectiveMaxRounds = maxRounds || config.max_rounds || CI_FEEDBACK_DEFAULTS.max_rounds;
630
+
631
+ // Execute all gates
632
+ const gateResults = executeGates(gates, { cwd, stopOnFailure: false });
633
+
634
+ if (gateResults.passed) {
635
+ return {
636
+ status: 'passed',
637
+ message: `All ${gateResults.passed_count} gates passed on round ${currentRound}`,
638
+ should_retry: false,
639
+ round: currentRound,
640
+ max_rounds: effectiveMaxRounds,
641
+ gate_results: gateResults,
642
+ };
643
+ }
644
+
645
+ // Gates failed - determine if we should retry
646
+ const hasRoundsLeft = currentRound < effectiveMaxRounds;
647
+
648
+ if (!hasRoundsLeft) {
649
+ return {
650
+ status: 'exhausted',
651
+ message: `Gates failed after ${currentRound}/${effectiveMaxRounds} rounds. Escalating to human.`,
652
+ should_retry: false,
653
+ round: currentRound,
654
+ max_rounds: effectiveMaxRounds,
655
+ gate_results: gateResults,
656
+ failures: gateResults.results
657
+ .filter(r => r.status === GATE_STATUS.FAILED || r.status === GATE_STATUS.ERROR)
658
+ .map(r => ({
659
+ gate: r.gate,
660
+ message: r.message,
661
+ output: r.output,
662
+ error: r.error,
663
+ })),
664
+ };
665
+ }
666
+
667
+ // Build structured feedback for agent retry
668
+ const failures = gateResults.results.filter(
669
+ r => r.status === GATE_STATUS.FAILED || r.status === GATE_STATUS.ERROR
670
+ );
671
+
672
+ const feedbackLines = [
673
+ `## CI Feedback Loop - Round ${currentRound}/${effectiveMaxRounds}`,
674
+ '',
675
+ `**${failures.length} gate(s) failed.** ${effectiveMaxRounds - currentRound} retry round(s) remaining.`,
676
+ '',
677
+ '### Failures',
678
+ '',
679
+ ];
680
+
681
+ for (const failure of failures) {
682
+ feedbackLines.push(`#### ${failure.gate} (${failure.type})`);
683
+ feedbackLines.push(`- **Status**: ${failure.status}`);
684
+ feedbackLines.push(`- **Message**: ${failure.message}`);
685
+ if (failure.output) {
686
+ feedbackLines.push('- **Output**:');
687
+ feedbackLines.push('```');
688
+ feedbackLines.push(failure.output);
689
+ feedbackLines.push('```');
690
+ }
691
+ if (failure.error) {
692
+ feedbackLines.push('- **Error**:');
693
+ feedbackLines.push('```');
694
+ feedbackLines.push(failure.error);
695
+ feedbackLines.push('```');
696
+ }
697
+ feedbackLines.push('');
698
+ }
699
+
700
+ feedbackLines.push('### Action Required');
701
+ feedbackLines.push('');
702
+ feedbackLines.push('Fix the failing gates above, then re-run verification.');
703
+
704
+ return {
705
+ status: 'retry',
706
+ message: `Round ${currentRound}/${effectiveMaxRounds} failed. Agent should fix and retry.`,
707
+ should_retry: true,
708
+ round: currentRound,
709
+ max_rounds: effectiveMaxRounds,
710
+ next_round: currentRound + 1,
711
+ gate_results: gateResults,
712
+ agent_feedback: feedbackLines.join('\n'),
713
+ failures: failures.map(r => ({
714
+ gate: r.gate,
715
+ message: r.message,
716
+ output: r.output,
717
+ error: r.error,
718
+ })),
719
+ };
720
+ }
721
+
564
722
  // ============================================================================
565
723
  // Exports
566
724
  // ============================================================================
@@ -595,4 +753,9 @@ module.exports = {
595
753
 
596
754
  // Reporting
597
755
  createValidationReport,
756
+
757
+ // CI Feedback Loop
758
+ CI_FEEDBACK_DEFAULTS,
759
+ loadCIFeedbackConfig,
760
+ executeCIFeedbackLoop,
598
761
  };
@@ -233,6 +233,24 @@ const FEATURE_DETECTORS = {
233
233
  });
234
234
  },
235
235
 
236
+ 'scale-adaptive': signals => {
237
+ const { scale } = signals;
238
+ if (!scale || !scale.tier) return null;
239
+ // Only trigger when scale info provides actionable guidance
240
+ const rec = scale.recommendations;
241
+ if (!rec) return null;
242
+ // Suggest scale-adaptive workflow when project is not medium (the default)
243
+ if (scale.tier === 'medium') return null;
244
+ const label = scale.tier.charAt(0).toUpperCase() + scale.tier.slice(1);
245
+ return recommend('scale-adaptive', {
246
+ priority: scale.tier === 'enterprise' || scale.tier === 'large' ? 'medium' : 'low',
247
+ trigger: `${label} project detected (${scale.metrics.files} files, ${scale.metrics.stories} stories) — ${rec.description}`,
248
+ action: 'suggest',
249
+ command: '/agileflow:workflow',
250
+ phase: 'pre-story',
251
+ });
252
+ },
253
+
236
254
  // =========================================================================
237
255
  // PLANNING PHASE
238
256
  // =========================================================================
@@ -517,6 +535,29 @@ const FEATURE_DETECTORS = {
517
535
  });
518
536
  },
519
537
 
538
+ 'ac-verify': signals => {
539
+ const { story, tests } = signals;
540
+ if (!story || story.status !== 'in-progress') return null;
541
+ if (!tests || tests.passing !== true) return null; // Only after tests pass
542
+ if (!storyHasAC(story)) return null;
543
+ // Check if AC already verified (count by index to avoid extra keys)
544
+ const acStatus = story.ac_status || {};
545
+ const acList = story.acceptance_criteria || story.ac || [];
546
+ const verifiedCount = acList.filter(
547
+ (_, i) =>
548
+ acStatus[i] === 'verified' || acStatus[i] === 'auto-verified' || acStatus[i] === true
549
+ ).length;
550
+ if (verifiedCount === acList.length) return null;
551
+ const unverifiedCount = acList.length - verifiedCount;
552
+ return recommend('ac-verify', {
553
+ priority: 'high',
554
+ trigger: `Tests pass but ${unverifiedCount}/${acList.length} AC unverified`,
555
+ action: 'suggest',
556
+ command: '/agileflow:audit',
557
+ phase: 'post-impl',
558
+ });
559
+ },
560
+
520
561
  // =========================================================================
521
562
  // POST-IMPLEMENTATION PHASE
522
563
  // =========================================================================
@@ -704,6 +745,7 @@ const PHASE_MAP = {
704
745
  'workflow',
705
746
  'template',
706
747
  'configure',
748
+ 'scale-adaptive',
707
749
  ],
708
750
  planning: [
709
751
  'impact',
@@ -728,6 +770,7 @@ const PHASE_MAP = {
728
770
  'serve',
729
771
  ],
730
772
  'post-impl': [
773
+ 'ac-verify',
731
774
  'review',
732
775
  'logic-audit',
733
776
  'docs',
@@ -0,0 +1,255 @@
1
+ /**
2
+ * status-writer.js - Canonical write module for status.json mutations
3
+ *
4
+ * ALL status.json story updates should go through this module to ensure:
5
+ * 1. Atomic read-modify-write via file-lock.js
6
+ * 2. State machine validation on status transitions
7
+ * 3. Automatic dependency resolution when stories complete
8
+ *
9
+ * Usage:
10
+ * const { updateStory, readStory } = require('./status-writer');
11
+ * updateStory(rootDir, 'US-0042', { status: 'completed' });
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+
19
+ // Lazy-load file-lock for atomic writes
20
+ let _fileLock;
21
+ function getFileLock() {
22
+ if (_fileLock === undefined) {
23
+ try {
24
+ _fileLock = require('./file-lock');
25
+ } catch (e) {
26
+ _fileLock = null;
27
+ }
28
+ }
29
+ return _fileLock;
30
+ }
31
+
32
+ // Lazy-load story-state-machine for transition validation
33
+ let _stateMachine;
34
+ function getStateMachine() {
35
+ if (_stateMachine === undefined) {
36
+ try {
37
+ _stateMachine = require('./story-state-machine');
38
+ } catch (e) {
39
+ _stateMachine = null;
40
+ }
41
+ }
42
+ return _stateMachine;
43
+ }
44
+
45
+ // Lazy-load paths module
46
+ let _paths;
47
+ function getPaths() {
48
+ if (_paths === undefined) {
49
+ try {
50
+ _paths = require('../../lib/paths');
51
+ } catch (e) {
52
+ _paths = null;
53
+ }
54
+ }
55
+ return _paths;
56
+ }
57
+
58
+ /**
59
+ * Resolve the status.json file path for a given project root.
60
+ * @param {string} rootDir - Project root directory
61
+ * @returns {string} Absolute path to status.json
62
+ */
63
+ function getStatusFilePath(rootDir) {
64
+ const paths = getPaths();
65
+ if (paths && typeof paths.getStatusPath === 'function') {
66
+ return paths.getStatusPath(rootDir);
67
+ }
68
+ return path.join(rootDir, 'docs', '09-agents', 'status.json');
69
+ }
70
+
71
+ /**
72
+ * Read a single story from status.json.
73
+ *
74
+ * @param {string} rootDir - Project root directory
75
+ * @param {string} storyId - Story ID (e.g., 'US-0042')
76
+ * @returns {{ ok: boolean, story?: object, error?: string }}
77
+ */
78
+ function readStory(rootDir, storyId) {
79
+ try {
80
+ const statusPath = getStatusFilePath(rootDir);
81
+ if (!fs.existsSync(statusPath)) {
82
+ return { ok: false, error: 'status.json not found' };
83
+ }
84
+
85
+ const data = JSON.parse(fs.readFileSync(statusPath, 'utf8'));
86
+ if (!data.stories || !data.stories[storyId]) {
87
+ return { ok: false, error: `Story ${storyId} not found` };
88
+ }
89
+
90
+ return { ok: true, story: data.stories[storyId] };
91
+ } catch (e) {
92
+ return { ok: false, error: e.message };
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Resolve dependencies when a story transitions to completed/done.
98
+ * Pure in-memory operation — mutates `data` in place.
99
+ *
100
+ * Iterates all stories, finds those with `depends_on` or `blocked_by`
101
+ * containing `completedStoryId`. If all dependencies are now
102
+ * completed/done, transitions the story from `blocked` → `ready`.
103
+ *
104
+ * @param {object} data - Full status.json data object (mutated in place)
105
+ * @param {string} completedStoryId - The story that just completed
106
+ * @returns {{ unblocked: string[] }} List of story IDs that were unblocked
107
+ */
108
+ function resolveDependencies(data, completedStoryId) {
109
+ const unblocked = [];
110
+ if (!data || !data.stories) return { unblocked };
111
+
112
+ const sm = getStateMachine();
113
+ const completedStatuses = sm ? sm.COMPLETED_STATUSES : ['completed', 'archived'];
114
+
115
+ // Helper: check if a story ID is in a completed/done state
116
+ function isCompleted(sid) {
117
+ const s = data.stories[sid];
118
+ if (!s) return false;
119
+ return completedStatuses.includes(s.status) || s.status === 'done';
120
+ }
121
+
122
+ for (const [storyId, story] of Object.entries(data.stories)) {
123
+ // Only consider blocked stories
124
+ if (story.status !== 'blocked') continue;
125
+
126
+ // Collect dependency IDs from both fields
127
+ const deps = [];
128
+ if (Array.isArray(story.depends_on)) deps.push(...story.depends_on);
129
+ if (Array.isArray(story.blocked_by)) deps.push(...story.blocked_by);
130
+
131
+ // Skip stories that don't depend on the completed story
132
+ if (!deps.includes(completedStoryId)) continue;
133
+
134
+ // Check if ALL dependencies are now completed/done
135
+ const allMet = deps.every(depId => isCompleted(depId));
136
+ if (!allMet) continue;
137
+
138
+ // Transition blocked → ready
139
+ if (sm) {
140
+ const result = sm.transition({ id: storyId, status: 'blocked' }, 'ready', {
141
+ actor: 'status-writer',
142
+ reason: `Dependencies resolved (${completedStoryId} completed)`,
143
+ });
144
+ if (result.success) {
145
+ story.status = 'ready';
146
+ story.updated_at = new Date().toISOString();
147
+ unblocked.push(storyId);
148
+ }
149
+ } else {
150
+ // No state machine available — direct transition
151
+ story.status = 'ready';
152
+ story.updated_at = new Date().toISOString();
153
+ unblocked.push(storyId);
154
+ }
155
+ }
156
+
157
+ return { unblocked };
158
+ }
159
+
160
+ /**
161
+ * Update a single story in status.json using atomic read-modify-write.
162
+ *
163
+ * When `updates.status` is provided and differs from the current status,
164
+ * validates the transition via story-state-machine. When transitioning
165
+ * to completed/done, triggers resolveDependencies() automatically.
166
+ *
167
+ * @param {string} rootDir - Project root directory
168
+ * @param {string} storyId - Story ID (e.g., 'US-0042')
169
+ * @param {object} updates - Fields to update (e.g., { status: 'completed', assigned_to: 'AG-API' })
170
+ * @param {object} [options={}] - Options
171
+ * @param {boolean} [options.skipValidation=false] - Skip state machine validation
172
+ * @returns {{ ok: boolean, unblocked?: string[], error?: string }}
173
+ */
174
+ function updateStory(rootDir, storyId, updates, options = {}) {
175
+ const { skipValidation = false } = options;
176
+
177
+ try {
178
+ const statusPath = getStatusFilePath(rootDir);
179
+ if (!fs.existsSync(statusPath)) {
180
+ return { ok: false, error: 'status.json not found' };
181
+ }
182
+
183
+ const fileLock = getFileLock();
184
+
185
+ // Mutation function applied inside the lock
186
+ let resultMeta = { unblocked: [] };
187
+
188
+ const modifyFn = data => {
189
+ if (!data.stories) data.stories = {};
190
+ if (!data.stories[storyId]) {
191
+ throw new Error(`Story ${storyId} not found`);
192
+ }
193
+
194
+ const story = data.stories[storyId];
195
+
196
+ // Validate status transition if status is changing
197
+ if (updates.status && updates.status !== story.status && !skipValidation) {
198
+ const sm = getStateMachine();
199
+ if (sm) {
200
+ const valid = sm.isValidTransition(story.status, updates.status);
201
+ if (!valid) {
202
+ const validTargets = sm.getValidTransitions(story.status);
203
+ throw new Error(
204
+ `Invalid transition: ${story.status} → ${updates.status}. ` +
205
+ `Valid transitions: ${validTargets.join(', ') || 'none'}`
206
+ );
207
+ }
208
+ }
209
+ }
210
+
211
+ // Apply updates (null values delete the field)
212
+ Object.assign(story, updates);
213
+ for (const [key, val] of Object.entries(updates)) {
214
+ if (val === null) delete story[key];
215
+ }
216
+ story.updated_at = new Date().toISOString();
217
+
218
+ // Trigger dependency resolution on completion
219
+ const sm = getStateMachine();
220
+ const completedStatuses = sm ? sm.COMPLETED_STATUSES : ['completed', 'archived'];
221
+ if (
222
+ updates.status &&
223
+ (completedStatuses.includes(updates.status) || updates.status === 'done')
224
+ ) {
225
+ const resolved = resolveDependencies(data, storyId);
226
+ resultMeta.unblocked = resolved.unblocked;
227
+ }
228
+
229
+ return data;
230
+ };
231
+
232
+ if (fileLock && typeof fileLock.atomicReadModifyWrite === 'function') {
233
+ const result = fileLock.atomicReadModifyWrite(statusPath, modifyFn);
234
+ if (!result.success) {
235
+ return { ok: false, error: result.error || 'Atomic write failed' };
236
+ }
237
+ } else {
238
+ // Fallback: direct read-modify-write (no lock)
239
+ const data = JSON.parse(fs.readFileSync(statusPath, 'utf8'));
240
+ const modified = modifyFn(data);
241
+ fs.writeFileSync(statusPath, JSON.stringify(modified, null, 2) + '\n');
242
+ }
243
+
244
+ return { ok: true, unblocked: resultMeta.unblocked };
245
+ } catch (e) {
246
+ return { ok: false, error: e.message };
247
+ }
248
+ }
249
+
250
+ module.exports = {
251
+ updateStory,
252
+ readStory,
253
+ resolveDependencies,
254
+ getStatusFilePath,
255
+ };