@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,234 @@
1
+ import { DEVCREW_NPM_PACKAGE, ROLE_SECTIONS } from "../../core/src/index.js";
2
+ export function resolveBackendName(input) {
3
+ if (input.configuredBackend && input.configuredBackend !== "host-preferred") {
4
+ return input.configuredBackend;
5
+ }
6
+ return input.host;
7
+ }
8
+ // Prompt-formatted role sections derived from the shared ROLE_SECTIONS constant.
9
+ export function roleGuidance(role) {
10
+ const sections = ROLE_SECTIONS[role];
11
+ if (!sections || sections.length === 0) {
12
+ return [];
13
+ }
14
+ const lines = ["Produce these exact H2 sections:"];
15
+ for (const section of sections) {
16
+ lines.push(`## ${section.heading}`, `Guidance: ${section.description}.`);
17
+ }
18
+ return lines;
19
+ }
20
+ function escapeRegExp(value) {
21
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
22
+ }
23
+ export function missingRoleSections(role, markdown) {
24
+ const sections = ROLE_SECTIONS[role];
25
+ if (!sections || sections.length === 0) {
26
+ return [];
27
+ }
28
+ return sections
29
+ .map((section) => section.heading)
30
+ .filter((heading) => !new RegExp(`^##\\s+${escapeRegExp(heading)}\\s*$`, "imu").test(markdown));
31
+ }
32
+ export class RoleOutputValidationError extends Error {
33
+ constructor(role, missingSections) {
34
+ super(`DevCrew ${role} output is missing required sections: ${missingSections.join(", ")}`);
35
+ this.name = "RoleOutputValidationError";
36
+ }
37
+ }
38
+ export function assertRoleSections(role, markdown) {
39
+ const missingSections = missingRoleSections(role, markdown);
40
+ if (missingSections.length > 0) {
41
+ throw new RoleOutputValidationError(role, missingSections);
42
+ }
43
+ }
44
+ export function renderRolePrompt(input) {
45
+ const executionMode = input.executionMode ?? "plan";
46
+ const answers = input.answers ?? [];
47
+ const feedback = input.feedback ?? [];
48
+ const priorArtifacts = input.priorArtifacts ?? {};
49
+ const lines = [
50
+ `Role: ${input.role}`,
51
+ `Phase: ${input.phase}`,
52
+ `Mode: ${input.mode}`,
53
+ `Execution Mode: ${executionMode}`,
54
+ `Request: ${input.request}`,
55
+ `Artifact Path: ${input.artifactPath}`,
56
+ "",
57
+ "Project Standards:",
58
+ input.standards,
59
+ ];
60
+ if (answers.length > 0) {
61
+ lines.push("", "Requester Answers:", ...answers.map((answer, index) => `${index + 1}. ${answer}`));
62
+ }
63
+ if (feedback.length > 0) {
64
+ lines.push("", "Rejection Feedback To Address:", ...feedback.map((entry) => `- ${entry}`));
65
+ }
66
+ const artifactEntries = Object.entries(priorArtifacts);
67
+ if (artifactEntries.length > 0) {
68
+ lines.push("", "Prior Artifacts:");
69
+ for (const [name, content] of artifactEntries) {
70
+ lines.push("", `### ${name}`, content.trim());
71
+ }
72
+ }
73
+ const canApply = executionMode === "apply" && (input.role === "implementer" || input.role === "tester");
74
+ const permissionInstruction = canApply
75
+ ? input.role === "tester"
76
+ ? "You may run validation commands needed for the approved scope and report exact evidence."
77
+ : "You may modify repository files needed for the approved scope and report changed files."
78
+ : "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));
80
+ return lines.join("\n");
81
+ }
82
+ function titleForPhase(phase) {
83
+ return {
84
+ requirements: "Requirements",
85
+ architecture: "Architecture",
86
+ implementation: "Implementation Plan",
87
+ testing: "Test Report",
88
+ acceptance: "Acceptance",
89
+ complete: "Acceptance",
90
+ }[phase];
91
+ }
92
+ export const HOST_SDK_PACKAGES = {
93
+ codex: "@openai/codex-sdk",
94
+ claude: "@anthropic-ai/claude-agent-sdk",
95
+ };
96
+ // Dynamic import with a non-literal specifier so the optional host SDKs do not
97
+ // need to be installed (or type-resolved) for DevCrew to build and run.
98
+ const importOptional = async (specifier) => {
99
+ const dynamicSpecifier = specifier;
100
+ return (await import(dynamicSpecifier));
101
+ };
102
+ function sdkPackageName(backend) {
103
+ return backend === "local" ? "local" : HOST_SDK_PACKAGES[backend];
104
+ }
105
+ function errorMessage(error) {
106
+ return error instanceof Error ? error.message : String(error);
107
+ }
108
+ function sdkResolutionHint(backend, reason) {
109
+ const packageName = sdkPackageName(backend);
110
+ if (backend === "local") {
111
+ return reason;
112
+ }
113
+ return `${reason}. DevCrew apply mode requires ${packageName} to be resolvable from the ${DEVCREW_NPM_PACKAGE} package. Reinstall DevCrew with optional dependencies enabled, for example: npm install -g ${DEVCREW_NPM_PACKAGE} --include=optional`;
114
+ }
115
+ export async function checkHostSdkResolution(backend, deps = {}) {
116
+ const packageName = sdkPackageName(backend);
117
+ if (backend === "local") {
118
+ return { backend, packageName, available: true };
119
+ }
120
+ const loadModule = deps.loadModule ?? importOptional;
121
+ try {
122
+ await loadModule(packageName);
123
+ return { backend, packageName, available: true };
124
+ }
125
+ catch (error) {
126
+ return { backend, packageName, available: false, error: errorMessage(error) };
127
+ }
128
+ }
129
+ // --- Pure, testable contract helpers ---------------------------------------
130
+ export function buildCodexThreadOptions(cwd, sandboxMode = "read-only") {
131
+ return { workingDirectory: cwd, skipGitRepoCheck: true, sandboxMode };
132
+ }
133
+ export function extractCodexText(turn) {
134
+ const text = typeof turn?.finalResponse === "string" ? turn.finalResponse.trim() : "";
135
+ if (!text) {
136
+ throw new Error("Codex SDK returned an empty finalResponse");
137
+ }
138
+ return text;
139
+ }
140
+ export function buildClaudeOptions(cwd, permissionMode = "plan", allowedTools = ["Read", "Grep", "Glob"]) {
141
+ // Read-only planning mode keeps roles from modifying the repository.
142
+ return { cwd, permissionMode, allowedTools };
143
+ }
144
+ export function extractClaudeResult(message) {
145
+ if (!message) {
146
+ throw new Error("Claude Agent SDK did not return a result message");
147
+ }
148
+ if (message.is_error || (message.subtype && message.subtype !== "success")) {
149
+ throw new Error(`Claude Agent SDK ended with subtype "${message.subtype ?? "unknown"}"`);
150
+ }
151
+ const text = typeof message.result === "string" ? message.result.trim() : "";
152
+ if (!text) {
153
+ throw new Error("Claude Agent SDK returned an empty result");
154
+ }
155
+ return text;
156
+ }
157
+ function roleCanApply(input) {
158
+ return input.executionMode === "apply" && (input.role === "implementer" || input.role === "tester");
159
+ }
160
+ function codexSandboxForRole(input) {
161
+ return roleCanApply(input) ? "workspace-write" : "read-only";
162
+ }
163
+ function claudeOptionsForRole(input) {
164
+ if (!roleCanApply(input)) {
165
+ return buildClaudeOptions(input.cwd);
166
+ }
167
+ const allowedTools = input.role === "implementer" ? ["Read", "Grep", "Glob", "Edit", "Write", "Bash"] : ["Read", "Grep", "Glob", "Bash"];
168
+ return buildClaudeOptions(input.cwd, "acceptEdits", allowedTools);
169
+ }
170
+ async function runWithCodex(input, prompt, loadModule) {
171
+ const mod = await loadModule(HOST_SDK_PACKAGES.codex);
172
+ const CodexClass = mod.Codex;
173
+ const codex = new CodexClass();
174
+ const thread = codex.startThread(buildCodexThreadOptions(input.cwd, codexSandboxForRole(input)));
175
+ const turn = await thread.run(prompt);
176
+ return extractCodexText(turn);
177
+ }
178
+ async function runWithClaude(input, prompt, loadModule) {
179
+ const mod = await loadModule(HOST_SDK_PACKAGES.claude);
180
+ const query = mod.query;
181
+ let resultMessage;
182
+ for await (const message of query({ prompt, options: claudeOptionsForRole(input) })) {
183
+ if (message?.type === "result") {
184
+ resultMessage = message;
185
+ }
186
+ }
187
+ return extractClaudeResult(resultMessage);
188
+ }
189
+ function fallbackResult(role, backend, title, summary) {
190
+ return {
191
+ role,
192
+ backend,
193
+ summary,
194
+ markdown: `# ${title}\n\n${summary}\n`,
195
+ usedFallback: true,
196
+ };
197
+ }
198
+ function shouldFailOnSdkError(input) {
199
+ return input.executionMode === "apply" && input.backend !== "local";
200
+ }
201
+ export async function runRole(input, deps = {}) {
202
+ const loadModule = deps.loadModule ?? importOptional;
203
+ const title = titleForPhase(input.phase);
204
+ const prompt = renderRolePrompt(input);
205
+ if (input.backend === "local") {
206
+ return fallbackResult(input.role, input.backend, title, `${input.role} prepared a deterministic ${title} because the local backend does not call an external SDK.`);
207
+ }
208
+ try {
209
+ const markdown = input.backend === "codex"
210
+ ? await runWithCodex(input, prompt, loadModule)
211
+ : await runWithClaude(input, prompt, loadModule);
212
+ assertRoleSections(input.role, markdown);
213
+ return {
214
+ role: input.role,
215
+ backend: input.backend,
216
+ summary: `${input.role} produced ${title} using the ${input.backend} SDK.`,
217
+ markdown,
218
+ usedFallback: false,
219
+ };
220
+ }
221
+ catch (error) {
222
+ const reason = errorMessage(error);
223
+ if (shouldFailOnSdkError(input)) {
224
+ if (error instanceof RoleOutputValidationError) {
225
+ throw new Error(`DevCrew apply mode rejected invalid ${input.role} SDK output: ${reason}`);
226
+ }
227
+ throw new Error(`Cannot run DevCrew apply mode with unavailable ${input.backend} SDK: ${sdkResolutionHint(input.backend, reason)}`);
228
+ }
229
+ const fallbackReason = error instanceof RoleOutputValidationError
230
+ ? `the ${input.backend} SDK output failed validation: ${reason}`
231
+ : `the ${input.backend} SDK was unavailable: ${reason}`;
232
+ return fallbackResult(input.role, input.backend, title, `${input.role} prepared a deterministic ${title} fallback because ${fallbackReason}`);
233
+ }
234
+ }
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ import { access, readFile } from "node:fs/promises";
3
+ import { resolve } from "node:path";
4
+ import { checkHostSdkResolution } from "../../adapters/src/index.js";
5
+ import { DEVCREW_VERSION } from "../../core/src/index.js";
6
+ import { initProject } from "../../plugins/src/index.js";
7
+ import { runStdioServer } from "../../service/src/index.js";
8
+ async function exists(path) {
9
+ try {
10
+ await access(path);
11
+ return true;
12
+ }
13
+ catch {
14
+ return false;
15
+ }
16
+ }
17
+ function usage() {
18
+ return `DevCrew ${DEVCREW_VERSION}\n\nUsage:\n devcrew init [cwd]\n devcrew serve --stdio\n devcrew doctor [cwd]\n devcrew validate [cwd]\n`;
19
+ }
20
+ async function doctor(cwd) {
21
+ const config = resolve(cwd, ".devcrew", "config.json");
22
+ const codex = resolve(cwd, "plugins", "devcrew-codex", ".codex-plugin", "plugin.json");
23
+ const claude = resolve(cwd, "plugins", "devcrew-claude", ".claude-plugin", "plugin.json");
24
+ const codexSdk = await checkHostSdkResolution("codex");
25
+ const claudeSdk = await checkHostSdkResolution("claude");
26
+ console.log(`Node: ${process.version}`);
27
+ console.log(`Config: ${(await exists(config)) ? "ok" : "missing"}`);
28
+ console.log(`Codex plugin: ${(await exists(codex)) ? "ok" : "missing"}`);
29
+ console.log(`Claude plugin: ${(await exists(claude)) ? "ok" : "missing"}`);
30
+ console.log(`Codex SDK (${codexSdk.packageName}): ${codexSdk.available ? "ok" : `missing - ${codexSdk.error}`}`);
31
+ console.log(`Claude SDK (${claudeSdk.packageName}): ${claudeSdk.available ? "ok" : `missing - ${claudeSdk.error}`}`);
32
+ }
33
+ async function validate(cwd) {
34
+ const configPath = resolve(cwd, ".devcrew", "config.json");
35
+ const raw = await readFile(configPath, "utf8");
36
+ const parsed = JSON.parse(raw);
37
+ if (parsed.version !== 1 || typeof parsed.workflow !== "object") {
38
+ throw new Error(`${configPath} is not a valid DevCrew config`);
39
+ }
40
+ console.log(`Validated ${configPath}`);
41
+ }
42
+ async function main(argv) {
43
+ const [command, maybeCwd, ...rest] = argv;
44
+ const cwd = resolve(maybeCwd && !maybeCwd.startsWith("--") ? maybeCwd : process.cwd());
45
+ if (!command || command === "--help" || command === "-h") {
46
+ console.log(usage());
47
+ return;
48
+ }
49
+ if (command === "init") {
50
+ const result = await initProject(cwd);
51
+ console.log(`Initialized DevCrew in ${cwd}`);
52
+ console.log(`Codex plugin: ${result.codex.path}`);
53
+ console.log(`Claude plugin: ${result.claude.path}`);
54
+ return;
55
+ }
56
+ if (command === "serve") {
57
+ if (!argv.includes("--stdio") && rest.length > 0) {
58
+ throw new Error(`Only stdio transport is supported in v${DEVCREW_VERSION}`);
59
+ }
60
+ runStdioServer();
61
+ return;
62
+ }
63
+ if (command === "doctor") {
64
+ await doctor(cwd);
65
+ return;
66
+ }
67
+ if (command === "validate") {
68
+ await validate(cwd);
69
+ return;
70
+ }
71
+ throw new Error(`Unknown command: ${command}\n\n${usage()}`);
72
+ }
73
+ main(process.argv.slice(2)).catch((error) => {
74
+ console.error(error instanceof Error ? error.message : String(error));
75
+ process.exitCode = 1;
76
+ });
@@ -0,0 +1,24 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { activeRunPath, ensureProjectDirectories } from "./paths.js";
3
+ export async function setActiveRun(cwd, runId) {
4
+ await ensureProjectDirectories(cwd);
5
+ const active = {
6
+ runId,
7
+ updatedAt: new Date().toISOString(),
8
+ };
9
+ await writeFile(activeRunPath(cwd), `${JSON.stringify(active, null, 2)}\n`, "utf8");
10
+ return active;
11
+ }
12
+ export async function getActiveRunId(cwd) {
13
+ try {
14
+ const raw = await readFile(activeRunPath(cwd), "utf8");
15
+ const parsed = JSON.parse(raw);
16
+ if (typeof parsed.runId === "string" && parsed.runId.trim()) {
17
+ return parsed.runId.trim();
18
+ }
19
+ }
20
+ catch {
21
+ // Fall through to the explicit error below.
22
+ }
23
+ throw new Error("No active DevCrew run. Pass runId or start a workflow first.");
24
+ }
@@ -0,0 +1,120 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+ import { artifactPath } from "./paths.js";
4
+ function headingForArtifact(name) {
5
+ return {
6
+ requirements: "Requirements",
7
+ architecture: "Architecture",
8
+ "implementation-plan": "Implementation Plan",
9
+ "implementation-review": "Implementation Review",
10
+ "test-report": "Test Report",
11
+ acceptance: "Acceptance",
12
+ }[name];
13
+ }
14
+ function standardsExcerpt(state) {
15
+ const excerpt = state.standards.combined.trim();
16
+ if (excerpt.length <= 1400) {
17
+ return excerpt;
18
+ }
19
+ return `${excerpt.slice(0, 1400).trim()}\n\n[Standards truncated in artifact. Full sources: ${state.standards.sources.join(", ")}]`;
20
+ }
21
+ function answerBlock(state) {
22
+ if (state.answers.length === 0) {
23
+ return "No requester answers have been recorded yet.";
24
+ }
25
+ return state.answers.map((answer, index) => `${index + 1}. ${answer.answer}`).join("\n");
26
+ }
27
+ function feedbackBlock(state) {
28
+ if (state.feedback.length === 0) {
29
+ return "No rejection feedback has been recorded.";
30
+ }
31
+ return state.feedback.map((feedback) => `- ${feedback.gate}: ${feedback.message}`).join("\n");
32
+ }
33
+ function workflowContextBlock(state) {
34
+ return `## Workflow Context\n\n### Requester Answers\n\n${answerBlock(state)}\n\n### Rejection Feedback\n\n${feedbackBlock(state)}\n`;
35
+ }
36
+ function changedFilesBlock(state) {
37
+ if (state.changedFiles.length === 0) {
38
+ return "No changed files were recorded.";
39
+ }
40
+ return state.changedFiles.map((file) => `- ${file}`).join("\n");
41
+ }
42
+ function verificationBlock(state) {
43
+ if (state.verification.length === 0) {
44
+ return "No verification commands have been executed.";
45
+ }
46
+ return state.verification
47
+ .map((result) => `### ${result.command}\n\nExit Code: ${result.exitCode}\n\nOutput:\n\n\`\`\`text\n${result.output || "(no output)"}\n\`\`\``)
48
+ .join("\n\n");
49
+ }
50
+ function lintResultsBlock(state) {
51
+ if (state.lintResults.length === 0) {
52
+ return "No lint or format commands have been executed.";
53
+ }
54
+ return state.lintResults
55
+ .map((result) => `### ${result.command}\n\nExit Code: ${result.exitCode}\n\nOutput:\n\n\`\`\`text\n${result.output || "(no output)"}\n\`\`\``)
56
+ .join("\n\n");
57
+ }
58
+ function implementationDiffBlock(state) {
59
+ const diff = state.implementationDiff.trim();
60
+ if (!diff) {
61
+ return "No implementation diff was captured. Review the changed files list and repository state manually.";
62
+ }
63
+ return `\`\`\`diff\n${diff}\n\`\`\``;
64
+ }
65
+ function architectureComplianceInputsBlock(state) {
66
+ const architectureArtifact = state.artifacts.architecture ? "present" : "missing";
67
+ const diffStatus = state.implementationDiff.trim() ? "present" : "missing";
68
+ return [
69
+ `- Architecture Artifact: ${architectureArtifact}${state.artifacts.architecture ? ` (${state.artifacts.architecture})` : ""}`,
70
+ `- Changed Files: ${state.changedFiles.length}`,
71
+ `- Captured Diff: ${diffStatus}`,
72
+ ].join("\n");
73
+ }
74
+ function architectureComplianceStatus(state) {
75
+ if (state.changedFiles.length === 0 && !state.implementationDiff.trim()) {
76
+ return "No implementation changes detected";
77
+ }
78
+ return "Needs Human Review";
79
+ }
80
+ export function renderArtifact(name, state) {
81
+ 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`;
83
+ if (name === "requirements") {
84
+ 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
+ }
86
+ if (name === "architecture") {
87
+ return `${common}${workflowContextBlock(state)}\n## Technical Decisions\n\nFor each key decision record Decision, Options Considered, Choice, Rationale, and Trade-offs.\n\n- Decision: owns state transitions, gates, and MCP tool handlers via a workflow service.\n - Options Considered: monolithic handler vs. layered service + adapters.\n - Choice: layered workflow service with a host adapter and artifact writer.\n - Rationale: keeps gate logic testable and host execution swappable.\n - Trade-offs: more modules to wire, but clearer boundaries.\n\n## Interface Contracts\n\nFor each interface give the signature, request/response schema, error contract, and data model.\n\n- Host adapter: selects Codex or Claude execution based on host or config override.\n- Artifact writer: persists Markdown outputs under docs/devcrew for review.\n\n## Data Flow and Deployment\n\n- Record data flow, deployment expectations, and rollback considerations.\n- For greenfield workflows keep the first implementation small enough to ship and test end to end.\n\n## Architecture Review Checklist\n\n- Architecture traces directly to approved requirements.\n- Implementation can be tested without a live production integration.\n- Security and permission decisions remain delegated to the host agent runtime.\n`;
88
+ }
89
+ if (name === "implementation-plan") {
90
+ return `${common}${workflowContextBlock(state)}\n## Implementation Summary\n\nImplement the smallest code path that satisfies the approved architecture.\n\n## Implementation Tasks\n\n1. Update or create focused tests for the requested behavior.\n2. Implement the smallest code path that satisfies the approved architecture.\n3. Preserve discovered standards and existing repository conventions.\n4. Write or update user-facing docs for changed behavior.\n5. Run the project validation commands and capture evidence.\n\n## Recorded Changes\n\n${changedFilesBlock(state)}\n\n## Lint Results\n\n${lintResultsBlock(state)}\n\n## Code Review Criteria\n\n- Changes stay inside the approved scope.\n- Public interfaces match the architecture artifact.\n- Tests cover success, failure, and gate behavior where applicable.\n`;
91
+ }
92
+ if (name === "implementation-review") {
93
+ 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
+ }
95
+ 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`;
97
+ }
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)
99
+ .map(([gate, status]) => `- ${gate}: ${status}`)
100
+ .join("\n")}\n`;
101
+ }
102
+ export async function writeArtifact(name, state) {
103
+ const path = artifactPath(state.cwd, state.runId, name);
104
+ await mkdir(dirname(path), { recursive: true });
105
+ await writeFile(path, renderArtifact(name, state), "utf8");
106
+ return path;
107
+ }
108
+ export async function readArtifact(state, name) {
109
+ const path = state.artifacts[name];
110
+ if (!path) {
111
+ throw new Error(`Artifact ${name} has not been created for run ${state.runId}`);
112
+ }
113
+ const content = await readFile(path, "utf8");
114
+ return {
115
+ name,
116
+ path,
117
+ content,
118
+ summary: content.split("\n").slice(0, 8).join("\n"),
119
+ };
120
+ }
@@ -0,0 +1,51 @@
1
+ import { access, readFile, writeFile } from "node:fs/promises";
2
+ import { configPath, ensureProjectDirectories } from "./paths.js";
3
+ export const DEFAULT_CONFIG = {
4
+ version: 1,
5
+ defaultBackend: "host-preferred",
6
+ executionMode: "plan",
7
+ verifyCommands: [],
8
+ lintCommands: [],
9
+ coverageCommands: [],
10
+ workflow: {
11
+ gates: ["requirements", "architecture", "implementation", "testing"],
12
+ artifactDirectory: "docs/devcrew",
13
+ },
14
+ };
15
+ async function exists(path) {
16
+ try {
17
+ await access(path);
18
+ return true;
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ }
24
+ export async function ensureConfig(cwd) {
25
+ await ensureProjectDirectories(cwd);
26
+ const path = configPath(cwd);
27
+ if (!(await exists(path))) {
28
+ await writeFile(path, `${JSON.stringify(DEFAULT_CONFIG, null, 2)}\n`, "utf8");
29
+ return DEFAULT_CONFIG;
30
+ }
31
+ return readConfig(cwd);
32
+ }
33
+ export async function readConfig(cwd) {
34
+ 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
+ };
51
+ }
@@ -0,0 +1,11 @@
1
+ export * from "./active-run.js";
2
+ export * from "./artifacts.js";
3
+ export * from "./config.js";
4
+ export * from "./paths.js";
5
+ export * from "./standards.js";
6
+ export * from "./store.js";
7
+ export * from "./types.js";
8
+ export * from "./validation.js";
9
+ export * from "./version.js";
10
+ export * from "./verification.js";
11
+ export * from "./workflow.js";
@@ -0,0 +1,51 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ export function devcrewDir(cwd) {
4
+ return join(cwd, ".devcrew");
5
+ }
6
+ export function runsDir(cwd) {
7
+ return join(devcrewDir(cwd), "runs");
8
+ }
9
+ export function runDir(cwd, runId) {
10
+ return join(runsDir(cwd), runId);
11
+ }
12
+ export function statePath(cwd, runId) {
13
+ return join(runDir(cwd, runId), "state.json");
14
+ }
15
+ export function configPath(cwd) {
16
+ return join(devcrewDir(cwd), "config.json");
17
+ }
18
+ export function activeRunPath(cwd) {
19
+ return join(devcrewDir(cwd), "active-run.json");
20
+ }
21
+ export function standardsPath(cwd) {
22
+ return join(devcrewDir(cwd), "standards.md");
23
+ }
24
+ export function docsRoot(cwd) {
25
+ return join(cwd, "docs", "devcrew");
26
+ }
27
+ export function docsRunDir(cwd, runId) {
28
+ return join(docsRoot(cwd), runId);
29
+ }
30
+ export function artifactPath(cwd, runId, artifact) {
31
+ const filenameByArtifact = {
32
+ requirements: "requirements.md",
33
+ architecture: "architecture.md",
34
+ "implementation-plan": "implementation-plan.md",
35
+ "implementation-review": "implementation-review.md",
36
+ "test-report": "test-report.md",
37
+ acceptance: "acceptance.md",
38
+ };
39
+ return join(docsRunDir(cwd, runId), filenameByArtifact[artifact]);
40
+ }
41
+ export async function ensureRunDirectories(cwd, runId) {
42
+ await mkdir(runDir(cwd, runId), { recursive: true });
43
+ await mkdir(docsRunDir(cwd, runId), { recursive: true });
44
+ }
45
+ export async function ensureProjectDirectories(cwd) {
46
+ await mkdir(devcrewDir(cwd), { recursive: true });
47
+ await mkdir(docsRoot(cwd), { recursive: true });
48
+ }
49
+ export function relativeArtifactMap(state) {
50
+ return state.artifacts;
51
+ }
@@ -0,0 +1,61 @@
1
+ import { access, readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { standardsPath } from "./paths.js";
4
+ import { readPackageJson } from "./verification.js";
5
+ async function readIfExists(path) {
6
+ try {
7
+ await access(path);
8
+ return readFile(path, "utf8");
9
+ }
10
+ catch {
11
+ return undefined;
12
+ }
13
+ }
14
+ function section(path, content) {
15
+ return `## ${path}\n\n${content.trim()}\n`;
16
+ }
17
+ async function packageJsonSummary(cwd) {
18
+ const parsed = await readPackageJson(cwd);
19
+ if (!parsed) {
20
+ return undefined;
21
+ }
22
+ const scripts = Object.keys(parsed.scripts ?? {});
23
+ if (scripts.length === 0) {
24
+ return "package.json scripts: none";
25
+ }
26
+ return `package.json scripts: ${scripts.join(", ")}`;
27
+ }
28
+ export async function discoverStandards(cwd) {
29
+ const candidates = [
30
+ standardsPath(cwd),
31
+ join(cwd, "AGENTS.md"),
32
+ join(cwd, "CLAUDE.md"),
33
+ join(cwd, "README.md"),
34
+ join(cwd, "README"),
35
+ join(cwd, "pyproject.toml"),
36
+ join(cwd, "go.mod"),
37
+ join(cwd, "Cargo.toml"),
38
+ ];
39
+ const sources = [];
40
+ const sections = [];
41
+ for (const candidate of candidates) {
42
+ const content = await readIfExists(candidate);
43
+ if (content?.trim()) {
44
+ sources.push(candidate);
45
+ sections.push(section(candidate, content));
46
+ }
47
+ }
48
+ const packageSummary = await packageJsonSummary(cwd);
49
+ if (packageSummary) {
50
+ const packagePath = join(cwd, "package.json");
51
+ sources.push(packagePath);
52
+ sections.push(section(packagePath, packageSummary));
53
+ }
54
+ if (sections.length === 0) {
55
+ sections.push("No project standards were discovered. Follow the current repository style and ask before broad refactors.\n");
56
+ }
57
+ return {
58
+ sources,
59
+ combined: sections.join("\n"),
60
+ };
61
+ }
@@ -0,0 +1,24 @@
1
+ import { readFile, rename, writeFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { ensureRunDirectories, statePath } from "./paths.js";
4
+ export async function saveState(state) {
5
+ state.updatedAt = new Date().toISOString();
6
+ await ensureRunDirectories(state.cwd, state.runId);
7
+ const target = statePath(state.cwd, state.runId);
8
+ const temp = join(dirname(target), `.state.${process.pid}.${Date.now()}.tmp`);
9
+ await writeFile(temp, `${JSON.stringify(state, null, 2)}\n`, "utf8");
10
+ await rename(temp, target);
11
+ return state;
12
+ }
13
+ export async function loadState(cwd, runId) {
14
+ const raw = await readFile(statePath(cwd, runId), "utf8");
15
+ const parsed = JSON.parse(raw);
16
+ return {
17
+ ...parsed,
18
+ executionMode: parsed.executionMode ?? "plan",
19
+ changedFiles: Array.isArray(parsed.changedFiles) ? parsed.changedFiles : [],
20
+ implementationDiff: typeof parsed.implementationDiff === "string" ? parsed.implementationDiff : "",
21
+ verification: Array.isArray(parsed.verification) ? parsed.verification : [],
22
+ lintResults: Array.isArray(parsed.lintResults) ? parsed.lintResults : [],
23
+ };
24
+ }