@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
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { mkdir, readFile,
|
|
3
|
-
import { dirname
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
4
|
import { runRole } from "../../adapters/src/index.js";
|
|
5
|
-
import { answerWorkflow, artifactForPhase, artifactPath, ARTIFACTS, discoverCoverageCommands, discoverLintCommands, discoverVerifyCommands, gateForPhase, getWorkflowStatus, readConfig, rejectWorkflow, renderArtifact, saveState, startWorkflow, } from "../../core/src/index.js";
|
|
5
|
+
import { answerWorkflow, approveWorkflow, artifactForPhase, artifactPath, ARTIFACTS, discoverCoverageCommands, discoverLintCommands, discoverVerifyCommands, gateForPhase, getWorkflowStatus, nextPhaseAfterGate, readConfig, rejectWorkflow, renderArtifact, saveState, startWorkflow, validateWorkflowApproval, waiveVerificationWorkflow, } from "../../core/src/index.js";
|
|
6
|
+
import { captureExecutionChanges, cleanupExecutionWorkspace, ensureExecutionWorkspace, promoteExecutionChanges, rollbackPromotedExecutionChanges, } from "./worktree.js";
|
|
6
7
|
// Hard cap so a hung apply/verify command cannot block the serialized MCP loop.
|
|
7
8
|
const COMMAND_TIMEOUT_MS = 300_000;
|
|
8
9
|
function roleForPhase(phase) {
|
|
@@ -10,6 +11,8 @@ function roleForPhase(phase) {
|
|
|
10
11
|
requirements: "pm",
|
|
11
12
|
architecture: "architect",
|
|
12
13
|
implementation: "implementer",
|
|
14
|
+
execution: "implementer",
|
|
15
|
+
review: "architect",
|
|
13
16
|
testing: "tester",
|
|
14
17
|
};
|
|
15
18
|
return roles[phase];
|
|
@@ -33,7 +36,7 @@ function priorArtifactNamesForPhase(phase) {
|
|
|
33
36
|
return ARTIFACTS.slice(0, currentIndex);
|
|
34
37
|
}
|
|
35
38
|
async function writeMarkdownArtifact(state, artifact, markdown) {
|
|
36
|
-
const path = artifactPath(state.cwd, state.runId, artifact);
|
|
39
|
+
const path = artifactPath(state.cwd, state.runId, artifact, state.artifactDirectory);
|
|
37
40
|
await mkdir(dirname(path), { recursive: true });
|
|
38
41
|
await writeFile(path, markdown, "utf8");
|
|
39
42
|
return path;
|
|
@@ -113,100 +116,6 @@ export async function runShellCommand(command, cwd, timeoutMs = COMMAND_TIMEOUT_
|
|
|
113
116
|
});
|
|
114
117
|
});
|
|
115
118
|
}
|
|
116
|
-
async function runGit(args, cwd) {
|
|
117
|
-
return new Promise((resolveResult) => {
|
|
118
|
-
const child = spawn("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
119
|
-
let stdout = "";
|
|
120
|
-
let stderr = "";
|
|
121
|
-
child.stdout.on("data", (chunk) => {
|
|
122
|
-
stdout += chunk.toString("utf8");
|
|
123
|
-
});
|
|
124
|
-
child.stderr.on("data", (chunk) => {
|
|
125
|
-
stderr += chunk.toString("utf8");
|
|
126
|
-
});
|
|
127
|
-
child.on("error", () => resolveResult({ exitCode: 1, stdout: "" }));
|
|
128
|
-
child.on("close", (code) => resolveResult({ exitCode: code ?? 1, stdout: stdout || stderr }));
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
async function listChangedLines(cwd, options = {}) {
|
|
132
|
-
const result = await runShellCommand("git status --porcelain -uall", cwd, 30_000);
|
|
133
|
-
if (result.exitCode !== 0) {
|
|
134
|
-
if (options.requireGit) {
|
|
135
|
-
throw new Error("DevCrew apply mode requires a git repository so implementation changes can be reviewed and reverted.");
|
|
136
|
-
}
|
|
137
|
-
return [];
|
|
138
|
-
}
|
|
139
|
-
return result.output
|
|
140
|
-
.split("\n")
|
|
141
|
-
.map((line) => line.trimEnd())
|
|
142
|
-
.filter(Boolean)
|
|
143
|
-
.filter((line) => {
|
|
144
|
-
const path = line.slice(3);
|
|
145
|
-
return !path.startsWith(".devcrew/") && !path.startsWith("docs/devcrew/");
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
async function assertCleanApplyWorkspace(cwd) {
|
|
149
|
-
const changedLines = await listChangedLines(cwd, { requireGit: true });
|
|
150
|
-
if (changedLines.length === 0) {
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
const visibleLines = changedLines.slice(0, 20).join(", ");
|
|
154
|
-
const suffix = changedLines.length > 20 ? `, and ${changedLines.length - 20} more` : "";
|
|
155
|
-
throw new Error(`DevCrew apply mode requires a clean working tree before implementation. Commit, stash, or remove these files: ${visibleLines}${suffix}`);
|
|
156
|
-
}
|
|
157
|
-
async function collectImplementationDiff(cwd) {
|
|
158
|
-
const result = await runShellCommand("git diff --no-ext-diff --", cwd, 30_000);
|
|
159
|
-
if (result.exitCode !== 0) {
|
|
160
|
-
return "";
|
|
161
|
-
}
|
|
162
|
-
return result.output;
|
|
163
|
-
}
|
|
164
|
-
// Files attributable to this run are the porcelain lines that appeared (or
|
|
165
|
-
// changed status) since the baseline captured before the role executed. This
|
|
166
|
-
// keeps a user's pre-existing uncommitted edits out of the changed-files list.
|
|
167
|
-
// The porcelain status prefix is preserved for review (?? = new, M = modified).
|
|
168
|
-
export function changedSinceBaseline(baseline, current) {
|
|
169
|
-
const baselineLines = new Set(baseline);
|
|
170
|
-
return current.filter((line) => !baselineLines.has(line));
|
|
171
|
-
}
|
|
172
|
-
// Strip the two-character porcelain status plus its separating space. For
|
|
173
|
-
// rename entries ("R old -> new") the destination path is what remains
|
|
174
|
-
// relevant, so keep only the post-arrow path when present.
|
|
175
|
-
export function porcelainPath(line) {
|
|
176
|
-
const raw = line.slice(3).trim();
|
|
177
|
-
const arrow = raw.indexOf(" -> ");
|
|
178
|
-
return arrow >= 0 ? raw.slice(arrow + 4) : raw;
|
|
179
|
-
}
|
|
180
|
-
// Roll back implementer edits when an apply-mode gate is rejected.
|
|
181
|
-
// Only the files this run introduced/changed are reverted, so unrelated work in
|
|
182
|
-
// the repository is preserved. Tracked files are restored from HEAD; files that
|
|
183
|
-
// did not exist in HEAD are deleted. Failures are surfaced to the MCP caller.
|
|
184
|
-
export async function revertChangedFiles(cwd, changedFiles, deps = {}) {
|
|
185
|
-
const git = deps.runGit ?? runGit;
|
|
186
|
-
const removeFile = deps.removeFile ?? ((absolutePath) => rm(absolutePath, { force: true }));
|
|
187
|
-
for (const line of changedFiles) {
|
|
188
|
-
const file = porcelainPath(line);
|
|
189
|
-
if (!file) {
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
const tracked = await git(["cat-file", "-e", `HEAD:${file}`], cwd);
|
|
193
|
-
if (tracked.exitCode === 0) {
|
|
194
|
-
const restored = await git(["restore", "--source=HEAD", "--", file], cwd);
|
|
195
|
-
if (restored.exitCode !== 0) {
|
|
196
|
-
throw new Error(`Failed to restore ${file}: ${restored.stdout || "git restore failed"}`);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
else {
|
|
200
|
-
try {
|
|
201
|
-
await removeFile(resolvePath(cwd, file));
|
|
202
|
-
}
|
|
203
|
-
catch (error) {
|
|
204
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
205
|
-
throw new Error(`Failed to remove ${file}: ${message}`);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
119
|
async function runCommands(commands, cwd) {
|
|
211
120
|
const results = [];
|
|
212
121
|
for (const command of commands) {
|
|
@@ -229,28 +138,22 @@ function uniqueCommands(commands) {
|
|
|
229
138
|
// Tester verification runs the normal verification path first, then coverage
|
|
230
139
|
// as supplemental evidence. Configured commands win per category; otherwise
|
|
231
140
|
// DevCrew discovers common project commands.
|
|
232
|
-
async function runConfiguredVerification(state) {
|
|
141
|
+
async function runConfiguredVerification(state, commandCwd) {
|
|
233
142
|
const config = await readConfig(state.cwd);
|
|
234
143
|
const configuredVerify = config.verifyCommands.filter((command) => command.trim().length > 0);
|
|
235
144
|
const configuredCoverage = (config.coverageCommands ?? []).filter((command) => command.trim().length > 0);
|
|
236
|
-
const verifyCommands = configuredVerify.length > 0 ? configuredVerify : await discoverVerifyCommands(
|
|
237
|
-
const coverageCommands = configuredCoverage.length > 0 ? configuredCoverage : await discoverCoverageCommands(
|
|
145
|
+
const verifyCommands = configuredVerify.length > 0 ? configuredVerify : await discoverVerifyCommands(commandCwd);
|
|
146
|
+
const coverageCommands = configuredCoverage.length > 0 ? configuredCoverage : await discoverCoverageCommands(commandCwd);
|
|
238
147
|
const commands = uniqueCommands([...verifyCommands, ...coverageCommands]);
|
|
239
|
-
return runCommands(commands,
|
|
148
|
+
return runCommands(commands, commandCwd);
|
|
240
149
|
}
|
|
241
150
|
// Implementer apply runs lint/format/typecheck so reviewers see standards
|
|
242
151
|
// compliance evidence. Configured lintCommands win, otherwise discover them.
|
|
243
|
-
async function runConfiguredLint(state) {
|
|
152
|
+
async function runConfiguredLint(state, commandCwd) {
|
|
244
153
|
const config = await readConfig(state.cwd);
|
|
245
154
|
const configuredLint = (config.lintCommands ?? []).filter((command) => command.trim().length > 0);
|
|
246
|
-
const commands = configuredLint.length > 0 ? configuredLint : await discoverLintCommands(
|
|
247
|
-
return runCommands(commands,
|
|
248
|
-
}
|
|
249
|
-
function changedFilesBlock(changedFiles) {
|
|
250
|
-
if (changedFiles.length === 0) {
|
|
251
|
-
return "No changed files were detected.";
|
|
252
|
-
}
|
|
253
|
-
return changedFiles.map((file) => `- ${file}`).join("\n");
|
|
155
|
+
const commands = configuredLint.length > 0 ? configuredLint : await discoverLintCommands(commandCwd);
|
|
156
|
+
return runCommands(commands, commandCwd);
|
|
254
157
|
}
|
|
255
158
|
function verificationBlock(results) {
|
|
256
159
|
if (results.length === 0) {
|
|
@@ -260,32 +163,155 @@ function verificationBlock(results) {
|
|
|
260
163
|
.map((result) => `### ${result.command}\n\nExit Code: ${result.exitCode}\n\nOutput:\n\n\`\`\`text\n${result.output || "(no output)"}\n\`\`\``)
|
|
261
164
|
.join("\n\n");
|
|
262
165
|
}
|
|
263
|
-
function
|
|
166
|
+
function verificationStatusFor(results) {
|
|
264
167
|
if (results.length === 0) {
|
|
265
|
-
return "
|
|
168
|
+
return "not_run";
|
|
266
169
|
}
|
|
267
|
-
return
|
|
170
|
+
return results.every((result) => result.exitCode === 0) ? "passed" : "failed";
|
|
268
171
|
}
|
|
269
|
-
function
|
|
270
|
-
if (
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
172
|
+
function setTestingGateFromVerification(state) {
|
|
173
|
+
if (state.verificationStatus === "failed") {
|
|
174
|
+
state.gates.testing = "rejected";
|
|
175
|
+
state.status = "awaiting_input";
|
|
176
|
+
state.feedback.push({
|
|
177
|
+
gate: "testing",
|
|
178
|
+
message: "Automated verification failed. Inspect the test report, revise the implementation, or record an explicit verification waiver with its reason.",
|
|
179
|
+
createdAt: now(),
|
|
180
|
+
});
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
state.gates.testing = "pending";
|
|
184
|
+
state.status = "awaiting_approval";
|
|
185
|
+
}
|
|
186
|
+
function executionInstructions(state, phase, workspacePath) {
|
|
187
|
+
if (phase === "execution") {
|
|
188
|
+
return `Use the native ${state.host} host agent to implement the approved change in ${workspacePath}. Do not modify the requester checkout at ${state.cwd}. When complete, call devcrew_complete_execution with a concise summary.`;
|
|
189
|
+
}
|
|
190
|
+
return `Use the native ${state.host} host agent to validate the approved change in ${workspacePath}. Then call devcrew_complete_execution with a concise summary and each command's exit code and output.`;
|
|
191
|
+
}
|
|
192
|
+
function hostCompletionResult(state, summary) {
|
|
193
|
+
const role = state.phase === "execution" ? "implementer" : "tester";
|
|
194
|
+
const artifact = artifactForPhase(state.phase);
|
|
195
|
+
return {
|
|
196
|
+
role,
|
|
197
|
+
backend: state.backend,
|
|
198
|
+
summary: `${role} completed through the native ${state.host} host: ${summary}`,
|
|
199
|
+
markdown: `${renderArtifact(artifact, state).trim()}\n\n## Native Host Summary\n\n${summary}\n`,
|
|
200
|
+
usedFallback: false,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function parseCompletionSummary(value) {
|
|
204
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
205
|
+
throw new Error("summary must be a non-empty string");
|
|
206
|
+
}
|
|
207
|
+
return value.trim();
|
|
208
|
+
}
|
|
209
|
+
function parseCompletionVerification(value) {
|
|
210
|
+
if (value === undefined) {
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
if (!Array.isArray(value)) {
|
|
214
|
+
throw new Error("verification must be an array");
|
|
215
|
+
}
|
|
216
|
+
return value.map((entry, index) => {
|
|
217
|
+
if (!entry ||
|
|
218
|
+
typeof entry !== "object" ||
|
|
219
|
+
typeof entry.command !== "string" ||
|
|
220
|
+
entry.command.trim().length === 0 ||
|
|
221
|
+
!Number.isInteger(entry.exitCode) ||
|
|
222
|
+
typeof entry.output !== "string" ||
|
|
223
|
+
typeof entry.startedAt !== "string" ||
|
|
224
|
+
typeof entry.completedAt !== "string") {
|
|
225
|
+
throw new Error(`verification[${index}] must include command, integer exitCode, output, startedAt, and completedAt`);
|
|
274
226
|
}
|
|
275
|
-
|
|
276
|
-
|
|
227
|
+
return {
|
|
228
|
+
command: entry.command.trim(),
|
|
229
|
+
exitCode: entry.exitCode,
|
|
230
|
+
output: entry.output,
|
|
231
|
+
startedAt: entry.startedAt,
|
|
232
|
+
completedAt: entry.completedAt,
|
|
233
|
+
};
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
function appendExecutionSections(artifact, markdown, state) {
|
|
237
|
+
if (artifact === "architecture-review" && state.architectureReview) {
|
|
238
|
+
let content = markdown.trim();
|
|
239
|
+
if (!content.includes("## Review Decision")) {
|
|
240
|
+
content += `\n\n## Review Decision\n\nDecision: ${state.architectureReview.decision}\n\nSummary: ${state.architectureReview.summary}`;
|
|
277
241
|
}
|
|
278
|
-
return `${
|
|
242
|
+
return `${content}\n`;
|
|
279
243
|
}
|
|
280
|
-
if (artifact === "test-report"
|
|
281
|
-
|
|
244
|
+
if (artifact === "test-report") {
|
|
245
|
+
let content = markdown.trim();
|
|
246
|
+
if (!content.includes("## Acceptance Evidence")) {
|
|
247
|
+
content += `\n\n## Acceptance Evidence\n\n${verificationBlock(state.verification)}`;
|
|
248
|
+
}
|
|
249
|
+
if (!content.includes("## Verification Outcome")) {
|
|
250
|
+
content += `\n\n## Verification Outcome\n\nStatus: ${state.verificationStatus}`;
|
|
251
|
+
}
|
|
252
|
+
if (state.verificationWaiver && !content.includes("## Verification Waiver")) {
|
|
253
|
+
content += `\n\n## Verification Waiver\n\nReason: ${state.verificationWaiver.reason}\nRecorded At: ${state.verificationWaiver.createdAt}`;
|
|
254
|
+
}
|
|
255
|
+
return `${content}\n`;
|
|
282
256
|
}
|
|
283
257
|
return markdown;
|
|
284
258
|
}
|
|
259
|
+
async function writeImplementationReview(state) {
|
|
260
|
+
state.artifacts["implementation-review"] = await writeMarkdownArtifact(state, "implementation-review", renderArtifact("implementation-review", state));
|
|
261
|
+
}
|
|
285
262
|
async function runCurrentPhaseRole(state, runner = runRole) {
|
|
286
|
-
|
|
263
|
+
if (state.phase === "execution") {
|
|
264
|
+
if (state.executionMode !== "apply") {
|
|
265
|
+
throw new Error("DevCrew execution phase requires apply mode");
|
|
266
|
+
}
|
|
267
|
+
const workspace = await ensureExecutionWorkspace(state);
|
|
268
|
+
state.executionWorkspace = workspace;
|
|
269
|
+
state.verification = [];
|
|
270
|
+
state.verificationStatus = "not_run";
|
|
271
|
+
delete state.verificationWaiver;
|
|
272
|
+
delete state.artifacts["test-report"];
|
|
273
|
+
await saveState(state);
|
|
274
|
+
if (state.executionPolicy === "interactive-host") {
|
|
275
|
+
state.executionInstruction = {
|
|
276
|
+
phase: "execution",
|
|
277
|
+
workspacePath: workspace.path,
|
|
278
|
+
instructions: executionInstructions(state, "execution", workspace.path),
|
|
279
|
+
createdAt: now(),
|
|
280
|
+
};
|
|
281
|
+
state.status = "awaiting_execution";
|
|
282
|
+
return saveState(state);
|
|
283
|
+
}
|
|
284
|
+
const result = await runner({
|
|
285
|
+
backend: state.backend,
|
|
286
|
+
role: "implementer",
|
|
287
|
+
phase: "execution",
|
|
288
|
+
request: state.request,
|
|
289
|
+
mode: state.mode,
|
|
290
|
+
executionMode: "apply",
|
|
291
|
+
executionPolicy: state.executionPolicy,
|
|
292
|
+
cwd: workspace.path,
|
|
293
|
+
standards: state.standards.combined,
|
|
294
|
+
artifactPath: artifactPath(state.cwd, state.runId, "implementation-review", state.artifactDirectory),
|
|
295
|
+
answers: state.answers.map((entry) => entry.answer),
|
|
296
|
+
feedback: state.feedback.map((entry) => `${entry.gate}: ${entry.message}`),
|
|
297
|
+
priorArtifacts: await readPriorArtifacts(state),
|
|
298
|
+
});
|
|
299
|
+
await captureExecutionChanges(workspace);
|
|
300
|
+
state.lintResults = await runConfiguredLint(state, workspace.path);
|
|
301
|
+
const captured = await captureExecutionChanges(workspace);
|
|
302
|
+
state.changedFiles = captured.changedFiles;
|
|
303
|
+
state.implementationDiff = captured.patch;
|
|
304
|
+
state.roles.push(result);
|
|
305
|
+
state.executionInstruction = undefined;
|
|
306
|
+
await writeImplementationReview(state);
|
|
307
|
+
state.phase = "review";
|
|
308
|
+
state.status = "ready";
|
|
309
|
+
return saveState(state);
|
|
310
|
+
}
|
|
311
|
+
const configuredGate = gateForPhase(state.phase);
|
|
312
|
+
const gate = gateForPhase(state.phase, state.enabledGates);
|
|
287
313
|
const role = roleForPhase(state.phase);
|
|
288
|
-
if (!
|
|
314
|
+
if (!role) {
|
|
289
315
|
const artifact = artifactForPhase(state.phase);
|
|
290
316
|
const markdown = renderArtifact(artifact, state);
|
|
291
317
|
state.artifacts[artifact] = await writeMarkdownArtifact(state, artifact, markdown);
|
|
@@ -294,15 +320,23 @@ async function runCurrentPhaseRole(state, runner = runRole) {
|
|
|
294
320
|
return saveState(state);
|
|
295
321
|
}
|
|
296
322
|
const artifact = artifactForPhase(state.phase);
|
|
297
|
-
const path = artifactPath(state.cwd, state.runId, artifact);
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
323
|
+
const path = artifactPath(state.cwd, state.runId, artifact, state.artifactDirectory);
|
|
324
|
+
const applyingTesting = state.executionMode === "apply" && state.phase === "testing";
|
|
325
|
+
const roleCwd = applyingTesting ? state.executionWorkspace?.path : state.cwd;
|
|
326
|
+
if (!roleCwd) {
|
|
327
|
+
throw new Error("DevCrew apply testing requires an execution workspace");
|
|
328
|
+
}
|
|
329
|
+
state.roles.push(conductorDecision(state, role, gate ?? `${state.phase} automatic transition`));
|
|
330
|
+
if (applyingTesting && state.executionPolicy === "interactive-host") {
|
|
331
|
+
state.executionInstruction = {
|
|
332
|
+
phase: "testing",
|
|
333
|
+
workspacePath: roleCwd,
|
|
334
|
+
instructions: executionInstructions(state, "testing", roleCwd),
|
|
335
|
+
createdAt: now(),
|
|
336
|
+
};
|
|
337
|
+
state.status = "awaiting_execution";
|
|
338
|
+
return saveState(state);
|
|
339
|
+
}
|
|
306
340
|
const result = await runner({
|
|
307
341
|
backend: state.backend,
|
|
308
342
|
role,
|
|
@@ -310,20 +344,31 @@ async function runCurrentPhaseRole(state, runner = runRole) {
|
|
|
310
344
|
request: state.request,
|
|
311
345
|
mode: state.mode,
|
|
312
346
|
executionMode: state.executionMode,
|
|
313
|
-
|
|
347
|
+
executionPolicy: state.executionPolicy,
|
|
348
|
+
cwd: roleCwd,
|
|
314
349
|
standards: state.standards.combined,
|
|
315
350
|
artifactPath: path,
|
|
316
351
|
answers: state.answers.map((entry) => entry.answer),
|
|
317
352
|
feedback: state.feedback.map((entry) => `${entry.gate}: ${entry.message}`),
|
|
318
353
|
priorArtifacts: await readPriorArtifacts(state),
|
|
319
354
|
});
|
|
320
|
-
if (
|
|
321
|
-
state.
|
|
322
|
-
state.
|
|
323
|
-
|
|
355
|
+
if (applyingTesting && state.executionWorkspace) {
|
|
356
|
+
state.verification = await runConfiguredVerification(state, roleCwd);
|
|
357
|
+
state.verificationStatus = verificationStatusFor(state.verification);
|
|
358
|
+
const captured = await captureExecutionChanges(state.executionWorkspace);
|
|
359
|
+
state.changedFiles = captured.changedFiles;
|
|
360
|
+
state.implementationDiff = captured.patch;
|
|
361
|
+
await writeImplementationReview(state);
|
|
324
362
|
}
|
|
325
|
-
if (state.
|
|
326
|
-
|
|
363
|
+
if (state.phase === "review") {
|
|
364
|
+
if (!result.reviewDecision) {
|
|
365
|
+
throw new Error("DevCrew architecture review must return a structured review decision");
|
|
366
|
+
}
|
|
367
|
+
state.architectureReview = {
|
|
368
|
+
decision: result.reviewDecision,
|
|
369
|
+
summary: result.summary,
|
|
370
|
+
reviewedAt: now(),
|
|
371
|
+
};
|
|
327
372
|
}
|
|
328
373
|
// When the backend cannot run a real SDK we keep a single deterministic
|
|
329
374
|
// artifact source by rendering the rich phase template from the core layer.
|
|
@@ -331,11 +376,38 @@ async function runCurrentPhaseRole(state, runner = runRole) {
|
|
|
331
376
|
const markdown = appendExecutionSections(artifact, baseMarkdown, state);
|
|
332
377
|
state.roles.push({ ...result, markdown });
|
|
333
378
|
state.artifacts[artifact] = await writeMarkdownArtifact(state, artifact, markdown);
|
|
334
|
-
|
|
335
|
-
|
|
379
|
+
state.executionInstruction = undefined;
|
|
380
|
+
if (state.phase === "requirements" && result.questions && result.questions.length > 0) {
|
|
381
|
+
state.pendingQuestions = result.questions;
|
|
382
|
+
state.gates.requirements = "not_started";
|
|
383
|
+
state.status = "awaiting_input";
|
|
384
|
+
return saveState(state);
|
|
385
|
+
}
|
|
386
|
+
if (state.phase === "review" && state.architectureReview?.decision === "changes_required") {
|
|
387
|
+
state.gates["implementation-review"] = "rejected";
|
|
388
|
+
state.status = "awaiting_input";
|
|
389
|
+
state.feedback.push({
|
|
390
|
+
gate: "implementation-review",
|
|
391
|
+
message: state.architectureReview.summary,
|
|
392
|
+
createdAt: now(),
|
|
393
|
+
});
|
|
394
|
+
return saveState(state);
|
|
395
|
+
}
|
|
396
|
+
if (!gate && configuredGate) {
|
|
397
|
+
state.phase = nextPhaseAfterGate(state, configuredGate);
|
|
398
|
+
state.status = "ready";
|
|
399
|
+
return saveState(state);
|
|
400
|
+
}
|
|
401
|
+
if (applyingTesting) {
|
|
402
|
+
setTestingGateFromVerification(state);
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
if (!gate) {
|
|
406
|
+
throw new Error(`DevCrew has no configured gate for ${state.phase}`);
|
|
407
|
+
}
|
|
408
|
+
state.gates[gate] = "pending";
|
|
409
|
+
state.status = "awaiting_approval";
|
|
336
410
|
}
|
|
337
|
-
state.gates[gate] = "pending";
|
|
338
|
-
state.status = "awaiting_approval";
|
|
339
411
|
return saveState(state);
|
|
340
412
|
}
|
|
341
413
|
export async function startOrchestratedWorkflow(input, runner = runRole) {
|
|
@@ -344,30 +416,137 @@ export async function startOrchestratedWorkflow(input, runner = runRole) {
|
|
|
344
416
|
}
|
|
345
417
|
export async function continueOrchestratedWorkflow(input, runner = runRole) {
|
|
346
418
|
const state = await getWorkflowStatus(input);
|
|
347
|
-
if (state.status === "awaiting_approval" ||
|
|
419
|
+
if (state.status === "awaiting_approval" ||
|
|
420
|
+
state.status === "awaiting_input" ||
|
|
421
|
+
state.status === "awaiting_execution" ||
|
|
422
|
+
state.status === "complete") {
|
|
348
423
|
return state;
|
|
349
424
|
}
|
|
350
425
|
return runCurrentPhaseRole(state, runner);
|
|
351
426
|
}
|
|
352
|
-
export async function
|
|
353
|
-
const before = await
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
before.
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
427
|
+
export async function approveOrchestratedWorkflow(input) {
|
|
428
|
+
const before = await validateWorkflowApproval(input);
|
|
429
|
+
const promotingTesting = input.gate === "testing" &&
|
|
430
|
+
before.executionMode === "apply" &&
|
|
431
|
+
before.phase === "testing" &&
|
|
432
|
+
before.status === "awaiting_approval" &&
|
|
433
|
+
before.gates.testing === "pending";
|
|
434
|
+
const cleaningCompletedPromotion = input.gate === "testing" &&
|
|
435
|
+
before.executionMode === "apply" &&
|
|
436
|
+
before.gates.testing === "approved" &&
|
|
437
|
+
before.executionWorkspace !== undefined;
|
|
438
|
+
async function cleanupAfterApproval(state) {
|
|
439
|
+
try {
|
|
440
|
+
await cleanupExecutionWorkspace(state);
|
|
441
|
+
}
|
|
442
|
+
catch {
|
|
443
|
+
return state;
|
|
444
|
+
}
|
|
445
|
+
state.executionWorkspace = undefined;
|
|
446
|
+
try {
|
|
447
|
+
return await saveState(state);
|
|
448
|
+
}
|
|
449
|
+
catch {
|
|
450
|
+
return state;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (cleaningCompletedPromotion) {
|
|
454
|
+
return cleanupAfterApproval(before);
|
|
455
|
+
}
|
|
456
|
+
if (promotingTesting) {
|
|
457
|
+
if (before.verificationStatus === "failed" && !before.verificationWaiver) {
|
|
458
|
+
throw new Error("Failed verification cannot be promoted without an explicit verification waiver");
|
|
459
|
+
}
|
|
460
|
+
await promoteExecutionChanges(before);
|
|
461
|
+
let approved;
|
|
462
|
+
try {
|
|
463
|
+
approved = await approveWorkflow(input);
|
|
464
|
+
}
|
|
465
|
+
catch (error) {
|
|
466
|
+
try {
|
|
467
|
+
await rollbackPromotedExecutionChanges(before);
|
|
468
|
+
}
|
|
469
|
+
catch (rollbackError) {
|
|
470
|
+
const detail = rollbackError instanceof Error ? rollbackError.message : String(rollbackError);
|
|
471
|
+
throw new Error(`DevCrew approval failed and promoted patch rollback failed: ${detail}`, { cause: error });
|
|
472
|
+
}
|
|
473
|
+
throw error;
|
|
474
|
+
}
|
|
475
|
+
return cleanupAfterApproval(approved);
|
|
476
|
+
}
|
|
477
|
+
return approveWorkflow(input);
|
|
478
|
+
}
|
|
479
|
+
export async function waiveOrchestratedVerification(input) {
|
|
480
|
+
const state = await waiveVerificationWorkflow(input);
|
|
481
|
+
const reportPath = state.artifacts["test-report"];
|
|
482
|
+
if (reportPath) {
|
|
483
|
+
const report = await readFile(reportPath, "utf8");
|
|
484
|
+
await writeFile(reportPath, appendExecutionSections("test-report", report, state), "utf8");
|
|
363
485
|
}
|
|
364
486
|
return state;
|
|
365
487
|
}
|
|
488
|
+
export async function completeOrchestratedExecution(input) {
|
|
489
|
+
const state = await getWorkflowStatus(input);
|
|
490
|
+
const instruction = state.executionInstruction;
|
|
491
|
+
if (state.executionMode !== "apply" ||
|
|
492
|
+
state.executionPolicy !== "interactive-host" ||
|
|
493
|
+
state.status !== "awaiting_execution" ||
|
|
494
|
+
!instruction ||
|
|
495
|
+
instruction.phase !== state.phase ||
|
|
496
|
+
!state.executionWorkspace ||
|
|
497
|
+
instruction.workspacePath !== state.executionWorkspace.path) {
|
|
498
|
+
throw new Error("DevCrew is not awaiting an interactive-host execution completion");
|
|
499
|
+
}
|
|
500
|
+
const summary = parseCompletionSummary(input.summary);
|
|
501
|
+
if (state.phase === "execution") {
|
|
502
|
+
if (input.verification !== undefined) {
|
|
503
|
+
throw new Error("verification can only be submitted after interactive-host testing");
|
|
504
|
+
}
|
|
505
|
+
const captured = await captureExecutionChanges(state.executionWorkspace);
|
|
506
|
+
state.changedFiles = captured.changedFiles;
|
|
507
|
+
state.implementationDiff = captured.patch;
|
|
508
|
+
state.lintResults = [];
|
|
509
|
+
state.roles.push(hostCompletionResult(state, summary));
|
|
510
|
+
state.executionInstruction = undefined;
|
|
511
|
+
await writeImplementationReview(state);
|
|
512
|
+
state.phase = "review";
|
|
513
|
+
state.status = "ready";
|
|
514
|
+
return saveState(state);
|
|
515
|
+
}
|
|
516
|
+
if (state.phase !== "testing") {
|
|
517
|
+
throw new Error(`Cannot complete interactive-host work during ${state.phase}`);
|
|
518
|
+
}
|
|
519
|
+
state.verification = parseCompletionVerification(input.verification);
|
|
520
|
+
state.verificationStatus = verificationStatusFor(state.verification);
|
|
521
|
+
const captured = await captureExecutionChanges(state.executionWorkspace);
|
|
522
|
+
state.changedFiles = captured.changedFiles;
|
|
523
|
+
state.implementationDiff = captured.patch;
|
|
524
|
+
await writeImplementationReview(state);
|
|
525
|
+
const result = hostCompletionResult(state, summary);
|
|
526
|
+
const artifact = artifactForPhase(state.phase);
|
|
527
|
+
const markdown = appendExecutionSections(artifact, result.markdown, state);
|
|
528
|
+
state.roles.push({ ...result, markdown });
|
|
529
|
+
state.artifacts[artifact] = await writeMarkdownArtifact(state, artifact, markdown);
|
|
530
|
+
state.executionInstruction = undefined;
|
|
531
|
+
setTestingGateFromVerification(state);
|
|
532
|
+
return saveState(state);
|
|
533
|
+
}
|
|
534
|
+
export async function rejectOrchestratedWorkflow(input) {
|
|
535
|
+
return rejectWorkflow(input);
|
|
536
|
+
}
|
|
366
537
|
export async function answerOrchestratedWorkflow(input, runner = runRole) {
|
|
538
|
+
const before = await getWorkflowStatus(input);
|
|
367
539
|
const state = await answerWorkflow(input, { skipArtifactWrite: true });
|
|
368
|
-
|
|
540
|
+
if (before.executionMode === "apply" &&
|
|
541
|
+
before.phase === "testing" &&
|
|
542
|
+
before.gates.testing === "rejected") {
|
|
543
|
+
state.phase = "execution";
|
|
544
|
+
state.status = "ready";
|
|
545
|
+
state.gates.testing = "not_started";
|
|
546
|
+
return saveState(state);
|
|
547
|
+
}
|
|
369
548
|
const role = roleForPhase(state.phase);
|
|
370
|
-
if (!
|
|
549
|
+
if (!role) {
|
|
371
550
|
return state;
|
|
372
551
|
}
|
|
373
552
|
// Re-run the current phase role so the artifact reflects the new answer and
|