agileflow 3.2.1 → 3.4.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 (134) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +6 -6
  3. package/lib/feature-flags.js +32 -4
  4. package/lib/skill-loader.js +0 -1
  5. package/package.json +1 -1
  6. package/scripts/agileflow-statusline.sh +81 -0
  7. package/scripts/babysit-clear-restore.js +154 -0
  8. package/scripts/claude-tmux.sh +120 -24
  9. package/scripts/claude-watchdog.sh +225 -0
  10. package/scripts/generators/agent-registry.js +14 -1
  11. package/scripts/generators/inject-babysit.js +22 -9
  12. package/scripts/generators/inject-help.js +19 -9
  13. package/scripts/lib/README-portable-tasks.md +424 -0
  14. package/scripts/lib/audit-cleanup.js +250 -0
  15. package/scripts/lib/audit-registry.js +248 -0
  16. package/scripts/lib/configure-detect.js +20 -0
  17. package/scripts/lib/feature-catalog.js +13 -2
  18. package/scripts/lib/gate-enforcer.js +295 -0
  19. package/scripts/lib/model-profiles.js +98 -0
  20. package/scripts/lib/signal-detectors.js +1 -1
  21. package/scripts/lib/skill-catalog.js +557 -0
  22. package/scripts/lib/skill-recommender.js +311 -0
  23. package/scripts/lib/tdd-phase-manager.js +455 -0
  24. package/scripts/lib/team-events.js +76 -8
  25. package/scripts/lib/tmux-group-colors.js +113 -0
  26. package/scripts/messaging-bridge.js +209 -1
  27. package/scripts/spawn-audit-sessions.js +549 -0
  28. package/scripts/team-manager.js +37 -16
  29. package/scripts/tmux-close-windows.sh +180 -0
  30. package/scripts/tmux-restore-window.sh +67 -0
  31. package/scripts/tmux-save-closed-window.sh +35 -0
  32. package/src/core/agents/ads-audit-budget.md +181 -0
  33. package/src/core/agents/ads-audit-compliance.md +169 -0
  34. package/src/core/agents/ads-audit-creative.md +164 -0
  35. package/src/core/agents/ads-audit-google.md +226 -0
  36. package/src/core/agents/ads-audit-meta.md +183 -0
  37. package/src/core/agents/ads-audit-tracking.md +197 -0
  38. package/src/core/agents/ads-consensus.md +322 -0
  39. package/src/core/agents/brainstorm-analyzer-features.md +169 -0
  40. package/src/core/agents/brainstorm-analyzer-growth.md +161 -0
  41. package/src/core/agents/brainstorm-analyzer-integration.md +172 -0
  42. package/src/core/agents/brainstorm-analyzer-market.md +147 -0
  43. package/src/core/agents/brainstorm-analyzer-ux.md +167 -0
  44. package/src/core/agents/brainstorm-consensus.md +237 -0
  45. package/src/core/agents/completeness-analyzer-api.md +190 -0
  46. package/src/core/agents/completeness-analyzer-conditional.md +201 -0
  47. package/src/core/agents/completeness-analyzer-handlers.md +159 -0
  48. package/src/core/agents/completeness-analyzer-imports.md +159 -0
  49. package/src/core/agents/completeness-analyzer-routes.md +182 -0
  50. package/src/core/agents/completeness-analyzer-state.md +188 -0
  51. package/src/core/agents/completeness-analyzer-stubs.md +198 -0
  52. package/src/core/agents/completeness-consensus.md +286 -0
  53. package/src/core/agents/perf-consensus.md +2 -2
  54. package/src/core/agents/security-consensus.md +2 -2
  55. package/src/core/agents/seo-analyzer-content.md +167 -0
  56. package/src/core/agents/seo-analyzer-images.md +187 -0
  57. package/src/core/agents/seo-analyzer-performance.md +206 -0
  58. package/src/core/agents/seo-analyzer-schema.md +176 -0
  59. package/src/core/agents/seo-analyzer-sitemap.md +172 -0
  60. package/src/core/agents/seo-analyzer-technical.md +144 -0
  61. package/src/core/agents/seo-consensus.md +289 -0
  62. package/src/core/agents/test-consensus.md +2 -2
  63. package/src/core/commands/ads/audit.md +375 -0
  64. package/src/core/commands/ads/budget.md +97 -0
  65. package/src/core/commands/ads/competitor.md +112 -0
  66. package/src/core/commands/ads/creative.md +85 -0
  67. package/src/core/commands/ads/google.md +112 -0
  68. package/src/core/commands/ads/landing.md +119 -0
  69. package/src/core/commands/ads/linkedin.md +112 -0
  70. package/src/core/commands/ads/meta.md +91 -0
  71. package/src/core/commands/ads/microsoft.md +115 -0
  72. package/src/core/commands/ads/plan.md +321 -0
  73. package/src/core/commands/ads/tiktok.md +129 -0
  74. package/src/core/commands/ads/youtube.md +124 -0
  75. package/src/core/commands/ads.md +128 -0
  76. package/src/core/commands/babysit.md +250 -1344
  77. package/src/core/commands/code/completeness.md +466 -0
  78. package/src/core/commands/{audit → code}/legal.md +26 -16
  79. package/src/core/commands/{audit → code}/logic.md +27 -16
  80. package/src/core/commands/{audit → code}/performance.md +30 -20
  81. package/src/core/commands/{audit → code}/security.md +32 -19
  82. package/src/core/commands/{audit → code}/test.md +30 -20
  83. package/src/core/commands/{discovery → ideate}/brief.md +12 -12
  84. package/src/core/commands/{discovery/new.md → ideate/discover.md} +13 -13
  85. package/src/core/commands/ideate/features.md +435 -0
  86. package/src/core/commands/seo/audit.md +373 -0
  87. package/src/core/commands/seo/competitor.md +174 -0
  88. package/src/core/commands/seo/content.md +107 -0
  89. package/src/core/commands/seo/geo.md +229 -0
  90. package/src/core/commands/seo/hreflang.md +140 -0
  91. package/src/core/commands/seo/images.md +96 -0
  92. package/src/core/commands/seo/page.md +198 -0
  93. package/src/core/commands/seo/plan.md +163 -0
  94. package/src/core/commands/seo/programmatic.md +131 -0
  95. package/src/core/commands/seo/references/cwv-thresholds.md +64 -0
  96. package/src/core/commands/seo/references/eeat-framework.md +110 -0
  97. package/src/core/commands/seo/references/quality-gates.md +91 -0
  98. package/src/core/commands/seo/references/schema-types.md +102 -0
  99. package/src/core/commands/seo/schema.md +183 -0
  100. package/src/core/commands/seo/sitemap.md +97 -0
  101. package/src/core/commands/seo/technical.md +100 -0
  102. package/src/core/commands/seo.md +107 -0
  103. package/src/core/commands/skill/list.md +68 -212
  104. package/src/core/commands/skill/recommend.md +216 -0
  105. package/src/core/commands/tdd-next.md +238 -0
  106. package/src/core/commands/tdd.md +210 -0
  107. package/src/core/experts/_core-expertise.yaml +105 -0
  108. package/src/core/experts/analytics/expertise.yaml +5 -99
  109. package/src/core/experts/codebase-query/expertise.yaml +3 -72
  110. package/src/core/experts/compliance/expertise.yaml +6 -72
  111. package/src/core/experts/database/expertise.yaml +9 -52
  112. package/src/core/experts/documentation/expertise.yaml +7 -140
  113. package/src/core/experts/integrations/expertise.yaml +7 -127
  114. package/src/core/experts/mentor/expertise.yaml +8 -35
  115. package/src/core/experts/monitoring/expertise.yaml +7 -49
  116. package/src/core/experts/performance/expertise.yaml +1 -26
  117. package/src/core/experts/security/expertise.yaml +9 -34
  118. package/src/core/experts/ui/expertise.yaml +6 -36
  119. package/src/core/knowledge/ads/ad-audit-checklist-scoring.md +424 -0
  120. package/src/core/knowledge/ads/ad-optimization-logic.md +590 -0
  121. package/src/core/knowledge/ads/ad-technical-specifications.md +385 -0
  122. package/src/core/knowledge/ads/definitive-advertising-reference-2026.md +506 -0
  123. package/src/core/knowledge/ads/paid-advertising-research-2026.md +445 -0
  124. package/src/core/templates/agileflow-metadata.json +15 -1
  125. package/tools/cli/installers/ide/_base-ide.js +42 -5
  126. package/tools/cli/installers/ide/claude-code.js +13 -4
  127. package/tools/cli/lib/content-injector.js +160 -12
  128. package/tools/cli/lib/docs-setup.js +1 -1
  129. package/src/core/commands/skill/create.md +0 -698
  130. package/src/core/commands/skill/delete.md +0 -316
  131. package/src/core/commands/skill/edit.md +0 -359
  132. package/src/core/commands/skill/test.md +0 -394
  133. package/src/core/commands/skill/upgrade.md +0 -552
  134. package/src/core/templates/skill-template.md +0 -117
