@shenlee/devcrew 0.1.1 → 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 +30 -8
- package/README.zh-CN.md +27 -8
- package/dist/packages/adapters/src/index.js +62 -8
- 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 +10 -6
- package/dist/packages/core/src/store.js +36 -0
- package/dist/packages/core/src/types.js +5 -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 +120 -31
- package/dist/packages/orchestrator/src/index.js +339 -160
- package/dist/packages/orchestrator/src/worktree.js +198 -0
- package/dist/packages/plugins/src/index.js +2 -32
- package/dist/packages/service/src/stdio.js +19 -3
- package/dist/packages/service/src/tools.js +45 -6
- package/docs/claude-code.md +5 -3
- package/docs/codex.md +12 -4
- package/docs/quickstart.md +1 -1
- package/docs/superpowers/plans/2026-07-10-p0-foundation-repair.md +939 -0
- package/docs/superpowers/plans/2026-07-13-safety-semantics.md +313 -0
- package/docs/superpowers/specs/2026-07-10-p0-foundation-repair-design.md +182 -0
- package/docs/superpowers/specs/2026-07-13-execution-boundaries-design.md +126 -0
- package/docs/workflow.md +36 -4
- package/package.json +1 -1
- package/packages/adapters/src/index.ts +87 -11
- package/packages/core/src/artifacts.ts +16 -4
- package/packages/core/src/config.ts +1 -1
- package/packages/core/src/paths.ts +11 -6
- package/packages/core/src/store.ts +41 -1
- package/packages/core/src/types.ts +53 -2
- package/packages/core/src/validation.ts +10 -0
- package/packages/core/src/version.ts +1 -1
- package/packages/core/src/workflow.ts +136 -30
- package/packages/orchestrator/src/index.ts +377 -182
- package/packages/orchestrator/src/worktree.ts +269 -0
- package/packages/plugins/src/index.ts +2 -44
- package/packages/service/src/stdio.ts +27 -6
- package/packages/service/src/tools.ts +46 -5
- package/plugins/devcrew-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/devcrew-codex/.mcp.json +1 -1
- package/plugins/devcrew-codex/assets/logo.png +0 -0
- 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 -6
- package/plugins/devcrew-codex/agents/implementer.toml +0 -6
- package/plugins/devcrew-codex/agents/pm.toml +0 -6
- package/plugins/devcrew-codex/agents/tester.toml +0 -6
package/docs/workflow.md
CHANGED
|
@@ -16,14 +16,38 @@ approval.
|
|
|
16
16
|
|
|
17
17
|
1. `requirements`: product manager clarifies scope, users, success criteria, and non-goals.
|
|
18
18
|
2. `architecture`: architect defines technical approach, interfaces, deployment notes, and review criteria.
|
|
19
|
-
3. `implementation`: implementer creates
|
|
20
|
-
4. `
|
|
21
|
-
5. `
|
|
19
|
+
3. `implementation`: implementer creates a read-only implementation plan and coding checklist. The implementation gate approves this plan, not repository changes.
|
|
20
|
+
4. `execution`: internal, nongated apply phase. `interactive-host` waits for the host-native agent to work in `.devcrew/worktrees/<run-id>` and report completion; explicit headless policies use DevCrew's SDK settings. DevCrew then captures changed files, a binary-capable patch, lint evidence when run headlessly, and `implementation-review.md`.
|
|
21
|
+
5. `testing`: records validation and acceptance evidence in the same isolated worktree. Interactive-host submits command evidence through `devcrew_complete_execution`; headless policies run DevCrew-configured verification. DevCrew refreshes the implementation diff and review before opening the testing gate.
|
|
22
|
+
6. `acceptance`: generated after the testing gate is approved.
|
|
22
23
|
|
|
23
24
|
`devcrew_start` runs the PM role for `requirements`. After the requester approves
|
|
24
25
|
a gate, `devcrew_continue` runs the role for the next phase before setting that
|
|
25
26
|
phase's gate to `pending`.
|
|
26
27
|
|
|
28
|
+
Plan mode advances directly from implementation-plan approval to testing. Apply mode uses this sequence:
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
requirements approval
|
|
32
|
+
-> architecture approval
|
|
33
|
+
-> implementation plan approval
|
|
34
|
+
-> isolated execution
|
|
35
|
+
-> implementation review approval
|
|
36
|
+
-> isolated testing
|
|
37
|
+
-> testing approval
|
|
38
|
+
-> patch promotion to requester repository
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The implementation-plan approval advances the run to `execution`. With the default `interactive-host` policy, each `devcrew_continue` pauses at `awaiting_execution`; the host-native agent works in the reported worktree and calls `devcrew_complete_execution`. Explicit headless policies run the corresponding SDK role and commands directly under their recorded DevCrew policy.
|
|
42
|
+
|
|
43
|
+
Apply requires Git and a clean requester worktree both when execution starts and when the reviewed patch is promoted. The requester repository remains unchanged until testing approval. A failed verification enters `awaiting_input`, rather than a promotable testing gate; it can be revised with `devcrew_answer` or deliberately reopened only through `devcrew_waive_verification` with a non-empty risk reason. If testing is rejected, `devcrew_answer` records the response and returns the run to `execution/ready` with the isolated worktree intact.
|
|
44
|
+
|
|
45
|
+
The execution review is also structured: the architect must return either
|
|
46
|
+
`Decision: approved` or `Decision: changes_required` in its
|
|
47
|
+
`architecture-review` artifact. A `changes_required` decision rejects the
|
|
48
|
+
implementation-review gate and keeps the run at `awaiting_input`; it cannot
|
|
49
|
+
enter testing until the feedback is addressed and a later review approves it.
|
|
50
|
+
|
|
27
51
|
`devcrew_start` records the created run as the active run for the repository.
|
|
28
52
|
Subsequent MCP calls can omit `runId`; DevCrew resolves it from
|
|
29
53
|
`.devcrew/active-run.json`. Plugin MCP configs set `DEVCREW_HOST`, so `host`
|
|
@@ -36,9 +60,16 @@ Each main phase has a gate:
|
|
|
36
60
|
- `requirements`
|
|
37
61
|
- `architecture`
|
|
38
62
|
- `implementation`
|
|
63
|
+
- `implementation-review`
|
|
39
64
|
- `testing`
|
|
40
65
|
|
|
41
|
-
The requester approves or rejects each gate. Rejection records feedback and returns the workflow to `awaiting_input`.
|
|
66
|
+
The requester approves or rejects each gate. Rejection records feedback and returns the workflow to `awaiting_input`. In apply mode, rejecting testing never rolls changes back in the requester repository because no patch has been promoted yet.
|
|
67
|
+
|
|
68
|
+
`workflow.gates` may omit `requirements`, `architecture`, or `implementation`
|
|
69
|
+
to make that phase advance automatically after its role has produced an
|
|
70
|
+
artifact. It never skips the role, artifact, execution, architecture review, or
|
|
71
|
+
verification. `implementation-review` and `testing` are mandatory and DevCrew
|
|
72
|
+
adds them back if a configuration omits them.
|
|
42
73
|
|
|
43
74
|
## State And Artifacts
|
|
44
75
|
|
|
@@ -66,5 +97,6 @@ The standard artifact set is:
|
|
|
66
97
|
- `architecture.md`
|
|
67
98
|
- `implementation-plan.md`
|
|
68
99
|
- `implementation-review.md`
|
|
100
|
+
- `architecture-review.md`
|
|
69
101
|
- `test-report.md`
|
|
70
102
|
- `acceptance.md`
|
package/package.json
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { DEVCREW_NPM_PACKAGE, ROLE_SECTIONS } from "../../core/src/index.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
ArtifactName,
|
|
4
|
+
BackendName,
|
|
5
|
+
ExecutionMode,
|
|
6
|
+
ExecutionPolicy,
|
|
7
|
+
Host,
|
|
8
|
+
Phase,
|
|
9
|
+
RoleResult,
|
|
10
|
+
RunState,
|
|
11
|
+
WorkflowMode,
|
|
12
|
+
} from "../../core/src/index.js";
|
|
3
13
|
|
|
4
14
|
export interface BackendResolutionInput {
|
|
5
15
|
host: Host;
|
|
@@ -13,6 +23,7 @@ export interface RoleRunInput {
|
|
|
13
23
|
request: string;
|
|
14
24
|
mode: WorkflowMode;
|
|
15
25
|
executionMode?: ExecutionMode;
|
|
26
|
+
executionPolicy?: ExecutionPolicy;
|
|
16
27
|
cwd: string;
|
|
17
28
|
standards: string;
|
|
18
29
|
artifactPath: string;
|
|
@@ -69,6 +80,27 @@ export function assertRoleSections(role: RoleResult["role"], markdown: string):
|
|
|
69
80
|
}
|
|
70
81
|
}
|
|
71
82
|
|
|
83
|
+
export function extractOpenQuestions(markdown: string): string[] {
|
|
84
|
+
const match = /^##\s+Open Questions\s*$(.*?)(?=^##\s|(?![\s\S]))/ims.exec(markdown);
|
|
85
|
+
if (!match) {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
const questions: string[] = [];
|
|
89
|
+
for (const line of match[1].split("\n")) {
|
|
90
|
+
const question = /^\s*[-*]\s+(.+?)\s*$/u.exec(line)?.[1]?.trim();
|
|
91
|
+
if (question && !/^(none|n\/a|no open questions)\.?$/i.test(question)) {
|
|
92
|
+
questions.push(question);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return questions;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function extractArchitectureReviewDecision(markdown: string): "approved" | "changes_required" | undefined {
|
|
99
|
+
const section = /^##\s+Review Decision\s*$(.*?)(?=^##\s|(?![\s\S]))/ims.exec(markdown)?.[1] ?? "";
|
|
100
|
+
const match = /^Decision:\s*(approved|changes_required)\s*$/im.exec(section);
|
|
101
|
+
return match?.[1] as "approved" | "changes_required" | undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
72
104
|
export function renderRolePrompt(input: Omit<RoleRunInput, "backend" | "cwd">): string {
|
|
73
105
|
const executionMode = input.executionMode ?? "plan";
|
|
74
106
|
const answers = input.answers ?? [];
|
|
@@ -102,7 +134,7 @@ export function renderRolePrompt(input: Omit<RoleRunInput, "backend" | "cwd">):
|
|
|
102
134
|
}
|
|
103
135
|
}
|
|
104
136
|
|
|
105
|
-
const canApply =
|
|
137
|
+
const canApply = roleCanApply({ ...input, executionMode });
|
|
106
138
|
const permissionInstruction = canApply
|
|
107
139
|
? input.role === "tester"
|
|
108
140
|
? "You may run validation commands needed for the approved scope and report exact evidence."
|
|
@@ -113,13 +145,22 @@ export function renderRolePrompt(input: Omit<RoleRunInput, "backend" | "cwd">):
|
|
|
113
145
|
"",
|
|
114
146
|
"Instructions:",
|
|
115
147
|
`Act as the DevCrew ${input.role} role and produce a complete, well-structured Markdown document for the ${input.phase} phase.`,
|
|
116
|
-
"Keep scope aligned with the approved gates and
|
|
148
|
+
"Keep scope aligned with the approved gates and the selected DevCrew execution policy.",
|
|
117
149
|
permissionInstruction,
|
|
118
150
|
"",
|
|
119
151
|
"Required Sections:",
|
|
120
152
|
...roleGuidance(input.role),
|
|
121
153
|
);
|
|
122
154
|
|
|
155
|
+
if (input.phase === "review") {
|
|
156
|
+
lines.push(
|
|
157
|
+
"",
|
|
158
|
+
"Architecture Review Decision:",
|
|
159
|
+
"Add an exact H2 `## Review Decision` section containing exactly `Decision: approved` or `Decision: changes_required`.",
|
|
160
|
+
"Use `changes_required` for any mismatch with the approved architecture; summarize the blocking mismatch in that section.",
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
123
164
|
return lines.join("\n");
|
|
124
165
|
}
|
|
125
166
|
|
|
@@ -128,6 +169,8 @@ function titleForPhase(phase: Phase): string {
|
|
|
128
169
|
requirements: "Requirements",
|
|
129
170
|
architecture: "Architecture",
|
|
130
171
|
implementation: "Implementation Plan",
|
|
172
|
+
execution: "Implementation Review",
|
|
173
|
+
review: "Architecture Review",
|
|
131
174
|
testing: "Test Report",
|
|
132
175
|
acceptance: "Acceptance",
|
|
133
176
|
complete: "Acceptance",
|
|
@@ -169,13 +212,14 @@ interface CodexClient {
|
|
|
169
212
|
|
|
170
213
|
type CodexConstructor = new () => CodexClient;
|
|
171
214
|
|
|
172
|
-
export type ClaudePermissionMode = "default" | "acceptEdits" | "bypassPermissions" | "plan";
|
|
215
|
+
export type ClaudePermissionMode = "default" | "acceptEdits" | "bypassPermissions" | "dontAsk" | "plan";
|
|
173
216
|
|
|
174
217
|
export interface ClaudeQueryOptions {
|
|
175
218
|
cwd?: string;
|
|
176
219
|
permissionMode?: ClaudePermissionMode;
|
|
177
220
|
allowedTools?: string[];
|
|
178
221
|
disallowedTools?: string[];
|
|
222
|
+
allowDangerouslySkipPermissions?: boolean;
|
|
179
223
|
}
|
|
180
224
|
|
|
181
225
|
export type ClaudeResultSubtype = "success" | "error_max_turns" | "error_during_execution";
|
|
@@ -293,28 +337,51 @@ export function extractClaudeResult(message: ClaudeResultMessage | undefined): s
|
|
|
293
337
|
return text;
|
|
294
338
|
}
|
|
295
339
|
|
|
296
|
-
function
|
|
297
|
-
return input.executionMode === "apply" && (input.
|
|
340
|
+
function isApplyPhase(input: Pick<RoleRunInput, "executionMode" | "phase">): boolean {
|
|
341
|
+
return input.executionMode === "apply" && (input.phase === "execution" || input.phase === "testing");
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function effectiveExecutionPolicy(input: Pick<RoleRunInput, "executionPolicy">): ExecutionPolicy {
|
|
345
|
+
return input.executionPolicy ?? "interactive-host";
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function roleCanApply(input: Pick<RoleRunInput, "executionMode" | "executionPolicy" | "phase">): boolean {
|
|
349
|
+
return isApplyPhase(input) && effectiveExecutionPolicy(input) !== "interactive-host";
|
|
298
350
|
}
|
|
299
351
|
|
|
300
|
-
function
|
|
301
|
-
|
|
352
|
+
function codexOptionsForRole(input: RoleRunInput): CodexThreadOptions {
|
|
353
|
+
if (!roleCanApply(input)) {
|
|
354
|
+
return buildCodexThreadOptions(input.cwd);
|
|
355
|
+
}
|
|
356
|
+
const executionPolicy = effectiveExecutionPolicy(input);
|
|
357
|
+
return {
|
|
358
|
+
...buildCodexThreadOptions(input.cwd, "workspace-write"),
|
|
359
|
+
approvalPolicy: executionPolicy === "headless-unattended" ? "never" : "on-request",
|
|
360
|
+
networkAccessEnabled: false,
|
|
361
|
+
};
|
|
302
362
|
}
|
|
303
363
|
|
|
304
364
|
function claudeOptionsForRole(input: RoleRunInput): ClaudeQueryOptions {
|
|
305
365
|
if (!roleCanApply(input)) {
|
|
306
366
|
return buildClaudeOptions(input.cwd);
|
|
307
367
|
}
|
|
368
|
+
if (effectiveExecutionPolicy(input) === "headless-unattended") {
|
|
369
|
+
return {
|
|
370
|
+
cwd: input.cwd,
|
|
371
|
+
permissionMode: "bypassPermissions",
|
|
372
|
+
allowDangerouslySkipPermissions: true,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
308
375
|
const allowedTools =
|
|
309
|
-
input.role === "implementer" ? ["Read", "Grep", "Glob", "Edit", "Write"
|
|
310
|
-
return buildClaudeOptions(input.cwd, "
|
|
376
|
+
input.role === "implementer" ? ["Read", "Grep", "Glob", "Edit", "Write"] : ["Read", "Grep", "Glob"];
|
|
377
|
+
return buildClaudeOptions(input.cwd, "dontAsk", allowedTools);
|
|
311
378
|
}
|
|
312
379
|
|
|
313
380
|
async function runWithCodex(input: RoleRunInput, prompt: string, loadModule: ModuleLoader): Promise<string> {
|
|
314
381
|
const mod = await loadModule(HOST_SDK_PACKAGES.codex);
|
|
315
382
|
const CodexClass = mod.Codex as CodexConstructor;
|
|
316
383
|
const codex = new CodexClass();
|
|
317
|
-
const thread = codex.startThread(
|
|
384
|
+
const thread = codex.startThread(codexOptionsForRole(input));
|
|
318
385
|
const turn = await thread.run(prompt);
|
|
319
386
|
return extractCodexText(turn);
|
|
320
387
|
}
|
|
@@ -355,6 +422,9 @@ export interface RunRoleDeps {
|
|
|
355
422
|
}
|
|
356
423
|
|
|
357
424
|
export async function runRole(input: RoleRunInput, deps: RunRoleDeps = {}): Promise<RoleResult> {
|
|
425
|
+
if (isApplyPhase(input) && effectiveExecutionPolicy(input) === "interactive-host") {
|
|
426
|
+
throw new Error("DevCrew interactive-host execution must be performed by the host, not a nested SDK");
|
|
427
|
+
}
|
|
358
428
|
const loadModule = deps.loadModule ?? importOptional;
|
|
359
429
|
const title = titleForPhase(input.phase);
|
|
360
430
|
const prompt = renderRolePrompt(input);
|
|
@@ -374,12 +444,18 @@ export async function runRole(input: RoleRunInput, deps: RunRoleDeps = {}): Prom
|
|
|
374
444
|
? await runWithCodex(input, prompt, loadModule)
|
|
375
445
|
: await runWithClaude(input, prompt, loadModule);
|
|
376
446
|
assertRoleSections(input.role, markdown);
|
|
447
|
+
const reviewDecision = input.phase === "review" ? extractArchitectureReviewDecision(markdown) : undefined;
|
|
448
|
+
if (input.phase === "review" && !reviewDecision) {
|
|
449
|
+
throw new RoleOutputValidationError(input.role, ["Review Decision"]);
|
|
450
|
+
}
|
|
377
451
|
return {
|
|
378
452
|
role: input.role,
|
|
379
453
|
backend: input.backend,
|
|
380
454
|
summary: `${input.role} produced ${title} using the ${input.backend} SDK.`,
|
|
381
455
|
markdown,
|
|
382
456
|
usedFallback: false,
|
|
457
|
+
questions: input.role === "pm" ? extractOpenQuestions(markdown) : undefined,
|
|
458
|
+
reviewDecision,
|
|
383
459
|
};
|
|
384
460
|
} catch (error) {
|
|
385
461
|
const reason = errorMessage(error);
|
|
@@ -10,6 +10,7 @@ function headingForArtifact(name: ArtifactName): string {
|
|
|
10
10
|
architecture: "Architecture",
|
|
11
11
|
"implementation-plan": "Implementation Plan",
|
|
12
12
|
"implementation-review": "Implementation Review",
|
|
13
|
+
"architecture-review": "Architecture Review",
|
|
13
14
|
"test-report": "Test Report",
|
|
14
15
|
acceptance: "Acceptance",
|
|
15
16
|
}[name];
|
|
@@ -60,6 +61,13 @@ function verificationBlock(state: RunState): string {
|
|
|
60
61
|
.join("\n\n");
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
function verificationWaiverBlock(state: RunState): string {
|
|
65
|
+
if (!state.verificationWaiver) {
|
|
66
|
+
return "No verification waiver has been recorded.";
|
|
67
|
+
}
|
|
68
|
+
return `Reason: ${state.verificationWaiver.reason}\nRecorded At: ${state.verificationWaiver.createdAt}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
63
71
|
function lintResultsBlock(state: RunState): string {
|
|
64
72
|
if (state.lintResults.length === 0) {
|
|
65
73
|
return "No lint or format commands have been executed.";
|
|
@@ -99,7 +107,7 @@ function architectureComplianceStatus(state: RunState): string {
|
|
|
99
107
|
|
|
100
108
|
export function renderArtifact(name: ArtifactName, state: RunState): string {
|
|
101
109
|
const title = headingForArtifact(name);
|
|
102
|
-
const common = `# ${title}\n\nRun: ${state.runId}\nMode: ${state.mode}\nExecution Mode: ${state.executionMode}\nHost: ${state.host}\nBackend: ${state.backend}\nRequest: ${state.request}\n\n`;
|
|
110
|
+
const common = `# ${title}\n\nRun: ${state.runId}\nMode: ${state.mode}\nExecution Mode: ${state.executionMode}\nExecution Policy: ${state.executionPolicy}\nVerification Status: ${state.verificationStatus}\nHost: ${state.host}\nBackend: ${state.backend}\nRequest: ${state.request}\n\n`;
|
|
103
111
|
|
|
104
112
|
if (name === "requirements") {
|
|
105
113
|
return `${common}## Functional Scope\n\n### In Scope\n\n- Capture the requested outcome in user-facing terms.\n\n### Out of Scope\n\n- Make explicit what is intentionally excluded before implementation.\n\n## Users and Scenarios\n\n- Identify the primary users and their key scenarios.\n\n## Acceptance Criteria\n\n- Express each criterion as Given / When / Then so it stays testable.\n\n## Priorities\n\n- Classify each requirement as Must / Should / Could / Won't (MoSCoW).\n\n## Current Requester Answers\n\n${answerBlock(state)}\n\n## Discovered Standards\n\n${standardsExcerpt(state)}\n\n## Open Questions\n\n- Confirm primary users and success criteria.\n- Confirm scope boundaries and non-goals.\n- Confirm deployment or environment constraints.\n\n## Rejection Feedback\n\n${feedbackBlock(state)}\n`;
|
|
@@ -117,17 +125,21 @@ export function renderArtifact(name: ArtifactName, state: RunState): string {
|
|
|
117
125
|
return `${common}${workflowContextBlock(state)}\n## Implementation Diff Review\n\n### Changed Files\n\n${changedFilesBlock(state)}\n\n### Captured Diff\n\n${implementationDiffBlock(state)}\n\n## Lint Results\n\n${lintResultsBlock(state)}\n\n## Architecture Compliance Inputs\n\n${architectureComplianceInputsBlock(state)}\n\n## Architecture Compliance Review\n\nStatus: ${architectureComplianceStatus(state)}\n\n- Confirm changed files map back to the approved architecture artifact.\n- Confirm public interfaces, data flow, and deployment assumptions remain consistent with the architecture.\n- Confirm implementation scope does not include unapproved requirements or unrelated refactors.\n- Record any mismatch as rejection feedback before approving the implementation gate.\n`;
|
|
118
126
|
}
|
|
119
127
|
|
|
128
|
+
if (name === "architecture-review") {
|
|
129
|
+
return `${common}${workflowContextBlock(state)}\n## Technical Decisions\n\nReview the captured implementation diff against the approved architecture. State whether the implementation keeps its approved interfaces, data flow, deployment, and rollback assumptions.\n\n## Interface Contracts\n\nIdentify each changed public interface and whether it conforms to the approved architecture artifact.\n\n## Data Flow and Deployment\n\nReview the implementation diff for data-flow, deployment, migration, and rollback deviations.\n\n## Architecture Review Checklist\n\n- Review the captured implementation diff and changed-files list.\n- Trace each material change to an approved architectural decision.\n- Record any mismatch as rejection feedback before the testing phase begins.\n`;
|
|
130
|
+
}
|
|
131
|
+
|
|
120
132
|
if (name === "test-report") {
|
|
121
|
-
return `${common}${workflowContextBlock(state)}\n## Test Cases\n\nEnumerate cases as a table covering happy, edge, failure, and regression paths.\n\n| ID | Scenario | Type | Expected |\n| --- | --- | --- | --- |\n| TC-1 | Primary success path | happy | Behaves as specified |\n| TC-2 | Boundary input | edge | Handled without error |\n| TC-3 | Invalid input | failure | Fails safely with clear error |\n\n## Coverage\n\nRun the coverage command and report the coverage summary plus any gaps. Evidence is captured under Acceptance Evidence.\n\n## Acceptance Evidence\n\n${verificationBlock(state)}\n\n## Known Risks\n\n-
|
|
133
|
+
return `${common}${workflowContextBlock(state)}\n## Test Cases\n\nEnumerate cases as a table covering happy, edge, failure, and regression paths.\n\n| ID | Scenario | Type | Expected |\n| --- | --- | --- | --- |\n| TC-1 | Primary success path | happy | Behaves as specified |\n| TC-2 | Boundary input | edge | Handled without error |\n| TC-3 | Invalid input | failure | Fails safely with clear error |\n\n## Coverage\n\nRun the coverage command and report the coverage summary plus any gaps. Evidence is captured under Acceptance Evidence.\n\n## Acceptance Evidence\n\n${verificationBlock(state)}\n\n## Verification Waiver\n\n${verificationWaiverBlock(state)}\n\n## Known Risks\n\n- Headless SDK capabilities are governed by the recorded DevCrew execution policy.\n- Interactive-host work is performed through the host's native agent controls.\n`;
|
|
122
134
|
}
|
|
123
135
|
|
|
124
|
-
return `${common}${workflowContextBlock(state)}\n## Acceptance Summary\n\nThe requester approved requirements, architecture, implementation planning, and test reporting gates for this run.\n\n## Approved Gates\n\n${Object.entries(state.gates)
|
|
136
|
+
return `${common}${workflowContextBlock(state)}\n## Acceptance Summary\n\nThe requester approved requirements, architecture, implementation planning, and test reporting gates for this run.\n\n## Verification Waiver\n\n${verificationWaiverBlock(state)}\n\n## Approved Gates\n\n${Object.entries(state.gates)
|
|
125
137
|
.map(([gate, status]) => `- ${gate}: ${status}`)
|
|
126
138
|
.join("\n")}\n`;
|
|
127
139
|
}
|
|
128
140
|
|
|
129
141
|
export async function writeArtifact(name: ArtifactName, state: RunState): Promise<string> {
|
|
130
|
-
const path = artifactPath(state.cwd, state.runId, name);
|
|
142
|
+
const path = artifactPath(state.cwd, state.runId, name, state.artifactDirectory);
|
|
131
143
|
await mkdir(dirname(path), { recursive: true });
|
|
132
144
|
await writeFile(path, renderArtifact(name, state), "utf8");
|
|
133
145
|
return path;
|
|
@@ -11,7 +11,7 @@ export const DEFAULT_CONFIG: DevCrewConfig = {
|
|
|
11
11
|
lintCommands: [],
|
|
12
12
|
coverageCommands: [],
|
|
13
13
|
workflow: {
|
|
14
|
-
gates: ["requirements", "architecture", "implementation", "testing"],
|
|
14
|
+
gates: ["requirements", "architecture", "implementation", "implementation-review", "testing"],
|
|
15
15
|
artifactDirectory: "docs/devcrew",
|
|
16
16
|
},
|
|
17
17
|
};
|
|
@@ -15,6 +15,10 @@ export function runDir(cwd: string, runId: string): string {
|
|
|
15
15
|
return join(runsDir(cwd), runId);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
export function executionWorktreePath(cwd: string, runId: string): string {
|
|
19
|
+
return join(devcrewDir(cwd), "worktrees", runId);
|
|
20
|
+
}
|
|
21
|
+
|
|
18
22
|
export function statePath(cwd: string, runId: string): string {
|
|
19
23
|
return join(runDir(cwd, runId), "state.json");
|
|
20
24
|
}
|
|
@@ -31,24 +35,25 @@ export function standardsPath(cwd: string): string {
|
|
|
31
35
|
return join(devcrewDir(cwd), "standards.md");
|
|
32
36
|
}
|
|
33
37
|
|
|
34
|
-
export function docsRoot(cwd: string): string {
|
|
35
|
-
return join(cwd,
|
|
38
|
+
export function docsRoot(cwd: string, artifactDirectory: string = "docs/devcrew"): string {
|
|
39
|
+
return join(cwd, artifactDirectory);
|
|
36
40
|
}
|
|
37
41
|
|
|
38
|
-
export function docsRunDir(cwd: string, runId: string): string {
|
|
39
|
-
return join(docsRoot(cwd), runId);
|
|
42
|
+
export function docsRunDir(cwd: string, runId: string, artifactDirectory: string = "docs/devcrew"): string {
|
|
43
|
+
return join(docsRoot(cwd, artifactDirectory), runId);
|
|
40
44
|
}
|
|
41
45
|
|
|
42
|
-
export function artifactPath(cwd: string, runId: string, artifact: ArtifactName): string {
|
|
46
|
+
export function artifactPath(cwd: string, runId: string, artifact: ArtifactName, artifactDirectory: string = "docs/devcrew"): string {
|
|
43
47
|
const filenameByArtifact: Record<ArtifactName, string> = {
|
|
44
48
|
requirements: "requirements.md",
|
|
45
49
|
architecture: "architecture.md",
|
|
46
50
|
"implementation-plan": "implementation-plan.md",
|
|
47
51
|
"implementation-review": "implementation-review.md",
|
|
52
|
+
"architecture-review": "architecture-review.md",
|
|
48
53
|
"test-report": "test-report.md",
|
|
49
54
|
acceptance: "acceptance.md",
|
|
50
55
|
};
|
|
51
|
-
return join(docsRunDir(cwd, runId), filenameByArtifact[artifact]);
|
|
56
|
+
return join(docsRunDir(cwd, runId, artifactDirectory), filenameByArtifact[artifact]);
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
export async function ensureRunDirectories(cwd: string, runId: string): Promise<void> {
|
|
@@ -2,7 +2,14 @@ import { readFile, rename, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
|
|
4
4
|
import { ensureRunDirectories, statePath } from "./paths.js";
|
|
5
|
-
import type
|
|
5
|
+
import { GATES, type GateName, type RunState } from "./types.js";
|
|
6
|
+
|
|
7
|
+
function enabledGatesFromState(value: unknown): GateName[] {
|
|
8
|
+
const configured = Array.isArray(value)
|
|
9
|
+
? value.filter((gate): gate is GateName => typeof gate === "string" && GATES.includes(gate as GateName))
|
|
10
|
+
: [...GATES];
|
|
11
|
+
return [...new Set<GateName>([...configured, "implementation-review", "testing"])];
|
|
12
|
+
}
|
|
6
13
|
|
|
7
14
|
export async function saveState(state: RunState): Promise<RunState> {
|
|
8
15
|
state.updatedAt = new Date().toISOString();
|
|
@@ -17,12 +24,45 @@ export async function saveState(state: RunState): Promise<RunState> {
|
|
|
17
24
|
export async function loadState(cwd: string, runId: string): Promise<RunState> {
|
|
18
25
|
const raw = await readFile(statePath(cwd, runId), "utf8");
|
|
19
26
|
const parsed = JSON.parse(raw) as RunState;
|
|
27
|
+
const executionWorkspace = parsed.executionWorkspace;
|
|
28
|
+
const executionInstruction = parsed.executionInstruction;
|
|
29
|
+
const verificationWaiver = parsed.verificationWaiver;
|
|
20
30
|
return {
|
|
21
31
|
...parsed,
|
|
22
32
|
executionMode: parsed.executionMode ?? "plan",
|
|
33
|
+
executionPolicy: parsed.executionPolicy ?? "interactive-host",
|
|
34
|
+
enabledGates: enabledGatesFromState(parsed.enabledGates),
|
|
35
|
+
artifactDirectory: typeof parsed.artifactDirectory === "string" && parsed.artifactDirectory.trim().length > 0
|
|
36
|
+
? parsed.artifactDirectory
|
|
37
|
+
: "docs/devcrew",
|
|
38
|
+
executionWorkspace:
|
|
39
|
+
executionWorkspace &&
|
|
40
|
+
typeof executionWorkspace.path === "string" &&
|
|
41
|
+
typeof executionWorkspace.baseCommit === "string"
|
|
42
|
+
? executionWorkspace
|
|
43
|
+
: undefined,
|
|
44
|
+
executionInstruction:
|
|
45
|
+
executionInstruction &&
|
|
46
|
+
(executionInstruction.phase === "execution" || executionInstruction.phase === "testing") &&
|
|
47
|
+
typeof executionInstruction.workspacePath === "string" &&
|
|
48
|
+
typeof executionInstruction.instructions === "string" &&
|
|
49
|
+
typeof executionInstruction.createdAt === "string"
|
|
50
|
+
? executionInstruction
|
|
51
|
+
: undefined,
|
|
23
52
|
changedFiles: Array.isArray(parsed.changedFiles) ? parsed.changedFiles : [],
|
|
53
|
+
pendingQuestions: Array.isArray(parsed.pendingQuestions)
|
|
54
|
+
? parsed.pendingQuestions.filter((question): question is string => typeof question === "string" && question.trim().length > 0)
|
|
55
|
+
: [],
|
|
24
56
|
implementationDiff: typeof parsed.implementationDiff === "string" ? parsed.implementationDiff : "",
|
|
25
57
|
verification: Array.isArray(parsed.verification) ? parsed.verification : [],
|
|
58
|
+
verificationStatus:
|
|
59
|
+
parsed.verificationStatus === "passed" || parsed.verificationStatus === "failed" ? parsed.verificationStatus : "not_run",
|
|
60
|
+
verificationWaiver:
|
|
61
|
+
verificationWaiver &&
|
|
62
|
+
typeof verificationWaiver.reason === "string" &&
|
|
63
|
+
typeof verificationWaiver.createdAt === "string"
|
|
64
|
+
? verificationWaiver
|
|
65
|
+
: undefined,
|
|
26
66
|
lintResults: Array.isArray(parsed.lintResults) ? parsed.lintResults : [],
|
|
27
67
|
};
|
|
28
68
|
}
|
|
@@ -1,34 +1,41 @@
|
|
|
1
1
|
export const WORKFLOW_MODES = ["feature", "greenfield"] as const;
|
|
2
2
|
export const EXECUTION_MODES = ["plan", "apply"] as const;
|
|
3
|
+
export const EXECUTION_POLICIES = ["interactive-host", "headless-restricted", "headless-unattended"] as const;
|
|
3
4
|
export const HOSTS = ["codex", "claude"] as const;
|
|
4
5
|
export const BACKENDS = ["codex", "claude", "local"] as const;
|
|
5
6
|
export const PHASES = [
|
|
6
7
|
"requirements",
|
|
7
8
|
"architecture",
|
|
8
9
|
"implementation",
|
|
10
|
+
"execution",
|
|
11
|
+
"review",
|
|
9
12
|
"testing",
|
|
10
13
|
"acceptance",
|
|
11
14
|
"complete",
|
|
12
15
|
] as const;
|
|
13
|
-
export const GATES = ["requirements", "architecture", "implementation", "testing"] as const;
|
|
16
|
+
export const GATES = ["requirements", "architecture", "implementation", "implementation-review", "testing"] as const;
|
|
14
17
|
export const ARTIFACTS = [
|
|
15
18
|
"requirements",
|
|
16
19
|
"architecture",
|
|
17
20
|
"implementation-plan",
|
|
18
21
|
"implementation-review",
|
|
22
|
+
"architecture-review",
|
|
19
23
|
"test-report",
|
|
20
24
|
"acceptance",
|
|
21
25
|
] as const;
|
|
22
26
|
|
|
23
27
|
export type WorkflowMode = (typeof WORKFLOW_MODES)[number];
|
|
24
28
|
export type ExecutionMode = (typeof EXECUTION_MODES)[number];
|
|
29
|
+
export type ExecutionPolicy = (typeof EXECUTION_POLICIES)[number];
|
|
30
|
+
export type VerificationStatus = "not_run" | "passed" | "failed";
|
|
31
|
+
export type ArchitectureReviewDecision = "approved" | "changes_required";
|
|
25
32
|
export type Host = (typeof HOSTS)[number];
|
|
26
33
|
export type BackendName = (typeof BACKENDS)[number];
|
|
27
34
|
export type Phase = (typeof PHASES)[number];
|
|
28
35
|
export type GateName = (typeof GATES)[number];
|
|
29
36
|
export type ArtifactName = (typeof ARTIFACTS)[number];
|
|
30
37
|
export type GateStatus = "not_started" | "pending" | "approved" | "rejected";
|
|
31
|
-
export type RunStatus = "ready" | "awaiting_input" | "awaiting_approval" | "complete";
|
|
38
|
+
export type RunStatus = "ready" | "awaiting_input" | "awaiting_approval" | "awaiting_execution" | "complete";
|
|
32
39
|
|
|
33
40
|
export interface RoleSection {
|
|
34
41
|
heading: string;
|
|
@@ -87,6 +94,14 @@ export interface RoleResult {
|
|
|
87
94
|
summary: string;
|
|
88
95
|
markdown: string;
|
|
89
96
|
usedFallback: boolean;
|
|
97
|
+
questions?: string[];
|
|
98
|
+
reviewDecision?: ArchitectureReviewDecision;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface ArchitectureReview {
|
|
102
|
+
decision: ArchitectureReviewDecision;
|
|
103
|
+
summary: string;
|
|
104
|
+
reviewedAt: string;
|
|
90
105
|
}
|
|
91
106
|
|
|
92
107
|
export interface WorkflowFeedback {
|
|
@@ -106,6 +121,11 @@ export interface WorkflowAnswer {
|
|
|
106
121
|
createdAt: string;
|
|
107
122
|
}
|
|
108
123
|
|
|
124
|
+
export interface VerificationWaiver {
|
|
125
|
+
reason: string;
|
|
126
|
+
createdAt: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
109
129
|
// Reused for both verification and lint results — the command shape is identical.
|
|
110
130
|
export interface VerificationResult {
|
|
111
131
|
command: string;
|
|
@@ -115,6 +135,18 @@ export interface VerificationResult {
|
|
|
115
135
|
completedAt: string;
|
|
116
136
|
}
|
|
117
137
|
|
|
138
|
+
export interface ExecutionWorkspace {
|
|
139
|
+
path: string;
|
|
140
|
+
baseCommit: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface ExecutionInstruction {
|
|
144
|
+
phase: "execution" | "testing";
|
|
145
|
+
workspacePath: string;
|
|
146
|
+
instructions: string;
|
|
147
|
+
createdAt: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
118
150
|
export interface RunState {
|
|
119
151
|
version: 1;
|
|
120
152
|
runId: string;
|
|
@@ -122,6 +154,11 @@ export interface RunState {
|
|
|
122
154
|
host: Host;
|
|
123
155
|
mode: WorkflowMode;
|
|
124
156
|
executionMode: ExecutionMode;
|
|
157
|
+
executionPolicy: ExecutionPolicy;
|
|
158
|
+
enabledGates: GateName[];
|
|
159
|
+
artifactDirectory: string;
|
|
160
|
+
executionWorkspace?: ExecutionWorkspace;
|
|
161
|
+
executionInstruction?: ExecutionInstruction;
|
|
125
162
|
backend: BackendName;
|
|
126
163
|
request: string;
|
|
127
164
|
phase: Phase;
|
|
@@ -131,6 +168,8 @@ export interface RunState {
|
|
|
131
168
|
gates: Record<GateName, GateStatus>;
|
|
132
169
|
artifacts: Partial<Record<ArtifactName, string>>;
|
|
133
170
|
roles: RoleResult[];
|
|
171
|
+
architectureReview?: ArchitectureReview;
|
|
172
|
+
pendingQuestions: string[];
|
|
134
173
|
answers: WorkflowAnswer[];
|
|
135
174
|
approvals: WorkflowApproval[];
|
|
136
175
|
feedback: WorkflowFeedback[];
|
|
@@ -138,6 +177,8 @@ export interface RunState {
|
|
|
138
177
|
changedFiles: string[];
|
|
139
178
|
implementationDiff: string;
|
|
140
179
|
verification: VerificationResult[];
|
|
180
|
+
verificationStatus: VerificationStatus;
|
|
181
|
+
verificationWaiver?: VerificationWaiver;
|
|
141
182
|
// VerificationResult is reused for lint output — same shape, different semantics.
|
|
142
183
|
lintResults: VerificationResult[];
|
|
143
184
|
}
|
|
@@ -149,6 +190,7 @@ export interface StartWorkflowInput {
|
|
|
149
190
|
request: string;
|
|
150
191
|
backend?: BackendName;
|
|
151
192
|
executionMode?: ExecutionMode;
|
|
193
|
+
executionPolicy?: ExecutionPolicy;
|
|
152
194
|
}
|
|
153
195
|
|
|
154
196
|
export interface RunRef {
|
|
@@ -170,6 +212,15 @@ export interface AnswerWorkflowInput extends RunRef {
|
|
|
170
212
|
answer: string;
|
|
171
213
|
}
|
|
172
214
|
|
|
215
|
+
export interface WaiveVerificationInput extends RunRef {
|
|
216
|
+
reason: string;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export interface CompleteExecutionInput extends RunRef {
|
|
220
|
+
summary: string;
|
|
221
|
+
verification?: VerificationResult[];
|
|
222
|
+
}
|
|
223
|
+
|
|
173
224
|
export interface ArtifactRef extends RunRef {
|
|
174
225
|
name: ArtifactName;
|
|
175
226
|
}
|
|
@@ -2,12 +2,14 @@ import {
|
|
|
2
2
|
ARTIFACTS,
|
|
3
3
|
BACKENDS,
|
|
4
4
|
EXECUTION_MODES,
|
|
5
|
+
EXECUTION_POLICIES,
|
|
5
6
|
GATES,
|
|
6
7
|
HOSTS,
|
|
7
8
|
WORKFLOW_MODES,
|
|
8
9
|
type ArtifactName,
|
|
9
10
|
type BackendName,
|
|
10
11
|
type ExecutionMode,
|
|
12
|
+
type ExecutionPolicy,
|
|
11
13
|
type GateName,
|
|
12
14
|
type Host,
|
|
13
15
|
type WorkflowMode,
|
|
@@ -35,6 +37,10 @@ export function parseExecutionMode(value: unknown): ExecutionMode {
|
|
|
35
37
|
return oneOf(value, "executionMode", EXECUTION_MODES);
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
export function parseExecutionPolicy(value: unknown): ExecutionPolicy {
|
|
41
|
+
return oneOf(value, "executionPolicy", EXECUTION_POLICIES);
|
|
42
|
+
}
|
|
43
|
+
|
|
38
44
|
export function parseHost(value: unknown): Host {
|
|
39
45
|
return oneOf(value, "host", HOSTS);
|
|
40
46
|
}
|
|
@@ -77,3 +83,7 @@ export function parseFeedback(value: unknown): string {
|
|
|
77
83
|
export function parseAnswer(value: unknown): string {
|
|
78
84
|
return assertString(value, "answer");
|
|
79
85
|
}
|
|
86
|
+
|
|
87
|
+
export function parseWaiverReason(value: unknown): string {
|
|
88
|
+
return assertString(value, "reason");
|
|
89
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const DEVCREW_VERSION = "0.1.
|
|
1
|
+
export const DEVCREW_VERSION = "0.1.3";
|
|
2
2
|
export const DEVCREW_NPM_PACKAGE = "@shenlee/devcrew";
|