@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
package/docs/workflow.md CHANGED
@@ -17,8 +17,8 @@ approval.
17
17
  1. `requirements`: product manager clarifies scope, users, success criteria, and non-goals.
18
18
  2. `architecture`: architect defines technical approach, interfaces, deployment notes, and review criteria.
19
19
  3. `implementation`: implementer creates a read-only implementation plan and coding checklist. The implementation gate approves this plan, not repository changes.
20
- 4. `execution`: internal, nongated apply phase. The implementer works in `.devcrew/worktrees/<run-id>`, and DevCrew captures changed files, a binary-capable patch, lint evidence, and `implementation-review.md`.
21
- 5. `testing`: tester records validation and acceptance evidence. In apply mode, the tester and verification commands run in the same isolated worktree, then DevCrew refreshes the implementation diff and review before opening the testing gate.
20
+ 4. `execution`: internal, nongated apply phase. `interactive-host` waits for the host-native agent to work in `.devcrew/worktrees/<run-id>` and report completion; explicit headless policies use DevCrew's SDK settings. DevCrew then captures changed files, a binary-capable patch, lint evidence when run headlessly, and `implementation-review.md`.
21
+ 5. `testing`: records validation and acceptance evidence in the same isolated worktree. Interactive-host submits command evidence through `devcrew_complete_execution`; headless policies run DevCrew-configured verification. DevCrew refreshes the implementation diff and review before opening the testing gate.
22
22
  6. `acceptance`: generated after the testing gate is approved.
23
23
 
24
24
  `devcrew_start` runs the PM role for `requirements`. After the requester approves
@@ -32,14 +32,21 @@ requirements approval
32
32
  -> architecture approval
33
33
  -> implementation plan approval
34
34
  -> isolated execution
35
+ -> implementation review approval
35
36
  -> isolated testing
36
37
  -> testing approval
37
38
  -> patch promotion to requester repository
