@shenlee/devcrew 0.1.2 → 0.1.4

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 (53) hide show
  1. package/README.md +15 -7
  2. package/README.zh-CN.md +12 -7
  3. package/dist/packages/adapters/src/index.js +204 -10
  4. package/dist/packages/core/src/active-run.js +13 -1
  5. package/dist/packages/core/src/artifacts.js +14 -4
  6. package/dist/packages/core/src/config.js +88 -17
  7. package/dist/packages/core/src/index.js +1 -0
  8. package/dist/packages/core/src/lock.js +89 -0
  9. package/dist/packages/core/src/paths.js +10 -6
  10. package/dist/packages/core/src/store.js +40 -0
  11. package/dist/packages/core/src/types.js +4 -1
  12. package/dist/packages/core/src/validation.js +7 -1
  13. package/dist/packages/core/src/version.js +1 -1
  14. package/dist/packages/core/src/workflow.js +87 -12
  15. package/dist/packages/orchestrator/src/index.js +317 -18
  16. package/dist/packages/plugins/src/index.js +2 -32
  17. package/dist/packages/service/src/tools.js +117 -17
  18. package/docs/claude-code.md +18 -3
  19. package/docs/codex.md +22 -5
  20. package/docs/quickstart.md +1 -1
  21. package/docs/superpowers/plans/2026-07-13-safety-semantics.md +313 -0
  22. package/docs/superpowers/plans/2026-07-14-p0-remediation.md +213 -0
  23. package/docs/superpowers/plans/2026-07-14-reliability-recovery.md +208 -0
  24. package/docs/superpowers/plans/2026-07-14-structured-role-results.md +231 -0
  25. package/docs/superpowers/specs/2026-07-13-execution-boundaries-design.md +126 -0
  26. package/docs/superpowers/specs/2026-07-14-p0-remediation-design.md +52 -0
  27. package/docs/superpowers/specs/2026-07-14-reliability-recovery-design.md +92 -0
  28. package/docs/superpowers/specs/2026-07-14-structured-role-results-design.md +96 -0
  29. package/docs/workflow.md +28 -4
  30. package/package.json +1 -1
  31. package/packages/adapters/src/index.ts +250 -13
  32. package/packages/core/src/active-run.ts +13 -1
  33. package/packages/core/src/artifacts.ts +16 -4
  34. package/packages/core/src/config.ts +97 -18
  35. package/packages/core/src/index.ts +1 -0
  36. package/packages/core/src/lock.ts +104 -0
  37. package/packages/core/src/paths.ts +11 -6
  38. package/packages/core/src/store.ts +45 -1
  39. package/packages/core/src/types.ts +92 -2
  40. package/packages/core/src/validation.ts +10 -0
  41. package/packages/core/src/version.ts +1 -1
  42. package/packages/core/src/workflow.ts +101 -11
  43. package/packages/orchestrator/src/index.ts +349 -19
  44. package/packages/plugins/src/index.ts +2 -44
  45. package/packages/service/src/tools.ts +126 -15
  46. package/plugins/devcrew-codex/.codex-plugin/plugin.json +1 -1
  47. package/plugins/devcrew-codex/.mcp.json +1 -1
  48. package/plugins/devcrew-codex/skills/devcrew/SKILL.md +8 -7
  49. package/scripts/smoke-codex-plugin.mjs +1 -1
  50. package/plugins/devcrew-codex/agents/architect.toml +0 -12
  51. package/plugins/devcrew-codex/agents/implementer.toml +0 -12
  52. package/plugins/devcrew-codex/agents/pm.toml +0 -13
  53. package/plugins/devcrew-codex/agents/tester.toml +0 -12
@@ -5,6 +5,7 @@ import { dirname } from "node:path";
5
5
  import { runRole } from "../../adapters/src/index.js";
6
6
  import type { RoleRunInput } from "../../adapters/src/index.js";
