@huseynsnmz/pi-board 0.15.3 → 0.17.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 (216) hide show
  1. package/CHANGELOG.md +24 -4
  2. package/CONTRIBUTING.md +7 -0
  3. package/README.md +55 -419
  4. package/SECURITY.md +19 -0
  5. package/docs/how-it-works.md +31 -0
  6. package/docs/reference.md +277 -0
  7. package/package.json +5 -11
  8. package/src/commands/agent-board.ts +26 -2
  9. package/src/commands/attach-flow.ts +4 -0
  10. package/src/core/double-press.mjs +6 -2
  11. package/src/core/slash-commands.mjs +52 -0
  12. package/src/index.ts +1 -1
  13. package/src/runtime/service.mjs +12 -0
  14. package/src/ui/dashboard.ts +59 -25
  15. package/src/ui/pty-attach.ts +8 -3
  16. package/IMPLEMENTATION_PLAN.md +0 -920
  17. package/PRD.md +0 -484
  18. package/PROGRESS.md +0 -142
  19. package/VERIFY.md +0 -115
  20. package/docs/BATCH_SELECTION_READ_FLOW.md +0 -277
  21. package/docs/EXPLORATION.md +0 -187
  22. package/docs/PTY_ATTACH_IMPLEMENTATION_PLAN.md +0 -581
  23. package/docs/RELEASE.md +0 -148
  24. package/docs/superpowers/plans/2026-08-15-screenlog-gc.md +0 -704
  25. package/docs/superpowers/plans/2026-08-16-attach-double-cursor-jiggle-retry.md +0 -499
  26. package/docs/superpowers/plans/2026-08-21-attach-coldstart-jiggle-rearm.md +0 -797
  27. package/docs/superpowers/plans/2026-08-21-dashboard-keypress-lag.md +0 -366
  28. package/docs/superpowers/plans/2026-08-21-issue-14-needs-input-no-auto-done.md +0 -576
  29. package/docs/superpowers/plans/2026-08-22-jiggle-shrink-and-hold.md +0 -337
  30. package/docs/superpowers/plans/2026-08-22-launch-cwd-favorites.md +0 -916
  31. package/docs/superpowers/plans/2026-08-22-question-tool-grouping.md +0 -211
  32. package/docs/superpowers/plans/2026-08-22-rename-state-labels.md +0 -182
  33. package/docs/superpowers/plans/2026-08-22-stable-list-order.md +0 -51
  34. package/docs/superpowers/plans/2026-08-27-locks-acquirelock-spin.md +0 -614
  35. package/docs/superpowers/plans/2026-08-27-pty-runner-test-flaky.md +0 -32
  36. package/docs/superpowers/plans/2026-08-29-code-refs-badges.md +0 -223
  37. package/docs/superpowers/plans/2026-08-30-circular-navigation.md +0 -308
  38. package/docs/superpowers/plans/2026-08-30-post-exit-timing-fix.md +0 -37
  39. package/docs/superpowers/plans/2026-08-30-pty-attach-quality-debt.md +0 -311
  40. package/docs/superpowers/plans/2026-08-30-readme-v2.md +0 -294
  41. package/docs/superpowers/plans/2026-09-01-attach-detach-gate-cursor-anchor.md +0 -284
  42. package/docs/superpowers/plans/2026-09-02-attach-detach-editor-state.md +0 -722
  43. package/docs/superpowers/plans/2026-09-03-code-refs-pr-backlink-narrow.md +0 -551
  44. package/docs/superpowers/plans/2026-09-03-detach-gate-glyph-fallback.md +0 -146
  45. package/docs/superpowers/plans/2026-09-04-evidence-outputpreview.md +0 -209
  46. package/docs/superpowers/plans/2026-09-04-warm-host-reclaim.md +0 -796
  47. package/docs/superpowers/plans/2026-09-05-issue-13-drainnextfollowup-pty-probe.md +0 -114
  48. package/docs/superpowers/plans/2026-09-05-issue-38-windows-wezterm-ime-cursor.md +0 -73
  49. package/docs/superpowers/plans/2026-09-05-issue-39-truncate-codepoint-boundary.md +0 -143
  50. package/docs/superpowers/plans/2026-09-05-issue-61-mention-fallback-guards.md +0 -226
  51. package/docs/superpowers/plans/2026-09-05-issue-63-flaky-manual-completion.md +0 -87
  52. package/docs/superpowers/plans/2026-09-05-issue-64-changelog-release-helper.md +0 -53
  53. package/docs/superpowers/plans/2026-09-05-pty-host-stacking-sock-race.md +0 -731
  54. package/docs/superpowers/plans/2026-09-08-attach-ctrl-left-detach.md +0 -30
  55. package/docs/superpowers/plans/2026-09-08-dashboard-shrink-repaint.md +0 -68
  56. package/docs/superpowers/plans/2026-09-08-issue-11-attach-runtime-desync-heal.md +0 -917
  57. package/docs/superpowers/plans/2026-09-08-legacy-stale-host-recovery.md +0 -125
  58. package/docs/superpowers/plans/2026-09-08-spawn-async-error-swallow.md +0 -56
  59. package/docs/superpowers/plans/2026-09-08-stale-model-attach-guard.md +0 -96
  60. package/docs/superpowers/plans/2026-09-09-claimpid-blocks-replace.md +0 -267
  61. package/docs/superpowers/plans/2026-09-09-harden-runner-architecture.md +0 -603
  62. package/docs/superpowers/plans/2026-09-09-single-writer-completion.md +0 -252
  63. package/docs/superpowers/plans/2026-09-10-reader-consistency.md +0 -115
  64. package/docs/superpowers/plans/2026-09-14-attach-cursor-dectcem-gate.md +0 -469
  65. package/docs/superpowers/plans/2026-09-14-attach-snapshot.md +0 -92
  66. package/docs/superpowers/plans/2026-09-14-host-meta-orphan-lock.md +0 -771
  67. package/docs/superpowers/plans/2026-09-14-issue-106-terminal-frame-cognition.md +0 -299
  68. package/docs/superpowers/plans/2026-09-14-issue-113-foreground-preview-race.md +0 -609
  69. package/docs/superpowers/plans/2026-09-14-terminal-model.md +0 -145
  70. package/docs/superpowers/plans/2026-09-15-coordinator-pipe-root-normalize.md +0 -224
  71. package/docs/superpowers/plans/2026-09-15-lease-publish-eprem-reclaim.md +0 -341
  72. package/docs/superpowers/plans/2026-09-18-detach-anchor-reporter-endpoint.md +0 -875
  73. package/docs/superpowers/plans/2026-09-20-control-lifecycle.md +0 -116
  74. package/docs/superpowers/plans/2026-09-20-issue-121-perf-gate-out-of-coverage.md +0 -517
  75. package/docs/superpowers/plans/2026-09-23-a5-mid-stream-assertion.md +0 -25
  76. package/docs/superpowers/plans/2026-09-23-cwd-browse-tab-drill.md +0 -393
  77. package/docs/superpowers/plans/2026-09-23-terminal-query-forwarding.md +0 -51
  78. package/docs/superpowers/plans/2026-09-23-unconditional-detach-key.md +0 -58
  79. package/docs/superpowers/plans/2026-09-24-control-reconcile-flake.md +0 -698
  80. package/docs/superpowers/plans/2026-09-24-probe-attached-clients.md +0 -605
  81. package/docs/superpowers/plans/2026-09-29-issue-145-holding-semantic-state.md +0 -1183
  82. package/docs/superpowers/plans/2026-09-29-issue-145-user-verification.md +0 -66
  83. package/docs/superpowers/plans/2026-09-30-board-delete-key-x-y-confirm.md +0 -649
  84. package/docs/superpowers/plans/2026-09-30-issue-153-runner-stop-boot-window.md +0 -618
  85. package/docs/superpowers/plans/2026-09-30-issue-95-test-discipline.md +0 -555
  86. package/docs/superpowers/plans/2026-09-30-publish-dispatch-tag-check.md +0 -259
  87. package/docs/superpowers/plans/2026-10-06-attach-settle-scheme-delivery.md +0 -751
  88. package/docs/superpowers/plans/2026-10-09-issue-167-attach-middle-paste.md +0 -509
  89. package/docs/superpowers/plans/2026-10-09-issue-169-fullscreen-mouse-ownership.md +0 -346
  90. package/docs/superpowers/plans/2026-10-10-attach-delete-in-place.md +0 -832
  91. package/docs/superpowers/plans/2026-10-10-issue-174-selection-mirror-paste-source.md +0 -510
  92. package/docs/superpowers/plans/2026-10-14-issue-170-attach-reply-guard.md +0 -771
  93. package/docs/superpowers/plans/2026-10-14-issue-178-accept-registration-race.md +0 -1257
  94. package/docs/superpowers/plans/2026-10-14-issue-184-snapshot-window-contract.md +0 -657
  95. package/docs/superpowers/plans/2026-10-14-issue-185-attach-wheel-alt-screen.md +0 -47
  96. package/docs/superpowers/plans/2026-10-14-issue-187-motionless-drag.md +0 -931
  97. package/docs/superpowers/plans/2026-10-14-issue-189-mixed-input-segments.md +0 -823
  98. package/docs/superpowers/plans/2026-10-15-issue-188-x10-mouse-release.md +0 -526
  99. package/docs/superpowers/plans/2026-10-16-issue-198-attach-wheel-forward-fallback.md +0 -32
  100. package/docs/superpowers/plans/2026-10-18-attach-wheel-replay-pseudo-scrollback.md +0 -437
  101. package/docs/superpowers/plans/2026-10-19-issue-213-reply-guard-csi-key.md +0 -257
  102. package/docs/superpowers/plans/2026-10-19-issue-214-attach-forward-alt-modifier.md +0 -202
  103. package/docs/superpowers/plans/2026-10-21-issue-221-ask-user-question.md +0 -198
  104. package/docs/superpowers/plans/2026-10-23-issue-222-attach-ready.md +0 -290
  105. package/docs/superpowers/plans/2026-10-23-issue-225-monotonic-foreground-state.md +0 -450
  106. package/docs/superpowers/plans/2026-10-23-issue-229-vim-jkl.md +0 -449
  107. package/docs/superpowers/plans/2026-10-23-issue-232-attach-bg-only-cells.md +0 -153
  108. package/docs/superpowers/plans/2026-10-23-lease-heartbeat-staleness.md +0 -537
  109. package/docs/superpowers/plans/2026-10-23-selected-row-highlight.md +0 -405
  110. package/docs/superpowers/plans/2026-10-24-issue-217-windows-pid-reuse-stopping.md +0 -367
  111. package/docs/superpowers/plans/2026-10-24-issue-230-warm-host-background-runs.md +0 -806
  112. package/docs/superpowers/plans/2026-10-24-issue-238-windows-process-identity-token.md +0 -490
  113. package/docs/superpowers/plans/2026-10-24-issue-241-coordinator-root-exit.md +0 -461
  114. package/docs/superpowers/plans/2026-10-25-issue-239-coordinator-replacement-identity.md +0 -504
  115. package/docs/superpowers/plans/2026-10-26-issue-245-key-hints-doc-sync.md +0 -244
  116. package/docs/superpowers/plans/2026-10-27-issue-172-fullscreen-selection-clear.md +0 -344
  117. package/docs/superpowers/plans/2026-10-27-issue-252-select-key-hints.md +0 -209
  118. package/docs/superpowers/plans/2026-10-28-issue-255-coordinator-endpoint-zombie.md +0 -442
  119. package/docs/superpowers/plans/2026-10-28-issue-257-legacy-pty-runner-endpoint-cleanup.md +0 -256
  120. package/docs/superpowers/plans/2026-11-03-cwd-favorites-full-pool.md +0 -266
  121. package/docs/superpowers/specs/2026-08-15-screenlog-gc-design.md +0 -105
  122. package/docs/superpowers/specs/2026-08-16-attach-double-cursor-jiggle-retry-design.md +0 -142
  123. package/docs/superpowers/specs/2026-08-21-attach-coldstart-jiggle-rearm-design.md +0 -110
  124. package/docs/superpowers/specs/2026-08-21-dashboard-keypress-lag-design.md +0 -59
  125. package/docs/superpowers/specs/2026-08-21-issue-14-needs-input-no-auto-done-design.md +0 -109
  126. package/docs/superpowers/specs/2026-08-22-cwd-favorites-design.md +0 -131
  127. package/docs/superpowers/specs/2026-08-22-jiggle-shrink-and-hold-design.md +0 -111
  128. package/docs/superpowers/specs/2026-08-22-question-tool-grouping-design.md +0 -114
  129. package/docs/superpowers/specs/2026-08-22-rename-state-labels-design.md +0 -80
  130. package/docs/superpowers/specs/2026-08-27-locks-acquirelock-spin-design.md +0 -97
  131. package/docs/superpowers/specs/2026-08-27-pty-runner-test-flaky-design.md +0 -35
  132. package/docs/superpowers/specs/2026-08-29-code-refs-badges-design.md +0 -128
  133. package/docs/superpowers/specs/2026-08-30-circular-navigation-design.md +0 -47
  134. package/docs/superpowers/specs/2026-08-30-eprm-atomicwrite-race-design.md +0 -77
  135. package/docs/superpowers/specs/2026-08-30-post-exit-timing-fix-design.md +0 -80
  136. package/docs/superpowers/specs/2026-08-30-pty-attach-quality-debt-design.md +0 -105
  137. package/docs/superpowers/specs/2026-08-30-readme-v2-design.md +0 -115
  138. package/docs/superpowers/specs/2026-09-01-attach-detach-gate-cursor-anchor-design.md +0 -78
  139. package/docs/superpowers/specs/2026-09-02-attach-detach-editor-state-design.md +0 -120
  140. package/docs/superpowers/specs/2026-09-03-code-refs-pr-backlink-narrow-design.md +0 -92
  141. package/docs/superpowers/specs/2026-09-03-detach-gate-glyph-fallback-design.md +0 -99
  142. package/docs/superpowers/specs/2026-09-04-evidence-outputpreview-design.md +0 -50
  143. package/docs/superpowers/specs/2026-09-04-warm-host-reclaim-design.md +0 -106
  144. package/docs/superpowers/specs/2026-09-05-issue-13-drainnextfollowup-pty-probe-design.md +0 -64
  145. package/docs/superpowers/specs/2026-09-05-issue-38-windows-wezterm-ime-design.md +0 -48
  146. package/docs/superpowers/specs/2026-09-05-issue-39-truncate-codepoint-boundary-design.md +0 -64
  147. package/docs/superpowers/specs/2026-09-05-issue-61-mention-fallback-design.md +0 -71
  148. package/docs/superpowers/specs/2026-09-05-issue-63-flaky-manual-completion-design.md +0 -49
  149. package/docs/superpowers/specs/2026-09-05-issue-64-changelog-helper-design.md +0 -76
  150. package/docs/superpowers/specs/2026-09-05-pty-host-stacking-sock-race-design.md +0 -510
  151. package/docs/superpowers/specs/2026-09-07-issue-11-attach-runtime-desync-heal-design.md +0 -130
  152. package/docs/superpowers/specs/2026-09-08-attach-ctrl-left-detach-design.md +0 -58
  153. package/docs/superpowers/specs/2026-09-08-dashboard-shrink-repaint-design.md +0 -52
  154. package/docs/superpowers/specs/2026-09-08-legacy-stale-host-recovery-design.md +0 -87
  155. package/docs/superpowers/specs/2026-09-08-spawn-async-error-swallow-design.md +0 -56
  156. package/docs/superpowers/specs/2026-09-08-stale-model-attach-guard-design.md +0 -79
  157. package/docs/superpowers/specs/2026-09-09-claimpid-blocks-replace-design.md +0 -147
  158. package/docs/superpowers/specs/2026-09-09-harden-runner-architecture-design.md +0 -304
  159. package/docs/superpowers/specs/2026-09-14-attach-cursor-dectcem-gate-design.md +0 -114
  160. package/docs/superpowers/specs/2026-09-14-host-meta-orphan-lock-design.md +0 -120
  161. package/docs/superpowers/specs/2026-09-14-issue-106-terminal-frame-cognition-design.md +0 -146
  162. package/docs/superpowers/specs/2026-09-14-issue-113-foreground-preview-race-design.md +0 -116
  163. package/docs/superpowers/specs/2026-09-15-coordinator-pipe-root-normalize-design.md +0 -84
  164. package/docs/superpowers/specs/2026-09-15-lease-publish-eprem-reclaim-design.md +0 -92
  165. package/docs/superpowers/specs/2026-09-18-detach-anchor-reporter-endpoint-design.md +0 -123
  166. package/docs/superpowers/specs/2026-09-20-issue-121-perf-gate-out-of-coverage-design.md +0 -204
  167. package/docs/superpowers/specs/2026-09-23-a5-mid-stream-assertion-design.md +0 -43
  168. package/docs/superpowers/specs/2026-09-23-cwd-browse-tab-drill-design.md +0 -95
  169. package/docs/superpowers/specs/2026-09-23-terminal-query-forwarding-design.md +0 -63
  170. package/docs/superpowers/specs/2026-09-23-unconditional-detach-key-design.md +0 -57
  171. package/docs/superpowers/specs/2026-09-24-control-reconcile-flake-design.md +0 -98
  172. package/docs/superpowers/specs/2026-09-24-probe-attached-clients-design.md +0 -190
  173. package/docs/superpowers/specs/2026-09-29-issue-145-holding-semantic-state-design.md +0 -394
  174. package/docs/superpowers/specs/2026-09-30-board-delete-key-x-y-confirm-design.md +0 -86
  175. package/docs/superpowers/specs/2026-09-30-issue-153-runner-stop-boot-window-design.md +0 -75
  176. package/docs/superpowers/specs/2026-09-30-issue-95-test-discipline-design.md +0 -226
  177. package/docs/superpowers/specs/2026-09-30-publish-dispatch-tag-check-design.md +0 -129
  178. package/docs/superpowers/specs/2026-10-06-attach-settle-scheme-delivery-design.md +0 -196
  179. package/docs/superpowers/specs/2026-10-09-issue-167-attach-middle-paste-design.md +0 -125
  180. package/docs/superpowers/specs/2026-10-09-issue-169-fullscreen-mouse-ownership-design.md +0 -130
  181. package/docs/superpowers/specs/2026-10-10-attach-delete-in-place-design.md +0 -224
  182. package/docs/superpowers/specs/2026-10-10-issue-174-selection-mirror-paste-source-design.md +0 -154
  183. package/docs/superpowers/specs/2026-10-14-issue-170-attach-reply-guard-design.md +0 -195
  184. package/docs/superpowers/specs/2026-10-14-issue-178-accept-registration-race-design.md +0 -287
  185. package/docs/superpowers/specs/2026-10-14-issue-184-snapshot-window-contract-design.md +0 -288
  186. package/docs/superpowers/specs/2026-10-14-issue-185-attach-wheel-alt-screen-design.md +0 -126
  187. package/docs/superpowers/specs/2026-10-14-issue-187-motionless-drag-design.md +0 -269
  188. package/docs/superpowers/specs/2026-10-14-issue-189-mixed-input-segments-design.md +0 -369
  189. package/docs/superpowers/specs/2026-10-15-issue-188-x10-mouse-release-design.md +0 -252
  190. package/docs/superpowers/specs/2026-10-16-issue-198-attach-wheel-forward-fallback-design.md +0 -67
  191. package/docs/superpowers/specs/2026-10-18-attach-wheel-replay-pseudo-scrollback-design.md +0 -250
  192. package/docs/superpowers/specs/2026-10-19-issue-213-reply-guard-csi-key-design.md +0 -165
  193. package/docs/superpowers/specs/2026-10-19-issue-214-attach-forward-alt-modifier-design.md +0 -131
  194. package/docs/superpowers/specs/2026-10-20-issue-221-ask-user-question-design.md +0 -270
  195. package/docs/superpowers/specs/2026-10-22-issue-225-monotonic-foreground-state-design.md +0 -225
  196. package/docs/superpowers/specs/2026-10-23-issue-222-attach-ready-design.md +0 -204
  197. package/docs/superpowers/specs/2026-10-23-issue-229-vim-jkl-design.md +0 -90
  198. package/docs/superpowers/specs/2026-10-23-issue-232-attach-bg-only-cells-design.md +0 -146
  199. package/docs/superpowers/specs/2026-10-23-lease-heartbeat-staleness-design.md +0 -271
  200. package/docs/superpowers/specs/2026-10-23-selected-row-highlight-design.md +0 -191
  201. package/docs/superpowers/specs/2026-10-24-issue-217-windows-pid-reuse-stopping-design.md +0 -334
  202. package/docs/superpowers/specs/2026-10-24-issue-230-warm-host-background-runs-design.md +0 -283
  203. package/docs/superpowers/specs/2026-10-24-issue-238-windows-process-identity-token-design.md +0 -286
  204. package/docs/superpowers/specs/2026-10-24-issue-241-coordinator-root-exit-design.md +0 -381
  205. package/docs/superpowers/specs/2026-10-25-issue-239-coordinator-replacement-identity-design.md +0 -331
  206. package/docs/superpowers/specs/2026-10-26-issue-245-key-hints-doc-sync-design.md +0 -72
  207. package/docs/superpowers/specs/2026-10-27-issue-172-fullscreen-selection-clear-design.md +0 -248
  208. package/docs/superpowers/specs/2026-10-27-issue-252-select-key-hints-design.md +0 -95
  209. package/docs/superpowers/specs/2026-10-28-issue-255-coordinator-endpoint-zombie-design.md +0 -380
  210. package/docs/superpowers/specs/2026-10-28-issue-257-legacy-pty-runner-endpoint-cleanup-design.md +0 -197
  211. package/docs/superpowers/specs/2026-11-03-cwd-favorites-full-pool-design.md +0 -106
  212. package/scripts/budget-audit.mjs +0 -135
  213. package/scripts/release.mjs +0 -446
  214. package/scripts/release_helper.mjs +0 -309
  215. package/scripts/run-perf-gate.mjs +0 -40
  216. package/scripts/run-tests.mjs +0 -375
