@fitchmultz/pi-subagents 0.0.0-stage → 0.44.5

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 (347) hide show
  1. package/CHANGELOG.md +1654 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1533 -2
  4. package/agents/context-builder.md +97 -0
  5. package/agents/debugger.md +51 -0
  6. package/agents/delegate.md +13 -0
  7. package/agents/fixer.md +37 -0
  8. package/agents/oracle.md +71 -0
  9. package/agents/planner.md +72 -0
  10. package/agents/researcher.md +51 -0
  11. package/agents/reviewer-claude.md +69 -0
  12. package/agents/reviewer-gpt.md +68 -0
  13. package/agents/reviewer-ponytail.md +82 -0
  14. package/agents/reviewer-security.md +81 -0
  15. package/agents/reviewer.md +69 -0
  16. package/agents/scout.md +66 -0
  17. package/agents/ui-designer.md +51 -0
  18. package/agents/watcher.md +26 -0
  19. package/agents/worker.md +46 -0
  20. package/agents/writer.md +36 -0
  21. package/dist/agents/agent-management.js +923 -0
  22. package/dist/agents/agent-scope.js +7 -0
  23. package/dist/agents/agent-selection.js +20 -0
  24. package/dist/agents/agent-serializer.js +100 -0
  25. package/dist/agents/agents.js +752 -0
  26. package/dist/agents/chain-serializer.js +367 -0
  27. package/dist/agents/frontmatter.js +21 -0
  28. package/dist/agents/identity.js +29 -0
  29. package/dist/agents/skills.js +570 -0
  30. package/dist/extension/build-info.js +1 -0
  31. package/dist/extension/compact-tools.js +112 -0
  32. package/dist/extension/config.js +22 -0
  33. package/dist/extension/control-notices.js +29 -0
  34. package/dist/extension/doctor.js +143 -0
  35. package/dist/extension/fanout-child.js +326 -0
  36. package/dist/extension/index.js +577 -0
  37. package/dist/extension/schemas.js +416 -0
  38. package/dist/extension/tool-input.js +20 -0
  39. package/dist/extension/tool-result.js +14 -0
  40. package/dist/history/canonical-result.js +81 -0
  41. package/dist/history/database.js +72 -0
  42. package/dist/history/index.js +199 -0
  43. package/dist/history/ingest.js +468 -0
  44. package/dist/history/queries.js +350 -0
  45. package/dist/history/store.js +216 -0
  46. package/dist/history/types.js +4 -0
  47. package/dist/history/worker.js +466 -0
  48. package/dist/intercom/intercom-bridge.js +112 -0
  49. package/dist/intercom/live-intercom.js +94 -0
  50. package/dist/intercom/result-intercom.js +373 -0
  51. package/dist/pi-intercom/agent-dir.js +10 -0
  52. package/dist/pi-intercom/bridge.js +750 -0
  53. package/dist/pi-intercom/broker/broker.js +426 -0
  54. package/dist/pi-intercom/broker/client.js +492 -0
  55. package/dist/pi-intercom/broker/framing.js +65 -0
  56. package/dist/pi-intercom/broker/paths.js +42 -0
  57. package/dist/pi-intercom/broker/pid.js +66 -0
  58. package/dist/pi-intercom/broker/spawn.js +221 -0
  59. package/dist/pi-intercom/config.js +102 -0
  60. package/dist/pi-intercom/index.js +2942 -0
  61. package/dist/pi-intercom/reply-tracker.js +182 -0
  62. package/dist/pi-intercom/session-targets.js +128 -0
  63. package/dist/pi-intercom/subagent-live-events.js +132 -0
  64. package/dist/pi-intercom/topics.js +157 -0
  65. package/dist/pi-intercom/types.js +163 -0
  66. package/dist/pi-intercom/ui/compose.js +269 -0
  67. package/dist/pi-intercom/ui/inline-message.js +80 -0
  68. package/dist/pi-intercom/ui/session-list.js +124 -0
  69. package/dist/runs/background/async-control.js +95 -0
  70. package/dist/runs/background/async-execution.js +676 -0
  71. package/dist/runs/background/async-job-tracker.js +383 -0
  72. package/dist/runs/background/async-result-file.js +58 -0
  73. package/dist/runs/background/async-resume.js +305 -0
  74. package/dist/runs/background/async-status.js +442 -0
  75. package/dist/runs/background/completion-dedupe.js +69 -0
  76. package/dist/runs/background/completion-delivery.js +245 -0
  77. package/dist/runs/background/notify.js +71 -0
  78. package/dist/runs/background/parallel-groups.js +55 -0
  79. package/dist/runs/background/result-watcher.js +426 -0
  80. package/dist/runs/background/run-id-resolver.js +64 -0
  81. package/dist/runs/background/run-status.js +382 -0
  82. package/dist/runs/background/stale-run-reconciler.js +335 -0
  83. package/dist/runs/background/subagent-runner-launcher.js +55 -0
  84. package/dist/runs/background/subagent-runner.js +1872 -0
  85. package/dist/runs/background/top-level-async.js +5 -0
  86. package/dist/runs/foreground/chain-clarify.js +1152 -0
  87. package/dist/runs/foreground/clarify-invocation.js +72 -0
  88. package/dist/runs/foreground/execution-input.js +423 -0
  89. package/dist/runs/foreground/foreground-control.js +817 -0
  90. package/dist/runs/foreground/question-control.js +88 -0
  91. package/dist/runs/foreground/run-async-path.js +205 -0
  92. package/dist/runs/foreground/subagent-executor.js +442 -0
  93. package/dist/runs/foreground/subagent-params.js +165 -0
  94. package/dist/runs/foreground/wait-run.js +141 -0
  95. package/dist/runs/shared/acceptance-contract.js +253 -0
  96. package/dist/runs/shared/acceptance-evaluation.js +256 -0
  97. package/dist/runs/shared/acceptance-finalization.js +280 -0
  98. package/dist/runs/shared/acceptance-reports.js +147 -0
  99. package/dist/runs/shared/acceptance.js +4 -0
  100. package/dist/runs/shared/chain-outputs.js +110 -0
  101. package/dist/runs/shared/child-attempt.js +693 -0
  102. package/dist/runs/shared/child-execution-cwd.js +96 -0
  103. package/dist/runs/shared/child-observations.js +114 -0
  104. package/dist/runs/shared/claude-code.js +199 -0
  105. package/dist/runs/shared/completion-guard.js +31 -0
  106. package/dist/runs/shared/dynamic-fanout.js +274 -0
  107. package/dist/runs/shared/history-index.js +72 -0
  108. package/dist/runs/shared/mcp-direct-tool-allowlist.js +334 -0
  109. package/dist/runs/shared/model-fallback.js +185 -0
  110. package/dist/runs/shared/mutating-tool-guard.js +170 -0
  111. package/dist/runs/shared/native-finalization.js +101 -0
  112. package/dist/runs/shared/native-usage.js +153 -0
  113. package/dist/runs/shared/nested-events.js +700 -0
  114. package/dist/runs/shared/nested-path.js +48 -0
  115. package/dist/runs/shared/nested-render.js +108 -0
  116. package/dist/runs/shared/parallel-utils.js +61 -0
  117. package/dist/runs/shared/parent-receipts.js +68 -0
  118. package/dist/runs/shared/parent-usage.js +121 -0
  119. package/dist/runs/shared/pi-args.js +310 -0
  120. package/dist/runs/shared/pi-spawn.js +52 -0
  121. package/dist/runs/shared/run-history-queries.js +45 -0
  122. package/dist/runs/shared/run-history.js +110 -0
  123. package/dist/runs/shared/run-id-ambiguity.js +4 -0
  124. package/dist/runs/shared/run-records.js +679 -0
  125. package/dist/runs/shared/session-cwd-preload.js +33 -0
  126. package/dist/runs/shared/single-output.js +222 -0
  127. package/dist/runs/shared/streaming-text.js +15 -0
  128. package/dist/runs/shared/structured-output.js +55 -0
  129. package/dist/runs/shared/subagent-control.js +179 -0
  130. package/dist/runs/shared/subagent-prompt-runtime.js +197 -0
  131. package/dist/runs/shared/subagent-tool-loop-guard.js +66 -0
  132. package/dist/runs/shared/supervisor-questions.js +350 -0
  133. package/dist/runs/shared/workflow-graph.js +214 -0
  134. package/dist/runs/shared/workflow-policy.js +84 -0
  135. package/dist/runs/shared/worktree.js +525 -0
  136. package/dist/shared/agent-context-policy.js +126 -0
  137. package/dist/shared/artifacts.js +96 -0
  138. package/dist/shared/atomic-json.js +13 -0
  139. package/dist/shared/cooperative.js +20 -0
  140. package/dist/shared/execution-cwd.js +14 -0
  141. package/dist/shared/file-coalescer.js +21 -0
  142. package/dist/shared/fork-context.js +49 -0
  143. package/dist/shared/formatters.js +85 -0
  144. package/dist/shared/journal-reader.js +407 -0
  145. package/dist/shared/lazy-tools.js +32 -0
  146. package/dist/shared/model-info.js +68 -0
  147. package/dist/shared/native-session.js +15 -0
  148. package/dist/shared/native-tui.js +18 -0
  149. package/dist/shared/native-typebox.js +20 -0
  150. package/dist/shared/post-exit-stdio-guard.js +197 -0
  151. package/dist/shared/prompt-sections.js +8 -0
  152. package/dist/shared/session-entries.js +52 -0
  153. package/dist/shared/session-identity.js +14 -0
  154. package/dist/shared/settings.js +254 -0
  155. package/dist/shared/status-format.js +111 -0
  156. package/dist/shared/temp-root.js +114 -0
  157. package/dist/shared/types.js +207 -0
  158. package/dist/shared/ui-mode.js +3 -0
  159. package/dist/shared/utils.js +334 -0
  160. package/dist/slash/prompt-template-bridge.js +298 -0
  161. package/dist/slash/slash-bridge.js +116 -0
  162. package/dist/slash/slash-commands.js +607 -0
  163. package/dist/slash/slash-live-state.js +246 -0
  164. package/dist/tui/action-hints.js +75 -0
  165. package/dist/tui/agent-history.js +197 -0
  166. package/dist/tui/agent-view.js +1656 -0
  167. package/dist/tui/render.js +1515 -0
  168. package/docs/intercom-bridge.md +212 -0
  169. package/docs/intercom.md +581 -0
  170. package/docs/performance.md +121 -0
  171. package/package.json +102 -4
  172. package/prompts/gather-context-and-clarify.md +14 -0
  173. package/prompts/parallel-cleanup.md +62 -0
  174. package/prompts/parallel-context-build.md +56 -0
  175. package/prompts/parallel-handoff-plan.md +62 -0
  176. package/prompts/parallel-research.md +51 -0
  177. package/prompts/parallel-review.md +59 -0
  178. package/prompts/review-loop.md +42 -0
  179. package/scripts/build.mjs +134 -0
  180. package/scripts/check-compat.mjs +48 -0
  181. package/scripts/compat-host.mjs +19 -0
  182. package/scripts/intercom-bridge-credentials.mjs +143 -0
  183. package/scripts/intercom-remote.mjs +103 -0
  184. package/scripts/linux-smoke.sh +51 -0
  185. package/scripts/local-install-smoke.mjs +66 -0
  186. package/scripts/native-package-smoke.mjs +82 -0
  187. package/scripts/package-smoke.mjs +209 -0
  188. package/scripts/prepare.mjs +67 -0
  189. package/scripts/real-pi-smoke.mjs +415 -0
  190. package/scripts/run-tests.mjs +149 -0
  191. package/skills/pi-intercom/SKILL.md +113 -0
  192. package/skills/pi-intercom/evals/evals.json +29 -0
  193. package/skills/pi-intercom/evals/trigger-evals.json +47 -0
  194. package/skills/pi-intercom/references/peer-sessions.md +69 -0
  195. package/skills/pi-subagents/SKILL.md +76 -0
  196. package/skills/pi-subagents/evals/evals.json +29 -0
  197. package/skills/pi-subagents/evals/trigger-evals.json +42 -0
  198. package/skills/pi-subagents/references/full-orchestration-guide.md +863 -0
  199. package/src/agents/agent-management.ts +813 -0
  200. package/src/agents/agent-scope.ts +7 -0
  201. package/src/agents/agent-selection.ts +23 -0
  202. package/src/agents/agent-serializer.ts +99 -0
  203. package/src/agents/agents.ts +916 -0
  204. package/src/agents/chain-serializer.ts +329 -0
  205. package/src/agents/frontmatter.ts +21 -0
  206. package/src/agents/identity.ts +30 -0
  207. package/src/agents/skills.ts +637 -0
  208. package/src/extension/build-info.ts +2 -0
  209. package/src/extension/compact-tools.ts +114 -0
  210. package/src/extension/config.ts +23 -0
  211. package/src/extension/control-notices.ts +47 -0
  212. package/src/extension/doctor.ts +171 -0
  213. package/src/extension/fanout-child.ts +312 -0
  214. package/src/extension/index.ts +649 -0
  215. package/src/extension/schemas.ts +444 -0
  216. package/src/extension/tool-input.ts +19 -0
  217. package/src/extension/tool-result.ts +16 -0
  218. package/src/history/canonical-result.ts +62 -0
  219. package/src/history/database.ts +59 -0
  220. package/src/history/index.ts +156 -0
  221. package/src/history/ingest.ts +349 -0
  222. package/src/history/queries.ts +241 -0
  223. package/src/history/store.ts +203 -0
  224. package/src/history/types.ts +166 -0
  225. package/src/history/worker.ts +321 -0
  226. package/src/intercom/intercom-bridge.ts +123 -0
  227. package/src/intercom/live-intercom.ts +104 -0
  228. package/src/intercom/result-intercom.ts +429 -0
  229. package/src/pi-intercom/agent-dir.ts +9 -0
  230. package/src/pi-intercom/bridge.ts +668 -0
  231. package/src/pi-intercom/broker/broker.ts +463 -0
  232. package/src/pi-intercom/broker/client.ts +558 -0
  233. package/src/pi-intercom/broker/framing.ts +77 -0
  234. package/src/pi-intercom/broker/paths.ts +47 -0
  235. package/src/pi-intercom/broker/pid.ts +59 -0
  236. package/src/pi-intercom/broker/spawn.ts +248 -0
  237. package/src/pi-intercom/config.ts +142 -0
  238. package/src/pi-intercom/index.ts +2929 -0
  239. package/src/pi-intercom/reply-tracker.ts +211 -0
  240. package/src/pi-intercom/session-targets.ts +172 -0
  241. package/src/pi-intercom/subagent-live-events.ts +133 -0
  242. package/src/pi-intercom/topics.ts +125 -0
  243. package/src/pi-intercom/types.ts +271 -0
  244. package/src/pi-intercom/ui/compose.ts +289 -0
  245. package/src/pi-intercom/ui/inline-message.ts +92 -0
  246. package/src/pi-intercom/ui/session-list.ts +139 -0
  247. package/src/runs/background/async-control.ts +89 -0
  248. package/src/runs/background/async-execution.ts +850 -0
  249. package/src/runs/background/async-job-tracker.ts +385 -0
  250. package/src/runs/background/async-result-file.ts +62 -0
  251. package/src/runs/background/async-resume.ts +333 -0
  252. package/src/runs/background/async-status.ts +481 -0
  253. package/src/runs/background/completion-dedupe.ts +79 -0
  254. package/src/runs/background/completion-delivery.ts +195 -0
  255. package/src/runs/background/notify.ts +123 -0
  256. package/src/runs/background/parallel-groups.ts +53 -0
  257. package/src/runs/background/result-watcher.ts +423 -0
  258. package/src/runs/background/run-id-resolver.ts +69 -0
  259. package/src/runs/background/run-status.ts +384 -0
  260. package/src/runs/background/stale-run-reconciler.ts +369 -0
  261. package/src/runs/background/subagent-runner-launcher.ts +56 -0
  262. package/src/runs/background/subagent-runner.ts +2053 -0
  263. package/src/runs/background/top-level-async.ts +13 -0
  264. package/src/runs/foreground/chain-clarify.ts +1340 -0
  265. package/src/runs/foreground/clarify-invocation.ts +77 -0
  266. package/src/runs/foreground/execution-input.ts +467 -0
  267. package/src/runs/foreground/foreground-control.ts +821 -0
  268. package/src/runs/foreground/question-control.ts +82 -0
  269. package/src/runs/foreground/run-async-path.ts +243 -0
  270. package/src/runs/foreground/subagent-executor.ts +501 -0
  271. package/src/runs/foreground/subagent-params.ts +320 -0
  272. package/src/runs/foreground/wait-run.ts +124 -0
  273. package/src/runs/shared/acceptance-contract.ts +257 -0
  274. package/src/runs/shared/acceptance-evaluation.ts +275 -0
  275. package/src/runs/shared/acceptance-finalization.ts +353 -0
  276. package/src/runs/shared/acceptance-reports.ts +152 -0
  277. package/src/runs/shared/acceptance.ts +33 -0
  278. package/src/runs/shared/chain-outputs.ts +114 -0
  279. package/src/runs/shared/child-attempt.ts +623 -0
  280. package/src/runs/shared/child-execution-cwd.ts +91 -0
  281. package/src/runs/shared/child-observations.ts +99 -0
  282. package/src/runs/shared/claude-code.ts +243 -0
  283. package/src/runs/shared/completion-guard.ts +35 -0
  284. package/src/runs/shared/dynamic-fanout.ts +295 -0
  285. package/src/runs/shared/history-index.ts +69 -0
  286. package/src/runs/shared/mcp-direct-tool-allowlist.ts +376 -0
  287. package/src/runs/shared/model-fallback.ts +219 -0
  288. package/src/runs/shared/mutating-tool-guard.ts +194 -0
  289. package/src/runs/shared/native-finalization.ts +128 -0
  290. package/src/runs/shared/native-usage.ts +113 -0
  291. package/src/runs/shared/nested-events.ts +752 -0
  292. package/src/runs/shared/nested-path.ts +52 -0
  293. package/src/runs/shared/nested-render.ts +119 -0
  294. package/src/runs/shared/parallel-utils.ts +153 -0
  295. package/src/runs/shared/parent-receipts.ts +59 -0
  296. package/src/runs/shared/parent-usage.ts +121 -0
  297. package/src/runs/shared/pi-args.ts +350 -0
  298. package/src/runs/shared/pi-spawn.ts +52 -0
  299. package/src/runs/shared/run-history-queries.ts +44 -0
  300. package/src/runs/shared/run-history.ts +100 -0
  301. package/src/runs/shared/run-id-ambiguity.ts +4 -0
  302. package/src/runs/shared/run-records.ts +607 -0
  303. package/src/runs/shared/session-cwd-preload.ts +27 -0
  304. package/src/runs/shared/single-output.ts +267 -0
  305. package/src/runs/shared/streaming-text.ts +18 -0
  306. package/src/runs/shared/structured-output.ts +68 -0
  307. package/src/runs/shared/subagent-control.ts +219 -0
  308. package/src/runs/shared/subagent-prompt-runtime.ts +201 -0
  309. package/src/runs/shared/subagent-tool-loop-guard.ts +89 -0
  310. package/src/runs/shared/supervisor-questions.ts +415 -0
  311. package/src/runs/shared/workflow-graph.ts +220 -0
  312. package/src/runs/shared/workflow-policy.ts +125 -0
  313. package/src/runs/shared/worktree.ts +635 -0
  314. package/src/shared/agent-context-policy.ts +184 -0
  315. package/src/shared/artifacts.ts +95 -0
  316. package/src/shared/atomic-json.ts +16 -0
  317. package/src/shared/cooperative.ts +21 -0
  318. package/src/shared/execution-cwd.ts +14 -0
  319. package/src/shared/file-coalescer.ts +29 -0
  320. package/src/shared/fork-context.ts +68 -0
  321. package/src/shared/formatters.ts +84 -0
  322. package/src/shared/journal-reader.ts +333 -0
  323. package/src/shared/lazy-tools.ts +36 -0
  324. package/src/shared/model-info.ts +81 -0
  325. package/src/shared/native-session.ts +8 -0
  326. package/src/shared/native-tui.ts +10 -0
  327. package/src/shared/native-typebox.ts +14 -0
  328. package/src/shared/post-exit-stdio-guard.ts +204 -0
  329. package/src/shared/prompt-sections.ts +10 -0
  330. package/src/shared/session-entries.ts +49 -0
  331. package/src/shared/session-identity.ts +18 -0
  332. package/src/shared/settings.ts +384 -0
  333. package/src/shared/status-format.ts +123 -0
  334. package/src/shared/temp-root.ts +107 -0
  335. package/src/shared/types.ts +1260 -0
  336. package/src/shared/ui-mode.ts +5 -0
  337. package/src/shared/utils.ts +342 -0
  338. package/src/slash/prompt-template-bridge.ts +408 -0
  339. package/src/slash/slash-bridge.ts +178 -0
  340. package/src/slash/slash-commands.ts +617 -0
  341. package/src/slash/slash-live-state.ts +292 -0
  342. package/src/tui/action-hints.ts +69 -0
  343. package/src/tui/agent-history.ts +210 -0
  344. package/src/tui/agent-view.ts +1323 -0
  345. package/src/tui/render.ts +1550 -0
  346. package/tsconfig.build.json +12 -0
  347. package/tsconfig.json +19 -0
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Purpose: Produce the compiled runtime files that the Pi extension manifest loads.
4
+ * Responsibilities: Run TypeScript emit into a staging directory, then atomically swap it
5
+ * into dist/ so a failed TypeScript emit never destroys a previously working dist.
6
+ * Usage: `npm run build`; also invoked by scripts/prepare.mjs during install lifecycles, which may pass
7
+ * the path of a throwaway compiler's `bin/tsc` as the first argument.
8
+ * Invariants/Assumptions: `node_modules` (or that argument) provides `typescript`; deleting `dist/` is safe generated output.
9
+ */
10
+
11
+ import { execFile as execFileCallback } from "node:child_process";
12
+ import { createHash } from "node:crypto";
13
+ import { existsSync } from "node:fs";
14
+ import { readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
15
+ import { join } from "node:path";
16
+ import process from "node:process";
17
+ import { setTimeout as delay } from "node:timers/promises";
18
+ import { promisify } from "node:util";
19
+
20
+ const execFile = promisify(execFileCallback);
21
+ const RM_OPTIONS = { force: true, maxRetries: 5, recursive: true, retryDelay: 100 };
22
+ const RENAME_RETRY_LIMIT = 50;
23
+ const RENAME_RETRY_MS = 50;
24
+ // Use the current Node binary without a shell, including install paths with spaces.
25
+ const tscPath = process.argv[2] ?? join(process.cwd(), "node_modules", "typescript", "bin", "tsc");
26
+
27
+ async function discardStaging(path) {
28
+ try {
29
+ await rm(path, RM_OPTIONS);
30
+ } catch (error) {
31
+ // Cleanup is always best-effort: it must not turn a successful race loss
32
+ // into failure or replace the compiler/publish error that matters.
33
+ console.warn(`could not remove staging ${path}: ${error?.message ?? error}`);
34
+ }
35
+ }
36
+
37
+ function isOwnerGone(pid) {
38
+ try {
39
+ process.kill(pid, 0);
40
+ return false;
41
+ } catch (error) {
42
+ return error?.code === "ESRCH";
43
+ }
44
+ }
45
+
46
+ async function hasPublishedDist(path) {
47
+ try {
48
+ return (await readdir(path)).length > 0;
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+
54
+ async function reapStrandedStaging(cwd) {
55
+ // Signal 0 proves only that the owner was gone at probe time. Pid reuse
56
+ // between this probe and removal is inherent to pid-based reaping.
57
+ for (const entry of await readdir(cwd, { withFileTypes: true })) {
58
+ const match = /^dist\.staging\.(\d+)$/.exec(entry.name);
59
+ if (!match || !entry.isDirectory()) continue;
60
+ const ownerPid = Number(match[1]);
61
+ if (ownerPid === process.pid || !isOwnerGone(ownerPid)) continue;
62
+ await discardStaging(join(cwd, entry.name));
63
+ }
64
+ }
65
+
66
+ async function compileToStaging(cwd, stagingDir) {
67
+ try {
68
+ const { stderr, stdout } = await execFile(
69
+ process.execPath,
70
+ [tscPath, "-p", "tsconfig.build.json", "--outDir", stagingDir],
71
+ { cwd, maxBuffer: 10 * 1024 * 1024 },
72
+ );
73
+ if (stdout) process.stdout.write(stdout);
74
+ if (stderr) process.stderr.write(stderr);
75
+ const stamp = "extension/build-info.js";
76
+ const hash = createHash("sha256");
77
+ for (const file of (await readdir(stagingDir, { recursive: true })).filter((file) => file.endsWith(".js") && file !== stamp).sort()) {
78
+ hash.update(file).update("\0").update(await readFile(join(stagingDir, file))).update("\0");
79
+ }
80
+ const { version } = JSON.parse(await readFile(join(cwd, "package.json"), "utf8"));
81
+ await writeFile(join(stagingDir, stamp), `export const EXTENSION_BUILD = Object.freeze(${JSON.stringify({ version, sha256: hash.digest("hex") })});\n`);
82
+ } catch (error) {
83
+ if (error?.stdout) process.stdout.write(error.stdout);
84
+ if (error?.stderr) process.stderr.write(error.stderr);
85
+ await discardStaging(stagingDir);
86
+ throw error;
87
+ }
88
+ }
89
+
90
+ async function publishStaging(stagingDir, distDir) {
91
+ try {
92
+ // A failed dist removal must fail loudly; keep it outside the retry loop.
93
+ // Remove dist exactly once so retries never delete a concurrent winner.
94
+ await rm(distDir, RM_OPTIONS);
95
+ for (let retry = 0; ; retry++) {
96
+ try {
97
+ await rename(stagingDir, distDir);
98
+ return;
99
+ } catch (error) {
100
+ // A completed staged build is non-empty; a bare directory is not proof
101
+ // that a concurrent publisher won the race.
102
+ if (await hasPublishedDist(distDir)) {
103
+ console.warn("dist/ was published by a concurrent build; discarding this build's staging tree.");
104
+ return;
105
+ }
106
+ // At most 50 retries (~2.5s) keep persistent filesystem errors bounded.
107
+ if (existsSync(stagingDir) && retry < RENAME_RETRY_LIMIT) {
108
+ await delay(RENAME_RETRY_MS);
109
+ continue;
110
+ }
111
+ throw error;
112
+ }
113
+ }
114
+ } finally {
115
+ // Safe after success too: rename moved stagingDir, so force makes this a no-op.
116
+ await discardStaging(stagingDir);
117
+ }
118
+ }
119
+
120
+ async function main() {
121
+ const cwd = process.cwd();
122
+ if (!existsSync(tscPath)) throw new Error(`typescript is not installed at ${tscPath}; run npm install first.`);
123
+ await reapStrandedStaging(cwd);
124
+ // Pid-scoped staging isolates concurrent emits; only a complete tree publishes.
125
+ const stagingDir = join(cwd, `dist.staging.${process.pid}`);
126
+ await rm(stagingDir, RM_OPTIONS);
127
+ await compileToStaging(cwd, stagingDir);
128
+ await publishStaging(stagingDir, join(cwd, "dist"));
129
+ }
130
+
131
+ main().catch((error) => {
132
+ console.error(error instanceof Error ? error.message : String(error));
133
+ process.exitCode = 1;
134
+ });
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+ import assert from "node:assert/strict";
3
+ import { spawnSync } from "node:child_process";
4
+ import { existsSync, mkdtempSync, rmSync } from "node:fs";
5
+ import { join, resolve } from "node:path";
6
+ import { tmpdir } from "node:os";
7
+
8
+ if (process.argv.includes("--help") || process.argv.includes("-h")) {
9
+ console.log("Usage: node scripts/check-compat.mjs\n\nRun selected-host full CI and native package checks with actual editor receipts.\nAn owned temporary editor checkout is removed after checks; supplied sources stay read-only.\nEnvironment: PI_EDITOR_RECEIPT_TEST_ROOT optionally supplies a clean frozen Git checkout.\nExamples:\n npm run check:compat\n PI_EDITOR_RECEIPT_TEST_ROOT=/path/to/pi-apply-edits npm run check:compat\nExit codes: 0 passed; 1 invalid input, unavailable source or failed checks.");
10
+ process.exit(0);
11
+ }
12
+ assert.equal(process.argv.length, 2, "Expected no arguments; use --help");
13
+ await import("./compat-host.mjs");
14
+ const run = (command, args, options = {}) => {
15
+ const result = spawnSync(command, args, { stdio: "inherit", timeout: 1_200_000, ...options });
16
+ if (result.error) throw result.error;
17
+ assert.equal(result.status, 0, `${command} ${args.join(" ")} failed (${result.status ?? result.signal})`);
18
+ return result.stdout?.trim();
19
+ };
20
+ const git = (cwd, args) => run("git", args, { cwd, stdio: ["ignore", "pipe", "inherit"], encoding: "utf8" });
21
+ const root = mkdtempSync(join(tmpdir(), "ps-editor-"));
22
+ const editor = join(root, "source");
23
+ try {
24
+ const supplied = process.env.PI_EDITOR_RECEIPT_TEST_ROOT;
25
+ let expectedRef;
26
+ if (supplied) {
27
+ const source = resolve(supplied);
28
+ expectedRef = git(source, ["rev-parse", "HEAD"]);
29
+ assert.equal(git(source, ["status", "--porcelain", "--untracked-files=all"]), "", "Supplied editor source must be clean");
30
+ run("git", ["clone", "--no-hardlinks", "--quiet", source, editor]);
31
+ git(editor, ["checkout", "--quiet", "--detach", expectedRef]);
32
+ } else {
33
+ run("git", ["clone", "--depth", "1", "--branch", "main", "--single-branch", "--quiet", "https://github.com/fitchmultz/pi-apply-edits.git", editor]);
34
+ }
35
+ const ref = git(editor, ["rev-parse", "HEAD"]);
36
+ assert.match(ref, /^[a-f0-9]{40}$/);
37
+ if (expectedRef) assert.equal(ref, expectedRef);
38
+ assert.ok(existsSync(join(editor, "extensions", "apply-edits.ts")), "Editor source lacks its native extension");
39
+ // Only the editor's real third-party runtime dependencies; Pi supplies its own peers through the loader.
40
+ run("npm", ["ci", "--omit=dev", "--ignore-scripts", "--legacy-peer-deps", "--no-audit", "--no-fund"], { cwd: editor });
41
+ git(editor, ["diff", "--exit-code"]);
42
+ console.log(JSON.stringify({ nativeEditor: { ref, source: editor, supplied: supplied ? resolve(supplied) : null } }));
43
+ const env = { ...process.env, PI_EDITOR_RECEIPT_TEST_ROOT: editor };
44
+ run("npm", ["run", "ci"], { env });
45
+ run(process.execPath, ["scripts/native-package-smoke.mjs"], { env });
46
+ } finally {
47
+ rmSync(root, { recursive: true, force: true });
48
+ }
@@ -0,0 +1,19 @@
1
+ // Validate the graph prepared by the compatibility runner; never install a host here.
2
+ import assert from "node:assert/strict";
3
+ import { createHash } from "node:crypto";
4
+ import { readFileSync, realpathSync } from "node:fs";
5
+ import { findPackageJSON } from "node:module";
6
+ import { dirname, join } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ export const hostRoot = realpathSync(dirname(findPackageJSON("@earendil-works/pi-coding-agent", import.meta.url)));
10
+ export const hostIndex = join(hostRoot, "dist/index.js");
11
+ const manifest = JSON.parse(readFileSync(join(hostRoot, "package.json"), "utf8"));
12
+ export const hostCli = realpathSync(join(hostRoot, manifest.bin.pi));
13
+ for (const [name, actual] of [["PI_COMPAT_EXPECTED_PACKAGE_DIR", hostRoot], ["PI_HOST_INDEX", hostIndex], ["PI_HOST_CLI", hostCli]]) {
14
+ if (process.env[name]) assert.equal(realpathSync(process.env[name]), realpathSync(actual), `${name} must select the installed graph`);
15
+ }
16
+ if (process.env.PI_COMPAT_EXPECTED_VERSION) assert.equal(manifest.version, process.env.PI_COMPAT_EXPECTED_VERSION);
17
+ assert.equal(realpathSync(fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"))), realpathSync(hostIndex));
18
+ const hash = path => createHash("sha256").update(readFileSync(path)).digest("hex");
19
+ console.log(JSON.stringify({ host: process.env.PI_COMPAT_HOST ?? "local", version: manifest.version, node: process.version, hostRoot, hostIndex, hostCli, indexSha256: hash(hostIndex), cliSha256: hash(hostCli) }));
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env node
2
+ import { execFileSync } from "node:child_process";
3
+ import { randomBytes, X509Certificate } from "node:crypto";
4
+ import { lstatSync, mkdirSync, readFileSync, renameSync, rmSync, rmdirSync, writeFileSync } from "node:fs";
5
+ import { resolve, join } from "node:path";
6
+ import { parseArgs } from "node:util";
7
+
8
+ function help() {
9
+ console.log(`Usage: node scripts/intercom-bridge-credentials.mjs <init|issue|revoke> DIR [NAME] [--cwd PATH]
10
+
11
+ init Create a new private CA, loopback server certificate and grok-bot credential.
12
+ issue Issue a fresh 7-day client certificate; replace that name's old authorization.
13
+ revoke Remove the named client from the allowlist. Does not delete saved credentials.
14
+
15
+ Options: --cwd PATH Local project identity for init/issue (default: current directory)
16
+ -h, --help Show help
17
+ Environment: OPENSSL OpenSSL executable (default: openssl; requires -addext support)
18
+
19
+ Examples:
20
+ node scripts/intercom-bridge-credentials.mjs init ~/.pi/intercom-bridge --cwd "$PWD"
21
+ node scripts/intercom-bridge-credentials.mjs issue ~/.pi/intercom-bridge grok-bot
22
+ node scripts/intercom-bridge-credentials.mjs revoke ~/.pi/intercom-bridge grok-bot
23
+
24
+ After issue/revoke, send SIGHUP to the helper PID to activate the allowlist.
25
+ Concurrent credential commands fail rather than overwrite each other's authorization.
26
+ After a crashed command, inspect DIR/.credentials.lock/owner and remove that lock
27
+ only after confirming its process has stopped.
28
+ Exit: 0 success, 1 filesystem/OpenSSL failure, 2 invalid arguments.
29
+ Never transfer ca.key or server.key to the remote host.`);
30
+ }
31
+
32
+ function privatePath(path, directory = false) {
33
+ const st = lstatSync(path);
34
+ if (st.isSymbolicLink() || !(directory ? st.isDirectory() : st.isFile()) ||
35
+ st.uid !== process.getuid() || (st.mode & 0o077)) {
36
+ throw new Error(`Expected an owned, private ${directory ? "directory" : "file"}: ${path}`);
37
+ }
38
+ }
39
+
40
+ function atomicConfig(path, config) {
41
+ const temp = `${path}.${randomBytes(8).toString("hex")}.tmp`;
42
+ try {
43
+ writeFileSync(temp, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600, flag: "wx" });
44
+ renameSync(temp, path);
45
+ } finally {
46
+ rmSync(temp, { force: true });
47
+ }
48
+ }
49
+
50
+ function openssl(...args) {
51
+ execFileSync(process.env.OPENSSL || "openssl", args, { stdio: ["ignore", "pipe", "pipe"], timeout: 30000 });
52
+ }
53
+
54
+ function key(path) {
55
+ openssl("ecparam", "-name", "prime256v1", "-genkey", "-noout", "-out", path);
56
+ privatePath(path);
57
+ }
58
+
59
+ function issueCertificate(dir, stem, name, server = false) {
60
+ const keyPath = join(dir, `${stem}.key`);
61
+ const certPath = join(dir, `${stem}.crt`);
62
+ const csrPath = join(dir, `${stem}.csr`);
63
+ const extPath = join(dir, `${stem}.ext`);
64
+ key(keyPath);
65
+ try {
66
+ openssl("req", "-new", "-key", keyPath, "-subj", `/CN=${name}`, "-out", csrPath);
67
+ writeFileSync(extPath, `basicConstraints=critical,CA:FALSE\nkeyUsage=critical,digitalSignature\nextendedKeyUsage=${server ? "serverAuth" : "clientAuth"}\n${server ? "subjectAltName=DNS:localhost,IP:127.0.0.1\n" : ""}`, { mode: 0o600, flag: "wx" });
68
+ openssl("x509", "-req", "-in", csrPath, "-CA", join(dir, "ca.crt"), "-CAkey", join(dir, "ca.key"),
69
+ "-set_serial", `0x${randomBytes(16).toString("hex")}`, "-days", server ? "365" : "7", "-sha256", "-extfile", extPath, "-out", certPath);
70
+ } finally {
71
+ rmSync(csrPath, { force: true });
72
+ rmSync(extPath, { force: true });
73
+ }
74
+ const cert = new X509Certificate(readFileSync(certPath));
75
+ return { cert: certPath, key: keyPath, fingerprint256: cert.fingerprint256, expires: cert.validTo };
76
+ }
77
+
78
+ let args;
79
+ try {
80
+ args = parseArgs({ allowPositionals: true, options: { help: { type: "boolean", short: "h" }, cwd: { type: "string" } } });
81
+ if (args.values.help) { help(); process.exit(0); }
82
+ const [action, dir, name] = args.positionals;
83
+ if (!["init", "issue", "revoke"].includes(action) || !dir || args.positionals.length > 3 ||
84
+ (action !== "init" && !name) || (name && !/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(name)) ||
85
+ (action === "revoke" && args.values.cwd)) throw new Error("Invalid arguments; use --help.");
86
+ } catch (error) {
87
+ console.error(error.message);
88
+ process.exit(2);
89
+ }
90
+
91
+ let lock;
92
+ try {
93
+ process.umask(0o077);
94
+ const [action, rawDir, suppliedName] = args.positionals;
95
+ const dir = resolve(rawDir);
96
+ const name = suppliedName || "grok-bot";
97
+ const configPath = join(dir, "config.json");
98
+ let config;
99
+ if (action === "init") mkdirSync(dir, { mode: 0o700 }); // Never overwrite a CA.
100
+ privatePath(dir, true);
101
+ const lockPath = join(dir, ".credentials.lock");
102
+ try { mkdirSync(lockPath, { mode: 0o700 }); }
103
+ catch (error) {
104
+ if (error.code === "EEXIST") throw new Error(`Another credential command holds ${lockPath}; inspect its owner before retrying.`);
105
+ throw error;
106
+ }
107
+ lock = lockPath;
108
+ writeFileSync(join(lock, "owner"), `${process.pid}\n`, { mode: 0o600, flag: "wx" });
109
+ if (action === "init") {
110
+ key(join(dir, "ca.key"));
111
+ openssl("req", "-new", "-x509", "-key", join(dir, "ca.key"), "-days", "3650", "-sha256", "-subj", "/CN=Pi Intercom Bridge CA",
112
+ "-addext", "basicConstraints=critical,CA:TRUE,pathlen:0", "-addext", "keyUsage=critical,keyCertSign,cRLSign", "-out", join(dir, "ca.crt"));
113
+ const server = issueCertificate(dir, "server", "localhost", true);
114
+ config = { port: 9443, ca: join(dir, "ca.crt"), cert: server.cert, key: server.key, clients: [] };
115
+ } else {
116
+ privatePath(dir, true);
117
+ privatePath(configPath);
118
+ config = JSON.parse(readFileSync(configPath, "utf8"));
119
+ if (!Array.isArray(config.clients)) throw new Error("Invalid clients allowlist.");
120
+ }
121
+ if (action === "revoke") {
122
+ if (!config.clients.some((client) => client.name === name)) throw new Error(`No authorized client named ${name}.`);
123
+ config.clients = config.clients.filter((client) => client.name !== name);
124
+ atomicConfig(configPath, config);
125
+ console.log(JSON.stringify({ revoked: name, config: configPath, next: "Send SIGHUP to the helper PID." }));
126
+ } else {
127
+ privatePath(join(dir, "ca.key"));
128
+ const credential = issueCertificate(dir, `${name}-${randomBytes(8).toString("hex")}`, name);
129
+ const previous = config.clients.find((client) => client.name === name);
130
+ config.clients = config.clients.filter((client) => client.name !== name);
131
+ config.clients.push({ fingerprint256: credential.fingerprint256, name, cwd: resolve(args.values.cwd || previous?.cwd || process.cwd()) });
132
+ atomicConfig(configPath, config);
133
+ console.log(JSON.stringify({ name, ...credential, ca: config.ca, config: configPath, next: "Transfer only client cert/key and ca.crt; reload helper allowlist with SIGHUP." }));
134
+ }
135
+ } catch (error) {
136
+ console.error(error.message);
137
+ process.exitCode = 1;
138
+ } finally {
139
+ if (lock) {
140
+ rmSync(join(lock, "owner"), { force: true });
141
+ rmdirSync(lock);
142
+ }
143
+ }
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+ import https from "node:https";
3
+ import { lstatSync, readFileSync } from "node:fs";
4
+ import { parseArgs } from "node:util";
5
+
6
+ function help() {
7
+ console.log(`Usage: node intercom-remote.mjs <register|list|inbox|send|ask|reply|ack> [options] [IDs...]
8
+
9
+ Environment (required):
10
+ INTERCOM_BRIDGE_CA Path to trusted bridge CA certificate
11
+ INTERCOM_BRIDGE_CERT Path to your client certificate
12
+ INTERCOM_BRIDGE_KEY Path to your private client key (owned, mode 0600)
13
+ INTERCOM_BRIDGE_URL HTTPS origin (default: https://127.0.0.1:9443)
14
+
15
+ Options:
16
+ --to PEER Recipient name or ID for send/ask
17
+ --message TEXT Message for send/ask/reply
18
+ --reply-to ID Inbound ask ID from inbox, for reply
19
+ --timeout-ms MS Ask timeout (100..120000, default: 120000)
20
+ -h, --help Show help
21
+
22
+ Examples:
23
+ node intercom-remote.mjs list
24
+ node intercom-remote.mjs ask --to planner --message 'What is the current status?'
25
+ node intercom-remote.mjs inbox
26
+ node intercom-remote.mjs reply --reply-to QUESTION_ID --message 'Proceed.'
27
+ node intercom-remote.mjs ack MESSAGE_ID
28
+
29
+ Outputs JSON. Exit: 0 success, 1 API/TLS/transport failure, 2 invalid arguments.
30
+ No automatic retries: a lost response does not prove a message was unsent.`);
31
+ }
32
+
33
+ let options, body, action, origin, timeout;
34
+ try {
35
+ const parsed = parseArgs({ allowPositionals: true, options: {
36
+ help: { type: "boolean", short: "h" }, to: { type: "string" }, message: { type: "string" },
37
+ "reply-to": { type: "string" }, "timeout-ms": { type: "string" },
38
+ } });
39
+ if (parsed.values.help) { help(); process.exit(0); }
40
+ options = parsed.values;
41
+ [action] = parsed.positionals;
42
+ if (!["register", "list", "inbox", "send", "ask", "reply", "ack"].includes(action)) throw new Error("Choose an action; use --help.");
43
+ const allowed = ["send", "ask"].includes(action) ? ["to", "message", ...(action === "ask" ? ["timeout-ms"] : [])] : action === "reply" ? ["reply-to", "message"] : [];
44
+ if (Object.keys(options).some((key) => !allowed.includes(key)) ||
45
+ (action !== "ack" && parsed.positionals.length !== 1)) throw new Error("Unexpected arguments for this action.");
46
+ timeout = options["timeout-ms"] === undefined ? 120000 : Number(options["timeout-ms"]);
47
+ if (!Number.isInteger(timeout) || timeout < 100 || timeout > 120000) throw new Error("--timeout-ms must be 100..120000.");
48
+ body = ["send", "ask"].includes(action) ? { to: options.to, message: options.message, ...(action === "ask" ? { timeoutMs: timeout } : {}) }
49
+ : action === "reply" ? { replyTo: options["reply-to"], message: options.message }
50
+ : action === "ack" ? { ids: parsed.positionals.slice(1) } : {};
51
+ if (["send", "ask", "reply"].includes(action) && (!body.message?.trim() || !(action === "reply" ? body.replyTo : body.to)?.trim())) throw new Error("Recipient/ask ID and message are required.");
52
+ if (action === "ack" && body.ids.length === 0) throw new Error("ack needs at least one message ID.");
53
+ origin = new URL(process.env.INTERCOM_BRIDGE_URL || "https://127.0.0.1:9443");
54
+ if (origin.protocol !== "https:" || origin.username || origin.password || origin.pathname !== "/" || origin.search || origin.hash) throw new Error("INTERCOM_BRIDGE_URL must be an HTTPS origin, without credentials or a path.");
55
+ } catch (error) {
56
+ console.error(JSON.stringify({ ok: false, error: { code: "invalid_arguments", message: error.message } }));
57
+ process.exit(2);
58
+ }
59
+
60
+ try {
61
+ const files = {};
62
+ for (const field of ["CA", "CERT", "KEY"]) {
63
+ const path = process.env[`INTERCOM_BRIDGE_${field}`];
64
+ if (!path) throw new Error(`INTERCOM_BRIDGE_${field} is required.`);
65
+ if (field === "KEY") {
66
+ const st = lstatSync(path);
67
+ if (!st.isFile() || st.uid !== process.getuid() || (st.mode & 0o077)) throw new Error("Client key must be an owned private regular file (0600).");
68
+ }
69
+ files[field] = readFileSync(path);
70
+ }
71
+ origin.pathname = `/v1/${action}`;
72
+ const payload = JSON.stringify(body);
73
+ const get = ["list", "inbox"].includes(action);
74
+ const result = await new Promise((resolve, reject) => {
75
+ const req = https.request(origin, {
76
+ method: get ? "GET" : "POST", ca: files.CA, cert: files.CERT, key: files.KEY,
77
+ rejectUnauthorized: true, minVersion: "TLSv1.2", agent: false,
78
+ headers: get ? {} : { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payload) },
79
+ }, (res) => {
80
+ const chunks = [];
81
+ let bytes = 0;
82
+ res.on("data", (chunk) => {
83
+ bytes += chunk.length;
84
+ if (bytes > 16 * 1024 * 1024) { req.destroy(new Error("Bridge response exceeds 16 MiB.")); return; }
85
+ chunks.push(chunk);
86
+ });
87
+ res.on("error", reject);
88
+ res.on("end", () => {
89
+ try { resolve({ status: res.statusCode, value: JSON.parse(Buffer.concat(chunks).toString("utf8")) }); }
90
+ catch { reject(new Error("Bridge returned invalid JSON.")); }
91
+ });
92
+ });
93
+ const timer = setTimeout(() => req.destroy(new Error("Bridge request deadline exceeded; delivery may be unknown.")), (action === "ask" ? timeout : 0) + 15000);
94
+ req.once("close", () => clearTimeout(timer));
95
+ req.once("error", reject);
96
+ req.end(get ? undefined : payload);
97
+ });
98
+ console.log(JSON.stringify(result.value, null, 2));
99
+ if (result.status < 200 || result.status >= 300 || result.value?.ok !== true) process.exitCode = 1;
100
+ } catch (error) {
101
+ console.error(JSON.stringify({ ok: false, error: { code: "transport_error", message: error.message, delivery: "unknown; do not blindly retry mutations" } }));
102
+ process.exitCode = 1;
103
+ }
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
5
+ printf '%s\n' 'Usage: bash scripts/linux-smoke.sh
6
+
7
+ Validate committed HEAD in Docker against a credential-free, prebuilt Pi 1.0
8
+ Linux archive with a single top-level pi/ directory and built SDK/CLI output.
9
+
10
+ Example:
11
+ PI_LINUX_PI_ARCHIVE=/tmp/pi-linux.tar.gz bash scripts/linux-smoke.sh
12
+
13
+ Environment:
14
+ PI_LINUX_PI_ARCHIVE Absolute archive path (required)
15
+ PI_LINUX_IMAGE Node 24.21.0+ Linux image (default: node:24-bookworm)
16
+
17
+ Exit codes: 0 passed; 1 validation failed; 2 invalid arguments.'
18
+ exit 0
19
+ fi
20
+ if [[ $# -ne 0 ]]; then
21
+ printf '%s\n' 'No arguments accepted; use --help.' >&2
22
+ exit 2
23
+ fi
24
+ : "${PI_LINUX_PI_ARCHIVE:?Set PI_LINUX_PI_ARCHIVE to an absolute path to a credential-free prebuilt Linux Pi 1.0 .tar.gz (top-level pi/) with its Linux dependencies and built SDK/CLI.}"
25
+
26
+ # Validate committed HEAD without passing host credentials or mounting the checkout.
27
+ git -C "$(dirname "$0")/.." archive HEAD | docker run --rm --init -i \
28
+ --mount "type=bind,src=$PI_LINUX_PI_ARCHIVE,dst=/native-pi.tar.gz,readonly" \
29
+ "${PI_LINUX_IMAGE:-node:24-bookworm}" bash -c '
30
+ set -euo pipefail
31
+ mkdir /workspace /native-pi
32
+ tar -xf - -C /workspace
33
+ tar -xzf /native-pi.tar.gz -C /native-pi --strip-components=1
34
+ chown -R node:node /workspace /native-pi
35
+ runuser -u node -- bash -c '\''
36
+ set -euo pipefail
37
+ cd /workspace
38
+ node --version
39
+ npm --version
40
+ uname -sm
41
+ npm ci
42
+ ln -sf /native-pi/packages/coding-agent/dist/bundle/cli.js node_modules/.bin/pi
43
+ export PI_INTERCOM_TEST_SDK=/native-pi/packages/coding-agent
44
+ export PI_OWNERSHIP_TEST_PACKAGE_ROOT="$PI_INTERCOM_TEST_SDK"
45
+ export PI_CONTEXT_TEST_PACKAGE_ROOT="$PI_INTERCOM_TEST_SDK"
46
+ export PI_PACKAGE_DIR="$PI_INTERCOM_TEST_SDK"
47
+ export PATH="$PWD/node_modules/.bin:$PATH"
48
+ pi --version
49
+ npm run ci
50
+ '\''
51
+ '
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+ import process from "node:process";
7
+ import { hostCli } from "./compat-host.mjs";
8
+
9
+ if (process.argv.includes("--help") || process.argv.includes("-h")) {
10
+ console.log(`Usage: node scripts/local-install-smoke.mjs\n\nInstalls this repository into an isolated temporary Pi home, then verifies pi list\ncan resolve the single package. User-level Pi settings are not modified.\n\nExit codes:\n 0 local path install/list smoke passed\n 1 pi install or pi list failed, or the installed package was not listed`);
11
+ process.exit(0);
12
+ }
13
+
14
+ function isolatedEnv(home) {
15
+ const env = { ...process.env };
16
+ for (const key of Object.keys(env)) if (key.startsWith("PI_SUBAGENT_")) delete env[key];
17
+ return {
18
+ ...env,
19
+ HOME: home,
20
+ XDG_CONFIG_HOME: join(home, ".config"),
21
+ XDG_CACHE_HOME: join(home, ".cache"),
22
+ PI_CODING_AGENT_DIR: join(home, ".pi", "agent"),
23
+ PI_OFFLINE: "1",
24
+ PATH: process.env.PATH ?? "",
25
+ };
26
+ }
27
+
28
+ function runPi(args, env) {
29
+ const result = spawnSync(process.execPath, [hostCli, ...args], {
30
+ cwd: process.cwd(),
31
+ env,
32
+ encoding: "utf-8",
33
+ stdio: ["ignore", "pipe", "pipe"],
34
+ });
35
+ return {
36
+ ok: !result.error && result.status === 0,
37
+ output: result.error ? result.error.message : `${result.stdout ?? ""}${result.stderr ?? ""}`,
38
+ status: result.status,
39
+ };
40
+ }
41
+
42
+ function requireSuccess(label, result) {
43
+ if (result.ok) return;
44
+ throw new Error(`${label} failed with ${result.status ?? "spawn error"}:\n${result.output}`);
45
+ }
46
+
47
+ const repoRoot = resolve(process.cwd());
48
+ const home = mkdtempSync(join(tmpdir(), "pi-subagents-install-smoke-"));
49
+
50
+ try {
51
+ const env = isolatedEnv(home);
52
+ requireSuccess("pi install pi-subagents", runPi(["install", repoRoot, "--approve"], env));
53
+
54
+ const list = runPi(["list", "--approve"], env);
55
+ requireSuccess("pi list", list);
56
+ if (!list.output.includes(repoRoot)) {
57
+ throw new Error(`pi list did not include installed local path ${repoRoot}:\n${list.output}`);
58
+ }
59
+
60
+ console.log("[local-install-smoke] installed and listed the single pi-subagents package with bundled intercom");
61
+ } catch (error) {
62
+ console.error(`[local-install-smoke] ${error instanceof Error ? error.message : String(error)}`);
63
+ process.exitCode = 1;
64
+ } finally {
65
+ rmSync(home, { recursive: true, force: true });
66
+ }
@@ -0,0 +1,82 @@
1
+ // Full runtime-only package: native SDK + bundled CLI, one private Intercom broker, no model calls.
2
+ import assert from "node:assert/strict";
3
+ import { spawn, spawnSync } from "node:child_process";
4
+ import { once } from "node:events";
5
+ import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
6
+ import { tmpdir } from "node:os";
7
+ import { join, resolve } from "node:path";
8
+ import { setTimeout as delay } from "node:timers/promises";
9
+ import { pathToFileURL } from "node:url";
10
+ import { hostCli, hostIndex, hostRoot } from "./compat-host.mjs";
11
+
12
+ const packageRoot = resolve(process.argv[2] ?? ".");
13
+ const root = mkdtempSync(join(tmpdir(), "ps-native-"));
14
+ const agentDir = join(root, "agent");
15
+ mkdirSync(agentDir);
16
+ Object.assign(process.env, { HOME: root, USERPROFILE: root, PI_CODING_AGENT_DIR: agentDir, PI_SUBAGENT_TEMP_ROOT: join(root, "pi-subagents-runs"), PI_OFFLINE: "1", PI_TELEMETRY: "0", PI_PACKAGE_DIR: hostRoot });
17
+ // A pruned extension cannot discover the embedding SDK via its own node_modules or PATH.
18
+ // PI_PACKAGE_DIR is the existing detached-runtime host contract, not a loader workaround.
19
+ const sdk = await import(pathToFileURL(hostIndex).href);
20
+ const broker = spawn(process.execPath, [join(packageRoot, "dist/pi-intercom/broker/broker.js")], { cwd: root, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
21
+ let brokerLog = "";
22
+ broker.stdout.on("data", data => { brokerLog += data; });
23
+ broker.stderr.on("data", data => { brokerLog += data; });
24
+ let session;
25
+ try {
26
+ for (let i = 0; !brokerLog.includes("Intercom broker started"); i++) {
27
+ assert.ok(i < 100 && broker.exitCode === null, brokerLog || "Private broker did not start");
28
+ await delay(50);
29
+ }
30
+ const settingsManager = sdk.SettingsManager.inMemory({ packages: [packageRoot], compaction: { enabled: false }, retry: { enabled: false } });
31
+ const loader = new sdk.DefaultResourceLoader({ cwd: root, agentDir, settingsManager, noSkills: true, noContextFiles: true, noPromptTemplates: true, noThemes: true });
32
+ await loader.reload();
33
+ assert.deepEqual(loader.getExtensions().errors, []);
34
+ assert.deepEqual(loader.getExtensions().warnings ?? [], []);
35
+ assert.equal(loader.getExtensions().extensions.length, 2);
36
+ const modelRuntime = await sdk.ModelRuntime.create({ authPath: join(agentDir, "auth.json"), modelsPath: null, modelsStorePath: join(agentDir, "models-store.json"), allowModelNetwork: false });
37
+ ({ session } = await sdk.createAgentSession({ cwd: root, agentDir, settingsManager, resourceLoader: loader, modelRuntime, sessionManager: sdk.SessionManager.create(root, join(root, "sessions")), noTools: "builtin" }));
38
+ const errors = [];
39
+ await session.bindExtensions({ mode: "print", onError: error => errors.push(error) });
40
+ for (const name of ["delegate", "load_subagent", "load_intercom"]) assert.ok(session.getActiveToolNames().includes(name), name);
41
+ for (const name of ["agent_runs", "subagent", "intercom"]) assert.ok(!session.getActiveToolNames().includes(name), `${name} starts lazy`);
42
+ await session.agent.state.tools.find(tool => tool.name === "load_subagent").execute("controls", { advanced: false }, new AbortController().signal);
43
+ assert.ok(session.getActiveToolNames().includes("agent_runs"));
44
+ assert.ok(!session.getActiveToolNames().includes("subagent"));
45
+ await session.agent.state.tools.find(tool => tool.name === "load_intercom").execute("peers", {}, new AbortController().signal);
46
+ const status = await session.agent.state.tools.find(tool => tool.name === "intercom").execute("status", { action: "status" }, new AbortController().signal);
47
+ assert.match(JSON.stringify(status.content), /Connected: Yes/);
48
+ const runs = await session.agent.state.tools.find(tool => tool.name === "agent_runs").execute("history-list", { action: "list" }, new AbortController().signal);
49
+ assert.notEqual(runs.isError, true, JSON.stringify(runs.content));
50
+ assert.equal(runs.details.runList.total, 0);
51
+ assert.deepEqual(errors, []);
52
+ const marker = join(root, "cli.json");
53
+ const observer = join(root, "observer.ts");
54
+ writeFileSync(observer, `import { writeFileSync } from "node:fs";
55
+ import { SubagentHistoryIndex } from ${JSON.stringify(join(packageRoot, "dist/history/index.js"))};
56
+ export default function(pi) { pi.on("session_start", async (_event, ctx) => {
57
+ const index = new SubagentHistoryIndex(${JSON.stringify(agentDir)});
58
+ try {
59
+ await index.setOwner({ ownerSessionId: ctx.sessionManager.getSessionId(), runs: [] });
60
+ const history = await index.listRuns();
61
+ writeFileSync(${JSON.stringify(marker)}, JSON.stringify({ tools: pi.getActiveTools(), commands: pi.getCommands().map(c => c.name), historyRuns: history.total }));
62
+ } finally { await index.close(); ctx.shutdown(); }
63
+ }); }`);
64
+ const cliEnv = { ...process.env, PATH: "/usr/bin:/bin" };
65
+ delete cliEnv.PI_PACKAGE_DIR; // Ordinary CLI consumers locate their host through argv, unlike detached SDK embeddings.
66
+ const child = spawnSync(process.execPath, [hostCli, "--mode", "rpc", "--no-session", "-ne", "-ns", "-np", "-nc", "--no-themes", "--approve", "-e", packageRoot, "-e", observer], { cwd: root, env: cliEnv, input: "", encoding: "utf8", timeout: 30_000 });
67
+ assert.equal(child.status, 0, `${child.error ?? ""}\n${child.stderr}`);
68
+ assert.doesNotMatch(child.stderr, /Failed to load extension|ERR_INTERNAL_ASSERTION|Extension error/);
69
+ const observed = JSON.parse(readFileSync(marker, "utf8"));
70
+ assert.equal(observed.historyRuns, 0, "CLI workers must inherit the selected host without Pi on PATH");
71
+ for (const name of ["delegate", "load_subagent", "load_intercom"]) assert.ok(observed.tools.includes(name), name);
72
+ for (const name of ["agent_runs", "subagent", "intercom"]) assert.ok(!observed.tools.includes(name), `${name} starts lazy in CLI`);
73
+ assert.ok(observed.commands.includes("subagents-doctor"));
74
+ console.log("[native-package-smoke] both compiled entries, history worker, broker registration/status, bundled RPC startup, and shutdown passed");
75
+ } finally {
76
+ try {
77
+ if (session) { await session.extensionRunner.emit({ type: "session_shutdown", reason: "quit" }); session.dispose(); }
78
+ } finally {
79
+ if (broker.exitCode === null) { const exited = once(broker, "exit"); broker.kill("SIGTERM"); await exited; }
80
+ rmSync(root, { recursive: true, force: true });
81
+ }
82
+ }