@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
package/README.md CHANGED
@@ -12,13 +12,14 @@ The first release is intentionally local-first. DevCrew stores workflow state an
12
12
 
13
13
  - Gated phases for requirements, architecture, implementation planning, and test reporting.
14
14
  - Two workflow modes: `feature` for existing repositories and `greenfield` for new products.
15
- - Safe execution modes: `plan` is the default; `apply` must be explicit. Implementation planning stays read-only, while implementation and testing run in a DevCrew-owned Git worktree.
15
+ - Safe execution modes: `plan` is the default; `apply` must be explicit. Apply defaults to `interactive-host`, which pauses for the host-native agent in a DevCrew-owned Git worktree; explicit headless policies use separately declared SDK permissions.
16
16
  - Host-preferred backend selection: Codex runs default to Codex, Claude Code runs default to Claude.
17
17
  - Orchestrated role execution: `devcrew_start` runs the PM role, and `devcrew_continue` runs the next phase role before opening the gate.
18
18
  - Implementation review artifact: isolated execution records changed files, a binary-capable diff, lint evidence, and architecture compliance notes; testing refreshes the reviewed diff before promotion.
19
+ - Structured architecture review: execution must receive an architect decision of `approved` or `changes_required`; required changes block testing.
19
20
  - Repository artifacts in `.devcrew/runs/<run-id>/state.json` and `docs/devcrew/<run-id>/`.
20
21
  - Standards discovery from `.devcrew/standards.md`, `AGENTS.md`, `CLAUDE.md`, README, and common project manifests.
21
- - MCP tools: `devcrew_start`, `devcrew_status`, `devcrew_answer`, `devcrew_approve`, `devcrew_reject`, `devcrew_continue`, and `devcrew_artifact`.
22
+ - MCP tools: `devcrew_start`, `devcrew_status`, `devcrew_answer`, `devcrew_approve`, `devcrew_reject`, `devcrew_continue`, `devcrew_complete_execution`, `devcrew_waive_verification`, and `devcrew_artifact`.
22
23
  - Codex and Claude Code plugin scaffolds generated by `devcrew init`.
23
24
 
24
25
  ## Install As A Codex Plugin
@@ -34,7 +35,7 @@ Restart Codex, open the plugin directory, choose the DevCrew marketplace, and in
34
35
  The plugin starts the DevCrew MCP server with:
35
36
 
36
37
  ```bash
37
- npm exec --silent --yes --package=@shenlee/devcrew@0.1.2 -- node -e "<DevCrew CLI wrapper>" -- serve --stdio
38
+ npm exec --silent --yes --package=@shenlee/devcrew@0.1.4 -- node -e "<DevCrew CLI wrapper>" -- serve --stdio
38
39
  ```
39
40
 
40
41
  The plugin locks the MCP server to the published npm package version, so users do not need to clone the source or build TypeScript at install time. You only need Node.js and network access the first time Codex starts the MCP server.
@@ -94,12 +95,13 @@ requirements approval
94
95
  -> architecture approval
95
96
  -> implementation plan approval
96
97
  -> isolated execution
98
+ -> architecture review approval
97
99
  -> isolated testing
98
100
  -> testing approval
99
101
  -> patch promotion to requester repository
100
102
  ```
101
103
 
102
- Apply requires a Git repository, a real Codex or Claude SDK backend, and a clean requester worktree when isolated execution starts and when the patch is promoted. After approving the implementation plan, call `devcrew_continue` once for isolated execution and again for isolated testing. The requester repository is unchanged until the testing gate is approved. Rejecting testing and answering the feedback returns the same isolated workspace to `execution` without changing the requester repository.
104
+ Apply requires a Git repository and a clean requester worktree when isolated execution starts and when the patch is promoted. The default `interactive-host` policy does not start a nested SDK: after each `devcrew_continue`, it waits at `awaiting_execution` for the host-native agent to work in the indicated isolated worktree. Call `devcrew_complete_execution` after implementation, and again after testing with command, exit-code, and output evidence. Explicit `headless-restricted` and `headless-unattended` policies instead use DevCrew-managed SDK permissions; they do not inherit the current host approval session. The requester repository is unchanged until the testing gate is approved. Failed verification moves the run to `awaiting_input` and cannot be promoted unless `devcrew_waive_verification` records an explicit risk reason. Rejecting testing and answering the feedback returns the same isolated workspace to `execution` without changing the requester repository.
103
105
 
104
106
  DevCrew auto-discovers verification commands from common project manifests. The current rules prefer `package.json` scripts (`validate`, then `test`, then `typecheck`/`lint`), then fall back to `go test ./...`, `cargo test`, or `python -m pytest` when matching manifests are present.
105
107
 
@@ -112,12 +114,18 @@ You can override discovery with explicit commands in `.devcrew/config.json`:
112
114
  "executionMode": "plan",
113
115
  "verifyCommands": ["npm run validate"],
114
116
  "workflow": {
115
- "gates": ["requirements", "architecture", "implementation", "testing"],
117
+ "gates": ["requirements", "architecture", "implementation", "implementation-review", "testing"],
116
118
  "artifactDirectory": "docs/devcrew"
117
119
  }
118
120
  }
119
121
  ```
