@shenlee/devcrew 0.1.1 → 0.1.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.
Files changed (48) hide show
  1. package/README.md +30 -8
  2. package/README.zh-CN.md +27 -8
  3. package/dist/packages/adapters/src/index.js +62 -8
  4. package/dist/packages/core/src/artifacts.js +14 -4
  5. package/dist/packages/core/src/config.js +1 -1
  6. package/dist/packages/core/src/paths.js +10 -6
  7. package/dist/packages/core/src/store.js +36 -0
  8. package/dist/packages/core/src/types.js +5 -1
  9. package/dist/packages/core/src/validation.js +7 -1
  10. package/dist/packages/core/src/version.js +1 -1
  11. package/dist/packages/core/src/workflow.js +120 -31
  12. package/dist/packages/orchestrator/src/index.js +339 -160
  13. package/dist/packages/orchestrator/src/worktree.js +198 -0
  14. package/dist/packages/plugins/src/index.js +2 -32
  15. package/dist/packages/service/src/stdio.js +19 -3
  16. package/dist/packages/service/src/tools.js +45 -6
  17. package/docs/claude-code.md +5 -3
  18. package/docs/codex.md +12 -4
  19. package/docs/quickstart.md +1 -1
  20. package/docs/superpowers/plans/2026-07-10-p0-foundation-repair.md +939 -0
  21. package/docs/superpowers/plans/2026-07-13-safety-semantics.md +313 -0
  22. package/docs/superpowers/specs/2026-07-10-p0-foundation-repair-design.md +182 -0
  23. package/docs/superpowers/specs/2026-07-13-execution-boundaries-design.md +126 -0
  24. package/docs/workflow.md +36 -4
  25. package/package.json +1 -1
  26. package/packages/adapters/src/index.ts +87 -11
  27. package/packages/core/src/artifacts.ts +16 -4
  28. package/packages/core/src/config.ts +1 -1
  29. package/packages/core/src/paths.ts +11 -6
  30. package/packages/core/src/store.ts +41 -1
  31. package/packages/core/src/types.ts +53 -2
  32. package/packages/core/src/validation.ts +10 -0
  33. package/packages/core/src/version.ts +1 -1
  34. package/packages/core/src/workflow.ts +136 -30
  35. package/packages/orchestrator/src/index.ts +377 -182
  36. package/packages/orchestrator/src/worktree.ts +269 -0
  37. package/packages/plugins/src/index.ts +2 -44
  38. package/packages/service/src/stdio.ts +27 -6
  39. package/packages/service/src/tools.ts +46 -5
  40. package/plugins/devcrew-codex/.codex-plugin/plugin.json +1 -1
  41. package/plugins/devcrew-codex/.mcp.json +1 -1
  42. package/plugins/devcrew-codex/assets/logo.png +0 -0
  43. package/plugins/devcrew-codex/skills/devcrew/SKILL.md +8 -7
  44. package/scripts/smoke-codex-plugin.mjs +1 -1
  45. package/plugins/devcrew-codex/agents/architect.toml +0 -6
  46. package/plugins/devcrew-codex/agents/implementer.toml +0 -6
  47. package/plugins/devcrew-codex/agents/pm.toml +0 -6
  48. package/plugins/devcrew-codex/agents/tester.toml +0 -6
@@ -1,8 +1,9 @@
1
1
  import { spawn } from "node:child_process";
2
- import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
- import { dirname, resolve as resolvePath } from "node:path";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { dirname } from "node:path";
4
4
  import { runRole } from "../../adapters/src/index.js";
5
- import { answerWorkflow, artifactForPhase, artifactPath, ARTIFACTS, discoverCoverageCommands, discoverLintCommands, discoverVerifyCommands, gateForPhase, getWorkflowStatus, readConfig, rejectWorkflow, renderArtifact, saveState, startWorkflow, } from "../../core/src/index.js";
5
+ import { answerWorkflow, approveWorkflow, artifactForPhase, artifactPath, ARTIFACTS, discoverCoverageCommands, discoverLintCommands, discoverVerifyCommands, gateForPhase, getWorkflowStatus, nextPhaseAfterGate, readConfig, rejectWorkflow, renderArtifact, saveState, startWorkflow, validateWorkflowApproval, waiveVerificationWorkflow, } from "../../core/src/index.js";
6
+ import { captureExecutionChanges, cleanupExecutionWorkspace, ensureExecutionWorkspace, promoteExecutionChanges, rollbackPromotedExecutionChanges, } from "./worktree.js";
6
7
  // Hard cap so a hung apply/verify command cannot block the serialized MCP loop.
