@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
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
2
2
  import { mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import { dirname } from "node:path";
4
4
  import { runRole } from "../../adapters/src/index.js";
5
- import { answerWorkflow, approveWorkflow, artifactForPhase, artifactPath, ARTIFACTS, discoverCoverageCommands, discoverLintCommands, discoverVerifyCommands, gateForPhase, getWorkflowStatus, readConfig, rejectWorkflow, renderArtifact, saveState, startWorkflow, validateWorkflowApproval, } from "../../core/src/index.js";
5
+ import { abortWorkflow, answerWorkflow, approveWorkflow, artifactForPhase, artifactPath, ARTIFACTS, discoverCoverageCommands, discoverLintCommands, discoverVerifyCommands, gateForPhase, getWorkflowStatus, nextPhaseAfterGate, readConfig, rejectWorkflow, renderArtifact, saveState, startWorkflow, validateWorkflowApproval, waiveVerificationWorkflow, } from "../../core/src/index.js";
6
6
  import { captureExecutionChanges, cleanupExecutionWorkspace, ensureExecutionWorkspace, promoteExecutionChanges, rollbackPromotedExecutionChanges, } from "./worktree.js";
7
7
  // Hard cap so a hung apply/verify command cannot block the serialized MCP loop.
8
8
  const COMMAND_TIMEOUT_MS = 300_000;
@@ -12,6 +12,7 @@ function roleForPhase(phase) {
12
12
  architecture: "architect",
13
13
  implementation: "implementer",
14
14
  execution: "implementer",
15
+ review: "architect",
15
16
  testing: "tester",
16
17
  };
17
18
  return roles[phase];
@@ -24,6 +25,7 @@ function conductorDecision(state, role, gate) {
24
25
  summary,
25
26
  markdown: `# Conductor Decision\n\n${summary}\n`,
26
27
  usedFallback: false,
28
+ format: "legacy",
27
29
  };
28
30
  }
