@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
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { access, mkdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
executionWorktreePath,
|
|
7
|
+
runDir,
|
|
8
|
+
type ExecutionWorkspace,
|
|
9
|
+
type RunState,
|
|
10
|
+
} from "../../core/src/index.js";
|
|
11
|
+
|
|
12
|
+
export interface CapturedExecutionChanges {
|
|
13
|
+
changedFiles: string[];
|
|
14
|
+
patch: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const REPOSITORY_PATHS = [
|
|
18
|
+
".",
|
|
19
|
+
":(exclude).devcrew",
|
|
20
|
+
":(exclude).devcrew/**",
|
|
21
|
+
":(exclude)docs/devcrew",
|
|
22
|
+
":(exclude)docs/devcrew/**",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
async function runGit(
|
|
26
|
+
args: string[],
|
|
27
|
+
cwd: string,
|
|
28
|
+
stdin?: string,
|
|
29
|
+
env?: NodeJS.ProcessEnv,
|
|
30
|
+
): Promise<string> {
|
|
31
|
+
return new Promise((resolveResult, rejectResult) => {
|
|
32
|
+
const child = spawn("git", args, { cwd, env, stdio: ["pipe", "pipe", "pipe"] });
|
|
33
|
+
let stdout = "";
|
|
34
|
+
let stderr = "";
|
|
35
|
+
let settled = false;
|
|
36
|
+
|
|
37
|
+
const rejectOnce = (error: Error): void => {
|
|
38
|
+
if (settled) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
settled = true;
|
|
42
|
+
rejectResult(error);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
child.stdout.on("data", (chunk: Buffer) => {
|
|
46
|
+
stdout += chunk.toString("utf8");
|
|
47
|
+
});
|
|
48
|
+
child.stderr.on("data", (chunk: Buffer) => {
|
|
49
|
+
stderr += chunk.toString("utf8");
|
|
50
|
+
});
|
|
51
|
+
child.on("error", rejectOnce);
|
|
52
|
+
child.on("close", (code) => {
|
|
53
|
+
if (settled) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
settled = true;
|
|
57
|
+
if (code === 0) {
|
|
58
|
+
resolveResult(stdout);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
rejectResult(new Error(`git ${args[0]} failed: ${(stderr || stdout).trim()}`));
|
|
62
|
+
});
|
|
63
|
+
child.stdin.end(stdin);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function pathExists(path: string): Promise<boolean> {
|
|
68
|
+
try {
|
|
69
|
+
await access(path);
|
|
70
|
+
return true;
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function assertCleanRequester(cwd: string): Promise<void> {
|
|
77
|
+
const status = await runGit(
|
|
78
|
+
[
|
|
79
|
+
"status",
|
|
80
|
+
"--porcelain",
|
|
81
|
+
"-uall",
|
|
82
|
+
"--",
|
|
83
|
+
...REPOSITORY_PATHS,
|
|
84
|
+
],
|
|
85
|
+
cwd,
|
|
86
|
+
);
|
|
87
|
+
if (status.trim()) {
|
|
88
|
+
throw new Error("DevCrew apply promotion requires a clean working tree");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function nulPaths(output: string): string[] {
|
|
93
|
+
return output.split("\0").filter(Boolean);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function parseChangedFiles(output: string): string[] {
|
|
97
|
+
const fields = nulPaths(output);
|
|
98
|
+
const paths: string[] = [];
|
|
99
|
+
for (let index = 0; index < fields.length; ) {
|
|
100
|
+
const status = fields[index++];
|
|
101
|
+
const firstPath = fields[index++];
|
|
102
|
+
if (!status || !firstPath) {
|
|
103
|
+
throw new Error("DevCrew could not parse git diff name status output");
|
|
104
|
+
}
|
|
105
|
+
paths.push(firstPath);
|
|
106
|
+
if (status.startsWith("R") || status.startsWith("C")) {
|
|
107
|
+
const secondPath = fields[index++];
|
|
108
|
+
if (!secondPath) {
|
|
109
|
+
throw new Error("DevCrew could not parse renamed git diff path");
|
|
110
|
+
}
|
|
111
|
+
paths.push(secondPath);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return [...new Set(paths)];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function markUntrackedIntentToAdd(cwd: string, env?: NodeJS.ProcessEnv): Promise<void> {
|
|
118
|
+
const untracked = nulPaths(
|
|
119
|
+
await runGit(
|
|
120
|
+
["ls-files", "--others", "--exclude-standard", "-z", "--", ...REPOSITORY_PATHS],
|
|
121
|
+
cwd,
|
|
122
|
+
undefined,
|
|
123
|
+
env,
|
|
124
|
+
),
|
|
125
|
+
);
|
|
126
|
+
if (untracked.length > 0) {
|
|
127
|
+
await runGit(["add", "-N", "--", ...untracked], cwd, undefined, env);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
interface CapturePatchOptions {
|
|
132
|
+
allowEmpty?: boolean;
|
|
133
|
+
env?: NodeJS.ProcessEnv;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function capturePatch(
|
|
137
|
+
workspace: ExecutionWorkspace,
|
|
138
|
+
options: CapturePatchOptions = {},
|
|
139
|
+
): Promise<CapturedExecutionChanges> {
|
|
140
|
+
await markUntrackedIntentToAdd(workspace.path, options.env);
|
|
141
|
+
const changedFiles = parseChangedFiles(
|
|
142
|
+
await runGit(
|
|
143
|
+
["diff", "--name-status", "-z", workspace.baseCommit, "--", ...REPOSITORY_PATHS],
|
|
144
|
+
workspace.path,
|
|
145
|
+
undefined,
|
|
146
|
+
options.env,
|
|
147
|
+
),
|
|
148
|
+
);
|
|
149
|
+
const patch = await runGit(
|
|
150
|
+
["diff", "--binary", "--no-ext-diff", workspace.baseCommit, "--", ...REPOSITORY_PATHS],
|
|
151
|
+
workspace.path,
|
|
152
|
+
undefined,
|
|
153
|
+
options.env,
|
|
154
|
+
);
|
|
155
|
+
if (!options.allowEmpty && !patch.trim()) {
|
|
156
|
+
throw new Error("DevCrew apply implementer produced no repository changes");
|
|
157
|
+
}
|
|
158
|
+
return { changedFiles, patch };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function captureRequesterChanges(state: RunState, workspace: ExecutionWorkspace): Promise<CapturedExecutionChanges> {
|
|
162
|
+
const indexPath = join(runDir(state.cwd, state.runId), "promotion.index");
|
|
163
|
+
await mkdir(dirname(indexPath), { recursive: true });
|
|
164
|
+
await rm(indexPath, { force: true });
|
|
165
|
+
const env = { ...process.env, GIT_INDEX_FILE: indexPath };
|
|
166
|
+
try {
|
|
167
|
+
await runGit(["read-tree", workspace.baseCommit], state.cwd, undefined, env);
|
|
168
|
+
return await capturePatch(
|
|
169
|
+
{ path: state.cwd, baseCommit: workspace.baseCommit },
|
|
170
|
+
{ allowEmpty: true, env },
|
|
171
|
+
);
|
|
172
|
+
} finally {
|
|
173
|
+
await rm(indexPath, { force: true });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function ensureExecutionWorkspace(state: RunState): Promise<ExecutionWorkspace> {
|
|
178
|
+
if (state.executionWorkspace && (await pathExists(state.executionWorkspace.path))) {
|
|
179
|
+
return state.executionWorkspace;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
await assertCleanRequester(state.cwd);
|
|
183
|
+
const baseCommit = (await runGit(["rev-parse", "HEAD"], state.cwd)).trim();
|
|
184
|
+
const path = executionWorktreePath(state.cwd, state.runId);
|
|
185
|
+
await mkdir(dirname(path), { recursive: true });
|
|
186
|
+
await runGit(["worktree", "add", "--detach", path, baseCommit], state.cwd);
|
|
187
|
+
return { path, baseCommit };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function captureExecutionChanges(
|
|
191
|
+
workspace: ExecutionWorkspace,
|
|
192
|
+
): Promise<CapturedExecutionChanges> {
|
|
193
|
+
const head = (await runGit(["rev-parse", "HEAD"], workspace.path)).trim();
|
|
194
|
+
if (head !== workspace.baseCommit) {
|
|
195
|
+
await runGit(["reset", "--mixed", workspace.baseCommit], workspace.path);
|
|
196
|
+
}
|
|
197
|
+
return capturePatch(workspace);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function promoteExecutionChanges(state: RunState): Promise<void> {
|
|
201
|
+
const workspace = state.executionWorkspace;
|
|
202
|
+
if (!workspace || !state.implementationDiff.trim()) {
|
|
203
|
+
throw new Error("DevCrew apply promotion requires reviewed execution changes");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const requesterHead = (await runGit(["rev-parse", "HEAD"], state.cwd)).trim();
|
|
207
|
+
if (requesterHead !== workspace.baseCommit) {
|
|
208
|
+
throw new Error("DevCrew apply promotion refused because requester HEAD changed");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const patchPath = join(runDir(state.cwd, state.runId), "implementation.patch");
|
|
212
|
+
await mkdir(dirname(patchPath), { recursive: true });
|
|
213
|
+
await writeFile(patchPath, state.implementationDiff);
|
|
214
|
+
|
|
215
|
+
const current = await captureRequesterChanges(state, workspace);
|
|
216
|
+
if (current.patch === state.implementationDiff) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (current.patch.trim()) {
|
|
220
|
+
throw new Error("DevCrew apply promotion requires a clean working tree");
|
|
221
|
+
}
|
|
222
|
+
await assertCleanRequester(state.cwd);
|
|
223
|
+
|
|
224
|
+
await runGit(["apply", "--check", "--binary", patchPath], state.cwd);
|
|
225
|
+
await runGit(["apply", "--binary", patchPath], state.cwd);
|
|
226
|
+
|
|
227
|
+
const promoted = await captureRequesterChanges(state, workspace);
|
|
228
|
+
if (promoted.patch !== state.implementationDiff) {
|
|
229
|
+
try {
|
|
230
|
+
await runGit(["apply", "--reverse", "--binary", patchPath], state.cwd);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
const detail = error instanceof Error ? `: ${error.message}` : "";
|
|
233
|
+
throw new Error(`DevCrew promoted patch verification failed and rollback failed${detail}`);
|
|
234
|
+
}
|
|
235
|
+
throw new Error("DevCrew promoted patch differs from the reviewed implementation diff");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export async function rollbackPromotedExecutionChanges(state: RunState): Promise<void> {
|
|
240
|
+
const workspace = state.executionWorkspace;
|
|
241
|
+
if (!workspace || !state.implementationDiff.trim()) {
|
|
242
|
+
throw new Error("DevCrew apply rollback requires reviewed execution changes");
|
|
243
|
+
}
|
|
244
|
+
const patchPath = join(runDir(state.cwd, state.runId), "implementation.patch");
|
|
245
|
+
await runGit(["apply", "--reverse", "--check", "--binary", patchPath], state.cwd);
|
|
246
|
+
await runGit(["apply", "--reverse", "--binary", patchPath], state.cwd);
|
|
247
|
+
const remaining = await captureRequesterChanges(state, workspace);
|
|
248
|
+
if (remaining.patch.trim()) {
|
|
249
|
+
throw new Error("DevCrew apply rollback left requester repository changes");
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export async function cleanupExecutionWorkspace(state: RunState): Promise<void> {
|
|
254
|
+
const workspace = state.executionWorkspace;
|
|
255
|
+
if (!workspace) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (await pathExists(workspace.path)) {
|
|
259
|
+
await runGit(["worktree", "remove", "--force", workspace.path], state.cwd);
|
|
260
|
+
}
|
|
261
|
+
await runGit(["worktree", "prune"], state.cwd);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export async function executionCwd(state: RunState): Promise<string> {
|
|
265
|
+
if (state.executionMode !== "apply") {
|
|
266
|
+
return state.cwd;
|
|
267
|
+
}
|
|
268
|
+
return (await ensureExecutionWorkspace(state)).path;
|
|
269
|
+
}
|
|
@@ -3,7 +3,7 @@ import { access, copyFile, mkdir, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
|
|
6
|
-
import { DEFAULT_CONFIG, DEVCREW_NPM_PACKAGE, DEVCREW_VERSION
|
|
6
|
+
import { DEFAULT_CONFIG, DEVCREW_NPM_PACKAGE, DEVCREW_VERSION } from "../../core/src/index.js";
|
|
7
7
|
|
|
8
8
|
export interface GeneratedPlugin {
|
|
9
9
|
name: "devcrew";
|
|
@@ -45,48 +45,8 @@ async function writeCodexAssets(pluginRoot: string): Promise<void> {
|
|
|
45
45
|
await copyFile(await bundledAssetPath("composer-icon.png"), join(assetDir, "composer-icon.png"));
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
function roleExpectations(name: string): string {
|
|
49
|
-
const sections = ROLE_SECTIONS[name as keyof typeof ROLE_SECTIONS];
|
|
50
|
-
if (!sections || sections.length === 0) {
|
|
51
|
-
return "";
|
|
52
|
-
}
|
|
53
|
-
return sections.map((s) => `- ${s.heading} (${s.description})`).join("\n");
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async function writeRoleAgents(root: string, format: "codex" | "claude"): Promise<void> {
|
|
57
|
-
const roles = [
|
|
58
|
-
["pm", "Product manager. Clarifies requirements, scope boundaries, success criteria, and requester approvals."],
|
|
59
|
-
["architect", "technical architecture specialist. Designs implementation, deployment, interfaces, and review criteria."],
|
|
60
|
-
["implementer", "Implementation engineer. Writes code according to approved architecture and discovered standards."],
|
|
61
|
-
["tester", "Testing and acceptance specialist. Verifies functionality, regressions, and acceptance evidence."],
|
|
62
|
-
] as const;
|
|
63
|
-
|
|
64
|
-
if (format === "claude") {
|
|
65
|
-
const agentDir = join(root, "agents");
|
|
66
|
-
await mkdir(agentDir, { recursive: true });
|
|
67
|
-
for (const [name, description] of roles) {
|
|
68
|
-
await writeFile(
|
|
69
|
-
join(agentDir, `${name}.md`),
|
|
70
|
-
`---\nname: ${name}\ndescription: ${description}\ntools: Read, Grep, Glob, Bash\n---\n\nYou are the DevCrew ${name} role. ${description} Return concise Markdown and keep inherited host permissions.\n\nProduce these required sections:\n\n${roleExpectations(name)}\n`,
|
|
71
|
-
"utf8",
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const agentDir = join(root, "agents");
|
|
78
|
-
await mkdir(agentDir, { recursive: true });
|
|
79
|
-
for (const [name, description] of roles) {
|
|
80
|
-
await writeFile(
|
|
81
|
-
join(agentDir, `${name}.toml`),
|
|
82
|
-
`name = "${name}"\ndescription = "${description}"\ndeveloper_instructions = """\nYou are the DevCrew ${name} role. ${description}\nReturn concise Markdown and keep inherited host permissions.\n\nProduce these required sections:\n${roleExpectations(name)}\n"""\n`,
|
|
83
|
-
"utf8",
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
48
|
function entrySkill(): string {
|
|
89
|
-
return `---\nname: devcrew\ndescription: Run the DevCrew PM -> architecture -> implementation -> testing workflow. Use when the user asks for structured feature or product development, requirements clarification, architecture review, implementation planning, testing acceptance, or Chinese requests such as 完整研发流程, 需求澄清, 产品经理, 架构师, 开发测试流程.\n---\n\nUse the DevCrew MCP tools to manage the workflow:\n\n1. Start with \`devcrew_start\` using the current repository cwd, mode, request, and optional executionMode
|
|
49
|
+
return `---\nname: devcrew\ndescription: Run the DevCrew PM -> architecture -> implementation -> testing workflow. Use when the user asks for structured feature or product development, requirements clarification, architecture review, implementation planning, testing acceptance, or Chinese requests such as 完整研发流程, 需求澄清, 产品经理, 架构师, 开发测试流程.\n---\n\nUse the DevCrew MCP tools to manage the workflow:\n\n1. Start with \`devcrew_start\` using the current repository cwd, mode, request, and optional \`executionMode\`. Host is inferred from the plugin's \`DEVCREW_HOST\`; pass host only for an explicit override. Omit \`executionMode\` unless the requester explicitly asks DevCrew to apply changes; the default safe mode is \`plan\`.\n2. After start, DevCrew records the active run for this repository. For follow-up tools, omit \`runId\` unless you need to target a different run explicitly.\n3. For \`executionMode: "apply"\`, choose an explicit \`executionPolicy\`. The default \`interactive-host\` pauses at implementation and testing for the host's native agent to work in DevCrew's isolated worktree. \`headless-restricted\` and \`headless-unattended\` are DevCrew SDK policies; they do not inherit the current host approval session.\n4. Use \`devcrew_status\` to show the current phase, pending gate, and any execution instruction.\n5. Use \`devcrew_answer\` when the requester gives clarification.\n6. Use \`devcrew_approve\` or \`devcrew_reject\` for each gate.\n7. Use \`devcrew_continue\` after approvals. Apply runs enter an \`implementation-review\` gate after execution: review the architect's \`architecture-review\` artifact before testing. For \`interactive-host\`, if status becomes \`awaiting_execution\`, perform the native-host work in the indicated worktree then call \`devcrew_complete_execution\`. For testing, include command, exit code, output, startedAt, and completedAt evidence.\n8. Failed verification is not approvable. Revise through \`devcrew_answer\`, or use \`devcrew_waive_verification\` only when the requester explicitly accepts the recorded risk and provides a reason.\n9. Use \`devcrew_artifact\` to read generated requirements, architecture, implementation-plan, implementation-review, architecture-review, test-report, or acceptance files.\n\nNever describe a nested SDK session as inheriting the current host's approval decisions.\n`;
|
|
90
50
|
}
|
|
91
51
|
|
|
92
52
|
function npmPackageSpecifier(): string {
|
|
@@ -169,7 +129,6 @@ export async function generateCodexPlugin(root: string): Promise<GeneratedPlugin
|
|
|
169
129
|
devcrew: mcpServerConfig("codex"),
|
|
170
130
|
},
|
|
171
131
|
});
|
|
172
|
-
await writeRoleAgents(pluginRoot, "codex");
|
|
173
132
|
return { name: "devcrew", path: pluginRoot };
|
|
174
133
|
}
|
|
175
134
|
|
|
@@ -215,7 +174,6 @@ export async function generateClaudePlugin(root: string): Promise<GeneratedPlugi
|
|
|
215
174
|
devcrew: mcpServerConfig("claude"),
|
|
216
175
|
},
|
|
217
176
|
});
|
|
218
|
-
await writeRoleAgents(pluginRoot, "claude");
|
|
219
177
|
return { name: "devcrew", path: pluginRoot };
|
|
220
178
|
}
|
|
221
179
|
|
|
@@ -2,14 +2,25 @@ import { callDevCrewTool, listDevCrewTools } from "./tools.js";
|
|
|
2
2
|
import { DEVCREW_VERSION } from "../../core/src/index.js";
|
|
3
3
|
|
|
4
4
|
interface JsonRpcRequest {
|
|
5
|
+
jsonrpc: "2.0";
|
|
5
6
|
id?: string | number | null;
|
|
6
|
-
method
|
|
7
|
+
method: string;
|
|
7
8
|
params?: Record<string, unknown>;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
type JsonWriter = (message: unknown) => void;
|
|
11
12
|
type RequestHandler = (request: JsonRpcRequest) => Promise<void>;
|
|
12
13
|
|
|
14
|
+
function isJsonRpcRequest(value: unknown): value is JsonRpcRequest {
|
|
15
|
+
return (
|
|
16
|
+
typeof value === "object" &&
|
|
17
|
+
value !== null &&
|
|
18
|
+
!Array.isArray(value) &&
|
|
19
|
+
(value as { jsonrpc?: unknown }).jsonrpc === "2.0" &&
|
|
20
|
+
typeof (value as { method?: unknown }).method === "string"
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
13
24
|
function writeJson(message: unknown): void {
|
|
14
25
|
process.stdout.write(`${JSON.stringify(message)}\n`);
|
|
15
26
|
}
|
|
@@ -61,7 +72,7 @@ async function handleRequest(request: JsonRpcRequest, write: JsonWriter): Promis
|
|
|
61
72
|
return;
|
|
62
73
|
}
|
|
63
74
|
|
|
64
|
-
throw new Error(`Unsupported JSON-RPC method: ${request.method
|
|
75
|
+
throw new Error(`Unsupported JSON-RPC method: ${request.method}`);
|
|
65
76
|
} catch (error) {
|
|
66
77
|
write({
|
|
67
78
|
jsonrpc: "2.0",
|
|
@@ -85,9 +96,9 @@ export function createStdioLineProcessor(
|
|
|
85
96
|
return queue;
|
|
86
97
|
}
|
|
87
98
|
|
|
88
|
-
let request:
|
|
99
|
+
let request: unknown;
|
|
89
100
|
try {
|
|
90
|
-
request = JSON.parse(trimmed) as
|
|
101
|
+
request = JSON.parse(trimmed) as unknown;
|
|
91
102
|
} catch {
|
|
92
103
|
write({
|
|
93
104
|
jsonrpc: "2.0",
|
|
@@ -97,8 +108,18 @@ export function createStdioLineProcessor(
|
|
|
97
108
|
return queue;
|
|
98
109
|
}
|
|
99
110
|
|
|
100
|
-
|
|
101
|
-
|
|
111
|
+
if (!isJsonRpcRequest(request)) {
|
|
112
|
+
write({
|
|
113
|
+
jsonrpc: "2.0",
|
|
114
|
+
id: null,
|
|
115
|
+
error: { code: -32600, message: "Invalid Request" },
|
|
116
|
+
});
|
|
117
|
+
return queue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const task = queue.then(() => handler(request));
|
|
121
|
+
queue = task.catch(() => undefined);
|
|
122
|
+
return task;
|
|
102
123
|
};
|
|
103
124
|
}
|
|
104
125
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {
|
|
2
|
-
approveWorkflow,
|
|
3
2
|
getArtifact,
|
|
4
3
|
getActiveRunId,
|
|
5
4
|
getWorkflowStatus,
|
|
@@ -9,9 +8,12 @@ import {
|
|
|
9
8
|
} from "../../core/src/index.js";
|
|
10
9
|
import {
|
|
11
10
|
answerOrchestratedWorkflow,
|
|
11
|
+
approveOrchestratedWorkflow,
|
|
12
|
+
completeOrchestratedExecution,
|
|
12
13
|
continueOrchestratedWorkflow,
|
|
13
14
|
rejectOrchestratedWorkflow,
|
|
14
15
|
startOrchestratedWorkflow,
|
|
16
|
+
waiveOrchestratedVerification,
|
|
15
17
|
} from "../../orchestrator/src/index.js";
|
|
16
18
|
|
|
17
19
|
export interface DevCrewTool {
|
|
@@ -73,6 +75,11 @@ export function listDevCrewTools(): DevCrewTool[] {
|
|
|
73
75
|
enum: ["plan", "apply"],
|
|
74
76
|
description: "Execution mode. Defaults to plan; apply must be explicit.",
|
|
75
77
|
},
|
|
78
|
+
executionPolicy: {
|
|
79
|
+
type: "string",
|
|
80
|
+
enum: ["interactive-host", "headless-restricted", "headless-unattended"],
|
|
81
|
+
description: "Apply execution policy. Defaults to interactive-host; headless policies are explicit DevCrew SDK policies.",
|
|
82
|
+
},
|
|
76
83
|
request: { type: "string" },
|
|
77
84
|
backend: { type: "string", enum: ["codex", "claude", "local"] },
|
|
78
85
|
},
|
|
@@ -105,7 +112,7 @@ export function listDevCrewTools(): DevCrewTool[] {
|
|
|
105
112
|
properties: {
|
|
106
113
|
cwd: cwdProperty,
|
|
107
114
|
runId: runIdProperty,
|
|
108
|
-
gate: { type: "string", enum: ["requirements", "architecture", "implementation", "testing"] },
|
|
115
|
+
gate: { type: "string", enum: ["requirements", "architecture", "implementation", "implementation-review", "testing"] },
|
|
109
116
|
note: { type: "string" },
|
|
110
117
|
},
|
|
111
118
|
},
|
|
@@ -119,7 +126,7 @@ export function listDevCrewTools(): DevCrewTool[] {
|
|
|
119
126
|
properties: {
|
|
120
127
|
cwd: cwdProperty,
|
|
121
128
|
runId: runIdProperty,
|
|
122
|
-
gate: { type: "string", enum: ["requirements", "architecture", "implementation", "testing"] },
|
|
129
|
+
gate: { type: "string", enum: ["requirements", "architecture", "implementation", "implementation-review", "testing"] },
|
|
123
130
|
feedback: { type: "string" },
|
|
124
131
|
},
|
|
125
132
|
},
|
|
@@ -133,6 +140,32 @@ export function listDevCrewTools(): DevCrewTool[] {
|
|
|
133
140
|
properties: { cwd: cwdProperty, runId: runIdProperty },
|
|
134
141
|
},
|
|
135
142
|
},
|
|
143
|
+
{
|
|
144
|
+
name: "devcrew_complete_execution",
|
|
145
|
+
description: "Record completion by the native host for an interactive-host execution or testing step.",
|
|
146
|
+
inputSchema: {
|
|
147
|
+
type: "object",
|
|
148
|
+
required: ["cwd", "summary"],
|
|
149
|
+
properties: {
|
|
150
|
+
cwd: cwdProperty,
|
|
151
|
+
runId: runIdProperty,
|
|
152
|
+
summary: { type: "string" },
|
|
153
|
+
verification: {
|
|
154
|
+
type: "array",
|
|
155
|
+
description: "Required only when completing testing: command, exitCode, output, startedAt, and completedAt for each validation command.",
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: "devcrew_waive_verification",
|
|
162
|
+
description: "Record a reasoned risk waiver after failed apply-mode verification, then reopen the testing gate for approval.",
|
|
163
|
+
inputSchema: {
|
|
164
|
+
type: "object",
|
|
165
|
+
required: ["cwd", "reason"],
|
|
166
|
+
properties: { cwd: cwdProperty, runId: runIdProperty, reason: { type: "string" } },
|
|
167
|
+
},
|
|
168
|
+
},
|
|
136
169
|
{
|
|
137
170
|
name: "devcrew_artifact",
|
|
138
171
|
description: "Read a generated workflow artifact.",
|
|
@@ -144,7 +177,7 @@ export function listDevCrewTools(): DevCrewTool[] {
|
|
|
144
177
|
runId: runIdProperty,
|
|
145
178
|
name: {
|
|
146
179
|
type: "string",
|
|
147
|
-
enum: ["requirements", "architecture", "implementation-plan", "implementation-review", "test-report", "acceptance"],
|
|
180
|
+
enum: ["requirements", "architecture", "implementation-plan", "implementation-review", "architecture-review", "test-report", "acceptance"],
|
|
148
181
|
},
|
|
149
182
|
},
|
|
150
183
|
},
|
|
@@ -193,7 +226,7 @@ export async function callDevCrewTool(name: string, args: Record<string, unknown
|
|
|
193
226
|
return success(`${summarizeState(state)}. Answer recorded.`, { state });
|
|
194
227
|
}
|
|
195
228
|
if (name === "devcrew_approve") {
|
|
196
|
-
const state = await
|
|
229
|
+
const state = await approveOrchestratedWorkflow((await withActiveRun(args)) as never);
|
|
197
230
|
return success(`${summarizeState(state)}. Gate approved.`, { state });
|
|
198
231
|
}
|
|
199
232
|
if (name === "devcrew_reject") {
|
|
@@ -204,6 +237,14 @@ export async function callDevCrewTool(name: string, args: Record<string, unknown
|
|
|
204
237
|
const state = await continueOrchestratedWorkflow((await withActiveRun(args)) as never);
|
|
205
238
|
return success(`${summarizeState(state)}.`, { state });
|
|
206
239
|
}
|
|
240
|
+
if (name === "devcrew_complete_execution") {
|
|
241
|
+
const state = await completeOrchestratedExecution((await withActiveRun(args)) as never);
|
|
242
|
+
return success(`${summarizeState(state)}. Native host completion recorded.`, { state });
|
|
243
|
+
}
|
|
244
|
+
if (name === "devcrew_waive_verification") {
|
|
245
|
+
const state = await waiveOrchestratedVerification((await withActiveRun(args)) as never);
|
|
246
|
+
return success(`${summarizeState(state)}. Verification waiver recorded.`, { state });
|
|
247
|
+
}
|
|
207
248
|
if (name === "devcrew_artifact") {
|
|
208
249
|
const artifact = await getArtifact((await withActiveRun(args)) as never);
|
|
209
250
|
return success(artifact.content, { artifact });
|
|
Binary file
|
|
@@ -5,13 +5,14 @@ description: Run the DevCrew PM -> architecture -> implementation -> testing wor
|
|
|
5
5
|
|
|
6
6
|
Use the DevCrew MCP tools to manage the workflow:
|
|
7
7
|
|
|
8
|
-
1. Start with `devcrew_start` using the current repository cwd, mode, request, and optional executionMode
|
|
9
|
-
2. After start, DevCrew records the active run for this repository. For follow-up tools, omit runId unless you need to target a different run explicitly.
|
|
10
|
-
3.
|
|
11
|
-
4. Use `devcrew_status` to show the current phase and
|
|
8
|
+
1. Start with `devcrew_start` using the current repository cwd, mode, request, and optional `executionMode`. Host is inferred from the plugin's `DEVCREW_HOST`; pass host only for an explicit override. Omit `executionMode` unless the requester explicitly asks DevCrew to apply changes; the default safe mode is `plan`.
|
|
9
|
+
2. After start, DevCrew records the active run for this repository. For follow-up tools, omit `runId` unless you need to target a different run explicitly.
|
|
10
|
+
3. For `executionMode: "apply"`, choose an explicit `executionPolicy`. The default `interactive-host` pauses at implementation and testing for the host's native agent to work in DevCrew's isolated worktree. `headless-restricted` and `headless-unattended` are DevCrew SDK policies; they do not inherit the current host approval session.
|
|
11
|
+
4. Use `devcrew_status` to show the current phase, pending gate, and any execution instruction.
|
|
12
12
|
5. Use `devcrew_answer` when the requester gives clarification.
|
|
13
13
|
6. Use `devcrew_approve` or `devcrew_reject` for each gate.
|
|
14
|
-
7. Use `devcrew_continue` after approvals.
|
|
15
|
-
8.
|
|
14
|
+
7. Use `devcrew_continue` after approvals. Apply runs enter an `implementation-review` gate after execution: review the architect's `architecture-review` artifact before testing. For `interactive-host`, if status becomes `awaiting_execution`, perform the native-host work in the indicated worktree then call `devcrew_complete_execution`. For testing, include command, exit code, output, startedAt, and completedAt evidence.
|
|
15
|
+
8. Failed verification is not approvable. Revise through `devcrew_answer`, or use `devcrew_waive_verification` only when the requester explicitly accepts the recorded risk and provides a reason.
|
|
16
|
+
9. Use `devcrew_artifact` to read generated requirements, architecture, implementation-plan, implementation-review, architecture-review, test-report, or acceptance files.
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
Never describe a nested SDK session as inheriting the current host's approval decisions.
|
|
@@ -298,7 +298,7 @@ async function main() {
|
|
|
298
298
|
await client.request("initialize", {
|
|
299
299
|
protocolVersion: "2025-03-26",
|
|
300
300
|
capabilities: {},
|
|
301
|
-
clientInfo: { name: "devcrew-codex-plugin-smoke", version: "0.1.
|
|
301
|
+
clientInfo: { name: "devcrew-codex-plugin-smoke", version: "0.1.3" },
|
|
302
302
|
});
|
|
303
303
|
client.notify("notifications/initialized", {});
|
|
304
304
|
const tools = await client.request("tools/list", {});
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
name = "architect"
|
|
2
|
-
description = "technical architecture specialist. Designs implementation, deployment, interfaces, and review criteria."
|
|
3
|
-
developer_instructions = """
|
|
4
|
-
You are the DevCrew architect role. technical architecture specialist. Designs implementation, deployment, interfaces, and review criteria.
|
|
5
|
-
Return concise Markdown and keep inherited host permissions.
|
|
6
|
-
"""
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
name = "implementer"
|
|
2
|
-
description = "Implementation engineer. Writes code according to approved architecture and discovered standards."
|
|
3
|
-
developer_instructions = """
|
|
4
|
-
You are the DevCrew implementer role. Implementation engineer. Writes code according to approved architecture and discovered standards.
|
|
5
|
-
Return concise Markdown and keep inherited host permissions.
|
|
6
|
-
"""
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
name = "pm"
|
|
2
|
-
description = "Product manager. Clarifies requirements, scope boundaries, success criteria, and requester approvals."
|
|
3
|
-
developer_instructions = """
|
|
4
|
-
You are the DevCrew pm role. Product manager. Clarifies requirements, scope boundaries, success criteria, and requester approvals.
|
|
5
|
-
Return concise Markdown and keep inherited host permissions.
|
|
6
|
-
"""
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
name = "tester"
|
|
2
|
-
description = "Testing and acceptance specialist. Verifies functionality, regressions, and acceptance evidence."
|
|
3
|
-
developer_instructions = """
|
|
4
|
-
You are the DevCrew tester role. Testing and acceptance specialist. Verifies functionality, regressions, and acceptance evidence.
|
|
5
|
-
Return concise Markdown and keep inherited host permissions.
|
|
6
|
-
"""
|