120
122
 
123
+ `workflow.gates` controls only whether the requester must manually approve the
124
+ `requirements`, `architecture`, and `implementation` artifacts. Omitting one
125
+ of those names still runs its role and writes its artifact, then advances to the
126
+ next phase automatically. `implementation-review` and `testing` are mandatory
127
+ safety gates and are always enabled, even when omitted from the configuration.
128
+
121
129
  ## Development
122
130
 
123
131
  ```bash
@@ -127,13 +135,13 @@ npm run validate
127
135
  npm pack --dry-run
128
136
  ```
129
137
 
130
- The SDK adapters use deterministic local fallback output when Codex or Claude SDK packages are not installed. This keeps local tests reliable while preserving the adapter boundary for real host integration. In `apply` mode, DevCrew still inherits the host sandbox and approval boundaries.
138
+ The SDK adapters use deterministic local fallback output when Codex or Claude SDK packages are not installed. This keeps local tests reliable while preserving the adapter boundary for real host integration. Nested SDK apply runs use the recorded DevCrew headless policy; they never inherit the current host sandbox or approval session.
131
139
 
132
140
  For published installs, the host SDK packages are pinned optional dependencies so the version-locked `npm exec --package=@shenlee/devcrew@<version>` wrapper can resolve them from the DevCrew package itself. Plan mode can still fall back to deterministic artifacts, but apply mode fails with a clear SDK resolution error if the selected host SDK is unavailable.
133
141
 
134
142
  Public npm publishing is handled by the `npm publish` GitHub Actions workflow. It runs validation, checks the package with `npm pack --dry-run`, and publishes with npm provenance when a GitHub Release is published or the workflow is manually dispatched with `NPM_TOKEN` configured.
135
143
 
136
- After publishing `@shenlee/devcrew@0.1.2`, run the real marketplace smoke test. This is a post-publication check because the plugin is version-locked to the npm package:
144
+ After publishing the version locked by the plugin, run the real marketplace smoke test. This is a post-publication check because the plugin is version-locked to the npm package:
137
145
 
138
146
  ```bash
139
147
  npm run smoke:codex-plugin
package/README.zh-CN.md CHANGED
@@ -12,13 +12,14 @@ DevCrew 是一个面向 Codex、Claude Code 等编程 Agent 的本地工作流
12
12
 
13
13
  - 内置阶段门禁:需求确认、架构确认、实现计划确认、测试报告确认。
14
14
  - 支持两种工作流:`feature` 用于已有项目功能开发,`greenfield` 用于从零开始的新产品。
15
- - 支持安全执行模式:默认是 `plan`,`apply` 必须显式开启。实现计划阶段始终只读,真正的实现和测试在 DevCrew 管理的 Git worktree 中隔离执行。
15
+ - 支持安全执行模式:默认是 `plan`,`apply` 必须显式开启。apply 默认使用 `interactive-host`,在 DevCrew 管理的 Git worktree 中暂停并交给宿主原生 agent 执行;headless 策略使用单独声明的 SDK 权限。
16
16
  - 默认按当前宿主选择后端:在 Codex 中优先使用 Codex,在 Claude Code 中优先使用 Claude。
17
17
  - 已接入角色编排:`devcrew_start` 会先运行 PM 角色,`devcrew_continue` 会运行下一阶段角色,然后再打开阶段门禁。
18
18
  - 实现评审产物:隔离执行会记录 changed files、支持二进制的 diff、lint 证据和架构符合性说明;测试结束后会重新生成评审 diff,再等待晋升。
19
+ - 架构复审结果结构化:执行后架构师必须给出 `approved` 或 `changes_required`;需要修改时会阻断 testing。
19
20
  - 运行状态写入 `.devcrew/runs/<run-id>/state.json`,评审产物写入 `docs/devcrew/<run-id>/`。
20
21
  - 自动发现项目规范:`.devcrew/standards.md`、`AGENTS.md`、`CLAUDE.md`、README 以及常见项目配置文件。
21
- - 提供 MCP 工具:`devcrew_start`、`devcrew_status`、`devcrew_answer`、`devcrew_approve`、`devcrew_reject`、`devcrew_continue`、`devcrew_artifact`。
22
+ - 提供 MCP 工具:`devcrew_start`、`devcrew_status`、`devcrew_answer`、`devcrew_approve`、`devcrew_reject`、`devcrew_continue`、`devcrew_complete_execution`、`devcrew_waive_verification`、`devcrew_artifact`。
22
23
  - 可通过 `devcrew init` 生成 Codex 和 Claude Code 插件骨架。
23
24
 
24
25
  ## 作为 Codex 插件安装
@@ -34,7 +35,7 @@ codex plugin marketplace add lishen802/devcrew
34
35
  插件会用下面的命令启动 DevCrew MCP 服务:
35
36
 
36
37
  ```bash
