@zigrivers/scaffold 3.13.0 → 3.14.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/README.md +1 -1
- package/dist/cli/commands/adopt.d.ts.map +1 -1
- package/dist/cli/commands/adopt.js +8 -7
- package/dist/cli/commands/adopt.js.map +1 -1
- package/dist/cli/commands/adopt.serialization.test.js +8 -0
- package/dist/cli/commands/adopt.serialization.test.js.map +1 -1
- package/dist/cli/commands/adopt.test.js +8 -0
- package/dist/cli/commands/adopt.test.js.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +191 -180
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/complete.d.ts.map +1 -1
- package/dist/cli/commands/complete.js +16 -12
- package/dist/cli/commands/complete.js.map +1 -1
- package/dist/cli/commands/complete.test.js +14 -5
- package/dist/cli/commands/complete.test.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +43 -49
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/init.test.js +33 -27
- package/dist/cli/commands/init.test.js.map +1 -1
- package/dist/cli/commands/reset.d.ts.map +1 -1
- package/dist/cli/commands/reset.js +44 -40
- package/dist/cli/commands/reset.js.map +1 -1
- package/dist/cli/commands/reset.test.js +42 -20
- package/dist/cli/commands/reset.test.js.map +1 -1
- package/dist/cli/commands/rework.d.ts.map +1 -1
- package/dist/cli/commands/rework.js +16 -12
- package/dist/cli/commands/rework.js.map +1 -1
- package/dist/cli/commands/rework.test.js +12 -3
- package/dist/cli/commands/rework.test.js.map +1 -1
- package/dist/cli/commands/run.d.ts.map +1 -1
- package/dist/cli/commands/run.js +318 -298
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/commands/run.test.js +92 -120
- package/dist/cli/commands/run.test.js.map +1 -1
- package/dist/cli/commands/skip.d.ts.map +1 -1
- package/dist/cli/commands/skip.js +19 -15
- package/dist/cli/commands/skip.js.map +1 -1
- package/dist/cli/commands/skip.test.js +22 -11
- package/dist/cli/commands/skip.test.js.map +1 -1
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +3 -1
- package/dist/cli/commands/update.js.map +1 -1
- package/dist/cli/commands/update.test.js +8 -4
- package/dist/cli/commands/update.test.js.map +1 -1
- package/dist/cli/commands/version.d.ts.map +1 -1
- package/dist/cli/commands/version.js +3 -1
- package/dist/cli/commands/version.js.map +1 -1
- package/dist/cli/commands/version.test.js +9 -5
- package/dist/cli/commands/version.test.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/output/interactive.d.ts +1 -0
- package/dist/cli/output/interactive.d.ts.map +1 -1
- package/dist/cli/output/interactive.js +5 -0
- package/dist/cli/output/interactive.js.map +1 -1
- package/dist/cli/shutdown.d.ts +51 -0
- package/dist/cli/shutdown.d.ts.map +1 -0
- package/dist/cli/shutdown.js +199 -0
- package/dist/cli/shutdown.js.map +1 -0
- package/dist/cli/shutdown.test.d.ts +2 -0
- package/dist/cli/shutdown.test.d.ts.map +1 -0
- package/dist/cli/shutdown.test.js +316 -0
- package/dist/cli/shutdown.test.js.map +1 -0
- package/dist/e2e/init.test.js +5 -4
- package/dist/e2e/init.test.js.map +1 -1
- package/dist/state/lock-manager.d.ts +1 -0
- package/dist/state/lock-manager.d.ts.map +1 -1
- package/dist/state/lock-manager.js +1 -1
- package/dist/state/lock-manager.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/commands/run.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import { StateManager } from '../../state/state-manager.js';
|
|
4
|
-
import { acquireLock, releaseLock } from '../../state/lock-manager.js';
|
|
4
|
+
import { acquireLock, getLockPath, releaseLock } from '../../state/lock-manager.js';
|
|
5
5
|
import { analyzeCrash } from '../../state/completion.js';
|
|
6
6
|
import { AssemblyEngine } from '../../core/assembly/engine.js';
|
|
7
7
|
import { getPackageKnowledgeDir } from '../../utils/fs.js';
|
|
@@ -18,6 +18,7 @@ import { createOutputContext } from '../../cli/output/context.js';
|
|
|
18
18
|
import { displayErrors } from '../../cli/output/error-display.js';
|
|
19
19
|
import { resolveOutputMode } from '../../cli/middleware/output-mode.js';
|
|
20
20
|
import { findClosestMatch } from '../../utils/levenshtein.js';
|
|
21
|
+
import { shutdown } from '../shutdown.js';
|
|
21
22
|
const runCommand = {
|
|
22
23
|
command: 'run <step>',
|
|
23
24
|
describe: 'Run a pipeline step',
|
|
@@ -51,7 +52,8 @@ const runCommand = {
|
|
|
51
52
|
if (!projectRoot) {
|
|
52
53
|
process.stderr.write('✗ error [PROJECT_NOT_INITIALIZED]: No .scaffold/ directory found\n' +
|
|
53
54
|
' Fix: Run `scaffold init` to initialize a project\n');
|
|
54
|
-
process.
|
|
55
|
+
process.exitCode = 1;
|
|
56
|
+
return;
|
|
55
57
|
}
|
|
56
58
|
const outputMode = resolveOutputMode(argv);
|
|
57
59
|
const output = createOutputContext(outputMode);
|
|
@@ -61,9 +63,10 @@ const runCommand = {
|
|
|
61
63
|
const context = loadPipelineContext(projectRoot, { includeTools: true });
|
|
62
64
|
if (!context.config) {
|
|
63
65
|
displayErrors(context.configErrors, context.configWarnings, output);
|
|
64
|
-
process.
|
|
66
|
+
process.exitCode = 1;
|
|
65
67
|
return;
|
|
66
68
|
}
|
|
69
|
+
const config = context.config;
|
|
67
70
|
const pipeline = resolvePipeline(context, { output });
|
|
68
71
|
const metaPrompt = context.metaPrompts.get(step);
|
|
69
72
|
if (!metaPrompt) {
|
|
@@ -76,7 +79,8 @@ const runCommand = {
|
|
|
76
79
|
exitCode: 1,
|
|
77
80
|
recovery: `Available steps: ${candidates.join(', ')}`,
|
|
78
81
|
});
|
|
79
|
-
process.
|
|
82
|
+
process.exitCode = 1;
|
|
83
|
+
return;
|
|
80
84
|
}
|
|
81
85
|
// -----------------------------------------------------------------------
|
|
82
86
|
// Step 3: Acquire lock
|
|
@@ -92,7 +96,8 @@ const runCommand = {
|
|
|
92
96
|
});
|
|
93
97
|
}
|
|
94
98
|
output.warn({ code: 'LOCK_HELD', message: 'Another scaffold process is running. Use --force to override.' });
|
|
95
|
-
process.
|
|
99
|
+
process.exitCode = 3;
|
|
100
|
+
return;
|
|
96
101
|
}
|
|
97
102
|
// --force: proceed without lock
|
|
98
103
|
lockAcquired = false;
|
|
@@ -101,337 +106,352 @@ const runCommand = {
|
|
|
101
106
|
output.warn(lockResult.warning);
|
|
102
107
|
}
|
|
103
108
|
// -----------------------------------------------------------------------
|
|
104
|
-
//
|
|
109
|
+
// Lock-protected body — wrapped in withResource when lock is acquired
|
|
105
110
|
// -----------------------------------------------------------------------
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
output.warn({
|
|
120
|
-
code: 'CRASH_RECOVERY_NEEDED',
|
|
121
|
-
message: `Previous run of '${crashedStep}' may be incomplete. ` +
|
|
122
|
-
'Some artifacts present, some missing. ' +
|
|
123
|
-
'Please manually verify and use --force to continue.',
|
|
124
|
-
});
|
|
125
|
-
if (lockAcquired)
|
|
126
|
-
releaseLock(projectRoot);
|
|
127
|
-
process.exit(4);
|
|
111
|
+
const lockProtectedBody = async () => {
|
|
112
|
+
// -----------------------------------------------------------------------
|
|
113
|
+
// Step 4: Load and validate state
|
|
114
|
+
// -----------------------------------------------------------------------
|
|
115
|
+
const stateManager = new StateManager(projectRoot, pipeline.computeEligible);
|
|
116
|
+
let state = stateManager.loadState();
|
|
117
|
+
// Crash recovery: in_progress is non-null from a previous run
|
|
118
|
+
if (state.in_progress !== null) {
|
|
119
|
+
const crashAction = analyzeCrash(state, projectRoot);
|
|
120
|
+
if (crashAction.action === 'auto_complete') {
|
|
121
|
+
const lastDepth = (state.steps[state.in_progress.step]?.depth ?? 3);
|
|
122
|
+
stateManager.markCompleted(state.in_progress.step, [], 'scaffold-crash-recovery', lastDepth);
|
|
123
|
+
stateManager.clearInProgress();
|
|
128
124
|
}
|
|
129
|
-
else {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
125
|
+
else if (crashAction.action === 'ask_user') {
|
|
126
|
+
if (outputMode === 'auto' || outputMode === 'json') {
|
|
127
|
+
const crashedStep = state.in_progress.step;
|
|
128
|
+
output.warn({
|
|
129
|
+
code: 'CRASH_RECOVERY_NEEDED',
|
|
130
|
+
message: `Previous run of '${crashedStep}' may be incomplete. ` +
|
|
131
|
+
'Some artifacts present, some missing. ' +
|
|
132
|
+
'Please manually verify and use --force to continue.',
|
|
133
|
+
});
|
|
134
|
+
process.exitCode = 4;
|
|
135
|
+
return;
|
|
136
136
|
}
|
|
137
137
|
else {
|
|
138
|
-
|
|
138
|
+
// Interactive: prompt user
|
|
139
|
+
const shouldComplete = await shutdown.withPrompt(() => output.confirm(`Previous run of '${state.in_progress.step}' appears partially complete. Mark as completed?`, false));
|
|
140
|
+
if (shouldComplete) {
|
|
141
|
+
const lastDepth = (state.steps[state.in_progress.step]?.depth ?? 3);
|
|
142
|
+
stateManager.markCompleted(state.in_progress.step, [], 'scaffold-crash-recovery', lastDepth);
|
|
143
|
+
stateManager.clearInProgress();
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
stateManager.clearInProgress();
|
|
147
|
+
}
|
|
139
148
|
}
|
|
140
149
|
}
|
|
150
|
+
else {
|
|
151
|
+
// recommend_rerun: just clear
|
|
152
|
+
stateManager.clearInProgress();
|
|
153
|
+
}
|
|
154
|
+
// Reload state after recovery
|
|
155
|
+
state = stateManager.loadState();
|
|
141
156
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
157
|
+
// -----------------------------------------------------------------------
|
|
158
|
+
// Step 5: Check dependencies
|
|
159
|
+
// -----------------------------------------------------------------------
|
|
160
|
+
const { graph } = pipeline;
|
|
161
|
+
const cycles = detectCycles(graph);
|
|
162
|
+
if (cycles.length > 0) {
|
|
163
|
+
displayErrors(cycles, [], output);
|
|
164
|
+
process.exitCode = 1;
|
|
165
|
+
return;
|
|
145
166
|
}
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
// Tools (category: 'tool') are not in the dependency graph — skip dep checking
|
|
161
|
-
const isTool = metaPrompt.frontmatter.category === 'tool';
|
|
162
|
-
const stepNode = isTool ? undefined : graph.nodes.get(step);
|
|
163
|
-
const deps = isTool
|
|
164
|
-
? (pipeline.overlay.dependencies[step] ?? [])
|
|
165
|
-
: (stepNode?.dependencies ?? []);
|
|
166
|
-
if (!isTool) {
|
|
167
|
-
const unmetDeps = deps.filter(dep => {
|
|
168
|
-
// Overlay-disabled deps are treated as satisfied (matches eligibility.ts)
|
|
169
|
-
const depNode = graph?.nodes.get(dep);
|
|
170
|
-
if (depNode && !depNode.enabled)
|
|
171
|
-
return false;
|
|
172
|
-
const depStatus = state.steps[dep]?.status;
|
|
173
|
-
return depStatus !== 'completed' && depStatus !== 'skipped';
|
|
174
|
-
});
|
|
175
|
-
if (unmetDeps.length > 0) {
|
|
176
|
-
output.error({
|
|
177
|
-
code: 'DEP_UNMET',
|
|
178
|
-
message: `Step '${step}' has unmet dependencies: ${unmetDeps.join(', ')}`,
|
|
179
|
-
exitCode: 2,
|
|
180
|
-
recovery: `Complete these steps first: ${unmetDeps.join(', ')}`,
|
|
167
|
+
// Tools (category: 'tool') are not in the dependency graph — skip dep checking
|
|
168
|
+
const isTool = metaPrompt.frontmatter.category === 'tool';
|
|
169
|
+
const stepNode = isTool ? undefined : graph.nodes.get(step);
|
|
170
|
+
const deps = isTool
|
|
171
|
+
? (pipeline.overlay.dependencies[step] ?? [])
|
|
172
|
+
: (stepNode?.dependencies ?? []);
|
|
173
|
+
if (!isTool) {
|
|
174
|
+
const unmetDeps = deps.filter(dep => {
|
|
175
|
+
// Overlay-disabled deps are treated as satisfied (matches eligibility.ts)
|
|
176
|
+
const depNode = graph?.nodes.get(dep);
|
|
177
|
+
if (depNode && !depNode.enabled)
|
|
178
|
+
return false;
|
|
179
|
+
const depStatus = state.steps[dep]?.status;
|
|
180
|
+
return depStatus !== 'completed' && depStatus !== 'skipped';
|
|
181
181
|
});
|
|
182
|
-
if (
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const { depth, provenance } = resolveDepth(step, context.config, pipeline.preset, cliDepth);
|
|
192
|
-
const updateModeResult = detectUpdateMode({ step, state, currentDepth: depth, projectRoot });
|
|
193
|
-
if (updateModeResult.isUpdateMode) {
|
|
194
|
-
if (!argv.force) {
|
|
195
|
-
if (outputMode === 'interactive') {
|
|
196
|
-
const proceed = await output.confirm(`Step '${step}' is already completed. Re-run in update mode?`, true);
|
|
197
|
-
if (!proceed) {
|
|
198
|
-
if (lockAcquired)
|
|
199
|
-
releaseLock(projectRoot);
|
|
200
|
-
process.exit(4);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
else {
|
|
204
|
-
output.info(`Re-running step '${step}' in update mode (auto)`);
|
|
182
|
+
if (unmetDeps.length > 0) {
|
|
183
|
+
output.error({
|
|
184
|
+
code: 'DEP_UNMET',
|
|
185
|
+
message: `Step '${step}' has unmet dependencies: ${unmetDeps.join(', ')}`,
|
|
186
|
+
exitCode: 2,
|
|
187
|
+
recovery: `Complete these steps first: ${unmetDeps.join(', ')}`,
|
|
188
|
+
});
|
|
189
|
+
process.exitCode = 2;
|
|
190
|
+
return;
|
|
205
191
|
}
|
|
206
192
|
}
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
193
|
+
// -----------------------------------------------------------------------
|
|
194
|
+
// Step 6: Check update mode and depth downgrade
|
|
195
|
+
// -----------------------------------------------------------------------
|
|
196
|
+
const cliDepth = argv.depth !== undefined ? argv.depth : undefined;
|
|
197
|
+
const { depth, provenance } = resolveDepth(step, config, pipeline.preset, cliDepth);
|
|
198
|
+
const updateModeResult = detectUpdateMode({ step, state, currentDepth: depth, projectRoot });
|
|
199
|
+
if (updateModeResult.isUpdateMode) {
|
|
200
|
+
if (!argv.force) {
|
|
201
|
+
if (outputMode === 'interactive') {
|
|
202
|
+
const proceed = await shutdown.withPrompt(() => output.confirm(`Step '${step}' is already completed. Re-run in update mode?`, true));
|
|
203
|
+
if (!proceed) {
|
|
204
|
+
process.exitCode = 4;
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
output.info(`Re-running step '${step}' in update mode (auto)`);
|
|
216
210
|
}
|
|
217
211
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
212
|
+
// Check for depth downgrade
|
|
213
|
+
const hasDowngrade = updateModeResult.warnings.some(w => w.code === 'ASM_DEPTH_DOWNGRADE');
|
|
214
|
+
if (hasDowngrade && !argv.force) {
|
|
215
|
+
if (outputMode === 'interactive') {
|
|
216
|
+
const proceedWithDowngrade = await shutdown.withPrompt(() => output.confirm('Depth downgrade detected. Continue?', false));
|
|
217
|
+
if (!proceedWithDowngrade) {
|
|
218
|
+
process.exitCode = 4;
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
for (const w of updateModeResult.warnings) {
|
|
224
|
+
output.warn(w);
|
|
225
|
+
}
|
|
221
226
|
}
|
|
222
227
|
}
|
|
223
228
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
for (const w of methodologyChangeResult.warnings) {
|
|
228
|
-
output.warn(w);
|
|
229
|
-
}
|
|
230
|
-
// -----------------------------------------------------------------------
|
|
231
|
-
// Step 7: Set step to in_progress (skip for stateless steps)
|
|
232
|
-
// -----------------------------------------------------------------------
|
|
233
|
-
const isStateless = metaPrompt.frontmatter.stateless === true;
|
|
234
|
-
if (!isStateless) {
|
|
235
|
-
stateManager.setInProgress(step, 'scaffold-run');
|
|
236
|
-
}
|
|
237
|
-
try {
|
|
238
|
-
// Reload state after setInProgress
|
|
239
|
-
state = stateManager.loadState();
|
|
240
|
-
// -----------------------------------------------------------------------
|
|
241
|
-
// Step 8: Load assembly components
|
|
242
|
-
// -----------------------------------------------------------------------
|
|
243
|
-
const { instructions } = loadInstructions(projectRoot, step, argv.instructions);
|
|
244
|
-
const kbIndex = buildIndexWithOverrides(projectRoot, getPackageKnowledgeDir(projectRoot));
|
|
245
|
-
const { entries: knowledgeEntries, warnings: kbWarnings } = loadEntries(kbIndex, pipeline.overlay.knowledge[step] ?? metaPrompt.frontmatter.knowledgeBase ?? []);
|
|
246
|
-
for (const w of kbWarnings) {
|
|
229
|
+
// Check methodology change
|
|
230
|
+
const methodologyChangeResult = detectMethodologyChange({ state, config });
|
|
231
|
+
for (const w of methodologyChangeResult.warnings) {
|
|
247
232
|
output.warn(w);
|
|
248
233
|
}
|
|
249
|
-
//
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
234
|
+
// -----------------------------------------------------------------------
|
|
235
|
+
// Step 7: Set step to in_progress (skip for stateless steps)
|
|
236
|
+
// -----------------------------------------------------------------------
|
|
237
|
+
const isStateless = metaPrompt.frontmatter.stateless === true;
|
|
238
|
+
try {
|
|
239
|
+
await shutdown.withContext(() => {
|
|
240
|
+
try {
|
|
241
|
+
const ctxState = stateManager.loadState();
|
|
242
|
+
return ctxState.in_progress !== null
|
|
243
|
+
? 'Cancelled. Step progress cleared.'
|
|
244
|
+
: 'Cancelled.';
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return 'Cancelled.';
|
|
248
|
+
}
|
|
249
|
+
}, async () => {
|
|
250
|
+
// Note: Do NOT wrap in withResource('in-progress') — clearing in_progress
|
|
251
|
+
// on Ctrl+C would break crash recovery. The existing analyzeCrash() logic
|
|
252
|
+
// handles interrupted steps correctly on the next run.
|
|
253
|
+
if (!isStateless) {
|
|
254
|
+
stateManager.setInProgress(step, 'scaffold-run');
|
|
255
|
+
}
|
|
256
|
+
// Reload state after setInProgress
|
|
257
|
+
state = stateManager.loadState();
|
|
258
|
+
// -----------------------------------------------------------------------
|
|
259
|
+
// Step 8: Load assembly components
|
|
260
|
+
// -----------------------------------------------------------------------
|
|
261
|
+
const { instructions } = loadInstructions(projectRoot, step, argv.instructions);
|
|
262
|
+
const kbIndex = buildIndexWithOverrides(projectRoot, getPackageKnowledgeDir(projectRoot));
|
|
263
|
+
const { entries: knowledgeEntries, warnings: kbWarnings } = loadEntries(kbIndex, pipeline.overlay.knowledge[step] ?? metaPrompt.frontmatter.knowledgeBase ?? []);
|
|
264
|
+
for (const w of kbWarnings) {
|
|
265
|
+
output.warn(w);
|
|
266
|
+
}
|
|
267
|
+
// Gather artifacts from completed dependency steps
|
|
268
|
+
const artifacts = [];
|
|
269
|
+
const gatheredPaths = new Set();
|
|
270
|
+
for (const dep of deps) {
|
|
271
|
+
const depEntry = state.steps[dep];
|
|
272
|
+
if (depEntry?.status === 'completed' && depEntry.produces) {
|
|
273
|
+
for (const relPath of depEntry.produces) {
|
|
274
|
+
const fullPath = path.resolve(projectRoot, relPath);
|
|
275
|
+
if (fs.existsSync(fullPath)) {
|
|
276
|
+
try {
|
|
277
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
278
|
+
artifacts.push({ stepName: dep, filePath: relPath, content });
|
|
279
|
+
gatheredPaths.add(relPath);
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
output.warn({
|
|
283
|
+
code: 'ARTIFACT_READ_ERROR',
|
|
284
|
+
message: `Could not read artifact '${relPath}' from step '${dep}': ${err.message}`,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
262
288
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// Gather artifacts from reads (optional cross-cutting references)
|
|
292
|
+
// Note: graph defaults missing steps to enabled:true, which may not reflect
|
|
293
|
+
// custom config overrides. This is a pre-existing graph builder limitation.
|
|
294
|
+
const reads = pipeline.overlay.reads[step] ?? metaPrompt.frontmatter.reads ?? [];
|
|
295
|
+
for (const readStep of reads) {
|
|
296
|
+
// Check dependency graph for enablement (overlay-disabled steps)
|
|
297
|
+
const readNode = graph?.nodes.get(readStep);
|
|
298
|
+
if (readNode && !readNode.enabled)
|
|
299
|
+
continue;
|
|
300
|
+
// Check state — silently skip if not completed (reads are optional)
|
|
301
|
+
const readEntry = state.steps[readStep];
|
|
302
|
+
if (readEntry?.status !== 'completed' || !readEntry.produces)
|
|
303
|
+
continue;
|
|
304
|
+
for (const relPath of readEntry.produces) {
|
|
305
|
+
// Deduplicate: skip paths already gathered from deps
|
|
306
|
+
if (gatheredPaths.has(relPath))
|
|
307
|
+
continue;
|
|
308
|
+
const fullPath = path.resolve(projectRoot, relPath);
|
|
309
|
+
if (fs.existsSync(fullPath)) {
|
|
310
|
+
try {
|
|
311
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
312
|
+
artifacts.push({ stepName: readStep, filePath: relPath, content });
|
|
313
|
+
gatheredPaths.add(relPath);
|
|
314
|
+
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
output.warn({
|
|
317
|
+
code: 'ARTIFACT_READ_ERROR',
|
|
318
|
+
message: `Could not read artifact '${relPath}' from step` +
|
|
319
|
+
` '${readStep}': ${err.message}`,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
268
322
|
}
|
|
269
323
|
}
|
|
270
324
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
// custom config overrides. This is a pre-existing graph builder limitation.
|
|
276
|
-
const reads = pipeline.overlay.reads[step] ?? metaPrompt.frontmatter.reads ?? [];
|
|
277
|
-
for (const readStep of reads) {
|
|
278
|
-
// Check dependency graph for enablement (overlay-disabled steps)
|
|
279
|
-
const readNode = graph?.nodes.get(readStep);
|
|
280
|
-
if (readNode && !readNode.enabled)
|
|
281
|
-
continue;
|
|
282
|
-
// Check state — silently skip if not completed (reads are optional)
|
|
283
|
-
const readEntry = state.steps[readStep];
|
|
284
|
-
if (readEntry?.status !== 'completed' || !readEntry.produces)
|
|
285
|
-
continue;
|
|
286
|
-
for (const relPath of readEntry.produces) {
|
|
287
|
-
// Deduplicate: skip paths already gathered from deps
|
|
288
|
-
if (gatheredPaths.has(relPath))
|
|
289
|
-
continue;
|
|
290
|
-
const fullPath = path.resolve(projectRoot, relPath);
|
|
291
|
-
if (fs.existsSync(fullPath)) {
|
|
325
|
+
// Read decisions log
|
|
326
|
+
const decisionsPath = path.join(projectRoot, '.scaffold', 'decisions.jsonl');
|
|
327
|
+
let decisions = '';
|
|
328
|
+
if (fs.existsSync(decisionsPath)) {
|
|
292
329
|
try {
|
|
293
|
-
|
|
294
|
-
artifacts.push({ stepName: readStep, filePath: relPath, content });
|
|
295
|
-
gatheredPaths.add(relPath);
|
|
330
|
+
decisions = fs.readFileSync(decisionsPath, 'utf8');
|
|
296
331
|
}
|
|
297
332
|
catch (err) {
|
|
298
333
|
output.warn({
|
|
299
|
-
code: '
|
|
300
|
-
message: `Could not read
|
|
334
|
+
code: 'DECISIONS_READ_ERROR',
|
|
335
|
+
message: `Could not read decisions log: ${err.message}`,
|
|
301
336
|
});
|
|
302
337
|
}
|
|
303
338
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
339
|
+
// -----------------------------------------------------------------------
|
|
340
|
+
// Step 9: Assemble prompt
|
|
341
|
+
// -----------------------------------------------------------------------
|
|
342
|
+
const engine = new AssemblyEngine();
|
|
343
|
+
const assemblyResult = engine.assemble(step, {
|
|
344
|
+
config,
|
|
345
|
+
state,
|
|
346
|
+
metaPrompt,
|
|
347
|
+
knowledgeEntries,
|
|
348
|
+
instructions,
|
|
349
|
+
arguments: argv.instructions,
|
|
350
|
+
depth,
|
|
351
|
+
depthProvenance: provenance,
|
|
352
|
+
updateMode: updateModeResult.isUpdateMode,
|
|
353
|
+
existingArtifact: updateModeResult.existingArtifact,
|
|
354
|
+
artifacts,
|
|
355
|
+
decisions,
|
|
317
356
|
});
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
// -----------------------------------------------------------------------
|
|
323
|
-
const engine = new AssemblyEngine();
|
|
324
|
-
const assemblyResult = engine.assemble(step, {
|
|
325
|
-
config: context.config,
|
|
326
|
-
state,
|
|
327
|
-
metaPrompt,
|
|
328
|
-
knowledgeEntries,
|
|
329
|
-
instructions,
|
|
330
|
-
arguments: argv.instructions,
|
|
331
|
-
depth,
|
|
332
|
-
depthProvenance: provenance,
|
|
333
|
-
updateMode: updateModeResult.isUpdateMode,
|
|
334
|
-
existingArtifact: updateModeResult.existingArtifact,
|
|
335
|
-
artifacts,
|
|
336
|
-
decisions,
|
|
337
|
-
});
|
|
338
|
-
if (!assemblyResult.success) {
|
|
339
|
-
displayErrors(assemblyResult.errors, assemblyResult.warnings, output);
|
|
340
|
-
if (lockAcquired)
|
|
341
|
-
releaseLock(projectRoot);
|
|
342
|
-
process.exit(5);
|
|
343
|
-
}
|
|
344
|
-
// -----------------------------------------------------------------------
|
|
345
|
-
// Step 10: Wait for completion (interactive) or exit (auto/json)
|
|
346
|
-
// -----------------------------------------------------------------------
|
|
347
|
-
if (outputMode === 'auto' || outputMode === 'json') {
|
|
348
|
-
// In auto/json mode: output the structured result and exit 0
|
|
349
|
-
// For stateful steps, step stays in_progress for crash recovery awareness
|
|
350
|
-
if (outputMode === 'json') {
|
|
351
|
-
if (isStateless) {
|
|
352
|
-
output.result({
|
|
353
|
-
step,
|
|
354
|
-
status: 'stateless',
|
|
355
|
-
depth,
|
|
356
|
-
depth_source: provenance,
|
|
357
|
-
prompt: assemblyResult.prompt.text,
|
|
358
|
-
});
|
|
357
|
+
if (!assemblyResult.success) {
|
|
358
|
+
displayErrors(assemblyResult.errors, assemblyResult.warnings, output);
|
|
359
|
+
process.exitCode = 5;
|
|
360
|
+
return;
|
|
359
361
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
output
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
362
|
+
// -----------------------------------------------------------------------
|
|
363
|
+
// Step 10: Wait for completion (interactive) or exit (auto/json)
|
|
364
|
+
// -----------------------------------------------------------------------
|
|
365
|
+
if (outputMode === 'auto' || outputMode === 'json') {
|
|
366
|
+
// In auto/json mode: output the structured result and exit 0
|
|
367
|
+
// For stateful steps, step stays in_progress for crash recovery awareness
|
|
368
|
+
if (outputMode === 'json') {
|
|
369
|
+
if (isStateless) {
|
|
370
|
+
output.result({
|
|
371
|
+
step,
|
|
372
|
+
status: 'stateless',
|
|
373
|
+
depth,
|
|
374
|
+
depth_source: provenance,
|
|
375
|
+
prompt: assemblyResult.prompt.text,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
// Reload state for next eligible
|
|
380
|
+
const stateForEligible = stateManager.loadState();
|
|
381
|
+
const nextSteps = pipeline.computeEligible(stateForEligible.steps);
|
|
382
|
+
output.result({
|
|
383
|
+
step,
|
|
384
|
+
status: 'in_progress',
|
|
385
|
+
depth,
|
|
386
|
+
depth_source: provenance,
|
|
387
|
+
nextEligible: nextSteps,
|
|
388
|
+
prompt: assemblyResult.prompt.text,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
// auto mode: write prompt to stdout for AI consumption
|
|
394
|
+
process.stdout.write(assemblyResult.prompt.text);
|
|
395
|
+
}
|
|
396
|
+
return;
|
|
372
397
|
}
|
|
373
|
-
|
|
374
|
-
else {
|
|
375
|
-
// auto mode: write prompt to stdout for AI consumption
|
|
398
|
+
// Write assembled prompt to stdout (raw, for AI consumption in interactive mode)
|
|
376
399
|
process.stdout.write(assemblyResult.prompt.text);
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
400
|
+
// Interactive mode: prompt user for completion
|
|
401
|
+
if (isStateless) {
|
|
402
|
+
// Stateless steps don't track completion — just exit
|
|
403
|
+
if (outputMode === 'interactive') {
|
|
404
|
+
output.info(`Stateless step '${step}' executed. Available for re-use anytime.`);
|
|
405
|
+
}
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const isComplete = await shutdown.withPrompt(() => output.confirm(`Step '${step}' complete?`, true));
|
|
409
|
+
if (!isComplete) {
|
|
410
|
+
const shouldSkip = await shutdown.withPrompt(() => output.confirm('Mark as skipped instead?', false));
|
|
411
|
+
if (shouldSkip) {
|
|
412
|
+
stateManager.markSkipped(step, 'user-cancelled', 'scaffold-run');
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
stateManager.clearInProgress();
|
|
416
|
+
}
|
|
417
|
+
process.exitCode = 4;
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
// -----------------------------------------------------------------------
|
|
421
|
+
// Step 11: Mark completed
|
|
422
|
+
// -----------------------------------------------------------------------
|
|
423
|
+
stateManager.markCompleted(step, metaPrompt.frontmatter.outputs ?? [], 'scaffold-run', depth);
|
|
424
|
+
// -----------------------------------------------------------------------
|
|
425
|
+
// Step 12: Show next eligible steps
|
|
426
|
+
// -----------------------------------------------------------------------
|
|
427
|
+
const finalState = stateManager.loadState();
|
|
428
|
+
const nextSteps = pipeline.computeEligible(finalState.steps);
|
|
429
|
+
if (outputMode === 'interactive') {
|
|
430
|
+
if (nextSteps.length > 0) {
|
|
431
|
+
output.info(`Next eligible: ${nextSteps.join(', ')}`);
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
output.info('No more eligible steps.');
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
});
|
|
381
438
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
// Stateless steps don't track completion — just release lock and exit
|
|
387
|
-
if (lockAcquired)
|
|
388
|
-
releaseLock(projectRoot);
|
|
389
|
-
if (outputMode === 'interactive') {
|
|
390
|
-
output.info(`Stateless step '${step}' executed. Available for re-use anytime.`);
|
|
391
|
-
}
|
|
392
|
-
process.exit(0);
|
|
439
|
+
catch (err) {
|
|
440
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
441
|
+
output.error({ code: 'RUN_UNEXPECTED_ERROR', message, exitCode: 1 });
|
|
442
|
+
process.exitCode = 1;
|
|
393
443
|
return;
|
|
394
444
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
stateManager.markSkipped(step, 'user-cancelled', 'scaffold-run');
|
|
400
|
-
}
|
|
401
|
-
else {
|
|
402
|
-
stateManager.clearInProgress();
|
|
403
|
-
}
|
|
404
|
-
if (lockAcquired)
|
|
405
|
-
releaseLock(projectRoot);
|
|
406
|
-
process.exit(4);
|
|
407
|
-
}
|
|
408
|
-
// -----------------------------------------------------------------------
|
|
409
|
-
// Step 11: Mark completed
|
|
410
|
-
// -----------------------------------------------------------------------
|
|
411
|
-
stateManager.markCompleted(step, metaPrompt.frontmatter.outputs ?? [], 'scaffold-run', depth);
|
|
412
|
-
if (lockAcquired)
|
|
445
|
+
};
|
|
446
|
+
if (lockAcquired) {
|
|
447
|
+
shutdown.registerLockOwnership(getLockPath(projectRoot));
|
|
448
|
+
await shutdown.withResource('lock', () => {
|
|
413
449
|
releaseLock(projectRoot);
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
// -----------------------------------------------------------------------
|
|
417
|
-
const finalState = stateManager.loadState();
|
|
418
|
-
const nextSteps = pipeline.computeEligible(finalState.steps);
|
|
419
|
-
if (outputMode === 'interactive') {
|
|
420
|
-
if (nextSteps.length > 0) {
|
|
421
|
-
output.info(`Next eligible: ${nextSteps.join(', ')}`);
|
|
422
|
-
}
|
|
423
|
-
else {
|
|
424
|
-
output.info('No more eligible steps.');
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
process.exit(0);
|
|
450
|
+
shutdown.releaseLockOwnership();
|
|
451
|
+
}, lockProtectedBody);
|
|
428
452
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
releaseLock(projectRoot);
|
|
432
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
433
|
-
output.error({ code: 'RUN_UNEXPECTED_ERROR', message, exitCode: 1 });
|
|
434
|
-
process.exit(1);
|
|
453
|
+
else {
|
|
454
|
+
await lockProtectedBody();
|
|
435
455
|
}
|
|
436
456
|
},
|
|
437
457
|
};
|