pi-crew 0.2.2 → 0.2.4

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 (354) hide show
  1. package/AGENTS.md +57 -32
  2. package/CHANGELOG.md +466 -413
  3. package/LICENSE +21 -21
  4. package/NOTICE.md +16 -16
  5. package/README.md +323 -323
  6. package/docs/FEATURE_INTAKE.md +126 -0
  7. package/docs/HARNESS.md +86 -0
  8. package/docs/HARNESS_BACKLOG.md +41 -0
  9. package/docs/TEST_MATRIX.md +49 -0
  10. package/docs/actions-reference.md +595 -595
  11. package/docs/architecture.md +180 -180
  12. package/docs/code-review-2026-05-11.md +592 -0
  13. package/docs/commands-reference.md +347 -347
  14. package/docs/comparison-pi-subagents-vs-pi-crew.md +303 -0
  15. package/docs/decisions/0001-durable-state.md +41 -0
  16. package/docs/decisions/0002-child-process-for-async.md +42 -0
  17. package/docs/decisions/0003-depth-guard.md +36 -0
  18. package/docs/decisions/0004-execfile-over-exec.md +34 -0
  19. package/docs/decisions/0005-no-parameter-properties.md +49 -0
  20. package/docs/decisions/0006-publish-bundled-esm.md +63 -0
  21. package/docs/decisions/0007-active-run-binary-index.md +54 -0
  22. package/docs/decisions/0008-child-pi-warm-pool.md +61 -0
  23. package/docs/decisions/README.md +23 -0
  24. package/docs/followup-plan-2026-05-12.md +463 -0
  25. package/docs/followup-review-2026-05-12.md +297 -0
  26. package/docs/followup-review-round3-2026-05-12.md +342 -0
  27. package/docs/followup-review-round4-2026-05-13.md +107 -0
  28. package/docs/implementation-plan-top3.md +333 -0
  29. package/docs/live-mailbox-runtime.md +36 -36
  30. package/docs/next-upgrade-roadmap.md +808 -808
  31. package/docs/oh-my-pi-research.md +509 -0
  32. package/docs/perf/baseline-2026-05.md +113 -0
  33. package/docs/perf/final-report-2026-05.md +206 -0
  34. package/docs/perf/sprint-1-report.md +71 -0
  35. package/docs/perf/sprint-2-report.md +81 -0
  36. package/docs/perf/sprint-2.5-report.md +53 -0
  37. package/docs/perf/sprint-3-report.md +36 -0
  38. package/docs/perf/sprint-4-report.md +47 -0
  39. package/docs/perf/sprint-5-report.md +51 -0
  40. package/docs/perf/sprint-6-report.md +94 -0
  41. package/docs/perf/sprint-7-report.md +74 -0
  42. package/docs/perf/upgrade-plan-2026-05.md +147 -0
  43. package/docs/pi-subagents3-deep-analysis.md +508 -0
  44. package/docs/product/README.md +31 -0
  45. package/docs/product/platform.md +27 -0
  46. package/docs/product/runtime-safety.md +37 -0
  47. package/docs/product/team-run.md +39 -0
  48. package/docs/product/team-tool.md +37 -0
  49. package/docs/publishing.md +65 -65
  50. package/docs/resource-formats.md +134 -134
  51. package/docs/runtime-analysis-child-vs-live.md +171 -0
  52. package/docs/runtime-flow.md +148 -148
  53. package/docs/runtime-migration-in-process-analysis.md +250 -0
  54. package/docs/stories/README.md +30 -0
  55. package/docs/stories/backlog.md +36 -0
  56. package/docs/templates/decision.md +27 -0
  57. package/docs/templates/story.md +44 -0
  58. package/docs/templates/validation-report.md +32 -0
  59. package/docs/usage.md +238 -238
  60. package/index.ts +7 -6
  61. package/install.mjs +65 -65
  62. package/package.json +107 -99
  63. package/schema.json +222 -222
  64. package/skills/child-pi-spawning/SKILL.md +213 -0
  65. package/skills/context-artifact-hygiene/SKILL.md +32 -0
  66. package/skills/event-log-tracing/SKILL.md +299 -0
  67. package/skills/git-master/SKILL.md +225 -24
  68. package/skills/live-agent-lifecycle/SKILL.md +192 -0
  69. package/skills/mailbox-interactive/SKILL.md +300 -19
  70. package/skills/model-routing-context/SKILL.md +94 -0
  71. package/skills/multi-perspective-review/SKILL.md +88 -0
  72. package/skills/read-only-explorer/SKILL.md +250 -26
  73. package/skills/safe-bash/SKILL.md +307 -21
  74. package/skills/verification-before-done/SKILL.md +11 -2
  75. package/skills/widget-rendering/SKILL.md +258 -0
  76. package/skills/workspace-isolation/SKILL.md +202 -0
  77. package/skills/worktree-isolation/SKILL.md +202 -18
  78. package/src/adapters/claude-adapter.ts +25 -25
  79. package/src/adapters/codex-adapter.ts +21 -21
  80. package/src/adapters/cursor-adapter.ts +17 -17
  81. package/src/adapters/export-util.ts +137 -137
  82. package/src/adapters/index.ts +15 -15
  83. package/src/adapters/registry.ts +18 -18
  84. package/src/adapters/types.ts +23 -23
  85. package/src/agents/agent-config.ts +38 -38
  86. package/src/agents/agent-serializer.ts +38 -38
  87. package/src/agents/discover-agents.ts +121 -118
  88. package/src/config/config.ts +740 -858
  89. package/src/config/defaults.ts +96 -96
  90. package/src/config/drift-detector.ts +211 -211
  91. package/src/config/markers.ts +327 -327
  92. package/src/config/resilient-parser.ts +109 -108
  93. package/src/config/suggestions.ts +74 -74
  94. package/src/config/types.ts +199 -0
  95. package/src/extension/async-notifier.ts +123 -89
  96. package/src/extension/autonomous-policy.ts +169 -169
  97. package/src/extension/cross-extension-rpc.ts +104 -103
  98. package/src/extension/help.ts +47 -47
  99. package/src/extension/import-index.ts +69 -69
  100. package/src/extension/management.ts +395 -382
  101. package/src/extension/notification-router.ts +116 -116
  102. package/src/extension/notification-sink.ts +51 -51
  103. package/src/extension/project-init.ts +168 -168
  104. package/src/extension/register.ts +859 -668
  105. package/src/extension/registration/artifact-cleanup.ts +15 -15
  106. package/src/extension/registration/command-utils.ts +54 -54
  107. package/src/extension/registration/commands.ts +559 -452
  108. package/src/extension/registration/compaction-guard.ts +125 -125
  109. package/src/extension/registration/subagent-helpers.ts +102 -102
  110. package/src/extension/registration/subagent-tools.ts +220 -158
  111. package/src/extension/registration/team-tool.ts +159 -98
  112. package/src/extension/registration/viewers.ts +29 -0
  113. package/src/extension/result-watcher.ts +128 -128
  114. package/src/extension/run-bundle-schema.ts +89 -89
  115. package/src/extension/run-export.ts +73 -73
  116. package/src/extension/run-import.ts +84 -84
  117. package/src/extension/run-index.ts +94 -94
  118. package/src/extension/run-maintenance.ts +142 -142
  119. package/src/extension/session-summary.ts +8 -8
  120. package/src/extension/team-manager-command.ts +96 -95
  121. package/src/extension/team-recommendation.ts +188 -188
  122. package/src/extension/team-tool/api.ts +5 -2
  123. package/src/extension/team-tool/cancel.ts +224 -209
  124. package/src/extension/team-tool/config-patch.ts +36 -36
  125. package/src/extension/team-tool/context.ts +60 -60
  126. package/src/extension/team-tool/doctor.ts +242 -242
  127. package/src/extension/team-tool/handle-settings.ts +421 -195
  128. package/src/extension/team-tool/inspect.ts +41 -41
  129. package/src/extension/team-tool/lifecycle-actions.ts +139 -139
  130. package/src/extension/team-tool/parallel-dispatch.ts +156 -156
  131. package/src/extension/team-tool/plan.ts +19 -19
  132. package/src/extension/team-tool/respond.ts +112 -111
  133. package/src/extension/team-tool/run.ts +246 -228
  134. package/src/extension/team-tool/status.ts +110 -110
  135. package/src/extension/team-tool-types.ts +13 -13
  136. package/src/extension/team-tool.ts +16 -4
  137. package/src/extension/tool-result.ts +16 -16
  138. package/src/extension/validate-resources.ts +77 -77
  139. package/src/hooks/registry.ts +61 -61
  140. package/src/hooks/types.ts +40 -40
  141. package/src/i18n.ts +184 -184
  142. package/src/observability/correlation.ts +35 -35
  143. package/src/observability/event-to-metric.ts +68 -68
  144. package/src/observability/exporters/adapter.ts +30 -30
  145. package/src/observability/exporters/otlp-exporter.ts +106 -92
  146. package/src/observability/exporters/prometheus-exporter.ts +54 -54
  147. package/src/observability/metric-registry.ts +87 -87
  148. package/src/observability/metric-retention.ts +54 -54
  149. package/src/observability/metric-sink.ts +81 -56
  150. package/src/observability/metrics-primitives.ts +167 -167
  151. package/src/prompt/prompt-runtime.ts +72 -72
  152. package/src/runtime/adaptive-plan.ts +338 -0
  153. package/src/runtime/agent-control.ts +169 -169
  154. package/src/runtime/agent-memory.ts +72 -72
  155. package/src/runtime/agent-observability.ts +114 -114
  156. package/src/runtime/async-marker.ts +26 -26
  157. package/src/runtime/async-runner.ts +153 -79
  158. package/src/runtime/attention-events.ts +28 -28
  159. package/src/runtime/auto-resume.ts +100 -100
  160. package/src/runtime/background-runner.ts +122 -88
  161. package/src/runtime/cancellation.ts +61 -61
  162. package/src/runtime/capability-inventory.ts +116 -116
  163. package/src/runtime/child-pi-pool.ts +68 -0
  164. package/src/runtime/child-pi.ts +541 -463
  165. package/src/runtime/code-summary.ts +247 -247
  166. package/src/runtime/compaction-summary.ts +271 -271
  167. package/src/runtime/concurrency.ts +58 -58
  168. package/src/runtime/crash-recovery.ts +317 -301
  169. package/src/runtime/crew-agent-records.ts +379 -281
  170. package/src/runtime/crew-agent-runtime.ts +60 -60
  171. package/src/runtime/cross-extension-rpc.ts +72 -0
  172. package/src/runtime/custom-tools/irc-tool.ts +201 -201
  173. package/src/runtime/custom-tools/submit-result-tool.ts +90 -90
  174. package/src/runtime/deadletter.ts +47 -47
  175. package/src/runtime/delivery-coordinator.ts +176 -176
  176. package/src/runtime/delta-conflict.ts +360 -360
  177. package/src/runtime/diagnostic-export.ts +102 -102
  178. package/src/runtime/direct-run.ts +35 -35
  179. package/src/runtime/effectiveness.ts +82 -81
  180. package/src/runtime/errors/crew-errors.ts +166 -0
  181. package/src/runtime/event-stream-bridge.ts +92 -92
  182. package/src/runtime/foreground-control.ts +82 -82
  183. package/src/runtime/green-contract.ts +46 -46
  184. package/src/runtime/group-join.ts +234 -106
  185. package/src/runtime/heartbeat-watcher.ts +145 -124
  186. package/src/runtime/iteration-hooks.ts +267 -264
  187. package/src/runtime/live-agent-control.ts +88 -88
  188. package/src/runtime/live-agent-manager.ts +377 -179
  189. package/src/runtime/live-control-realtime.ts +36 -36
  190. package/src/runtime/live-session-runtime.ts +676 -599
  191. package/src/runtime/loop-gates.ts +129 -129
  192. package/src/runtime/manifest-cache.ts +263 -263
  193. package/src/runtime/mcp-proxy.ts +113 -113
  194. package/src/runtime/metric-parser.ts +40 -40
  195. package/src/runtime/model-fallback.ts +282 -274
  196. package/src/runtime/model-resolver.ts +118 -0
  197. package/src/runtime/output-validator.ts +187 -187
  198. package/src/runtime/overflow-recovery.ts +175 -175
  199. package/src/runtime/parallel-research.ts +44 -44
  200. package/src/runtime/parallel-utils.ts +156 -156
  201. package/src/runtime/parent-guard.ts +80 -80
  202. package/src/runtime/phase-progress.ts +217 -217
  203. package/src/runtime/pi-args.ts +165 -165
  204. package/src/runtime/pi-json-output.ts +111 -111
  205. package/src/runtime/pi-spawn.ts +167 -167
  206. package/src/runtime/policy-engine.ts +79 -79
  207. package/src/runtime/post-checks.ts +125 -122
  208. package/src/runtime/post-exit-stdio-guard.ts +86 -86
  209. package/src/runtime/process-status.ts +97 -73
  210. package/src/runtime/progress-event-coalescer.ts +43 -43
  211. package/src/runtime/recovery-recipes.ts +74 -74
  212. package/src/runtime/retry-executor.ts +81 -81
  213. package/src/runtime/role-permission.ts +39 -39
  214. package/src/runtime/run-tracker.ts +99 -0
  215. package/src/runtime/runtime-policy.ts +21 -0
  216. package/src/runtime/runtime-resolver.ts +94 -90
  217. package/src/runtime/scheduler.ts +294 -0
  218. package/src/runtime/semaphore.ts +131 -131
  219. package/src/runtime/sensitive-paths.ts +92 -92
  220. package/src/runtime/session-usage.ts +79 -79
  221. package/src/runtime/settings-store.ts +103 -0
  222. package/src/runtime/sidechain-output.ts +29 -29
  223. package/src/runtime/skill-instructions.ts +222 -222
  224. package/src/runtime/stale-reconciler.ts +198 -189
  225. package/src/runtime/streaming-output.ts +47 -0
  226. package/src/runtime/subagent-manager.ts +404 -395
  227. package/src/runtime/subprocess-tool-registry.ts +67 -67
  228. package/src/runtime/task-display.ts +38 -38
  229. package/src/runtime/task-graph-scheduler.ts +122 -122
  230. package/src/runtime/task-graph.ts +207 -207
  231. package/src/runtime/task-output-context.ts +177 -177
  232. package/src/runtime/task-packet.ts +93 -93
  233. package/src/runtime/task-quality.ts +207 -207
  234. package/src/runtime/task-runner/capabilities.ts +78 -78
  235. package/src/runtime/task-runner/live-executor.ts +131 -113
  236. package/src/runtime/task-runner/progress.ts +119 -119
  237. package/src/runtime/task-runner/prompt-builder.ts +139 -139
  238. package/src/runtime/task-runner/prompt-pipeline.ts +64 -64
  239. package/src/runtime/task-runner/result-utils.ts +14 -14
  240. package/src/runtime/task-runner/run-projection.ts +103 -103
  241. package/src/runtime/task-runner/state-helpers.ts +22 -22
  242. package/src/runtime/task-runner.ts +469 -458
  243. package/src/runtime/team-runner.ts +693 -945
  244. package/src/runtime/usage-tracker.ts +71 -0
  245. package/src/runtime/worker-heartbeat.ts +21 -21
  246. package/src/runtime/worker-startup.ts +57 -57
  247. package/src/runtime/workflow-state.ts +187 -187
  248. package/src/runtime/yield-handler.ts +190 -189
  249. package/src/schema/config-schema.ts +172 -168
  250. package/src/schema/team-tool-schema.ts +126 -125
  251. package/src/schema/validation-types.ts +151 -148
  252. package/src/skills/discover-skills.ts +67 -67
  253. package/src/skills/skill-templates.ts +374 -374
  254. package/src/state/active-run-registry.ts +227 -191
  255. package/src/state/artifact-store.ts +130 -129
  256. package/src/state/atomic-write.ts +262 -178
  257. package/src/state/blob-store.ts +116 -116
  258. package/src/state/contracts.ts +111 -111
  259. package/src/state/event-log-rotation.ts +161 -158
  260. package/src/state/event-log.ts +383 -240
  261. package/src/state/event-reconstructor.ts +217 -217
  262. package/src/state/jsonl-writer.ts +82 -82
  263. package/src/state/locks.ts +146 -148
  264. package/src/state/mailbox.ts +446 -405
  265. package/src/state/state-store.ts +364 -351
  266. package/src/state/task-claims.ts +44 -44
  267. package/src/state/types.ts +285 -285
  268. package/src/state/usage.ts +29 -29
  269. package/src/subagents/async-entry.ts +1 -1
  270. package/src/subagents/index.ts +3 -3
  271. package/src/subagents/live/control.ts +1 -1
  272. package/src/subagents/live/manager.ts +1 -1
  273. package/src/subagents/live/realtime.ts +1 -1
  274. package/src/subagents/live/session-runtime.ts +1 -1
  275. package/src/subagents/manager.ts +1 -1
  276. package/src/subagents/spawn.ts +1 -1
  277. package/src/teams/discover-teams.ts +116 -116
  278. package/src/teams/team-config.ts +27 -27
  279. package/src/teams/team-serializer.ts +38 -38
  280. package/src/types/diff.d.ts +18 -18
  281. package/src/ui/agent-management-overlay.ts +144 -144
  282. package/src/ui/crew-widget.ts +487 -370
  283. package/src/ui/dashboard-panes/agents-pane.ts +109 -28
  284. package/src/ui/dashboard-panes/cancellation-pane.ts +42 -42
  285. package/src/ui/dashboard-panes/capability-pane.ts +59 -59
  286. package/src/ui/dashboard-panes/health-pane.ts +30 -30
  287. package/src/ui/dashboard-panes/mailbox-pane.ts +35 -35
  288. package/src/ui/dashboard-panes/progress-pane.ts +30 -30
  289. package/src/ui/dashboard-panes/transcript-pane.ts +10 -10
  290. package/src/ui/heartbeat-aggregator.ts +63 -63
  291. package/src/ui/keybinding-map.ts +97 -94
  292. package/src/ui/live-conversation-overlay.ts +152 -0
  293. package/src/ui/live-run-sidebar.ts +180 -180
  294. package/src/ui/mascot.ts +442 -442
  295. package/src/ui/overlays/agent-picker-overlay.ts +57 -57
  296. package/src/ui/overlays/confirm-overlay.ts +58 -58
  297. package/src/ui/overlays/mailbox-compose-overlay.ts +144 -144
  298. package/src/ui/overlays/mailbox-compose-preview.ts +63 -63
  299. package/src/ui/overlays/mailbox-detail-overlay.ts +122 -122
  300. package/src/ui/pi-ui-compat.ts +57 -57
  301. package/src/ui/powerbar-publisher.ts +221 -197
  302. package/src/ui/render-scheduler.ts +216 -143
  303. package/src/ui/run-action-dispatcher.ts +118 -117
  304. package/src/ui/run-dashboard.ts +526 -464
  305. package/src/ui/run-event-bus.ts +208 -208
  306. package/src/ui/run-snapshot-cache.ts +826 -777
  307. package/src/ui/settings-overlay.ts +721 -0
  308. package/src/ui/snapshot-types.ts +86 -70
  309. package/src/ui/theme-adapter.ts +190 -190
  310. package/src/ui/tool-progress-formatter.ts +89 -0
  311. package/src/ui/transcript-cache.ts +94 -94
  312. package/src/ui/transcript-viewer.ts +335 -335
  313. package/src/utils/conflict-detect.ts +662 -0
  314. package/src/utils/env-filter.ts +30 -0
  315. package/src/utils/file-coalescer.ts +86 -86
  316. package/src/utils/frontmatter.ts +68 -68
  317. package/src/utils/fs-watch.ts +88 -31
  318. package/src/utils/gh-protocol.ts +479 -0
  319. package/src/utils/ids.ts +17 -17
  320. package/src/utils/incremental-reader.ts +104 -104
  321. package/src/utils/internal-error.ts +6 -6
  322. package/src/utils/names.ts +27 -27
  323. package/src/utils/paths.ts +102 -63
  324. package/src/utils/redaction.ts +44 -44
  325. package/src/utils/resolve-shell.ts +34 -0
  326. package/src/utils/safe-paths.ts +47 -47
  327. package/src/utils/scan-cache.ts +136 -136
  328. package/src/utils/sleep.ts +2 -1
  329. package/src/utils/sse-parser.ts +134 -134
  330. package/src/utils/task-name-generator.ts +337 -337
  331. package/src/utils/timings.ts +33 -33
  332. package/src/utils/visual.ts +243 -198
  333. package/src/workflows/discover-workflows.ts +139 -139
  334. package/src/workflows/validate-workflow.ts +40 -40
  335. package/src/workflows/workflow-config.ts +26 -26
  336. package/src/workflows/workflow-serializer.ts +32 -32
  337. package/src/worktree/branch-freshness.ts +45 -45
  338. package/src/worktree/cleanup.ts +75 -72
  339. package/src/worktree/worktree-manager.ts +188 -146
  340. package/teams/default.team.md +12 -12
  341. package/teams/fast-fix.team.md +11 -11
  342. package/teams/implementation.team.md +18 -18
  343. package/teams/parallel-research.team.md +14 -14
  344. package/teams/research.team.md +11 -11
  345. package/teams/review.team.md +12 -12
  346. package/tsconfig.json +19 -19
  347. package/workflows/default.workflow.md +30 -30
  348. package/workflows/fast-fix.workflow.md +23 -23
  349. package/workflows/implementation.workflow.md +43 -43
  350. package/workflows/parallel-research.workflow.md +46 -46
  351. package/workflows/research.workflow.md +22 -22
  352. package/workflows/review.workflow.md +30 -30
  353. package/skills/task-packet/SKILL.md +0 -28
  354. package/skills/verify-evidence/SKILL.md +0 -27
