wogiflow 2.29.1 → 2.29.3
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/docs/intent-grounded-reasoning.md +1 -1
- package/.workflow/templates/partials/methodology-rules.hbs +60 -0
- package/lib/commands/team-connection.js +5 -28
- package/lib/mode-schema.js +2 -1
- package/lib/utils.js +12 -26
- package/lib/wogi-claude +40 -1
- package/lib/workspace-messages.js +2 -1
- package/lib/workspace.js +7 -14
- package/package.json +2 -2
- package/scripts/flow +4 -0
- package/scripts/flow-autonomous-detector.js +29 -4
- package/scripts/flow-autonomous-mode.js +27 -7
- package/scripts/flow-completion-summary.js +2 -16
- package/scripts/flow-id.js +31 -0
- package/scripts/flow-io.js +78 -0
- package/scripts/flow-long-input-pending.js +110 -0
- package/scripts/flow-long-input-stories.js +8 -0
- package/scripts/flow-orchestrate-corrections.js +158 -0
- package/scripts/flow-orchestrate.js +22 -97
- package/scripts/flow-question-queue.js +73 -7
- package/scripts/flow-scanner-base.js +77 -1
- package/scripts/flow-session-state.js +47 -0
- package/scripts/flow-source-fidelity.js +279 -0
- package/scripts/flow-time-format.js +42 -0
- package/scripts/flow-utils.js +3 -16
- package/scripts/flow-worker-mcp-strip.js +12 -11
- package/scripts/flow-workspace-summary.js +38 -19
- package/scripts/hooks/adapters/claude-code.js +7 -4
- package/scripts/hooks/core/long-input-enforcement.js +311 -0
- package/scripts/hooks/core/pre-tool-deps.js +185 -0
- package/scripts/hooks/core/pre-tool-orchestrator.js +22 -0
- package/scripts/hooks/core/session-context.js +26 -0
- package/scripts/hooks/core/task-boundary-reset.js +13 -0
- package/scripts/hooks/core/worker-boundary-gate.js +67 -16
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +21 -95
- package/scripts/hooks/entry/claude-code/user-prompt-submit.js +33 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Wogi Flow — long-input-pending CLI
|
|
7
|
+
*
|
|
8
|
+
* Subcommands:
|
|
9
|
+
* status — show whether the marker is set + payload
|
|
10
|
+
* dismiss [--reason="<text>"] — clear the marker after the AI / user has
|
|
11
|
+
* decided this prompt does NOT create work
|
|
12
|
+
* (escape hatch for the P11.6 gate)
|
|
13
|
+
*
|
|
14
|
+
* The marker file is written by user-prompt-submit when a long-form prompt
|
|
15
|
+
* arrives without a source-link. The PreToolUse `checkLongInputPendingGate`
|
|
16
|
+
* (long-input-enforcement.js) consults it and blocks Edit/Write/Bash/Skill
|
|
17
|
+
* (with a small allow-list) until either /wogi-extract-review runs or this
|
|
18
|
+
* dispatcher's `dismiss` command clears it.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require('node:fs');
|
|
22
|
+
const path = require('node:path');
|
|
23
|
+
const { PATHS } = require('./flow-utils');
|
|
24
|
+
const {
|
|
25
|
+
isLongInputPending,
|
|
26
|
+
readLongInputPending,
|
|
27
|
+
clearLongInputPending,
|
|
28
|
+
PENDING_PATH
|
|
29
|
+
} = require('./hooks/core/long-input-enforcement');
|
|
30
|
+
|
|
31
|
+
const DISMISS_LOG = path.join(PATHS.state, 'long-input-pending-dismiss.log');
|
|
32
|
+
|
|
33
|
+
function showStatus() {
|
|
34
|
+
if (!isLongInputPending()) {
|
|
35
|
+
process.stdout.write('long-input-pending: not set\n');
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
const payload = readLongInputPending() || {};
|
|
39
|
+
process.stdout.write('long-input-pending: SET\n');
|
|
40
|
+
process.stdout.write(` marker: ${PENDING_PATH}\n`);
|
|
41
|
+
process.stdout.write(` level: ${payload.level || 'unknown'}\n`);
|
|
42
|
+
process.stdout.write(` reason: ${payload.reason || 'unknown'}\n`);
|
|
43
|
+
process.stdout.write(` marked: ${payload.markedAt || 'unknown'}\n`);
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function dismiss(args) {
|
|
48
|
+
if (!isLongInputPending()) {
|
|
49
|
+
process.stdout.write('long-input-pending: nothing to dismiss (marker not set)\n');
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
const reasonArg = args.find(a => a.startsWith('--reason='));
|
|
53
|
+
const reason = reasonArg ? reasonArg.slice('--reason='.length).replace(/^['"]|['"]$/g, '').trim() : '';
|
|
54
|
+
if (!reason) {
|
|
55
|
+
process.stderr.write([
|
|
56
|
+
'Usage: flow long-input-pending dismiss --reason="<concrete reason>"',
|
|
57
|
+
'',
|
|
58
|
+
'A reason is required so the dismissal is auditable. Examples:',
|
|
59
|
+
' --reason="log dump, no work created"',
|
|
60
|
+
' --reason="verbatim error trace, already linked to wf-12345678"',
|
|
61
|
+
' --reason="conversational question, not work-creating"'
|
|
62
|
+
].join('\n') + '\n');
|
|
63
|
+
return 1;
|
|
64
|
+
}
|
|
65
|
+
const payload = readLongInputPending() || {};
|
|
66
|
+
clearLongInputPending();
|
|
67
|
+
try {
|
|
68
|
+
fs.mkdirSync(path.dirname(DISMISS_LOG), { recursive: true });
|
|
69
|
+
fs.appendFileSync(DISMISS_LOG, JSON.stringify({
|
|
70
|
+
dismissedAt: new Date().toISOString(),
|
|
71
|
+
reason,
|
|
72
|
+
markerPayload: payload
|
|
73
|
+
}) + '\n');
|
|
74
|
+
} catch (_err) { /* best-effort log */ }
|
|
75
|
+
process.stdout.write('long-input-pending: dismissed\n');
|
|
76
|
+
process.stdout.write(` reason: ${reason}\n`);
|
|
77
|
+
process.stdout.write(` logged: ${DISMISS_LOG}\n`);
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function showHelp() {
|
|
82
|
+
process.stdout.write([
|
|
83
|
+
'Usage: flow long-input-pending <subcommand> [options]',
|
|
84
|
+
'',
|
|
85
|
+
'Subcommands:',
|
|
86
|
+
' status Show whether the P11.6 long-input-pending marker is set',
|
|
87
|
+
' dismiss --reason="<text>" Clear the marker after deciding the prompt does',
|
|
88
|
+
' NOT create work. A reason is required and is',
|
|
89
|
+
' appended to .workflow/state/long-input-pending-dismiss.log',
|
|
90
|
+
' for telemetry/learning.',
|
|
91
|
+
' help Show this help',
|
|
92
|
+
''
|
|
93
|
+
].join('\n'));
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const [, , sub, ...rest] = process.argv;
|
|
98
|
+
let exitCode = 0;
|
|
99
|
+
switch (sub) {
|
|
100
|
+
case 'status': exitCode = showStatus(); break;
|
|
101
|
+
case 'dismiss': exitCode = dismiss(rest); break;
|
|
102
|
+
case 'help':
|
|
103
|
+
case '--help':
|
|
104
|
+
case '-h':
|
|
105
|
+
case undefined: exitCode = showHelp(); break;
|
|
106
|
+
default:
|
|
107
|
+
process.stderr.write(`Unknown subcommand: ${sub}\nRun 'flow long-input-pending help' for usage.\n`);
|
|
108
|
+
exitCode = 2;
|
|
109
|
+
}
|
|
110
|
+
process.exit(exitCode);
|
|
@@ -2099,6 +2099,14 @@ async function finalizeDigestion(options = {}) {
|
|
|
2099
2099
|
cleanupResult = cleanupTempFiles(activeDigest.session.digest_id);
|
|
2100
2100
|
}
|
|
2101
2101
|
|
|
2102
|
+
// 7. Clear the P11.6 long-input-pending marker — extraction is the
|
|
2103
|
+
// documented "way out" of the gate, so finalize-success means the
|
|
2104
|
+
// forcing reason is now resolved. Best-effort: a missing marker is
|
|
2105
|
+
// expected when finalize was triggered without the gate firing.
|
|
2106
|
+
try {
|
|
2107
|
+
require('./hooks/core/long-input-enforcement').clearLongInputPending();
|
|
2108
|
+
} catch (_err) { /* best-effort */ }
|
|
2109
|
+
|
|
2102
2110
|
return {
|
|
2103
2111
|
success: true,
|
|
2104
2112
|
approved_count: exportResult.summary.total_approved,
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow — Auto-Correction Helpers (Story 14 / wf-d0937c83)
|
|
5
|
+
*
|
|
6
|
+
* Splits flow-orchestrate's autoCorrectCode into focused helpers:
|
|
7
|
+
*
|
|
8
|
+
* - fixForbiddenImports (section 1: doNotImport, default/combined/namespace forms)
|
|
9
|
+
* - fixComponentPaths (section 2: shadcn @/components/ui/X mapping)
|
|
10
|
+
* - fixFeatureTypePaths (section 3: ../types in /features/ files)
|
|
11
|
+
* - fixNoExternalUtils (section 4: @/lib/utils removal + formatCurrency inline + cn() unwrap)
|
|
12
|
+
* - normalizeQuotes (section 5: double-quote → single-quote when single dominates)
|
|
13
|
+
* - cleanupEmptyImports (section 6: empty `import {} from "..."` cleanup)
|
|
14
|
+
* - collapseBlankLines (section 7: 3+ blanks → 2)
|
|
15
|
+
*
|
|
16
|
+
* Each helper takes a `(code, ...args)` shape and returns
|
|
17
|
+
* `{ corrected, corrections }`. The orchestrator (flow-orchestrate.js
|
|
18
|
+
* `autoCorrectCode`) chains them in section order.
|
|
19
|
+
*
|
|
20
|
+
* Behavior is preserved verbatim from the pre-extraction implementation;
|
|
21
|
+
* pinned by characterization tests in
|
|
22
|
+
* `tests/flow-orchestrate-corrections.test.js` (Tier-3 integration test
|
|
23
|
+
* per the Cross-Story Integration Tier-3 Rule — feeds real input through
|
|
24
|
+
* the public `autoCorrectCode` API and asserts the output).
|
|
25
|
+
*
|
|
26
|
+
* Programmatic:
|
|
27
|
+
* const c = require('./flow-orchestrate-corrections');
|
|
28
|
+
* const { corrected, corrections } = c.fixForbiddenImports(code, ['React']);
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
'use strict';
|
|
32
|
+
|
|
33
|
+
function fixForbiddenImports(code, doNotImport) {
|
|
34
|
+
let corrected = code;
|
|
35
|
+
const corrections = [];
|
|
36
|
+
const list = Array.isArray(doNotImport) && doNotImport.length ? doNotImport : ['React'];
|
|
37
|
+
for (const forbidden of list) {
|
|
38
|
+
const defaultImportRegex = new RegExp(`^import ${forbidden} from ['"][^'"]+['"];?\\s*\\n?`, 'gm');
|
|
39
|
+
if (defaultImportRegex.test(corrected)) {
|
|
40
|
+
corrected = corrected.replace(defaultImportRegex, '');
|
|
41
|
+
corrections.push(`Removed forbidden import: ${forbidden}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const combinedImportRegex = new RegExp(`^import ${forbidden},\\s*(\\{[^}]+\\})\\s+from\\s+(['"][^'"]+['"])`, 'gm');
|
|
45
|
+
if (combinedImportRegex.test(corrected)) {
|
|
46
|
+
corrected = corrected.replace(combinedImportRegex, 'import $1 from $2');
|
|
47
|
+
corrections.push(`Removed ${forbidden} from combined import`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const namespaceImportRegex = new RegExp(`^import \\* as ${forbidden} from ['"][^'"]+['"];?\\s*\\n?`, 'gm');
|
|
51
|
+
if (namespaceImportRegex.test(corrected)) {
|
|
52
|
+
corrected = corrected.replace(namespaceImportRegex, '');
|
|
53
|
+
corrections.push(`Removed namespace import: ${forbidden}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { corrected, corrections };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function fixComponentPaths(code, componentPaths) {
|
|
60
|
+
let corrected = code;
|
|
61
|
+
const corrections = [];
|
|
62
|
+
const map = (componentPaths && typeof componentPaths === 'object') ? componentPaths : {};
|
|
63
|
+
const shadcnPattern = /@\/components\/ui\/(\w+)/g;
|
|
64
|
+
corrected = corrected.replace(shadcnPattern, (match, component) => {
|
|
65
|
+
const capitalName = component.charAt(0).toUpperCase() + component.slice(1);
|
|
66
|
+
const configPath = map[capitalName];
|
|
67
|
+
if (configPath) {
|
|
68
|
+
corrections.push(`Fixed import: ${match} → ${configPath}`);
|
|
69
|
+
return configPath;
|
|
70
|
+
}
|
|
71
|
+
return match;
|
|
72
|
+
});
|
|
73
|
+
return { corrected, corrections };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function fixFeatureTypePaths(code, filePath, typePaths) {
|
|
77
|
+
let corrected = code;
|
|
78
|
+
const corrections = [];
|
|
79
|
+
const paths = (typePaths && typeof typePaths === 'object') ? typePaths : { features: '../api/types' };
|
|
80
|
+
if (filePath && filePath.includes('/features/') && paths.features) {
|
|
81
|
+
const wrongPaths = ["'../types'", '"../types"', "'./types'", '"./types"'];
|
|
82
|
+
for (const wrong of wrongPaths) {
|
|
83
|
+
if (corrected.includes(wrong)) {
|
|
84
|
+
corrected = corrected.replace(new RegExp(wrong.replace(/['"]/g, '[\'"]'), 'g'), `'${paths.features}'`);
|
|
85
|
+
corrections.push('Fixed type import path');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return { corrected, corrections };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function fixNoExternalUtils(code, ctx) {
|
|
93
|
+
let corrected = code;
|
|
94
|
+
const corrections = [];
|
|
95
|
+
if (!(ctx && ctx.noExternalUtils && corrected.includes('@/lib/utils'))) {
|
|
96
|
+
return { corrected, corrections };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const hadFormatCurrency = corrected.includes('formatCurrency');
|
|
100
|
+
const hadCn = corrected.includes(' cn(') || corrected.includes(' cn`');
|
|
101
|
+
|
|
102
|
+
corrected = corrected.replace(/^import.*from ['"]@\/lib\/utils['"];?\s*\n?/gm, '');
|
|
103
|
+
corrections.push('Removed @/lib/utils import');
|
|
104
|
+
|
|
105
|
+
if (hadFormatCurrency) {
|
|
106
|
+
const formatCurrencyFn = `\nconst formatCurrency = (amount: number) =>\n new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount);\n`;
|
|
107
|
+
const lastImportMatch = corrected.match(/^import[^;]+;?\s*\n/gm);
|
|
108
|
+
if (lastImportMatch) {
|
|
109
|
+
const lastImport = lastImportMatch[lastImportMatch.length - 1];
|
|
110
|
+
const insertPos = corrected.lastIndexOf(lastImport) + lastImport.length;
|
|
111
|
+
corrected = corrected.slice(0, insertPos) + formatCurrencyFn + corrected.slice(insertPos);
|
|
112
|
+
}
|
|
113
|
+
corrections.push('Inlined formatCurrency');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (hadCn) {
|
|
117
|
+
corrected = corrected.replace(/cn\((['"`][^'"`]+['"`])\)/g, '$1');
|
|
118
|
+
corrections.push('Removed cn() wrapper');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { corrected, corrections };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function normalizeQuotes(code) {
|
|
125
|
+
let corrected = code;
|
|
126
|
+
const corrections = [];
|
|
127
|
+
const singleQuoteCount = (corrected.match(/from '/g) || []).length;
|
|
128
|
+
const doubleQuoteCount = (corrected.match(/from "/g) || []).length;
|
|
129
|
+
if (singleQuoteCount > doubleQuoteCount && doubleQuoteCount > 0) {
|
|
130
|
+
corrected = corrected.replace(/from "([^"]+)"/g, "from '$1'");
|
|
131
|
+
corrections.push('Normalized import quotes to single quotes');
|
|
132
|
+
}
|
|
133
|
+
return { corrected, corrections };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function cleanupEmptyImports(code) {
|
|
137
|
+
return {
|
|
138
|
+
corrected: code.replace(/^import\s*\{\s*\}\s*from\s*['"][^'"]+['"];?\s*\n?/gm, ''),
|
|
139
|
+
corrections: []
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function collapseBlankLines(code) {
|
|
144
|
+
return {
|
|
145
|
+
corrected: code.replace(/\n{3,}/g, '\n\n'),
|
|
146
|
+
corrections: []
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = {
|
|
151
|
+
fixForbiddenImports,
|
|
152
|
+
fixComponentPaths,
|
|
153
|
+
fixFeatureTypePaths,
|
|
154
|
+
fixNoExternalUtils,
|
|
155
|
+
normalizeQuotes,
|
|
156
|
+
cleanupEmptyImports,
|
|
157
|
+
collapseBlankLines
|
|
158
|
+
};
|
|
@@ -307,109 +307,34 @@ function autoCorrectCode(code, filePath, projectConfig = null) {
|
|
|
307
307
|
return { corrected: code, corrections: [] };
|
|
308
308
|
}
|
|
309
309
|
|
|
310
|
-
// Load project context from config if not provided
|
|
311
310
|
const ctx = projectConfig?.projectContext ?? getProjectContext();
|
|
312
|
-
|
|
313
|
-
let corrected = code;
|
|
314
311
|
const corrections = [];
|
|
315
312
|
|
|
316
|
-
//
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
// Case A: Default import - "import X from '...'"
|
|
320
|
-
const defaultImportRegex = new RegExp(`^import ${forbidden} from ['"][^'"]+['"];?\\s*\\n?`, 'gm');
|
|
321
|
-
if (defaultImportRegex.test(corrected)) {
|
|
322
|
-
corrected = corrected.replace(defaultImportRegex, '');
|
|
323
|
-
corrections.push(`Removed forbidden import: ${forbidden}`);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Case B: Combined with named imports - "import X, { y, z } from '...'"
|
|
327
|
-
const combinedImportRegex = new RegExp(`^import ${forbidden},\\s*(\\{[^}]+\\})\\s+from\\s+(['"][^'"]+['"])`, 'gm');
|
|
328
|
-
if (combinedImportRegex.test(corrected)) {
|
|
329
|
-
corrected = corrected.replace(combinedImportRegex, 'import $1 from $2');
|
|
330
|
-
corrections.push(`Removed ${forbidden} from combined import`);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Case C: Namespace import - "import * as X from '...'"
|
|
334
|
-
const namespaceImportRegex = new RegExp(`^import \\* as ${forbidden} from ['"][^'"]+['"];?\\s*\\n?`, 'gm');
|
|
335
|
-
if (namespaceImportRegex.test(corrected)) {
|
|
336
|
-
corrected = corrected.replace(namespaceImportRegex, '');
|
|
337
|
-
corrections.push(`Removed namespace import: ${forbidden}`);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// 2. Fix component paths based on config mappings
|
|
342
|
-
const componentPaths = ctx.componentPaths ?? {};
|
|
343
|
-
|
|
344
|
-
// Build reverse mapping from shadcn-style to project paths
|
|
345
|
-
// @/components/ui/button → project's Button path
|
|
346
|
-
const shadcnPattern = /@\/components\/ui\/(\w+)/g;
|
|
347
|
-
corrected = corrected.replace(shadcnPattern, (match, component) => {
|
|
348
|
-
const capitalName = component.charAt(0).toUpperCase() + component.slice(1);
|
|
349
|
-
const configPath = componentPaths[capitalName];
|
|
350
|
-
if (configPath) {
|
|
351
|
-
corrections.push(`Fixed import: ${match} → ${configPath}`);
|
|
352
|
-
return configPath;
|
|
353
|
-
}
|
|
354
|
-
return match; // Leave as-is if no mapping
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
// 3. Fix type paths for features (from config)
|
|
358
|
-
const typePaths = ctx.typePaths || { features: '../api/types' };
|
|
359
|
-
if (filePath && filePath.includes('/features/') && typePaths.features) {
|
|
360
|
-
const wrongPaths = ["'../types'", '"../types"', "'./types'", '"./types"'];
|
|
361
|
-
for (const wrong of wrongPaths) {
|
|
362
|
-
if (corrected.includes(wrong)) {
|
|
363
|
-
corrected = corrected.replace(new RegExp(wrong.replace(/['"]/g, '[\'"]'), 'g'), `'${typePaths.features}'`);
|
|
364
|
-
corrections.push('Fixed type import path');
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// 4. Remove external utils if configured (noExternalUtils: true)
|
|
370
|
-
if (ctx.noExternalUtils && corrected.includes('@/lib/utils')) {
|
|
371
|
-
const hadFormatCurrency = corrected.includes('formatCurrency');
|
|
372
|
-
const hadCn = corrected.includes(' cn(') || corrected.includes(' cn`');
|
|
373
|
-
|
|
374
|
-
// Remove the import
|
|
375
|
-
corrected = corrected.replace(/^import.*from ['"]@\/lib\/utils['"];?\s*\n?/gm, '');
|
|
376
|
-
corrections.push('Removed @/lib/utils import');
|
|
377
|
-
|
|
378
|
-
// Inline formatCurrency if it was used
|
|
379
|
-
if (hadFormatCurrency) {
|
|
380
|
-
const formatCurrencyFn = `\nconst formatCurrency = (amount: number) =>\n new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount);\n`;
|
|
381
|
-
// Insert after imports
|
|
382
|
-
const lastImportMatch = corrected.match(/^import[^;]+;?\s*\n/gm);
|
|
383
|
-
if (lastImportMatch) {
|
|
384
|
-
const lastImport = lastImportMatch[lastImportMatch.length - 1];
|
|
385
|
-
const insertPos = corrected.lastIndexOf(lastImport) + lastImport.length;
|
|
386
|
-
corrected = corrected.slice(0, insertPos) + formatCurrencyFn + corrected.slice(insertPos);
|
|
387
|
-
}
|
|
388
|
-
corrections.push('Inlined formatCurrency');
|
|
389
|
-
}
|
|
313
|
+
// Lazy-load the helpers — avoids module-load-order issues with the
|
|
314
|
+
// legacy CLI bootstrap at the bottom of this file.
|
|
315
|
+
const c = require('./flow-orchestrate-corrections');
|
|
390
316
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
317
|
+
// CL-007 fix (2026-04-26): non-closure data-driven form. The previous
|
|
318
|
+
// array-of-closure form worked correctly under sequential iteration but
|
|
319
|
+
// would silently break under any future parallel/lazy execution because
|
|
320
|
+
// each closure captured `corrected` by reference. Using a plain
|
|
321
|
+
// step-name + args list is equally readable and closure-trap-free.
|
|
322
|
+
let corrected = code;
|
|
323
|
+
const steps = [
|
|
324
|
+
['fixForbiddenImports', [ctx.doNotImport]],
|
|
325
|
+
['fixComponentPaths', [ctx.componentPaths]],
|
|
326
|
+
['fixFeatureTypePaths', [filePath, ctx.typePaths]],
|
|
327
|
+
['fixNoExternalUtils', [ctx]],
|
|
328
|
+
['normalizeQuotes', []],
|
|
329
|
+
['cleanupEmptyImports', []],
|
|
330
|
+
['collapseBlankLines', []]
|
|
331
|
+
];
|
|
332
|
+
for (const [fn, args] of steps) {
|
|
333
|
+
const r = c[fn](corrected, ...args);
|
|
334
|
+
corrected = r.corrected;
|
|
335
|
+
if (r.corrections.length) corrections.push(...r.corrections);
|
|
404
336
|
}
|
|
405
337
|
|
|
406
|
-
// 6. Remove empty import statements (artifact of removing imports)
|
|
407
|
-
corrected = corrected.replace(/^import\s*\{\s*\}\s*from\s*['"][^'"]+['"];?\s*\n?/gm, '');
|
|
408
|
-
|
|
409
|
-
// 7. Fix multiple consecutive blank lines (cleanup)
|
|
410
|
-
corrected = corrected.replace(/\n{3,}/g, '\n\n');
|
|
411
|
-
|
|
412
|
-
// Log corrections if any
|
|
413
338
|
if (corrections.length > 0 && typeof log === 'function') {
|
|
414
339
|
log('dim', ` 🔧 Auto-corrected: ${corrections.join(', ')}`);
|
|
415
340
|
}
|
|
@@ -26,10 +26,17 @@
|
|
|
26
26
|
const path = require('node:path');
|
|
27
27
|
const fs = require('node:fs');
|
|
28
28
|
const { PATHS } = require('./flow-paths');
|
|
29
|
-
const { readJson, writeJson } = require('./flow-io');
|
|
29
|
+
const { readJson, writeJson, withLock } = require('./flow-io');
|
|
30
30
|
|
|
31
31
|
const QUEUE_PATH = path.join(PATHS.state, 'question-queue.json');
|
|
32
32
|
|
|
33
|
+
// SEC-004 caps (2026-04-26): bound disk consumption for buggy classifier
|
|
34
|
+
// over-flag and prompt-injection that intentionally generates many questions.
|
|
35
|
+
// Overflow rotates the current queue to an archive file, never silently drops.
|
|
36
|
+
const MAX_QUESTIONS_PER_FILE = 100;
|
|
37
|
+
const MAX_QUESTION_TEXT_BYTES = 4 * 1024; // 4 KB per question text
|
|
38
|
+
const MAX_QUEUE_FILE_BYTES = 1 * 1024 * 1024; // 1 MB total queue file
|
|
39
|
+
|
|
33
40
|
function emptyQueue() {
|
|
34
41
|
return { questions: [], skippedTasks: [] };
|
|
35
42
|
}
|
|
@@ -52,6 +59,29 @@ function saveQueue(data) {
|
|
|
52
59
|
return data;
|
|
53
60
|
}
|
|
54
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Rotate the current queue to a timestamped archive file, then return an
|
|
64
|
+
* empty queue. Used when overflow caps are hit (SEC-004).
|
|
65
|
+
*/
|
|
66
|
+
function rotateQueue() {
|
|
67
|
+
try {
|
|
68
|
+
if (!fs.existsSync(QUEUE_PATH)) return emptyQueue();
|
|
69
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
70
|
+
const archivePath = path.join(PATHS.state, `question-queue-archive-${ts}.json`);
|
|
71
|
+
fs.renameSync(QUEUE_PATH, archivePath);
|
|
72
|
+
} catch (_err) { /* best-effort archive */ }
|
|
73
|
+
return emptyQueue();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function truncateText(text) {
|
|
77
|
+
if (typeof text !== 'string') return '';
|
|
78
|
+
const buf = Buffer.from(text, 'utf-8');
|
|
79
|
+
if (buf.byteLength <= MAX_QUESTION_TEXT_BYTES) return text;
|
|
80
|
+
// Truncate by bytes, then drop last char to avoid mid-codepoint cuts.
|
|
81
|
+
const truncated = buf.slice(0, MAX_QUESTION_TEXT_BYTES - 1).toString('utf-8');
|
|
82
|
+
return truncated.replace(/.$/, '') + '… [truncated]';
|
|
83
|
+
}
|
|
84
|
+
|
|
55
85
|
function clearQueue() {
|
|
56
86
|
try {
|
|
57
87
|
if (fs.existsSync(QUEUE_PATH)) fs.unlinkSync(QUEUE_PATH);
|
|
@@ -74,10 +104,18 @@ function shortId() {
|
|
|
74
104
|
*/
|
|
75
105
|
function addQuestion(q) {
|
|
76
106
|
if (!q || !q.text) throw new Error('addQuestion: text is required');
|
|
77
|
-
|
|
107
|
+
// SEC-004: cap text size, count, file size with archive rotation on overflow
|
|
108
|
+
const text = truncateText(String(q.text));
|
|
109
|
+
let queue = loadQueue();
|
|
110
|
+
|
|
111
|
+
// Count cap — rotate before append if at limit
|
|
112
|
+
if (queue.questions.length >= MAX_QUESTIONS_PER_FILE) {
|
|
113
|
+
queue = rotateQueue();
|
|
114
|
+
}
|
|
115
|
+
|
|
78
116
|
const entry = {
|
|
79
117
|
id: `q-${shortId()}`,
|
|
80
|
-
text
|
|
118
|
+
text,
|
|
81
119
|
classifiedBucket: q.classifiedBucket || null,
|
|
82
120
|
taskContext: q.taskContext || null,
|
|
83
121
|
dependencies: Array.isArray(q.dependencies) ? q.dependencies : [],
|
|
@@ -86,10 +124,27 @@ function addQuestion(q) {
|
|
|
86
124
|
answered: false
|
|
87
125
|
};
|
|
88
126
|
queue.questions.push(entry);
|
|
127
|
+
|
|
128
|
+
// File-size cap (defense-in-depth — covers degenerate per-question payloads).
|
|
129
|
+
const candidate = JSON.stringify(queue);
|
|
130
|
+
if (Buffer.byteLength(candidate, 'utf-8') > MAX_QUEUE_FILE_BYTES) {
|
|
131
|
+
queue = rotateQueue();
|
|
132
|
+
queue.questions.push(entry);
|
|
133
|
+
}
|
|
134
|
+
|
|
89
135
|
saveQueue(queue);
|
|
90
136
|
return entry;
|
|
91
137
|
}
|
|
92
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Async variant of addQuestion that holds an inter-process lock around the
|
|
141
|
+
* read-modify-write cycle (CL-002 fix). Prefer this in concurrent contexts
|
|
142
|
+
* (autonomous worker + manager dispatch handler running in parallel).
|
|
143
|
+
*/
|
|
144
|
+
async function addQuestionAsync(q) {
|
|
145
|
+
return withLock(QUEUE_PATH, async () => addQuestion(q));
|
|
146
|
+
}
|
|
147
|
+
|
|
93
148
|
/**
|
|
94
149
|
* Mark a task as skipped, recording the reason and (optionally) the question
|
|
95
150
|
* blocking it.
|
|
@@ -113,12 +168,17 @@ function skipTask({ taskId, reason, blockingQuestionId } = {}) {
|
|
|
113
168
|
return record;
|
|
114
169
|
}
|
|
115
170
|
|
|
171
|
+
async function skipTaskAsync(args) {
|
|
172
|
+
return withLock(QUEUE_PATH, async () => skipTask(args));
|
|
173
|
+
}
|
|
174
|
+
|
|
116
175
|
/**
|
|
117
176
|
* Conservative dependency classifier — text-match only.
|
|
118
|
-
* AI classifier
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
177
|
+
* AI classifier integration is handled by `classifyDependenciesSafe(text,
|
|
178
|
+
* tasks, aiClassifier)` below — callers pass an injected classifier
|
|
179
|
+
* function. This keeps the hot path classifier-free for tests and for
|
|
180
|
+
* environments without Anthropic credentials. (CL-008 fix 2026-04-26 —
|
|
181
|
+
* removed misleading reference to a non-existent classifyDependenciesWithAi.)
|
|
122
182
|
*
|
|
123
183
|
* Rules:
|
|
124
184
|
* 1. Exact task ID match (wf-XXXXXXXX) → flag dependency.
|
|
@@ -216,12 +276,18 @@ function listSkippedTasks() {
|
|
|
216
276
|
|
|
217
277
|
module.exports = {
|
|
218
278
|
QUEUE_PATH,
|
|
279
|
+
MAX_QUESTIONS_PER_FILE,
|
|
280
|
+
MAX_QUESTION_TEXT_BYTES,
|
|
281
|
+
MAX_QUEUE_FILE_BYTES,
|
|
219
282
|
emptyQueue,
|
|
220
283
|
loadQueue,
|
|
221
284
|
saveQueue,
|
|
285
|
+
rotateQueue,
|
|
222
286
|
clearQueue,
|
|
223
287
|
addQuestion,
|
|
288
|
+
addQuestionAsync,
|
|
224
289
|
skipTask,
|
|
290
|
+
skipTaskAsync,
|
|
225
291
|
classifyDependencies,
|
|
226
292
|
classifyDependenciesSafe,
|
|
227
293
|
unionDependencies,
|
|
@@ -387,9 +387,63 @@ class BaseScanner {
|
|
|
387
387
|
}
|
|
388
388
|
});
|
|
389
389
|
|
|
390
|
-
// Pass 2: Collect all exported names
|
|
390
|
+
// Pass 2: Collect all exported names (handles BOTH ESM `export` and
|
|
391
|
+
// CommonJS `module.exports`/`exports` patterns).
|
|
392
|
+
// arch-005 fix (2026-04-26): the original Babel scanner only handled
|
|
393
|
+
// ESM. wogi-flow's source is CommonJS, so all exports were invisible
|
|
394
|
+
// and function-map.md was empty after every scan.
|
|
391
395
|
const exported = new Map();
|
|
392
396
|
|
|
397
|
+
// CJS helper: handle `module.exports = { ... }` and
|
|
398
|
+
// `module.exports.x = ...` and `exports.x = ...`.
|
|
399
|
+
const handleCjsExportAssignment = (assignNode) => {
|
|
400
|
+
const left = assignNode.left;
|
|
401
|
+
const right = assignNode.right;
|
|
402
|
+
if (!left || !right) return;
|
|
403
|
+
|
|
404
|
+
// Pattern: module.exports = { foo, bar, baz }
|
|
405
|
+
// or: exports = { foo, bar, baz } (rare)
|
|
406
|
+
const isModuleExports =
|
|
407
|
+
left.type === 'MemberExpression' &&
|
|
408
|
+
left.object?.name === 'module' &&
|
|
409
|
+
left.property?.name === 'exports' &&
|
|
410
|
+
!left.computed;
|
|
411
|
+
const isBareExports =
|
|
412
|
+
left.type === 'Identifier' && left.name === 'exports';
|
|
413
|
+
|
|
414
|
+
if ((isModuleExports || isBareExports) && right.type === 'ObjectExpression') {
|
|
415
|
+
for (const prop of right.properties) {
|
|
416
|
+
if (prop.type === 'ObjectProperty' || prop.type === 'Property') {
|
|
417
|
+
// Shorthand: { foo } → key = foo (identifier), value = foo
|
|
418
|
+
// Long form: { foo: bar } → key = foo, value = bar (identifier)
|
|
419
|
+
const keyName = prop.key?.name || prop.key?.value;
|
|
420
|
+
if (typeof keyName === 'string' && keyName) {
|
|
421
|
+
exported.set(keyName, { isDefault: false });
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Pattern: module.exports.foo = bar
|
|
429
|
+
// exports.foo = bar
|
|
430
|
+
// (left is MemberExpression where the property is the export name)
|
|
431
|
+
const isModuleExportsDotX =
|
|
432
|
+
left.type === 'MemberExpression' &&
|
|
433
|
+
left.object?.type === 'MemberExpression' &&
|
|
434
|
+
left.object.object?.name === 'module' &&
|
|
435
|
+
left.object.property?.name === 'exports';
|
|
436
|
+
const isExportsDotX =
|
|
437
|
+
left.type === 'MemberExpression' &&
|
|
438
|
+
left.object?.name === 'exports';
|
|
439
|
+
if (isModuleExportsDotX || isExportsDotX) {
|
|
440
|
+
const name = left.property?.name;
|
|
441
|
+
if (typeof name === 'string' && name) {
|
|
442
|
+
exported.set(name, { isDefault: false });
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
|
|
393
447
|
this.traverse(ast, {
|
|
394
448
|
ExportNamedDeclaration: (nodePath) => {
|
|
395
449
|
const decl = nodePath.node.declaration;
|
|
@@ -415,6 +469,10 @@ class BaseScanner {
|
|
|
415
469
|
} else if (decl.type === 'Identifier') {
|
|
416
470
|
exported.set(decl.name, { isDefault: true });
|
|
417
471
|
}
|
|
472
|
+
},
|
|
473
|
+
// CommonJS exports — `module.exports = { ... }`, `exports.x = ...`
|
|
474
|
+
AssignmentExpression: (nodePath) => {
|
|
475
|
+
handleCjsExportAssignment(nodePath.node);
|
|
418
476
|
}
|
|
419
477
|
});
|
|
420
478
|
|
|
@@ -456,6 +514,24 @@ class BaseScanner {
|
|
|
456
514
|
}
|
|
457
515
|
}
|
|
458
516
|
|
|
517
|
+
// CommonJS: module.exports = { foo, bar, baz }
|
|
518
|
+
// arch-005 fix (2026-04-26): regex fallback was ESM-only, missing all
|
|
519
|
+
// CJS exports. Same gap as the Babel scanner had.
|
|
520
|
+
const cjsObjectExportRegex = /module\.exports\s*=\s*\{([^}]+)\}/g;
|
|
521
|
+
while ((match = cjsObjectExportRegex.exec(content)) !== null) {
|
|
522
|
+
const specifiers = match[1].split(',');
|
|
523
|
+
for (const spec of specifiers) {
|
|
524
|
+
const name = spec.trim().split(/[:\s]/)[0].trim();
|
|
525
|
+
if (name && /^[a-zA-Z_$][\w$]*$/.test(name)) exported.add(name);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// CommonJS: module.exports.foo = ... or exports.foo = ...
|
|
530
|
+
const cjsDotExportRegex = /(?:module\.exports|^exports)\s*\.\s*([a-zA-Z_$][\w$]*)\s*=/gm;
|
|
531
|
+
while ((match = cjsDotExportRegex.exec(content)) !== null) {
|
|
532
|
+
exported.add(match[1]);
|
|
533
|
+
}
|
|
534
|
+
|
|
459
535
|
return exported;
|
|
460
536
|
}
|
|
461
537
|
|