@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
@@ -3,7 +3,8 @@ import { ensureConfig } from "./config.js";
3
3
  import { readArtifact, writeArtifact } from "./artifacts.js";
4
4
  import { discoverStandards } from "./standards.js";
5
5
  import { loadState, saveState } from "./store.js";
6
- import { parseAnswer, parseArtifactName, parseBackend, parseCwd, parseExecutionMode, parseFeedback, parseGate, parseHost, parseOptionalNote, parseRequest, parseRunId, parseWorkflowMode, } from "./validation.js";
6
+ import { parseAnswer, parseArtifactName, parseBackend, parseCwd, parseExecutionMode, parseExecutionPolicy, parseFeedback, parseGate, parseHost, parseOptionalNote, parseRequest, parseRunId, parseWaiverReason, parseWorkflowMode, } from "./validation.js";
7
+ import { GATES } from "./types.js";
7
8
  function now() {
8
9
  return new Date().toISOString();
9
10
  }
@@ -18,6 +19,8 @@ export function nextPhaseAfterGate(state, gate) {
18
19
  return "implementation";
19
20
  case "implementation":
20
21
  return state.executionMode === "apply" ? "execution" : "testing";
22
+ case "implementation-review":
23
+ return "testing";
21
24
  case "testing":
22
25
  return "acceptance";
23
26
  }
@@ -28,20 +31,32 @@ export function artifactForPhase(phase) {
28
31
  architecture: "architecture",
29
32
  implementation: "implementation-plan",
30
33
  execution: "implementation-review",
34
+ review: "architecture-review",
31
35
  testing: "test-report",
32
36
  acceptance: "acceptance",
33
37
  complete: "acceptance",
34
38
  };
35
39
  return artifactByPhase[phase];
36
40
  }
