@shenlee/devcrew 0.1.1 → 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.
- package/README.md +30 -8
- package/README.zh-CN.md +27 -8
- package/dist/packages/adapters/src/index.js +62 -8
- package/dist/packages/core/src/artifacts.js +14 -4
- package/dist/packages/core/src/config.js +1 -1
- package/dist/packages/core/src/paths.js +10 -6
- package/dist/packages/core/src/store.js +36 -0
- package/dist/packages/core/src/types.js +5 -1
- package/dist/packages/core/src/validation.js +7 -1
- package/dist/packages/core/src/version.js +1 -1
- package/dist/packages/core/src/workflow.js +120 -31
- package/dist/packages/orchestrator/src/index.js +339 -160
- package/dist/packages/orchestrator/src/worktree.js +198 -0
- package/dist/packages/plugins/src/index.js +2 -32
- package/dist/packages/service/src/stdio.js +19 -3
- package/dist/packages/service/src/tools.js +45 -6
- package/docs/claude-code.md +5 -3
- package/docs/codex.md +12 -4
- package/docs/quickstart.md +1 -1
- package/docs/superpowers/plans/2026-07-10-p0-foundation-repair.md +939 -0
- package/docs/superpowers/plans/2026-07-13-safety-semantics.md +313 -0
- package/docs/superpowers/specs/2026-07-10-p0-foundation-repair-design.md +182 -0
- package/docs/superpowers/specs/2026-07-13-execution-boundaries-design.md +126 -0
- package/docs/workflow.md +36 -4
- package/package.json +1 -1
- package/packages/adapters/src/index.ts +87 -11
- package/packages/core/src/artifacts.ts +16 -4
- package/packages/core/src/config.ts +1 -1
- package/packages/core/src/paths.ts +11 -6
- package/packages/core/src/store.ts +41 -1
- package/packages/core/src/types.ts +53 -2
- package/packages/core/src/validation.ts +10 -0
- package/packages/core/src/version.ts +1 -1
- package/packages/core/src/workflow.ts +136 -30
- package/packages/orchestrator/src/index.ts +377 -182
- package/packages/orchestrator/src/worktree.ts +269 -0
- package/packages/plugins/src/index.ts +2 -44
- package/packages/service/src/stdio.ts +27 -6
- package/packages/service/src/tools.ts +46 -5
- package/plugins/devcrew-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/devcrew-codex/.mcp.json +1 -1
- package/plugins/devcrew-codex/assets/logo.png +0 -0
- package/plugins/devcrew-codex/skills/devcrew/SKILL.md +8 -7
- package/scripts/smoke-codex-plugin.mjs +1 -1
- package/plugins/devcrew-codex/agents/architect.toml +0 -6
- package/plugins/devcrew-codex/agents/implementer.toml +0 -6
- package/plugins/devcrew-codex/agents/pm.toml +0 -6
- package/plugins/devcrew-codex/agents/tester.toml +0 -6
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
|
|
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
|
-
- Implementation review artifact:
|
|
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.
|
|
38
|
+
npm exec --silent --yes --package=@shenlee/devcrew@0.1.3 -- 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.
|
|
@@ -81,12 +82,27 @@ The agent should call `devcrew_start`, show the PM-generated requirements artifa
|
|
|
81
82
|
|
|
82
83
|
Plugins set `DEVCREW_HOST` for host detection, so `devcrew_start` can omit `host` unless you want to override it. DevCrew records the latest run as the repository's active run, so follow-up MCP calls can omit `runId`.
|
|
83
84
|
|
|
84
|
-
By default DevCrew runs in `plan` mode. To allow
|
|
85
|
+
By default DevCrew runs in `plan` mode. To allow isolated execution to modify files and isolated testing to run configured verification commands, explicitly request apply mode:
|
|
85
86
|
|
|
86
87
|
```text
|
|
87
88
|
Use DevCrew in apply mode to implement audit logging for the billing API.
|
|
88
89
|
```
|
|
89
90
|
|
|
91
|
+
Apply mode follows this sequence:
|
|
92
|
+
|
|
93
|
+
```text
|
|
94
|
+
requirements approval
|
|
95
|
+
-> architecture approval
|
|
96
|
+
-> implementation plan approval
|
|
97
|
+
-> isolated execution
|
|
98
|
+
-> architecture review approval
|
|
99
|
+
-> isolated testing
|
|
100
|
+
-> testing approval
|
|
101
|
+
-> patch promotion to requester repository
|
|
102
|
+
```
|
|
103
|
+
|
|
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.
|
|
105
|
+
|
|
90
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.
|
|
91
107
|
|
|
92
108
|
You can override discovery with explicit commands in `.devcrew/config.json`:
|
|
@@ -98,12 +114,18 @@ You can override discovery with explicit commands in `.devcrew/config.json`:
|
|
|
98
114
|
"executionMode": "plan",
|
|
99
115
|
"verifyCommands": ["npm run validate"],
|
|
100
116
|
"workflow": {
|
|
101
|
-
"gates": ["requirements", "architecture", "implementation", "testing"],
|
|
117
|
+
"gates": ["requirements", "architecture", "implementation", "implementation-review", "testing"],
|
|
102
118
|
"artifactDirectory": "docs/devcrew"
|
|
103
119
|
}
|
|
104
120
|
}
|
|
105
121
|
```
|
|
106
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
|
+
|
|
107
129
|
## Development
|
|
108
130
|
|
|
109
131
|
```bash
|
|
@@ -113,13 +135,13 @@ npm run validate
|
|
|
113
135
|
npm pack --dry-run
|
|
114
136
|
```
|
|
115
137
|
|
|
116
|
-
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.
|
|
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.
|
|
117
139
|
|
|
118
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.
|
|
119
141
|
|
|
120
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.
|
|
121
143
|
|
|
122
|
-
After publishing the
|
|
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:
|
|
123
145
|
|
|
124
146
|
```bash
|
|
125
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
|
|
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.
|
|
38
|
+
npm exec --silent --yes --package=@shenlee/devcrew@0.1.3 -- node -e "<DevCrew CLI wrapper>" -- serve --stdio
|
|
38
39
|
```
|
|
39
40
|
|
|
40
41
|
插件会锁定到已发布的 npm 包版本,因此用户不需要克隆源码,也不需要在安装时编译 TypeScript;只需要本机有 Node.js,并且 Codex 第一次启动 MCP 服务时可以访问网络。
|
|
@@ -95,12 +96,27 @@ devcrew serve --stdio
|
|
|
95
96
|
|
|
96
97
|
插件会设置 `DEVCREW_HOST` 用于宿主识别,因此 `devcrew_start` 可以省略 `host`,除非你需要显式覆盖。DevCrew 会把最新 run 记录为当前仓库的 active run,后续 MCP 调用可以省略 `runId`。
|
|
97
98
|
|
|
98
|
-
默认情况下 DevCrew 使用安全的 `plan`
|
|
99
|
+
默认情况下 DevCrew 使用安全的 `plan` 模式。如果你希望隔离执行阶段真正修改文件,并在隔离测试阶段运行配置好的验证命令,需要明确要求 apply 模式:
|
|
99
100
|
|
|
100
101
|
```text
|
|
101
102
|
使用 DevCrew apply 模式帮我实现 billing API 的审计日志功能。
|
|
102
103
|
```
|
|
103
104
|
|
|
105
|
+
apply 模式的固定顺序是:
|
|
106
|
+
|
|
107
|
+
```text
|
|
108
|
+
需求确认
|
|
109
|
+
-> 架构确认
|
|
110
|
+
-> 实现计划确认
|
|
111
|
+
-> 隔离执行
|
|
112
|
+
-> 架构审查确认
|
|
113
|
+
-> 隔离测试
|
|
114
|
+
-> 测试报告确认
|
|
115
|
+
-> 将补丁晋升到需求方仓库
|
|
116
|
+
```
|
|
117
|
+
|
|
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` 阶段,不会修改需求方仓库。
|
|
119
|
+
|
|
104
120
|
DevCrew 会自动从常见项目清单中发现验证命令。当前规则会优先读取 `package.json` scripts(`validate`,然后是 `test`,再到 `typecheck`/`lint`),再按项目清单回退到 `go test ./...`、`cargo test` 或 `python -m pytest`。
|
|
105
121
|
|
|
106
122
|
你也可以在 `.devcrew/config.json` 中显式覆盖:
|
|
@@ -112,12 +128,15 @@ DevCrew 会自动从常见项目清单中发现验证命令。当前规则会优
|
|
|
112
128
|
"executionMode": "plan",
|
|
113
129
|
"verifyCommands": ["npm run validate"],
|
|
114
130
|
"workflow": {
|
|
115
|
-
"gates": ["requirements", "architecture", "implementation", "testing"],
|
|
131
|
+
"gates": ["requirements", "architecture", "implementation", "implementation-review", "testing"],
|
|
116
132
|
"artifactDirectory": "docs/devcrew"
|
|
117
133
|
}
|
|
118
134
|
}
|
|
119
135
|
```
|
|
120
136
|
|
|
137
|
+
`workflow.gates` 只控制是否需要人工审批 `requirements`、`architecture` 和
|
|
138
|
+
`implementation` 产物。省略其中任意项仍会执行相应角色并写入产物,只是不再停下来等待审批,而是自动进入下一阶段。`implementation-review` 与 `testing` 是强制安全门禁:即使配置中省略,也始终启用。
|
|
139
|
+
|
|
121
140
|
## 开发命令
|
|
122
141
|
|
|
123
142
|
```bash
|
|
@@ -127,13 +146,13 @@ npm run validate
|
|
|
127
146
|
npm pack --dry-run
|
|
128
147
|
```
|
|
129
148
|
|
|
130
|
-
当前适配器在未安装 Codex SDK 或 Claude SDK 时会使用确定性的本地 fallback 输出。这样可以保证测试和演示稳定,同时保留接入真实宿主 SDK
|
|
149
|
+
当前适配器在未安装 Codex SDK 或 Claude SDK 时会使用确定性的本地 fallback 输出。这样可以保证测试和演示稳定,同时保留接入真实宿主 SDK 的边界。嵌套 SDK 的 apply 运行使用记录在状态中的 DevCrew headless 策略,不会继承当前宿主的 sandbox、审批或工具会话。
|
|
131
150
|
|
|
132
151
|
对于发布安装,宿主 SDK 包会作为精确锁定的 optional dependencies 随 DevCrew 一起安装,因此锁定版本的 `npm exec --package=@shenlee/devcrew@<version>` wrapper 可以从 DevCrew 包自身解析这些 SDK。plan 模式仍允许 deterministic fallback;但 apply 模式在选定宿主 SDK 不可用时会直接失败,并给出明确的 SDK 解析错误。
|
|
133
152
|
|
|
134
153
|
公开 npm 发布由 `npm publish` GitHub Actions 工作流处理。发布 GitHub Release 或手动触发 workflow 时,它会先运行验证,再执行 `npm pack --dry-run` 检查包内容,最后在配置 `NPM_TOKEN` 后使用 npm provenance 发布公开包。
|
|
135
154
|
|
|
136
|
-
|
|
155
|
+
发布插件锁定的 npm 版本后,再运行真实 marketplace smoke test。由于插件锁定了 npm 版本,这是发布后的检查:
|
|
137
156
|
|
|
138
157
|
```bash
|
|
139
158
|
npm run smoke:codex-plugin
|
|
@@ -41,6 +41,25 @@ 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
|
+
}
|
|
44
63
|
export function renderRolePrompt(input) {
|
|
45
64
|
const executionMode = input.executionMode ?? "plan";
|
|
46
65
|
const answers = input.answers ?? [];
|
|
@@ -70,13 +89,16 @@ export function renderRolePrompt(input) {
|
|
|
70
89
|
lines.push("", `### ${name}`, content.trim());
|
|
71
90
|
}
|
|
72
91
|
}
|
|
73
|
-
const canApply =
|
|
92
|
+
const canApply = roleCanApply({ ...input, executionMode });
|
|
74
93
|
const permissionInstruction = canApply
|
|
75
94
|
? input.role === "tester"
|
|
76
95
|
? "You may run validation commands needed for the approved scope and report exact evidence."
|
|
77
96
|
: "You may modify repository files needed for the approved scope and report changed files."
|
|
78
97
|
: "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
|
|
98
|
+
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, "", "Required Sections:", ...roleGuidance(input.role));
|
|
99
|
+
if (input.phase === "review") {
|
|
100
|
+
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.");
|
|
101
|
+
}
|
|
80
102
|
return lines.join("\n");
|
|
81
103
|
}
|
|
82
104
|
function titleForPhase(phase) {
|
|
@@ -84,6 +106,8 @@ function titleForPhase(phase) {
|
|
|
84
106
|
requirements: "Requirements",
|
|
85
107
|
architecture: "Architecture",
|
|
86
108
|
implementation: "Implementation Plan",
|
|
109
|
+
execution: "Implementation Review",
|
|
110
|
+
review: "Architecture Review",
|
|
87
111
|
testing: "Test Report",
|
|
88
112
|
acceptance: "Acceptance",
|
|
89
113
|
complete: "Acceptance",
|
|
@@ -154,24 +178,45 @@ export function extractClaudeResult(message) {
|
|
|
154
178
|
}
|
|
155
179
|
return text;
|
|
156
180
|
}
|
|
181
|
+
function isApplyPhase(input) {
|
|
182
|
+
return input.executionMode === "apply" && (input.phase === "execution" || input.phase === "testing");
|
|
183
|
+
}
|
|
184
|
+
function effectiveExecutionPolicy(input) {
|
|
185
|
+
return input.executionPolicy ?? "interactive-host";
|
|
186
|
+
}
|
|
157
187
|
function roleCanApply(input) {
|
|
158
|
-
return input
|
|
188
|
+
return isApplyPhase(input) && effectiveExecutionPolicy(input) !== "interactive-host";
|
|
159
189
|
}
|
|
160
|
-
function
|
|
161
|
-
|
|
190
|
+
function codexOptionsForRole(input) {
|
|
191
|
+
if (!roleCanApply(input)) {
|
|
192
|
+
return buildCodexThreadOptions(input.cwd);
|
|
193
|
+
}
|
|
194
|
+
const executionPolicy = effectiveExecutionPolicy(input);
|
|
195
|
+
return {
|
|
196
|
+
...buildCodexThreadOptions(input.cwd, "workspace-write"),
|
|
197
|
+
approvalPolicy: executionPolicy === "headless-unattended" ? "never" : "on-request",
|
|
198
|
+
networkAccessEnabled: false,
|
|
199
|
+
};
|
|
162
200
|
}
|
|
163
201
|
function claudeOptionsForRole(input) {
|
|
164
202
|
if (!roleCanApply(input)) {
|
|
165
203
|
return buildClaudeOptions(input.cwd);
|
|
166
204
|
}
|
|
167
|
-
|
|
168
|
-
|
|
205
|
+
if (effectiveExecutionPolicy(input) === "headless-unattended") {
|
|
206
|
+
return {
|
|
207
|
+
cwd: input.cwd,
|
|
208
|
+
permissionMode: "bypassPermissions",
|
|
209
|
+
allowDangerouslySkipPermissions: true,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const allowedTools = input.role === "implementer" ? ["Read", "Grep", "Glob", "Edit", "Write"] : ["Read", "Grep", "Glob"];
|
|
213
|
+
return buildClaudeOptions(input.cwd, "dontAsk", allowedTools);
|
|
169
214
|
}
|
|
170
215
|
async function runWithCodex(input, prompt, loadModule) {
|
|
171
216
|
const mod = await loadModule(HOST_SDK_PACKAGES.codex);
|
|
172
217
|
const CodexClass = mod.Codex;
|
|
173
218
|
const codex = new CodexClass();
|
|
174
|
-
const thread = codex.startThread(
|
|
219
|
+
const thread = codex.startThread(codexOptionsForRole(input));
|
|
175
220
|
const turn = await thread.run(prompt);
|
|
176
221
|
return extractCodexText(turn);
|
|
177
222
|
}
|
|
@@ -199,6 +244,9 @@ function shouldFailOnSdkError(input) {
|
|
|
199
244
|
return input.executionMode === "apply" && input.backend !== "local";
|
|
200
245
|
}
|
|
201
246
|
export async function runRole(input, deps = {}) {
|
|
247
|
+
if (isApplyPhase(input) && effectiveExecutionPolicy(input) === "interactive-host") {
|
|
248
|
+
throw new Error("DevCrew interactive-host execution must be performed by the host, not a nested SDK");
|
|
249
|
+
}
|
|
202
250
|
const loadModule = deps.loadModule ?? importOptional;
|
|
203
251
|
const title = titleForPhase(input.phase);
|
|
204
252
|
const prompt = renderRolePrompt(input);
|
|
@@ -210,12 +258,18 @@ export async function runRole(input, deps = {}) {
|
|
|
210
258
|
? await runWithCodex(input, prompt, loadModule)
|
|
211
259
|
: await runWithClaude(input, prompt, loadModule);
|
|
212
260
|
assertRoleSections(input.role, markdown);
|
|
261
|
+
const reviewDecision = input.phase === "review" ? extractArchitectureReviewDecision(markdown) : undefined;
|
|
262
|
+
if (input.phase === "review" && !reviewDecision) {
|
|
263
|
+
throw new RoleOutputValidationError(input.role, ["Review Decision"]);
|
|
264
|
+
}
|
|
213
265
|
return {
|
|
214
266
|
role: input.role,
|
|
215
267
|
backend: input.backend,
|
|
216
268
|
summary: `${input.role} produced ${title} using the ${input.backend} SDK.`,
|
|
217
269
|
markdown,
|
|
218
270
|
usedFallback: false,
|
|
271
|
+
questions: input.role === "pm" ? extractOpenQuestions(markdown) : undefined,
|
|
272
|
+
reviewDecision,
|
|
219
273
|
};
|
|
220
274
|
}
|
|
221
275
|
catch (error) {
|
|
@@ -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-
|
|
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;
|
|
@@ -8,7 +8,7 @@ export const DEFAULT_CONFIG = {
|
|
|
8
8
|
lintCommands: [],
|
|
9
9
|
coverageCommands: [],
|
|
10
10
|
workflow: {
|
|
11
|
-
gates: ["requirements", "architecture", "implementation", "testing"],
|
|
11
|
+
gates: ["requirements", "architecture", "implementation", "implementation-review", "testing"],
|
|
12
12
|
artifactDirectory: "docs/devcrew",
|
|
13
13
|
},
|
|
14
14
|
};
|
|
@@ -9,6 +9,9 @@ export function runsDir(cwd) {
|
|
|
9
9
|
export function runDir(cwd, runId) {
|
|
10
10
|
return join(runsDir(cwd), runId);
|
|
11
11
|
}
|
|
12
|
+
export function executionWorktreePath(cwd, runId) {
|
|
13
|
+
return join(devcrewDir(cwd), "worktrees", runId);
|
|
14
|
+
}
|
|
12
15
|
export function statePath(cwd, runId) {
|
|
13
16
|
return join(runDir(cwd, runId), "state.json");
|
|
14
17
|
}
|
|
@@ -21,22 +24,23 @@ export function activeRunPath(cwd) {
|
|
|
21
24
|
export function standardsPath(cwd) {
|
|
22
25
|
return join(devcrewDir(cwd), "standards.md");
|
|
23
26
|
}
|
|
24
|
-
export function docsRoot(cwd) {
|
|
25
|
-
return join(cwd,
|
|
27
|
+
export function docsRoot(cwd, artifactDirectory = "docs/devcrew") {
|
|
28
|
+
return join(cwd, artifactDirectory);
|
|
26
29
|
}
|
|
27
|
-
export function docsRunDir(cwd, runId) {
|
|
28
|
-
return join(docsRoot(cwd), runId);
|
|
30
|
+
export function docsRunDir(cwd, runId, artifactDirectory = "docs/devcrew") {
|
|
31
|
+
return join(docsRoot(cwd, artifactDirectory), runId);
|
|
29
32
|
}
|
|
30
|
-
export function artifactPath(cwd, runId, artifact) {
|
|
33
|
+
export function artifactPath(cwd, runId, artifact, artifactDirectory = "docs/devcrew") {
|
|
31
34
|
const filenameByArtifact = {
|
|
32
35
|
requirements: "requirements.md",
|
|
33
36
|
architecture: "architecture.md",
|
|
34
37
|
"implementation-plan": "implementation-plan.md",
|
|
35
38
|
"implementation-review": "implementation-review.md",
|
|
39
|
+
"architecture-review": "architecture-review.md",
|
|
36
40
|
"test-report": "test-report.md",
|
|
37
41
|
acceptance: "acceptance.md",
|
|
38
42
|
};
|
|
39
|
-
return join(docsRunDir(cwd, runId), filenameByArtifact[artifact]);
|
|
43
|
+
return join(docsRunDir(cwd, runId, artifactDirectory), filenameByArtifact[artifact]);
|
|
40
44
|
}
|
|
41
45
|
export async function ensureRunDirectories(cwd, runId) {
|
|
42
46
|
await mkdir(runDir(cwd, runId), { recursive: true });
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { readFile, rename, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { ensureRunDirectories, statePath } from "./paths.js";
|
|
4
|
+
import { GATES } from "./types.js";
|
|
5
|
+
function enabledGatesFromState(value) {
|
|
6
|
+
const configured = Array.isArray(value)
|
|
7
|
+
? value.filter((gate) => typeof gate === "string" && GATES.includes(gate))
|
|
8
|
+
: [...GATES];
|
|
9
|
+
return [...new Set([...configured, "implementation-review", "testing"])];
|
|
10
|
+
}
|
|
4
11
|
export async function saveState(state) {
|
|
5
12
|
state.updatedAt = new Date().toISOString();
|
|
6
13
|
await ensureRunDirectories(state.cwd, state.runId);
|
|
@@ -13,12 +20,41 @@ export async function saveState(state) {
|
|
|
13
20
|
export async function loadState(cwd, runId) {
|
|
14
21
|
const raw = await readFile(statePath(cwd, runId), "utf8");
|
|
15
22
|
const parsed = JSON.parse(raw);
|
|
23
|
+
const executionWorkspace = parsed.executionWorkspace;
|
|
24
|
+
const executionInstruction = parsed.executionInstruction;
|
|
25
|
+
const verificationWaiver = parsed.verificationWaiver;
|
|
16
26
|
return {
|
|
17
27
|
...parsed,
|
|
18
28
|
executionMode: parsed.executionMode ?? "plan",
|
|
29
|
+
executionPolicy: parsed.executionPolicy ?? "interactive-host",
|
|
30
|
+
enabledGates: enabledGatesFromState(parsed.enabledGates),
|
|
31
|
+
artifactDirectory: typeof parsed.artifactDirectory === "string" && parsed.artifactDirectory.trim().length > 0
|
|
32
|
+
? parsed.artifactDirectory
|
|
33
|
+
: "docs/devcrew",
|
|
34
|
+
executionWorkspace: executionWorkspace &&
|
|
35
|
+
typeof executionWorkspace.path === "string" &&
|
|
36
|
+
typeof executionWorkspace.baseCommit === "string"
|
|
37
|
+
? executionWorkspace
|
|
38
|
+
: undefined,
|
|
39
|
+
executionInstruction: executionInstruction &&
|
|
40
|
+
(executionInstruction.phase === "execution" || executionInstruction.phase === "testing") &&
|
|
41
|
+
typeof executionInstruction.workspacePath === "string" &&
|
|
42
|
+
typeof executionInstruction.instructions === "string" &&
|
|
43
|
+
typeof executionInstruction.createdAt === "string"
|
|
44
|
+
? executionInstruction
|
|
45
|
+
: undefined,
|
|
19
46
|
changedFiles: Array.isArray(parsed.changedFiles) ? parsed.changedFiles : [],
|
|
47
|
+
pendingQuestions: Array.isArray(parsed.pendingQuestions)
|
|
48
|
+
? parsed.pendingQuestions.filter((question) => typeof question === "string" && question.trim().length > 0)
|
|
49
|
+
: [],
|
|
20
50
|
implementationDiff: typeof parsed.implementationDiff === "string" ? parsed.implementationDiff : "",
|
|
21
51
|
verification: Array.isArray(parsed.verification) ? parsed.verification : [],
|
|
52
|
+
verificationStatus: parsed.verificationStatus === "passed" || parsed.verificationStatus === "failed" ? parsed.verificationStatus : "not_run",
|
|
53
|
+
verificationWaiver: verificationWaiver &&
|
|
54
|
+
typeof verificationWaiver.reason === "string" &&
|
|
55
|
+
typeof verificationWaiver.createdAt === "string"
|
|
56
|
+
? verificationWaiver
|
|
57
|
+
: undefined,
|
|
22
58
|
lintResults: Array.isArray(parsed.lintResults) ? parsed.lintResults : [],
|
|
23
59
|
};
|
|
24
60
|
}
|
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
export const WORKFLOW_MODES = ["feature", "greenfield"];
|
|
2
2
|
export const EXECUTION_MODES = ["plan", "apply"];
|
|
3
|
+
export const EXECUTION_POLICIES = ["interactive-host", "headless-restricted", "headless-unattended"];
|
|
3
4
|
export const HOSTS = ["codex", "claude"];
|
|
4
5
|
export const BACKENDS = ["codex", "claude", "local"];
|
|
5
6
|
export const PHASES = [
|
|
6
7
|
"requirements",
|
|
7
8
|
"architecture",
|
|
8
9
|
"implementation",
|
|
10
|
+
"execution",
|
|
11
|
+
"review",
|
|
9
12
|
"testing",
|
|
10
13
|
"acceptance",
|
|
11
14
|
"complete",
|
|
12
15
|
];
|
|
13
|
-
export const GATES = ["requirements", "architecture", "implementation", "testing"];
|
|
16
|
+
export const GATES = ["requirements", "architecture", "implementation", "implementation-review", "testing"];
|
|
14
17
|
export const ARTIFACTS = [
|
|
15
18
|
"requirements",
|
|
16
19
|
"architecture",
|
|
17
20
|
"implementation-plan",
|
|
18
21
|
"implementation-review",
|
|
22
|
+
"architecture-review",
|
|
19
23
|
"test-report",
|
|
20
24
|
"acceptance",
|
|
21
25
|
];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ARTIFACTS, BACKENDS, EXECUTION_MODES, GATES, HOSTS, WORKFLOW_MODES, } from "./types.js";
|
|
1
|
+
import { ARTIFACTS, BACKENDS, EXECUTION_MODES, EXECUTION_POLICIES, GATES, HOSTS, WORKFLOW_MODES, } from "./types.js";
|
|
2
2
|
function assertString(value, field) {
|
|
3
3
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
4
4
|
throw new Error(`${field} must be a non-empty string`);
|
|
@@ -17,6 +17,9 @@ export function parseWorkflowMode(value) {
|
|
|
17
17
|
export function parseExecutionMode(value) {
|
|
18
18
|
return oneOf(value, "executionMode", EXECUTION_MODES);
|
|
19
19
|
}
|
|
20
|
+
export function parseExecutionPolicy(value) {
|
|
21
|
+
return oneOf(value, "executionPolicy", EXECUTION_POLICIES);
|
|
22
|
+
}
|
|
20
23
|
export function parseHost(value) {
|
|
21
24
|
return oneOf(value, "host", HOSTS);
|
|
22
25
|
}
|
|
@@ -50,3 +53,6 @@ export function parseFeedback(value) {
|
|
|
50
53
|
export function parseAnswer(value) {
|
|
51
54
|
return assertString(value, "answer");
|
|
52
55
|
}
|
|
56
|
+
export function parseWaiverReason(value) {
|
|
57
|
+
return assertString(value, "reason");
|
|
58
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const DEVCREW_VERSION = "0.1.
|
|
1
|
+
export const DEVCREW_VERSION = "0.1.3";
|
|
2
2
|
export const DEVCREW_NPM_PACKAGE = "@shenlee/devcrew";
|