wogiflow 1.9.7 → 1.9.9

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 (50) hide show
  1. package/lib/installer.js +8 -0
  2. package/lib/utils.js +1 -1
  3. package/package.json +2 -5
  4. package/scripts/flow-config-loader.js +16 -0
  5. package/scripts/flow-consistency-check.js +0 -4
  6. package/scripts/flow-context-estimator.js +4 -20
  7. package/scripts/flow-correct.js +2 -1
  8. package/scripts/flow-decision-tracker.js +2 -2
  9. package/scripts/flow-done.js +4 -4
  10. package/scripts/flow-entropy-monitor.js +3 -24
  11. package/scripts/flow-export-scanner.js +3 -3
  12. package/scripts/flow-gitignore.js +263 -0
  13. package/scripts/flow-health.js +23 -2
  14. package/scripts/flow-hybrid-test.js +2 -3
  15. package/scripts/flow-long-input-stories.js +4 -6
  16. package/scripts/flow-long-input.js +5 -8
  17. package/scripts/flow-memory-compactor.js +1 -13
  18. package/scripts/flow-memory-db.js +1 -1
  19. package/scripts/flow-memory-sync.js +1 -17
  20. package/scripts/flow-orchestrate-llm.js +3 -7
  21. package/scripts/flow-orchestrate.js +2 -13
  22. package/scripts/flow-pattern-extractor.js +3 -17
  23. package/scripts/flow-peer-review.js +4 -12
  24. package/scripts/flow-plugin-registry.js +2 -2
  25. package/scripts/flow-project-analyzer.js +4 -4
  26. package/scripts/flow-providers.js +2 -7
  27. package/scripts/flow-safety.js +6 -10
  28. package/scripts/flow-script-resolver.js +4 -16
  29. package/scripts/flow-skill-freshness.js +3 -12
  30. package/scripts/flow-skill-generator.js +14 -24
  31. package/scripts/flow-strict-adherence.js +2 -2
  32. package/scripts/flow-task-analyzer.js +3 -3
  33. package/scripts/flow-test-discovery.js +3 -3
  34. package/scripts/flow-utils.js +5 -0
  35. package/scripts/flow-webmcp-generator.js +1 -1
  36. package/scripts/flow-workflow-steps.js +31 -23
  37. package/scripts/hooks/core/component-check.js +22 -7
  38. package/scripts/hooks/core/observation-capture.js +7 -4
  39. package/scripts/hooks/core/task-gate.js +2 -2
  40. package/scripts/hooks/entry/claude-code/post-tool-use.js +10 -8
  41. package/scripts/hooks/entry/claude-code/pre-tool-use.js +6 -40
  42. package/scripts/hooks/entry/claude-code/session-start.js +4 -12
  43. package/scripts/hooks/entry/shared/read-stdin.js +33 -0
  44. package/scripts/flow-done +0 -151
  45. package/scripts/flow-file-ops.js +0 -307
  46. package/scripts/flow-health +0 -185
  47. package/scripts/flow-ready +0 -82
  48. package/scripts/flow-start +0 -74
  49. package/scripts/flow-status +0 -110
  50. package/scripts/flow-story +0 -105
package/lib/installer.js CHANGED
@@ -532,6 +532,14 @@ function createWorkflowStructure(projectRoot, config) {
532
532
  });
533
533
  fs.writeFileSync(configPath, JSON.stringify(configContent, null, 2));
534
534
 
535
+ // Sync .gitignore entries for enabled features
536
+ try {
537
+ const { syncGitignore } = require('../scripts/flow-gitignore');
538
+ syncGitignore(configContent);
539
+ } catch (err) {
540
+ // Non-blocking — gitignore sync should never fail installation
541
+ }
542
+
535
543
  // Create ready.json
536
544
  const readyPath = path.join(workflowDir, 'state', 'ready.json');