@@ -0,0 +1,248 @@
1
+ /**
2
+ * audit-registry.js - Static registry mapping audit types to analyzers
3
+ *
4
+ * Centralizes the mapping of 6 audit commands to their analyzer agents,
5
+ * consensus coordinators, and depth configurations. Previously this info
6
+ * was duplicated across 6 .md command files.
7
+ *
8
+ * Usage:
9
+ * const { getAuditType, getAnalyzersForAudit } = require('./audit-registry');
10
+ * const security = getAuditType('security');
11
+ * const focused = getAnalyzersForAudit('security', 'deep', ['injection', 'auth']);
12
+ */
13
+
14
+ /**
15
+ * Complete audit type registry.
16
+ * Each entry defines: name, short prefix for tmux, color for tab groups,
17
+ * all analyzers with their subagent_type, which are quick vs deep,
18
+ * and the consensus coordinator.
19
+ */
20
+ const AUDIT_TYPES = {
21
+ logic: {
22
+ name: 'Logic Analysis',
23
+ prefix: 'Logic',
24
+ color: '#7aa2f7', // sky
25
+ command: 'code/logic',
26
+ analyzers: {
27
+ edge: { subagent_type: 'logic-analyzer-edge', label: 'Edge Cases' },
28
+ invariant: { subagent_type: 'logic-analyzer-invariant', label: 'Invariants' },
29
+ flow: { subagent_type: 'logic-analyzer-flow', label: 'Control Flow' },
30
+ type: { subagent_type: 'logic-analyzer-type', label: 'Type Safety' },
31
+ race: { subagent_type: 'logic-analyzer-race', label: 'Race Conditions' },
32
+ },
33
+ consensus: { subagent_type: 'logic-consensus', label: 'Logic Consensus' },
34
+ quick_analyzers: ['edge', 'invariant', 'flow', 'type', 'race'],
35
+ deep_analyzers: ['edge', 'invariant', 'flow', 'type', 'race'],
36
+ },
37
+
38
+ security: {
39
+ name: 'Security Vulnerability',
40
+ prefix: 'Sec',
41
+ color: '#f7768e', // coral
42
+ command: 'code/security',
43
+ analyzers: {
44
+ injection: { subagent_type: 'security-analyzer-injection', label: 'Injection' },
45
+ auth: { subagent_type: 'security-analyzer-auth', label: 'Authentication' },
46
+ authz: { subagent_type: 'security-analyzer-authz', label: 'Authorization' },
47
+ secrets: { subagent_type: 'security-analyzer-secrets', label: 'Secrets' },
48
+ input: { subagent_type: 'security-analyzer-input', label: 'Input Validation' },
49
+ deps: { subagent_type: 'security-analyzer-deps', label: 'Dependencies' },
50
+ infra: { subagent_type: 'security-analyzer-infra', label: 'Infrastructure' },
51
+ api: { subagent_type: 'security-analyzer-api', label: 'API Security' },
52
+ },
53
+ consensus: { subagent_type: 'security-consensus', label: 'Security Consensus' },
54
+ quick_analyzers: ['injection', 'auth', 'authz', 'secrets', 'input'],
55
+ deep_analyzers: ['injection', 'auth', 'authz', 'secrets', 'input', 'deps', 'infra', 'api'],
56
+ },
57
+
58
+ performance: {
59
+ name: 'Performance Bottleneck',
60
+ prefix: 'Perf',
61
+ color: '#73daca', // mint
62
+ command: 'code/performance',
63
+ analyzers: {
64
+ queries: { subagent_type: 'perf-analyzer-queries', label: 'Queries' },
65
+ rendering: { subagent_type: 'perf-analyzer-rendering', label: 'Rendering' },
66
+ memory: { subagent_type: 'perf-analyzer-memory', label: 'Memory' },
67
+ bundle: { subagent_type: 'perf-analyzer-bundle', label: 'Bundle Size' },
68
+ compute: { subagent_type: 'perf-analyzer-compute', label: 'Compute' },
69
+ network: { subagent_type: 'perf-analyzer-network', label: 'Network' },
70
+ caching: { subagent_type: 'perf-analyzer-caching', label: 'Caching' },
71
+ assets: { subagent_type: 'perf-analyzer-assets', label: 'Assets' },
72
+ },
73
+ consensus: { subagent_type: 'perf-consensus', label: 'Performance Consensus' },
74
+ quick_analyzers: ['queries', 'rendering', 'memory', 'bundle', 'compute'],
75
+ deep_analyzers: [
76
+ 'queries',
77
+ 'rendering',
78
+ 'memory',
79
+ 'bundle',
80
+ 'compute',
81
+ 'network',
82
+ 'caching',
83
+ 'assets',
84
+ ],
85
+ },
86
+
87
+ test: {
88
+ name: 'Test Quality',
89
+ prefix: 'Test',
90
+ color: '#e0af68', // amber
91
+ command: 'code/test',
92
+ analyzers: {
93
+ coverage: { subagent_type: 'test-analyzer-coverage', label: 'Coverage' },
94
+ fragility: { subagent_type: 'test-analyzer-fragility', label: 'Fragility' },
95
+ mocking: { subagent_type: 'test-analyzer-mocking', label: 'Mocking' },
96
+ assertions: { subagent_type: 'test-analyzer-assertions', label: 'Assertions' },
97
+ structure: { subagent_type: 'test-analyzer-structure', label: 'Structure' },
98
+ integration: { subagent_type: 'test-analyzer-integration', label: 'Integration' },
99
+ maintenance: { subagent_type: 'test-analyzer-maintenance', label: 'Maintenance' },
100
+ patterns: { subagent_type: 'test-analyzer-patterns', label: 'Anti-Patterns' },
101
+ },
102
+ consensus: { subagent_type: 'test-consensus', label: 'Test Consensus' },
103
+ quick_analyzers: ['coverage', 'fragility', 'mocking', 'assertions', 'structure'],
104
+ deep_analyzers: [
105
+ 'coverage',
106
+ 'fragility',
107
+ 'mocking',
108
+ 'assertions',
109
+ 'structure',
110
+ 'integration',
111
+ 'maintenance',
112
+ 'patterns',
113
+ ],
114
+ },
115
+
116
+ completeness: {
117
+ name: 'Completeness',
118
+ prefix: 'Comp',
119
+ color: '#bb9af7', // violet
120
+ command: 'code/completeness',
121
+ analyzers: {
122
+ handlers: { subagent_type: 'completeness-analyzer-handlers', label: 'Handlers' },
123
+ routes: { subagent_type: 'completeness-analyzer-routes', label: 'Routes' },
124
+ api: { subagent_type: 'completeness-analyzer-api', label: 'API Endpoints' },
125
+ stubs: { subagent_type: 'completeness-analyzer-stubs', label: 'Stubs' },
126
+ state: { subagent_type: 'completeness-analyzer-state', label: 'State' },
127
+ imports: { subagent_type: 'completeness-analyzer-imports', label: 'Imports' },
128
+ conditional: { subagent_type: 'completeness-analyzer-conditional', label: 'Conditionals' },
129
+ },
130
+ consensus: { subagent_type: 'completeness-consensus', label: 'Completeness Consensus' },
131
+ quick_analyzers: ['handlers', 'routes', 'api', 'stubs', 'state'],
132
+ deep_analyzers: ['handlers', 'routes', 'api', 'stubs', 'state', 'imports', 'conditional'],
133
+ },
134
+
135
+ legal: {
136
+ name: 'Legal Risk',
137
+ prefix: 'Legal',
138
+ color: '#9ece6a', // lime
139
+ command: 'code/legal',
140
+ analyzers: {
141
+ privacy: { subagent_type: 'legal-analyzer-privacy', label: 'Privacy' },
142
+ terms: { subagent_type: 'legal-analyzer-terms', label: 'Terms' },
143
+ a11y: { subagent_type: 'legal-analyzer-a11y', label: 'Accessibility' },
144
+ licensing: { subagent_type: 'legal-analyzer-licensing', label: 'Licensing' },
145
+ consumer: { subagent_type: 'legal-analyzer-consumer', label: 'Consumer' },
146
+ security: { subagent_type: 'legal-analyzer-security', label: 'Security' },
147
+ ai: { subagent_type: 'legal-analyzer-ai', label: 'AI Compliance' },
148
+ content: { subagent_type: 'legal-analyzer-content', label: 'Content' },
149
+ international: { subagent_type: 'legal-analyzer-international', label: 'International' },
150
+ },
151
+ consensus: { subagent_type: 'legal-consensus', label: 'Legal Consensus' },
152
+ quick_analyzers: ['privacy', 'terms', 'a11y', 'licensing', 'consumer'],
153
+ deep_analyzers: [
154
+ 'privacy',
155
+ 'terms',
156
+ 'a11y',
157
+ 'licensing',
158
+ 'consumer',
159
+ 'security',
160
+ 'ai',
161
+ 'content',
162
+ 'international',
163
+ ],
164
+ },
165
+ };
166
+
167
+ /**
168
+ * Get audit type configuration.
169
+ *
170
+ * @param {string} type - Audit type key (logic, security, performance, test, completeness, legal)
171
+ * @returns {object|null} Audit type config or null if invalid
172
+ */
173
+ function getAuditType(type) {
174
+ return AUDIT_TYPES[type] || null;
175
+ }
176
+
177
+ /**
178
+ * Get all valid audit type keys.
179
+ *
180
+ * @returns {string[]} Array of audit type keys
181
+ */
182
+ function getAuditTypeKeys() {
183
+ return Object.keys(AUDIT_TYPES);
184
+ }
185
+
186
+ /**
187
+ * Get analyzers for a given audit type, depth, and focus.
188
+ *
189
+ * @param {string} type - Audit type key
190
+ * @param {string} [depth='quick'] - 'quick', 'deep', or 'ultradeep'
191
+ * @param {string[]} [focus] - Array of focus areas, or null/['all'] for all
192
+ * @returns {{ analyzers: Array<{ key: string, subagent_type: string, label: string }>, consensus: object }|null}
193
+ */
194
+ function getAnalyzersForAudit(type, depth, focus) {
195
+ const audit = AUDIT_TYPES[type];
196
+ if (!audit) return null;
197
+
198
+ const effectiveDepth = depth === 'ultradeep' ? 'deep' : depth || 'quick';
199
+ const analyzerKeys = effectiveDepth === 'deep' ? audit.deep_analyzers : audit.quick_analyzers;
200
+
201
+ // Filter by focus if specified
202
+ let selectedKeys = analyzerKeys;
203
+ if (focus && focus.length > 0 && !focus.includes('all')) {
204
+ selectedKeys = analyzerKeys.filter(key => focus.includes(key));
205
+ // If focus specifies keys not in current depth, include them anyway
206
+ for (const f of focus) {
207
+ if (audit.analyzers[f] && !selectedKeys.includes(f)) {
208
+ selectedKeys.push(f);
209
+ }
210
+ }
211
+ }
212
+
213
+ const analyzers = selectedKeys.map(key => ({
214
+ key,
215
+ subagent_type: audit.analyzers[key].subagent_type,
216
+ label: audit.analyzers[key].label,
217
+ }));
218
+
219
+ return {
220
+ analyzers,
221
+ consensus: audit.consensus,
222
+ };
223
+ }
224
+
225
+ /**
226
+ * Get analyzer count for a given audit type at each depth.
227
+ *
228
+ * @param {string} type - Audit type key
229
+ * @returns {{ quick: number, deep: number, total: number }|null}
230
+ */
231
+ function getAnalyzerCounts(type) {
232
+ const audit = AUDIT_TYPES[type];
233
+ if (!audit) return null;
234
+
235
+ return {
236
+ quick: audit.quick_analyzers.length,
237
+ deep: audit.deep_analyzers.length,
238
+ total: Object.keys(audit.analyzers).length,
239
+ };
240
+ }
241
+
242
+ module.exports = {
243
+ AUDIT_TYPES,
244
+ getAuditType,
245
+ getAuditTypeKeys,
246
+ getAnalyzersForAudit,
247
+ getAnalyzerCounts,
248
+ };
@@ -11,6 +11,7 @@ const { execFileSync } = require('child_process');
11
11
  const { c, log, header, readJSON } = require('./configure-utils');
