@shenlee/devcrew 0.1.2 → 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 (41) hide show
  1. package/README.md +15 -7
  2. package/README.zh-CN.md +12 -7
  3. package/dist/packages/adapters/src/index.js +60 -7
  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 +7 -6
  7. package/dist/packages/core/src/store.js +30 -0
  8. package/dist/packages/core/src/types.js +4 -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 +70 -12
  12. package/dist/packages/orchestrator/src/index.js +230 -15
  13. package/dist/packages/plugins/src/index.js +2 -32
  14. package/dist/packages/service/src/tools.js +43 -4
  15. package/docs/claude-code.md +5 -3
  16. package/docs/codex.md +9 -5
  17. package/docs/quickstart.md +1 -1
  18. package/docs/superpowers/plans/2026-07-13-safety-semantics.md +313 -0
  19. package/docs/superpowers/specs/2026-07-13-execution-boundaries-design.md +126 -0
  20. package/docs/workflow.md +19 -4
  21. package/package.json +1 -1
  22. package/packages/adapters/src/index.ts +84 -9
  23. package/packages/core/src/artifacts.ts +16 -4
  24. package/packages/core/src/config.ts +1 -1
  25. package/packages/core/src/paths.ts +7 -6
  26. package/packages/core/src/store.ts +34 -1
  27. package/packages/core/src/types.ts +46 -2
  28. package/packages/core/src/validation.ts +10 -0
  29. package/packages/core/src/version.ts +1 -1
  30. package/packages/core/src/workflow.ts +82 -11
  31. package/packages/orchestrator/src/index.ts +252 -14
  32. package/packages/plugins/src/index.ts +2 -44
  33. package/packages/service/src/tools.ts +44 -3
  34. package/plugins/devcrew-codex/.codex-plugin/plugin.json +1 -1
  35. package/plugins/devcrew-codex/.mcp.json +1 -1
  36. package/plugins/devcrew-codex/skills/devcrew/SKILL.md +8 -7
  37. package/scripts/smoke-codex-plugin.mjs +1 -1
  38. package/plugins/devcrew-codex/agents/architect.toml +0 -12
  39. package/plugins/devcrew-codex/agents/implementer.toml +0 -12
  40. package/plugins/devcrew-codex/agents/pm.toml +0 -13
  41. package/plugins/devcrew-codex/agents/tester.toml +0 -12
@@ -10,12 +10,14 @@ import {
10
10
  parseBackend,
11
11
  parseCwd,
12
12
  parseExecutionMode,
13
+ parseExecutionPolicy,
13
14
  parseFeedback,
14
15
  parseGate,
15
16
  parseHost,
16
17
  parseOptionalNote,
17
18
  parseRequest,
18
19
  parseRunId,
20
+ parseWaiverReason,
19
21
  parseWorkflowMode,
20
22
  } from "./validation.js";
