@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
|
@@ -3,7 +3,8 @@ import { ensureConfig } from "./config.js";
|
|
|
3
3
|
import { readArtifact, writeArtifact } from "./artifacts.js";
|
|
4
4
|
import { discoverStandards } from "./standards.js";
|
|
5
5
|
import { loadState, saveState } from "./store.js";
|
|
6
|
-
import { parseAnswer, parseArtifactName, parseBackend, parseCwd, parseExecutionMode, parseFeedback, parseGate, parseHost, parseOptionalNote, parseRequest, parseRunId, parseWorkflowMode, } from "./validation.js";
|
|
6
|
+
import { parseAnswer, parseArtifactName, parseBackend, parseCwd, parseExecutionMode, parseExecutionPolicy, parseFeedback, parseGate, parseHost, parseOptionalNote, parseRequest, parseRunId, parseWaiverReason, parseWorkflowMode, } from "./validation.js";
|
|
7
|
+
import { GATES } from "./types.js";
|
|
7
8
|
function now() {
|
|
8
9
|
return new Date().toISOString();
|
|
9
10
|
}
|
|
@@ -18,6 +19,8 @@ export function nextPhaseAfterGate(state, gate) {
|
|
|
18
19
|
return "implementation";
|
|
19
20
|
case "implementation":
|
|
20
21
|
return state.executionMode === "apply" ? "execution" : "testing";
|
|
22
|
+
case "implementation-review":
|
|
23
|
+
return "testing";
|
|
21
24
|
case "testing":
|
|
22
25
|
return "acceptance";
|
|
23
26
|
}
|
|
@@ -28,20 +31,32 @@ export function artifactForPhase(phase) {
|
|
|
28
31
|
architecture: "architecture",
|
|
29
32
|
implementation: "implementation-plan",
|
|
30
33
|
execution: "implementation-review",
|
|
34
|
+
review: "architecture-review",
|
|
31
35
|
testing: "test-report",
|
|
32
36
|
acceptance: "acceptance",
|
|
33
37
|
complete: "acceptance",
|
|
34
38
|
};
|
|
35
39
|
return artifactByPhase[phase];
|
|
36
40
|
}
|
|
37
|
-
export function gateForPhase(phase) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
export function gateForPhase(phase, enabledGates) {
|
|
42
|
+
const gate = phase === "review"
|
|
43
|
+
? "implementation-review"
|
|
44
|
+
: (phase === "requirements" ||
|
|
45
|
+
phase === "architecture" ||
|
|
46
|
+
phase === "implementation" ||
|
|
47
|
+
phase === "testing"
|
|
48
|
+
? phase
|
|
49
|
+
: undefined);
|
|
50
|
+
return gate && (!enabledGates || enabledGates.includes(gate)) ? gate : undefined;
|
|
51
|
+
}
|
|
52
|
+
function configuredGates(gates) {
|
|
53
|
+
const configured = new Set(gates.filter((gate) => GATES.includes(gate)));
|
|
54
|
+
configured.add("implementation-review");
|
|
55
|
+
configured.add("testing");
|
|
56
|
+
return [...configured];
|
|
42
57
|
}
|
|
43
58
|
function assertPendingCurrentGate(state, gate) {
|
|
44
|
-
const expected = gateForPhase(state.phase);
|
|
59
|
+
const expected = gateForPhase(state.phase, state.enabledGates);
|
|
45
60
|
if (expected !== gate) {
|
|
46
61
|
throw new Error(expected
|
|
47
62
|
? `Cannot act on ${gate} while current gate is ${expected}`
|
|
@@ -65,6 +80,8 @@ export async function startWorkflow(input, options = {}) {
|
|
|
65
80
|
const config = await ensureConfig(cwd);
|
|
66
81
|
const backend = input.backend ? parseBackend(input.backend) : config.defaultBackend === "host-preferred" ? host : config.defaultBackend;
|
|
67
82
|
const executionMode = input.executionMode ? parseExecutionMode(input.executionMode) : config.executionMode;
|
|
83
|
+
const executionPolicy = input.executionPolicy ? parseExecutionPolicy(input.executionPolicy) : "interactive-host";
|
|
84
|
+
const enabledGates = configuredGates(config.workflow.gates);
|
|
68
85
|
if (executionMode === "apply" && backend === "local") {
|
|
69
86
|
throw new Error("DevCrew apply mode requires a codex or claude backend; local is plan-only");
|
|
70
87
|
}
|
|
@@ -76,20 +93,25 @@ export async function startWorkflow(input, options = {}) {
|
|
|
76
93
|
host,
|
|
77
94
|
mode,
|
|
78
95
|
executionMode,
|
|
96
|
+
executionPolicy,
|
|
97
|
+
enabledGates,
|
|
98
|
+
artifactDirectory: config.workflow.artifactDirectory,
|
|
79
99
|
backend,
|
|
80
100
|
request,
|
|
81
101
|
phase: "requirements",
|
|
82
|
-
status: "awaiting_approval",
|
|
102
|
+
status: enabledGates.includes("requirements") ? "awaiting_approval" : "ready",
|
|
83
103
|
createdAt,
|
|
84
104
|
updatedAt: createdAt,
|
|
85
105
|
gates: {
|
|
86
|
-
requirements: "pending",
|
|
106
|
+
requirements: enabledGates.includes("requirements") ? "pending" : "not_started",
|
|
87
107
|
architecture: "not_started",
|
|
88
108
|
implementation: "not_started",
|
|
109
|
+
"implementation-review": "not_started",
|
|
89
110
|
testing: "not_started",
|
|
90
111
|
},
|
|
91
112
|
artifacts: {},
|
|
92
113
|
roles: [],
|
|
114
|
+
pendingQuestions: [],
|
|
93
115
|
answers: [],
|
|
94
116
|
approvals: [],
|
|
95
117
|
feedback: [],
|
|
@@ -97,6 +119,7 @@ export async function startWorkflow(input, options = {}) {
|
|
|
97
119
|
changedFiles: [],
|
|
98
120
|
implementationDiff: "",
|
|
99
121
|
verification: [],
|
|
122
|
+
verificationStatus: "not_run",
|
|
100
123
|
lintResults: [],
|
|
101
124
|
};
|
|
102
125
|
if (!options.skipArtifactWrite) {
|
|
@@ -109,7 +132,10 @@ export async function getWorkflowStatus(input) {
|
|
|
109
132
|
}
|
|
110
133
|
export async function continueWorkflow(input) {
|
|
111
134
|
const state = await getWorkflowStatus(input);
|
|
112
|
-
if (state.status === "awaiting_approval" ||
|
|
135
|
+
if (state.status === "awaiting_approval" ||
|
|
136
|
+
state.status === "awaiting_input" ||
|
|
137
|
+
state.status === "awaiting_execution" ||
|
|
138
|
+
state.status === "complete") {
|
|
113
139
|
return state;
|
|
114
140
|
}
|
|
115
141
|
if (state.phase === "acceptance") {
|
|
@@ -121,8 +147,14 @@ export async function continueWorkflow(input) {
|
|
|
121
147
|
if (state.phase === "execution") {
|
|
122
148
|
throw new Error("DevCrew execution phase requires orchestrated continuation");
|
|
123
149
|
}
|
|
124
|
-
const
|
|
150
|
+
const configuredGate = gateForPhase(state.phase);
|
|
151
|
+
const gate = gateForPhase(state.phase, state.enabledGates);
|
|
125
152
|
if (!gate) {
|
|
153
|
+
if (configuredGate) {
|
|
154
|
+
state.phase = nextPhaseAfterGate(state, configuredGate);
|
|
155
|
+
state.status = "ready";
|
|
156
|
+
return saveState(state);
|
|
157
|
+
}
|
|
126
158
|
state.status = "complete";
|
|
127
159
|
return saveState(state);
|
|
128
160
|
}
|
|
@@ -176,7 +208,16 @@ export async function rejectWorkflow(input) {
|
|
|
176
208
|
}
|
|
177
209
|
export async function answerWorkflow(input, options = {}) {
|
|
178
210
|
const state = await getWorkflowStatus(input);
|
|
179
|
-
const gate = gateForPhase(state.phase);
|
|
211
|
+
const gate = gateForPhase(state.phase, state.enabledGates);
|
|
212
|
+
if (state.status === "awaiting_input" && state.pendingQuestions.length > 0 && state.phase === "requirements") {
|
|
213
|
+
state.answers.push({
|
|
214
|
+
answer: parseAnswer(input.answer),
|
|
215
|
+
createdAt: now(),
|
|
216
|
+
});
|
|
217
|
+
state.pendingQuestions = [];
|
|
218
|
+
state.status = "ready";
|
|
219
|
+
return saveState(state);
|
|
220
|
+
}
|
|
180
221
|
if (state.status !== "awaiting_input" || !gate || state.gates[gate] !== "rejected") {
|
|
181
222
|
throw new Error("Workflow must be awaiting_input at a rejected current gate before recording an answer");
|
|
182
223
|
}
|
|
@@ -191,6 +232,23 @@ export async function answerWorkflow(input, options = {}) {
|
|
|
191
232
|
}
|
|
192
233
|
return saveState(state);
|
|
193
234
|
}
|
|
235
|
+
export async function waiveVerificationWorkflow(input) {
|
|
236
|
+
const state = await getWorkflowStatus(input);
|
|
237
|
+
if (state.executionMode !== "apply" ||
|
|
238
|
+
state.phase !== "testing" ||
|
|
239
|
+
state.status !== "awaiting_input" ||
|
|
240
|
+
state.gates.testing !== "rejected" ||
|
|
241
|
+
state.verificationStatus !== "failed") {
|
|
242
|
+
throw new Error("A verification waiver is only available after failed apply-mode testing");
|
|
243
|
+
}
|
|
244
|
+
state.verificationWaiver = {
|
|
245
|
+
reason: parseWaiverReason(input.reason),
|
|
246
|
+
createdAt: now(),
|
|
247
|
+
};
|
|
248
|
+
state.gates.testing = "pending";
|
|
249
|
+
state.status = "awaiting_approval";
|
|
250
|
+
return saveState(state);
|
|
251
|
+
}
|
|
194
252
|
export async function getArtifact(input) {
|
|
195
253
|
const state = await getWorkflowStatus(input);
|
|
196
254
|
return readArtifact(state, parseArtifactName(input.name));
|
|
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
4
|
import { runRole } from "../../adapters/src/index.js";
|
|
5
|
-
import { answerWorkflow, approveWorkflow, artifactForPhase, artifactPath, ARTIFACTS, discoverCoverageCommands, discoverLintCommands, discoverVerifyCommands, gateForPhase, getWorkflowStatus, readConfig, rejectWorkflow, renderArtifact, saveState, startWorkflow, validateWorkflowApproval, } from "../../core/src/index.js";
|
|
5
|
+
import { answerWorkflow, approveWorkflow, artifactForPhase, artifactPath, ARTIFACTS, discoverCoverageCommands, discoverLintCommands, discoverVerifyCommands, gateForPhase, getWorkflowStatus, nextPhaseAfterGate, readConfig, rejectWorkflow, renderArtifact, saveState, startWorkflow, validateWorkflowApproval, waiveVerificationWorkflow, } from "../../core/src/index.js";
|
|
6
6
|
import { captureExecutionChanges, cleanupExecutionWorkspace, ensureExecutionWorkspace, promoteExecutionChanges, rollbackPromotedExecutionChanges, } from "./worktree.js";
|
|
7
7
|
// Hard cap so a hung apply/verify command cannot block the serialized MCP loop.
|
|
8
8
|
const COMMAND_TIMEOUT_MS = 300_000;
|
|
@@ -12,6 +12,7 @@ function roleForPhase(phase) {
|
|
|
12
12
|
architecture: "architect",
|
|
13
13
|
implementation: "implementer",
|
|
14
14
|
execution: "implementer",
|
|
15
|
+
review: "architect",
|
|
15
16
|
testing: "tester",
|
|
16
17
|
};
|
|
17
18
|
return roles[phase];
|
|
@@ -35,7 +36,7 @@ function priorArtifactNamesForPhase(phase) {
|
|
|
35
36
|
return ARTIFACTS.slice(0, currentIndex);
|
|
36
37
|
}
|
|
37
38
|
async function writeMarkdownArtifact(state, artifact, markdown) {
|
|
38
|
-
const path = artifactPath(state.cwd, state.runId, artifact);
|
|
39
|
+
const path = artifactPath(state.cwd, state.runId, artifact, state.artifactDirectory);
|
|
39
40
|
await mkdir(dirname(path), { recursive: true });
|
|
40
41
|
await writeFile(path, markdown, "utf8");
|
|
41
42
|
return path;
|
|
@@ -162,9 +163,96 @@ function verificationBlock(results) {
|
|
|
162
163
|
.map((result) => `### ${result.command}\n\nExit Code: ${result.exitCode}\n\nOutput:\n\n\`\`\`text\n${result.output || "(no output)"}\n\`\`\``)
|
|
163
164
|
.join("\n\n");
|
|
164
165
|
}
|
|
166
|
+
function verificationStatusFor(results) {
|
|
167
|
+
if (results.length === 0) {
|
|
168
|
+
return "not_run";
|
|
169
|
+
}
|
|
170
|
+
return results.every((result) => result.exitCode === 0) ? "passed" : "failed";
|
|
171
|
+
}
|
|
172
|
+
function setTestingGateFromVerification(state) {
|
|
173
|
+
if (state.verificationStatus === "failed") {
|
|
174
|
+
state.gates.testing = "rejected";
|
|
175
|
+
state.status = "awaiting_input";
|
|
176
|
+
state.feedback.push({
|
|
177
|
+
gate: "testing",
|
|
178
|
+
message: "Automated verification failed. Inspect the test report, revise the implementation, or record an explicit verification waiver with its reason.",
|
|
179
|
+
createdAt: now(),
|
|
180
|
+
});
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
state.gates.testing = "pending";
|
|
184
|
+
state.status = "awaiting_approval";
|
|
185
|
+
}
|
|
186
|
+
function executionInstructions(state, phase, workspacePath) {
|
|
187
|
+
if (phase === "execution") {
|
|
188
|
+
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.`;
|
|
189
|
+
}
|
|
190
|
+
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.`;
|
|
191
|
+
}
|
|
192
|
+
function hostCompletionResult(state, summary) {
|
|
193
|
+
const role = state.phase === "execution" ? "implementer" : "tester";
|
|
194
|
+
const artifact = artifactForPhase(state.phase);
|
|
195
|
+
return {
|
|
196
|
+
role,
|
|
197
|
+
backend: state.backend,
|
|
198
|
+
summary: `${role} completed through the native ${state.host} host: ${summary}`,
|
|
199
|
+
markdown: `${renderArtifact(artifact, state).trim()}\n\n## Native Host Summary\n\n${summary}\n`,
|
|
200
|
+
usedFallback: false,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function parseCompletionSummary(value) {
|
|
204
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
205
|
+
throw new Error("summary must be a non-empty string");
|
|
206
|
+
}
|
|
207
|
+
return value.trim();
|
|
208
|
+
}
|
|
209
|
+
function parseCompletionVerification(value) {
|
|
210
|
+
if (value === undefined) {
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
if (!Array.isArray(value)) {
|
|
214
|
+
throw new Error("verification must be an array");
|
|
215
|
+
}
|
|
216
|
+
return value.map((entry, index) => {
|
|
217
|
+
if (!entry ||
|
|
218
|
+
typeof entry !== "object" ||
|
|
219
|
+
typeof entry.command !== "string" ||
|
|
220
|
+
entry.command.trim().length === 0 ||
|
|
221
|
+
!Number.isInteger(entry.exitCode) ||
|
|
222
|
+
typeof entry.output !== "string" ||
|
|
223
|
+
typeof entry.startedAt !== "string" ||
|
|
224
|
+
typeof entry.completedAt !== "string") {
|
|
225
|
+
throw new Error(`verification[${index}] must include command, integer exitCode, output, startedAt, and completedAt`);
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
command: entry.command.trim(),
|
|
229
|
+
exitCode: entry.exitCode,
|
|
230
|
+
output: entry.output,
|
|
231
|
+
startedAt: entry.startedAt,
|
|
232
|
+
completedAt: entry.completedAt,
|
|
233
|
+
};
|
|
234
|
+
});
|
|
235
|
+
}
|
|
165
236
|
function appendExecutionSections(artifact, markdown, state) {
|
|
166
|
-
if (artifact === "
|
|
167
|
-
|
|
237
|
+
if (artifact === "architecture-review" && state.architectureReview) {
|
|
238
|
+
let content = markdown.trim();
|
|
239
|
+
if (!content.includes("## Review Decision")) {
|
|
240
|
+
content += `\n\n## Review Decision\n\nDecision: ${state.architectureReview.decision}\n\nSummary: ${state.architectureReview.summary}`;
|
|
241
|
+
}
|
|
242
|
+
return `${content}\n`;
|
|
243
|
+
}
|
|
244
|
+
if (artifact === "test-report") {
|
|
245
|
+
let content = markdown.trim();
|
|
246
|
+
if (!content.includes("## Acceptance Evidence")) {
|
|
247
|
+
content += `\n\n## Acceptance Evidence\n\n${verificationBlock(state.verification)}`;
|
|
248
|
+
}
|
|
249
|
+
if (!content.includes("## Verification Outcome")) {
|
|
250
|
+
content += `\n\n## Verification Outcome\n\nStatus: ${state.verificationStatus}`;
|
|
251
|
+
}
|
|
252
|
+
if (state.verificationWaiver && !content.includes("## Verification Waiver")) {
|
|
253
|
+
content += `\n\n## Verification Waiver\n\nReason: ${state.verificationWaiver.reason}\nRecorded At: ${state.verificationWaiver.createdAt}`;
|
|
254
|
+
}
|
|
255
|
+
return `${content}\n`;
|
|
168
256
|
}
|
|
169
257
|
return markdown;
|
|
170
258
|
}
|
|
@@ -179,8 +267,20 @@ async function runCurrentPhaseRole(state, runner = runRole) {
|
|
|
179
267
|
const workspace = await ensureExecutionWorkspace(state);
|
|
180
268
|
state.executionWorkspace = workspace;
|
|
181
269
|
state.verification = [];
|
|
270
|
+
state.verificationStatus = "not_run";
|
|
271
|
+
delete state.verificationWaiver;
|
|
182
272
|
delete state.artifacts["test-report"];
|
|
183
273
|
await saveState(state);
|
|
274
|
+
if (state.executionPolicy === "interactive-host") {
|
|
275
|
+
state.executionInstruction = {
|
|
276
|
+
phase: "execution",
|
|
277
|
+
workspacePath: workspace.path,
|
|
278
|
+
instructions: executionInstructions(state, "execution", workspace.path),
|
|
279
|
+
createdAt: now(),
|
|
280
|
+
};
|
|
281
|
+
state.status = "awaiting_execution";
|
|
282
|
+
return saveState(state);
|
|
283
|
+
}
|
|
184
284
|
const result = await runner({
|
|
185
285
|
backend: state.backend,
|
|
186
286
|
role: "implementer",
|
|
@@ -188,9 +288,10 @@ async function runCurrentPhaseRole(state, runner = runRole) {
|
|
|
188
288
|
request: state.request,
|
|
189
289
|
mode: state.mode,
|
|
190
290
|
executionMode: "apply",
|
|
291
|
+
executionPolicy: state.executionPolicy,
|
|
191
292
|
cwd: workspace.path,
|
|
192
293
|
standards: state.standards.combined,
|
|
193
|
-
artifactPath: artifactPath(state.cwd, state.runId, "implementation-review"),
|
|
294
|
+
artifactPath: artifactPath(state.cwd, state.runId, "implementation-review", state.artifactDirectory),
|
|
194
295
|
answers: state.answers.map((entry) => entry.answer),
|
|
195
296
|
feedback: state.feedback.map((entry) => `${entry.gate}: ${entry.message}`),
|
|
196
297
|
priorArtifacts: await readPriorArtifacts(state),
|
|
@@ -201,14 +302,16 @@ async function runCurrentPhaseRole(state, runner = runRole) {
|
|
|
201
302
|
state.changedFiles = captured.changedFiles;
|
|
202
303
|
state.implementationDiff = captured.patch;
|
|
203
304
|
state.roles.push(result);
|
|
305
|
+
state.executionInstruction = undefined;
|
|
204
306
|
await writeImplementationReview(state);
|
|
205
|
-
state.phase = "
|
|
307
|
+
state.phase = "review";
|
|
206
308
|
state.status = "ready";
|
|
207
309
|
return saveState(state);
|
|
208
310
|
}
|
|
209
|
-
const
|
|
311
|
+
const configuredGate = gateForPhase(state.phase);
|
|
312
|
+
const gate = gateForPhase(state.phase, state.enabledGates);
|
|
210
313
|
const role = roleForPhase(state.phase);
|
|
211
|
-
if (!
|
|
314
|
+
if (!role) {
|
|
212
315
|
const artifact = artifactForPhase(state.phase);
|
|
213
316
|
const markdown = renderArtifact(artifact, state);
|
|
214
317
|
state.artifacts[artifact] = await writeMarkdownArtifact(state, artifact, markdown);
|
|
@@ -217,13 +320,23 @@ async function runCurrentPhaseRole(state, runner = runRole) {
|
|
|
217
320
|
return saveState(state);
|
|
218
321
|
}
|
|
219
322
|
const artifact = artifactForPhase(state.phase);
|
|
220
|
-
const path = artifactPath(state.cwd, state.runId, artifact);
|
|
323
|
+
const path = artifactPath(state.cwd, state.runId, artifact, state.artifactDirectory);
|
|
221
324
|
const applyingTesting = state.executionMode === "apply" && state.phase === "testing";
|
|
222
325
|
const roleCwd = applyingTesting ? state.executionWorkspace?.path : state.cwd;
|
|
223
326
|
if (!roleCwd) {
|
|
224
327
|
throw new Error("DevCrew apply testing requires an execution workspace");
|
|
225
328
|
}
|
|
226
|
-
state.roles.push(conductorDecision(state, role, gate));
|
|
329
|
+
state.roles.push(conductorDecision(state, role, gate ?? `${state.phase} automatic transition`));
|
|
330
|
+
if (applyingTesting && state.executionPolicy === "interactive-host") {
|
|
331
|
+
state.executionInstruction = {
|
|
332
|
+
phase: "testing",
|
|
333
|
+
workspacePath: roleCwd,
|
|
334
|
+
instructions: executionInstructions(state, "testing", roleCwd),
|
|
335
|
+
createdAt: now(),
|
|
336
|
+
};
|
|
337
|
+
state.status = "awaiting_execution";
|
|
338
|
+
return saveState(state);
|
|
339
|
+
}
|
|
227
340
|
const result = await runner({
|
|
228
341
|
backend: state.backend,
|
|
229
342
|
role,
|
|
@@ -231,6 +344,7 @@ async function runCurrentPhaseRole(state, runner = runRole) {
|
|
|
231
344
|
request: state.request,
|
|
232
345
|
mode: state.mode,
|
|
233
346
|
executionMode: state.executionMode,
|
|
347
|
+
executionPolicy: state.executionPolicy,
|
|
234
348
|
cwd: roleCwd,
|
|
235
349
|
standards: state.standards.combined,
|
|
236
350
|
artifactPath: path,
|
|
@@ -240,19 +354,60 @@ async function runCurrentPhaseRole(state, runner = runRole) {
|
|
|
240
354
|
});
|
|
241
355
|
if (applyingTesting && state.executionWorkspace) {
|
|
242
356
|
state.verification = await runConfiguredVerification(state, roleCwd);
|
|
357
|
+
state.verificationStatus = verificationStatusFor(state.verification);
|
|
243
358
|
const captured = await captureExecutionChanges(state.executionWorkspace);
|
|
244
359
|
state.changedFiles = captured.changedFiles;
|
|
245
360
|
state.implementationDiff = captured.patch;
|
|
246
361
|
await writeImplementationReview(state);
|
|
247
362
|
}
|
|
363
|
+
if (state.phase === "review") {
|
|
364
|
+
if (!result.reviewDecision) {
|
|
365
|
+
throw new Error("DevCrew architecture review must return a structured review decision");
|
|
366
|
+
}
|
|
367
|
+
state.architectureReview = {
|
|
368
|
+
decision: result.reviewDecision,
|
|
369
|
+
summary: result.summary,
|
|
370
|
+
reviewedAt: now(),
|
|
371
|
+
};
|
|
372
|
+
}
|
|
248
373
|
// When the backend cannot run a real SDK we keep a single deterministic
|
|
249
374
|
// artifact source by rendering the rich phase template from the core layer.
|
|
250
375
|
const baseMarkdown = result.usedFallback ? `${fallbackNotice(result)}${renderArtifact(artifact, state)}` : result.markdown;
|
|
251
376
|
const markdown = appendExecutionSections(artifact, baseMarkdown, state);
|
|
252
377
|
state.roles.push({ ...result, markdown });
|
|
253
378
|
state.artifacts[artifact] = await writeMarkdownArtifact(state, artifact, markdown);
|
|
254
|
-
state.
|
|
255
|
-
state.
|
|
379
|
+
state.executionInstruction = undefined;
|
|
380
|
+
if (state.phase === "requirements" && result.questions && result.questions.length > 0) {
|
|
381
|
+
state.pendingQuestions = result.questions;
|
|
382
|
+
state.gates.requirements = "not_started";
|
|
383
|
+
state.status = "awaiting_input";
|
|
384
|
+
return saveState(state);
|
|
385
|
+
}
|
|
386
|
+
if (state.phase === "review" && state.architectureReview?.decision === "changes_required") {
|
|
387
|
+
state.gates["implementation-review"] = "rejected";
|
|
388
|
+
state.status = "awaiting_input";
|
|
389
|
+
state.feedback.push({
|
|
390
|
+
gate: "implementation-review",
|
|
391
|
+
message: state.architectureReview.summary,
|
|
392
|
+
createdAt: now(),
|
|
393
|
+
});
|
|
394
|
+
return saveState(state);
|
|
395
|
+
}
|
|
396
|
+
if (!gate && configuredGate) {
|
|
397
|
+
state.phase = nextPhaseAfterGate(state, configuredGate);
|
|
398
|
+
state.status = "ready";
|
|
399
|
+
return saveState(state);
|
|
400
|
+
}
|
|
401
|
+
if (applyingTesting) {
|
|
402
|
+
setTestingGateFromVerification(state);
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
if (!gate) {
|
|
406
|
+
throw new Error(`DevCrew has no configured gate for ${state.phase}`);
|
|
407
|
+
}
|
|
408
|
+
state.gates[gate] = "pending";
|
|
409
|
+
state.status = "awaiting_approval";
|
|
410
|
+
}
|
|
256
411
|
return saveState(state);
|
|
257
412
|
}
|
|
258
413
|
export async function startOrchestratedWorkflow(input, runner = runRole) {
|
|
@@ -261,7 +416,10 @@ export async function startOrchestratedWorkflow(input, runner = runRole) {
|
|
|
261
416
|
}
|
|
262
417
|
export async function continueOrchestratedWorkflow(input, runner = runRole) {
|
|
263
418
|
const state = await getWorkflowStatus(input);
|
|
264
|
-
if (state.status === "awaiting_approval" ||
|
|
419
|
+
if (state.status === "awaiting_approval" ||
|
|
420
|
+
state.status === "awaiting_input" ||
|
|
421
|
+
state.status === "awaiting_execution" ||
|
|
422
|
+
state.status === "complete") {
|
|
265
423
|
return state;
|
|
266
424
|
}
|
|
267
425
|
return runCurrentPhaseRole(state, runner);
|
|
@@ -296,6 +454,9 @@ export async function approveOrchestratedWorkflow(input) {
|
|
|
296
454
|
return cleanupAfterApproval(before);
|
|
297
455
|
}
|
|
298
456
|
if (promotingTesting) {
|
|
457
|
+
if (before.verificationStatus === "failed" && !before.verificationWaiver) {
|
|
458
|
+
throw new Error("Failed verification cannot be promoted without an explicit verification waiver");
|
|
459
|
+
}
|
|
299
460
|
await promoteExecutionChanges(before);
|
|
300
461
|
let approved;
|
|
301
462
|
try {
|
|
@@ -315,6 +476,61 @@ export async function approveOrchestratedWorkflow(input) {
|
|
|
315
476
|
}
|
|
316
477
|
return approveWorkflow(input);
|
|
317
478
|
}
|
|
479
|
+
export async function waiveOrchestratedVerification(input) {
|
|
480
|
+
const state = await waiveVerificationWorkflow(input);
|
|
481
|
+
const reportPath = state.artifacts["test-report"];
|
|
482
|
+
if (reportPath) {
|
|
483
|
+
const report = await readFile(reportPath, "utf8");
|
|
484
|
+
await writeFile(reportPath, appendExecutionSections("test-report", report, state), "utf8");
|
|
485
|
+
}
|
|
486
|
+
return state;
|
|
487
|
+
}
|
|
488
|
+
export async function completeOrchestratedExecution(input) {
|
|
489
|
+
const state = await getWorkflowStatus(input);
|
|
490
|
+
const instruction = state.executionInstruction;
|
|
491
|
+
if (state.executionMode !== "apply" ||
|
|
492
|
+
state.executionPolicy !== "interactive-host" ||
|
|
493
|
+
state.status !== "awaiting_execution" ||
|
|
494
|
+
!instruction ||
|
|
495
|
+
instruction.phase !== state.phase ||
|
|
496
|
+
!state.executionWorkspace ||
|
|
497
|
+
instruction.workspacePath !== state.executionWorkspace.path) {
|
|
498
|
+
throw new Error("DevCrew is not awaiting an interactive-host execution completion");
|
|
499
|
+
}
|
|
500
|
+
const summary = parseCompletionSummary(input.summary);
|
|
501
|
+
if (state.phase === "execution") {
|
|
502
|
+
if (input.verification !== undefined) {
|
|
503
|
+
throw new Error("verification can only be submitted after interactive-host testing");
|
|
504
|
+
}
|
|
505
|
+
const captured = await captureExecutionChanges(state.executionWorkspace);
|
|
506
|
+
state.changedFiles = captured.changedFiles;
|
|
507
|
+
state.implementationDiff = captured.patch;
|
|
508
|
+
state.lintResults = [];
|
|
509
|
+
state.roles.push(hostCompletionResult(state, summary));
|
|
510
|
+
state.executionInstruction = undefined;
|
|
511
|
+
await writeImplementationReview(state);
|
|
512
|
+
state.phase = "review";
|
|
513
|
+
state.status = "ready";
|
|
514
|
+
return saveState(state);
|
|
515
|
+
}
|
|
516
|
+
if (state.phase !== "testing") {
|
|
517
|
+
throw new Error(`Cannot complete interactive-host work during ${state.phase}`);
|
|
518
|
+
}
|
|
519
|
+
state.verification = parseCompletionVerification(input.verification);
|
|
520
|
+
state.verificationStatus = verificationStatusFor(state.verification);
|
|
521
|
+
const captured = await captureExecutionChanges(state.executionWorkspace);
|
|
522
|
+
state.changedFiles = captured.changedFiles;
|
|
523
|
+
state.implementationDiff = captured.patch;
|
|
524
|
+
await writeImplementationReview(state);
|
|
525
|
+
const result = hostCompletionResult(state, summary);
|
|
526
|
+
const artifact = artifactForPhase(state.phase);
|
|
527
|
+
const markdown = appendExecutionSections(artifact, result.markdown, state);
|
|
528
|
+
state.roles.push({ ...result, markdown });
|
|
529
|
+
state.artifacts[artifact] = await writeMarkdownArtifact(state, artifact, markdown);
|
|
530
|
+
state.executionInstruction = undefined;
|
|
531
|
+
setTestingGateFromVerification(state);
|
|
532
|
+
return saveState(state);
|
|
533
|
+
}
|
|
318
534
|
export async function rejectOrchestratedWorkflow(input) {
|
|
319
535
|
return rejectWorkflow(input);
|
|
320
536
|
}
|
|
@@ -329,9 +545,8 @@ export async function answerOrchestratedWorkflow(input, runner = runRole) {
|
|
|
329
545
|
state.gates.testing = "not_started";
|
|
330
546
|
return saveState(state);
|
|
331
547
|
}
|
|
332
|
-
const gate = gateForPhase(state.phase);
|
|
333
548
|
const role = roleForPhase(state.phase);
|
|
334
|
-
if (!
|
|
549
|
+
if (!role) {
|
|
335
550
|
return state;
|
|
336
551
|
}
|
|
337
552
|
// Re-run the current phase role so the artifact reflects the new answer and
|
|
@@ -2,7 +2,7 @@ import { constants } from "node:fs";
|
|
|
2
2
|
import { access, copyFile, mkdir, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { DEFAULT_CONFIG, DEVCREW_NPM_PACKAGE, DEVCREW_VERSION
|
|
5
|
+
import { DEFAULT_CONFIG, DEVCREW_NPM_PACKAGE, DEVCREW_VERSION } from "../../core/src/index.js";
|
|
6
6
|
async function writeJson(path, value) {
|
|
7
7
|
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
8
8
|
}
|
|
@@ -29,36 +29,8 @@ async function writeCodexAssets(pluginRoot) {
|
|
|
29
29
|
await copyFile(await bundledAssetPath("logo.png"), join(assetDir, "logo.png"));
|
|
30
30
|
await copyFile(await bundledAssetPath("composer-icon.png"), join(assetDir, "composer-icon.png"));
|
|
31
31
|
}
|
|
32
|
-
function roleExpectations(name) {
|
|
33
|
-
const sections = ROLE_SECTIONS[name];
|
|
34
|
-
if (!sections || sections.length === 0) {
|
|
35
|
-
return "";
|
|
36
|
-
}
|
|
37
|
-
return sections.map((s) => `- ${s.heading} (${s.description})`).join("\n");
|
|
38
|
-
}
|
|
39
|
-
async function writeRoleAgents(root, format) {
|
|
40
|
-
const roles = [
|
|
41
|
-
["pm", "Product manager. Clarifies requirements, scope boundaries, success criteria, and requester approvals."],
|
|
42
|
-
["architect", "technical architecture specialist. Designs implementation, deployment, interfaces, and review criteria."],
|
|
43
|
-
["implementer", "Implementation engineer. Writes code according to approved architecture and discovered standards."],
|
|
44
|
-
["tester", "Testing and acceptance specialist. Verifies functionality, regressions, and acceptance evidence."],
|
|
45
|
-
];
|
|
46
|
-
if (format === "claude") {
|
|
47
|
-
const agentDir = join(root, "agents");
|
|
48
|
-
await mkdir(agentDir, { recursive: true });
|
|
49
|
-
for (const [name, description] of roles) {
|
|
50
|
-
await writeFile(join(agentDir, `${name}.md`), `---\nname: ${name}\ndescription: ${description}\ntools: Read, Grep, Glob, Bash\n---\n\nYou are the DevCrew ${name} role. ${description} Return concise Markdown and keep inherited host permissions.\n\nProduce these required sections:\n\n${roleExpectations(name)}\n`, "utf8");
|
|
51
|
-
}
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
const agentDir = join(root, "agents");
|
|
55
|
-
await mkdir(agentDir, { recursive: true });
|
|
56
|
-
for (const [name, description] of roles) {
|
|
57
|
-
await writeFile(join(agentDir, `${name}.toml`), `name = "${name}"\ndescription = "${description}"\ndeveloper_instructions = """\nYou are the DevCrew ${name} role. ${description}\nReturn concise Markdown and keep inherited host permissions.\n\nProduce these required sections:\n${roleExpectations(name)}\n"""\n`, "utf8");
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
32
|
function entrySkill() {
|
|
61
|
-
return `---\nname: devcrew\ndescription: Run the DevCrew PM -> architecture -> implementation -> testing workflow. Use when the user asks for structured feature or product development, requirements clarification, architecture review, implementation planning, testing acceptance, or Chinese requests such as 完整研发流程, 需求澄清, 产品经理, 架构师, 开发测试流程.\n---\n\nUse the DevCrew MCP tools to manage the workflow:\n\n1. Start with \`devcrew_start\` using the current repository cwd, mode, request, and optional executionMode
|
|
33
|
+
return `---\nname: devcrew\ndescription: Run the DevCrew PM -> architecture -> implementation -> testing workflow. Use when the user asks for structured feature or product development, requirements clarification, architecture review, implementation planning, testing acceptance, or Chinese requests such as 完整研发流程, 需求澄清, 产品经理, 架构师, 开发测试流程.\n---\n\nUse the DevCrew MCP tools to manage the workflow:\n\n1. Start with \`devcrew_start\` using the current repository cwd, mode, request, and optional \`executionMode\`. Host is inferred from the plugin's \`DEVCREW_HOST\`; pass host only for an explicit override. Omit \`executionMode\` unless the requester explicitly asks DevCrew to apply changes; the default safe mode is \`plan\`.\n2. After start, DevCrew records the active run for this repository. For follow-up tools, omit \`runId\` unless you need to target a different run explicitly.\n3. For \`executionMode: "apply"\`, choose an explicit \`executionPolicy\`. The default \`interactive-host\` pauses at implementation and testing for the host's native agent to work in DevCrew's isolated worktree. \`headless-restricted\` and \`headless-unattended\` are DevCrew SDK policies; they do not inherit the current host approval session.\n4. Use \`devcrew_status\` to show the current phase, pending gate, and any execution instruction.\n5. Use \`devcrew_answer\` when the requester gives clarification.\n6. Use \`devcrew_approve\` or \`devcrew_reject\` for each gate.\n7. Use \`devcrew_continue\` after approvals. Apply runs enter an \`implementation-review\` gate after execution: review the architect's \`architecture-review\` artifact before testing. For \`interactive-host\`, if status becomes \`awaiting_execution\`, perform the native-host work in the indicated worktree then call \`devcrew_complete_execution\`. For testing, include command, exit code, output, startedAt, and completedAt evidence.\n8. Failed verification is not approvable. Revise through \`devcrew_answer\`, or use \`devcrew_waive_verification\` only when the requester explicitly accepts the recorded risk and provides a reason.\n9. Use \`devcrew_artifact\` to read generated requirements, architecture, implementation-plan, implementation-review, architecture-review, test-report, or acceptance files.\n\nNever describe a nested SDK session as inheriting the current host's approval decisions.\n`;
|
|
62
34
|
}
|
|
63
35
|
function npmPackageSpecifier() {
|
|
64
36
|
return `${DEVCREW_NPM_PACKAGE}@${DEVCREW_VERSION}`;
|
|
@@ -136,7 +108,6 @@ export async function generateCodexPlugin(root) {
|
|
|
136
108
|
devcrew: mcpServerConfig("codex"),
|
|
137
109
|
},
|
|
138
110
|
});
|
|
139
|
-
await writeRoleAgents(pluginRoot, "codex");
|
|
140
111
|
return { name: "devcrew", path: pluginRoot };
|
|
141
112
|
}
|
|
142
113
|
export async function generateCodexMarketplace(root) {
|
|
@@ -180,7 +151,6 @@ export async function generateClaudePlugin(root) {
|
|
|
180
151
|
devcrew: mcpServerConfig("claude"),
|
|
181
152
|
},
|
|
182
153
|
});
|
|
183
|
-
await writeRoleAgents(pluginRoot, "claude");
|
|
184
154
|
return { name: "devcrew", path: pluginRoot };
|
|
185
155
|
}
|
|
186
156
|
export async function initProject(root) {
|