@@ -0,0 +1,479 @@
1
+ /**
2
+ * gh-protocol.ts — GitHub URL protocol for issue:// and pr:// URLs.
3
+ *
4
+ * Forked from oh-my-pi packages/coding-agent/src/internal-urls/issue-pr-protocol.ts
5
+ * with adaptations for pi-crew's needs (no SQLite cache, no session registry).
6
+ *
7
+ * URL shapes:
8
+ * - `issue://` — list recent issues (repo derived from cwd)
9
+ * - `issue://owner/repo` — list issues for repo
10
+ * - `issue://123` — single issue (repo from cwd)
11
+ * - `issue://owner/repo/123` — fully qualified
12
+ * - `issue://owner/repo/123?comments=0` — suppress comments
13
+ *
14
+ * - `pr://` — list recent PRs (repo derived from cwd)
15
+ * - `pr://owner/repo` — list PRs for repo
16
+ * - `pr://456` — single PR (repo from cwd)
17
+ * - `pr://owner/repo/456` — fully qualified
18
+ * - `pr://owner/repo/456/diff` — list changed files
19
+ * - `pr://owner/repo/456/diff/all` — full unified diff
20
+ *
21
+ * Requirements: GitHub CLI (`gh`) installed and authenticated.
22
+ * Repo resolution: git remote get-url origin from cwd.
23
+ */
24
+ import { execFileSync, execSync } from "node:child_process";
25
+ import { readFileSync } from "node:fs";
26
+ import * as path from "node:path";
27
+
28
+ /** Resolve the default repo from `git remote get-url origin` in cwd. */
29
+ export function resolveDefaultRepo(cwd: string): string {
30
+ try {
31
+ const remoteUrl = execSync("git remote get-url origin", {
32
+ cwd,
33
+ encoding: "utf-8",
34
+ timeout: 10_000,
35
+ stdio: ["pipe", "pipe", "pipe"],
36
+ }).trim();
37
+
38
+ // git@github.com:owner/repo.git or https://github.com/owner/repo.git
39
+ const sshMatch = remoteUrl.match(/git@github\.com:([^/]+)\/([^/.]+)/);
40
+ if (sshMatch) return `${sshMatch[1]}/${sshMatch[2]}`;
41
+
42
+ const httpsMatch = remoteUrl.match(/github\.com\/([^/]+)\/([^/.]+)/);
43
+ if (httpsMatch) return `${httpsMatch[1]}/${httpsMatch[2]}`;
44
+
45
+ throw new Error(`Could not parse git remote URL: ${remoteUrl}`);
46
+ } catch (err) {
47
+ const msg = err instanceof Error ? err.message : String(err);
48
+ throw new Error(`Failed to resolve default repo from git: ${msg}`);
49
+ }
50
+ }
51
+
52
+ interface ParsedListOptions {
53
+ kind: "list";
54
+ repo: string;
55
+ state: "open" | "closed" | "all";
56
+ limit: number;
57
+ author?: string;
58
+ label?: string;
59
+ }
60
+
61
+ interface ParsedSingle {
62
+ kind: "single";
63
+ repo?: string;
64
+ number: number;
65
+ comments: boolean;
66
+ }
67
+
68
+ interface ParsedPrDiff {
69
+ kind: "pr-diff";
70
+ repo?: string;
71
+ number: number;
72
+ mode: "list" | "all" | "slice";
73
+ index?: number;
74
+ }
75
+
76
+ type Parsed = ParsedListOptions | ParsedSingle | ParsedPrDiff;
77
+
78
+ const LIST_LIMIT_DEFAULT = 30;
79
+ const LIST_LIMIT_MAX = 100;
80
+
81
+ function parsePositiveInt(value: string): number | undefined {
82
+ const n = Number.parseInt(value, 10);
83
+ return Number.isInteger(n) && n > 0 ? n : undefined;
84
+ }
85
+
86
+ /** Parse search params for list options. */
87
+ function parseListOptions(url: URL, scheme: "issue" | "pr", repo: string): ParsedListOptions {
88
+ const allowedStates = scheme === "pr" ? ["open", "closed", "merged", "all"] : ["open", "closed", "all"];
89
+ const stateRaw = url.searchParams.get("state");
90
+ const state = (
91
+ stateRaw && (allowedStates as string[]).includes(stateRaw) ? stateRaw : "open"
92
+ ) as ParsedListOptions["state"];
93
+
94
+ let limit = LIST_LIMIT_DEFAULT;
95
+ const limitRaw = url.searchParams.get("limit");
96
+ if (limitRaw !== null) {
97
+ const parsed = parsePositiveInt(limitRaw);
98
+ if (parsed !== undefined) limit = Math.min(parsed, LIST_LIMIT_MAX);
99
+ }
100
+
101
+ return {
102
+ kind: "list",
103
+ repo,
104
+ state,
105
+ limit,
106
+ author: url.searchParams.get("author") ?? undefined,
107
+ label: url.searchParams.get("label") ?? undefined,
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Parse an issue:// or pr:// URL.
113
+ *
114
+ * Supported shapes:
115
+ * - `scheme://` — list default repo
116
+ * - `scheme://<number>` — single item, default repo
117
+ * - `scheme://owner/repo` — list specific repo
118
+ * - `scheme://owner/repo/<number>` — single item, specific repo
119
+ * - `scheme://owner/repo/<number>/diff[/all|<N>]` — PR diff (pr:// only)
120
+ */
121
+ export function parseGitHubUrl(raw: string, scheme: "issue" | "pr"): Parsed {
122
+ let url: URL;
123
+ try {
124
+ // Treat as a URL-like path; prepend scheme if missing
125
+ const withScheme = raw.startsWith(`${scheme}://`) ? raw : `${scheme}://${raw}`;
126
+ url = new URL(withScheme);
127
+ } catch {
128
+ throw new Error(
129
+ `Invalid ${scheme}:// URL '${raw}'. Expected ${scheme}://, ${scheme}://<number>, ${scheme}://<owner>/<repo>, or ${scheme}://<owner>/<repo>/<number>.`,
130
+ );
131
+ }
132
+
133
+ const host = url.hostname;
134
+ const rawPathname = url.pathname;
135
+ // Strip leading slash
136
+ const stripped = rawPathname.startsWith("/") ? rawPathname.slice(1) : rawPathname;
137
+ const pathParts: string[] = stripped !== "" ? stripped.split("/").filter(Boolean) : [];
138
+
139
+ // Empty → list default repo
140
+ if (!host && pathParts.length === 0) {
141
+ return { kind: "list", repo: "", state: "open", limit: LIST_LIMIT_DEFAULT };
142
+ }
143
+
144
+ // If host looks like a number, treat as single-item shorthand
145
+ if (parsePositiveInt(host) !== undefined) {
146
+ // pathParts.length === 0 → scheme://N (single item, default repo)
147
+ // pathParts[0] === "diff" → scheme://N/diff[/sub] (PR diff, default repo)
148
+ if (pathParts.length === 0) {
149
+ const commentsParam = url.searchParams.get("comments");
150
+ const comments = !(commentsParam === "0" || (commentsParam?.toLowerCase() === "false"));
151
+ return { kind: "single", repo: undefined, number: parsePositiveInt(host)!, comments };
152
+ }
153
+ if (pathParts[0] === "diff") {
154
+ if (scheme !== "pr") throw new Error(`Invalid ${scheme}:// URL. Diff is only available for pr:// URLs.`);
155
+ const number = parsePositiveInt(host)!;
156
+ const diffParts = pathParts;
157
+ if (diffParts.length === 1) {
158
+ return { kind: "pr-diff", repo: undefined, number, mode: "list" };
159
+ }
160
+ if (diffParts[1] === "all") {
161
+ return { kind: "pr-diff", repo: undefined, number, mode: "all" };
162
+ }
163
+ const idx = parsePositiveInt(diffParts[1]);
164
+ if (idx === undefined) {
165
+ throw new Error(`Invalid pr:// diff sub-path '${diffParts[1]}'. Use 'all' or a 1-indexed file number.`);
166
+ }
167
+ return { kind: "pr-diff", repo: undefined, number, mode: "slice", index: idx };
168
+ }
169
+ // Numeric host with non-diff path — invalid number
170
+ throw new Error(`Invalid ${scheme}:// number: ${host}`);
171
+ }
172
+
173
+ // Non-numeric host with 0 path parts
174
+ if (pathParts.length === 0 && host) {
175
+ // If host looks like a domain name (contains dots), it's a partial repo ref
176
+ // (scheme://owner.missing-repo). Otherwise, treat as invalid number.
177
+ if (!host.includes(".")) {
178
+ throw new Error(`Invalid ${scheme}:// number: ${host}`);
179
+ }
180
+ throw new Error(
181
+ `Invalid ${scheme}:// URL. Expected ${scheme}://<owner>/<repo> or ${scheme}://<owner>/<repo>/<number>.`,
182
+ );
183
+ }
184
+
185
+ // scheme://owner/repo → list
186
+ if (pathParts.length === 1 && host) {
187
+ const repo = `${host}/${pathParts[0]}`;
188
+ return parseListOptions(url, scheme, repo);
189
+ }
190
+
191
+ // scheme://owner/repo/N[/diff[/sub]]]
192
+ if (pathParts.length >= 2 && host) {
193
+ const repo = `${host}/${pathParts[0]}`;
194
+ const numberPart = pathParts[1];
195
+
196
+ const num = parsePositiveInt(numberPart);
197
+ if (num === undefined) {
198
+ throw new Error(`Invalid ${scheme}:// number: ${numberPart ?? "(missing)"}`);
199
+ }
200
+
201
+ // PR diff path
202
+ const diffParts = pathParts.slice(2);
203
+ if (diffParts.length > 0) {
204
+ if (scheme === "issue") {
205
+ throw new Error(
206
+ `Invalid issue:// URL. Issue views do not have a diff; use pr://<owner>/<repo>/<n>/diff for pull requests.`,
207
+ );
208
+ }
209
+ if (diffParts[0] !== "diff" || diffParts.length > 2) {
210
+ throw new Error(
211
+ `Invalid pr:// URL. Expected pr://<n>/diff, pr://<n>/diff/all, or pr://<n>/diff/<i>.`,
212
+ );
213
+ }
214
+ if (diffParts.length === 1) {
215
+ return { kind: "pr-diff", repo, number: num, mode: "list" };
216
+ }
217
+ const sub = diffParts[1] ?? "";
218
+ if (sub === "all") {
219
+ return { kind: "pr-diff", repo, number: num, mode: "all" };
220
+ }
221
+ const idx = parsePositiveInt(sub);
222
+ if (idx === undefined) {
223
+ throw new Error(`Invalid pr:// diff sub-path '${sub}'. Use 'all' or a 1-indexed file number.`);
224
+ }
225
+ return { kind: "pr-diff", repo, number: num, mode: "slice", index: idx };
226
+ }
227
+
228
+ const commentsParam2 = url.searchParams.get("comments");
229
+ const comments = !(commentsParam2 === "0" || (commentsParam2?.toLowerCase() === "false"));
230
+ return { kind: "single", repo, number: num, comments };
231
+ }
232
+
233
+ // scheme://N (numeric in path) — single, default repo
234
+ if (pathParts.length === 1 && parsePositiveInt(pathParts[0]) !== undefined) {
235
+ const commentsParam3 = url.searchParams.get("comments");
236
+ const comments = !(commentsParam3 === "0" || (commentsParam3?.toLowerCase() === "false"));
237
+ return { kind: "single", repo: undefined, number: parsePositiveInt(pathParts[0])!, comments };
238
+ }
239
+
240
+ // Fallback: unrecognized shape
241
+ throw new Error(
242
+ `Invalid ${scheme}:// URL. Expected ${scheme}://, ${scheme}://<number>, ${scheme}://<owner>/<repo>, or ${scheme}://<owner>/<repo>/<number>.`,
243
+ );
244
+ }
245
+
246
+ interface GitHubListItem {
247
+ number?: number;
248
+ title?: string;
249
+ state?: string;
250
+ stateReason?: string | null;
251
+ author?: { login?: string } | null;
252
+ labels?: Array<{ name?: string }>;
253
+ createdAt?: string;
254
+ updatedAt?: string;
255
+ url?: string;
256
+ isDraft?: boolean;
257
+ baseRefName?: string;
258
+ headRefName?: string;
259
+ }
260
+
261
+ function formatListItem(scheme: "issue" | "pr", repo: string, item: GitHubListItem): string {
262
+ const num = item.number ?? "?";
263
+ const title = item.title ?? "(no title)";
264
+ const state = item.state?.toLowerCase() ?? "?";
265
+ const author = item.author?.login ?? "?";
266
+ const updated = item.updatedAt ?? item.createdAt ?? "";
267
+ const draftSuffix = scheme === "pr" && item.isDraft ? " [draft]" : "";
268
+ const labels = (item.labels ?? [])
269
+ .map(l => l.name)
270
+ .filter(Boolean)
271
+ .join(", ");
272
+ const labelSuffix = labels ? ` labels: ${labels}` : "";
273
+ const itemUrl = num === "?" ? `${scheme}://${repo}` : `${scheme}://${repo}/${num}`;
274
+ return `- [${state}${draftSuffix}] #${num} @${author} ${updated}\n ${title}${labelSuffix}\n ${itemUrl}`;
275
+ }
276
+
277
+ function runGh(cwd: string, args: string[]): string {
278
+ try {
279
+ return execFileSync("gh", args, {
280
+ cwd,
281
+ encoding: "utf-8",
282
+ timeout: 30_000,
283
+ stdio: ["pipe", "pipe", "pipe"],
284
+ });
285
+ } catch (err) {
286
+ const msg = err instanceof Error ? err.message : String(err);
287
+ throw new Error(`gh command failed: ${msg}`);
288
+ }
289
+ }
290
+
291
+ function ghJson<T>(cwd: string, args: string[]): T {
292
+ const jsonOut = execFileSync("gh", args, {
293
+ cwd, encoding: "utf-8", timeout: 30_000, stdio: ["pipe", "pipe", "pipe"],
294
+ });
295
+ try {
296
+ return JSON.parse(jsonOut) as T;
297
+ } catch {
298
+ throw new Error(`gh JSON parse failed for: ${args.join(" ")}\nOutput: ${jsonOut.slice(0, 200)}`);
299
+ }
300
+ }
301
+
302
+ interface GhResult<T> {
303
+ content: string;
304
+ notes: string[];
305
+ data?: T;
306
+ }
307
+
308
+ /**
309
+ * Execute a parsed GitHub URL and return the result.
310
+ *
311
+ * @param parsed Parsed URL result from `parseGitHubUrl`
312
+ * @param scheme "issue" or "pr"
313
+ * @param cwd Working directory for repo resolution
314
+ */
315
+ export function resolveGitHubUrl(parsed: Parsed, scheme: "issue" | "pr", cwd: string): GhResult<unknown> {
316
+ // Resolve repo for list operations
317
+ if (parsed.kind === "list") {
318
+ const repo = parsed.repo || resolveDefaultRepo(cwd);
319
+ const fields = scheme === "issue"
320
+ ? ["number", "title", "state", "stateReason", "author", "labels", "createdAt", "updatedAt", "url"]
321
+ : ["number", "title", "state", "isDraft", "author", "baseRefName", "headRefName", "labels", "createdAt", "updatedAt", "url"];
322
+
323
+ const args = [
324
+ scheme, "list",
325
+ "--repo", repo,
326
+ "--state", parsed.state,
327
+ "--limit", String(parsed.limit),
328
+ "--json", fields.join(","),
329
+ ];
330
+ if (parsed.author) args.push("--author", parsed.author);
331
+ if (parsed.label) args.push("--label", parsed.label);
332
+
333
+ let items: GitHubListItem[] = [];
334
+ try {
335
+ items = ghJson<GitHubListItem[]>(cwd, args);
336
+ } catch (err) {
337
+ const msg = err instanceof Error ? err.message : String(err);
338
+ throw new Error(`${scheme}:// listing failed: ${msg}`);
339
+ }
340
+
341
+ const header = `# ${scheme === "issue" ? "Issues" : "Pull Requests"} in ${repo} (${parsed.state}, up to ${parsed.limit})`;
342
+ const body = items.length === 0
343
+ ? "_No matches._"
344
+ : items.map(item => formatListItem(scheme, repo, item)).join("\n\n");
345
+ const footer = `\n\n---\nRead a specific item: \`${scheme}://${repo}/<N>\` (or \`${scheme}://<N>\` for the current repo).`;
346
+ return {
347
+ content: `${header}\n\n${body}${footer}`,
348
+ notes: [`Live listing for ${repo} via gh`],
349
+ data: items,
350
+ };
351
+ }
352
+
353
+ // Resolve repo for single items
354
+ let repo = parsed.repo;
355
+ if (!repo) {
356
+ try {
357
+ repo = resolveDefaultRepo(cwd);
358
+ } catch (err) {
359
+ const msg = err instanceof Error ? err.message : String(err);
360
+ throw new Error(
361
+ `${scheme}://${(parsed as ParsedSingle).number} could not resolve a default repo from cwd '${cwd}': ${msg}\nUse ${scheme}://<owner>/<repo>/${(parsed as ParsedSingle).number} instead.`,
362
+ );
363
+ }
364
+ }
365
+
366
+ // PR diff
367
+ if (parsed.kind === "pr-diff") {
368
+ if (parsed.mode === "all") {
369
+ try {
370
+ const diff = execFileSync("gh", ["pr", "diff", String(parsed.number), "--repo", repo], {
371
+ cwd,
372
+ encoding: "utf-8",
373
+ timeout: 30_000,
374
+ stdio: ["pipe", "pipe", "pipe"],
375
+ });
376
+ return {
377
+ content: diff,
378
+ notes: [`Full diff for ${scheme}://${repo}/${parsed.number}`],
379
+ };
380
+ } catch (err) {
381
+ const msg = err instanceof Error ? err.message : String(err);
382
+ throw new Error(`pr://${parsed.number}/diff failed: ${msg}`);
383
+ }
384
+ }
385
+
386
+ if (parsed.mode === "list") {
387
+ try {
388
+ const raw = execFileSync(
389
+ "gh", ["pr", "view", String(parsed.number), "--repo", repo, "--json", "files", "--jq", ".files[] | \"\(.filename) +\(.additions) -\(.deletions) [\(.status)]\""],
390
+ { cwd, encoding: "utf-8", timeout: 30_000, stdio: ["pipe", "pipe", "pipe"] },
391
+ );
392
+ const fileLines = raw.split("\n").filter(Boolean);
393
+ const header = `# Pull Request Diff: ${repo}#${parsed.number} (${fileLines.length} file${fileLines.length === 1 ? "" : "s"})`;
394
+ const body = fileLines.length === 0
395
+ ? "_No file changes._"
396
+ : fileLines.map((line, i) => `${i + 1}. ${line}\n pr://${repo}/${parsed.number}/diff/${i + 1}`).join("\n\n");
397
+ const footer = `\n\n---\nRead all: \`pr://${repo}/${parsed.number}/diff/all\`. Each file is also available as \`pr://${repo}/${parsed.number}/diff/<i>\`.`;
398
+ return {
399
+ content: `${header}\n\n${body}${footer}`,
400
+ notes: [`File listing for pr://${repo}/${parsed.number}`],
401
+ };
402
+ } catch (err) {
403
+ const msg = err instanceof Error ? err.message : String(err);
404
+ throw new Error(`pr://${parsed.number}/diff failed: ${msg}`);
405
+ }
406
+ }
407
+
408
+ if (parsed.mode === "slice" && parsed.index !== undefined) {
409
+ try {
410
+ const raw = execFileSync(
411
+ "gh", ["pr", "view", String(parsed.number), "--repo", repo, "--json", "files", "--jq", ".files[] | \"\(.filename) +\(.additions) -\(.deletions) [\(.status)]\""],
412
+ { cwd, encoding: "utf-8", timeout: 30_000, stdio: ["pipe", "pipe", "pipe"] },
413
+ );
414
+ const fileLines = raw.split("\n").filter(Boolean);
415
+ const targetIdx = parsed.index - 1;
416
+ if (targetIdx < 0 || targetIdx >= fileLines.length) {
417
+ throw new Error(`File index ${parsed.index} out of range (1..${fileLines.length}).`);
418
+ }
419
+ const fullDiff = execFileSync("gh", ["pr", "diff", String(parsed.number), "--repo", repo], {
420
+ cwd, encoding: "utf-8", timeout: 30_000, stdio: ["pipe", "pipe", "pipe"],
421
+ });
422
+ // Extract the diff for the target file by splitting on "diff --git" headers
423
+ const diffBlocks = fullDiff.split(/(?=diff --git )/);
424
+ const fileName = fileLines[targetIdx]!.split(" ")[0];
425
+ const matched = diffBlocks.find((block) => block.includes(fileName!));
426
+ return {
427
+ content: matched || `File ${fileName} not found in diff.`,
428
+ notes: [`Diff for file ${parsed.index}/${fileLines.length}: ${fileName} in pr://${repo}/${parsed.number}`],
429
+ };
430
+ } catch (err) {
431
+ const msg = err instanceof Error ? err.message : String(err);
432
+ throw new Error(`pr://${parsed.number}/diff/${parsed.index} failed: ${msg}`);
433
+ }
434
+ }
435
+ }
436
+
437
+ // Single issue/PR
438
+ const single = parsed as ParsedSingle;
439
+ try {
440
+ let content: string;
441
+ if (scheme === "issue") {
442
+ const args = ["issue", "view", String(single.number), "--repo", repo];
443
+ if (!single.comments) args.push("--comments", "false");
444
+ content = execFileSync("gh", args, {
445
+ cwd,
446
+ encoding: "utf-8",
447
+ timeout: 30_000,
448
+ stdio: ["pipe", "pipe", "pipe"],
449
+ });
450
+ } else {
451
+ const args = ["pr", "view", String(single.number), "--repo", repo];
452
+ if (!single.comments) args.push("--comments", "false");
453
+ content = execFileSync("gh", args, {
454
+ cwd,
455
+ encoding: "utf-8",
456
+ timeout: 30_000,
457
+ stdio: ["pipe", "pipe", "pipe"],
458
+ });
459
+ // Append diff URL hint
460
+ content += `\n\n---\nDiff: pr://${repo}/${single.number}/diff\nFiles: pr://${repo}/${single.number}/diff/list`;
461
+ }
462
+ return {
463
+ content,
464
+ notes: [`${scheme}://${repo}/${single.number} via gh`],
465
+ };
466
+ } catch (err) {
467
+ const msg = err instanceof Error ? err.message : String(err);
468
+ throw new Error(`${scheme}://${repo}/${single.number} failed: ${msg}`);
469
+ }
470
+ }
471
+
472
+ /**
473
+ * Resolve a raw `issue://` or `pr://` URL string.
474
+ * Convenience wrapper combining parse + resolve.
475
+ */
476
+ export function resolveGitHubProtocol(raw: string, scheme: "issue" | "pr", cwd: string): GhResult<unknown> {
477
+ const parsed = parseGitHubUrl(raw, scheme);
478
+ return resolveGitHubUrl(parsed, scheme, cwd);
479
+ }
package/src/utils/ids.ts CHANGED
@@ -1,17 +1,17 @@
1
- import { randomBytes } from "node:crypto";
2
- import { generateTaskName } from "./task-name-generator.ts";
3
-
4
- export function createRunId(prefix = "team"): string {
5
- const stamp = new Date().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
6
- const suffix = randomBytes(8).toString("hex");
7
- return `${prefix}_${stamp}_${suffix}`;
8
- }
9
-
10
- export function createTaskId(stepId: string, index: number): string {
11
- const normalized = stepId.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "") || "task";
12
- return `${String(index + 1).padStart(2, "0")}_${normalized}`;
13
- }
14
-
15
- export function createDisplayName(): string {
16
- return generateTaskName();
17
- }
1
+ import { randomBytes } from "node:crypto";
2
+ import { generateTaskName } from "./task-name-generator.ts";
3
+
4
+ export function createRunId(prefix = "team"): string {
5
+ const stamp = new Date().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
6
+ const suffix = randomBytes(8).toString("hex");
7
+ return `${prefix}_${stamp}_${suffix}`;
8
+ }
9
+
10
+ export function createTaskId(stepId: string, index: number): string {
11
+ const normalized = stepId.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "") || "task";
12
+ return `${String(index + 1).padStart(2, "0")}_${normalized}`;
13
+ }
14
+
15
+ export function createDisplayName(): string {
16
+ return generateTaskName();
17
+ }