@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
|
@@ -5,6 +5,7 @@ import { dirname } from "node:path";
|
|
|
5
5
|
import { runRole } from "../../adapters/src/index.js";
|
|
6
6
|
import type { RoleRunInput } from "../../adapters/src/index.js";
|
|
7
7
|
import {
|
|
8
|
+
abortWorkflow,
|
|
8
9
|
answerWorkflow,
|
|
9
10
|
approveWorkflow,
|
|
10
11
|
artifactForPhase,
|
|
@@ -15,15 +16,19 @@ import {
|
|
|
15
16
|
discoverVerifyCommands,
|
|
16
17
|
gateForPhase,
|
|
17
18
|
getWorkflowStatus,
|
|
19
|
+
nextPhaseAfterGate,
|
|
18
20
|
readConfig,
|
|
19
21
|
rejectWorkflow,
|
|
20
22
|
renderArtifact,
|
|
21
23
|
saveState,
|
|
22
24
|
startWorkflow,
|
|
23
25
|
validateWorkflowApproval,
|
|
26
|
+
waiveVerificationWorkflow,
|
|
24
27
|
type AnswerWorkflowInput,
|
|
28
|
+
type AbortWorkflowInput,
|
|
25
29
|
type ApproveWorkflowInput,
|
|
26
30
|
type ArtifactName,
|
|
31
|
+
type CompleteExecutionInput,
|
|
27
32
|
type Phase,
|
|
28
33
|
type RejectWorkflowInput,
|
|
29
34
|
type RoleResult,
|
|
@@ -31,6 +36,8 @@ import {
|
|
|
31
36
|
type RunState,
|
|
32
37
|
type StartWorkflowInput,
|
|
33
38
|
type VerificationResult,
|
|
39
|
+
type VerificationStatus,
|
|
40
|
+
type WaiveVerificationInput,
|
|
34
41
|
} from "../../core/src/index.js";
|
|
35
42
|
import {
|
|
36
43
|
captureExecutionChanges,
|
|
@@ -44,13 +51,15 @@ import {
|
|
|
44
51
|
const COMMAND_TIMEOUT_MS = 300_000;
|
|
45
52
|
|
|
46
53
|
type RoleRunner = (input: RoleRunInput) => Promise<RoleResult>;
|
|
54
|
+
type ExecutingRole = Exclude<RoleResult["role"], "conductor">;
|
|
47
55
|
|
|
48
|
-
function roleForPhase(phase: Phase):
|
|
49
|
-
const roles: Partial<Record<Phase,
|
|
56
|
+
function roleForPhase(phase: Phase): ExecutingRole | undefined {
|
|
57
|
+
const roles: Partial<Record<Phase, ExecutingRole>> = {
|
|
50
58
|
requirements: "pm",
|
|
51
59
|
architecture: "architect",
|
|
52
60
|
implementation: "implementer",
|
|
53
61
|
execution: "implementer",
|
|
62
|
+
review: "architect",
|
|
54
63
|
testing: "tester",
|
|
55
64
|
};
|
|
56
65
|
return roles[phase];
|
|
@@ -64,6 +73,7 @@ function conductorDecision(state: RunState, role: RoleResult["role"], gate: stri
|
|
|
64
73
|
summary,
|
|
65
74
|
markdown: `# Conductor Decision\n\n${summary}\n`,
|
|
66
75
|
usedFallback: false,
|
|
76
|
+
format: "legacy",
|
|
67
77
|
};
|
|
68
78
|
}
|
|
69
79
|
|
|
@@ -77,7 +87,7 @@ function priorArtifactNamesForPhase(phase: Phase): ArtifactName[] {
|
|
|
77
87
|
}
|
|
78
88
|
|
|
79
89
|
async function writeMarkdownArtifact(state: RunState, artifact: ArtifactName, markdown: string): Promise<string> {
|
|
80
|
-
const path = artifactPath(state.cwd, state.runId, artifact);
|
|
90
|
+
const path = artifactPath(state.cwd, state.runId, artifact, state.artifactDirectory);
|
|
81
91
|
await mkdir(dirname(path), { recursive: true });
|
|
82
92
|
await writeFile(path, markdown, "utf8");
|
|
83
93
|
return path;
|
|
@@ -225,11 +235,151 @@ function verificationBlock(results: VerificationResult[]): string {
|
|
|
225
235
|
.join("\n\n");
|
|
226
236
|
}
|
|
227
237
|
|
|
228
|
-
function
|
|
229
|
-
if (
|
|
230
|
-
return
|
|
238
|
+
function verificationStatusFor(results: VerificationResult[]): VerificationStatus {
|
|
239
|
+
if (results.length === 0) {
|
|
240
|
+
return "not_run";
|
|
241
|
+
}
|
|
242
|
+
return results.every((result) => result.exitCode === 0) ? "passed" : "failed";
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function setTestingGateFromVerification(state: RunState): void {
|
|
246
|
+
if (state.verificationStatus !== "passed") {
|
|
247
|
+
state.gates.testing = "rejected";
|
|
248
|
+
state.status = "awaiting_input";
|
|
249
|
+
state.feedback.push({
|
|
250
|
+
gate: "testing",
|
|
251
|
+
message:
|
|
252
|
+
state.verificationStatus === "failed"
|
|
253
|
+
? "Automated verification failed. Inspect the test report, revise the implementation, or record an explicit verification waiver with its reason."
|
|
254
|
+
: "No verification evidence was recorded. Configure or run validation, revise the implementation, or record an explicit verification waiver with its reason.",
|
|
255
|
+
createdAt: now(),
|
|
256
|
+
});
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
state.gates.testing = "pending";
|
|
260
|
+
state.status = "awaiting_approval";
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function executionInstructions(state: RunState, phase: "execution" | "testing", workspacePath: string): string {
|
|
264
|
+
if (phase === "execution") {
|
|
265
|
+
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.`;
|
|
266
|
+
}
|
|
267
|
+
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.`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function hostCompletionResult(state: RunState, summary: string): RoleResult {
|
|
271
|
+
const role = state.phase === "execution" ? "implementer" : "tester";
|
|
272
|
+
const artifact = artifactForPhase(state.phase);
|
|
273
|
+
return {
|
|
274
|
+
role,
|
|
275
|
+
backend: state.backend,
|
|
276
|
+
summary: `${role} completed through the native ${state.host} host: ${summary}`,
|
|
277
|
+
markdown: `${renderArtifact(artifact, state).trim()}\n\n## Native Host Summary\n\n${summary}\n`,
|
|
278
|
+
usedFallback: false,
|
|
279
|
+
format: "legacy",
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function parseCompletionSummary(value: unknown): string {
|
|
284
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
285
|
+
throw new Error("summary must be a non-empty string");
|
|
286
|
+
}
|
|
287
|
+
return value.trim();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function parseCompletionVerification(value: unknown): VerificationResult[] {
|
|
291
|
+
if (value === undefined) {
|
|
292
|
+
return [];
|
|
293
|
+
}
|
|
294
|
+
if (!Array.isArray(value)) {
|
|
295
|
+
throw new Error("verification must be an array");
|
|
296
|
+
}
|
|
297
|
+
return value.map((entry, index) => {
|
|
298
|
+
if (
|
|
299
|
+
!entry ||
|
|
300
|
+
typeof entry !== "object" ||
|
|
301
|
+
typeof entry.command !== "string" ||
|
|
302
|
+
entry.command.trim().length === 0 ||
|
|
303
|
+
!Number.isInteger(entry.exitCode) ||
|
|
304
|
+
typeof entry.output !== "string" ||
|
|
305
|
+
typeof entry.startedAt !== "string" ||
|
|
306
|
+
typeof entry.completedAt !== "string"
|
|
307
|
+
) {
|
|
308
|
+
throw new Error(`verification[${index}] must include command, integer exitCode, output, startedAt, and completedAt`);
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
command: entry.command.trim(),
|
|
312
|
+
exitCode: entry.exitCode,
|
|
313
|
+
output: entry.output,
|
|
314
|
+
startedAt: entry.startedAt,
|
|
315
|
+
completedAt: entry.completedAt,
|
|
316
|
+
};
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function structuredRoleResultBlock(result: RoleResult): string {
|
|
321
|
+
if (result.format !== "structured" || !result.structured) {
|
|
322
|
+
return "";
|
|
323
|
+
}
|
|
324
|
+
const data = result.structured;
|
|
325
|
+
const sections = [
|
|
326
|
+
"## Structured Role Result",
|
|
327
|
+
"",
|
|
328
|
+
`Schema Version: ${data.schemaVersion}`,
|
|
329
|
+
`Summary: ${data.summary}`,
|
|
330
|
+
];
|
|
331
|
+
if (data.questions?.length) {
|
|
332
|
+
sections.push("", "### Questions", "", ...data.questions.map((question) => `- ${question.id}: ${question.prompt}`));
|
|
333
|
+
}
|
|
334
|
+
if (data.decisions?.length) {
|
|
335
|
+
sections.push("", "### Decisions", "", ...data.decisions.map((decision) => `- ${decision}`));
|
|
336
|
+
}
|
|
337
|
+
if (data.changedFiles?.length) {
|
|
338
|
+
sections.push("", "### Changed Files", "", ...data.changedFiles.map((file) => `- ${file}`));
|
|
339
|
+
}
|
|
340
|
+
if (data.evidence.length) {
|
|
341
|
+
sections.push("", "### Command Evidence", "");
|
|
342
|
+
for (const evidence of data.evidence) {
|
|
343
|
+
sections.push(`- \`${evidence.command}\` — exit code ${evidence.exitCode}`);
|
|
344
|
+
if (evidence.output) {
|
|
345
|
+
sections.push("", "```text", evidence.output, "```");
|
|
346
|
+
}
|
|
347
|
+
}
|
|
231
348
|
}
|
|
232
|
-
|
|
349
|
+
if (data.testCases?.length) {
|
|
350
|
+
sections.push("", "### Test Cases", "", "| ID | Scenario | Type | Expected |", "| --- | --- | --- | --- |");
|
|
351
|
+
sections.push(...data.testCases.map((testCase) => `| ${testCase.id} | ${testCase.scenario} | ${testCase.type} | ${testCase.expected} |`));
|
|
352
|
+
}
|
|
353
|
+
if (data.risks.length) {
|
|
354
|
+
sections.push("", "### Risks", "", ...data.risks.map((risk) => `- ${risk}`));
|
|
355
|
+
}
|
|
356
|
+
return `\n\n${sections.join("\n")}`;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function appendExecutionSections(artifact: ArtifactName, markdown: string, state: RunState, result?: RoleResult): string {
|
|
360
|
+
let content = markdown.trim();
|
|
361
|
+
if (result?.format === "structured" && !content.includes("## Structured Role Result")) {
|
|
362
|
+
content += structuredRoleResultBlock(result);
|
|
363
|
+
}
|
|
364
|
+
if (artifact === "architecture-review" && state.architectureReview) {
|
|
365
|
+
if (!content.includes("## Review Decision")) {
|
|
366
|
+
content += `\n\n## Review Decision\n\nDecision: ${state.architectureReview.decision}\n\nSummary: ${state.architectureReview.summary}`;
|
|
367
|
+
}
|
|
368
|
+
return `${content}\n`;
|
|
369
|
+
}
|
|
370
|
+
if (artifact === "test-report") {
|
|
371
|
+
if (!content.includes("## Acceptance Evidence")) {
|
|
372
|
+
content += `\n\n## Acceptance Evidence\n\n${verificationBlock(state.verification)}`;
|
|
373
|
+
}
|
|
374
|
+
if (!content.includes("## Verification Outcome")) {
|
|
375
|
+
content += `\n\n## Verification Outcome\n\nStatus: ${state.verificationStatus}`;
|
|
376
|
+
}
|
|
377
|
+
if (state.verificationWaiver && !content.includes("## Verification Waiver")) {
|
|
378
|
+
content += `\n\n## Verification Waiver\n\nReason: ${state.verificationWaiver.reason}\nRecorded At: ${state.verificationWaiver.createdAt}`;
|
|
379
|
+
}
|
|
380
|
+
return `${content}\n`;
|
|
381
|
+
}
|
|
382
|
+
return `${content}\n`;
|
|
233
383
|
}
|
|
234
384
|
|
|
235
385
|
async function writeImplementationReview(state: RunState): Promise<void> {
|
|
@@ -248,9 +398,22 @@ async function runCurrentPhaseRole(state: RunState, runner: RoleRunner = runRole
|
|
|
248
398
|
const workspace = await ensureExecutionWorkspace(state);
|
|
249
399
|
state.executionWorkspace = workspace;
|
|
250
400
|
state.verification = [];
|
|
401
|
+
state.verificationStatus = "not_run";
|
|
402
|
+
delete state.verificationWaiver;
|
|
251
403
|
delete state.artifacts["test-report"];
|
|
252
404
|
await saveState(state);
|
|
253
405
|
|
|
406
|
+
if (state.executionPolicy === "interactive-host") {
|
|
407
|
+
state.executionInstruction = {
|
|
408
|
+
phase: "execution",
|
|
409
|
+
workspacePath: workspace.path,
|
|
410
|
+
instructions: executionInstructions(state, "execution", workspace.path),
|
|
411
|
+
createdAt: now(),
|
|
412
|
+
};
|
|
413
|
+
state.status = "awaiting_execution";
|
|
414
|
+
return saveState(state);
|
|
415
|
+
}
|
|
416
|
+
|
|
254
417
|
const result = await runner({
|
|
255
418
|
backend: state.backend,
|
|
256
419
|
role: "implementer",
|
|
@@ -258,9 +421,10 @@ async function runCurrentPhaseRole(state: RunState, runner: RoleRunner = runRole
|
|
|
258
421
|
request: state.request,
|
|
259
422
|
mode: state.mode,
|
|
260
423
|
executionMode: "apply",
|
|
424
|
+
executionPolicy: state.executionPolicy,
|
|
261
425
|
cwd: workspace.path,
|
|
262
426
|
standards: state.standards.combined,
|
|
263
|
-
artifactPath: artifactPath(state.cwd, state.runId, "implementation-review"),
|
|
427
|
+
artifactPath: artifactPath(state.cwd, state.runId, "implementation-review", state.artifactDirectory),
|
|
264
428
|
answers: state.answers.map((entry) => entry.answer),
|
|
265
429
|
feedback: state.feedback.map((entry) => `${entry.gate}: ${entry.message}`),
|
|
266
430
|
priorArtifacts: await readPriorArtifacts(state),
|
|
@@ -271,15 +435,17 @@ async function runCurrentPhaseRole(state: RunState, runner: RoleRunner = runRole
|
|
|
271
435
|
state.changedFiles = captured.changedFiles;
|
|
272
436
|
state.implementationDiff = captured.patch;
|
|
273
437
|
state.roles.push(result);
|
|
438
|
+
state.executionInstruction = undefined;
|
|
274
439
|
await writeImplementationReview(state);
|
|
275
|
-
state.phase = "
|
|
440
|
+
state.phase = "review";
|
|
276
441
|
state.status = "ready";
|
|
277
442
|
return saveState(state);
|
|
278
443
|
}
|
|
279
444
|
|
|
280
|
-
const
|
|
445
|
+
const configuredGate = gateForPhase(state.phase);
|
|
446
|
+
const gate = gateForPhase(state.phase, state.enabledGates);
|
|
281
447
|
const role = roleForPhase(state.phase);
|
|
282
|
-
if (!
|
|
448
|
+
if (!role) {
|
|
283
449
|
const artifact = artifactForPhase(state.phase);
|
|
284
450
|
const markdown = renderArtifact(artifact, state);
|
|
285
451
|
state.artifacts[artifact] = await writeMarkdownArtifact(state, artifact, markdown);
|
|
@@ -289,13 +455,24 @@ async function runCurrentPhaseRole(state: RunState, runner: RoleRunner = runRole
|
|
|
289
455
|
}
|
|
290
456
|
|
|
291
457
|
const artifact = artifactForPhase(state.phase);
|
|
292
|
-
const path = artifactPath(state.cwd, state.runId, artifact);
|
|
458
|
+
const path = artifactPath(state.cwd, state.runId, artifact, state.artifactDirectory);
|
|
293
459
|
const applyingTesting = state.executionMode === "apply" && state.phase === "testing";
|
|
294
460
|
const roleCwd = applyingTesting ? state.executionWorkspace?.path : state.cwd;
|
|
295
461
|
if (!roleCwd) {
|
|
296
462
|
throw new Error("DevCrew apply testing requires an execution workspace");
|
|
297
463
|
}
|
|
298
|
-
state.roles.push(conductorDecision(state, role, gate));
|
|
464
|
+
state.roles.push(conductorDecision(state, role, gate ?? `${state.phase} automatic transition`));
|
|
465
|
+
|
|
466
|
+
if (applyingTesting && state.executionPolicy === "interactive-host") {
|
|
467
|
+
state.executionInstruction = {
|
|
468
|
+
phase: "testing",
|
|
469
|
+
workspacePath: roleCwd,
|
|
470
|
+
instructions: executionInstructions(state, "testing", roleCwd),
|
|
471
|
+
createdAt: now(),
|
|
472
|
+
};
|
|
473
|
+
state.status = "awaiting_execution";
|
|
474
|
+
return saveState(state);
|
|
475
|
+
}
|
|
299
476
|
|
|
300
477
|
const result = await runner({
|
|
301
478
|
backend: state.backend,
|
|
@@ -304,6 +481,7 @@ async function runCurrentPhaseRole(state: RunState, runner: RoleRunner = runRole
|
|
|
304
481
|
request: state.request,
|
|
305
482
|
mode: state.mode,
|
|
306
483
|
executionMode: state.executionMode,
|
|
484
|
+
executionPolicy: state.executionPolicy,
|
|
307
485
|
cwd: roleCwd,
|
|
308
486
|
standards: state.standards.combined,
|
|
309
487
|
artifactPath: path,
|
|
@@ -314,20 +492,65 @@ async function runCurrentPhaseRole(state: RunState, runner: RoleRunner = runRole
|
|
|
314
492
|
|
|
315
493
|
if (applyingTesting && state.executionWorkspace) {
|
|
316
494
|
state.verification = await runConfiguredVerification(state, roleCwd);
|
|
495
|
+
state.verificationStatus = verificationStatusFor(state.verification);
|
|
317
496
|
const captured = await captureExecutionChanges(state.executionWorkspace);
|
|
318
497
|
state.changedFiles = captured.changedFiles;
|
|
319
498
|
state.implementationDiff = captured.patch;
|
|
320
499
|
await writeImplementationReview(state);
|
|
321
500
|
}
|
|
322
501
|
|
|
502
|
+
if (state.phase === "review") {
|
|
503
|
+
const reviewDecision = result.format === "structured" ? result.structured?.reviewDecision : result.reviewDecision;
|
|
504
|
+
if (!reviewDecision) {
|
|
505
|
+
throw new Error("DevCrew architecture review must return a structured review decision");
|
|
506
|
+
}
|
|
507
|
+
state.architectureReview = {
|
|
508
|
+
decision: reviewDecision,
|
|
509
|
+
summary: result.summary,
|
|
510
|
+
reviewedAt: now(),
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
323
514
|
// When the backend cannot run a real SDK we keep a single deterministic
|
|
324
515
|
// artifact source by rendering the rich phase template from the core layer.
|
|
325
516
|
const baseMarkdown = result.usedFallback ? `${fallbackNotice(result)}${renderArtifact(artifact, state)}` : result.markdown;
|
|
326
|
-
const markdown = appendExecutionSections(artifact, baseMarkdown, state);
|
|
517
|
+
const markdown = appendExecutionSections(artifact, baseMarkdown, state, result);
|
|
327
518
|
state.roles.push({ ...result, markdown });
|
|
328
519
|
state.artifacts[artifact] = await writeMarkdownArtifact(state, artifact, markdown);
|
|
329
|
-
state.
|
|
330
|
-
|
|
520
|
+
state.executionInstruction = undefined;
|
|
521
|
+
const questions = result.format === "structured"
|
|
522
|
+
? result.structured?.questions?.map((question) => question.prompt) ?? []
|
|
523
|
+
: result.questions ?? [];
|
|
524
|
+
if (state.phase === "requirements" && questions.length > 0) {
|
|
525
|
+
state.pendingQuestions = questions;
|
|
526
|
+
state.gates.requirements = "not_started";
|
|
527
|
+
state.status = "awaiting_input";
|
|
528
|
+
return saveState(state);
|
|
529
|
+
}
|
|
530
|
+
if (state.phase === "review" && state.architectureReview?.decision === "changes_required") {
|
|
531
|
+
state.gates["implementation-review"] = "rejected";
|
|
532
|
+
state.status = "awaiting_input";
|
|
533
|
+
state.feedback.push({
|
|
534
|
+
gate: "implementation-review",
|
|
535
|
+
message: state.architectureReview.summary,
|
|
536
|
+
createdAt: now(),
|
|
537
|
+
});
|
|
538
|
+
return saveState(state);
|
|
539
|
+
}
|
|
540
|
+
if (!gate && configuredGate) {
|
|
541
|
+
state.phase = nextPhaseAfterGate(state, configuredGate);
|
|
542
|
+
state.status = "ready";
|
|
543
|
+
return saveState(state);
|
|
544
|
+
}
|
|
545
|
+
if (applyingTesting) {
|
|
546
|
+
setTestingGateFromVerification(state);
|
|
547
|
+
} else {
|
|
548
|
+
if (!gate) {
|
|
549
|
+
throw new Error(`DevCrew has no configured gate for ${state.phase}`);
|
|
550
|
+
}
|
|
551
|
+
state.gates[gate] = "pending";
|
|
552
|
+
state.status = "awaiting_approval";
|
|
553
|
+
}
|
|
331
554
|
return saveState(state);
|
|
332
555
|
}
|
|
333
556
|
|
|
@@ -338,7 +561,13 @@ export async function startOrchestratedWorkflow(input: StartWorkflowInput, runne
|
|
|
338
561
|
|
|
339
562
|
export async function continueOrchestratedWorkflow(input: RunRef, runner: RoleRunner = runRole): Promise<RunState> {
|
|
340
563
|
const state = await getWorkflowStatus(input);
|
|
341
|
-
if (
|
|
564
|
+
if (
|
|
565
|
+
state.status === "awaiting_approval" ||
|
|
566
|
+
state.status === "awaiting_input" ||
|
|
567
|
+
state.status === "awaiting_execution" ||
|
|
568
|
+
state.status === "complete" ||
|
|
569
|
+
state.status === "aborted"
|
|
570
|
+
) {
|
|
342
571
|
return state;
|
|
343
572
|
}
|
|
344
573
|
|
|
@@ -377,6 +606,9 @@ export async function approveOrchestratedWorkflow(input: ApproveWorkflowInput):
|
|
|
377
606
|
return cleanupAfterApproval(before);
|
|
378
607
|
}
|
|
379
608
|
if (promotingTesting) {
|
|
609
|
+
if (before.verificationStatus !== "passed" && !before.verificationWaiver) {
|
|
610
|
+
throw new Error("Verification must pass before promotion or have an explicit verification waiver");
|
|
611
|
+
}
|
|
380
612
|
await promoteExecutionChanges(before);
|
|
381
613
|
let approved: RunState;
|
|
382
614
|
try {
|
|
@@ -396,10 +628,98 @@ export async function approveOrchestratedWorkflow(input: ApproveWorkflowInput):
|
|
|
396
628
|
return approveWorkflow(input);
|
|
397
629
|
}
|
|
398
630
|
|
|
631
|
+
export async function waiveOrchestratedVerification(input: WaiveVerificationInput): Promise<RunState> {
|
|
632
|
+
const state = await waiveVerificationWorkflow(input);
|
|
633
|
+
const reportPath = state.artifacts["test-report"];
|
|
634
|
+
if (reportPath) {
|
|
635
|
+
const report = await readFile(reportPath, "utf8");
|
|
636
|
+
await writeFile(reportPath, appendExecutionSections("test-report", report, state), "utf8");
|
|
637
|
+
}
|
|
638
|
+
return state;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
export async function completeOrchestratedExecution(input: CompleteExecutionInput): Promise<RunState> {
|
|
642
|
+
const state = await getWorkflowStatus(input);
|
|
643
|
+
const instruction = state.executionInstruction;
|
|
644
|
+
if (
|
|
645
|
+
state.executionMode !== "apply" ||
|
|
646
|
+
state.executionPolicy !== "interactive-host" ||
|
|
647
|
+
state.status !== "awaiting_execution" ||
|
|
648
|
+
!instruction ||
|
|
649
|
+
instruction.phase !== state.phase ||
|
|
650
|
+
!state.executionWorkspace ||
|
|
651
|
+
instruction.workspacePath !== state.executionWorkspace.path
|
|
652
|
+
) {
|
|
653
|
+
throw new Error("DevCrew is not awaiting an interactive-host execution completion");
|
|
654
|
+
}
|
|
655
|
+
const summary = parseCompletionSummary(input.summary);
|
|
656
|
+
|
|
657
|
+
if (state.phase === "execution") {
|
|
658
|
+
if (input.verification !== undefined) {
|
|
659
|
+
throw new Error("verification can only be submitted after interactive-host testing");
|
|
660
|
+
}
|
|
661
|
+
const captured = await captureExecutionChanges(state.executionWorkspace);
|
|
662
|
+
state.changedFiles = captured.changedFiles;
|
|
663
|
+
state.implementationDiff = captured.patch;
|
|
664
|
+
state.lintResults = [];
|
|
665
|
+
state.roles.push(hostCompletionResult(state, summary));
|
|
666
|
+
state.executionInstruction = undefined;
|
|
667
|
+
await writeImplementationReview(state);
|
|
668
|
+
state.phase = "review";
|
|
669
|
+
state.status = "ready";
|
|
670
|
+
return saveState(state);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (state.phase !== "testing") {
|
|
674
|
+
throw new Error(`Cannot complete interactive-host work during ${state.phase}`);
|
|
675
|
+
}
|
|
676
|
+
state.verification = parseCompletionVerification(input.verification);
|
|
677
|
+
state.verificationStatus = verificationStatusFor(state.verification);
|
|
678
|
+
const captured = await captureExecutionChanges(state.executionWorkspace);
|
|
679
|
+
state.changedFiles = captured.changedFiles;
|
|
680
|
+
state.implementationDiff = captured.patch;
|
|
681
|
+
await writeImplementationReview(state);
|
|
682
|
+
const result = hostCompletionResult(state, summary);
|
|
683
|
+
const artifact = artifactForPhase(state.phase);
|
|
684
|
+
const markdown = appendExecutionSections(artifact, result.markdown, state, result);
|
|
685
|
+
state.roles.push({ ...result, markdown });
|
|
686
|
+
state.artifacts[artifact] = await writeMarkdownArtifact(state, artifact, markdown);
|
|
687
|
+
state.executionInstruction = undefined;
|
|
688
|
+
setTestingGateFromVerification(state);
|
|
689
|
+
return saveState(state);
|
|
690
|
+
}
|
|
691
|
+
|
|
399
692
|
export async function rejectOrchestratedWorkflow(input: RejectWorkflowInput): Promise<RunState> {
|
|
400
693
|
return rejectWorkflow(input);
|
|
401
694
|
}
|
|
402
695
|
|
|
696
|
+
export async function abortOrchestratedWorkflow(input: AbortWorkflowInput): Promise<RunState> {
|
|
697
|
+
const state = await abortWorkflow(input);
|
|
698
|
+
if (!state.executionWorkspace) {
|
|
699
|
+
return state;
|
|
700
|
+
}
|
|
701
|
+
try {
|
|
702
|
+
await cleanupExecutionWorkspace(state);
|
|
703
|
+
} catch {
|
|
704
|
+
return state;
|
|
705
|
+
}
|
|
706
|
+
state.executionWorkspace = undefined;
|
|
707
|
+
return saveState(state);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
export async function recoverOrchestratedWorkflow(input: RunRef): Promise<RunState> {
|
|
711
|
+
const state = await getWorkflowStatus(input);
|
|
712
|
+
if (state.status !== "aborted" && state.status !== "complete") {
|
|
713
|
+
throw new Error("Only terminal DevCrew runs can be recovered");
|
|
714
|
+
}
|
|
715
|
+
if (!state.executionWorkspace) {
|
|
716
|
+
return state;
|
|
717
|
+
}
|
|
718
|
+
await cleanupExecutionWorkspace(state);
|
|
719
|
+
state.executionWorkspace = undefined;
|
|
720
|
+
return saveState(state);
|
|
721
|
+
}
|
|
722
|
+
|
|
403
723
|
export async function answerOrchestratedWorkflow(input: AnswerWorkflowInput, runner: RoleRunner = runRole): Promise<RunState> {
|
|
404
724
|
const before = await getWorkflowStatus(input);
|
|
405
725
|
const state = await answerWorkflow(input, { skipArtifactWrite: true });
|
|
@@ -413,10 +733,20 @@ export async function answerOrchestratedWorkflow(input: AnswerWorkflowInput, run
|
|
|
413
733
|
state.gates.testing = "not_started";
|
|
414
734
|
return saveState(state);
|
|
415
735
|
}
|
|
736
|
+
if (
|
|
737
|
+
before.executionMode === "apply" &&
|
|
738
|
+
before.phase === "review" &&
|
|
739
|
+
before.gates["implementation-review"] === "rejected" &&
|
|
740
|
+
before.architectureReview?.decision === "changes_required"
|
|
741
|
+
) {
|
|
742
|
+
state.phase = "execution";
|
|
743
|
+
state.status = "ready";
|
|
744
|
+
state.gates["implementation-review"] = "not_started";
|
|
745
|
+
return saveState(state);
|
|
746
|
+
}
|
|
416
747
|
|
|
417
|
-
const gate = gateForPhase(state.phase);
|
|
418
748
|
const role = roleForPhase(state.phase);
|
|
419
|
-
if (!
|
|
749
|
+
if (!role) {
|
|
420
750
|
return state;
|
|
421
751
|
}
|
|
422
752
|
|
|
@@ -3,7 +3,7 @@ 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
5
|
|
|
6
|
-
import { DEFAULT_CONFIG, DEVCREW_NPM_PACKAGE, DEVCREW_VERSION
|
|
6
|
+
import { DEFAULT_CONFIG, DEVCREW_NPM_PACKAGE, DEVCREW_VERSION } from "../../core/src/index.js";
|
|
7
7
|
|
|
8
8
|
export interface GeneratedPlugin {
|
|
9
9
|
name: "devcrew";
|
|
@@ -45,48 +45,8 @@ async function writeCodexAssets(pluginRoot: string): Promise<void> {
|
|
|
45
45
|
await copyFile(await bundledAssetPath("composer-icon.png"), join(assetDir, "composer-icon.png"));
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
function roleExpectations(name: string): string {
|
|
49
|
-
const sections = ROLE_SECTIONS[name as keyof typeof ROLE_SECTIONS];
|
|
50
|
-
if (!sections || sections.length === 0) {
|
|
51
|
-
return "";
|
|
52
|
-
}
|
|
53
|
-
return sections.map((s) => `- ${s.heading} (${s.description})`).join("\n");
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async function writeRoleAgents(root: string, format: "codex" | "claude"): Promise<void> {
|
|
57
|
-
const roles = [
|
|
58
|
-
["pm", "Product manager. Clarifies requirements, scope boundaries, success criteria, and requester approvals."],
|
|
59
|
-
["architect", "technical architecture specialist. Designs implementation, deployment, interfaces, and review criteria."],
|
|
60
|
-
["implementer", "Implementation engineer. Writes code according to approved architecture and discovered standards."],
|
|
61
|
-
["tester", "Testing and acceptance specialist. Verifies functionality, regressions, and acceptance evidence."],
|
|
62
|
-
] as const;
|
|
63
|
-
|
|
64
|
-
if (format === "claude") {
|
|
65
|
-
const agentDir = join(root, "agents");
|
|
66
|
-
await mkdir(agentDir, { recursive: true });
|
|
67
|
-
for (const [name, description] of roles) {
|
|
68
|
-
await writeFile(
|
|
69
|
-
join(agentDir, `${name}.md`),
|
|
70
|
-
`---\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`,
|
|
71
|
-
"utf8",
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const agentDir = join(root, "agents");
|
|
78
|
-
await mkdir(agentDir, { recursive: true });
|
|
79
|
-
for (const [name, description] of roles) {
|
|
80
|
-
await writeFile(
|
|
81
|
-
join(agentDir, `${name}.toml`),
|
|
82
|
-
`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`,
|
|
83
|
-
"utf8",
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
48
|
function entrySkill(): string {
|
|
89
|
-
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
|
|
49
|
+
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`;
|
|
90
50
|
}
|
|
91
51
|
|
|
92
52
|
function npmPackageSpecifier(): string {
|
|
@@ -169,7 +129,6 @@ export async function generateCodexPlugin(root: string): Promise<GeneratedPlugin
|
|
|
169
129
|
devcrew: mcpServerConfig("codex"),
|
|
170
130
|
},
|
|
171
131
|
});
|
|
172
|
-
await writeRoleAgents(pluginRoot, "codex");
|
|
173
132
|
return { name: "devcrew", path: pluginRoot };
|
|
174
133
|
}
|
|
175
134
|
|
|
@@ -215,7 +174,6 @@ export async function generateClaudePlugin(root: string): Promise<GeneratedPlugi
|
|
|
215
174
|
devcrew: mcpServerConfig("claude"),
|
|
216
175
|
},
|
|
217
176
|
});
|
|
218
|
-
await writeRoleAgents(pluginRoot, "claude");
|
|
219
177
|
return { name: "devcrew", path: pluginRoot };
|
|
220
178
|
}
|
|
221
179
|
|