21
23
  import type {
@@ -29,7 +31,9 @@ import type {
29
31
  RunRef,
30
32
  RunState,
31
33
  StartWorkflowInput,
34
+ WaiveVerificationInput,
32
35
  } from "./types.js";
36
+ import { GATES } from "./types.js";
33
37
 
34
38
  function now(): string {
35
39
  return new Date().toISOString();
@@ -47,6 +51,8 @@ export function nextPhaseAfterGate(state: RunState, gate: GateName): RunState["p
47
51
  return "implementation";
48
52
  case "implementation":
49
53
  return state.executionMode === "apply" ? "execution" : "testing";
54
+ case "implementation-review":
55
+ return "testing";
50
56
  case "testing":
51
57
  return "acceptance";
52
58
  }
@@ -58,6 +64,7 @@ export function artifactForPhase(phase: RunState["phase"]): ArtifactName {
58
64
  architecture: "architecture",
59
65
  implementation: "implementation-plan",
60
66
  execution: "implementation-review",
67
+ review: "architecture-review",
61
68
  testing: "test-report",
62
69
  acceptance: "acceptance",
63
70
  complete: "acceptance",
@@ -65,15 +72,31 @@ export function artifactForPhase(phase: RunState["phase"]): ArtifactName {
65
72
  return artifactByPhase[phase];
66
73
  }
67
74
 
68
- export function gateForPhase(phase: RunState["phase"]): GateName | undefined {
69
- if (phase === "requirements" || phase === "architecture" || phase === "implementation" || phase === "testing") {
70
- return phase;
71
- }
72
- return undefined;
75
+ export function gateForPhase(phase: RunState["phase"], enabledGates?: readonly GateName[]): GateName | undefined {
76
+ const gate = phase === "review"
77
+ ? "implementation-review"
78
+ : (
79
+ phase === "requirements" ||
80
+ phase === "architecture" ||
81
+ phase === "implementation" ||
82
+ phase === "testing"
83
+ ? phase
84
+ : undefined
85
+ );
86
+ return gate && (!enabledGates || enabledGates.includes(gate)) ? gate : undefined;
87
+ }
88
+
89
+ function configuredGates(gates: readonly GateName[]): GateName[] {
90
+ const configured = new Set(
91
+ gates.filter((gate): gate is GateName => GATES.includes(gate)),
92
+ );
93
+ configured.add("implementation-review");
94
+ configured.add("testing");
95
+ return [...configured];
73
96
  }
74
97
 
75
98
  function assertPendingCurrentGate(state: RunState, gate: GateName): void {
76
- const expected = gateForPhase(state.phase);
99
+ const expected = gateForPhase(state.phase, state.enabledGates);
77
100
  if (expected !== gate) {
78
101
  throw new Error(
79
102
  expected
@@ -105,6 +128,8 @@ export async function startWorkflow(input: StartWorkflowInput, options: Workflow
105
128
  const config = await ensureConfig(cwd);
106
129
  const backend = input.backend ? parseBackend(input.backend) : config.defaultBackend === "host-preferred" ? host : config.defaultBackend;
107
130
  const executionMode = input.executionMode ? parseExecutionMode(input.executionMode) : config.executionMode;
131
+ const executionPolicy = input.executionPolicy ? parseExecutionPolicy(input.executionPolicy) : "interactive-host";
132
+ const enabledGates = configuredGates(config.workflow.gates);
108
133
  if (executionMode === "apply" && backend === "local") {
109
134
  throw new Error("DevCrew apply mode requires a codex or claude backend; local is plan-only");
110
135
  }
@@ -116,20 +141,25 @@ export async function startWorkflow(input: StartWorkflowInput, options: Workflow
116
141
  host,
117
142
  mode,
118
143
  executionMode,
144
+ executionPolicy,
145
+ enabledGates,
146
+ artifactDirectory: config.workflow.artifactDirectory,
119
147
  backend,
120
148
  request,
121
149
  phase: "requirements",
122
- status: "awaiting_approval",
150
+ status: enabledGates.includes("requirements") ? "awaiting_approval" : "ready",
123
151
  createdAt,
124
152
  updatedAt: createdAt,
125
153
  gates: {
126
- requirements: "pending",
154
+ requirements: enabledGates.includes("requirements") ? "pending" : "not_started",
127
155
  architecture: "not_started",
128
156
  implementation: "not_started",
157
+ "implementation-review": "not_started",
129
158
  testing: "not_started",
130
159
  },
131
160
  artifacts: {},
132
161
  roles: [],
162
+ pendingQuestions: [],
133
163
  answers: [],
134
164
  approvals: [],
135
165
  feedback: [],
@@ -137,6 +167,7 @@ export async function startWorkflow(input: StartWorkflowInput, options: Workflow
137
167
  changedFiles: [],
138
168
  implementationDiff: "",
139
169
  verification: [],
170
+ verificationStatus: "not_run",
140
171
  lintResults: [],
141
172
  };
142
173
 
@@ -152,7 +183,12 @@ export async function getWorkflowStatus(input: RunRef): Promise<RunState> {
152
183
 
153
184
  export async function continueWorkflow(input: RunRef): Promise<RunState> {
154
185
  const state = await getWorkflowStatus(input);
155
- if (state.status === "awaiting_approval" || state.status === "awaiting_input" || state.status === "complete") {
186
+ if (
187
+ state.status === "awaiting_approval" ||
188
+ state.status === "awaiting_input" ||
189
+ state.status === "awaiting_execution" ||
190
+ state.status === "complete"
191
+ ) {
156
192
  return state;
157
193
  }
158
194
 
@@ -167,8 +203,14 @@ export async function continueWorkflow(input: RunRef): Promise<RunState> {
167
203
  throw new Error("DevCrew execution phase requires orchestrated continuation");
168
204
  }
169
205
 
170
- const gate = gateForPhase(state.phase);
206
+ const configuredGate = gateForPhase(state.phase);
207
+ const gate = gateForPhase(state.phase, state.enabledGates);
171
208
  if (!gate) {
209
+ if (configuredGate) {
210
+ state.phase = nextPhaseAfterGate(state, configuredGate);
211
+ state.status = "ready";
212
+ return saveState(state);
213
+ }
172
214
  state.status = "complete";
173
215
  return saveState(state);
174
216
  }
@@ -227,7 +269,16 @@ export async function rejectWorkflow(input: RejectWorkflowInput): Promise<RunSta
227
269
 
228
270
  export async function answerWorkflow(input: AnswerWorkflowInput, options: WorkflowMutationOptions = {}): Promise<RunState> {
229
271
  const state = await getWorkflowStatus(input);
230
- const gate = gateForPhase(state.phase);
272
+ const gate = gateForPhase(state.phase, state.enabledGates);
273
+ if (state.status === "awaiting_input" && state.pendingQuestions.length > 0 && state.phase === "requirements") {
274
+ state.answers.push({
275
+ answer: parseAnswer(input.answer),
276
+ createdAt: now(),
277
+ });
278
+ state.pendingQuestions = [];
279
+ state.status = "ready";
280
+ return saveState(state);
281
+ }
231
282
  if (state.status !== "awaiting_input" || !gate || state.gates[gate] !== "rejected") {
232
283
  throw new Error("Workflow must be awaiting_input at a rejected current gate before recording an answer");
233
284
  }
@@ -243,6 +294,26 @@ export async function answerWorkflow(input: AnswerWorkflowInput, options: Workfl
243
294
  return saveState(state);
244
295
  }
245
296
 
297
+ export async function waiveVerificationWorkflow(input: WaiveVerificationInput): Promise<RunState> {
298
+ const state = await getWorkflowStatus(input);
299
+ if (
300
+ state.executionMode !== "apply" ||
301
+ state.phase !== "testing" ||
302
+ state.status !== "awaiting_input" ||
303
+ state.gates.testing !== "rejected" ||
304
+ state.verificationStatus !== "failed"
305
+ ) {
306
+ throw new Error("A verification waiver is only available after failed apply-mode testing");
307
+ }
308
+ state.verificationWaiver = {
309
+ reason: parseWaiverReason(input.reason),
310
+ createdAt: now(),
311
+ };
312
+ state.gates.testing = "pending";
313
+ state.status = "awaiting_approval";
314
+ return saveState(state);
315
+ }
316
+
246
317
  export async function getArtifact(input: ArtifactRef): Promise<ArtifactReadResult> {
247
318
  const state = await getWorkflowStatus(input);
248
319
  return readArtifact(state, parseArtifactName(input.name));
@@ -15,15 +15,18 @@ import {
15
15
  discoverVerifyCommands,
16
16
  gateForPhase,
17
17
  getWorkflowStatus,
18
+ nextPhaseAfterGate,
18
19
  readConfig,
19
20
  rejectWorkflow,
20
21
  renderArtifact,
21
22
  saveState,
22
23
  startWorkflow,
23
24
  validateWorkflowApproval,
25
+ waiveVerificationWorkflow,
24
26
  type AnswerWorkflowInput,
25
27
  type ApproveWorkflowInput,
26
28
  type ArtifactName,
29
+ type CompleteExecutionInput,
27
30
  type Phase,
28
31
  type RejectWorkflowInput,
29
32
  type RoleResult,
@@ -31,6 +34,8 @@ import {
31
34
  type RunState,
32
35
  type StartWorkflowInput,
33
36
  type VerificationResult,
37
+ type VerificationStatus,
38
+ type WaiveVerificationInput,
34
39
  } from "../../core/src/index.js";
35
40
  import {
36
41
  captureExecutionChanges,
@@ -51,6 +56,7 @@ function roleForPhase(phase: Phase): RoleResult["role"] | undefined {
51
56
  architecture: "architect",
52
57
  implementation: "implementer",
53
58
  execution: "implementer",
59
+ review: "architect",
54
60
  testing: "tester",
55
61
  };
56
62
  return roles[phase];
@@ -77,7 +83,7 @@ function priorArtifactNamesForPhase(phase: Phase): ArtifactName[] {
77
83
  }
78
84
 
79
85
  async function writeMarkdownArtifact(state: RunState, artifact: ArtifactName, markdown: string): Promise<string> {
80
- const path = artifactPath(state.cwd, state.runId, artifact);
86
+ const path = artifactPath(state.cwd, state.runId, artifact, state.artifactDirectory);
81
87
  await mkdir(dirname(path), { recursive: true });
82
88
  await writeFile(path, markdown, "utf8");
83
89
  return path;
@@ -225,9 +231,104 @@ function verificationBlock(results: VerificationResult[]): string {
225
231
  .join("\n\n");
226
232
  }
227
233
 
234
+ function verificationStatusFor(results: VerificationResult[]): VerificationStatus {
235
+ if (results.length === 0) {
236
+ return "not_run";
237
+ }
238
+ return results.every((result) => result.exitCode === 0) ? "passed" : "failed";
239
+ }
240
+
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`);
301
+ }
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
+
228
312
  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`;
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}`;
317
+ }
318
+ return `${content}\n`;
319
+ }
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`;
231
332
  }
232
333
  return markdown;
233
334
  }
@@ -248,9 +349,22 @@ async function runCurrentPhaseRole(state: RunState, runner: RoleRunner = runRole
248
349
  const workspace = await ensureExecutionWorkspace(state);
249
350
  state.executionWorkspace = workspace;
250
351
  state.verification = [];
352
+ state.verificationStatus = "not_run";
353
+ delete state.verificationWaiver;
251
354
  delete state.artifacts["test-report"];
252
355
  await saveState(state);
253
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
+
254
368
  const result = await runner({
255
369
  backend: state.backend,
256
370
  role: "implementer",
@@ -258,9 +372,10 @@ async function runCurrentPhaseRole(state: RunState, runner: RoleRunner = runRole
258
372
  request: state.request,
259
373
  mode: state.mode,
260
374
  executionMode: "apply",
375
+ executionPolicy: state.executionPolicy,
261
376
  cwd: workspace.path,
262
377
  standards: state.standards.combined,
263
- artifactPath: artifactPath(state.cwd, state.runId, "implementation-review"),
378
+ artifactPath: artifactPath(state.cwd, state.runId, "implementation-review", state.artifactDirectory),
264
379
  answers: state.answers.map((entry) => entry.answer),
265
380
  feedback: state.feedback.map((entry) => `${entry.gate}: ${entry.message}`),
266
381
  priorArtifacts: await readPriorArtifacts(state),
@@ -271,15 +386,17 @@ async function runCurrentPhaseRole(state: RunState, runner: RoleRunner = runRole
271
386
  state.changedFiles = captured.changedFiles;
272
387
  state.implementationDiff = captured.patch;
273
388
  state.roles.push(result);
389
+ state.executionInstruction = undefined;
274
390
  await writeImplementationReview(state);
275
- state.phase = "testing";
391
+ state.phase = "review";
276
392
  state.status = "ready";
277
393
  return saveState(state);
278
394
  }
279
395
 
280
- const gate = gateForPhase(state.phase);
396
+ const configuredGate = gateForPhase(state.phase);
397
+ const gate = gateForPhase(state.phase, state.enabledGates);
281
398
  const role = roleForPhase(state.phase);
282
- if (!gate || !role) {
399
+ if (!role) {
283
400
  const artifact = artifactForPhase(state.phase);
284
401
  const markdown = renderArtifact(artifact, state);
285
402
  state.artifacts[artifact] = await writeMarkdownArtifact(state, artifact, markdown);
@@ -289,13 +406,24 @@ async function runCurrentPhaseRole(state: RunState, runner: RoleRunner = runRole
289
406
  }
290
407
 
291
408
  const artifact = artifactForPhase(state.phase);
292
- const path = artifactPath(state.cwd, state.runId, artifact);
409
+ const path = artifactPath(state.cwd, state.runId, artifact, state.artifactDirectory);
293
410
  const applyingTesting = state.executionMode === "apply" && state.phase === "testing";
294
411
  const roleCwd = applyingTesting ? state.executionWorkspace?.path : state.cwd;
295
412
  if (!roleCwd) {
296
413
  throw new Error("DevCrew apply testing requires an execution workspace");
297
414
  }
298
- state.roles.push(conductorDecision(state, role, gate));
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);
426
+ }
299
427
 
300
428
  const result = await runner({
301
429
  backend: state.backend,
@@ -304,6 +432,7 @@ async function runCurrentPhaseRole(state: RunState, runner: RoleRunner = runRole
304
432
  request: state.request,
305
433
  mode: state.mode,
306
434
  executionMode: state.executionMode,
435
+ executionPolicy: state.executionPolicy,
307
436
  cwd: roleCwd,
308
437
  standards: state.standards.combined,
309
438
  artifactPath: path,
@@ -314,20 +443,61 @@ async function runCurrentPhaseRole(state: RunState, runner: RoleRunner = runRole
314
443
 
315
444
  if (applyingTesting && state.executionWorkspace) {
316
445
  state.verification = await runConfiguredVerification(state, roleCwd);
446
+ state.verificationStatus = verificationStatusFor(state.verification);
317
447
  const captured = await captureExecutionChanges(state.executionWorkspace);
318
448
  state.changedFiles = captured.changedFiles;
319
449
  state.implementationDiff = captured.patch;
320
450
  await writeImplementationReview(state);
321
451
  }
322
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
+ };
462
+ }
463
+
323
464
  // When the backend cannot run a real SDK we keep a single deterministic
324
465
  // artifact source by rendering the rich phase template from the core layer.
325
466
  const baseMarkdown = result.usedFallback ? `${fallbackNotice(result)}${renderArtifact(artifact, state)}` : result.markdown;
326
467
  const markdown = appendExecutionSections(artifact, baseMarkdown, state);
327
468
  state.roles.push({ ...result, markdown });
328
469
  state.artifacts[artifact] = await writeMarkdownArtifact(state, artifact, markdown);
329
- state.gates[gate] = "pending";
330
- 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
+ }
331
501
  return saveState(state);
332
502
  }
333
503
 
@@ -338,7 +508,12 @@ export async function startOrchestratedWorkflow(input: StartWorkflowInput, runne
338
508
 
339
509
  export async function continueOrchestratedWorkflow(input: RunRef, runner: RoleRunner = runRole): Promise<RunState> {
340
510
  const state = await getWorkflowStatus(input);
341
- 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
+ ) {
342
517
  return state;
343
518
  }
344
519
 
@@ -377,6 +552,9 @@ export async function approveOrchestratedWorkflow(input: ApproveWorkflowInput):
377
552
  return cleanupAfterApproval(before);
378
553
  }
379
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
+ }
380
558
  await promoteExecutionChanges(before);
381
559
  let approved: RunState;
382
560
  try {
@@ -396,6 +574,67 @@ export async function approveOrchestratedWorkflow(input: ApproveWorkflowInput):
396
574
  return approveWorkflow(input);
397
575
  }
398
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
+ }
586
+
587
+ export async function completeOrchestratedExecution(input: CompleteExecutionInput): Promise<RunState> {
588
+ const state = await getWorkflowStatus(input);
589
+ const instruction = state.executionInstruction;
590
+ if (
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
598
+ ) {
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";
616
+ return saveState(state);
617
+ }
618
+
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
+
399
638
  export async function rejectOrchestratedWorkflow(input: RejectWorkflowInput): Promise<RunState> {
400
639
  return rejectWorkflow(input);
401
640
  }
@@ -414,9 +653,8 @@ export async function answerOrchestratedWorkflow(input: AnswerWorkflowInput, run
414
653
  return saveState(state);
415
654
  }
416
655
 
417
- const gate = gateForPhase(state.phase);
418
656
  const role = roleForPhase(state.phase);
419
- if (!gate || !role) {
657
+ if (!role) {
420
658
  return state;
421
659
  }
422
660
 
@@ -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