pi-crew 0.1.51 → 0.2.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.
Files changed (239) hide show
  1. package/CHANGELOG.md +56 -1
  2. package/README.md +176 -781
  3. package/agents/analyst.md +11 -11
  4. package/agents/critic.md +11 -11
  5. package/agents/executor.md +11 -11
  6. package/agents/explorer.md +11 -11
  7. package/agents/planner.md +11 -11
  8. package/agents/reviewer.md +11 -11
  9. package/agents/security-reviewer.md +11 -11
  10. package/agents/test-engineer.md +11 -11
  11. package/agents/verifier.md +70 -11
  12. package/agents/writer.md +11 -11
  13. package/docs/actions-reference.md +595 -0
  14. package/docs/commands-reference.md +347 -0
  15. package/docs/runtime-flow.md +148 -148
  16. package/index.ts +6 -6
  17. package/package.json +99 -99
  18. package/skills/async-worker-recovery/SKILL.md +42 -42
  19. package/skills/context-artifact-hygiene/SKILL.md +52 -52
  20. package/skills/delegation-patterns/SKILL.md +54 -54
  21. package/skills/mailbox-interactive/SKILL.md +40 -40
  22. package/skills/model-routing-context/SKILL.md +39 -39
  23. package/skills/multi-perspective-review/SKILL.md +58 -58
  24. package/skills/observability-reliability/SKILL.md +41 -41
  25. package/skills/orchestration/SKILL.md +157 -157
  26. package/skills/ownership-session-security/SKILL.md +41 -41
  27. package/skills/pi-extension-lifecycle/SKILL.md +39 -39
  28. package/skills/requirements-to-task-packet/SKILL.md +63 -63
  29. package/skills/resource-discovery-config/SKILL.md +41 -41
  30. package/skills/runtime-state-reader/SKILL.md +44 -44
  31. package/skills/secure-agent-orchestration-review/SKILL.md +45 -45
  32. package/skills/state-mutation-locking/SKILL.md +42 -42
  33. package/skills/systematic-debugging/SKILL.md +67 -67
  34. package/skills/ui-render-performance/SKILL.md +39 -39
  35. package/skills/verification-before-done/SKILL.md +57 -57
  36. package/skills/worktree-isolation/SKILL.md +39 -39
  37. package/src/adapters/claude-adapter.ts +25 -0
  38. package/src/adapters/codex-adapter.ts +21 -0
  39. package/src/adapters/cursor-adapter.ts +17 -0
  40. package/src/adapters/export-util.ts +137 -0
  41. package/src/adapters/index.ts +15 -0
  42. package/src/adapters/registry.ts +18 -0
  43. package/src/adapters/types.ts +23 -0
  44. package/src/agents/agent-config.ts +2 -0
  45. package/src/agents/agent-search.ts +98 -98
  46. package/src/agents/discover-agents.ts +2 -1
  47. package/src/config/config.ts +13 -1
  48. package/src/config/drift-detector.ts +211 -0
  49. package/src/config/markers.ts +327 -0
  50. package/src/config/resilient-parser.ts +108 -0
  51. package/src/config/suggestions.ts +74 -0
  52. package/src/extension/cross-extension-rpc.ts +103 -94
  53. package/src/extension/project-init.ts +21 -1
  54. package/src/extension/register.ts +45 -14
  55. package/src/extension/registration/commands.ts +77 -8
  56. package/src/extension/registration/subagent-tools.ts +10 -1
  57. package/src/extension/registration/team-tool.ts +10 -1
  58. package/src/extension/registration/viewers.ts +48 -34
  59. package/src/extension/run-bundle-schema.ts +89 -89
  60. package/src/extension/run-import.ts +25 -1
  61. package/src/extension/run-index.ts +5 -1
  62. package/src/extension/run-maintenance.ts +142 -68
  63. package/src/extension/team-manager-command.ts +10 -1
  64. package/src/extension/team-tool/doctor.ts +28 -3
  65. package/src/extension/team-tool/handle-settings.ts +195 -188
  66. package/src/extension/team-tool/inspect.ts +41 -41
  67. package/src/extension/team-tool/intent-policy.ts +42 -42
  68. package/src/extension/team-tool/lifecycle-actions.ts +27 -8
  69. package/src/extension/team-tool/plan.ts +19 -19
  70. package/src/extension/team-tool/run.ts +12 -1
  71. package/src/extension/team-tool.ts +11 -1
  72. package/src/i18n.ts +184 -184
  73. package/src/observability/exporters/otlp-exporter.ts +92 -77
  74. package/src/prompt/prompt-runtime.ts +72 -72
  75. package/src/runtime/agent-memory.ts +72 -72
  76. package/src/runtime/agent-observability.ts +114 -114
  77. package/src/runtime/async-marker.ts +26 -26
  78. package/src/runtime/attention-events.ts +28 -28
  79. package/src/runtime/auto-resume.ts +100 -0
  80. package/src/runtime/background-runner.ts +11 -1
  81. package/src/runtime/cancellation-token.ts +89 -89
  82. package/src/runtime/cancellation.ts +61 -61
  83. package/src/runtime/capability-inventory.ts +116 -116
  84. package/src/runtime/child-pi.ts +7 -2
  85. package/src/runtime/compaction-summary.ts +271 -0
  86. package/src/runtime/completion-guard.ts +190 -190
  87. package/src/runtime/crash-recovery.ts +33 -0
  88. package/src/runtime/delta-conflict.ts +360 -0
  89. package/src/runtime/direct-run.ts +35 -35
  90. package/src/runtime/foreground-control.ts +82 -82
  91. package/src/runtime/green-contract.ts +46 -46
  92. package/src/runtime/group-join.ts +106 -106
  93. package/src/runtime/heartbeat-gradient.ts +28 -28
  94. package/src/runtime/heartbeat-watcher.ts +124 -124
  95. package/src/runtime/iteration-hooks.ts +262 -0
  96. package/src/runtime/live-agent-control.ts +88 -88
  97. package/src/runtime/live-control-realtime.ts +36 -36
  98. package/src/runtime/live-extension-bridge.ts +150 -150
  99. package/src/runtime/live-irc.ts +92 -92
  100. package/src/runtime/live-session-health.ts +100 -100
  101. package/src/runtime/loop-gates.ts +129 -0
  102. package/src/runtime/metric-parser.ts +40 -0
  103. package/src/runtime/notebook-helpers.ts +90 -90
  104. package/src/runtime/orphan-sentinel.ts +7 -7
  105. package/src/runtime/parallel-research.ts +44 -44
  106. package/src/runtime/phase-progress.ts +217 -0
  107. package/src/runtime/pi-args.ts +38 -11
  108. package/src/runtime/pi-json-output.ts +111 -111
  109. package/src/runtime/pi-spawn.ts +57 -7
  110. package/src/runtime/policy-engine.ts +79 -79
  111. package/src/runtime/post-checks.ts +122 -0
  112. package/src/runtime/progress-event-coalescer.ts +43 -43
  113. package/src/runtime/prose-compressor.ts +164 -164
  114. package/src/runtime/recovery-recipes.ts +74 -74
  115. package/src/runtime/result-extractor.ts +121 -121
  116. package/src/runtime/role-permission.ts +39 -39
  117. package/src/runtime/sensitive-paths.ts +2 -2
  118. package/src/runtime/session-resources.ts +25 -25
  119. package/src/runtime/session-snapshot.ts +59 -59
  120. package/src/runtime/session-usage.ts +79 -79
  121. package/src/runtime/sidechain-output.ts +29 -29
  122. package/src/runtime/stream-preview.ts +177 -177
  123. package/src/runtime/supervisor-contact.ts +59 -59
  124. package/src/runtime/task-display.ts +38 -38
  125. package/src/runtime/task-graph.ts +207 -0
  126. package/src/runtime/task-quality.ts +207 -0
  127. package/src/runtime/task-runner/capabilities.ts +78 -78
  128. package/src/runtime/task-runner/live-executor.ts +7 -1
  129. package/src/runtime/task-runner/progress.ts +119 -119
  130. package/src/runtime/task-runner/prompt-pipeline.ts +64 -64
  131. package/src/runtime/task-runner/result-utils.ts +14 -14
  132. package/src/runtime/task-runner/run-projection.ts +103 -103
  133. package/src/runtime/task-runner/state-helpers.ts +22 -22
  134. package/src/runtime/team-runner.ts +117 -7
  135. package/src/runtime/worker-heartbeat.ts +21 -21
  136. package/src/runtime/worker-startup.ts +57 -57
  137. package/src/runtime/workflow-state.ts +187 -0
  138. package/src/runtime/workspace-tree.ts +298 -298
  139. package/src/schema/config-schema.ts +11 -0
  140. package/src/schema/validation-types.ts +148 -0
  141. package/src/skills/skill-templates.ts +374 -0
  142. package/src/state/active-run-registry.ts +35 -11
  143. package/src/state/atomic-write.ts +33 -26
  144. package/src/state/contracts.ts +1 -0
  145. package/src/state/event-reconstructor.ts +217 -0
  146. package/src/state/locks.ts +2 -13
  147. package/src/state/mailbox.ts +4 -3
  148. package/src/state/state-store.ts +32 -14
  149. package/src/state/task-claims.ts +44 -44
  150. package/src/state/types.ts +9 -0
  151. package/src/state/usage.ts +29 -29
  152. package/src/subagents/async-entry.ts +1 -1
  153. package/src/subagents/index.ts +3 -3
  154. package/src/subagents/live/control.ts +1 -1
  155. package/src/subagents/live/manager.ts +1 -1
  156. package/src/subagents/live/realtime.ts +1 -1
  157. package/src/subagents/live/session-runtime.ts +1 -1
  158. package/src/subagents/manager.ts +1 -1
  159. package/src/subagents/spawn.ts +1 -1
  160. package/src/teams/team-serializer.ts +38 -38
  161. package/src/types/diff.d.ts +18 -18
  162. package/src/ui/crew-footer.ts +101 -101
  163. package/src/ui/crew-select-list.ts +111 -111
  164. package/src/ui/crew-widget.ts +5 -2
  165. package/src/ui/dashboard-panes/cancellation-pane.ts +42 -42
  166. package/src/ui/dashboard-panes/capability-pane.ts +59 -59
  167. package/src/ui/dashboard-panes/mailbox-pane.ts +35 -35
  168. package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
  169. package/src/ui/dashboard-panes/progress-pane.ts +11 -0
  170. package/src/ui/dynamic-border.ts +25 -25
  171. package/src/ui/layout-primitives.ts +106 -106
  172. package/src/ui/loaders.ts +158 -158
  173. package/src/ui/render-coalescer.ts +51 -51
  174. package/src/ui/render-diff.ts +119 -119
  175. package/src/ui/render-scheduler.ts +143 -143
  176. package/src/ui/run-action-dispatcher.ts +10 -1
  177. package/src/ui/spinner.ts +17 -17
  178. package/src/ui/status-colors.ts +58 -58
  179. package/src/ui/syntax-highlight.ts +116 -116
  180. package/src/ui/transcript-entries.ts +258 -258
  181. package/src/utils/completion-dedupe.ts +63 -63
  182. package/src/utils/frontmatter.ts +68 -68
  183. package/src/utils/git.ts +262 -262
  184. package/src/utils/ids.ts +17 -17
  185. package/src/utils/incremental-reader.ts +104 -104
  186. package/src/utils/names.ts +27 -27
  187. package/src/utils/redaction.ts +44 -44
  188. package/src/utils/safe-paths.ts +47 -47
  189. package/src/utils/scan-cache.ts +136 -136
  190. package/src/utils/sleep.ts +40 -26
  191. package/src/utils/task-name-generator.ts +337 -337
  192. package/src/workflows/validate-workflow.ts +40 -40
  193. package/src/worktree/branch-freshness.ts +45 -45
  194. package/teams/default.team.md +12 -12
  195. package/teams/fast-fix.team.md +11 -11
  196. package/teams/implementation.team.md +18 -18
  197. package/teams/parallel-research.team.md +14 -14
  198. package/teams/research.team.md +11 -11
  199. package/teams/review.team.md +12 -12
  200. package/workflows/default.workflow.md +30 -29
  201. package/workflows/fast-fix.workflow.md +23 -22
  202. package/workflows/implementation.workflow.md +43 -43
  203. package/workflows/parallel-research.workflow.md +46 -46
  204. package/workflows/research.workflow.md +22 -22
  205. package/workflows/review.workflow.md +30 -30
  206. package/docs/refactor-tasks-phase3.md +0 -394
  207. package/docs/refactor-tasks-phase4.md +0 -564
  208. package/docs/refactor-tasks-phase5.md +0 -402
  209. package/docs/refactor-tasks-phase6.md +0 -662
  210. package/docs/refactor-tasks.md +0 -1484
  211. package/docs/research/AGENT-EXECUTION-ARCHITECTURE.md +0 -261
  212. package/docs/research/AGENT-LIFECYCLE-COMPARISON.md +0 -111
  213. package/docs/research/AUDIT_OH_MY_PI.md +0 -261
  214. package/docs/research/AUDIT_PI_CREW.md +0 -457
  215. package/docs/research/CAVEMAN-DEEP-RESEARCH.md +0 -281
  216. package/docs/research/COMPARISON_OH_MY_PI_VS_PI_CREW.md +0 -264
  217. package/docs/research/DEEP-RESEARCH-PI-POWERBAR.md +0 -343
  218. package/docs/research/DEEP_RESEARCH_SUBAGENT_ARCHITECTURE.md +0 -480
  219. package/docs/research/GAP_CLOSURE_IMPLEMENTATION_PLAN.md +0 -354
  220. package/docs/research/IMPLEMENTATION_PLAN.md +0 -385
  221. package/docs/research/LIVE-SESSION-PRODUCTION-READY-PLAN.md +0 -502
  222. package/docs/research/OH-MY-PI-DEEP-RESEARCH-v14.7.6.md +0 -266
  223. package/docs/research/REMAINING-GAPS-PLAN.md +0 -363
  224. package/docs/research/SESSION-SUMMARY-2026-05-08.md +0 -146
  225. package/docs/research/UI-RESPONSIVENESS-AUDIT.md +0 -173
  226. package/docs/research-awesome-agent-skills-distillation.md +0 -100
  227. package/docs/research-extension-examples.md +0 -297
  228. package/docs/research-extension-system.md +0 -324
  229. package/docs/research-oh-my-pi-distillation.md +0 -369
  230. package/docs/research-optimization-plan.md +0 -548
  231. package/docs/research-phase10-distillation.md +0 -199
  232. package/docs/research-phase11-distillation.md +0 -201
  233. package/docs/research-phase8-operator-experience-plan.md +0 -819
  234. package/docs/research-phase9-observability-reliability-plan.md +0 -1190
  235. package/docs/research-pi-coding-agent.md +0 -357
  236. package/docs/research-source-pi-crew-reference.md +0 -174
  237. package/docs/research-ui-optimization-plan.md +0 -480
  238. package/docs/source-runtime-refactor-map.md +0 -107
  239. package/src/utils/atomic-write.ts +0 -33
