@shenlee/devcrew 0.1.0

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 (64) hide show
  1. package/CONTRIBUTING.md +21 -0
  2. package/LICENSE +186 -0
  3. package/README.md +142 -0
  4. package/README.zh-CN.md +156 -0
  5. package/SECURITY.md +19 -0
  6. package/dist/packages/adapters/src/index.js +234 -0
  7. package/dist/packages/cli/src/index.js +76 -0
  8. package/dist/packages/core/src/active-run.js +24 -0
  9. package/dist/packages/core/src/artifacts.js +120 -0
  10. package/dist/packages/core/src/config.js +51 -0
  11. package/dist/packages/core/src/index.js +11 -0
  12. package/dist/packages/core/src/paths.js +51 -0
  13. package/dist/packages/core/src/standards.js +61 -0
  14. package/dist/packages/core/src/store.js +24 -0
  15. package/dist/packages/core/src/types.js +48 -0
  16. package/dist/packages/core/src/validation.js +52 -0
  17. package/dist/packages/core/src/verification.js +157 -0
  18. package/dist/packages/core/src/version.js +2 -0
  19. package/dist/packages/core/src/workflow.js +166 -0
  20. package/dist/packages/orchestrator/src/index.js +376 -0
  21. package/dist/packages/plugins/src/index.js +173 -0
  22. package/dist/packages/service/src/index.js +2 -0
  23. package/dist/packages/service/src/stdio.js +100 -0
  24. package/dist/packages/service/src/tools.js +177 -0
  25. package/docs/claude-code.md +37 -0
  26. package/docs/codex.md +80 -0
  27. package/docs/quickstart.md +64 -0
  28. package/docs/roles.md +43 -0
  29. package/docs/workflow.md +70 -0
  30. package/examples/README.md +11 -0
  31. package/examples/feature-request.md +13 -0
  32. package/examples/greenfield-request.md +14 -0
  33. package/package.json +60 -0
  34. package/packages/adapters/src/index.ts +404 -0
  35. package/packages/cli/src/index.ts +88 -0
  36. package/packages/core/src/active-run.ts +31 -0
  37. package/packages/core/src/artifacts.ts +148 -0
  38. package/packages/core/src/config.ts +56 -0
  39. package/packages/core/src/index.ts +11 -0
  40. package/packages/core/src/paths.ts +66 -0
  41. package/packages/core/src/standards.ts +70 -0
  42. package/packages/core/src/store.ts +28 -0
  43. package/packages/core/src/types.ts +182 -0
  44. package/packages/core/src/validation.ts +79 -0
  45. package/packages/core/src/verification.ts +163 -0
  46. package/packages/core/src/version.ts +2 -0
  47. package/packages/core/src/workflow.ts +214 -0
  48. package/packages/orchestrator/src/index.ts +469 -0
  49. package/packages/plugins/assets/composer-icon.png +0 -0
  50. package/packages/plugins/assets/logo.png +0 -0
  51. package/packages/plugins/src/index.ts +211 -0
  52. package/packages/service/src/index.ts +2 -0
  53. package/packages/service/src/stdio.ts +121 -0
  54. package/packages/service/src/tools.ts +215 -0
  55. package/plugins/devcrew-codex/.codex-plugin/plugin.json +41 -0
  56. package/plugins/devcrew-codex/.mcp.json +16 -0
  57. package/plugins/devcrew-codex/agents/architect.toml +6 -0
  58. package/plugins/devcrew-codex/agents/implementer.toml +6 -0
  59. package/plugins/devcrew-codex/agents/pm.toml +6 -0
  60. package/plugins/devcrew-codex/agents/tester.toml +6 -0
  61. package/plugins/devcrew-codex/assets/composer-icon.png +0 -0
  62. package/plugins/devcrew-codex/assets/logo.png +0 -0
  63. package/plugins/devcrew-codex/skills/devcrew/SKILL.md +17 -0
  64. package/scripts/smoke-codex-plugin.mjs +331 -0
