@shenlee/devcrew 0.1.1 → 0.1.2
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 +19 -5
- package/README.zh-CN.md +19 -5
- package/dist/packages/adapters/src/index.js +3 -2
- package/dist/packages/core/src/paths.js +3 -0
- package/dist/packages/core/src/store.js +6 -0
- package/dist/packages/core/src/types.js +1 -0
- package/dist/packages/core/src/version.js +1 -1
- package/dist/packages/core/src/workflow.js +51 -20
- package/dist/packages/orchestrator/src/index.js +118 -154
- package/dist/packages/orchestrator/src/worktree.js +198 -0
- package/dist/packages/service/src/stdio.js +19 -3
- package/dist/packages/service/src/tools.js +3 -3
- package/docs/claude-code.md +1 -1
- package/docs/codex.md +6 -2
- package/docs/quickstart.md +1 -1
- package/docs/superpowers/plans/2026-07-10-p0-foundation-repair.md +939 -0
- package/docs/superpowers/specs/2026-07-10-p0-foundation-repair-design.md +182 -0
- package/docs/workflow.md +21 -4
- package/package.json +1 -1
- package/packages/adapters/src/index.ts +4 -3
- package/packages/core/src/paths.ts +4 -0
- package/packages/core/src/store.ts +7 -0
- package/packages/core/src/types.ts +7 -0
- package/packages/core/src/version.ts +1 -1
- package/packages/core/src/workflow.ts +55 -20
- package/packages/orchestrator/src/index.ts +136 -179
- package/packages/orchestrator/src/worktree.ts +269 -0
- package/packages/service/src/stdio.ts +27 -6
- package/packages/service/src/tools.ts +2 -2
- package/plugins/devcrew-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/devcrew-codex/.mcp.json +1 -1
- package/plugins/devcrew-codex/agents/architect.toml +6 -0
- package/plugins/devcrew-codex/agents/implementer.toml +6 -0
- package/plugins/devcrew-codex/agents/pm.toml +7 -0
- package/plugins/devcrew-codex/agents/tester.toml +6 -0
- package/plugins/devcrew-codex/assets/logo.png +0 -0
- package/scripts/smoke-codex-plugin.mjs +1 -1
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# DevCrew P0 Foundation Repair Design
|
|
2
|
+
|
|
3
|
+
## Objective
|
|
4
|
+
|
|
5
|
+
Repair the P0 workflow and execution-safety defects identified in the 0.1.1 review without expanding into the P1 role-model, semantic architecture review, or configurable workflow work.
|
|
6
|
+
|
|
7
|
+
The repaired workflow must guarantee that requester approval happens before code changes, apply-mode changes remain isolated until final approval, malformed MCP input cannot poison the server, gate mutations are valid and idempotent, local fallback cannot impersonate apply execution, and the checked-in plugin cannot drift from the package version.
|
|
8
|
+
|
|
9
|
+
## Scope
|
|
10
|
+
|
|
11
|
+
This design includes:
|
|
12
|
+
|
|
13
|
+
- Move real implementation after the existing `implementation` gate.
|
|
14
|
+
- Execute apply-mode implementation and verification in a run-owned Git worktree.
|
|
15
|
+
- Capture committed, staged, unstaged, renamed, deleted, binary, and new-file changes as one reviewable patch.
|
|
16
|
+
- Apply the reviewed patch to the requester's repository only after the `testing` gate is approved.
|
|
17
|
+
- Keep rejected testing work isolated and allow the implementer to revise it.
|
|
18
|
+
- Enforce legal, idempotent `approve`, `reject`, and `answer` transitions.
|
|
19
|
+
- Validate JSON-RPC request shape and recover the stdio queue after handler failures.
|
|
20
|
+
- Reject `executionMode=apply` with the `local` backend.
|
|
21
|
+
- Synchronize package, generated plugin, checked-in plugin, and MCP runtime versions.
|
|
22
|
+
|
|
23
|
+
This design does not include:
|
|
24
|
+
|
|
25
|
+
- Real architect-driven semantic diff review.
|
|
26
|
+
- Structured PM questions and machine-readable role verdicts.
|
|
27
|
+
- Host-native role/subagent integration.
|
|
28
|
+
- Configurable gates or artifact directories.
|
|
29
|
+
- Claude marketplace packaging or smoke coverage.
|
|
30
|
+
- Cross-process run locking or HTTP transport.
|
|
31
|
+
|
|
32
|
+
## Workflow Model
|
|
33
|
+
|
|
34
|
+
The public gate names remain unchanged for MCP compatibility:
|
|
35
|
+
|
|
36
|
+
1. `requirements`
|
|
37
|
+
2. `architecture`
|
|
38
|
+
3. `implementation`
|
|
39
|
+
4. `testing`
|
|
40
|
+
|
|
41
|
+
The internal phase sequence becomes:
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
requirements
|
|
45
|
+
-> architecture
|
|
46
|
+
-> implementation
|
|
47
|
+
-> execution (apply only)
|
|
48
|
+
-> testing
|
|
49
|
+
-> acceptance
|
|
50
|
+
-> complete
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The existing `implementation` phase becomes plan-only in both execution modes. The implementer receives a read-only role prompt and writes `implementation-plan.md`. Approving the `implementation` gate advances plan mode directly to `testing`, while apply mode advances to the new nongated `execution` phase.
|
|
54
|
+
|
|
55
|
+
The `execution` phase runs the implementer in an isolated worktree, writes `implementation-review.md`, and advances to `testing` with status `ready`. The next `continue` runs the tester and verification commands in the same worktree, then opens the `testing` gate.
|
|
56
|
+
|
|
57
|
+
Approving the `testing` gate applies the captured patch to the original repository, removes the worktree, and advances to acceptance. Rejecting the testing gate leaves the worktree isolated. The rejection feedback can be answered, after which the run returns to `execution` so the implementer can revise the existing isolated changes.
|
|
58
|
+
|
|
59
|
+
## Apply Isolation
|
|
60
|
+
|
|
61
|
+
### Workspace creation
|
|
62
|
+
|
|
63
|
+
Before the first execution attempt DevCrew must:
|
|
64
|
+
|
|
65
|
+
1. Confirm the requester repository is a Git repository with a clean working tree, excluding DevCrew runtime and artifact paths.
|
|
66
|
+
2. Record the current `HEAD` as `baseCommit`.
|
|
67
|
+
3. Create a detached worktree under `.devcrew/worktrees/<run-id>`.
|
|
68
|
+
4. Persist the worktree path and base commit before invoking the SDK role.
|
|
69
|
+
|
|
70
|
+
All implementation, lint, test, and coverage commands run with the worktree as their working directory. Artifacts and state remain in the original repository.
|
|
71
|
+
|
|
72
|
+
### Capturing all changes
|
|
73
|
+
|
|
74
|
+
After the implementer returns DevCrew must normalize any commits created by the role back into worktree changes relative to `baseCommit`. This is safe because the worktree is owned by the run and has not been promoted.
|
|
75
|
+
|
|
76
|
+
DevCrew then marks untracked files as intent-to-add inside the worktree and captures:
|
|
77
|
+
|
|
78
|
+
- `git status --porcelain=v1 -z -uall` for changed paths.
|
|
79
|
+
- `git diff --binary --no-ext-diff <baseCommit> --` for the complete patch.
|
|
80
|
+
|
|
81
|
+
Using the base commit includes staged and unstaged changes. Intent-to-add makes new files visible in the patch. NUL-delimited status parsing avoids filename quoting and rename parsing bugs.
|
|
82
|
+
|
|
83
|
+
### Promotion
|
|
84
|
+
|
|
85
|
+
Before applying an approved patch DevCrew must re-check that the requester repository is clean and still points at `baseCommit`. If either condition changed, approval fails without deleting the worktree or mutating the requester repository.
|
|
86
|
+
|
|
87
|
+
Promotion writes the captured patch under the run directory, applies it to the requester repository with `git apply --binary`, verifies the resulting requester diff matches the reviewed patch, and then removes the worktree. The promoted files remain uncommitted so the requester retains normal review and commit control.
|
|
88
|
+
|
|
89
|
+
An empty apply patch is an error. Apply mode must never report successful implementation when no repository change was produced.
|
|
90
|
+
|
|
91
|
+
## State Changes
|
|
92
|
+
|
|
93
|
+
`RunState` gains optional execution workspace metadata:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
interface ExecutionWorkspace {
|
|
97
|
+
path: string;
|
|
98
|
+
baseCommit: string;
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The `execution` phase is added to `PHASES`. Older state files without execution workspace metadata continue to load.
|
|
103
|
+
|
|
104
|
+
Gate mutations use these rules:
|
|
105
|
+
|
|
106
|
+
- Approving an already approved gate returns the unchanged state and does not append another approval.
|
|
107
|
+
- Rejecting an already rejected gate returns the unchanged state and does not append duplicate feedback.
|
|
108
|
+
- A pending gate can only be approved or rejected when it is the current phase gate and status is `awaiting_approval`.
|
|
109
|
+
- `answer` is only legal while status is `awaiting_input` and the current gate is rejected.
|
|
110
|
+
- Calls against `acceptance` or `complete` cannot move the run backwards.
|
|
111
|
+
|
|
112
|
+
The service uses a new orchestrated approval entry point so testing approval can promote the patch before the core state advances.
|
|
113
|
+
|
|
114
|
+
## Backend Rules
|
|
115
|
+
|
|
116
|
+
After host/config backend resolution, workflow creation rejects this combination:
|
|
117
|
+
|
|
118
|
+
```text
|
|
119
|
+
executionMode=apply, backend=local
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Plan mode keeps deterministic local fallback. Apply mode requires a real Codex or Claude SDK backend at every role phase.
|
|
123
|
+
|
|
124
|
+
The implementation-plan role is forced to read-only behavior even when the run execution mode is apply. Only the new execution phase and apply-mode tester receive write/Bash capabilities.
|
|
125
|
+
|
|
126
|
+
## Stdio Reliability
|
|
127
|
+
|
|
128
|
+
The line processor validates that parsed JSON is a non-array object with `jsonrpc: "2.0"` and a string `method`. Invalid request objects receive JSON-RPC error `-32600` with `id: null` and do not enter the request queue.
|
|
129
|
+
|
|
130
|
+
For queued requests, each returned task may reject to its caller, but the internal queue stores a recovered promise. One handler failure therefore cannot prevent later requests from executing.
|
|
131
|
+
|
|
132
|
+
Existing parse error behavior remains `-32700`. Notifications without an ID remain response-free.
|
|
133
|
+
|
|
134
|
+
## Version Discipline
|
|
135
|
+
|
|
136
|
+
The next implementation version is `0.1.2`. The following must agree:
|
|
137
|
+
|
|
138
|
+
- `package.json`
|
|
139
|
+
- `package-lock.json`
|
|
140
|
+
- `DEVCREW_VERSION`
|
|
141
|
+
- generated Codex and Claude manifests
|
|
142
|
+
- checked-in Codex manifest
|
|
143
|
+
- checked-in Codex MCP npm package specifier
|
|
144
|
+
- smoke client metadata and version assertions
|
|
145
|
+
|
|
146
|
+
CI adds a checked-in plugin drift test that generates a fresh Codex plugin in a temporary directory and compares all generated text files with the checked-in bundle. Binary assets are checked by hash. Publication order is npm `0.1.2` first, then marketplace push, so the public plugin never references an unavailable npm version.
|
|
147
|
+
|
|
148
|
+
## Error Handling
|
|
149
|
+
|
|
150
|
+
- Worktree creation failure leaves the run in `execution/ready` with no role result recorded.
|
|
151
|
+
- SDK failure leaves the isolated worktree available for diagnosis and retry.
|
|
152
|
+
- Empty implementation patch fails before testing.
|
|
153
|
+
- Verification failures remain visible in the testing artifact; automatic test verdict blocking remains P1.
|
|
154
|
+
- Promotion precondition failure leaves the worktree and state intact for manual resolution.
|
|
155
|
+
- Worktree cleanup runs only after successful promotion or explicit run rejection cleanup added in a later lifecycle feature.
|
|
156
|
+
|
|
157
|
+
## Testing Strategy
|
|
158
|
+
|
|
159
|
+
Tests are added before implementation and must demonstrate the original failures:
|
|
160
|
+
|
|
161
|
+
- Implementation-plan approval occurs before the first writable role invocation.
|
|
162
|
+
- Local apply start is rejected.
|
|
163
|
+
- An implementer-created commit is converted into a complete reviewed patch.
|
|
164
|
+
- Staged, unstaged, renamed, deleted, binary, and untracked files appear in review evidence.
|
|
165
|
+
- Rejecting testing keeps changes outside the requester repository and supports revision.
|
|
166
|
+
- Approving testing promotes exactly the reviewed patch.
|
|
167
|
+
- Changed requester `HEAD` or dirty requester worktree blocks promotion.
|
|
168
|
+
- Empty apply output fails.
|
|
169
|
+
- Duplicate approvals/rejections are idempotent.
|
|
170
|
+
- Complete runs cannot be reopened.
|
|
171
|
+
- Answer outside `awaiting_input` fails.
|
|
172
|
+
- JSON `null`, arrays, missing methods, and wrong JSON-RPC versions return `-32600`.
|
|
173
|
+
- A rejected request handler does not block the next stdio request.
|
|
174
|
+
- Checked-in plugin version and generated bundle match `DEVCREW_VERSION`.
|
|
175
|
+
|
|
176
|
+
The final verification command remains `npm run validate`, followed by the isolated Codex marketplace smoke after `@shenlee/devcrew@0.1.2` is published.
|
|
177
|
+
|
|
178
|
+
## Compatibility
|
|
179
|
+
|
|
180
|
+
MCP tool names and gate names remain stable. Existing plan-mode callers continue using the same approve/continue sequence. Apply-mode sequencing gains one additional `continue` call for the execution phase before testing.
|
|
181
|
+
|
|
182
|
+
Existing 0.1.1 run files remain loadable. Runs already in the old `implementation` phase are treated as implementation planning and cannot write code until their implementation gate is approved under the new state transition.
|
package/docs/workflow.md
CHANGED
|
@@ -16,14 +16,31 @@ 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. The implementer works in `.devcrew/worktrees/<run-id>`, and DevCrew captures changed files, a binary-capable patch, lint evidence, and `implementation-review.md`.
|
|
21
|
+
5. `testing`: tester records validation and acceptance evidence. In apply mode, the tester and verification commands run in the same isolated worktree, then 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
|
+
-> isolated testing
|
|
36
|
+
-> testing approval
|
|
37
|
+
-> patch promotion to requester repository
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The implementation-plan approval advances the run to `execution`. One `devcrew_continue` call runs the implementer in the isolated worktree and leaves the run at `testing/ready`; another `devcrew_continue` runs the tester and verification commands before opening the testing gate.
|
|
41
|
+
|
|
42
|
+
Apply requires Git, a real Codex or Claude SDK backend, and a clean requester worktree both when execution starts and when the reviewed patch is promoted. The requester repository remains unchanged until testing approval. If testing is rejected, `devcrew_answer` records the response and returns the run to `execution/ready` with the isolated worktree intact.
|
|
43
|
+
|
|
27
44
|
`devcrew_start` records the created run as the active run for the repository.
|
|
28
45
|
Subsequent MCP calls can omit `runId`; DevCrew resolves it from
|
|
29
46
|
`.devcrew/active-run.json`. Plugin MCP configs set `DEVCREW_HOST`, so `host`
|
|
@@ -38,7 +55,7 @@ Each main phase has a gate:
|
|
|
38
55
|
- `implementation`
|
|
39
56
|
- `testing`
|
|
40
57
|
|
|
41
|
-
The requester approves or rejects each gate. Rejection records feedback and returns the workflow to `awaiting_input`.
|
|
58
|
+
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.
|
|
42
59
|
|
|
43
60
|
## State And Artifacts
|
|
44
61
|
|
package/package.json
CHANGED
|
@@ -102,7 +102,7 @@ export function renderRolePrompt(input: Omit<RoleRunInput, "backend" | "cwd">):
|
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
const canApply =
|
|
105
|
+
const canApply = roleCanApply({ ...input, executionMode });
|
|
106
106
|
const permissionInstruction = canApply
|
|
107
107
|
? input.role === "tester"
|
|
108
108
|
? "You may run validation commands needed for the approved scope and report exact evidence."
|
|
@@ -128,6 +128,7 @@ function titleForPhase(phase: Phase): string {
|
|
|
128
128
|
requirements: "Requirements",
|
|
129
129
|
architecture: "Architecture",
|
|
130
130
|
implementation: "Implementation Plan",
|
|
131
|
+
execution: "Implementation Review",
|
|
131
132
|
testing: "Test Report",
|
|
132
133
|
acceptance: "Acceptance",
|
|
133
134
|
complete: "Acceptance",
|
|
@@ -293,8 +294,8 @@ export function extractClaudeResult(message: ClaudeResultMessage | undefined): s
|
|
|
293
294
|
return text;
|
|
294
295
|
}
|
|
295
296
|
|
|
296
|
-
function roleCanApply(input: RoleRunInput): boolean {
|
|
297
|
-
return input.executionMode === "apply" && (input.
|
|
297
|
+
function roleCanApply(input: Pick<RoleRunInput, "executionMode" | "phase">): boolean {
|
|
298
|
+
return input.executionMode === "apply" && (input.phase === "execution" || input.phase === "testing");
|
|
298
299
|
}
|
|
299
300
|
|
|
300
301
|
function codexSandboxForRole(input: RoleRunInput): CodexSandboxMode {
|
|
@@ -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
|
}
|
|
@@ -17,9 +17,16 @@ export async function saveState(state: RunState): Promise<RunState> {
|
|
|
17
17
|
export async function loadState(cwd: string, runId: string): Promise<RunState> {
|
|
18
18
|
const raw = await readFile(statePath(cwd, runId), "utf8");
|
|
19
19
|
const parsed = JSON.parse(raw) as RunState;
|
|
20
|
+
const executionWorkspace = parsed.executionWorkspace;
|
|
20
21
|
return {
|
|
21
22
|
...parsed,
|
|
22
23
|
executionMode: parsed.executionMode ?? "plan",
|
|
24
|
+
executionWorkspace:
|
|
25
|
+
executionWorkspace &&
|
|
26
|
+
typeof executionWorkspace.path === "string" &&
|
|
27
|
+
typeof executionWorkspace.baseCommit === "string"
|
|
28
|
+
? executionWorkspace
|
|
29
|
+
: undefined,
|
|
23
30
|
changedFiles: Array.isArray(parsed.changedFiles) ? parsed.changedFiles : [],
|
|
24
31
|
implementationDiff: typeof parsed.implementationDiff === "string" ? parsed.implementationDiff : "",
|
|
25
32
|
verification: Array.isArray(parsed.verification) ? parsed.verification : [],
|
|
@@ -6,6 +6,7 @@ export const PHASES = [
|
|
|
6
6
|
"requirements",
|
|
7
7
|
"architecture",
|
|
8
8
|
"implementation",
|
|
9
|
+
"execution",
|
|
9
10
|
"testing",
|
|
10
11
|
"acceptance",
|
|
11
12
|
"complete",
|
|
@@ -115,6 +116,11 @@ export interface VerificationResult {
|
|
|
115
116
|
completedAt: string;
|
|
116
117
|
}
|
|
117
118
|
|
|
119
|
+
export interface ExecutionWorkspace {
|
|
120
|
+
path: string;
|
|
121
|
+
baseCommit: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
118
124
|
export interface RunState {
|
|
119
125
|
version: 1;
|
|
120
126
|
runId: string;
|
|
@@ -122,6 +128,7 @@ export interface RunState {
|
|
|
122
128
|
host: Host;
|
|
123
129
|
mode: WorkflowMode;
|
|
124
130
|
executionMode: ExecutionMode;
|
|
131
|
+
executionWorkspace?: ExecutionWorkspace;
|
|
125
132
|
backend: BackendName;
|
|
126
133
|
request: string;
|
|
127
134
|
phase: Phase;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const DEVCREW_VERSION = "0.1.
|
|
1
|
+
export const DEVCREW_VERSION = "0.1.2";
|
|
2
2
|
export const DEVCREW_NPM_PACKAGE = "@shenlee/devcrew";
|
|
@@ -39,14 +39,17 @@ function newRunId(): string {
|
|
|
39
39
|
return `dc-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
export function nextPhaseAfterGate(gate: GateName): RunState["phase"] {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
42
|
+
export function nextPhaseAfterGate(state: RunState, gate: GateName): RunState["phase"] {
|
|
43
|
+
switch (gate) {
|
|
44
|
+
case "requirements":
|
|
45
|
+
return "architecture";
|
|
46
|
+
case "architecture":
|
|
47
|
+
return "implementation";
|
|
48
|
+
case "implementation":
|
|
49
|
+
return state.executionMode === "apply" ? "execution" : "testing";
|
|
50
|
+
case "testing":
|
|
51
|
+
return "acceptance";
|
|
52
|
+
}
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
export function artifactForPhase(phase: RunState["phase"]): ArtifactName {
|
|
@@ -54,6 +57,7 @@ export function artifactForPhase(phase: RunState["phase"]): ArtifactName {
|
|
|
54
57
|
requirements: "requirements",
|
|
55
58
|
architecture: "architecture",
|
|
56
59
|
implementation: "implementation-plan",
|
|
60
|
+
execution: "implementation-review",
|
|
57
61
|
testing: "test-report",
|
|
58
62
|
acceptance: "acceptance",
|
|
59
63
|
complete: "acceptance",
|
|
@@ -68,10 +72,17 @@ export function gateForPhase(phase: RunState["phase"]): GateName | undefined {
|
|
|
68
72
|
return undefined;
|
|
69
73
|
}
|
|
70
74
|
|
|
71
|
-
function
|
|
75
|
+
function assertPendingCurrentGate(state: RunState, gate: GateName): void {
|
|
72
76
|
const expected = gateForPhase(state.phase);
|
|
73
|
-
if (expected
|
|
74
|
-
throw new Error(
|
|
77
|
+
if (expected !== gate) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
expected
|
|
80
|
+
? `Cannot act on ${gate} while current gate is ${expected}`
|
|
81
|
+
: `Cannot act on ${gate} while workflow phase is ${state.phase}`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
if (state.status !== "awaiting_approval" || state.gates[gate] !== "pending") {
|
|
85
|
+
throw new Error(`Gate ${gate} is not pending approval`);
|
|
75
86
|
}
|
|
76
87
|
}
|
|
77
88
|
|
|
@@ -94,6 +105,9 @@ export async function startWorkflow(input: StartWorkflowInput, options: Workflow
|
|
|
94
105
|
const config = await ensureConfig(cwd);
|
|
95
106
|
const backend = input.backend ? parseBackend(input.backend) : config.defaultBackend === "host-preferred" ? host : config.defaultBackend;
|
|
96
107
|
const executionMode = input.executionMode ? parseExecutionMode(input.executionMode) : config.executionMode;
|
|
108
|
+
if (executionMode === "apply" && backend === "local") {
|
|
109
|
+
throw new Error("DevCrew apply mode requires a codex or claude backend; local is plan-only");
|
|
110
|
+
}
|
|
97
111
|
const createdAt = now();
|
|
98
112
|
const state: RunState = {
|
|
99
113
|
version: 1,
|
|
@@ -149,6 +163,10 @@ export async function continueWorkflow(input: RunRef): Promise<RunState> {
|
|
|
149
163
|
return saveState(state);
|
|
150
164
|
}
|
|
151
165
|
|
|
166
|
+
if (state.phase === "execution") {
|
|
167
|
+
throw new Error("DevCrew execution phase requires orchestrated continuation");
|
|
168
|
+
}
|
|
169
|
+
|
|
152
170
|
const gate = gateForPhase(state.phase);
|
|
153
171
|
if (!gate) {
|
|
154
172
|
state.status = "complete";
|
|
@@ -160,10 +178,23 @@ export async function continueWorkflow(input: RunRef): Promise<RunState> {
|
|
|
160
178
|
return saveState(state);
|
|
161
179
|
}
|
|
162
180
|
|
|
163
|
-
export async function
|
|
181
|
+
export async function validateWorkflowApproval(input: ApproveWorkflowInput): Promise<RunState> {
|
|
164
182
|
const state = await getWorkflowStatus(input);
|
|
165
183
|
const gate = parseGate(input.gate);
|
|
166
|
-
|
|
184
|
+
if (state.gates[gate] === "approved") {
|
|
185
|
+
return state;
|
|
186
|
+
}
|
|
187
|
+
assertPendingCurrentGate(state, gate);
|
|
188
|
+
parseOptionalNote(input.note);
|
|
189
|
+
return state;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function approveWorkflow(input: ApproveWorkflowInput): Promise<RunState> {
|
|
193
|
+
const state = await validateWorkflowApproval(input);
|
|
194
|
+
const gate = parseGate(input.gate);
|
|
195
|
+
if (state.gates[gate] === "approved") {
|
|
196
|
+
return state;
|
|
197
|
+
}
|
|
167
198
|
state.gates[gate] = "approved";
|
|
168
199
|
state.approvals.push({
|
|
169
200
|
gate,
|
|
@@ -171,7 +202,7 @@ export async function approveWorkflow(input: ApproveWorkflowInput): Promise<RunS
|
|
|
171
202
|
createdAt: now(),
|
|
172
203
|
});
|
|
173
204
|
|
|
174
|
-
const nextPhase = nextPhaseAfterGate(gate);
|
|
205
|
+
const nextPhase = nextPhaseAfterGate(state, gate);
|
|
175
206
|
state.phase = nextPhase;
|
|
176
207
|
state.status = "ready";
|
|
177
208
|
return saveState(state);
|
|
@@ -180,7 +211,10 @@ export async function approveWorkflow(input: ApproveWorkflowInput): Promise<RunS
|
|
|
180
211
|
export async function rejectWorkflow(input: RejectWorkflowInput): Promise<RunState> {
|
|
181
212
|
const state = await getWorkflowStatus(input);
|
|
182
213
|
const gate = parseGate(input.gate);
|
|
183
|
-
|
|
214
|
+
if (state.gates[gate] === "rejected") {
|
|
215
|
+
return state;
|
|
216
|
+
}
|
|
217
|
+
assertPendingCurrentGate(state, gate);
|
|
184
218
|
state.gates[gate] = "rejected";
|
|
185
219
|
state.status = "awaiting_input";
|
|
186
220
|
state.feedback.push({
|
|
@@ -193,15 +227,16 @@ export async function rejectWorkflow(input: RejectWorkflowInput): Promise<RunSta
|
|
|
193
227
|
|
|
194
228
|
export async function answerWorkflow(input: AnswerWorkflowInput, options: WorkflowMutationOptions = {}): Promise<RunState> {
|
|
195
229
|
const state = await getWorkflowStatus(input);
|
|
230
|
+
const gate = gateForPhase(state.phase);
|
|
231
|
+
if (state.status !== "awaiting_input" || !gate || state.gates[gate] !== "rejected") {
|
|
232
|
+
throw new Error("Workflow must be awaiting_input at a rejected current gate before recording an answer");
|
|
233
|
+
}
|
|
196
234
|
state.answers.push({
|
|
197
235
|
answer: parseAnswer(input.answer),
|
|
198
236
|
createdAt: now(),
|
|
199
237
|
});
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
state.gates[gate] = "pending";
|
|
203
|
-
state.status = "awaiting_approval";
|
|
204
|
-
}
|
|
238
|
+
state.gates[gate] = "pending";
|
|
239
|
+
state.status = "awaiting_approval";
|
|
205
240
|
if (!options.skipArtifactWrite) {
|
|
206
241
|
await writeCurrentArtifact(state);
|
|
207
242
|
}
|