7
7
  import {
8
+ abortWorkflow,
8
9
  answerWorkflow,
9
10
  approveWorkflow,
10
11
  artifactForPhase,
@@ -15,15 +16,19 @@ import {
15
16
  discoverVerifyCommands,
16
17
  gateForPhase,
17
18
  getWorkflowStatus,
19
+ nextPhaseAfterGate,
18
20
  readConfig,
19
21
  rejectWorkflow,
20
22
  renderArtifact,
21
23
  saveState,
22
24
  startWorkflow,
23
25
  validateWorkflowApproval,
26
+ waiveVerificationWorkflow,
24
27
  type AnswerWorkflowInput,
28
+ type AbortWorkflowInput,
25
29
  type ApproveWorkflowInput,
26
30
  type ArtifactName,
31
+ type CompleteExecutionInput,
27
32
  type Phase,
28
33
  type RejectWorkflowInput,
29
34
  type RoleResult,
@@ -31,6 +36,8 @@ import {
31
36
  type RunState,
32
37
  type StartWorkflowInput,
33
38
  type VerificationResult,
39
+ type VerificationStatus,
40
+ type WaiveVerificationInput,
34
41
  } from "../../core/src/index.js";
35
42
  import {
36
43
  captureExecutionChanges,
@@ -44,13 +51,15 @@ import {
44
51
  const COMMAND_TIMEOUT_MS = 300_000;
45
52
 
46
53
  type RoleRunner = (input: RoleRunInput) => Promise<RoleResult>;
54
+ type ExecutingRole = Exclude<RoleResult["role"], "conductor">;
47
55
 
48
- function roleForPhase(phase: Phase): RoleResult["role"] | undefined {
49
- const roles: Partial<Record<Phase, RoleResult["role"]>> = {
56
+ function roleForPhase(phase: Phase): ExecutingRole | undefined {
57
+ const roles: Partial<Record<Phase, ExecutingRole>> = {
50
58
  requirements: "pm",
51
59
  architecture: "architect",
52
60
  implementation: "implementer",
53
61
  execution: "implementer",
62
+ review: "architect",
54
63
  testing: "tester",
55
64
  };
56
65
  return roles[phase];
@@ -64,6 +73,7 @@ function conductorDecision(state: RunState, role: RoleResult["role"], gate: stri
64
73
  summary,
65
74
  markdown: `# Conductor Decision\n\n${summary}\n`,
66
75
  usedFallback: false,
76
+ format: "legacy",
67
77
  };
68
78
  }
69
79
 
@@ -77,7 +87,7 @@ function priorArtifactNamesForPhase(phase: Phase): ArtifactName[] {
77
87
  }
78
88
 
79
89
  async function writeMarkdownArtifact(state: RunState, artifact: ArtifactName, markdown: string): Promise<string> {
80
- const path = artifactPath(state.cwd, state.runId, artifact);
90
+ const path = artifactPath(state.cwd, state.runId, artifact, state.artifactDirectory);
81
91
  await mkdir(dirname(path), { recursive: true });
82
92
  await writeFile(path, markdown, "utf8");
83
93
  return path;
@@ -225,11 +235,151 @@ function verificationBlock(results: VerificationResult[]): string {
225
235
  .join("\n\n");
226
236
  }
227
237
 
228
- function appendExecutionSections(artifact: ArtifactName, markdown: string, state: RunState): string {
229
- if (artifact === "test-report" && !markdown.includes("## Acceptance Evidence")) {
230
- return `${markdown.trim()}\n\n## Acceptance Evidence\n\n${verificationBlock(state.verification)}\n`;
238
+ function verificationStatusFor(results: VerificationResult[]): VerificationStatus {
239
+ if (results.length === 0) {
240
+ return "not_run";
241
+ }
242
+ return results.every((result) => result.exitCode === 0) ? "passed" : "failed";
243
+ }
244
+
245
+ function setTestingGateFromVerification(state: RunState): void {
246
+ if (state.verificationStatus !== "passed") {
247
+ state.gates.testing = "rejected";
248
+ state.status = "awaiting_input";
249
+ state.feedback.push({
250
+ gate: "testing",
251
+ message:
252
+ state.verificationStatus === "failed"
253
+ ? "Automated verification failed. Inspect the test report, revise the implementation, or record an explicit verification waiver with its reason."
254
+ : "No verification evidence was recorded. Configure or run validation, revise the implementation, or record an explicit verification waiver with its reason.",
255
+ createdAt: now(),
256
+ });
257
+ return;
258
+ }
259
+ state.gates.testing = "pending";
260
+ state.status = "awaiting_approval";
261
+ }
262
+
263
+ function executionInstructions(state: RunState, phase: "execution" | "testing", workspacePath: string): string {
264
+ if (phase === "execution") {
265
+ 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.`;
266
+ }
267
+ 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.`;
268
+ }
269
+
270
+ function hostCompletionResult(state: RunState, summary: string): RoleResult {
271
+ const role = state.phase === "execution" ? "implementer" : "tester";
272
+ const artifact = artifactForPhase(state.phase);
273
+ return {
274
+ role,
275
+ backend: state.backend,
276
+ summary: `${role} completed through the native ${state.host} host: ${summary}`,
277
+ markdown: `${renderArtifact(artifact, state).trim()}\n\n## Native Host Summary\n\n${summary}\n`,
278
+ usedFallback: false,
279
+ format: "legacy",
280
+ };
281
+ }
282
+
283
+ function parseCompletionSummary(value: unknown): string {
284
+ if (typeof value !== "string" || value.trim().length === 0) {
285
+ throw new Error("summary must be a non-empty string");
286
+ }
287
+ return value.trim();
288
+ }
289
+
290
+ function parseCompletionVerification(value: unknown): VerificationResult[] {
291
+ if (value === undefined) {
292
+ return [];
293
+ }
294
+ if (!Array.isArray(value)) {
295
+ throw new Error("verification must be an array");
296
+ }
297
+ return value.map((entry, index) => {
298
+ if (
299
+ !entry ||
300
+ typeof entry !== "object" ||
301
+ typeof entry.command !== "string" ||
302
+ entry.command.trim().length === 0 ||
303
+ !Number.isInteger(entry.exitCode) ||
304
+ typeof entry.output !== "string" ||
305
+ typeof entry.startedAt !== "string" ||
306
+ typeof entry.completedAt !== "string"
307
+ ) {
308
+ throw new Error(`verification[${index}] must include command, integer exitCode, output, startedAt, and completedAt`);
309
+ }
310
+ return {
311
+ command: entry.command.trim(),
312
+ exitCode: entry.exitCode,
313
+ output: entry.output,
314
+ startedAt: entry.startedAt,
315
+ completedAt: entry.completedAt,
316
+ };
317
+ });
318
+ }
319
+
320
+ function structuredRoleResultBlock(result: RoleResult): string {
321
+ if (result.format !== "structured" || !result.structured) {
322
+ return "";
323
+ }
324
+ const data = result.structured;
325
+ const sections = [
326
+ "## Structured Role Result",
327
+ "",
328
+ `Schema Version: ${data.schemaVersion}`,
329
+ `Summary: ${data.summary}`,
330
+ ];
331
+ if (data.questions?.length) {
332
+ sections.push("", "### Questions", "", ...data.questions.map((question) => `- ${question.id}: ${question.prompt}`));
333
+ }
334
+ if (data.decisions?.length) {
335
+ sections.push("", "### Decisions", "", ...data.decisions.map((decision) => `- ${decision}`));
336
+ }
337
+ if (data.changedFiles?.length) {
338
+ sections.push("", "### Changed Files", "", ...data.changedFiles.map((file) => `- ${file}`));
339
+ }
340
+ if (data.evidence.length) {
341
+ sections.push("", "### Command Evidence", "");
342
+ for (const evidence of data.evidence) {
343
+ sections.push(`- \`${evidence.command}\` — exit code ${evidence.exitCode}`);
344
+ if (evidence.output) {
345
+ sections.push("", "```text", evidence.output, "```");
346
+ }
347
+ }
231
348
  }
232
- return markdown;
349
+ if (data.testCases?.length) {
350
+ sections.push("", "### Test Cases", "", "| ID | Scenario | Type | Expected |", "| --- | --- | --- | --- |");
351
+ sections.push(...data.testCases.map((testCase) => `| ${testCase.id} | ${testCase.scenario} | ${testCase.type} | ${testCase.expected} |`));
352
+ }
353
+ if (data.risks.length) {
354
+ sections.push("", "### Risks", "", ...data.risks.map((risk) => `- ${risk}`));
355
+ }
356
+ return `\n\n${sections.join("\n")}`;
357
+ }
358
+
359
+ function appendExecutionSections(artifact: ArtifactName, markdown: string, state: RunState, result?: RoleResult): string {
360
+ let content = markdown.trim();
361
+ if (result?.format === "structured" && !content.includes("## Structured Role Result")) {
362
+ content += structuredRoleResultBlock(result);
363
+ }
364
+ if (artifact === "architecture-review" && state.architectureReview) {
365
+ if (!content.includes("## Review Decision")) {
366
+ content += `\n\n## Review Decision\n\nDecision: ${state.architectureReview.decision}\n\nSummary: ${state.architectureReview.summary}`;
367
+ }
368
+ return `${content}\n`;
369
+ }
370
+ if (artifact === "test-report") {
371
+ if (!content.includes("## Acceptance Evidence")) {
372
+ content += `\n\n## Acceptance Evidence\n\n${verificationBlock(state.verification)}`;
373
+ }
374
+ if (!content.includes("## Verification Outcome")) {
375
+ content += `\n\n## Verification Outcome\n\nStatus: ${state.verificationStatus}`;
376
+ }
377
+ if (state.verificationWaiver && !content.includes("## Verification Waiver")) {
378
+ content += `\n\n## Verification Waiver\n\nReason: ${state.verificationWaiver.reason}\nRecorded At: ${state.verificationWaiver.createdAt}`;
379
+ }
380
+ return `${content}\n`;
381
+ }
382
+ return `${content}\n`;
233
383
  }
234
384
 
235
385
  async function writeImplementationReview(state: RunState): Promise<void> {
@@ -248,9 +398,22 @@ async function runCurrentPhaseRole(state: RunState, runner: RoleRunner = runRole
248
398
  const workspace = await ensureExecutionWorkspace(state);
249
399
  state.executionWorkspace = workspace;
250
400
  state.verification = [];
401
+ state.verificationStatus = "not_run";
402
+ delete state.verificationWaiver;
251
403
  delete state.artifacts["test-report"];
252
404
  await saveState(state);
253
405
 
406
+ if (state.executionPolicy === "interactive-host") {
407
+ state.executionInstruction = {
408
+ phase: "execution",
409
+ workspacePath: workspace.path,
410
+ instructions: executionInstructions(state, "execution", workspace.path),
411
+ createdAt: now(),
412
+ };
413
+ state.status = "awaiting_execution";
414
+ return saveState(state);
415
+ }
416
+
254
417
  const result = await runner({
255
418
  backend: state.backend,
256
419
  role: "implementer",
@@ -258,9 +421,10 @@ async function runCurrentPhaseRole(state: RunState, runner: RoleRunner = runRole
258
421
  request: state.request,
259
422
  mode: state.mode,
260
423
  executionMode: "apply",
424
+ executionPolicy: state.executionPolicy,
261
425
  cwd: workspace.path,
262
426
  standards: state.standards.combined,
263
- artifactPath: artifactPath(state.cwd, state.runId, "implementation-review"),
427
+ artifactPath: artifactPath(state.cwd, state.runId, "implementation-review", state.artifactDirectory),
264
428
  answers: state.answers.map((entry) => entry.answer),
265
429
  feedback: state.feedback.map((entry) => `${entry.gate}: ${entry.message}`),
266
430
  priorArtifacts: await readPriorArtifacts(state),
@@ -271,15 +435,17 @@ async function runCurrentPhaseRole(state: RunState, runner: RoleRunner = runRole
271
435
  state.changedFiles = captured.changedFiles;
272
436
  state.implementationDiff = captured.patch;
273
437
  state.roles.push(result);
438
+ state.executionInstruction = undefined;
274
439
  await writeImplementationReview(state);
275
- state.phase = "testing";
440
+ state.phase = "review";
276
441
  state.status = "ready";
277
442
  return saveState(state);
278
443
  }
279
444
 
280
- const gate = gateForPhase(state.phase);
445
+ const configuredGate = gateForPhase(state.phase);
446
+ const gate = gateForPhase(state.phase, state.enabledGates);
281
447
  const role = roleForPhase(state.phase);
282
- if (!gate || !role) {
448
+ if (!role) {
283
449
  const artifact = artifactForPhase(state.phase);
284
450
  const markdown = renderArtifact(artifact, state);
285
451
  state.artifacts[artifact] = await writeMarkdownArtifact(state, artifact, markdown);
@@ -289,13 +455,24 @@ async function runCurrentPhaseRole(state: RunState, runner: RoleRunner = runRole
289
455
  }
290
456
 
291
457
  const artifact = artifactForPhase(state.phase);
292
- const path = artifactPath(state.cwd, state.runId, artifact);
458
+ const path = artifactPath(state.cwd, state.runId, artifact, state.artifactDirectory);
293
459
  const applyingTesting = state.executionMode === "apply" && state.phase === "testing";
294
460
  const roleCwd = applyingTesting ? state.executionWorkspace?.path : state.cwd;
295
461
  if (!roleCwd) {
296
462
  throw new Error("DevCrew apply testing requires an execution workspace");
297
463
  }
298
- state.roles.push(conductorDecision(state, role, gate));
464
+ state.roles.push(conductorDecision(state, role, gate ?? `${state.phase} automatic transition`));
465
+
466
+ if (applyingTesting && state.executionPolicy === "interactive-host") {
467
+ state.executionInstruction = {
468
+ phase: "testing",
469
+ workspacePath: roleCwd,
470
+ instructions: executionInstructions(state, "testing", roleCwd),
471
+ createdAt: now(),
472
+ };
473
+ state.status = "awaiting_execution";
474
+ return saveState(state);
475
+ }
299
476
 
300
477
  const result = await runner({
301
478
  backend: state.backend,
@@ -304,6 +481,7 @@ async function runCurrentPhaseRole(state: RunState, runner: RoleRunner = runRole
304
481
  request: state.request,
305
482
  mode: state.mode,
306
483
  executionMode: state.executionMode,
484
+ executionPolicy: state.executionPolicy,
307
485
  cwd: roleCwd,
308
486
  standards: state.standards.combined,
309
487
  artifactPath: path,
@@ -314,20 +492,65 @@ async function runCurrentPhaseRole(state: RunState, runner: RoleRunner = runRole
314
492
 
315
493
  if (applyingTesting && state.executionWorkspace) {
316
494
  state.verification = await runConfiguredVerification(state, roleCwd);
495
+ state.verificationStatus = verificationStatusFor(state.verification);
317
496
  const captured = await captureExecutionChanges(state.executionWorkspace);
318
497
  state.changedFiles = captured.changedFiles;
319
498
  state.implementationDiff = captured.patch;
320
499
  await writeImplementationReview(state);
321
500
  }
322
501
 
502
+ if (state.phase === "review") {
503
+ const reviewDecision = result.format === "structured" ? result.structured?.reviewDecision : result.reviewDecision;
504
+ if (!reviewDecision) {
505
+ throw new Error("DevCrew architecture review must return a structured review decision");
506
+ }
507
+ state.architectureReview = {
508
+ decision: reviewDecision,
509
+ summary: result.summary,
510
+ reviewedAt: now(),
511
+ };
512
+ }
513
+
323
514
  // When the backend cannot run a real SDK we keep a single deterministic
324
515
  // artifact source by rendering the rich phase template from the core layer.
325
516
  const baseMarkdown = result.usedFallback ? `${fallbackNotice(result)}${renderArtifact(artifact, state)}` : result.markdown;
326
- const markdown = appendExecutionSections(artifact, baseMarkdown, state);
517
+ const markdown = appendExecutionSections(artifact, baseMarkdown, state, result);
327
518
  state.roles.push({ ...result, markdown });
328
519
  state.artifacts[artifact] = await writeMarkdownArtifact(state, artifact, markdown);
329
- state.gates[gate] = "pending";
330
- state.status = "awaiting_approval";
520
+ state.executionInstruction = undefined;
521
+ const questions = result.format === "structured"
522
+ ? result.structured?.questions?.map((question) => question.prompt) ?? []
523
+ : result.questions ?? [];
524
+ if (state.phase === "requirements" && questions.length > 0) {
525
+ state.pendingQuestions = questions;
526
+ state.gates.requirements = "not_started";
527
+ state.status = "awaiting_input";
528
+ return saveState(state);
529
+ }
530
+ if (state.phase === "review" && state.architectureReview?.decision === "changes_required") {
531
+ state.gates["implementation-review"] = "rejected";
532
+ state.status = "awaiting_input";
533
+ state.feedback.push({
534
+ gate: "implementation-review",
535
+ message: state.architectureReview.summary,
536
+ createdAt: now(),
537
+ });
538
+ return saveState(state);
539
+ }
540
+ if (!gate && configuredGate) {
541
+ state.phase = nextPhaseAfterGate(state, configuredGate);
542
+ state.status = "ready";
543
+ return saveState(state);
544
+ }
545
+ if (applyingTesting) {
546
+ setTestingGateFromVerification(state);
547
+ } else {
548
+ if (!gate) {
549
+ throw new Error(`DevCrew has no configured gate for ${state.phase}`);
550
+ }
551
+ state.gates[gate] = "pending";
552
+ state.status = "awaiting_approval";
553
+ }
331
554
  return saveState(state);
332
555
  }
333
556
 
@@ -338,7 +561,13 @@ export async function startOrchestratedWorkflow(input: StartWorkflowInput, runne
338
561
 
339
562
  export async function continueOrchestratedWorkflow(input: RunRef, runner: RoleRunner = runRole): Promise<RunState> {
340
563
  const state = await getWorkflowStatus(input);
341
- if (state.status === "awaiting_approval" || state.status === "awaiting_input" || state.status === "complete") {
564
+ if (
565
+ state.status === "awaiting_approval" ||
566
+ state.status === "awaiting_input" ||
567
+ state.status === "awaiting_execution" ||
568
+ state.status === "complete" ||
569
+ state.status === "aborted"
570
+ ) {
342
571
  return state;
343
572
  }
344
573
 
@@ -377,6 +606,9 @@ export async function approveOrchestratedWorkflow(input: ApproveWorkflowInput):
377
606
  return cleanupAfterApproval(before);
378
607
  }
379
608
  if (promotingTesting) {
609
+ if (before.verificationStatus !== "passed" && !before.verificationWaiver) {
610
+ throw new Error("Verification must pass before promotion or have an explicit verification waiver");
611
+ }
380
612
  await promoteExecutionChanges(before);
381
613
  let approved: RunState;
382
614
  try {
@@ -396,10 +628,98 @@ export async function approveOrchestratedWorkflow(input: ApproveWorkflowInput):
396
628
  return approveWorkflow(input);
397
629
  }
398
630
 
631
+ export async function waiveOrchestratedVerification(input: WaiveVerificationInput): Promise<RunState> {
632
+ const state = await waiveVerificationWorkflow(input);
633
+ const reportPath = state.artifacts["test-report"];
634
+ if (reportPath) {
635
+ const report = await readFile(reportPath, "utf8");
636
+ await writeFile(reportPath, appendExecutionSections("test-report", report, state), "utf8");
637
+ }
638
+ return state;
639
+ }
640
+
641
+ export async function completeOrchestratedExecution(input: CompleteExecutionInput): Promise<RunState> {
642
+ const state = await getWorkflowStatus(input);
643
+ const instruction = state.executionInstruction;
644
+ if (
645
+ state.executionMode !== "apply" ||
646
+ state.executionPolicy !== "interactive-host" ||
647
+ state.status !== "awaiting_execution" ||
648
+ !instruction ||
649
+ instruction.phase !== state.phase ||
650
+ !state.executionWorkspace ||
651
+ instruction.workspacePath !== state.executionWorkspace.path
652
+ ) {
653
+ throw new Error("DevCrew is not awaiting an interactive-host execution completion");
654
+ }
655
+ const summary = parseCompletionSummary(input.summary);
656
+
657
+ if (state.phase === "execution") {
658
+ if (input.verification !== undefined) {
659
+ throw new Error("verification can only be submitted after interactive-host testing");
660
+ }
661
+ const captured = await captureExecutionChanges(state.executionWorkspace);
662
+ state.changedFiles = captured.changedFiles;
663
+ state.implementationDiff = captured.patch;
664
+ state.lintResults = [];
665
+ state.roles.push(hostCompletionResult(state, summary));
666
+ state.executionInstruction = undefined;
667
+ await writeImplementationReview(state);
668
+ state.phase = "review";
669
+ state.status = "ready";
670
+ return saveState(state);
671
+ }
672
+
673
+ if (state.phase !== "testing") {
674
+ throw new Error(`Cannot complete interactive-host work during ${state.phase}`);
675
+ }
676
+ state.verification = parseCompletionVerification(input.verification);
677
+ state.verificationStatus = verificationStatusFor(state.verification);
678
+ const captured = await captureExecutionChanges(state.executionWorkspace);
679
+ state.changedFiles = captured.changedFiles;
680
+ state.implementationDiff = captured.patch;
681
+ await writeImplementationReview(state);
682
+ const result = hostCompletionResult(state, summary);
683
+ const artifact = artifactForPhase(state.phase);
684
+ const markdown = appendExecutionSections(artifact, result.markdown, state, result);
685
+ state.roles.push({ ...result, markdown });
686
+ state.artifacts[artifact] = await writeMarkdownArtifact(state, artifact, markdown);
687
+ state.executionInstruction = undefined;
688
+ setTestingGateFromVerification(state);
689
+ return saveState(state);
690
+ }
691
+
399
692
  export async function rejectOrchestratedWorkflow(input: RejectWorkflowInput): Promise<RunState> {
400
693
  return rejectWorkflow(input);
401
694
  }
402
695
 
696
+ export async function abortOrchestratedWorkflow(input: AbortWorkflowInput): Promise<RunState> {
697
+ const state = await abortWorkflow(input);
698
+ if (!state.executionWorkspace) {
699
+ return state;
700
+ }
701
+ try {
702
+ await cleanupExecutionWorkspace(state);
703
+ } catch {
704
+ return state;
705
+ }
706
+ state.executionWorkspace = undefined;
707
+ return saveState(state);
708
+ }
709
+
710
+ export async function recoverOrchestratedWorkflow(input: RunRef): Promise<RunState> {
711
+ const state = await getWorkflowStatus(input);
712
+ if (state.status !== "aborted" && state.status !== "complete") {
713
+ throw new Error("Only terminal DevCrew runs can be recovered");
714
+ }
715
+ if (!state.executionWorkspace) {
716
+ return state;
717
+ }
718
+ await cleanupExecutionWorkspace(state);
719
+ state.executionWorkspace = undefined;
720
+ return saveState(state);
721
+ }
722
+
403
723
  export async function answerOrchestratedWorkflow(input: AnswerWorkflowInput, runner: RoleRunner = runRole): Promise<RunState> {
404
724
  const before = await getWorkflowStatus(input);
405
725
  const state = await answerWorkflow(input, { skipArtifactWrite: true });
@@ -413,10 +733,20 @@ export async function answerOrchestratedWorkflow(input: AnswerWorkflowInput, run
413
733
  state.gates.testing = "not_started";
414
734
  return saveState(state);
415
735
  }
736
+ if (
737
+ before.executionMode === "apply" &&
738
+ before.phase === "review" &&
739
+ before.gates["implementation-review"] === "rejected" &&
740
+ before.architectureReview?.decision === "changes_required"
741
+ ) {
742
+ state.phase = "execution";
743
+ state.status = "ready";
744
+ state.gates["implementation-review"] = "not_started";
745
+ return saveState(state);
746
+ }
416
747
 
417
- const gate = gateForPhase(state.phase);
418
748
  const role = roleForPhase(state.phase);
419
- if (!gate || !role) {
749
+ if (!role) {
420
750
  return state;
421
751
  }
422
752
 
@@ -3,7 +3,7 @@ import { access, copyFile, mkdir, writeFile } from "node:fs/promises";
3
3
  import { dirname, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
 
6
- import { DEFAULT_CONFIG, DEVCREW_NPM_PACKAGE, DEVCREW_VERSION, ROLE_SECTIONS } from "../../core/src/index.js";
6
+ import { DEFAULT_CONFIG, DEVCREW_NPM_PACKAGE, DEVCREW_VERSION } from "../../core/src/index.js";
7
7
 
8
8
  export interface GeneratedPlugin {
9
9
  name: "devcrew";
@@ -45,48 +45,8 @@ async function writeCodexAssets(pluginRoot: string): Promise<void> {
45
45
  await copyFile(await bundledAssetPath("composer-icon.png"), join(assetDir, "composer-icon.png"));
46
46
  }
47
47
 
48
- function roleExpectations(name: string): string {
49
- const sections = ROLE_SECTIONS[name as keyof typeof ROLE_SECTIONS];
50
- if (!sections || sections.length === 0) {
51
- return "";
52
- }
53
- return sections.map((s) => `- ${s.heading} (${s.description})`).join("\n");
54
- }
55
-
56
- async function writeRoleAgents(root: string, format: "codex" | "claude"): Promise<void> {
57
- const roles = [
58
- ["pm", "Product manager. Clarifies requirements, scope boundaries, success criteria, and requester approvals."],
59
- ["architect", "technical architecture specialist. Designs implementation, deployment, interfaces, and review criteria."],
60
- ["implementer", "Implementation engineer. Writes code according to approved architecture and discovered standards."],
61
- ["tester", "Testing and acceptance specialist. Verifies functionality, regressions, and acceptance evidence."],
62
- ] as const;
63
-
64
- if (format === "claude") {
65
- const agentDir = join(root, "agents");
66
- await mkdir(agentDir, { recursive: true });
67
- for (const [name, description] of roles) {
68
- await writeFile(
69
- join(agentDir, `${name}.md`),
70
- `---\nname: ${name}\ndescription: ${description}\ntools: Read, Grep, Glob, Bash\n---\n\nYou are the DevCrew ${name} role. ${description} Return concise Markdown and keep inherited host permissions.\n\nProduce these required sections:\n\n${roleExpectations(name)}\n`,
71
- "utf8",
72
- );
73
- }
74
- return;
75
- }
76
-
77
- const agentDir = join(root, "agents");
78
- await mkdir(agentDir, { recursive: true });
79
- for (const [name, description] of roles) {
80
- await writeFile(
81
- join(agentDir, `${name}.toml`),
82
- `name = "${name}"\ndescription = "${description}"\ndeveloper_instructions = """\nYou are the DevCrew ${name} role. ${description}\nReturn concise Markdown and keep inherited host permissions.\n\nProduce these required sections:\n${roleExpectations(name)}\n"""\n`,
83
- "utf8",
84
- );
85
- }
86
- }
87
-
88
48
  function entrySkill(): string {
89
- return `---\nname: devcrew\ndescription: Run the DevCrew PM -> architecture -> implementation -> testing workflow. Use when the user asks for structured feature or product development, requirements clarification, architecture review, implementation planning, testing acceptance, or Chinese requests such as 完整研发流程, 需求澄清, 产品经理, 架构师, 开发测试流程.\n---\n\nUse the DevCrew MCP tools to manage the workflow:\n\n1. Start with \`devcrew_start\` using the current repository cwd, mode, request, and optional executionMode. Host is inferred from the plugin's \`DEVCREW_HOST\`; pass host only for an explicit override. Omit executionMode unless the requester explicitly asks DevCrew to apply changes; the default safe mode is \`plan\`.\n2. After start, DevCrew records the active run for this repository. For follow-up tools, omit runId unless you need to target a different run explicitly.\n3. Use \`executionMode: "apply"\` only when the requester explicitly wants DevCrew to write code or run validation commands. This still inherits host sandbox, approval, and tool permissions.\n4. Use \`devcrew_status\` to show the current phase and pending gate.\n5. Use \`devcrew_answer\` when the requester gives clarification.\n6. Use \`devcrew_approve\` or \`devcrew_reject\` for each gate.\n7. Use \`devcrew_continue\` after approvals. This executes the next phase role, writes the phase artifact, and opens the next gate. The implementation phase also writes \`implementation-review\` for diff and architecture compliance review.\n8. Use \`devcrew_artifact\` to read generated requirements, architecture, implementation-plan, implementation-review, test-report, or acceptance files.\n\nDo not bypass host sandbox, approval, or tool permissions.\n`;
49
+ return `---\nname: devcrew\ndescription: Run the DevCrew PM -> architecture -> implementation -> testing workflow. Use when the user asks for structured feature or product development, requirements clarification, architecture review, implementation planning, testing acceptance, or Chinese requests such as 完整研发流程, 需求澄清, 产品经理, 架构师, 开发测试流程.\n---\n\nUse the DevCrew MCP tools to manage the workflow:\n\n1. Start with \`devcrew_start\` using the current repository cwd, mode, request, and optional \`executionMode\`. Host is inferred from the plugin's \`DEVCREW_HOST\`; pass host only for an explicit override. Omit \`executionMode\` unless the requester explicitly asks DevCrew to apply changes; the default safe mode is \`plan\`.\n2. After start, DevCrew records the active run for this repository. For follow-up tools, omit \`runId\` unless you need to target a different run explicitly.\n3. For \`executionMode: "apply"\`, choose an explicit \`executionPolicy\`. The default \`interactive-host\` pauses at implementation and testing for the host's native agent to work in DevCrew's isolated worktree. \`headless-restricted\` and \`headless-unattended\` are DevCrew SDK policies; they do not inherit the current host approval session.\n4. Use \`devcrew_status\` to show the current phase, pending gate, and any execution instruction.\n5. Use \`devcrew_answer\` when the requester gives clarification.\n6. Use \`devcrew_approve\` or \`devcrew_reject\` for each gate.\n7. Use \`devcrew_continue\` after approvals. Apply runs enter an \`implementation-review\` gate after execution: review the architect's \`architecture-review\` artifact before testing. For \`interactive-host\`, if status becomes \`awaiting_execution\`, perform the native-host work in the indicated worktree then call \`devcrew_complete_execution\`. For testing, include command, exit code, output, startedAt, and completedAt evidence.\n8. Failed verification is not approvable. Revise through \`devcrew_answer\`, or use \`devcrew_waive_verification\` only when the requester explicitly accepts the recorded risk and provides a reason.\n9. Use \`devcrew_artifact\` to read generated requirements, architecture, implementation-plan, implementation-review, architecture-review, test-report, or acceptance files.\n\nNever describe a nested SDK session as inheriting the current host's approval decisions.\n`;
90
50
  }
91
51
 
92
52
  function npmPackageSpecifier(): string {
@@ -169,7 +129,6 @@ export async function generateCodexPlugin(root: string): Promise<GeneratedPlugin
169
129
  devcrew: mcpServerConfig("codex"),
170
130
  },
171
131
  });
172
- await writeRoleAgents(pluginRoot, "codex");
173
132
  return { name: "devcrew", path: pluginRoot };
174
133
  }
175
134
 
@@ -215,7 +174,6 @@ export async function generateClaudePlugin(root: string): Promise<GeneratedPlugi
215
174
  devcrew: mcpServerConfig("claude"),
216
175
  },
217
176
  });
218
- await writeRoleAgents(pluginRoot, "claude");
219
177
  return { name: "devcrew", path: pluginRoot };
220
178
  }
221
179