@huseynsnmz/pi-suite 0.0.0-stage → 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 (274) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/LICENSE +21 -0
  3. package/README.md +88 -2
  4. package/SECURITY.md +5 -0
  5. package/package.json +112 -4
  6. package/packages/board/CHANGELOG.md +340 -0
  7. package/packages/board/CONTRIBUTING.md +7 -0
  8. package/packages/board/LICENSE +21 -0
  9. package/packages/board/README.md +88 -0
  10. package/packages/board/SECURITY.md +19 -0
  11. package/packages/board/docs/how-it-works.md +31 -0
  12. package/packages/board/docs/reference.md +278 -0
  13. package/packages/board/package.json +81 -0
  14. package/packages/board/runner/job-runner-legacy.mjs +68 -0
  15. package/packages/board/runner/job-runner.mjs +834 -0
  16. package/packages/board/runner/pty-runner-legacy.mjs +50 -0
  17. package/packages/board/runner/pty-runner.mjs +2175 -0
  18. package/packages/board/runner/state-coordinator.mjs +673 -0
  19. package/packages/board/runner/state-runner.mjs +203 -0
  20. package/packages/board/runner/title-runner.mjs +80 -0
  21. package/packages/board/src/commands/agent-board.ts +394 -0
  22. package/packages/board/src/commands/attach-decision.mjs +66 -0
  23. package/packages/board/src/commands/attach-flow.ts +251 -0
  24. package/packages/board/src/commands/bg.ts +103 -0
  25. package/packages/board/src/commands/board-model.ts +21 -0
  26. package/packages/board/src/core/atomic.mjs +184 -0
  27. package/packages/board/src/core/attach-delete.mjs +24 -0
  28. package/packages/board/src/core/attach-frame-proof.mjs +870 -0
  29. package/packages/board/src/core/attach-frame-reporter.mjs +648 -0
  30. package/packages/board/src/core/attach-frame-stream.mjs +266 -0
  31. package/packages/board/src/core/attach-view-proof.mjs +709 -0
  32. package/packages/board/src/core/auto-state.mjs +377 -0
  33. package/packages/board/src/core/background-runs.mjs +273 -0
  34. package/packages/board/src/core/code-refs-store.mjs +315 -0
  35. package/packages/board/src/core/code-refs.mjs +913 -0
  36. package/packages/board/src/core/config.mjs +77 -0
  37. package/packages/board/src/core/control-protocol.mjs +482 -0
  38. package/packages/board/src/core/coordinator-client.mjs +633 -0
  39. package/packages/board/src/core/coordinator-journal.mjs +282 -0
  40. package/packages/board/src/core/coordinator-protocol.mjs +14 -0
  41. package/packages/board/src/core/cwd-stats.mjs +114 -0
  42. package/packages/board/src/core/dashboard-render.mjs +10 -0
  43. package/packages/board/src/core/derive.mjs +117 -0
  44. package/packages/board/src/core/diagnostics.mjs +133 -0
  45. package/packages/board/src/core/double-press.mjs +26 -0
  46. package/packages/board/src/core/editor-state-reporter.mjs +112 -0
  47. package/packages/board/src/core/events.mjs +286 -0
  48. package/packages/board/src/core/evidence.mjs +245 -0
  49. package/packages/board/src/core/follow-up-queue.mjs +211 -0
  50. package/packages/board/src/core/foreground-preview-cache.mjs +117 -0
  51. package/packages/board/src/core/foreground-projection-cache.mjs +99 -0
  52. package/packages/board/src/core/frame-witness-update.mjs +35 -0
  53. package/packages/board/src/core/heuristics.mjs +313 -0
  54. package/packages/board/src/core/hold-input.mjs +27 -0
  55. package/packages/board/src/core/host-coordination.mjs +300 -0
  56. package/packages/board/src/core/host-crash.mjs +79 -0
  57. package/packages/board/src/core/host-probe.mjs +207 -0
  58. package/packages/board/src/core/host-protocol.mjs +73 -0
  59. package/packages/board/src/core/ids.mjs +35 -0
  60. package/packages/board/src/core/ime-cursor-coalesce.mjs +193 -0
  61. package/packages/board/src/core/invocation.mjs +43 -0
  62. package/packages/board/src/core/launch-options.mjs +418 -0
  63. package/packages/board/src/core/launch.mjs +157 -0
  64. package/packages/board/src/core/load-config.mjs +4 -0
  65. package/packages/board/src/core/locks.mjs +530 -0
  66. package/packages/board/src/core/paths.mjs +183 -0
  67. package/packages/board/src/core/pid.mjs +75 -0
  68. package/packages/board/src/core/prewarm-schedule.mjs +41 -0
  69. package/packages/board/src/core/process-identity-probe.mjs +228 -0
  70. package/packages/board/src/core/prompt-transport.mjs +13 -0
  71. package/packages/board/src/core/pty-attach-jiggle-controller.mjs +356 -0
  72. package/packages/board/src/core/pty-attach-jiggle-retry.mjs +109 -0
  73. package/packages/board/src/core/pty-attach-reconnect.mjs +61 -0
  74. package/packages/board/src/core/pty-attach-render.mjs +229 -0
  75. package/packages/board/src/core/pty-links.mjs +81 -0
  76. package/packages/board/src/core/pty-scroll.mjs +522 -0
  77. package/packages/board/src/core/pty-support.mjs +327 -0
  78. package/packages/board/src/core/repo.mjs +103 -0
  79. package/packages/board/src/core/rows.mjs +414 -0
  80. package/packages/board/src/core/screen-log-gc.mjs +198 -0
  81. package/packages/board/src/core/screen-log.mjs +160 -0
  82. package/packages/board/src/core/session-view.mjs +174 -0
  83. package/packages/board/src/core/slash-commands.mjs +52 -0
  84. package/packages/board/src/core/state-commands.mjs +757 -0
  85. package/packages/board/src/core/status-consistency.mjs +98 -0
  86. package/packages/board/src/core/steering-prompts.mjs +34 -0
  87. package/packages/board/src/core/steering.mjs +133 -0
  88. package/packages/board/src/core/stop-latch.mjs +41 -0
  89. package/packages/board/src/core/store.mjs +523 -0
  90. package/packages/board/src/core/suspend.mjs +36 -0
  91. package/packages/board/src/core/terminal-attach-client.mjs +981 -0
  92. package/packages/board/src/core/terminal-attach-protocol.mjs +460 -0
  93. package/packages/board/src/core/terminal-model.mjs +222 -0
  94. package/packages/board/src/core/terminal-query-sequences.mjs +239 -0
  95. package/packages/board/src/core/terminal-reply-guard.mjs +222 -0
  96. package/packages/board/src/core/terminal-snapshot.mjs +440 -0
  97. package/packages/board/src/core/test-knobs.mjs +28 -0
  98. package/packages/board/src/core/title.mjs +43 -0
  99. package/packages/board/src/core/types.mjs +414 -0
  100. package/packages/board/src/core/warm-host-sweeper.mjs +168 -0
  101. package/packages/board/src/core/worktree.mjs +65 -0
  102. package/packages/board/src/index.ts +303 -0
  103. package/packages/board/src/runtime/service.mjs +3222 -0
  104. package/packages/board/src/ui/chrome.ts +20 -0
  105. package/packages/board/src/ui/dashboard-decisions.mjs +55 -0
  106. package/packages/board/src/ui/dashboard-evidence.mjs +85 -0
  107. package/packages/board/src/ui/dashboard.ts +2237 -0
  108. package/packages/board/src/ui/double-left.ts +30 -0
  109. package/packages/board/src/ui/external-editor.ts +25 -0
  110. package/packages/board/src/ui/focus.ts +32 -0
  111. package/packages/board/src/ui/pty-attach.ts +3184 -0
  112. package/packages/board/src/ui/waiting.ts +30 -0
  113. package/packages/commands/LICENSE +21 -0
  114. package/packages/commands/README.md +19 -0
  115. package/packages/commands/package.json +22 -0
  116. package/packages/commands/src/index.ts +43 -0
  117. package/packages/commands/src/security-review.ts +109 -0
  118. package/packages/frame/CHANGELOG.md +113 -0
  119. package/packages/frame/CONTRIBUTING.md +7 -0
  120. package/packages/frame/LICENSE +22 -0
  121. package/packages/frame/README.md +105 -0
  122. package/packages/frame/SECURITY.md +16 -0
  123. package/packages/frame/docs/how-it-works.md +53 -0
  124. package/packages/frame/docs/reference.md +78 -0
  125. package/packages/frame/package.json +72 -0
  126. package/packages/frame/src/data/git.ts +90 -0
  127. package/packages/frame/src/data/github.ts +140 -0
  128. package/packages/frame/src/data/gitlab.ts +63 -0
  129. package/packages/frame/src/data/mode.ts +49 -0
  130. package/packages/frame/src/data/past-prompts.ts +57 -0
  131. package/packages/frame/src/data/runner.ts +32 -0
  132. package/packages/frame/src/data/snapshot.ts +223 -0
  133. package/packages/frame/src/data/start-notes.ts +104 -0
  134. package/packages/frame/src/data/telemetry.ts +337 -0
  135. package/packages/frame/src/data/todos.ts +162 -0
  136. package/packages/frame/src/data/totals.ts +94 -0
  137. package/packages/frame/src/data/visits.ts +45 -0
  138. package/packages/frame/src/index.ts +696 -0
  139. package/packages/frame/src/kit/LICENSE +21 -0
  140. package/packages/frame/src/kit/app/attribution.ts +30 -0
  141. package/packages/frame/src/kit/app/builder.ts +186 -0
  142. package/packages/frame/src/kit/app/claim.ts +27 -0
  143. package/packages/frame/src/kit/app/feature.ts +91 -0
  144. package/packages/frame/src/kit/app/host.ts +214 -0
  145. package/packages/frame/src/kit/app/renderers.ts +62 -0
  146. package/packages/frame/src/kit/app/scope.ts +125 -0
  147. package/packages/frame/src/kit/board/model.ts +337 -0
  148. package/packages/frame/src/kit/contract.ts +79 -0
  149. package/packages/frame/src/kit/contracts/accounts.ts +33 -0
  150. package/packages/frame/src/kit/contracts/ask.ts +120 -0
  151. package/packages/frame/src/kit/contracts/board.ts +6 -0
  152. package/packages/frame/src/kit/contracts/by.ts +22 -0
  153. package/packages/frame/src/kit/contracts/calls.ts +46 -0
  154. package/packages/frame/src/kit/contracts/events.ts +89 -0
  155. package/packages/frame/src/kit/contracts/mode.ts +26 -0
  156. package/packages/frame/src/kit/contracts/names.ts +12 -0
  157. package/packages/frame/src/kit/contracts/peers.ts +77 -0
  158. package/packages/frame/src/kit/contracts/prompts.ts +8 -0
  159. package/packages/frame/src/kit/contracts/screen.ts +75 -0
  160. package/packages/frame/src/kit/contracts/skills.ts +81 -0
  161. package/packages/frame/src/kit/contracts/sources.ts +81 -0
  162. package/packages/frame/src/kit/contracts/views.ts +42 -0
  163. package/packages/frame/src/kit/control.ts +59 -0
  164. package/packages/frame/src/kit/dark-theme.ts +41 -0
  165. package/packages/frame/src/kit/decode.ts +210 -0
  166. package/packages/frame/src/kit/events/log.ts +313 -0
  167. package/packages/frame/src/kit/format.ts +25 -0
  168. package/packages/frame/src/kit/host.ts +4 -0
  169. package/packages/frame/src/kit/index.ts +25 -0
  170. package/packages/frame/src/kit/legacy-tasks.ts +39 -0
  171. package/packages/frame/src/kit/overlay-images.ts +36 -0
  172. package/packages/frame/src/kit/paths.ts +16 -0
  173. package/packages/frame/src/kit/screen/client.ts +74 -0
  174. package/packages/frame/src/kit/screen/feature.ts +64 -0
  175. package/packages/frame/src/kit/screen/model.ts +182 -0
  176. package/packages/frame/src/kit/screen/view.ts +838 -0
  177. package/packages/frame/src/kit/settings/files.ts +90 -0
  178. package/packages/frame/src/kit/settings/setting.ts +57 -0
  179. package/packages/frame/src/kit/settings/store.ts +192 -0
  180. package/packages/frame/src/kit/sources/file.ts +129 -0
  181. package/packages/frame/src/kit/sources/follow.ts +155 -0
  182. package/packages/frame/src/kit/testing.ts +260 -0
  183. package/packages/frame/src/kit/totals.ts +72 -0
  184. package/packages/frame/src/kit/ui/entry.ts +75 -0
  185. package/packages/frame/src/kit/ui/frame.ts +212 -0
  186. package/packages/frame/src/kit/ui/glyphs.ts +132 -0
  187. package/packages/frame/src/kit/ui/scroll.ts +14 -0
  188. package/packages/frame/src/render/diff.ts +157 -0
  189. package/packages/frame/src/render/fit.ts +86 -0
  190. package/packages/frame/src/render/format.ts +108 -0
  191. package/packages/frame/src/render/frame.ts +106 -0
  192. package/packages/frame/src/render/glyphs.ts +189 -0
  193. package/packages/frame/src/render/keys.ts +43 -0
  194. package/packages/frame/src/render/lead.ts +37 -0
  195. package/packages/frame/src/render/paint.ts +135 -0
  196. package/packages/frame/src/render/safe.ts +19 -0
  197. package/packages/frame/src/render/segments.ts +735 -0
  198. package/packages/frame/src/render/streaming.ts +223 -0
  199. package/packages/frame/src/render/timing.ts +106 -0
  200. package/packages/frame/src/render/tools.ts +167 -0
  201. package/packages/frame/src/settings.ts +105 -0
  202. package/packages/frame/src/ui/editor.ts +272 -0
  203. package/packages/frame/src/ui/effort.ts +36 -0
  204. package/packages/frame/src/ui/footer.ts +165 -0
  205. package/packages/frame/src/ui/header.ts +362 -0
  206. package/packages/frame/src/ui/history-search.ts +23 -0
  207. package/packages/frame/src/ui/interrupt.ts +21 -0
  208. package/packages/frame/src/ui/outputs.ts +170 -0
  209. package/packages/frame/src/ui/pi-entries.ts +298 -0
  210. package/packages/frame/src/ui/previews.ts +72 -0
  211. package/packages/frame/src/ui/queue.ts +75 -0
  212. package/packages/frame/src/ui/screen.ts +59 -0
  213. package/packages/frame/src/ui/timings.ts +48 -0
  214. package/packages/frame/src/ui/todos.ts +205 -0
  215. package/packages/frame/src/ui/tool-frame.ts +595 -0
  216. package/packages/frame/src/ui/working.ts +25 -0
  217. package/packages/frame/src/view/state.ts +97 -0
  218. package/packages/frame/src/view/transcript.ts +175 -0
  219. package/packages/frame/src/view/view.ts +162 -0
  220. package/packages/memory/CONTRIBUTING.md +7 -0
  221. package/packages/memory/LICENSE +21 -0
  222. package/packages/memory/README.md +44 -0
  223. package/packages/memory/SECURITY.md +20 -0
  224. package/packages/memory/package.json +55 -0
  225. package/packages/memory/skills/review-memory/SKILL.md +48 -0
  226. package/packages/memory/src/index.ts +163 -0
  227. package/packages/memory/src/prompt.ts +25 -0
  228. package/packages/memory/src/review.ts +150 -0
  229. package/packages/memory/src/secrets.ts +22 -0
  230. package/packages/memory/src/store.ts +173 -0
  231. package/packages/modes/CHANGELOG.md +104 -0
  232. package/packages/modes/CONTRIBUTING.md +7 -0
  233. package/packages/modes/LICENSE +21 -0
  234. package/packages/modes/README.md +174 -0
  235. package/packages/modes/SECURITY.md +17 -0
  236. package/packages/modes/package.json +55 -0
  237. package/packages/modes/skills/review-permissions/SKILL.md +54 -0
  238. package/packages/modes/src/bash.ts +218 -0
  239. package/packages/modes/src/deny-off.ts +62 -0
  240. package/packages/modes/src/git-safety.ts +72 -0
  241. package/packages/modes/src/index.ts +338 -0
  242. package/packages/modes/src/interview.ts +206 -0
  243. package/packages/modes/src/judge-gate.ts +113 -0
  244. package/packages/modes/src/modes.ts +121 -0
  245. package/packages/modes/src/review/apply.mjs +156 -0
  246. package/packages/modes/src/review/collect.mjs +208 -0
  247. package/packages/modes/src/review/tools.ts +71 -0
  248. package/packages/modes/src/sandbox.ts +37 -0
  249. package/packages/router/CHANGELOG.md +8 -0
  250. package/packages/router/LICENSE +21 -0
  251. package/packages/router/README.md +90 -0
  252. package/packages/router/package.json +51 -0
  253. package/packages/router/src/config.ts +65 -0
  254. package/packages/router/src/index.ts +172 -0
  255. package/packages/router/src/judge.ts +51 -0
  256. package/packages/router/src/route.ts +67 -0
  257. package/packages/web/CHANGELOG.md +36 -0
  258. package/packages/web/LICENSE +21 -0
  259. package/packages/web/README.md +87 -0
  260. package/packages/web/SECURITY.md +13 -0
  261. package/packages/web/package.json +59 -0
  262. package/packages/web/src/config.ts +109 -0
  263. package/packages/web/src/dnsfilter.ts +97 -0
  264. package/packages/web/src/extract.ts +65 -0
  265. package/packages/web/src/index.ts +217 -0
  266. package/packages/web/src/markdown.ts +122 -0
  267. package/packages/web/src/net.ts +109 -0
  268. package/packages/web/src/reader.ts +73 -0
  269. package/packages/web/src/sanitize.ts +50 -0
  270. package/packages/web/src/search.ts +76 -0
  271. package/packages/web/src/secrets.ts +22 -0
  272. package/packages/web/src/validate.ts +186 -0
  273. package/src/index.ts +156 -0
  274. package/src/settings.ts +79 -0
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Filesystem layout for the agent-board store.
3
+ *
4
+ * Every helper takes an explicit `root` so tests can point at a tmp dir.
5
+ * The live default is `~/.pi/agent/agent-board/` (override with $AGENT_BOARD_ROOT;
6
+ * legacy $AGENT_VIEW_ROOT is also honored for migration).
7
+ */
8
+ import { createHash } from "node:crypto";
9
+ import * as os from "node:os";
10
+ import * as path from "node:path";
11
+
12
+ /** @returns {string} the live store root (env override or ~/.pi/agent/agent-board). */
13
+ export function defaultRoot() {
14
+ if (process.env.AGENT_BOARD_ROOT) return path.resolve(process.env.AGENT_BOARD_ROOT);
15
+ if (process.env.AGENT_VIEW_ROOT) return path.resolve(process.env.AGENT_VIEW_ROOT);
16
+ return path.join(os.homedir(), ".pi", "agent", "agent-board");
17
+ }
18
+
19
+ /** @param {string} root */
20
+ export const rosterPath = (root) => path.join(root, "roster.json");
21
+ /** @param {string} root */
22
+ export const launchPrefsPath = (root) => path.join(root, "launch-prefs.json");
23
+ /** @param {string} root */
24
+ export const gcHistoryPath = (root) => path.join(root, "gc-history.jsonl");
25
+ /** @param {string} root */
26
+ export const cwdStatsPath = (root) => path.join(root, "cwd-stats.json");
27
+ /** @param {string} root */
28
+ export const providersPath = (root) => path.join(root, "providers.json");
29
+
30
+ /** @param {string} root */
31
+ export const viewsDir = (root) => path.join(root, "views");
32
+ /** @param {string} root @param {string} viewId */
33
+ export const viewDir = (root, viewId) => path.join(root, "views", viewId);
34
+ /** @param {string} root @param {string} viewId */
35
+ export const metaPath = (root, viewId) => path.join(viewDir(root, viewId), "meta.json");
36
+ /** @param {string} root @param {string} viewId */
37
+ export const statePath = (root, viewId) => path.join(viewDir(root, viewId), "state.json");
38
+ /** @param {string} root @param {string} viewId */
39
+ export const hostPath = (root, viewId) => path.join(viewDir(root, viewId), "host.json");
40
+ /** Durable control-command journal for a view's host (issue #91 phase 5, spec
41
+ * D4): one JSONL line per durable-command lifecycle transition
42
+ * (accepted/applied). Sibling of host.json in the view dir. */
43
+ export const controlJournalPath = (root, viewId) => path.join(viewDir(root, viewId), "control-journal.jsonl");
44
+ /** @param {string} root @param {string} viewId */
45
+ export const hostConfigPath = (root, viewId) => path.join(viewDir(root, viewId), "host-config.json");
46
+ /**
47
+ * Per-instance host config path. Each host launch owns its own config file so a
48
+ * concurrent launch can never overwrite another instance's prompt/cwd/model (issue #70).
49
+ * @param {string} root @param {string} viewId @param {string} instanceId
50
+ */
51
+ export const hostConfigPathFor = (root, viewId, instanceId) =>
52
+ path.join(viewDir(root, viewId), `host-config.${instanceId}.json`);
53
+ /** @param {string} root @param {string} viewId */
54
+ export const titleConfigPath = (root, viewId) => path.join(viewDir(root, viewId), "title-config.json");
55
+ /** @param {string} root @param {string} viewId */
56
+ export const autoStateConfigPath = (root, viewId) => path.join(viewDir(root, viewId), "auto-state-config.json");
57
+ /**
58
+ * Named pipe used for the host control socket on Windows. Named pipes are kernel
59
+ * objects, not filesystem entries, so existsSync/unlink never apply to them; they
60
+ * vanish automatically when the owning process exits. Names must be ≤ 256 chars.
61
+ * @param {string} viewId
62
+ */
63
+ export const controlPipeName = (viewId) => `\\\\.\\pipe\\pi-agent-board-${viewId}`;
64
+ /**
65
+ * Control socket address for a view, platform-aware. On Windows plain filesystem
66
+ * paths cannot be bound by node's net module (EACCES) — only `\\.\pipe\`-style
67
+ * names work (see libuv#458, nodejs/node#55979).
68
+ * @param {"win32"|"linux"|"darwin"} platform
69
+ * @param {string} root
70
+ * @param {string} viewId
71
+ */
72
+ export function controlSocketPathFor(platform, root, viewId) {
73
+ if (platform === "win32") return controlPipeName(viewId);
74
+ return path.join(viewDir(root, viewId), "control.sock");
75
+ }
76
+ /** @param {string} root @param {string} viewId */
77
+ export const controlSocketPath = (root, viewId) => controlSocketPathFor(process.platform, root, viewId);
78
+ /**
79
+ * Endpoint a hosted child's editor-state reporter should connect to.
80
+ *
81
+ * The runner knows which endpoint it actually bound and injects it into the
82
+ * child env as AGENT_BOARD_CONTROL_SOCKET (issue #103): since #70 the owned
83
+ * runner binds a per-instance address, while the reporter kept dialing the
84
+ * stable per-view one, so it never connected and the attach gate lost its
85
+ * authoritative editor state. The injected value wins; the stable address
86
+ * remains the fallback for legacy hosts and older runners that predate the key.
87
+ * @param {{ envSocketPath?: string | null, platform: "win32"|"linux"|"darwin", root: string, viewId: string }} args
88
+ */
89
+ export function resolveControlEndpoint({ envSocketPath, platform, root, viewId }) {
90
+ const injected = typeof envSocketPath === "string" ? envSocketPath.trim() : "";
91
+ return injected.length > 0 ? injected : controlSocketPathFor(platform, root, viewId);
92
+ }
93
+ /**
94
+ * Per-instance control endpoint. Each new host instance binds its own socket/pipe,
95
+ * so a superseded runner can never unlink the current owner's endpoint (issue #70).
96
+ * win32 pipe names embed an 8-hex hash of the instanceId to stay under the 256-char limit.
97
+ * @param {"win32"|"linux"|"darwin"} platform
98
+ * @param {string} root
99
+ * @param {string} viewId
100
+ * @param {string} instanceId
101
+ */
102
+ export function hostEndpointPathFor(platform, root, viewId, instanceId) {
103
+ if (platform === "win32") {
104
+ const hash = createHash("sha256").update(String(instanceId)).digest("hex").slice(0, 8);
105
+ return `\\\\.\\pipe\\pi-agent-board-${viewId}-${hash}`;
106
+ }
107
+ return path.join(viewDir(root, viewId), `control.${instanceId}.sock`);
108
+ }
109
+ /** @param {string} root @param {string} viewId */
110
+ export const screenLogPath = (root, viewId) => path.join(viewDir(root, viewId), "screen.log");
111
+ /**
112
+ * Well-known endpoint for the board-root View State Coordinator (issue #91, spec D3).
113
+ * Exactly one coordinator may own a root (token-fenced lease), so one stable path
114
+ * suffices; a stale POSIX socket left by a crashed coordinator is unlinked by the
115
+ * new lease owner before bind. win32 pipe names embed a 16-hex hash of the
116
+ * *normalized* root to stay under the 256-char limit and keep per-root isolation.
117
+ * @param {"win32"|"linux"|"darwin"} platform
118
+ * @param {string} root
119
+ */
120
+ export function coordinatorEndpointPathFor(platform, root) {
121
+ if (platform === "win32") {
122
+ // Normalize before hashing: the pipe name must be invariant to the root's
123
+ // spelling (C:/x vs C:\x, trailing separators, dot segments). The lock path
124
+ // derived from the same root already is (via path.join); a mismatch yields
125
+ // "same lock, two pipes" — the panel probes a pipe nobody bound, spawns
126
+ // replacements that cannot take the held lease, and locks itself out
127
+ // (issue #124). resolve() is idempotent on canonical roots, so coordinators
128
+ // already deployed keep their pipe name and need no restart.
129
+ // win32 semantics are named explicitly rather than using the ambient
130
+ // path.resolve: this branch must emit the same pipe name on every host OS
131
+ // (on POSIX, ambient resolve treats `C:\x` as a relative path: no
132
+ // drive-letter or backslash-separator semantics), so platform-injected
133
+ // tests and CI behave identically everywhere.
134
+ const normalized = path.win32.resolve(root);
135
+ const hash = createHash("sha256").update(normalized).digest("hex").slice(0, 16);
136
+ return `\\\\.\\pipe\\agent-board-coordinator-${hash}`;
137
+ }
138
+ return path.join(root, "coordinator.sock");
139
+ }
140
+ /** @param {string} root @param {string} viewId */
141
+ export const hostPidPath = (root, viewId) => path.join(viewDir(root, viewId), "host-pid.json");
142
+ /** @param {string} root @param {string} viewId */
143
+ export const runsDir = (root, viewId) => path.join(viewDir(root, viewId), "runs");
144
+ /** @param {string} root @param {string} viewId @param {string} runId */
145
+ export const runDir = (root, viewId, runId) => path.join(runsDir(root, viewId), runId);
146
+ /** @param {string} root @param {string} viewId @param {string} runId */
147
+ export const statusPath = (root, viewId, runId) => path.join(runDir(root, viewId, runId), "status.json");
148
+ /** @param {string} root @param {string} viewId @param {string} runId */
149
+ export const eventsPath = (root, viewId, runId) => path.join(runDir(root, viewId, runId), "events.jsonl");
150
+ /** @param {string} root @param {string} viewId @param {string} runId */
151
+ export const stdoutPath = (root, viewId, runId) => path.join(runDir(root, viewId, runId), "stdout.log");
152
+ /** @param {string} root @param {string} viewId @param {string} runId */
153
+ export const stderrPath = (root, viewId, runId) => path.join(runDir(root, viewId, runId), "stderr.log");
154
+ /** @param {string} root @param {string} viewId @param {string} runId */
155
+ export const pidPath = (root, viewId, runId) => path.join(runDir(root, viewId, runId), "pid.json");
156
+ /** @param {string} root @param {string} viewId */
157
+ export const diagnosticsPath = (root, viewId) => path.join(viewDir(root, viewId), "diagnostics.jsonl");
158
+ /** @param {string} root @param {string} viewId */
159
+ export const evidencePath = (root, viewId) => path.join(viewDir(root, viewId), "evidence.json");
160
+ /** @param {string} root @param {string} viewId */
161
+ export const codeRefsPath = (root, viewId) => path.join(viewDir(root, viewId), "github.json");
162
+ /** @param {string} root @param {string} viewId */
163
+ export const viewEvidencePath = evidencePath;
164
+ /** @param {string} root @param {string} viewId */
165
+ export const followUpQueuePath = (root, viewId) => path.join(viewDir(root, viewId), "queue.json");
166
+ /** @param {string} root @param {string} viewId */
167
+ export const queuePath = followUpQueuePath;
168
+ /** @param {string} root @param {string} viewId */
169
+ export const steeringPath = (root, viewId) => path.join(viewDir(root, viewId), "steering.json");
170
+ /** @param {string} root @param {string} viewId @param {string} name */
171
+ export const viewLockPath = (root, viewId, name) => path.join(viewDir(root, viewId), `${name}.lock`);
172
+ /** @param {string} root @param {string} viewId @param {string} runId */
173
+ export const runEvidencePath = (root, viewId, runId) => path.join(runDir(root, viewId, runId), "evidence.json");
174
+
175
+ /** @param {string} root */
176
+ export const sessionsDir = (root) => path.join(root, "sessions");
177
+ /** @param {string} root @param {string} viewId */
178
+ export const sessionFilePath = (root, viewId) => path.join(sessionsDir(root), `${viewId}.jsonl`);
179
+
180
+ /** @param {string} root */
181
+ export const worktreesDir = (root) => path.join(root, "worktrees");
182
+ /** @param {string} root @param {string} viewId */
183
+ export const worktreePath = (root, viewId) => path.join(worktreesDir(root), viewId);
@@ -0,0 +1,75 @@
1
+ /** Process liveness checks and process identity capture. */
2
+ import { readFileSync } from "node:fs";
3
+
4
+ /**
5
+ * Whether `pid` refers to a live process.
6
+ * `process.kill(pid, 0)` throws ESRCH when the process is gone, and EPERM when it
7
+ * exists but we lack permission to signal it — EPERM still means "alive".
8
+ * @param {number|null|undefined} pid
9
+ * @returns {boolean}
10
+ */
11
+ export function isAlive(pid) {
12
+ if (!pid || pid <= 0) return false;
13
+ try {
14
+ process.kill(pid, 0);
15
+ return true;
16
+ } catch (err) {
17
+ return /** @type {NodeJS.ErrnoException} */ (err).code === "EPERM";
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Try to terminate a process tree gently, then force after `graceMs`.
23
+ * Safe no-op if already dead.
24
+ * @param {number|null|undefined} pid
25
+ * @param {number} [graceMs]
26
+ */
27
+ export function killProcess(pid, graceMs = 4000) {
28
+ if (!isAlive(pid)) return;
29
+ try {
30
+ process.kill(/** @type {number} */ (pid), "SIGTERM");
31
+ } catch {
32
+ /* ignore */
33
+ }
34
+ setTimeout(() => {
35
+ if (isAlive(pid)) {
36
+ try {
37
+ process.kill(/** @type {number} */ (pid), "SIGKILL");
38
+ } catch {
39
+ /* ignore */
40
+ }
41
+ }
42
+ }, graceMs).unref?.();
43
+ }
44
+
45
+ /**
46
+ * POSIX process start token — /proc/<pid>/stat field 22 (starttime), stable
47
+ * across exec(2). Distinguishes an owned-live pid from a reused one; null on
48
+ * failure or non-Linux platforms. This is the single synchronous implementation
49
+ * (issue #238 removed the runner copies); the cross-platform async reader in
50
+ * process-identity-probe.mjs layers a win32 creation-time token on top, and
51
+ * host-meta lease identity uses this function directly (issue #112).
52
+ * @param {number|null|undefined} pid
53
+ * @returns {string|null}
54
+ */
55
+ export function captureStartToken(pid) {
56
+ if (process.platform !== "linux" || !pid) return null;
57
+ try {
58
+ const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
59
+ // comm (field 2) may contain spaces and parens — fields resume AFTER the
60
+ // last ')'. fields[0] is state (field 3) → starttime (field 22) is [19].
61
+ const afterComm = stat.slice(stat.lastIndexOf(")") + 1).trimStart();
62
+ return afterComm.split(/\s+/)[19] ?? null;
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Launch-time identity for the current process, as stamped on host-meta
70
+ * acquisitions (issue #112).
71
+ * @returns {{pid: number, startToken: string|null}}
72
+ */
73
+ export function currentProcessIdentity() {
74
+ return { pid: process.pid, startToken: captureStartToken(process.pid) };
75
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Single-flight debounce for dashboard prewarm.
3
+ *
4
+ * Arrow-key navigation must move the selection and repaint immediately; host
5
+ * prewarm (which may spawn a PTY host and re-scan rows) is deferred so bursts
6
+ * of keypresses trigger exactly one prewarm for the final resting selection.
7
+ */
8
+
9
+ /**
10
+ * @param {() => void} prewarm Invoked (with errors swallowed) once scheduling
11
+ * goes quiet for `delayMs`. Re-reads current state at fire time.
12
+ * @param {number} [delayMs=200]
13
+ * @returns {{ schedule: () => void, cancel: () => void }}
14
+ */
15
+ export function createPrewarmScheduler(prewarm, delayMs = 200) {
16
+ /** @type {ReturnType<typeof setTimeout> | null} */
17
+ let timer = null;
18
+ const fire = () => {
19
+ timer = null;
20
+ try {
21
+ prewarm();
22
+ } catch {
23
+ /* prewarm is best-effort; never break navigation */
24
+ }
25
+ };
26
+ return {
27
+ schedule() {
28
+ if (timer !== null) clearTimeout(timer);
29
+ const pending = setTimeout(fire, delayMs);
30
+ // Never let a pending debounce hold the event loop open on exit.
31
+ if (typeof pending.unref === "function") pending.unref();
32
+ timer = pending;
33
+ },
34
+ cancel() {
35
+ if (timer !== null) {
36
+ clearTimeout(timer);
37
+ timer = null;
38
+ }
39
+ },
40
+ };
41
+ }
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Cross-platform process identity probe (issue #217) and start-token reader
3
+ * (issue #238).
4
+ *
5
+ * The probe produces the two corroborating signals — process creation time and
6
+ * command line — that `classifyUnverifiableProcess` judges when a recorded
7
+ * start token is missing. `readProcessStartToken` produces the comparable token
8
+ * itself: linux reads `/proc` and win32 converts the probe's creation time into
9
+ * a `ct:<ms>` token. Pure parsers are exported for exhaustive unit tests;
10
+ * `readProcessIdentityProbe` is the only I/O layer (one async `execFile` per
11
+ * pid, never throwing).
12
+ *
13
+ * `readProcessStartToken` has no darwin path on purpose: `ps` reports elapsed
14
+ * time at second resolution, so two reads of one process drift by up to ~1 s
15
+ * and a token comparison would misclassify a live process as `foreign`.
16
+ * Returning `null` keeps callers on the "unverifiable" path instead of
17
+ * guessing. The probe itself stays off linux: the `/proc` token already
18
+ * identifies processes there, so probing would only add latency.
19
+ */
20
+ import { execFile } from "node:child_process";
21
+ import { captureStartToken } from "./pid.mjs";
22
+
23
+ /** Hard cap on one probe: a hung PowerShell/ps must not stall recovery. */
24
+ export const PROBE_TIMEOUT_MS = 3_000;
25
+
26
+ /** Cap on captured stdout — a command line is small; anything larger is junk. */
27
+ const PROBE_MAX_BUFFER = 64 * 1024;
28
+
29
+ /**
30
+ * Format an absolute creation time as the comparable `ct:` identity token
31
+ * (issue #238): two reads of the same process yield the same token regardless
32
+ * of when they happen. Only positive safe integers format — anything else
33
+ * (missing creation time, a fractional or out-of-range value, a coerced
34
+ * string) would produce a token that can never match a recorded one.
35
+ * @param {number} creationTimeMs
36
+ * @returns {string|null}
37
+ */
38
+ export function creationTimeToken(creationTimeMs) {
39
+ if (!Number.isSafeInteger(creationTimeMs) || creationTimeMs <= 0) return null;
40
+ return `ct:${creationTimeMs}`;
41
+ }
42
+
43
+ /**
44
+ * Read the comparable start token for a process (issue #238): the token is
45
+ * recorded next to a lease and compared later to tell "same process" from
46
+ * "reused pid".
47
+ *
48
+ * - linux: `captureStartToken(pid)` — `/proc/<pid>/stat` starttime.
49
+ * - win32: probe → `creationTimeToken(creationTimeMs)` — CIM creation time is
50
+ * millisecond-resolution, so two reads of one process are strictly equal.
51
+ * - anything else, including darwin: `null`, and the probe is never called.
52
+ *
53
+ * A pid that is not a positive safe integer yields `null` before any I/O; a
54
+ * probe rejection or unusable result also yields `null` (unverifiable, never
55
+ * "foreign").
56
+ *
57
+ * @param {number} pid
58
+ * @param {{ platform?: string, probe?: typeof readProcessIdentityProbe }} [opts]
59
+ * @returns {Promise<string|null>}
60
+ */
61
+ export async function readProcessStartToken(pid, {
62
+ platform = process.platform,
63
+ probe = readProcessIdentityProbe,
64
+ } = {}) {
65
+ if (!Number.isSafeInteger(pid) || pid <= 0) return null;
66
+ if (platform === "linux") return captureStartToken(pid);
67
+ if (platform !== "win32") return null;
68
+ try {
69
+ const observed = await probe(pid);
70
+ return observed ? creationTimeToken(observed.creationTimeMs) : null;
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Parse an epoch-milliseconds value from CIM JSON output. Accepts decimal
78
+ * digits only: PowerShell can hand back an empty string or a negative number
79
+ * for protected processes, neither of which is a usable creation time.
80
+ * @param {string} text
81
+ * @returns {number|null}
82
+ */
83
+ export function parseUnixMillis(text) {
84
+ const trimmed = String(text ?? "").trim();
85
+ if (!/^\d+$/.test(trimmed)) return null;
86
+ const millis = Number(trimmed);
87
+ return Number.isSafeInteger(millis) && millis > 0 ? millis : null;
88
+ }
89
+
90
+ /**
91
+ * Parse the compressed CIM object emitted for a Windows process:
92
+ * `{"creation":<unix ms>|null,"cmd":"<command line>"|null}`. Malformed JSON or
93
+ * a non-object payload yields `null` (the judgment layer reads that as
94
+ * `unknown`, which is always safe).
95
+ * @param {string} text
96
+ * @returns {{creationTimeMs:number|null, commandLine:string|null}|null}
97
+ */
98
+ export function parseProbeJson(text) {
99
+ if (typeof text !== "string") return null;
100
+ const trimmed = text.trim();
101
+ if (!trimmed) return null;
102
+ /** @type {any} */ let parsed;
103
+ try {
104
+ parsed = JSON.parse(trimmed);
105
+ } catch {
106
+ return null;
107
+ }
108
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
109
+ return {
110
+ creationTimeMs: parseUnixMillis(parsed.creation),
111
+ commandLine: typeof parsed.cmd === "string" && parsed.cmd.trim() ? parsed.cmd : null,
112
+ };
113
+ }
114
+
115
+ /** `[DD-]HH:MM:SS` (ps drops leading units it does not need). */
116
+ const PS_ELAPSED_PATTERN = /^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/;
117
+
118
+ /**
119
+ * Parse a POSIX `ps` elapsed-time field into milliseconds. Handles the three
120
+ * shapes `ps -o etime=` emits: `MM:SS`, `HH:MM:SS`, and `D-HH:MM:SS`.
121
+ * @param {string} text
122
+ * @returns {number|null}
123
+ */
124
+ export function parsePsElapsedMs(text) {
125
+ const match = PS_ELAPSED_PATTERN.exec(String(text ?? "").trim());
126
+ if (!match) return null;
127
+ const days = match[1] == null ? 0 : Number(match[1]);
128
+ const hours = match[2] == null ? 0 : Number(match[2]);
129
+ const minutes = Number(match[3]);
130
+ const seconds = Number(match[4]);
131
+ if (minutes > 59 || seconds > 59) return null;
132
+ return ((days * 24 + hours) * 3600 + minutes * 60 + seconds) * 1000;
133
+ }
134
+
135
+ /**
136
+ * Parse one `ps -p <pid> -o etime=,command=` line: the first whitespace-
137
+ * separated token is the elapsed time, everything after it is the command.
138
+ * `ps` reports age, not creation time, so callers pass the clock reading.
139
+ * @param {string} text
140
+ * @param {number} nowMs Epoch ms at which the probe output was read.
141
+ * @returns {{creationTimeMs:number|null, commandLine:string|null}|null}
142
+ */
143
+ export function parsePsProbeLine(text, nowMs) {
144
+ if (typeof text !== "string") return null;
145
+ const trimmed = text.trim();
146
+ if (!trimmed) return null;
147
+ const split = trimmed.search(/\s/);
148
+ const elapsedText = split === -1 ? trimmed : trimmed.slice(0, split);
149
+ const commandLine = split === -1 ? "" : trimmed.slice(split).trim();
150
+ const elapsedMs = parsePsElapsedMs(elapsedText);
151
+ if (elapsedMs == null) return null;
152
+ return {
153
+ creationTimeMs: nowMs - elapsedMs,
154
+ commandLine: commandLine || null,
155
+ };
156
+ }
157
+
158
+ /**
159
+ * Windows CIM query: one round trip returns creation time (converted to Unix
160
+ * milliseconds in PowerShell so the parent needs no date parsing) and the
161
+ * command line. `-ErrorAction SilentlyContinue` keeps a vanished pid from
162
+ * turning into a non-zero exit; the empty result is handled as "unavailable".
163
+ * @param {number} pid
164
+ * @returns {string}
165
+ */
166
+ function windowsProbeScript(pid) {
167
+ return "$p = Get-CimInstance Win32_Process -Filter \"ProcessId=" + pid + "\" -ErrorAction SilentlyContinue; "
168
+ + "if ($p) { [pscustomobject]@{creation=[long]([datetimeoffset]$p.CreationDate).ToUnixTimeMilliseconds(); cmd=$p.CommandLine} | ConvertTo-Json -Compress }";
169
+ }
170
+
171
+ /**
172
+ * Resolve the platform-specific probe command. Platforms without a probe
173
+ * (`linux` and everything else) and invalid pids return `null`, so callers
174
+ * never spawn anything for them.
175
+ * @param {string} platform
176
+ * @param {number} pid
177
+ * @returns {{command:string, args:string[]}|null}
178
+ */
179
+ export function resolveProbeCommand(platform, pid) {
180
+ if (!Number.isSafeInteger(pid) || pid <= 0) return null;
181
+ if (platform === "win32") {
182
+ return {
183
+ command: "powershell.exe",
184
+ args: ["-NoProfile", "-NonInteractive", "-Command", windowsProbeScript(pid)],
185
+ };
186
+ }
187
+ if (platform === "darwin") {
188
+ return { command: "ps", args: ["-p", String(pid), "-o", "etime=,command="] };
189
+ }
190
+ return null;
191
+ }
192
+
193
+ /**
194
+ * Read `{ creationTimeMs, commandLine }` for `pid`, or `null` when the process
195
+ * cannot be observed (unsupported platform, invalid pid, non-zero exit,
196
+ * timeout, or empty output). `null` means "unverifiable", never "foreign".
197
+ *
198
+ * `execFileFn` is injectable for tests; the default is Node's async
199
+ * `execFile`. `windowsHide` mirrors `launchRun`'s detached spawn (issue #49):
200
+ * without it a console window flashes on Windows 11 25H2.
201
+ *
202
+ * @param {number} pid
203
+ * @param {{ platform?: string, execFileFn?: typeof execFile, timeoutMs?: number, nowMs?: () => number }} [opts]
204
+ * @returns {Promise<{creationTimeMs:number|null, commandLine:string|null}|null>}
205
+ */
206
+ export async function readProcessIdentityProbe(pid, {
207
+ platform = process.platform,
208
+ execFileFn = execFile,
209
+ timeoutMs = PROBE_TIMEOUT_MS,
210
+ nowMs = Date.now,
211
+ } = {}) {
212
+ const resolved = resolveProbeCommand(platform, pid);
213
+ if (!resolved) return null;
214
+ return new Promise((resolve) => {
215
+ execFileFn(resolved.command, resolved.args, { timeout: timeoutMs, windowsHide: true, maxBuffer: PROBE_MAX_BUFFER }, (error, stdout) => {
216
+ if (error) {
217
+ resolve(null);
218
+ return;
219
+ }
220
+ const text = String(stdout ?? "").trim();
221
+ if (!text) {
222
+ resolve(null);
223
+ return;
224
+ }
225
+ resolve(platform === "win32" ? parseProbeJson(text) : parsePsProbeLine(text, nowMs()));
226
+ });
227
+ });
228
+ }
@@ -0,0 +1,13 @@
1
+ /** Helpers for safely delivering prompts to Pi child processes. */
2
+
3
+ /**
4
+ * Protect a prompt that is delivered as a positional CLI argument.
5
+ * Pi parses argv before it knows which tokens are prompt text, so a leading `-`
6
+ * would be treated as another flag and abort the run.
7
+ * @param {string} text
8
+ * @returns {string}
9
+ */
10
+ export function encodePromptForCliArg(text) {
11
+ const raw = String(text ?? "");
12
+ return /^\s*-/.test(raw) ? ` ${raw}` : raw;
13
+ }