pi-crew 0.9.53 → 0.9.55

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 (493) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/package.json +6 -3
  3. package/scripts/clean-strip-types.mjs +60 -0
  4. package/src/adapters/claude-adapter.ts +23 -0
  5. package/src/adapters/codex-adapter.ts +21 -0
  6. package/src/adapters/cursor-adapter.ts +17 -0
  7. package/src/adapters/export-util.ts +143 -0
  8. package/src/adapters/index.ts +15 -0
  9. package/src/adapters/registry.ts +18 -0
  10. package/src/adapters/types.ts +23 -0
  11. package/src/agents/agent-config.ts +194 -0
  12. package/src/agents/agent-search.ts +98 -0
  13. package/src/agents/agent-serializer.ts +38 -0
  14. package/src/agents/discover-agents.ts +623 -0
  15. package/src/benchmark/benchmark-runner.ts +313 -0
  16. package/src/benchmark/feedback-loop.ts +73 -0
  17. package/src/config/config.ts +1270 -0
  18. package/src/config/defaults.ts +193 -0
  19. package/src/config/drift-detector.ts +290 -0
  20. package/src/config/markers.ts +330 -0
  21. package/src/config/resilient-parser.ts +117 -0
  22. package/src/config/role-tools.ts +118 -0
  23. package/src/config/suggestions.ts +75 -0
  24. package/src/config/types.ts +280 -0
  25. package/src/errors.ts +194 -0
  26. package/src/extension/action-suggestions.ts +117 -0
  27. package/src/extension/async-notifier.ts +179 -0
  28. package/src/extension/autonomous-policy.ts +209 -0
  29. package/src/extension/command-completions.ts +120 -0
  30. package/src/extension/context-status-injection.ts +185 -0
  31. package/src/extension/crew-autocomplete.ts +131 -0
  32. package/src/extension/crew-cleanup.ts +168 -0
  33. package/src/extension/crew-input-router.ts +93 -0
  34. package/src/extension/crew-shortcuts.ts +87 -0
  35. package/src/extension/crew-vibes/cat-frames.ts +18 -0
  36. package/src/extension/crew-vibes/config.ts +195 -0
  37. package/src/extension/crew-vibes/figures.ts +101 -0
  38. package/src/extension/crew-vibes/font-detect.ts +71 -0
  39. package/src/extension/crew-vibes/footer.ts +292 -0
  40. package/src/extension/crew-vibes/index.ts +432 -0
  41. package/src/extension/crew-vibes/provider-usage.ts +396 -0
  42. package/src/extension/crew-vibes/render.ts +206 -0
  43. package/src/extension/crew-vibes/speed.ts +286 -0
  44. package/src/extension/cross-extension-rpc.ts +350 -0
  45. package/src/extension/help.ts +62 -0
  46. package/src/extension/import-index.ts +74 -0
  47. package/src/extension/knowledge-injection.ts +402 -0
  48. package/src/extension/management.ts +571 -0
  49. package/src/extension/message-renderers.ts +108 -0
  50. package/src/extension/notification-router.ts +152 -0
  51. package/src/extension/notification-sink.ts +54 -0
  52. package/src/extension/pi-api.ts +56 -0
  53. package/src/extension/plan-orchestrate.ts +302 -0
  54. package/src/extension/project-init.ts +170 -0
  55. package/src/extension/register.ts +113 -0
  56. package/src/extension/registration/artifact-cleanup.ts +20 -0
  57. package/src/extension/registration/command-registration.ts +58 -0
  58. package/src/extension/registration/command-utils.ts +58 -0
  59. package/src/extension/registration/commands.ts +1228 -0
  60. package/src/extension/registration/compaction-guard.ts +333 -0
  61. package/src/extension/registration/context-builder.ts +135 -0
  62. package/src/extension/registration/crash-recovery-cache.ts +54 -0
  63. package/src/extension/registration/foreground-run-controller.ts +238 -0
  64. package/src/extension/registration/hook-registration.ts +119 -0
  65. package/src/extension/registration/lazy-configurers.ts +124 -0
  66. package/src/extension/registration/lifecycle-handlers.ts +923 -0
  67. package/src/extension/registration/lifecycle.ts +259 -0
  68. package/src/extension/registration/observability.ts +322 -0
  69. package/src/extension/registration/registration-types.ts +158 -0
  70. package/src/extension/registration/runtime-cleanup.ts +230 -0
  71. package/src/extension/registration/subagent-helpers.ts +125 -0
  72. package/src/extension/registration/subagent-manager-setup.ts +330 -0
  73. package/src/extension/registration/subagent-tools.ts +456 -0
  74. package/src/extension/registration/team-tool.ts +225 -0
  75. package/src/extension/registration/tool-registration.ts +50 -0
  76. package/src/extension/registration/ui.ts +172 -0
  77. package/src/extension/registration/viewers.ts +121 -0
  78. package/src/extension/registration/wire-cross-extension.ts +37 -0
  79. package/src/extension/result-watcher.ts +139 -0
  80. package/src/extension/rpc-hmac.ts +256 -0
  81. package/src/extension/run-bundle-schema.ts +104 -0
  82. package/src/extension/run-export.ts +97 -0
  83. package/src/extension/run-import.ts +168 -0
  84. package/src/extension/run-index.ts +131 -0
  85. package/src/extension/run-maintenance.ts +190 -0
  86. package/src/extension/session-summary.ts +18 -0
  87. package/src/extension/team-manager-command.ts +153 -0
  88. package/src/extension/team-onboard.ts +180 -0
  89. package/src/extension/team-recommendation.ts +288 -0
  90. package/src/extension/team-tool/anchor.ts +172 -0
  91. package/src/extension/team-tool/api.ts +1244 -0
  92. package/src/extension/team-tool/auto-summarize.ts +142 -0
  93. package/src/extension/team-tool/cache-control.ts +19 -0
  94. package/src/extension/team-tool/cancel.ts +337 -0
  95. package/src/extension/team-tool/chain-dispatch.ts +95 -0
  96. package/src/extension/team-tool/chain-executor.ts +379 -0
  97. package/src/extension/team-tool/config-patch.ts +50 -0
  98. package/src/extension/team-tool/context.ts +149 -0
  99. package/src/extension/team-tool/destructive-gate.ts +52 -0
  100. package/src/extension/team-tool/dispatch/automate.ts +80 -0
  101. package/src/extension/team-tool/dispatch/control.ts +42 -0
  102. package/src/extension/team-tool/dispatch/index.ts +95 -0
  103. package/src/extension/team-tool/dispatch/manage.ts +161 -0
  104. package/src/extension/team-tool/dispatch/run.ts +53 -0
  105. package/src/extension/team-tool/dispatch/status.ts +187 -0
  106. package/src/extension/team-tool/doctor.ts +431 -0
  107. package/src/extension/team-tool/explain.ts +282 -0
  108. package/src/extension/team-tool/failure-patterns.ts +118 -0
  109. package/src/extension/team-tool/goal-wrap.ts +330 -0
  110. package/src/extension/team-tool/goal.ts +556 -0
  111. package/src/extension/team-tool/handle-schedule.ts +332 -0
  112. package/src/extension/team-tool/handle-settings.ts +520 -0
  113. package/src/extension/team-tool/health-monitor.ts +481 -0
  114. package/src/extension/team-tool/inspect.ts +88 -0
  115. package/src/extension/team-tool/intent-policy.ts +45 -0
  116. package/src/extension/team-tool/lifecycle-actions.ts +615 -0
  117. package/src/extension/team-tool/orchestrate.ts +98 -0
  118. package/src/extension/team-tool/parallel-dispatch.ts +183 -0
  119. package/src/extension/team-tool/plan.ts +50 -0
  120. package/src/extension/team-tool/respond.ts +150 -0
  121. package/src/extension/team-tool/run-deadline.ts +66 -0
  122. package/src/extension/team-tool/run-not-found.ts +49 -0
  123. package/src/extension/team-tool/run.ts +1051 -0
  124. package/src/extension/team-tool/status.ts +260 -0
  125. package/src/extension/team-tool/workflow-manage.ts +261 -0
  126. package/src/extension/team-tool-types.ts +26 -0
  127. package/src/extension/team-tool.ts +750 -0
  128. package/src/extension/tool-result.ts +16 -0
  129. package/src/extension/validate-resources.ts +108 -0
  130. package/src/hooks/registry.ts +200 -0
  131. package/src/hooks/types.ts +69 -0
  132. package/src/i18n.ts +212 -0
  133. package/src/observability/correlation.ts +50 -0
  134. package/src/observability/event-bus.ts +86 -0
  135. package/src/observability/event-to-metric.ts +170 -0
  136. package/src/observability/exporters/adapter.ts +27 -0
  137. package/src/observability/exporters/otlp-exporter.ts +356 -0
  138. package/src/observability/exporters/prometheus-exporter.ts +54 -0
  139. package/src/observability/metric-registry.ts +100 -0
  140. package/src/observability/metric-retention.ts +64 -0
  141. package/src/observability/metric-sink.ts +99 -0
  142. package/src/observability/metrics-primitives.ts +219 -0
  143. package/src/plugins/plugin-define.ts +6 -0
  144. package/src/plugins/plugin-registry.ts +32 -0
  145. package/src/plugins/plugins/index.ts +3 -0
  146. package/src/plugins/plugins/nextjs.ts +19 -0
  147. package/src/plugins/plugins/vite.ts +10 -0
  148. package/src/plugins/plugins/vitest.ts +9 -0
  149. package/src/prompt/prompt-runtime.ts +380 -0
  150. package/src/runtime/adaptive-plan.ts +563 -0
  151. package/src/runtime/agent-control.ts +215 -0
  152. package/src/runtime/agent-memory.ts +81 -0
  153. package/src/runtime/agent-observability.ts +122 -0
  154. package/src/runtime/anchor-manager.ts +475 -0
  155. package/src/runtime/async-marker.ts +32 -0
  156. package/src/runtime/async-runner.ts +381 -0
  157. package/src/runtime/attention-events.ts +28 -0
  158. package/src/runtime/auto-summarize.ts +344 -0
  159. package/src/runtime/background-runner.ts +840 -0
  160. package/src/runtime/batch-barrier.ts +147 -0
  161. package/src/runtime/broker-issuer.ts +39 -0
  162. package/src/runtime/cancellation-token.ts +99 -0
  163. package/src/runtime/cancellation.ts +99 -0
  164. package/src/runtime/capability-inventory.ts +137 -0
  165. package/src/runtime/chain-parser.ts +237 -0
  166. package/src/runtime/chain-runner.ts +606 -0
  167. package/src/runtime/checkpoint.ts +291 -0
  168. package/src/runtime/child-pi-constants.ts +42 -0
  169. package/src/runtime/child-pi-kill.ts +180 -0
  170. package/src/runtime/child-pi-pool.ts +68 -0
  171. package/src/runtime/child-pi-spawn.ts +287 -0
  172. package/src/runtime/child-pi-steering.ts +128 -0
  173. package/src/runtime/child-pi-streams.ts +296 -0
  174. package/src/runtime/child-pi-transcript.ts +169 -0
  175. package/src/runtime/child-pi.ts +1105 -0
  176. package/src/runtime/coalesce-tasks.ts +268 -0
  177. package/src/runtime/code-summary.ts +292 -0
  178. package/src/runtime/command-trace.ts +105 -0
  179. package/src/runtime/compact-pipeline.ts +56 -0
  180. package/src/runtime/compact-stages/ansi-strip-stage.ts +25 -0
  181. package/src/runtime/compact-stages/blank-collapse-stage.ts +31 -0
  182. package/src/runtime/compact-stages/deduplicate-stage.ts +34 -0
  183. package/src/runtime/compact-stages/head-snap-stage.ts +57 -0
  184. package/src/runtime/compact-stages/index.ts +23 -0
  185. package/src/runtime/compact-stages/tail-capture-stage.ts +77 -0
  186. package/src/runtime/compact-stages/truncation-stage.ts +74 -0
  187. package/src/runtime/compaction-summary.ts +278 -0
  188. package/src/runtime/completion-guard.ts +207 -0
  189. package/src/runtime/concurrency.ts +58 -0
  190. package/src/runtime/crash-classification.ts +229 -0
  191. package/src/runtime/crash-recovery.ts +594 -0
  192. package/src/runtime/crew-agent-records.ts +595 -0
  193. package/src/runtime/crew-agent-runtime.ts +62 -0
  194. package/src/runtime/crew-broker-child.ts +88 -0
  195. package/src/runtime/crew-broker-client.ts +673 -0
  196. package/src/runtime/crew-broker-tokens.ts +159 -0
  197. package/src/runtime/crew-broker.ts +1304 -0
  198. package/src/runtime/crew-hooks.ts +227 -0
  199. package/src/runtime/cross-extension-rpc.ts +143 -0
  200. package/src/runtime/custom-tools/irc-tool.ts +282 -0
  201. package/src/runtime/custom-tools/submit-result-tool.ts +102 -0
  202. package/src/runtime/deadletter.ts +47 -0
  203. package/src/runtime/delivery-coordinator.ts +211 -0
  204. package/src/runtime/delta-conflict.ts +348 -0
  205. package/src/runtime/deterministic-ast.ts +161 -0
  206. package/src/runtime/diagnostic-export.ts +171 -0
  207. package/src/runtime/direct-run.ts +45 -0
  208. package/src/runtime/dwf-state-store.ts +102 -0
  209. package/src/runtime/dynamic-workflow-context.ts +1013 -0
  210. package/src/runtime/dynamic-workflow-runner.ts +325 -0
  211. package/src/runtime/effectiveness.ts +90 -0
  212. package/src/runtime/errors/crew-errors.ts +162 -0
  213. package/src/runtime/event-stream-bridge.ts +98 -0
  214. package/src/runtime/foreground-control.ts +193 -0
  215. package/src/runtime/foreground-watchdog.ts +132 -0
  216. package/src/runtime/global-worker-cap.ts +96 -0
  217. package/src/runtime/goal-achievement.ts +148 -0
  218. package/src/runtime/goal-evaluator.ts +365 -0
  219. package/src/runtime/goal-loop-runner.ts +825 -0
  220. package/src/runtime/goal-state-store.ts +219 -0
  221. package/src/runtime/green-contract.ts +55 -0
  222. package/src/runtime/group-join.ts +267 -0
  223. package/src/runtime/handoff-manager.ts +598 -0
  224. package/src/runtime/heartbeat-gradient.ts +36 -0
  225. package/src/runtime/heartbeat-watcher.ts +239 -0
  226. package/src/runtime/hidden-handoff.ts +407 -0
  227. package/src/runtime/important-line-classifier.ts +140 -0
  228. package/src/runtime/intercom-bridge.ts +187 -0
  229. package/src/runtime/iteration-hooks.ts +305 -0
  230. package/src/runtime/live-agent-control.ts +136 -0
  231. package/src/runtime/live-agent-manager.ts +704 -0
  232. package/src/runtime/live-control-realtime.ts +56 -0
  233. package/src/runtime/live-extension-bridge.ts +148 -0
  234. package/src/runtime/live-irc.ts +97 -0
  235. package/src/runtime/live-session-health.ts +108 -0
  236. package/src/runtime/live-session-runtime.ts +1146 -0
  237. package/src/runtime/loop-gates.ts +128 -0
  238. package/src/runtime/manifest-cache.ts +375 -0
  239. package/src/runtime/mcp-proxy.ts +105 -0
  240. package/src/runtime/metric-parser.ts +36 -0
  241. package/src/runtime/model-fallback.ts +409 -0
  242. package/src/runtime/model-resolver.ts +126 -0
  243. package/src/runtime/model-scope.ts +158 -0
  244. package/src/runtime/orphan-worker-registry.ts +465 -0
  245. package/src/runtime/output-validator.ts +202 -0
  246. package/src/runtime/overflow-recovery.ts +206 -0
  247. package/src/runtime/parallel-research.ts +68 -0
  248. package/src/runtime/parallel-utils.ts +160 -0
  249. package/src/runtime/parent-guard.ts +134 -0
  250. package/src/runtime/path-overlap.ts +150 -0
  251. package/src/runtime/peer-dep.ts +292 -0
  252. package/src/runtime/per-write-validator.ts +181 -0
  253. package/src/runtime/phase-progress.ts +217 -0
  254. package/src/runtime/phase-tracker.ts +385 -0
  255. package/src/runtime/pi-args.ts +655 -0
  256. package/src/runtime/pi-json-output.ts +170 -0
  257. package/src/runtime/pi-spawn.ts +273 -0
  258. package/src/runtime/pipeline-runner.ts +523 -0
  259. package/src/runtime/plan-templates.ts +201 -0
  260. package/src/runtime/policy-engine.ts +115 -0
  261. package/src/runtime/post-checks.ts +142 -0
  262. package/src/runtime/post-exit-stdio-guard.ts +109 -0
  263. package/src/runtime/process-lifecycle.ts +491 -0
  264. package/src/runtime/process-status.ts +154 -0
  265. package/src/runtime/progress-event-coalescer.ts +44 -0
  266. package/src/runtime/progress-tracker.ts +124 -0
  267. package/src/runtime/prose-compressor.ts +162 -0
  268. package/src/runtime/recovery-recipes.ts +203 -0
  269. package/src/runtime/replace.ts +570 -0
  270. package/src/runtime/resilient-edit.ts +153 -0
  271. package/src/runtime/result-extractor.ts +217 -0
  272. package/src/runtime/retry-executor.ts +109 -0
  273. package/src/runtime/retry-runner.ts +336 -0
  274. package/src/runtime/role-permission.ts +59 -0
  275. package/src/runtime/run-coalesced-task-group.ts +308 -0
  276. package/src/runtime/run-drift.ts +219 -0
  277. package/src/runtime/run-tracker.ts +116 -0
  278. package/src/runtime/run-worker.ts +78 -0
  279. package/src/runtime/runtime-policy.ts +29 -0
  280. package/src/runtime/runtime-resolver.ts +172 -0
  281. package/src/runtime/runtime-warmup.ts +160 -0
  282. package/src/runtime/scheduler.ts +353 -0
  283. package/src/runtime/semaphore.ts +141 -0
  284. package/src/runtime/sensitive-paths.ts +93 -0
  285. package/src/runtime/session-resources.ts +25 -0
  286. package/src/runtime/session-snapshot.ts +59 -0
  287. package/src/runtime/session-usage.ts +79 -0
  288. package/src/runtime/settings-store.ts +155 -0
  289. package/src/runtime/sidechain-output.ts +35 -0
  290. package/src/runtime/single-agent-compose.ts +84 -0
  291. package/src/runtime/skill-effectiveness.ts +524 -0
  292. package/src/runtime/skill-instructions.ts +394 -0
  293. package/src/runtime/stale-reconciler.ts +680 -0
  294. package/src/runtime/stream-preview.ts +184 -0
  295. package/src/runtime/streaming-output.ts +50 -0
  296. package/src/runtime/subagent-manager.ts +561 -0
  297. package/src/runtime/subprocess-tool-registry.ts +70 -0
  298. package/src/runtime/supervisor-contact.ts +64 -0
  299. package/src/runtime/task-display.ts +52 -0
  300. package/src/runtime/task-graph-scheduler.ts +227 -0
  301. package/src/runtime/task-graph.ts +290 -0
  302. package/src/runtime/task-health.ts +83 -0
  303. package/src/runtime/task-id.ts +161 -0
  304. package/src/runtime/task-output-context.ts +602 -0
  305. package/src/runtime/task-packet.ts +268 -0
  306. package/src/runtime/task-quality.ts +199 -0
  307. package/src/runtime/task-runner/capabilities.ts +78 -0
  308. package/src/runtime/task-runner/child-executor.ts +790 -0
  309. package/src/runtime/task-runner/context-retrieval.ts +163 -0
  310. package/src/runtime/task-runner/live-executor.ts +227 -0
  311. package/src/runtime/task-runner/output-splitter.ts +152 -0
  312. package/src/runtime/task-runner/post-execution.ts +480 -0
  313. package/src/runtime/task-runner/pre-execution.ts +365 -0
  314. package/src/runtime/task-runner/progress.ts +157 -0
  315. package/src/runtime/task-runner/prompt-builder.ts +278 -0
  316. package/src/runtime/task-runner/prompt-pipeline.ts +88 -0
  317. package/src/runtime/task-runner/result-utils.ts +16 -0
  318. package/src/runtime/task-runner/retrieval-orchestrator.ts +310 -0
  319. package/src/runtime/task-runner/run-projection.ts +125 -0
  320. package/src/runtime/task-runner/scaffold-executor.ts +25 -0
  321. package/src/runtime/task-runner/state-helpers.ts +176 -0
  322. package/src/runtime/task-runner/tail-read.ts +34 -0
  323. package/src/runtime/task-runner.ts +216 -0
  324. package/src/runtime/team-runner-artifacts.ts +13 -0
  325. package/src/runtime/team-runner.ts +2396 -0
  326. package/src/runtime/tool-output-pruner.ts +333 -0
  327. package/src/runtime/tool-progress.ts +278 -0
  328. package/src/runtime/usage-tracker.ts +73 -0
  329. package/src/runtime/verification-gates.ts +579 -0
  330. package/src/runtime/verification-integrity.ts +108 -0
  331. package/src/runtime/verification-worktree.ts +186 -0
  332. package/src/runtime/worker-heartbeat.ts +25 -0
  333. package/src/runtime/worker-startup.ts +83 -0
  334. package/src/runtime/workflow-state.ts +195 -0
  335. package/src/runtime/workspace-lock.ts +443 -0
  336. package/src/runtime/workspace-tree.ts +312 -0
  337. package/src/runtime/yield-handler.ts +224 -0
  338. package/src/runtime/zombie-scanner.ts +298 -0
  339. package/src/schema/config-schema.ts +327 -0
  340. package/src/schema/team-tool-schema.ts +492 -0
  341. package/src/schema/validation-types.ts +159 -0
  342. package/src/skills/discover-skills.ts +203 -0
  343. package/src/skills/skill-templates.ts +456 -0
  344. package/src/skills/validate.ts +285 -0
  345. package/src/state/active-run-registry.ts +439 -0
  346. package/src/state/artifact-store.ts +176 -0
  347. package/src/state/atomic-write.ts +970 -0
  348. package/src/state/blob-store.ts +308 -0
  349. package/src/state/contracts.ts +160 -0
  350. package/src/state/crew-init.ts +269 -0
  351. package/src/state/decision-ledger.ts +372 -0
  352. package/src/state/event-log-rotation.ts +399 -0
  353. package/src/state/event-log.ts +1465 -0
  354. package/src/state/event-reconstructor.ts +229 -0
  355. package/src/state/gitignore-manager.ts +46 -0
  356. package/src/state/health-store.ts +79 -0
  357. package/src/state/hook-instinct-bridge.ts +94 -0
  358. package/src/state/hook-integrations.ts +51 -0
  359. package/src/state/instinct-store.ts +275 -0
  360. package/src/state/jsonl-writer.ts +107 -0
  361. package/src/state/locks.ts +562 -0
  362. package/src/state/mailbox.ts +916 -0
  363. package/src/state/observation-store.ts +176 -0
  364. package/src/state/run-cache.ts +193 -0
  365. package/src/state/run-graph.ts +183 -0
  366. package/src/state/run-metrics.ts +165 -0
  367. package/src/state/schedule.ts +166 -0
  368. package/src/state/session-state-map.ts +51 -0
  369. package/src/state/state-store.ts +973 -0
  370. package/src/state/task-claims.ts +61 -0
  371. package/src/state/tiered-eval.ts +480 -0
  372. package/src/state/types-eval.ts +58 -0
  373. package/src/state/types.ts +447 -0
  374. package/src/state/usage.ts +138 -0
  375. package/src/state/worker-atomic-writer.ts +198 -0
  376. package/src/subagents/async-entry.ts +1 -0
  377. package/src/subagents/index.ts +3 -0
  378. package/src/subagents/live/control.ts +1 -0
  379. package/src/subagents/live/manager.ts +1 -0
  380. package/src/subagents/live/realtime.ts +1 -0
  381. package/src/subagents/live/session-runtime.ts +1 -0
  382. package/src/subagents/manager.ts +1 -0
  383. package/src/subagents/spawn.ts +1 -0
  384. package/src/teams/discover-teams.ts +187 -0
  385. package/src/teams/team-config.ts +27 -0
  386. package/src/teams/team-serializer.ts +38 -0
  387. package/src/tools/safe-bash-extension.ts +54 -0
  388. package/src/tools/safe-bash.ts +505 -0
  389. package/src/types/diff.d.ts +18 -0
  390. package/src/types/new-api-types.ts +35 -0
  391. package/src/ui/agent-management-overlay.ts +160 -0
  392. package/src/ui/card-colors.ts +139 -0
  393. package/src/ui/crew-footer.ts +102 -0
  394. package/src/ui/crew-select-list.ts +112 -0
  395. package/src/ui/dashboard-panes/agents-pane.ts +164 -0
  396. package/src/ui/dashboard-panes/cancellation-pane.ts +66 -0
  397. package/src/ui/dashboard-panes/capability-pane.ts +77 -0
  398. package/src/ui/dashboard-panes/health-pane.ts +31 -0
  399. package/src/ui/dashboard-panes/mailbox-pane.ts +35 -0
  400. package/src/ui/dashboard-panes/metrics-pane.ts +37 -0
  401. package/src/ui/dashboard-panes/progress-pane.ts +38 -0
  402. package/src/ui/dashboard-panes/transcript-pane.ts +10 -0
  403. package/src/ui/deploy-bundled-themes.ts +71 -0
  404. package/src/ui/dwf-phase-display.ts +151 -0
  405. package/src/ui/dynamic-border.ts +35 -0
  406. package/src/ui/format-helpers.ts +31 -0
  407. package/src/ui/heartbeat-aggregator.ts +74 -0
  408. package/src/ui/key-utils.ts +42 -0
  409. package/src/ui/keybinding-map.ts +248 -0
  410. package/src/ui/layout-primitives.ts +106 -0
  411. package/src/ui/live-conversation-overlay.ts +183 -0
  412. package/src/ui/live-duration.ts +55 -0
  413. package/src/ui/live-run-sidebar.ts +293 -0
  414. package/src/ui/loaders.ts +6 -0
  415. package/src/ui/mascot.ts +449 -0
  416. package/src/ui/overlays/agent-picker-overlay.ts +66 -0
  417. package/src/ui/overlays/confirm-overlay.ts +60 -0
  418. package/src/ui/overlays/help-overlay.ts +177 -0
  419. package/src/ui/overlays/mailbox-compose-overlay.ts +181 -0
  420. package/src/ui/overlays/mailbox-compose-preview.ts +78 -0
  421. package/src/ui/overlays/mailbox-detail-overlay.ts +155 -0
  422. package/src/ui/pi-ui-compat.ts +68 -0
  423. package/src/ui/powerbar-publisher.ts +459 -0
  424. package/src/ui/render-coalescer.ts +101 -0
  425. package/src/ui/render-diff.ts +160 -0
  426. package/src/ui/render-scheduler.ts +278 -0
  427. package/src/ui/run-action-dispatcher.ts +182 -0
  428. package/src/ui/run-dashboard.ts +910 -0
  429. package/src/ui/run-event-bus.ts +381 -0
  430. package/src/ui/run-snapshot-cache.ts +1057 -0
  431. package/src/ui/settings-overlay.ts +1067 -0
  432. package/src/ui/shared-overlay-scheduler.ts +96 -0
  433. package/src/ui/snapshot-types.ts +90 -0
  434. package/src/ui/spinner.ts +17 -0
  435. package/src/ui/status-colors.ts +143 -0
  436. package/src/ui/syntax-highlight.ts +136 -0
  437. package/src/ui/terminal-status.ts +269 -0
  438. package/src/ui/theme-adapter.ts +271 -0
  439. package/src/ui/theme-discovery.ts +199 -0
  440. package/src/ui/tool-progress-formatter.ts +93 -0
  441. package/src/ui/tool-renderers/brief-mode.ts +225 -0
  442. package/src/ui/tool-renderers/index.ts +726 -0
  443. package/src/ui/transcript-cache.ts +128 -0
  444. package/src/ui/transcript-entries.ts +256 -0
  445. package/src/ui/transcript-viewer.ts +424 -0
  446. package/src/ui/widget/index.ts +418 -0
  447. package/src/ui/widget/widget-formatters.ts +182 -0
  448. package/src/ui/widget/widget-model.ts +110 -0
  449. package/src/ui/widget/widget-renderer.ts +182 -0
  450. package/src/ui/widget/widget-types.ts +36 -0
  451. package/src/utils/bm25-search.ts +235 -0
  452. package/src/utils/completion-dedupe.ts +63 -0
  453. package/src/utils/conflict-detect.ts +668 -0
  454. package/src/utils/env-allowlist.ts +30 -0
  455. package/src/utils/env-filter.ts +155 -0
  456. package/src/utils/file-coalescer.ts +98 -0
  457. package/src/utils/fingerprint.ts +180 -0
  458. package/src/utils/frontmatter.ts +70 -0
  459. package/src/utils/fs-watch.ts +37 -0
  460. package/src/utils/gh-protocol.ts +556 -0
  461. package/src/utils/git.ts +260 -0
  462. package/src/utils/guards.ts +107 -0
  463. package/src/utils/ids.ts +24 -0
  464. package/src/utils/incremental-reader.ts +220 -0
  465. package/src/utils/internal-error.ts +10 -0
  466. package/src/utils/names.ts +36 -0
  467. package/src/utils/ndjson.ts +115 -0
  468. package/src/utils/paths.ts +227 -0
  469. package/src/utils/project-detector.ts +160 -0
  470. package/src/utils/redaction.ts +370 -0
  471. package/src/utils/resolve-shell.ts +36 -0
  472. package/src/utils/run-watcher-registry.ts +152 -0
  473. package/src/utils/safe-paths.ts +393 -0
  474. package/src/utils/scan-cache.ts +144 -0
  475. package/src/utils/session-utils.ts +110 -0
  476. package/src/utils/sleep.ts +54 -0
  477. package/src/utils/socket-path.ts +164 -0
  478. package/src/utils/sse-parser.ts +131 -0
  479. package/src/utils/task-name-generator.ts +337 -0
  480. package/src/utils/timings.ts +33 -0
  481. package/src/utils/token-counter.ts +200 -0
  482. package/src/utils/visual.ts +207 -0
  483. package/src/workflows/cost-estimator.ts +34 -0
  484. package/src/workflows/discover-workflows.ts +308 -0
  485. package/src/workflows/intermediate-store.ts +166 -0
  486. package/src/workflows/preflight-validator.ts +168 -0
  487. package/src/workflows/topology-analyzer.ts +203 -0
  488. package/src/workflows/validate-workflow.ts +50 -0
  489. package/src/workflows/workflow-config.ts +75 -0
  490. package/src/workflows/workflow-serializer.ts +32 -0
  491. package/src/worktree/branch-freshness.ts +112 -0
  492. package/src/worktree/cleanup.ts +341 -0
  493. package/src/worktree/worktree-manager.ts +1204 -0
