@shenlee/devcrew 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -7
- package/README.zh-CN.md +12 -7
- package/dist/packages/adapters/src/index.js +60 -7
- package/dist/packages/core/src/artifacts.js +14 -4
- package/dist/packages/core/src/config.js +1 -1
- package/dist/packages/core/src/paths.js +7 -6
- package/dist/packages/core/src/store.js +30 -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 +70 -12
- package/dist/packages/orchestrator/src/index.js +230 -15
- package/dist/packages/plugins/src/index.js +2 -32
- package/dist/packages/service/src/tools.js +43 -4
- package/docs/claude-code.md +5 -3
- package/docs/codex.md +9 -5
- package/docs/quickstart.md +1 -1
- package/docs/superpowers/plans/2026-07-13-safety-semantics.md +313 -0
- package/docs/superpowers/specs/2026-07-13-execution-boundaries-design.md +126 -0
- package/docs/workflow.md +19 -4
- package/package.json +1 -1
- package/packages/adapters/src/index.ts +84 -9
- package/packages/core/src/artifacts.ts +16 -4
- package/packages/core/src/config.ts +1 -1
- package/packages/core/src/paths.ts +7 -6
- package/packages/core/src/store.ts +34 -1
- package/packages/core/src/types.ts +46 -2
- package/packages/core/src/validation.ts +10 -0
- package/packages/core/src/version.ts +1 -1
- package/packages/core/src/workflow.ts +82 -11
- package/packages/orchestrator/src/index.ts +252 -14
- package/packages/plugins/src/index.ts +2 -44
- package/packages/service/src/tools.ts +44 -3
- 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
|
@@ -10,12 +10,14 @@ import {
|
|
|
10
10
|
parseBackend,
|
|
11
11
|
parseCwd,
|
|
12
12
|
parseExecutionMode,
|
|
13
|
+
parseExecutionPolicy,
|
|
13
14
|
parseFeedback,
|
|
14
15
|
parseGate,
|
|
15
16
|
parseHost,
|
|
16
17
|
parseOptionalNote,
|
|
17
18
|
parseRequest,
|
|
18
19
|
parseRunId,
|
|
20
|
+
parseWaiverReason,
|
|
19
21
|
parseWorkflowMode,
|
|
20
22
|
} from "./validation.js";
|
|
21
23
|
import type {
|
|
@@ -29,7 +31,9 @@ import type {
|
|
|
29
31
|
RunRef,
|
|
30
32
|
RunState,
|
|
31
33
|
StartWorkflowInput,
|
|
34
|
+
WaiveVerificationInput,
|
|
32
35
|
} from "./types.js";
|
|
36
|
+
import { GATES } from "./types.js";
|
|
33
37
|
|
|
34
38
|
function now(): string {
|
|
35
39
|
return new Date().toISOString();
|
|
@@ -47,6 +51,8 @@ export function nextPhaseAfterGate(state: RunState, gate: GateName): RunState["p
|
|
|
47
51
|
return "implementation";
|
|
48
52
|
case "implementation":
|
|
49
53
|
return state.executionMode === "apply" ? "execution" : "testing";
|
|
54
|
+
case "implementation-review":
|
|
55
|
+
return "testing";
|
|
50
56
|
case "testing":
|
|
51
57
|
return "acceptance";
|
|
52
58
|
}
|
|
@@ -58,6 +64,7 @@ export function artifactForPhase(phase: RunState["phase"]): ArtifactName {
|
|
|
58
64
|
architecture: "architecture",
|
|
59
65
|
implementation: "implementation-plan",
|
|
60
66
|
execution: "implementation-review",
|
|
67
|
+
review: "architecture-review",
|
|
61
68
|
testing: "test-report",
|
|
62
69
|
acceptance: "acceptance",
|
|
63
70
|
complete: "acceptance",
|
|
@@ -65,15 +72,31 @@ export function artifactForPhase(phase: RunState["phase"]): ArtifactName {
|
|
|
65
72
|
return artifactByPhase[phase];
|
|
66
73
|
}
|
|
67
74
|
|
|
68
|
-
export function gateForPhase(phase: RunState["phase"]): GateName | undefined {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
75
|
+
export function gateForPhase(phase: RunState["phase"], enabledGates?: readonly GateName[]): GateName | undefined {
|
|
76
|
+
const gate = phase === "review"
|
|
77
|
+
? "implementation-review"
|
|
78
|
+
: (
|
|
79
|
+
phase === "requirements" ||
|
|
80
|
+
phase === "architecture" ||
|
|
81
|
+
phase === "implementation" ||
|
|
82
|
+
phase === "testing"
|
|
83
|
+
? phase
|
|
84
|
+
: undefined
|
|
85
|
+
);
|
|
86
|
+
return gate && (!enabledGates || enabledGates.includes(gate)) ? gate : undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function configuredGates(gates: readonly GateName[]): GateName[] {
|
|
90
|
+
const configured = new Set(
|
|
91
|
+
gates.filter((gate): gate is GateName => GATES.includes(gate)),
|
|
92
|
+
);
|
|
93
|
+
configured.add("implementation-review");
|
|
94
|
+
configured.add("testing");
|
|
95
|
+
return [...configured];
|
|
73
96
|
}
|
|
74
97
|
|
|
75
98
|
function assertPendingCurrentGate(state: RunState, gate: GateName): void {
|
|
76
|
-
const expected = gateForPhase(state.phase);
|
|
99
|
+
const expected = gateForPhase(state.phase, state.enabledGates);
|
|
77
100
|
if (expected !== gate) {
|
|
78
101
|
throw new Error(
|
|
79
102
|
expected
|
|
@@ -105,6 +128,8 @@ export async function startWorkflow(input: StartWorkflowInput, options: Workflow
|
|
|
105
128
|
const config = await ensureConfig(cwd);
|
|
106
129
|
const backend = input.backend ? parseBackend(input.backend) : config.defaultBackend === "host-preferred" ? host : config.defaultBackend;
|
|
107
130
|
const executionMode = input.executionMode ? parseExecutionMode(input.executionMode) : config.executionMode;
|
|
131
|
+
const executionPolicy = input.executionPolicy ? parseExecutionPolicy(input.executionPolicy) : "interactive-host";
|
|
132
|
+
const enabledGates = configuredGates(config.workflow.gates);
|
|
108
133
|
if (executionMode === "apply" && backend === "local") {
|
|
109
134
|
throw new Error("DevCrew apply mode requires a codex or claude backend; local is plan-only");
|
|
110
135
|
}
|
|
@@ -116,20 +141,25 @@ export async function startWorkflow(input: StartWorkflowInput, options: Workflow
|
|
|
116
141
|
host,
|
|
117
142
|
mode,
|
|
118
143
|
executionMode,
|
|
144
|
+
executionPolicy,
|
|
145
|
+
enabledGates,
|
|
146
|
+
artifactDirectory: config.workflow.artifactDirectory,
|
|
119
147
|
backend,
|
|
120
148
|
request,
|
|
121
149
|
phase: "requirements",
|
|
122
|
-
status: "awaiting_approval",
|
|
150
|
+
status: enabledGates.includes("requirements") ? "awaiting_approval" : "ready",
|
|
123
151
|
createdAt,
|
|
124
152
|
updatedAt: createdAt,
|
|
125
153
|
gates: {
|
|
126
|
-
requirements: "pending",
|
|
154
|
+
requirements: enabledGates.includes("requirements") ? "pending" : "not_started",
|
|
127
155
|
architecture: "not_started",
|
|
128
156
|
implementation: "not_started",
|
|
157
|
+
"implementation-review": "not_started",
|
|
129
158
|
testing: "not_started",
|
|
130
159
|
},
|
|
131
160
|
artifacts: {},
|
|
132
161
|
roles: [],
|
|
162
|
+
pendingQuestions: [],
|
|
133
163
|
answers: [],
|
|
134
164
|
approvals: [],
|
|
135
165
|
feedback: [],
|
|
@@ -137,6 +167,7 @@ export async function startWorkflow(input: StartWorkflowInput, options: Workflow
|
|
|
137
167
|
changedFiles: [],
|
|
138
168
|
implementationDiff: "",
|
|
139
169
|
verification: [],
|
|
170
|
+
verificationStatus: "not_run",
|
|
140
171
|
lintResults: [],
|
|
141
172
|
};
|
|
142
173
|
|
|
@@ -152,7 +183,12 @@ export async function getWorkflowStatus(input: RunRef): Promise<RunState> {
|
|
|
152
183
|
|
|
153
184
|
export async function continueWorkflow(input: RunRef): Promise<RunState> {
|
|
154
185
|
const state = await getWorkflowStatus(input);
|
|
155
|
-
if (
|
|
186
|
+
if (
|
|
187
|
+
state.status === "awaiting_approval" ||
|
|
188
|
+
state.status === "awaiting_input" ||
|
|
189
|
+
state.status === "awaiting_execution" ||
|
|
190
|
+
state.status === "complete"
|
|
191
|
+
) {
|
|
156
192
|
return state;
|
|
157
193
|
}
|
|
158
194
|
|
|
@@ -167,8 +203,14 @@ export async function continueWorkflow(input: RunRef): Promise<RunState> {
|
|
|
167
203
|
throw new Error("DevCrew execution phase requires orchestrated continuation");
|
|
168
204
|
}
|
|
169
205
|
|
|
170
|
-
const
|
|
206
|
+
const configuredGate = gateForPhase(state.phase);
|
|
207
|
+
const gate = gateForPhase(state.phase, state.enabledGates);
|
|
171
208
|
if (!gate) {
|
|
209
|
+
if (configuredGate) {
|
|
210
|
+
state.phase = nextPhaseAfterGate(state, configuredGate);
|
|
211
|
+
state.status = "ready";
|
|
212
|
+
return saveState(state);
|
|
213
|
+
}
|
|
172
214
|
state.status = "complete";
|
|
173
215
|
return saveState(state);
|
|
174
216
|
}
|
|
@@ -227,7 +269,16 @@ export async function rejectWorkflow(input: RejectWorkflowInput): Promise<RunSta
|
|
|
227
269
|
|
|
228
270
|
export async function answerWorkflow(input: AnswerWorkflowInput, options: WorkflowMutationOptions = {}): Promise<RunState> {
|
|
229
271
|
const state = await getWorkflowStatus(input);
|
|
230
|
-
const gate = gateForPhase(state.phase);
|
|
272
|
+
const gate = gateForPhase(state.phase, state.enabledGates);
|
|
273
|
+
if (state.status === "awaiting_input" && state.pendingQuestions.length > 0 && state.phase === "requirements") {
|
|
274
|
+
state.answers.push({
|
|
275
|
+
answer: parseAnswer(input.answer),
|
|
276
|
+
createdAt: now(),
|
|
277
|
+
});
|
|
278
|
+
state.pendingQuestions = [];
|
|
279
|
+
state.status = "ready";
|
|
280
|
+
return saveState(state);
|
|
281
|
+
}
|
|
231
282
|
if (state.status !== "awaiting_input" || !gate || state.gates[gate] !== "rejected") {
|
|
232
283
|
throw new Error("Workflow must be awaiting_input at a rejected current gate before recording an answer");
|
|
233
284
|
}
|
|
@@ -243,6 +294,26 @@ export async function answerWorkflow(input: AnswerWorkflowInput, options: Workfl
|
|
|
243
294
|
return saveState(state);
|
|
244
295
|
}
|
|
245
296
|
|
|
297
|
+
export async function waiveVerificationWorkflow(input: WaiveVerificationInput): Promise<RunState> {
|
|
298
|
+
const state = await getWorkflowStatus(input);
|
|
299
|
+
if (
|
|
300
|
+
state.executionMode !== "apply" ||
|
|
301
|
+
state.phase !== "testing" ||
|
|
302
|
+
state.status !== "awaiting_input" ||
|
|
303
|
+
state.gates.testing !== "rejected" ||
|
|
304
|
+
state.verificationStatus !== "failed"
|
|
305
|
+
) {
|
|
306
|
+
throw new Error("A verification waiver is only available after failed apply-mode testing");
|
|
307
|
+
}
|
|
308
|
+
state.verificationWaiver = {
|
|
309
|
+
reason: parseWaiverReason(input.reason),
|
|
310
|
+
createdAt: now(),
|
|
311
|
+
};
|
|
312
|
+
state.gates.testing = "pending";
|
|
313
|
+
state.status = "awaiting_approval";
|
|
314
|
+
return saveState(state);
|
|
315
|
+
}
|
|
316
|
+
|
|
246
317
|
export async function getArtifact(input: ArtifactRef): Promise<ArtifactReadResult> {
|
|
247
318
|
const state = await getWorkflowStatus(input);
|
|
248
319
|
return readArtifact(state, parseArtifactName(input.name));
|
|
@@ -15,15 +15,18 @@ import {
|
|
|
15
15
|
discoverVerifyCommands,
|
|
16
16
|
gateForPhase,
|
|
17
17
|
getWorkflowStatus,
|
|
18
|
+
nextPhaseAfterGate,
|
|
18
19
|
readConfig,
|
|
19
20
|
rejectWorkflow,
|
|
20
21
|
renderArtifact,
|
|
21
22
|
saveState,
|
|
22
23
|
startWorkflow,
|
|
23
24
|
validateWorkflowApproval,
|
|
25
|
+
waiveVerificationWorkflow,
|
|
24
26
|
type AnswerWorkflowInput,
|
|
25
27
|
type ApproveWorkflowInput,
|
|
26
28
|
type ArtifactName,
|
|
29
|
+
type CompleteExecutionInput,
|
|
27
30
|
type Phase,
|
|
28
31
|
type RejectWorkflowInput,
|
|
29
32
|
type RoleResult,
|
|
@@ -31,6 +34,8 @@ import {
|
|
|
31
34
|
type RunState,
|
|
32
35
|
type StartWorkflowInput,
|
|
33
36
|
type VerificationResult,
|
|
37
|
+
type VerificationStatus,
|
|
38
|
+
type WaiveVerificationInput,
|
|
34
39
|
} from "../../core/src/index.js";
|
|
35
40
|
import {
|
|
36
41
|
captureExecutionChanges,
|
|
@@ -51,6 +56,7 @@ function roleForPhase(phase: Phase): RoleResult["role"] | undefined {
|
|
|
51
56
|
architecture: "architect",
|
|
52
57
|
implementation: "implementer",
|
|
53
58
|
execution: "implementer",
|
|
59
|
+
review: "architect",
|
|
54
60
|
testing: "tester",
|
|
55
61
|
};
|
|
56
62
|
return roles[phase];
|
|
@@ -77,7 +83,7 @@ function priorArtifactNamesForPhase(phase: Phase): ArtifactName[] {
|
|
|
77
83
|
}
|
|
78
84
|
|
|
79
85
|
async function writeMarkdownArtifact(state: RunState, artifact: ArtifactName, markdown: string): Promise<string> {
|
|
80
|
-
const path = artifactPath(state.cwd, state.runId, artifact);
|
|
86
|
+
const path = artifactPath(state.cwd, state.runId, artifact, state.artifactDirectory);
|
|
81
87
|
await mkdir(dirname(path), { recursive: true });
|
|
82
88
|
await writeFile(path, markdown, "utf8");
|
|
83
89
|
return path;
|
|
@@ -225,9 +231,104 @@ function verificationBlock(results: VerificationResult[]): string {
|
|
|
225
231
|
.join("\n\n");
|
|
226
232
|
}
|
|
227
233
|
|
|
234
|
+
function verificationStatusFor(results: VerificationResult[]): VerificationStatus {
|
|
235
|
+
if (results.length === 0) {
|
|
236
|
+
return "not_run";
|
|
237
|
+
}
|
|
238
|
+
return results.every((result) => result.exitCode === 0) ? "passed" : "failed";
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function setTestingGateFromVerification(state: RunState): void {
|
|
242
|
+
if (state.verificationStatus === "failed") {
|
|
243
|
+
state.gates.testing = "rejected";
|
|
244
|
+
state.status = "awaiting_input";
|
|
245
|
+
state.feedback.push({
|
|
246
|
+
gate: "testing",
|
|
247
|
+
message: "Automated verification failed. Inspect the test report, revise the implementation, or record an explicit verification waiver with its reason.",
|
|
248
|
+
createdAt: now(),
|
|
249
|
+
});
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
state.gates.testing = "pending";
|
|
253
|
+
state.status = "awaiting_approval";
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function executionInstructions(state: RunState, phase: "execution" | "testing", workspacePath: string): string {
|
|
257
|
+
if (phase === "execution") {
|
|
258
|
+
return `Use the native ${state.host} host agent to implement the approved change in ${workspacePath}. Do not modify the requester checkout at ${state.cwd}. When complete, call devcrew_complete_execution with a concise summary.`;
|
|
259
|
+
}
|
|
260
|
+
return `Use the native ${state.host} host agent to validate the approved change in ${workspacePath}. Then call devcrew_complete_execution with a concise summary and each command's exit code and output.`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function hostCompletionResult(state: RunState, summary: string): RoleResult {
|
|
264
|
+
const role = state.phase === "execution" ? "implementer" : "tester";
|
|
265
|
+
const artifact = artifactForPhase(state.phase);
|
|
266
|
+
return {
|
|
267
|
+
role,
|
|
268
|
+
backend: state.backend,
|
|
269
|
+
summary: `${role} completed through the native ${state.host} host: ${summary}`,
|
|
270
|
+
markdown: `${renderArtifact(artifact, state).trim()}\n\n## Native Host Summary\n\n${summary}\n`,
|
|
271
|
+
usedFallback: false,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function parseCompletionSummary(value: unknown): string {
|
|
276
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
277
|
+
throw new Error("summary must be a non-empty string");
|
|
278
|
+
}
|
|
279
|
+
return value.trim();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function parseCompletionVerification(value: unknown): VerificationResult[] {
|
|
283
|
+
if (value === undefined) {
|
|
284
|
+
return [];
|
|
285
|
+
}
|
|
286
|
+
if (!Array.isArray(value)) {
|
|
287
|
+
throw new Error("verification must be an array");
|
|
288
|
+
}
|
|
289
|
+
return value.map((entry, index) => {
|
|
290
|
+
if (
|
|
291
|
+
!entry ||
|
|
292
|
+
typeof entry !== "object" ||
|
|
293
|
+
typeof entry.command !== "string" ||
|
|
294
|
+
entry.command.trim().length === 0 ||
|
|
295
|
+
!Number.isInteger(entry.exitCode) ||
|
|
296
|
+
typeof entry.output !== "string" ||
|
|
297
|
+
typeof entry.startedAt !== "string" ||
|
|
298
|
+
typeof entry.completedAt !== "string"
|
|
299
|
+
) {
|
|
300
|
+
throw new Error(`verification[${index}] must include command, integer exitCode, output, startedAt, and completedAt`);
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
command: entry.command.trim(),
|
|
304
|
+
exitCode: entry.exitCode,
|
|
305
|
+
output: entry.output,
|
|
306
|
+
startedAt: entry.startedAt,
|
|
307
|
+
completedAt: entry.completedAt,
|
|
308
|
+
};
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
228
312
|
function appendExecutionSections(artifact: ArtifactName, markdown: string, state: RunState): string {
|
|
229
|
-
if (artifact === "
|
|
230
|
-
|
|
313
|
+
if (artifact === "architecture-review" && state.architectureReview) {
|
|
314
|
+
let content = markdown.trim();
|
|
315
|
+
if (!content.includes("## Review Decision")) {
|
|
316
|
+
content += `\n\n## Review Decision\n\nDecision: ${state.architectureReview.decision}\n\nSummary: ${state.architectureReview.summary}`;
|
|
317
|
+
}
|
|
318
|
+
return `${content}\n`;
|
|
319
|
+
}
|
|
320
|
+
if (artifact === "test-report") {
|
|
321
|
+
let content = markdown.trim();
|
|
322
|
+
if (!content.includes("## Acceptance Evidence")) {
|
|
323
|
+
content += `\n\n## Acceptance Evidence\n\n${verificationBlock(state.verification)}`;
|
|
324
|
+
}
|
|
325
|
+
if (!content.includes("## Verification Outcome")) {
|
|
326
|
+
content += `\n\n## Verification Outcome\n\nStatus: ${state.verificationStatus}`;
|
|
327
|
+
}
|
|
328
|
+
if (state.verificationWaiver && !content.includes("## Verification Waiver")) {
|
|
329
|
+
content += `\n\n## Verification Waiver\n\nReason: ${state.verificationWaiver.reason}\nRecorded At: ${state.verificationWaiver.createdAt}`;
|
|
330
|
+
}
|
|
331
|
+
return `${content}\n`;
|
|
231
332
|
}
|
|
232
333
|
return markdown;
|
|
233
334
|
}
|
|
@@ -248,9 +349,22 @@ async function runCurrentPhaseRole(state: RunState, runner: RoleRunner = runRole
|
|
|
248
349
|
const workspace = await ensureExecutionWorkspace(state);
|
|
249
350
|
state.executionWorkspace = workspace;
|
|
250
351
|
state.verification = [];
|
|
352
|
+
state.verificationStatus = "not_run";
|
|
353
|
+
delete state.verificationWaiver;
|
|
251
354
|
delete state.artifacts["test-report"];
|
|
252
355
|
await saveState(state);
|
|
253
356
|
|
|
357
|
+
if (state.executionPolicy === "interactive-host") {
|
|
358
|
+
state.executionInstruction = {
|
|
359
|
+
phase: "execution",
|
|
360
|
+
workspacePath: workspace.path,
|
|
361
|
+
instructions: executionInstructions(state, "execution", workspace.path),
|
|
362
|
+
createdAt: now(),
|
|
363
|
+
};
|
|
364
|
+
state.status = "awaiting_execution";
|
|
365
|
+
return saveState(state);
|
|
366
|
+
}
|
|
367
|
+
|
|
254
368
|
const result = await runner({
|
|
255
369
|
backend: state.backend,
|
|
256
370
|
role: "implementer",
|
|
@@ -258,9 +372,10 @@ async function runCurrentPhaseRole(state: RunState, runner: RoleRunner = runRole
|
|
|
258
372
|
request: state.request,
|
|
259
373
|
mode: state.mode,
|
|
260
374
|
executionMode: "apply",
|
|
375
|
+
executionPolicy: state.executionPolicy,
|
|
261
376
|
cwd: workspace.path,
|
|
262
377
|
standards: state.standards.combined,
|
|
263
|
-
artifactPath: artifactPath(state.cwd, state.runId, "implementation-review"),
|
|
378
|
+
artifactPath: artifactPath(state.cwd, state.runId, "implementation-review", state.artifactDirectory),
|
|
264
379
|
answers: state.answers.map((entry) => entry.answer),
|
|
265
380
|
feedback: state.feedback.map((entry) => `${entry.gate}: ${entry.message}`),
|
|
266
381
|
priorArtifacts: await readPriorArtifacts(state),
|
|
@@ -271,15 +386,17 @@ async function runCurrentPhaseRole(state: RunState, runner: RoleRunner = runRole
|
|
|
271
386
|
state.changedFiles = captured.changedFiles;
|
|
272
387
|
state.implementationDiff = captured.patch;
|
|
273
388
|
state.roles.push(result);
|
|
389
|
+
state.executionInstruction = undefined;
|
|
274
390
|
await writeImplementationReview(state);
|
|
275
|
-
state.phase = "
|
|
391
|
+
state.phase = "review";
|
|
276
392
|
state.status = "ready";
|
|
277
393
|
return saveState(state);
|
|
278
394
|
}
|
|
279
395
|
|
|
280
|
-
const
|
|
396
|
+
const configuredGate = gateForPhase(state.phase);
|
|
397
|
+
const gate = gateForPhase(state.phase, state.enabledGates);
|
|
281
398
|
const role = roleForPhase(state.phase);
|
|
282
|
-
if (!
|
|
399
|
+
if (!role) {
|
|
283
400
|
const artifact = artifactForPhase(state.phase);
|
|
284
401
|
const markdown = renderArtifact(artifact, state);
|
|
285
402
|
state.artifacts[artifact] = await writeMarkdownArtifact(state, artifact, markdown);
|
|
@@ -289,13 +406,24 @@ async function runCurrentPhaseRole(state: RunState, runner: RoleRunner = runRole
|
|
|
289
406
|
}
|
|
290
407
|
|
|
291
408
|
const artifact = artifactForPhase(state.phase);
|
|
292
|
-
const path = artifactPath(state.cwd, state.runId, artifact);
|
|
409
|
+
const path = artifactPath(state.cwd, state.runId, artifact, state.artifactDirectory);
|
|
293
410
|
const applyingTesting = state.executionMode === "apply" && state.phase === "testing";
|
|
294
411
|
const roleCwd = applyingTesting ? state.executionWorkspace?.path : state.cwd;
|
|
295
412
|
if (!roleCwd) {
|
|
296
413
|
throw new Error("DevCrew apply testing requires an execution workspace");
|
|
297
414
|
}
|
|
298
|
-
state.roles.push(conductorDecision(state, role, gate));
|
|
415
|
+
state.roles.push(conductorDecision(state, role, gate ?? `${state.phase} automatic transition`));
|
|
416
|
+
|
|
417
|
+
if (applyingTesting && state.executionPolicy === "interactive-host") {
|
|
418
|
+
state.executionInstruction = {
|
|
419
|
+
phase: "testing",
|
|
420
|
+
workspacePath: roleCwd,
|
|
421
|
+
instructions: executionInstructions(state, "testing", roleCwd),
|
|
422
|
+
createdAt: now(),
|
|
423
|
+
};
|
|
424
|
+
state.status = "awaiting_execution";
|
|
425
|
+
return saveState(state);
|
|
426
|
+
}
|
|
299
427
|
|
|
300
428
|
const result = await runner({
|
|
301
429
|
backend: state.backend,
|
|
@@ -304,6 +432,7 @@ async function runCurrentPhaseRole(state: RunState, runner: RoleRunner = runRole
|
|
|
304
432
|
request: state.request,
|
|
305
433
|
mode: state.mode,
|
|
306
434
|
executionMode: state.executionMode,
|
|
435
|
+
executionPolicy: state.executionPolicy,
|
|
307
436
|
cwd: roleCwd,
|
|
308
437
|
standards: state.standards.combined,
|
|
309
438
|
artifactPath: path,
|
|
@@ -314,20 +443,61 @@ async function runCurrentPhaseRole(state: RunState, runner: RoleRunner = runRole
|
|
|
314
443
|
|
|
315
444
|
if (applyingTesting && state.executionWorkspace) {
|
|
316
445
|
state.verification = await runConfiguredVerification(state, roleCwd);
|
|
446
|
+
state.verificationStatus = verificationStatusFor(state.verification);
|
|
317
447
|
const captured = await captureExecutionChanges(state.executionWorkspace);
|
|
318
448
|
state.changedFiles = captured.changedFiles;
|
|
319
449
|
state.implementationDiff = captured.patch;
|
|
320
450
|
await writeImplementationReview(state);
|
|
321
451
|
}
|
|
322
452
|
|
|
453
|
+
if (state.phase === "review") {
|
|
454
|
+
if (!result.reviewDecision) {
|
|
455
|
+
throw new Error("DevCrew architecture review must return a structured review decision");
|
|
456
|
+
}
|
|
457
|
+
state.architectureReview = {
|
|
458
|
+
decision: result.reviewDecision,
|
|
459
|
+
summary: result.summary,
|
|
460
|
+
reviewedAt: now(),
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
323
464
|
// When the backend cannot run a real SDK we keep a single deterministic
|
|
324
465
|
// artifact source by rendering the rich phase template from the core layer.
|
|
325
466
|
const baseMarkdown = result.usedFallback ? `${fallbackNotice(result)}${renderArtifact(artifact, state)}` : result.markdown;
|
|
326
467
|
const markdown = appendExecutionSections(artifact, baseMarkdown, state);
|
|
327
468
|
state.roles.push({ ...result, markdown });
|
|
328
469
|
state.artifacts[artifact] = await writeMarkdownArtifact(state, artifact, markdown);
|
|
329
|
-
state.
|
|
330
|
-
state.
|
|
470
|
+
state.executionInstruction = undefined;
|
|
471
|
+
if (state.phase === "requirements" && result.questions && result.questions.length > 0) {
|
|
472
|
+
state.pendingQuestions = result.questions;
|
|
473
|
+
state.gates.requirements = "not_started";
|
|
474
|
+
state.status = "awaiting_input";
|
|
475
|
+
return saveState(state);
|
|
476
|
+
}
|
|
477
|
+
if (state.phase === "review" && state.architectureReview?.decision === "changes_required") {
|
|
478
|
+
state.gates["implementation-review"] = "rejected";
|
|
479
|
+
state.status = "awaiting_input";
|
|
480
|
+
state.feedback.push({
|
|
481
|
+
gate: "implementation-review",
|
|
482
|
+
message: state.architectureReview.summary,
|
|
483
|
+
createdAt: now(),
|
|
484
|
+
});
|
|
485
|
+
return saveState(state);
|
|
486
|
+
}
|
|
487
|
+
if (!gate && configuredGate) {
|
|
488
|
+
state.phase = nextPhaseAfterGate(state, configuredGate);
|
|
489
|
+
state.status = "ready";
|
|
490
|
+
return saveState(state);
|
|
491
|
+
}
|
|
492
|
+
if (applyingTesting) {
|
|
493
|
+
setTestingGateFromVerification(state);
|
|
494
|
+
} else {
|
|
495
|
+
if (!gate) {
|
|
496
|
+
throw new Error(`DevCrew has no configured gate for ${state.phase}`);
|
|
497
|
+
}
|
|
498
|
+
state.gates[gate] = "pending";
|
|
499
|
+
state.status = "awaiting_approval";
|
|
500
|
+
}
|
|
331
501
|
return saveState(state);
|
|
332
502
|
}
|
|
333
503
|
|
|
@@ -338,7 +508,12 @@ export async function startOrchestratedWorkflow(input: StartWorkflowInput, runne
|
|
|
338
508
|
|
|
339
509
|
export async function continueOrchestratedWorkflow(input: RunRef, runner: RoleRunner = runRole): Promise<RunState> {
|
|
340
510
|
const state = await getWorkflowStatus(input);
|
|
341
|
-
if (
|
|
511
|
+
if (
|
|
512
|
+
state.status === "awaiting_approval" ||
|
|
513
|
+
state.status === "awaiting_input" ||
|
|
514
|
+
state.status === "awaiting_execution" ||
|
|
515
|
+
state.status === "complete"
|
|
516
|
+
) {
|
|
342
517
|
return state;
|
|
343
518
|
}
|
|
344
519
|
|
|
@@ -377,6 +552,9 @@ export async function approveOrchestratedWorkflow(input: ApproveWorkflowInput):
|
|
|
377
552
|
return cleanupAfterApproval(before);
|
|
378
553
|
}
|
|
379
554
|
if (promotingTesting) {
|
|
555
|
+
if (before.verificationStatus === "failed" && !before.verificationWaiver) {
|
|
556
|
+
throw new Error("Failed verification cannot be promoted without an explicit verification waiver");
|
|
557
|
+
}
|
|
380
558
|
await promoteExecutionChanges(before);
|
|
381
559
|
let approved: RunState;
|
|
382
560
|
try {
|
|
@@ -396,6 +574,67 @@ export async function approveOrchestratedWorkflow(input: ApproveWorkflowInput):
|
|
|
396
574
|
return approveWorkflow(input);
|
|
397
575
|
}
|
|
398
576
|
|
|
577
|
+
export async function waiveOrchestratedVerification(input: WaiveVerificationInput): Promise<RunState> {
|
|
578
|
+
const state = await waiveVerificationWorkflow(input);
|
|
579
|
+
const reportPath = state.artifacts["test-report"];
|
|
580
|
+
if (reportPath) {
|
|
581
|
+
const report = await readFile(reportPath, "utf8");
|
|
582
|
+
await writeFile(reportPath, appendExecutionSections("test-report", report, state), "utf8");
|
|
583
|
+
}
|
|
584
|
+
return state;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
export async function completeOrchestratedExecution(input: CompleteExecutionInput): Promise<RunState> {
|
|
588
|
+
const state = await getWorkflowStatus(input);
|
|
589
|
+
const instruction = state.executionInstruction;
|
|
590
|
+
if (
|
|
591
|
+
state.executionMode !== "apply" ||
|
|
592
|
+
state.executionPolicy !== "interactive-host" ||
|
|
593
|
+
state.status !== "awaiting_execution" ||
|
|
594
|
+
!instruction ||
|
|
595
|
+
instruction.phase !== state.phase ||
|
|
596
|
+
!state.executionWorkspace ||
|
|
597
|
+
instruction.workspacePath !== state.executionWorkspace.path
|
|
598
|
+
) {
|
|
599
|
+
throw new Error("DevCrew is not awaiting an interactive-host execution completion");
|
|
600
|
+
}
|
|
601
|
+
const summary = parseCompletionSummary(input.summary);
|
|
602
|
+
|
|
603
|
+
if (state.phase === "execution") {
|
|
604
|
+
if (input.verification !== undefined) {
|
|
605
|
+
throw new Error("verification can only be submitted after interactive-host testing");
|
|
606
|
+
}
|
|
607
|
+
const captured = await captureExecutionChanges(state.executionWorkspace);
|
|
608
|
+
state.changedFiles = captured.changedFiles;
|
|
609
|
+
state.implementationDiff = captured.patch;
|
|
610
|
+
state.lintResults = [];
|
|
611
|
+
state.roles.push(hostCompletionResult(state, summary));
|
|
612
|
+
state.executionInstruction = undefined;
|
|
613
|
+
await writeImplementationReview(state);
|
|
614
|
+
state.phase = "review";
|
|
615
|
+
state.status = "ready";
|
|
616
|
+
return saveState(state);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (state.phase !== "testing") {
|
|
620
|
+
throw new Error(`Cannot complete interactive-host work during ${state.phase}`);
|
|
621
|
+
}
|
|
622
|
+
state.verification = parseCompletionVerification(input.verification);
|
|
623
|
+
state.verificationStatus = verificationStatusFor(state.verification);
|
|
624
|
+
const captured = await captureExecutionChanges(state.executionWorkspace);
|
|
625
|
+
state.changedFiles = captured.changedFiles;
|
|
626
|
+
state.implementationDiff = captured.patch;
|
|
627
|
+
await writeImplementationReview(state);
|
|
628
|
+
const result = hostCompletionResult(state, summary);
|
|
629
|
+
const artifact = artifactForPhase(state.phase);
|
|
630
|
+
const markdown = appendExecutionSections(artifact, result.markdown, state);
|
|
631
|
+
state.roles.push({ ...result, markdown });
|
|
632
|
+
state.artifacts[artifact] = await writeMarkdownArtifact(state, artifact, markdown);
|
|
633
|
+
state.executionInstruction = undefined;
|
|
634
|
+
setTestingGateFromVerification(state);
|
|
635
|
+
return saveState(state);
|
|
636
|
+
}
|
|
637
|
+
|
|
399
638
|
export async function rejectOrchestratedWorkflow(input: RejectWorkflowInput): Promise<RunState> {
|
|
400
639
|
return rejectWorkflow(input);
|
|
401
640
|
}
|
|
@@ -414,9 +653,8 @@ export async function answerOrchestratedWorkflow(input: AnswerWorkflowInput, run
|
|
|
414
653
|
return saveState(state);
|
|
415
654
|
}
|
|
416
655
|
|
|
417
|
-
const gate = gateForPhase(state.phase);
|
|
418
656
|
const role = roleForPhase(state.phase);
|
|
419
|
-
if (!
|
|
657
|
+
if (!role) {
|
|
420
658
|
return state;
|
|
421
659
|
}
|
|
422
660
|
|
|
@@ -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
|
|