38
39
  ```
39
40
 
40
- The implementation-plan approval advances the run to `execution`. One `devcrew_continue` call runs the implementer in the isolated worktree and leaves the run at `testing/ready`; another `devcrew_continue` runs the tester and verification commands before opening the testing gate.
41
+ The implementation-plan approval advances the run to `execution`. With the default `interactive-host` policy, each `devcrew_continue` pauses at `awaiting_execution`; the host-native agent works in the reported worktree and calls `devcrew_complete_execution`. Explicit headless policies run the corresponding SDK role and commands directly under their recorded DevCrew policy.
41
42
 
42
- Apply requires Git, a real Codex or Claude SDK backend, and a clean requester worktree both when execution starts and when the reviewed patch is promoted. The requester repository remains unchanged until testing approval. If testing is rejected, `devcrew_answer` records the response and returns the run to `execution/ready` with the isolated worktree intact.
43
+ Apply requires Git and a clean requester worktree both when execution starts and when the reviewed patch is promoted. The requester repository remains unchanged until testing approval. A failed verification enters `awaiting_input`, rather than a promotable testing gate; it can be revised with `devcrew_answer` or deliberately reopened only through `devcrew_waive_verification` with a non-empty risk reason. If testing is rejected, `devcrew_answer` records the response and returns the run to `execution/ready` with the isolated worktree intact.
44
+
45
+ The execution review is also structured: the architect must return either
46
+ `Decision: approved` or `Decision: changes_required` in its
47
+ `architecture-review` artifact. A `changes_required` decision rejects the
48
+ implementation-review gate and keeps the run at `awaiting_input`; it cannot
49
+ enter testing until the feedback is addressed and a later review approves it.
43
50
 
44
51
  `devcrew_start` records the created run as the active run for the repository.
45
52
  Subsequent MCP calls can omit `runId`; DevCrew resolves it from
@@ -53,10 +60,17 @@ Each main phase has a gate:
53
60
  - `requirements`
54
61
  - `architecture`
55
62
  - `implementation`
63
+ - `implementation-review`
56
64
  - `testing`
57
65
 
58
66
  The requester approves or rejects each gate. Rejection records feedback and returns the workflow to `awaiting_input`. In apply mode, rejecting testing never rolls changes back in the requester repository because no patch has been promoted yet.
59
67
 
68
+ `workflow.gates` may omit `requirements`, `architecture`, or `implementation`
69
+ to make that phase advance automatically after its role has produced an
70
+ artifact. It never skips the role, artifact, execution, architecture review, or
71
+ verification. `implementation-review` and `testing` are mandatory and DevCrew
72
+ adds them back if a configuration omits them.
73
+
60
74
  ## State And Artifacts
61
75
 
62
76
  Runtime state is stored in:
@@ -83,5 +97,6 @@ The standard artifact set is:
83
97
  - `architecture.md`
84
98
  - `implementation-plan.md`
85
99
  - `implementation-review.md`
100
+ - `architecture-review.md`
86
101
  - `test-report.md`
87
102
  - `acceptance.md`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shenlee/devcrew",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Multi-host agent workflow service for Codex and Claude Code.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -1,5 +1,15 @@
1
1
  import { DEVCREW_NPM_PACKAGE, ROLE_SECTIONS } from "../../core/src/index.js";
2
- import type { ArtifactName, BackendName, ExecutionMode, Host, Phase, RoleResult, RunState, WorkflowMode } from "../../core/src/index.js";
2
+ import type {
3
+ ArtifactName,
4
+ BackendName,
5
+ ExecutionMode,
6
+ ExecutionPolicy,
7
+ Host,
8
+ Phase,
9
+ RoleResult,
10
+ RunState,
11
+ WorkflowMode,
12
+ } from "../../core/src/index.js";
3
13
 
4
14
  export interface BackendResolutionInput {
5
15
  host: Host;
@@ -13,6 +23,7 @@ export interface RoleRunInput {
13
23
  request: string;
14
24
  mode: WorkflowMode;
15
25
  executionMode?: ExecutionMode;
26
+ executionPolicy?: ExecutionPolicy;
16
27
  cwd: string;
17
28
  standards: string;
18
29
  artifactPath: string;
@@ -69,6 +80,27 @@ export function assertRoleSections(role: RoleResult["role"], markdown: string):
69
80
  }
70
81
  }
71
82
 
83
+ export function extractOpenQuestions(markdown: string): string[] {
84
+ const match = /^##\s+Open Questions\s*$(.*?)(?=^##\s|(?![\s\S]))/ims.exec(markdown);
85
+ if (!match) {
86
+ return [];
87
+ }
88
+ const questions: string[] = [];
89
+ for (const line of match[1].split("\n")) {
90
+ const question = /^\s*[-*]\s+(.+?)\s*$/u.exec(line)?.[1]?.trim();
91
+ if (question && !/^(none|n\/a|no open questions)\.?$/i.test(question)) {
92
+ questions.push(question);
93
+ }
94
+ }
95
+ return questions;
96
+ }
97
+
98
+ export function extractArchitectureReviewDecision(markdown: string): "approved" | "changes_required" | undefined {
99
+ const section = /^##\s+Review Decision\s*$(.*?)(?=^##\s|(?![\s\S]))/ims.exec(markdown)?.[1] ?? "";
100
+ const match = /^Decision:\s*(approved|changes_required)\s*$/im.exec(section);
101
+ return match?.[1] as "approved" | "changes_required" | undefined;
102
+ }
103
+
72
104
  export function renderRolePrompt(input: Omit<RoleRunInput, "backend" | "cwd">): string {
73
105
  const executionMode = input.executionMode ?? "plan";
74
106
  const answers = input.answers ?? [];
@@ -113,13 +145,22 @@ export function renderRolePrompt(input: Omit<RoleRunInput, "backend" | "cwd">):
113
145
  "",
114
146
  "Instructions:",
115
147
  `Act as the DevCrew ${input.role} role and produce a complete, well-structured Markdown document for the ${input.phase} phase.`,
116
- "Keep scope aligned with the approved gates and inherited host permissions.",
148
+ "Keep scope aligned with the approved gates and the selected DevCrew execution policy.",
117
149
  permissionInstruction,
118
150
  "",
119
151
  "Required Sections:",
120
152
  ...roleGuidance(input.role),
121
153
  );
122
154
 
155
+ if (input.phase === "review") {
156
+ lines.push(
157
+ "",
158
+ "Architecture Review Decision:",
159
+ "Add an exact H2 `## Review Decision` section containing exactly `Decision: approved` or `Decision: changes_required`.",
160
+ "Use `changes_required` for any mismatch with the approved architecture; summarize the blocking mismatch in that section.",
161
+ );
162
+ }
163
+
123
164
  return lines.join("\n");