@@ -0,0 +1,331 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { basename, join, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const repoRoot = resolve(fileURLToPath(new URL("..", import.meta.url)));
9
+ const defaultMarketplaceSource = "lishen802/devcrew";
10
+ const defaultMarketplaceRef = "main";
11
+ const jsonRpcTimeoutMs = 120_000;
12
+
13
+ function help() {
14
+ return `DevCrew Codex plugin smoke test
15
+
16
+ Installs the DevCrew Codex plugin from a Codex marketplace into an isolated
17
+ CODEX_HOME, starts the plugin MCP server, and runs a full plan-mode workflow.
18
+
19
+ Usage:
20
+ npm run smoke:codex-plugin
21
+ node scripts/smoke-codex-plugin.mjs [--keep-temp] [--source <marketplace>] [--ref <git-ref>]
22
+
23
+ Defaults:
24
+ source: ${defaultMarketplaceSource}
25
+ ref: ${defaultMarketplaceRef}
26
+
27
+ Environment:
28
+ DEVCREW_SMOKE_MARKETPLACE_SOURCE Override marketplace source.
29
+ DEVCREW_SMOKE_MARKETPLACE_REF Override marketplace git ref.
30
+ DEVCREW_SMOKE_CODEX_BIN Override Codex executable.
31
+ `;
32
+ }
33
+
34
+ function parseArgs(argv) {
35
+ const options = {
36
+ keepTemp: false,
37
+ source: process.env.DEVCREW_SMOKE_MARKETPLACE_SOURCE || defaultMarketplaceSource,
38
+ ref: process.env.DEVCREW_SMOKE_MARKETPLACE_REF || defaultMarketplaceRef,
39
+ codexBin: process.env.DEVCREW_SMOKE_CODEX_BIN || "codex",
40
+ };
41
+
42
+ for (let index = 0; index < argv.length; index += 1) {
43
+ const arg = argv[index];
44
+ if (arg === "--help" || arg === "-h") {
45
+ console.log(help());
46
+ process.exit(0);
47
+ }
48
+ if (arg === "--keep-temp") {
49
+ options.keepTemp = true;
50
+ continue;
51
+ }
52
+ if (arg === "--source") {
53
+ options.source = argv[++index];
54
+ continue;
55
+ }
56
+ if (arg === "--ref") {
57
+ options.ref = argv[++index];
58
+ continue;
59
+ }
60
+ throw new Error(`Unknown argument: ${arg}`);
61
+ }
62
+
63
+ if (!options.source) {
64
+ throw new Error("Marketplace source cannot be empty.");
65
+ }
66
+ return options;
67
+ }
68
+
69
+ function log(message) {
70
+ console.log(`[devcrew-smoke] ${message}`);
71
+ }
72
+
73
+ async function run(command, args, options = {}) {
74
+ return new Promise((resolveRun, rejectRun) => {
75
+ const child = spawn(command, args, {
76
+ cwd: options.cwd || repoRoot,
77
+ env: options.env || process.env,
78
+ stdio: ["ignore", "pipe", "pipe"],
79
+ });
80
+ let stdout = "";
81
+ let stderr = "";
82
+ child.stdout.on("data", (chunk) => {
83
+ stdout += chunk.toString("utf8");
84
+ });
85
+ child.stderr.on("data", (chunk) => {
86
+ stderr += chunk.toString("utf8");
87
+ });
88
+ child.on("error", rejectRun);
89
+ child.on("close", (code) => {
90
+ if (code === 0) {
91
+ resolveRun({ stdout, stderr });
92
+ return;
93
+ }
94
+ rejectRun(
95
+ new Error(
96
+ `${command} ${args.join(" ")} exited with ${code}\nSTDOUT:\n${stdout.trim()}\nSTDERR:\n${stderr.trim()}`,
97
+ ),
98
+ );
99
+ });
100
+ });
101
+ }
102
+
103
+ function marketplaceName(source) {
104
+ const normalized = source.replace(/\/$/u, "");
105
+ if (normalized === defaultMarketplaceSource || normalized.endsWith("/lishen802/devcrew")) {
106
+ return "devcrew";
107
+ }
108
+ return basename(normalized).replace(/\.git$/u, "") || "devcrew";
109
+ }
110
+
111
+ async function readInstalledPlugin(codexBin, codexEnv, marketplace) {
112
+ const result = await run(codexBin, ["plugin", "list", "--json", "--available"], { env: codexEnv });
113
+ const parsed = JSON.parse(result.stdout);
114
+ const installed = parsed.installed?.find(
115
+ (entry) => entry.name === "devcrew" && entry.marketplaceName === marketplace && entry.installed === true,
116
+ );
117
+ if (!installed?.source?.path) {
118
+ throw new Error(`Installed devcrew plugin was not found in marketplace ${marketplace}.`);
119
+ }
120
+ return installed;
121
+ }
122
+
123
+ class JsonRpcClient {
124
+ constructor(command, args, env, cwd) {
125
+ this.nextId = 1;
126
+ this.pending = new Map();
127
+ this.buffer = "";
128
+ this.stderr = "";
129
+ this.child = spawn(command, args, {
130
+ cwd,
131
+ env,
132
+ stdio: ["pipe", "pipe", "pipe"],
133
+ });
134
+
135
+ this.child.stdout.on("data", (chunk) => this.handleStdout(chunk.toString("utf8")));
136
+ this.child.stderr.on("data", (chunk) => {
137
+ this.stderr += chunk.toString("utf8");
138
+ });
139
+ this.child.on("error", (error) => this.rejectAll(error));
140
+ this.child.on("close", (code) => {
141
+ if (this.pending.size > 0) {
142
+ this.rejectAll(new Error(`MCP server exited with ${code}\nSTDERR:\n${this.stderr.trim()}`));
143
+ }
144
+ });
145
+ }
146
+
147
+ handleStdout(chunk) {
148
+ this.buffer += chunk;
149
+ const lines = this.buffer.split("\n");
150
+ this.buffer = lines.pop() || "";
151
+ for (const line of lines) {
152
+ const trimmed = line.trim();
153
+ if (!trimmed) {
154
+ continue;
155
+ }
156
+ let message;
157
+ try {
158
+ message = JSON.parse(trimmed);
159
+ } catch {
160
+ continue;
161
+ }
162
+ const pending = this.pending.get(message.id);
163
+ if (!pending) {
164
+ continue;
165
+ }
166
+ clearTimeout(pending.timer);
167
+ this.pending.delete(message.id);
168
+ if (message.error) {
169
+ pending.reject(new Error(JSON.stringify(message.error)));
170
+ } else {
171
+ pending.resolve(message.result);
172
+ }
173
+ }
174
+ }
175
+
176
+ rejectAll(error) {
177
+ for (const [id, pending] of this.pending.entries()) {
178
+ clearTimeout(pending.timer);
179
+ this.pending.delete(id);
180
+ pending.reject(error);
181
+ }
182
+ }
183
+
184
+ request(method, params) {
185
+ const id = this.nextId;
186
+ this.nextId += 1;
187
+ const payload = { jsonrpc: "2.0", id, method, params };
188
+ return new Promise((resolveRequest, rejectRequest) => {
189
+ const timer = setTimeout(() => {
190
+ this.pending.delete(id);
191
+ rejectRequest(new Error(`Timed out waiting for ${method}\nSTDERR:\n${this.stderr.trim()}`));
192
+ }, jsonRpcTimeoutMs);
193
+ this.pending.set(id, { resolve: resolveRequest, reject: rejectRequest, timer });
194
+ this.child.stdin.write(`${JSON.stringify(payload)}\n`);
195
+ });
196
+ }
197
+
198
+ notify(method, params) {
199
+ this.child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}\n`);
200
+ }
201
+
202
+ async close() {
203
+ this.child.stdin.end();
204
+ if (this.child.exitCode === null) {
205
+ this.child.kill("SIGTERM");
206
+ }
207
+ }
208
+ }
209
+
210
+ function assertToolResult(name, result) {
211
+ if (result?.isError) {
212
+ const text = result.content?.map((entry) => entry.text).join("\n") || JSON.stringify(result);
213
+ throw new Error(`${name} returned an MCP error: ${text}`);
214
+ }
215
+ return result;
216
+ }
217
+
218
+ async function callTool(client, name, args) {
219
+ const result = await client.request("tools/call", { name, arguments: args });
220
+ return assertToolResult(name, result);
221
+ }
222
+
223
+ async function runPlanWorkflow(client, cwd) {
224
+ const start = await callTool(client, "devcrew_start", {
225
+ cwd,
226
+ mode: "feature",
227
+ request: "Smoke test DevCrew planning flow for a small README update.",
228
+ });
229
+ const runId = start.structuredContent?.state?.runId;
230
+ if (!runId || !String(runId).startsWith("dc-")) {
231
+ throw new Error(`devcrew_start did not return a dc-* run id: ${JSON.stringify(start)}`);
232
+ }
233
+
234
+ const gates = ["requirements", "architecture", "implementation", "testing"];
235
+ for (const gate of gates) {
236
+ await callTool(client, "devcrew_approve", { cwd, gate, note: `smoke approved ${gate}` });
237
+ await callTool(client, "devcrew_continue", { cwd });
238
+ }
239
+
240
+ const status = await callTool(client, "devcrew_status", { cwd });
241
+ const state = status.structuredContent?.state;
242
+ if (state?.status !== "complete" || state?.phase !== "complete") {
243
+ throw new Error(`Expected complete workflow, got ${JSON.stringify(state)}`);
244
+ }
245
+
246
+ for (const name of ["requirements", "architecture", "implementation-plan", "test-report", "acceptance"]) {
247
+ const artifact = await callTool(client, "devcrew_artifact", { cwd, name });
248
+ const path = artifact.structuredContent?.artifact?.path;
249
+ if (!path) {
250
+ throw new Error(`Artifact ${name} did not include a path.`);
251
+ }
252
+ }
253
+
254
+ return { runId };
255
+ }
256
+
257
+ async function main() {
258
+ const options = parseArgs(process.argv.slice(2));
259
+ const tempRoot = await mkdtemp(join(tmpdir(), "devcrew-codex-smoke-"));
260
+ const codexHome = join(tempRoot, "codex-home");
261
+ const fixture = join(tempRoot, "fixture-repo");
262
+ await mkdir(codexHome, { recursive: true });
263
+ await mkdir(fixture, { recursive: true });
264
+ await writeFile(join(fixture, "README.md"), "# DevCrew Smoke Fixture\n", "utf8");
265
+ await writeFile(join(fixture, "AGENTS.md"), "Use concise Markdown and keep changes scoped.\n", "utf8");
266
+
267
+ const codexEnv = { ...process.env, CODEX_HOME: codexHome };
268
+ const marketplace = marketplaceName(options.source);
269
+
270
+ try {
271
+ log(`using isolated CODEX_HOME=${codexHome}`);
272
+ log(`adding marketplace ${options.source} at ref ${options.ref}`);
273
+ const addArgs = ["plugin", "marketplace", "add", options.source];
274
+ if (options.ref) {
275
+ addArgs.push("--ref", options.ref);
276
+ }
277
+ await run(options.codexBin, addArgs, { env: codexEnv });
278
+
279
+ log(`installing devcrew@${marketplace}`);
280
+ await run(options.codexBin, ["plugin", "add", `devcrew@${marketplace}`], { env: codexEnv });
281
+
282
+ const installed = await readInstalledPlugin(options.codexBin, codexEnv, marketplace);
283
+ log(`installed plugin path: ${installed.source.path}`);
284
+
285
+ const mcpConfigPath = join(installed.source.path, ".mcp.json");
286
+ const mcpConfig = JSON.parse(await readFile(mcpConfigPath, "utf8"));
287
+ const server = mcpConfig.mcpServers?.devcrew;
288
+ if (!server?.command || !Array.isArray(server.args)) {
289
+ throw new Error(`Invalid DevCrew MCP config at ${mcpConfigPath}`);
290
+ }
291
+ log(`starting MCP server: ${server.command} ${server.args.join(" ")}`);
292
+
293
+ const client = new JsonRpcClient(server.command, server.args, {
294
+ ...process.env,
295
+ ...(server.env || {}),
296
+ }, fixture);
297
+ try {
298
+ await client.request("initialize", {
299
+ protocolVersion: "2025-03-26",
300
+ capabilities: {},
301
+ clientInfo: { name: "devcrew-codex-plugin-smoke", version: "0.1.0" },
302
+ });
303
+ client.notify("notifications/initialized", {});
304
+ const tools = await client.request("tools/list", {});
305
+ const toolNames = tools.tools?.map((tool) => tool.name) || [];
306
+ for (const required of ["devcrew_start", "devcrew_continue", "devcrew_artifact"]) {
307
+ if (!toolNames.includes(required)) {
308
+ throw new Error(`MCP tools/list missing ${required}: ${toolNames.join(", ")}`);
309
+ }
310
+ }
311
+
312
+ const { runId } = await runPlanWorkflow(client, fixture);
313
+ log(`plan workflow completed: ${runId}`);
314
+ } finally {
315
+ await client.close();
316
+ }
317
+
318
+ log("Codex plugin marketplace smoke test passed.");
319
+ } finally {
320
+ if (options.keepTemp) {
321
+ log(`kept temp directory: ${tempRoot}`);
322
+ } else {
323
+ await rm(tempRoot, { recursive: true, force: true });
324
+ }
325
+ }
326
+ }
327
+
328
+ main().catch((error) => {
329
+ console.error(`[devcrew-smoke] ${error instanceof Error ? error.message : String(error)}`);
330
+ process.exitCode = 1;
331
+ });