@crouton-kit/crouter 0.3.17 → 0.3.19

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 (283) hide show
  1. package/dist/build-root.js +6 -0
  2. package/dist/builtin-memory/crouter-development/marketplaces.md +164 -0
  3. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +58 -0
  4. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +60 -0
  5. package/dist/builtin-memory/crouter-development/personas.md +117 -0
  6. package/dist/builtin-memory/crouter-development/plugins.md +163 -0
  7. package/dist/builtin-memory/design.md +60 -0
  8. package/dist/builtin-memory/development.md +116 -0
  9. package/dist/builtin-memory/planning.md +66 -0
  10. package/dist/builtin-memory/spec.md +99 -0
  11. package/dist/builtin-personas/design/{base.md → PERSONA.md} +4 -0
  12. package/dist/builtin-personas/design/orchestrator.md +1 -1
  13. package/dist/builtin-personas/developer/{base.md → PERSONA.md} +4 -0
  14. package/dist/builtin-personas/developer/orchestrator.md +1 -1
  15. package/dist/builtin-personas/explore/{base.md → PERSONA.md} +4 -0
  16. package/dist/builtin-personas/general/{base.md → PERSONA.md} +4 -0
  17. package/dist/builtin-personas/lifecycle/resident.md +1 -1
  18. package/dist/builtin-personas/lifecycle/terminal.md +5 -2
  19. package/dist/builtin-personas/orchestration-kernel.md +15 -24
  20. package/dist/builtin-personas/plan/{base.md → PERSONA.md} +4 -0
  21. package/dist/builtin-personas/plan/orchestrator.md +1 -1
  22. package/dist/builtin-personas/plan/reviewers/architecture-fit/{base.md → PERSONA.md} +1 -1
  23. package/dist/builtin-personas/plan/reviewers/code-smells/{base.md → PERSONA.md} +1 -1
  24. package/dist/builtin-personas/plan/reviewers/pattern-consistency/{base.md → PERSONA.md} +1 -1
  25. package/dist/builtin-personas/plan/reviewers/requirements-coverage/{base.md → PERSONA.md} +1 -1
  26. package/dist/builtin-personas/plan/reviewers/security/{base.md → PERSONA.md} +1 -1
  27. package/dist/builtin-personas/review/{base.md → PERSONA.md} +4 -0
  28. package/dist/builtin-personas/runtime-base.md +6 -3
  29. package/dist/builtin-personas/spec/{base.md → PERSONA.md} +4 -0
  30. package/dist/builtin-personas/spec/orchestrator.md +1 -1
  31. package/dist/builtin-personas/waiting.md +8 -0
  32. package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +24 -14
  33. package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +4 -4
  34. package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +1 -1
  35. package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +2 -2
  36. package/dist/builtin-views/_lib/states.mjs +161 -0
  37. package/dist/builtin-views/canvas/client.mjs +303 -0
  38. package/dist/builtin-views/canvas/view.mjs +576 -0
  39. package/dist/builtin-views/git-pr/client.mjs +440 -0
  40. package/dist/builtin-views/git-pr/view.mjs +675 -0
  41. package/dist/builtin-views/inbox/_lib/render.mjs +177 -0
  42. package/dist/builtin-views/inbox/sources/gmail.mjs +965 -0
  43. package/dist/builtin-views/inbox/sources/linkedin.mjs +427 -0
  44. package/dist/builtin-views/inbox/view.mjs +889 -0
  45. package/dist/builtin-views/linkedin/client.mjs +610 -0
  46. package/dist/builtin-views/linkedin/view.mjs +1171 -0
  47. package/dist/clients/attach/attach-cmd.d.ts +2 -0
  48. package/dist/clients/attach/attach-cmd.js +354 -0
  49. package/dist/clients/attach/chat-view.d.ts +77 -0
  50. package/dist/clients/attach/chat-view.js +450 -0
  51. package/dist/clients/attach/clipboard-image.d.ts +16 -0
  52. package/dist/clients/attach/clipboard-image.js +113 -0
  53. package/dist/clients/attach/config-load.d.ts +31 -0
  54. package/dist/clients/attach/config-load.js +113 -0
  55. package/dist/clients/attach/extension-dialogs.d.ts +29 -0
  56. package/dist/clients/attach/extension-dialogs.js +101 -0
  57. package/dist/clients/attach/input-controller.d.ts +54 -0
  58. package/dist/clients/attach/input-controller.js +204 -0
  59. package/dist/clients/attach/slash-commands.d.ts +36 -0
  60. package/dist/clients/attach/slash-commands.js +200 -0
  61. package/dist/clients/attach/view-socket.d.ts +48 -0
  62. package/dist/clients/attach/view-socket.js +126 -0
  63. package/dist/commands/attention.js +3 -3
  64. package/dist/commands/canvas-prune.js +1 -1
  65. package/dist/commands/daemon.js +4 -3
  66. package/dist/commands/human/prompts.js +4 -10
  67. package/dist/commands/human/queue.js +43 -8
  68. package/dist/commands/human/shared.d.ts +28 -1
  69. package/dist/commands/human/shared.js +48 -10
  70. package/dist/commands/memory/find.d.ts +1 -0
  71. package/dist/commands/memory/find.js +180 -0
  72. package/dist/commands/memory/lint.d.ts +1 -0
  73. package/dist/commands/memory/lint.js +140 -0
  74. package/dist/commands/memory/list.d.ts +1 -0
  75. package/dist/commands/memory/list.js +79 -0
  76. package/dist/commands/memory/read.js +103 -0
  77. package/dist/commands/memory/shared.d.ts +30 -0
  78. package/dist/commands/memory/shared.js +122 -0
  79. package/dist/commands/memory/write.d.ts +1 -0
  80. package/dist/commands/memory/write.js +85 -0
  81. package/dist/commands/memory.d.ts +2 -0
  82. package/dist/commands/memory.js +27 -0
  83. package/dist/commands/node.d.ts +3 -2
  84. package/dist/commands/node.js +660 -65
  85. package/dist/commands/pkg/market-manage.js +1 -1
  86. package/dist/commands/push.js +6 -6
  87. package/dist/commands/revive.js +1 -1
  88. package/dist/commands/skill/author.js +1 -1
  89. package/dist/commands/skill/shared.d.ts +1 -8
  90. package/dist/commands/skill/shared.js +2 -55
  91. package/dist/commands/skill.js +9 -14
  92. package/dist/commands/sys/doctor.js +1 -1
  93. package/dist/commands/sys/update.js +1 -1
  94. package/dist/commands/view-cycle.d.ts +2 -0
  95. package/dist/commands/view-cycle.js +125 -0
  96. package/dist/commands/view-list.d.ts +2 -0
  97. package/dist/commands/view-list.js +66 -0
  98. package/dist/commands/view-new.d.ts +2 -0
  99. package/dist/commands/view-new.js +70 -0
  100. package/dist/commands/view-pick.d.ts +2 -0
  101. package/dist/commands/view-pick.js +119 -0
  102. package/dist/commands/view-run.d.ts +2 -0
  103. package/dist/commands/view-run.js +191 -0
  104. package/dist/commands/view.d.ts +2 -0
  105. package/dist/commands/view.js +29 -0
  106. package/dist/core/__tests__/broker-lifecycle.test.d.ts +1 -0
  107. package/dist/core/__tests__/broker-lifecycle.test.js +677 -0
  108. package/dist/core/__tests__/broker-sdk-wiring.test.d.ts +1 -0
  109. package/dist/core/__tests__/broker-sdk-wiring.test.js +166 -0
  110. package/dist/core/__tests__/cascade-close.test.js +12 -2
  111. package/dist/core/__tests__/child-death-wake.test.d.ts +1 -0
  112. package/dist/core/__tests__/child-death-wake.test.js +245 -0
  113. package/dist/core/__tests__/context-intro.test.js +76 -62
  114. package/dist/core/__tests__/daemon-boot.test.js +14 -5
  115. package/dist/core/__tests__/daemon-liveness.test.js +34 -9
  116. package/dist/core/__tests__/detach-focus.test.d.ts +1 -0
  117. package/dist/core/__tests__/detach-focus.test.js +206 -0
  118. package/dist/core/__tests__/draw-style.test.d.ts +1 -0
  119. package/dist/core/__tests__/draw-style.test.js +258 -0
  120. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.d.ts +2 -0
  121. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.js +18 -0
  122. package/dist/core/__tests__/fixtures/fake-engine.d.ts +138 -0
  123. package/dist/core/__tests__/fixtures/fake-engine.js +614 -0
  124. package/dist/core/__tests__/fixtures/fake-pi-host.js +1 -0
  125. package/dist/core/__tests__/flagship-lifecycle.test.js +7 -1
  126. package/dist/core/__tests__/frame-decoder-perf.test.d.ts +1 -0
  127. package/dist/core/__tests__/frame-decoder-perf.test.js +198 -0
  128. package/dist/core/__tests__/helpers/harness.d.ts +9 -0
  129. package/dist/core/__tests__/helpers/harness.js +111 -1
  130. package/dist/core/__tests__/home-session.test.js +41 -8
  131. package/dist/core/__tests__/human-new-window-regression.test.d.ts +1 -0
  132. package/dist/core/__tests__/human-new-window-regression.test.js +101 -0
  133. package/dist/core/__tests__/human-surface-target.test.d.ts +1 -0
  134. package/dist/core/__tests__/human-surface-target.test.js +98 -0
  135. package/dist/core/__tests__/kickoff.test.js +37 -3
  136. package/dist/core/__tests__/live-mutation.test.js +50 -33
  137. package/dist/core/__tests__/memory.test.js +23 -115
  138. package/dist/core/__tests__/persona-subkind.test.js +18 -15
  139. package/dist/core/__tests__/placement-focus.test.js +5 -0
  140. package/dist/core/__tests__/placement-teardown.test.js +54 -11
  141. package/dist/core/__tests__/relaunch.test.js +4 -3
  142. package/dist/core/__tests__/review-render-pane-regression.test.d.ts +1 -0
  143. package/dist/core/__tests__/review-render-pane-regression.test.js +133 -0
  144. package/dist/core/__tests__/spawn-root.test.js +10 -0
  145. package/dist/core/__tests__/spike-harness.test.js +1 -0
  146. package/dist/core/__tests__/wake-bearings.test.d.ts +1 -0
  147. package/dist/core/__tests__/wake-bearings.test.js +156 -0
  148. package/dist/core/__tests__/wake-origin.test.d.ts +1 -0
  149. package/dist/core/__tests__/wake-origin.test.js +110 -0
  150. package/dist/core/bootstrap.js +1 -1
  151. package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
  152. package/dist/core/canvas/browse/app.js +24 -2
  153. package/dist/core/canvas/browse/model.d.ts +38 -2
  154. package/dist/core/canvas/browse/model.js +134 -10
  155. package/dist/core/canvas/browse/render.d.ts +6 -12
  156. package/dist/core/canvas/browse/render.js +72 -104
  157. package/dist/core/canvas/canvas.js +15 -22
  158. package/dist/core/canvas/db.js +46 -0
  159. package/dist/core/canvas/index.d.ts +1 -0
  160. package/dist/core/canvas/index.js +1 -0
  161. package/dist/core/canvas/paths.d.ts +4 -1
  162. package/dist/core/canvas/paths.js +10 -4
  163. package/dist/core/canvas/pid.d.ts +4 -0
  164. package/dist/core/canvas/pid.js +23 -0
  165. package/dist/core/canvas/render.d.ts +11 -2
  166. package/dist/core/canvas/render.js +69 -0
  167. package/dist/core/canvas/types.d.ts +85 -3
  168. package/dist/core/canvas/types.js +2 -2
  169. package/dist/core/canvas/wakeups.d.ts +76 -0
  170. package/dist/core/canvas/wakeups.js +185 -0
  171. package/dist/core/config.js +4 -1
  172. package/dist/core/frontmatter.js +37 -124
  173. package/dist/core/help.d.ts +6 -0
  174. package/dist/core/help.js +7 -0
  175. package/dist/core/memory-resolver.d.ts +49 -0
  176. package/dist/core/memory-resolver.js +141 -0
  177. package/dist/core/personas/index.d.ts +4 -3
  178. package/dist/core/personas/index.js +3 -2
  179. package/dist/core/personas/loader.d.ts +41 -16
  180. package/dist/core/personas/loader.js +133 -29
  181. package/dist/core/personas/resolve.d.ts +4 -4
  182. package/dist/core/personas/resolve.js +28 -16
  183. package/dist/core/predicate.d.ts +63 -0
  184. package/dist/core/predicate.js +189 -0
  185. package/dist/core/resolver.js +26 -5
  186. package/dist/core/runtime/bearings.d.ts +53 -12
  187. package/dist/core/runtime/bearings.js +132 -59
  188. package/dist/core/runtime/broker-cli.d.ts +1 -0
  189. package/dist/core/runtime/broker-cli.js +46 -0
  190. package/dist/core/runtime/broker-protocol.d.ts +332 -0
  191. package/dist/core/runtime/broker-protocol.js +153 -0
  192. package/dist/core/runtime/broker-sdk.d.ts +48 -0
  193. package/dist/core/runtime/broker-sdk.js +72 -0
  194. package/dist/core/runtime/broker.d.ts +55 -0
  195. package/dist/core/runtime/broker.js +1128 -0
  196. package/dist/core/runtime/close.js +35 -6
  197. package/dist/core/runtime/host.d.ts +53 -0
  198. package/dist/core/runtime/host.js +186 -0
  199. package/dist/core/runtime/kickoff.d.ts +2 -1
  200. package/dist/core/runtime/kickoff.js +91 -5
  201. package/dist/core/runtime/launch.d.ts +45 -2
  202. package/dist/core/runtime/launch.js +65 -2
  203. package/dist/core/runtime/lifecycle.js +23 -6
  204. package/dist/core/runtime/memory.d.ts +2 -42
  205. package/dist/core/runtime/memory.js +11 -162
  206. package/dist/core/runtime/nodes.d.ts +33 -0
  207. package/dist/core/runtime/nodes.js +59 -1
  208. package/dist/core/runtime/persona.js +21 -11
  209. package/dist/core/runtime/pi-vendored.d.ts +18 -0
  210. package/dist/core/runtime/pi-vendored.js +49 -0
  211. package/dist/core/runtime/placement.d.ts +42 -14
  212. package/dist/core/runtime/placement.js +228 -38
  213. package/dist/core/runtime/promote.d.ts +0 -6
  214. package/dist/core/runtime/promote.js +1 -12
  215. package/dist/core/runtime/{demote.d.ts → recycle.d.ts} +5 -5
  216. package/dist/core/runtime/{demote.js → recycle.js} +27 -11
  217. package/dist/core/runtime/reset.js +8 -6
  218. package/dist/core/runtime/revive.d.ts +2 -0
  219. package/dist/core/runtime/revive.js +34 -34
  220. package/dist/core/runtime/spawn.d.ts +19 -0
  221. package/dist/core/runtime/spawn.js +75 -22
  222. package/dist/core/runtime/stop-guard.d.ts +1 -1
  223. package/dist/core/runtime/stop-guard.js +6 -1
  224. package/dist/core/runtime/tmux-chrome.d.ts +1 -1
  225. package/dist/core/runtime/tmux-chrome.js +1 -1
  226. package/dist/core/runtime/tmux.d.ts +28 -0
  227. package/dist/core/runtime/tmux.js +80 -6
  228. package/dist/core/scope.d.ts +11 -0
  229. package/dist/core/scope.js +39 -0
  230. package/dist/core/spawn.d.ts +25 -1
  231. package/dist/core/spawn.js +72 -7
  232. package/dist/core/substrate/gate.d.ts +13 -0
  233. package/dist/core/substrate/gate.js +21 -0
  234. package/dist/core/substrate/index.d.ts +7 -0
  235. package/dist/core/substrate/index.js +18 -0
  236. package/dist/core/substrate/on-read.d.ts +14 -0
  237. package/dist/core/substrate/on-read.js +292 -0
  238. package/dist/core/substrate/render.d.ts +25 -0
  239. package/dist/core/substrate/render.js +256 -0
  240. package/dist/core/substrate/schema.d.ts +76 -0
  241. package/dist/core/substrate/schema.js +124 -0
  242. package/dist/core/substrate/session-cache.d.ts +30 -0
  243. package/dist/core/substrate/session-cache.js +77 -0
  244. package/dist/core/substrate/subject.d.ts +41 -0
  245. package/dist/core/substrate/subject.js +54 -0
  246. package/dist/core/tui/contract.d.ts +83 -0
  247. package/dist/core/tui/contract.js +8 -0
  248. package/dist/core/tui/draw.d.ts +96 -0
  249. package/dist/core/tui/draw.js +339 -0
  250. package/dist/core/tui/host.d.ts +29 -0
  251. package/dist/core/tui/host.js +379 -0
  252. package/dist/core/tui/loader.d.ts +16 -0
  253. package/dist/core/tui/loader.js +94 -0
  254. package/dist/core/{canvas/browse → tui}/terminal.js +7 -6
  255. package/dist/core/wake.d.ts +86 -0
  256. package/dist/core/wake.js +308 -0
  257. package/dist/daemon/crtrd.d.ts +29 -4
  258. package/dist/daemon/crtrd.js +662 -46
  259. package/dist/pi-extensions/__tests__/canvas-context-intro.test.d.ts +1 -0
  260. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +171 -0
  261. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +19 -12
  262. package/dist/pi-extensions/canvas-commands.d.ts +3 -0
  263. package/dist/pi-extensions/canvas-commands.js +10 -0
  264. package/dist/pi-extensions/canvas-context-intro.d.ts +17 -0
  265. package/dist/pi-extensions/canvas-context-intro.js +55 -15
  266. package/dist/pi-extensions/canvas-doc-substrate.d.ts +44 -0
  267. package/dist/pi-extensions/canvas-doc-substrate.js +112 -0
  268. package/dist/pi-extensions/canvas-nav.d.ts +3 -0
  269. package/dist/pi-extensions/canvas-nav.js +145 -34
  270. package/dist/pi-extensions/canvas-stophook.js +17 -8
  271. package/dist/pi-extensions/canvas-view.d.ts +21 -0
  272. package/dist/pi-extensions/canvas-view.js +75 -0
  273. package/dist/prompts/skill.js +19 -26
  274. package/dist/prompts/view.d.ts +7 -0
  275. package/dist/prompts/view.js +101 -0
  276. package/dist/types.d.ts +4 -0
  277. package/dist/types.js +1 -0
  278. package/package.json +9 -4
  279. package/dist/commands/skill/find.d.ts +0 -4
  280. package/dist/commands/skill/find.js +0 -257
  281. package/dist/commands/skill/read.js +0 -91
  282. /package/dist/commands/{skill → memory}/read.d.ts +0 -0
  283. /package/dist/core/{canvas/browse → tui}/terminal.d.ts +0 -0
