@shenlee/devcrew 0.1.4 → 0.1.5

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.
@@ -1,939 +0,0 @@
1
- # DevCrew P0 Foundation Repair Implementation Plan
2
-
3
- > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
-
5
- **Goal:** Make DevCrew apply mode approval-first and worktree-isolated while repairing gate idempotency, stdio recovery, local-backend false success, and plugin version drift.
6
-
7
- **Architecture:** Keep the four public gates stable. Add an internal nongated `execution` phase after implementation-plan approval, execute implementation and verification in `.devcrew/worktrees/<run-id>`, and promote the reviewed binary patch only when the testing gate is approved. Core owns legal state transitions, the orchestrator owns execution workspaces and promotion, and stdio remains a serialized but failure-recovering transport.
8
-
9
- **Tech Stack:** TypeScript 5.8, Node.js 20+, Node test runner, Git worktrees, JSON-RPC/MCP, npm/Codex plugins.
10
-
11
- ---
12
-
13
- ## File Map
14
-
15
- - `packages/core/src/types.ts`: add the internal execution phase and persisted workspace metadata.
16
- - `packages/core/src/workflow.ts`: enforce gate invariants, reject local apply, and branch plan/apply after implementation-plan approval.
17
- - `packages/core/src/store.ts`: load older state while validating optional workspace metadata.
18
- - `packages/adapters/src/index.ts`: keep implementation planning read-only and allow writes only in the execution/testing phases.
19
- - `packages/orchestrator/src/worktree.ts`: own worktree creation, complete patch capture, promotion, and cleanup.
20
- - `packages/orchestrator/src/index.ts`: route planning, execution, testing, rejection revision, and testing approval.
21
- - `packages/service/src/tools.ts`: use orchestrated approval for patch promotion.
22
- - `packages/service/src/stdio.ts`: validate JSON-RPC request shape and recover the serialized queue.
23
- - `packages/plugins/src/index.ts`: continue generating every plugin version reference from shared constants.
24
- - `plugins/devcrew-codex/**`: synchronize the checked-in generated bundle.
25
- - `tests/core.test.ts`, `tests/stdio.test.ts`, `tests/orchestrator.test.ts`, `tests/service.test.ts`, `tests/plugins.test.ts`: regression coverage.
26
- - `package.json`, `package-lock.json`, `packages/core/src/version.ts`, `scripts/smoke-codex-plugin.mjs`: version `0.1.2` synchronization.
27
- - `README.md`, `README.zh-CN.md`, `docs/workflow.md`, `docs/codex.md`: describe the corrected apply flow and release order.
28
-
29
- ### Task 1: Enforce Core State And Backend Invariants
30
-
31
- **Files:**
32
- - Modify: `packages/core/src/types.ts`
33
- - Modify: `packages/core/src/workflow.ts`
34
- - Modify: `packages/core/src/store.ts`
35
- - Test: `tests/core.test.ts`
36
-
37
- - [ ] **Step 1: Write failing core regression tests**
38
-
39
- Add tests that express the desired API before changing production code:
40
-
41
- ```ts
42
- test("apply mode rejects the deterministic local backend", async () => {
43
- const cwd = await tempProject();
44
- await assert.rejects(
45
- () => startWorkflow({
46
- cwd,
47
- host: "codex",
48
- mode: "feature",
49
- request: "Make a real change",
50
- backend: "local",
51
- executionMode: "apply",
52
- }),
53
- /apply mode requires a codex or claude backend/i,
54
- );
55
- });
56
-
57
- test("apply implementation approval advances to execution while plan advances to testing", async () => {
58
- const apply = await startWorkflow({
59
- cwd: await tempProject(),
60
- host: "codex",
61
- mode: "feature",
62
- request: "Apply a change",
63
- backend: "codex",
64
- executionMode: "apply",
65
- });
66
- apply.phase = "implementation";
67
- apply.status = "awaiting_approval";
68
- apply.gates.requirements = "approved";
69
- apply.gates.architecture = "approved";
70
- apply.gates.implementation = "pending";
71
- await saveState(apply);
72
- assert.equal((await approveWorkflow({ cwd: apply.cwd, runId: apply.runId, gate: "implementation" })).phase, "execution");
73
-
74
- const plan = await startWorkflow({
75
- cwd: await tempProject(),
76
- host: "codex",
77
- mode: "feature",
78
- request: "Plan a change",
79
- backend: "local",
80
- });
81
- plan.phase = "implementation";
82
- plan.status = "awaiting_approval";
83
- plan.gates.requirements = "approved";
84
- plan.gates.architecture = "approved";
85
- plan.gates.implementation = "pending";
86
- await saveState(plan);
87
- assert.equal((await approveWorkflow({ cwd: plan.cwd, runId: plan.runId, gate: "implementation" })).phase, "testing");
88
- });
89
-
90
- test("completed workflows cannot be reopened and duplicate approval is idempotent", async () => {
91
- const cwd = await tempProject();
92
- const state = await startWorkflow({ cwd, host: "codex", mode: "feature", request: "Plan", backend: "local" });
93
- state.phase = "complete";
94
- state.status = "complete";
95
- state.gates.requirements = "approved";
96
- state.approvals.push({ gate: "requirements", createdAt: new Date().toISOString() });
97
- await saveState(state);
98
-
99
- const repeated = await approveWorkflow({ cwd, runId: state.runId, gate: "requirements" });
100
- assert.equal(repeated.phase, "complete");
101
- assert.equal(repeated.status, "complete");
102
- assert.equal(repeated.approvals.length, 1);
103
- });
104
-
105
- test("reject and answer enforce the current gate state", async () => {
106
- const cwd = await tempProject();
107
- const state = await startWorkflow({ cwd, host: "codex", mode: "feature", request: "Plan", backend: "local" });
108
- await assert.rejects(
109
- () => answerWorkflow({ cwd, runId: state.runId, answer: "Unsolicited answer" }),
110
- /awaiting_input/,
111
- );
112
- await assert.rejects(
113
- () => rejectWorkflow({ cwd, runId: state.runId, gate: "architecture", feedback: "Wrong gate" }),
114
- /current gate is requirements/,
115
- );
116
- });
117
- ```
118
-
119
- - [ ] **Step 2: Run the focused tests and verify RED**
120
-
121
- Run:
122
-
123
- ```bash
124
- node --import tsx --test --test-name-pattern="local backend|advances to execution|cannot be reopened|enforce the current gate" tests/core.test.ts
125
- ```
126
-
127
- Expected: failures showing local apply is accepted, `execution` is not a valid phase, completed approval reopens the run, and answer is accepted outside `awaiting_input`.
128
-
129
- - [ ] **Step 3: Add execution metadata and state migration**
130
-
131
- Add the phase and state type in `packages/core/src/types.ts`:
132
-
133
- ```ts
134
- export const PHASES = [
135
- "requirements",
136
- "architecture",
137
- "implementation",
138
- "execution",
139
- "testing",
140
- "acceptance",
141
- "complete",
142
- ] as const;
143
-
144
- export interface ExecutionWorkspace {
145
- path: string;
146
- baseCommit: string;
147
- }
148
-
149
- export interface RunState {
150
- // existing fields stay unchanged
151
- executionWorkspace?: ExecutionWorkspace;
152
- }
153
- ```
154
-
155
- Normalize the optional field in `loadState`:
156
-
157
- ```ts
158
- const executionWorkspace = parsed.executionWorkspace;
159
- return {
160
- ...parsed,
161
- executionMode: parsed.executionMode ?? "plan",
162
- executionWorkspace:
163
- executionWorkspace &&
164
- typeof executionWorkspace.path === "string" &&
165
- typeof executionWorkspace.baseCommit === "string"
166
- ? executionWorkspace
167
- : undefined,
168
- changedFiles: Array.isArray(parsed.changedFiles) ? parsed.changedFiles : [],
169
- implementationDiff: typeof parsed.implementationDiff === "string" ? parsed.implementationDiff : "",
170
- verification: Array.isArray(parsed.verification) ? parsed.verification : [],
171
- lintResults: Array.isArray(parsed.lintResults) ? parsed.lintResults : [],
172
- };
173
- ```
174
-
175
- - [ ] **Step 4: Implement legal and idempotent transitions**
176
-
177
- Change phase routing to use the run mode:
178
-
179
- ```ts
180
- export function nextPhaseAfterGate(state: RunState, gate: GateName): RunState["phase"] {
181
- switch (gate) {
182
- case "requirements":
183
- return "architecture";
184
- case "architecture":
185
- return "implementation";
186
- case "implementation":
187
- return state.executionMode === "apply" ? "execution" : "testing";
188
- case "testing":
189
- return "acceptance";
190
- }
191
- }
192
- ```
193
-
194
- Map `execution` to the review artifact and no public gate:
195
-
196
- ```ts
197
- execution: "implementation-review",
198
- ```
199
-
200
- Replace the permissive assertion with:
201
-
202
- ```ts
203
- function assertPendingCurrentGate(state: RunState, gate: GateName): void {
204
- const expected = gateForPhase(state.phase);
205
- if (expected !== gate) {
206
- throw new Error(expected
207
- ? `Cannot act on ${gate} while current gate is ${expected}`
208
- : `Cannot act on ${gate} while workflow phase is ${state.phase}`);
209
- }
210
- if (state.status !== "awaiting_approval" || state.gates[gate] !== "pending") {
211
- throw new Error(`Gate ${gate} is not pending approval`);
212
- }
213
- }
214
- ```
215
-
216
- In `approveWorkflow`, return immediately when `state.gates[gate] === "approved"`; otherwise call the strict assertion, record one approval, and use `nextPhaseAfterGate(state, gate)`. Apply the equivalent early return for an already rejected gate in `rejectWorkflow`.
217
-
218
- Guard answers before mutation:
219
-
220
- ```ts
221
- const gate = gateForPhase(state.phase);
222
- if (state.status !== "awaiting_input" || !gate || state.gates[gate] !== "rejected") {
223
- throw new Error("Workflow must be awaiting_input at a rejected current gate before recording an answer");
224
- }
225
- ```
226
-
227
- After resolving `backend` and `executionMode` in `startWorkflow`, reject false apply:
228
-
229
- ```ts
230
- if (executionMode === "apply" && backend === "local") {
231
- throw new Error("DevCrew apply mode requires a codex or claude backend; local is plan-only");
232
- }
233
- ```
234
-
235
- - [ ] **Step 5: Run core tests and verify GREEN**
236
-
237
- Run:
238
-
239
- ```bash
240
- node --import tsx --test tests/core.test.ts
241
- ```
242
-
243
- Expected: all core tests pass, including existing 0.1.1 state migration coverage.
244
-
245
- - [ ] **Step 6: Commit the core contract**
246
-
247
- ```bash
248
- git add packages/core/src/types.ts packages/core/src/workflow.ts packages/core/src/store.ts tests/core.test.ts
249
- git commit -m "fix: enforce workflow state invariants"
250
- ```
251
-
252
- ### Task 2: Make Stdio Validation And Queue Recovery Complete
253
-
254
- **Files:**
255
- - Modify: `packages/service/src/stdio.ts`
256
- - Test: `tests/stdio.test.ts`
257
-
258
- - [ ] **Step 1: Write failing JSON-RPC and queue recovery tests**
259
-
260
- ```ts
261
- test("stdio rejects valid JSON with an invalid request shape", async () => {
262
- for (const line of [
263
- "null",
264
- "[]",
265
- JSON.stringify({ jsonrpc: "1.0", id: 1, method: "tools/list" }),
266
- JSON.stringify({ jsonrpc: "2.0", id: 1 }),
267
- ]) {
268
- const output: unknown[] = [];
269
- const processLine = createStdioLineProcessor((message) => output.push(message));
270
- await processLine(line);
271
- assert.deepEqual((output[0] as { error: unknown }).error, {
272
- code: -32600,
273
- message: "Invalid Request",
274
- });
275
- }
276
- });
277
-
278
- test("stdio continues after a queued handler rejects", async () => {
279
- const calls: string[] = [];
280
- const processLine = createStdioLineProcessor(() => {}, async (request) => {
281
- calls.push(String(request.id));
282
- if (request.id === 1) throw new Error("first failed");
283
- });
284
-
285
- await assert.rejects(
286
- () => processLine(JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" })),
287
- /first failed/,
288
- );
289
- await processLine(JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list" }));
290
- assert.deepEqual(calls, ["1", "2"]);
291
- });
292
- ```
293
-
294
- - [ ] **Step 2: Run focused stdio tests and verify RED**
295
-
296
- ```bash
297
- node --import tsx --test --test-name-pattern="invalid request shape|continues after" tests/stdio.test.ts
298
- ```
299
-
300
- Expected: `null` throws before a response and the second queued request inherits the first rejection.
301
-
302
- - [ ] **Step 3: Validate request objects and recover the internal queue**
303
-
304
- Extend the request shape and add a type guard:
305
-
306
- ```ts
307
- interface JsonRpcRequest {
308
- jsonrpc: "2.0";
309
- id?: string | number | null;
310
- method: string;
311
- params?: Record<string, unknown>;
312
- }
313
-
314
- function isJsonRpcRequest(value: unknown): value is JsonRpcRequest {
315
- return typeof value === "object" &&
316
- value !== null &&
317
- !Array.isArray(value) &&
318
- (value as { jsonrpc?: unknown }).jsonrpc === "2.0" &&
319
- typeof (value as { method?: unknown }).method === "string";
320
- }
321
- ```
322
-
323
- Parse to `unknown`; after parsing, return this response when the guard fails:
324
-
325
- ```ts
326
- write({
327
- jsonrpc: "2.0",
328
- id: null,
329
- error: { code: -32600, message: "Invalid Request" },
330
- });
331
- return queue;
332
- ```
333
-
334
- Replace queue assignment with a caller-visible task plus an internally recovered queue:
335
-
336
- ```ts
337
- const task = queue.then(() => handler(request));
338
- queue = task.catch(() => undefined);
339
- return task;
340
- ```
341
-
342
- - [ ] **Step 4: Run stdio tests and verify GREEN**
343
-
344
- ```bash
345
- node --import tsx --test tests/stdio.test.ts
346
- ```
347
-
348
- Expected: parse errors, invalid requests, notifications, ordering, recovery, and version tests pass.
349
-
350
- - [ ] **Step 5: Commit stdio reliability**
351
-
352
- ```bash
353
- git add packages/service/src/stdio.ts tests/stdio.test.ts
354
- git commit -m "fix: recover stdio after invalid requests"
355
- ```
356
-
357
- ### Task 3: Build The Isolated Git Worktree Execution Layer
358
-
359
- **Files:**
360
- - Create: `packages/orchestrator/src/worktree.ts`
361
- - Modify: `packages/core/src/paths.ts`
362
- - Test: `tests/worktree.test.ts`
363
-
364
- - [ ] **Step 1: Write real-Git failing tests for capture and promotion**
365
-
366
- Create `tests/worktree.test.ts` with a real temporary repository helper and these behaviors:
367
-
368
- ```ts
369
- import { execFile } from "node:child_process";
370
- import { access, mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
371
- import { tmpdir } from "node:os";
372
- import { join } from "node:path";
373
- import { promisify } from "node:util";
374
- import test from "node:test";
375
- import assert from "node:assert/strict";
376
-
377
- import { startWorkflow, type RunState } from "../packages/core/src/index.js";
378
- import {
379
- captureExecutionChanges,
380
- ensureExecutionWorkspace,
381
- promoteExecutionChanges,
382
- } from "../packages/orchestrator/src/worktree.js";
383
-
384
- const execFileAsync = promisify(execFile);
385
-
386
- async function git(cwd: string, args: string[]): Promise<string> {
387
- return (await execFileAsync("git", args, { cwd })).stdout.trim();
388
- }
389
-
390
- async function pathExists(path: string): Promise<boolean> {
391
- try {
392
- await access(path);
393
- return true;
394
- } catch {
395
- return false;
396
- }
397
- }
398
-
399
- async function applyStateFixture(): Promise<RunState> {
400
- const cwd = await mkdtemp(join(tmpdir(), "devcrew-worktree-"));
401
- await execFileAsync("git", ["init"], { cwd });
402
- await git(cwd, ["config", "user.email", "devcrew@example.test"]);
403
- await git(cwd, ["config", "user.name", "DevCrew Test"]);
404
- await mkdir(join(cwd, "src"), { recursive: true });
405
- await writeFile(join(cwd, ".gitignore"), ".devcrew/\ndocs/devcrew/\n");
406
- await writeFile(join(cwd, "README.md"), "# Fixture\n");
407
- await writeFile(join(cwd, "rename-me.txt"), "rename me\n");
408
- await writeFile(join(cwd, "delete-me.txt"), "delete me\n");
409
- await git(cwd, ["add", "."]);
410
- await git(cwd, ["commit", "-m", "base"]);
411
-
412
- return startWorkflow({
413
- cwd,
414
- host: "codex",
415
- mode: "feature",
416
- request: "Apply fixture changes",
417
- backend: "codex",
418
- executionMode: "apply",
419
- });
420
- }
421
-
422
- test("captureExecutionChanges includes commits, staged, unstaged, renamed, deleted, binary, and untracked files", async () => {
423
- const state = await applyStateFixture();
424
- const workspace = await ensureExecutionWorkspace(state);
425
-
426
- await writeFile(join(workspace.path, "committed.txt"), "committed\n");
427
- await git(workspace.path, ["add", "committed.txt"]);
428
- await git(workspace.path, ["commit", "-m", "agent commit"]);
429
- await writeFile(join(workspace.path, "unstaged.txt"), "unstaged\n");
430
- await writeFile(join(workspace.path, "staged.txt"), "staged\n");
431
- await git(workspace.path, ["add", "staged.txt"]);
432
- await git(workspace.path, ["mv", "rename-me.txt", "renamed.txt"]);
433
- await rm(join(workspace.path, "delete-me.txt"));
434
- await writeFile(join(workspace.path, "binary.bin"), Buffer.from([0, 1, 2, 255]));
435
-
436
- const captured = await captureExecutionChanges(workspace);
437
- assert.match(captured.patch, /committed\.txt/);
438
- assert.match(captured.patch, /staged\.txt/);
439
- assert.match(captured.patch, /unstaged\.txt/);
440
- assert.match(captured.patch, /renamed\.txt/);
441
- assert.match(captured.patch, /delete-me\.txt/);
442
- assert.match(captured.patch, /GIT binary patch|binary\.bin/);
443
- assert.equal((await git(workspace.path, ["rev-parse", "HEAD"])).trim(), workspace.baseCommit);
444
- });
445
-
446
- test("promoteExecutionChanges applies exactly the reviewed patch", async () => {
447
- const state = await applyStateFixture();
448
- const workspace = await ensureExecutionWorkspace(state);
449
- await writeFile(join(workspace.path, "feature.ts"), "export const feature = true;\n");
450
- const captured = await captureExecutionChanges(workspace);
451
- state.executionWorkspace = workspace;
452
- state.implementationDiff = captured.patch;
453
-
454
- await promoteExecutionChanges(state);
455
-
456
- assert.equal(await readFile(join(state.cwd, "feature.ts"), "utf8"), "export const feature = true;\n");
457
- assert.equal(await pathExists(workspace.path), false);
458
- });
459
-
460
- test("promotion refuses a changed requester HEAD or dirty requester worktree", async () => {
461
- const state = await applyStateFixture();
462
- const workspace = await ensureExecutionWorkspace(state);
463
- await writeFile(join(workspace.path, "feature.ts"), "export const feature = true;\n");
464
- state.executionWorkspace = workspace;
465
- state.implementationDiff = (await captureExecutionChanges(workspace)).patch;
466
-
467
- await writeFile(join(state.cwd, "README.md"), "dirty\n");
468
- await assert.rejects(() => promoteExecutionChanges(state), /clean working tree/);
469
- assert.equal(await pathExists(workspace.path), true);
470
- });
471
-
472
- test("captureExecutionChanges rejects an empty apply result", async () => {
473
- const state = await applyStateFixture();
474
- const workspace = await ensureExecutionWorkspace(state);
475
- await assert.rejects(() => captureExecutionChanges(workspace), /produced no repository changes/);
476
- });
477
- ```
478
-
479
- - [ ] **Step 2: Run worktree tests and verify RED**
480
-
481
- ```bash
482
- node --import tsx --test tests/worktree.test.ts
483
- ```
484
-
485
- Expected: module-not-found failure for `packages/orchestrator/src/worktree.ts`.
486
-
487
- - [ ] **Step 3: Add the run-owned worktree path**
488
-
489
- In `packages/core/src/paths.ts`:
490
-
491
- ```ts
492
- export function executionWorktreePath(cwd: string, runId: string): string {
493
- return join(devcrewDir(cwd), "worktrees", runId);
494
- }
495
- ```
496
-
497
- - [ ] **Step 4: Implement the worktree module**
498
-
499
- Create these exported contracts in `packages/orchestrator/src/worktree.ts`:
500
-
501
- ```ts
502
- export interface CapturedExecutionChanges {
503
- changedFiles: string[];
504
- patch: string;
505
- }
506
-
507
- export async function ensureExecutionWorkspace(state: RunState): Promise<ExecutionWorkspace>;
508
- export async function captureExecutionChanges(workspace: ExecutionWorkspace): Promise<CapturedExecutionChanges>;
509
- export async function promoteExecutionChanges(state: RunState): Promise<void>;
510
- export async function executionCwd(state: RunState): Promise<string>;
511
- ```
512
-
513
- Use one non-shell Git runner so paths are never interpolated:
514
-
515
- ```ts
516
- async function runGit(args: string[], cwd: string, stdin?: string): Promise<string> {
517
- return new Promise((resolveResult, rejectResult) => {
518
- const child = spawn("git", args, { cwd, stdio: ["pipe", "pipe", "pipe"] });
519
- let stdout = "";
520
- let stderr = "";
521
- child.stdout.on("data", (chunk: Buffer) => { stdout += chunk.toString("utf8"); });
522
- child.stderr.on("data", (chunk: Buffer) => { stderr += chunk.toString("utf8"); });
523
- child.on("error", rejectResult);
524
- child.on("close", (code) => {
525
- if (code === 0) resolveResult(stdout);
526
- else rejectResult(new Error(`git ${args[0]} failed: ${(stderr || stdout).trim()}`));
527
- });
528
- child.stdin.end(stdin);
529
- });
530
- }
531
- ```
532
-
533
- `ensureExecutionWorkspace` must return existing persisted metadata when its path is accessible. Otherwise it asserts requester cleanliness, resolves `HEAD`, creates the parent directory, runs:
534
-
535
- ```ts
536
- await runGit(["worktree", "add", "--detach", path, baseCommit], state.cwd);
537
- ```
538
-
539
- `captureExecutionChanges` must:
540
-
541
- 1. Compare worktree `HEAD` with `baseCommit` and run `git reset --mixed <baseCommit>` when the role committed.
542
- 2. Read untracked paths with `git ls-files --others --exclude-standard -z` and run `git add -N -- <paths>` inside the isolated worktree.
543
- 3. Read changed names with `git diff --name-status -z <baseCommit> --` and parse status/path tuples, including two paths for `R*` and `C*` statuses.
544
- 4. Capture `git diff --binary --no-ext-diff <baseCommit> --`.
545
- 5. Throw `DevCrew apply implementer produced no repository changes` when the patch is empty.
546
-
547
- `promoteExecutionChanges` must:
548
-
549
- 1. Assert requester cleanliness and unchanged `HEAD`.
550
- 2. Write `implementation.patch` under `runDir(state.cwd, state.runId)`.
551
- 3. Run `git apply --check --binary <patch-path>` then `git apply --binary <patch-path>` in the requester repository.
552
- 4. Mark promoted untracked files intent-to-add, capture the requester patch against `baseCommit`, then run `git reset --mixed <baseCommit>` to leave all promoted changes unstaged.
553
- 5. If the captured patch differs from `state.implementationDiff`, reverse the patch and throw.
554
- 6. Run `git worktree remove --force <workspace.path>` and `git worktree prune` only after successful verification.
555
-
556
- - [ ] **Step 5: Run worktree tests and verify GREEN**
557
-
558
- ```bash
559
- node --import tsx --test tests/worktree.test.ts
560
- ```
561
-
562
- Expected: all real-Git capture, promotion, precondition, and empty-output tests pass.
563
-
564
- - [ ] **Step 6: Commit worktree isolation**
565
-
566
- ```bash
567
- git add packages/core/src/paths.ts packages/orchestrator/src/worktree.ts tests/worktree.test.ts
568
- git commit -m "feat: isolate apply changes in git worktrees"
569
- ```
570
-
571
- ### Task 4: Route Planning, Execution, Testing, And Promotion
572
-
573
- **Files:**
574
- - Modify: `packages/adapters/src/index.ts`
575
- - Modify: `packages/orchestrator/src/index.ts`
576
- - Modify: `packages/service/src/tools.ts`
577
- - Test: `tests/adapters-sdk.test.ts`
578
- - Test: `tests/orchestrator.test.ts`
579
- - Test: `tests/service.test.ts`
580
-
581
- - [ ] **Step 1: Write failing orchestration sequence tests**
582
-
583
- Add tests proving the first writable implementer call occurs only after implementation approval and uses the isolated path:
584
-
585
- ```ts
586
- function validRoleResult(input: RoleRunInput): RoleResult {
587
- return {
588
- role: input.role,
589
- backend: input.backend,
590
- summary: `${input.role} completed`,
591
- markdown: `# ${input.role}\n\nCompleted ${input.phase}.\n`,
592
- usedFallback: false,
593
- };
594
- }
595
-
596
- test("apply mode plans read-only before executing in a worktree", async () => {
597
- const cwd = await tempProject();
598
- await initGitRepo(cwd);
599
- const calls: RoleRunInput[] = [];
600
- const runner = async (input: RoleRunInput): Promise<RoleResult> => {
601
- calls.push(input);
602
- if (input.phase === "execution") {
603
- await writeFile(join(input.cwd, "generated.ts"), "export const generated = true;\n");
604
- }
605
- return validRoleResult(input);
606
- };
607
-
608
- const started = await startOrchestratedWorkflow({
609
- cwd,
610
- host: "codex",
611
- mode: "feature",
612
- request: "Add generated code",
613
- backend: "codex",
614
- executionMode: "apply",
615
- }, runner);
616
- await approveWorkflow({ cwd, runId: started.runId, gate: "requirements" });
617
- await continueOrchestratedWorkflow({ cwd, runId: started.runId }, runner);
618
- await approveWorkflow({ cwd, runId: started.runId, gate: "architecture" });
619
- const planned = await continueOrchestratedWorkflow({ cwd, runId: started.runId }, runner);
620
-
621
- assert.equal(calls.at(-1)?.phase, "implementation");
622
- assert.equal(calls.at(-1)?.executionMode, "plan");
623
- assert.equal(calls.at(-1)?.cwd, cwd);
624
- assert.equal(await pathExists(join(cwd, "generated.ts")), false);
625
-
626
- await approveWorkflow({ cwd, runId: started.runId, gate: "implementation" });
627
- const executed = await continueOrchestratedWorkflow({ cwd, runId: started.runId }, runner);
628
- assert.equal(calls.at(-1)?.phase, "execution");
629
- assert.equal(calls.at(-1)?.executionMode, "apply");
630
- assert.notEqual(calls.at(-1)?.cwd, cwd);
631
- assert.equal(executed.phase, "testing");
632
- assert.equal(executed.status, "ready");
633
- assert.match(executed.implementationDiff, /generated\.ts/);
634
- assert.equal(await pathExists(join(cwd, "generated.ts")), false);
635
- });
636
- ```
637
-
638
- Add service/orchestrator tests that testing approval promotes the patch once, duplicate approval is a no-op, and rejected testing plus answer returns to `execution/ready` without touching the requester repository.
639
-
640
- - [ ] **Step 2: Run orchestration tests and verify RED**
641
-
642
- ```bash
643
- node --import tsx --test --test-name-pattern="plans read-only|promotes the patch|returns to execution" tests/orchestrator.test.ts tests/service.test.ts
644
- ```
645
-
646
- Expected: implementation writes before its gate, there is no execution routing, and approval does not promote a worktree patch.
647
-
648
- - [ ] **Step 3: Make adapter permissions phase-aware**
649
-
650
- Update phase titles:
651
-
652
- ```ts
653
- execution: "Implementation Review",
654
- ```
655
-
656
- Restrict apply capability to real execution and testing:
657
-
658
- ```ts
659
- function roleCanApply(input: RoleRunInput): boolean {
660
- return input.executionMode === "apply" &&
661
- (input.phase === "execution" || input.phase === "testing");
662
- }
663
- ```
664
-
665
- The orchestrator passes `executionMode: "plan"` for the implementation-plan phase even when `state.executionMode` is apply.
666
-
667
- - [ ] **Step 4: Refactor current-phase orchestration**
668
-
669
- Add `execution: "implementer"` to `roleForPhase`. Handle the nongated execution phase before the existing gate branch:
670
-
671
- ```ts
672
- if (state.phase === "execution") {
673
- const workspace = await ensureExecutionWorkspace(state);
674
- state.executionWorkspace = workspace;
675
- await saveState(state);
676
-
677
- const result = await runner({
678
- backend: state.backend,
679
- role: "implementer",
680
- phase: "execution",
681
- request: state.request,
682
- mode: state.mode,
683
- executionMode: "apply",
684
- cwd: workspace.path,
685
- standards: state.standards.combined,
686
- artifactPath: artifactPath(state.cwd, state.runId, "implementation-review"),
687
- answers: state.answers.map((entry) => entry.answer),
688
- feedback: state.feedback.map((entry) => `${entry.gate}: ${entry.message}`),
689
- priorArtifacts: await readPriorArtifacts(state),
690
- });
691
- const captured = await captureExecutionChanges(workspace);
692
- state.changedFiles = captured.changedFiles;
693
- state.implementationDiff = captured.patch;
694
- state.lintResults = await runConfiguredLint(state, workspace.path);
695
- state.roles.push(result);
696
- state.artifacts["implementation-review"] = await writeMarkdownArtifact(
697
- state,
698
- "implementation-review",
699
- renderArtifact("implementation-review", state),
700
- );
701
- state.phase = "testing";
702
- state.status = "ready";
703
- return saveState(state);
704
- }
705
- ```
706
-
707
- For the normal implementation phase, pass `executionMode: "plan"` and the requester `cwd`. For apply-mode testing, use `executionWorkspace.path` for the tester and for `runConfiguredVerification`. After tester/verification completion, call `captureExecutionChanges` again and rewrite `implementation-review.md` so testing cannot silently alter the reviewed patch.
708
-
709
- Change `runConfiguredLint` and `runConfiguredVerification` to accept an explicit command cwd while still reading config from `state.cwd`.
710
-
711
- - [ ] **Step 5: Add orchestrated approval and revision routing**
712
-
713
- Export:
714
-
715
- ```ts
716
- export async function approveOrchestratedWorkflow(input: ApproveWorkflowInput): Promise<RunState> {
717
- const before = await getWorkflowStatus(input);
718
- if (
719
- input.gate === "testing" &&
720
- before.executionMode === "apply" &&
721
- before.gates.testing !== "approved"
722
- ) {
723
- await promoteExecutionChanges(before);
724
- }
725
- const state = await approveWorkflow(input);
726
- if (input.gate === "testing" && state.executionWorkspace) {
727
- state.executionWorkspace = undefined;
728
- return saveState(state);
729
- }
730
- return state;
731
- }
732
- ```
733
-
734
- In `answerOrchestratedWorkflow`, inspect the state before answering. When it is an apply-mode rejected testing gate, record the answer, then set:
735
-
736
- ```ts
737
- state.phase = "execution";
738
- state.status = "ready";
739
- state.gates.testing = "not_started";
740
- return saveState(state);
741
- ```
742
-
743
- Update `packages/service/src/tools.ts` to call `approveOrchestratedWorkflow` instead of the core `approveWorkflow`.
744
-
745
- - [ ] **Step 6: Remove obsolete in-place rollback behavior**
746
-
747
- Delete the in-place `revertChangedFiles`, baseline-diff, and testing-rejection rollback branches from `packages/orchestrator/src/index.ts`. Replace their unit tests with the worktree isolation and revision tests. Do not leave two apply ownership models active.
748
-
749
- - [ ] **Step 7: Run adapter, orchestrator, and service tests and verify GREEN**
750
-
751
- ```bash
752
- node --import tsx --test tests/adapters.test.ts tests/adapters-sdk.test.ts tests/orchestrator.test.ts tests/service.test.ts tests/worktree.test.ts
753
- ```
754
-
755
- Expected: planning is read-only, execution/testing use the worktree, rejection revises isolated changes, and testing approval promotes once.
756
-
757
- - [ ] **Step 8: Commit orchestration integration**
758
-
759
- ```bash
760
- git add packages/adapters/src/index.ts packages/orchestrator/src/index.ts packages/service/src/tools.ts tests/adapters-sdk.test.ts tests/orchestrator.test.ts tests/service.test.ts
761
- git commit -m "feat: gate and promote isolated apply execution"
762
- ```
763
-
764
- ### Task 5: Synchronize Version And Checked-In Plugin Generation
765
-
766
- **Files:**
767
- - Modify: `package.json`
768
- - Modify: `package-lock.json`
769
- - Modify: `packages/core/src/version.ts`
770
- - Modify: `plugins/devcrew-codex/.codex-plugin/plugin.json`
771
- - Modify: `plugins/devcrew-codex/.mcp.json`
772
- - Modify: `plugins/devcrew-codex/agents/*.toml`
773
- - Modify: `plugins/devcrew-codex/assets/logo.png`
774
- - Modify: `scripts/smoke-codex-plugin.mjs`
775
- - Modify: `tests/core.test.ts`
776
- - Modify: `tests/plugins.test.ts`
777
- - Test: `tests/package.test.ts`
778
-
779
- - [ ] **Step 1: Write a failing checked-in plugin drift test**
780
-
781
- Generate a fresh plugin and compare the tracked bundle:
782
-
783
- ```ts
784
- test("checked-in Codex plugin matches the shared generator", async () => {
785
- const root = await tempProject();
786
- const generated = await generateCodexPlugin(root);
787
- const checkedIn = join(process.cwd(), "plugins", "devcrew-codex");
788
- const relativeFiles = [
789
- ".codex-plugin/plugin.json",
790
- ".mcp.json",
791
- "skills/devcrew/SKILL.md",
792
- "agents/pm.toml",
793
- "agents/architect.toml",
794
- "agents/implementer.toml",
795
- "agents/tester.toml",
796
- "assets/composer-icon.png",
797
- "assets/logo.png",
798
- ];
799
-
800
- for (const file of relativeFiles) {
801
- assert.deepEqual(
802
- await readFile(join(checkedIn, file)),
803
- await readFile(join(generated.path, file)),
804
- `${file} drifted from generateCodexPlugin`,
805
- );
806
- }
807
- });
808
- ```
809
-
810
- - [ ] **Step 2: Run the plugin drift test and verify RED**
811
-
812
- ```bash
813
- node --import tsx --test --test-name-pattern="matches the shared generator" tests/plugins.test.ts
814
- ```
815
-
816
- Expected: manifest version, role TOML, and logo differences are reported.
817
-
818
- - [ ] **Step 3: Bump all shared version sources to 0.1.2**
819
-
820
- Set:
821
-
822
- ```ts
823
- export const DEVCREW_VERSION = "0.1.2";
824
- ```
825
-
826
- Update root and lockfile package versions first:
827
-
828
- ```bash
829
- npm version 0.1.2 --no-git-tag-version
830
- ```
831
-
832
- Update test assertions and smoke client metadata to `0.1.2`. Regenerate the checked-in Codex plugin through `generateCodexPlugin` so manifest, MCP specifier, role templates, and binary assets come from one source:
833
-
834
- ```bash
835
- node --import tsx --input-type=module -e 'import { generateCodexPlugin } from "./packages/plugins/src/index.ts"; await generateCodexPlugin(process.cwd());'
836
- ```
837
-
838
- The generator command performs the asset synchronization; do not edit or copy plugin files separately afterward.
839
-
840
- - [ ] **Step 4: Run package and plugin tests and verify GREEN**
841
-
842
- ```bash
843
- node --import tsx --test tests/core.test.ts tests/package.test.ts tests/plugins.test.ts
844
- ```
845
-
846
- Expected: package, shared constant, generated plugins, checked-in plugin, and npm runtime lock all report `0.1.2` with no drift.
847
-
848
- - [ ] **Step 5: Commit version synchronization**
849
-
850
- ```bash
851
- git add package.json package-lock.json packages/core/src/version.ts plugins/devcrew-codex scripts/smoke-codex-plugin.mjs tests/core.test.ts tests/plugins.test.ts tests/package.test.ts
852
- git commit -m "chore: prepare DevCrew 0.1.2 plugin release"
853
- ```
854
-
855
- ### Task 6: Update Workflow Documentation And Run Full Verification
856
-
857
- **Files:**
858
- - Modify: `README.md`
859
- - Modify: `README.zh-CN.md`
860
- - Modify: `docs/workflow.md`
861
- - Modify: `docs/codex.md`
862
-
863
- - [ ] **Step 1: Update user-facing flow documentation**
864
-
865
- Document this exact apply sequence in English and Chinese:
866
-
867
- ```text
868
- requirements approval
869
- -> architecture approval
870
- -> implementation plan approval
871
- -> isolated execution
872
- -> isolated testing
873
- -> testing approval
874
- -> patch promotion to requester repository
875
- ```
876
-
877
- State that apply requires Git, a clean requester worktree at execution and promotion, a real host SDK backend, and one additional `devcrew_continue` call for `execution`. Explain that rejection feedback after testing returns the isolated run to execution without changing the requester repository.
878
-
879
- Update every npm and plugin example to `@shenlee/devcrew@0.1.2`, but mark marketplace smoke as a post-publication check.
880
-
881
- - [ ] **Step 2: Run the full validation suite**
882
-
883
- ```bash
884
- npm run validate
885
- ```
886
-
887
- Expected: TypeScript build succeeds and every test passes with zero failures.
888
-
889
- - [ ] **Step 3: Inspect package contents with an isolated writable npm cache**
890
-
891
- ```bash
892
- env npm_config_cache=/tmp/devcrew-npm-cache npm pack --dry-run --json
893
- ```
894
-
895
- Expected: package name `@shenlee/devcrew`, version `0.1.2`, CLI dist files, docs, and `plugins/devcrew-codex` are present.
896
-
897
- - [ ] **Step 4: Verify repository changes and generated plugin consistency**
898
-
899
- ```bash
900
- git diff --check
901
- git status --short
902
- ```
903
-
904
- Expected: no whitespace errors; only intentional P0 implementation and documentation files are modified or committed.
905
-
906
- - [ ] **Step 5: Commit documentation**
907
-
908
- ```bash
909
- git add README.md README.zh-CN.md docs/workflow.md docs/codex.md
910
- git commit -m "docs: describe isolated apply execution"
911
- ```
912
-
913
- - [ ] **Step 6: Stop before public marketplace publication**
914
-
915
- Do not push the marketplace commit while `@shenlee/devcrew@0.1.2` is unavailable. Ask the requester to run:
916
-
917
- ```bash
918
- npm publish --access public
919
- ```
920
-
921
- After registry propagation, verify:
922
-
923
- ```bash
924
- npm view @shenlee/devcrew version --registry https://registry.npmjs.org/
925
- npm run smoke:codex-plugin
926
- ```
927
-
928
- Expected: registry reports `0.1.2`, then the isolated Codex marketplace plan smoke passes. A real apply smoke remains manual because it consumes host-agent execution and modifies a fixture repository.
929
-
930
- ---
931
-
932
- ## Plan Self-Review
933
-
934
- - Every P0 item in `docs/superpowers/specs/2026-07-10-p0-foundation-repair-design.md` maps to a task above.
935
- - Public MCP tool and gate names remain unchanged.
936
- - The only new persisted field is optional, so 0.1.1 states remain readable.
937
- - Worktree capture and promotion use argument arrays, NUL-delimited paths, binary patches, clean requester preconditions, and no shell interpolation.
938
- - The release plan prevents the marketplace from referencing npm `0.1.2` before publication.
939
- - P1 semantic review, structured role output, configurable workflows, and Claude distribution remain out of scope.