@shenlee/devcrew 0.1.3 → 0.1.5
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 +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/packages/adapters/src/index.js +188 -7
- package/dist/packages/core/src/active-run.js +13 -1
- package/dist/packages/core/src/config.js +87 -16
- 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 +3 -0
- package/dist/packages/core/src/store.js +10 -0
- package/dist/packages/core/src/version.js +1 -1
- package/dist/packages/core/src/workflow.js +20 -3
- package/dist/packages/orchestrator/src/index.js +100 -16
- package/dist/packages/service/src/tools.js +79 -18
- package/docs/claude-code.md +14 -1
- package/docs/codex.md +15 -2
- package/docs/quickstart.md +1 -1
- package/docs/workflow.md +11 -2
- package/package.json +2 -2
- package/packages/adapters/src/index.ts +220 -9
- package/packages/core/src/active-run.ts +13 -1
- package/packages/core/src/config.ts +96 -17
- package/packages/core/src/index.ts +1 -0
- package/packages/core/src/lock.ts +104 -0
- package/packages/core/src/paths.ts +4 -0
- package/packages/core/src/store.ts +12 -1
- package/packages/core/src/types.ts +47 -1
- package/packages/core/src/version.ts +1 -1
- package/packages/core/src/workflow.ts +22 -3
- package/packages/orchestrator/src/index.ts +109 -17
- package/packages/service/src/tools.ts +86 -16
- package/plugins/devcrew-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/devcrew-codex/.mcp.json +1 -1
- package/scripts/smoke-codex-plugin.mjs +1 -1
- package/docs/superpowers/plans/2026-07-10-p0-foundation-repair.md +0 -939
- package/docs/superpowers/plans/2026-07-13-safety-semantics.md +0 -313
- package/docs/superpowers/specs/2026-07-10-p0-foundation-repair-design.md +0 -182
- package/docs/superpowers/specs/2026-07-13-execution-boundaries-design.md +0 -126
|
@@ -35,13 +35,47 @@ export type Phase = (typeof PHASES)[number];
|
|
|
35
35
|
export type GateName = (typeof GATES)[number];
|
|
36
36
|
export type ArtifactName = (typeof ARTIFACTS)[number];
|
|
37
37
|
export type GateStatus = "not_started" | "pending" | "approved" | "rejected";
|
|
38
|
-
export type RunStatus = "ready" | "awaiting_input" | "awaiting_approval" | "awaiting_execution" | "complete";
|
|
38
|
+
export type RunStatus = "ready" | "awaiting_input" | "awaiting_approval" | "awaiting_execution" | "complete" | "aborted";
|
|
39
39
|
|
|
40
40
|
export interface RoleSection {
|
|
41
41
|
heading: string;
|
|
42
42
|
description: string;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
export type RoleResultFormat = "legacy" | "structured";
|
|
46
|
+
|
|
47
|
+
export interface CommandEvidence {
|
|
48
|
+
command: string;
|
|
49
|
+
exitCode: number;
|
|
50
|
+
output?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface RoleQuestion {
|
|
54
|
+
id: string;
|
|
55
|
+
prompt: string;
|
|
56
|
+
context?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface TestCaseEvidence {
|
|
60
|
+
id: string;
|
|
61
|
+
scenario: string;
|
|
62
|
+
type: "happy" | "edge" | "failure" | "regression";
|
|
63
|
+
expected: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface StructuredRoleData {
|
|
67
|
+
schemaVersion: 1;
|
|
68
|
+
role: "pm" | "architect" | "implementer" | "tester";
|
|
69
|
+
summary: string;
|
|
70
|
+
risks: string[];
|
|
71
|
+
evidence: CommandEvidence[];
|
|
72
|
+
questions?: RoleQuestion[];
|
|
73
|
+
decisions?: string[];
|
|
74
|
+
reviewDecision?: ArchitectureReviewDecision;
|
|
75
|
+
changedFiles?: string[];
|
|
76
|
+
testCases?: TestCaseEvidence[];
|
|
77
|
+
}
|
|
78
|
+
|
|
45
79
|
export const ROLE_SECTIONS: Record<Exclude<RoleResult["role"], "conductor">, RoleSection[]> = {
|
|
46
80
|
pm: [
|
|
47
81
|
{ heading: "Functional Scope", description: "explicit In Scope and Out of Scope lists" },
|
|
@@ -94,6 +128,8 @@ export interface RoleResult {
|
|
|
94
128
|
summary: string;
|
|
95
129
|
markdown: string;
|
|
96
130
|
usedFallback: boolean;
|
|
131
|
+
format: RoleResultFormat;
|
|
132
|
+
structured?: StructuredRoleData;
|
|
97
133
|
questions?: string[];
|
|
98
134
|
reviewDecision?: ArchitectureReviewDecision;
|
|
99
135
|
}
|
|
@@ -126,6 +162,11 @@ export interface VerificationWaiver {
|
|
|
126
162
|
createdAt: string;
|
|
127
163
|
}
|
|
128
164
|
|
|
165
|
+
export interface RunAbort {
|
|
166
|
+
reason: string;
|
|
167
|
+
abortedAt: string;
|
|
168
|
+
}
|
|
169
|
+
|
|
129
170
|
// Reused for both verification and lint results — the command shape is identical.
|
|
130
171
|
export interface VerificationResult {
|
|
131
172
|
command: string;
|
|
@@ -179,6 +220,7 @@ export interface RunState {
|
|
|
179
220
|
verification: VerificationResult[];
|
|
180
221
|
verificationStatus: VerificationStatus;
|
|
181
222
|
verificationWaiver?: VerificationWaiver;
|
|
223
|
+
abort?: RunAbort;
|
|
182
224
|
// VerificationResult is reused for lint output — same shape, different semantics.
|
|
183
225
|
lintResults: VerificationResult[];
|
|
184
226
|
}
|
|
@@ -216,6 +258,10 @@ export interface WaiveVerificationInput extends RunRef {
|
|
|
216
258
|
reason: string;
|
|
217
259
|
}
|
|
218
260
|
|
|
261
|
+
export interface AbortWorkflowInput extends RunRef {
|
|
262
|
+
reason: string;
|
|
263
|
+
}
|
|
264
|
+
|
|
219
265
|
export interface CompleteExecutionInput extends RunRef {
|
|
220
266
|
summary: string;
|
|
221
267
|
verification?: VerificationResult[];
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const DEVCREW_VERSION = "0.1.
|
|
1
|
+
export const DEVCREW_VERSION = "0.1.5";
|
|
2
2
|
export const DEVCREW_NPM_PACKAGE = "@shenlee/devcrew";
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from "./validation.js";
|
|
23
23
|
import type {
|
|
24
24
|
AnswerWorkflowInput,
|
|
25
|
+
AbortWorkflowInput,
|
|
25
26
|
ApproveWorkflowInput,
|
|
26
27
|
ArtifactName,
|
|
27
28
|
ArtifactReadResult,
|
|
@@ -187,7 +188,8 @@ export async function continueWorkflow(input: RunRef): Promise<RunState> {
|
|
|
187
188
|
state.status === "awaiting_approval" ||
|
|
188
189
|
state.status === "awaiting_input" ||
|
|
189
190
|
state.status === "awaiting_execution" ||
|
|
190
|
-
state.status === "complete"
|
|
191
|
+
state.status === "complete" ||
|
|
192
|
+
state.status === "aborted"
|
|
191
193
|
) {
|
|
192
194
|
return state;
|
|
193
195
|
}
|
|
@@ -220,6 +222,23 @@ export async function continueWorkflow(input: RunRef): Promise<RunState> {
|
|
|
220
222
|
return saveState(state);
|
|
221
223
|
}
|
|
222
224
|
|
|
225
|
+
export async function abortWorkflow(input: AbortWorkflowInput): Promise<RunState> {
|
|
226
|
+
const state = await getWorkflowStatus(input);
|
|
227
|
+
if (state.status === "aborted") {
|
|
228
|
+
return state;
|
|
229
|
+
}
|
|
230
|
+
if (state.status === "complete") {
|
|
231
|
+
throw new Error("Completed DevCrew runs cannot be aborted");
|
|
232
|
+
}
|
|
233
|
+
state.abort = {
|
|
234
|
+
reason: parseWaiverReason(input.reason),
|
|
235
|
+
abortedAt: now(),
|
|
236
|
+
};
|
|
237
|
+
delete state.executionInstruction;
|
|
238
|
+
state.status = "aborted";
|
|
239
|
+
return saveState(state);
|
|
240
|
+
}
|
|
241
|
+
|
|
223
242
|
export async function validateWorkflowApproval(input: ApproveWorkflowInput): Promise<RunState> {
|
|
224
243
|
const state = await getWorkflowStatus(input);
|
|
225
244
|
const gate = parseGate(input.gate);
|
|
@@ -301,9 +320,9 @@ export async function waiveVerificationWorkflow(input: WaiveVerificationInput):
|
|
|
301
320
|
state.phase !== "testing" ||
|
|
302
321
|
state.status !== "awaiting_input" ||
|
|
303
322
|
state.gates.testing !== "rejected" ||
|
|
304
|
-
state.verificationStatus !== "failed"
|
|
323
|
+
(state.verificationStatus !== "failed" && state.verificationStatus !== "not_run")
|
|
305
324
|
) {
|
|
306
|
-
throw new Error("A verification waiver is only available after failed apply-mode
|
|
325
|
+
throw new Error("A verification waiver is only available after failed or missing apply-mode verification");
|
|
307
326
|
}
|
|
308
327
|
state.verificationWaiver = {
|
|
309
328
|
reason: parseWaiverReason(input.reason),
|
|
@@ -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,
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
validateWorkflowApproval,
|
|
25
26
|
waiveVerificationWorkflow,
|
|
26
27
|
type AnswerWorkflowInput,
|
|
28
|
+
type AbortWorkflowInput,
|
|
27
29
|
type ApproveWorkflowInput,
|
|
28
30
|
type ArtifactName,
|
|
29
31
|
type CompleteExecutionInput,
|
|
@@ -49,9 +51,10 @@ import {
|
|
|
49
51
|
const COMMAND_TIMEOUT_MS = 300_000;
|
|
50
52
|
|
|
51
53
|
type RoleRunner = (input: RoleRunInput) => Promise<RoleResult>;
|
|
54
|
+
type ExecutingRole = Exclude<RoleResult["role"], "conductor">;
|
|
52
55
|
|
|
53
|
-
function roleForPhase(phase: Phase):
|
|
54
|
-
const roles: Partial<Record<Phase,
|
|
56
|
+
function roleForPhase(phase: Phase): ExecutingRole | undefined {
|
|
57
|
+
const roles: Partial<Record<Phase, ExecutingRole>> = {
|
|
55
58
|
requirements: "pm",
|
|
56
59
|
architecture: "architect",
|
|
57
60
|
implementation: "implementer",
|
|
@@ -70,6 +73,7 @@ function conductorDecision(state: RunState, role: RoleResult["role"], gate: stri
|
|
|
70
73
|
summary,
|
|
71
74
|
markdown: `# Conductor Decision\n\n${summary}\n`,
|
|
72
75
|
usedFallback: false,
|
|
76
|
+
format: "legacy",
|
|
73
77
|
};
|
|
74
78
|
}
|
|
75
79
|
|
|
@@ -239,12 +243,15 @@ function verificationStatusFor(results: VerificationResult[]): VerificationStatu
|
|
|
239
243
|
}
|
|
240
244
|
|
|
241
245
|
function setTestingGateFromVerification(state: RunState): void {
|
|
242
|
-
if (state.verificationStatus
|
|
246
|
+
if (state.verificationStatus !== "passed") {
|
|
243
247
|
state.gates.testing = "rejected";
|
|
244
248
|
state.status = "awaiting_input";
|
|
245
249
|
state.feedback.push({
|
|
246
250
|
gate: "testing",
|
|
247
|
-
message:
|
|
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.",
|
|
248
255
|
createdAt: now(),
|
|
249
256
|
});
|
|
250
257
|
return;
|
|
@@ -269,6 +276,7 @@ function hostCompletionResult(state: RunState, summary: string): RoleResult {
|
|
|
269
276
|
summary: `${role} completed through the native ${state.host} host: ${summary}`,
|
|
270
277
|
markdown: `${renderArtifact(artifact, state).trim()}\n\n## Native Host Summary\n\n${summary}\n`,
|
|
271
278
|
usedFallback: false,
|
|
279
|
+
format: "legacy",
|
|
272
280
|
};
|
|
273
281
|
}
|
|
274
282
|
|
|
@@ -309,16 +317,57 @@ function parseCompletionVerification(value: unknown): VerificationResult[] {
|
|
|
309
317
|
});
|
|
310
318
|
}
|
|
311
319
|
|
|
312
|
-
function
|
|
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
|
+
}
|
|
348
|
+
}
|
|
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
|
+
}
|
|
313
364
|
if (artifact === "architecture-review" && state.architectureReview) {
|
|
314
|
-
let content = markdown.trim();
|
|
315
365
|
if (!content.includes("## Review Decision")) {
|
|
316
366
|
content += `\n\n## Review Decision\n\nDecision: ${state.architectureReview.decision}\n\nSummary: ${state.architectureReview.summary}`;
|
|
317
367
|
}
|
|
318
368
|
return `${content}\n`;
|
|
319
369
|
}
|
|
320
370
|
if (artifact === "test-report") {
|
|
321
|
-
let content = markdown.trim();
|
|
322
371
|
if (!content.includes("## Acceptance Evidence")) {
|
|
323
372
|
content += `\n\n## Acceptance Evidence\n\n${verificationBlock(state.verification)}`;
|
|
324
373
|
}
|
|
@@ -330,7 +379,7 @@ function appendExecutionSections(artifact: ArtifactName, markdown: string, state
|
|
|
330
379
|
}
|
|
331
380
|
return `${content}\n`;
|
|
332
381
|
}
|
|
333
|
-
return
|
|
382
|
+
return `${content}\n`;
|
|
334
383
|
}
|
|
335
384
|
|
|
336
385
|
async function writeImplementationReview(state: RunState): Promise<void> {
|
|
@@ -451,11 +500,12 @@ async function runCurrentPhaseRole(state: RunState, runner: RoleRunner = runRole
|
|
|
451
500
|
}
|
|
452
501
|
|
|
453
502
|
if (state.phase === "review") {
|
|
454
|
-
|
|
503
|
+
const reviewDecision = result.format === "structured" ? result.structured?.reviewDecision : result.reviewDecision;
|
|
504
|
+
if (!reviewDecision) {
|
|
455
505
|
throw new Error("DevCrew architecture review must return a structured review decision");
|
|
456
506
|
}
|
|
457
507
|
state.architectureReview = {
|
|
458
|
-
decision:
|
|
508
|
+
decision: reviewDecision,
|
|
459
509
|
summary: result.summary,
|
|
460
510
|
reviewedAt: now(),
|
|
461
511
|
};
|
|
@@ -464,12 +514,15 @@ async function runCurrentPhaseRole(state: RunState, runner: RoleRunner = runRole
|
|
|
464
514
|
// When the backend cannot run a real SDK we keep a single deterministic
|
|
465
515
|
// artifact source by rendering the rich phase template from the core layer.
|
|
466
516
|
const baseMarkdown = result.usedFallback ? `${fallbackNotice(result)}${renderArtifact(artifact, state)}` : result.markdown;
|
|
467
|
-
const markdown = appendExecutionSections(artifact, baseMarkdown, state);
|
|
517
|
+
const markdown = appendExecutionSections(artifact, baseMarkdown, state, result);
|
|
468
518
|
state.roles.push({ ...result, markdown });
|
|
469
519
|
state.artifacts[artifact] = await writeMarkdownArtifact(state, artifact, markdown);
|
|
470
520
|
state.executionInstruction = undefined;
|
|
471
|
-
|
|
472
|
-
|
|
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;
|
|
473
526
|
state.gates.requirements = "not_started";
|
|
474
527
|
state.status = "awaiting_input";
|
|
475
528
|
return saveState(state);
|
|
@@ -512,7 +565,8 @@ export async function continueOrchestratedWorkflow(input: RunRef, runner: RoleRu
|
|
|
512
565
|
state.status === "awaiting_approval" ||
|
|
513
566
|
state.status === "awaiting_input" ||
|
|
514
567
|
state.status === "awaiting_execution" ||
|
|
515
|
-
state.status === "complete"
|
|
568
|
+
state.status === "complete" ||
|
|
569
|
+
state.status === "aborted"
|
|
516
570
|
) {
|
|
517
571
|
return state;
|
|
518
572
|
}
|
|
@@ -552,8 +606,8 @@ export async function approveOrchestratedWorkflow(input: ApproveWorkflowInput):
|
|
|
552
606
|
return cleanupAfterApproval(before);
|
|
553
607
|
}
|
|
554
608
|
if (promotingTesting) {
|
|
555
|
-
if (before.verificationStatus
|
|
556
|
-
throw new Error("
|
|
609
|
+
if (before.verificationStatus !== "passed" && !before.verificationWaiver) {
|
|
610
|
+
throw new Error("Verification must pass before promotion or have an explicit verification waiver");
|
|
557
611
|
}
|
|
558
612
|
await promoteExecutionChanges(before);
|
|
559
613
|
let approved: RunState;
|
|
@@ -627,7 +681,7 @@ export async function completeOrchestratedExecution(input: CompleteExecutionInpu
|
|
|
627
681
|
await writeImplementationReview(state);
|
|
628
682
|
const result = hostCompletionResult(state, summary);
|
|
629
683
|
const artifact = artifactForPhase(state.phase);
|
|
630
|
-
const markdown = appendExecutionSections(artifact, result.markdown, state);
|
|
684
|
+
const markdown = appendExecutionSections(artifact, result.markdown, state, result);
|
|
631
685
|
state.roles.push({ ...result, markdown });
|
|
632
686
|
state.artifacts[artifact] = await writeMarkdownArtifact(state, artifact, markdown);
|
|
633
687
|
state.executionInstruction = undefined;
|
|
@@ -639,6 +693,33 @@ export async function rejectOrchestratedWorkflow(input: RejectWorkflowInput): Pr
|
|
|
639
693
|
return rejectWorkflow(input);
|
|
640
694
|
}
|
|
641
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
|
+
|
|
642
723
|
export async function answerOrchestratedWorkflow(input: AnswerWorkflowInput, runner: RoleRunner = runRole): Promise<RunState> {
|
|
643
724
|
const before = await getWorkflowStatus(input);
|
|
644
725
|
const state = await answerWorkflow(input, { skipArtifactWrite: true });
|
|
@@ -652,6 +733,17 @@ export async function answerOrchestratedWorkflow(input: AnswerWorkflowInput, run
|
|
|
652
733
|
state.gates.testing = "not_started";
|
|
653
734
|
return saveState(state);
|
|
654
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
|
+
}
|
|
655
747
|
|
|
656
748
|
const role = roleForPhase(state.phase);
|
|
657
749
|
if (!role) {
|
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import {
|
|
2
|
+
clearActiveRunIfMatches,
|
|
2
3
|
getArtifact,
|
|
3
4
|
getActiveRunId,
|
|
4
5
|
getWorkflowStatus,
|
|
6
|
+
recoverRepositoryLock,
|
|
5
7
|
setActiveRun,
|
|
8
|
+
withRepositoryLock,
|
|
6
9
|
type Host,
|
|
7
10
|
type RunState,
|
|
8
11
|
} from "../../core/src/index.js";
|
|
9
12
|
import {
|
|
13
|
+
abortOrchestratedWorkflow,
|
|
10
14
|
answerOrchestratedWorkflow,
|
|
11
15
|
approveOrchestratedWorkflow,
|
|
12
16
|
completeOrchestratedExecution,
|
|
13
17
|
continueOrchestratedWorkflow,
|
|
14
18
|
rejectOrchestratedWorkflow,
|
|
19
|
+
recoverOrchestratedWorkflow,
|
|
15
20
|
startOrchestratedWorkflow,
|
|
16
21
|
waiveOrchestratedVerification,
|
|
17
22
|
} from "../../orchestrator/src/index.js";
|
|
@@ -58,8 +63,24 @@ function withInferredHost(args: Record<string, unknown>): Record<string, unknown
|
|
|
58
63
|
return { ...args, host: inferHost() };
|
|
59
64
|
}
|
|
60
65
|
|
|
66
|
+
async function withMutationLock<T>(args: Record<string, unknown>, action: () => Promise<T>): Promise<T> {
|
|
67
|
+
if (typeof args.cwd !== "string" || !args.cwd.trim()) {
|
|
68
|
+
return action();
|
|
69
|
+
}
|
|
70
|
+
return withRepositoryLock(args.cwd, action);
|
|
71
|
+
}
|
|
72
|
+
|
|
61
73
|
export function listDevCrewTools(): DevCrewTool[] {
|
|
62
74
|
return [
|
|
75
|
+
{
|
|
76
|
+
name: "devcrew_abort",
|
|
77
|
+
description: "Abort a nonterminal run, preserve its audit evidence, and clean its isolated worktree when possible.",
|
|
78
|
+
inputSchema: {
|
|
79
|
+
type: "object",
|
|
80
|
+
required: ["cwd", "reason"],
|
|
81
|
+
properties: { cwd: cwdProperty, runId: runIdProperty, reason: { type: "string" } },
|
|
82
|
+
},
|
|
83
|
+
},
|
|
63
84
|
{
|
|
64
85
|
name: "devcrew_start",
|
|
65
86
|
description: "Create a new gated DevCrew workflow run.",
|
|
@@ -85,6 +106,15 @@ export function listDevCrewTools(): DevCrewTool[] {
|
|
|
85
106
|
},
|
|
86
107
|
},
|
|
87
108
|
},
|
|
109
|
+
{
|
|
110
|
+
name: "devcrew_recover",
|
|
111
|
+
description: "Explicitly clear a confirmed stale lock and retry cleanup for a terminal run without executing an agent.",
|
|
112
|
+
inputSchema: {
|
|
113
|
+
type: "object",
|
|
114
|
+
required: ["cwd"],
|
|
115
|
+
properties: { cwd: cwdProperty, runId: runIdProperty },
|
|
116
|
+
},
|
|
117
|
+
},
|
|
88
118
|
{
|
|
89
119
|
name: "devcrew_status",
|
|
90
120
|
description: "Read the status of a DevCrew workflow run.",
|
|
@@ -190,7 +220,8 @@ function summarizeState(state: RunState): string {
|
|
|
190
220
|
const role = state.roles.at(-1);
|
|
191
221
|
const roleFallback =
|
|
192
222
|
role?.usedFallback === true ? (role.backend === "local" ? "local" : "sdk") : role ? "none" : "none";
|
|
193
|
-
|
|
223
|
+
const roleFormat = role?.format ?? "none";
|
|
224
|
+
return `Run ${state.runId}: phase=${state.phase}, status=${state.status}, execution_mode=${state.executionMode}, pending_gate=${pendingGate}, role_fallback=${roleFallback}, role_format=${roleFormat}`;
|
|
194
225
|
}
|
|
195
226
|
|
|
196
227
|
function success(text: string, structuredContent?: Record<string, unknown>): ToolResult {
|
|
@@ -213,37 +244,76 @@ function failure(error: unknown): ToolResult {
|
|
|
213
244
|
export async function callDevCrewTool(name: string, args: Record<string, unknown>): Promise<ToolResult> {
|
|
214
245
|
try {
|
|
215
246
|
if (name === "devcrew_start") {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
247
|
+
return await withMutationLock(args, async () => {
|
|
248
|
+
const state = await startOrchestratedWorkflow(withInferredHost(args) as never);
|
|
249
|
+
await setActiveRun(state.cwd, state.runId);
|
|
250
|
+
return success(`${summarizeState(state)}. Review ${state.artifacts.requirements}`, { state });
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
if (name === "devcrew_abort") {
|
|
254
|
+
return await withMutationLock(args, async () => {
|
|
255
|
+
const runArgs = await withActiveRun(args);
|
|
256
|
+
const state = await abortOrchestratedWorkflow(runArgs as never);
|
|
257
|
+
await clearActiveRunIfMatches(state.cwd, state.runId);
|
|
258
|
+
return success(`${summarizeState(state)}. Run aborted.`, { state });
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
if (name === "devcrew_recover") {
|
|
262
|
+
if (typeof args.cwd !== "string" || !args.cwd.trim()) {
|
|
263
|
+
throw new Error("cwd must be a non-empty string");
|
|
264
|
+
}
|
|
265
|
+
const recoveredLock = await recoverRepositoryLock(args.cwd);
|
|
266
|
+
if (typeof args.runId !== "string" || !args.runId.trim()) {
|
|
267
|
+
return success(
|
|
268
|
+
recoveredLock ? "Repository lock recovery completed." : "No stale repository lock was present.",
|
|
269
|
+
{ recoveredLock },
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
return await withMutationLock(args, async () => {
|
|
273
|
+
const runArgs = await withActiveRun(args);
|
|
274
|
+
const state = await recoverOrchestratedWorkflow(runArgs as never);
|
|
275
|
+
return success(`${summarizeState(state)}. Recovery cleanup completed.`, { state });
|
|
276
|
+
});
|
|
219
277
|
}
|
|
220
278
|
if (name === "devcrew_status") {
|
|
221
279
|
const state = await getWorkflowStatus((await withActiveRun(args)) as never);
|
|
222
280
|
return success(summarizeState(state), { state });
|
|
223
281
|
}
|
|
224
282
|
if (name === "devcrew_answer") {
|
|
225
|
-
|
|
226
|
-
|
|
283
|
+
return await withMutationLock(args, async () => {
|
|
284
|
+
const state = await answerOrchestratedWorkflow((await withActiveRun(args)) as never);
|
|
285
|
+
return success(`${summarizeState(state)}. Answer recorded.`, { state });
|
|
286
|
+
});
|
|
227
287
|
}
|
|
228
288
|
if (name === "devcrew_approve") {
|
|
229
|
-
|
|
230
|
-
|
|
289
|
+
return await withMutationLock(args, async () => {
|
|
290
|
+
const state = await approveOrchestratedWorkflow((await withActiveRun(args)) as never);
|
|
291
|
+
return success(`${summarizeState(state)}. Gate approved.`, { state });
|
|
292
|
+
});
|
|
231
293
|
}
|
|
232
294
|
if (name === "devcrew_reject") {
|
|
233
|
-
|
|
234
|
-
|
|
295
|
+
return await withMutationLock(args, async () => {
|
|
296
|
+
const state = await rejectOrchestratedWorkflow((await withActiveRun(args)) as never);
|
|
297
|
+
return success(`${summarizeState(state)}. Gate rejected.`, { state });
|
|
298
|
+
});
|
|
235
299
|
}
|
|
236
300
|
if (name === "devcrew_continue") {
|
|
237
|
-
|
|
238
|
-
|
|
301
|
+
return await withMutationLock(args, async () => {
|
|
302
|
+
const state = await continueOrchestratedWorkflow((await withActiveRun(args)) as never);
|
|
303
|
+
return success(`${summarizeState(state)}.`, { state });
|
|
304
|
+
});
|
|
239
305
|
}
|
|
240
306
|
if (name === "devcrew_complete_execution") {
|
|
241
|
-
|
|
242
|
-
|
|
307
|
+
return await withMutationLock(args, async () => {
|
|
308
|
+
const state = await completeOrchestratedExecution((await withActiveRun(args)) as never);
|
|
309
|
+
return success(`${summarizeState(state)}. Native host completion recorded.`, { state });
|
|
310
|
+
});
|
|
243
311
|
}
|
|
244
312
|
if (name === "devcrew_waive_verification") {
|
|
245
|
-
|
|
246
|
-
|
|
313
|
+
return await withMutationLock(args, async () => {
|
|
314
|
+
const state = await waiveOrchestratedVerification((await withActiveRun(args)) as never);
|
|
315
|
+
return success(`${summarizeState(state)}. Verification waiver recorded.`, { state });
|
|
316
|
+
});
|
|
247
317
|
}
|
|
248
318
|
if (name === "devcrew_artifact") {
|
|
249
319
|
const artifact = await getArtifact((await withActiveRun(args)) as never);
|
|
@@ -298,7 +298,7 @@ async function main() {
|
|
|
298
298
|
await client.request("initialize", {
|
|
299
299
|
protocolVersion: "2025-03-26",
|
|
300
300
|
capabilities: {},
|
|
301
|
-
clientInfo: { name: "devcrew-codex-plugin-smoke", version: "0.1.
|
|
301
|
+
clientInfo: { name: "devcrew-codex-plugin-smoke", version: "0.1.5" },
|
|
302
302
|
});
|
|
303
303
|
client.notify("notifications/initialized", {});
|
|
304
304
|
const tools = await client.request("tools/list", {});
|