@@ -0,0 +1,440 @@
1
+ // @ts-check
2
+ /**
3
+ * Git / PR board data layer for the crtr `git-pr` view (a monitor archetype).
4
+ *
5
+ * Self-contained ESM, Node-builtins-only. Imports NOTHING from crtr so it ships
6
+ * verbatim (`cp -R src/builtin-views dist/builtin-views`) and is dynamically
7
+ * `import()`ed by the view at runtime where there is no TS toolchain.
8
+ *
9
+ * It shells `git` (local state) and `gh` (GitHub PR/CI state) over the view's
10
+ * cwd — exactly as the canvas view shells `crtr` and the LinkedIn view shells
11
+ * `capture`. Two independent domains:
12
+ * - `fetchGit()` → branch / upstream ahead·behind / working-tree status +
13
+ * churn / last commit. The PRIMARY instrument.
14
+ * - `fetchPrs(branch)` → open PRs for this repo (review decision + CI rollup),
15
+ * the current branch's PR first. BEST-EFFORT — its failure is
16
+ * a guided note in the PR section, never a crash; the git
17
+ * section still renders (graceful partial failure).
18
+ *
19
+ * NOTHING here throws. Every exported function returns a `Result<T>`; failures
20
+ * surface as a typed `ClientError` so the view renders guidance, not a crash.
21
+ * The error taxonomy (each → a view state):
22
+ * git: git-missing · not-a-repo · git-failed (empty-repo / no-upstream are
23
+ * NOT errors — they degrade to null fields the header renders).
24
+ * gh: gh-missing · gh-unauthed · gh-no-remote · gh-network · gh-failed.
25
+ *
26
+ * @module git-pr/client
27
+ */
28
+
29
+ import { execFile } from 'node:child_process';
30
+
31
+ // ── Types ────────────────────────────────────────────────────────────────────
32
+
33
+ /**
34
+ * One changed file, flattened from `git status --porcelain=v1 -z`.
35
+ * @typedef {Object} ChangedFile
36
+ * @property {string} path Display path (destination on a rename).
37
+ * @property {string} xy The two-char porcelain code (e.g. "M ", " M", "??", "UU").
38
+ * @property {'staged'|'modified'|'untracked'|'conflict'} cls Primary class (drives glyph + hue).
39
+ * @property {number} add Lines added (numstat; 0 if unknown/binary/untracked).
40
+ * @property {number} del Lines removed.
41
+ */
42
+
43
+ /**
44
+ * Local git state. `lastCommit` / `upstream` are null on an empty repo / a
45
+ * branch with no upstream — the header renders the degraded form, not an error.
46
+ * @typedef {Object} GitState
47
+ * @property {string} branch Branch name, or "(detached <sha>)" / "(no branch)".
48
+ * @property {boolean} detached True ⇒ detached HEAD.
49
+ * @property {string|null} upstream Tracking ref (e.g. "origin/main"), or null.
50
+ * @property {number} ahead Commits ahead of upstream.
51
+ * @property {number} behind Commits behind upstream.
52
+ * @property {{sha:string, subject:string, when:string}|null} lastCommit
53
+ * @property {ChangedFile[]} files Changed files, problems-first then path.
54
+ * @property {{staged:number, modified:number, untracked:number, conflict:number}} counts
55
+ */
56
+
57
+ /**
58
+ * One open pull request, flattened from `gh pr list --json …`.
59
+ * @typedef {Object} Pr
60
+ * @property {number} number
61
+ * @property {string} title
62
+ * @property {string} headRefName
63
+ * @property {boolean} isDraft
64
+ * @property {boolean} current True ⇒ this PR's head is the checked-out branch.
65
+ * @property {'approved'|'changes'|'review'} review Normalized reviewDecision.
66
+ * @property {'pass'|'fail'|'pending'|'none'} ci Rolled-up statusCheckRollup.
67
+ * @property {string} updatedAt ISO 8601 (drives the right-flush age).
68
+ */
69
+
70
+ /**
71
+ * Typed failure. `kind` drives the view state / guidance.
72
+ * @typedef {{kind:'git-missing', message:string}
73
+ * | {kind:'not-a-repo', message:string}
74
+ * | {kind:'git-failed', message:string}} GitError
75
+ */
76
+ /**
77
+ * @typedef {{kind:'gh-missing', message:string}
78
+ * | {kind:'gh-unauthed', message:string}
79
+ * | {kind:'gh-no-remote', message:string}
80
+ * | {kind:'gh-network', message:string}
81
+ * | {kind:'gh-failed', message:string}} GhError
82
+ */
83
+ /** @typedef {GitError|GhError} ClientError */
84
+
85
+ /**
86
+ * Never-throw return contract.
87
+ * @template T
88
+ * @typedef {{ok:true, data:T} | {ok:false, error:ClientError}} Result
89
+ */
90
+
91
+ // ── Result helpers ───────────────────────────────────────────────────────────
92
+
93
+ /** @template T @param {T} data @returns {{ok:true, data:T}} */
94
+ function ok(data) {
95
+ return { ok: true, data };
96
+ }
97
+ /** @param {ClientError} error @returns {{ok:false, error:ClientError}} */
98
+ function fail(error) {
99
+ return { ok: false, error };
100
+ }
101
+
102
+ // ── Process runner (never throws) ────────────────────────────────────────────
103
+
104
+ /**
105
+ * @typedef {Object} RunResult
106
+ * @property {boolean} spawned False ⇒ the binary is missing (ENOENT).
107
+ * @property {number} exitCode 0 on success; -1 if not spawned.
108
+ * @property {string} stdout
109
+ * @property {string} stderr
110
+ */
111
+
112
+ /**
113
+ * Run a binary. Resolves (never rejects). ENOENT ⇒ `spawned:false`. Runs in the
114
+ * view's cwd (the repo under inspection) so `git`/`gh` operate on the right tree.
115
+ * @param {string} bin
116
+ * @param {string[]} argv
117
+ * @returns {Promise<RunResult>}
118
+ */
119
+ function run(bin, argv) {
120
+ return new Promise((resolve) => {
121
+ execFile(
122
+ bin,
123
+ argv,
124
+ { maxBuffer: 32 * 1024 * 1024, encoding: 'utf8', cwd: process.cwd() },
125
+ (err, stdout, stderr) => {
126
+ const out = typeof stdout === 'string' ? stdout : '';
127
+ const errOut = typeof stderr === 'string' ? stderr : '';
128
+ if (err && /** @type {any} */ (err).code === 'ENOENT') {
129
+ resolve({ spawned: false, exitCode: -1, stdout: out, stderr: errOut });
130
+ return;
131
+ }
132
+ const code = err
133
+ ? typeof /** @type {any} */ (err).code === 'number'
134
+ ? /** @type {any} */ (err).code
135
+ : 1
136
+ : 0;
137
+ resolve({ spawned: true, exitCode: code, stdout: out, stderr: errOut });
138
+ }
139
+ );
140
+ });
141
+ }
142
+
143
+ /** @param {string} s @returns {string} */
144
+ function firstLine(s) {
145
+ const lines = String(s || '')
146
+ .split(/\r?\n/)
147
+ .map((l) => l.trim())
148
+ .filter(Boolean);
149
+ return lines.length ? lines[0] : '';
150
+ }
151
+
152
+ /** @param {string} v @returns {number} */
153
+ function toInt(v) {
154
+ const n = parseInt(String(v), 10);
155
+ return Number.isFinite(n) ? n : 0;
156
+ }
157
+
158
+ // ── git: status parsing ───────────────────────────────────────────────────────
159
+
160
+ /**
161
+ * Classify a porcelain XY pair into the file's primary class. A file can be both
162
+ * staged AND modified (e.g. `MM`); the worktree change wins the glyph (it's the
163
+ * freshest, un-committed edit), but `counts` tallies both columns independently.
164
+ * @param {string} x Index column.
165
+ * @param {string} y Worktree column.
166
+ * @returns {'staged'|'modified'|'untracked'|'conflict'}
167
+ */
168
+ function classify(x, y) {
169
+ if (x === 'U' || y === 'U' || (x === 'A' && y === 'A') || (x === 'D' && y === 'D')) return 'conflict';
170
+ if (x === '?' || y === '?') return 'untracked';
171
+ if (y !== ' ' && y !== '') return 'modified'; // worktree change (incl. D)
172
+ if (x !== ' ' && x !== '') return 'staged'; // index-only change
173
+ return 'modified';
174
+ }
175
+
176
+ /**
177
+ * Parse `git status --porcelain=v1 -z -uall` into changed files + column tallies.
178
+ * The `-z` form is NUL-terminated; a rename/copy entry is followed by an extra
179
+ * NUL field (the original path) which we consume.
180
+ * @param {string} stdout
181
+ * @returns {{files: ChangedFile[], counts: GitState['counts']}}
182
+ */
183
+ function parseStatus(stdout) {
184
+ const parts = String(stdout || '').split('\0');
185
+ /** @type {ChangedFile[]} */
186
+ const files = [];
187
+ const counts = { staged: 0, modified: 0, untracked: 0, conflict: 0 };
188
+ for (let i = 0; i < parts.length; i++) {
189
+ const entry = parts[i];
190
+ if (!entry || entry.length < 3) continue;
191
+ const x = entry[0];
192
+ const y = entry[1];
193
+ const path = entry.slice(3);
194
+ // A rename/copy carries the source path in the next NUL field — skip it.
195
+ if (x === 'R' || x === 'C' || y === 'R' || y === 'C') i++;
196
+ const cls = classify(x, y);
197
+ if (cls === 'conflict') counts.conflict++;
198
+ else if (cls === 'untracked') counts.untracked++;
199
+ else {
200
+ if (x !== ' ' && x !== '?' && x !== '') counts.staged++;
201
+ if (y !== ' ' && y !== '?' && y !== '') counts.modified++;
202
+ }
203
+ files.push({ path, xy: `${x}${y}`, cls, add: 0, del: 0 });
204
+ }
205
+ return { files, counts };
206
+ }
207
+
208
+ /**
209
+ * Parse `git diff [--cached] --numstat` into a path→churn map. Binary files
210
+ * (`-\t-`) and renames (`old => new`) are skipped — churn is best-effort.
211
+ * @param {string} stdout
212
+ * @param {Map<string,{add:number,del:number}>} into
213
+ */
214
+ function accChurn(stdout, into) {
215
+ for (const line of String(stdout || '').split(/\r?\n/)) {
216
+ if (!line.trim()) continue;
217
+ const m = line.split('\t');
218
+ if (m.length < 3) continue;
219
+ const add = m[0];
220
+ const del = m[1];
221
+ const path = m.slice(2).join('\t');
222
+ if (add === '-' || del === '-' || path.includes(' => ')) continue;
223
+ const prev = into.get(path) || { add: 0, del: 0 };
224
+ prev.add += toInt(add);
225
+ prev.del += toInt(del);
226
+ into.set(path, prev);
227
+ }
228
+ }
229
+
230
+ const CLASS_RANK = { conflict: 0, staged: 1, modified: 2, untracked: 3 };
231
+
232
+ // ── git: public API ───────────────────────────────────────────────────────────
233
+
234
+ /**
235
+ * Read local git state for the view's cwd. Returns a typed error for the three
236
+ * hard cases (no git binary, not a repo, a git command failing); empty-repo and
237
+ * no-upstream degrade to null fields, NOT errors.
238
+ * @returns {Promise<Result<GitState>>}
239
+ */
240
+ export async function fetchGit() {
241
+ const inside = await run('git', ['rev-parse', '--is-inside-work-tree']);
242
+ if (!inside.spawned) {
243
+ return fail({ kind: 'git-missing', message: 'git was not found on PATH — install git to use this view.' });
244
+ }
245
+ if (inside.exitCode !== 0 || inside.stdout.trim() !== 'true') {
246
+ return fail({ kind: 'not-a-repo', message: 'The current directory is not a git repository.' });
247
+ }
248
+
249
+ // Branch (empty ⇒ detached HEAD; works even before the first commit).
250
+ const br = await run('git', ['branch', '--show-current']);
251
+ let branch = br.stdout.trim();
252
+ let detached = false;
253
+ if (branch === '') {
254
+ detached = true;
255
+ const head = await run('git', ['rev-parse', '--short', 'HEAD']);
256
+ branch = head.exitCode === 0 ? `(detached ${head.stdout.trim()})` : '(no branch)';
257
+ }
258
+
259
+ // Upstream + ahead/behind (no upstream ⇒ null, ahead/behind 0).
260
+ let upstream = null;
261
+ let ahead = 0;
262
+ let behind = 0;
263
+ const up = await run('git', ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}']);
264
+ if (up.exitCode === 0) {
265
+ upstream = up.stdout.trim();
266
+ const counts = await run('git', ['rev-list', '--left-right', '--count', '@{upstream}...HEAD']);
267
+ if (counts.exitCode === 0) {
268
+ const nums = counts.stdout.trim().split(/\s+/);
269
+ behind = toInt(nums[0]); // left = in upstream, not HEAD
270
+ ahead = toInt(nums[1]); // right = in HEAD, not upstream
271
+ }
272
+ }
273
+
274
+ // Last commit (null on an empty repo).
275
+ let lastCommit = null;
276
+ const log = await run('git', ['log', '-1', '--pretty=%h%x1f%s%x1f%cI']);
277
+ if (log.exitCode === 0 && log.stdout.trim() !== '') {
278
+ const [sha, subject, when] = log.stdout.replace(/\n$/, '').split('\x1f');
279
+ lastCommit = { sha: sha || '', subject: subject || '', when: when || '' };
280
+ }
281
+
282
+ // Working-tree status. A failure here (rare once we know it's a repo) is the
283
+ // one genuine git-failed case.
284
+ const st = await run('git', ['status', '--porcelain=v1', '-z', '--untracked-files=all']);
285
+ if (!st.spawned) return fail({ kind: 'git-missing', message: 'git disappeared mid-read.' });
286
+ if (st.exitCode !== 0) {
287
+ return fail({ kind: 'git-failed', message: firstLine(st.stderr) || 'git status failed.' });
288
+ }
289
+ const { files, counts } = parseStatus(st.stdout);
290
+
291
+ // Churn (best-effort; never fatal).
292
+ /** @type {Map<string,{add:number,del:number}>} */
293
+ const churn = new Map();
294
+ const unstaged = await run('git', ['diff', '--numstat']);
295
+ if (unstaged.exitCode === 0) accChurn(unstaged.stdout, churn);
296
+ const staged = await run('git', ['diff', '--cached', '--numstat']);
297
+ if (staged.exitCode === 0) accChurn(staged.stdout, churn);
298
+ for (const f of files) {
299
+ const c = churn.get(f.path);
300
+ if (c) {
301
+ f.add = c.add;
302
+ f.del = c.del;
303
+ }
304
+ }
305
+
306
+ // Problems first (conflict → staged → modified → untracked), then by path.
307
+ files.sort((a, b) => {
308
+ const r = CLASS_RANK[a.cls] - CLASS_RANK[b.cls];
309
+ return r !== 0 ? r : a.path < b.path ? -1 : a.path > b.path ? 1 : 0;
310
+ });
311
+
312
+ return ok({ branch, detached, upstream, ahead, behind, lastCommit, files, counts });
313
+ }
314
+
315
+ // ── gh: rollups + classification ───────────────────────────────────────────────
316
+
317
+ const CI_BAD = new Set(['FAILURE', 'ERROR', 'CANCELLED', 'TIMED_OUT', 'ACTION_REQUIRED', 'STARTUP_FAILURE']);
318
+ const CI_GOOD = new Set(['SUCCESS', 'NEUTRAL', 'SKIPPED']);
319
+
320
+ /**
321
+ * Roll a PR's statusCheckRollup array into one verdict. CheckRun entries carry
322
+ * `status` (QUEUED/IN_PROGRESS/COMPLETED) + `conclusion`; StatusContext entries
323
+ * carry `state`. Any bad → fail; else any in-flight/unknown → pending; else pass.
324
+ * @param {any[]|undefined} items
325
+ * @returns {'pass'|'fail'|'pending'|'none'}
326
+ */
327
+ function rollupCi(items) {
328
+ if (!Array.isArray(items) || items.length === 0) return 'none';
329
+ let pending = false;
330
+ for (const it of items) {
331
+ const concl = String((it && it.conclusion) || '').toUpperCase();
332
+ const state = String((it && it.state) || '').toUpperCase();
333
+ const status = String((it && it.status) || '').toUpperCase();
334
+ if (CI_BAD.has(concl) || CI_BAD.has(state)) return 'fail';
335
+ if (status && status !== 'COMPLETED') pending = true;
336
+ else if (state && !CI_GOOD.has(state) && !CI_BAD.has(state)) pending = true; // PENDING/EXPECTED/QUEUED
337
+ else if (!concl && !state && !status) pending = true; // shapeless ⇒ treat as in-flight
338
+ }
339
+ return pending ? 'pending' : 'pass';
340
+ }
341
+
342
+ /** @param {string} d @returns {'approved'|'changes'|'review'} */
343
+ function normReview(d) {
344
+ const s = String(d || '').toUpperCase();
345
+ if (s === 'APPROVED') return 'approved';
346
+ if (s === 'CHANGES_REQUESTED') return 'changes';
347
+ return 'review'; // REVIEW_REQUIRED / "" / null
348
+ }
349
+
350
+ /**
351
+ * Classify a failed `gh` invocation. Auth first (the most common), then
352
+ * network, then a missing/!GitHub remote, else generic.
353
+ * @param {string} stderr
354
+ * @returns {GhError}
355
+ */
356
+ function classifyGh(stderr) {
357
+ const s = String(stderr || '').toLowerCase();
358
+ if (/auth|logged in|gh auth login|authentication|not logged/.test(s)) {
359
+ return { kind: 'gh-unauthed', message: 'gh is not authenticated — run `gh auth login`.' };
360
+ }
361
+ if (/could not resolve host|dial tcp|network is unreachable|no such host|timeout|temporary failure|connection refused/.test(s)) {
362
+ return { kind: 'gh-network', message: 'Cannot reach GitHub (offline?).' };
363
+ }
364
+ if (/could not resolve to a repository|no git remote|none of the git remotes|no default remote|not a github|head branch could not/.test(s)) {
365
+ return { kind: 'gh-no-remote', message: 'No GitHub remote for this repository.' };
366
+ }
367
+ return { kind: 'gh-failed', message: firstLine(stderr) || 'gh command failed.' };
368
+ }
369
+
370
+ // ── gh: public API ─────────────────────────────────────────────────────────────
371
+
372
+ /**
373
+ * Open PRs for this repo, the current branch's PR first then most-recently
374
+ * updated. BEST-EFFORT: a typed error means the PR section degrades to a guided
375
+ * note — the git section is unaffected.
376
+ * @param {string} branch The checked-out branch (to mark the current PR).
377
+ * @returns {Promise<Result<Pr[]>>}
378
+ */
379
+ export async function fetchPrs(branch) {
380
+ const r = await run('gh', [
381
+ 'pr',
382
+ 'list',
383
+ '--state',
384
+ 'open',
385
+ '--limit',
386
+ '30',
387
+ '--json',
388
+ 'number,title,headRefName,reviewDecision,statusCheckRollup,updatedAt,isDraft',
389
+ ]);
390
+ if (!r.spawned) {
391
+ return fail({ kind: 'gh-missing', message: 'gh (GitHub CLI) not found — install it to see PRs.' });
392
+ }
393
+ if (r.exitCode !== 0) return fail(classifyGh(r.stderr || r.stdout));
394
+
395
+ let arr;
396
+ try {
397
+ arr = JSON.parse(r.stdout.trim() || '[]');
398
+ } catch {
399
+ return fail({ kind: 'gh-failed', message: 'could not parse gh output as JSON.' });
400
+ }
401
+ if (!Array.isArray(arr)) arr = [];
402
+
403
+ /** @type {Pr[]} */
404
+ const prs = arr.map((p) => {
405
+ // Guard the whole element — gh shouldn't emit null array entries, but this
406
+ // file's contract is never-throw, so dereference only off a safe object.
407
+ const o = p || {};
408
+ const head = String(o.headRefName || '');
409
+ return {
410
+ number: typeof o.number === 'number' ? o.number : toInt(o.number),
411
+ title: String(o.title || '(untitled)'),
412
+ headRefName: head,
413
+ isDraft: !!o.isDraft,
414
+ current: head !== '' && head === branch,
415
+ review: normReview(o.reviewDecision),
416
+ ci: rollupCi(o.statusCheckRollup),
417
+ updatedAt: String(o.updatedAt || ''),
418
+ };
419
+ });
420
+
421
+ prs.sort((a, b) => {
422
+ if (a.current !== b.current) return a.current ? -1 : 1; // current branch first
423
+ return a.updatedAt < b.updatedAt ? 1 : a.updatedAt > b.updatedAt ? -1 : 0; // newest first
424
+ });
425
+
426
+ return ok(prs);
427
+ }
428
+
429
+ /**
430
+ * Introspection helper (not used at runtime): the exact shell-outs this client
431
+ * makes, so the shape can be eyeballed without a live repo.
432
+ * @returns {Record<string,string>}
433
+ */
434
+ export function describeCommands() {
435
+ return {
436
+ fetchGit:
437
+ 'git rev-parse --is-inside-work-tree · branch --show-current · rev-list --left-right --count @{u}...HEAD · log -1 · status --porcelain=v1 -z · diff [--cached] --numstat',
438
+ fetchPrs: 'gh pr list --state open --json number,title,headRefName,reviewDecision,statusCheckRollup,updatedAt,isDraft',
439
+ };
440
+ }