@@ -1,614 +0,0 @@
1
- # locks.mjs Bounded Acquisition Fix Implementation Plan
2
-
3
- > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
-
5
- **Goal:** Make `acquireLock` in `src/core/locks.mjs` fail fast (bounded attempts → throw) instead of spinning forever when the lock can never be acquired, and harden every layer above it (queue, runner, test harness) so a lock failure can never produce a 100%-CPU zombie process again (issue #33).
6
-
7
- **Architecture:** Four defense layers. (1) `locks.mjs`: classify acquisition failures — EEXIST contention keeps the existing wait-window + force-steal semantics (bounded to 2 steal attempts), all other errors (ENOENT/EACCES/ENOTDIR/EROFS/ENOSPC…) get 3 quick retries with `ensureDir` re-run for self-heal, then throw. (2) `follow-up-queue.mjs` catches any throw and returns `{ok:false, error}` per the module's existing convention. (3) `runner/job-runner.mjs` wraps the post-exit finalize chain in try/catch so `process.exit` always runs. (4) `test/runner.integration.test.mjs` kills every detached runner in teardown before deleting the root.
8
-
9
- **Tech Stack:** Node 24 ESM (`.mjs`), `node:test` + `node:assert/strict`, sync `node:fs`, tabs indentation, JSDoc types, zero new dependencies.
10
-
11
- ## Global Constraints
12
-
13
- - **Worktree only:** all file paths below are relative to `/home/elling/git-repo/github/pi-agent-board/.pi/worktrees/issue-33-locks-acquirelock-spin` — never touch the main checkout.
14
- - **Existing semantics must not change:** the 5 existing tests in `test/locks.test.mjs` stay unmodified and green (wait-window contention + stale steal + force-steal-after-window are contract).
15
- - Every new test carries `{ timeout: 5000 }` so a regression to a spin loop fails fast instead of hanging the suite.
16
- - fs injection follows the `defaultScreenLogFs` precedent in `src/core/screen-log.mjs` (frozen object of raw `node:fs` functions passed via opts).
17
- - Stage by explicit file path (`git add <file>`), never `git add -A`.
18
- - Conventional commits, English messages. Comments in English, tabs for indentation.
19
- - Test command: `npm test` (= `node --test test/*.test.mjs`). Final gate: `npm run verify`.
20
- - `appendLine`/`appendDiagnostic` are NOT best-effort (verified: `ensureDir` + `appendFileSync` throw on deleted roots) — any diagnostic call on a possibly-dead root must be wrapped.
21
-
22
- ---
23
-
24
- ### Task 1: locks.mjs — bounded acquisition, error classification, fs injection
25
-
26
- **Files:**
27
- - Modify: `src/core/locks.mjs` (full rewrite of internals, same public API + new opts)
28
- - Test: `test/locks.test.mjs` (append new tests; do not modify existing 5)
29
-
30
- **Interfaces:**
31
- - Consumes: `ensureDir(dir)` from `src/core/atomic.mjs`; `P.viewLockPath(root, viewId, name)`.
32
- - Produces (other tasks rely on these):
33
- - `withFileLockSync(lockPath, fn, opts?)` — `opts.fs` (frozen `{existsSync, mkdirSync, readFileSync, rmSync, writeFileSync}`, default `defaultLocksFs`), `opts.staleMs` (default 30000). Throws `Error` with `err.code === "LOCK_TIMEOUT"` and message containing the lockPath when acquisition is impossible.
34
- - `withViewLockSync(root, viewId, name, fn, opts?)` — passes opts through.
35
- - `defaultLocksFs` exported (for tests).
36
-
37
- - [ ] **Step 1: Write the failing tests**
38
-
39
- Append to `test/locks.test.mjs` (also add `defaultLocksFs` to the import from `../src/core/locks.mjs`, and `writeFileSync` to the existing `node:fs` import — it is already there for the stale-lock tests, verify):
40
-
41
- ```js
42
- const LOCK_FS = defaultLocksFs;
43
-
44
- function eexist() {
45
- const e = new Error("EEXIST");
46
- e.code = "EEXIST";
47
- return e;
48
- }
49
-
50
- test("withFileLockSync throws promptly when mkdirSync keeps failing", { timeout: 5000 }, () => {
51
- const root = freshRoot();
52
- try {
53
- const fs = {
54
- ...LOCK_FS,
55
- mkdirSync: (p, o) => {
56
- const e = new Error("EACCES");
57
- e.code = "EACCES";
58
- throw e;
59
- },
60
- };
61
- const started = Date.now();
62
- assert.throws(
63
- () => withFileLockSync(join(root, "x.lock"), () => "no", { fs }),
64
- /lock path unusable/,
65
- );
66
- assert.ok(Date.now() - started < 2000, "must fail fast, not spin");
67
- } finally {
68
- rmSync(root, { recursive: true, force: true });
69
- }
70
- });
71
-
72
- test("withFileLockSync throws and cleans up when owner.json writes keep failing", { timeout: 5000 }, () => {
73
- const root = freshRoot();
74
- try {
75
- const fs = {
76
- ...LOCK_FS,
77
- writeFileSync: (file, data, opts) => {
78
- if (String(file).endsWith("owner.json")) {
79
- const e = new Error("ENOSPC");
80
- e.code = "ENOSPC";
81
- throw e;
82
- }
83
- return LOCK_FS.writeFileSync(file, data, opts);
84
- },
85
- };
86
- assert.throws(
87
- () => withFileLockSync(join(root, "w.lock"), () => "no", { fs }),
88
- /lock path unusable/,
89
- );
90
- assert.equal(existsSync(join(root, "w.lock")), false, "half-created lock must be cleaned up");
91
- } finally {
92
- rmSync(root, { recursive: true, force: true });
93
- }
94
- });
95
-
96
- test("withFileLockSync throws after bounded steal attempts when rmSync keeps failing", { timeout: 5000 }, () => {
97
- const root = freshRoot();
98
- try {
99
- const lockPath = join(root, "s.lock");
100
- mkdirSync(lockPath);
101
- writeFileSync(join(lockPath, "owner.json"), JSON.stringify({ pid: 1, at: Date.now() - 60_000 }), "utf8"); // stale
102
- const fs = {
103
- ...LOCK_FS,
104
- rmSync: (p, o) => {
105
- const e = new Error("EPERM");
106
- e.code = "EPERM";
107
- throw e;
108
- },
109
- };
110
- const started = Date.now();
111
- assert.throws(
112
- () => withFileLockSync(lockPath, () => "no", { fs, staleMs: 50 }),
113
- /stale lock could not be stolen/,
114
- );
115
- assert.ok(Date.now() - started < 2000);
116
- } finally {
117
- rmSync(root, { recursive: true, force: true });
118
- }
119
- });
120
-
121
- test("withFileLockSync waits through transient contention and acquires", { timeout: 5000 }, () => {
122
- const root = freshRoot();
123
- try {
124
- const lockPath = join(root, "c.lock");
125
- mkdirSync(lockPath);
126
- writeFileSync(join(lockPath, "owner.json"), JSON.stringify({ pid: 1, at: Date.now() }), "utf8"); // fresh holder
127
- let eexistLeft = 3;
128
- const fs = {
129
- ...LOCK_FS,
130
- mkdirSync: (p, o) => {
131
- if (p === lockPath && eexistLeft-- > 0) throw eexist();
132
- if (p === lockPath) LOCK_FS.rmSync(lockPath, { recursive: true, force: true }); // holder releases
133
- return LOCK_FS.mkdirSync(p, o);
134
- },
135
- };
136
- const started = Date.now();
137
- const result = withFileLockSync(lockPath, () => "won", { fs, staleMs: 30_000 });
138
- assert.equal(result, "won");
139
- assert.ok(Date.now() - started >= 40, "should have slept through contention ticks");
140
- } finally {
141
- rmSync(root, { recursive: true, force: true });
142
- }
143
- });
144
-
145
- test("withFileLockSync self-heals a parent dir deleted mid-acquisition", { timeout: 5000 }, () => {
146
- const root = freshRoot();
147
- try {
148
- const lockPath = join(root, "del", "parent", "p.lock");
149
- let failedOnce = false;
150
- const fs = {
151
- ...LOCK_FS,
152
- mkdirSync: (p, o) => {
153
- if (p === lockPath && !failedOnce) {
154
- failedOnce = true; // simulate parent deleted between ensureDir and mkdir
155
- const e = new Error("ENOENT");
156
- e.code = "ENOENT";
157
- throw e;
158
- }
159
- return LOCK_FS.mkdirSync(p, o);
160
- },
161
- };
162
- const result = withFileLockSync(lockPath, () => "healed", { fs });
163
- assert.equal(result, "healed");
164
- assert.equal(existsSync(lockPath), false);
165
- } finally {
166
- rmSync(root, { recursive: true, force: true });
167
- }
168
- });
169
-
170
- test("lock timeout errors carry LOCK_TIMEOUT code and the lock path", { timeout: 5000 }, () => {
171
- const root = freshRoot();
172
- try {
173
- const fs = {
174
- ...LOCK_FS,
175
- mkdirSync: (p, o) => {
176
- const e = new Error("EROFS");
177
- e.code = "EROFS";
178
- throw e;
179
- },
180
- };
181
- const lockPath = join(root, "code.lock");
182
- assert.throws(
183
- () => withFileLockSync(lockPath, () => "no", { fs }),
184
- (err) => err.code === "LOCK_TIMEOUT" && String(err.message).includes(lockPath),
185
- );
186
- } finally {
187
- rmSync(root, { recursive: true, force: true });
188
- }
189
- });
190
- ```
191
-
192
- - [ ] **Step 2: Run tests to verify they fail**
193
-
194
- Run: `npm test -- --test-name-pattern "withFileLockSync (throws|waits|self-heals)|lock timeout"`
195
- Expected: the spin-path tests (`mkdirSync keeps failing`, `self-heals`) FAIL via the 5000ms timeout (current code spins forever); the rmSync-steal test fails by timeout too; `owner.json writes` may fail by timeout. RED confirmed.
196
-
197
- - [ ] **Step 3: Implement the new locks.mjs**
198
-
199
- Replace `src/core/locks.mjs` with:
200
-
201
- ```js
202
- /**
203
- * Tiny dependency-free synchronous file lock helpers for local agent-board artifacts.
204
- * Locks use atomic mkdir on a sibling .lock directory and are cleaned up in finally.
205
- *
206
- * Failure model (issue #33): acquisition failures are classified.
207
- * - EEXIST (contention): wait in 20ms ticks until the stale window passes, then
208
- * force-steal (bounded to MAX_STEAL_ATTEMPTS). This preserves the original
209
- * wait/steal contract (see test/locks.test.mjs).
210
- * - Anything else (deleted parent, read-only fs, permissions, disk full, ...):
211
- * MAX_ENV_ATTEMPTS quick retries — each retry re-runs ensureDir so a parent
212
- * deleted mid-acquisition self-heals — then throw LOCK_TIMEOUT. Never spin.
213
- */
214
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
215
- import * as path from "node:path";
216
- import { ensureDir } from "./atomic.mjs";
217
- import * as P from "./paths.mjs";
218
-
219
- const DEFAULT_STALE_MS = 30_000;
220
- /** Minimum contention window before a fresh lock can be force-stolen. */
221
- const MIN_WINDOW_MS = 250;
222
- const WAIT_TICK_MS = 20;
223
- /** Max stale-lock steal attempts before giving up. */
224
- const MAX_STEAL_ATTEMPTS = 2;
225
- /** Max quick retries for environmental failures before giving up. */
226
- const MAX_ENV_ATTEMPTS = 3;
227
-
228
- export const defaultLocksFs = Object.freeze({ existsSync, mkdirSync, readFileSync, rmSync, writeFileSync });
229
-
230
- /**
231
- * @template T
232
- * @param {string} lockPath
233
- * @param {() => T} fn
234
- * @param {{ staleMs?: number, fs?: typeof defaultLocksFs }} [opts]
235
- * @returns {T}
236
- */
237
- export function withFileLockSync(lockPath, fn, opts = {}) {
238
- const fs = opts.fs ?? defaultLocksFs;
239
- acquireLock(lockPath, opts.staleMs ?? DEFAULT_STALE_MS, fs);
240
- let result;
241
- try {
242
- result = fn();
243
- } finally {
244
- releaseLock(lockPath, fs);
245
- }
246
- return result;
247
- }
248
-
249
- /**
250
- * @template T
251
- * @param {string} root
252
- * @param {string} viewId
253
- * @param {string} name
254
- * @param {() => T} fn
255
- * @param {{ staleMs?: number, fs?: typeof defaultLocksFs }} [opts]
256
- * @returns {T}
257
- */
258
- export function withViewLockSync(root, viewId, name, fn, opts = {}) {
259
- return withFileLockSync(P.viewLockPath(root, viewId, name), fn, opts);
260
- }
261
-
262
- /** @param {string} lockPath @param {number} staleMs @param {typeof defaultLocksFs} fs */
263
- function acquireLock(lockPath, staleMs, fs) {
264
- const deadline = Date.now() + Math.max(MIN_WINDOW_MS, staleMs);
265
- let steals = 0;
266
- let envAttempts = 0;
267
- for (;;) {
268
- let created = false;
269
- try {
270
- // Re-run every attempt: a parent deleted mid-acquisition self-heals here.
271
- ensureDir(path.dirname(lockPath));
272
- fs.mkdirSync(lockPath);
273
- created = true;
274
- fs.writeFileSync(
275
- path.join(lockPath, "owner.json"),
276
- JSON.stringify({ pid: process.pid, at: Date.now() }),
277
- "utf8",
278
- );
279
- return;
280
- } catch (err) {
281
- if (err && err.code === "EEXIST") {
282
- const expired = Date.now() >= deadline;
283
- if (isLockStale(lockPath, staleMs, fs) || expired) {
284
- if (steals >= MAX_STEAL_ATTEMPTS) {
285
- throw lockError(lockPath, `stale lock could not be stolen after ${steals} attempts`);
286
- }
287
- steals += 1;
288
- releaseLock(lockPath, fs);
289
- continue;
290
- }
291
- sleep(WAIT_TICK_MS);
292
- continue;
293
- }
294
- // Environmental failure: bounded quick retries, then fail fast.
295
- if (created) releaseLock(lockPath, fs);
296
- envAttempts += 1;
297
- if (envAttempts >= MAX_ENV_ATTEMPTS) {
298
- const reason = (err && (err.code || err.message)) || "unknown error";
299
- throw lockError(lockPath, `lock path unusable (${reason})`);
300
- }
301
- sleep(WAIT_TICK_MS);
302
- }
303
- }
304
- }
305
-
306
- /** @param {string} lockPath @param {string} reason */
307
- function lockError(lockPath, reason) {
308
- const err = new Error(`file lock unavailable: ${lockPath} (${reason})`);
309
- err.code = "LOCK_TIMEOUT";
310
- return err;
311
- }
312
-
313
- /** @param {number} ms */
314
- function sleep(ms) {
315
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
316
- }
317
-
318
- /** @param {string} lockPath @param {number} staleMs @param {typeof defaultLocksFs} fs */
319
- function isLockStale(lockPath, staleMs, fs) {
320
- try {
321
- if (!fs.existsSync(lockPath)) return false;
322
- const raw = fs.readFileSync(path.join(lockPath, "owner.json"), "utf8");
323
- const owner = JSON.parse(raw);
324
- return Date.now() - Number(owner.at ?? 0) > staleMs;
325
- } catch {
326
- return true;
327
- }
328
- }
329
-
330
- /** @param {string} lockPath @param {typeof defaultLocksFs} fs */
331
- function releaseLock(lockPath, fs) {
332
- try {
333
- fs.rmSync(lockPath, { recursive: true, force: true });
334
- } catch {
335
- /* best effort */
336
- }
337
- }
338
- ```
339
-
340
- - [ ] **Step 4: Run tests to verify green**
341
-
342
- Run: `npm test -- --test-name-pattern "withFileLockSync|withViewLockSync|lock timeout"`
343
- Expected: ALL PASS — the 5 existing tests unmodified plus the 6 new ones.
344
-
345
- - [ ] **Step 5: Commit**
346
-
347
- ```bash
348
- git add src/core/locks.mjs test/locks.test.mjs
349
- git commit -m "fix: bound lock acquisition with error classification and fs injection (issue #33)"
350
- ```
351
-
352
- ---
353
-
354
- ### Task 2: follow-up-queue.mjs — catch-all {ok:false} translation
355
-
356
- **Files:**
357
- - Modify: `src/core/follow-up-queue.mjs` (route all 5 lock sites through one guarded helper)
358
- - Test: `test/follow-up-queue.test.mjs` (append)
359
-
360
- **Interfaces:**
361
- - Consumes: Task 1's `withViewLockSync(root, viewId, name, fn, opts?)` (throws `LOCK_TIMEOUT` on failure).
362
- - Produces: unchanged public signatures; NEW behavior — every mutating queue op (`enqueueFollowUp`, `claimNextFollowUp`, `completeFollowUp`, `failFollowUp`, `releaseFollowUp`, `removeLastFollowUp`, `clearQueuedFollowUps`) returns `{ok:false, error}` instead of throwing when the lock (or any fs op inside fn) fails. `service.mjs` and `runner/job-runner.mjs` consume the `{ok}` convention unchanged.
363
-
364
- - [ ] **Step 1: Write the failing test**
365
-
366
- Append to `test/follow-up-queue.test.mjs` (add `writeFileSync` to the `node:fs` import):
367
-
368
- ```js
369
- test("queue ops return {ok:false} when the lock path is unusable", { timeout: 5000 }, () => {
370
- const root = freshRoot();
371
- try {
372
- const fileRoot = join(root, "notadir");
373
- writeFileSync(fileRoot, "x", "utf8"); // ensureDir under a file path -> ENOTDIR, unrecoverable
374
- const enq = enqueueFollowUp(fileRoot, "v1", "hello");
375
- assert.equal(enq.ok, false);
376
- assert.match(String(enq.error), /lock unavailable/);
377
- const claim = claimNextFollowUp(fileRoot, "v1");
378
- assert.equal(claim.ok, false);
379
- assert.match(String(claim.error), /lock unavailable/);
380
- const removed = removeLastFollowUp(fileRoot, "v1");
381
- assert.equal(removed.ok, false);
382
- } finally {
383
- rmSync(root, { recursive: true, force: true });
384
- }
385
- });
386
- ```
387
-
388
- - [ ] **Step 2: Run to verify it fails**
389
-
390
- Run: `npm test -- --test-name-pattern "lock path is unusable"`
391
- Expected: FAIL — current code propagates the raw ENOTDIR throw (uncaught error fails the test).
392
-
393
- - [ ] **Step 3: Implement**
394
-
395
- In `src/core/follow-up-queue.mjs`: add a guarded wrapper after the imports and route the 5 `withViewLockSync(...)` call sites (`enqueueFollowUp`, `claimNextFollowUp`, `removeLastFollowUp`, `clearQueuedFollowUps`, and the private `updateItem`) through it — replace each `return withViewLockSync(root, viewId, "queue", () => { ... })` with `return lockedQueueOp(root, viewId, () => { ... })` (body unchanged):
396
-
397
- ```js
398
- import { withViewLockSync } from "./locks.mjs";
399
- // ...existing imports...
400
-
401
- /**
402
- * Run a queue mutation under the view lock, translating any failure (lock
403
- * unavailable, fs errors inside the mutation) into {ok:false} so callers on
404
- * the {ok} convention never see a throw (issue #33).
405
- * @template T
406
- * @param {string} root
407
- * @param {string} viewId
408
- * @param {() => T} fn
409
- * @returns {T | { ok: false, error: string }}
410
- */
411
- function lockedQueueOp(root, viewId, fn) {
412
- try {
413
- return withViewLockSync(root, viewId, "queue", fn);
414
- } catch (err) {
415
- return { ok: false, error: `follow-up queue lock unavailable: ${err instanceof Error ? err.message : String(err)}` };
416
- }
417
- }
418
- ```
419
-
420
- - [ ] **Step 4: Run the full queue suite**
421
-
422
- Run: `npm test -- --test-name-pattern "follow-up|queue"`
423
- Expected: ALL PASS (existing FIFO/durability tests + the new one).
424
-
425
- - [ ] **Step 5: Commit**
426
-
427
- ```bash
428
- git add src/core/follow-up-queue.mjs test/follow-up-queue.test.mjs
429
- git commit -m "fix: translate follow-up queue lock failures into {ok:false} results (issue #33)"
430
- ```
431
-
432
- ---
433
-
434
- ### Task 3: job-runner.mjs — finalize chain can never skip process.exit
435
-
436
- **Files:**
437
- - Modify: `runner/job-runner.mjs` (the `worker.on("close")` `.finally` block only)
438
-
439
- **Interfaces:**
440
- - Consumes: Task 2's `{ok:false}` queue results (`claimNextFollowUp` no longer throws on lock failure — the existing `if (!claimed.ok || !claimed.item) return;` already handles it).
441
- - Produces: no signature changes. Behavioral guarantee: `finalizeSteeringIfNeeded` and `drainQueuedFollowUp` failures are logged best-effort and `process.exit` always runs.
442
-
443
- **Note:** this task has no practical unit test (module runs `main()` on import and the guarded code is inside a child-process event chain). It is covered by Task 4's integration tests + the fix is structurally simple. Red-green is not applicable; code-review verification is the gate (spec test-plan item 7).
444
-
445
- - [ ] **Step 1: Implement the guards**
446
-
447
- In `runner/job-runner.mjs`, replace the `.finally(() => { ... })` block:
448
-
449
- ```js
450
- .finally(() => {
451
- // The finalize chain must never prevent process.exit: a lock/fs failure
452
- // here used to pin the runner as a 100% CPU zombie (issue #33).
453
- try {
454
- finalizeSteeringIfNeeded(config, status, evidence);
455
- } catch (err) {
456
- tryAppendDiagnostic(config, "finalize_steering_failed", err);
457
- }
458
- try {
459
- drainQueuedFollowUp(config, status);
460
- } catch (err) {
461
- tryAppendDiagnostic(config, "follow_up_drain_failed", err);
462
- }
463
- process.exit(stoppedByUser ? 0 : (code ?? 0));
464
- });
465
- ```
466
-
467
- And add this helper next to `drainQueuedFollowUp` (appendDiagnostic itself throws on a deleted root — verified `appendLine` is not best-effort):
468
-
469
- ```js
470
- /** @param {import("../src/core/types.mjs").RunConfig} config @param {string} code @param {unknown} err */
471
- function tryAppendDiagnostic(config, code, err) {
472
- try {
473
- appendDiagnostic(config.root, config.viewId, {
474
- source: "runner",
475
- runId: config.runId,
476
- level: "error",
477
- code,
478
- message: "Finalize step failed",
479
- details: { error: err instanceof Error ? err.message : String(err) },
480
- });
481
- } catch {
482
- /* root may be deleted — nothing to persist, exit anyway */
483
- }
484
- }
485
- ```
486
-
487
- - [ ] **Step 2: Sanity-check syntax + typecheck**
488
-
489
- Run: `node --check runner/job-runner.mjs && npm run typecheck`
490
- Expected: both clean.
491
-
492
- - [ ] **Step 3: Commit**
493
-
494
- ```bash
495
- git add runner/job-runner.mjs
496
- git commit -m "fix: guard job-runner finalize chain so process.exit always runs (issue #33)"
497
- ```
498
-
499
- ---
500
-
501
- ### Task 4: integration tests — kill detached runners in teardown
502
-
503
- **Files:**
504
- - Modify: `test/runner.integration.test.mjs`
505
-
506
- **Interfaces:**
507
- - Consumes: `launchRun(root, config, {runnerScript})` returns `{pid}` (all 7 call sites must capture it — 4 currently discard: the `needs_input`, `dash-prefixed`, `worker exits nonzero`, and `worker error` tests).
508
- - Produces: `killDetached(pid)` async helper; every test's `finally` kills the runner BEFORE `rmSync(root)`.
509
-
510
- - [ ] **Step 1: Add the helper (after the existing `sleep` definition)**
511
-
512
- ```js
513
- /** Kill a detached runner before deleting its root so it can never orphan (issue #33). */
514
- async function killDetached(pid) {
515
- if (!pid || pid <= 0) return;
516
- try {
517
- process.kill(pid, "SIGTERM");
518
- } catch {
519
- return; // already exited
520
- }
521
- const deadline = Date.now() + 1000;
522
- while (Date.now() < deadline) {
523
- await sleep(50);
524
- try {
525
- process.kill(pid, 0);
526
- } catch {
527
- return; // exited
528
- }
529
- }
530
- try {
531
- process.kill(pid, "SIGKILL");
532
- } catch {
533
- /* already gone */
534
- }
535
- }
536
- ```
537
-
538
- - [ ] **Step 2: Capture pids at the 4 discarding call sites**
539
-
540
- Change `launchRun(root, config, { runnerScript: RUNNER });` to `const { pid } = launchRun(root, config, { runnerScript: RUNNER });` in the `needs_input`, `dash-prefixed prompts`, `worker exits nonzero`, and `worker error` tests. The `auto-classifies`, `readPid`, and remaining tests already capture the pid — leave them (rename only if a shadow conflict with `pid` arises; use `runnerPid` as the variable name at ALL sites for uniformity).
541
-
542
- - [ ] **Step 3: Kill before rmSync in every finally**
543
-
544
- In each test's `finally`, add `await killDetached(runnerPid);` as the FIRST statement, before `rmSync(root, ...)`. Example (first test):
545
-
546
- ```js
547
- } finally {
548
- delete process.env.FAKE_PI_MODE;
549
- // ...
550
- await killDetached(runnerPid);
551
- rmSync(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
552
- }
553
- ```
554
-
555
- - [ ] **Step 4: Run the integration suite and check for leftovers**
556
-
557
- Run: `npm test -- --test-name-pattern "runner" && (ps -ef | grep '[j]ob-runner.mjs' || echo "no leftover runners")`
558
- Expected: all runner tests PASS and `no leftover runners` prints.
559
-
560
- - [ ] **Step 5: Commit**
561
-
562
- ```bash
563
- git add test/runner.integration.test.mjs
564
- git commit -m "test: kill detached job-runners in integration test teardown (issue #33)"
565
- ```
566
-
567
- ---
568
-
569
- ### Task 5: full verify + field-repro regression check
570
-
571
- **Files:**
572
- - Create: none (verification only; fixups allowed if verification exposes issues)
573
-
574
- - [ ] **Step 1: Full gate**
575
-
576
- Run: `npm run verify`
577
- Expected: typecheck + tests + coverage thresholds + pack:dry all green. If coverage thresholds fail on new branches, extend the Task 1 tests (not the thresholds).
578
-
579
- - [ ] **Step 2: Field-repro regression check (the /sys read-only scenario from the issue)**
580
-
581
- Run:
582
- ```bash
583
- node -e '
584
- import("/home/elling/git-repo/github/pi-agent-board/.pi/worktrees/issue-33-locks-acquirelock-spin/src/core/locks.mjs").then(({ withFileLockSync }) => {
585
- const t0 = Date.now();
586
- try {
587
- withFileLockSync("/sys/bus/pi-agent-board-repro.lock", () => {});
588
- console.log("UNEXPECTED: acquired");
589
- } catch (err) {
590
- console.log("OK: threw in", Date.now() - t0, "ms:", err.message);
591
- }
592
- });'
593
- ```
594
- Expected: `OK: threw in < 1000 ms: file lock unavailable: /sys/bus/... (lock path unusable (EROFS))` — the pre-fix behavior was an infinite spin.
595
-
596
- - [ ] **Step 3: No zombies after the suite**
597
-
598
- Run: `ps -ef | grep -E '[j]ob-runner|[p]ty-runner.*agentview' || echo clean`
599
- Expected: `clean` (or only the user's real agent-board runners under ~/.pi/agent/agent-board).
600
-
601
- - [ ] **Step 4: Commit any fixups (only if Steps 1-3 required changes)**
602
-
603
- ```bash
604
- git add <changed files>
605
- git commit -m "fix: address verify findings for issue #33"
606
- ```
607
-
608
- ---
609
-
610
- ## Self-Review
611
-
612
- - **Spec coverage:** D1/D3/D4/D5/D8 → Task 1; D4b (re-ensureDir self-heal) → Task 1 implementation + self-heal test; D2 (wait/steal contract) → Task 1 keeps existing tests unmodified; D6 → Task 2; D7 → Task 3; D9 → Task 4; spec test items 1-3 → Task 1 tests, item 4/4b → Task 1 contention/self-heal tests, item 5 → Task 1 Step 4, item 6 → Task 2, item 7 → Task 3 (CR gate, documented), item 8 → Task 4 Step 4. Acceptance items map to Task 5. No gaps.
613
- - **Placeholder scan:** all steps contain concrete code / commands / expected output. No TBDs.
614
- - **Type consistency:** `defaultLocksFs` name used in Task 1 tests + implementation; `LOCK_TIMEOUT` code asserted in Task 1 and produced by `lockError`; `lockedQueueOp` defined in Task 2 and used at all 5 sites; `killDetached` defined and used in Task 4; `runnerPid` naming uniform in Task 4.
@@ -1,32 +0,0 @@
1
- # Plan: fix pty-runner integration test flaky timeout (issue #34)
2
-
3
- ## Goal
4
-
5
- Make `pty-runner creates host socket, broadcasts output, forwards input, finalizes` deterministic on CI by not depending on pre-connect socket output. No product code change.
6
-
7
- ## Tasks
8
-
9
- 1. **Edit test** — `test/pty-runner.integration.test.mjs`
10
- - Replace `await waitFor(() => messages.find((m) => m.type === "output" && m.data.includes("fake pi ready")));`
11
- with a screen-log read wait:
12
- ```js
13
- await waitFor(() => {
14
- try {
15
- return readFileSync(P.screenLogPath(root, "v1"), "utf8").includes("fake pi ready");
16
- } catch {
17
- return false;
18
- }
19
- });
20
- ```
21
- - Keep all socket-based assertions (echo:hello, resize, exit) unchanged.
22
- 2. **Verify** — run the pty-runner integration test repeatedly:
23
- - `node --test test/pty-runner.integration.test.mjs` × 6, expect 3/3 pass each run.
24
- - Forced late-connect scenario (output before connect) passes.
25
- - Full suite `npm test` — 313/313 pass.
26
- 3. **Commit** — conventional commit: `fix: make pty-runner integration test timing-independent (issue #34)`
27
- 4. **PR** — push `issue-34-pty-runner-test-flaky`, open PR against main, tag `zima:needs-review`, await CR, converge, merge.
28
-
29
- ## Verification
30
-
31
- - Deterministic under the previously-failing timing (output emitted before client connects).
32
- - No regression: full test suite green.