37
- npm exec --silent --yes --package=@shenlee/devcrew@0.1.2 -- node -e "<DevCrew CLI wrapper>" -- serve --stdio
38
+ npm exec --silent --yes --package=@shenlee/devcrew@0.1.4 -- node -e "<DevCrew CLI wrapper>" -- serve --stdio
38
39
  ```
39
40
 
40
41
  插件会锁定到已发布的 npm 包版本,因此用户不需要克隆源码,也不需要在安装时编译 TypeScript;只需要本机有 Node.js,并且 Codex 第一次启动 MCP 服务时可以访问网络。
@@ -108,12 +109,13 @@ apply 模式的固定顺序是:
108
109
  -> 架构确认
109
110
  -> 实现计划确认
110
111
  -> 隔离执行
112
+ -> 架构审查确认
111
113
  -> 隔离测试
112
114
  -> 测试报告确认
113
115
  -> 将补丁晋升到需求方仓库
114
116
  ```
115
117
 
116
- apply 模式要求项目是 Git 仓库、使用真实的 Codex Claude SDK 后端,并且在开始隔离执行和最终晋升补丁时,需求方工作树都必须保持干净。实现计划审批后,需要额外调用一次 `devcrew_continue` 完成隔离执行,再调用一次进入隔离测试。测试门禁批准前,需求方仓库不会出现实现改动;如果驳回测试并提交反馈答案,流程会回到同一个隔离工作树的 `execution` 阶段,不会修改需求方仓库。
118
+ apply 模式要求项目是 Git 仓库,并且在开始隔离执行和最终晋升补丁时,需求方工作树都必须保持干净。默认的 `interactive-host` 不会启动嵌套 SDK:每次 `devcrew_continue` 后会在 `awaiting_execution` 等待宿主原生 agent 在指定隔离 worktree 中工作;实现完成后调用 `devcrew_complete_execution`,测试完成时还要提交命令、退出码和输出证据。显式的 `headless-restricted` 与 `headless-unattended` 才使用 DevCrew 管理的 SDK 权限,它们不会继承当前宿主的审批会话。测试门禁批准前,需求方仓库不会出现实现改动;验证失败会进入 `awaiting_input`,只有通过 `devcrew_waive_verification` 记录明确风险原因后才可重新打开审批。驳回测试并提交反馈答案会回到同一个隔离工作树的 `execution` 阶段,不会修改需求方仓库。
117
119
 
118
120
  DevCrew 会自动从常见项目清单中发现验证命令。当前规则会优先读取 `package.json` scripts(`validate`,然后是 `test`,再到 `typecheck`/`lint`),再按项目清单回退到 `go test ./...`、`cargo test` 或 `python -m pytest`。
119
121
 
@@ -126,12 +128,15 @@ DevCrew 会自动从常见项目清单中发现验证命令。当前规则会优
126
128
  "executionMode": "plan",
127
129
  "verifyCommands": ["npm run validate"],
128
130
  "workflow": {
129
- "gates": ["requirements", "architecture", "implementation", "testing"],
131
+ "gates": ["requirements", "architecture", "implementation", "implementation-review", "testing"],
130
132
  "artifactDirectory": "docs/devcrew"
131
133
  }
132
134
  }
133
135
  ```
134
136
 
137
+ `workflow.gates` 只控制是否需要人工审批 `requirements`、`architecture` 和
138
+ `implementation` 产物。省略其中任意项仍会执行相应角色并写入产物,只是不再停下来等待审批,而是自动进入下一阶段。`implementation-review` 与 `testing` 是强制安全门禁:即使配置中省略,也始终启用。
139
+
135
140
  ## 开发命令
136
141
 
137
142
  ```bash
@@ -141,13 +146,13 @@ npm run validate
141
146
  npm pack --dry-run
142
147
  ```
143
148
 
144
- 当前适配器在未安装 Codex SDK 或 Claude SDK 时会使用确定性的本地 fallback 输出。这样可以保证测试和演示稳定,同时保留接入真实宿主 SDK 的边界。即使在 `apply` 模式下,DevCrew 仍然继承宿主的 sandbox、审批和工具权限。
149
+ 当前适配器在未安装 Codex SDK 或 Claude SDK 时会使用确定性的本地 fallback 输出。这样可以保证测试和演示稳定,同时保留接入真实宿主 SDK 的边界。嵌套 SDK 的 apply 运行使用记录在状态中的 DevCrew headless 策略,不会继承当前宿主的 sandbox、审批或工具会话。
145
150
 
146
151
  对于发布安装,宿主 SDK 包会作为精确锁定的 optional dependencies 随 DevCrew 一起安装,因此锁定版本的 `npm exec --package=@shenlee/devcrew@<version>` wrapper 可以从 DevCrew 包自身解析这些 SDK。plan 模式仍允许 deterministic fallback;但 apply 模式在选定宿主 SDK 不可用时会直接失败,并给出明确的 SDK 解析错误。
147
152
 
148
153
  公开 npm 发布由 `npm publish` GitHub Actions 工作流处理。发布 GitHub Release 或手动触发 workflow 时,它会先运行验证,再执行 `npm pack --dry-run` 检查包内容,最后在配置 `NPM_TOKEN` 后使用 npm provenance 发布公开包。
149
154
 
150
- 发布 `@shenlee/devcrew@0.1.2` 后,再运行真实 marketplace smoke test。由于插件锁定了 npm 版本,这是发布后的检查:
155
+ 发布插件锁定的 npm 版本后,再运行真实 marketplace smoke test。由于插件锁定了 npm 版本,这是发布后的检查:
151
156
 
152
157
  ```bash