12
12
  const { tryOptional } = require('../../lib/errors');
13
13
  const { FEATURES } = require('./configure-features');
14
+ const ff = require('../../lib/feature-flags');
14
15
 
15
16
  // ============================================================================
16
17
  // CONTENT HASH HELPERS
@@ -121,6 +122,11 @@ function detectConfig(version) {
121
122
  outdated: false,
122
123
  mode: 'full',
123
124
  },
125
+ agentteams: {
126
+ enabled: false,
127
+ mode: 'subagent',
128
+ tools: [],
129
+ },
124
130
  },
125
131
  metadata: { exists: false, version: null },
126
132
  currentVersion: version,
@@ -304,6 +310,12 @@ function detectStatusLine(settings, status) {
304
310
  * Detect metadata file configuration
305
311
  */
306
312
  function detectMetadata(status, version) {
313
+ // Agent Teams detection (independent of metadata file - reads env vars)
314
+ const rootDir = process.cwd();
315
+ status.features.agentteams.enabled = ff.isAgentTeamsEnabled({ rootDir });
316
+ status.features.agentteams.mode = ff.getAgentTeamsMode({ rootDir });
317
+ status.features.agentteams.tools = ff.getAvailableTools({ rootDir });
318
+
307
319
  const metaPath = 'docs/00-meta/agileflow-metadata.json';
308
320
  if (!fs.existsSync(metaPath)) return;
309
321
 
@@ -542,6 +554,14 @@ function printStatus(status) {
542
554
  }
543
555
  }
544
556
 
557
+ // Agent Teams
558
+ const at = status.features.agentteams;
559
+ if (at.enabled) {
560
+ log(` Agent Teams: ${at.mode} (${at.tools.length} tools)`, c.green);
561
+ } else {
562
+ log(` Agent Teams: ${at.mode}`, c.dim);
563
+ }
564
+
545
565
  // Metadata version
546
566
  if (status.metadata.exists) {
547
567
  log(`\nMetadata: v${status.metadata.version}`, c.dim);
@@ -116,7 +116,7 @@ const FEATURE_CATALOG = [
116
116
  feature: 'discovery',
117
117
  name: 'Discovery',
118
118
  description: 'Brainstorm, research, and synthesize findings into a Product Brief',
119
- how_to_use: '/agileflow:discovery:new "<topic>"',
119
+ how_to_use: '/agileflow:ideate:discover "<topic>"',
120
120
  category: 'workflow',
121
121
  detector: null,
122
122
  auto_mode: null,
@@ -168,7 +168,18 @@ const FEATURE_CATALOG = [
168
168
  feature: 'logic-audit',
169
169
  name: 'Logic Audit',
170
170
  description: 'Multi-agent analysis for edge cases, race conditions, type bugs, and dead code',
171
- how_to_use: '/agileflow:audit:logic',
171
+ how_to_use: '/agileflow:code:logic',
172
+ category: 'analysis',
173
+ detector: null,
174
+ auto_mode: null,
175
+ prerequisites: null,
176
+ },
177
+ {
178
+ feature: 'completeness-audit',
179
+ name: 'Completeness Audit',
180
+ description:
181
+ 'Multi-agent analysis for forgotten features, dead handlers, stub code, and incomplete implementations',
182
+ how_to_use: '/agileflow:code:completeness',
172
183
  category: 'analysis',
173
184
  detector: null,
174
185
  auto_mode: null,
@@ -0,0 +1,295 @@
1
+ /**
2
+ * gate-enforcer.js - Workflow Gate Enforcement for Babysit Strict Mode
3
+ *
4
+ * Filters AskUserQuestion options based on workflow gate status.
5
+ * When STRICT=true in babysit, certain options are removed or blocked
6
+ * based on whether gates have been satisfied (tests passed, review done, etc.).
7
+ *
8
+ * Gates:
9
+ * - tests_passed: Must run and pass tests before commit
10
+ * - review_done: Must run code review for 5+ source files before commit
11
+ * - logic_audit_done: Must run logic audit before commit (advisory in non-strict)
12
+ *
13
+ * Usage:
14
+ * const { filterOptions, getGateStatus, updateGate } = require('./gate-enforcer');
15
+ * const filtered = filterOptions(options, gateState, { strict: true });
16
+ */
17
+
18
+ 'use strict';
19
+
20
+ // ============================================================================
21
+ // Constants
22
+ // ============================================================================
23
+
24
+ /**
25
+ * Gate types that can block workflow progression
26
+ */
27
+ const GATES = {
28
+ TESTS_PASSED: 'tests_passed',
29
+ REVIEW_DONE: 'review_done',
30
+ LOGIC_AUDIT_DONE: 'logic_audit_done',
31
+ };
32
+
33
+ /**
34
+ * Actions that require specific gates to be satisfied
35
+ */
36
+ const GATE_REQUIREMENTS = {
37
+ commit: {
38
+ strict: [GATES.TESTS_PASSED],
39
+ strict_5plus: [GATES.TESTS_PASSED, GATES.REVIEW_DONE],
40
+ },
41
+ next_story: {
42
+ strict: [GATES.TESTS_PASSED],
43
+ },
44
+ };
45
+
46
+ /**
47
+ * Patterns that identify commit-related options
48
+ */
49
+ const COMMIT_PATTERNS = [/^commit/i, /^git commit/i, /commit.*changes/i, /commit:?\s/i];
50
+
51
+ /**
52
+ * Patterns that identify "skip" options
53
+ */
54
+ const SKIP_PATTERNS = [/skip.*test/i, /skip.*review/i, /skip.*audit/i, /skip.*verif/i];
55
+
56
+ /**
57
+ * Patterns that identify "next story" options
58
+ */
59
+ const NEXT_STORY_PATTERNS = [/continue to/i, /next story/i, /move to.*US-/i];
60
+
61
+ // ============================================================================
62
+ // Gate State Management
63
+ // ============================================================================
64
+
65
+ /**
66
+ * Create initial gate state for a workflow session
67
+ * @param {Object} options
68
+ * @param {number} options.filesChanged - Number of source files modified
69
+ * @param {boolean} options.strict - Whether strict mode is enabled
70
+ * @returns {Object} Initial gate state
71
+ */
72
+ function createGateState(options = {}) {
73
+ const { filesChanged = 0, strict = false } = options;
74
+
75
+ return {
76
+ strict,
77
+ files_changed: filesChanged,
78
+ gates: {
79
+ [GATES.TESTS_PASSED]: false,
80
+ [GATES.REVIEW_DONE]: false,
81
+ [GATES.LOGIC_AUDIT_DONE]: false,
82
+ },
83
+ history: [],
84
+ created_at: new Date().toISOString(),
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Update a gate's status
90
+ * @param {Object} gateState - Current gate state
91
+ * @param {string} gate - Gate name from GATES
92
+ * @param {boolean} passed - Whether the gate passed
93
+ * @param {Object} metadata - Additional info (e.g., test count, review findings)
94
+ * @returns {Object} Updated gate state
95
+ */
96
+ function updateGate(gateState, gate, passed, metadata = {}) {
97
+ if (!Object.values(GATES).includes(gate)) {
98
+ throw new Error(`Unknown gate: ${gate}. Valid: ${Object.values(GATES).join(', ')}`);
99
+ }
100
+
101
+ const updated = {
102
+ ...gateState,
103
+ gates: {
104
+ ...gateState.gates,
105
+ [gate]: passed,
106
+ },
107
+ history: [
108
+ ...gateState.history,
109
+ {
110
+ gate,
111
+ passed,
112
+ at: new Date().toISOString(),
113
+ ...metadata,
114
+ },
115
+ ],
116
+ };
117
+
118
+ return updated;
119
+ }
120
+
121
+ /**
122
+ * Check if all required gates for an action are satisfied
123
+ * @param {Object} gateState - Current gate state
124
+ * @param {string} action - Action to check ('commit', 'next_story')
125
+ * @returns {{ allowed: boolean, missing: string[] }}
126
+ */
127
+ function checkGates(gateState, action) {
128
+ if (!gateState.strict) {
129
+ return { allowed: true, missing: [] };
130
+ }
131
+
132
+ let requirements;
133
+ if (action === 'commit' && gateState.files_changed >= 5) {
134
+ requirements = GATE_REQUIREMENTS.commit.strict_5plus || [];
135
+ } else {
136
+ requirements = (GATE_REQUIREMENTS[action] && GATE_REQUIREMENTS[action].strict) || [];
137
+ }
138
+
139
+ const missing = requirements.filter(gate => !gateState.gates[gate]);
140
+
141
+ return {
142
+ allowed: missing.length === 0,
143
+ missing,
144
+ };
145
+ }
146
+
147
+ // ============================================================================
148
+ // Option Filtering
149
+ // ============================================================================
150
+
151
+ /**
152
+ * Check if an option label matches any pattern in a list
153
+ * @param {string} label - Option label text
154
+ * @param {RegExp[]} patterns - Patterns to match against
155
+ * @returns {boolean}
156
+ */
157
+ function matchesPattern(label, patterns) {
158
+ if (typeof label !== 'string') return false;
159
+ return patterns.some(p => p.test(label));
160
+ }
161
+
162
+ /**
163
+ * Filter AskUserQuestion options based on gate state
164
+ *
165
+ * In strict mode:
166
+ * - Remove "commit" options if tests haven't passed
167
+ * - Remove "commit" options if review not done (5+ files)
168
+ * - Remove "skip tests/review/audit" options
169
+ * - Add gate status hints to option descriptions
170
+ *
171
+ * In non-strict mode:
172
+ * - Options are returned unchanged (soft guidance only)
173
+ *
174
+ * @param {Object[]} options - AskUserQuestion options array
175
+ * @param {Object} gateState - Current gate state
176
+ * @returns {Object[]} Filtered options
177
+ */
178
+ function filterOptions(options, gateState) {
179
+ if (!Array.isArray(options)) return [];
180
+ if (!gateState || !gateState.strict) {
181
+ return options;
182
+ }
183
+
184
+ return options
185
+ .filter(opt => {
186
+ const label = opt.label || '';
187
+
188
+ // In strict mode, remove skip options entirely
189
+ if (matchesPattern(label, SKIP_PATTERNS)) {
190
+ return false;
191
+ }
192
+
193
+ // Check commit gates
194
+ if (matchesPattern(label, COMMIT_PATTERNS)) {
195
+ const { allowed } = checkGates(gateState, 'commit');
196
+ if (!allowed) {
197
+ return false;
198
+ }
199
+ }
200
+
201
+ // Check next story gates
202
+ if (matchesPattern(label, NEXT_STORY_PATTERNS)) {
203
+ const { allowed } = checkGates(gateState, 'next_story');
204
+ if (!allowed) {
205
+ return false;
206
+ }
207
+ }
208
+
209
+ return true;
210
+ })
211
+ .map(opt => {
212
+ // Add gate status hints to descriptions
213
+ const label = opt.label || '';
214
+
215
+ if (matchesPattern(label, COMMIT_PATTERNS) && gateState.strict) {
216
+ const checks = [];
217
+ if (gateState.gates[GATES.TESTS_PASSED]) checks.push('tests passed');
218
+ if (gateState.gates[GATES.REVIEW_DONE]) checks.push('review done');
219
+ if (gateState.gates[GATES.LOGIC_AUDIT_DONE]) checks.push('audit done');
220
+
221
+ if (checks.length > 0) {
222
+ return {
223
+ ...opt,
224
+ description: `${opt.description || ''} [Gates: ${checks.join(', ')}]`.trim(),
225
+ };
226
+ }
227
+ }
228
+
229
+ return opt;
230
+ });
231
+ }
232
+
233
+ /**
234
+ * Get a human-readable gate status summary
235
+ * @param {Object} gateState - Current gate state
236
+ * @returns {string} Status summary
237
+ */
238
+ function getGateStatusSummary(gateState) {
239
+ if (!gateState || !gateState.strict) {
240
+ return 'Strict mode: OFF (soft guidance)';
241
+ }
242
+
243
+ const lines = [`Strict mode: ON | ${gateState.files_changed} files changed`];
244
+
245
+ for (const [gate, passed] of Object.entries(gateState.gates)) {
246
+ const icon = passed ? '✅' : '⬜';
247
+ const name = gate.replace(/_/g, ' ');
248
+ lines.push(` ${icon} ${name}`);
249
+ }
250
+
251
+ return lines.join('\n');
252
+ }
253
+
254
+ /**
255
+ * Get blocking message for a failed gate check
256
+ * @param {string[]} missingGates - List of unsatisfied gates
257
+ * @returns {string} Human-readable blocking message
258
+ */
259
+ function getBlockingMessage(missingGates) {
260
+ const messages = {
261
+ [GATES.TESTS_PASSED]: 'Run tests first (tests must pass before committing)',
262
+ [GATES.REVIEW_DONE]: 'Run code review first (required for 5+ modified files)',
263
+ [GATES.LOGIC_AUDIT_DONE]: 'Run logic audit first',
264
+ };
265
+
266
+ return missingGates.map(gate => `🚫 ${messages[gate] || gate}`).join('\n');
267
+ }
268
+
269
+ // ============================================================================
270
+ // Exports
271
+ // ============================================================================
272
+
273
+ module.exports = {
274
+ // Constants
275
+ GATES,
276
+ GATE_REQUIREMENTS,
277
+
278
+ // State management
279
+ createGateState,
280
+ updateGate,
281
+ checkGates,
282
+
283
+ // Option filtering
284
+ filterOptions,
285
+ matchesPattern,
286
+
287
+ // Reporting
288
+ getGateStatusSummary,
289
+ getBlockingMessage,
290
+
291
+ // Patterns (for testing)
292
+ COMMIT_PATTERNS,
293
+ SKIP_PATTERNS,
294
+ NEXT_STORY_PATTERNS,
295
+ };