@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
@@ -0,0 +1,104 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
+
4
+ import { devcrewDir, repositoryLockPath } from "./paths.js";
5
+
6
+ interface RepositoryLockMetadata {
7
+ ownerId: string;
8
+ pid: number;
9
+ createdAt: string;
10
+ }
11
+
12
+ function metadataPath(cwd: string): string {
13
+ return `${repositoryLockPath(cwd)}/lock.json`;
14
+ }
15
+
16
+ async function readMetadata(cwd: string): Promise<RepositoryLockMetadata | undefined> {
17
+ try {
18
+ const parsed = JSON.parse(await readFile(metadataPath(cwd), "utf8")) as Partial<RepositoryLockMetadata>;
19
+ const pid = parsed.pid;
20
+ if (
21
+ typeof parsed.ownerId !== "string" ||
22
+ typeof pid !== "number" ||
23
+ !Number.isInteger(pid) ||
24
+ typeof parsed.createdAt !== "string"
25
+ ) {
26
+ return undefined;
27
+ }
28
+ return {
29
+ ownerId: parsed.ownerId,
30
+ pid,
31
+ createdAt: parsed.createdAt,
32
+ };
33
+ } catch {
34
+ return undefined;
35
+ }
36
+ }
37
+
38
+ function processIsLive(pid: number): boolean {
39
+ if (pid <= 0) {
40
+ return false;
41
+ }
42
+ try {
43
+ process.kill(pid, 0);
44
+ return true;
45
+ } catch (error) {
46
+ return (error as NodeJS.ErrnoException).code === "EPERM";
47
+ }
48
+ }
49
+
50
+ async function releaseRepositoryLock(cwd: string, ownerId: string): Promise<void> {
51
+ const metadata = await readMetadata(cwd);
52
+ if (metadata?.ownerId === ownerId) {
53
+ await rm(repositoryLockPath(cwd), { recursive: true, force: true });
54
+ }
55
+ }
56
+
57
+ export async function withRepositoryLock<T>(cwd: string, action: () => Promise<T>): Promise<T> {
58
+ await mkdir(devcrewDir(cwd), { recursive: true });
59
+ const ownerId = randomUUID();
60
+ try {
61
+ await mkdir(repositoryLockPath(cwd));
62
+ } catch (error) {
63
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") {
64
+ throw error;
65
+ }
66
+ const metadata = await readMetadata(cwd);
67
+ if (metadata && processIsLive(metadata.pid)) {
68
+ throw new Error("A DevCrew operation is already in progress for this repository");
69
+ }
70
+ throw new Error("A stale DevCrew repository lock exists; use devcrew_recover for explicit recovery");
71
+ }
72
+
73
+ await writeFile(
74
+ metadataPath(cwd),
75
+ `${JSON.stringify({ ownerId, pid: process.pid, createdAt: new Date().toISOString() } satisfies RepositoryLockMetadata, null, 2)}\n`,
76
+ "utf8",
77
+ );
78
+ try {
79
+ return await action();
80
+ } finally {
81
+ await releaseRepositoryLock(cwd, ownerId);
82
+ }
83
+ }
84
+
85
+ export async function recoverRepositoryLock(cwd: string): Promise<boolean> {
86
+ const lockPath = repositoryLockPath(cwd);
87
+ const metadata = await readMetadata(cwd);
88
+ if (!metadata) {
89
+ try {
90
+ await rm(lockPath, { recursive: true, force: false });
91
+ return true;
92
+ } catch (error) {
93
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
94
+ return false;
95
+ }
96
+ throw error;
97
+ }
98
+ }
99
+ if (processIsLive(metadata.pid)) {
100
+ throw new Error("Cannot recover a DevCrew repository lock held by a live process");
101
+ }
102
+ await rm(lockPath, { recursive: true, force: true });
103
+ return true;
104
+ }
@@ -31,28 +31,33 @@ export function activeRunPath(cwd: string): string {
31
31
  return join(devcrewDir(cwd), "active-run.json");
32
32
  }
33
33
 
34
+ export function repositoryLockPath(cwd: string): string {
35
+ return join(devcrewDir(cwd), "lock");
36
+ }
37
+
34
38
  export function standardsPath(cwd: string): string {
35
39
  return join(devcrewDir(cwd), "standards.md");
36
40
  }