153
158
  npm run smoke:codex-plugin
@@ -41,6 +41,162 @@ export function assertRoleSections(role, markdown) {
41
41
  throw new RoleOutputValidationError(role, missingSections);
42
42
  }
43
43
  }
44
+ export function extractOpenQuestions(markdown) {
45
+ const match = /^##\s+Open Questions\s*$(.*?)(?=^##\s|(?![\s\S]))/ims.exec(markdown);
46
+ if (!match) {
47
+ return [];
48
+ }
49
+ const questions = [];
50
+ for (const line of match[1].split("\n")) {
51
+ const question = /^\s*[-*]\s+(.+?)\s*$/u.exec(line)?.[1]?.trim();
52
+ if (question && !/^(none|n\/a|no open questions)\.?$/i.test(question)) {
53
+ questions.push(question);
54
+ }
55
+ }
56
+ return questions;
57
+ }
58
+ export function extractArchitectureReviewDecision(markdown) {
59
+ const section = /^##\s+Review Decision\s*$(.*?)(?=^##\s|(?![\s\S]))/ims.exec(markdown)?.[1] ?? "";
60
+ const match = /^Decision:\s*(approved|changes_required)\s*$/im.exec(section);
61
+ return match?.[1];
62
+ }
63
+ const STRUCTURED_RESULT_MARKER = "<!-- devcrew-role-result -->";
64
+ const STRUCTURED_RESULT_BLOCK = /<!--\s*devcrew-role-result\s*-->\s*```json\s*\r?\n([\s\S]*?)\r?\n```/g;
65
+ function structuredOutputError(role, reason) {
66
+ return new RoleOutputValidationError(role, [`marked structured role result: ${reason}`]);
67
+ }
68
+ function asRecord(value, role, field) {
69
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
70
+ throw structuredOutputError(role, `${field} must be an object`);
71
+ }
72
+ return value;
73
+ }
74
+ function asNonEmptyString(value, role, field) {
75
+ if (typeof value !== "string" || value.trim().length === 0) {
76
+ throw structuredOutputError(role, `${field} must be a non-empty string`);
77
+ }
78
+ return value.trim();
79
+ }
80
+ function asStringArray(value, role, field) {
81
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || entry.trim().length === 0)) {
82
+ throw structuredOutputError(role, `${field} must be an array of non-empty strings`);
83
+ }
84
+ return value.map((entry) => entry.trim());
85
+ }
86
+ function asEvidence(value, role) {
87
+ if (!Array.isArray(value)) {
88
+ throw structuredOutputError(role, "evidence must be an array");
89
+ }
90
+ return value.map((entry, index) => {
91
+ const evidence = asRecord(entry, role, `evidence[${index}]`);
92
+ const command = asNonEmptyString(evidence.command, role, `evidence[${index}].command`);
93
+ if (!Number.isInteger(evidence.exitCode)) {
94
+ throw structuredOutputError(role, `evidence[${index}].exitCode must be an integer`);
95
+ }
96
+ if (evidence.output !== undefined && typeof evidence.output !== "string") {
97
+ throw structuredOutputError(role, `evidence[${index}].output must be a string`);
98
+ }
99
+ return { command, exitCode: evidence.exitCode, output: evidence.output };
100
+ });
101
+ }
102
+ function asQuestions(value, role) {
103
+ if (!Array.isArray(value)) {
104
+ throw structuredOutputError(role, "questions must be an array");
105
+ }
106
+ const ids = new Set();
107
+ return value.map((entry, index) => {
108
+ const question = asRecord(entry, role, `questions[${index}]`);
109
+ const id = asNonEmptyString(question.id, role, `questions[${index}].id`);
110
+ if (ids.has(id)) {
111
+ throw structuredOutputError(role, `questions[${index}].id must be unique`);
112
+ }
113
+ ids.add(id);
114
+ const prompt = asNonEmptyString(question.prompt, role, `questions[${index}].prompt`);
115
+ if (question.context !== undefined && typeof question.context !== "string") {
116
+ throw structuredOutputError(role, `questions[${index}].context must be a string`);
117
+ }
118
+ return { id, prompt, context: question.context };
119
+ });
120
+ }
121
+ function parseStructuredRoleData(role, phase, raw) {
122
+ const value = asRecord(raw, role, "result");
123
+ if (value.schemaVersion !== 1) {
124
+ throw structuredOutputError(role, "schemaVersion must be 1");
125
+ }
126
+ if (value.role !== role) {
127
+ throw structuredOutputError(role, `role must be ${role}`);
128
+ }
129
+ const data = {
130
+ schemaVersion: 1,
131
+ role,
132
+ summary: asNonEmptyString(value.summary, role, "summary"),
133
+ risks: asStringArray(value.risks, role, "risks"),
134
+ evidence: asEvidence(value.evidence, role),
135
+ };
136
+ if (role === "pm") {
137
+ data.questions = asQuestions(value.questions, role);
138
+ }
139
+ else if (role === "architect") {
140
+ data.decisions = asStringArray(value.decisions, role, "decisions");
141
+ if (phase === "review") {
142
+ if (value.reviewDecision !== "approved" && value.reviewDecision !== "changes_required") {
143
+ throw structuredOutputError(role, "reviewDecision must be approved or changes_required");
144
+ }
145
+ data.reviewDecision = value.reviewDecision;
146
+ }
147
+ }
148
+ else if (role === "implementer") {
149
+ data.changedFiles = asStringArray(value.changedFiles, role, "changedFiles");
150
+ }
151
+ else {
152
+ if (!Array.isArray(value.testCases)) {
153
+ throw structuredOutputError(role, "testCases must be an array");
154
+ }
155
+ data.testCases = value.testCases.map((entry, index) => {
156
+ const testCase = asRecord(entry, role, `testCases[${index}]`);
157
+ const type = testCase.type;
158
+ if (type !== "happy" && type !== "edge" && type !== "failure" && type !== "regression") {
159
+ throw structuredOutputError(role, `testCases[${index}].type is invalid`);
160
+ }
161
+ return {
162
+ id: asNonEmptyString(testCase.id, role, `testCases[${index}].id`),
163
+ scenario: asNonEmptyString(testCase.scenario, role, `testCases[${index}].scenario`),
164
+ type,
165
+ expected: asNonEmptyString(testCase.expected, role, `testCases[${index}].expected`),
166
+ };
167
+ });
168
+ }
169
+ return data;
170
+ }
171
+ export function parseRoleResultOutput(role, phase, output) {
172
+ if (!output.includes(STRUCTURED_RESULT_MARKER)) {
173
+ return {
174
+ format: "legacy",
175
+ markdown: output,
176
+ questions: role === "pm" ? extractOpenQuestions(output) : undefined,
177
+ reviewDecision: phase === "review" ? extractArchitectureReviewDecision(output) : undefined,
178
+ };
179
+ }
180
+ const blocks = [...output.matchAll(STRUCTURED_RESULT_BLOCK)];
181
+ if (blocks.length !== 1) {
182
+ throw structuredOutputError(role, blocks.length === 0 ? "must use a JSON fenced block" : "must appear exactly once");
183
+ }
184
+ let raw;
185
+ try {
186
+ raw = JSON.parse(blocks[0]?.[1] ?? "");
187
+ }
188
+ catch {
189
+ throw structuredOutputError(role, "contains invalid JSON");
190
+ }
191
+ const structured = parseStructuredRoleData(role, phase, raw);
192
+ return {
193
+ format: "structured",
194
+ markdown: output.slice(0, blocks[0]?.index).concat(output.slice((blocks[0]?.index ?? 0) + (blocks[0]?.[0].length ?? 0))).trim(),
195
+ structured,
196
+ questions: structured.questions?.map((question) => question.prompt),
197
+ reviewDecision: structured.reviewDecision,
198
+ };
199
+ }
44
200
  export function renderRolePrompt(input) {
45
201
  const executionMode = input.executionMode ?? "plan";
46
202
  const answers = input.answers ?? [];
@@ -76,7 +232,10 @@ export function renderRolePrompt(input) {
76
232
  ? "You may run validation commands needed for the approved scope and report exact evidence."
77
233
  : "You may modify repository files needed for the approved scope and report changed files."
78
234
  : "Do not modify repository files. Return only the Markdown document content for the artifact.";
79
- lines.push("", "Instructions:", `Act as the DevCrew ${input.role} role and produce a complete, well-structured Markdown document for the ${input.phase} phase.`, "Keep scope aligned with the approved gates and inherited host permissions.", permissionInstruction, "", "Required Sections:", ...roleGuidance(input.role));
235
+ lines.push("", "Instructions:", `Act as the DevCrew ${input.role} role and produce a complete, well-structured Markdown document for the ${input.phase} phase.`, "Keep scope aligned with the approved gates and the selected DevCrew execution policy.", permissionInstruction, "", "Return this protocol block first:", STRUCTURED_RESULT_MARKER, "```json", `{\"schemaVersion\":1,\"role\":\"${input.role}\",\"summary\":\"...\",\"risks\":[],\"evidence\":[]}`, "```", "Then return the required Markdown H2 sections. Do not include a second marked result block.", "", "Required Sections:", ...roleGuidance(input.role));
236
+ if (input.phase === "review") {
237
+ lines.push("", "Architecture Review Decision:", "Add an exact H2 `## Review Decision` section containing exactly `Decision: approved` or `Decision: changes_required`.", "Use `changes_required` for any mismatch with the approved architecture; summarize the blocking mismatch in that section.");
238
+ }
80
239
  return lines.join("\n");
81
240
  }
82
241
  function titleForPhase(phase) {
@@ -85,6 +244,7 @@ function titleForPhase(phase) {
85
244
  architecture: "Architecture",
86
245
  implementation: "Implementation Plan",
87
246
  execution: "Implementation Review",
247
+ review: "Architecture Review",
88
248
  testing: "Test Report",
89
249
  acceptance: "Acceptance",
90
250
  complete: "Acceptance",
@@ -155,24 +315,45 @@ export function extractClaudeResult(message) {
155
315
  }
156
316
  return text;
157
317
  }
158
- function roleCanApply(input) {
318
+ function isApplyPhase(input) {
159
319
  return input.executionMode === "apply" && (input.phase === "execution" || input.phase === "testing");
160
320
  }
161
- function codexSandboxForRole(input) {
162
- return roleCanApply(input) ? "workspace-write" : "read-only";
321
+ function effectiveExecutionPolicy(input) {
322
+ return input.executionPolicy ?? "interactive-host";
323
+ }
324
+ function roleCanApply(input) {
325
+ return isApplyPhase(input) && effectiveExecutionPolicy(input) !== "interactive-host";
326
+ }
327
+ function codexOptionsForRole(input) {
328
+ if (!roleCanApply(input)) {
329
+ return buildCodexThreadOptions(input.cwd);
330
+ }
331
+ const executionPolicy = effectiveExecutionPolicy(input);
332
+ return {
333
+ ...buildCodexThreadOptions(input.cwd, "workspace-write"),
334
+ approvalPolicy: executionPolicy === "headless-unattended" ? "never" : "on-request",
335
+ networkAccessEnabled: false,
336
+ };
163
337
  }
164
338
  function claudeOptionsForRole(input) {
165
339
  if (!roleCanApply(input)) {
166
340
  return buildClaudeOptions(input.cwd);
167
341
  }
168
- const allowedTools = input.role === "implementer" ? ["Read", "Grep", "Glob", "Edit", "Write", "Bash"] : ["Read", "Grep", "Glob", "Bash"];
169
- return buildClaudeOptions(input.cwd, "acceptEdits", allowedTools);
342
+ if (effectiveExecutionPolicy(input) === "headless-unattended") {
343
+ return {
344
+ cwd: input.cwd,
345
+ permissionMode: "bypassPermissions",
346
+ allowDangerouslySkipPermissions: true,
347
+ };
348
+ }
349
+ const allowedTools = input.role === "implementer" ? ["Read", "Grep", "Glob", "Edit", "Write"] : ["Read", "Grep", "Glob"];
350
+ return buildClaudeOptions(input.cwd, "dontAsk", allowedTools);
170
351
  }
171
352
  async function runWithCodex(input, prompt, loadModule) {
172
353
  const mod = await loadModule(HOST_SDK_PACKAGES.codex);
173
354
  const CodexClass = mod.Codex;
174
355
  const codex = new CodexClass();
175
- const thread = codex.startThread(buildCodexThreadOptions(input.cwd, codexSandboxForRole(input)));
356
+ const thread = codex.startThread(codexOptionsForRole(input));
176
357
  const turn = await thread.run(prompt);
177
358
  return extractCodexText(turn);
178
359
  }
@@ -194,12 +375,16 @@ function fallbackResult(role, backend, title, summary) {
194
375
  summary,
195
376
  markdown: `# ${title}\n\n${summary}\n`,
196
377
  usedFallback: true,
378
+ format: "legacy",
197
379
  };
198
380
  }
199
381
  function shouldFailOnSdkError(input) {
200
382
  return input.executionMode === "apply" && input.backend !== "local";
201
383
  }
202
384
  export async function runRole(input, deps = {}) {
385
+ if (isApplyPhase(input) && effectiveExecutionPolicy(input) === "interactive-host") {
386
+ throw new Error("DevCrew interactive-host execution must be performed by the host, not a nested SDK");
387
+ }
203
388
  const loadModule = deps.loadModule ?? importOptional;
204
389
  const title = titleForPhase(input.phase);
205
390
  const prompt = renderRolePrompt(input);
@@ -210,13 +395,22 @@ export async function runRole(input, deps = {}) {
210
395
  const markdown = input.backend === "codex"
211
396
  ? await runWithCodex(input, prompt, loadModule)
212
397
  : await runWithClaude(input, prompt, loadModule);
213
- assertRoleSections(input.role, markdown);
398
+ const parsed = parseRoleResultOutput(input.role, input.phase, markdown);
399
+ assertRoleSections(input.role, parsed.markdown);
400
+ const reviewDecision = parsed.reviewDecision;
401
+ if (input.phase === "review" && !reviewDecision) {
402
+ throw new RoleOutputValidationError(input.role, ["Review Decision"]);
403
+ }
214
404
  return {
215
405
  role: input.role,
216
406
  backend: input.backend,
217
- summary: `${input.role} produced ${title} using the ${input.backend} SDK.`,
218
- markdown,
407
+ summary: parsed.structured?.summary ?? `${input.role} produced ${title} using the ${input.backend} SDK.`,
408
+ markdown: parsed.markdown,
219
409
  usedFallback: false,
410
+ format: parsed.format,
411
+ structured: parsed.structured,
412
+ questions: parsed.questions,
413
+ reviewDecision,
220
414
  };
221
415
  }
222
416
  catch (error) {
@@ -1,4 +1,4 @@
1
- import { readFile, writeFile } from "node:fs/promises";
1
+ import { readFile, unlink, writeFile } from "node:fs/promises";
2
2
  import { activeRunPath, ensureProjectDirectories } from "./paths.js";
3
3
  export async function setActiveRun(cwd, runId) {
4
4
  await ensureProjectDirectories(cwd);
@@ -22,3 +22,15 @@ export async function getActiveRunId(cwd) {
22
22
  }
23
23
  throw new Error("No active DevCrew run. Pass runId or start a workflow first.");
24
24
  }
25
+ export async function clearActiveRunIfMatches(cwd, runId) {
26
+ try {
27
+ if ((await getActiveRunId(cwd)) !== runId) {
28
+ return false;
29
+ }
30
+ await unlink(activeRunPath(cwd));
31
+ return true;
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ }
@@ -7,6 +7,7 @@ function headingForArtifact(name) {
7
7
  architecture: "Architecture",
8
8
  "implementation-plan": "Implementation Plan",
9
9
  "implementation-review": "Implementation Review",
10
+ "architecture-review": "Architecture Review",
10
11
  "test-report": "Test Report",
11
12
  acceptance: "Acceptance",
12
13
  }[name];
@@ -47,6 +48,12 @@ function verificationBlock(state) {
47
48
  .map((result) => `### ${result.command}\n\nExit Code: ${result.exitCode}\n\nOutput:\n\n\`\`\`text\n${result.output || "(no output)"}\n\`\`\``)
48
49
  .join("\n\n");
49
50
  }
51
+ function verificationWaiverBlock(state) {
52
+ if (!state.verificationWaiver) {
53
+ return "No verification waiver has been recorded.";
54
+ }
55
+ return `Reason: ${state.verificationWaiver.reason}\nRecorded At: ${state.verificationWaiver.createdAt}`;
56
+ }
50
57
  function lintResultsBlock(state) {
51
58
  if (state.lintResults.length === 0) {
52
59
  return "No lint or format commands have been executed.";
@@ -79,7 +86,7 @@ function architectureComplianceStatus(state) {
79
86
  }
80
87
  export function renderArtifact(name, state) {
81
88
  const title = headingForArtifact(name);
82
- 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`;
89
+ 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`;
83
90
  if (name === "requirements") {
84
91
  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`;
85
92
  }
@@ -92,15 +99,18 @@ export function renderArtifact(name, state) {
92
99
  if (name === "implementation-review") {
93
100
  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`;
94
101
  }
102
+ if (name === "architecture-review") {
103
+ 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`;
104
+ }
95
105
  if (name === "test-report") {
96
- 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`;
106
+ 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`;
97
107
  }
98
- 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)
108
+ 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)
99
109
  .map(([gate, status]) => `- ${gate}: ${status}`)
100
110
  .join("\n")}\n`;
101
111
  }
102
112
  export async function writeArtifact(name, state) {
103
- const path = artifactPath(state.cwd, state.runId, name);
113
+ const path = artifactPath(state.cwd, state.runId, name, state.artifactDirectory);
104
114
  await mkdir(dirname(path), { recursive: true });
105
115
  await writeFile(path, renderArtifact(name, state), "utf8");
106
116
  return path;
@@ -1,5 +1,7 @@
1
1
  import { access, readFile, writeFile } from "node:fs/promises";
2
+ import { isAbsolute, relative, resolve, sep } from "node:path";
2
3
  import { configPath, ensureProjectDirectories } from "./paths.js";
4
+ import { BACKENDS, EXECUTION_MODES, GATES } from "./types.js";
3
5
  export const DEFAULT_CONFIG = {
4
6
  version: 1,
5
7
  defaultBackend: "host-preferred",
@@ -8,7 +10,7 @@ export const DEFAULT_CONFIG = {
8
10
  lintCommands: [],
9
11
  coverageCommands: [],
10
12
  workflow: {
11
- gates: ["requirements", "architecture", "implementation", "testing"],
13
+ gates: ["requirements", "architecture", "implementation", "implementation-review", "testing"],
12
14
  artifactDirectory: "docs/devcrew",
13
15
  },
14
16
  };
@@ -21,6 +23,90 @@ async function exists(path) {
21
23
  return false;
22
24
  }
23
25
  }
26
+ function asRecord(value, field) {
27
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
28
+ throw new Error(`Invalid .devcrew/config.json: ${field} must be an object`);
29
+ }
30
+ return value;
31
+ }
32
+ function assertKnownKeys(value, field, allowed) {
33
+ for (const key of Object.keys(value)) {
34
+ if (!allowed.includes(key)) {
35
+ throw new Error(`Invalid .devcrew/config.json: ${field} has unknown key ${key}`);
36
+ }
37
+ }
38
+ }
39
+ function requiredString(value, field) {
40
+ if (typeof value !== "string" || value.trim().length === 0) {
41
+ throw new Error(`Invalid .devcrew/config.json: ${field} must be a non-empty string`);
42
+ }
43
+ return value.trim();
44
+ }
45
+ function parseCommandList(value, field) {
46
+ if (value === undefined) {
47
+ return [];
48
+ }
49
+ if (!Array.isArray(value)) {
50
+ throw new Error(`Invalid .devcrew/config.json: ${field} must be an array`);
51
+ }
52
+ return value.map((entry, index) => requiredString(entry, `${field}[${index}]`));
53
+ }
54
+ function parseGates(value) {
55
+ if (!Array.isArray(value)) {
56
+ throw new Error("Invalid .devcrew/config.json: workflow.gates must be an array");
57
+ }
58
+ const gates = [];
59
+ for (const [index, entry] of value.entries()) {
60
+ if (typeof entry !== "string" || !GATES.includes(entry)) {
61
+ throw new Error(`Invalid .devcrew/config.json: workflow.gates[${index}] must be a known gate, received ${String(entry)}`);
62
+ }
63
+ if (gates.includes(entry)) {
64
+ throw new Error(`Invalid .devcrew/config.json: workflow.gates contains duplicate ${entry}`);
65
+ }
66
+ gates.push(entry);
67
+ }
68
+ return gates;
69
+ }
70
+ function parseArtifactDirectory(cwd, value) {
71
+ const artifactDirectory = requiredString(value, "workflow.artifactDirectory");
72
+ if (isAbsolute(artifactDirectory)) {
73
+ throw new Error("Invalid .devcrew/config.json: workflow.artifactDirectory must be relative to the repository");
74
+ }
75
+ const projectRoot = resolve(cwd);
76
+ const target = resolve(projectRoot, artifactDirectory);
77
+ const fromProject = relative(projectRoot, target);
78
+ if (fromProject === ".." || fromProject.startsWith(`..${sep}`) || isAbsolute(fromProject)) {
79
+ throw new Error("Invalid .devcrew/config.json: workflow.artifactDirectory must resolve inside the repository");
80
+ }
81
+ return artifactDirectory;
82
+ }
83
+ function parseConfig(cwd, value) {
84
+ const parsed = asRecord(value, "root");
85
+ assertKnownKeys(parsed, "root", ["version", "defaultBackend", "executionMode", "verifyCommands", "lintCommands", "coverageCommands", "workflow"]);
86
+ if (parsed.version !== 1) {
87
+ throw new Error("Unsupported .devcrew/config.json version");
88
+ }
89
+ if (typeof parsed.defaultBackend !== "string" || (parsed.defaultBackend !== "host-preferred" && !BACKENDS.includes(parsed.defaultBackend))) {
90
+ throw new Error("Invalid .devcrew/config.json: defaultBackend must be host-preferred, codex, claude, or local");
91
+ }
92
+ if (typeof parsed.executionMode !== "string" || !EXECUTION_MODES.includes(parsed.executionMode)) {
93
+ throw new Error("Invalid .devcrew/config.json: executionMode must be plan or apply");
94
+ }
95
+ const workflow = asRecord(parsed.workflow, "workflow");
96
+ assertKnownKeys(workflow, "workflow", ["gates", "artifactDirectory"]);
97
+ return {
98
+ version: 1,
99
+ defaultBackend: parsed.defaultBackend,
100
+ executionMode: parsed.executionMode,
101
+ verifyCommands: parseCommandList(parsed.verifyCommands, "verifyCommands"),
102
+ lintCommands: parseCommandList(parsed.lintCommands, "lintCommands"),
103
+ coverageCommands: parseCommandList(parsed.coverageCommands, "coverageCommands"),
104
+ workflow: {
105
+ gates: parseGates(workflow.gates),
106
+ artifactDirectory: parseArtifactDirectory(cwd, workflow.artifactDirectory),
107
+ },
108
+ };
109
+ }
24
110
  export async function ensureConfig(cwd) {
25
111
  await ensureProjectDirectories(cwd);
26
112
  const path = configPath(cwd);
@@ -32,20 +118,5 @@ export async function ensureConfig(cwd) {
32
118
  }
33
119
  export async function readConfig(cwd) {
34
120
  const raw = await readFile(configPath(cwd), "utf8");
35
- const parsed = JSON.parse(raw);
36
- if (parsed.version !== 1) {
37
- throw new Error("Unsupported .devcrew/config.json version");
38
- }
39
- return {
40
- ...DEFAULT_CONFIG,
41
- ...parsed,
42
- executionMode: parsed.executionMode ?? "plan",
43
- verifyCommands: Array.isArray(parsed.verifyCommands) ? parsed.verifyCommands : [],
44
- lintCommands: Array.isArray(parsed.lintCommands) ? parsed.lintCommands : [],
45
- coverageCommands: Array.isArray(parsed.coverageCommands) ? parsed.coverageCommands : [],
46
- workflow: {
47
- ...DEFAULT_CONFIG.workflow,
48
- ...parsed.workflow,
49
- },
50
- };
121
+ return parseConfig(cwd, JSON.parse(raw));
51
122
  }
@@ -1,6 +1,7 @@
1
1
  export * from "./active-run.js";
2
2
  export * from "./artifacts.js";
3
3
  export * from "./config.js";
4
+ export * from "./lock.js";
4
5
  export * from "./paths.js";
5
6
  export * from "./standards.js";
6
7
  export * from "./store.js";