537
545
  const readyContent = {
package/lib/utils.js CHANGED
@@ -40,7 +40,7 @@ function findProjectRoot() {
40
40
  /**
41
41
  * Safely parse JSON content with prototype pollution protection
42
42
  *
43
- * Note: For parsing JSON files, use safeJsonParseFile from flow-file-ops.js
43
+ * Note: For parsing JSON files, use safeJsonParse from flow-utils.js
44
44
  * or safeReadJson from this module instead.
45
45
  *
46
46
  * @param {string} content - JSON string to parse
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wogiflow",
3
- "version": "1.9.7",
3
+ "version": "1.9.9",
4
4
  "description": "AI-powered development workflow management system with multi-model support",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -9,8 +9,6 @@
9
9
  },
10
10
  "scripts": {
11
11
  "flow": "./scripts/flow",
12
- "test": "node mcp-memory-server/test.js",
13
- "memory-server": "node mcp-memory-server/index.js",
14
12
  "postinstall": "node scripts/postinstall.js",
15
13
  "preuninstall": "node scripts/preuninstall.js"
16
14
  },
@@ -58,10 +56,9 @@
58
56
  },
59
57
  "homepage": "https://github.com/Tomer-Wogi/WogiFlow#readme",
60
58
  "dependencies": {
61
- "sql.js": "^1.10.0"
59
+ "sql.js": "^1.14.1"
62
60
  },
63
61
  "optionalDependencies": {
64
- "@modelcontextprotocol/sdk": "^1.0.0",
65
62
  "@xenova/transformers": "^2.15.0"
66
63
  },
67
64
  "engines": {
@@ -518,6 +518,14 @@ async function setConfigValue(configPath, newValue) {
518
518
  obj[parts[parts.length - 1]] = newValue;
519
519
  writeJson(PATHS.config, config);
520
520
  invalidateConfigCache();
521
+
522
+ // Auto-sync .gitignore when config changes enable features with runtime artifacts
523
+ try {
524
+ const { syncGitignore } = require('./flow-gitignore');
525
+ syncGitignore(config);
526
+ } catch (err) {
527
+ // Non-blocking — gitignore sync should never fail config writes
528
+ }
521
529
  } finally {
522
530
  if (release) release();
523
531
  }
@@ -548,6 +556,14 @@ function setConfigValueSync(configPath, newValue) {
548
556
  obj[parts[parts.length - 1]] = newValue;
549
557
  writeJson(PATHS.config, config);
550
558
  invalidateConfigCache();
559
+
560
+ // Auto-sync .gitignore when config changes enable features with runtime artifacts
561
+ try {
562
+ const { syncGitignore } = require('./flow-gitignore');
563
+ syncGitignore(config);
564
+ } catch (err) {
565
+ // Non-blocking — gitignore sync should never fail config writes
566
+ }
551
567
  }
552
568
 
553
569
  /**
@@ -511,10 +511,6 @@ function runConsistencyCheck(options = {}) {
511
511
  }
512
512
  }
513
513
 
514
- // TODO: Implement crossMapConsistency check (config key exists but check is not yet implemented)
515
- // This would verify that components referenced in one map exist in others
516
- // (e.g., a function used by a component is also in function-map)
517
-
518
514
  // Determine overall status
519
515
  const mode = consistencyConfig.mode || 'warn';
520
516
  const orphanMode = consistencyConfig.orphanMode || 'warn';
@@ -13,7 +13,7 @@
13
13
 
14
14
  const fs = require('fs');
15
15
  const path = require('path');
16
- const { getConfig, PATHS, safeJsonParse } = require('./flow-utils');
16
+ const { getConfig, PATHS, safeJsonParse, validateTaskId } = require('./flow-utils');
17
17
 
18
18
  /**
19
19
  * Default estimation config (can be overridden in config.json)
@@ -33,20 +33,6 @@ const DEFAULT_REFACTOR_KEYWORDS = [
33
33
  'restructure', 'rearchitect', 'modernize', 'upgrade'
34
34
  ];
35
35
 
36
- // Valid task ID pattern — enforces wf-[8 hex] format and prevents path traversal
37
- // Also accepts legacy TASK-NNN/BUG-NNN and sub-tasks wf-XXXXXXXX-NN
38
- const VALID_TASK_ID_PATTERN = /^(wf-[a-f0-9]{8}(-\d{2})?|(TASK|BUG)-\d{3,})$/i;
39
-
40
- /**
41
- * Validate task ID format — must be wf-[8 hex chars] or legacy TASK-NNN/BUG-NNN.
42
- * Also prevents path traversal attacks.
43
- * @param {string} taskId - Task ID to validate
44
- * @returns {boolean} True if valid
45
- */
46
- function isValidTaskId(taskId) {
47
- return typeof taskId === 'string' && VALID_TASK_ID_PATTERN.test(taskId);
48
- }
49
-
50
36
  /**
51
37
  * Get smart compaction config from config.json
52
38
  * @returns {Object} Smart compaction configuration
@@ -90,7 +76,7 @@ function getSmartCompactionConfig() {
90
76
  */
91
77
  function readSpecFile(taskId) {
92
78
  // Validate taskId to prevent path traversal (Security Rule)
93
- if (!isValidTaskId(taskId)) {
79
+ if (!validateTaskId(taskId).valid) {
94
80
  return null;
95
81
  }
96
82
 
@@ -476,7 +462,7 @@ if (require.main === module) {
476
462
  const taskId = args[1];
477
463
 
478
464
  // Validate taskId to prevent path traversal
479
- if (!isValidTaskId(taskId)) {
465
+ if (!validateTaskId(taskId).valid) {
480
466
  console.error(`Invalid task ID format: ${taskId}`);
481
467
  console.error('Task IDs must contain only alphanumeric characters, hyphens, and underscores.');
482
468
  process.exit(1);
@@ -521,7 +507,7 @@ if (require.main === module) {
521
507
  const taskId = args[1];
522
508
 
523
509
  // Validate taskId to prevent path traversal
524
- if (!isValidTaskId(taskId)) {
510
+ if (!validateTaskId(taskId).valid) {
525
511
  console.error(`Invalid task ID format: ${taskId}`);
526
512
  console.error('Task IDs must contain only alphanumeric characters, hyphens, and underscores.');
527
513
  process.exit(1);
@@ -775,8 +761,6 @@ module.exports = {
775
761
  formatEstimationResult,
776
762
  extractCriteriaCount,
777
763
  extractFileCount,
778
- isValidTaskId,
779
- VALID_TASK_ID_PATTERN,
780
764
  // Finding-level estimation (for review-fix sessions)
781
765
  estimateFindingContextCost,
782
766
  calculateDynamicBatchSize,
@@ -23,6 +23,7 @@ const {
23
23
  dirExists,
24
24
  readFile,
25
25
  writeFile,
26
+ getConfig,
26
27
  color,
27
28
  success,
28
29
  warn,
@@ -35,7 +36,7 @@ const {
35
36
 
36
37
  function getCorrectionsDir() {
37
38
  try {
38
- const config = JSON.parse(fs.readFileSync(PATHS.config, 'utf-8'));
39
+ const config = getConfig();
39
40
  const detailPath = config?.corrections?.detailPath;
40
41
  if (detailPath) {
41
42
  return path.isAbsolute(detailPath) ? detailPath : path.join(PROJECT_ROOT, detailPath);
@@ -22,10 +22,10 @@
22
22
 
23
23
  const fs = require('fs');
24
24
  const path = require('path');
25
- const crypto = require('crypto');
26
25
  const {
27
26
  PATHS,
28
27
  getConfig,
28
+ generateHashId,
29
29
  success,
30
30
  warn,
31
31
  error,
@@ -163,7 +163,7 @@ function recordAmendment(params) {
163
163
  }
164
164
 
165
165
  // Generate amendment record
166
- const id = `amend-${crypto.randomBytes(4).toString('hex')}`;
166
+ const id = generateHashId('amend', '', '');
167
167
  const amendment = {
168
168
  id,
169
169
  timestamp: new Date().toISOString(),
@@ -210,7 +210,7 @@ function checkOutstandingFindings() {
210
210
  */
211
211
  function runQualityGates(taskId, taskType) {
212
212
  // Validate taskId before using in any path construction
213
- if (taskId && !validateTaskId(taskId)) {
213
+ if (taskId && !validateTaskId(taskId).valid) {
214
214
  console.log(color('red', `Invalid task ID format: ${String(taskId).slice(0, 30)}`));
215
215
  return { passed: false, failed: ['invalidTaskId'], errors: { invalidTaskId: 'Task ID failed validation' } };
216
216
  }
@@ -670,7 +670,7 @@ function runQualityGates(taskId, taskType) {
670
670
  }
671
671
  } else if (gate === 'generatedTestsPass') {
672
672
  if (config.testing?.enabled && config.testing?.generation?.autoGenerate) {
673
- if (!validateTaskId(taskId)) {
673
+ if (!validateTaskId(taskId).valid) {
674
674
  console.log(` ${color('yellow', '⚠')} generatedTestsPass (invalid task ID)`);
675
675
  } else {
676
676
  const testDir = path.join(PATHS.workflow, 'tests', 'generated', taskId);
@@ -727,7 +727,7 @@ function runQualityGates(taskId, taskType) {
727
727
  const gateModes = isUI ? ['ui', 'full', 'auto'] : ['api', 'full', 'auto'];
728
728
  const testingMode = config.testing?.mode || 'off';
729
729
  if (config.testing?.enabled && gateModes.includes(testingMode)) {
730
- if (!validateTaskId(taskId)) {
730
+ if (!validateTaskId(taskId).valid) {
731
731
  console.log(` ${color('yellow', '⚠')} ${gate} (invalid task ID)`);
732
732
  } else {
733
733
  try {
@@ -778,7 +778,7 @@ function runQualityGates(taskId, taskType) {
778
778
  }
779
779
 
780
780
  // Also check scenario verification results if available
781
- if (!isUI && validateTaskId(taskId)) {
781
+ if (!isUI && validateTaskId(taskId).valid) {
782
782
  const scenarioReportPath = path.join(PATHS.workflow, 'verifications', `${taskId}-scenarios.json`);
783
783
  if (fs.existsSync(scenarioReportPath)) {
784
784
  try {
@@ -17,40 +17,19 @@
17
17
  const fs = require('fs');
18
18
  const path = require('path');
19
19
  const memoryDb = require('./flow-memory-db');
20
+ const { getConfig } = require('./flow-config-loader');
21
+ const { color } = require('./flow-output');
20
22
 
21
23
  // ============================================================
22
24
  // Configuration
23
25
  // ============================================================
24
26
 
25
27
  const PROJECT_ROOT = process.env.WOGI_PROJECT_ROOT || process.cwd();
26
- const CONFIG_PATH = path.join(PROJECT_ROOT, '.workflow', 'config.json');
27
-
28
- function loadConfig() {
29
- try {
30
- if (fs.existsSync(CONFIG_PATH)) {
31
- return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
32
- }
33
- } catch {}
34
- return {};
35
- }
36
28
 
37
29
  // ============================================================
38
30
  // Output Formatting
39
31
  // ============================================================
40
32
 
41
- function color(c, text) {
42
- const colors = {
43
- red: '\x1b[31m',
44
- green: '\x1b[32m',
45
- yellow: '\x1b[33m',
46
- blue: '\x1b[34m',
47
- cyan: '\x1b[36m',
48
- gray: '\x1b[90m',
49
- reset: '\x1b[0m'
50
- };
51
- return `${colors[c] || ''}${text}${colors.reset}`;
52
- }
53
-
54
33
  function formatEntropy(entropy) {
55
34
  if (entropy < 0.4) return color('green', `${entropy} (healthy)`);
56
35
  if (entropy < 0.7) return color('yellow', `${entropy} (moderate)`);
@@ -299,7 +278,7 @@ async function showPromotionCandidates(config) {
299
278
 
300
279
  async function main() {
301
280
  const args = process.argv.slice(2);
302
- const config = loadConfig();
281
+ const config = getConfig();
303
282
 
304
283
  try {
305
284
  if (args.includes('--auto')) {
@@ -16,10 +16,10 @@
16
16
 
17
17
  const fs = require('fs');
18
18
  const path = require('path');
19
- const { getProjectRoot: getProjectRootFromUtils, getConfig } = require('./flow-utils');
19
+ const { PATHS, getConfig } = require('./flow-utils');
20
20
 
21
- // Default to getProjectRoot from utils, can be overridden via setProjectRoot() or CLI arg
22
- let PROJECT_ROOT = getProjectRootFromUtils();
21
+ // Default to PATHS.root from flow-utils, can be overridden via setProjectRoot() or CLI arg
22
+ let PROJECT_ROOT = PATHS.root;
23
23
  let CONFIG_PATH = path.join(PROJECT_ROOT, '.workflow/config.json');
24
24
  let CACHE_PATH = path.join(PROJECT_ROOT, '.workflow/state/export-map.json');
25
25
  const CACHE_MAX_AGE_MS = 5 * 60 * 1000; // 5 minutes
@@ -0,0 +1,263 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Gitignore Auto-Management
5
+ *
6
+ * Declarative mapping of config features to .gitignore entries.
7
+ * When a config change enables a feature that produces runtime artifacts,
8
+ * the relevant entries are automatically appended to .gitignore.
9
+ *
10
+ * Design:
11
+ * - Append-only: never removes existing entries
12
+ * - Idempotent: no duplicate entries on repeated calls
13
+ * - Grouped under "# WogiFlow runtime (auto-managed)" comment
14
+ * - Declarative: RUNTIME_ARTIFACT_MAP defines all mappings
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ const { getConfig } = require('./flow-config-loader');
22
+ const { PROJECT_ROOT } = require('./flow-paths');
23
+
24
+ // ============================================================================
25
+ // Declarative Config-to-Gitignore Mapping
26
+ // ============================================================================
27
+
28
+ /**
29
+ * Each entry maps a config condition to gitignore patterns.
30
+ * - configPath: dot-notation path into config object
31
+ * - matchValue: value that triggers the entry (true for boolean, string for exact match)
32
+ * If matchValue is true, any truthy value triggers the entry.
33
+ * - patterns: gitignore entries to add when condition is met
34
+ * - description: human-readable description for health check output
35
+ */
36
+ const RUNTIME_ARTIFACT_MAP = [
37
+ {
38
+ configPath: 'testing.uiProvider',
39
+ matchValue: 'playwright-mcp',
40
+ patterns: ['.playwright-mcp/'],
41
+ description: 'Playwright MCP logs and screenshots'
42
+ },
43
+ {
44
+ configPath: 'testing.enabled',
45
+ matchValue: true,
46
+ patterns: ['.workflow/verifications/', '.workflow/tests/generated/'],
47
+ description: 'Test verification artifacts and generated tests'
48
+ },
49
+ {
50
+ configPath: 'webmcp.enabled',
51
+ matchValue: true,
52
+ patterns: ['.workflow/webmcp/'],
53
+ description: 'WebMCP tool definitions'
54
+ }
55
+ ];
56
+
57
+ const GITIGNORE_SECTION_HEADER = '# WogiFlow runtime (auto-managed)';
58
+
59
+ // ============================================================================
60
+ // Core Functions
61
+ // ============================================================================
62
+
63
+ /**
64
+ * Get the value at a dot-notation path in an object.
65
+ * @param {Object} obj
66
+ * @param {string} dotPath - e.g. 'testing.uiProvider'
67
+ * @returns {*} Value at path, or undefined
68
+ */
69
+ function getNestedValue(obj, dotPath) {
70
+ const parts = dotPath.split('.');
71
+ let current = obj;
72
+ for (const part of parts) {
73
+ if (current == null || typeof current !== 'object') return undefined;
74
+ current = current[part];
75
+ }
76
+ return current;
77
+ }
78
+
79
+ /**
80
+ * Determine which gitignore entries are needed based on current config.
81
+ * @param {Object} [config] - Pre-loaded config (optional)
82
+ * @returns {string[]} Gitignore patterns that should exist
83
+ */
84
+ function getRequiredEntries(config) {
85
+ if (!config) config = getConfig();
86
+ const needed = [];
87
+
88
+ for (const mapping of RUNTIME_ARTIFACT_MAP) {
89
+ const value = getNestedValue(config, mapping.configPath);
90
+ let matches = false;
91
+
92
+ if (mapping.matchValue === true) {
93
+ matches = !!value;
94
+ } else {
95
+ matches = value === mapping.matchValue;
96
+ }
97
+
98
+ if (matches) {
99
+ needed.push(...mapping.patterns);
100
+ }
101
+ }
102
+
103
+ return needed;
104
+ }
105
+
106
+ /**
107
+ * Read the current .gitignore content.
108
+ * @returns {string} Content of .gitignore, or empty string if not found
109
+ */
110
+ function readGitignore() {
111
+ const gitignorePath = path.join(PROJECT_ROOT, '.gitignore');
112
+ try {
113
+ return fs.readFileSync(gitignorePath, 'utf-8');
114
+ } catch (err) {
115
+ return '';
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Parse existing gitignore entries (trimmed, non-empty, non-comment lines).
121
+ * @param {string} content - .gitignore content
122
+ * @returns {Set<string>} Set of existing entries
123
+ */
124
+ function parseExistingEntries(content) {
125
+ const entries = new Set();
126
+ for (const line of content.split('\n')) {
127
+ const trimmed = line.trim();
128
+ if (trimmed && !trimmed.startsWith('#')) {
129
+ entries.push ? entries.add(trimmed) : entries.add(trimmed);
130
+ }
131
+ }
132
+ return entries;
133
+ }
134
+
135
+ /**
136
+ * Sync .gitignore with config-required entries.
137
+ * Appends missing entries under the auto-managed section header.
138
+ * @param {Object} [config] - Pre-loaded config (optional)
139
+ * @returns {{ added: string[], alreadyPresent: string[] }} What was done
140
+ */
141
+ function syncGitignore(config) {
142
+ const required = getRequiredEntries(config);
143
+ if (required.length === 0) {
144
+ return { added: [], alreadyPresent: [] };
145
+ }
146
+
147
+ const gitignorePath = path.join(PROJECT_ROOT, '.gitignore');
148
+ const content = readGitignore();
149
+ const existing = parseExistingEntries(content);
150
+
151
+ const missing = required.filter(entry => !existing.has(entry));
152
+ const alreadyPresent = required.filter(entry => existing.has(entry));
153
+
154
+ if (missing.length === 0) {
155
+ return { added: [], alreadyPresent };
156
+ }
157
+
158
+ // Build the new section content
159
+ const hasSection = content.includes(GITIGNORE_SECTION_HEADER);
160
+ let newContent;
161
+
162
+ if (hasSection) {
163
+ // Append to existing section — find the section and add after it
164
+ const lines = content.split('\n');
165
+ const headerIdx = lines.findIndex(l => l.trim() === GITIGNORE_SECTION_HEADER);
166
+ // Find the end of the managed section (next blank line or next comment section)
167
+ let insertIdx = headerIdx + 1;
168
+ while (insertIdx < lines.length) {
169
+ const line = lines[insertIdx].trim();
170
+ if (line === '' || (line.startsWith('#') && line !== GITIGNORE_SECTION_HEADER)) {
171
+ break;
172
+ }
173
+ insertIdx++;
174
+ }
175
+ // Insert missing entries
176
+ lines.splice(insertIdx, 0, ...missing);
177
+ newContent = lines.join('\n');
178
+ } else {
179
+ // Create new section at end of file
180
+ const separator = content.endsWith('\n') ? '\n' : '\n\n';
181
+ newContent = content + separator + GITIGNORE_SECTION_HEADER + '\n' + missing.join('\n') + '\n';
182
+ }
183
+
184
+ fs.writeFileSync(gitignorePath, newContent, 'utf-8');
185
+
186
+ return { added: missing, alreadyPresent };
187
+ }
188
+
189
+ /**
190
+ * Check gitignore health — returns missing entries for /wogi-health.
191
+ * @param {Object} [config] - Pre-loaded config (optional)
192
+ * @returns {{ ok: boolean, missing: Array<{pattern: string, description: string}> }}
193
+ */
194
+ function checkGitignoreHealth(config) {
195
+ if (!config) config = getConfig();
196
+ const content = readGitignore();
197
+ const existing = parseExistingEntries(content);
198
+ const missing = [];
199
+
200
+ for (const mapping of RUNTIME_ARTIFACT_MAP) {
201
+ const value = getNestedValue(config, mapping.configPath);
202
+ let matches = false;
203
+
204
+ if (mapping.matchValue === true) {
205
+ matches = !!value;
206
+ } else {
207
+ matches = value === mapping.matchValue;
208
+ }
209
+
210
+ if (matches) {
211
+ for (const pattern of mapping.patterns) {
212
+ if (!existing.has(pattern)) {
213
+ missing.push({ pattern, description: mapping.description });
214
+ }
215
+ }
216
+ }
217
+ }
218
+
219
+ return { ok: missing.length === 0, missing };
220
+ }
221
+
222
+ // ============================================================================
223
+ // CLI
224
+ // ============================================================================
225
+
226
+ if (require.main === module) {
227
+ const args = process.argv.slice(2);
228
+ const command = args[0] || 'sync';
229
+
230
+ if (command === 'sync') {
231
+ const result = syncGitignore();
232
+ if (result.added.length > 0) {
233
+ console.log(`Added ${result.added.length} entries to .gitignore:`);
234
+ for (const entry of result.added) {
235
+ console.log(` + ${entry}`);
236
+ }
237
+ } else {
238
+ console.log('.gitignore is up to date');
239
+ }
240
+ } else if (command === 'check') {
241
+ const health = checkGitignoreHealth();
242
+ if (health.ok) {
243
+ console.log('.gitignore: all required entries present');
244
+ } else {
245
+ console.log(`Missing ${health.missing.length} .gitignore entries:`);
246
+ for (const m of health.missing) {
247
+ console.log(` - ${m.pattern} (${m.description})`);
248
+ }
249
+ console.log('\nRun: flow gitignore sync');
250
+ process.exit(1);
251
+ }
252
+ } else {
253
+ console.log('Usage: flow-gitignore.js [sync|check]');
254
+ }
255
+ }
256
+
257
+ module.exports = {
258
+ RUNTIME_ARTIFACT_MAP,
259
+ GITIGNORE_SECTION_HEADER,
260
+ getRequiredEntries,
261
+ syncGitignore,
262
+ checkGitignoreHealth
263
+ };
@@ -38,7 +38,8 @@ const {
38
38
  checkSpecMigration,
39
39
  safeJsonParse,
40
40
  meetsVersion,
41
- getFdCommand
41
+ getFdCommand,
42
+ getConfig
42
43
  } = require('./flow-utils');
43
44
 
44
45
  const { execSync, execFileSync } = require('child_process');
@@ -128,7 +129,7 @@ function main() {
128
129
  let cliType = 'claude-code'; // default
129
130
  if (fileExists(PATHS.config)) {
130
131
  try {
131
- const config = require(PATHS.config);
132
+ const config = getConfig();
132
133
  cliType = config.cli?.type || 'claude-code';
133
134
  } catch {}
134
135
  }
@@ -650,6 +651,26 @@ function main() {
650
651
  }
651
652
  }
652
653
 
654
+ // Check .gitignore sync
655
+ console.log('');
656
+ printSection('Checking .gitignore sync...');
657
+
658
+ try {
659
+ const { checkGitignoreHealth } = require('./flow-gitignore');
660
+ const gitignoreHealth = checkGitignoreHealth(config);
661
+ if (gitignoreHealth.ok) {
662
+ console.log(` ${color('green', '✓')} All required .gitignore entries present`);
663
+ } else {
664
+ for (const m of gitignoreHealth.missing) {
665
+ console.log(` ${color('yellow', '⚠')} Missing: ${m.pattern} (${m.description})`);
666
+ }
667
+ console.log(` ${color('yellow', '⚠')} Run: node scripts/flow-gitignore.js sync`);
668
+ warnings += gitignoreHealth.missing.length;
669
+ }
670
+ } catch (err) {
671
+ console.log(` ${color('yellow', '○')} Gitignore check unavailable`);
672
+ }
673
+
653
674
  // Check git status
654
675
  console.log('');
655
676
  printSection('Checking git status...');
@@ -9,7 +9,7 @@
9
9
  const fs = require('fs');
10
10
  const path = require('path');
11
11
  const { spawnSync } = require('child_process');
12
- const { getProjectRoot } = require('./flow-utils');
12
+ const { getProjectRoot, getConfig } = require('./flow-utils');
13
13
 
14
14
  const PROJECT_ROOT = getProjectRoot();
15
15
  const TESTS = [];
@@ -52,8 +52,7 @@ test('Config file exists', () => {
52
52
  });
53
53
 
54
54
  test('Config has hybrid section', () => {
55
- const configPath = path.join(PROJECT_ROOT, '.workflow', 'config.json');
56
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
55
+ const config = getConfig();
57
56
  if (!config.hybrid) {
58
57
  throw new Error('hybrid section missing from config');
59
58
  }
@@ -13,10 +13,8 @@
13
13
 
14
14
  const fs = require('fs');
15
15
  const path = require('path');
16
- const crypto = require('crypto');
17
-
18
16
  // Import safe utilities
19
- const { safeJsonParse, writeJson, generateTaskId, withLock, PATHS } = require('./flow-utils');
17
+ const { safeJsonParse, writeJson, generateTaskId, generateHashId, withLock, PATHS } = require('./flow-utils');
20
18
 
21
19
  // Utility: ISO timestamp
22
20
  function now() {
@@ -95,7 +93,7 @@ const SCENARIO_PATTERNS = [
95
93
  * Generate unique story ID
96
94
  */
97
95
  function generateStoryId() {
98
- return 'story-' + crypto.randomBytes(4).toString('hex');
96
+ return generateHashId('story', '', '');
99
97
  }
100
98
 
101
99
  /**
@@ -1063,14 +1061,14 @@ function resetPresentation() {
1063
1061
  * Generate unique edit session ID
1064
1062
  */
1065
1063
  function generateEditSessionId() {
1066
- return 'edit-' + crypto.randomBytes(4).toString('hex');
1064
+ return generateHashId('edit', '', '');
1067
1065
  }
1068
1066
 
1069
1067
  /**
1070
1068
  * Generate unique change ID
1071
1069
  */
1072
1070
  function generateChangeId() {
1073
- return 'change-' + crypto.randomBytes(3).toString('hex');
1071
+ return generateHashId('change', '', '');
1074
1072
  }
1075
1073
 
1076
1074
  /**