@@ -1,1484 +0,0 @@
1
- # pi-crew Refactor & Optimization Backlog
2
-
3
- > Tài liệu này liệt kê chi tiết các task tối ưu/cải thiện cho `pi-crew/`, sắp xếp theo thứ tự ưu tiên thực hiện.
4
- > Task #1 (tách `register.ts` & `team-tool.ts`) đã hoàn thành — xem CHANGELOG hoặc `src/extension/team-tool/`, `src/extension/registration/`.
5
-
6
- Mỗi task gồm:
7
- - **Vấn đề (Problem)** — bug/inefficiency hiện tại
8
- - **Vị trí (Location)** — file:line
9
- - **Đề xuất (Proposed fix)** — cách sửa
10
- - **Verification** — lệnh test xác nhận
11
- - **Rủi ro (Risk)** — tác động/rollback
12
-
13
- ---
14
-
15
- ## Trạng thái hoàn thành
16
-
17
- - [x] Task #1 — Tách `register.ts` & `team-tool.ts` (đã hoàn thành)
18
- - [x] Task #2 — Sửa `withRunLock` / `withRunLockSync` race condition + async blocking
19
- - [x] Task #3 — Tối ưu `nextSequence` trong `event-log.ts` (O(n²) → O(1))
20
- - [x] Task #4 — Cache `loadRunManifestById` resolution
21
- - [x] Task #5 — Memoize task-graph maps trong `team-runner` loop
22
- - [x] Task #6 — Cleanup timers trong `child-pi.ts`
23
- - [x] Task #7 — `useProjectState` walk-up tìm git root
24
- - [x] Task #8 — Gom hard-coded constants vào `config/defaults.ts`
25
- - [x] Task #9 — Validate config bằng TypeBox
26
- - [x] Task #10 — Tách `ensureMailbox` khỏi read path
27
- - [x] Task #11 — `injectAdaptivePlanIfReady` chạy ít hơn
28
- - [x] Task #12 — Bỏ `jiti` khỏi runtime dependencies
29
- - [x] Task #13 — `atomicWriteFile` non-blocking variant
30
- - [x] Task #14 — `defaultWorkflowConcurrency` đọc từ workflow frontmatter
31
- - [x] Task #15 — Logging cho silent catches
32
- - [x] Task #16 — Cosmetic & cleanup
33
-
34
- ## #2 — Sửa `withRunLock` / `withRunLockSync` race condition + async blocking
35
-
36
- **Priority:** High — ảnh hưởng tính đúng đắn multi-process.
37
-
38
- ### Vấn đề
39
- - File: `src/state/locks.ts`
40
- - `withRunLockSync`:
41
- 1. Check `existsSync(filePath)` → nếu stale thì `rmSync` rồi `writeFileSync(flag: "wx")`. Hai process cùng thấy stale có thể chạy `rmSync` đồng thời, một process `wx` thành công, process kia ném lỗi `EEXIST` ngay → caller phải retry thủ công nhưng không có cơ chế retry.
42
- 2. Lock chỉ tồn tại trong scope `fn()` — nếu `fn()` throw, lock được release qua `finally` (đúng), nhưng khoảng thời gian giữa check stale và create file là race window.
43
- - `withRunLock` (async) chỉ wrap `withRunLockSync`:
44
- ```ts
45
- export async function withRunLock<T>(manifest, fn, options) {
46
- return withRunLockSync(manifest, () => fn(), options);
47
- }
48
- ```
49
- ⇒ Lock giữ trong khi `fn()` async chạy, nhưng `withRunLockSync` trả về **Promise object ngay sau khi gọi `fn()`** chứ không đợi Promise resolve → lock release **trước khi** async work hoàn tất.
50
-
51
- ### Đề xuất
52
-
53
- **Phương án A (nhỏ, bug-fix only):**
54
- ```ts
55
- export async function withRunLock<T>(manifest: TeamRunManifest, fn: () => Promise<T>, options: RunLockOptions = {}): Promise<T> {
56
- const filePath = lockPath(manifest);
57
- const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
58
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
59
- await acquireLockWithRetry(filePath, staleMs);
60
- try {
61
- return await fn();
62
- } finally {
63
- try { fs.rmSync(filePath, { force: true }); } catch {}
64
- }
65
- }
66
-
67
- async function acquireLockWithRetry(filePath: string, staleMs: number): Promise<void> {
68
- const deadline = Date.now() + staleMs * 2;
69
- let attempt = 0;
70
- while (true) {
71
- try {
72
- // O_CREAT | O_EXCL | O_WRONLY (atomic)
73
- const fd = fs.openSync(filePath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 0o644);
74
- fs.writeSync(fd, JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }));
75
- fs.closeSync(fd);
76
- return;
77
- } catch (error) {
78
- const code = (error as NodeJS.ErrnoException).code;
79
- if (code !== "EEXIST") throw error;
80
- // Check stale
81
- try {
82
- const stat = fs.statSync(filePath);
83
- if (Date.now() - stat.mtimeMs > staleMs) {
84
- fs.rmSync(filePath, { force: true });
85
- continue;
86
- }
87
- } catch {}
88
- if (Date.now() > deadline) throw new Error(`Run lock '${filePath}' busy.`);
89
- await new Promise((resolve) => setTimeout(resolve, Math.min(250, 25 * 2 ** attempt)));
90
- attempt++;
91
- }
92
- }
93
- }
94
- ```
95
-
96
- **Phương án B (dùng thư viện):** Cài `proper-lockfile` (~13KB, MIT). API: `lockfile.lock(filePath, { stale, retries })`. Production-grade, nhưng thêm dependency.
97
-
98
- ### Verification
99
- ```powershell
100
- npx tsc --noEmit
101
- node --experimental-strip-types --test test/unit/api-locks.test.ts
102
- node --experimental-strip-types --test test/unit/resume-cancel.test.ts test/unit/mailbox-api.test.ts
103
- ```
104
-
105
- Test mới cần thêm: 2 process đồng thời gọi `withRunLock` cùng manifest → đúng 1 thành công tại một thời điểm.
106
-
107
- ### Rủi ro
108
- - API giữ nguyên (`withRunLock(manifest, fn, options)`) → backward compat.
109
- - Trên Windows, `O_EXCL` đôi khi flaky với antivirus — vẫn cần retry với backoff.
110
-
111
- ---
112
-
113
- ## #3 — Tối ưu `nextSequence` trong `event-log.ts` (O(n²) → O(1))
114
-
115
- **Priority:** High — performance trên run dài (10k+ events).
116
-
117
- ### Vấn đề
118
- - File: `src/state/event-log.ts:46-65`
119
- - Cache hit nhanh, nhưng cache miss = đọc toàn bộ file + `JSON.parse` mỗi line:
120
- ```ts
121
- for (const line of fs.readFileSync(eventsPath, "utf-8").split("\n")) {
122
- const event = JSON.parse(line);
123
- max = Math.max(max, event.metadata?.seq ?? 0);
124
- }
125
- ```
126
- - Mỗi process khác (background async runner, child Pi) ghi event → invalidate cache của process khác → mỗi append ở leader có thể trở thành full scan.
127
- - Kết quả: với 10k events, mỗi append ~5-50ms; tổng cộng O(n²).
128
-
129
- ### Đề xuất
130
-
131
- Lưu seq counter vào file riêng `events.seq`:
132
-
133
- ```ts
134
- // src/state/event-log.ts
135
- function seqFilePath(eventsPath: string): string {
136
- return `${eventsPath}.seq`;
137
- }
138
-
139
- function nextSequence(eventsPath: string): number {
140
- const seqPath = seqFilePath(eventsPath);
141
- let current = 0;
142
- try {
143
- current = Number.parseInt(fs.readFileSync(seqPath, "utf-8").trim(), 10);
144
- if (!Number.isFinite(current) || current < 0) current = 0;
145
- } catch {
146
- // First write or corrupted: scan once to recover
147
- if (fs.existsSync(eventsPath)) {
148
- for (const line of fs.readFileSync(eventsPath, "utf-8").split("\n")) {
149
- if (!line.trim()) continue;
150
- try { current = Math.max(current, (JSON.parse(line) as TeamEvent).metadata?.seq ?? 0); } catch { current++; }
151
- }
152
- }
153
- }
154
- const next = current + 1;
155
- try {
156
- atomicWriteFile(seqPath, String(next));
157
- } catch {
158
- // Best effort; sequence will recover on next read
159
- }
160
- return next;
161
- }
162
- ```
163
-
164
- Hoặc tốt hơn: dùng **incremental tail-read** từ cached size offset:
165
-
166
- ```ts
167
- const sequenceCache = new Map<string, { size: number; mtimeMs: number; seq: number; offset: number }>();
168
-
169
- function nextSequence(eventsPath: string): number {
170
- if (!fs.existsSync(eventsPath)) return 1;
171
- const stat = fs.statSync(eventsPath);
172
- const cached = sequenceCache.get(eventsPath);
173
- if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) return cached.seq + 1;
174
-
175
- let max = cached?.seq ?? 0;
176
- let startOffset = cached && cached.size < stat.size ? cached.offset : 0;
177
- if (cached && cached.size > stat.size) { max = 0; startOffset = 0; } // file rotated
178
-
179
- const fd = fs.openSync(eventsPath, "r");
180
- try {
181
- const buf = Buffer.alloc(stat.size - startOffset);
182
- fs.readSync(fd, buf, 0, buf.length, startOffset);
183
- for (const line of buf.toString("utf-8").split("\n")) {
184
- if (!line.trim()) continue;
185
- try { max = Math.max(max, (JSON.parse(line) as TeamEvent).metadata?.seq ?? 0); } catch { max++; }
186
- }
187
- } finally {
188
- fs.closeSync(fd);
189
- }
190
- sequenceCache.set(eventsPath, { size: stat.size, mtimeMs: stat.mtimeMs, seq: max, offset: stat.size });
191
- return max + 1;
192
- }
193
- ```
194
-
195
- ### Verification
196
- ```powershell
197
- node --experimental-strip-types --test test/unit/event-metadata.test.ts test/unit/run-events-artifacts.test.ts test/unit/phase5-observability.test.ts
198
- ```
199
-
200
- Benchmark mới (optional): append 10k events, đo tổng thời gian — kỳ vọng < 1s thay vì 10-30s.
201
-
202
- ### Rủi ro
203
- - Phương án "seq file" đơn giản hơn, dễ verify; phải chú ý cleanup (`forget`/`prune` xóa luôn `.seq`).
204
- - Phương án incremental đọc đúng nhưng phức tạp hơn, cần test đặc biệt cho file rotation/truncate.
205
-
206
- ---
207
-
208
- ## #4 — Cache `loadRunManifestById` resolution
209
-
210
- **Priority:** Medium — UI overhead (powerbar/sidebar 1Hz).
211
-
212
- ### Vấn đề
213
- - File: `src/state/state-store.ts:104-115`
214
- - Mỗi lần gọi: 2 lần `fs.existsSync` + 2 lần `path.join` + `readFileSync`+`JSON.parse` cho cả manifest và tasks.
215
- - Được gọi từ:
216
- - `live-run-sidebar.ts` (1Hz timer)
217
- - `powerbar-publisher.ts` (1Hz)
218
- - `crew-widget.ts` (1Hz)
219
- - `subagent-helpers.refreshPersistedSubagentRecord`
220
- - `team-tool.handleStatus/Cancel/Resume/Events/Artifacts/Summary/Worktrees/Forget/Cleanup/Export/Api`
221
-
222
- ### Đề xuất
223
-
224
- Thêm tầng cache stat-based (giống `nextSequence`):
225
-
226
- ```ts
227
- // src/state/state-store.ts
228
- interface ManifestCacheEntry {
229
- manifest: TeamRunManifest;
230
- tasks: TeamTaskState[];
231
- manifestMtime: number;
232
- tasksMtime: number;
233
- }
234
- const manifestCache = new Map<string, ManifestCacheEntry>();
235
-
236
- function resolvedStateRoot(cwd: string, runId: string): string | undefined {
237
- const projectPath = path.join(projectPiRoot(cwd), "teams", "state", "runs", runId);
238
- if (fs.existsSync(projectPath)) return projectPath;
239
- const userPath = path.join(userPiRoot(), "extensions", "pi-crew", "runs", "state", "runs", runId);
240
- return fs.existsSync(userPath) ? userPath : undefined;
241
- }
242
-
243
- export function loadRunManifestById(cwd: string, runId: string): { manifest: TeamRunManifest; tasks: TeamTaskState[] } | undefined {
244
- const stateRoot = resolvedStateRoot(cwd, runId);
245
- if (!stateRoot) return undefined;
246
- const manifestPath = path.join(stateRoot, "manifest.json");
247
- const tasksPath = path.join(stateRoot, "tasks.json");
248
-
249
- let mStat: fs.Stats | undefined;
250
- let tStat: fs.Stats | undefined;
251
- try { mStat = fs.statSync(manifestPath); } catch { return undefined; }
252
- try { tStat = fs.statSync(tasksPath); } catch {}
253
-
254
- const cacheKey = `${stateRoot}`;
255
- const cached = manifestCache.get(cacheKey);
256
- if (cached && cached.manifestMtime === mStat.mtimeMs && cached.tasksMtime === (tStat?.mtimeMs ?? 0)) {
257
- return { manifest: cached.manifest, tasks: cached.tasks };
258
- }
259
-
260
- const manifest = readJsonFile<TeamRunManifest>(manifestPath);
261
- if (!manifest) return undefined;
262
- const tasks = readJsonFile<TeamTaskState[]>(tasksPath) ?? [];
263
- manifestCache.set(cacheKey, { manifest, tasks, manifestMtime: mStat.mtimeMs, tasksMtime: tStat?.mtimeMs ?? 0 });
264
- return { manifest, tasks };
265
- }
266
- ```
267
-
268
- Quan trọng: `saveRunManifest` / `saveRunTasks` phải invalidate cache:
269
- ```ts
270
- export function saveRunManifest(manifest: TeamRunManifest): void {
271
- atomicWriteJson(path.join(manifest.stateRoot, "manifest.json"), manifest);
272
- manifestCache.delete(manifest.stateRoot); // OR: refresh entry
273
- }
274
- ```
275
-
276
- ### Verification
277
- ```powershell
278
- node --experimental-strip-types --test test/unit/state-store.test.ts test/unit/team-run.test.ts test/unit/resume-cancel.test.ts test/unit/run-dashboard.test.ts test/unit/live-run-sidebar.test.ts
279
- ```
280
-
281
- ### Rủi ro
282
- - Cross-process: process A cache → process B ghi → process A vẫn dùng cache cũ trong tầng mtime check. mtime resolution thường ≥1ms nên acceptable.
283
- - **Memory leak**: cache không bound. Thêm LRU max 50 entries.
284
-
285
- ---
286
-
287
- ## #5 — Memoize task-graph maps trong `team-runner` loop
288
-
289
- **Priority:** Medium — CPU/GC overhead trên run lớn (>100 tasks).
290
-
291
- ### Vấn đề
292
- - File: `src/runtime/task-graph-scheduler.ts`
293
- - Mỗi function (`getReadyTasks`, `markTaskRunning`, `markTaskDone`, `cancelTaskSubtree`, `failTaskAndBlockChildren`, `taskGraphSnapshot`) đều build lại 3 maps:
294
- - `completedStepIds(tasks)` — Set
295
- - `taskById(tasks)` — Map
296
- - `stepIdToTaskId(tasks)` — Map
297
- - Trong `executeTeamRun` loop, `refreshTaskGraphQueues` được gọi nhiều lần per iteration:
298
- - `team-runner.ts:240` (getReadyTasks)
299
- - `team-runner.ts:228` (taskGraphSnapshot)
300
- - Mỗi snapshot/refresh = 3 maps × O(n)
301
-
302
- ### Đề xuất
303
-
304
- Build maps 1 lần ở caller, truyền xuống:
305
-
306
- ```ts
307
- // task-graph-scheduler.ts
308
- export interface TaskGraphIndex {
309
- doneSteps: Set<string>;
310
- byId: Map<string, TeamTaskState>;
311
- byStepId: Map<string, string>;
312
- }
313
-
314
- export function buildTaskGraphIndex(tasks: TeamTaskState[]): TaskGraphIndex {
315
- return {
316
- doneSteps: completedStepIds(tasks),
317
- byId: taskById(tasks),
318
- byStepId: stepIdToTaskId(tasks),
319
- };
320
- }
321
-
322
- export function refreshTaskGraphQueues(tasks: TeamTaskState[], index?: TaskGraphIndex): TeamTaskState[] {
323
- const idx = index ?? buildTaskGraphIndex(tasks);
324
- return tasks.map((task) => {
325
- // ... use idx.doneSteps, idx.byId, idx.byStepId
326
- });
327
- }
328
- ```
329
-
330
- Trong `team-runner.executeTeamRun`:
331
- ```ts
332
- while (tasks.some((task) => task.status === "queued")) {
333
- const idx = buildTaskGraphIndex(tasks);
334
- const snapshot = taskGraphSnapshot(tasks, idx);
335
- const readyBatch = getReadyTasks(tasks, concurrency.selectedCount, idx);
336
- // ...
337
- tasks = mergeTaskUpdates(tasks, results);
338
- // (rebuild index after mutations)
339
- }
340
- ```
341
-
342
- Hoặc: memoize bằng WeakMap với task array reference làm key:
343
- ```ts
344
- const indexCache = new WeakMap<TeamTaskState[], TaskGraphIndex>();
345
- function ensureIndex(tasks: TeamTaskState[]): TaskGraphIndex {
346
- let idx = indexCache.get(tasks);
347
- if (!idx) { idx = buildTaskGraphIndex(tasks); indexCache.set(tasks, idx); }
348
- return idx;
349
- }
350
- ```
351
- (Pattern này hoạt động vì `tasks.map()` luôn trả mảng mới → cache key đổi tự động khi mutation.)
352
-
353
- ### Verification
354
- ```powershell
355
- node --experimental-strip-types --test test/unit/task-graph-scheduler.test.ts test/unit/phase3-runtime.test.ts test/unit/phase4-runtime.test.ts test/unit/implementation-fanout.test.ts
356
- ```
357
-
358
- ### Rủi ro
359
- - Refactor lan rộng (5-6 callsite) nhưng giữ API cũ với optional param `index?` → backward compat.
360
-
361
- ---
362
-
363
- ## #6 — Cleanup timers trong `child-pi.ts`
364
-
365
- **Priority:** Medium — leak nhẹ nhưng dễ tích lũy.
366
-
367
- ### Vấn đề
368
- - File: `src/runtime/child-pi.ts:31-39`
369
- - `killProcessTree` schedule SIGKILL sau `HARD_KILL_MS`:
370
- ```ts
371
- setTimeout(() => { try { process.kill(-pid, "SIGKILL"); } catch { ... } }, HARD_KILL_MS).unref?.();
372
- ```
373
- - Không clear khi child exit bình thường giữa SIGTERM-SIGKILL window. Trên hệ thống chạy nhiều run, hàng trăm timer pending mỗi giờ.
374
-
375
- ### Đề xuất
376
-
377
- Track timer và clear trong `child.on('exit')`:
378
-
379
- ```ts
380
- function killProcessTree(pid: number | undefined, child?: ChildProcess): void {
381
- if (!pid || !Number.isInteger(pid) || pid <= 0) return;
382
- try {
383
- if (process.platform === "win32") {
384
- spawn("taskkill", ["/pid", String(pid), "/t", "/f"], { stdio: "ignore", windowsHide: true });
385
- return;
386
- }
387
- try { process.kill(-pid, "SIGTERM"); } catch { process.kill(pid, "SIGTERM"); }
388
- const killTimer = setTimeout(() => {
389
- try { process.kill(-pid, "SIGKILL"); } catch { try { process.kill(pid, "SIGKILL"); } catch {} }
390
- }, HARD_KILL_MS);
391
- killTimer.unref?.();
392
- child?.once("exit", () => clearTimeout(killTimer));
393
- } catch {
394
- // Ignore shutdown races.
395
- }
396
- }
397
- ```
398
-
399
- Caller đã có `child` reference (từ `activeChildProcesses`), nên truyền xuống.
400
-
401
- ### Verification
402
- ```powershell
403
- node --experimental-strip-types --test test/unit/pi-spawn.test.ts test/unit/mock-child-run.test.ts
404
- ```
405
-
406
- Manual check (Linux/Mac): chạy `node -e "process.exit()"` trong test, đảm bảo không có timer leak qua `process._getActiveHandles()`.
407
-
408
- ### Rủi ro
409
- - Thấp: chỉ thêm clearTimeout khi exit. Behavior không đổi ở fast-exit case (timer vẫn fire nếu child chưa exit).
410
-
411
- ---
412
-
413
- ## #7 — `useProjectState` walk-up tìm git root
414
-
415
- **Priority:** Medium — DX bug trong monorepo.
416
-
417
- ### Vấn đề
418
- - File: `src/state/state-store.ts:21-23`
419
- ```ts
420
- function useProjectState(cwd: string): boolean {
421
- return fs.existsSync(path.join(cwd, ".pi")) || fs.existsSync(path.join(cwd, ".git"));
422
- }
423
- ```
424
- - Nếu user `cd` vào subfolder của repo (ví dụ `pi-crew/src/`), không tìm thấy `.git` ngay → fallback `~/.pi/agent/extensions/pi-crew/runs/...` → state không phải project-local nữa.
425
- - Tương tự: `projectPiRoot(cwd) = path.join(cwd, ".pi")` → `.pi/` được tạo trong subfolder, không phải repo root.
426
-
427
- ### Đề xuất
428
-
429
- ```ts
430
- // src/utils/paths.ts
431
- export function findRepoRoot(cwd: string): string | undefined {
432
- let current = path.resolve(cwd);
433
- const root = path.parse(current).root;
434
- while (current !== root) {
435
- if (fs.existsSync(path.join(current, ".git")) || fs.existsSync(path.join(current, ".pi"))) {
436
- return current;
437
- }
438
- const parent = path.dirname(current);
439
- if (parent === current) break;
440
- current = parent;
441
- }
442
- return undefined;
443
- }
444
-
445
- export function projectPiRoot(cwd: string): string {
446
- return path.join(findRepoRoot(cwd) ?? cwd, ".pi");
447
- }
448
- ```
449
-
450
- Và `useProjectState`:
451
- ```ts
452
- function useProjectState(cwd: string): boolean {
453
- return findRepoRoot(cwd) !== undefined;
454
- }
455
- ```
456
-
457
- ### Verification
458
- ```powershell
459
- node --experimental-strip-types --test test/unit/state-store.test.ts test/unit/team-run.test.ts test/unit/discovery.test.ts test/unit/project-init.test.ts
460
- ```
461
-
462
- Test mới: tạo fixture `tmp/repo/.git/`, chạy `loadConfig(tmp/repo/sub/folder)` → expect path resolve về `tmp/repo/.pi`.
463
-
464
- ### Rủi ro
465
- - **Medium:** Có thể đổi semantics nếu user cố ý dùng subfolder làm pi-crew root. Cần check `discovery.test.ts` và `project-init.test.ts` không assume `cwd === root`.
466
- - Workaround: thêm config flag `pi-crew.useGitRoot: false` để giữ behavior cũ.
467
-
468
- ---
469
-
470
- ## #8 — Gom hard-coded constants vào `config/defaults.ts`
471
-
472
- **Priority:** Low — DX/maintainability.
473
-
474
- ### Vấn đề
475
- Các magic numbers rải rác:
476
- - `child-pi.ts`: `POST_EXIT_STDIO_GUARD_MS=3000`, `FINAL_DRAIN_MS=5000`, `HARD_KILL_MS=3000`, `MAX_CAPTURE_BYTES=256*1024`, `MAX_ASSISTANT_TEXT_CHARS=8192`, ...
477
- - `concurrency.ts`: `defaultWorkflowConcurrency` switch-case.
478
- - `event-log.ts`: `TERMINAL_EVENT_TYPES` set.
479
- - `state-store.ts`: paths.
480
- - `locks.ts`: `DEFAULT_STALE_MS=30_000`.
481
-
482
- ### Đề xuất
483
-
484
- Tạo `src/config/defaults.ts`:
485
- ```ts
486
- export const CrewDefaults = {
487
- childPi: {
488
- postExitStdioGuardMs: 3000,
489
- finalDrainMs: 5000,
490
- hardKillMs: 3000,
491
- maxCaptureBytes: 256 * 1024,
492
- maxAssistantTextChars: 8192,
493
- maxToolResultChars: 1024,
494
- maxToolInputChars: 2048,
495
- maxCompactContentChars: 4096,
496
- },
497
- locks: {
498
- defaultStaleMs: 30_000,
499
- },
500
- concurrency: {
501
- workflows: { "parallel-research": 4, research: 2, implementation: 2, review: 2, default: 2 } as Record<string, number>,
502
- fallback: 1,
503
- },
504
- ui: {
505
- widgetRefreshMs: 1000,
506
- sidebarRefreshMs: 1000,
507
- },
508
- } as const;
509
- ```
510
-
511
- Cập nhật từng file thay vì hard-code. Cho phép override qua `loadConfig(cwd).config`:
512
- ```ts
513
- export function effectiveLimits(config: PiTeamsConfig): typeof CrewDefaults & { /* overrides */ } {
514
- return {
515
- ...CrewDefaults,
516
- childPi: { ...CrewDefaults.childPi, ...(config.runtime?.childPi ?? {}) },
517
- };
518
- }
519
- ```
520
-
521
- ### Verification
522
- ```powershell
523
- npx tsc --noEmit
524
- node --experimental-strip-types --test # full suite
525
- ```
526
-
527
- ### Rủi ro
528
- - Thấp: chỉ refactor constants. Test phải pass không đổi.
529
-
530
- ---
531
-
532
- ## #9 — Validate config bằng TypeBox
533
-
534
- **Priority:** Low — chuẩn hóa, bắt config invalid sớm.
535
-
536
- ### Vấn đề
537
- - File: `src/extension/team-tool/config-patch.ts`
538
- - `configPatchFromConfig` validate manual: ~40 dòng `typeof x === "number" && Number.isInteger(x) && x > 0 ? x : undefined`.
539
- - TypeBox đã có cho tool params (`team-tool-schema.ts`), nhưng config schema được load từ `loadConfig` không qua TypeBox — chỉ JSON parse.
540
-
541
- ### Đề xuất
542
-
543
- Thêm `src/schema/config-schema.ts`:
544
- ```ts
545
- import { Type, type Static } from "typebox";
546
-
547
- export const PiTeamsLimitsSchema = Type.Object({
548
- maxConcurrentWorkers: Type.Optional(Type.Integer({ minimum: 1 })),
549
- maxTaskDepth: Type.Optional(Type.Integer({ minimum: 1 })),
550
- // ...
551
- });
552
-
553
- export const PiTeamsRuntimeSchema = Type.Object({
554
- mode: Type.Optional(Type.Union([Type.Literal("auto"), Type.Literal("scaffold"), Type.Literal("child-process"), Type.Literal("live-session")])),
555
- // ...
556
- });
557
-
558
- export const PiTeamsConfigSchema = Type.Object({
559
- asyncByDefault: Type.Optional(Type.Boolean()),
560
- executeWorkers: Type.Optional(Type.Boolean()),
561
- limits: Type.Optional(PiTeamsLimitsSchema),
562
- runtime: Type.Optional(PiTeamsRuntimeSchema),
563
- // ...
564
- });
565
-
566
- export type PiTeamsConfig = Static<typeof PiTeamsConfigSchema>;
567
- ```
568
-
569
- Trong `config.ts`:
570
- ```ts
571
- import { Value } from "typebox/value";
572
- import { PiTeamsConfigSchema } from "../schema/config-schema.ts";
573
-
574
- export function loadConfig(cwd: string): { config: PiTeamsConfig; path: string; error?: string } {
575
- const raw = readJsonFile(...);
576
- const errors = [...Value.Errors(PiTeamsConfigSchema, raw)];
577
- if (errors.length) {
578
- return { config: defaultConfig(), path, error: errors.map(e => `${e.path}: ${e.message}`).join("; ") };
579
- }
580
- return { config: Value.Cast(PiTeamsConfigSchema, raw), path };
581
- }
582
- ```
583
-
584
- ### Verification
585
- ```powershell
586
- node --experimental-strip-types --test test/unit/config.test.ts test/unit/config-update.test.ts test/unit/project-config.test.ts
587
- ```
588
-
589
- ### Rủi ro
590
- - Medium: thay đổi config validation → invalid config (đang silently bỏ qua) sẽ thành error → cần backward compat (downgrade error → warning hoặc dùng `Value.Cast` để cast best-effort).
591
-
592
- ---
593
-
594
- ## #10 — Tách `ensureMailbox` khỏi read path
595
-
596
- **Priority:** Low — side effect không cần thiết ở read.
597
-
598
- ### Vấn đề
599
- - File: `src/state/mailbox.ts:97-103`
600
- - `readMailbox()` luôn gọi `ensureMailbox()` → `mkdirSync` + 4× `writeFileSync` empty + 1× `writeFileSync` delivery.json nếu thiếu.
601
- - Read path không nên có side effects.
602
-
603
- ### Đề xuất
604
- ```ts
605
- function safeReadMailboxFile(filePath: string, direction: MailboxDirection): MailboxMessage[] {
606
- if (!fs.existsSync(filePath)) return [];
607
- return readMailboxFile(filePath, direction);
608
- }
609
-
610
- export function readMailbox(manifest: TeamRunManifest, direction?: MailboxDirection, taskId?: string): MailboxMessage[] {
611
- // No ensureMailbox here
612
- const directions = direction ? [direction] : ["inbox", "outbox"] as const;
613
- return directions.flatMap((item) => safeReadMailboxFile(mailboxPath(manifest, item, taskId), item))
614
- .sort((a, b) => a.createdAt.localeCompare(b.createdAt));
615
- }
616
-
617
- export function appendMailboxMessage(...) {
618
- ensureMailbox(manifest, message.taskId); // Only here
619
- // ...
620
- }
621
- ```
622
-
623
- ### Verification
624
- ```powershell
625
- node --experimental-strip-types --test test/unit/mailbox-api.test.ts test/unit/mailbox-validation.test.ts
626
- ```
627
-
628
- ### Rủi ro
629
- - Thấp: append vẫn ensure dir → cấu trúc mailbox luôn được tạo khi cần.
630
-
631
- ---
632
-
633
- ## #11 — `injectAdaptivePlanIfReady` chạy ít hơn
634
-
635
- **Priority:** Low-Medium — performance + log noise.
636
-
637
- ### Vấn đề
638
- - File: `src/runtime/team-runner.ts`
639
- - `injectAdaptivePlanIfReady` được gọi 3 lần per scheduler iteration:
640
- 1. Initial (line ~244)
641
- 2. Mỗi vòng while (line ~268)
642
- 3. Sau mỗi batch (line ~308)
643
- - Mỗi lần đọc `assess` artifact + parse JSON nếu chưa inject. Đã có guard "tasks.some(adaptive-)" nhưng vẫn execute regex/IO.
644
-
645
- ### Đề xuất
646
-
647
- Track flag trong manifest hoặc local state:
648
- ```ts
649
- let adaptivePlanInjected = tasks.some((task) => task.stepId?.startsWith("adaptive-"));
650
- let adaptivePlanFailed = false;
651
-
652
- // Replace 3 invocations with:
653
- function maybeInjectAdaptive() {
654
- if (adaptivePlanInjected || adaptivePlanFailed) return;
655
- const r = injectAdaptivePlanIfReady({ manifest, tasks, workflow, team: input.team });
656
- if (r.missingPlan) { adaptivePlanFailed = true; /* mark blocked */ }
657
- if (r.injected) { adaptivePlanInjected = true; tasks = r.tasks; workflow = r.workflow; }
658
- }
659
- ```
660
-
661
- ### Verification
662
- ```powershell
663
- node --experimental-strip-types --test test/unit/adaptive-implementation.test.ts test/unit/implementation-fanout.test.ts
664
- ```
665
-
666
- ### Rủi ro
667
- - Thấp: chỉ thay đổi điều kiện trigger, không thay đổi logic inject.
668
-
669
- ---
670
-
671
- ## #12 — Bỏ `jiti` khỏi runtime dependencies
672
-
673
- **Priority:** Low — install size.
674
-
675
- ### Vấn đề
676
- - `package.json` declare `"jiti": "^2.6.1"` trong `dependencies`.
677
- - Grep trong source: không có `import.*jiti` nào trong `src/`.
678
-
679
- ### Đề xuất
680
-
681
- ```powershell
682
- # Verify nothing imports jiti
683
- Select-String -Path "D:\my\my_project\pi-crew\src\*","D:\my\my_project\pi-crew\index.ts" -Pattern "jiti" -Recurse
684
- ```
685
-
686
- Nếu không có hit → remove khỏi `dependencies`:
687
- ```json
688
- "dependencies": {
689
- "typebox": "^1.1.24"
690
- }
691
- ```
692
-
693
- ### Verification
694
- ```powershell
695
- npm install
696
- npx tsc --noEmit
697
- npm test
698
- npm pack --dry-run
699
- ```
700
-
701
- ### Rủi ro
702
- - Thấp. Nếu dynamic require thì sẽ fail rõ ràng.
703
-
704
- ---
705
-
706
- ## #13 — `atomicWriteFile` non-blocking variant
707
-
708
- **Priority:** Low — chỉ matter trên hot path.
709
-
710
- ### Vấn đề
711
- - File: `src/state/atomic-write.ts:5-9`
712
- - `sleepSync` dùng `Atomics.wait` block thread chính:
713
- ```ts
714
- function sleepSync(ms: number): void {
715
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
716
- }
717
- ```
718
- - `__test__renameWithRetry` retry up to 20 lần với backoff → có thể block 5+ giây trên main thread (Windows EBUSY/EPERM).
719
-
720
- ### Đề xuất
721
-
722
- Thêm async variant cho hot path (saveRunTasks/saveRunManifest trong loop):
723
- ```ts
724
- export async function atomicWriteFileAsync(filePath: string, content: string): Promise<void> {
725
- await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
726
- const tempPath = `${filePath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
727
- try {
728
- await fs.promises.writeFile(tempPath, content, "utf-8");
729
- await renameWithRetryAsync(tempPath, filePath);
730
- } catch (error) {
731
- try { await fs.promises.rm(tempPath, { force: true }); } catch {}
732
- throw error;
733
- }
734
- }
735
-
736
- async function renameWithRetryAsync(tempPath: string, filePath: string, retries = 20): Promise<void> {
737
- for (let attempt = 0; attempt <= retries; attempt++) {
738
- try { await fs.promises.rename(tempPath, filePath); return; }
739
- catch (error) {
740
- if (!isRetryableRenameError(error) || attempt === retries) throw error;
741
- await new Promise((r) => setTimeout(r, Math.min(250, 10 * 2 ** attempt)));
742
- }
743
- }
744
- }
745
- ```
746
-
747
- Dùng trong `saveRunTasks`/`saveRunManifest` (gọi từ async context):
748
- ```ts
749
- export async function saveRunManifestAsync(manifest: TeamRunManifest): Promise<void> {
750
- await atomicWriteFileAsync(path.join(manifest.stateRoot, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
751
- }
752
- ```
753
-
754
- ### Verification
755
- ```powershell
756
- node --experimental-strip-types --test test/unit/atomic-write.test.ts test/unit/state-store.test.ts
757
- ```
758
-
759
- ### Rủi ro
760
- - Medium: phải convert call chain sang async → nhiều file thay đổi. Có thể chỉ apply cho 1-2 hot path để tránh ripple.
761
-
762
- ---
763
-
764
- ## #14 — `defaultWorkflowConcurrency` đọc từ workflow frontmatter
765
-
766
- **Priority:** Low — DX.
767
-
768
- ### Vấn đề
769
- - File: `src/runtime/concurrency.ts:18-23`
770
- ```ts
771
- export function defaultWorkflowConcurrency(workflowName: string): number {
772
- if (workflowName === "parallel-research") return 4;
773
- if (workflowName === "research") return 2;
774
- // ...
775
- }
776
- ```
777
- - User custom workflow không thể set default concurrency mà phải pass `team.maxConcurrency`.
778
-
779
- ### Đề xuất
780
-
781
- `WorkflowConfig` đã có frontmatter loader. Thêm field:
782
- ```ts
783
- // src/workflows/workflow-config.ts
784
- export interface WorkflowConfig {
785
- // ...
786
- maxConcurrency?: number;
787
- }
788
- ```
789
-
790
- Cập nhật `resolveBatchConcurrency`:
791
- ```ts
792
- export interface ResolveBatchConcurrencyInput {
793
- workflowName: string;
794
- workflowMaxConcurrency?: number; // NEW
795
- teamMaxConcurrency?: number;
796
- limitMaxConcurrentWorkers?: number;
797
- readyCount: number;
798
- // ...
799
- }
800
-
801
- const requested = limitMax ?? teamMax ?? workflowMax ?? defaultByName ?? 1;
802
- ```
803
-
804
- Trong `team-runner.ts:executeTeamRun`:
805
- ```ts
806
- const concurrency = resolveBatchConcurrency({
807
- workflowName: workflow.name,
808
- workflowMaxConcurrency: workflow.maxConcurrency, // pass through
809
- // ...
810
- });
811
- ```
812
-
813
- ### Verification
814
- ```powershell
815
- node --experimental-strip-types --test test/unit/concurrency.test.ts test/unit/parallel-research-dynamic.test.ts test/unit/workflow-validation.test.ts
816
- ```
817
-
818
- ### Rủi ro
819
- - Thấp: thêm optional field, backward compat.
820
-
821
- ---
822
-
823
- ## #15 — Logging cho silent catches
824
-
825
- **Priority:** Low — observability.
826
-
827
- ### Vấn đề
828
-
829
- Nhiều `try { ... } catch {}` nuốt lỗi:
830
- - `child-pi.ts`: `try { this.input.onJsonEvent?.(event); } catch {}` (line ~165)
831
- - `state-store.ts`: lock cleanup `catch {}`
832
- - `event-log.ts`: cache update `catch {}` (line ~93)
833
- - `team-tool.ts:handleCancel`: `try { saveCrewAgents(...); } catch {}`
834
- - `team-tool.ts:handleCancel`: `try { writeForegroundInterruptRequest(...); } catch {}`
835
-
836
- ### Đề xuất
837
-
838
- Thêm helper `logInternalError`:
839
- ```ts
840
- // src/utils/log.ts
841
- export function logInternalError(scope: string, error: unknown, eventsPath?: string): void {
842
- const message = error instanceof Error ? error.message : String(error);
843
- if (process.env.PI_TEAMS_DEBUG) {
844
- console.error(`[pi-crew:${scope}] ${message}`);
845
- }
846
- if (eventsPath) {
847
- try { appendEvent(eventsPath, { type: "internal.error", runId: "", message: `${scope}: ${message}` }); } catch {}
848
- }
849
- }
850
- ```
851
-
852
- Thay `catch {}` bằng `catch (e) { logInternalError("...", e); }` ở các điểm critical.
853
-
854
- ### Verification
855
- ```powershell
856
- $env:PI_TEAMS_DEBUG = "1"
857
- node --experimental-strip-types --test test/unit/runtime-hardening.test.ts
858
- ```
859
-
860
- ### Rủi ro
861
- - Thấp: chỉ thêm observability, không thay đổi behavior khi `PI_TEAMS_DEBUG` chưa set.
862
-
863
- ---
864
-
865
- ## #16 — Cosmetic & cleanup
866
-
867
- ### 16a. `tsconfig.json` duplicate include
868
-
869
- ```json
870
- "include": [
871
- "*.ts",
872
- "src/**/*.ts",
873
- "src/**/*.ts" // <-- duplicate
874
- ]
875
- ```
876
-
877
- Sửa thành:
878
- ```json
879
- "include": [
880
- "*.ts",
881
- "src/**/*.ts",
882
- "test/**/*.ts"
883
- ]
884
- ```
885
-
886
- ### 16b. Test folder structure
887
-
888
- 90 unit tests không phân loại. Đề xuất:
889
- ```
890
- test/
891
- unit/ # pure logic (no fs, no spawn)
892
- integration/ # spawn child Pi, tạo runs
893
- fixtures/
894
- ```
895
-
896
- Cập nhật `package.json`:
897
- ```json
898
- "test:unit": "node --experimental-strip-types --test test/unit/*.test.ts",
899
- "test:integration": "node --experimental-strip-types --test test/integration/*.test.ts",
900
- "test": "npm run test:unit && npm run test:integration"
901
- ```
902
-
903
- Move các file `phase[N]-*.test.ts`, `worktree-run.test.ts`, `mock-child-*.test.ts` sang `integration/`.
904
-
905
- ### 16c. Subagent stuck-blocked notification
906
-
907
- File: `src/runtime/subagent-manager.ts`
908
- - `SubagentManager` callback chỉ trigger khi `completed/failed/cancelled/error`. Status `blocked` (run-level) không trigger.
909
- - Đề xuất: khi `record.runId` linked manifest có status `blocked`, tự động gọi callback.
910
-
911
- ### Verification
912
- ```powershell
913
- npx tsc --noEmit
914
- npm test
915
- ```
916
-
917
- ### Rủi ro
918
- - Thấp.
919
-
920
- ---
921
-
922
- ## Thứ tự thực hiện đề xuất
923
-
924
- 1. **#2** — Lock fix (correctness, multi-process)
925
- 2. **#3** — Sequence O(n²) → O(1) (performance)
926
- 3. **#4** — Cache loadRunManifestById (UI 1Hz overhead)
927
- 4. **#6** — Cleanup child-pi timers (memory leak)
928
- 5. **#7** — Walk-up git root (DX bug)
929
- 6. **#5** — Memoize task-graph maps (CPU)
930
- 7. **#11** — Adaptive plan trigger optimization
931
- 8. **#10** — ensureMailbox khỏi read path
932
- 9. **#8** — Gom constants
933
- 10. **#13** — Async atomic write
934
- 11. **#14** — Workflow.maxConcurrency
935
- 12. **#9** — TypeBox validate config
936
- 13. **#12** — Drop jiti
937
- 14. **#15** — Internal error logging
938
- 15. **#16** — Cosmetic
939
-
940
- ---
941
-
942
- ## Quy ước test cho mỗi task
943
-
944
- Theo workflow `~/.factory/AGENTS.md` mục 11:
945
-
946
- ```powershell
947
- # Sau mỗi thay đổi:
948
- Set-Location D:\my\my_project\pi-crew
949
- npx tsc --noEmit # Type check
950
- node --experimental-strip-types --test test/unit/<related>.test.ts # Targeted tests
951
- npm test # Full suite (nếu thay đổi module core)
952
- ```
953
-
954
- PR template (tham khảo `~/.factory/AGENTS.md` mục 10):
955
- ```
956
- Summary: <task #N: short description>
957
- Plan: ...
958
- Files & Rationale: ...
959
- Tests: ...
960
- Verification:
961
- - npx tsc --noEmit → Passed
962
- - node --experimental-strip-types --test ... → N pass
963
- Risks & Rollback: ...
964
- ```
965
-
966
- ---
967
-
968
- ## Phase 2 — Follow-up Tasks (sau review #2–#16)
969
-
970
- > Phát hiện trong review ngày 28/04/2026 sau khi các task #2–#16 đã hoàn thành. Đây là các vấn đề lộ ra do fix tsconfig (#15) và một số chỗ chưa hoàn thiện.
971
-
972
- ### Trạng thái Phase 2
973
-
974
- - [x] Task #17 — Fix 71 TS errors trong test files (CRITICAL)
975
- - [x] Task #18 — LRU bound cho `manifestCache` (MEDIUM)
976
- - [x] Task #19 — Cross-process cache staleness check (MEDIUM)
977
- - [x] Task #20 — Tách `ensureMailbox` (LOW)
978
- - [x] Task #21 — Giảm circular import giữa `team-tool.ts` ↔ `tool-result.ts` (đã fix trong review)
979
- - [x] Task #22 — Codemod `TeamContext` import (LOW)
980
- - [x] Task #23 — Subagent stuck-blocked notification (LOW)
981
- - [x] Task #24 — TypeBox config validation warnings (MEDIUM)
982
- - [x] Task #25 — `atomicWriteFileAsync` idempotent retry (LOW)
983
-
984
- ### Thứ tự thực hiện đề xuất
985
-
986
- 1. **#17** ✅ (CRITICAL — chặn CI) → hoàn thành
987
- 2. **#18** + **#19** (MEDIUM — cùng file `state-store.ts`, gộp 1 PR)
988
- 3. **#24** (MEDIUM — UX cải thiện rõ ràng)
989
- 4. **#20** + **#22** (LOW — refactor cosmetic)
990
- 5. **#23** (LOW — feature mới)
991
- 6. **#25** (LOW — edge case hiếm)
992
-
993
- ---
994
-
995
- ### Task #17 — Fix 71 TypeScript errors trong test files (CRITICAL)
996
-
997
- **Vấn đề:**
998
- Sau khi `tsconfig.json` được sửa để include `test/**/*.ts` (#15), 71 lỗi type pre-existing lộ ra. `src/` không có lỗi nào — toàn bộ ở `test/`. Tests vẫn chạy pass nhờ `node --experimental-strip-types` xoá type ở runtime, nhưng `npm run typecheck` (CI) sẽ fail.
999
-
1000
- **Nguyên nhân:**
1001
- `AgentToolResult.content` có kiểu `(TextContent | ImageContent)[]`. Test cũ dùng `result.content[0]?.text ?? ""` không hợp lệ vì `ImageContent` không có field `text`. Trước đây tests không bị typecheck nên không phát hiện.
1002
-
1003
- **Vị trí:** ~32 file trong `test/unit/` và `test/integration/` (số trong ngoặc = số lỗi):
1004
- ```
1005
- test/integration/phase5-observability.test.ts (6)
1006
- test/integration/phase6-control.test.ts (2)
1007
- test/integration/worktree-run.test.ts (2)
1008
- test/unit/agent-runtime-files.test.ts (5)
1009
- test/unit/api-claim.test.ts (1)
1010
- test/unit/api-locks.test.ts (1)
1011
- test/unit/async-stale.test.ts (2)
1012
- test/unit/autonomy-config.test.ts (3)
1013
- test/unit/config-action.test.ts (1)
1014
- test/unit/crew-gap-lessons.test.ts (4)
1015
- test/unit/cross-extension-rpc.test.ts (1)
1016
- test/unit/doctor-smoke.test.ts (1)
1017
- test/unit/doctor-validation.test.ts (4)
1018
- test/unit/foreground-nonblocking.test.ts (1)
1019
- test/unit/help.test.ts (1)
1020
- test/unit/import-list.test.ts (1)
1021
- test/unit/lazy-agent-materialization.test.ts (1)
1022
- test/unit/live-agent-control.test.ts (2)
1023
- test/unit/live-control-realtime.test.ts (1)
1024
- test/unit/live-session-context.test.ts (3)
1025
- test/unit/live-session-runtime.test.ts (4)
1026
- test/unit/mailbox-api.test.ts (4)
1027
- test/unit/mailbox-validation.test.ts (1)
1028
- test/unit/management-references.test.ts (1)
1029
- test/unit/project-init.test.ts (1)
1030
- test/unit/run-events-artifacts.test.ts (4)
1031
- test/unit/runtime-hardening.test.ts (2)
1032
- test/unit/subagent-manager.test.ts (5)
1033
- test/unit/summary.test.ts (2)
1034
- test/unit/team-recommendation.test.ts (1)
1035
- test/unit/team-run.test.ts (2)
1036
- test/unit/validate-resources.test.ts (1)
1037
- ```
1038
-
1039
- **Đề xuất fix:**
1040
-
1041
- **Bước 1 — Tạo helper** trong `test/fixtures/tool-result-helpers.ts`:
1042
- ```ts
1043
- export function textOf(result: { content?: Array<{ type: string; text?: string }> }): string {
1044
- return result.content
1045
- ?.filter((item): item is { type: "text"; text: string } => item.type === "text" && typeof item.text === "string")
1046
- .map((item) => item.text)
1047
- .join("\n") ?? "";
1048
- }
1049
-
1050
- export function firstText(result: { content?: Array<{ type: string; text?: string }> }): string {
1051
- const first = result.content?.find((item) => item.type === "text" && typeof item.text === "string");
1052
- return first?.text ?? "";
1053
- }
1054
- ```
1055
-
1056
- **Bước 2 — Codemod** thay `result.content[0]?.text ?? ""` → `firstText(result)`:
1057
- ```powershell
1058
- Get-ChildItem D:\my\my_project\pi-crew\test -Recurse -Filter *.test.ts | ForEach-Object {
1059
- $content = Get-Content $_.FullName -Raw
1060
- $new = $content -replace '(\w+)\.content\[0\]\?\.text \?\? ""', 'firstText($1)'
1061
- if ($new -ne $content) {
1062
- Set-Content $_.FullName $new -NoNewline
1063
- }
1064
- }
1065
- ```
1066
- Sau đó thêm `import { firstText } from "../fixtures/tool-result-helpers.ts";` vào mỗi file đã sửa.
1067
-
1068
- **Bước 3 — Sửa riêng** 5 lỗi trong `test/unit/subagent-manager.test.ts` (mock `SpawnRunner` trả `status: string`):
1069
- ```ts
1070
- // Cũ
1071
- const runner = async () => ({ content: [...], details: { action: "x", status: "ok" } });
1072
- // Mới
1073
- const runner: SpawnRunner = async () => ({ content: [...], details: { action: "x", status: "ok" as const } });
1074
- ```
1075
-
1076
- **Bước 4 — Sửa** 2 lỗi `cross-extension-rpc.test.ts(37)` & `live-control-realtime.test.ts(18)` — `setTimeout(...)` trả `number` không match `() => void | Promise<void>`. Bọc:
1077
- ```ts
1078
- // Cũ
1079
- setTimeout(() => doSomething(), 100);
1080
- // Mới
1081
- () => { setTimeout(() => doSomething(), 100); }
1082
- ```
1083
-
1084
- **Bước 5 — Sửa** `test/unit/live-session-context.test.ts(27)`: mock object thiếu các field bắt buộc của `TeamContext`. Thêm `as TeamContext` cast hoặc bổ sung field.
1085
-
1086
- **Verification:**
1087
- ```bash
1088
- npx tsc --noEmit # → 0 errors
1089
- npm test # → all pass
1090
- npm run ci # → typecheck + test + pack OK
1091
- ```
1092
-
1093
- **Risk:** Thấp — thuần test code, không ảnh hưởng runtime. Chạy `git diff test/` review trước khi commit để chắc codemod không thay nhầm.
1094
-
1095
- ---
1096
-
1097
- ### Task #18 — LRU bound cho manifestCache (MEDIUM)
1098
-
1099
- **Vấn đề:**
1100
- `manifestCache` trong `src/state/state-store.ts:29` là `Map<string, ManifestCacheEntry>` không có giới hạn. Trong long-running session với nhiều run (status query liên tục), Map có thể grow vô hạn → memory leak.
1101
-
1102
- **Vị trí:** `src/state/state-store.ts:29, 206`
1103
-
1104
- **Đề xuất fix:**
1105
- ```ts
1106
- // config/defaults.ts
1107
- export const DEFAULT_CACHE = {
1108
- manifestMaxEntries: 64,
1109
- };
1110
-
1111
- // state-store.ts
1112
- import { DEFAULT_CACHE } from "../config/defaults.ts";
1113
-
1114
- const manifestCache = new Map<string, ManifestCacheEntry>();
1115
-
1116
- function setManifestCache(stateRoot: string, entry: ManifestCacheEntry): void {
1117
- if (manifestCache.has(stateRoot)) manifestCache.delete(stateRoot); // refresh recency
1118
- manifestCache.set(stateRoot, entry);
1119
- while (manifestCache.size > DEFAULT_CACHE.manifestMaxEntries) {
1120
- const oldest = manifestCache.keys().next().value;
1121
- if (!oldest) break;
1122
- manifestCache.delete(oldest);
1123
- }
1124
- }
1125
-
1126
- // Trong loadRunManifestById, đổi:
1127
- // manifestCache.set(stateRoot, { ... });
1128
- // thành:
1129
- // setManifestCache(stateRoot, { ... });
1130
- ```
1131
-
1132
- **Verification:**
1133
- ```bash
1134
- node --experimental-strip-types --test test/unit/state-store.test.ts
1135
- # Thêm test mới: load 100 run khác nhau, verify manifestCache.size <= 64
1136
- ```
1137
-
1138
- **Risk:** Thấp — chỉ ảnh hưởng cache, không ảnh hưởng đúng đắn vì mtime invalidation vẫn đảm bảo data fresh.
1139
-
1140
- ---
1141
-
1142
- ### Task #19 — Cross-process cache staleness check (MEDIUM)
1143
-
1144
- **Vấn đề:**
1145
- `manifestCache` invalidate dựa trên `manifestStat.mtimeMs`. Trên Windows, mtime granularity ~1ms. Nếu process A ghi manifest tại t=0ms và process B đọc cùng lúc với cache cũ tại t=1ms, mtime có thể trùng → cache stale.
1146
-
1147
- **Vị trí:** `src/state/state-store.ts:175-208` (`loadRunManifestById`)
1148
-
1149
- **Đề xuất fix:**
1150
- Kết hợp mtime + size (cheap):
1151
- ```ts
1152
- interface ManifestCacheEntry {
1153
- manifest: TeamRunManifest;
1154
- tasks: TeamTaskState[];
1155
- manifestMtimeMs: number;
1156
- manifestSize: number; // <-- thêm
1157
- tasksMtimeMs: number;
1158
- tasksSize: number; // <-- thêm
1159
- }
1160
-
1161
- // Validate
1162
- if (cached
1163
- && cached.manifestMtimeMs === manifestStat.mtimeMs
1164
- && cached.manifestSize === manifestStat.size
1165
- && cached.tasksMtimeMs === tasksMtimeMs
1166
- && cached.tasksSize === (tasksStat?.size ?? 0)) {
1167
- return { manifest: cached.manifest, tasks: cached.tasks };
1168
- }
1169
-
1170
- // Khi cache:
1171
- manifestCache.set(stateRoot, {
1172
- manifest, tasks,
1173
- manifestMtimeMs: manifestStat.mtimeMs,
1174
- manifestSize: manifestStat.size,
1175
- tasksMtimeMs,
1176
- tasksSize: tasksStat?.size ?? 0,
1177
- });
1178
- ```
1179
-
1180
- **Verification:**
1181
- ```bash
1182
- node --experimental-strip-types --test test/unit/state-store.test.ts
1183
- # Test mới: write manifest 2 lần liên tiếp với content khác nhau cùng mtime giả định, verify load lần 2 không trả cached
1184
- ```
1185
-
1186
- **Risk:** Thấp — chỉ thắt chặt validation, không loại trừ cache hit hợp lệ.
1187
-
1188
- ---
1189
-
1190
- ### Task #20 — Tách ensureMailbox thành 2 hàm rõ ràng (LOW)
1191
-
1192
- **Vấn đề:**
1193
- `ensureMailbox(manifest, taskId?)` trong `src/state/mailbox.ts:54-62` xử lý cả run-level và task-level. Dòng 60 gọi `mkdirSync(mailboxDir(manifest), ...)` lặp lại vì task path đã chứa run path. Code khó đọc, dễ regress khi thêm scope mới.
1194
-
1195
- **Vị trí:** `src/state/mailbox.ts:54-62`
1196
-
1197
- **Đề xuất fix:**
1198
- ```ts
1199
- function ensureRunMailbox(manifest: TeamRunManifest): void {
1200
- fs.mkdirSync(mailboxDir(manifest), { recursive: true });
1201
- for (const direction of ["inbox", "outbox"] as const) {
1202
- const filePath = mailboxPath(manifest, direction);
1203
- if (!fs.existsSync(filePath)) fs.writeFileSync(filePath, "", "utf-8");
1204
- }
1205
- const delivery = deliveryPath(manifest);
1206
- if (!fs.existsSync(delivery)) {
1207
- fs.writeFileSync(delivery, `${JSON.stringify({ messages: {}, updatedAt: new Date().toISOString() }, null, 2)}\n`, "utf-8");
1208
- }
1209
- }
1210
-
1211
- function ensureTaskMailbox(manifest: TeamRunManifest, taskId: string): void {
1212
- ensureRunMailbox(manifest); // task-level cần delivery.json ở run-level
1213
- fs.mkdirSync(taskMailboxDir(manifest, taskId), { recursive: true });
1214
- for (const direction of ["inbox", "outbox"] as const) {
1215
- const filePath = mailboxPath(manifest, direction, taskId);
1216
- if (!fs.existsSync(filePath)) fs.writeFileSync(filePath, "", "utf-8");
1217
- }
1218
- }
1219
-
1220
- // Update tất cả call sites:
1221
- // ensureMailbox(manifest) → ensureRunMailbox(manifest)
1222
- // ensureMailbox(manifest, taskId) → ensureTaskMailbox(manifest, taskId)
1223
- ```
1224
-
1225
- **Verification:**
1226
- ```bash
1227
- node --experimental-strip-types --test test/unit/mailbox-api.test.ts test/unit/mailbox-validation.test.ts
1228
- ```
1229
-
1230
- **Risk:** Thấp — refactor thuần, behavior không đổi.
1231
-
1232
- ---
1233
-
1234
- ### Task #21 — ✅ ĐÃ HOÀN THÀNH trong review
1235
-
1236
- Giảm circular import giữa `team-tool.ts` ↔ `tool-result.ts`:
1237
- - `src/extension/tool-result.ts` — đổi `import type { TeamToolDetails } from "./team-tool.ts"` → `from "./team-tool-types.ts"`
1238
- - `src/extension/management.ts` — tương tự
1239
-
1240
- Cần verify thêm chưa có chỗ nào còn import xấu:
1241
- ```powershell
1242
- Select-String -Path src\extension\*.ts,src\extension\**\*.ts -Pattern 'TeamToolDetails.*from "(\.\.?/)*team-tool\.ts"'
1243
- ```
1244
- Nếu còn match → đổi sang `team-tool-types.ts`.
1245
-
1246
- ---
1247
-
1248
- ### Task #22 — Codemod TeamContext import rời khỏi team-tool.ts (LOW)
1249
-
1250
- **Vấn đề:**
1251
- `team-tool.ts:40` re-export `TeamContext` từ `./team-tool/context.ts`. Các file khác import qua `team-tool.ts` tạo dependency chain dài. Nên import trực tiếp từ `./team-tool/context.ts` để rõ chuỗi dependency.
1252
-
1253
- **Vị trí:** Search:
1254
- ```powershell
1255
- Select-String -Path src\**\*.ts -Pattern 'TeamContext.*from "(\.\.?/)*extension/team-tool\.ts"'
1256
- ```
1257
-
1258
- **Đề xuất fix:**
1259
- Đổi import sang `team-tool/context.ts`. Có thể giữ re-export ở `team-tool.ts` cho backward compat ngoài (extension API public).
1260
-
1261
- **Verification:**
1262
- ```bash
1263
- npx tsc --noEmit
1264
- npm test
1265
- ```
1266
-
1267
- **Risk:** Thấp — refactor pure.
1268
-
1269
- ---
1270
-
1271
- ### Task #23 — Subagent stuck-blocked notification (LOW, từ #16c Phase 1)
1272
-
1273
- **Vấn đề:**
1274
- `subagent-manager.ts` có status `"blocked"` trong `TERMINAL_RUN_STATUS` nhưng không có notification UI khi child run blocked > N phút. User không biết child đang stuck.
1275
-
1276
- **Vị trí:** `src/runtime/subagent-manager.ts`
1277
-
1278
- **Đề xuất fix:**
1279
-
1280
- 1. Thêm constant trong `config/defaults.ts`:
1281
- ```ts
1282
- export const DEFAULT_SUBAGENT = {
1283
- stuckBlockedNotifyMs: 5 * 60_000, // 5 phút
1284
- };
1285
- ```
1286
-
1287
- 2. Bổ sung field vào `SubagentRecord`:
1288
- ```ts
1289
- export interface SubagentRecord {
1290
- // ... existing
1291
- stuckNotified?: boolean;
1292
- }
1293
- ```
1294
-
1295
- 3. Trong polling/watch loop kiểm tra child status:
1296
- ```ts
1297
- import { DEFAULT_SUBAGENT } from "../config/defaults.ts";
1298
-
1299
- if (record.status === "blocked"
1300
- && record.startedAt
1301
- && Date.now() - record.startedAt > DEFAULT_SUBAGENT.stuckBlockedNotifyMs
1302
- && !record.stuckNotified) {
1303
- emitEvent("subagent.stuck-blocked", {
1304
- id: record.id,
1305
- runId: record.runId,
1306
- durationMs: Date.now() - record.startedAt,
1307
- });
1308
- record.stuckNotified = true;
1309
- savePersistedSubagentRecord(cwd, record);
1310
- }
1311
- ```
1312
-
1313
- 4. UI/dashboard subscribe event `subagent.stuck-blocked` hiển thị badge cảnh báo.
1314
-
1315
- **Verification:**
1316
- Test mới `test/unit/subagent-stuck-notify.test.ts`:
1317
- ```ts
1318
- test("subagent blocked > threshold emits stuck-blocked event", async () => {
1319
- const record = createRecord({ status: "blocked", startedAt: Date.now() - 10 * 60_000 });
1320
- const events: string[] = [];
1321
- checkSubagentStuck(record, (type) => events.push(type));
1322
- assert.ok(events.includes("subagent.stuck-blocked"));
1323
- assert.equal(record.stuckNotified, true);
1324
- });
1325
- ```
1326
-
1327
- **Risk:** Thấp — feature mới, không ảnh hưởng path hiện có.
1328
-
1329
- ---
1330
-
1331
- ### Task #24 — TypeBox config validation warnings (MEDIUM)
1332
-
1333
- **Vấn đề:**
1334
- `config.ts` `parseConfig()` dùng `parseWithSchema` trả `undefined` khi schema fail → silent drop. User config sai sẽ bị bỏ qua không cảnh báo.
1335
-
1336
- **Vị trí:** `src/config/config.ts:189-207, parseConfig()`
1337
-
1338
- **Đề xuất fix:**
1339
-
1340
- 1. Thêm hàm `validateConfigStrict` trả về warnings:
1341
- ```ts
1342
- import { Value } from "typebox/value";
1343
- import { PiTeamsConfigSchema } from "../schema/config-schema.ts";
1344
-
1345
- export interface ConfigValidation {
1346
- config: PiTeamsConfig;
1347
- warnings: string[];
1348
- }
1349
-
1350
- export function validateConfigStrict(raw: unknown): ConfigValidation {
1351
- const warnings: string[] = [];
1352
- if (raw && typeof raw === "object" && !Value.Check(PiTeamsConfigSchema, raw)) {
1353
- for (const err of Value.Errors(PiTeamsConfigSchema, raw)) {
1354
- warnings.push(`Config invalid at ${err.path}: ${err.message}`);
1355
- }
1356
- }
1357
- return { config: parseConfig(raw), warnings };
1358
- }
1359
- ```
1360
-
1361
- 2. Thêm field `warnings` vào `LoadedPiTeamsConfig`:
1362
- ```ts
1363
- export interface LoadedPiTeamsConfig {
1364
- config: PiTeamsConfig;
1365
- path: string;
1366
- paths: string[];
1367
- error?: string;
1368
- warnings?: string[]; // <-- thêm
1369
- }
1370
- ```
1371
-
1372
- 3. `loadConfig()` populate warnings từ cả user + project config:
1373
- ```ts
1374
- export function loadConfig(cwd?: string): LoadedPiTeamsConfig {
1375
- const filePath = configPath();
1376
- const paths = cwd ? [filePath, projectConfigPath(cwd)] : [filePath];
1377
- const warnings: string[] = [];
1378
- try {
1379
- const userValidation = validateConfigStrict(readConfigRecord(filePath));
1380
- warnings.push(...userValidation.warnings.map((w) => `${filePath}: ${w}`));
1381
- let config = userValidation.config;
1382
- if (cwd) {
1383
- const projectValidation = validateConfigStrict(readConfigRecord(projectConfigPath(cwd)));
1384
- warnings.push(...projectValidation.warnings.map((w) => `${projectConfigPath(cwd)}: ${w}`));
1385
- config = mergeConfig(config, projectValidation.config);
1386
- }
1387
- return { path: filePath, paths, config, warnings: warnings.length ? warnings : undefined };
1388
- } catch (error) {
1389
- const message = error instanceof Error ? error.message : String(error);
1390
- return { path: filePath, paths, config: {}, error: message };
1391
- }
1392
- }
1393
- ```
1394
-
1395
- 4. UI hiển thị warnings trong action `team doctor` (`handleDoctor`).
1396
-
1397
- **Verification:**
1398
- ```bash
1399
- node --experimental-strip-types --test test/unit/config-schema-validation.test.ts
1400
- # Thêm test:
1401
- // - invalid config (e.g., notifierIntervalMs: 100) → warnings non-empty
1402
- // - valid config → warnings undefined
1403
- ```
1404
-
1405
- **Risk:** Thấp — chỉ thêm thông tin, không thay đổi runtime behavior. Backward-compat: nếu callers không đọc `warnings` thì không ảnh hưởng.
1406
-
1407
- ---
1408
-
1409
- ### Task #25 — atomicWriteFileAsync idempotent retry (LOW)
1410
-
1411
- **Vấn đề:**
1412
- `atomicWriteFileAsync` ghi temp + rename. Nếu 2 process song song write cùng `filePath`, đôi khi rename của process A vào lúc process B đã rename xong → `EPERM`/`EBUSY` Windows. Retry handle nhưng có thể spin nhiều lần không cần thiết.
1413
-
1414
- **Vị trí:** `src/state/atomic-write.ts:34-46`
1415
-
1416
- **Đề xuất fix:**
1417
- Sau retry exhaust, kiểm tra nếu file đã tồn tại với content khớp mong muốn → coi như success (idempotent write):
1418
-
1419
- ```ts
1420
- export async function atomicWriteFileAsync(filePath: string, content: string): Promise<void> {
1421
- await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
1422
- const tempPath = `${filePath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
1423
- try {
1424
- await fs.promises.writeFile(tempPath, content, "utf-8");
1425
- try {
1426
- await __test__renameWithRetryAsync(tempPath, filePath);
1427
- } catch (renameError) {
1428
- // Idempotent fallback: nếu file đã có nội dung khớp → success
1429
- try {
1430
- const existing = await fs.promises.readFile(filePath, "utf-8");
1431
- if (existing === content) {
1432
- await fs.promises.rm(tempPath, { force: true });
1433
- return;
1434
- }
1435
- } catch {
1436
- // file không tồn tại hoặc không đọc được → throw original
1437
- }
1438
- throw renameError;
1439
- }
1440
- } catch (error) {
1441
- try {
1442
- await fs.promises.rm(tempPath, { force: true });
1443
- } catch (cleanupError) {
1444
- logInternalError("atomic-write.cleanupAsync", cleanupError, `tempPath=${tempPath}`);
1445
- }
1446
- throw error;
1447
- }
1448
- }
1449
- ```
1450
-
1451
- **Verification:**
1452
- Stress test mới `test/unit/atomic-write-concurrent.test.ts`:
1453
- ```ts
1454
- test("100 concurrent writes of same content succeed", async () => {
1455
- const filePath = path.join(tmpDir, "concurrent.json");
1456
- await Promise.all(
1457
- Array.from({ length: 100 }, () => atomicWriteFileAsync(filePath, '{"v":1}'))
1458
- );
1459
- assert.equal(fs.readFileSync(filePath, "utf-8"), '{"v":1}');
1460
- });
1461
- ```
1462
-
1463
- **Risk:** Trung bình — race rất hiếm, ưu tiên sau cùng. Cần test kỹ trên Windows + Linux để chắc fallback không hide bug.
1464
-
1465
- ---
1466
-
1467
- ## Quick Reference — Verification cho mọi follow-up task
1468
-
1469
- ```powershell
1470
- Set-Location D:\my\my_project\pi-crew
1471
-
1472
- # Type check (PHẢI pass sau task #17)
1473
- npx tsc --noEmit
1474
-
1475
- # Targeted tests
1476
- node --experimental-strip-types --test test/unit/<file>.test.ts
1477
-
1478
- # Full unit suite
1479
- npm run test:unit
1480
-
1481
- # Full CI
1482
- npm run ci # = typecheck + test + npm pack --dry-run
1483
- ```
1484
-