@@ -0,0 +1,393 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+
4
+ export function isSafePathId(value: string): boolean {
5
+ return /^[A-Za-z0-9_-]+$/.test(value);
6
+ }
7
+
8
+ export function assertSafePathId(kind: string, value: string): string {
9
+ if (!isSafePathId(value)) throw new Error(`Invalid ${kind}: ${value}`);
10
+ return value;
11
+ }
12
+
13
+ /**
14
+ * Resolve `targetPath` against `baseDir` and verify the result stays inside it.
15
+ *
16
+ * @deprecated This returns a non-canonical `resolved` path and only validates
17
+ * the final resolved location, so callers can follow symlink components
18
+ * (TOCTOU risk). Prefer {@link resolveRealContainedPath}, which performs an
19
+ * O_NOFOLLOW ancestor walk plus post-realpath revalidation. The internal
20
+ * callers in this module (resolveRealContainedPath / resolveContainedRelativePath)
21
+ * still use this for the initial containment check by design.
22
+ */
23
+ export function resolveContainedPath(baseDir: string, targetPath: string): string {
24
+ if (targetPath.includes("\0")) {
25
+ throw new Error(`Security: path contains null byte`);
26
+ }
27
+ const base = path.resolve(baseDir);
28
+ const resolved = path.isAbsolute(targetPath) ? path.resolve(targetPath) : path.resolve(base, targetPath);
29
+ // Normalize BOTH paths to canonical form on ALL platforms. This resolves
30
+ // symlinks so that a base and target that refer to the same physical dir
31
+ // via different paths (Windows 8.3 short-name; macOS /var → /private/var)
32
+ // compare equal. Without this, a legitimately-contained target under an
33
+ // OS-managed symlink is wrongly rejected as "outside" the base.
34
+ const baseNorm = resolveCanonicalPath(base);
35
+ const resolvedNorm = resolveCanonicalPath(resolved);
36
+ const relative =
37
+ process.platform === "win32"
38
+ ? path.relative(baseNorm.toLowerCase(), resolvedNorm.toLowerCase())
39
+ : path.relative(baseNorm, resolvedNorm);
40
+ if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`Path is outside ${baseDir}: ${targetPath}`);
41
+ return resolved;
42
+ }
43
+
44
+ /**
45
+ * Platform-agnostic canonical path resolution. Resolves symlinks (via
46
+ * realpathSync) to normalize paths so the same physical location always
47
+ * yields the same string regardless of how it was referenced.
48
+ *
49
+ * - Windows: delegates to resolveWindowsCanonical (canonical LONG-NAME form,
50
+ * resolving 8.3 short-name aliases like RUNNER~1 → runneradmin).
51
+ * - POSIX: uses realpathSync, which resolves system symlinks such as the
52
+ * macOS /var → /private/var mapping.
53
+ *
54
+ * For non-existent paths (write targets), walks up to the deepest existing
55
+ * ancestor and joins the remaining components, so the canonical prefix is
56
+ * still comparable.
57
+ */
58
+ function resolveCanonicalPath(p: string): string {
59
+ if (process.platform === "win32") return resolveWindowsCanonical(p);
60
+ try {
61
+ return fs.realpathSync(p);
62
+ } catch {
63
+ const parts: string[] = [];
64
+ let current = p;
65
+ while (current !== path.dirname(current)) {
66
+ try {
67
+ const real = fs.realpathSync(current);
68
+ let acc = real;
69
+ for (let i = parts.length - 1; i >= 0; i--) acc = path.join(acc, parts[i]);
70
+ return acc;
71
+ } catch {
72
+ /* keep walking up */
73
+ }
74
+ parts.push(path.basename(current));
75
+ current = path.dirname(current);
76
+ }
77
+ return p;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * On Windows, resolve a path to its canonical (long-name) form.
83
+ * Walks up ancestors until finding one that exists, then joins back down.
84
+ * This handles paths where intermediate directories don't exist yet but
85
+ * their ancestors do (and may use short-name aliases).
86
+ *
87
+ * Uses fs.realpathSync.native (canonical long-name form) as the primary
88
+ * resolver, falling back to non-native realpathSync if .native fails.
89
+ */
90
+ function resolveWindowsCanonical(p: string): string {
91
+ try {
92
+ // Use the NATIVE realpath to resolve to canonical LONG-NAME form.
93
+ // On Windows, fs.realpathSync.native resolves 8.3 short-name aliases
94
+ // (e.g. RUNNER~1) to long-name (runneradmin). This is essential for
95
+ // containment checks: base (from cwd, often long-name) and target
96
+ // (from os.tmpdir()/mkdtempSync, often short-name) must normalize to
97
+ // the SAME form or a contained target is wrongly rejected as "outside".
98
+ const real = fs.realpathSync.native(p);
99
+ // Guard against NTFS internal paths (e.g. C:\$Extend\$Deleted)
100
+ if (real.includes("$Extend") || real.includes("$Deleted")) throw new Error("NTFS internal path");
101
+ return real;
102
+ } catch {
103
+ // Fallback: try realpathSync (non-native) which may succeed where .native fails
104
+ try {
105
+ const real = fs.realpathSync(p);
106
+ return real;
107
+ } catch {
108
+ /* proceed to ancestor walk */
109
+ }
110
+ // Walk up to find the deepest existing ancestor
111
+ const parts: string[] = [];
112
+ let current = p;
113
+ while (current !== path.dirname(current)) {
114
+ try {
115
+ let real: string;
116
+ try {
117
+ real = fs.realpathSync.native(current);
118
+ } catch {
119
+ real = fs.realpathSync(current);
120
+ }
121
+ // Guard against NTFS internal paths
122
+ if (real.includes("$Extend") || real.includes("$Deleted")) throw new Error("NTFS internal path");
123
+ // Found existing ancestor — join with remaining parts in reverse order
124
+ // (parts were pushed bottom-up, so iterate from last to first)
125
+ for (let i = parts.length - 1; i >= 0; i--) {
126
+ real = path.join(real, parts[i]);
127
+ }
128
+ return real;
129
+ } catch {
130
+ /* keep walking */
131
+ }
132
+ parts.push(path.basename(current));
133
+ current = path.dirname(current);
134
+ }
135
+ // Couldn't resolve any ancestor — return original
136
+ return p;
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Resolve a target path to its real (symlink-resolved) absolute path while
142
+ * guaranteeing the result stays inside `baseDir`.
143
+ *
144
+ * ## Security model — asymmetric ancestor handling
145
+ *
146
+ * `baseDir` and `targetPath` are validated with different policies because
147
+ * they play different roles:
148
+ *
149
+ * - **baseDir** (the container): all ancestors MUST exist and MUST NOT be
150
+ * symlinks. We refuse to operate if any component is missing or symlinked,
151
+ * because a symlinked container could point the caller outside the
152
+ * intended trust boundary (e.g. `/var/run -> /run` resolving into an
153
+ * attacker-controlled directory).
154
+ *
155
+ * - **targetPath** (the contained file): the FINAL component may be
156
+ * non-existent (for write operations creating a new file) and EXISTING
157
+ * ancestors of the target may also be missing. We DO require that any
158
+ * ancestor that DOES exist must not be a symlink — an attacker who can
159
+ * create a directory in the container must not be able to redirect the
160
+ * file being created.
161
+ *
162
+ * This asymmetry is intentional: callers that need to create a new file
163
+ * pass a non-existent targetPath. Callers that operate on an existing file
164
+ * get full symlink protection. Callers MUST NOT pass a symlinked
165
+ * intermediate component; if you need to, use `resolveContainedPath`
166
+ * instead (which only checks the resolved path, not the chain).
167
+ *
168
+ * Throws on:
169
+ * - null byte in targetPath
170
+ * - targetPath resolves outside baseDir
171
+ * - any existing ancestor (base or target) is a symlink
172
+ * - baseDir itself does not exist
173
+ *
174
+ * Returns the resolved real path on success, or the resolved (but not
175
+ * realpathed) path when the target does not exist yet.
176
+ *
177
+ * NOTE: There is a race condition window between validation and use where an
178
+ * attacker could create a directory component after validation but before the
179
+ * file is created. Callers MUST create parent directories atomically
180
+ * (e.g., mkdirSync with { recursive: true }) and use O_CREAT | O_NOFOLLOW | O_EXCL
181
+ * for atomic file creation, as atomicWriteFile does. This ensures the entire
182
+ * operation is atomic and prevents TOCTOU attacks.
183
+ */
184
+ export function resolveRealContainedPath(baseDir: string, targetPath: string): string {
185
+ if (targetPath.includes("\0")) {
186
+ throw new Error(`Security: path contains null byte`);
187
+ }
188
+ const resolved = resolveContainedPath(baseDir, targetPath);
189
+
190
+ // Open baseDir with O_NOFOLLOW to atomically validate no symlinks in the path.
191
+ // O_NOFOLLOW makes the open fail with ELOOP if any path component is a symlink.
192
+ let baseFd: number | undefined;
193
+ try {
194
+ baseFd = fs.openSync(baseDir, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
195
+ } catch (error) {
196
+ const errCode = (error as NodeJS.ErrnoException).code;
197
+ if (errCode === "ENOENT") {
198
+ // baseDir doesn't exist yet — create it and retry
199
+ try {
200
+ fs.mkdirSync(baseDir, { recursive: true });
201
+ baseFd = fs.openSync(baseDir, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
202
+ } catch (retryError) {
203
+ throw new Error(
204
+ `Cannot open base directory ${baseDir}: ${retryError instanceof Error ? retryError.message : String(retryError)}`,
205
+ );
206
+ }
207
+ } else if (errCode === "ELOOP") {
208
+ // On macOS, system directories like /var → /private/var contain symlinks.
209
+ // If baseDir is under such a path, resolve through realpath and retry.
210
+ if (process.platform === "darwin") {
211
+ try {
212
+ const realBaseDir = fs.realpathSync(baseDir);
213
+ if (realBaseDir !== baseDir) {
214
+ baseFd = fs.openSync(realBaseDir, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
215
+ baseDir = realBaseDir; // update for later use
216
+ // Fall through to fstatSync below
217
+ }
218
+ } catch {
219
+ /* throw original */
220
+ }
221
+ }
222
+ if (baseFd === undefined) throw new Error("Refusing to resolve: baseDir path contains a symlink: " + baseDir);
223
+ } else {
224
+ throw new Error(`Cannot open base directory ${baseDir}: ${error instanceof Error ? error.message : String(error)}`);
225
+ }
226
+ }
227
+ let realBase: string;
228
+ try {
229
+ const stat = fs.fstatSync(baseFd);
230
+ if (!stat.isDirectory()) throw new Error(`baseDir ${baseDir} is not a directory`);
231
+ // Use regular realpathSync (not .native) to preserve input path form.
232
+ // On Windows CI, .native always returns long-name form (runneradmin)
233
+ // while non-native preserves short-name (RUNNER~1). Using non-native
234
+ // ensures the result matches what the caller passed in.
235
+ realBase = fs.realpathSync(baseDir);
236
+ } catch (error) {
237
+ // baseDir MUST exist and be resolvable for the containment guarantee to hold.
238
+ // Callers creating new directories must create baseDir atomically (e.g.,
239
+ // mkdirSync with { recursive: true }) BEFORE calling this function, and use
240
+ // O_NOFOLLOW|O_CREAT|O_EXCL for the actual file creation to ensure atomicity.
241
+ // The safe-paths validation and the file creation are two separate operations
242
+ // with a gap between them — callers must close this gap with atomic primitives.
243
+ throw new Error(`Cannot resolve real path of base directory ${baseDir}: ${error instanceof Error ? error.message : String(error)}`);
244
+ } finally {
245
+ fs.closeSync(baseFd);
246
+ }
247
+
248
+ // Walk the ancestor chain of the resolved target path, using O_NOFOLLOW
249
+ // on each ancestor to atomically validate none are symlinks.
250
+ const O_NOFOLLOW = fs.constants.O_NOFOLLOW;
251
+ const O_RDONLY = fs.constants.O_RDONLY;
252
+ const resolvedParts = resolved.split(path.sep);
253
+ let resolvedAccumulated = "";
254
+ if (resolvedParts[0] === "") resolvedAccumulated = "/"; // Unix root
255
+ for (let i = 1; i < resolvedParts.length; i++) {
256
+ if (resolvedParts[i] === "") continue;
257
+ resolvedAccumulated = path.join(resolvedAccumulated, resolvedParts[i]);
258
+ try {
259
+ const fd = fs.openSync(resolvedAccumulated, O_RDONLY | O_NOFOLLOW);
260
+ fs.closeSync(fd);
261
+ } catch (error) {
262
+ if ((error as NodeJS.ErrnoException).code === "ELOOP") {
263
+ // On macOS, /var → /private/var, /tmp → /private/tmp, /etc → /private/etc
264
+ // are system symlinks managed by the OS. Allow them.
265
+ if (process.platform === "darwin") {
266
+ const resolvedSymlink = resolvedAccumulated;
267
+ const knownDarwinSymlinks = ["/var", "/tmp", "/etc", "/private/var", "/private/tmp", "/private/etc"];
268
+ // Use prefix match to handle subdirectories like /var/folders/...
269
+ if (knownDarwinSymlinks.some((s) => resolvedSymlink === s || resolvedSymlink.startsWith(s + "/"))) continue;
270
+ }
271
+ throw new Error("Refusing to resolve: target path ancestor is a symlink: " + resolvedAccumulated);
272
+ }
273
+ // EPERM on Windows when opening a directory — skip validation
274
+ if ((error as NodeJS.ErrnoException).code === "EPERM" && process.platform === "win32") continue;
275
+ // ENOENT means component doesn't exist — that's OK. Only existing symlinks
276
+ // are a security risk (symlinks to attacker-controlled targets). Non-existent
277
+ // paths can be created by the caller and don't pose a symlink risk.
278
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
279
+ // For the final component (target itself), ENOENT is expected for non-existent targets.
280
+ if (i === resolvedParts.length - 1) continue;
281
+ }
282
+ }
283
+
284
+ // Open the target with O_NOFOLLOW to catch any symlinks.
285
+ // ENOENT is acceptable for write operations — the file may not exist yet.
286
+ let targetFd: number;
287
+ try {
288
+ targetFd = fs.openSync(resolved, O_RDONLY | O_NOFOLLOW);
289
+ } catch (error) {
290
+ if ((error as NodeJS.ErrnoException).code === "ELOOP")
291
+ throw new Error("Refusing to resolve: target path is a symlink: " + resolved);
292
+ // EPERM on Windows when opening a directory — treat as non-existent
293
+ if ((error as NodeJS.ErrnoException).code === "EPERM" && process.platform === "win32") return resolved;
294
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
295
+ // Target doesn't exist yet — that's OK for write operations.
296
+ // All ancestors have been validated above (no symlinks).
297
+ // The caller will create the file with atomic primitives.
298
+ return resolved;
299
+ }
300
+ throw new Error(`Cannot open ${resolved}: ${error instanceof Error ? error.message : String(error)}`);
301
+ }
302
+
303
+ let realTarget: string;
304
+ try {
305
+ // Use regular realpathSync (not .native) to preserve input path form.
306
+ realTarget = fs.realpathSync(resolved);
307
+ } catch (targetError) {
308
+ if ((targetError as NodeJS.ErrnoException).code === "ENOENT") {
309
+ // Target doesn't exist yet — this is OK for write operations.
310
+ // Return the resolved path so the caller can create the file.
311
+ // We already validated all ancestors are not symlinks above.
312
+ return resolved;
313
+ }
314
+ throw new Error(
315
+ `Cannot resolve real path of ${resolved}: ${targetError instanceof Error ? targetError.message : String(targetError)}`,
316
+ );
317
+ } finally {
318
+ fs.closeSync(targetFd);
319
+ }
320
+
321
+ // Re-validate the ancestor chain of the resolved path to catch any TOCTOU
322
+ // races that occurred between the initial O_NOFOLLOW validation and the
323
+ // realpathSync call. An attacker could have replaced a validated ancestor
324
+ // with a symlink during this window.
325
+ //
326
+ // Skip the final path component (realTarget itself) — we just successfully
327
+ // realpathSync'd it, so it exists. Re-validating it can spuriously fail on
328
+ // Windows where the resolved path uses short-name (8.3) form that
329
+ // openSync cannot reopen, or where the realpathSync result differs in
330
+ // case/separator form from the original.
331
+ //
332
+ // Walk via path.dirname which is portable across all platforms and
333
+ // correctly handles extended-length (\\?\), UNC (\\server\share), and
334
+ // short-name paths on Windows without manual parsing.
335
+ let ancestor = path.dirname(realTarget);
336
+ while (ancestor && ancestor !== path.dirname(ancestor)) {
337
+ try {
338
+ const fd = fs.openSync(ancestor, O_RDONLY | O_NOFOLLOW);
339
+ fs.closeSync(fd);
340
+ } catch (error) {
341
+ const errCode = (error as NodeJS.ErrnoException).code;
342
+ if (errCode === "ELOOP") throw new Error("Refusing to resolve: TOCTOU race detected, path became a symlink: " + ancestor);
343
+ // Windows: EPERM can occur when opening system directories (e.g. C:\)
344
+ // or NTFS internal paths ($Extend/$Deleted). Skip and continue walking.
345
+ if (process.platform === "win32" && errCode === "EPERM") {
346
+ if (ancestor.includes("$Extend") || ancestor.includes("$Deleted")) {
347
+ // NTFS internal path — stop walking, we've reached the filesystem root
348
+ break;
349
+ }
350
+ // System directory — continue walking up
351
+ ancestor = path.dirname(ancestor);
352
+ continue;
353
+ }
354
+ if (errCode !== "ENOENT") throw error;
355
+ // ENOENT on an ancestor of realTarget after realpathSync is concerning
356
+ // — the path existed when we validated it but now doesn't. This could
357
+ // indicate a race or attack. For safety, treat this as an error.
358
+ throw new Error(
359
+ `Cannot validate resolved path: ${ancestor} disappeared after realpathSync: ${error instanceof Error ? error.message : String(error)}`,
360
+ );
361
+ }
362
+ ancestor = path.dirname(ancestor);
363
+ }
364
+
365
+ // Verify the resolved real path is still within baseDir.
366
+ // On Windows, realpathSync.native may return different short/long-name forms
367
+ // for the same physical directory depending on how the path was opened.
368
+ // Use resolveWindowsCanonical (same as resolveContainedPath) to normalize
369
+ // both paths consistently before comparison.
370
+ if (process.platform === "win32") {
371
+ const normBase = resolveWindowsCanonical(realBase).replace(/\\/g, "/").toLowerCase();
372
+ const normTarget = resolveWindowsCanonical(realTarget).replace(/\\/g, "/").toLowerCase();
373
+ if (!normTarget.startsWith(normBase + "/") && normBase !== normTarget) {
374
+ throw new Error(`Path is outside ${baseDir}: ${targetPath}`);
375
+ }
376
+ } else {
377
+ const relative = path.relative(realBase, realTarget);
378
+ if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`Path is outside ${baseDir}: ${targetPath}`);
379
+ }
380
+ return realTarget;
381
+ }
382
+
383
+ export function resolveContainedRelativePath(baseDir: string, relativePath: string, kind = "path"): string {
384
+ if (relativePath.includes("\0")) {
385
+ throw new Error(`Security: path contains null byte: ${kind}`);
386
+ }
387
+ const normalized = relativePath.replace(/\\/g, "/").replace(/^\.\/+/, "");
388
+ // Detect Windows absolute paths (C:\, \\server\share) that path.isAbsolute may miss after normalization
389
+ if (/^[A-Za-z]:/.test(normalized)) throw new Error(`Invalid ${kind}: ${relativePath}`);
390
+ if (!normalized || normalized.split("/").some((segment) => segment === "..") || path.isAbsolute(normalized))
391
+ throw new Error(`Invalid ${kind}: ${relativePath}`);
392
+ return resolveContainedPath(baseDir, path.resolve(baseDir, normalized));
393
+ }
@@ -0,0 +1,144 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+
4
+ export interface ScanEntry {
5
+ /** Unique key for this entry (e.g., runId, artifactPath) */
6
+ key: string;
7
+ /** Filesystem path */
8
+ path: string;
9
+ /** Raw content (parsed JSON or text) */
10
+ raw: unknown;
11
+ /** File modification time */
12
+ mtimeMs: number;
13
+ /** File size in bytes */
14
+ sizeBytes: number;
15
+ /** When this entry was loaded */
16
+ loadedAtMs: number;
17
+ }
18
+
19
+ export interface ScanCacheOptions {
20
+ /** TTL in ms for cached entries. Default 1000. */
21
+ ttlMs?: number;
22
+ /** Maximum number of entries. Default 100. */
23
+ maxEntries?: number;
24
+ }
25
+
26
+ interface CacheBucket {
27
+ entries: Map<string, ScanEntry>;
28
+ expireAtMs: number;
29
+ }
30
+
31
+ /**
32
+ * Shared raw scan-entry cache for runs, artifacts, mailbox, transcripts.
33
+ * Provides deterministic sort order and invalidation on mutation.
34
+ */
35
+ export class SharedScanCache {
36
+ #buckets = new Map<string, CacheBucket>();
37
+ #ttlMs: number;
38
+ #maxEntries: number;
39
+ #now: () => number;
40
+
41
+ constructor(options: ScanCacheOptions = {}) {
42
+ this.#ttlMs = options.ttlMs ?? 1000;
43
+ this.#maxEntries = options.maxEntries ?? 100;
44
+ this.#now = () => Date.now();
45
+ }
46
+
47
+ /** Get a cached entry by bucket and key. Returns undefined if not cached or expired. */
48
+ get(bucket: string, key: string): ScanEntry | undefined {
49
+ const b = this.#buckets.get(bucket);
50
+ if (!b) return undefined;
51
+ if (this.#now() > b.expireAtMs) {
52
+ this.#buckets.delete(bucket);
53
+ return undefined;
54
+ }
55
+ return b.entries.get(key);
56
+ }
57
+
58
+ /** Get all entries in a bucket. Returns empty array if expired or missing. */
59
+ list(bucket: string): ScanEntry[] {
60
+ const b = this.#buckets.get(bucket);
61
+ if (!b) return [];
62
+ if (this.#now() > b.expireAtMs) {
63
+ this.#buckets.delete(bucket);
64
+ return [];
65
+ }
66
+ return [...b.entries.values()].sort((a, b) => a.key.localeCompare(b.key));
67
+ }
68
+
69
+ /** Set an entry in a bucket. */
70
+ set(bucket: string, entry: ScanEntry): void {
71
+ let b = this.#buckets.get(bucket);
72
+ if (!b || this.#now() > b.expireAtMs) {
73
+ b = { entries: new Map(), expireAtMs: this.#now() + this.#ttlMs };
74
+ this.#buckets.set(bucket, b);
75
+ }
76
+ if (b.entries.size >= this.#maxEntries) {
77
+ // Evict oldest entry
78
+ const firstKey = b.entries.keys().next().value;
79
+ if (firstKey !== undefined) b.entries.delete(firstKey);
80
+ }
81
+ b.entries.set(entry.key, entry);
82
+ }
83
+
84
+ /** Invalidate a specific key in a bucket. */
85
+ invalidate(bucket: string, key: string): void {
86
+ const b = this.#buckets.get(bucket);
87
+ if (b) b.entries.delete(key);
88
+ }
89
+
90
+ /** Invalidate an entire bucket. */
91
+ invalidateBucket(bucket: string): void {
92
+ this.#buckets.delete(bucket);
93
+ }
94
+
95
+ /** Invalidate all buckets. */
96
+ clear(): void {
97
+ this.#buckets.clear();
98
+ }
99
+
100
+ /** Read a file, parse if JSON, and cache the result. */
101
+ readAndCache(bucket: string, key: string, filePath: string, parseJson = true): ScanEntry | undefined {
102
+ try {
103
+ if (!fs.existsSync(filePath)) return undefined;
104
+ const stat = fs.statSync(filePath);
105
+ const cached = this.get(bucket, key);
106
+ if (cached && cached.mtimeMs >= stat.mtimeMs && cached.sizeBytes === stat.size) return cached;
107
+ const content = fs.readFileSync(filePath, "utf-8");
108
+ const raw = parseJson ? JSON.parse(content) : content;
109
+ const entry: ScanEntry = {
110
+ key,
111
+ path: filePath,
112
+ raw,
113
+ mtimeMs: stat.mtimeMs,
114
+ sizeBytes: stat.size,
115
+ loadedAtMs: this.#now(),
116
+ };
117
+ this.set(bucket, entry);
118
+ return entry;
119
+ } catch {
120
+ return undefined;
121
+ }
122
+ }
123
+
124
+ /** Read a directory and cache entries for each file. */
125
+ scanAndCache(bucket: string, dirPath: string, parseJson = true): ScanEntry[] {
126
+ try {
127
+ if (!fs.existsSync(dirPath)) return [];
128
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
129
+ const results: ScanEntry[] = [];
130
+ for (const entry of entries) {
131
+ if (!entry.isFile()) continue;
132
+ const filePath = path.join(dirPath, entry.name);
133
+ const cached = this.readAndCache(bucket, entry.name, filePath, parseJson);
134
+ if (cached) results.push(cached);
135
+ }
136
+ return results.sort((a, b) => a.key.localeCompare(b.key));
137
+ } catch {
138
+ return [];
139
+ }
140
+ }
141
+ }
142
+
143
+ /** Global shared scan cache instance. */
144
+ export const sharedScanCache = new SharedScanCache();
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Session ID utilities for pi-crew / pi session alignment.
3
+ *
4
+ * pi's session IDs use the format:
5
+ * ^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$
6
+ *
7
+ * This module provides utilities to generate valid pi session IDs
8
+ * that align with pi-crew run IDs for easy cross-referencing.
9
+ */
10
+
11
+ /**
12
+ * Validate session ID format per pi's requirements.
13
+ * Format: ^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$
14
+ */
15
+ export function assertValidSessionId(id: string): void {
16
+ if (!id || !/^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/.test(id)) {
17
+ throw new Error(`Invalid session id: must be non-empty, alphanumeric with '-', '_', '.' and start/end with alphanumeric`);
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Convert a pi-crew run ID to a valid pi session ID.
23
+ *
24
+ * - Strips non-alphanumeric characters
25
+ * - Lowercases
26
+ * - Prefixes with "crew-"
27
+ * - Truncates to 16 chars for safety
28
+ *
29
+ * @param runId - The pi-crew run ID (e.g., "team_20260528133725_02e05cc5480d0175")
30
+ * @returns Valid pi session ID (e.g., "crew-team20260528133")
31
+ */
32
+ export function toPiSessionId(runId: string): string {
33
+ // Strip non-alphanumeric, lowercase, prefix with "crew-"
34
+ const sanitized = runId.replace(/[^A-Za-z0-9]/g, "").toLowerCase();
35
+ return `crew-${sanitized.slice(0, 16)}`;
36
+ }
37
+
38
+ /**
39
+ * Validate and convert a run ID to a pi session ID.
40
+ * Returns the session ID if valid, or undefined if conversion would produce invalid ID.
41
+ */
42
+ export function safeToPiSessionId(runId: string): string | undefined {
43
+ try {
44
+ const sessionId = toPiSessionId(runId);
45
+ assertValidSessionId(sessionId);
46
+ return sessionId;
47
+ } catch {
48
+ return undefined;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Extract the current Pi session id from an ExtensionContext.
54
+ *
55
+ * `ExtensionContext` does not declare `sessionId` in its type, but the runtime
56
+ * attaches it as an own property. We read it via `getOwnPropertyDescriptor`
57
+ * to safely bypass any Proxy traps, then validate it as a non-empty string.
58
+ *
59
+ * This is the canonical accessor — every site that filters the SHARED
60
+ * per-project `.crew/state/` tree down to the current session MUST use this,
61
+ * otherwise cross-session state leaks (e.g. compaction-guard resuming another
62
+ * session's runs, ambient-status injecting another session's runs).
63
+ *
64
+ * Returns undefined when the session id is absent or unparseable — callers
65
+ * must decide whether to treat that as "no filter" (back-compat) or "no runs".
66
+ */
67
+ export function extractSessionId(ctx: unknown): string | undefined {
68
+ if (typeof ctx !== "object" || ctx === null) return undefined;
69
+ let raw: unknown;
70
+ try {
71
+ raw = Object.getOwnPropertyDescriptor(ctx, "sessionId")?.value;
72
+ } catch {
73
+ // Defensive: a hostile Proxy or exotic object may trap descriptor
74
+ // access. Real Pi ExtensionContext objects are plain, so this is
75
+ // only hit by adversarial/degenerate inputs — treat as no session id.
76
+ return undefined;
77
+ }
78
+ if (typeof raw !== "string" || raw.length === 0) return undefined;
79
+ return raw;
80
+ }
81
+
82
+ /**
83
+ * Broker-only session id extractor.
84
+ *
85
+ * Pi's `ExtensionContext` does NOT expose a top-level `sessionId` property on
86
+ * its public surface — the id is reachable via `ctx.sessionManager.getSessionId()`.
87
+ * This helper is only called from `installCrewBrokerLifecycleController.setSessionId`
88
+ * (once per session_start), so the extra method invocation is safe here. It is
89
+ * INTENTIONALLY a separate function from `extractSessionId`, which is called on
90
+ * every `context` event (before every LLM call) from `context-status-injection.ts`
91
+ * — extending that hot path with method calls was observed to freeze the TUI
92
+ * (dashboard opens but is unresponsive, footer does not render) during smoke
93
+ * testing, so it stays on the trivial property lookup.
94
+ *
95
+ * Tries the sessionManager path first, then falls back to a direct
96
+ * `ctx.sessionId` for test mock compatibility.
97
+ */
98
+ export function extractBrokerSessionId(ctx: unknown): string | undefined {
99
+ if (typeof ctx !== "object" || ctx === null) return undefined;
100
+ try {
101
+ const sm = (ctx as { sessionManager?: { getSessionId?: () => unknown } }).sessionManager;
102
+ const viaManager = sm?.getSessionId?.();
103
+ if (typeof viaManager === "string" && viaManager.length > 0) return viaManager;
104
+ const direct = Object.getOwnPropertyDescriptor(ctx, "sessionId")?.value;
105
+ if (typeof direct === "string" && direct.length > 0) return direct;
106
+ return undefined;
107
+ } catch {
108
+ return undefined;
109
+ }
110
+ }