124
165
  }
125
166
 
@@ -129,6 +170,7 @@ function titleForPhase(phase: Phase): string {
129
170
  architecture: "Architecture",
130
171
  implementation: "Implementation Plan",
131
172
  execution: "Implementation Review",
173
+ review: "Architecture Review",
132
174
  testing: "Test Report",
133
175
  acceptance: "Acceptance",
134
176
  complete: "Acceptance",
@@ -170,13 +212,14 @@ interface CodexClient {
170
212
 
171
213
  type CodexConstructor = new () => CodexClient;
172
214
 
173
- export type ClaudePermissionMode = "default" | "acceptEdits" | "bypassPermissions" | "plan";
215
+ export type ClaudePermissionMode = "default" | "acceptEdits" | "bypassPermissions" | "dontAsk" | "plan";
174
216
 
175
217
  export interface ClaudeQueryOptions {
176
218
  cwd?: string;
177
219
  permissionMode?: ClaudePermissionMode;
178
220
  allowedTools?: string[];
179
221
  disallowedTools?: string[];
222
+ allowDangerouslySkipPermissions?: boolean;
180
223
  }
181
224
 
182
225
  export type ClaudeResultSubtype = "success" | "error_max_turns" | "error_during_execution";
@@ -294,28 +337,51 @@ export function extractClaudeResult(message: ClaudeResultMessage | undefined): s
294
337
  return text;
295
338
  }
296
339
 
297
- function roleCanApply(input: Pick<RoleRunInput, "executionMode" | "phase">): boolean {
340
+ function isApplyPhase(input: Pick<RoleRunInput, "executionMode" | "phase">): boolean {
298
341
  return input.executionMode === "apply" && (input.phase === "execution" || input.phase === "testing");
299
342
  }
300
343
 
301
- function codexSandboxForRole(input: RoleRunInput): CodexSandboxMode {
302
- return roleCanApply(input) ? "workspace-write" : "read-only";
344
+ function effectiveExecutionPolicy(input: Pick<RoleRunInput, "executionPolicy">): ExecutionPolicy {
345
+ return input.executionPolicy ?? "interactive-host";
346
+ }
347
+
348
+ function roleCanApply(input: Pick<RoleRunInput, "executionMode" | "executionPolicy" | "phase">): boolean {
349
+ return isApplyPhase(input) && effectiveExecutionPolicy(input) !== "interactive-host";
350
+ }
351
+
352
+ function codexOptionsForRole(input: RoleRunInput): CodexThreadOptions {
353
+ if (!roleCanApply(input)) {
354
+ return buildCodexThreadOptions(input.cwd);
355
+ }
356
+ const executionPolicy = effectiveExecutionPolicy(input);
357
+ return {
358
+ ...buildCodexThreadOptions(input.cwd, "workspace-write"),
359
+ approvalPolicy: executionPolicy === "headless-unattended" ? "never" : "on-request",
360
+ networkAccessEnabled: false,
361
+ };
303
362
  }
304
363
 
305
364
  function claudeOptionsForRole(input: RoleRunInput): ClaudeQueryOptions {
306
365
  if (!roleCanApply(input)) {
307
366
  return buildClaudeOptions(input.cwd);
308
367
  }
368
+ if (effectiveExecutionPolicy(input) === "headless-unattended") {
369
+ return {
370
+ cwd: input.cwd,
371
+ permissionMode: "bypassPermissions",
372
+ allowDangerouslySkipPermissions: true,
373
+ };
374
+ }
309
375
  const allowedTools =
310
- input.role === "implementer" ? ["Read", "Grep", "Glob", "Edit", "Write", "Bash"] : ["Read", "Grep", "Glob", "Bash"];
311
- return buildClaudeOptions(input.cwd, "acceptEdits", allowedTools);
376
+ input.role === "implementer" ? ["Read", "Grep", "Glob", "Edit", "Write"] : ["Read", "Grep", "Glob"];
377
+ return buildClaudeOptions(input.cwd, "dontAsk", allowedTools);
312
378
  }
313
379
 
314
380
  async function runWithCodex(input: RoleRunInput, prompt: string, loadModule: ModuleLoader): Promise<string> {
315
381
  const mod = await loadModule(HOST_SDK_PACKAGES.codex);
316
382
  const CodexClass = mod.Codex as CodexConstructor;
317
383
  const codex = new CodexClass();
318
- const thread = codex.startThread(buildCodexThreadOptions(input.cwd, codexSandboxForRole(input)));
384
+ const thread = codex.startThread(codexOptionsForRole(input));
319
385
  const turn = await thread.run(prompt);
320
386
  return extractCodexText(turn);
321
387
  }
@@ -356,6 +422,9 @@ export interface RunRoleDeps {
356
422
  }
357
423
 
358
424
  export async function runRole(input: RoleRunInput, deps: RunRoleDeps = {}): Promise<RoleResult> {
425
+ if (isApplyPhase(input) && effectiveExecutionPolicy(input) === "interactive-host") {
426
+ throw new Error("DevCrew interactive-host execution must be performed by the host, not a nested SDK");
427
+ }
359
428
  const loadModule = deps.loadModule ?? importOptional;
360
429
  const title = titleForPhase(input.phase);
361
430
  const prompt = renderRolePrompt(input);
@@ -375,12 +444,18 @@ export async function runRole(input: RoleRunInput, deps: RunRoleDeps = {}): Prom
375
444
  ? await runWithCodex(input, prompt, loadModule)
376
445
  : await runWithClaude(input, prompt, loadModule);
377
446
  assertRoleSections(input.role, markdown);
447
+ const reviewDecision = input.phase === "review" ? extractArchitectureReviewDecision(markdown) : undefined;
448
+ if (input.phase === "review" && !reviewDecision) {
449
+ throw new RoleOutputValidationError(input.role, ["Review Decision"]);
450
+ }
378
451
  return {
379
452
  role: input.role,
380
453
  backend: input.backend,
381
454
  summary: `${input.role} produced ${title} using the ${input.backend} SDK.`,
382
455
  markdown,
383
456
  usedFallback: false,
457
+ questions: input.role === "pm" ? extractOpenQuestions(markdown) : undefined,
458
+ reviewDecision,
384
459
  };
385
460
  } catch (error) {
386
461
  const reason = errorMessage(error);
@@ -10,6 +10,7 @@ function headingForArtifact(name: ArtifactName): string {
10
10
  architecture: "Architecture",
11
11
  "implementation-plan": "Implementation Plan",
12
12
  "implementation-review": "Implementation Review",
13
+ "architecture-review": "Architecture Review",
13
14
  "test-report": "Test Report",
14
15
  acceptance: "Acceptance",
15
16
  }[name];
@@ -60,6 +61,13 @@ function verificationBlock(state: RunState): string {
60
61
  .join("\n\n");
61
62
  }
62
63
 
64
+ function verificationWaiverBlock(state: RunState): string {
65
+ if (!state.verificationWaiver) {
66
+ return "No verification waiver has been recorded.";
67
+ }
68
+ return `Reason: ${state.verificationWaiver.reason}\nRecorded At: ${state.verificationWaiver.createdAt}`;
69
+ }
70
+
63
71
  function lintResultsBlock(state: RunState): string {
64
72
  if (state.lintResults.length === 0) {
65
73
  return "No lint or format commands have been executed.";
@@ -99,7 +107,7 @@ function architectureComplianceStatus(state: RunState): string {
99
107
 
100
108
  export function renderArtifact(name: ArtifactName, state: RunState): string {
101
109
  const title = headingForArtifact(name);
102
- const common = `# ${title}\n\nRun: ${state.runId}\nMode: ${state.mode}\nExecution Mode: ${state.executionMode}\nHost: ${state.host}\nBackend: ${state.backend}\nRequest: ${state.request}\n\n`;
110
+ const common = `# ${title}\n\nRun: ${state.runId}\nMode: ${state.mode}\nExecution Mode: ${state.executionMode}\nExecution Policy: ${state.executionPolicy}\nVerification Status: ${state.verificationStatus}\nHost: ${state.host}\nBackend: ${state.backend}\nRequest: ${state.request}\n\n`;
103
111
 
104
112
  if (name === "requirements") {
105
113
  return `${common}## Functional Scope\n\n### In Scope\n\n- Capture the requested outcome in user-facing terms.\n\n### Out of Scope\n\n- Make explicit what is intentionally excluded before implementation.\n\n## Users and Scenarios\n\n- Identify the primary users and their key scenarios.\n\n## Acceptance Criteria\n\n- Express each criterion as Given / When / Then so it stays testable.\n\n## Priorities\n\n- Classify each requirement as Must / Should / Could / Won't (MoSCoW).\n\n## Current Requester Answers\n\n${answerBlock(state)}\n\n## Discovered Standards\n\n${standardsExcerpt(state)}\n\n## Open Questions\n\n- Confirm primary users and success criteria.\n- Confirm scope boundaries and non-goals.\n- Confirm deployment or environment constraints.\n\n## Rejection Feedback\n\n${feedbackBlock(state)}\n`;
@@ -117,17 +125,21 @@ export function renderArtifact(name: ArtifactName, state: RunState): string {
117
125
  return `${common}${workflowContextBlock(state)}\n## Implementation Diff Review\n\n### Changed Files\n\n${changedFilesBlock(state)}\n\n### Captured Diff\n\n${implementationDiffBlock(state)}\n\n## Lint Results\n\n${lintResultsBlock(state)}\n\n## Architecture Compliance Inputs\n\n${architectureComplianceInputsBlock(state)}\n\n## Architecture Compliance Review\n\nStatus: ${architectureComplianceStatus(state)}\n\n- Confirm changed files map back to the approved architecture artifact.\n- Confirm public interfaces, data flow, and deployment assumptions remain consistent with the architecture.\n- Confirm implementation scope does not include unapproved requirements or unrelated refactors.\n- Record any mismatch as rejection feedback before approving the implementation gate.\n`;
118
126
  }
119
127
 
128
+ if (name === "architecture-review") {
129
+ return `${common}${workflowContextBlock(state)}\n## Technical Decisions\n\nReview the captured implementation diff against the approved architecture. State whether the implementation keeps its approved interfaces, data flow, deployment, and rollback assumptions.\n\n## Interface Contracts\n\nIdentify each changed public interface and whether it conforms to the approved architecture artifact.\n\n## Data Flow and Deployment\n\nReview the implementation diff for data-flow, deployment, migration, and rollback deviations.\n\n## Architecture Review Checklist\n\n- Review the captured implementation diff and changed-files list.\n- Trace each material change to an approved architectural decision.\n- Record any mismatch as rejection feedback before the testing phase begins.\n`;
130
+ }
131
+
120
132
  if (name === "test-report") {
121
- return `${common}${workflowContextBlock(state)}\n## Test Cases\n\nEnumerate cases as a table covering happy, edge, failure, and regression paths.\n\n| ID | Scenario | Type | Expected |\n| --- | --- | --- | --- |\n| TC-1 | Primary success path | happy | Behaves as specified |\n| TC-2 | Boundary input | edge | Handled without error |\n| TC-3 | Invalid input | failure | Fails safely with clear error |\n\n## Coverage\n\nRun the coverage command and report the coverage summary plus any gaps. Evidence is captured under Acceptance Evidence.\n\n## Acceptance Evidence\n\n${verificationBlock(state)}\n\n## Known Risks\n\n- Host SDK availability may vary by user environment.\n- Agent permissions are inherited from the host and must be reviewed there.\n`;
133
+ return `${common}${workflowContextBlock(state)}\n## Test Cases\n\nEnumerate cases as a table covering happy, edge, failure, and regression paths.\n\n| ID | Scenario | Type | Expected |\n| --- | --- | --- | --- |\n| TC-1 | Primary success path | happy | Behaves as specified |\n| TC-2 | Boundary input | edge | Handled without error |\n| TC-3 | Invalid input | failure | Fails safely with clear error |\n\n## Coverage\n\nRun the coverage command and report the coverage summary plus any gaps. Evidence is captured under Acceptance Evidence.\n\n## Acceptance Evidence\n\n${verificationBlock(state)}\n\n## Verification Waiver\n\n${verificationWaiverBlock(state)}\n\n## Known Risks\n\n- Headless SDK capabilities are governed by the recorded DevCrew execution policy.\n- Interactive-host work is performed through the host's native agent controls.\n`;
122
134
  }
123
135
 
124
- return `${common}${workflowContextBlock(state)}\n## Acceptance Summary\n\nThe requester approved requirements, architecture, implementation planning, and test reporting gates for this run.\n\n## Approved Gates\n\n${Object.entries(state.gates)
136
+ return `${common}${workflowContextBlock(state)}\n## Acceptance Summary\n\nThe requester approved requirements, architecture, implementation planning, and test reporting gates for this run.\n\n## Verification Waiver\n\n${verificationWaiverBlock(state)}\n\n## Approved Gates\n\n${Object.entries(state.gates)
125
137
  .map(([gate, status]) => `- ${gate}: ${status}`)
126
138
  .join("\n")}\n`;
127
139
  }
128
140
 
129
141
  export async function writeArtifact(name: ArtifactName, state: RunState): Promise<string> {
130
- const path = artifactPath(state.cwd, state.runId, name);
142
+ const path = artifactPath(state.cwd, state.runId, name, state.artifactDirectory);
131
143
  await mkdir(dirname(path), { recursive: true });
132
144
  await writeFile(path, renderArtifact(name, state), "utf8");
133
145
  return path;
@@ -11,7 +11,7 @@ export const DEFAULT_CONFIG: DevCrewConfig = {
11
11
  lintCommands: [],
12
12
  coverageCommands: [],
13
13
  workflow: {
14
- gates: ["requirements", "architecture", "implementation", "testing"],
14
+ gates: ["requirements", "architecture", "implementation", "implementation-review", "testing"],
15
15
  artifactDirectory: "docs/devcrew",
16
16
  },
17
17
  };
@@ -35,24 +35,25 @@ export function standardsPath(cwd: string): string {
35
35
  return join(devcrewDir(cwd), "standards.md");
36
36
  }
37
37
 
38
- export function docsRoot(cwd: string): string {
39
- return join(cwd, "docs", "devcrew");
38
+ export function docsRoot(cwd: string, artifactDirectory: string = "docs/devcrew"): string {
39
+ return join(cwd, artifactDirectory);
40
40
  }
41
41
 
42
- export function docsRunDir(cwd: string, runId: string): string {
43
- return join(docsRoot(cwd), runId);
42
+ export function docsRunDir(cwd: string, runId: string, artifactDirectory: string = "docs/devcrew"): string {
43
+ return join(docsRoot(cwd, artifactDirectory), runId);
44
44
  }
45
45
 
46
- export function artifactPath(cwd: string, runId: string, artifact: ArtifactName): string {
46
+ export function artifactPath(cwd: string, runId: string, artifact: ArtifactName, artifactDirectory: string = "docs/devcrew"): string {
47
47
  const filenameByArtifact: Record<ArtifactName, string> = {
48
48
  requirements: "requirements.md",
49
49
  architecture: "architecture.md",
50
50
  "implementation-plan": "implementation-plan.md",
51
51
  "implementation-review": "implementation-review.md",
52
+ "architecture-review": "architecture-review.md",
52
53
  "test-report": "test-report.md",
53
54
  acceptance: "acceptance.md",
54
55
  };
55
- return join(docsRunDir(cwd, runId), filenameByArtifact[artifact]);
56
+ return join(docsRunDir(cwd, runId, artifactDirectory), filenameByArtifact[artifact]);
56
57
  }
57
58
 
58
59
  export async function ensureRunDirectories(cwd: string, runId: string): Promise<void> {
@@ -2,7 +2,14 @@ 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 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
+ }
6
13
 
7
14
  export async function saveState(state: RunState): Promise<RunState> {
8
15
  state.updatedAt = new Date().toISOString();
@@ -18,18 +25,44 @@ export async function loadState(cwd: string, runId: string): Promise<RunState> {
18
25
  const raw = await readFile(statePath(cwd, runId), "utf8");
19
26
  const parsed = JSON.parse(raw) as RunState;
20
27
  const executionWorkspace = parsed.executionWorkspace;
28
+ const executionInstruction = parsed.executionInstruction;
29
+ const verificationWaiver = parsed.verificationWaiver;
21
30
  return {
22
31
  ...parsed,
23
32
  executionMode: parsed.executionMode ?? "plan",
33
+ executionPolicy: parsed.executionPolicy ?? "interactive-host",
34
+ enabledGates: enabledGatesFromState(parsed.enabledGates),
35
+ artifactDirectory: typeof parsed.artifactDirectory === "string" && parsed.artifactDirectory.trim().length > 0
36
+ ? parsed.artifactDirectory
37
+ : "docs/devcrew",
24
38
  executionWorkspace:
25
39
  executionWorkspace &&
26
40
  typeof executionWorkspace.path === "string" &&
27
41
  typeof executionWorkspace.baseCommit === "string"
28
42
  ? executionWorkspace
29
43
  : undefined,
44
+ executionInstruction:
45
+ executionInstruction &&
46
+ (executionInstruction.phase === "execution" || executionInstruction.phase === "testing") &&
47
+ typeof executionInstruction.workspacePath === "string" &&
48
+ typeof executionInstruction.instructions === "string" &&
49
+ typeof executionInstruction.createdAt === "string"
50
+ ? executionInstruction
51
+ : undefined,
30
52
  changedFiles: Array.isArray(parsed.changedFiles) ? parsed.changedFiles : [],
53
+ pendingQuestions: Array.isArray(parsed.pendingQuestions)
54
+ ? parsed.pendingQuestions.filter((question): question is string => typeof question === "string" && question.trim().length > 0)
55
+ : [],
31
56
  implementationDiff: typeof parsed.implementationDiff === "string" ? parsed.implementationDiff : "",
32
57
  verification: Array.isArray(parsed.verification) ? parsed.verification : [],
58
+ verificationStatus:
59
+ parsed.verificationStatus === "passed" || parsed.verificationStatus === "failed" ? parsed.verificationStatus : "not_run",
60
+ verificationWaiver:
61
+ verificationWaiver &&
62
+ typeof verificationWaiver.reason === "string" &&
63
+ typeof verificationWaiver.createdAt === "string"
64
+ ? verificationWaiver
65
+ : undefined,
33
66
  lintResults: Array.isArray(parsed.lintResults) ? parsed.lintResults : [],
34
67
  };
35
68
  }
@@ -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,29 +8,34 @@ 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";
33
39
 
34
40
  export interface RoleSection {
35
41
  heading: string;
@@ -88,6 +94,14 @@ export interface RoleResult {
88
94
  summary: string;
89
95
  markdown: string;
90
96
  usedFallback: boolean;
97
+ questions?: string[];
98
+ reviewDecision?: ArchitectureReviewDecision;
99
+ }
100
+
101
+ export interface ArchitectureReview {
102
+ decision: ArchitectureReviewDecision;
103
+ summary: string;
104
+ reviewedAt: string;
91
105
  }
92
106
 
93
107
  export interface WorkflowFeedback {
@@ -107,6 +121,11 @@ export interface WorkflowAnswer {
107
121
  createdAt: string;
108
122
  }
109
123
 
124
+ export interface VerificationWaiver {
125
+ reason: string;
126
+ createdAt: string;
127
+ }
128
+
110
129
  // Reused for both verification and lint results — the command shape is identical.
111
130
  export interface VerificationResult {
112
131
  command: string;
@@ -121,6 +140,13 @@ export interface ExecutionWorkspace {
121
140
  baseCommit: string;
122
141
  }
123
142
 
143
+ export interface ExecutionInstruction {
144
+ phase: "execution" | "testing";
145
+ workspacePath: string;
146
+ instructions: string;
147
+ createdAt: string;
148
+ }
149
+
124
150
  export interface RunState {
125
151
  version: 1;
126
152
  runId: string;
@@ -128,7 +154,11 @@ export interface RunState {
128
154
  host: Host;
129
155
  mode: WorkflowMode;
130
156
  executionMode: ExecutionMode;
157
+ executionPolicy: ExecutionPolicy;
158
+ enabledGates: GateName[];
159
+ artifactDirectory: string;
131
160
  executionWorkspace?: ExecutionWorkspace;
161
+ executionInstruction?: ExecutionInstruction;
132
162
  backend: BackendName;
133
163
  request: string;
134
164
  phase: Phase;
@@ -138,6 +168,8 @@ export interface RunState {
138
168
  gates: Record<GateName, GateStatus>;
139
169
  artifacts: Partial<Record<ArtifactName, string>>;
140
170
  roles: RoleResult[];
171
+ architectureReview?: ArchitectureReview;
172
+ pendingQuestions: string[];
141
173
  answers: WorkflowAnswer[];
142
174
  approvals: WorkflowApproval[];
143
175
  feedback: WorkflowFeedback[];
@@ -145,6 +177,8 @@ export interface RunState {
145
177
  changedFiles: string[];
146
178
  implementationDiff: string;
147
179
  verification: VerificationResult[];
180
+ verificationStatus: VerificationStatus;
181
+ verificationWaiver?: VerificationWaiver;
148
182
  // VerificationResult is reused for lint output — same shape, different semantics.
149
183
  lintResults: VerificationResult[];
150
184
  }
@@ -156,6 +190,7 @@ export interface StartWorkflowInput {
156
190
  request: string;
157
191
  backend?: BackendName;
158
192
  executionMode?: ExecutionMode;
193
+ executionPolicy?: ExecutionPolicy;
159
194
  }
160
195
 
161
196
  export interface RunRef {
@@ -177,6 +212,15 @@ export interface AnswerWorkflowInput extends RunRef {
177
212
  answer: string;
178
213
  }
179
214
 
215
+ export interface WaiveVerificationInput extends RunRef {
216
+ reason: string;
217
+ }
218
+
219
+ export interface CompleteExecutionInput extends RunRef {
220
+ summary: string;
221
+ verification?: VerificationResult[];
222
+ }
223
+
180
224
  export interface ArtifactRef extends RunRef {
181
225
  name: ArtifactName;
182
226
  }
@@ -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.3";
2
2
  export const DEVCREW_NPM_PACKAGE = "@shenlee/devcrew";