wogiflow 2.17.0 → 2.18.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 (111) hide show
  1. package/.claude/commands/wogi-audit.md +212 -17
  2. package/.claude/commands/wogi-research.md +37 -0
  3. package/.claude/commands/wogi-review.md +200 -22
  4. package/.claude/commands/wogi-start.md +45 -0
  5. package/.claude/docs/claude-code-compatibility.md +46 -1
  6. package/.claude/docs/intent-grounded-review.md +209 -0
  7. package/.claude/settings.json +34 -1
  8. package/.workflow/agents/logic-adversary.md +8 -0
  9. package/.workflow/templates/claude-md.hbs +18 -0
  10. package/lib/installer.js +22 -0
  11. package/lib/utils.js +29 -3
  12. package/lib/workspace-changelog.js +2 -1
  13. package/lib/workspace-channel-server.js +4 -6
  14. package/lib/workspace-contracts.js +5 -4
  15. package/lib/workspace-events.js +8 -7
  16. package/lib/workspace-gates.js +4 -3
  17. package/lib/workspace-integration-tests.js +2 -1
  18. package/lib/workspace-intelligence.js +3 -2
  19. package/lib/workspace-locks.js +2 -1
  20. package/lib/workspace-messages.js +7 -6
  21. package/lib/workspace-routing.js +14 -26
  22. package/lib/workspace-session.js +7 -6
  23. package/lib/workspace-sync.js +9 -8
  24. package/package.json +4 -2
  25. package/scripts/base-workflow-step.js +1 -1
  26. package/scripts/flow +19 -0
  27. package/scripts/flow-adaptive-learning.js +1 -1
  28. package/scripts/flow-aggregate.js +2 -1
  29. package/scripts/flow-architect-pass.js +3 -3
  30. package/scripts/flow-archive-runs.js +372 -0
  31. package/scripts/flow-ask.js +1 -1
  32. package/scripts/flow-ast-grep.js +216 -0
  33. package/scripts/flow-audit-gates.js +1 -1
  34. package/scripts/flow-auto-learn.js +8 -11
  35. package/scripts/flow-bug.js +2 -2
  36. package/scripts/flow-capture-gate.js +644 -0
  37. package/scripts/flow-capture.js +4 -3
  38. package/scripts/flow-cli-flags.js +95 -0
  39. package/scripts/flow-community-sync.js +2 -1
  40. package/scripts/flow-community.js +6 -6
  41. package/scripts/flow-conclusion-classifier.js +310 -0
  42. package/scripts/flow-config-defaults.js +3 -3
  43. package/scripts/flow-constants.js +8 -11
  44. package/scripts/flow-context-scoring.js +1 -0
  45. package/scripts/flow-correction-detector.js +344 -3
  46. package/scripts/flow-damage-control.js +1 -1
  47. package/scripts/flow-decisions-merge.js +1 -0
  48. package/scripts/flow-done-gates.js +20 -0
  49. package/scripts/flow-done-report.js +2 -2
  50. package/scripts/flow-done.js +4 -4
  51. package/scripts/flow-epics.js +5 -11
  52. package/scripts/flow-health.js +145 -1
  53. package/scripts/flow-id.js +92 -0
  54. package/scripts/flow-io.js +15 -5
  55. package/scripts/flow-knowledge-router.js +2 -1
  56. package/scripts/flow-links.js +1 -1
  57. package/scripts/flow-log-manager.js +2 -1
  58. package/scripts/flow-logic-adversary.js +4 -4
  59. package/scripts/flow-long-input-cli.js +6 -0
  60. package/scripts/flow-long-input-stories.js +1 -1
  61. package/scripts/flow-loop-retry-learning.js +1 -1
  62. package/scripts/flow-mcp-capabilities.js +2 -3
  63. package/scripts/flow-mcp-docs.js +2 -1
  64. package/scripts/flow-memory-blocks.js +2 -1
  65. package/scripts/flow-memory-sync.js +1 -1
  66. package/scripts/flow-memory.js +767 -0
  67. package/scripts/flow-migrate-igr.js +1 -1
  68. package/scripts/flow-migrate.js +2 -1
  69. package/scripts/flow-model-adapter.js +1 -1
  70. package/scripts/flow-model-config.js +5 -1
  71. package/scripts/flow-model-profile.js +2 -1
  72. package/scripts/flow-orchestrate.js +3 -3
  73. package/scripts/flow-output.js +29 -0
  74. package/scripts/flow-parallel.js +10 -9
  75. package/scripts/flow-pattern-enforcer.js +2 -1
  76. package/scripts/flow-permissions-audit.js +124 -0
  77. package/scripts/flow-plugin-registry.js +2 -2
  78. package/scripts/flow-progress.js +5 -1
  79. package/scripts/flow-project-analyzer.js +1 -1
  80. package/scripts/flow-promote.js +510 -0
  81. package/scripts/flow-registries.js +86 -0
  82. package/scripts/flow-request-log.js +133 -0
  83. package/scripts/flow-research-protocol.js +0 -1
  84. package/scripts/flow-revision-tracker.js +2 -1
  85. package/scripts/flow-roadmap.js +2 -1
  86. package/scripts/flow-rules-sync.js +3 -7
  87. package/scripts/flow-session-end.js +3 -1
  88. package/scripts/flow-session-learning.js +6 -13
  89. package/scripts/flow-session-state.js +2 -2
  90. package/scripts/flow-setup-hooks.js +2 -1
  91. package/scripts/flow-skill-create.js +1 -1
  92. package/scripts/flow-skill-freshness.js +6 -7
  93. package/scripts/flow-skill-learn.js +1 -1
  94. package/scripts/flow-step-coverage.js +1 -1
  95. package/scripts/flow-step-security.js +1 -1
  96. package/scripts/flow-story.js +58 -10
  97. package/scripts/flow-sys.js +204 -0
  98. package/scripts/flow-task-hierarchy.js +88 -0
  99. package/scripts/flow-tech-debt.js +2 -1
  100. package/scripts/flow-test-api.js +1 -1
  101. package/scripts/flow-utils.js +60 -890
  102. package/scripts/hooks/core/bugfix-scope-gate.js +5 -4
  103. package/scripts/hooks/core/deploy-gate.js +1 -1
  104. package/scripts/hooks/core/pre-tool-helpers.js +72 -0
  105. package/scripts/hooks/core/pre-tool-orchestrator.js +442 -0
  106. package/scripts/hooks/core/routing-gate.js +8 -0
  107. package/scripts/hooks/core/session-end.js +28 -0
  108. package/scripts/hooks/entry/claude-code/pre-tool-use.js +48 -492
  109. package/scripts/hooks/entry/shared/hook-runner.js +1 -1
  110. package/scripts/registries/schema-registry.js +1 -1
  111. package/scripts/registries/service-registry.js +1 -1
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Request Log Operations
5
+ *
6
+ * Read/count/append operations for request-log.md. Extracted from
7
+ * flow-utils.js (wf-94cc3b72 epic — flow-utils decomposition).
8
+ *
9
+ * Uses appendFileSync for atomic append (avoids read-modify-write race).
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ const fs = require('node:fs');
15
+ const { PATHS } = require('./flow-paths');
16
+ const { readFile } = require('./flow-io');
17
+ const { error } = require('./flow-output');
18
+
19
+ /**
20
+ * Get the current AI CLI session ID (kept local to avoid circular flow-utils dep).
21
+ * @returns {string|null}
22
+ */
23
+ function getSessionId() {
24
+ return process.env.CLAUDE_SESSION_ID
25
+ || process.env.AI_SESSION_ID
26
+ || null;
27
+ }
28
+
29
+ /**
30
+ * Count entries in request-log.md
31
+ * @returns {number}
32
+ */
33
+ function countRequestLogEntries() {
34
+ try {
35
+ const content = readFile(PATHS.requestLog, '');
36
+ const matches = content.match(/^### R-/gm);
37
+ return matches ? matches.length : 0;
38
+ } catch (_err) {
39
+ return 0;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Get the last request log entry (header line only)
45
+ * @returns {string|null}
46
+ */
47
+ function getLastRequestLogEntry() {
48
+ try {
49
+ const content = readFile(PATHS.requestLog, '');
50
+ const matches = content.match(/^### R-.*$/gm);
51
+ return matches ? matches[matches.length - 1] : null;
52
+ } catch (_err) {
53
+ return null;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Get the highest request ID number from request-log.md.
59
+ * More robust than counting — handles gaps and deleted entries.
60
+ * @returns {number}
61
+ */
62
+ function getHighestRequestId() {
63
+ try {
64
+ const content = readFile(PATHS.requestLog, '');
65
+ const matches = content.match(/### R-(\d{3,})/g);
66
+ if (!matches || matches.length === 0) return 0;
67
+
68
+ const numbers = matches.map(m => {
69
+ const num = m.match(/R-(\d+)/);
70
+ return num ? parseInt(num[1], 10) : 0;
71
+ });
72
+ return Math.max(...numbers);
73
+ } catch (_err) {
74
+ return 0;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Get next request ID (highest existing + 1, zero-padded to 3 digits).
80
+ * @returns {string} e.g., 'R-042'
81
+ */
82
+ function getNextRequestId() {
83
+ const highestId = getHighestRequestId();
84
+ return `R-${String(highestId + 1).padStart(3, '0')}`;
85
+ }
86
+
87
+ /**
88
+ * Append an entry to request-log.md.
89
+ * @param {Object} entry
90
+ * @param {string} entry.type - new | fix | change | refactor
91
+ * @param {string[]} entry.tags - e.g., ['#figma', '#component:Button']
92
+ * @param {string} entry.request - What was requested
93
+ * @param {string} entry.result - What was done
94
+ * @param {string[]} [entry.files] - Files changed
95
+ * @param {string} [entry.sessionId] - CLI session ID (auto-detected if absent)
96
+ * @returns {string|null} Assigned R-ID, or null on failure.
97
+ */
98
+ function addRequestLogEntry(entry) {
99
+ const { type, tags, request, result, files = [], sessionId } = entry;
100
+ const id = getNextRequestId();
101
+ const now = new Date();
102
+ const timestamp = now.toISOString().replace('T', ' ').substring(0, 16);
103
+
104
+ const session = sessionId || getSessionId();
105
+ const sessionLine = session ? `\n**Session**: ${session}` : '';
106
+
107
+ const filesLine = files.length > 0 ? `\n**Files**: ${files.join(', ')}` : '';
108
+ const tagsStr = tags.join(' ');
109
+
110
+ const logEntry = `
111
+ ### ${id} | ${timestamp}
112
+ **Type**: ${type}
113
+ **Tags**: ${tagsStr}${sessionLine}
114
+ **Request**: "${request}"
115
+ **Result**: ${result}${filesLine}
116
+ `;
117
+
118
+ try {
119
+ fs.appendFileSync(PATHS.requestLog, logEntry);
120
+ return id;
121
+ } catch (err) {
122
+ error(`Failed to add request log entry: ${err.message}`);
123
+ return null;
124
+ }
125
+ }
126
+
127
+ module.exports = {
128
+ countRequestLogEntries,
129
+ getLastRequestLogEntry,
130
+ getHighestRequestId,
131
+ getNextRequestId,
132
+ addRequestLogEntry,
133
+ };
@@ -149,7 +149,6 @@ function getResearchConfig() {
149
149
  [DEPTHS.DEEP]: 50000,
150
150
  [DEPTHS.EXHAUSTIVE]: 100000
151
151
  },
152
- requireCitations: research.requireCitations !== false,
153
152
  cacheVerifications: research.cacheVerifications !== false,
154
153
  cacheExpiryHours: research.cacheExpiryHours || 24,
155
154
  triggers: research.triggers || {
@@ -18,7 +18,8 @@ const {
18
18
  getConfig,
19
19
  PATHS,
20
20
  readJson,
21
- fileExists
21
+ fileExists,
22
+ getTodayDate
22
23
  } = require('./flow-utils');
23
24
  const { recordRevision, loadStats } = require('./flow-stats-collector');
24
25
 
@@ -25,7 +25,8 @@ const {
25
25
  parseFlags,
26
26
  getConfig,
27
27
  isPathWithinProject,
28
- escapeRegex
28
+ escapeRegex,
29
+ getTodayDate
29
30
  } = require('./flow-utils');
30
31
  const { success, warn, error, info, printHeader } = require('./flow-output');
31
32
 
@@ -141,13 +141,9 @@ function parseMarkdownSections(content) {
141
141
  * @param {string} title - Section title
142
142
  * @returns {string} - Slugified filename
143
143
  */
144
- function slugify(title) {
145
- return title
146
- .toLowerCase()
147
- .replace(/[^a-z0-9]+/g, '-')
148
- .replace(/^-+|-+$/g, '')
149
- .substring(0, 50); // Limit length
150
- }
144
+ // Local slugify removed in favor of canonical flow-output slugify (wf-7072d3ac).
145
+ const { slugify: _slugify } = require('./flow-output');
146
+ const slugify = (title) => _slugify(title, { maxLength: 50 });
151
147
 
152
148
  /**
153
149
  * Determine path scope for a section based on title keywords
@@ -12,6 +12,7 @@ const {
12
12
  spawnSync } = require('node:child_process');
13
13
  const readline = require('node:readline/promises');
14
14
  const path = require('node:path');
15
+ const fs = require('node:fs');
15
16
  const {
16
17
  PATHS,
17
18
  PROJECT_ROOT,
@@ -23,7 +24,8 @@ const {
23
24
  writeFile,
24
25
  isGitRepo,
25
26
  getGitStatus,
26
- safeJsonParse
27
+ safeJsonParse,
28
+ getTodayDate
27
29
  } = require('./flow-utils')
28
30
  const { color, printSection, success, warn, error } = require('./flow-output');;
29
31
 
@@ -14,6 +14,8 @@
14
14
 
15
15
  const fs = require('node:fs');
16
16
  const path = require('node:path');
17
+ const { slugify: _slugify } = require('./flow-output');
18
+ const slugify = (s) => _slugify(s, { maxLength: 40 });
17
19
  const {
18
20
  PATHS,
19
21
  fileExists,
@@ -23,7 +25,8 @@ const {
23
25
  success,
24
26
  info,
25
27
  warn,
26
- safeJsonParse
28
+ safeJsonParse,
29
+ getTodayDate
27
30
  } = require('./flow-utils');
28
31
 
29
32
  // Import shared parsing functions from log manager (DRY - avoid duplication)
@@ -700,18 +703,8 @@ ${learning.description}
700
703
  // Utility Functions
701
704
  // ============================================================
702
705
 
703
- /**
704
- * Convert string to kebab-case slug
705
- */
706
- function slugify(str) {
707
- return str
708
- .toLowerCase()
709
- .replace(/[^a-z0-9\s-]/g, '')
710
- .replace(/\s+/g, '-')
711
- .replace(/-+/g, '-')
712
- .replace(/^-|-$/g, '')
713
- .slice(0, 40);
714
- }
706
+ // Local slugify removed in favor of canonical flow-output slugify (wf-7072d3ac).
707
+ // Callers below now use `slugify(str, { maxLength: 40 })` imported at top.
715
708
 
716
709
  /**
717
710
  * Calculate confidence based on occurrences
@@ -117,10 +117,10 @@ function loadSessionState() {
117
117
  ...getDefaultState(),
118
118
  ...state
119
119
  };
120
- } catch (parseError) {
120
+ } catch (err) {
121
121
  // Log in debug mode to help diagnose issues
122
122
  if (process.env.DEBUG) {
123
- console.warn(`[DEBUG] Could not parse session state: ${parseError.message}`);
123
+ console.warn(`[DEBUG] Could not parse session state: ${err.message}`);
124
124
  }
125
125
  return getDefaultState();
126
126
  }
@@ -17,7 +17,8 @@ const fs = require('node:fs');
17
17
  const path = require('node:path');
18
18
  const {
19
19
  getProjectRoot,
20
- getConfig, PATHS
20
+ getConfig, PATHS,
21
+ getTodayDate
21
22
  } = require('./flow-utils')
22
23
  const { color, success, warn, error } = require('./flow-output');;
23
24
 
@@ -14,7 +14,7 @@
14
14
  const fs = require('node:fs');
15
15
  const path = require('node:path');
16
16
  const readline = require('node:readline/promises');
17
- const { getProjectRoot, colors, PATHS } = require('./flow-utils');
17
+ const { getProjectRoot, colors, PATHS, getTodayDate } = require('./flow-utils');
18
18
  const { success, warn, error: errorMsg, info, print } = require('./flow-output');
19
19
  const { getAllSkills, getSkillDir } = require('./flow-skill-matcher');
20
20
 
@@ -8,6 +8,7 @@
8
8
  const fs = require('node:fs');
9
9
  const path = require('node:path');
10
10
  const { getConfig } = require('./flow-config-loader');
11
+ const { getTodayDate } = require('./flow-output');
11
12
 
12
13
  // ============================================
13
14
  // CONFIGURATION
@@ -15,8 +16,8 @@ const { getConfig } = require('./flow-config-loader');
15
16
 
16
17
  const DEFAULT_FRESHNESS_THRESHOLD_DAYS = 90;
17
18
 
18
- // Dangerous keys that could cause prototype pollution
19
- const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
19
+ // Local DANGEROUS_KEYS consolidated to flow-io canonical (wf-2f6fbb12 / dup-002).
20
+ const { DANGEROUS_KEYS } = require('./flow-io');
20
21
 
21
22
  // ============================================
22
23
  // FRESHNESS CHECKING
@@ -51,7 +52,7 @@ function parseFrontmatter(content) {
51
52
  * @returns {Array<{name: string, source: string, lastDocCheck: string|null, daysSinceCheck: number|null, isStale: boolean, prebuiltVersion: string|null}>}
52
53
  */
53
54
  function getSkillFreshnessReport(projectRoot) {
54
- const config = loadConfig(projectRoot);
55
+ const config = getConfig();
55
56
  const threshold = config?.skills?.freshnessThreshold || DEFAULT_FRESHNESS_THRESHOLD_DAYS;
56
57
  const skillsDir = path.join(projectRoot, '.claude', 'skills');
57
58
 
@@ -160,9 +161,7 @@ function updateLastDocCheck(skillDir, date) {
160
161
  // CONFIG LOADING
161
162
  // ============================================
162
163
 
163
- function loadConfig(_projectRoot) {
164
- return getConfig();
165
- }
164
+ // loadConfig(_projectRoot) trivial wrapper removed — callers now use getConfig() directly (wf-7072d3ac / dup-001).
166
165
 
167
166
  // ============================================
168
167
  // CLI
@@ -197,7 +196,7 @@ function printFreshnessReport(report, threshold) {
197
196
 
198
197
  if (require.main === module) {
199
198
  const projectRoot = process.cwd();
200
- const config = loadConfig(projectRoot);
199
+ const config = getConfig();
201
200
  const threshold = config?.skills?.freshnessThreshold || DEFAULT_FRESHNESS_THRESHOLD_DAYS;
202
201
 
203
202
  const args = process.argv.slice(2);
@@ -20,7 +20,7 @@
20
20
  const fs = require('node:fs');
21
21
  const path = require('node:path');
22
22
  const { execSync } = require('node:child_process');
23
- const { getProjectRoot, getConfig, colors, PATHS } = require('./flow-utils');
23
+ const { getProjectRoot, getConfig, colors, PATHS, getTodayDate } = require('./flow-utils');
24
24
  const { getAllSkills, getSkillDir } = require('./flow-skill-matcher');
25
25
 
26
26
  const SKILLS_DIR = path.join(PATHS.root, '.claude', 'skills');
@@ -10,7 +10,7 @@
10
10
  const fs = require('node:fs');
11
11
  const path = require('node:path');
12
12
  const { execSync } = require('node:child_process');
13
- const { getProjectRoot, colors, getConfig, safeJsonParse, readJson, error } = require('./flow-utils');
13
+ const { getProjectRoot, colors, getConfig, safeJsonParse, readJson, error, PATHS } = require('./flow-utils');
14
14
  const { getCommand, getExec } = require('./flow-script-resolver');
15
15
 
16
16
  // Common coverage output locations
@@ -115,7 +115,7 @@ class SecurityStep extends BaseWorkflowStep {
115
115
  });
116
116
  }
117
117
  }
118
- } catch (_parseError) {
118
+ } catch (_err) {
119
119
  // Ignore parse errors
120
120
  }
121
121
  }
@@ -283,16 +283,10 @@ function analyzeForDecomposition(title) {
283
283
  * @param {string} title - Title to slugify
284
284
  * @returns {string} Slugified title
285
285
  */
286
- function slugify(title) {
287
- return title
288
- .toLowerCase()
289
- .trim()
290
- .replace(/[^\w\s-]/g, '') // Remove non-word chars (except spaces and hyphens)
291
- .replace(/[\s_]+/g, '-') // Replace spaces and underscores with hyphens
292
- .replace(/-+/g, '-') // Replace multiple hyphens with single
293
- .replace(/^-+|-+$/g, '') // Trim hyphens from start/end
294
- .substring(0, 50); // Limit length
295
- }
286
+ // Local slugify removed in favor of canonical flow-output slugify (wf-7072d3ac).
287
+ // 'word' mode preserves underscores (old behavior).
288
+ const { slugify: _slugify } = require('./flow-output');
289
+ const slugify = (title) => _slugify(title, { mode: 'word', maxLength: 50 });
296
290
 
297
291
  /**
298
292
  * Create story with optional deep decomposition
@@ -375,6 +369,46 @@ async function createStory(title, options = {}) {
375
369
  result.patterns = analysis.patterns;
376
370
  }
377
371
 
372
+ // Propagate simple (non-decomposed) stories to ready.json.
373
+ // wf-b5cff650: previously only decomposed stories reached the task queue;
374
+ // simple stories sat in .workflow/changes/ and were invisible to /wogi-start.
375
+ if (!shouldDecompose && !dryRun && fs.existsSync(READY_PATH)) {
376
+ try {
377
+ await withLock(READY_PATH, async () => {
378
+ const ready = safeJsonParse(READY_PATH, { ready: [] });
379
+ ready.ready = ready.ready || [];
380
+ // Idempotent: skip if this task ID is already present anywhere in the queue.
381
+ const allIds = [
382
+ ...(ready.ready || []),
383
+ ...(ready.inProgress || []),
384
+ ...(ready.blocked || []),
385
+ ...(ready.recentlyCompleted || []),
386
+ ...(ready.backlog || []),
387
+ ].map(t => t && t.id).filter(Boolean);
388
+ if (!allIds.includes(taskId)) {
389
+ ready.ready.push({
390
+ id: taskId,
391
+ title,
392
+ type: 'story',
393
+ level: 'L1',
394
+ status: 'ready',
395
+ priority,
396
+ created: new Date().toISOString(),
397
+ specPath: path.relative(PATHS.root, storyFile),
398
+ });
399
+ ready.lastUpdated = new Date().toISOString();
400
+ fs.writeFileSync(READY_PATH, JSON.stringify(ready, null, 2));
401
+ }
402
+ });
403
+ result.addedToReady = true;
404
+ } catch (err) {
405
+ result.addedToReady = false;
406
+ result.readyError = err.message;
407
+ }
408
+ } else if (!shouldDecompose && dryRun) {
409
+ result.wouldAddToReady = true;
410
+ }
411
+
378
412
  if (shouldDecompose && analysis.suggestedSubTasks.length > 0) {
379
413
  // Create sub-task files in feature folder
380
414
  let subNum = 1;
@@ -612,6 +646,20 @@ Examples:
612
646
  log('dim', ` Run: flow story "${title}" --deep`);
613
647
  }
614
648
 
649
+ // Simple (non-decomposed) story ready.json status — wf-b5cff650 fix
650
+ if (!result.decomposed) {
651
+ if (result.addedToReady) {
652
+ console.log('');
653
+ success('Added to ready.json (run: flow ready)');
654
+ } else if (result.wouldAddToReady) {
655
+ console.log('');
656
+ warn('[DRY RUN] Would add to ready.json');
657
+ } else if (result.readyError) {
658
+ console.log('');
659
+ warn(`Could not add to ready.json: ${result.readyError}`);
660
+ }
661
+ }
662
+
615
663
  console.log('');
616
664
  if (result.dryRun) {
617
665
  log('yellow', '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - System-Level Helpers
5
+ *
6
+ * CLI tool detection (fd, semver compare) + git repo utilities + app-map
7
+ * state-file operations. Extracted from flow-utils.js (wf-94cc3b72 epic
8
+ * — flow-utils decomposition).
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ const fs = require('node:fs');
14
+ const path = require('node:path');
15
+ const { execSync, execFileSync } = require('node:child_process');
16
+ const { PATHS, PROJECT_ROOT } = require('./flow-paths');
17
+ const { readFile, writeFile } = require('./flow-io');
18
+ const { warn, error } = require('./flow-output');
19
+
20
+ // ============================================================
21
+ // CLI Tool Detection (Claude Code 2.1.72+ compatibility)
22
+ // ============================================================
23
+
24
+ /**
25
+ * Compare a parsed semver against a required minimum.
26
+ * @param {number} major - Parsed major
27
+ * @param {number} minor - Parsed minor
28
+ * @param {number} patch - Parsed patch
29
+ * @param {number} rMajor - Required major
30
+ * @param {number} rMinor - Required minor
31
+ * @param {number} rPatch - Required patch
32
+ * @returns {boolean}
33
+ */
34
+ function meetsVersion(major, minor, patch, rMajor, rMinor, rPatch) {
35
+ return major > rMajor ||
36
+ (major === rMajor && minor > rMinor) ||
37
+ (major === rMajor && minor === rMinor && patch >= rPatch);
38
+ }
39
+
40
+ /**
41
+ * Detect if fd or fdfind is available on the system.
42
+ * fd/fdfind is auto-approved in Claude Code 2.1.72+ bash allowlist,
43
+ * making it a better choice than find for reduced permission prompts.
44
+ *
45
+ * Result is memoized for the process lifetime.
46
+ * @returns {string|false} The fd command name ('fd' or 'fdfind'), or false
47
+ */
48
+ let _fdCommand = null;
49
+ function getFdCommand() {
50
+ if (_fdCommand !== null) return _fdCommand;
51
+ for (const cmd of ['fd', 'fdfind']) {
52
+ try {
53
+ execFileSync(cmd, ['--version'], { stdio: 'pipe', timeout: 3000 });
54
+ _fdCommand = cmd;
55
+ return cmd;
56
+ } catch (_err) {
57
+ // Not available
58
+ }
59
+ }
60
+ _fdCommand = false;
61
+ return false;
62
+ }
63
+
64
+ // ============================================================
65
+ // Git Operations
66
+ // ============================================================
67
+
68
+ /**
69
+ * Check if current directory is a git repo.
70
+ * Note: .git can be a directory (normal repo) or file (worktree).
71
+ */
72
+ function isGitRepo() {
73
+ const gitPath = path.join(PROJECT_ROOT, '.git');
74
+ return fs.existsSync(gitPath);
75
+ }
76
+
77
+ /**
78
+ * Get git status info.
79
+ * @returns {{ isRepo: boolean, branch?: string, uncommitted?: number, clean?: boolean, error?: string }}
80
+ */
81
+ function getGitStatus() {
82
+ if (!isGitRepo()) {
83
+ return { isRepo: false };
84
+ }
85
+
86
+ try {
87
+ const branch = execSync('git branch --show-current', {
88
+ encoding: 'utf-8',
89
+ stdio: ['pipe', 'pipe', 'pipe']
90
+ }).trim();
91
+
92
+ const status = execSync('git status --porcelain', {
93
+ encoding: 'utf-8',
94
+ stdio: ['pipe', 'pipe', 'pipe']
95
+ });
96
+
97
+ const uncommitted = status.split('\n').filter(Boolean).length;
98
+
99
+ return {
100
+ isRepo: true,
101
+ branch,
102
+ uncommitted,
103
+ clean: uncommitted === 0
104
+ };
105
+ } catch (err) {
106
+ return { isRepo: true, error: err.message };
107
+ }
108
+ }
109
+
110
+ // ============================================================
111
+ // App Map Operations
112
+ // ============================================================
113
+
114
+ /**
115
+ * Count components in app-map.md (data rows only — excludes headers).
116
+ * @returns {number}
117
+ */
118
+ function countAppMapComponents() {
119
+ try {
120
+ const content = readFile(PATHS.appMap, '');
121
+ const dataRows = content.match(/^\|[^-|][^|]*\|/gm);
122
+ const headerCount = (content.match(/^## /gm) || []).length * 1;
123
+ return dataRows ? Math.max(0, dataRows.length - headerCount) : 0;
124
+ } catch (_err) {
125
+ return 0;
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Add a component row to app-map.md in the appropriate section.
131
+ * @param {Object} component
132
+ * @param {string} component.name
133
+ * @param {string} component.type - 'screen' | 'modal' | 'component' | 'layout'
134
+ * @param {string} component.path
135
+ * @param {string[]} [component.variants]
136
+ * @param {string} [component.description]
137
+ * @returns {boolean}
138
+ */
139
+ function addAppMapComponent(component) {
140
+ const { name, type, path: filePath, variants = [], description = '' } = component;
141
+
142
+ try {
143
+ let content = readFile(PATHS.appMap, '');
144
+
145
+ const sectionMap = {
146
+ screen: '## Screens',
147
+ modal: '## Modals',
148
+ component: '## Components',
149
+ layout: '## Layouts'
150
+ };
151
+
152
+ const section = sectionMap[type] || '## Components';
153
+ const variantsStr = variants.length > 0 ? variants.join(', ') : '-';
154
+ const descStr = description || '-';
155
+
156
+ const newRow = `| ${name} | ${filePath} | ${variantsStr} | ${descStr} |`;
157
+
158
+ const sectionIndex = content.indexOf(section);
159
+ if (sectionIndex === -1) {
160
+ warn(`Section "${section}" not found in app-map.md`);
161
+ return false;
162
+ }
163
+
164
+ const nextSectionMatch = content.substring(sectionIndex + section.length).match(/\n## /);
165
+ const endIndex = nextSectionMatch
166
+ ? sectionIndex + section.length + nextSectionMatch.index
167
+ : content.length;
168
+
169
+ const sectionContent = content.substring(sectionIndex, endIndex);
170
+ const lastPipeIndex = sectionContent.lastIndexOf('\n|');
171
+
172
+ if (lastPipeIndex !== -1) {
173
+ const afterPipe = sectionContent.substring(lastPipeIndex);
174
+ const newlineOffset = afterPipe.indexOf('\n', 1);
175
+ const insertOffset = newlineOffset !== -1 ? newlineOffset : afterPipe.length;
176
+ const insertIndex = sectionIndex + lastPipeIndex + insertOffset;
177
+ content = content.substring(0, insertIndex) + '\n' + newRow + content.substring(insertIndex);
178
+ } else {
179
+ const headerEnd = sectionContent.indexOf('\n\n');
180
+ if (headerEnd !== -1) {
181
+ const insertIndex = sectionIndex + headerEnd;
182
+ content = content.substring(0, insertIndex) + '\n' + newRow + content.substring(insertIndex);
183
+ } else {
184
+ warn(`Could not find proper insertion point in section "${section}"`);
185
+ return false;
186
+ }
187
+ }
188
+
189
+ writeFile(PATHS.appMap, content);
190
+ return true;
191
+ } catch (err) {
192
+ error(`Failed to add component to app-map: ${err.message}`);
193
+ return false;
194
+ }
195
+ }
196
+
197
+ module.exports = {
198
+ meetsVersion,
199
+ getFdCommand,
200
+ isGitRepo,
201
+ getGitStatus,
202
+ countAppMapComponents,
203
+ addAppMapComponent,
204
+ };