@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.
Files changed (37) hide show
  1. package/README.md +19 -5
  2. package/README.zh-CN.md +19 -5
  3. package/dist/packages/adapters/src/index.js +3 -2
  4. package/dist/packages/core/src/paths.js +3 -0
  5. package/dist/packages/core/src/store.js +6 -0
  6. package/dist/packages/core/src/types.js +1 -0
  7. package/dist/packages/core/src/version.js +1 -1
  8. package/dist/packages/core/src/workflow.js +51 -20
  9. package/dist/packages/orchestrator/src/index.js +118 -154
  10. package/dist/packages/orchestrator/src/worktree.js +198 -0
  11. package/dist/packages/service/src/stdio.js +19 -3
  12. package/dist/packages/service/src/tools.js +3 -3
  13. package/docs/claude-code.md +1 -1
  14. package/docs/codex.md +6 -2
  15. package/docs/quickstart.md +1 -1
  16. package/docs/superpowers/plans/2026-07-10-p0-foundation-repair.md +939 -0
  17. package/docs/superpowers/specs/2026-07-10-p0-foundation-repair-design.md +182 -0
  18. package/docs/workflow.md +21 -4
  19. package/package.json +1 -1
  20. package/packages/adapters/src/index.ts +4 -3
  21. package/packages/core/src/paths.ts +4 -0
  22. package/packages/core/src/store.ts +7 -0
  23. package/packages/core/src/types.ts +7 -0
  24. package/packages/core/src/version.ts +1 -1
  25. package/packages/core/src/workflow.ts +55 -20
  26. package/packages/orchestrator/src/index.ts +136 -179
  27. package/packages/orchestrator/src/worktree.ts +269 -0
  28. package/packages/service/src/stdio.ts +27 -6
  29. package/packages/service/src/tools.ts +2 -2
  30. package/plugins/devcrew-codex/.codex-plugin/plugin.json +1 -1
  31. package/plugins/devcrew-codex/.mcp.json +1 -1
  32. package/plugins/devcrew-codex/agents/architect.toml +6 -0
  33. package/plugins/devcrew-codex/agents/implementer.toml +6 -0
  34. package/plugins/devcrew-codex/agents/pm.toml +7 -0
  35. package/plugins/devcrew-codex/agents/tester.toml +6 -0
  36. package/plugins/devcrew-codex/assets/logo.png +0 -0
  37. 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 the implementation plan and coding checklist. DevCrew also writes `implementation-review.md` with changed files, captured diff, and architecture compliance review prompts before opening the implementation gate.
20
- 4. `testing`: tester records validation strategy and acceptance evidence.
21
- 5. `acceptance`: generated after the testing gate is approved.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shenlee/devcrew",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Multi-host agent workflow service for Codex and Claude Code.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -102,7 +102,7 @@ export function renderRolePrompt(input: Omit<RoleRunInput, "backend" | "cwd">):
102
102
  }
103
103
  }
104
104
 
105
- const canApply = executionMode === "apply" && (input.role === "implementer" || input.role === "tester");
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.role === "implementer" || input.role === "tester");
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";
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
- const nextByGate: Record<GateName, RunState["phase"]> = {
44
- requirements: "architecture",
45
- architecture: "implementation",
46
- implementation: "testing",
47
- testing: "acceptance",
48
- };
49
- return nextByGate[gate];
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 assertCurrentGate(state: RunState, gate: GateName): void {
75
+ function assertPendingCurrentGate(state: RunState, gate: GateName): void {
72
76
  const expected = gateForPhase(state.phase);
73
- if (expected && expected !== gate) {
74
- throw new Error(`Cannot act on ${gate} while current gate is ${expected}`);
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 approveWorkflow(input: ApproveWorkflowInput): Promise<RunState> {
181
+ export async function validateWorkflowApproval(input: ApproveWorkflowInput): Promise<RunState> {
164
182
  const state = await getWorkflowStatus(input);
165
183
  const gate = parseGate(input.gate);
166
- assertCurrentGate(state, gate);
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
- assertCurrentGate(state, gate);
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
- const gate = gateForPhase(state.phase);
201
- if (gate) {
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
  }