29
31
  function priorArtifactNamesForPhase(phase) {
@@ -35,7 +37,7 @@ function priorArtifactNamesForPhase(phase) {
35
37
  return ARTIFACTS.slice(0, currentIndex);
36
38
  }
37
39
  async function writeMarkdownArtifact(state, artifact, markdown) {
38
- const path = artifactPath(state.cwd, state.runId, artifact);
40
+ const path = artifactPath(state.cwd, state.runId, artifact, state.artifactDirectory);
39
41
  await mkdir(dirname(path), { recursive: true });
40
42
  await writeFile(path, markdown, "utf8");
41
43
  return path;
@@ -162,11 +164,141 @@ function verificationBlock(results) {
162
164
  .map((result) => `### ${result.command}\n\nExit Code: ${result.exitCode}\n\nOutput:\n\n\`\`\`text\n${result.output || "(no output)"}\n\`\`\``)
163
165
  .join("\n\n");
164
166
  }
165
- function appendExecutionSections(artifact, markdown, state) {
166
- if (artifact === "test-report" && !markdown.includes("## Acceptance Evidence")) {
167
- return `${markdown.trim()}\n\n## Acceptance Evidence\n\n${verificationBlock(state.verification)}\n`;
167
+ function verificationStatusFor(results) {
168
+ if (results.length === 0) {
169
+ return "not_run";
170
+ }
171
+ return results.every((result) => result.exitCode === 0) ? "passed" : "failed";
172
+ }
173
+ function setTestingGateFromVerification(state) {
174
+ if (state.verificationStatus !== "passed") {
175
+ state.gates.testing = "rejected";
176
+ state.status = "awaiting_input";
177
+ state.feedback.push({
178
+ gate: "testing",
179
+ message: state.verificationStatus === "failed"
180
+ ? "Automated verification failed. Inspect the test report, revise the implementation, or record an explicit verification waiver with its reason."
181
+ : "No verification evidence was recorded. Configure or run validation, revise the implementation, or record an explicit verification waiver with its reason.",
182
+ createdAt: now(),
183
+ });
184
+ return;
185
+ }
186
+ state.gates.testing = "pending";
187
+ state.status = "awaiting_approval";
188
+ }
189
+ function executionInstructions(state, phase, workspacePath) {
190
+ if (phase === "execution") {
191
+ 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.`;
192
+ }
193
+ 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.`;
194
+ }
195
+ function hostCompletionResult(state, summary) {
196
+ const role = state.phase === "execution" ? "implementer" : "tester";
197
+ const artifact = artifactForPhase(state.phase);
198
+ return {
199
+ role,
200
+ backend: state.backend,
201
+ summary: `${role} completed through the native ${state.host} host: ${summary}`,
202
+ markdown: `${renderArtifact(artifact, state).trim()}\n\n## Native Host Summary\n\n${summary}\n`,
203
+ usedFallback: false,
204
+ format: "legacy",
205
+ };
206
+ }
207
+ function parseCompletionSummary(value) {
208
+ if (typeof value !== "string" || value.trim().length === 0) {
209
+ throw new Error("summary must be a non-empty string");
210
+ }
211
+ return value.trim();
212
+ }
213
+ function parseCompletionVerification(value) {
214
+ if (value === undefined) {
215
+ return [];
216
+ }
217
+ if (!Array.isArray(value)) {
218
+ throw new Error("verification must be an array");
219
+ }
220
+ return value.map((entry, index) => {
221
+ if (!entry ||
222
+ typeof entry !== "object" ||
223
+ typeof entry.command !== "string" ||
224
+ entry.command.trim().length === 0 ||
225
+ !Number.isInteger(entry.exitCode) ||
226
+ typeof entry.output !== "string" ||
227
+ typeof entry.startedAt !== "string" ||
228
+ typeof entry.completedAt !== "string") {
229
+ throw new Error(`verification[${index}] must include command, integer exitCode, output, startedAt, and completedAt`);
230
+ }
231
+ return {
232
+ command: entry.command.trim(),
233
+ exitCode: entry.exitCode,
234
+ output: entry.output,
235
+ startedAt: entry.startedAt,
236
+ completedAt: entry.completedAt,
237
+ };
238
+ });
239
+ }
240
+ function structuredRoleResultBlock(result) {
241
+ if (result.format !== "structured" || !result.structured) {
242
+ return "";
243
+ }
244
+ const data = result.structured;
245
+ const sections = [
246
+ "## Structured Role Result",
247
+ "",
248
+ `Schema Version: ${data.schemaVersion}`,
249
+ `Summary: ${data.summary}`,
250
+ ];
251
+ if (data.questions?.length) {
252
+ sections.push("", "### Questions", "", ...data.questions.map((question) => `- ${question.id}: ${question.prompt}`));
253
+ }
254
+ if (data.decisions?.length) {
255
+ sections.push("", "### Decisions", "", ...data.decisions.map((decision) => `- ${decision}`));
256
+ }
257
+ if (data.changedFiles?.length) {
258
+ sections.push("", "### Changed Files", "", ...data.changedFiles.map((file) => `- ${file}`));
259
+ }
260
+ if (data.evidence.length) {
261
+ sections.push("", "### Command Evidence", "");
262
+ for (const evidence of data.evidence) {
263
+ sections.push(`- \`${evidence.command}\` — exit code ${evidence.exitCode}`);
264
+ if (evidence.output) {
265
+ sections.push("", "```text", evidence.output, "```");
266
+ }
267
+ }
268
+ }
269
+ if (data.testCases?.length) {
270
+ sections.push("", "### Test Cases", "", "| ID | Scenario | Type | Expected |", "| --- | --- | --- | --- |");
271
+ sections.push(...data.testCases.map((testCase) => `| ${testCase.id} | ${testCase.scenario} | ${testCase.type} | ${testCase.expected} |`));
272
+ }
273
+ if (data.risks.length) {
274
+ sections.push("", "### Risks", "", ...data.risks.map((risk) => `- ${risk}`));
275
+ }
276
+ return `\n\n${sections.join("\n")}`;
277
+ }
278
+ function appendExecutionSections(artifact, markdown, state, result) {
279
+ let content = markdown.trim();
280
+ if (result?.format === "structured" && !content.includes("## Structured Role Result")) {
281
+ content += structuredRoleResultBlock(result);
282
+ }
283
+ if (artifact === "architecture-review" && state.architectureReview) {
284
+ if (!content.includes("## Review Decision")) {
285
+ content += `\n\n## Review Decision\n\nDecision: ${state.architectureReview.decision}\n\nSummary: ${state.architectureReview.summary}`;
286
+ }
287
+ return `${content}\n`;
288
+ }
289
+ if (artifact === "test-report") {
290
+ if (!content.includes("## Acceptance Evidence")) {
291
+ content += `\n\n## Acceptance Evidence\n\n${verificationBlock(state.verification)}`;
292
+ }
293
+ if (!content.includes("## Verification Outcome")) {
294
+ content += `\n\n## Verification Outcome\n\nStatus: ${state.verificationStatus}`;
295
+ }
296
+ if (state.verificationWaiver && !content.includes("## Verification Waiver")) {
297
+ content += `\n\n## Verification Waiver\n\nReason: ${state.verificationWaiver.reason}\nRecorded At: ${state.verificationWaiver.createdAt}`;
298
+ }
299
+ return `${content}\n`;
168
300
  }
169
- return markdown;
301
+ return `${content}\n`;
170
302
  }
171
303
  async function writeImplementationReview(state) {
172
304
  state.artifacts["implementation-review"] = await writeMarkdownArtifact(state, "implementation-review", renderArtifact("implementation-review", state));
@@ -179,8 +311,20 @@ async function runCurrentPhaseRole(state, runner = runRole) {
179
311
  const workspace = await ensureExecutionWorkspace(state);
180
312
  state.executionWorkspace = workspace;
181
313
  state.verification = [];
314
+ state.verificationStatus = "not_run";
315
+ delete state.verificationWaiver;
182
316
  delete state.artifacts["test-report"];
183
317
  await saveState(state);
318
+ if (state.executionPolicy === "interactive-host") {
319
+ state.executionInstruction = {
320
+ phase: "execution",
321
+ workspacePath: workspace.path,
322
+ instructions: executionInstructions(state, "execution", workspace.path),
323
+ createdAt: now(),
324
+ };
325
+ state.status = "awaiting_execution";
326
+ return saveState(state);
327
+ }
184
328
  const result = await runner({
185
329
  backend: state.backend,
186
330
  role: "implementer",
@@ -188,9 +332,10 @@ async function runCurrentPhaseRole(state, runner = runRole) {
188
332
  request: state.request,
189
333
  mode: state.mode,
190
334
  executionMode: "apply",
335
+ executionPolicy: state.executionPolicy,
191
336
  cwd: workspace.path,
192
337
  standards: state.standards.combined,
193
- artifactPath: artifactPath(state.cwd, state.runId, "implementation-review"),
338
+ artifactPath: artifactPath(state.cwd, state.runId, "implementation-review", state.artifactDirectory),
194
339
  answers: state.answers.map((entry) => entry.answer),
195
340
  feedback: state.feedback.map((entry) => `${entry.gate}: ${entry.message}`),
196
341
  priorArtifacts: await readPriorArtifacts(state),
@@ -201,14 +346,16 @@ async function runCurrentPhaseRole(state, runner = runRole) {
201
346
  state.changedFiles = captured.changedFiles;
202
347
  state.implementationDiff = captured.patch;
203
348
  state.roles.push(result);
349
+ state.executionInstruction = undefined;
204
350
  await writeImplementationReview(state);
205
- state.phase = "testing";
351
+ state.phase = "review";
206
352
  state.status = "ready";
207
353
  return saveState(state);
208
354
  }
209
- const gate = gateForPhase(state.phase);
355
+ const configuredGate = gateForPhase(state.phase);
356
+ const gate = gateForPhase(state.phase, state.enabledGates);
210
357
  const role = roleForPhase(state.phase);
211
- if (!gate || !role) {
358
+ if (!role) {
212
359
  const artifact = artifactForPhase(state.phase);
213
360
  const markdown = renderArtifact(artifact, state);
214
361
  state.artifacts[artifact] = await writeMarkdownArtifact(state, artifact, markdown);
@@ -217,13 +364,23 @@ async function runCurrentPhaseRole(state, runner = runRole) {
217
364
  return saveState(state);
218
365
  }
219
366
  const artifact = artifactForPhase(state.phase);
220
- const path = artifactPath(state.cwd, state.runId, artifact);
367
+ const path = artifactPath(state.cwd, state.runId, artifact, state.artifactDirectory);
221
368
  const applyingTesting = state.executionMode === "apply" && state.phase === "testing";
222
369
  const roleCwd = applyingTesting ? state.executionWorkspace?.path : state.cwd;
223
370
  if (!roleCwd) {
224
371
  throw new Error("DevCrew apply testing requires an execution workspace");
225
372
  }
226
- state.roles.push(conductorDecision(state, role, gate));
373
+ state.roles.push(conductorDecision(state, role, gate ?? `${state.phase} automatic transition`));
374
+ if (applyingTesting && state.executionPolicy === "interactive-host") {
375
+ state.executionInstruction = {
376
+ phase: "testing",
377
+ workspacePath: roleCwd,
378
+ instructions: executionInstructions(state, "testing", roleCwd),
379
+ createdAt: now(),
380
+ };
381
+ state.status = "awaiting_execution";
382
+ return saveState(state);
383
+ }
227
384
  const result = await runner({
228
385
  backend: state.backend,
229
386
  role,
@@ -231,6 +388,7 @@ async function runCurrentPhaseRole(state, runner = runRole) {
231
388
  request: state.request,
232
389
  mode: state.mode,
233
390
  executionMode: state.executionMode,
391
+ executionPolicy: state.executionPolicy,
234
392
  cwd: roleCwd,
235
393
  standards: state.standards.combined,
236
394
  artifactPath: path,
@@ -240,19 +398,64 @@ async function runCurrentPhaseRole(state, runner = runRole) {
240
398
  });
241
399
  if (applyingTesting && state.executionWorkspace) {
242
400
  state.verification = await runConfiguredVerification(state, roleCwd);
401
+ state.verificationStatus = verificationStatusFor(state.verification);
243
402
  const captured = await captureExecutionChanges(state.executionWorkspace);
244
403
  state.changedFiles = captured.changedFiles;
245
404
  state.implementationDiff = captured.patch;
246
405
  await writeImplementationReview(state);
247
406
  }
407
+ if (state.phase === "review") {
408
+ const reviewDecision = result.format === "structured" ? result.structured?.reviewDecision : result.reviewDecision;
409
+ if (!reviewDecision) {
410
+ throw new Error("DevCrew architecture review must return a structured review decision");
411
+ }
412
+ state.architectureReview = {
413
+ decision: reviewDecision,
414
+ summary: result.summary,
415
+ reviewedAt: now(),
416
+ };
417
+ }
248
418
  // When the backend cannot run a real SDK we keep a single deterministic
249
419
  // artifact source by rendering the rich phase template from the core layer.
250
420
  const baseMarkdown = result.usedFallback ? `${fallbackNotice(result)}${renderArtifact(artifact, state)}` : result.markdown;
251
- const markdown = appendExecutionSections(artifact, baseMarkdown, state);
421
+ const markdown = appendExecutionSections(artifact, baseMarkdown, state, result);
252
422
  state.roles.push({ ...result, markdown });
253
423
  state.artifacts[artifact] = await writeMarkdownArtifact(state, artifact, markdown);
254
- state.gates[gate] = "pending";
255
- state.status = "awaiting_approval";
424
+ state.executionInstruction = undefined;
425
+ const questions = result.format === "structured"
426
+ ? result.structured?.questions?.map((question) => question.prompt) ?? []
427
+ : result.questions ?? [];
428
+ if (state.phase === "requirements" && questions.length > 0) {
429
+ state.pendingQuestions = questions;
430
+ state.gates.requirements = "not_started";
431
+ state.status = "awaiting_input";
432
+ return saveState(state);
433
+ }
434
+ if (state.phase === "review" && state.architectureReview?.decision === "changes_required") {
435
+ state.gates["implementation-review"] = "rejected";
436
+ state.status = "awaiting_input";
437
+ state.feedback.push({
438
+ gate: "implementation-review",
439
+ message: state.architectureReview.summary,
440
+ createdAt: now(),
441
+ });
442
+ return saveState(state);
443
+ }
444
+ if (!gate && configuredGate) {
445
+ state.phase = nextPhaseAfterGate(state, configuredGate);
446
+ state.status = "ready";
447
+ return saveState(state);
448
+ }
449
+ if (applyingTesting) {
450
+ setTestingGateFromVerification(state);
451
+ }
452
+ else {
453
+ if (!gate) {
454
+ throw new Error(`DevCrew has no configured gate for ${state.phase}`);
455
+ }
456
+ state.gates[gate] = "pending";
457
+ state.status = "awaiting_approval";
458
+ }
256
459
  return saveState(state);
257
460
  }
258
461
  export async function startOrchestratedWorkflow(input, runner = runRole) {
@@ -261,7 +464,11 @@ export async function startOrchestratedWorkflow(input, runner = runRole) {
261
464
  }
262
465
  export async function continueOrchestratedWorkflow(input, runner = runRole) {
263
466
  const state = await getWorkflowStatus(input);
264
- if (state.status === "awaiting_approval" || state.status === "awaiting_input" || state.status === "complete") {
467
+ if (state.status === "awaiting_approval" ||
468
+ state.status === "awaiting_input" ||
469
+ state.status === "awaiting_execution" ||
470
+ state.status === "complete" ||
471
+ state.status === "aborted") {
265
472
  return state;
266
473
  }
267
474
  return runCurrentPhaseRole(state, runner);
@@ -296,6 +503,9 @@ export async function approveOrchestratedWorkflow(input) {
296
503
  return cleanupAfterApproval(before);
297
504
  }
298
505
  if (promotingTesting) {
506
+ if (before.verificationStatus !== "passed" && !before.verificationWaiver) {
507
+ throw new Error("Verification must pass before promotion or have an explicit verification waiver");
508
+ }
299
509
  await promoteExecutionChanges(before);
300
510
  let approved;
301
511
  try {
@@ -315,9 +525,90 @@ export async function approveOrchestratedWorkflow(input) {
315
525
  }
316
526
  return approveWorkflow(input);
317
527
  }
528
+ export async function waiveOrchestratedVerification(input) {
529
+ const state = await waiveVerificationWorkflow(input);
530
+ const reportPath = state.artifacts["test-report"];
531
+ if (reportPath) {
532
+ const report = await readFile(reportPath, "utf8");
533
+ await writeFile(reportPath, appendExecutionSections("test-report", report, state), "utf8");
534
+ }
535
+ return state;
536
+ }
537
+ export async function completeOrchestratedExecution(input) {
538
+ const state = await getWorkflowStatus(input);
539
+ const instruction = state.executionInstruction;
540
+ if (state.executionMode !== "apply" ||
541
+ state.executionPolicy !== "interactive-host" ||
542
+ state.status !== "awaiting_execution" ||
543
+ !instruction ||
544
+ instruction.phase !== state.phase ||
545
+ !state.executionWorkspace ||
546
+ instruction.workspacePath !== state.executionWorkspace.path) {
547
+ throw new Error("DevCrew is not awaiting an interactive-host execution completion");
548
+ }
549
+ const summary = parseCompletionSummary(input.summary);
550
+ if (state.phase === "execution") {
551
+ if (input.verification !== undefined) {
552
+ throw new Error("verification can only be submitted after interactive-host testing");
553
+ }
554
+ const captured = await captureExecutionChanges(state.executionWorkspace);
555
+ state.changedFiles = captured.changedFiles;
556
+ state.implementationDiff = captured.patch;
557
+ state.lintResults = [];
558
+ state.roles.push(hostCompletionResult(state, summary));
559
+ state.executionInstruction = undefined;
560
+ await writeImplementationReview(state);
561
+ state.phase = "review";
562
+ state.status = "ready";
563
+ return saveState(state);
564
+ }
565
+ if (state.phase !== "testing") {
566
+ throw new Error(`Cannot complete interactive-host work during ${state.phase}`);
567
+ }
568
+ state.verification = parseCompletionVerification(input.verification);
569
+ state.verificationStatus = verificationStatusFor(state.verification);
570
+ const captured = await captureExecutionChanges(state.executionWorkspace);
571
+ state.changedFiles = captured.changedFiles;
572
+ state.implementationDiff = captured.patch;
573
+ await writeImplementationReview(state);
574
+ const result = hostCompletionResult(state, summary);
575
+ const artifact = artifactForPhase(state.phase);
576
+ const markdown = appendExecutionSections(artifact, result.markdown, state, result);
577
+ state.roles.push({ ...result, markdown });
578
+ state.artifacts[artifact] = await writeMarkdownArtifact(state, artifact, markdown);
579
+ state.executionInstruction = undefined;
580
+ setTestingGateFromVerification(state);
581
+ return saveState(state);
582
+ }
318
583
  export async function rejectOrchestratedWorkflow(input) {
319
584
  return rejectWorkflow(input);
320
585
  }
586
+ export async function abortOrchestratedWorkflow(input) {
587
+ const state = await abortWorkflow(input);
588
+ if (!state.executionWorkspace) {
589
+ return state;
590
+ }
591
+ try {
592
+ await cleanupExecutionWorkspace(state);
593
+ }
594
+ catch {
595
+ return state;
596
+ }
597
+ state.executionWorkspace = undefined;
598
+ return saveState(state);
599
+ }
600
+ export async function recoverOrchestratedWorkflow(input) {
601
+ const state = await getWorkflowStatus(input);
602
+ if (state.status !== "aborted" && state.status !== "complete") {
603
+ throw new Error("Only terminal DevCrew runs can be recovered");
604
+ }
605
+ if (!state.executionWorkspace) {
606
+ return state;
607
+ }
608
+ await cleanupExecutionWorkspace(state);
609
+ state.executionWorkspace = undefined;
610
+ return saveState(state);
611
+ }
321
612
  export async function answerOrchestratedWorkflow(input, runner = runRole) {
322
613
  const before = await getWorkflowStatus(input);
323
614
  const state = await answerWorkflow(input, { skipArtifactWrite: true });
@@ -329,9 +620,17 @@ export async function answerOrchestratedWorkflow(input, runner = runRole) {
329
620
  state.gates.testing = "not_started";
330
621
  return saveState(state);
331
622
  }
332
- const gate = gateForPhase(state.phase);
623
+ if (before.executionMode === "apply" &&
624
+ before.phase === "review" &&
625
+ before.gates["implementation-review"] === "rejected" &&
626
+ before.architectureReview?.decision === "changes_required") {
627
+ state.phase = "execution";
628
+ state.status = "ready";
629
+ state.gates["implementation-review"] = "not_started";
630
+ return saveState(state);
631
+ }
333
632
  const role = roleForPhase(state.phase);
334
- if (!gate || !role) {
633
+ if (!role) {
335
634
  return state;
336
635
  }
337
636
  // Re-run the current phase role so the artifact reflects the new answer and
@@ -2,7 +2,7 @@ import { constants } from "node:fs";
2
2
  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
- import { DEFAULT_CONFIG, DEVCREW_NPM_PACKAGE, DEVCREW_VERSION, ROLE_SECTIONS } from "../../core/src/index.js";
5
+ import { DEFAULT_CONFIG, DEVCREW_NPM_PACKAGE, DEVCREW_VERSION } from "../../core/src/index.js";
6
6
  async function writeJson(path, value) {
7
7
  await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
8
8
  }
@@ -29,36 +29,8 @@ async function writeCodexAssets(pluginRoot) {
29
29
  await copyFile(await bundledAssetPath("logo.png"), join(assetDir, "logo.png"));
30
30
  await copyFile(await bundledAssetPath("composer-icon.png"), join(assetDir, "composer-icon.png"));
31
31
  }
32
- function roleExpectations(name) {
33
- const sections = ROLE_SECTIONS[name];
34
- if (!sections || sections.length === 0) {
35
- return "";
36
- }
37
- return sections.map((s) => `- ${s.heading} (${s.description})`).join("\n");
38
- }
39
- async function writeRoleAgents(root, format) {
40
- const roles = [
41
- ["pm", "Product manager. Clarifies requirements, scope boundaries, success criteria, and requester approvals."],
42
- ["architect", "technical architecture specialist. Designs implementation, deployment, interfaces, and review criteria."],
43
- ["implementer", "Implementation engineer. Writes code according to approved architecture and discovered standards."],
44
- ["tester", "Testing and acceptance specialist. Verifies functionality, regressions, and acceptance evidence."],
45
- ];
46
- if (format === "claude") {
47
- const agentDir = join(root, "agents");
48
- await mkdir(agentDir, { recursive: true });
49
- for (const [name, description] of roles) {
50
- await writeFile(join(agentDir, `${name}.md`), `---\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`, "utf8");
51
- }
52
- return;
53
- }
54
- const agentDir = join(root, "agents");
55
- await mkdir(agentDir, { recursive: true });
56
- for (const [name, description] of roles) {
57
- await writeFile(join(agentDir, `${name}.toml`), `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`, "utf8");
58
- }
59
- }
60
32
  function entrySkill() {
61
- 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`;
33
+ 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`;
62
34
  }
63
35
  function npmPackageSpecifier() {
64
36
  return `${DEVCREW_NPM_PACKAGE}@${DEVCREW_VERSION}`;
@@ -136,7 +108,6 @@ export async function generateCodexPlugin(root) {
136
108
  devcrew: mcpServerConfig("codex"),
137
109
  },
138
110
  });
139
- await writeRoleAgents(pluginRoot, "codex");
140
111
  return { name: "devcrew", path: pluginRoot };
141
112
  }
142
113
  export async function generateCodexMarketplace(root) {
@@ -180,7 +151,6 @@ export async function generateClaudePlugin(root) {
180
151
  devcrew: mcpServerConfig("claude"),
181
152
  },
182
153
  });
183
- await writeRoleAgents(pluginRoot, "claude");
184
154
  return { name: "devcrew", path: pluginRoot };
185
155
  }
186
156
  export async function initProject(root) {