7
8
  const COMMAND_TIMEOUT_MS = 300_000;
8
9
  function roleForPhase(phase) {
@@ -10,6 +11,8 @@ function roleForPhase(phase) {
10
11
  requirements: "pm",
11
12
  architecture: "architect",
12
13
  implementation: "implementer",
14
+ execution: "implementer",
15
+ review: "architect",
13
16
  testing: "tester",
14
17
  };
15
18
  return roles[phase];
@@ -33,7 +36,7 @@ function priorArtifactNamesForPhase(phase) {
33
36
  return ARTIFACTS.slice(0, currentIndex);
34
37
  }
35
38
  async function writeMarkdownArtifact(state, artifact, markdown) {
36
- const path = artifactPath(state.cwd, state.runId, artifact);
39
+ const path = artifactPath(state.cwd, state.runId, artifact, state.artifactDirectory);
37
40
  await mkdir(dirname(path), { recursive: true });
38
41
  await writeFile(path, markdown, "utf8");
39
42
  return path;
@@ -113,100 +116,6 @@ export async function runShellCommand(command, cwd, timeoutMs = COMMAND_TIMEOUT_
113
116
  });
114
117
  });
115
118
  }
116
- async function runGit(args, cwd) {
117
- return new Promise((resolveResult) => {
118
- const child = spawn("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
119
- let stdout = "";
120
- let stderr = "";
121
- child.stdout.on("data", (chunk) => {
122
- stdout += chunk.toString("utf8");
123
- });
124
- child.stderr.on("data", (chunk) => {
125
- stderr += chunk.toString("utf8");
126
- });
127
- child.on("error", () => resolveResult({ exitCode: 1, stdout: "" }));
128
- child.on("close", (code) => resolveResult({ exitCode: code ?? 1, stdout: stdout || stderr }));
129
- });
130
- }
131
- async function listChangedLines(cwd, options = {}) {
132
- const result = await runShellCommand("git status --porcelain -uall", cwd, 30_000);
133
- if (result.exitCode !== 0) {
134
- if (options.requireGit) {
135
- throw new Error("DevCrew apply mode requires a git repository so implementation changes can be reviewed and reverted.");
136
- }
137
- return [];
138
- }
139
- return result.output
140
- .split("\n")
141
- .map((line) => line.trimEnd())
142
- .filter(Boolean)
143
- .filter((line) => {
144
- const path = line.slice(3);
145
- return !path.startsWith(".devcrew/") && !path.startsWith("docs/devcrew/");
146
- });
147
- }
148
- async function assertCleanApplyWorkspace(cwd) {
149
- const changedLines = await listChangedLines(cwd, { requireGit: true });
150
- if (changedLines.length === 0) {
151
- return;
152
- }
153
- const visibleLines = changedLines.slice(0, 20).join(", ");
154
- const suffix = changedLines.length > 20 ? `, and ${changedLines.length - 20} more` : "";
155
- throw new Error(`DevCrew apply mode requires a clean working tree before implementation. Commit, stash, or remove these files: ${visibleLines}${suffix}`);
156
- }
157
- async function collectImplementationDiff(cwd) {
158
- const result = await runShellCommand("git diff --no-ext-diff --", cwd, 30_000);
159
- if (result.exitCode !== 0) {
160
- return "";
161
- }
162
- return result.output;
163
- }
164
- // Files attributable to this run are the porcelain lines that appeared (or
165
- // changed status) since the baseline captured before the role executed. This
166
- // keeps a user's pre-existing uncommitted edits out of the changed-files list.
167
- // The porcelain status prefix is preserved for review (?? = new, M = modified).
168
- export function changedSinceBaseline(baseline, current) {
169
- const baselineLines = new Set(baseline);
170
- return current.filter((line) => !baselineLines.has(line));
171
- }
172
- // Strip the two-character porcelain status plus its separating space. For
173
- // rename entries ("R old -> new") the destination path is what remains
174
- // relevant, so keep only the post-arrow path when present.
175
- export function porcelainPath(line) {
176
- const raw = line.slice(3).trim();
177
- const arrow = raw.indexOf(" -> ");
178
- return arrow >= 0 ? raw.slice(arrow + 4) : raw;
179
- }
180
- // Roll back implementer edits when an apply-mode gate is rejected.
181
- // Only the files this run introduced/changed are reverted, so unrelated work in
182
- // the repository is preserved. Tracked files are restored from HEAD; files that
183
- // did not exist in HEAD are deleted. Failures are surfaced to the MCP caller.
184
- export async function revertChangedFiles(cwd, changedFiles, deps = {}) {
185
- const git = deps.runGit ?? runGit;
186
- const removeFile = deps.removeFile ?? ((absolutePath) => rm(absolutePath, { force: true }));
187
- for (const line of changedFiles) {
188
- const file = porcelainPath(line);
189
- if (!file) {
190
- continue;
191
- }
192
- const tracked = await git(["cat-file", "-e", `HEAD:${file}`], cwd);
193
- if (tracked.exitCode === 0) {
194
- const restored = await git(["restore", "--source=HEAD", "--", file], cwd);
195
- if (restored.exitCode !== 0) {
196
- throw new Error(`Failed to restore ${file}: ${restored.stdout || "git restore failed"}`);
197
- }
198
- }
199
- else {
200
- try {
201
- await removeFile(resolvePath(cwd, file));
202
- }
203
- catch (error) {
204
- const message = error instanceof Error ? error.message : String(error);
205
- throw new Error(`Failed to remove ${file}: ${message}`);
206
- }
207
- }
208
- }
209
- }
210
119
  async function runCommands(commands, cwd) {
211
120
  const results = [];
212
121
  for (const command of commands) {
@@ -229,28 +138,22 @@ function uniqueCommands(commands) {
229
138
  // Tester verification runs the normal verification path first, then coverage
230
139
  // as supplemental evidence. Configured commands win per category; otherwise
231
140
  // DevCrew discovers common project commands.
232
- async function runConfiguredVerification(state) {
141
+ async function runConfiguredVerification(state, commandCwd) {
233
142
  const config = await readConfig(state.cwd);
234
143
  const configuredVerify = config.verifyCommands.filter((command) => command.trim().length > 0);
235
144
  const configuredCoverage = (config.coverageCommands ?? []).filter((command) => command.trim().length > 0);
236
- const verifyCommands = configuredVerify.length > 0 ? configuredVerify : await discoverVerifyCommands(state.cwd);
237
- const coverageCommands = configuredCoverage.length > 0 ? configuredCoverage : await discoverCoverageCommands(state.cwd);
145
+ const verifyCommands = configuredVerify.length > 0 ? configuredVerify : await discoverVerifyCommands(commandCwd);
146
+ const coverageCommands = configuredCoverage.length > 0 ? configuredCoverage : await discoverCoverageCommands(commandCwd);
238
147
  const commands = uniqueCommands([...verifyCommands, ...coverageCommands]);
239
- return runCommands(commands, state.cwd);
148
+ return runCommands(commands, commandCwd);
240
149
  }
241
150
  // Implementer apply runs lint/format/typecheck so reviewers see standards
242
151
  // compliance evidence. Configured lintCommands win, otherwise discover them.
243
- async function runConfiguredLint(state) {
152
+ async function runConfiguredLint(state, commandCwd) {
244
153
  const config = await readConfig(state.cwd);
245
154
  const configuredLint = (config.lintCommands ?? []).filter((command) => command.trim().length > 0);
246
- const commands = configuredLint.length > 0 ? configuredLint : await discoverLintCommands(state.cwd);
247
- return runCommands(commands, state.cwd);
248
- }
249
- function changedFilesBlock(changedFiles) {
250
- if (changedFiles.length === 0) {
251
- return "No changed files were detected.";
252
- }
253
- return changedFiles.map((file) => `- ${file}`).join("\n");
155
+ const commands = configuredLint.length > 0 ? configuredLint : await discoverLintCommands(commandCwd);
156
+ return runCommands(commands, commandCwd);
254
157
  }
255
158
  function verificationBlock(results) {
256
159
  if (results.length === 0) {
@@ -260,32 +163,155 @@ function verificationBlock(results) {
260
163
  .map((result) => `### ${result.command}\n\nExit Code: ${result.exitCode}\n\nOutput:\n\n\`\`\`text\n${result.output || "(no output)"}\n\`\`\``)
261
164
  .join("\n\n");
262
165
  }
263
- function lintBlock(results) {
166
+ function verificationStatusFor(results) {
264
167
  if (results.length === 0) {
265
- return "No lint or format commands were detected.";
168
+ return "not_run";
266
169
  }
267
- return verificationBlock(results);
170
+ return results.every((result) => result.exitCode === 0) ? "passed" : "failed";
268
171
  }
269
- function appendExecutionSections(artifact, markdown, state) {
270
- if (artifact === "implementation-plan") {
271
- let next = markdown.trim();
272
- if (!next.includes("## Recorded Changes")) {
273
- next = `${next}\n\n## Recorded Changes\n\n${changedFilesBlock(state.changedFiles)}`;
172
+ function setTestingGateFromVerification(state) {
173
+ if (state.verificationStatus === "failed") {
174
+ state.gates.testing = "rejected";
175
+ state.status = "awaiting_input";
176
+ state.feedback.push({
177
+ gate: "testing",
178
+ message: "Automated verification failed. Inspect the test report, revise the implementation, or record an explicit verification waiver with its reason.",
179
+ createdAt: now(),
180
+ });
181
+ return;
182
+ }
183
+ state.gates.testing = "pending";
184
+ state.status = "awaiting_approval";
185
+ }
186
+ function executionInstructions(state, phase, workspacePath) {
187
+ if (phase === "execution") {
188
+ return `Use the native ${state.host} host agent to implement the approved change in ${workspacePath}. Do not modify the requester checkout at ${state.cwd}. When complete, call devcrew_complete_execution with a concise summary.`;
189
+ }
190
+ return `Use the native ${state.host} host agent to validate the approved change in ${workspacePath}. Then call devcrew_complete_execution with a concise summary and each command's exit code and output.`;
191
+ }
192
+ function hostCompletionResult(state, summary) {
193
+ const role = state.phase === "execution" ? "implementer" : "tester";
194
+ const artifact = artifactForPhase(state.phase);
195
+ return {
196
+ role,
197
+ backend: state.backend,
198
+ summary: `${role} completed through the native ${state.host} host: ${summary}`,
199
+ markdown: `${renderArtifact(artifact, state).trim()}\n\n## Native Host Summary\n\n${summary}\n`,
200
+ usedFallback: false,
201
+ };
202
+ }
203
+ function parseCompletionSummary(value) {
204
+ if (typeof value !== "string" || value.trim().length === 0) {
205
+ throw new Error("summary must be a non-empty string");
206
+ }
207
+ return value.trim();
208
+ }
209
+ function parseCompletionVerification(value) {
210
+ if (value === undefined) {
211
+ return [];
212
+ }
213
+ if (!Array.isArray(value)) {
214
+ throw new Error("verification must be an array");
215
+ }
216
+ return value.map((entry, index) => {
217
+ if (!entry ||
218
+ typeof entry !== "object" ||
219
+ typeof entry.command !== "string" ||
220
+ entry.command.trim().length === 0 ||
221
+ !Number.isInteger(entry.exitCode) ||
222
+ typeof entry.output !== "string" ||
223
+ typeof entry.startedAt !== "string" ||
224
+ typeof entry.completedAt !== "string") {
225
+ throw new Error(`verification[${index}] must include command, integer exitCode, output, startedAt, and completedAt`);
274
226
  }
275
- if (!next.includes("## Lint Results")) {
276
- next = `${next}\n\n## Lint Results\n\n${lintBlock(state.lintResults)}`;
227
+ return {
228
+ command: entry.command.trim(),
229
+ exitCode: entry.exitCode,
230
+ output: entry.output,
231
+ startedAt: entry.startedAt,
232
+ completedAt: entry.completedAt,
233
+ };
234
+ });
235
+ }
236
+ function appendExecutionSections(artifact, markdown, state) {
237
+ if (artifact === "architecture-review" && state.architectureReview) {
238
+ let content = markdown.trim();
239
+ if (!content.includes("## Review Decision")) {
240
+ content += `\n\n## Review Decision\n\nDecision: ${state.architectureReview.decision}\n\nSummary: ${state.architectureReview.summary}`;
277
241
  }
278
- return `${next}\n`;
242
+ return `${content}\n`;
279
243
  }
280
- if (artifact === "test-report" && !markdown.includes("## Acceptance Evidence")) {
281
- return `${markdown.trim()}\n\n## Acceptance Evidence\n\n${verificationBlock(state.verification)}\n`;
244
+ if (artifact === "test-report") {
245
+ let content = markdown.trim();
246
+ if (!content.includes("## Acceptance Evidence")) {
247
+ content += `\n\n## Acceptance Evidence\n\n${verificationBlock(state.verification)}`;
248
+ }
249
+ if (!content.includes("## Verification Outcome")) {
250
+ content += `\n\n## Verification Outcome\n\nStatus: ${state.verificationStatus}`;
251
+ }
252
+ if (state.verificationWaiver && !content.includes("## Verification Waiver")) {
253
+ content += `\n\n## Verification Waiver\n\nReason: ${state.verificationWaiver.reason}\nRecorded At: ${state.verificationWaiver.createdAt}`;
254
+ }
255
+ return `${content}\n`;
282
256
  }
283
257
  return markdown;
284
258
  }
259
+ async function writeImplementationReview(state) {
260
+ state.artifacts["implementation-review"] = await writeMarkdownArtifact(state, "implementation-review", renderArtifact("implementation-review", state));
261
+ }
285
262
  async function runCurrentPhaseRole(state, runner = runRole) {
286
- const gate = gateForPhase(state.phase);
263
+ if (state.phase === "execution") {
264
+ if (state.executionMode !== "apply") {
265
+ throw new Error("DevCrew execution phase requires apply mode");
266
+ }
267
+ const workspace = await ensureExecutionWorkspace(state);
268
+ state.executionWorkspace = workspace;
269
+ state.verification = [];
270
+ state.verificationStatus = "not_run";
271
+ delete state.verificationWaiver;
272
+ delete state.artifacts["test-report"];
273
+ await saveState(state);
274
+ if (state.executionPolicy === "interactive-host") {
275
+ state.executionInstruction = {
276
+ phase: "execution",
277
+ workspacePath: workspace.path,
278
+ instructions: executionInstructions(state, "execution", workspace.path),
279
+ createdAt: now(),
280
+ };
281
+ state.status = "awaiting_execution";
282
+ return saveState(state);
283
+ }
284
+ const result = await runner({
285
+ backend: state.backend,
286
+ role: "implementer",
287
+ phase: "execution",
288
+ request: state.request,
289
+ mode: state.mode,
290
+ executionMode: "apply",
291
+ executionPolicy: state.executionPolicy,
292
+ cwd: workspace.path,
293
+ standards: state.standards.combined,
294
+ artifactPath: artifactPath(state.cwd, state.runId, "implementation-review", state.artifactDirectory),
295
+ answers: state.answers.map((entry) => entry.answer),
296
+ feedback: state.feedback.map((entry) => `${entry.gate}: ${entry.message}`),
297
+ priorArtifacts: await readPriorArtifacts(state),
298
+ });
299
+ await captureExecutionChanges(workspace);
300
+ state.lintResults = await runConfiguredLint(state, workspace.path);
301
+ const captured = await captureExecutionChanges(workspace);
302
+ state.changedFiles = captured.changedFiles;
303
+ state.implementationDiff = captured.patch;
304
+ state.roles.push(result);
305
+ state.executionInstruction = undefined;
306
+ await writeImplementationReview(state);
307
+ state.phase = "review";
308
+ state.status = "ready";
309
+ return saveState(state);
310
+ }
311
+ const configuredGate = gateForPhase(state.phase);
312
+ const gate = gateForPhase(state.phase, state.enabledGates);
287
313
  const role = roleForPhase(state.phase);
288
- if (!gate || !role) {
314
+ if (!role) {
289
315
  const artifact = artifactForPhase(state.phase);
290
316
  const markdown = renderArtifact(artifact, state);
291
317
  state.artifacts[artifact] = await writeMarkdownArtifact(state, artifact, markdown);
@@ -294,15 +320,23 @@ async function runCurrentPhaseRole(state, runner = runRole) {
294
320
  return saveState(state);
295
321
  }
296
322
  const artifact = artifactForPhase(state.phase);
297
- const path = artifactPath(state.cwd, state.runId, artifact);
298
- // Apply-mode implementation starts from a clean tree, so changed files after
299
- // the role runs are attributable to the current DevCrew run.
300
- const applyingImplementation = state.executionMode === "apply" && state.phase === "implementation";
301
- const implementationBaseline = [];
302
- if (applyingImplementation) {
303
- await assertCleanApplyWorkspace(state.cwd);
304
- }
305
- state.roles.push(conductorDecision(state, role, gate));
323
+ const path = artifactPath(state.cwd, state.runId, artifact, state.artifactDirectory);
324
+ const applyingTesting = state.executionMode === "apply" && state.phase === "testing";
325
+ const roleCwd = applyingTesting ? state.executionWorkspace?.path : state.cwd;
326
+ if (!roleCwd) {
327
+ throw new Error("DevCrew apply testing requires an execution workspace");
328
+ }
329
+ state.roles.push(conductorDecision(state, role, gate ?? `${state.phase} automatic transition`));
330
+ if (applyingTesting && state.executionPolicy === "interactive-host") {
331
+ state.executionInstruction = {
332
+ phase: "testing",
333
+ workspacePath: roleCwd,
334
+ instructions: executionInstructions(state, "testing", roleCwd),
335
+ createdAt: now(),
336
+ };
337
+ state.status = "awaiting_execution";
338
+ return saveState(state);
339
+ }
306
340
  const result = await runner({
307
341
  backend: state.backend,
308
342
  role,
@@ -310,20 +344,31 @@ async function runCurrentPhaseRole(state, runner = runRole) {
310
344
  request: state.request,
311
345
  mode: state.mode,
312
346
  executionMode: state.executionMode,
313
- cwd: state.cwd,
347
+ executionPolicy: state.executionPolicy,
348
+ cwd: roleCwd,
314
349
  standards: state.standards.combined,
315
350
  artifactPath: path,
316
351
  answers: state.answers.map((entry) => entry.answer),
317
352
  feedback: state.feedback.map((entry) => `${entry.gate}: ${entry.message}`),
318
353
  priorArtifacts: await readPriorArtifacts(state),
319
354
  });
320
- if (applyingImplementation) {
321
- state.changedFiles = changedSinceBaseline(implementationBaseline, await listChangedLines(state.cwd));
322
- state.implementationDiff = await collectImplementationDiff(state.cwd);
323
- state.lintResults = await runConfiguredLint(state);
355
+ if (applyingTesting && state.executionWorkspace) {
356
+ state.verification = await runConfiguredVerification(state, roleCwd);
357
+ state.verificationStatus = verificationStatusFor(state.verification);
358
+ const captured = await captureExecutionChanges(state.executionWorkspace);
359
+ state.changedFiles = captured.changedFiles;
360
+ state.implementationDiff = captured.patch;
361
+ await writeImplementationReview(state);
324
362
  }
325
- if (state.executionMode === "apply" && state.phase === "testing") {
326
- state.verification = await runConfiguredVerification(state);
363
+ if (state.phase === "review") {
364
+ if (!result.reviewDecision) {
365
+ throw new Error("DevCrew architecture review must return a structured review decision");
366
+ }
367
+ state.architectureReview = {
368
+ decision: result.reviewDecision,
369
+ summary: result.summary,
370
+ reviewedAt: now(),
371
+ };
327
372
  }
328
373
  // When the backend cannot run a real SDK we keep a single deterministic
329
374
  // artifact source by rendering the rich phase template from the core layer.
@@ -331,11 +376,38 @@ async function runCurrentPhaseRole(state, runner = runRole) {
331
376
  const markdown = appendExecutionSections(artifact, baseMarkdown, state);
332
377
  state.roles.push({ ...result, markdown });
333
378
  state.artifacts[artifact] = await writeMarkdownArtifact(state, artifact, markdown);
334
- if (artifact === "implementation-plan") {
335
- state.artifacts["implementation-review"] = await writeMarkdownArtifact(state, "implementation-review", renderArtifact("implementation-review", state));
379
+ state.executionInstruction = undefined;
380
+ if (state.phase === "requirements" && result.questions && result.questions.length > 0) {
381
+ state.pendingQuestions = result.questions;
382
+ state.gates.requirements = "not_started";
383
+ state.status = "awaiting_input";
384
+ return saveState(state);
385
+ }
386
+ if (state.phase === "review" && state.architectureReview?.decision === "changes_required") {
387
+ state.gates["implementation-review"] = "rejected";
388
+ state.status = "awaiting_input";
389
+ state.feedback.push({
390
+ gate: "implementation-review",
391
+ message: state.architectureReview.summary,
392
+ createdAt: now(),
393
+ });
394
+ return saveState(state);
395
+ }
396
+ if (!gate && configuredGate) {
397
+ state.phase = nextPhaseAfterGate(state, configuredGate);
398
+ state.status = "ready";
399
+ return saveState(state);
400
+ }
401
+ if (applyingTesting) {
402
+ setTestingGateFromVerification(state);
403
+ }
404
+ else {
405
+ if (!gate) {
406
+ throw new Error(`DevCrew has no configured gate for ${state.phase}`);
407
+ }
408
+ state.gates[gate] = "pending";
409
+ state.status = "awaiting_approval";
336
410
  }
337
- state.gates[gate] = "pending";
338
- state.status = "awaiting_approval";
339
411
  return saveState(state);
340
412
  }
341
413
  export async function startOrchestratedWorkflow(input, runner = runRole) {
@@ -344,30 +416,137 @@ export async function startOrchestratedWorkflow(input, runner = runRole) {
344
416
  }
345
417
  export async function continueOrchestratedWorkflow(input, runner = runRole) {
346
418
  const state = await getWorkflowStatus(input);
347
- if (state.status === "awaiting_approval" || state.status === "awaiting_input" || state.status === "complete") {
419
+ if (state.status === "awaiting_approval" ||
420
+ state.status === "awaiting_input" ||
421
+ state.status === "awaiting_execution" ||
422
+ state.status === "complete") {
348
423
  return state;
349
424
  }
350
425
  return runCurrentPhaseRole(state, runner);
351
426
  }
352
- export async function rejectOrchestratedWorkflow(input) {
353
- const before = await getWorkflowStatus(input);
354
- const state = await rejectWorkflow(input);
355
- // Roll back implementer edits when an apply-mode implementation gate is
356
- // rejected so the next attempt starts from a clean working tree.
357
- if (before.executionMode === "apply" &&
358
- before.phase === "implementation" &&
359
- before.changedFiles.length > 0) {
360
- await revertChangedFiles(before.cwd, before.changedFiles);
361
- state.changedFiles = [];
362
- return saveState(state);
427
+ export async function approveOrchestratedWorkflow(input) {
428
+ const before = await validateWorkflowApproval(input);
429
+ const promotingTesting = input.gate === "testing" &&
430
+ before.executionMode === "apply" &&
431
+ before.phase === "testing" &&
432
+ before.status === "awaiting_approval" &&
433
+ before.gates.testing === "pending";
434
+ const cleaningCompletedPromotion = input.gate === "testing" &&
435
+ before.executionMode === "apply" &&
436
+ before.gates.testing === "approved" &&
437
+ before.executionWorkspace !== undefined;
438
+ async function cleanupAfterApproval(state) {
439
+ try {
440
+ await cleanupExecutionWorkspace(state);
441
+ }
442
+ catch {
443
+ return state;
444
+ }
445
+ state.executionWorkspace = undefined;
446
+ try {
447
+ return await saveState(state);
448
+ }
449
+ catch {
450
+ return state;
451
+ }
452
+ }
453
+ if (cleaningCompletedPromotion) {
454
+ return cleanupAfterApproval(before);
455
+ }
456
+ if (promotingTesting) {
457
+ if (before.verificationStatus === "failed" && !before.verificationWaiver) {
458
+ throw new Error("Failed verification cannot be promoted without an explicit verification waiver");
459
+ }
460
+ await promoteExecutionChanges(before);
461
+ let approved;
462
+ try {
463
+ approved = await approveWorkflow(input);
464
+ }
465
+ catch (error) {
466
+ try {
467
+ await rollbackPromotedExecutionChanges(before);
468
+ }
469
+ catch (rollbackError) {
470
+ const detail = rollbackError instanceof Error ? rollbackError.message : String(rollbackError);
471
+ throw new Error(`DevCrew approval failed and promoted patch rollback failed: ${detail}`, { cause: error });
472
+ }
473
+ throw error;
474
+ }
475
+ return cleanupAfterApproval(approved);
476
+ }
477
+ return approveWorkflow(input);
478
+ }
479
+ export async function waiveOrchestratedVerification(input) {
480
+ const state = await waiveVerificationWorkflow(input);
481
+ const reportPath = state.artifacts["test-report"];
482
+ if (reportPath) {
483
+ const report = await readFile(reportPath, "utf8");
484
+ await writeFile(reportPath, appendExecutionSections("test-report", report, state), "utf8");
363
485
  }
364
486
  return state;
365
487
  }
488
+ export async function completeOrchestratedExecution(input) {
489
+ const state = await getWorkflowStatus(input);
490
+ const instruction = state.executionInstruction;
491
+ if (state.executionMode !== "apply" ||
492
+ state.executionPolicy !== "interactive-host" ||
493
+ state.status !== "awaiting_execution" ||
494
+ !instruction ||
495
+ instruction.phase !== state.phase ||
496
+ !state.executionWorkspace ||
497
+ instruction.workspacePath !== state.executionWorkspace.path) {
498
+ throw new Error("DevCrew is not awaiting an interactive-host execution completion");
499
+ }
500
+ const summary = parseCompletionSummary(input.summary);
501
+ if (state.phase === "execution") {
502
+ if (input.verification !== undefined) {
503
+ throw new Error("verification can only be submitted after interactive-host testing");
504
+ }
505
+ const captured = await captureExecutionChanges(state.executionWorkspace);
506
+ state.changedFiles = captured.changedFiles;
507
+ state.implementationDiff = captured.patch;
508
+ state.lintResults = [];
509
+ state.roles.push(hostCompletionResult(state, summary));
510
+ state.executionInstruction = undefined;
511
+ await writeImplementationReview(state);
512
+ state.phase = "review";
513
+ state.status = "ready";
514
+ return saveState(state);
515
+ }
516
+ if (state.phase !== "testing") {
517
+ throw new Error(`Cannot complete interactive-host work during ${state.phase}`);
518
+ }
519
+ state.verification = parseCompletionVerification(input.verification);
520
+ state.verificationStatus = verificationStatusFor(state.verification);
521
+ const captured = await captureExecutionChanges(state.executionWorkspace);
522
+ state.changedFiles = captured.changedFiles;
523
+ state.implementationDiff = captured.patch;
524
+ await writeImplementationReview(state);
525
+ const result = hostCompletionResult(state, summary);
526
+ const artifact = artifactForPhase(state.phase);
527
+ const markdown = appendExecutionSections(artifact, result.markdown, state);
528
+ state.roles.push({ ...result, markdown });
529
+ state.artifacts[artifact] = await writeMarkdownArtifact(state, artifact, markdown);
530
+ state.executionInstruction = undefined;
531
+ setTestingGateFromVerification(state);
532
+ return saveState(state);
533
+ }
534
+ export async function rejectOrchestratedWorkflow(input) {
535
+ return rejectWorkflow(input);
536
+ }
366
537
  export async function answerOrchestratedWorkflow(input, runner = runRole) {
538
+ const before = await getWorkflowStatus(input);
367
539
  const state = await answerWorkflow(input, { skipArtifactWrite: true });
368
- const gate = gateForPhase(state.phase);
540
+ if (before.executionMode === "apply" &&
541
+ before.phase === "testing" &&
542
+ before.gates.testing === "rejected") {
543
+ state.phase = "execution";
544
+ state.status = "ready";
545
+ state.gates.testing = "not_started";
546
+ return saveState(state);
547
+ }
369
548
  const role = roleForPhase(state.phase);
370
- if (!gate || !role) {
549
+ if (!role) {
371
550
  return state;
372
551
  }
373
552
  // Re-run the current phase role so the artifact reflects the new answer and