pi-crew 0.8.13 → 0.9.0
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/CHANGELOG.md +296 -0
- package/README.md +118 -2
- package/docs/FEATURE_INTAKE.md +1 -1
- package/docs/HARNESS.md +20 -19
- package/docs/PROJECT_REVIEW.md +132 -133
- package/docs/PROJECT_REVIEW_FIXES.md +130 -131
- package/docs/actions-reference.md +127 -121
- package/docs/architecture.md +1 -1
- package/docs/code-review-2026-05-11.md +134 -134
- package/docs/commands-reference.md +108 -106
- package/docs/comparison-pi-subagents-vs-pi-crew.md +105 -105
- package/docs/deep-review-report.md +1 -1
- package/docs/dynamic-workflows.md +90 -0
- package/docs/fixes/BATCH_A_H1_H2.md +17 -17
- package/docs/fixes/bug-007-async-notifier-stale-ctx.md +23 -23
- package/docs/followup-plan-2026-05-12.md +135 -135
- package/docs/followup-review-2026-05-12.md +86 -86
- package/docs/followup-review-round3-2026-05-12.md +123 -123
- package/docs/goals.md +59 -0
- package/docs/implementation-plan-top3.md +4 -4
- package/docs/issue-29-analysis.md +2 -2
- package/docs/oh-my-pi-research.md +154 -154
- package/docs/optimization-plan.md +2 -0
- package/docs/perf/baseline-2026-05.md +9 -9
- package/docs/perf/final-report-2026-05.md +2 -2
- package/docs/perf/sprint-1-report.md +2 -2
- package/docs/perf/sprint-2-report.md +1 -1
- package/docs/perf/upgrade-plan-2026-05.md +72 -72
- package/docs/pi-crew-bugs.md +230 -230
- package/docs/pi-crew-investigation-report.md +102 -102
- package/docs/pi-crew-test-round5.md +4 -4
- package/docs/runtime-analysis-child-vs-live.md +57 -57
- package/docs/runtime-migration-in-process-analysis.md +97 -97
- package/install.mjs +3 -2
- package/package.json +2 -4
- package/skills/orchestration/SKILL.md +11 -11
- package/src/agents/agent-config.ts +4 -0
- package/src/config/config.ts +39 -0
- package/src/config/types.ts +11 -0
- package/src/extension/action-suggestions.ts +2 -1
- package/src/extension/async-notifier.ts +10 -0
- package/src/extension/help.ts +14 -0
- package/src/extension/project-init.ts +7 -20
- package/src/extension/registration/commands.ts +27 -0
- package/src/extension/team-tool/destructive-gate.ts +1 -1
- package/src/extension/team-tool/goal-wrap.ts +288 -0
- package/src/extension/team-tool/goal.ts +405 -0
- package/src/extension/team-tool/run.ts +103 -4
- package/src/extension/team-tool/workflow-manage.ts +194 -0
- package/src/extension/team-tool.ts +20 -0
- package/src/hooks/types.ts +3 -1
- package/src/runtime/async-runner.ts +24 -2
- package/src/runtime/background-runner.ts +68 -19
- package/src/runtime/child-pi.ts +6 -1
- package/src/runtime/completion-guard.ts +1 -1
- package/src/runtime/dynamic-workflow-context.ts +450 -0
- package/src/runtime/dynamic-workflow-runner.ts +180 -0
- package/src/runtime/global-worker-cap.ts +96 -0
- package/src/runtime/goal-evaluator.ts +294 -0
- package/src/runtime/goal-loop-runner.ts +612 -0
- package/src/runtime/goal-state-store.ts +209 -0
- package/src/runtime/pi-args.ts +10 -2
- package/src/runtime/result-extractor.ts +32 -0
- package/src/runtime/team-runner.ts +11 -1
- package/src/runtime/verification-gates.ts +85 -5
- package/src/runtime/verification-integrity.ts +110 -0
- package/src/runtime/verification-worktree.ts +136 -0
- package/src/runtime/workspace-lock.ts +448 -0
- package/src/schema/config-schema.ts +26 -0
- package/src/schema/team-tool-schema.ts +39 -4
- package/src/state/atomic-write.ts +9 -0
- package/src/state/contracts.ts +14 -0
- package/src/state/crew-init.ts +18 -5
- package/src/state/event-log.ts +7 -1
- package/src/state/state-store.ts +2 -0
- package/src/state/types.ts +82 -0
- package/src/state/worker-atomic-writer.ts +176 -0
- package/src/utils/redaction.ts +104 -24
- package/src/workflows/discover-workflows.ts +25 -1
- package/src/workflows/workflow-config.ts +13 -0
- package/teams/parallel-research.team.md +1 -1
- package/workflows/examples/hello.dwf.ts +24 -0
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
# Code Review Findings — pi-crew (2026-05-11)
|
|
2
2
|
|
|
3
3
|
Reviewer: Droid (Factory)
|
|
4
|
-
Scope:
|
|
5
|
-
|
|
4
|
+
Scope: the entire `pi-crew/` directory (src + schema + worktree + state + extension), read-only.
|
|
5
|
+
Method: cross-referenced code against `AGENTS.md` (project + workspace), reviewed security/concurrency/cleanup per OWASP + best practices.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## Severity Summary
|
|
10
10
|
|
|
11
|
-
| ID | Severity |
|
|
11
|
+
| ID | Severity | Area | Title |
|
|
12
12
|
|---|---|---|---|
|
|
13
|
-
| BUG-001 | **High** | Schema / Tool dispatch | `action: "retry"`
|
|
14
|
-
| BUG-002 | **High** | Artifact integrity | `contentHash`
|
|
15
|
-
| BUG-003 | Medium | AGENTS.md compliance | 12
|
|
16
|
-
| BUG-004 | Medium | Concurrency | `withRunLockSync`
|
|
17
|
-
| BUG-005 | Medium | Worktree lifecycle | `git worktree add -b <branch>`
|
|
18
|
-
| BUG-006 | Low/Med | Worktree | `linkNodeModulesIfPresent`
|
|
19
|
-
| BUG-007 | Low | Worktree setup hook |
|
|
20
|
-
| NIT-001 | Low | API hygiene | `__test__renameWithRetry`
|
|
21
|
-
| NIT-002 | Low | Code style | Empty-string argv flag
|
|
22
|
-
| NIT-003 | Low | Immutability | `executedConfig.runtime`
|
|
23
|
-
| NIT-004 | Low | Redaction |
|
|
13
|
+
| BUG-001 | **High** | Schema / Tool dispatch | `action: "retry"` rejected by schema but has a handler |
|
|
14
|
+
| BUG-002 | **High** | Artifact integrity | `contentHash` does not match the bytes written to disk |
|
|
15
|
+
| BUG-003 | Medium | AGENTS.md compliance | 12 `await import(...)` sites violate the "no dynamic inline imports" rule |
|
|
16
|
+
| BUG-004 | Medium | Concurrency | `withRunLockSync` and `withRunLock` handle stale locks differently |
|
|
17
|
+
| BUG-005 | Medium | Worktree lifecycle | `git worktree add -b <branch>` fails when the branch already exists from a previous run |
|
|
18
|
+
| BUG-006 | Low/Med | Worktree | `linkNodeModulesIfPresent` does not verify the source is a directory |
|
|
19
|
+
| BUG-007 | Low | Worktree setup hook | Errored / non-JSON hook output is swallowed entirely with no log |
|
|
20
|
+
| NIT-001 | Low | API hygiene | `__test__renameWithRetry` is called from a production code path |
|
|
21
|
+
| NIT-002 | Low | Code style | Empty-string argv flag in `git worktree remove` |
|
|
22
|
+
| NIT-003 | Low | Immutability | `executedConfig.runtime` is mutated on resume |
|
|
23
|
+
| NIT-004 | Low | Redaction | Need to verify the transcript on disk is always redacted |
|
|
24
24
|
|
|
25
25
|
---
|
|
26
26
|
|
|
27
|
-
## BUG-001 — `action: "retry"`
|
|
27
|
+
## BUG-001 — `action: "retry"` rejected by schema but has a handler
|
|
28
28
|
|
|
29
29
|
**Severity:** High
|
|
30
30
|
**Files:**
|
|
@@ -33,9 +33,9 @@ Phương pháp: đối chiếu code với `AGENTS.md` (project + workspace), ki
|
|
|
33
33
|
- `src/extension/team-tool.ts:264` (dispatch)
|
|
34
34
|
- `src/extension/team-tool/cancel.ts` (`handleRetry`)
|
|
35
35
|
|
|
36
|
-
###
|
|
36
|
+
### Description
|
|
37
37
|
|
|
38
|
-
TypeBox schema `TeamToolParams`
|
|
38
|
+
The TypeBox schema `TeamToolParams` defines `action` as a `Type.Union` of `Type.Literal` values. The literal list **does not include** `"retry"`:
|
|
39
39
|
|
|
40
40
|
```ts
|
|
41
41
|
// src/schema/team-tool-schema.ts:18-49
|
|
@@ -47,43 +47,43 @@ action: Type.Optional(Type.Union([
|
|
|
47
47
|
Type.Literal("list"),
|
|
48
48
|
Type.Literal("get"),
|
|
49
49
|
Type.Literal("cancel"),
|
|
50
|
-
// ...
|
|
50
|
+
// ... there is NO Type.Literal("retry") here
|
|
51
51
|
Type.Literal("resume"),
|
|
52
52
|
Type.Literal("respond"),
|
|
53
53
|
...
|
|
54
54
|
])),
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
But the TypeScript interface **does include** `"retry"`:
|
|
58
58
|
|
|
59
59
|
```ts
|
|
60
60
|
// src/schema/team-tool-schema.ts:95
|
|
61
61
|
action?: "run" | "parallel" | "plan" | "status" | "list" | "get" | "cancel" | "retry" | "resume" | ...;
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
And `handleTeamTool` dispatches it:
|
|
65
65
|
|
|
66
66
|
```ts
|
|
67
67
|
// src/extension/team-tool.ts:264
|
|
68
68
|
case "retry": return handleRetry(params, ctx);
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
###
|
|
71
|
+
### Consequences
|
|
72
72
|
|
|
73
|
-
-
|
|
74
|
-
- TS interface
|
|
73
|
+
- When pi-coding-agent validates tool params via the TypeBox schema (the usual way to gate input from the LLM), a call like `team {action: "retry"}` is **rejected at the validation layer** and never reaches `handleRetry`.
|
|
74
|
+
- The TS interface and TypeBox schema are out of sync; from the tool runtime's perspective, the `handleRetry` code path is **dead code**.
|
|
75
75
|
|
|
76
|
-
###
|
|
76
|
+
### How to reproduce
|
|
77
77
|
|
|
78
78
|
```bash
|
|
79
|
-
#
|
|
79
|
+
# From the pi REPL or via the tool API:
|
|
80
80
|
team(action="retry", runId="<id>")
|
|
81
81
|
# → schema validation error "must be equal to one of the allowed values"
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
###
|
|
84
|
+
### Suggested fix
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
Add the literal to the union and sync the tests:
|
|
87
87
|
|
|
88
88
|
```ts
|
|
89
89
|
// src/schema/team-tool-schema.ts
|
|
@@ -91,13 +91,13 @@ action: Type.Optional(Type.Union([
|
|
|
91
91
|
Type.Literal("run"),
|
|
92
92
|
...
|
|
93
93
|
Type.Literal("cancel"),
|
|
94
|
-
Type.Literal("retry"), // ←
|
|
94
|
+
Type.Literal("retry"), // ← add this line
|
|
95
95
|
Type.Literal("resume"),
|
|
96
96
|
...
|
|
97
97
|
])),
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
And add a test in `test/unit/team-tool-schema.test.ts`:
|
|
101
101
|
|
|
102
102
|
```ts
|
|
103
103
|
test("schema accepts action: retry", () => {
|
|
@@ -108,12 +108,12 @@ test("schema accepts action: retry", () => {
|
|
|
108
108
|
|
|
109
109
|
---
|
|
110
110
|
|
|
111
|
-
## BUG-002 — `writeArtifact`
|
|
111
|
+
## BUG-002 — `writeArtifact` writes redacted content but hashes the original bytes
|
|
112
112
|
|
|
113
113
|
**Severity:** High
|
|
114
114
|
**File:** `src/state/artifact-store.ts:106-129`
|
|
115
115
|
|
|
116
|
-
###
|
|
116
|
+
### Description
|
|
117
117
|
|
|
118
118
|
```ts
|
|
119
119
|
// src/state/artifact-store.ts:117-121
|
|
@@ -126,43 +126,43 @@ return {
|
|
|
126
126
|
kind: options.kind,
|
|
127
127
|
path: filePath,
|
|
128
128
|
...
|
|
129
|
-
sizeBytes: stats.size, // ← size
|
|
130
|
-
contentHash, // ← hash
|
|
129
|
+
sizeBytes: stats.size, // ← size of the redacted bytes
|
|
130
|
+
contentHash, // ← hash of the original, pre-redaction bytes
|
|
131
131
|
...
|
|
132
132
|
};
|
|
133
133
|
```
|
|
134
134
|
|
|
135
|
-
`contentHash`
|
|
135
|
+
`contentHash` is computed on `options.content` (pre-redaction) while the file on disk is `redactSecretString(options.content)`. `sizeBytes` is taken from `fs.statSync(filePath)` → it is the size of the redacted bytes.
|
|
136
136
|
|
|
137
|
-
###
|
|
137
|
+
### Consequences
|
|
138
138
|
|
|
139
|
-
-
|
|
140
|
-
- `sizeBytes`
|
|
141
|
-
-
|
|
139
|
+
- Any consumer that "verifies integrity" by re-hashing the file path will always get a digest **different** from `contentHash` whenever the original content contains a secret pattern.
|
|
140
|
+
- `sizeBytes` and `contentHash` are inconsistent with each other (size is post-redaction, hash is pre-redaction).
|
|
141
|
+
- The comment "Compute hash on original content for integrity verification" states the **rationale**, but the contract is still wrong: an integrity check compares the hash against the file on disk, not against an in-memory value.
|
|
142
142
|
|
|
143
|
-
###
|
|
143
|
+
### Two ways to fix
|
|
144
144
|
|
|
145
|
-
**Option A — Hash post-redaction (
|
|
145
|
+
**Option A — Hash post-redaction (recommended):**
|
|
146
146
|
```ts
|
|
147
147
|
const content = redactSecretString(options.content);
|
|
148
148
|
atomicWriteFile(filePath, content);
|
|
149
149
|
const contentHash = hashContent(content);
|
|
150
150
|
const stats = fs.statSync(filePath);
|
|
151
151
|
```
|
|
152
|
-
|
|
152
|
+
Guarantees `contentHash === sha256(fs.readFileSync(filePath))`. You lose the ability to "trace back to the pre-redaction source" — but that is the safe behavior for the artifact store.
|
|
153
153
|
|
|
154
|
-
**Option B —
|
|
154
|
+
**Option B — Store both fields if needed:**
|
|
155
155
|
```ts
|
|
156
156
|
return {
|
|
157
157
|
...,
|
|
158
158
|
contentHash, // pre-redaction (source-of-truth)
|
|
159
|
-
storedContentHash: hashContent(content), // post-redaction (
|
|
159
|
+
storedContentHash: hashContent(content), // post-redaction (matches the file)
|
|
160
160
|
sizeBytes: stats.size,
|
|
161
161
|
};
|
|
162
162
|
```
|
|
163
|
-
|
|
163
|
+
Then update `ArtifactDescriptor` in `src/state/types.ts:8-16` and every consumer.
|
|
164
164
|
|
|
165
|
-
###
|
|
165
|
+
### Test to add
|
|
166
166
|
|
|
167
167
|
```ts
|
|
168
168
|
test("writeArtifact: contentHash matches bytes on disk", () => {
|
|
@@ -179,14 +179,14 @@ test("writeArtifact: contentHash matches bytes on disk", () => {
|
|
|
179
179
|
|
|
180
180
|
---
|
|
181
181
|
|
|
182
|
-
## BUG-003 — 12
|
|
182
|
+
## BUG-003 — 12 `await import(...)` sites violate the "Avoid dynamic inline imports" rule
|
|
183
183
|
|
|
184
|
-
**Severity:** Medium (rule violation,
|
|
185
|
-
**
|
|
184
|
+
**Severity:** Medium (rule violation, not a runtime bug)
|
|
185
|
+
**Source rule:** `pi-crew/AGENTS.md` — "Avoid dynamic inline imports."
|
|
186
186
|
|
|
187
|
-
###
|
|
187
|
+
### List of violations
|
|
188
188
|
|
|
189
|
-
| File | Line | Module
|
|
189
|
+
| File | Line | Module lazily imported |
|
|
190
190
|
|---|---|---|
|
|
191
191
|
| `src/extension/team-tool.ts` | 35 | `../runtime/team-runner.ts` |
|
|
192
192
|
| `src/extension/team-tool/run.ts` | 18 | `../../runtime/team-runner.ts` |
|
|
@@ -201,35 +201,35 @@ test("writeArtifact: contentHash matches bytes on disk", () => {
|
|
|
201
201
|
| `src/runtime/yield-handler.ts` | 9 | `ajv` |
|
|
202
202
|
| `src/ui/run-action-dispatcher.ts` | 8 | `../extension/team-tool.ts` |
|
|
203
203
|
|
|
204
|
-
###
|
|
204
|
+
### Analysis
|
|
205
205
|
|
|
206
|
-
|
|
206
|
+
Some have a comment explaining the reason (extension/team-tool.ts:33-34):
|
|
207
207
|
> Heavy runtime — lazy-loaded to avoid 1.4s import cost at extension registration. executeTeamRun is only called when a team run actually executes.
|
|
208
208
|
|
|
209
|
-
|
|
209
|
+
This is a legitimate optimization. But AGENTS.md states an absolute "avoid" with no exceptions. Two ways to resolve:
|
|
210
210
|
|
|
211
|
-
**Option A — Update AGENTS.md
|
|
211
|
+
**Option A — Update AGENTS.md to legitimize the lazy boundary:**
|
|
212
212
|
```md
|
|
213
213
|
- Avoid dynamic inline imports, EXCEPT at documented lazy-load boundaries
|
|
214
214
|
to defer heavy runtime cost (mark with `// LAZY: <reason>`).
|
|
215
215
|
```
|
|
216
216
|
|
|
217
|
-
**Option B — Refactor
|
|
218
|
-
- Move heavy modules
|
|
219
|
-
-
|
|
217
|
+
**Option B — Refactor to top-level imports:**
|
|
218
|
+
- Move heavy modules into a separate package, or use `import type` for type-only and a top-level runtime import.
|
|
219
|
+
- You could keep the lazy import for `runtime-resolver.ts:40` (`@mariozechner/pi-coding-agent`) because it is an optional peer dependency.
|
|
220
220
|
|
|
221
221
|
### Recommendation
|
|
222
222
|
|
|
223
|
-
|
|
223
|
+
Choose **Option A**, add a `// LAZY: <reason>` marker comment to each site, and add a grep check in CI to block unmarked dynamic imports.
|
|
224
224
|
|
|
225
225
|
---
|
|
226
226
|
|
|
227
|
-
## BUG-004 — `withRunLockSync`
|
|
227
|
+
## BUG-004 — `withRunLockSync` and `withRunLock` handle stale locks differently
|
|
228
228
|
|
|
229
229
|
**Severity:** Medium
|
|
230
230
|
**File:** `src/state/locks.ts:50-91`
|
|
231
231
|
|
|
232
|
-
###
|
|
232
|
+
### Description
|
|
233
233
|
|
|
234
234
|
**Sync path** (`acquireLockWithRetry` → `readLockState`):
|
|
235
235
|
```ts
|
|
@@ -238,9 +238,9 @@ function readLockState(filePath: string, staleMs: number): boolean {
|
|
|
238
238
|
if (!isLockStale(filePath, staleMs)) return false;
|
|
239
239
|
try {
|
|
240
240
|
fs.rmSync(filePath, { force: true });
|
|
241
|
-
return true; // ←
|
|
241
|
+
return true; // ← only true when rmSync succeeds
|
|
242
242
|
} catch {
|
|
243
|
-
return false; // ← throw
|
|
243
|
+
return false; // ← a throw will happen at the caller
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
246
|
|
|
@@ -271,23 +271,23 @@ async function acquireLockWithRetryAsync(...) {
|
|
|
271
271
|
if (Date.now() > deadline) {
|
|
272
272
|
throw new Error(`Run '...' is locked by another operation.`);
|
|
273
273
|
}
|
|
274
|
-
readLockStateAsync(filePath, staleMs); // ←
|
|
274
|
+
readLockStateAsync(filePath, staleMs); // ← return value not checked
|
|
275
275
|
await sleep(delay);
|
|
276
276
|
attempt++;
|
|
277
|
-
// ←
|
|
277
|
+
// ← always loops again
|
|
278
278
|
}
|
|
279
279
|
```
|
|
280
280
|
|
|
281
|
-
###
|
|
281
|
+
### Consequences
|
|
282
282
|
|
|
283
|
-
- Sync version:
|
|
284
|
-
- Async version:
|
|
283
|
+
- Sync version: if `rmSync` fails (file is locked by another process on Windows), it throws **immediately** the first time it sees a stale lock, with no retry.
|
|
284
|
+
- Async version: always retries until the `deadline`.
|
|
285
285
|
|
|
286
|
-
Inconsistent behavior →
|
|
286
|
+
Inconsistent behavior → the same stale-lock + transient `rmSync` race can fail in the sync code path but pass in the async path.
|
|
287
287
|
|
|
288
|
-
###
|
|
288
|
+
### Suggested fix
|
|
289
289
|
|
|
290
|
-
|
|
290
|
+
Align the behavior: the sync version should also retry until the deadline:
|
|
291
291
|
|
|
292
292
|
```ts
|
|
293
293
|
function acquireLockWithRetry(filePath: string, staleMs: number): void {
|
|
@@ -303,7 +303,7 @@ function acquireLockWithRetry(filePath: string, staleMs: number): void {
|
|
|
303
303
|
if (Date.now() > deadline) {
|
|
304
304
|
throw new Error(`Run '${path.basename(filePath)}' is locked by another operation.`);
|
|
305
305
|
}
|
|
306
|
-
// Try to clear stale, but don't bail on rmSync error — let loop retry
|
|
306
|
+
// Try to clear the stale lock, but don't bail on an rmSync error — let the loop retry
|
|
307
307
|
try {
|
|
308
308
|
if (isLockStale(filePath, staleMs)) fs.rmSync(filePath, { force: true });
|
|
309
309
|
} catch { /* race — let loop retry */ }
|
|
@@ -314,18 +314,18 @@ function acquireLockWithRetry(filePath: string, staleMs: number): void {
|
|
|
314
314
|
}
|
|
315
315
|
```
|
|
316
316
|
|
|
317
|
-
### Test
|
|
317
|
+
### Test to add
|
|
318
318
|
|
|
319
|
-
|
|
319
|
+
Expand `test/unit/locks-race.test.ts` with a case: stale lock + `rmSync` race (mock `fs.rmSync` to throw the first time and pass the second) → assert the lock is acquired after a retry.
|
|
320
320
|
|
|
321
321
|
---
|
|
322
322
|
|
|
323
|
-
## BUG-005 — `git worktree add -b <branch>`
|
|
323
|
+
## BUG-005 — `git worktree add -b <branch>` fails when the branch already exists from a previous run
|
|
324
324
|
|
|
325
325
|
**Severity:** Medium
|
|
326
326
|
**File:** `src/worktree/worktree-manager.ts:100-114`
|
|
327
327
|
|
|
328
|
-
###
|
|
328
|
+
### Description
|
|
329
329
|
|
|
330
330
|
```ts
|
|
331
331
|
// worktree-manager.ts:100-114
|
|
@@ -336,16 +336,16 @@ if (fs.existsSync(worktreePath)) {
|
|
|
336
336
|
git(repoRoot, ["worktree", "add", "-b", branch, worktreePath, "HEAD"]);
|
|
337
337
|
```
|
|
338
338
|
|
|
339
|
-
|
|
339
|
+
The reuse condition only checks the `worktreePath` directory. But the branch `pi-crew/<runId>/<taskId>` can exist in git while the worktree directory was deleted manually (or `cleanupRunWorktrees` deleted the directory while the git worktree metadata remained).
|
|
340
340
|
|
|
341
|
-
###
|
|
341
|
+
### Consequences
|
|
342
342
|
|
|
343
|
-
-
|
|
344
|
-
-
|
|
343
|
+
- After a crash or an incomplete cleanup, a retry/resume run fails with a git error: `fatal: a branch named 'pi-crew/.../...' already exists`.
|
|
344
|
+
- The user gets stuck and must run `git branch -D` manually.
|
|
345
345
|
|
|
346
|
-
###
|
|
346
|
+
### Suggested fix
|
|
347
347
|
|
|
348
|
-
|
|
348
|
+
Add a branch-existence check before `add`:
|
|
349
349
|
|
|
350
350
|
```ts
|
|
351
351
|
function branchExists(repoRoot: string, branch: string): boolean {
|
|
@@ -365,27 +365,27 @@ function pruneStaleWorktrees(repoRoot: string): void {
|
|
|
365
365
|
// In prepareTaskWorkspace, before `git worktree add`:
|
|
366
366
|
pruneStaleWorktrees(repoRoot);
|
|
367
367
|
if (branchExists(repoRoot, branch)) {
|
|
368
|
-
// Option 1: reuse
|
|
368
|
+
// Option 1: reuse the existing branch
|
|
369
369
|
git(repoRoot, ["worktree", "add", worktreePath, branch]);
|
|
370
370
|
} else {
|
|
371
371
|
git(repoRoot, ["worktree", "add", "-b", branch, worktreePath, "HEAD"]);
|
|
372
372
|
}
|
|
373
373
|
```
|
|
374
374
|
|
|
375
|
-
### Test
|
|
375
|
+
### Test to add
|
|
376
376
|
|
|
377
|
-
`test/unit/worktree-manager.test.ts` (
|
|
378
|
-
1. Create worktree,
|
|
379
|
-
2. Call `prepareTaskWorkspace` again → expect success, not fatal.
|
|
377
|
+
`test/unit/worktree-manager.test.ts` (does not yet exist):
|
|
378
|
+
1. Create a worktree, manually delete the directory (`rm -rf` outside of git), branch still exists.
|
|
379
|
+
2. Call `prepareTaskWorkspace` again → expect success, not a fatal error.
|
|
380
380
|
|
|
381
381
|
---
|
|
382
382
|
|
|
383
|
-
## BUG-006 — `linkNodeModulesIfPresent`
|
|
383
|
+
## BUG-006 — `linkNodeModulesIfPresent` does not verify the source is a directory
|
|
384
384
|
|
|
385
385
|
**Severity:** Low/Medium
|
|
386
386
|
**File:** `src/worktree/worktree-manager.ts:43-53`
|
|
387
387
|
|
|
388
|
-
###
|
|
388
|
+
### Description
|
|
389
389
|
|
|
390
390
|
```ts
|
|
391
391
|
function linkNodeModulesIfPresent(repoRoot: string, worktreePath: string): boolean {
|
|
@@ -401,10 +401,10 @@ function linkNodeModulesIfPresent(repoRoot: string, worktreePath: string): boole
|
|
|
401
401
|
}
|
|
402
402
|
```
|
|
403
403
|
|
|
404
|
-
-
|
|
405
|
-
-
|
|
404
|
+
- If `repoRoot/node_modules` is a **file** (rare but possible with a corrupt setup), `existsSync` is still true, and the symlink is created with type `"dir"/"junction"` → undefined behavior, especially since a junction on Windows requires a directory.
|
|
405
|
+
- If the source is a **symlink to a directory**, a link chain can result → hard to debug.
|
|
406
406
|
|
|
407
|
-
###
|
|
407
|
+
### Suggested fix
|
|
408
408
|
|
|
409
409
|
```ts
|
|
410
410
|
function linkNodeModulesIfPresent(repoRoot: string, worktreePath: string): boolean {
|
|
@@ -423,16 +423,16 @@ function linkNodeModulesIfPresent(repoRoot: string, worktreePath: string): boole
|
|
|
423
423
|
}
|
|
424
424
|
```
|
|
425
425
|
|
|
426
|
-
|
|
426
|
+
Use `statSync` (follows symlinks) instead of `existsSync` to also catch the "source is a dangling symlink" case.
|
|
427
427
|
|
|
428
428
|
---
|
|
429
429
|
|
|
430
|
-
## BUG-007 —
|
|
430
|
+
## BUG-007 — Errored / non-JSON setup hook output is swallowed entirely with no log
|
|
431
431
|
|
|
432
432
|
**Severity:** Low
|
|
433
433
|
**File:** `src/worktree/worktree-manager.ts:75-89`
|
|
434
434
|
|
|
435
|
-
###
|
|
435
|
+
### Description
|
|
436
436
|
|
|
437
437
|
```ts
|
|
438
438
|
try {
|
|
@@ -447,9 +447,9 @@ try {
|
|
|
447
447
|
}
|
|
448
448
|
```
|
|
449
449
|
|
|
450
|
-
|
|
450
|
+
The hook returns a JSON parse error → returns `[]` silently. The user has no idea the hook is misbehaving until the worktree is missing paths.
|
|
451
451
|
|
|
452
|
-
###
|
|
452
|
+
### Suggested fix
|
|
453
453
|
|
|
454
454
|
```ts
|
|
455
455
|
} catch (error) {
|
|
@@ -459,11 +459,11 @@ Hook trả về JSON parse error → return `[]` silently. User không biết ho
|
|
|
459
459
|
}
|
|
460
460
|
```
|
|
461
461
|
|
|
462
|
-
|
|
462
|
+
Alternatively, if the hook output is non-empty but JSON parsing fails → emit an event into the run's event log.
|
|
463
463
|
|
|
464
464
|
---
|
|
465
465
|
|
|
466
|
-
## NIT-001 — `__test__renameWithRetry`
|
|
466
|
+
## NIT-001 — `__test__renameWithRetry` called from a production code path
|
|
467
467
|
|
|
468
468
|
**File:** `src/state/atomic-write.ts:55-67, 99`
|
|
469
469
|
|
|
@@ -479,11 +479,11 @@ export function atomicWriteFile(filePath: string, content: string): void {
|
|
|
479
479
|
}
|
|
480
480
|
```
|
|
481
481
|
|
|
482
|
-
Convention:
|
|
482
|
+
Convention: the `__test__` name implies "test-only, not stable." Using it in production is a code smell. Rename it to `renameWithRetry` (a public utility) and re-export the test version under an alias.
|
|
483
483
|
|
|
484
484
|
---
|
|
485
485
|
|
|
486
|
-
## NIT-002 — Empty-string argv flag
|
|
486
|
+
## NIT-002 — Empty-string argv flag in `git worktree remove`
|
|
487
487
|
|
|
488
488
|
**File:** `src/worktree/cleanup.ts:64`
|
|
489
489
|
|
|
@@ -491,7 +491,7 @@ Convention: tên `__test__` ngụ ý "chỉ dùng cho test, không stable". Prod
|
|
|
491
491
|
git(manifest.cwd, ["worktree", "remove", options.force ? "--force" : "", worktreePath].filter(Boolean));
|
|
492
492
|
```
|
|
493
493
|
|
|
494
|
-
|
|
494
|
+
The `cond ? "--force" : ""` then `.filter(Boolean)` pattern works but is fragile. Better:
|
|
495
495
|
|
|
496
496
|
```ts
|
|
497
497
|
const args = ["worktree", "remove"];
|
|
@@ -502,7 +502,7 @@ git(manifest.cwd, args);
|
|
|
502
502
|
|
|
503
503
|
---
|
|
504
504
|
|
|
505
|
-
## NIT-003 — `executedConfig.runtime`
|
|
505
|
+
## NIT-003 — `executedConfig.runtime` mutated on resume
|
|
506
506
|
|
|
507
507
|
**File:** `src/extension/team-tool.ts:184-190`
|
|
508
508
|
|
|
@@ -514,7 +514,7 @@ if (!executedConfig.runtime?.mode && resumeManifest.runtimeResolution?.safety ==
|
|
|
514
514
|
}
|
|
515
515
|
```
|
|
516
516
|
|
|
517
|
-
|
|
517
|
+
The code may be assuming `effectiveRunConfig` returns a fresh object. Verify and document immutability, or replace with an explicit clone:
|
|
518
518
|
|
|
519
519
|
```ts
|
|
520
520
|
const executedConfig: PiTeamsConfig = {
|
|
@@ -524,9 +524,9 @@ const executedConfig: PiTeamsConfig = {
|
|
|
524
524
|
|
|
525
525
|
---
|
|
526
526
|
|
|
527
|
-
## NIT-004 — Verify transcript
|
|
527
|
+
## NIT-004 — Verify the transcript on disk is always redacted
|
|
528
528
|
|
|
529
|
-
**File:** `src/runtime/child-pi.ts:148-152`,
|
|
529
|
+
**File:** `src/runtime/child-pi.ts:148-152`, cross-referenced with `recoverCheckpointedTasks` (`src/extension/team-tool.ts:155-156`)
|
|
530
530
|
|
|
531
531
|
```ts
|
|
532
532
|
// child-pi.ts:148-152
|
|
@@ -537,7 +537,7 @@ function appendTranscript(input: ChildPiRunInput, line: string): void {
|
|
|
537
537
|
}
|
|
538
538
|
```
|
|
539
539
|
|
|
540
|
-
|
|
540
|
+
The transcript is redacted via `redactJsonLine` — good. But in the recovery path:
|
|
541
541
|
|
|
542
542
|
```ts
|
|
543
543
|
// team-tool.ts:155-156
|
|
@@ -549,44 +549,44 @@ const resultArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
|
549
549
|
});
|
|
550
550
|
```
|
|
551
551
|
|
|
552
|
-
|
|
552
|
+
Because `writeArtifact` redacts again (verified in BUG-002), double-redaction is idempotent (`***` does not match the secret pattern). OK.
|
|
553
553
|
|
|
554
|
-
**Action:**
|
|
555
|
-
1. Spawn mock child producing JSON line
|
|
556
|
-
2. Read transcript file → assert
|
|
557
|
-
3. Run `recoverCheckpointedTasks` → assert result artifact
|
|
554
|
+
**Action:** add a test `test/unit/redaction-transcript-roundtrip.test.ts`:
|
|
555
|
+
1. Spawn a mock child producing a JSON line with a secret.
|
|
556
|
+
2. Read the transcript file → assert it contains no raw secret.
|
|
557
|
+
3. Run `recoverCheckpointedTasks` → assert the result artifact also contains no secret.
|
|
558
558
|
|
|
559
559
|
---
|
|
560
560
|
|
|
561
|
-
##
|
|
561
|
+
## Test coverage gaps
|
|
562
562
|
|
|
563
|
-
| Module |
|
|
563
|
+
| Module | Status |
|
|
564
564
|
|---|---|
|
|
565
|
-
| `src/worktree/worktree-manager.ts` |
|
|
566
|
-
| `src/worktree/cleanup.ts` |
|
|
567
|
-
| `src/state/locks.ts` (sync vs async parity) | `locks-race.test.ts` + `api-locks.test.ts`
|
|
568
|
-
| `src/state/artifact-store.ts` |
|
|
569
|
-
| `src/schema/team-tool-schema.ts` | `team-tool-schema.test.ts`
|
|
565
|
+
| `src/worktree/worktree-manager.ts` | Only has `branch-freshness.test.ts`. Missing tests for `prepareTaskWorkspace` (reuse path, branch mismatch, setupHook). |
|
|
566
|
+
| `src/worktree/cleanup.ts` | Has `lifecycle-actions.test.ts` indirectly. Missing a direct test for dirty-preserve + diff artifact. |
|
|
567
|
+
| `src/state/locks.ts` (sync vs async parity) | `locks-race.test.ts` + `api-locks.test.ts` do not assert the difference described in BUG-004. |
|
|
568
|
+
| `src/state/artifact-store.ts` | Needs a hash/size match test (BUG-002). |
|
|
569
|
+
| `src/schema/team-tool-schema.ts` | `team-tool-schema.test.ts` has no case for `retry` (BUG-001). |
|
|
570
570
|
|
|
571
571
|
---
|
|
572
572
|
|
|
573
|
-
##
|
|
573
|
+
## Positives
|
|
574
574
|
|
|
575
|
-
- **Path-traversal guards**
|
|
576
|
-
- **Atomic write**
|
|
577
|
-
- **Process management**
|
|
578
|
-
- **Env-secret filtering**
|
|
579
|
-
- **Default-safe execution**: `executeWorkers=false` / `PI_CREW_EXECUTE_WORKERS=0` / `PI_TEAMS_EXECUTE_WORKERS=0` block
|
|
580
|
-
- **Index.ts minimal**:
|
|
581
|
-
- **Lockstep destructive gates**: `delete` requires `confirm:true`, referenced resources block
|
|
575
|
+
- **Path-traversal guards** in `resolveInside` (`artifact-store.ts:96-105`) combine a relative-segment check, a `path.relative` check, and a `path.normalize + startsWith(base + sep)` check.
|
|
576
|
+
- **Atomic write** uses `O_EXCL | O_NOFOLLOW`, a post-open `fstatSync().isFile()` verification, and a Windows EPERM/EBUSY rename retry.
|
|
577
|
+
- **Process management** in `child-pi.ts` tracks the PID in `activeChildProcesses`, supports `taskkill /T /F` (Win) + `process.kill(-pid, ...)` (POSIX), has a hard-kill fallback, and a post-exit stdio guard.
|
|
578
|
+
- **Env-secret filtering** before spawning the child Pi (`child-pi.ts:113`) uses `SECRET_KEY_PATTERN` to strip token/api_key/password from the env.
|
|
579
|
+
- **Default-safe execution**: `executeWorkers=false` / `PI_CREW_EXECUTE_WORKERS=0` / `PI_TEAMS_EXECUTE_WORKERS=0` block workers; `runtime.mode=scaffold` for dry-runs.
|
|
580
|
+
- **Index.ts minimal**: follows the rule, only 5 lines.
|
|
581
|
+
- **Lockstep destructive gates**: `delete` requires `confirm:true`, referenced resources block unless `force:true` (verified in `management.ts:344-353`).
|
|
582
582
|
|
|
583
583
|
---
|
|
584
584
|
|
|
585
|
-
##
|
|
585
|
+
## Suggested fix priority
|
|
586
586
|
|
|
587
|
-
1. **BUG-001** (5
|
|
588
|
-
2. **BUG-002** (15
|
|
589
|
-
3. **BUG-004** (30
|
|
590
|
-
4. **BUG-005** (1
|
|
591
|
-
5. **BUG-003** (1
|
|
592
|
-
6.
|
|
587
|
+
1. **BUG-001** (5 minutes): add one line `Type.Literal("retry")` + 1 test.
|
|
588
|
+
2. **BUG-002** (15 minutes): choose Option A, swap the hash/write order + add an integrity test.
|
|
589
|
+
3. **BUG-004** (30 minutes): align the sync/async lock retry behavior + test.
|
|
590
|
+
4. **BUG-005** (1 hour): add a branch-existence check + worktree prune before add, write tests.
|
|
591
|
+
5. **BUG-003** (1 hour): update AGENTS.md with a rule exception for lazy boundaries, add marker comments.
|
|
592
|
+
6. The rest: batch into a later release.
|