37
- export function gateForPhase(phase) {
38
- if (phase === "requirements" || phase === "architecture" || phase === "implementation" || phase === "testing") {
39
- return phase;
40
- }
41
- return undefined;
41
+ export function gateForPhase(phase, enabledGates) {
42
+ const gate = phase === "review"
43
+ ? "implementation-review"
44
+ : (phase === "requirements" ||
45
+ phase === "architecture" ||
46
+ phase === "implementation" ||
47
+ phase === "testing"
48
+ ? phase
49
+ : undefined);
50
+ return gate && (!enabledGates || enabledGates.includes(gate)) ? gate : undefined;
51
+ }
52
+ function configuredGates(gates) {
53
+ const configured = new Set(gates.filter((gate) => GATES.includes(gate)));
54
+ configured.add("implementation-review");
55
+ configured.add("testing");
56
+ return [...configured];
42
57
  }
43
58
  function assertPendingCurrentGate(state, gate) {
44
- const expected = gateForPhase(state.phase);
59
+ const expected = gateForPhase(state.phase, state.enabledGates);
45
60
  if (expected !== gate) {
46
61
  throw new Error(expected
47
62
  ? `Cannot act on ${gate} while current gate is ${expected}`
@@ -65,6 +80,8 @@ export async function startWorkflow(input, options = {}) {
65
80
  const config = await ensureConfig(cwd);
66
81
  const backend = input.backend ? parseBackend(input.backend) : config.defaultBackend === "host-preferred" ? host : config.defaultBackend;
67
82
  const executionMode = input.executionMode ? parseExecutionMode(input.executionMode) : config.executionMode;
83
+ const executionPolicy = input.executionPolicy ? parseExecutionPolicy(input.executionPolicy) : "interactive-host";
84
+ const enabledGates = configuredGates(config.workflow.gates);
68
85
  if (executionMode === "apply" && backend === "local") {
69
86
  throw new Error("DevCrew apply mode requires a codex or claude backend; local is plan-only");
70
87
  }
@@ -76,20 +93,25 @@ export async function startWorkflow(input, options = {}) {
76
93
  host,
77
94
  mode,
78
95
  executionMode,
96
+ executionPolicy,
97
+ enabledGates,
98
+ artifactDirectory: config.workflow.artifactDirectory,
79
99
  backend,
80
100
  request,
81
101
  phase: "requirements",
82
- status: "awaiting_approval",
102
+ status: enabledGates.includes("requirements") ? "awaiting_approval" : "ready",
83
103
  createdAt,
84
104
  updatedAt: createdAt,
85
105
  gates: {
86
- requirements: "pending",
106
+ requirements: enabledGates.includes("requirements") ? "pending" : "not_started",
87
107
  architecture: "not_started",
88
108
  implementation: "not_started",
109
+ "implementation-review": "not_started",
89
110
  testing: "not_started",
90
111
  },
91
112
  artifacts: {},
92
113
  roles: [],
114
+ pendingQuestions: [],
93
115
  answers: [],
94
116
  approvals: [],
95
117
  feedback: [],
@@ -97,6 +119,7 @@ export async function startWorkflow(input, options = {}) {
97
119
  changedFiles: [],
98
120
  implementationDiff: "",
99
121
  verification: [],
122
+ verificationStatus: "not_run",
100
123
  lintResults: [],
101
124
  };
102
125
  if (!options.skipArtifactWrite) {
@@ -109,7 +132,10 @@ export async function getWorkflowStatus(input) {
109
132
  }
110
133
  export async function continueWorkflow(input) {
111
134
  const state = await getWorkflowStatus(input);
112
- if (state.status === "awaiting_approval" || state.status === "awaiting_input" || state.status === "complete") {
135
+ if (state.status === "awaiting_approval" ||
136
+ state.status === "awaiting_input" ||
137
+ state.status === "awaiting_execution" ||
138
+ state.status === "complete") {
113
139
  return state;
114
140
  }
115
141
  if (state.phase === "acceptance") {
@@ -121,8 +147,14 @@ export async function continueWorkflow(input) {
121
147
  if (state.phase === "execution") {
122
148
  throw new Error("DevCrew execution phase requires orchestrated continuation");
123
149
  }
124
- const gate = gateForPhase(state.phase);
150
+ const configuredGate = gateForPhase(state.phase);
151
+ const gate = gateForPhase(state.phase, state.enabledGates);
125
152
  if (!gate) {
153
+ if (configuredGate) {
154
+ state.phase = nextPhaseAfterGate(state, configuredGate);
155
+ state.status = "ready";
156
+ return saveState(state);
157
+ }
126
158
  state.status = "complete";
127
159
  return saveState(state);
128
160
  }
@@ -176,7 +208,16 @@ export async function rejectWorkflow(input) {
176
208
  }
177
209
  export async function answerWorkflow(input, options = {}) {
178
210
  const state = await getWorkflowStatus(input);
179
- const gate = gateForPhase(state.phase);
211
+ const gate = gateForPhase(state.phase, state.enabledGates);
212
+ if (state.status === "awaiting_input" && state.pendingQuestions.length > 0 && state.phase === "requirements") {
213
+ state.answers.push({
214
+ answer: parseAnswer(input.answer),
215
+ createdAt: now(),
216
+ });
217
+ state.pendingQuestions = [];
218
+ state.status = "ready";
219
+ return saveState(state);
220
+ }
180
221
  if (state.status !== "awaiting_input" || !gate || state.gates[gate] !== "rejected") {
181
222
  throw new Error("Workflow must be awaiting_input at a rejected current gate before recording an answer");
182
223
  }
@@ -191,6 +232,23 @@ export async function answerWorkflow(input, options = {}) {
191
232
  }
192
233
  return saveState(state);
193
234
  }
235
+ export async function waiveVerificationWorkflow(input) {
236
+ const state = await getWorkflowStatus(input);
237
+ if (state.executionMode !== "apply" ||
238
+ state.phase !== "testing" ||
239
+ state.status !== "awaiting_input" ||
240
+ state.gates.testing !== "rejected" ||
241
+ state.verificationStatus !== "failed") {
242
+ throw new Error("A verification waiver is only available after failed apply-mode testing");
243
+ }
244
+ state.verificationWaiver = {
245
+ reason: parseWaiverReason(input.reason),
246
+ createdAt: now(),
247
+ };
248
+ state.gates.testing = "pending";
249
+ state.status = "awaiting_approval";
250
+ return saveState(state);
251
+ }
194
252
  export async function getArtifact(input) {
195
253
  const state = await getWorkflowStatus(input);
196
254
  return readArtifact(state, parseArtifactName(input.name));
@@ -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 { 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];
@@ -35,7 +36,7 @@ function priorArtifactNamesForPhase(phase) {
35
36
  return ARTIFACTS.slice(0, currentIndex);
36
37
  }
37
38
  async function writeMarkdownArtifact(state, artifact, markdown) {
38
- const path = artifactPath(state.cwd, state.runId, artifact);
39
+ const path = artifactPath(state.cwd, state.runId, artifact, state.artifactDirectory);
39
40
  await mkdir(dirname(path), { recursive: true });
40
41
  await writeFile(path, markdown, "utf8");
41
42
  return path;
@@ -162,9 +163,96 @@ function verificationBlock(results) {
162
163
  .map((result) => `### ${result.command}\n\nExit Code: ${result.exitCode}\n\nOutput:\n\n\`\`\`text\n${result.output || "(no output)"}\n\`\`\``)
163
164
  .join("\n\n");
164
165
  }
166
+ function verificationStatusFor(results) {
167
+ if (results.length === 0) {
168
+ return "not_run";
169
+ }
170
+ return results.every((result) => result.exitCode === 0) ? "passed" : "failed";
171
+ }
172
+ function setTestingGateFromVerification(state) {
173
+ if (state.verificationStatus === "failed") {
174
+ state.gates.testing = "rejected";
175
+ state.status = "awaiting_input";
176
+ state.feedback.push({
177
+ gate: "testing",
178
+ message: "Automated verification failed. Inspect the test report, revise the implementation, or record an explicit verification waiver with its reason.",
179
+ createdAt: now(),
180
+ });
181
+ return;
182
+ }
183
+ state.gates.testing = "pending";
184
+ state.status = "awaiting_approval";
185
+ }
186
+ function executionInstructions(state, phase, workspacePath) {
187
+ if (phase === "execution") {
188
+ return `Use the native ${state.host} host agent to implement the approved change in ${workspacePath}. Do not modify the requester checkout at ${state.cwd}. When complete, call devcrew_complete_execution with a concise summary.`;
189
+ }
190
+ return `Use the native ${state.host} host agent to validate the approved change in ${workspacePath}. Then call devcrew_complete_execution with a concise summary and each command's exit code and output.`;
191
+ }
192
+ function hostCompletionResult(state, summary) {
193
+ const role = state.phase === "execution" ? "implementer" : "tester";
194
+ const artifact = artifactForPhase(state.phase);
195
+ return {
196
+ role,
197
+ backend: state.backend,
198
+ summary: `${role} completed through the native ${state.host} host: ${summary}`,
199
+ markdown: `${renderArtifact(artifact, state).trim()}\n\n## Native Host Summary\n\n${summary}\n`,
200
+ usedFallback: false,
201
+ };
202
+ }
203
+ function parseCompletionSummary(value) {
204
+ if (typeof value !== "string" || value.trim().length === 0) {
205
+ throw new Error("summary must be a non-empty string");
206
+ }
207
+ return value.trim();
208
+ }
209
+ function parseCompletionVerification(value) {
210
+ if (value === undefined) {
211
+ return [];
212
+ }
213
+ if (!Array.isArray(value)) {
214
+ throw new Error("verification must be an array");
215
+ }
216
+ return value.map((entry, index) => {
217
+ if (!entry ||
218
+ typeof entry !== "object" ||
219
+ typeof entry.command !== "string" ||
220
+ entry.command.trim().length === 0 ||
221
+ !Number.isInteger(entry.exitCode) ||
222
+ typeof entry.output !== "string" ||
223
+ typeof entry.startedAt !== "string" ||
224
+ typeof entry.completedAt !== "string") {
225
+ throw new Error(`verification[${index}] must include command, integer exitCode, output, startedAt, and completedAt`);
226
+ }
227
+ return {
228
+ command: entry.command.trim(),
229
+ exitCode: entry.exitCode,
230
+ output: entry.output,
231
+ startedAt: entry.startedAt,
232
+ completedAt: entry.completedAt,
233
+ };
234
+ });
235
+ }
165
236
  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`;
237
+ if (artifact === "architecture-review" && state.architectureReview) {
238
+ let content = markdown.trim();
239
+ if (!content.includes("## Review Decision")) {
240
+ content += `\n\n## Review Decision\n\nDecision: ${state.architectureReview.decision}\n\nSummary: ${state.architectureReview.summary}`;
241
+ }
242
+ return `${content}\n`;
243
+ }
244
+ if (artifact === "test-report") {
245
+ let content = markdown.trim();
246
+ if (!content.includes("## Acceptance Evidence")) {
247
+ content += `\n\n## Acceptance Evidence\n\n${verificationBlock(state.verification)}`;
248
+ }
249
+ if (!content.includes("## Verification Outcome")) {
250
+ content += `\n\n## Verification Outcome\n\nStatus: ${state.verificationStatus}`;
251
+ }
252
+ if (state.verificationWaiver && !content.includes("## Verification Waiver")) {
253
+ content += `\n\n## Verification Waiver\n\nReason: ${state.verificationWaiver.reason}\nRecorded At: ${state.verificationWaiver.createdAt}`;
254
+ }
255
+ return `${content}\n`;
168
256
  }
169
257
  return markdown;
170
258
  }
@@ -179,8 +267,20 @@ async function runCurrentPhaseRole(state, runner = runRole) {
179
267
  const workspace = await ensureExecutionWorkspace(state);
180
268
  state.executionWorkspace = workspace;
181
269
  state.verification = [];
270
+ state.verificationStatus = "not_run";
271
+ delete state.verificationWaiver;
182
272
  delete state.artifacts["test-report"];
183
273
  await saveState(state);
274
+ if (state.executionPolicy === "interactive-host") {
275
+ state.executionInstruction = {
276
+ phase: "execution",
277
+ workspacePath: workspace.path,
278
+ instructions: executionInstructions(state, "execution", workspace.path),
279
+ createdAt: now(),
280
+ };
281
+ state.status = "awaiting_execution";
282
+ return saveState(state);
283
+ }
184
284
  const result = await runner({
185
285
  backend: state.backend,
186
286
  role: "implementer",
@@ -188,9 +288,10 @@ async function runCurrentPhaseRole(state, runner = runRole) {
188
288
  request: state.request,
189
289
  mode: state.mode,
190
290
  executionMode: "apply",
291
+ executionPolicy: state.executionPolicy,
191
292
  cwd: workspace.path,
192
293
  standards: state.standards.combined,
193
- artifactPath: artifactPath(state.cwd, state.runId, "implementation-review"),
294
+ artifactPath: artifactPath(state.cwd, state.runId, "implementation-review", state.artifactDirectory),
194
295
  answers: state.answers.map((entry) => entry.answer),
195
296
  feedback: state.feedback.map((entry) => `${entry.gate}: ${entry.message}`),
196
297
  priorArtifacts: await readPriorArtifacts(state),
@@ -201,14 +302,16 @@ async function runCurrentPhaseRole(state, runner = runRole) {
201
302
  state.changedFiles = captured.changedFiles;
202
303
  state.implementationDiff = captured.patch;
203
304
  state.roles.push(result);
305
+ state.executionInstruction = undefined;
204
306
  await writeImplementationReview(state);
205
- state.phase = "testing";
307
+ state.phase = "review";
206
308
  state.status = "ready";
207
309
  return saveState(state);
208
310
  }
209
- const gate = gateForPhase(state.phase);
311
+ const configuredGate = gateForPhase(state.phase);
312
+ const gate = gateForPhase(state.phase, state.enabledGates);
210
313
  const role = roleForPhase(state.phase);
211
- if (!gate || !role) {
314
+ if (!role) {
212
315
  const artifact = artifactForPhase(state.phase);
213
316
  const markdown = renderArtifact(artifact, state);
214
317
  state.artifacts[artifact] = await writeMarkdownArtifact(state, artifact, markdown);
@@ -217,13 +320,23 @@ async function runCurrentPhaseRole(state, runner = runRole) {
217
320
  return saveState(state);
218
321
  }
219
322
  const artifact = artifactForPhase(state.phase);
220
- const path = artifactPath(state.cwd, state.runId, artifact);
323
+ const path = artifactPath(state.cwd, state.runId, artifact, state.artifactDirectory);
221
324
  const applyingTesting = state.executionMode === "apply" && state.phase === "testing";
222
325
  const roleCwd = applyingTesting ? state.executionWorkspace?.path : state.cwd;
223
326
  if (!roleCwd) {
224
327
  throw new Error("DevCrew apply testing requires an execution workspace");
225
328
  }
226
- state.roles.push(conductorDecision(state, role, gate));
329
+ state.roles.push(conductorDecision(state, role, gate ?? `${state.phase} automatic transition`));
330
+ if (applyingTesting && state.executionPolicy === "interactive-host") {
331
+ state.executionInstruction = {
332
+ phase: "testing",
333
+ workspacePath: roleCwd,
334
+ instructions: executionInstructions(state, "testing", roleCwd),
335
+ createdAt: now(),
336
+ };
337
+ state.status = "awaiting_execution";
338
+ return saveState(state);
339
+ }
227
340
  const result = await runner({
228
341
  backend: state.backend,
229
342
  role,
@@ -231,6 +344,7 @@ async function runCurrentPhaseRole(state, runner = runRole) {
231
344
  request: state.request,
232
345
  mode: state.mode,
233
346
  executionMode: state.executionMode,
347
+ executionPolicy: state.executionPolicy,
234
348
  cwd: roleCwd,
235
349
  standards: state.standards.combined,
236
350
  artifactPath: path,
@@ -240,19 +354,60 @@ async function runCurrentPhaseRole(state, runner = runRole) {
240
354
  });
241
355
  if (applyingTesting && state.executionWorkspace) {
242
356
  state.verification = await runConfiguredVerification(state, roleCwd);
357
+ state.verificationStatus = verificationStatusFor(state.verification);
243
358
  const captured = await captureExecutionChanges(state.executionWorkspace);
244
359
  state.changedFiles = captured.changedFiles;
245
360
  state.implementationDiff = captured.patch;
246
361
  await writeImplementationReview(state);
247
362
  }
363
+ if (state.phase === "review") {
364
+ if (!result.reviewDecision) {
365
+ throw new Error("DevCrew architecture review must return a structured review decision");
366
+ }
367
+ state.architectureReview = {
368
+ decision: result.reviewDecision,
369
+ summary: result.summary,
370
+ reviewedAt: now(),
371
+ };
372
+ }
248
373
  // When the backend cannot run a real SDK we keep a single deterministic
249
374
  // artifact source by rendering the rich phase template from the core layer.
250
375
  const baseMarkdown = result.usedFallback ? `${fallbackNotice(result)}${renderArtifact(artifact, state)}` : result.markdown;
251
376
  const markdown = appendExecutionSections(artifact, baseMarkdown, state);
252
377
  state.roles.push({ ...result, markdown });
253
378
  state.artifacts[artifact] = await writeMarkdownArtifact(state, artifact, markdown);
254
- state.gates[gate] = "pending";
255
- state.status = "awaiting_approval";
379
+ state.executionInstruction = undefined;
380
+ if (state.phase === "requirements" && result.questions && result.questions.length > 0) {
381
+ state.pendingQuestions = result.questions;
382
+ state.gates.requirements = "not_started";
383
+ state.status = "awaiting_input";
384
+ return saveState(state);
385
+ }
386
+ if (state.phase === "review" && state.architectureReview?.decision === "changes_required") {
387
+ state.gates["implementation-review"] = "rejected";
388
+ state.status = "awaiting_input";
389
+ state.feedback.push({
390
+ gate: "implementation-review",
391
+ message: state.architectureReview.summary,
392
+ createdAt: now(),
393
+ });
394
+ return saveState(state);
395
+ }
396
+ if (!gate && configuredGate) {
397
+ state.phase = nextPhaseAfterGate(state, configuredGate);
398
+ state.status = "ready";
399
+ return saveState(state);
400
+ }
401
+ if (applyingTesting) {
402
+ setTestingGateFromVerification(state);
403
+ }
404
+ else {
405
+ if (!gate) {
406
+ throw new Error(`DevCrew has no configured gate for ${state.phase}`);
407
+ }
408
+ state.gates[gate] = "pending";
409
+ state.status = "awaiting_approval";
410
+ }
256
411
  return saveState(state);
257
412
  }
258
413
  export async function startOrchestratedWorkflow(input, runner = runRole) {
@@ -261,7 +416,10 @@ export async function startOrchestratedWorkflow(input, runner = runRole) {
261
416
  }
262
417
  export async function continueOrchestratedWorkflow(input, runner = runRole) {
263
418
  const state = await getWorkflowStatus(input);
264
- if (state.status === "awaiting_approval" || state.status === "awaiting_input" || state.status === "complete") {
419
+ if (state.status === "awaiting_approval" ||
420
+ state.status === "awaiting_input" ||
421
+ state.status === "awaiting_execution" ||
422
+ state.status === "complete") {
265
423
  return state;
266
424
  }
267
425
  return runCurrentPhaseRole(state, runner);
@@ -296,6 +454,9 @@ export async function approveOrchestratedWorkflow(input) {
296
454
  return cleanupAfterApproval(before);
297
455
  }
298
456
  if (promotingTesting) {
457
+ if (before.verificationStatus === "failed" && !before.verificationWaiver) {
458
+ throw new Error("Failed verification cannot be promoted without an explicit verification waiver");
459
+ }
299
460
  await promoteExecutionChanges(before);
300
461
  let approved;
301
462
  try {
@@ -315,6 +476,61 @@ export async function approveOrchestratedWorkflow(input) {
315
476
  }
316
477
  return approveWorkflow(input);
317
478
  }
479
+ export async function waiveOrchestratedVerification(input) {
480
+ const state = await waiveVerificationWorkflow(input);
481
+ const reportPath = state.artifacts["test-report"];
482
+ if (reportPath) {
483
+ const report = await readFile(reportPath, "utf8");
484
+ await writeFile(reportPath, appendExecutionSections("test-report", report, state), "utf8");
485
+ }
486
+ return state;
487
+ }
488
+ export async function completeOrchestratedExecution(input) {
489
+ const state = await getWorkflowStatus(input);
490
+ const instruction = state.executionInstruction;
491
+ if (state.executionMode !== "apply" ||
492
+ state.executionPolicy !== "interactive-host" ||
493
+ state.status !== "awaiting_execution" ||
494
+ !instruction ||
495
+ instruction.phase !== state.phase ||
496
+ !state.executionWorkspace ||
497
+ instruction.workspacePath !== state.executionWorkspace.path) {
498
+ throw new Error("DevCrew is not awaiting an interactive-host execution completion");
499
+ }
500
+ const summary = parseCompletionSummary(input.summary);
501
+ if (state.phase === "execution") {
502
+ if (input.verification !== undefined) {
503
+ throw new Error("verification can only be submitted after interactive-host testing");
504
+ }
505
+ const captured = await captureExecutionChanges(state.executionWorkspace);
506
+ state.changedFiles = captured.changedFiles;
507
+ state.implementationDiff = captured.patch;
508
+ state.lintResults = [];
509
+ state.roles.push(hostCompletionResult(state, summary));
510
+ state.executionInstruction = undefined;
511
+ await writeImplementationReview(state);
512
+ state.phase = "review";
513
+ state.status = "ready";
514
+ return saveState(state);
515
+ }
516
+ if (state.phase !== "testing") {
517
+ throw new Error(`Cannot complete interactive-host work during ${state.phase}`);
518
+ }
519
+ state.verification = parseCompletionVerification(input.verification);
520
+ state.verificationStatus = verificationStatusFor(state.verification);
521
+ const captured = await captureExecutionChanges(state.executionWorkspace);
522
+ state.changedFiles = captured.changedFiles;
523
+ state.implementationDiff = captured.patch;
524
+ await writeImplementationReview(state);
525
+ const result = hostCompletionResult(state, summary);
526
+ const artifact = artifactForPhase(state.phase);
527
+ const markdown = appendExecutionSections(artifact, result.markdown, state);
528
+ state.roles.push({ ...result, markdown });
529
+ state.artifacts[artifact] = await writeMarkdownArtifact(state, artifact, markdown);
530
+ state.executionInstruction = undefined;
531
+ setTestingGateFromVerification(state);
532
+ return saveState(state);
533
+ }
318
534
  export async function rejectOrchestratedWorkflow(input) {
319
535
  return rejectWorkflow(input);
320
536
  }
@@ -329,9 +545,8 @@ export async function answerOrchestratedWorkflow(input, runner = runRole) {
329
545
  state.gates.testing = "not_started";
330
546
  return saveState(state);
331
547
  }
332
- const gate = gateForPhase(state.phase);
333
548
  const role = roleForPhase(state.phase);
334
- if (!gate || !role) {
549
+ if (!role) {
335
550
  return state;
336
551
  }
337
552
  // 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) {