@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,11 +1,12 @@
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
 
5
5
  import { runRole } from "../../adapters/src/index.js";
6
6
  import type { RoleRunInput } from "../../adapters/src/index.js";
7
7
  import {
8
8
  answerWorkflow,
9
+ approveWorkflow,
9
10
  artifactForPhase,
10
11
  artifactPath,
11
12
  ARTIFACTS,
@@ -14,13 +15,18 @@ import {
14
15
  discoverVerifyCommands,
15
16
  gateForPhase,
16
17
  getWorkflowStatus,
18
+ nextPhaseAfterGate,
17
19
  readConfig,
18
20
  rejectWorkflow,
19
21
  renderArtifact,
20
22
  saveState,
21
23
  startWorkflow,
24
+ validateWorkflowApproval,
25
+ waiveVerificationWorkflow,
22
26
  type AnswerWorkflowInput,
27
+ type ApproveWorkflowInput,
23
28
  type ArtifactName,
29
+ type CompleteExecutionInput,
24
30
  type Phase,
25
31
  type RejectWorkflowInput,
26
32
  type RoleResult,
@@ -28,7 +34,16 @@ import {
28
34
  type RunState,
29
35
  type StartWorkflowInput,
30
36
  type VerificationResult,
37
+ type VerificationStatus,
38
+ type WaiveVerificationInput,
31
39
  } from "../../core/src/index.js";
40
+ import {
41
+ captureExecutionChanges,
42
+ cleanupExecutionWorkspace,
43
+ ensureExecutionWorkspace,
44
+ promoteExecutionChanges,
45
+ rollbackPromotedExecutionChanges,
46
+ } from "./worktree.js";
32
47
 
33
48
  // Hard cap so a hung apply/verify command cannot block the serialized MCP loop.
34
49
  const COMMAND_TIMEOUT_MS = 300_000;
@@ -40,6 +55,8 @@ function roleForPhase(phase: Phase): RoleResult["role"] | undefined {
40
55
  requirements: "pm",
41
56
  architecture: "architect",
42
57
  implementation: "implementer",
58
+ execution: "implementer",
59
+ review: "architect",
43
60
  testing: "tester",
44
61
  };
45
62
  return roles[phase];
@@ -66,7 +83,7 @@ function priorArtifactNamesForPhase(phase: Phase): ArtifactName[] {
66
83
  }
67
84
 
68
85
  async function writeMarkdownArtifact(state: RunState, artifact: ArtifactName, markdown: string): Promise<string> {
69
- const path = artifactPath(state.cwd, state.runId, artifact);
86
+ const path = artifactPath(state.cwd, state.runId, artifact, state.artifactDirectory);
70
87
  await mkdir(dirname(path), { recursive: true });
71
88
  await writeFile(path, markdown, "utf8");
72
89
  return path;
@@ -159,116 +176,6 @@ export async function runShellCommand(
159
176
  });
160
177
  }
161
178
 
162
- async function runGit(args: string[], cwd: string): Promise<{ exitCode: number; stdout: string }> {
163
- return new Promise((resolveResult) => {
164
- const child = spawn("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
165
- let stdout = "";
166
- let stderr = "";
167
- child.stdout.on("data", (chunk: Buffer) => {
168
- stdout += chunk.toString("utf8");
169
- });
170
- child.stderr.on("data", (chunk: Buffer) => {
171
- stderr += chunk.toString("utf8");
172
- });
173
- child.on("error", () => resolveResult({ exitCode: 1, stdout: "" }));
174
- child.on("close", (code) => resolveResult({ exitCode: code ?? 1, stdout: stdout || stderr }));
175
- });
176
- }
177
-
178
- async function listChangedLines(cwd: string, options: { requireGit?: boolean } = {}): Promise<string[]> {
179
- const result = await runShellCommand("git status --porcelain -uall", cwd, 30_000);
180
- if (result.exitCode !== 0) {
181
- if (options.requireGit) {
182
- throw new Error("DevCrew apply mode requires a git repository so implementation changes can be reviewed and reverted.");
183
- }
184
- return [];
185
- }
186
- return result.output
187
- .split("\n")
188
- .map((line) => line.trimEnd())
189
- .filter(Boolean)
190
- .filter((line) => {
191
- const path = line.slice(3);
192
- return !path.startsWith(".devcrew/") && !path.startsWith("docs/devcrew/");
193
- });
194
- }
195
-
196
- async function assertCleanApplyWorkspace(cwd: string): Promise<void> {
197
- const changedLines = await listChangedLines(cwd, { requireGit: true });
198
- if (changedLines.length === 0) {
199
- return;
200
- }
201
- const visibleLines = changedLines.slice(0, 20).join(", ");
202
- const suffix = changedLines.length > 20 ? `, and ${changedLines.length - 20} more` : "";
203
- throw new Error(
204
- `DevCrew apply mode requires a clean working tree before implementation. Commit, stash, or remove these files: ${visibleLines}${suffix}`,
205
- );
206
- }
207
-
208
- async function collectImplementationDiff(cwd: string): Promise<string> {
209
- const result = await runShellCommand("git diff --no-ext-diff --", cwd, 30_000);
210
- if (result.exitCode !== 0) {
211
- return "";
212
- }
213
- return result.output;
214
- }
215
-
216
- // Files attributable to this run are the porcelain lines that appeared (or
217
- // changed status) since the baseline captured before the role executed. This
218
- // keeps a user's pre-existing uncommitted edits out of the changed-files list.
219
- // The porcelain status prefix is preserved for review (?? = new, M = modified).
220
- export function changedSinceBaseline(baseline: string[], current: string[]): string[] {
221
- const baselineLines = new Set(baseline);
222
- return current.filter((line) => !baselineLines.has(line));
223
- }
224
-
225
- // Strip the two-character porcelain status plus its separating space. For
226
- // rename entries ("R old -> new") the destination path is what remains
227
- // relevant, so keep only the post-arrow path when present.
228
- export function porcelainPath(line: string): string {
229
- const raw = line.slice(3).trim();
230
- const arrow = raw.indexOf(" -> ");
231
- return arrow >= 0 ? raw.slice(arrow + 4) : raw;
232
- }
233
-
234
- export interface RevertDeps {
235
- runGit?: (args: string[], cwd: string) => Promise<{ exitCode: number; stdout: string }>;
236
- removeFile?: (absolutePath: string) => Promise<void>;
237
- }
238
-
239
- // Roll back implementer edits when an apply-mode gate is rejected.
240
- // Only the files this run introduced/changed are reverted, so unrelated work in
241
- // the repository is preserved. Tracked files are restored from HEAD; files that
242
- // did not exist in HEAD are deleted. Failures are surfaced to the MCP caller.
243
- export async function revertChangedFiles(
244
- cwd: string,
245
- changedFiles: string[],
246
- deps: RevertDeps = {},
247
- ): Promise<void> {
248
- const git = deps.runGit ?? runGit;
249
- const removeFile = deps.removeFile ?? ((absolutePath: string) => rm(absolutePath, { force: true }));
250
- for (const line of changedFiles) {
251
- const file = porcelainPath(line);
252
- if (!file) {
253
- continue;
254
- }
255
- const tracked = await git(["cat-file", "-e", `HEAD:${file}`], cwd);
256
- if (tracked.exitCode === 0) {
257
- const restored = await git(["restore", "--source=HEAD", "--", file], cwd);
258
- if (restored.exitCode !== 0) {
259
- throw new Error(`Failed to restore ${file}: ${restored.stdout || "git restore failed"}`);
260
- }
261
- } else {
262
- try {
263
- await removeFile(resolvePath(cwd, file));
264
- } catch (error) {
265
- const message = error instanceof Error ? error.message : String(error);
266
- throw new Error(`Failed to remove ${file}: ${message}`);
267
- }
268
- }
269
- }
270
- }
271
-
272
179
  async function runCommands(commands: string[], cwd: string): Promise<VerificationResult[]> {
273
180
  const results: VerificationResult[] = [];
274
181
  for (const command of commands) {
@@ -293,30 +200,23 @@ function uniqueCommands(commands: string[]): string[] {
293
200
  // Tester verification runs the normal verification path first, then coverage
294
201
  // as supplemental evidence. Configured commands win per category; otherwise
295
202
  // DevCrew discovers common project commands.
296
- async function runConfiguredVerification(state: RunState): Promise<VerificationResult[]> {
203
+ async function runConfiguredVerification(state: RunState, commandCwd: string): Promise<VerificationResult[]> {
297
204
  const config = await readConfig(state.cwd);
298
205
  const configuredVerify = config.verifyCommands.filter((command) => command.trim().length > 0);
299
206
  const configuredCoverage = (config.coverageCommands ?? []).filter((command) => command.trim().length > 0);
300
- const verifyCommands = configuredVerify.length > 0 ? configuredVerify : await discoverVerifyCommands(state.cwd);
301
- const coverageCommands = configuredCoverage.length > 0 ? configuredCoverage : await discoverCoverageCommands(state.cwd);
207
+ const verifyCommands = configuredVerify.length > 0 ? configuredVerify : await discoverVerifyCommands(commandCwd);
208
+ const coverageCommands = configuredCoverage.length > 0 ? configuredCoverage : await discoverCoverageCommands(commandCwd);
302
209
  const commands = uniqueCommands([...verifyCommands, ...coverageCommands]);
303
- return runCommands(commands, state.cwd);
210
+ return runCommands(commands, commandCwd);
304
211
  }
305
212
 
306
213
  // Implementer apply runs lint/format/typecheck so reviewers see standards
307
214
  // compliance evidence. Configured lintCommands win, otherwise discover them.
308
- async function runConfiguredLint(state: RunState): Promise<VerificationResult[]> {
215
+ async function runConfiguredLint(state: RunState, commandCwd: string): Promise<VerificationResult[]> {
309
216
  const config = await readConfig(state.cwd);
310
217
  const configuredLint = (config.lintCommands ?? []).filter((command) => command.trim().length > 0);
311
- const commands = configuredLint.length > 0 ? configuredLint : await discoverLintCommands(state.cwd);
312
- return runCommands(commands, state.cwd);
313
- }
314
-
315
- function changedFilesBlock(changedFiles: string[]): string {
316
- if (changedFiles.length === 0) {
317
- return "No changed files were detected.";
318
- }
319
- return changedFiles.map((file) => `- ${file}`).join("\n");
218
+ const commands = configuredLint.length > 0 ? configuredLint : await discoverLintCommands(commandCwd);
219
+ return runCommands(commands, commandCwd);
320
220
  }
321
221
 
322
222
  function verificationBlock(results: VerificationResult[]): string {
@@ -331,34 +231,172 @@ function verificationBlock(results: VerificationResult[]): string {
331
231
  .join("\n\n");
332
232
  }
333
233
 
334
- function lintBlock(results: VerificationResult[]): string {
234
+ function verificationStatusFor(results: VerificationResult[]): VerificationStatus {
335
235
  if (results.length === 0) {
336
- return "No lint or format commands were detected.";
236
+ return "not_run";
337
237
  }
338
- return verificationBlock(results);
238
+ return results.every((result) => result.exitCode === 0) ? "passed" : "failed";
339
239
  }
340
240
 
341
- function appendExecutionSections(artifact: ArtifactName, markdown: string, state: RunState): string {
342
- if (artifact === "implementation-plan") {
343
- let next = markdown.trim();
344
- if (!next.includes("## Recorded Changes")) {
345
- next = `${next}\n\n## Recorded Changes\n\n${changedFilesBlock(state.changedFiles)}`;
241
+ function setTestingGateFromVerification(state: RunState): void {
242
+ if (state.verificationStatus === "failed") {
243
+ state.gates.testing = "rejected";
244
+ state.status = "awaiting_input";
245
+ state.feedback.push({
246
+ gate: "testing",
247
+ message: "Automated verification failed. Inspect the test report, revise the implementation, or record an explicit verification waiver with its reason.",
248
+ createdAt: now(),
249
+ });
250
+ return;
251
+ }
252
+ state.gates.testing = "pending";
253
+ state.status = "awaiting_approval";
254
+ }
255
+
256
+ function executionInstructions(state: RunState, phase: "execution" | "testing", workspacePath: string): string {
257
+ if (phase === "execution") {
258
+ 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.`;
259
+ }
260
+ 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.`;
261
+ }
262
+
263
+ function hostCompletionResult(state: RunState, summary: string): RoleResult {
264
+ const role = state.phase === "execution" ? "implementer" : "tester";
265
+ const artifact = artifactForPhase(state.phase);
266
+ return {
267
+ role,
268
+ backend: state.backend,
269
+ summary: `${role} completed through the native ${state.host} host: ${summary}`,
270
+ markdown: `${renderArtifact(artifact, state).trim()}\n\n## Native Host Summary\n\n${summary}\n`,
271
+ usedFallback: false,
272
+ };
273
+ }
274
+
275
+ function parseCompletionSummary(value: unknown): string {
276
+ if (typeof value !== "string" || value.trim().length === 0) {
277
+ throw new Error("summary must be a non-empty string");
278
+ }
279
+ return value.trim();
280
+ }
281
+
282
+ function parseCompletionVerification(value: unknown): VerificationResult[] {
283
+ if (value === undefined) {
284
+ return [];
285
+ }
286
+ if (!Array.isArray(value)) {
287
+ throw new Error("verification must be an array");
288
+ }
289
+ return value.map((entry, index) => {
290
+ if (
291
+ !entry ||
292
+ typeof entry !== "object" ||
293
+ typeof entry.command !== "string" ||
294
+ entry.command.trim().length === 0 ||
295
+ !Number.isInteger(entry.exitCode) ||
296
+ typeof entry.output !== "string" ||
297
+ typeof entry.startedAt !== "string" ||
298
+ typeof entry.completedAt !== "string"
299
+ ) {
300
+ throw new Error(`verification[${index}] must include command, integer exitCode, output, startedAt, and completedAt`);
346
301
  }
347
- if (!next.includes("## Lint Results")) {
348
- next = `${next}\n\n## Lint Results\n\n${lintBlock(state.lintResults)}`;
302
+ return {
303
+ command: entry.command.trim(),
304
+ exitCode: entry.exitCode,
305
+ output: entry.output,
306
+ startedAt: entry.startedAt,
307
+ completedAt: entry.completedAt,
308
+ };
309
+ });
310
+ }
311
+
312
+ function appendExecutionSections(artifact: ArtifactName, markdown: string, state: RunState): string {
313
+ if (artifact === "architecture-review" && state.architectureReview) {
314
+ let content = markdown.trim();
315
+ if (!content.includes("## Review Decision")) {
316
+ content += `\n\n## Review Decision\n\nDecision: ${state.architectureReview.decision}\n\nSummary: ${state.architectureReview.summary}`;
349
317
  }
350
- return `${next}\n`;
318
+ return `${content}\n`;
351
319
  }
352
- if (artifact === "test-report" && !markdown.includes("## Acceptance Evidence")) {
353
- return `${markdown.trim()}\n\n## Acceptance Evidence\n\n${verificationBlock(state.verification)}\n`;
320
+ if (artifact === "test-report") {
321
+ let content = markdown.trim();
322
+ if (!content.includes("## Acceptance Evidence")) {
323
+ content += `\n\n## Acceptance Evidence\n\n${verificationBlock(state.verification)}`;
324
+ }
325
+ if (!content.includes("## Verification Outcome")) {
326
+ content += `\n\n## Verification Outcome\n\nStatus: ${state.verificationStatus}`;
327
+ }
328
+ if (state.verificationWaiver && !content.includes("## Verification Waiver")) {
329
+ content += `\n\n## Verification Waiver\n\nReason: ${state.verificationWaiver.reason}\nRecorded At: ${state.verificationWaiver.createdAt}`;
330
+ }
331
+ return `${content}\n`;
354
332
  }
355
333
  return markdown;
356
334
  }
357
335
 
336
+ async function writeImplementationReview(state: RunState): Promise<void> {
337
+ state.artifacts["implementation-review"] = await writeMarkdownArtifact(
338
+ state,
339
+ "implementation-review",
340
+ renderArtifact("implementation-review", state),
341
+ );
342
+ }
343
+
358
344
  async function runCurrentPhaseRole(state: RunState, runner: RoleRunner = runRole): Promise<RunState> {
359
- const gate = gateForPhase(state.phase);
345
+ if (state.phase === "execution") {
346
+ if (state.executionMode !== "apply") {
347
+ throw new Error("DevCrew execution phase requires apply mode");
348
+ }
349
+ const workspace = await ensureExecutionWorkspace(state);
350
+ state.executionWorkspace = workspace;
351
+ state.verification = [];
352
+ state.verificationStatus = "not_run";
353
+ delete state.verificationWaiver;
354
+ delete state.artifacts["test-report"];
355
+ await saveState(state);
356
+
357
+ if (state.executionPolicy === "interactive-host") {
358
+ state.executionInstruction = {
359
+ phase: "execution",
360
+ workspacePath: workspace.path,
361
+ instructions: executionInstructions(state, "execution", workspace.path),
362
+ createdAt: now(),
363
+ };
364
+ state.status = "awaiting_execution";
365
+ return saveState(state);
366
+ }
367
+
368
+ const result = await runner({
369
+ backend: state.backend,
370
+ role: "implementer",
371
+ phase: "execution",
372
+ request: state.request,
373
+ mode: state.mode,
374
+ executionMode: "apply",
375
+ executionPolicy: state.executionPolicy,
376
+ cwd: workspace.path,
377
+ standards: state.standards.combined,
378
+ artifactPath: artifactPath(state.cwd, state.runId, "implementation-review", state.artifactDirectory),
379
+ answers: state.answers.map((entry) => entry.answer),
380
+ feedback: state.feedback.map((entry) => `${entry.gate}: ${entry.message}`),
381
+ priorArtifacts: await readPriorArtifacts(state),
382
+ });
383
+ await captureExecutionChanges(workspace);
384
+ state.lintResults = await runConfiguredLint(state, workspace.path);
385
+ const captured = await captureExecutionChanges(workspace);
386
+ state.changedFiles = captured.changedFiles;
387
+ state.implementationDiff = captured.patch;
388
+ state.roles.push(result);
389
+ state.executionInstruction = undefined;
390
+ await writeImplementationReview(state);
391
+ state.phase = "review";
392
+ state.status = "ready";
393
+ return saveState(state);
394
+ }
395
+
396
+ const configuredGate = gateForPhase(state.phase);
397
+ const gate = gateForPhase(state.phase, state.enabledGates);
360
398
  const role = roleForPhase(state.phase);
361
- if (!gate || !role) {
399
+ if (!role) {
362
400
  const artifact = artifactForPhase(state.phase);
363
401
  const markdown = renderArtifact(artifact, state);
364
402
  state.artifacts[artifact] = await writeMarkdownArtifact(state, artifact, markdown);
@@ -368,17 +406,24 @@ async function runCurrentPhaseRole(state: RunState, runner: RoleRunner = runRole
368
406
  }
369
407
 
370
408
  const artifact = artifactForPhase(state.phase);
371
- const path = artifactPath(state.cwd, state.runId, artifact);
372
-
373
- // Apply-mode implementation starts from a clean tree, so changed files after
374
- // the role runs are attributable to the current DevCrew run.
375
- const applyingImplementation = state.executionMode === "apply" && state.phase === "implementation";
376
- const implementationBaseline: string[] = [];
377
- if (applyingImplementation) {
378
- await assertCleanApplyWorkspace(state.cwd);
409
+ const path = artifactPath(state.cwd, state.runId, artifact, state.artifactDirectory);
410
+ const applyingTesting = state.executionMode === "apply" && state.phase === "testing";
411
+ const roleCwd = applyingTesting ? state.executionWorkspace?.path : state.cwd;
412
+ if (!roleCwd) {
413
+ throw new Error("DevCrew apply testing requires an execution workspace");
414
+ }
415
+ state.roles.push(conductorDecision(state, role, gate ?? `${state.phase} automatic transition`));
416
+
417
+ if (applyingTesting && state.executionPolicy === "interactive-host") {
418
+ state.executionInstruction = {
419
+ phase: "testing",
420
+ workspacePath: roleCwd,
421
+ instructions: executionInstructions(state, "testing", roleCwd),
422
+ createdAt: now(),
423
+ };
424
+ state.status = "awaiting_execution";
425
+ return saveState(state);
379
426
  }
380
-
381
- state.roles.push(conductorDecision(state, role, gate));
382
427
 
383
428
  const result = await runner({
384
429
  backend: state.backend,
@@ -387,7 +432,8 @@ async function runCurrentPhaseRole(state: RunState, runner: RoleRunner = runRole
387
432
  request: state.request,
388
433
  mode: state.mode,
389
434
  executionMode: state.executionMode,
390
- cwd: state.cwd,
435
+ executionPolicy: state.executionPolicy,
436
+ cwd: roleCwd,
391
437
  standards: state.standards.combined,
392
438
  artifactPath: path,
393
439
  answers: state.answers.map((entry) => entry.answer),
@@ -395,13 +441,24 @@ async function runCurrentPhaseRole(state: RunState, runner: RoleRunner = runRole
395
441
  priorArtifacts: await readPriorArtifacts(state),
396
442
  });
397
443
 
398
- if (applyingImplementation) {
399
- state.changedFiles = changedSinceBaseline(implementationBaseline, await listChangedLines(state.cwd));
400
- state.implementationDiff = await collectImplementationDiff(state.cwd);
401
- state.lintResults = await runConfiguredLint(state);
444
+ if (applyingTesting && state.executionWorkspace) {
445
+ state.verification = await runConfiguredVerification(state, roleCwd);
446
+ state.verificationStatus = verificationStatusFor(state.verification);
447
+ const captured = await captureExecutionChanges(state.executionWorkspace);
448
+ state.changedFiles = captured.changedFiles;
449
+ state.implementationDiff = captured.patch;
450
+ await writeImplementationReview(state);
402
451
  }
403
- if (state.executionMode === "apply" && state.phase === "testing") {
404
- state.verification = await runConfiguredVerification(state);
452
+
453
+ if (state.phase === "review") {
454
+ if (!result.reviewDecision) {
455
+ throw new Error("DevCrew architecture review must return a structured review decision");
456
+ }
457
+ state.architectureReview = {
458
+ decision: result.reviewDecision,
459
+ summary: result.summary,
460
+ reviewedAt: now(),
461
+ };
405
462
  }
406
463
 
407
464
  // When the backend cannot run a real SDK we keep a single deterministic
@@ -410,15 +467,37 @@ async function runCurrentPhaseRole(state: RunState, runner: RoleRunner = runRole
410
467
  const markdown = appendExecutionSections(artifact, baseMarkdown, state);
411
468
  state.roles.push({ ...result, markdown });
412
469
  state.artifacts[artifact] = await writeMarkdownArtifact(state, artifact, markdown);
413
- if (artifact === "implementation-plan") {
414
- state.artifacts["implementation-review"] = await writeMarkdownArtifact(
415
- state,
416
- "implementation-review",
417
- renderArtifact("implementation-review", state),
418
- );
419
- }
420
- state.gates[gate] = "pending";
421
- state.status = "awaiting_approval";
470
+ state.executionInstruction = undefined;
471
+ if (state.phase === "requirements" && result.questions && result.questions.length > 0) {
472
+ state.pendingQuestions = result.questions;
473
+ state.gates.requirements = "not_started";
474
+ state.status = "awaiting_input";
475
+ return saveState(state);
476
+ }
477
+ if (state.phase === "review" && state.architectureReview?.decision === "changes_required") {
478
+ state.gates["implementation-review"] = "rejected";
479
+ state.status = "awaiting_input";
480
+ state.feedback.push({
481
+ gate: "implementation-review",
482
+ message: state.architectureReview.summary,
483
+ createdAt: now(),
484
+ });
485
+ return saveState(state);
486
+ }
487
+ if (!gate && configuredGate) {
488
+ state.phase = nextPhaseAfterGate(state, configuredGate);
489
+ state.status = "ready";
490
+ return saveState(state);
491
+ }
492
+ if (applyingTesting) {
493
+ setTestingGateFromVerification(state);
494
+ } else {
495
+ if (!gate) {
496
+ throw new Error(`DevCrew has no configured gate for ${state.phase}`);
497
+ }
498
+ state.gates[gate] = "pending";
499
+ state.status = "awaiting_approval";
500
+ }
422
501
  return saveState(state);
423
502
  }
424
503
 
@@ -429,37 +508,153 @@ export async function startOrchestratedWorkflow(input: StartWorkflowInput, runne
429
508
 
430
509
  export async function continueOrchestratedWorkflow(input: RunRef, runner: RoleRunner = runRole): Promise<RunState> {
431
510
  const state = await getWorkflowStatus(input);
432
- if (state.status === "awaiting_approval" || state.status === "awaiting_input" || state.status === "complete") {
511
+ if (
512
+ state.status === "awaiting_approval" ||
513
+ state.status === "awaiting_input" ||
514
+ state.status === "awaiting_execution" ||
515
+ state.status === "complete"
516
+ ) {
433
517
  return state;
434
518
  }
435
519
 
436
520
  return runCurrentPhaseRole(state, runner);
437
521
  }
438
522
 
439
- export async function rejectOrchestratedWorkflow(input: RejectWorkflowInput): Promise<RunState> {
440
- const before = await getWorkflowStatus(input);
441
- const state = await rejectWorkflow(input);
523
+ export async function approveOrchestratedWorkflow(input: ApproveWorkflowInput): Promise<RunState> {
524
+ const before = await validateWorkflowApproval(input);
525
+ const promotingTesting =
526
+ input.gate === "testing" &&
527
+ before.executionMode === "apply" &&
528
+ before.phase === "testing" &&
529
+ before.status === "awaiting_approval" &&
530
+ before.gates.testing === "pending";
531
+ const cleaningCompletedPromotion =
532
+ input.gate === "testing" &&
533
+ before.executionMode === "apply" &&
534
+ before.gates.testing === "approved" &&
535
+ before.executionWorkspace !== undefined;
536
+
537
+ async function cleanupAfterApproval(state: RunState): Promise<RunState> {
538
+ try {
539
+ await cleanupExecutionWorkspace(state);
540
+ } catch {
541
+ return state;
542
+ }
543
+ state.executionWorkspace = undefined;
544
+ try {
545
+ return await saveState(state);
546
+ } catch {
547
+ return state;
548
+ }
549
+ }
550
+
551
+ if (cleaningCompletedPromotion) {
552
+ return cleanupAfterApproval(before);
553
+ }
554
+ if (promotingTesting) {
555
+ if (before.verificationStatus === "failed" && !before.verificationWaiver) {
556
+ throw new Error("Failed verification cannot be promoted without an explicit verification waiver");
557
+ }
558
+ await promoteExecutionChanges(before);
559
+ let approved: RunState;
560
+ try {
561
+ approved = await approveWorkflow(input);
562
+ } catch (error) {
563
+ try {
564
+ await rollbackPromotedExecutionChanges(before);
565
+ } catch (rollbackError) {
566
+ const detail = rollbackError instanceof Error ? rollbackError.message : String(rollbackError);
567
+ throw new Error(`DevCrew approval failed and promoted patch rollback failed: ${detail}`, { cause: error });
568
+ }
569
+ throw error;
570
+ }
571
+ return cleanupAfterApproval(approved);
572
+ }
573
+
574
+ return approveWorkflow(input);
575
+ }
576
+
577
+ export async function waiveOrchestratedVerification(input: WaiveVerificationInput): Promise<RunState> {
578
+ const state = await waiveVerificationWorkflow(input);
579
+ const reportPath = state.artifacts["test-report"];
580
+ if (reportPath) {
581
+ const report = await readFile(reportPath, "utf8");
582
+ await writeFile(reportPath, appendExecutionSections("test-report", report, state), "utf8");
583
+ }
584
+ return state;
585
+ }
442
586
 
443
- // Roll back implementer edits when an apply-mode implementation gate is
444
- // rejected so the next attempt starts from a clean working tree.
587
+ export async function completeOrchestratedExecution(input: CompleteExecutionInput): Promise<RunState> {
588
+ const state = await getWorkflowStatus(input);
589
+ const instruction = state.executionInstruction;
445
590
  if (
446
- before.executionMode === "apply" &&
447
- before.phase === "implementation" &&
448
- before.changedFiles.length > 0
591
+ state.executionMode !== "apply" ||
592
+ state.executionPolicy !== "interactive-host" ||
593
+ state.status !== "awaiting_execution" ||
594
+ !instruction ||
595
+ instruction.phase !== state.phase ||
596
+ !state.executionWorkspace ||
597
+ instruction.workspacePath !== state.executionWorkspace.path
449
598
  ) {
450
- await revertChangedFiles(before.cwd, before.changedFiles);
451
- state.changedFiles = [];
599
+ throw new Error("DevCrew is not awaiting an interactive-host execution completion");
600
+ }
601
+ const summary = parseCompletionSummary(input.summary);
602
+
603
+ if (state.phase === "execution") {
604
+ if (input.verification !== undefined) {
605
+ throw new Error("verification can only be submitted after interactive-host testing");
606
+ }
607
+ const captured = await captureExecutionChanges(state.executionWorkspace);
608
+ state.changedFiles = captured.changedFiles;
609
+ state.implementationDiff = captured.patch;
610
+ state.lintResults = [];
611
+ state.roles.push(hostCompletionResult(state, summary));
612
+ state.executionInstruction = undefined;
613
+ await writeImplementationReview(state);
614
+ state.phase = "review";
615
+ state.status = "ready";
452
616
  return saveState(state);
453
617
  }
454
618
 
455
- return state;
619
+ if (state.phase !== "testing") {
620
+ throw new Error(`Cannot complete interactive-host work during ${state.phase}`);
621
+ }
622
+ state.verification = parseCompletionVerification(input.verification);
623
+ state.verificationStatus = verificationStatusFor(state.verification);
624
+ const captured = await captureExecutionChanges(state.executionWorkspace);
625
+ state.changedFiles = captured.changedFiles;
626
+ state.implementationDiff = captured.patch;
627
+ await writeImplementationReview(state);
628
+ const result = hostCompletionResult(state, summary);
629
+ const artifact = artifactForPhase(state.phase);
630
+ const markdown = appendExecutionSections(artifact, result.markdown, state);
631
+ state.roles.push({ ...result, markdown });
632
+ state.artifacts[artifact] = await writeMarkdownArtifact(state, artifact, markdown);
633
+ state.executionInstruction = undefined;
634
+ setTestingGateFromVerification(state);
635
+ return saveState(state);
636
+ }
637
+
638
+ export async function rejectOrchestratedWorkflow(input: RejectWorkflowInput): Promise<RunState> {
639
+ return rejectWorkflow(input);
456
640
  }
457
641
 
458
642
  export async function answerOrchestratedWorkflow(input: AnswerWorkflowInput, runner: RoleRunner = runRole): Promise<RunState> {
643
+ const before = await getWorkflowStatus(input);
459
644
  const state = await answerWorkflow(input, { skipArtifactWrite: true });
460
- const gate = gateForPhase(state.phase);
645
+ if (
646
+ before.executionMode === "apply" &&
647
+ before.phase === "testing" &&
648
+ before.gates.testing === "rejected"
649
+ ) {
650
+ state.phase = "execution";
651
+ state.status = "ready";
652
+ state.gates.testing = "not_started";
653
+ return saveState(state);
654
+ }
655
+
461
656
  const role = roleForPhase(state.phase);
462
- if (!gate || !role) {
657
+ if (!role) {
463
658
  return state;
464
659
  }
465
660