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.
- package/.claude/commands/wogi-audit.md +212 -17
- package/.claude/commands/wogi-research.md +37 -0
- package/.claude/commands/wogi-review.md +200 -22
- package/.claude/commands/wogi-start.md +45 -0
- package/.claude/docs/claude-code-compatibility.md +46 -1
- package/.claude/docs/intent-grounded-review.md +209 -0
- package/.claude/settings.json +34 -1
- package/.workflow/agents/logic-adversary.md +8 -0
- package/.workflow/templates/claude-md.hbs +18 -0
- package/lib/installer.js +22 -0
- package/lib/utils.js +29 -3
- package/lib/workspace-changelog.js +2 -1
- package/lib/workspace-channel-server.js +4 -6
- package/lib/workspace-contracts.js +5 -4
- package/lib/workspace-events.js +8 -7
- package/lib/workspace-gates.js +4 -3
- package/lib/workspace-integration-tests.js +2 -1
- package/lib/workspace-intelligence.js +3 -2
- package/lib/workspace-locks.js +2 -1
- package/lib/workspace-messages.js +7 -6
- package/lib/workspace-routing.js +14 -26
- package/lib/workspace-session.js +7 -6
- package/lib/workspace-sync.js +9 -8
- package/package.json +4 -2
- package/scripts/base-workflow-step.js +1 -1
- package/scripts/flow +19 -0
- package/scripts/flow-adaptive-learning.js +1 -1
- package/scripts/flow-aggregate.js +2 -1
- package/scripts/flow-architect-pass.js +3 -3
- package/scripts/flow-archive-runs.js +372 -0
- package/scripts/flow-ask.js +1 -1
- package/scripts/flow-ast-grep.js +216 -0
- package/scripts/flow-audit-gates.js +1 -1
- package/scripts/flow-auto-learn.js +8 -11
- package/scripts/flow-bug.js +2 -2
- package/scripts/flow-capture-gate.js +644 -0
- package/scripts/flow-capture.js +4 -3
- package/scripts/flow-cli-flags.js +95 -0
- package/scripts/flow-community-sync.js +2 -1
- package/scripts/flow-community.js +6 -6
- package/scripts/flow-conclusion-classifier.js +310 -0
- package/scripts/flow-config-defaults.js +3 -3
- package/scripts/flow-constants.js +8 -11
- package/scripts/flow-context-scoring.js +1 -0
- package/scripts/flow-correction-detector.js +344 -3
- package/scripts/flow-damage-control.js +1 -1
- package/scripts/flow-decisions-merge.js +1 -0
- package/scripts/flow-done-gates.js +20 -0
- package/scripts/flow-done-report.js +2 -2
- package/scripts/flow-done.js +4 -4
- package/scripts/flow-epics.js +5 -11
- package/scripts/flow-health.js +145 -1
- package/scripts/flow-id.js +92 -0
- package/scripts/flow-io.js +15 -5
- package/scripts/flow-knowledge-router.js +2 -1
- package/scripts/flow-links.js +1 -1
- package/scripts/flow-log-manager.js +2 -1
- package/scripts/flow-logic-adversary.js +4 -4
- package/scripts/flow-long-input-cli.js +6 -0
- package/scripts/flow-long-input-stories.js +1 -1
- package/scripts/flow-loop-retry-learning.js +1 -1
- package/scripts/flow-mcp-capabilities.js +2 -3
- package/scripts/flow-mcp-docs.js +2 -1
- package/scripts/flow-memory-blocks.js +2 -1
- package/scripts/flow-memory-sync.js +1 -1
- package/scripts/flow-memory.js +767 -0
- package/scripts/flow-migrate-igr.js +1 -1
- package/scripts/flow-migrate.js +2 -1
- package/scripts/flow-model-adapter.js +1 -1
- package/scripts/flow-model-config.js +5 -1
- package/scripts/flow-model-profile.js +2 -1
- package/scripts/flow-orchestrate.js +3 -3
- package/scripts/flow-output.js +29 -0
- package/scripts/flow-parallel.js +10 -9
- package/scripts/flow-pattern-enforcer.js +2 -1
- package/scripts/flow-permissions-audit.js +124 -0
- package/scripts/flow-plugin-registry.js +2 -2
- package/scripts/flow-progress.js +5 -1
- package/scripts/flow-project-analyzer.js +1 -1
- package/scripts/flow-promote.js +510 -0
- package/scripts/flow-registries.js +86 -0
- package/scripts/flow-request-log.js +133 -0
- package/scripts/flow-research-protocol.js +0 -1
- package/scripts/flow-revision-tracker.js +2 -1
- package/scripts/flow-roadmap.js +2 -1
- package/scripts/flow-rules-sync.js +3 -7
- package/scripts/flow-session-end.js +3 -1
- package/scripts/flow-session-learning.js +6 -13
- package/scripts/flow-session-state.js +2 -2
- package/scripts/flow-setup-hooks.js +2 -1
- package/scripts/flow-skill-create.js +1 -1
- package/scripts/flow-skill-freshness.js +6 -7
- package/scripts/flow-skill-learn.js +1 -1
- package/scripts/flow-step-coverage.js +1 -1
- package/scripts/flow-step-security.js +1 -1
- package/scripts/flow-story.js +58 -10
- package/scripts/flow-sys.js +204 -0
- package/scripts/flow-task-hierarchy.js +88 -0
- package/scripts/flow-tech-debt.js +2 -1
- package/scripts/flow-test-api.js +1 -1
- package/scripts/flow-utils.js +60 -890
- package/scripts/hooks/core/bugfix-scope-gate.js +5 -4
- package/scripts/hooks/core/deploy-gate.js +1 -1
- package/scripts/hooks/core/pre-tool-helpers.js +72 -0
- package/scripts/hooks/core/pre-tool-orchestrator.js +442 -0
- package/scripts/hooks/core/routing-gate.js +8 -0
- package/scripts/hooks/core/session-end.js +28 -0
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +48 -492
- package/scripts/hooks/entry/shared/hook-runner.js +1 -1
- package/scripts/registries/schema-registry.js +1 -1
- 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 || {
|
package/scripts/flow-roadmap.js
CHANGED
|
@@ -141,13 +141,9 @@ function parseMarkdownSections(content) {
|
|
|
141
141
|
* @param {string} title - Section title
|
|
142
142
|
* @returns {string} - Slugified filename
|
|
143
143
|
*/
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
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 (
|
|
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: ${
|
|
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
|
-
//
|
|
19
|
-
const DANGEROUS_KEYS =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
package/scripts/flow-story.js
CHANGED
|
@@ -283,16 +283,10 @@ function analyzeForDecomposition(title) {
|
|
|
283
283
|
* @param {string} title - Title to slugify
|
|
284
284
|
* @returns {string} Slugified title
|
|
285
285
|
*/
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
+
};
|