@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,11 +1,12 @@
|
|
|
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
|
|
|
5
5
|
import { runRole } from "../../adapters/src/index.js";
|
|
6
6
|
import type { RoleRunInput } from "../../adapters/src/index.js";
|
|
7
7
|
import {
|
|
8
8
|
answerWorkflow,
|
|
9
|
+
approveWorkflow,
|
|
9
10
|
artifactForPhase,
|
|
10
11
|
artifactPath,
|
|
11
12
|
ARTIFACTS,
|
|
@@ -14,13 +15,18 @@ import {
|
|
|
14
15
|
discoverVerifyCommands,
|
|
15
16
|
gateForPhase,
|
|
16
17
|
getWorkflowStatus,
|
|
18
|
+
nextPhaseAfterGate,
|
|
17
19
|
readConfig,
|
|
18
20
|
rejectWorkflow,
|
|
19
21
|
renderArtifact,
|
|
20
22
|
saveState,
|
|
21
23
|
startWorkflow,
|
|
24
|
+
validateWorkflowApproval,
|
|
25
|
+
waiveVerificationWorkflow,
|
|
22
26
|
type AnswerWorkflowInput,
|
|
27
|
+
type ApproveWorkflowInput,
|
|
23
28
|
type ArtifactName,
|
|
29
|
+
type CompleteExecutionInput,
|
|
24
30
|
type Phase,
|
|
25
31
|
type RejectWorkflowInput,
|
|
26
32
|
type RoleResult,
|
|
@@ -28,7 +34,16 @@ import {
|
|
|
28
34
|
type RunState,
|
|
29
35
|
type StartWorkflowInput,
|
|
30
36
|
type VerificationResult,
|
|
37
|
+
type VerificationStatus,
|
|
38
|
+
type WaiveVerificationInput,
|
|
31
39
|
} from "../../core/src/index.js";
|
|
40
|
+
import {
|
|
41
|
+
captureExecutionChanges,
|
|
42
|
+
cleanupExecutionWorkspace,
|
|
43
|
+
ensureExecutionWorkspace,
|
|
44
|
+
promoteExecutionChanges,
|
|
45
|
+
rollbackPromotedExecutionChanges,
|
|
46
|
+
} from "./worktree.js";
|
|
32
47
|
|
|
33
48
|
// Hard cap so a hung apply/verify command cannot block the serialized MCP loop.
|
|
34
49
|
const COMMAND_TIMEOUT_MS = 300_000;
|
|
@@ -40,6 +55,8 @@ function roleForPhase(phase: Phase): RoleResult["role"] | undefined {
|
|
|
40
55
|
requirements: "pm",
|
|
41
56
|
architecture: "architect",
|
|
42
57
|
implementation: "implementer",
|
|
58
|
+
execution: "implementer",
|
|
59
|
+
review: "architect",
|
|
43
60
|
testing: "tester",
|
|
44
61
|
};
|
|
45
62
|
return roles[phase];
|
|
@@ -66,7 +83,7 @@ function priorArtifactNamesForPhase(phase: Phase): ArtifactName[] {
|
|
|
66
83
|
}
|
|
67
84
|
|
|
68
85
|
async function writeMarkdownArtifact(state: RunState, artifact: ArtifactName, markdown: string): Promise<string> {
|
|
69
|
-
const path = artifactPath(state.cwd, state.runId, artifact);
|
|
86
|
+
const path = artifactPath(state.cwd, state.runId, artifact, state.artifactDirectory);
|
|
70
87
|
await mkdir(dirname(path), { recursive: true });
|
|
71
88
|
await writeFile(path, markdown, "utf8");
|
|
72
89
|
return path;
|
|
@@ -159,116 +176,6 @@ export async function runShellCommand(
|
|
|
159
176
|
});
|
|
160
177
|
}
|
|
161
178
|
|
|
162
|
-
async function runGit(args: string[], cwd: string): Promise<{ exitCode: number; stdout: string }> {
|
|
163
|
-
return new Promise((resolveResult) => {
|
|
164
|
-
const child = spawn("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
165
|
-
let stdout = "";
|
|
166
|
-
let stderr = "";
|
|
167
|
-
child.stdout.on("data", (chunk: Buffer) => {
|
|
168
|
-
stdout += chunk.toString("utf8");
|
|
169
|
-
});
|
|
170
|
-
child.stderr.on("data", (chunk: Buffer) => {
|
|
171
|
-
stderr += chunk.toString("utf8");
|
|
172
|
-
});
|
|
173
|
-
child.on("error", () => resolveResult({ exitCode: 1, stdout: "" }));
|
|
174
|
-
child.on("close", (code) => resolveResult({ exitCode: code ?? 1, stdout: stdout || stderr }));
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
async function listChangedLines(cwd: string, options: { requireGit?: boolean } = {}): Promise<string[]> {
|
|
179
|
-
const result = await runShellCommand("git status --porcelain -uall", cwd, 30_000);
|
|
180
|
-
if (result.exitCode !== 0) {
|
|
181
|
-
if (options.requireGit) {
|
|
182
|
-
throw new Error("DevCrew apply mode requires a git repository so implementation changes can be reviewed and reverted.");
|
|
183
|
-
}
|
|
184
|
-
return [];
|
|
185
|
-
}
|
|
186
|
-
return result.output
|
|
187
|
-
.split("\n")
|
|
188
|
-
.map((line) => line.trimEnd())
|
|
189
|
-
.filter(Boolean)
|
|
190
|
-
.filter((line) => {
|
|
191
|
-
const path = line.slice(3);
|
|
192
|
-
return !path.startsWith(".devcrew/") && !path.startsWith("docs/devcrew/");
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
async function assertCleanApplyWorkspace(cwd: string): Promise<void> {
|
|
197
|
-
const changedLines = await listChangedLines(cwd, { requireGit: true });
|
|
198
|
-
if (changedLines.length === 0) {
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
const visibleLines = changedLines.slice(0, 20).join(", ");
|
|
202
|
-
const suffix = changedLines.length > 20 ? `, and ${changedLines.length - 20} more` : "";
|
|
203
|
-
throw new Error(
|
|
204
|
-
`DevCrew apply mode requires a clean working tree before implementation. Commit, stash, or remove these files: ${visibleLines}${suffix}`,
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
async function collectImplementationDiff(cwd: string): Promise<string> {
|
|
209
|
-
const result = await runShellCommand("git diff --no-ext-diff --", cwd, 30_000);
|
|
210
|
-
if (result.exitCode !== 0) {
|
|
211
|
-
return "";
|
|
212
|
-
}
|
|
213
|
-
return result.output;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Files attributable to this run are the porcelain lines that appeared (or
|
|
217
|
-
// changed status) since the baseline captured before the role executed. This
|
|
218
|
-
// keeps a user's pre-existing uncommitted edits out of the changed-files list.
|
|
219
|
-
// The porcelain status prefix is preserved for review (?? = new, M = modified).
|
|
220
|
-
export function changedSinceBaseline(baseline: string[], current: string[]): string[] {
|
|
221
|
-
const baselineLines = new Set(baseline);
|
|
222
|
-
return current.filter((line) => !baselineLines.has(line));
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Strip the two-character porcelain status plus its separating space. For
|
|
226
|
-
// rename entries ("R old -> new") the destination path is what remains
|
|
227
|
-
// relevant, so keep only the post-arrow path when present.
|
|
228
|
-
export function porcelainPath(line: string): string {
|
|
229
|
-
const raw = line.slice(3).trim();
|
|
230
|
-
const arrow = raw.indexOf(" -> ");
|
|
231
|
-
return arrow >= 0 ? raw.slice(arrow + 4) : raw;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
export interface RevertDeps {
|
|
235
|
-
runGit?: (args: string[], cwd: string) => Promise<{ exitCode: number; stdout: string }>;
|
|
236
|
-
removeFile?: (absolutePath: string) => Promise<void>;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Roll back implementer edits when an apply-mode gate is rejected.
|
|
240
|
-
// Only the files this run introduced/changed are reverted, so unrelated work in
|
|
241
|
-
// the repository is preserved. Tracked files are restored from HEAD; files that
|
|
242
|
-
// did not exist in HEAD are deleted. Failures are surfaced to the MCP caller.
|
|
243
|
-
export async function revertChangedFiles(
|
|
244
|
-
cwd: string,
|
|
245
|
-
changedFiles: string[],
|
|
246
|
-
deps: RevertDeps = {},
|
|
247
|
-
): Promise<void> {
|
|
248
|
-
const git = deps.runGit ?? runGit;
|
|
249
|
-
const removeFile = deps.removeFile ?? ((absolutePath: string) => rm(absolutePath, { force: true }));
|
|
250
|
-
for (const line of changedFiles) {
|
|
251
|
-
const file = porcelainPath(line);
|
|
252
|
-
if (!file) {
|
|
253
|
-
continue;
|
|
254
|
-
}
|
|
255
|
-
const tracked = await git(["cat-file", "-e", `HEAD:${file}`], cwd);
|
|
256
|
-
if (tracked.exitCode === 0) {
|
|
257
|
-
const restored = await git(["restore", "--source=HEAD", "--", file], cwd);
|
|
258
|
-
if (restored.exitCode !== 0) {
|
|
259
|
-
throw new Error(`Failed to restore ${file}: ${restored.stdout || "git restore failed"}`);
|
|
260
|
-
}
|
|
261
|
-
} else {
|
|
262
|
-
try {
|
|
263
|
-
await removeFile(resolvePath(cwd, file));
|
|
264
|
-
} catch (error) {
|
|
265
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
266
|
-
throw new Error(`Failed to remove ${file}: ${message}`);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
179
|
async function runCommands(commands: string[], cwd: string): Promise<VerificationResult[]> {
|
|
273
180
|
const results: VerificationResult[] = [];
|
|
274
181
|
for (const command of commands) {
|
|
@@ -293,30 +200,23 @@ function uniqueCommands(commands: string[]): string[] {
|
|
|
293
200
|
// Tester verification runs the normal verification path first, then coverage
|
|
294
201
|
// as supplemental evidence. Configured commands win per category; otherwise
|
|
295
202
|
// DevCrew discovers common project commands.
|
|
296
|
-
async function runConfiguredVerification(state: RunState): Promise<VerificationResult[]> {
|
|
203
|
+
async function runConfiguredVerification(state: RunState, commandCwd: string): Promise<VerificationResult[]> {
|
|
297
204
|
const config = await readConfig(state.cwd);
|
|
298
205
|
const configuredVerify = config.verifyCommands.filter((command) => command.trim().length > 0);
|
|
299
206
|
const configuredCoverage = (config.coverageCommands ?? []).filter((command) => command.trim().length > 0);
|
|
300
|
-
const verifyCommands = configuredVerify.length > 0 ? configuredVerify : await discoverVerifyCommands(
|
|
301
|
-
const coverageCommands = configuredCoverage.length > 0 ? configuredCoverage : await discoverCoverageCommands(
|
|
207
|
+
const verifyCommands = configuredVerify.length > 0 ? configuredVerify : await discoverVerifyCommands(commandCwd);
|
|
208
|
+
const coverageCommands = configuredCoverage.length > 0 ? configuredCoverage : await discoverCoverageCommands(commandCwd);
|
|
302
209
|
const commands = uniqueCommands([...verifyCommands, ...coverageCommands]);
|
|
303
|
-
return runCommands(commands,
|
|
210
|
+
return runCommands(commands, commandCwd);
|
|
304
211
|
}
|
|
305
212
|
|
|
306
213
|
// Implementer apply runs lint/format/typecheck so reviewers see standards
|
|
307
214
|
// compliance evidence. Configured lintCommands win, otherwise discover them.
|
|
308
|
-
async function runConfiguredLint(state: RunState): Promise<VerificationResult[]> {
|
|
215
|
+
async function runConfiguredLint(state: RunState, commandCwd: string): Promise<VerificationResult[]> {
|
|
309
216
|
const config = await readConfig(state.cwd);
|
|
310
217
|
const configuredLint = (config.lintCommands ?? []).filter((command) => command.trim().length > 0);
|
|
311
|
-
const commands = configuredLint.length > 0 ? configuredLint : await discoverLintCommands(
|
|
312
|
-
return runCommands(commands,
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function changedFilesBlock(changedFiles: string[]): string {
|
|
316
|
-
if (changedFiles.length === 0) {
|
|
317
|
-
return "No changed files were detected.";
|
|
318
|
-
}
|
|
319
|
-
return changedFiles.map((file) => `- ${file}`).join("\n");
|
|
218
|
+
const commands = configuredLint.length > 0 ? configuredLint : await discoverLintCommands(commandCwd);
|
|
219
|
+
return runCommands(commands, commandCwd);
|
|
320
220
|
}
|
|
321
221
|
|
|
322
222
|
function verificationBlock(results: VerificationResult[]): string {
|
|
@@ -331,34 +231,172 @@ function verificationBlock(results: VerificationResult[]): string {
|
|
|
331
231
|
.join("\n\n");
|
|
332
232
|
}
|
|
333
233
|
|
|
334
|
-
function
|
|
234
|
+
function verificationStatusFor(results: VerificationResult[]): VerificationStatus {
|
|
335
235
|
if (results.length === 0) {
|
|
336
|
-
return "
|
|
236
|
+
return "not_run";
|
|
337
237
|
}
|
|
338
|
-
return
|
|
238
|
+
return results.every((result) => result.exitCode === 0) ? "passed" : "failed";
|
|
339
239
|
}
|
|
340
240
|
|
|
341
|
-
function
|
|
342
|
-
if (
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
241
|
+
function setTestingGateFromVerification(state: RunState): void {
|
|
242
|
+
if (state.verificationStatus === "failed") {
|
|
243
|
+
state.gates.testing = "rejected";
|
|
244
|
+
state.status = "awaiting_input";
|
|
245
|
+
state.feedback.push({
|
|
246
|
+
gate: "testing",
|
|
247
|
+
message: "Automated verification failed. Inspect the test report, revise the implementation, or record an explicit verification waiver with its reason.",
|
|
248
|
+
createdAt: now(),
|
|
249
|
+
});
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
state.gates.testing = "pending";
|
|
253
|
+
state.status = "awaiting_approval";
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function executionInstructions(state: RunState, phase: "execution" | "testing", workspacePath: string): string {
|
|
257
|
+
if (phase === "execution") {
|
|
258
|
+
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.`;
|
|
259
|
+
}
|
|
260
|
+
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.`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function hostCompletionResult(state: RunState, summary: string): RoleResult {
|
|
264
|
+
const role = state.phase === "execution" ? "implementer" : "tester";
|
|
265
|
+
const artifact = artifactForPhase(state.phase);
|
|
266
|
+
return {
|
|
267
|
+
role,
|
|
268
|
+
backend: state.backend,
|
|
269
|
+
summary: `${role} completed through the native ${state.host} host: ${summary}`,
|
|
270
|
+
markdown: `${renderArtifact(artifact, state).trim()}\n\n## Native Host Summary\n\n${summary}\n`,
|
|
271
|
+
usedFallback: false,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function parseCompletionSummary(value: unknown): string {
|
|
276
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
277
|
+
throw new Error("summary must be a non-empty string");
|
|
278
|
+
}
|
|
279
|
+
return value.trim();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function parseCompletionVerification(value: unknown): VerificationResult[] {
|
|
283
|
+
if (value === undefined) {
|
|
284
|
+
return [];
|
|
285
|
+
}
|
|
286
|
+
if (!Array.isArray(value)) {
|
|
287
|
+
throw new Error("verification must be an array");
|
|
288
|
+
}
|
|
289
|
+
return value.map((entry, index) => {
|
|
290
|
+
if (
|
|
291
|
+
!entry ||
|
|
292
|
+
typeof entry !== "object" ||
|
|
293
|
+
typeof entry.command !== "string" ||
|
|
294
|
+
entry.command.trim().length === 0 ||
|
|
295
|
+
!Number.isInteger(entry.exitCode) ||
|
|
296
|
+
typeof entry.output !== "string" ||
|
|
297
|
+
typeof entry.startedAt !== "string" ||
|
|
298
|
+
typeof entry.completedAt !== "string"
|
|
299
|
+
) {
|
|
300
|
+
throw new Error(`verification[${index}] must include command, integer exitCode, output, startedAt, and completedAt`);
|
|
346
301
|
}
|
|
347
|
-
|
|
348
|
-
|
|
302
|
+
return {
|
|
303
|
+
command: entry.command.trim(),
|
|
304
|
+
exitCode: entry.exitCode,
|
|
305
|
+
output: entry.output,
|
|
306
|
+
startedAt: entry.startedAt,
|
|
307
|
+
completedAt: entry.completedAt,
|
|
308
|
+
};
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function appendExecutionSections(artifact: ArtifactName, markdown: string, state: RunState): string {
|
|
313
|
+
if (artifact === "architecture-review" && state.architectureReview) {
|
|
314
|
+
let content = markdown.trim();
|
|
315
|
+
if (!content.includes("## Review Decision")) {
|
|
316
|
+
content += `\n\n## Review Decision\n\nDecision: ${state.architectureReview.decision}\n\nSummary: ${state.architectureReview.summary}`;
|
|
349
317
|
}
|
|
350
|
-
return `${
|
|
318
|
+
return `${content}\n`;
|
|
351
319
|
}
|
|
352
|
-
if (artifact === "test-report"
|
|
353
|
-
|
|
320
|
+
if (artifact === "test-report") {
|
|
321
|
+
let content = markdown.trim();
|
|
322
|
+
if (!content.includes("## Acceptance Evidence")) {
|
|
323
|
+
content += `\n\n## Acceptance Evidence\n\n${verificationBlock(state.verification)}`;
|
|
324
|
+
}
|
|
325
|
+
if (!content.includes("## Verification Outcome")) {
|
|
326
|
+
content += `\n\n## Verification Outcome\n\nStatus: ${state.verificationStatus}`;
|
|
327
|
+
}
|
|
328
|
+
if (state.verificationWaiver && !content.includes("## Verification Waiver")) {
|
|
329
|
+
content += `\n\n## Verification Waiver\n\nReason: ${state.verificationWaiver.reason}\nRecorded At: ${state.verificationWaiver.createdAt}`;
|
|
330
|
+
}
|
|
331
|
+
return `${content}\n`;
|
|
354
332
|
}
|
|
355
333
|
return markdown;
|
|
356
334
|
}
|
|
357
335
|
|
|
336
|
+
async function writeImplementationReview(state: RunState): Promise<void> {
|
|
337
|
+
state.artifacts["implementation-review"] = await writeMarkdownArtifact(
|
|
338
|
+
state,
|
|
339
|
+
"implementation-review",
|
|
340
|
+
renderArtifact("implementation-review", state),
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
358
344
|
async function runCurrentPhaseRole(state: RunState, runner: RoleRunner = runRole): Promise<RunState> {
|
|
359
|
-
|
|
345
|
+
if (state.phase === "execution") {
|
|
346
|
+
if (state.executionMode !== "apply") {
|
|
347
|
+
throw new Error("DevCrew execution phase requires apply mode");
|
|
348
|
+
}
|
|
349
|
+
const workspace = await ensureExecutionWorkspace(state);
|
|
350
|
+
state.executionWorkspace = workspace;
|
|
351
|
+
state.verification = [];
|
|
352
|
+
state.verificationStatus = "not_run";
|
|
353
|
+
delete state.verificationWaiver;
|
|
354
|
+
delete state.artifacts["test-report"];
|
|
355
|
+
await saveState(state);
|
|
356
|
+
|
|
357
|
+
if (state.executionPolicy === "interactive-host") {
|
|
358
|
+
state.executionInstruction = {
|
|
359
|
+
phase: "execution",
|
|
360
|
+
workspacePath: workspace.path,
|
|
361
|
+
instructions: executionInstructions(state, "execution", workspace.path),
|
|
362
|
+
createdAt: now(),
|
|
363
|
+
};
|
|
364
|
+
state.status = "awaiting_execution";
|
|
365
|
+
return saveState(state);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const result = await runner({
|
|
369
|
+
backend: state.backend,
|
|
370
|
+
role: "implementer",
|
|
371
|
+
phase: "execution",
|
|
372
|
+
request: state.request,
|
|
373
|
+
mode: state.mode,
|
|
374
|
+
executionMode: "apply",
|
|
375
|
+
executionPolicy: state.executionPolicy,
|
|
376
|
+
cwd: workspace.path,
|
|
377
|
+
standards: state.standards.combined,
|
|
378
|
+
artifactPath: artifactPath(state.cwd, state.runId, "implementation-review", state.artifactDirectory),
|
|
379
|
+
answers: state.answers.map((entry) => entry.answer),
|
|
380
|
+
feedback: state.feedback.map((entry) => `${entry.gate}: ${entry.message}`),
|
|
381
|
+
priorArtifacts: await readPriorArtifacts(state),
|
|
382
|
+
});
|
|
383
|
+
await captureExecutionChanges(workspace);
|
|
384
|
+
state.lintResults = await runConfiguredLint(state, workspace.path);
|
|
385
|
+
const captured = await captureExecutionChanges(workspace);
|
|
386
|
+
state.changedFiles = captured.changedFiles;
|
|
387
|
+
state.implementationDiff = captured.patch;
|
|
388
|
+
state.roles.push(result);
|
|
389
|
+
state.executionInstruction = undefined;
|
|
390
|
+
await writeImplementationReview(state);
|
|
391
|
+
state.phase = "review";
|
|
392
|
+
state.status = "ready";
|
|
393
|
+
return saveState(state);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const configuredGate = gateForPhase(state.phase);
|
|
397
|
+
const gate = gateForPhase(state.phase, state.enabledGates);
|
|
360
398
|
const role = roleForPhase(state.phase);
|
|
361
|
-
if (!
|
|
399
|
+
if (!role) {
|
|
362
400
|
const artifact = artifactForPhase(state.phase);
|
|
363
401
|
const markdown = renderArtifact(artifact, state);
|
|
364
402
|
state.artifacts[artifact] = await writeMarkdownArtifact(state, artifact, markdown);
|
|
@@ -368,17 +406,24 @@ async function runCurrentPhaseRole(state: RunState, runner: RoleRunner = runRole
|
|
|
368
406
|
}
|
|
369
407
|
|
|
370
408
|
const artifact = artifactForPhase(state.phase);
|
|
371
|
-
const path = artifactPath(state.cwd, state.runId, artifact);
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
409
|
+
const path = artifactPath(state.cwd, state.runId, artifact, state.artifactDirectory);
|
|
410
|
+
const applyingTesting = state.executionMode === "apply" && state.phase === "testing";
|
|
411
|
+
const roleCwd = applyingTesting ? state.executionWorkspace?.path : state.cwd;
|
|
412
|
+
if (!roleCwd) {
|
|
413
|
+
throw new Error("DevCrew apply testing requires an execution workspace");
|
|
414
|
+
}
|
|
415
|
+
state.roles.push(conductorDecision(state, role, gate ?? `${state.phase} automatic transition`));
|
|
416
|
+
|
|
417
|
+
if (applyingTesting && state.executionPolicy === "interactive-host") {
|
|
418
|
+
state.executionInstruction = {
|
|
419
|
+
phase: "testing",
|
|
420
|
+
workspacePath: roleCwd,
|
|
421
|
+
instructions: executionInstructions(state, "testing", roleCwd),
|
|
422
|
+
createdAt: now(),
|
|
423
|
+
};
|
|
424
|
+
state.status = "awaiting_execution";
|
|
425
|
+
return saveState(state);
|
|
379
426
|
}
|
|
380
|
-
|
|
381
|
-
state.roles.push(conductorDecision(state, role, gate));
|
|
382
427
|
|
|
383
428
|
const result = await runner({
|
|
384
429
|
backend: state.backend,
|
|
@@ -387,7 +432,8 @@ async function runCurrentPhaseRole(state: RunState, runner: RoleRunner = runRole
|
|
|
387
432
|
request: state.request,
|
|
388
433
|
mode: state.mode,
|
|
389
434
|
executionMode: state.executionMode,
|
|
390
|
-
|
|
435
|
+
executionPolicy: state.executionPolicy,
|
|
436
|
+
cwd: roleCwd,
|
|
391
437
|
standards: state.standards.combined,
|
|
392
438
|
artifactPath: path,
|
|
393
439
|
answers: state.answers.map((entry) => entry.answer),
|
|
@@ -395,13 +441,24 @@ async function runCurrentPhaseRole(state: RunState, runner: RoleRunner = runRole
|
|
|
395
441
|
priorArtifacts: await readPriorArtifacts(state),
|
|
396
442
|
});
|
|
397
443
|
|
|
398
|
-
if (
|
|
399
|
-
state.
|
|
400
|
-
state.
|
|
401
|
-
|
|
444
|
+
if (applyingTesting && state.executionWorkspace) {
|
|
445
|
+
state.verification = await runConfiguredVerification(state, roleCwd);
|
|
446
|
+
state.verificationStatus = verificationStatusFor(state.verification);
|
|
447
|
+
const captured = await captureExecutionChanges(state.executionWorkspace);
|
|
448
|
+
state.changedFiles = captured.changedFiles;
|
|
449
|
+
state.implementationDiff = captured.patch;
|
|
450
|
+
await writeImplementationReview(state);
|
|
402
451
|
}
|
|
403
|
-
|
|
404
|
-
|
|
452
|
+
|
|
453
|
+
if (state.phase === "review") {
|
|
454
|
+
if (!result.reviewDecision) {
|
|
455
|
+
throw new Error("DevCrew architecture review must return a structured review decision");
|
|
456
|
+
}
|
|
457
|
+
state.architectureReview = {
|
|
458
|
+
decision: result.reviewDecision,
|
|
459
|
+
summary: result.summary,
|
|
460
|
+
reviewedAt: now(),
|
|
461
|
+
};
|
|
405
462
|
}
|
|
406
463
|
|
|
407
464
|
// When the backend cannot run a real SDK we keep a single deterministic
|
|
@@ -410,15 +467,37 @@ async function runCurrentPhaseRole(state: RunState, runner: RoleRunner = runRole
|
|
|
410
467
|
const markdown = appendExecutionSections(artifact, baseMarkdown, state);
|
|
411
468
|
state.roles.push({ ...result, markdown });
|
|
412
469
|
state.artifacts[artifact] = await writeMarkdownArtifact(state, artifact, markdown);
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
);
|
|
419
|
-
}
|
|
420
|
-
state.
|
|
421
|
-
|
|
470
|
+
state.executionInstruction = undefined;
|
|
471
|
+
if (state.phase === "requirements" && result.questions && result.questions.length > 0) {
|
|
472
|
+
state.pendingQuestions = result.questions;
|
|
473
|
+
state.gates.requirements = "not_started";
|
|
474
|
+
state.status = "awaiting_input";
|
|
475
|
+
return saveState(state);
|
|
476
|
+
}
|
|
477
|
+
if (state.phase === "review" && state.architectureReview?.decision === "changes_required") {
|
|
478
|
+
state.gates["implementation-review"] = "rejected";
|
|
479
|
+
state.status = "awaiting_input";
|
|
480
|
+
state.feedback.push({
|
|
481
|
+
gate: "implementation-review",
|
|
482
|
+
message: state.architectureReview.summary,
|
|
483
|
+
createdAt: now(),
|
|
484
|
+
});
|
|
485
|
+
return saveState(state);
|
|
486
|
+
}
|
|
487
|
+
if (!gate && configuredGate) {
|
|
488
|
+
state.phase = nextPhaseAfterGate(state, configuredGate);
|
|
489
|
+
state.status = "ready";
|
|
490
|
+
return saveState(state);
|
|
491
|
+
}
|
|
492
|
+
if (applyingTesting) {
|
|
493
|
+
setTestingGateFromVerification(state);
|
|
494
|
+
} else {
|
|
495
|
+
if (!gate) {
|
|
496
|
+
throw new Error(`DevCrew has no configured gate for ${state.phase}`);
|
|
497
|
+
}
|
|
498
|
+
state.gates[gate] = "pending";
|
|
499
|
+
state.status = "awaiting_approval";
|
|
500
|
+
}
|
|
422
501
|
return saveState(state);
|
|
423
502
|
}
|
|
424
503
|
|
|
@@ -429,37 +508,153 @@ export async function startOrchestratedWorkflow(input: StartWorkflowInput, runne
|
|
|
429
508
|
|
|
430
509
|
export async function continueOrchestratedWorkflow(input: RunRef, runner: RoleRunner = runRole): Promise<RunState> {
|
|
431
510
|
const state = await getWorkflowStatus(input);
|
|
432
|
-
if (
|
|
511
|
+
if (
|
|
512
|
+
state.status === "awaiting_approval" ||
|
|
513
|
+
state.status === "awaiting_input" ||
|
|
514
|
+
state.status === "awaiting_execution" ||
|
|
515
|
+
state.status === "complete"
|
|
516
|
+
) {
|
|
433
517
|
return state;
|
|
434
518
|
}
|
|
435
519
|
|
|
436
520
|
return runCurrentPhaseRole(state, runner);
|
|
437
521
|
}
|
|
438
522
|
|
|
439
|
-
export async function
|
|
440
|
-
const before = await
|
|
441
|
-
const
|
|
523
|
+
export async function approveOrchestratedWorkflow(input: ApproveWorkflowInput): Promise<RunState> {
|
|
524
|
+
const before = await validateWorkflowApproval(input);
|
|
525
|
+
const promotingTesting =
|
|
526
|
+
input.gate === "testing" &&
|
|
527
|
+
before.executionMode === "apply" &&
|
|
528
|
+
before.phase === "testing" &&
|
|
529
|
+
before.status === "awaiting_approval" &&
|
|
530
|
+
before.gates.testing === "pending";
|
|
531
|
+
const cleaningCompletedPromotion =
|
|
532
|
+
input.gate === "testing" &&
|
|
533
|
+
before.executionMode === "apply" &&
|
|
534
|
+
before.gates.testing === "approved" &&
|
|
535
|
+
before.executionWorkspace !== undefined;
|
|
536
|
+
|
|
537
|
+
async function cleanupAfterApproval(state: RunState): Promise<RunState> {
|
|
538
|
+
try {
|
|
539
|
+
await cleanupExecutionWorkspace(state);
|
|
540
|
+
} catch {
|
|
541
|
+
return state;
|
|
542
|
+
}
|
|
543
|
+
state.executionWorkspace = undefined;
|
|
544
|
+
try {
|
|
545
|
+
return await saveState(state);
|
|
546
|
+
} catch {
|
|
547
|
+
return state;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (cleaningCompletedPromotion) {
|
|
552
|
+
return cleanupAfterApproval(before);
|
|
553
|
+
}
|
|
554
|
+
if (promotingTesting) {
|
|
555
|
+
if (before.verificationStatus === "failed" && !before.verificationWaiver) {
|
|
556
|
+
throw new Error("Failed verification cannot be promoted without an explicit verification waiver");
|
|
557
|
+
}
|
|
558
|
+
await promoteExecutionChanges(before);
|
|
559
|
+
let approved: RunState;
|
|
560
|
+
try {
|
|
561
|
+
approved = await approveWorkflow(input);
|
|
562
|
+
} catch (error) {
|
|
563
|
+
try {
|
|
564
|
+
await rollbackPromotedExecutionChanges(before);
|
|
565
|
+
} catch (rollbackError) {
|
|
566
|
+
const detail = rollbackError instanceof Error ? rollbackError.message : String(rollbackError);
|
|
567
|
+
throw new Error(`DevCrew approval failed and promoted patch rollback failed: ${detail}`, { cause: error });
|
|
568
|
+
}
|
|
569
|
+
throw error;
|
|
570
|
+
}
|
|
571
|
+
return cleanupAfterApproval(approved);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return approveWorkflow(input);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
export async function waiveOrchestratedVerification(input: WaiveVerificationInput): Promise<RunState> {
|
|
578
|
+
const state = await waiveVerificationWorkflow(input);
|
|
579
|
+
const reportPath = state.artifacts["test-report"];
|
|
580
|
+
if (reportPath) {
|
|
581
|
+
const report = await readFile(reportPath, "utf8");
|
|
582
|
+
await writeFile(reportPath, appendExecutionSections("test-report", report, state), "utf8");
|
|
583
|
+
}
|
|
584
|
+
return state;
|
|
585
|
+
}
|
|
442
586
|
|
|
443
|
-
|
|
444
|
-
|
|
587
|
+
export async function completeOrchestratedExecution(input: CompleteExecutionInput): Promise<RunState> {
|
|
588
|
+
const state = await getWorkflowStatus(input);
|
|
589
|
+
const instruction = state.executionInstruction;
|
|
445
590
|
if (
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
591
|
+
state.executionMode !== "apply" ||
|
|
592
|
+
state.executionPolicy !== "interactive-host" ||
|
|
593
|
+
state.status !== "awaiting_execution" ||
|
|
594
|
+
!instruction ||
|
|
595
|
+
instruction.phase !== state.phase ||
|
|
596
|
+
!state.executionWorkspace ||
|
|
597
|
+
instruction.workspacePath !== state.executionWorkspace.path
|
|
449
598
|
) {
|
|
450
|
-
|
|
451
|
-
|
|
599
|
+
throw new Error("DevCrew is not awaiting an interactive-host execution completion");
|
|
600
|
+
}
|
|
601
|
+
const summary = parseCompletionSummary(input.summary);
|
|
602
|
+
|
|
603
|
+
if (state.phase === "execution") {
|
|
604
|
+
if (input.verification !== undefined) {
|
|
605
|
+
throw new Error("verification can only be submitted after interactive-host testing");
|
|
606
|
+
}
|
|
607
|
+
const captured = await captureExecutionChanges(state.executionWorkspace);
|
|
608
|
+
state.changedFiles = captured.changedFiles;
|
|
609
|
+
state.implementationDiff = captured.patch;
|
|
610
|
+
state.lintResults = [];
|
|
611
|
+
state.roles.push(hostCompletionResult(state, summary));
|
|
612
|
+
state.executionInstruction = undefined;
|
|
613
|
+
await writeImplementationReview(state);
|
|
614
|
+
state.phase = "review";
|
|
615
|
+
state.status = "ready";
|
|
452
616
|
return saveState(state);
|
|
453
617
|
}
|
|
454
618
|
|
|
455
|
-
|
|
619
|
+
if (state.phase !== "testing") {
|
|
620
|
+
throw new Error(`Cannot complete interactive-host work during ${state.phase}`);
|
|
621
|
+
}
|
|
622
|
+
state.verification = parseCompletionVerification(input.verification);
|
|
623
|
+
state.verificationStatus = verificationStatusFor(state.verification);
|
|
624
|
+
const captured = await captureExecutionChanges(state.executionWorkspace);
|
|
625
|
+
state.changedFiles = captured.changedFiles;
|
|
626
|
+
state.implementationDiff = captured.patch;
|
|
627
|
+
await writeImplementationReview(state);
|
|
628
|
+
const result = hostCompletionResult(state, summary);
|
|
629
|
+
const artifact = artifactForPhase(state.phase);
|
|
630
|
+
const markdown = appendExecutionSections(artifact, result.markdown, state);
|
|
631
|
+
state.roles.push({ ...result, markdown });
|
|
632
|
+
state.artifacts[artifact] = await writeMarkdownArtifact(state, artifact, markdown);
|
|
633
|
+
state.executionInstruction = undefined;
|
|
634
|
+
setTestingGateFromVerification(state);
|
|
635
|
+
return saveState(state);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
export async function rejectOrchestratedWorkflow(input: RejectWorkflowInput): Promise<RunState> {
|
|
639
|
+
return rejectWorkflow(input);
|
|
456
640
|
}
|
|
457
641
|
|
|
458
642
|
export async function answerOrchestratedWorkflow(input: AnswerWorkflowInput, runner: RoleRunner = runRole): Promise<RunState> {
|
|
643
|
+
const before = await getWorkflowStatus(input);
|
|
459
644
|
const state = await answerWorkflow(input, { skipArtifactWrite: true });
|
|
460
|
-
|
|
645
|
+
if (
|
|
646
|
+
before.executionMode === "apply" &&
|
|
647
|
+
before.phase === "testing" &&
|
|
648
|
+
before.gates.testing === "rejected"
|
|
649
|
+
) {
|
|
650
|
+
state.phase = "execution";
|
|
651
|
+
state.status = "ready";
|
|
652
|
+
state.gates.testing = "not_started";
|
|
653
|
+
return saveState(state);
|
|
654
|
+
}
|
|
655
|
+
|
|
461
656
|
const role = roleForPhase(state.phase);
|
|
462
|
-
if (!
|
|
657
|
+
if (!role) {
|
|
463
658
|
return state;
|
|
464
659
|
}
|
|
465
660
|
|