pi-crew 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +35 -0
- package/docs/code-review-2026-05-11.md +592 -0
- package/docs/followup-plan-2026-05-12.md +463 -0
- package/docs/followup-review-2026-05-12.md +297 -0
- package/docs/followup-review-round3-2026-05-12.md +342 -0
- package/package.json +3 -2
- package/src/extension/cross-extension-rpc.ts +1 -0
- package/src/extension/registration/subagent-tools.ts +1 -0
- package/src/extension/registration/team-tool.ts +1 -0
- package/src/extension/team-manager-command.ts +1 -0
- package/src/extension/team-tool/run.ts +1 -0
- package/src/extension/team-tool.ts +344 -332
- package/src/runtime/async-runner.ts +89 -15
- package/src/runtime/background-runner.ts +1 -0
- package/src/runtime/child-pi.ts +2 -4
- package/src/runtime/iteration-hooks.ts +5 -2
- package/src/runtime/live-session-runtime.ts +1 -0
- package/src/runtime/post-checks.ts +5 -2
- package/src/runtime/runtime-resolver.ts +1 -0
- package/src/runtime/subagent-manager.ts +5 -0
- package/src/runtime/task-runner.ts +1 -0
- package/src/runtime/yield-handler.ts +1 -0
- package/src/schema/team-tool-schema.ts +1 -0
- package/src/state/artifact-store.ts +2 -2
- package/src/state/atomic-write.ts +21 -4
- package/src/state/event-log.ts +110 -47
- package/src/state/locks.ts +12 -14
- package/src/ui/run-action-dispatcher.ts +1 -0
- package/src/utils/env-filter.ts +30 -0
- package/src/utils/redaction.ts +1 -1
- package/src/utils/resolve-shell.ts +34 -0
- package/src/utils/sleep.ts +2 -1
- package/src/worktree/cleanup.ts +5 -2
- package/src/worktree/worktree-manager.ts +47 -5
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
# Follow-up Plan — pi-crew (2026-05-12)
|
|
2
|
+
|
|
3
|
+
Tác giả: Droid (Factory) | Liên quan: `docs/code-review-2026-05-11.md`, commits `2aebf33`, `7c5b3c2`.
|
|
4
|
+
|
|
5
|
+
Tài liệu này tổng hợp các điểm tồn đọng SAU khi đã fix BUG-001..BUG-007 + NIT-001..NIT-004, gồm:
|
|
6
|
+
1. **Quan ngại nhỏ** phát sinh từ chính các fix vừa apply.
|
|
7
|
+
2. **Gaps mới phát hiện** khi review lại toàn bộ `pi-crew/` lần nữa.
|
|
8
|
+
|
|
9
|
+
Tất cả mục đều có ước lượng effort, mức ưu tiên, file/đoạn code liên quan, và đề xuất fix cụ thể.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Phần A — Điều chỉnh các "quan ngại nhỏ" của fix vừa apply
|
|
14
|
+
|
|
15
|
+
### A1 — `branchExists` chỉ check local ref → bỏ sót remote-tracking branch
|
|
16
|
+
|
|
17
|
+
**Severity:** Low | **Effort:** ~20 phút | **File:** `src/worktree/worktree-manager.ts:100-107`
|
|
18
|
+
|
|
19
|
+
**Hiện trạng:**
|
|
20
|
+
```ts
|
|
21
|
+
function branchExists(repoRoot: string, branch: string): boolean {
|
|
22
|
+
try {
|
|
23
|
+
git(repoRoot, ["rev-parse", "--verify", `refs/heads/${branch}`]);
|
|
24
|
+
return true;
|
|
25
|
+
} catch { return false; }
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Vấn đề:**
|
|
30
|
+
- Chỉ check `refs/heads/<branch>`. Nếu repo có remote-tracking `refs/remotes/origin/<branch>` (push từ máy khác) mà chưa có local, hàm trả `false` → đi vào nhánh `worktree add -b <branch> HEAD` → git có thể fail với "a branch named ... already exists" (vì git tạo local branch từ remote) hoặc tạo local branch divergent với remote.
|
|
31
|
+
- Hiếm trong workflow đơn-máy nhưng dễ gặp với CI/runner shared repo.
|
|
32
|
+
|
|
33
|
+
**Fix đề xuất:**
|
|
34
|
+
```ts
|
|
35
|
+
function branchExists(repoRoot: string, branch: string): { local: boolean; remoteOnly: boolean } {
|
|
36
|
+
let local = false;
|
|
37
|
+
try { git(repoRoot, ["rev-parse", "--verify", `refs/heads/${branch}`]); local = true; } catch {}
|
|
38
|
+
if (local) return { local: true, remoteOnly: false };
|
|
39
|
+
// Check remote-tracking
|
|
40
|
+
try {
|
|
41
|
+
const out = execFileSync("git", ["for-each-ref", "--format=%(refname)", `refs/remotes/*/${branch}`],
|
|
42
|
+
{ cwd: repoRoot, encoding: "utf-8" }).trim();
|
|
43
|
+
return { local: false, remoteOnly: out.length > 0 };
|
|
44
|
+
} catch { return { local: false, remoteOnly: false }; }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// In prepareTaskWorkspace:
|
|
48
|
+
const exists = branchExists(repoRoot, branch);
|
|
49
|
+
if (exists.local) {
|
|
50
|
+
git(repoRoot, ["worktree", "add", worktreePath, branch]);
|
|
51
|
+
} else if (exists.remoteOnly) {
|
|
52
|
+
// Create local from HEAD instead of remote (avoid divergent track)
|
|
53
|
+
git(repoRoot, ["worktree", "add", "-b", branch, worktreePath, "HEAD"]);
|
|
54
|
+
} else {
|
|
55
|
+
git(repoRoot, ["worktree", "add", "-b", branch, worktreePath, "HEAD"]);
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Test cần thêm:** `test/unit/worktree-manager.test.ts`:
|
|
60
|
+
1. Mock `git for-each-ref` trả về remote-tracking → assert không throw.
|
|
61
|
+
2. Branch tồn tại cả local lẫn remote → ưu tiên local (reuse).
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
### A2 — `resolveJitiRegisterPath` fallback giờ bắt buộc `exists()` check
|
|
66
|
+
|
|
67
|
+
**Severity:** Low | **Effort:** ~10 phút | **File:** `src/runtime/async-runner.ts:33-39`
|
|
68
|
+
|
|
69
|
+
**Hiện trạng:**
|
|
70
|
+
```ts
|
|
71
|
+
try {
|
|
72
|
+
const fromRequire = jitiRegisterPathFromPackageJson(requireFromHere.resolve("jiti/package.json"));
|
|
73
|
+
if (exists(fromRequire)) return fromRequire; // ← thêm exists() check
|
|
74
|
+
} catch { /* Fall through. */ }
|
|
75
|
+
return undefined;
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Vấn đề:**
|
|
79
|
+
- Behaviour cũ: `require.resolve()` thành công → push vào candidates → return path (giả định `lib/jiti-register.mjs` luôn tồn tại bên cạnh `package.json`).
|
|
80
|
+
- Behaviour mới: thêm `exists()` check → nếu test mock `exists` chỉ accept đúng 1 path khác, fallback luôn fail. Behaviour ổn cho production (file luôn tồn tại), nhưng giảm robustness với jiti packaging exotic (vd. `lib` được rename trong distro).
|
|
81
|
+
- Không thực sự là bug, chỉ là defensive change.
|
|
82
|
+
|
|
83
|
+
**Fix đề xuất:**
|
|
84
|
+
Giữ nguyên `exists()` check (an toàn hơn), nhưng thêm 1 fallback nữa với `path.join(dirname, "register.mjs")` cho các phiên bản jiti khác nhau, và log diagnostic event khi cả 2 đều miss:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
try {
|
|
88
|
+
const pkgPath = requireFromHere.resolve("jiti/package.json");
|
|
89
|
+
const candidates = [
|
|
90
|
+
jitiRegisterPathFromPackageJson(pkgPath), // lib/jiti-register.mjs
|
|
91
|
+
path.join(path.dirname(pkgPath), "register.mjs"), // register.mjs (older jiti)
|
|
92
|
+
path.join(path.dirname(pkgPath), "dist", "register.mjs"), // dist/register.mjs (some pkg layouts)
|
|
93
|
+
];
|
|
94
|
+
for (const c of candidates) if (exists(c)) return c;
|
|
95
|
+
} catch { /* Fall through. */ }
|
|
96
|
+
return undefined;
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Test cần thêm:** `test/unit/async-runner.test.ts`:
|
|
100
|
+
- Case `lib/jiti-register.mjs` missing nhưng `register.mjs` tồn tại → resolver return.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
### A3 — Test alias `__test__renameWithRetry*` trở thành `const`, có thể vô hiệu monkey-patch
|
|
105
|
+
|
|
106
|
+
**Severity:** Info | **Effort:** ~5 phút | **File:** `src/state/atomic-write.ts:73, 91`
|
|
107
|
+
|
|
108
|
+
**Hiện trạng:**
|
|
109
|
+
```ts
|
|
110
|
+
export const __test__renameWithRetry = renameWithRetry;
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Vấn đề:**
|
|
114
|
+
- Nếu test cũ làm `import * as mod from "../atomic-write.ts"; mod.__test__renameWithRetry = mockFn;` → assignment fail (read-only) hoặc không có effect trên `atomicWriteFile` (nó gọi `renameWithRetry` local).
|
|
115
|
+
- Hiện tại không có test nào trong repo monkey-patch field này (`Grep` confirm), nên không phải bug.
|
|
116
|
+
|
|
117
|
+
**Fix đề xuất:**
|
|
118
|
+
- Giữ nguyên alias (zero impact thực tế).
|
|
119
|
+
- Hoặc xoá hẳn 2 alias để giảm surface: chỉ export `renameWithRetry`, `renameWithRetryAsync`. Update test nếu có chỗ dùng alias.
|
|
120
|
+
|
|
121
|
+
**Action:** chỉ làm khi cần dọn dẹp API surface, không khẩn cấp.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Phần B — Gaps mới phát hiện khi review lại
|
|
126
|
+
|
|
127
|
+
### B1 — `bash` hardcoded → broken trên Windows (root cause của 8 test fail)
|
|
128
|
+
|
|
129
|
+
**Severity:** Medium | **Effort:** ~45 phút | **Files:**
|
|
130
|
+
- `src/runtime/post-checks.ts:82`
|
|
131
|
+
- `src/runtime/iteration-hooks.ts:137`
|
|
132
|
+
|
|
133
|
+
**Hiện trạng:**
|
|
134
|
+
```ts
|
|
135
|
+
// post-checks.ts:82
|
|
136
|
+
const output = execFileSync("bash", [scriptPath], { ... });
|
|
137
|
+
|
|
138
|
+
// iteration-hooks.ts:137
|
|
139
|
+
const child = spawn("bash", [hookScriptPath], { ... });
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Vấn đề:**
|
|
143
|
+
- Trên Windows, `bash` thường không có trên PATH (trừ khi cài Git Bash/WSL). 8 tests fail hiện tại đều do điều này.
|
|
144
|
+
- Code path nóng (post-task check, iteration hook) → user Windows không dùng được tính năng này.
|
|
145
|
+
- Comment trong file đã ghi "Spawns `bash <script>`" → docs cũng cần update.
|
|
146
|
+
|
|
147
|
+
**Fix đề xuất:**
|
|
148
|
+
1. Tìm bash thông minh:
|
|
149
|
+
```ts
|
|
150
|
+
function resolveBashCmd(): string {
|
|
151
|
+
if (process.platform !== "win32") return "bash";
|
|
152
|
+
// Try Git Bash locations
|
|
153
|
+
for (const cand of [
|
|
154
|
+
process.env.SHELL,
|
|
155
|
+
"C:\\Program Files\\Git\\bin\\bash.exe",
|
|
156
|
+
"C:\\Program Files (x86)\\Git\\bin\\bash.exe",
|
|
157
|
+
]) {
|
|
158
|
+
if (cand && fs.existsSync(cand)) return cand;
|
|
159
|
+
}
|
|
160
|
+
return "bash"; // last resort — let spawn fail with clearer error
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
2. Thay 2 chỗ `"bash"` bằng `resolveBashCmd()`.
|
|
164
|
+
3. Trên Windows, nếu script là `.ps1` → dùng `powershell -File`; nếu `.cmd/.bat` → spawn trực tiếp.
|
|
165
|
+
4. Hoặc đơn giản hơn: skip test nếu `!isScriptRunnable("bash")`.
|
|
166
|
+
|
|
167
|
+
**Test cần thêm:**
|
|
168
|
+
- Trên CI Linux: tests hiện tại pass.
|
|
169
|
+
- Trên Windows: skip nếu không có bash, hoặc tạo `.ps1` variant.
|
|
170
|
+
- Mock test cho `resolveBashCmd()` với platform stub.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
### B2 — Thiếu test trực tiếp cho `worktree-manager.ts` (covers BUG-005, BUG-006)
|
|
175
|
+
|
|
176
|
+
**Severity:** Medium | **Effort:** ~1h | **File:** thiếu `test/unit/worktree-manager.test.ts`
|
|
177
|
+
|
|
178
|
+
**Vấn đề:**
|
|
179
|
+
- BUG-005 fix (`branchExists` + `pruneStaleWorktrees`) và BUG-006 fix (`isDirectory()` check) **không có test trực tiếp**.
|
|
180
|
+
- Code review đã flag điều này nhưng commit fix chỉ thêm test cho schema.
|
|
181
|
+
|
|
182
|
+
**Fix đề xuất:** tạo `test/unit/worktree-manager.test.ts` với (sử dụng `tmpdir` + real git):
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
import { test } from "node:test";
|
|
186
|
+
import assert from "node:assert/strict";
|
|
187
|
+
import * as fs from "node:fs";
|
|
188
|
+
import * as os from "node:os";
|
|
189
|
+
import * as path from "node:path";
|
|
190
|
+
import { execFileSync } from "node:child_process";
|
|
191
|
+
import { prepareTaskWorkspace } from "../../src/worktree/worktree-manager.ts";
|
|
192
|
+
import type { TeamRunManifest, TeamTaskState } from "../../src/state/types.ts";
|
|
193
|
+
|
|
194
|
+
function initGitRepo(dir: string) {
|
|
195
|
+
execFileSync("git", ["init", "-q", "--initial-branch=main"], { cwd: dir });
|
|
196
|
+
execFileSync("git", ["-c", "user.email=t@t", "-c", "user.name=t", "commit", "--allow-empty", "-m", "init"], { cwd: dir });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
test("prepareTaskWorkspace recovers when branch exists but worktree dir is gone", () => {
|
|
200
|
+
const repo = fs.mkdtempSync(path.join(os.tmpdir(), "pi-crew-wt-"));
|
|
201
|
+
initGitRepo(repo);
|
|
202
|
+
// Pre-create the branch (simulating leftover from crashed run)
|
|
203
|
+
execFileSync("git", ["branch", "pi-crew/run1/task1"], { cwd: repo });
|
|
204
|
+
const manifest = { /* ... minimal manifest ... */ } as TeamRunManifest;
|
|
205
|
+
const task: TeamTaskState = { id: "task1", /* ... */ } as TeamTaskState;
|
|
206
|
+
const result = prepareTaskWorkspace(manifest, task);
|
|
207
|
+
assert.ok(result.worktreePath);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("linkNodeModulesIfPresent rejects when node_modules is a file", () => {
|
|
211
|
+
// ... create file at node_modules location → assert no symlink created
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
### B3 — `artifact-store.ts` thiếu test cho hash/size integrity (covers BUG-002)
|
|
218
|
+
|
|
219
|
+
**Severity:** Medium | **Effort:** ~20 phút | **File:** thiếu `test/unit/artifact-store.test.ts`
|
|
220
|
+
|
|
221
|
+
**Vấn đề:**
|
|
222
|
+
- BUG-002 fix đổi hash từ `options.content` sang `redactSecretString(options.content)` đảm bảo `contentHash` khớp bytes trên đĩa.
|
|
223
|
+
- Không có test verify điều này. Có `api-artifact-security.test.ts` nhưng không assert hash.
|
|
224
|
+
|
|
225
|
+
**Fix đề xuất:** tạo `test/unit/artifact-store.test.ts`:
|
|
226
|
+
```ts
|
|
227
|
+
import { test } from "node:test";
|
|
228
|
+
import assert from "node:assert/strict";
|
|
229
|
+
import * as fs from "node:fs";
|
|
230
|
+
import * as path from "node:path";
|
|
231
|
+
import * as os from "node:os";
|
|
232
|
+
import { createHash } from "node:crypto";
|
|
233
|
+
import { writeArtifact } from "../../src/state/artifact-store.ts";
|
|
234
|
+
|
|
235
|
+
test("writeArtifact: contentHash matches sha256 of bytes on disk", () => {
|
|
236
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-crew-art-"));
|
|
237
|
+
const desc = writeArtifact(root, {
|
|
238
|
+
kind: "log", relativePath: "x.log",
|
|
239
|
+
content: "api_key=AKIA0123456789ABCDEF\nplain text",
|
|
240
|
+
producer: "test",
|
|
241
|
+
});
|
|
242
|
+
const onDisk = fs.readFileSync(desc.path);
|
|
243
|
+
const expected = createHash("sha256").update(onDisk).digest("hex");
|
|
244
|
+
assert.strictEqual(desc.contentHash, expected);
|
|
245
|
+
assert.strictEqual(desc.sizeBytes, onDisk.length);
|
|
246
|
+
assert.ok(!onDisk.toString("utf-8").includes("AKIA0123456789ABCDEF"));
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("writeArtifact: rejects path traversal", () => {
|
|
250
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-crew-art-"));
|
|
251
|
+
assert.throws(() => writeArtifact(root, {
|
|
252
|
+
kind: "log", relativePath: "../escape.log", content: "x", producer: "t",
|
|
253
|
+
}), /Invalid artifact path/);
|
|
254
|
+
});
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
### B4 — Thiếu test parity sync vs async cho lock retry (covers BUG-004)
|
|
260
|
+
|
|
261
|
+
**Severity:** Low | **Effort:** ~30 phút | **File:** mở rộng `test/unit/locks-race.test.ts`
|
|
262
|
+
|
|
263
|
+
**Vấn đề:**
|
|
264
|
+
- Fix BUG-004 đồng bộ hoá behaviour sync ↔ async (cả 2 cùng retry tới deadline). `locks-race.test.ts` chỉ test sơ bộ; không có case `rmSync` race + assert lock được acquire sau retry trên cả sync và async.
|
|
265
|
+
|
|
266
|
+
**Fix đề xuất:** thêm 2 test:
|
|
267
|
+
```ts
|
|
268
|
+
test("withRunLockSync retries when rmSync fails once on stale lock", async () => {
|
|
269
|
+
// Create stale lock, monkey-patch fs.rmSync first call → throw EBUSY
|
|
270
|
+
// Assert lock is acquired on retry
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("withRunLock (async) and withRunLockSync exhibit identical stale-lock recovery", async () => {
|
|
274
|
+
// Same scenario for both → both succeed
|
|
275
|
+
});
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
### B5 — `runSetupHook` không filter dangerous env vars khi spawn hook
|
|
281
|
+
|
|
282
|
+
**Severity:** Low (defense-in-depth) | **Effort:** ~15 phút | **File:** `src/worktree/worktree-manager.ts:67-88`
|
|
283
|
+
|
|
284
|
+
**Hiện trạng:**
|
|
285
|
+
```ts
|
|
286
|
+
const result = spawnSync(nodeHook ? process.execPath : hookPath, ..., {
|
|
287
|
+
cwd: worktreePath,
|
|
288
|
+
encoding: "utf-8",
|
|
289
|
+
input: JSON.stringify({...}),
|
|
290
|
+
timeout: cfg.setupHookTimeoutMs ?? 30_000,
|
|
291
|
+
shell: false,
|
|
292
|
+
// ← KHÔNG truyền env → spawn dùng process.env (full inherit)
|
|
293
|
+
});
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**Vấn đề:**
|
|
297
|
+
- Hook chạy với `process.env` đầy đủ → có thể leak `API_KEY`, `*_TOKEN`, `OPENAI_KEY`, etc. Đối lập với `post-checks.ts` và `iteration-hooks.ts` (đã restrict env).
|
|
298
|
+
- AGENTS.md security baseline: "env-secret filtering before spawn".
|
|
299
|
+
|
|
300
|
+
**Fix đề xuất:**
|
|
301
|
+
```ts
|
|
302
|
+
import { sanitizeEnvSecrets } from "../utils/redaction.ts"; // hoặc tương đương
|
|
303
|
+
|
|
304
|
+
const result = spawnSync(..., {
|
|
305
|
+
cwd: worktreePath,
|
|
306
|
+
encoding: "utf-8",
|
|
307
|
+
input: JSON.stringify({...}),
|
|
308
|
+
timeout: cfg.setupHookTimeoutMs ?? 30_000,
|
|
309
|
+
shell: false,
|
|
310
|
+
env: sanitizeEnvSecrets(process.env, { allowList: ["PATH", "HOME", "USERPROFILE", "TEMP", "TMP", "LANG", "PI_*"] }),
|
|
311
|
+
});
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
(Tận dụng helper đã có trong `child-pi.ts` — refactor thành util chung.)
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
### B6 — `prepareTaskWorkspace` không có assertion về branch sanity sau `branchExists`
|
|
319
|
+
|
|
320
|
+
**Severity:** Info | **Effort:** ~10 phút | **File:** `src/worktree/worktree-manager.ts:127-133`
|
|
321
|
+
|
|
322
|
+
**Hiện trạng:**
|
|
323
|
+
```ts
|
|
324
|
+
pruneStaleWorktrees(repoRoot);
|
|
325
|
+
if (branchExists(repoRoot, branch)) {
|
|
326
|
+
git(repoRoot, ["worktree", "add", worktreePath, branch]);
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
**Vấn đề:**
|
|
331
|
+
- Nếu branch hiện đang **checked out** ở một worktree khác (chưa bị prune), `git worktree add <path> <branch>` sẽ fail. Cần catch và emit hint rõ hơn cho user.
|
|
332
|
+
|
|
333
|
+
**Fix đề xuất:** wrap với try/catch, parse error message, throw error có actionable message:
|
|
334
|
+
```ts
|
|
335
|
+
try {
|
|
336
|
+
git(repoRoot, ["worktree", "add", worktreePath, branch]);
|
|
337
|
+
} catch (error) {
|
|
338
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
339
|
+
if (/already checked out at/.test(msg)) {
|
|
340
|
+
throw new Error(`Branch '${branch}' is checked out at another worktree. Run \`team cleanup runId=${manifest.runId} force=true\` or manually remove the conflicting worktree.`);
|
|
341
|
+
}
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
### B7 — `team-tool.ts` có 1 lazy-import `handleRun` không theo style `// LAZY: <reason>`
|
|
349
|
+
|
|
350
|
+
**Severity:** Info | **Effort:** ~2 phút | **File:** `src/extension/team-tool.ts:56-62`
|
|
351
|
+
|
|
352
|
+
**Hiện trạng:**
|
|
353
|
+
```ts
|
|
354
|
+
// Lazy-loaded: run.ts pulls in spawnBackgroundTeamRun, resolveCrewRuntime, etc.
|
|
355
|
+
// Static import fails silently in some jiti contexts (child-process), leaving handleRun undefined.
|
|
356
|
+
import type { handleRun as HandleRunFn } from "./team-tool/run.ts";
|
|
357
|
+
let _cachedHandleRun: typeof HandleRunFn | undefined;
|
|
358
|
+
async function handleRun(...args: Parameters<typeof HandleRunFn>): Promise<...> {
|
|
359
|
+
if (!_cachedHandleRun) {
|
|
360
|
+
const mod = await import("./team-tool/run.ts"); // ← thiếu // LAZY: marker
|
|
361
|
+
...
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
**Fix đề xuất:** thêm marker để đồng nhất với 11 chỗ còn lại đã được đánh dấu:
|
|
367
|
+
```ts
|
|
368
|
+
async function handleRun(...) {
|
|
369
|
+
if (!_cachedHandleRun) {
|
|
370
|
+
// LAZY: run.ts pulls in spawnBackgroundTeamRun + resolveCrewRuntime; also avoids jiti import race.
|
|
371
|
+
const mod = await import("./team-tool/run.ts");
|
|
372
|
+
...
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
### B8 — `redaction-transcript-roundtrip.test.ts` chưa tồn tại (NIT-004)
|
|
380
|
+
|
|
381
|
+
**Severity:** Low | **Effort:** ~30 phút | **File:** thiếu test mới
|
|
382
|
+
|
|
383
|
+
**Vấn đề:**
|
|
384
|
+
- Code review NIT-004 đề xuất test verify rằng transcript trên đĩa và artifact result đều không chứa secret raw. Hiện chưa có test này.
|
|
385
|
+
|
|
386
|
+
**Fix đề xuất:** tạo `test/unit/redaction-transcript-roundtrip.test.ts`:
|
|
387
|
+
1. Tạo fake transcript JSONL với line chứa `OPENAI_API_KEY=sk-abc...`.
|
|
388
|
+
2. Call `appendTranscript` (qua `child-pi` helper export).
|
|
389
|
+
3. Read file → assert không có secret raw.
|
|
390
|
+
4. Call recovery path → assert artifact result cũng đã redact.
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
### B9 — CI grep-check chặn `await import(...)` không marker
|
|
395
|
+
|
|
396
|
+
**Severity:** Low | **Effort:** ~15 phút | **File:** thêm script + GitHub Actions
|
|
397
|
+
|
|
398
|
+
**Vấn đề:**
|
|
399
|
+
- Code review BUG-003 Option A đề xuất "thêm grep-check trong CI để chặn dynamic import không marker". Chưa làm.
|
|
400
|
+
|
|
401
|
+
**Fix đề xuất:** tạo `scripts/check-lazy-imports.mjs`:
|
|
402
|
+
```js
|
|
403
|
+
import { execSync } from "node:child_process";
|
|
404
|
+
const out = execSync(
|
|
405
|
+
`git grep -nE 'await import\\(' -- 'src/**/*.ts' | grep -v '// LAZY:'`,
|
|
406
|
+
{ encoding: "utf-8" }
|
|
407
|
+
).split("\n").filter((l) => l && !l.includes("// LAZY:"));
|
|
408
|
+
// Hoặc kiểm tra dòng ngay trước có chứa `// LAZY:`
|
|
409
|
+
if (out.length > 0) {
|
|
410
|
+
console.error("Dynamic imports without `// LAZY:` marker:\n" + out.join("\n"));
|
|
411
|
+
process.exit(1);
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
Thêm vào `package.json`:
|
|
415
|
+
```json
|
|
416
|
+
"scripts": {
|
|
417
|
+
"check:lazy-imports": "node scripts/check-lazy-imports.mjs",
|
|
418
|
+
"ci": "npm run typecheck && npm run check:lazy-imports && npm test && npm pack --dry-run"
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## Ưu tiên thực hiện
|
|
425
|
+
|
|
426
|
+
| # | Item | Severity | Effort | Khuyến nghị |
|
|
427
|
+
|---|---|---|---|---|
|
|
428
|
+
| 1 | B1 (bash portability) | Medium | 45 phút | Sprint hiện tại — fix 8 test fail trên Windows |
|
|
429
|
+
| 2 | B2 (worktree test) | Medium | 1h | Sprint hiện tại — regression guard cho BUG-005/006 |
|
|
430
|
+
| 3 | B3 (artifact-store test) | Medium | 20 phút | Sprint hiện tại — verify BUG-002 fix |
|
|
431
|
+
| 4 | A1 (branchExists remote-tracking) | Low | 20 phút | Sprint kế tiếp |
|
|
432
|
+
| 5 | B5 (setup-hook env filter) | Low | 15 phút | Sprint kế tiếp — defense-in-depth |
|
|
433
|
+
| 6 | B6 (worktree checked-out hint) | Info | 10 phút | Sprint kế tiếp — UX |
|
|
434
|
+
| 7 | B7 (LAZY marker đồng nhất) | Info | 2 phút | Lúc nào cũng được |
|
|
435
|
+
| 8 | B4 (lock parity test) | Low | 30 phút | Sprint kế tiếp |
|
|
436
|
+
| 9 | B8 (redaction roundtrip test) | Low | 30 phút | Sprint kế tiếp |
|
|
437
|
+
| 10 | B9 (CI grep-check) | Low | 15 phút | Sprint kế tiếp |
|
|
438
|
+
| 11 | A2 (async-runner fallback robust) | Low | 10 phút | Không cấp bách |
|
|
439
|
+
| 12 | A3 (test alias cleanup) | Info | 5 phút | Tuỳ ý |
|
|
440
|
+
|
|
441
|
+
**Tổng effort ưu tiên 1 (medium):** ~2h 5 phút.
|
|
442
|
+
**Tổng effort ưu tiên 2 (low):** ~1h 50 phút.
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## Đề xuất commit batches
|
|
447
|
+
|
|
448
|
+
- **Batch 1 (must-fix):** B1 + B2 + B3 → 1 PR "test+portability hardening" (~2h).
|
|
449
|
+
- **Batch 2 (nice-to-have):** A1 + B5 + B6 + B7 + B9 → 1 PR "worktree + lazy-import polish" (~1h).
|
|
450
|
+
- **Batch 3 (test debt):** B4 + B8 → 1 PR "additional regression tests" (~1h).
|
|
451
|
+
- **Defer:** A2, A3 cho đến khi có user-visible issue.
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
## Điểm tích cực sau review lần 2
|
|
456
|
+
|
|
457
|
+
- Tất cả 7 BUG + 4 NIT từ code review trước đã được fix với commit rõ ràng, có test cho schema.
|
|
458
|
+
- Comment `// LAZY:` đã được thêm consistent ở 11/12 site dynamic import.
|
|
459
|
+
- Lock retry logic giờ thống nhất sync ↔ async (cả 2 đều có deadline + retry).
|
|
460
|
+
- Worktree handle resume crash đúng cách (prune + branchExists fallback).
|
|
461
|
+
- Artifact `contentHash` giờ verifiable bằng `sha256(fs.readFileSync(desc.path))`.
|
|
462
|
+
- Không còn `any` type trong `src/` (grep confirm).
|
|
463
|
+
- `node_modules/jiti` resolution robust với mọi monorepo layout.
|