@shenlee/devcrew 0.1.2 → 0.1.4
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 +15 -7
- package/README.zh-CN.md +12 -7
- package/dist/packages/adapters/src/index.js +204 -10
- package/dist/packages/core/src/active-run.js +13 -1
- package/dist/packages/core/src/artifacts.js +14 -4
- package/dist/packages/core/src/config.js +88 -17
- package/dist/packages/core/src/index.js +1 -0
- package/dist/packages/core/src/lock.js +89 -0
- package/dist/packages/core/src/paths.js +10 -6
- package/dist/packages/core/src/store.js +40 -0
- package/dist/packages/core/src/types.js +4 -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 +87 -12
- package/dist/packages/orchestrator/src/index.js +317 -18
- package/dist/packages/plugins/src/index.js +2 -32
- package/dist/packages/service/src/tools.js +117 -17
- package/docs/claude-code.md +18 -3
- package/docs/codex.md +22 -5
- package/docs/quickstart.md +1 -1
- package/docs/superpowers/plans/2026-07-13-safety-semantics.md +313 -0
- package/docs/superpowers/plans/2026-07-14-p0-remediation.md +213 -0
- package/docs/superpowers/plans/2026-07-14-reliability-recovery.md +208 -0
- package/docs/superpowers/plans/2026-07-14-structured-role-results.md +231 -0
- package/docs/superpowers/specs/2026-07-13-execution-boundaries-design.md +126 -0
- package/docs/superpowers/specs/2026-07-14-p0-remediation-design.md +52 -0
- package/docs/superpowers/specs/2026-07-14-reliability-recovery-design.md +92 -0
- package/docs/superpowers/specs/2026-07-14-structured-role-results-design.md +96 -0
- package/docs/workflow.md +28 -4
- package/package.json +1 -1
- package/packages/adapters/src/index.ts +250 -13
- package/packages/core/src/active-run.ts +13 -1
- package/packages/core/src/artifacts.ts +16 -4
- package/packages/core/src/config.ts +97 -18
- package/packages/core/src/index.ts +1 -0
- package/packages/core/src/lock.ts +104 -0
- package/packages/core/src/paths.ts +11 -6
- package/packages/core/src/store.ts +45 -1
- package/packages/core/src/types.ts +92 -2
- package/packages/core/src/validation.ts +10 -0
- package/packages/core/src/version.ts +1 -1
- package/packages/core/src/workflow.ts +101 -11
- package/packages/orchestrator/src/index.ts +349 -19
- package/packages/plugins/src/index.ts +2 -44
- package/packages/service/src/tools.ts +126 -15
- package/plugins/devcrew-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/devcrew-codex/.mcp.json +1 -1
- 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 -12
- package/plugins/devcrew-codex/agents/implementer.toml +0 -12
- package/plugins/devcrew-codex/agents/pm.toml +0 -13
- package/plugins/devcrew-codex/agents/tester.toml +0 -12
|
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
4
|
import { runRole } from "../../adapters/src/index.js";
|
|
5
|
-
import { answerWorkflow, approveWorkflow, artifactForPhase, artifactPath, ARTIFACTS, discoverCoverageCommands, discoverLintCommands, discoverVerifyCommands, gateForPhase, getWorkflowStatus, readConfig, rejectWorkflow, renderArtifact, saveState, startWorkflow, validateWorkflowApproval, } from "../../core/src/index.js";
|
|
5
|
+
import { abortWorkflow, answerWorkflow, approveWorkflow, artifactForPhase, artifactPath, ARTIFACTS, discoverCoverageCommands, discoverLintCommands, discoverVerifyCommands, gateForPhase, getWorkflowStatus, nextPhaseAfterGate, readConfig, rejectWorkflow, renderArtifact, saveState, startWorkflow, validateWorkflowApproval, waiveVerificationWorkflow, } from "../../core/src/index.js";
|
|
6
6
|
import { captureExecutionChanges, cleanupExecutionWorkspace, ensureExecutionWorkspace, promoteExecutionChanges, rollbackPromotedExecutionChanges, } from "./worktree.js";
|
|
7
7
|
// Hard cap so a hung apply/verify command cannot block the serialized MCP loop.
|
|
8
8
|
const COMMAND_TIMEOUT_MS = 300_000;
|
|
@@ -12,6 +12,7 @@ function roleForPhase(phase) {
|
|
|
12
12
|
architecture: "architect",
|
|
13
13
|
implementation: "implementer",
|
|
14
14
|
execution: "implementer",
|
|
15
|
+
review: "architect",
|
|
15
16
|
testing: "tester",
|
|
16
17
|
};
|
|
17
18
|
return roles[phase];
|
|
@@ -24,6 +25,7 @@ function conductorDecision(state, role, gate) {
|
|
|
24
25
|
summary,
|
|
25
26
|
markdown: `# Conductor Decision\n\n${summary}\n`,
|
|
26
27
|
usedFallback: false,
|
|
28
|
+
format: "legacy",
|
|
27
29
|
};
|
|
28
30
|
}
|
|
29
31
|
function priorArtifactNamesForPhase(phase) {
|
|
@@ -35,7 +37,7 @@ function priorArtifactNamesForPhase(phase) {
|
|
|
35
37
|
return ARTIFACTS.slice(0, currentIndex);
|
|
36
38
|
}
|
|
37
39
|
async function writeMarkdownArtifact(state, artifact, markdown) {
|
|
38
|
-
const path = artifactPath(state.cwd, state.runId, artifact);
|
|
40
|
+
const path = artifactPath(state.cwd, state.runId, artifact, state.artifactDirectory);
|
|
39
41
|
await mkdir(dirname(path), { recursive: true });
|
|
40
42
|
await writeFile(path, markdown, "utf8");
|
|
41
43
|
return path;
|
|
@@ -162,11 +164,141 @@ function verificationBlock(results) {
|
|
|
162
164
|
.map((result) => `### ${result.command}\n\nExit Code: ${result.exitCode}\n\nOutput:\n\n\`\`\`text\n${result.output || "(no output)"}\n\`\`\``)
|
|
163
165
|
.join("\n\n");
|
|
164
166
|
}
|
|
165
|
-
function
|
|
166
|
-
if (
|
|
167
|
-
return
|
|
167
|
+
function verificationStatusFor(results) {
|
|
168
|
+
if (results.length === 0) {
|
|
169
|
+
return "not_run";
|
|
170
|
+
}
|
|
171
|
+
return results.every((result) => result.exitCode === 0) ? "passed" : "failed";
|
|
172
|
+
}
|
|
173
|
+
function setTestingGateFromVerification(state) {
|
|
174
|
+
if (state.verificationStatus !== "passed") {
|
|
175
|
+
state.gates.testing = "rejected";
|
|
176
|
+
state.status = "awaiting_input";
|
|
177
|
+
state.feedback.push({
|
|
178
|
+
gate: "testing",
|
|
179
|
+
message: state.verificationStatus === "failed"
|
|
180
|
+
? "Automated verification failed. Inspect the test report, revise the implementation, or record an explicit verification waiver with its reason."
|
|
181
|
+
: "No verification evidence was recorded. Configure or run validation, revise the implementation, or record an explicit verification waiver with its reason.",
|
|
182
|
+
createdAt: now(),
|
|
183
|
+
});
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
state.gates.testing = "pending";
|
|
187
|
+
state.status = "awaiting_approval";
|
|
188
|
+
}
|
|
189
|
+
function executionInstructions(state, phase, workspacePath) {
|
|
190
|
+
if (phase === "execution") {
|
|
191
|
+
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.`;
|
|
192
|
+
}
|
|
193
|
+
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.`;
|
|
194
|
+
}
|
|
195
|
+
function hostCompletionResult(state, summary) {
|
|
196
|
+
const role = state.phase === "execution" ? "implementer" : "tester";
|
|
197
|
+
const artifact = artifactForPhase(state.phase);
|
|
198
|
+
return {
|
|
199
|
+
role,
|
|
200
|
+
backend: state.backend,
|
|
201
|
+
summary: `${role} completed through the native ${state.host} host: ${summary}`,
|
|
202
|
+
markdown: `${renderArtifact(artifact, state).trim()}\n\n## Native Host Summary\n\n${summary}\n`,
|
|
203
|
+
usedFallback: false,
|
|
204
|
+
format: "legacy",
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function parseCompletionSummary(value) {
|
|
208
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
209
|
+
throw new Error("summary must be a non-empty string");
|
|
210
|
+
}
|
|
211
|
+
return value.trim();
|
|
212
|
+
}
|
|
213
|
+
function parseCompletionVerification(value) {
|
|
214
|
+
if (value === undefined) {
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
if (!Array.isArray(value)) {
|
|
218
|
+
throw new Error("verification must be an array");
|
|
219
|
+
}
|
|
220
|
+
return value.map((entry, index) => {
|
|
221
|
+
if (!entry ||
|
|
222
|
+
typeof entry !== "object" ||
|
|
223
|
+
typeof entry.command !== "string" ||
|
|
224
|
+
entry.command.trim().length === 0 ||
|
|
225
|
+
!Number.isInteger(entry.exitCode) ||
|
|
226
|
+
typeof entry.output !== "string" ||
|
|
227
|
+
typeof entry.startedAt !== "string" ||
|
|
228
|
+
typeof entry.completedAt !== "string") {
|
|
229
|
+
throw new Error(`verification[${index}] must include command, integer exitCode, output, startedAt, and completedAt`);
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
command: entry.command.trim(),
|
|
233
|
+
exitCode: entry.exitCode,
|
|
234
|
+
output: entry.output,
|
|
235
|
+
startedAt: entry.startedAt,
|
|
236
|
+
completedAt: entry.completedAt,
|
|
237
|
+
};
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
function structuredRoleResultBlock(result) {
|
|
241
|
+
if (result.format !== "structured" || !result.structured) {
|
|
242
|
+
return "";
|
|
243
|
+
}
|
|
244
|
+
const data = result.structured;
|
|
245
|
+
const sections = [
|
|
246
|
+
"## Structured Role Result",
|
|
247
|
+
"",
|
|
248
|
+
`Schema Version: ${data.schemaVersion}`,
|
|
249
|
+
`Summary: ${data.summary}`,
|
|
250
|
+
];
|
|
251
|
+
if (data.questions?.length) {
|
|
252
|
+
sections.push("", "### Questions", "", ...data.questions.map((question) => `- ${question.id}: ${question.prompt}`));
|
|
253
|
+
}
|
|
254
|
+
if (data.decisions?.length) {
|
|
255
|
+
sections.push("", "### Decisions", "", ...data.decisions.map((decision) => `- ${decision}`));
|
|
256
|
+
}
|
|
257
|
+
if (data.changedFiles?.length) {
|
|
258
|
+
sections.push("", "### Changed Files", "", ...data.changedFiles.map((file) => `- ${file}`));
|
|
259
|
+
}
|
|
260
|
+
if (data.evidence.length) {
|
|
261
|
+
sections.push("", "### Command Evidence", "");
|
|
262
|
+
for (const evidence of data.evidence) {
|
|
263
|
+
sections.push(`- \`${evidence.command}\` — exit code ${evidence.exitCode}`);
|
|
264
|
+
if (evidence.output) {
|
|
265
|
+
sections.push("", "```text", evidence.output, "```");
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (data.testCases?.length) {
|
|
270
|
+
sections.push("", "### Test Cases", "", "| ID | Scenario | Type | Expected |", "| --- | --- | --- | --- |");
|
|
271
|
+
sections.push(...data.testCases.map((testCase) => `| ${testCase.id} | ${testCase.scenario} | ${testCase.type} | ${testCase.expected} |`));
|
|
272
|
+
}
|
|
273
|
+
if (data.risks.length) {
|
|
274
|
+
sections.push("", "### Risks", "", ...data.risks.map((risk) => `- ${risk}`));
|
|
275
|
+
}
|
|
276
|
+
return `\n\n${sections.join("\n")}`;
|
|
277
|
+
}
|
|
278
|
+
function appendExecutionSections(artifact, markdown, state, result) {
|
|
279
|
+
let content = markdown.trim();
|
|
280
|
+
if (result?.format === "structured" && !content.includes("## Structured Role Result")) {
|
|
281
|
+
content += structuredRoleResultBlock(result);
|
|
282
|
+
}
|
|
283
|
+
if (artifact === "architecture-review" && state.architectureReview) {
|
|
284
|
+
if (!content.includes("## Review Decision")) {
|
|
285
|
+
content += `\n\n## Review Decision\n\nDecision: ${state.architectureReview.decision}\n\nSummary: ${state.architectureReview.summary}`;
|
|
286
|
+
}
|
|
287
|
+
return `${content}\n`;
|
|
288
|
+
}
|
|
289
|
+
if (artifact === "test-report") {
|
|
290
|
+
if (!content.includes("## Acceptance Evidence")) {
|
|
291
|
+
content += `\n\n## Acceptance Evidence\n\n${verificationBlock(state.verification)}`;
|
|
292
|
+
}
|
|
293
|
+
if (!content.includes("## Verification Outcome")) {
|
|
294
|
+
content += `\n\n## Verification Outcome\n\nStatus: ${state.verificationStatus}`;
|
|
295
|
+
}
|
|
296
|
+
if (state.verificationWaiver && !content.includes("## Verification Waiver")) {
|
|
297
|
+
content += `\n\n## Verification Waiver\n\nReason: ${state.verificationWaiver.reason}\nRecorded At: ${state.verificationWaiver.createdAt}`;
|
|
298
|
+
}
|
|
299
|
+
return `${content}\n`;
|
|
168
300
|
}
|
|
169
|
-
return
|
|
301
|
+
return `${content}\n`;
|
|
170
302
|
}
|
|
171
303
|
async function writeImplementationReview(state) {
|
|
172
304
|
state.artifacts["implementation-review"] = await writeMarkdownArtifact(state, "implementation-review", renderArtifact("implementation-review", state));
|
|
@@ -179,8 +311,20 @@ async function runCurrentPhaseRole(state, runner = runRole) {
|
|
|
179
311
|
const workspace = await ensureExecutionWorkspace(state);
|
|
180
312
|
state.executionWorkspace = workspace;
|
|
181
313
|
state.verification = [];
|
|
314
|
+
state.verificationStatus = "not_run";
|
|
315
|
+
delete state.verificationWaiver;
|
|
182
316
|
delete state.artifacts["test-report"];
|
|
183
317
|
await saveState(state);
|
|
318
|
+
if (state.executionPolicy === "interactive-host") {
|
|
319
|
+
state.executionInstruction = {
|
|
320
|
+
phase: "execution",
|
|
321
|
+
workspacePath: workspace.path,
|
|
322
|
+
instructions: executionInstructions(state, "execution", workspace.path),
|
|
323
|
+
createdAt: now(),
|
|
324
|
+
};
|
|
325
|
+
state.status = "awaiting_execution";
|
|
326
|
+
return saveState(state);
|
|
327
|
+
}
|
|
184
328
|
const result = await runner({
|
|
185
329
|
backend: state.backend,
|
|
186
330
|
role: "implementer",
|
|
@@ -188,9 +332,10 @@ async function runCurrentPhaseRole(state, runner = runRole) {
|
|
|
188
332
|
request: state.request,
|
|
189
333
|
mode: state.mode,
|
|
190
334
|
executionMode: "apply",
|
|
335
|
+
executionPolicy: state.executionPolicy,
|
|
191
336
|
cwd: workspace.path,
|
|
192
337
|
standards: state.standards.combined,
|
|
193
|
-
artifactPath: artifactPath(state.cwd, state.runId, "implementation-review"),
|
|
338
|
+
artifactPath: artifactPath(state.cwd, state.runId, "implementation-review", state.artifactDirectory),
|
|
194
339
|
answers: state.answers.map((entry) => entry.answer),
|
|
195
340
|
feedback: state.feedback.map((entry) => `${entry.gate}: ${entry.message}`),
|
|
196
341
|
priorArtifacts: await readPriorArtifacts(state),
|
|
@@ -201,14 +346,16 @@ async function runCurrentPhaseRole(state, runner = runRole) {
|
|
|
201
346
|
state.changedFiles = captured.changedFiles;
|
|
202
347
|
state.implementationDiff = captured.patch;
|
|
203
348
|
state.roles.push(result);
|
|
349
|
+
state.executionInstruction = undefined;
|
|
204
350
|
await writeImplementationReview(state);
|
|
205
|
-
state.phase = "
|
|
351
|
+
state.phase = "review";
|
|
206
352
|
state.status = "ready";
|
|
207
353
|
return saveState(state);
|
|
208
354
|
}
|
|
209
|
-
const
|
|
355
|
+
const configuredGate = gateForPhase(state.phase);
|
|
356
|
+
const gate = gateForPhase(state.phase, state.enabledGates);
|
|
210
357
|
const role = roleForPhase(state.phase);
|
|
211
|
-
if (!
|
|
358
|
+
if (!role) {
|
|
212
359
|
const artifact = artifactForPhase(state.phase);
|
|
213
360
|
const markdown = renderArtifact(artifact, state);
|
|
214
361
|
state.artifacts[artifact] = await writeMarkdownArtifact(state, artifact, markdown);
|
|
@@ -217,13 +364,23 @@ async function runCurrentPhaseRole(state, runner = runRole) {
|
|
|
217
364
|
return saveState(state);
|
|
218
365
|
}
|
|
219
366
|
const artifact = artifactForPhase(state.phase);
|
|
220
|
-
const path = artifactPath(state.cwd, state.runId, artifact);
|
|
367
|
+
const path = artifactPath(state.cwd, state.runId, artifact, state.artifactDirectory);
|
|
221
368
|
const applyingTesting = state.executionMode === "apply" && state.phase === "testing";
|
|
222
369
|
const roleCwd = applyingTesting ? state.executionWorkspace?.path : state.cwd;
|
|
223
370
|
if (!roleCwd) {
|
|
224
371
|
throw new Error("DevCrew apply testing requires an execution workspace");
|
|
225
372
|
}
|
|
226
|
-
state.roles.push(conductorDecision(state, role, gate));
|
|
373
|
+
state.roles.push(conductorDecision(state, role, gate ?? `${state.phase} automatic transition`));
|
|
374
|
+
if (applyingTesting && state.executionPolicy === "interactive-host") {
|
|
375
|
+
state.executionInstruction = {
|
|
376
|
+
phase: "testing",
|
|
377
|
+
workspacePath: roleCwd,
|
|
378
|
+
instructions: executionInstructions(state, "testing", roleCwd),
|
|
379
|
+
createdAt: now(),
|
|
380
|
+
};
|
|
381
|
+
state.status = "awaiting_execution";
|
|
382
|
+
return saveState(state);
|
|
383
|
+
}
|
|
227
384
|
const result = await runner({
|
|
228
385
|
backend: state.backend,
|
|
229
386
|
role,
|
|
@@ -231,6 +388,7 @@ async function runCurrentPhaseRole(state, runner = runRole) {
|
|
|
231
388
|
request: state.request,
|
|
232
389
|
mode: state.mode,
|
|
233
390
|
executionMode: state.executionMode,
|
|
391
|
+
executionPolicy: state.executionPolicy,
|
|
234
392
|
cwd: roleCwd,
|
|
235
393
|
standards: state.standards.combined,
|
|
236
394
|
artifactPath: path,
|
|
@@ -240,19 +398,64 @@ async function runCurrentPhaseRole(state, runner = runRole) {
|
|
|
240
398
|
});
|
|
241
399
|
if (applyingTesting && state.executionWorkspace) {
|
|
242
400
|
state.verification = await runConfiguredVerification(state, roleCwd);
|
|
401
|
+
state.verificationStatus = verificationStatusFor(state.verification);
|
|
243
402
|
const captured = await captureExecutionChanges(state.executionWorkspace);
|
|
244
403
|
state.changedFiles = captured.changedFiles;
|
|
245
404
|
state.implementationDiff = captured.patch;
|
|
246
405
|
await writeImplementationReview(state);
|
|
247
406
|
}
|
|
407
|
+
if (state.phase === "review") {
|
|
408
|
+
const reviewDecision = result.format === "structured" ? result.structured?.reviewDecision : result.reviewDecision;
|
|
409
|
+
if (!reviewDecision) {
|
|
410
|
+
throw new Error("DevCrew architecture review must return a structured review decision");
|
|
411
|
+
}
|
|
412
|
+
state.architectureReview = {
|
|
413
|
+
decision: reviewDecision,
|
|
414
|
+
summary: result.summary,
|
|
415
|
+
reviewedAt: now(),
|
|
416
|
+
};
|
|
417
|
+
}
|
|
248
418
|
// When the backend cannot run a real SDK we keep a single deterministic
|
|
249
419
|
// artifact source by rendering the rich phase template from the core layer.
|
|
250
420
|
const baseMarkdown = result.usedFallback ? `${fallbackNotice(result)}${renderArtifact(artifact, state)}` : result.markdown;
|
|
251
|
-
const markdown = appendExecutionSections(artifact, baseMarkdown, state);
|
|
421
|
+
const markdown = appendExecutionSections(artifact, baseMarkdown, state, result);
|
|
252
422
|
state.roles.push({ ...result, markdown });
|
|
253
423
|
state.artifacts[artifact] = await writeMarkdownArtifact(state, artifact, markdown);
|
|
254
|
-
state.
|
|
255
|
-
|
|
424
|
+
state.executionInstruction = undefined;
|
|
425
|
+
const questions = result.format === "structured"
|
|
426
|
+
? result.structured?.questions?.map((question) => question.prompt) ?? []
|
|
427
|
+
: result.questions ?? [];
|
|
428
|
+
if (state.phase === "requirements" && questions.length > 0) {
|
|
429
|
+
state.pendingQuestions = questions;
|
|
430
|
+
state.gates.requirements = "not_started";
|
|
431
|
+
state.status = "awaiting_input";
|
|
432
|
+
return saveState(state);
|
|
433
|
+
}
|
|
434
|
+
if (state.phase === "review" && state.architectureReview?.decision === "changes_required") {
|
|
435
|
+
state.gates["implementation-review"] = "rejected";
|
|
436
|
+
state.status = "awaiting_input";
|
|
437
|
+
state.feedback.push({
|
|
438
|
+
gate: "implementation-review",
|
|
439
|
+
message: state.architectureReview.summary,
|
|
440
|
+
createdAt: now(),
|
|
441
|
+
});
|
|
442
|
+
return saveState(state);
|
|
443
|
+
}
|
|
444
|
+
if (!gate && configuredGate) {
|
|
445
|
+
state.phase = nextPhaseAfterGate(state, configuredGate);
|
|
446
|
+
state.status = "ready";
|
|
447
|
+
return saveState(state);
|
|
448
|
+
}
|
|
449
|
+
if (applyingTesting) {
|
|
450
|
+
setTestingGateFromVerification(state);
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
if (!gate) {
|
|
454
|
+
throw new Error(`DevCrew has no configured gate for ${state.phase}`);
|
|
455
|
+
}
|
|
456
|
+
state.gates[gate] = "pending";
|
|
457
|
+
state.status = "awaiting_approval";
|
|
458
|
+
}
|
|
256
459
|
return saveState(state);
|
|
257
460
|
}
|
|
258
461
|
export async function startOrchestratedWorkflow(input, runner = runRole) {
|
|
@@ -261,7 +464,11 @@ export async function startOrchestratedWorkflow(input, runner = runRole) {
|
|
|
261
464
|
}
|
|
262
465
|
export async function continueOrchestratedWorkflow(input, runner = runRole) {
|
|
263
466
|
const state = await getWorkflowStatus(input);
|
|
264
|
-
if (state.status === "awaiting_approval" ||
|
|
467
|
+
if (state.status === "awaiting_approval" ||
|
|
468
|
+
state.status === "awaiting_input" ||
|
|
469
|
+
state.status === "awaiting_execution" ||
|
|
470
|
+
state.status === "complete" ||
|
|
471
|
+
state.status === "aborted") {
|
|
265
472
|
return state;
|
|
266
473
|
}
|
|
267
474
|
return runCurrentPhaseRole(state, runner);
|
|
@@ -296,6 +503,9 @@ export async function approveOrchestratedWorkflow(input) {
|
|
|
296
503
|
return cleanupAfterApproval(before);
|
|
297
504
|
}
|
|
298
505
|
if (promotingTesting) {
|
|
506
|
+
if (before.verificationStatus !== "passed" && !before.verificationWaiver) {
|
|
507
|
+
throw new Error("Verification must pass before promotion or have an explicit verification waiver");
|
|
508
|
+
}
|
|
299
509
|
await promoteExecutionChanges(before);
|
|
300
510
|
let approved;
|
|
301
511
|
try {
|
|
@@ -315,9 +525,90 @@ export async function approveOrchestratedWorkflow(input) {
|
|
|
315
525
|
}
|
|
316
526
|
return approveWorkflow(input);
|
|
317
527
|
}
|
|
528
|
+
export async function waiveOrchestratedVerification(input) {
|
|
529
|
+
const state = await waiveVerificationWorkflow(input);
|
|
530
|
+
const reportPath = state.artifacts["test-report"];
|
|
531
|
+
if (reportPath) {
|
|
532
|
+
const report = await readFile(reportPath, "utf8");
|
|
533
|
+
await writeFile(reportPath, appendExecutionSections("test-report", report, state), "utf8");
|
|
534
|
+
}
|
|
535
|
+
return state;
|
|
536
|
+
}
|
|
537
|
+
export async function completeOrchestratedExecution(input) {
|
|
538
|
+
const state = await getWorkflowStatus(input);
|
|
539
|
+
const instruction = state.executionInstruction;
|
|
540
|
+
if (state.executionMode !== "apply" ||
|
|
541
|
+
state.executionPolicy !== "interactive-host" ||
|
|
542
|
+
state.status !== "awaiting_execution" ||
|
|
543
|
+
!instruction ||
|
|
544
|
+
instruction.phase !== state.phase ||
|
|
545
|
+
!state.executionWorkspace ||
|
|
546
|
+
instruction.workspacePath !== state.executionWorkspace.path) {
|
|
547
|
+
throw new Error("DevCrew is not awaiting an interactive-host execution completion");
|
|
548
|
+
}
|
|
549
|
+
const summary = parseCompletionSummary(input.summary);
|
|
550
|
+
if (state.phase === "execution") {
|
|
551
|
+
if (input.verification !== undefined) {
|
|
552
|
+
throw new Error("verification can only be submitted after interactive-host testing");
|
|
553
|
+
}
|
|
554
|
+
const captured = await captureExecutionChanges(state.executionWorkspace);
|
|
555
|
+
state.changedFiles = captured.changedFiles;
|
|
556
|
+
state.implementationDiff = captured.patch;
|
|
557
|
+
state.lintResults = [];
|
|
558
|
+
state.roles.push(hostCompletionResult(state, summary));
|
|
559
|
+
state.executionInstruction = undefined;
|
|
560
|
+
await writeImplementationReview(state);
|
|
561
|
+
state.phase = "review";
|
|
562
|
+
state.status = "ready";
|
|
563
|
+
return saveState(state);
|
|
564
|
+
}
|
|
565
|
+
if (state.phase !== "testing") {
|
|
566
|
+
throw new Error(`Cannot complete interactive-host work during ${state.phase}`);
|
|
567
|
+
}
|
|
568
|
+
state.verification = parseCompletionVerification(input.verification);
|
|
569
|
+
state.verificationStatus = verificationStatusFor(state.verification);
|
|
570
|
+
const captured = await captureExecutionChanges(state.executionWorkspace);
|
|
571
|
+
state.changedFiles = captured.changedFiles;
|
|
572
|
+
state.implementationDiff = captured.patch;
|
|
573
|
+
await writeImplementationReview(state);
|
|
574
|
+
const result = hostCompletionResult(state, summary);
|
|
575
|
+
const artifact = artifactForPhase(state.phase);
|
|
576
|
+
const markdown = appendExecutionSections(artifact, result.markdown, state, result);
|
|
577
|
+
state.roles.push({ ...result, markdown });
|
|
578
|
+
state.artifacts[artifact] = await writeMarkdownArtifact(state, artifact, markdown);
|
|
579
|
+
state.executionInstruction = undefined;
|
|
580
|
+
setTestingGateFromVerification(state);
|
|
581
|
+
return saveState(state);
|
|
582
|
+
}
|
|
318
583
|
export async function rejectOrchestratedWorkflow(input) {
|
|
319
584
|
return rejectWorkflow(input);
|
|
320
585
|
}
|
|
586
|
+
export async function abortOrchestratedWorkflow(input) {
|
|
587
|
+
const state = await abortWorkflow(input);
|
|
588
|
+
if (!state.executionWorkspace) {
|
|
589
|
+
return state;
|
|
590
|
+
}
|
|
591
|
+
try {
|
|
592
|
+
await cleanupExecutionWorkspace(state);
|
|
593
|
+
}
|
|
594
|
+
catch {
|
|
595
|
+
return state;
|
|
596
|
+
}
|
|
597
|
+
state.executionWorkspace = undefined;
|
|
598
|
+
return saveState(state);
|
|
599
|
+
}
|
|
600
|
+
export async function recoverOrchestratedWorkflow(input) {
|
|
601
|
+
const state = await getWorkflowStatus(input);
|
|
602
|
+
if (state.status !== "aborted" && state.status !== "complete") {
|
|
603
|
+
throw new Error("Only terminal DevCrew runs can be recovered");
|
|
604
|
+
}
|
|
605
|
+
if (!state.executionWorkspace) {
|
|
606
|
+
return state;
|
|
607
|
+
}
|
|
608
|
+
await cleanupExecutionWorkspace(state);
|
|
609
|
+
state.executionWorkspace = undefined;
|
|
610
|
+
return saveState(state);
|
|
611
|
+
}
|
|
321
612
|
export async function answerOrchestratedWorkflow(input, runner = runRole) {
|
|
322
613
|
const before = await getWorkflowStatus(input);
|
|
323
614
|
const state = await answerWorkflow(input, { skipArtifactWrite: true });
|
|
@@ -329,9 +620,17 @@ export async function answerOrchestratedWorkflow(input, runner = runRole) {
|
|
|
329
620
|
state.gates.testing = "not_started";
|
|
330
621
|
return saveState(state);
|
|
331
622
|
}
|
|
332
|
-
|
|
623
|
+
if (before.executionMode === "apply" &&
|
|
624
|
+
before.phase === "review" &&
|
|
625
|
+
before.gates["implementation-review"] === "rejected" &&
|
|
626
|
+
before.architectureReview?.decision === "changes_required") {
|
|
627
|
+
state.phase = "execution";
|
|
628
|
+
state.status = "ready";
|
|
629
|
+
state.gates["implementation-review"] = "not_started";
|
|
630
|
+
return saveState(state);
|
|
631
|
+
}
|
|
333
632
|
const role = roleForPhase(state.phase);
|
|
334
|
-
if (!
|
|
633
|
+
if (!role) {
|
|
335
634
|
return state;
|
|
336
635
|
}
|
|
337
636
|
// Re-run the current phase role so the artifact reflects the new answer and
|
|
@@ -2,7 +2,7 @@ import { constants } from "node:fs";
|
|
|
2
2
|
import { access, copyFile, mkdir, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { DEFAULT_CONFIG, DEVCREW_NPM_PACKAGE, DEVCREW_VERSION
|
|
5
|
+
import { DEFAULT_CONFIG, DEVCREW_NPM_PACKAGE, DEVCREW_VERSION } from "../../core/src/index.js";
|
|
6
6
|
async function writeJson(path, value) {
|
|
7
7
|
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
8
8
|
}
|
|
@@ -29,36 +29,8 @@ async function writeCodexAssets(pluginRoot) {
|
|
|
29
29
|
await copyFile(await bundledAssetPath("logo.png"), join(assetDir, "logo.png"));
|
|
30
30
|
await copyFile(await bundledAssetPath("composer-icon.png"), join(assetDir, "composer-icon.png"));
|
|
31
31
|
}
|
|
32
|
-
function roleExpectations(name) {
|
|
33
|
-
const sections = ROLE_SECTIONS[name];
|
|
34
|
-
if (!sections || sections.length === 0) {
|
|
35
|
-
return "";
|
|
36
|
-
}
|
|
37
|
-
return sections.map((s) => `- ${s.heading} (${s.description})`).join("\n");
|
|
38
|
-
}
|
|
39
|
-
async function writeRoleAgents(root, format) {
|
|
40
|
-
const roles = [
|
|
41
|
-
["pm", "Product manager. Clarifies requirements, scope boundaries, success criteria, and requester approvals."],
|
|
42
|
-
["architect", "technical architecture specialist. Designs implementation, deployment, interfaces, and review criteria."],
|
|
43
|
-
["implementer", "Implementation engineer. Writes code according to approved architecture and discovered standards."],
|
|
44
|
-
["tester", "Testing and acceptance specialist. Verifies functionality, regressions, and acceptance evidence."],
|
|
45
|
-
];
|
|
46
|
-
if (format === "claude") {
|
|
47
|
-
const agentDir = join(root, "agents");
|
|
48
|
-
await mkdir(agentDir, { recursive: true });
|
|
49
|
-
for (const [name, description] of roles) {
|
|
50
|
-
await writeFile(join(agentDir, `${name}.md`), `---\nname: ${name}\ndescription: ${description}\ntools: Read, Grep, Glob, Bash\n---\n\nYou are the DevCrew ${name} role. ${description} Return concise Markdown and keep inherited host permissions.\n\nProduce these required sections:\n\n${roleExpectations(name)}\n`, "utf8");
|
|
51
|
-
}
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
const agentDir = join(root, "agents");
|
|
55
|
-
await mkdir(agentDir, { recursive: true });
|
|
56
|
-
for (const [name, description] of roles) {
|
|
57
|
-
await writeFile(join(agentDir, `${name}.toml`), `name = "${name}"\ndescription = "${description}"\ndeveloper_instructions = """\nYou are the DevCrew ${name} role. ${description}\nReturn concise Markdown and keep inherited host permissions.\n\nProduce these required sections:\n${roleExpectations(name)}\n"""\n`, "utf8");
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
32
|
function entrySkill() {
|
|
61
|
-
return `---\nname: devcrew\ndescription: Run the DevCrew PM -> architecture -> implementation -> testing workflow. Use when the user asks for structured feature or product development, requirements clarification, architecture review, implementation planning, testing acceptance, or Chinese requests such as 完整研发流程, 需求澄清, 产品经理, 架构师, 开发测试流程.\n---\n\nUse the DevCrew MCP tools to manage the workflow:\n\n1. Start with \`devcrew_start\` using the current repository cwd, mode, request, and optional executionMode
|
|
33
|
+
return `---\nname: devcrew\ndescription: Run the DevCrew PM -> architecture -> implementation -> testing workflow. Use when the user asks for structured feature or product development, requirements clarification, architecture review, implementation planning, testing acceptance, or Chinese requests such as 完整研发流程, 需求澄清, 产品经理, 架构师, 开发测试流程.\n---\n\nUse the DevCrew MCP tools to manage the workflow:\n\n1. Start with \`devcrew_start\` using the current repository cwd, mode, request, and optional \`executionMode\`. Host is inferred from the plugin's \`DEVCREW_HOST\`; pass host only for an explicit override. Omit \`executionMode\` unless the requester explicitly asks DevCrew to apply changes; the default safe mode is \`plan\`.\n2. After start, DevCrew records the active run for this repository. For follow-up tools, omit \`runId\` unless you need to target a different run explicitly.\n3. For \`executionMode: "apply"\`, choose an explicit \`executionPolicy\`. The default \`interactive-host\` pauses at implementation and testing for the host's native agent to work in DevCrew's isolated worktree. \`headless-restricted\` and \`headless-unattended\` are DevCrew SDK policies; they do not inherit the current host approval session.\n4. Use \`devcrew_status\` to show the current phase, pending gate, and any execution instruction.\n5. Use \`devcrew_answer\` when the requester gives clarification.\n6. Use \`devcrew_approve\` or \`devcrew_reject\` for each gate.\n7. Use \`devcrew_continue\` after approvals. Apply runs enter an \`implementation-review\` gate after execution: review the architect's \`architecture-review\` artifact before testing. For \`interactive-host\`, if status becomes \`awaiting_execution\`, perform the native-host work in the indicated worktree then call \`devcrew_complete_execution\`. For testing, include command, exit code, output, startedAt, and completedAt evidence.\n8. Failed verification is not approvable. Revise through \`devcrew_answer\`, or use \`devcrew_waive_verification\` only when the requester explicitly accepts the recorded risk and provides a reason.\n9. Use \`devcrew_artifact\` to read generated requirements, architecture, implementation-plan, implementation-review, architecture-review, test-report, or acceptance files.\n\nNever describe a nested SDK session as inheriting the current host's approval decisions.\n`;
|
|
62
34
|
}
|
|
63
35
|
function npmPackageSpecifier() {
|
|
64
36
|
return `${DEVCREW_NPM_PACKAGE}@${DEVCREW_VERSION}`;
|
|
@@ -136,7 +108,6 @@ export async function generateCodexPlugin(root) {
|
|
|
136
108
|
devcrew: mcpServerConfig("codex"),
|
|
137
109
|
},
|
|
138
110
|
});
|
|
139
|
-
await writeRoleAgents(pluginRoot, "codex");
|
|
140
111
|
return { name: "devcrew", path: pluginRoot };
|
|
141
112
|
}
|
|
142
113
|
export async function generateCodexMarketplace(root) {
|
|
@@ -180,7 +151,6 @@ export async function generateClaudePlugin(root) {
|
|
|
180
151
|
devcrew: mcpServerConfig("claude"),
|
|
181
152
|
},
|
|
182
153
|
});
|
|
183
|
-
await writeRoleAgents(pluginRoot, "claude");
|
|
184
154
|
return { name: "devcrew", path: pluginRoot };
|
|
185
155
|
}
|
|
186
156
|
export async function initProject(root) {
|