37
41
 
38
- export function docsRoot(cwd: string): string {
39
- return join(cwd, "docs", "devcrew");
42
+ export function docsRoot(cwd: string, artifactDirectory: string = "docs/devcrew"): string {
43
+ return join(cwd, artifactDirectory);
40
44
  }
41
45
 
42
- export function docsRunDir(cwd: string, runId: string): string {
43
- return join(docsRoot(cwd), runId);
46
+ export function docsRunDir(cwd: string, runId: string, artifactDirectory: string = "docs/devcrew"): string {
47
+ return join(docsRoot(cwd, artifactDirectory), runId);
44
48
  }
45
49
 
46
- export function artifactPath(cwd: string, runId: string, artifact: ArtifactName): string {
50
+ export function artifactPath(cwd: string, runId: string, artifact: ArtifactName, artifactDirectory: string = "docs/devcrew"): string {
47
51
  const filenameByArtifact: Record<ArtifactName, string> = {
48
52
  requirements: "requirements.md",
49
53
  architecture: "architecture.md",
50
54
  "implementation-plan": "implementation-plan.md",
51
55
  "implementation-review": "implementation-review.md",
56
+ "architecture-review": "architecture-review.md",
52
57
  "test-report": "test-report.md",
53
58
  acceptance: "acceptance.md",
54
59
  };
55
- return join(docsRunDir(cwd, runId), filenameByArtifact[artifact]);
60
+ return join(docsRunDir(cwd, runId, artifactDirectory), filenameByArtifact[artifact]);
56
61
  }
57
62
 
58
63
  export async function ensureRunDirectories(cwd: string, runId: string): Promise<void> {
@@ -2,7 +2,24 @@ import { readFile, rename, writeFile } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
3
 
4
4
  import { ensureRunDirectories, statePath } from "./paths.js";
5
- import type { RunState } from "./types.js";
5
+ import { GATES, type GateName, type RoleResult, type RunState } from "./types.js";
6
+
7
+ function enabledGatesFromState(value: unknown): GateName[] {
8
+ const configured = Array.isArray(value)
9
+ ? value.filter((gate): gate is GateName => typeof gate === "string" && GATES.includes(gate as GateName))
10
+ : [...GATES];
11
+ return [...new Set<GateName>([...configured, "implementation-review", "testing"])];
12
+ }
13
+
14
+ function roleResultsFromState(value: unknown): RoleResult[] {
15
+ if (!Array.isArray(value)) {
16
+ return [];
17
+ }
18
+ return value.filter((role): role is RoleResult => typeof role === "object" && role !== null).map((role) => ({
19
+ ...role,
20
+ format: role.format === "structured" ? "structured" : "legacy",
21
+ }));
22
+ }
6
23
 
7
24
  export async function saveState(state: RunState): Promise<RunState> {
8
25
  state.updatedAt = new Date().toISOString();
@@ -18,18 +35,45 @@ export async function loadState(cwd: string, runId: string): Promise<RunState> {
18
35
  const raw = await readFile(statePath(cwd, runId), "utf8");
19
36
  const parsed = JSON.parse(raw) as RunState;
20
37
  const executionWorkspace = parsed.executionWorkspace;
38
+ const executionInstruction = parsed.executionInstruction;
39
+ const verificationWaiver = parsed.verificationWaiver;
21
40
  return {
22
41
  ...parsed,
23
42
  executionMode: parsed.executionMode ?? "plan",
43
+ executionPolicy: parsed.executionPolicy ?? "interactive-host",
44
+ roles: roleResultsFromState(parsed.roles),
45
+ enabledGates: enabledGatesFromState(parsed.enabledGates),
46
+ artifactDirectory: typeof parsed.artifactDirectory === "string" && parsed.artifactDirectory.trim().length > 0
47
+ ? parsed.artifactDirectory
48
+ : "docs/devcrew",
24
49
  executionWorkspace:
25
50
  executionWorkspace &&
26
51
  typeof executionWorkspace.path === "string" &&
27
52
  typeof executionWorkspace.baseCommit === "string"
28
53
  ? executionWorkspace
29
54
  : undefined,
55
+ executionInstruction:
56
+ executionInstruction &&
57
+ (executionInstruction.phase === "execution" || executionInstruction.phase === "testing") &&
58
+ typeof executionInstruction.workspacePath === "string" &&
59
+ typeof executionInstruction.instructions === "string" &&
60
+ typeof executionInstruction.createdAt === "string"
61
+ ? executionInstruction
62
+ : undefined,
30
63
  changedFiles: Array.isArray(parsed.changedFiles) ? parsed.changedFiles : [],
64
+ pendingQuestions: Array.isArray(parsed.pendingQuestions)
65
+ ? parsed.pendingQuestions.filter((question): question is string => typeof question === "string" && question.trim().length > 0)
66
+ : [],
31
67
  implementationDiff: typeof parsed.implementationDiff === "string" ? parsed.implementationDiff : "",
32
68
  verification: Array.isArray(parsed.verification) ? parsed.verification : [],
69
+ verificationStatus:
70
+ parsed.verificationStatus === "passed" || parsed.verificationStatus === "failed" ? parsed.verificationStatus : "not_run",
71
+ verificationWaiver:
72
+ verificationWaiver &&
73
+ typeof verificationWaiver.reason === "string" &&
74
+ typeof verificationWaiver.createdAt === "string"
75
+ ? verificationWaiver
76
+ : undefined,
33
77
  lintResults: Array.isArray(parsed.lintResults) ? parsed.lintResults : [],
34
78
  };
35
79
  }
@@ -1,5 +1,6 @@
1
1
  export const WORKFLOW_MODES = ["feature", "greenfield"] as const;
2
2
  export const EXECUTION_MODES = ["plan", "apply"] as const;
3
+ export const EXECUTION_POLICIES = ["interactive-host", "headless-restricted", "headless-unattended"] as const;
3
4
  export const HOSTS = ["codex", "claude"] as const;
4
5
  export const BACKENDS = ["codex", "claude", "local"] as const;
5
6
  export const PHASES = [
@@ -7,35 +8,74 @@ export const PHASES = [
7
8
  "architecture",
8
9
  "implementation",
9
10
  "execution",
11
+ "review",
10
12
  "testing",
11
13
  "acceptance",
12
14
  "complete",
13
15
  ] as const;
14
- export const GATES = ["requirements", "architecture", "implementation", "testing"] as const;
16
+ export const GATES = ["requirements", "architecture", "implementation", "implementation-review", "testing"] as const;
15
17
  export const ARTIFACTS = [
16
18
  "requirements",
17
19
  "architecture",
18
20
  "implementation-plan",
19
21
  "implementation-review",
22
+ "architecture-review",
20
23
  "test-report",
21
24
  "acceptance",
22
25
  ] as const;
23
26
 
24
27
  export type WorkflowMode = (typeof WORKFLOW_MODES)[number];
25
28
  export type ExecutionMode = (typeof EXECUTION_MODES)[number];
29
+ export type ExecutionPolicy = (typeof EXECUTION_POLICIES)[number];
30
+ export type VerificationStatus = "not_run" | "passed" | "failed";
31
+ export type ArchitectureReviewDecision = "approved" | "changes_required";
26
32
  export type Host = (typeof HOSTS)[number];
27
33
  export type BackendName = (typeof BACKENDS)[number];
28
34
  export type Phase = (typeof PHASES)[number];
29
35
  export type GateName = (typeof GATES)[number];
30
36
  export type ArtifactName = (typeof ARTIFACTS)[number];
31
37
  export type GateStatus = "not_started" | "pending" | "approved" | "rejected";
32
- export type RunStatus = "ready" | "awaiting_input" | "awaiting_approval" | "complete";
38
+ export type RunStatus = "ready" | "awaiting_input" | "awaiting_approval" | "awaiting_execution" | "complete" | "aborted";
33
39
 
34
40
  export interface RoleSection {
35
41
  heading: string;
36
42
  description: string;
37
43
  }
38
44
 
45
+ export type RoleResultFormat = "legacy" | "structured";
46
+
47
+ export interface CommandEvidence {
48
+ command: string;
49
+ exitCode: number;
50
+ output?: string;
51
+ }
52
+
53
+ export interface RoleQuestion {
54
+ id: string;
55
+ prompt: string;
56
+ context?: string;
57
+ }
58
+
59
+ export interface TestCaseEvidence {
60
+ id: string;
61
+ scenario: string;
62
+ type: "happy" | "edge" | "failure" | "regression";
63
+ expected: string;
64
+ }
65
+
66
+ export interface StructuredRoleData {
67
+ schemaVersion: 1;
68
+ role: "pm" | "architect" | "implementer" | "tester";
69
+ summary: string;
70
+ risks: string[];
71
+ evidence: CommandEvidence[];
72
+ questions?: RoleQuestion[];
73
+ decisions?: string[];
74
+ reviewDecision?: ArchitectureReviewDecision;
75
+ changedFiles?: string[];
76
+ testCases?: TestCaseEvidence[];
77
+ }
78
+
39
79
  export const ROLE_SECTIONS: Record<Exclude<RoleResult["role"], "conductor">, RoleSection[]> = {
40
80
  pm: [
41
81
  { heading: "Functional Scope", description: "explicit In Scope and Out of Scope lists" },
@@ -88,6 +128,16 @@ export interface RoleResult {
88
128
  summary: string;
89
129
  markdown: string;
90
130
  usedFallback: boolean;
131
+ format: RoleResultFormat;
132
+ structured?: StructuredRoleData;
133
+ questions?: string[];
134
+ reviewDecision?: ArchitectureReviewDecision;
135
+ }
136
+
137
+ export interface ArchitectureReview {
138
+ decision: ArchitectureReviewDecision;
139
+ summary: string;
140
+ reviewedAt: string;
91
141
  }
92
142
 
93
143
  export interface WorkflowFeedback {
@@ -107,6 +157,16 @@ export interface WorkflowAnswer {
107
157
  createdAt: string;
108
158
  }
109
159
 
160
+ export interface VerificationWaiver {
161
+ reason: string;
162
+ createdAt: string;
163
+ }
164
+
165
+ export interface RunAbort {
166
+ reason: string;
167
+ abortedAt: string;
168
+ }
169
+
110
170
  // Reused for both verification and lint results — the command shape is identical.
111
171
  export interface VerificationResult {
112
172
  command: string;
@@ -121,6 +181,13 @@ export interface ExecutionWorkspace {
121
181
  baseCommit: string;
122
182
  }
123
183
 
184
+ export interface ExecutionInstruction {
185
+ phase: "execution" | "testing";
186
+ workspacePath: string;
187
+ instructions: string;
188
+ createdAt: string;
189
+ }
190
+
124
191
  export interface RunState {
125
192
  version: 1;
126
193
  runId: string;
@@ -128,7 +195,11 @@ export interface RunState {
128
195
  host: Host;
129
196
  mode: WorkflowMode;
130
197
  executionMode: ExecutionMode;
198
+ executionPolicy: ExecutionPolicy;
199
+ enabledGates: GateName[];
200
+ artifactDirectory: string;
131
201
  executionWorkspace?: ExecutionWorkspace;
202
+ executionInstruction?: ExecutionInstruction;
132
203
  backend: BackendName;
133
204
  request: string;
134
205
  phase: Phase;
@@ -138,6 +209,8 @@ export interface RunState {
138
209
  gates: Record<GateName, GateStatus>;
139
210
  artifacts: Partial<Record<ArtifactName, string>>;
140
211
  roles: RoleResult[];
212
+ architectureReview?: ArchitectureReview;
213
+ pendingQuestions: string[];
141
214
  answers: WorkflowAnswer[];
142
215
  approvals: WorkflowApproval[];
143
216
  feedback: WorkflowFeedback[];
@@ -145,6 +218,9 @@ export interface RunState {
145
218
  changedFiles: string[];
146
219
  implementationDiff: string;
147
220
  verification: VerificationResult[];
221
+ verificationStatus: VerificationStatus;
222
+ verificationWaiver?: VerificationWaiver;
223
+ abort?: RunAbort;
148
224
  // VerificationResult is reused for lint output — same shape, different semantics.
149
225
  lintResults: VerificationResult[];
150
226
  }
@@ -156,6 +232,7 @@ export interface StartWorkflowInput {
156
232
  request: string;
157
233
  backend?: BackendName;
158
234
  executionMode?: ExecutionMode;
235
+ executionPolicy?: ExecutionPolicy;
159
236
  }
160
237
 
161
238
  export interface RunRef {
@@ -177,6 +254,19 @@ export interface AnswerWorkflowInput extends RunRef {
177
254
  answer: string;
178
255
  }
179
256
 
257
+ export interface WaiveVerificationInput extends RunRef {
258
+ reason: string;
259
+ }
260
+
261
+ export interface AbortWorkflowInput extends RunRef {
262
+ reason: string;
263
+ }
264
+
265
+ export interface CompleteExecutionInput extends RunRef {
266
+ summary: string;
267
+ verification?: VerificationResult[];
268
+ }
269
+
180
270
  export interface ArtifactRef extends RunRef {
181
271
  name: ArtifactName;
182
272
  }
@@ -2,12 +2,14 @@ import {
2
2
  ARTIFACTS,
3
3
  BACKENDS,
4
4
  EXECUTION_MODES,
5
+ EXECUTION_POLICIES,
5
6
  GATES,
6
7
  HOSTS,
7
8
  WORKFLOW_MODES,
8
9
  type ArtifactName,
9
10
  type BackendName,
10
11
  type ExecutionMode,
12
+ type ExecutionPolicy,
11
13
  type GateName,
12
14
  type Host,
13
15
  type WorkflowMode,
@@ -35,6 +37,10 @@ export function parseExecutionMode(value: unknown): ExecutionMode {
35
37
  return oneOf(value, "executionMode", EXECUTION_MODES);
36
38
  }
37
39
 
40
+ export function parseExecutionPolicy(value: unknown): ExecutionPolicy {
41
+ return oneOf(value, "executionPolicy", EXECUTION_POLICIES);
42
+ }
43
+
38
44
  export function parseHost(value: unknown): Host {
39
45
  return oneOf(value, "host", HOSTS);
40
46
  }
@@ -77,3 +83,7 @@ export function parseFeedback(value: unknown): string {
77
83
  export function parseAnswer(value: unknown): string {
78
84
  return assertString(value, "answer");
79
85
  }
86
+
87
+ export function parseWaiverReason(value: unknown): string {
88
+ return assertString(value, "reason");
89
+ }
@@ -1,2 +1,2 @@
1
- export const DEVCREW_VERSION = "0.1.2";
1
+ export const DEVCREW_VERSION = "0.1.4";
2
2
  export const DEVCREW_NPM_PACKAGE = "@shenlee/devcrew";
@@ -10,16 +10,19 @@ 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 {
22
24
  AnswerWorkflowInput,
25
+ AbortWorkflowInput,
23
26
  ApproveWorkflowInput,
24
27
  ArtifactName,
25
28
  ArtifactReadResult,
@@ -29,7 +32,9 @@ import type {
29
32
  RunRef,
30
33
  RunState,
31
34
  StartWorkflowInput,
35
+ WaiveVerificationInput,
32
36
  } from "./types.js";
37
+ import { GATES } from "./types.js";
33
38
 
34
39
  function now(): string {
35
40
  return new Date().toISOString();
@@ -47,6 +52,8 @@ export function nextPhaseAfterGate(state: RunState, gate: GateName): RunState["p
47
52
  return "implementation";
48
53
  case "implementation":
49
54
  return state.executionMode === "apply" ? "execution" : "testing";
55
+ case "implementation-review":
56
+ return "testing";
50
57
  case "testing":
51
58
  return "acceptance";
52
59
  }
@@ -58,6 +65,7 @@ export function artifactForPhase(phase: RunState["phase"]): ArtifactName {
58
65
  architecture: "architecture",
59
66
  implementation: "implementation-plan",
60
67
  execution: "implementation-review",
68
+ review: "architecture-review",
61
69
  testing: "test-report",
62
70
  acceptance: "acceptance",
63
71
  complete: "acceptance",
@@ -65,15 +73,31 @@ export function artifactForPhase(phase: RunState["phase"]): ArtifactName {
65
73
  return artifactByPhase[phase];
66
74
  }
67
75
 
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;
76
+ export function gateForPhase(phase: RunState["phase"], enabledGates?: readonly GateName[]): GateName | undefined {
77
+ const gate = phase === "review"
78
+ ? "implementation-review"
79
+ : (
80
+ phase === "requirements" ||
81
+ phase === "architecture" ||
82
+ phase === "implementation" ||
83
+ phase === "testing"
84
+ ? phase
85
+ : undefined
86
+ );
87
+ return gate && (!enabledGates || enabledGates.includes(gate)) ? gate : undefined;
88
+ }
89
+
90
+ function configuredGates(gates: readonly GateName[]): GateName[] {
91
+ const configured = new Set(
92
+ gates.filter((gate): gate is GateName => GATES.includes(gate)),
93
+ );
94
+ configured.add("implementation-review");
95
+ configured.add("testing");
96
+ return [...configured];
73
97
  }
74
98
 
75
99
  function assertPendingCurrentGate(state: RunState, gate: GateName): void {
76
- const expected = gateForPhase(state.phase);
100
+ const expected = gateForPhase(state.phase, state.enabledGates);
77
101
  if (expected !== gate) {
78
102
  throw new Error(
79
103
  expected
@@ -105,6 +129,8 @@ export async function startWorkflow(input: StartWorkflowInput, options: Workflow
105
129
  const config = await ensureConfig(cwd);
106
130
  const backend = input.backend ? parseBackend(input.backend) : config.defaultBackend === "host-preferred" ? host : config.defaultBackend;
107
131
  const executionMode = input.executionMode ? parseExecutionMode(input.executionMode) : config.executionMode;
132
+ const executionPolicy = input.executionPolicy ? parseExecutionPolicy(input.executionPolicy) : "interactive-host";
133
+ const enabledGates = configuredGates(config.workflow.gates);
108
134
  if (executionMode === "apply" && backend === "local") {
109
135
  throw new Error("DevCrew apply mode requires a codex or claude backend; local is plan-only");
110
136
  }
@@ -116,20 +142,25 @@ export async function startWorkflow(input: StartWorkflowInput, options: Workflow
116
142
  host,
117
143
  mode,
118
144
  executionMode,
145
+ executionPolicy,
146
+ enabledGates,
147
+ artifactDirectory: config.workflow.artifactDirectory,
119
148
  backend,
120
149
  request,
121
150
  phase: "requirements",
122
- status: "awaiting_approval",
151
+ status: enabledGates.includes("requirements") ? "awaiting_approval" : "ready",
123
152
  createdAt,
124
153
  updatedAt: createdAt,
125
154
  gates: {
126
- requirements: "pending",
155
+ requirements: enabledGates.includes("requirements") ? "pending" : "not_started",
127
156
  architecture: "not_started",
128
157
  implementation: "not_started",
158
+ "implementation-review": "not_started",
129
159
  testing: "not_started",
130
160
  },
131
161
  artifacts: {},
132
162
  roles: [],
163
+ pendingQuestions: [],
133
164
  answers: [],
134
165
  approvals: [],
135
166
  feedback: [],
@@ -137,6 +168,7 @@ export async function startWorkflow(input: StartWorkflowInput, options: Workflow
137
168
  changedFiles: [],
138
169
  implementationDiff: "",
139
170
  verification: [],
171
+ verificationStatus: "not_run",
140
172
  lintResults: [],
141
173
  };
142
174
 
@@ -152,7 +184,13 @@ export async function getWorkflowStatus(input: RunRef): Promise<RunState> {
152
184
 
153
185
  export async function continueWorkflow(input: RunRef): Promise<RunState> {
154
186
  const state = await getWorkflowStatus(input);
155
- if (state.status === "awaiting_approval" || state.status === "awaiting_input" || state.status === "complete") {
187
+ if (
188
+ state.status === "awaiting_approval" ||
189
+ state.status === "awaiting_input" ||
190
+ state.status === "awaiting_execution" ||
191
+ state.status === "complete" ||
192
+ state.status === "aborted"
193
+ ) {
156
194
  return state;
157
195
  }
158
196
 
@@ -167,8 +205,14 @@ export async function continueWorkflow(input: RunRef): Promise<RunState> {
167
205
  throw new Error("DevCrew execution phase requires orchestrated continuation");
168
206
  }
169
207
 
170
- const gate = gateForPhase(state.phase);
208
+ const configuredGate = gateForPhase(state.phase);
209
+ const gate = gateForPhase(state.phase, state.enabledGates);
171
210
  if (!gate) {
211
+ if (configuredGate) {
212
+ state.phase = nextPhaseAfterGate(state, configuredGate);
213
+ state.status = "ready";
214
+ return saveState(state);
215
+ }
172
216
  state.status = "complete";
173
217
  return saveState(state);
174
218
  }
@@ -178,6 +222,23 @@ export async function continueWorkflow(input: RunRef): Promise<RunState> {
178
222
  return saveState(state);
179
223
  }
180
224
 
225
+ export async function abortWorkflow(input: AbortWorkflowInput): Promise<RunState> {
226
+ const state = await getWorkflowStatus(input);
227
+ if (state.status === "aborted") {
228
+ return state;
229
+ }
230
+ if (state.status === "complete") {
231
+ throw new Error("Completed DevCrew runs cannot be aborted");
232
+ }
233
+ state.abort = {
234
+ reason: parseWaiverReason(input.reason),
235
+ abortedAt: now(),
236
+ };
237
+ delete state.executionInstruction;
238
+ state.status = "aborted";
239
+ return saveState(state);
240
+ }
241
+
181
242
  export async function validateWorkflowApproval(input: ApproveWorkflowInput): Promise<RunState> {
182
243
  const state = await getWorkflowStatus(input);
183
244
  const gate = parseGate(input.gate);
@@ -227,7 +288,16 @@ export async function rejectWorkflow(input: RejectWorkflowInput): Promise<RunSta
227
288
 
228
289
  export async function answerWorkflow(input: AnswerWorkflowInput, options: WorkflowMutationOptions = {}): Promise<RunState> {
229
290
  const state = await getWorkflowStatus(input);
230
- const gate = gateForPhase(state.phase);
291
+ const gate = gateForPhase(state.phase, state.enabledGates);
292
+ if (state.status === "awaiting_input" && state.pendingQuestions.length > 0 && state.phase === "requirements") {
293
+ state.answers.push({
294
+ answer: parseAnswer(input.answer),
295
+ createdAt: now(),
296
+ });
297
+ state.pendingQuestions = [];
298
+ state.status = "ready";
299
+ return saveState(state);
300
+ }
231
301
  if (state.status !== "awaiting_input" || !gate || state.gates[gate] !== "rejected") {
232
302
  throw new Error("Workflow must be awaiting_input at a rejected current gate before recording an answer");
233
303
  }
@@ -243,6 +313,26 @@ export async function answerWorkflow(input: AnswerWorkflowInput, options: Workfl
243
313
  return saveState(state);
244
314
  }
245
315
 
316
+ export async function waiveVerificationWorkflow(input: WaiveVerificationInput): Promise<RunState> {
317
+ const state = await getWorkflowStatus(input);
318
+ if (
319
+ state.executionMode !== "apply" ||
320
+ state.phase !== "testing" ||
321
+ state.status !== "awaiting_input" ||
322
+ state.gates.testing !== "rejected" ||
323
+ (state.verificationStatus !== "failed" && state.verificationStatus !== "not_run")
324
+ ) {
325
+ throw new Error("A verification waiver is only available after failed or missing apply-mode verification");
326
+ }
327
+ state.verificationWaiver = {
328
+ reason: parseWaiverReason(input.reason),
329
+ createdAt: now(),
330
+ };
331
+ state.gates.testing = "pending";
332
+ state.status = "awaiting_approval";
333
+ return saveState(state);
334
+ }
335
+
246
336
  export async function getArtifact(input: ArtifactRef): Promise<ArtifactReadResult> {
247
337
  const state = await getWorkflowStatus(input);
248
338
  return readArtifact(state, parseArtifactName(input.name));