@blackbelt-technology/pi-agent-dashboard 0.4.6 → 0.5.1

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 (181) hide show
  1. package/AGENTS.md +339 -190
  2. package/README.md +50 -7
  3. package/docs/architecture.md +238 -23
  4. package/package.json +23 -13
  5. package/packages/extension/package.json +11 -3
  6. package/packages/extension/src/__tests__/ask-user-tool.test.ts +1 -1
  7. package/packages/extension/src/__tests__/build-provider-catalogue.test.ts +176 -0
  8. package/packages/extension/src/__tests__/command-handler.test.ts +68 -0
  9. package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +1 -1
  10. package/packages/extension/src/__tests__/markdown-image-inliner.test.ts +355 -0
  11. package/packages/extension/src/__tests__/no-tui-multiselect-arm-regression.test.ts +1 -1
  12. package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +68 -0
  13. package/packages/extension/src/__tests__/prompt-expander.test.ts +45 -0
  14. package/packages/extension/src/__tests__/provider-register-reload.test.ts +74 -0
  15. package/packages/extension/src/__tests__/retry-tracker.test.ts +147 -0
  16. package/packages/extension/src/__tests__/server-launcher.test.ts +24 -1
  17. package/packages/extension/src/__tests__/session-sync.test.ts +72 -0
  18. package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +105 -0
  19. package/packages/extension/src/ask-user-tool.ts +1 -1
  20. package/packages/extension/src/bridge-context.ts +1 -1
  21. package/packages/extension/src/bridge.ts +169 -4
  22. package/packages/extension/src/command-handler.ts +65 -2
  23. package/packages/extension/src/flow-event-wiring.ts +1 -1
  24. package/packages/extension/src/markdown-image-inliner.ts +268 -0
  25. package/packages/extension/src/multiselect-list.ts +1 -1
  26. package/packages/extension/src/pi-env.d.ts +16 -9
  27. package/packages/extension/src/prompt-expander.ts +50 -2
  28. package/packages/extension/src/provider-register.ts +132 -8
  29. package/packages/extension/src/retry-tracker.ts +123 -0
  30. package/packages/extension/src/server-launcher.ts +18 -1
  31. package/packages/extension/src/session-sync.ts +15 -1
  32. package/packages/extension/src/usage-limit-orderer.ts +76 -0
  33. package/packages/server/package.json +6 -6
  34. package/packages/server/src/__tests__/auto-attach-slug-defense.test.ts +104 -0
  35. package/packages/server/src/__tests__/bootstrap-install-from-list.test.ts +263 -0
  36. package/packages/server/src/__tests__/browser-gateway-snapshot-on-connect.test.ts +143 -0
  37. package/packages/server/src/__tests__/build-auth-status.test.ts +190 -0
  38. package/packages/server/src/__tests__/changelog-fs.test.ts +171 -0
  39. package/packages/server/src/__tests__/changelog-parser.test.ts +220 -0
  40. package/packages/server/src/__tests__/changelog-remote.test.ts +193 -0
  41. package/packages/server/src/__tests__/cli-parse.test.ts +22 -4
  42. package/packages/server/src/__tests__/cold-boot-openspec-broadcast.test.ts +161 -0
  43. package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +1 -1
  44. package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +1 -1
  45. package/packages/server/src/__tests__/directory-service-toctou.test.ts +1 -1
  46. package/packages/server/src/__tests__/directory-service.test.ts +1 -1
  47. package/packages/server/src/__tests__/doctor-route.test.ts +132 -0
  48. package/packages/server/src/__tests__/event-wiring-providers-list.test.ts +154 -0
  49. package/packages/server/src/__tests__/fixtures/pi-changelog-slice.md +180 -0
  50. package/packages/server/src/__tests__/fork-empty-session-preflight.test.ts +268 -0
  51. package/packages/server/src/__tests__/has-openspec-dir.test.ts +64 -0
  52. package/packages/server/src/__tests__/headless-pid-registry.test.ts +83 -0
  53. package/packages/server/src/__tests__/health-shape.test.ts +43 -0
  54. package/packages/server/src/__tests__/idle-timer-respects-terminals.test.ts +115 -0
  55. package/packages/server/src/__tests__/is-pi-process.test.ts +1 -1
  56. package/packages/server/src/__tests__/openspec-connect-snapshot.test.ts +92 -0
  57. package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +4 -4
  58. package/packages/server/src/__tests__/package-routes.test.ts +1 -1
  59. package/packages/server/src/__tests__/pending-fork-registry.test.ts +48 -24
  60. package/packages/server/src/__tests__/pi-changelog-integration.test.ts +165 -0
  61. package/packages/server/src/__tests__/pi-changelog-routes.test.ts +409 -0
  62. package/packages/server/src/__tests__/pi-core-checker.test.ts +155 -13
  63. package/packages/server/src/__tests__/pi-core-updater-managed-path.test.ts +236 -0
  64. package/packages/server/src/__tests__/pi-core-updater.test.ts +1 -1
  65. package/packages/server/src/__tests__/pi-dev-version-check.test.ts +184 -0
  66. package/packages/server/src/__tests__/pi-version-skew.test.ts +4 -4
  67. package/packages/server/src/__tests__/process-manager-codes.test.ts +80 -0
  68. package/packages/server/src/__tests__/process-manager-managed-path.test.ts +73 -0
  69. package/packages/server/src/__tests__/provider-auth-routes.test.ts +12 -4
  70. package/packages/server/src/__tests__/provider-auth-storage.test.ts +42 -11
  71. package/packages/server/src/__tests__/provider-catalogue-cache.test.ts +44 -0
  72. package/packages/server/src/__tests__/recommended-routes.test.ts +1 -1
  73. package/packages/server/src/__tests__/session-action-handler-spawn-error.test.ts +17 -2
  74. package/packages/server/src/__tests__/session-action-handler-spawn.test.ts +150 -0
  75. package/packages/server/src/__tests__/session-discovery-skill-firstmessage.test.ts +95 -0
  76. package/packages/server/src/__tests__/spawn-correlation-token-integration.test.ts +91 -0
  77. package/packages/server/src/__tests__/spawn-failure-log.test.ts +118 -0
  78. package/packages/server/src/__tests__/spawn-preflight.test.ts +91 -0
  79. package/packages/server/src/__tests__/spawn-register-watchdog.test.ts +250 -0
  80. package/packages/server/src/__tests__/spawn-token.test.ts +57 -0
  81. package/packages/server/src/__tests__/subscription-handler.test.ts +98 -6
  82. package/packages/server/src/__tests__/system-routes-reextract.test.ts +91 -0
  83. package/packages/server/src/__tests__/system-routes-spawn-failures.test.ts +84 -0
  84. package/packages/server/src/__tests__/terminal-manager.test.ts +45 -0
  85. package/packages/server/src/bootstrap-install-from-list.ts +232 -0
  86. package/packages/server/src/bootstrap-state.ts +18 -0
  87. package/packages/server/src/browser-gateway.ts +70 -24
  88. package/packages/server/src/browser-handlers/directory-handler.ts +4 -0
  89. package/packages/server/src/browser-handlers/handler-context.ts +9 -0
  90. package/packages/server/src/browser-handlers/session-action-handler.ts +159 -18
  91. package/packages/server/src/browser-handlers/subscription-handler.ts +50 -3
  92. package/packages/server/src/changelog-fs.ts +167 -0
  93. package/packages/server/src/changelog-parser.ts +321 -0
  94. package/packages/server/src/changelog-remote.ts +134 -0
  95. package/packages/server/src/cli.ts +23 -2
  96. package/packages/server/src/directory-service.ts +31 -0
  97. package/packages/server/src/event-wiring.ts +105 -5
  98. package/packages/server/src/headless-pid-registry.ts +54 -5
  99. package/packages/server/src/home-lock.d.ts +124 -0
  100. package/packages/server/src/home-lock.js +330 -0
  101. package/packages/server/src/home-lock.js.map +1 -0
  102. package/packages/server/src/idle-timer.ts +15 -1
  103. package/packages/server/src/pending-client-correlations.ts +73 -0
  104. package/packages/server/src/pending-fork-registry.ts +24 -12
  105. package/packages/server/src/pi-core-checker.ts +77 -17
  106. package/packages/server/src/pi-core-updater.ts +81 -15
  107. package/packages/server/src/pi-dev-version-check.ts +145 -0
  108. package/packages/server/src/pi-gateway.ts +10 -0
  109. package/packages/server/src/pi-version-skew.ts +12 -4
  110. package/packages/server/src/process-manager.ts +115 -21
  111. package/packages/server/src/provider-auth-handlers.ts +9 -0
  112. package/packages/server/src/provider-auth-storage.ts +83 -51
  113. package/packages/server/src/provider-catalogue-cache.ts +47 -0
  114. package/packages/server/src/routes/doctor-routes.ts +140 -0
  115. package/packages/server/src/routes/pi-changelog-routes.ts +194 -0
  116. package/packages/server/src/routes/pi-core-routes.ts +1 -1
  117. package/packages/server/src/routes/provider-auth-routes.ts +14 -1
  118. package/packages/server/src/routes/provider-routes.ts +4 -4
  119. package/packages/server/src/routes/system-routes.ts +38 -1
  120. package/packages/server/src/server.ts +85 -66
  121. package/packages/server/src/session-api.ts +54 -3
  122. package/packages/server/src/session-bootstrap.ts +27 -12
  123. package/packages/server/src/session-discovery.ts +11 -4
  124. package/packages/server/src/session-file-reader.ts +1 -1
  125. package/packages/server/src/session-scanner.ts +4 -2
  126. package/packages/server/src/spawn-failure-log.ts +130 -0
  127. package/packages/server/src/spawn-preflight.ts +82 -0
  128. package/packages/server/src/spawn-register-watchdog.ts +291 -0
  129. package/packages/server/src/spawn-token.ts +20 -0
  130. package/packages/server/src/terminal-manager.ts +12 -1
  131. package/packages/shared/package.json +1 -1
  132. package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +25 -17
  133. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +5 -4
  134. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +6 -5
  135. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +1 -0
  136. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +5 -4
  137. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +2 -1
  138. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +6 -3
  139. package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +1 -1
  140. package/packages/shared/src/__tests__/bootstrap-install-resolve-npm.test.ts +72 -0
  141. package/packages/shared/src/__tests__/browser-protocol-types.test.ts +47 -1
  142. package/packages/shared/src/__tests__/changelog-types.test.ts +78 -0
  143. package/packages/shared/src/__tests__/config.test.ts +48 -0
  144. package/packages/shared/src/__tests__/dashboard-starter.test.ts +40 -0
  145. package/packages/shared/src/__tests__/detached-spawn.test.ts +24 -0
  146. package/packages/shared/src/__tests__/doctor-core.test.ts +134 -0
  147. package/packages/shared/src/__tests__/doctor-fault-tolerance.test.ts +218 -0
  148. package/packages/shared/src/__tests__/doctor-format.test.ts +121 -0
  149. package/packages/shared/src/__tests__/install-managed-node-bootstrap-order.test.ts +68 -0
  150. package/packages/shared/src/__tests__/install-managed-node.test.ts +192 -0
  151. package/packages/shared/src/__tests__/installable-list.test.ts +130 -0
  152. package/packages/shared/src/__tests__/managed-node-path.test.ts +122 -0
  153. package/packages/shared/src/__tests__/managed-runtime-strategy.test.ts +74 -0
  154. package/packages/shared/src/__tests__/no-installable-list-in-bridge.test.ts +52 -0
  155. package/packages/shared/src/__tests__/no-raw-openspec-status-in-skills.test.ts +6 -1
  156. package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +56 -20
  157. package/packages/shared/src/__tests__/resolve-jiti.test.ts +140 -9
  158. package/packages/shared/src/__tests__/skill-block-parser.test.ts +153 -0
  159. package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +1 -1
  160. package/packages/shared/src/bootstrap-install.ts +197 -3
  161. package/packages/shared/src/browser-protocol.ts +155 -1
  162. package/packages/shared/src/changelog-types.ts +111 -0
  163. package/packages/shared/src/config.ts +15 -0
  164. package/packages/shared/src/dashboard-starter.ts +33 -0
  165. package/packages/shared/src/doctor-core.ts +821 -0
  166. package/packages/shared/src/index.ts +9 -0
  167. package/packages/shared/src/installable-list.ts +152 -0
  168. package/packages/shared/src/launch-source-flag.ts +14 -0
  169. package/packages/shared/src/launch-source-types.ts +18 -0
  170. package/packages/shared/src/openspec-activity-detector.ts +25 -7
  171. package/packages/shared/src/platform/detached-spawn.ts +13 -2
  172. package/packages/shared/src/platform/managed-node-path.ts +77 -0
  173. package/packages/shared/src/platform/node-spawn.ts +29 -21
  174. package/packages/shared/src/protocol.ts +54 -2
  175. package/packages/shared/src/resolve-jiti.ts +62 -9
  176. package/packages/shared/src/rest-api.ts +4 -0
  177. package/packages/shared/src/skill-block-parser.ts +115 -0
  178. package/packages/shared/src/tool-registry/__tests__/managed-runtime-strategy.test.ts +166 -0
  179. package/packages/shared/src/tool-registry/definitions.ts +33 -8
  180. package/packages/shared/src/tool-registry/strategies.ts +42 -0
  181. package/packages/shared/src/types.ts +64 -0
package/AGENTS.md CHANGED
@@ -5,9 +5,134 @@
5
5
 
6
6
  Web-based dashboard for monitoring and interacting with pi agent sessions remotely. Three-component architecture: bridge extension + Node.js server + React web client.
7
7
 
8
+ ## STOP — Docs-First Gate
9
+
10
+ **Before any build / run / install / setup / release / "how do I X" question: `grep -i <keyword> docs/faq.md README.md docs/` FIRST. No source reads until that returns nothing.**
11
+
12
+ If you read a script, config, or source file before grepping docs on a how-to, what-is question, you violated the protocol. Re-grep, then answer.
13
+
14
+ - ❌ User: "how do I ..." → read `<src files>` → guess answer
15
+ - ✅ User: "how do I ..." → `grep -ni '<words>' docs/faq.md` → quote the FAQ entry
16
+
17
+ - ❌ User: "what is ..." → read `scripts/build-installer.sh`, `forge.config.ts` → guess answer
18
+ - ✅ User: "what is ..." → `grep -ni '<words>' docs/index-*.md` → quote the entry
19
+
20
+ Full protocol (index-first for code questions, file-index splits, etc.) is in [Investigation Protocol — Index First](#investigation-protocol--index-first) below.
21
+
22
+ ## Code Instructions
23
+
24
+ Behavioral guidelines to reduce common LLM coding mistakes. Bias toward caution over speed. For trivial tasks, use judgment.
25
+
26
+ ### 1. Think Before Coding
27
+
28
+ **Don't assume. Don't hide confusion. Surface tradeoffs.**
29
+
30
+ Before implementing:
31
+ - State your assumptions explicitly. If uncertain, ask via `ask_user`.
32
+ - If multiple interpretations exist, present them — don't pick silently.
33
+ - If a simpler approach exists, say so. Push back when warranted.
34
+ - If something is unclear, stop. Name what's confusing. Ask.
35
+ - **Never speculate about code you have not opened.** If the user references a specific file, read it before answering. No claims about the codebase without investigation — grounded, hallucination-free answers only.
36
+ - Before any major change, check in with the user and confirm the plan.
37
+
38
+ ### 2. Simplicity First
39
+
40
+ **Minimum code that solves the problem. Nothing speculative.**
41
+
42
+ - No features beyond what was asked.
43
+ - No abstractions for single-use code.
44
+ - No "flexibility" or "configurability" that wasn't requested.
45
+ - No error handling for impossible scenarios.
46
+ - If you write 200 lines and it could be 50, rewrite it.
47
+ - **DRY:** if the same pattern appears in multiple places, extract a shared helper/class/component. Don't pre-extract for a single call site.
48
+
49
+ Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
50
+
51
+ ### 3. Surgical Changes
52
+
53
+ **Touch only what you must. Clean up only your own mess.**
54
+
55
+ When editing existing code:
56
+ - Don't "improve" adjacent code, comments, or formatting.
57
+ - Don't refactor things that aren't broken.
58
+ - Match existing style, even if you'd do it differently.
59
+ - If you notice unrelated dead code, mention it — don't delete it.
60
+
61
+ When your changes create orphans:
62
+ - Remove imports/variables/functions that YOUR changes made unused.
63
+ - Don't remove pre-existing dead code unless asked.
64
+
65
+ The test: every changed line should trace directly to the user's request.
66
+
67
+ ### 4. Goal-Driven Execution (TDD)
68
+
69
+ **Define success criteria. Loop until verified.**
70
+
71
+ Transform tasks into verifiable goals:
72
+ - "Add validation" → "Write tests for invalid inputs, then make them pass"
73
+ - "Fix the bug" → "Write a test that reproduces it, then make it pass"
74
+ - "Refactor X" → "Ensure tests pass before and after"
75
+
76
+ For implementation, use **TDD**: write or update tests first to define expected behaviour, verify they fail, then write the minimal implementation to make them pass.
77
+
78
+ For multi-step tasks, state a brief plan:
79
+ ```
80
+ 1. [Step] → verify: [check]
81
+ 2. [Step] → verify: [check]
82
+ 3. [Step] → verify: [check]
83
+ ```
84
+
85
+ Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
86
+
87
+ ### 5. Communication
88
+
89
+ - At every step, give a high-level explanation of what changed — don't dump diffs without summary.
90
+ - Use `ask_user` (not plain-text questions) when you need clarification, confirmation, or a choice.
91
+
92
+ ---
93
+
94
+ **These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.
95
+
96
+ ## Documentation Update Protocol
97
+
98
+ **Default assumption: your update does NOT belong in AGENTS.md.** AGENTS.md loads into every agent's context on every turn — every byte costs tokens. Route by kind:
99
+
100
+ | Kind of update | Goes in |
101
+ |---|---|
102
+ | New file, or per-file detail / change-history / contract / "See change: …" annotation | Matching per-area split `docs/file-index-<area>.md` (see `docs/file-index.md`). Add row in path-alphabetical order. |
103
+ | New top-level area / new split file | New row in `docs/file-index.md` splits table. Pointer in AGENTS.md only if architectural backbone. |
104
+ | Data flow, persistence, reconnection, protocol, config reference | `docs/architecture.md` |
105
+ | End-user / developer setup, prerequisites, CI badges, project structure | `README.md` |
106
+ | Cross-cutting rule EVERY agent needs on EVERY turn (rare) | AGENTS.md, ≤ 200 chars per row, no inline change history |
107
+
108
+ Rules:
109
+
110
+ 1. **AGENTS.md "Key Files" rows MUST stay ≤ 200 characters** — one terse purpose, no change-history, no contracts, no "See change: …" parentheticals.
111
+
112
+ 2. **Per-file detail goes into `docs/file-index-<area>.md`.** Search the matching split for the path; if a row exists, append/update; else add in path-alphabetical order.
113
+
114
+ 3. **If a split grows past ~50 KB**, sub-split it (e.g. `file-index-server-routes.md`) and update `docs/file-index.md`.
115
+
116
+ 4. **Long-form docs** (architecture decisions, rationale, protocol details) belong in `docs/architecture.md` or `docs/<topic>.md`. Reference from AGENTS.md with a one-line pointer, never inline.
117
+
118
+ 5. **When you create a new split doc**, add a one-line pointer in AGENTS.md so future agents find it.
119
+
120
+ 6. **Every write under `docs/` MUST be delegated to a general-purpose subagent with the caveman-style rule passed verbatim in its prompt.** Main agent orchestrates, never edits `docs/` directly.
121
+
122
+ **Caveman style** (all `docs/` prose — file-index rows, architecture notes, topic docs):
123
+ - Short declarative fragments. Drop articles (a/an/the) and most copulas (is/are/was) when meaning survives.
124
+ - Subject → verb → object, present tense. No hedging, no marketing voice, no "we", no "you".
125
+ - One fact per line/row. No restating context the file already establishes.
126
+ - Prefer concrete tokens (paths, function names, env vars, ports, exit codes) over prose.
127
+ - Keep symbols/identifiers verbatim; only connective tissue compresses.
128
+ - Example — verbose: "This module is responsible for parsing the user's input and then dispatching it to the correct handler based on the command prefix." Caveman: "Parses user input. Dispatches to handler by command prefix."
129
+
130
+ Why this exists: AGENTS.md ballooned to 107 KB (~27k tokens) by accreting per-change annotations on every row over months. Split was already done (file-index.md exists) but agents kept appending to AGENTS.md instead.
131
+
8
132
  ## Architecture
9
133
 
10
134
  See [docs/architecture.md](docs/architecture.md) for full details.
135
+ - See [docs/electron-bootstrap-flow.md](docs/electron-bootstrap-flow.md) for the Electron app→server bootstrap state machine and end states.
11
136
 
12
137
  - **Bridge Extension** (`src/extension/`) — Runs in every pi session, forwards events via WebSocket
13
138
  - **Dashboard Server** (`src/server/`) — Aggregates events, in-memory + JSON persistence, dual WebSocket servers
@@ -65,264 +190,301 @@ make clean # Destroy all cloned VMs
65
190
  | `qa/tests/` | Test suite (install, server, websocket, terminal, git) |
66
191
  | `qa/README.md` | Full setup and usage documentation |
67
192
 
193
+ ## Investigation Protocol — Index First
194
+
195
+ **Before reading source, consult `docs/file-index.md` and the relevant `docs/file-index-<area>.md` split.** The index is the cheapest map of the codebase — every architecturally significant file has a one-line purpose plus change-history pointers. Reading source blind wastes tokens and risks hallucination.
196
+
197
+ **For "how do I X" / build / run / setup questions: grep `README.md` + `docs/` first.** These already document every supported workflow (build, install, release, QA, troubleshooting). Reading source before checking docs wastes tokens and produces wrong answers (e.g. claiming a feature is missing when it ships). Check `docs/faq.md` for recurring questions.
198
+
199
+ Workflow for any non-trivial "where is X" / "how does Y work" question:
200
+
201
+ 1. **Pick the split** from `docs/file-index.md` table (shared / extension / server / client / electron / plugins / skills-misc) by path prefix or topic.
202
+ 2. **Delegate harvesting to a subagent** (`Explore` preferred). Give it:
203
+ - the user's question,
204
+ - the split file(s) to read,
205
+ - explicit instruction: *"return only rows + file paths relevant to the question — no source reads, no speculation."*
206
+ 3. **Receive a short list** of candidate files (≤ ~10 rows). Only then open source for the ones that matter.
207
+ 4. If the split lacks coverage, fall back to `rg` / `Explore` over the source tree — and add the missing row per the Documentation Update Protocol.
208
+
209
+ Why subagents: the splits are large (`file-index-server.md`, `file-index-client.md` each > 20 KB). Loading them into the main context on every question pollutes the budget. A subagent reads the split, returns the 5–10 relevant rows, and discards the rest.
210
+
211
+ Do **not**:
212
+ - Grep source before checking the index.
213
+ - Read a whole split file into the main agent's context — delegate.
214
+ - Trust the AGENTS.md "Key Files" backbone as exhaustive; it is a subset.
215
+
68
216
  ## Key Files
69
217
 
70
- > **Full file map**: see [`docs/file-index.md`](docs/file-index.md) — read it on demand when locating a file or understanding its full responsibilities (incl. change-history annotations).
218
+ > **Full file map**: see [`docs/file-index.md`](docs/file-index.md) — a thin index of per-area split files (`docs/file-index-<area>.md`). Read the relevant split on demand when locating a file or understanding its full responsibilities (incl. change-history annotations).
71
219
 
72
- This section lists only the **architectural backbone** — the files agents touch most often or need to know about for any non-trivial change. For everything else (renderers, individual tool cards, narrow helpers, build/CI internals) consult `docs/file-index.md`.
220
+ This section lists only the **architectural backbone** — the files agents touch most often or need to know about for any non-trivial change. For everything else (renderers, individual tool cards, narrow helpers, build/CI internals) consult the appropriate split via `docs/file-index.md`.
73
221
 
74
222
  ### Protocol & types
75
223
  | File | Purpose |
76
224
  |------|---------|
77
- | `src/shared/protocol.ts` | Extension↔Server WebSocket messages. Phase-1 Extension UI System adds `ui_modules_list` / `ui_data_list` (extension → server) and `ui_management` (server → extension); these messages MUST stay in `ExtensionToServerMessage` / `ServerToExtensionMessage` unions or esbuild strips the switch cases in production. See change: add-extension-ui-modal. |
78
- | `src/shared/browser-protocol.ts` | Server↔Browser WebSocket messages (all message types including PromptBus `prompt_request`/`prompt_dismiss`/`prompt_cancel` and Extension UI System Phase-1 `ui_modules_list`/`ui_data_list` (server → browser) + `ui_management` (browser → server) must be in the `ServerToBrowserMessage` / `BrowserToServerMessage` unions — `as any` switch cases are stripped by esbuild in production). **`prompt_request.metadata.toolCallId`** (change: fix-interactive-ui-reorder): optional field set by the bridge's `ctx.ui.{select, input, confirm, editor}` wrappers when the prompt is bound to a tool execution; consumed by the client reducer's `addInteractiveRequest` to pair the resulting `interactiveUi` row with its parent `toolResult`. The `metadata` field is typed `Record<string, unknown>`, so the addition is opt-in and forward/backward-compatible. **`SessionViewBrowserMessage` / `SessionUnviewBrowserMessage`** (change: session-card-unread-stripes): browser → server messages declaring the currently-displayed session id (`/session/:id`). Both must stay in the `BrowserToServerMessage` union. The browser is required to re-send `session_view` for the active session on every WebSocket reconnect (handled by `useViewDispatcher`). |
79
- | `src/shared/types.ts` | Data models (Session, Workspace, Event). **`DashboardSession.lastActivityAt?: number`** (epoch ms) is server-stamped on activity events via `isActivityEvent` in `event-status-extraction.ts`; cold-start seeded from `events.jsonl` mtime in `session-scanner.ts`; consumed by client `selectBadgeTimestamp` to render the session-card relative-time badge as time-since-last-activity instead of time-since-spawn. NOT persisted to `.meta.json`. See change: session-card-last-activity-badge. **`DashboardSession.unread?: boolean`** (change: session-card-unread-stripes): server-managed per-session unread bit. Set to `true` by `event-wiring.ts` when `isUnreadTrigger(...)` fires AND no browser is currently viewing the session AND the event is not part of a replay. Cleared when any browser sends `session_view`. Persisted to `.meta.json` so unread sessions stay flagged across server restarts. Bridges SHALL NOT send this field. Consumed by `SessionCard.tsx::getCardPulseClass` to render the cyan-stripes (`card-unread-pulse`, Tailwind `cyan-400`) decoration with lower priority than `card-input-pulse` (purple) and `card-working-pulse` (yellow). |
80
- | `src/shared/config.ts` | Shared config loader (`~/.pi/dashboard/config.json`). Exports `ReattachPlacement = "preserve" | "streaming-only" | "always"` + `parseReattachPlacement(raw)` validator (default `"always"`); the `reattachPlacement` config field controls how the server places re-registering bridges in `sessionOrder` after a dashboard restart. See change: reattach-move-to-front. Includes `openspec: OpenSpecPollConfig` block (`pollIntervalSeconds` 5–3600, `maxConcurrentSpawns` 1–16, `changeDetection` `"mtime"\|"always"`, `jitterSeconds` 0–60) with clamping via `parseOpenSpecPollConfig`. See change: optimize-openspec-poll-burst |
81
- | `src/shared/semaphore.ts` | Tiny FIFO semaphore (`createSemaphore(max)` → `{run, setMax, size}`). Used by `directory-service.ts` to cap concurrent `openspec` CLI spawns. Supports live resize via `setMax(n)` for runtime reconfig. |
82
- | `src/extension/bridge.ts` | **`hasRegisteredOnce: boolean`** local (default `false`, synced through `BridgeContext`) flips to `true` after the first `sendStateSync`; consumed by `session-sync.ts` to tag the `session_register` message with `registerReason: "spawn" | "reattach"` so the server can apply the configured `reattachPlacement` policy on dashboard-restart reattaches. See change: reattach-move-to-front. Main extension entry point (composes sync/tracker/flow modules, tracks `isAgentStreaming` in persistent BridgeState). **Invariant**: bridge code MUST NOT call `pi.newSession(...)` / `ctx.fork(...)` / `ctx.switchSession(...)` — enforced by `packages/extension/src/__tests__/no-session-replacement-calls.test.ts`. pi 0.69+ invalidates captured `pi`/`ctx` after these calls; the bridge re-captures state in `session_start` keyed on `event.reason ∈ {"new","fork","resume"}`. **PromptBus patch site**: patches `ctx.ui.select/input/confirm/editor/multiselect` in `session_start`; the TUI adapter handles `select/input/confirm/editor` only. `multiselect` is bridge-attached (pi has no native API) and routed exclusively through the bus to the `DashboardDefaultAdapter`'s browser dialog — decoded via `decodeMultiselectAnswer`. There is intentionally NO TUI adapter arm for multiselect: pi 0.70 RPC mode's `ctx.ui.custom` is a no-op, so any TUI arm awaiting it would auto-cancel the dashboard render in <1s (changes: fix-multiselect-auto-cancel-on-dashboard, fix-multiselect-tui-arm-self-cancel; regression-pinned by `no-tui-multiselect-arm-regression.test.ts`). **Per-message-fork entryId stamping** (change: fix-per-message-fork): `message_start` events now stamp a `nonce` (no entryId, since the user entry isn't persisted yet on pi 0.69+); `message_end` enrichment is deferred via `setTimeout(0)` (NOT `queueMicrotask`, which resolves inside pi's awaited extension dispatcher and misses `appendMessage`); a wrapped `ctx.sessionManager.appendMessage` emits `entry_persisted { entryId, nonce }` so the client reducer can back-fill the user-message bubble's `entryId`. |
225
+ | `src/shared/protocol.ts` | Extension↔Server WebSocket message types |
226
+ | `src/shared/browser-protocol.ts` | Server↔Browser WebSocket message types |
227
+ | `src/shared/types.ts` | Data models (Session, Workspace, Event) |
228
+ | `src/shared/config.ts` | Shared config loader (`~/.pi/dashboard/config.json`) |
229
+ | `src/shared/semaphore.ts` | Tiny FIFO semaphore (`createSemaphore(max)`) |
230
+ | `src/extension/bridge.ts` | Main bridge extension entry; PromptBus patch site, sync/tracker/flow composition |
83
231
  | `src/extension/bridge-context.ts` | Shared mutable state type + helpers for bridge modules |
84
- | `src/extension/session-sync.ts` | Session register, replay, and switch/fork handling. `sendStateSync` tags `session_register.registerReason` as `"spawn"` on first invocation per process and `"reattach"` thereafter, flipping `bc.hasRegisteredOnce`. `handleSessionChange` (new/fork/resume — fresh sessionId) ALWAYS tags `"spawn"` regardless of the flag. See change: reattach-move-to-front. |
232
+ | `src/extension/session-sync.ts` | Session register, replay, and switch/fork handling |
85
233
  | `src/extension/model-tracker.ts` | Model/thinking-level/git/name change detection |
86
234
  | `src/extension/flow-event-wiring.ts` | Flow event listener registration (flow:* → event_forward) |
87
- | `src/extension/connection.ts` | WebSocket with exponential backoff. **Auto-start suppression** (change: fix-restart-bridge-auto-start-race): exposes `pauseAutoStart(ms)` (idempotent extend-only) + `shouldSuppressAutoStart()`. The bridge calls `pauseAutoStart(quiesceMs)` on receipt of `server_restarting`; `server-auto-start.ts` consults `shouldSuppressAutoStart()` and skips the spawn step while the window is active so bridges never race the `restart-helper.ts` orchestrator. Discovery + reconnection are NOT suppressed. |
235
+ | `src/extension/connection.ts` | WebSocket with exponential backoff; auto-start suppression on `server_restarting` |
88
236
  | `src/extension/server-probe.ts` | TCP probe to detect running server |
89
- | `src/shared/server-identity.ts` | Identity-verified health check (`isDashboardRunning`) replacing bare TCP probes |
90
- | `src/shared/mdns-discovery.ts` | mDNS advertise/discover/browse for `_pi-dashboard._tcp` services. Exports `pickBestHost(service)` which returns `service.host` only when it matches the DNS-safe pattern `/^[A-Za-z0-9.-]+$/` (no leading/trailing hyphen); otherwise falls back to the first IPv4 address in `service.addresses`, then to any address, finally to the original host. Bonjour on macOS advertises the OS computer-name verbatim (e.g. `"MacBook 242"`) which contains spaces — without this fallback, saved known-server entries are unresolvable in the browser. `serviceToServer` calls `pickBestHost` so discovered servers always carry a host the browser can resolve. |
91
- | `src/extension/server-launcher.ts` | Auto-start server as detached process; captures **both stdout AND stderr** to `~/.pi/dashboard/server.log` (append mode) by passing `stdoutFd: logFd` alongside `logFd` — parity with `pi-dashboard start`'s `stdio: ["ignore", logFd, logFd]`. Exports pure `buildSpawnDetachedOptions` and `buildReadyTimeoutMessage`; the latter appends a `nodejs/node#58515` upgrade hint when `isKnownBadNode(process.version)` is true. |
237
+ | `src/shared/server-identity.ts` | Identity-verified health check (`isDashboardRunning`) |
238
+ | `src/shared/mdns-discovery.ts` | mDNS advertise/discover/browse for `_pi-dashboard._tcp` |
239
+ | `src/extension/server-launcher.ts` | Auto-start server as detached process; logs to `~/.pi/dashboard/server.log` |
92
240
  | `src/extension/command-handler.ts` | Command routing: `!`/`!!` bash, `/compact`, slash commands |
93
- | `src/extension/prompt-expander.ts` | Slash command → prompt template expansion (supports colon-to-hyphen aliasing: `/opsx:cmd` → `opsx-cmd.md`) |
241
+ | `src/extension/prompt-expander.ts` | Slash command → prompt template expansion |
94
242
  | `src/extension/dev-build.ts` | Dev build-on-reload helper (client build + server shutdown) |
95
- | `src/extension/server-auto-start.ts` | mDNS-first discovery → health check fallback → auto-start with concurrent launch detection |
96
- | `src/shared/session-meta.ts` | Session metadata sidecar (.meta.json) read/write helpers. **`SessionMeta.unread?: boolean`** (change: session-card-unread-stripes) mirrors `DashboardSession.unread`; persisted by `metaPersistence.save(...)` in `server.ts onChange` and restored by `session-scanner.ts::sessionFromMeta` so the unread bit survives server reboot. Backwards compatible — absent field reads as `undefined` (treated as `false`). |
97
- | `src/extension/process-metrics.ts` | Lightweight CPU/memory/event-loop metrics collector for heartbeats |
98
- | `src/extension/process-scanner.ts` | Child process detection via ps + PGID tracking (leaf-only, grandchild recursion) and PGID-based kill |
99
- | `src/client/components/ProcessList.tsx` | Session card process list with elapsed time and red ✕ kill button |
243
+ | `src/extension/server-auto-start.ts` | mDNS-first → health check → auto-start with concurrent launch detection |
244
+ | `src/shared/session-meta.ts` | Session metadata sidecar (.meta.json) read/write helpers |
245
+ | `src/extension/process-metrics.ts` | Lightweight CPU/memory/event-loop metrics for heartbeats |
246
+ | `src/extension/process-scanner.ts` | Child process detection via ps + PGID tracking and PGID-based kill |
247
+ | `src/client/components/ProcessList.tsx` | Session card process list with elapsed time and kill button |
100
248
  | `src/extension/git-info.ts` | Git branch/remote/PR detection (polled every 30s) |
101
249
  | `src/extension/git-link-builder.ts` | Git remote URL parsing and platform-specific links |
102
250
  | `src/server/git-operations.ts` | Server-side git commands: branch listing, checkout, init, stash pop |
103
251
  | `src/client/components/BranchPicker.tsx` | Typeahead branch picker with keyboard navigation |
104
252
  | `src/client/components/BranchSwitchDialog.tsx` | Checkout orchestration: dirty-state stash, pop prompt |
105
253
  | `src/client/lib/git-api.ts` | Client-side fetch helpers for git API endpoints |
106
- | `src/client/hooks/useImagePaste.ts` | Reusable clipboard-image-paste hook with controlled/uncontrolled modes. Uncontrolled (`useImagePaste()` with no args): hook owns `pendingImages` in local `useState` — used by `ExploreDialog` whose lifetime IS the dialog. Controlled (`useImagePaste({ images, onImagesChange })`): caller owns the array — used by `<CommandInput>` so App can key pending images by `sessionId`. `imageError` stays local in BOTH modes (auto-clears after 3 s, no value in lifting). Supports image/png, image/jpeg, image/gif, image/webp; 10 MB base64 cap. See change: lift-pending-images-to-app |
107
- | `src/extension/prompt-bus.ts` | PromptBus — unified prompt routing to registered adapters (TUI, dashboard, custom). First-response-wins, cross-adapter dismissal. |
108
- | `src/extension/dashboard-default-adapter.ts` | Built-in PromptBus adapter that renders prompts as generic interactive dialogs in dashboard chat |
109
- | `src/extension/ui-modules.ts` | Extension UI System Phases 1+2: `refreshUiModules(ctx)` partitions `probe.modules` by `kind` — `management-modal` → `ui_modules_list` (Phase 1, last-write-wins on duplicate `id`); `footer-segment`/`agent-metric`/`breadcrumb`/`gate`/`toast` → one `ext_ui_decorator` per descriptor (Phase 2, last-write-wins on `(kind,namespace,id)`; `namespace` validated against `/^[a-z0-9-]+$/`; `removed: true` forwarded verbatim). `subscribeUiInvalidate(ctx)` throttles `ui:invalidate` re-probes to 20/sec (50 ms leading + trailing) and emits one warning per offending burst (`INVALIDATE_RATE_CAP_PER_SEC = 20`). `handleUiManagement(ctx, msg)` re-emits browser-originated `ui_management` on `pi.events` with `_reply` injected and forwards synchronous `data.items` as `ui_data_list`. Wired into `bridge.ts` at `session_start` and the `onReconnect` callback. See changes: add-extension-ui-modal, add-extension-ui-decorations. |
110
- | `src/client/components/extension-ui/GenericExtensionDialog.tsx` | Phase-1 modal renderer for `ExtensionUiModule`. Supports `view.kind ∈ {table, grid, form}`, dispatches `ui_management { action: "list", event: dataEvent }` on mount for table/grid, gates `UiAction.confirm` through `ConfirmDialog`. MDI icons resolved via `mdi-icon-lookup.ts` (unknown keys render no icon). |
111
- | `src/client/components/extension-ui/decorator-utils.ts` | Phase-2 helper: `decoratorsOfKind(decorators, kind)` — typed filter over `Session.uiDecorators` returning only descriptors of the requested `DecoratorKind`. Used by every Phase-2 slot component. See change: add-extension-ui-decorations. |
112
- | `src/client/components/extension-ui/FooterSegmentSlot.tsx` | Phase-2 slot. Renders all `kind: "footer-segment"` descriptors as inline pills mounted inside `SessionHeader.tsx` to the right of the model/thinking-level info. Supports MDI `payload.icon` (resolved via `mdi-icon-lookup`) and `payload.tooltip` (HTML title attribute). |
113
- | `src/client/components/extension-ui/AgentMetricSlot.tsx` | Phase-2 slot. Renders `agent-metric` descriptors whose `payload.agentId` matches the `FlowAgentCard.agent.agentName` of the parent. Mounted inside `FlowAgentCard.tsx`. Orphan descriptors (no matching agent) render nothing. |
114
- | `src/client/components/extension-ui/BreadcrumbSlot.tsx` | Phase-2 slot. Renders the most-recently-cached `breadcrumb` descriptor as a horizontal step indicator at the top of `FlowDashboard.tsx`. Steps with `status: "done"` show a check; `"error"` show a red alert; the active step is matched against `payload.current` (or first `status: "active"`). |
115
- | `src/client/components/extension-ui/GateSlot.tsx` | Phase-2 slot. Aggregates `gate` descriptors targeting the same `flowId` via the pure helper `aggregateGateState(decorators, flowId)`: most-restrictive-wins (any `available: false` blocks; reasons concatenated). Mounted inline in `FlowLaunchDialog.tsx`; the dialog also reads `aggregateGateState` directly to disable the Run button. |
116
- | `src/client/components/extension-ui/ToastSlot.tsx` | Phase-2 slot. Top-right fixed tray mounted in `App.tsx`. Reads toast descriptors across every session in the `sessions` Map, stacks without deduplication, auto-dismisses each toast after `payload.durationMs` (default 5000ms; `0` = sticky), caps simultaneous-display at 5 (FIFO eviction — cache is unaffected). Manual dismiss via the close button latches the descriptor key into a local `dismissed` set so cache replay does not resurrect dismissed toasts. |
117
- | `src/client/lib/mdi-icon-lookup.ts` | `resolveMdiIcon(key) → string \| null` — looks up an MDI key string against `@mdi/js` exports; allowlists by `mdi`-prefix, returns `null` for unknown keys (no error). XSS-safe icon vocabulary for extension descriptors. |
118
- | `src/client/lib/prompt-component-registry.ts` | Client-side component registry mapping prompt type strings to render metadata (placement, component) |
119
- | `src/extension/ask-user-tool.ts` | `ask_user` tool registration (bundled in bridge, registered at session_start to avoid static tool-name conflicts with other extensions). `multiselect` dispatches through `polyfillMultiselect` (since pi-coding-agent's `ExtensionUIContext` has no native `multiselect` method). Tool description instructs agents: *"UI provides a Select all toggle; do not add one."* **Schema shape**: `parameters` is a single flat `Type.Object` (root `type: "object"`) — NOT a root `Type.Union` — because OpenAI's function-calling validator rejects root-level `anyOf` with *"schema must be a JSON Schema of 'type: \"object\"'"*. To restore Anthropic-friendly per-method strictness after commit a53933f, the root object now carries a body-level `oneOf` discriminator over `method` (confirm/select/multiselect/input/batch) with per-arm `required` + `minItems` constraints; sub-questions use the same flat-object + `oneOf` pattern with no `batch` arm (no nesting). `prepareArguments` still provides runtime rescue/normalization (`params` unwrap, `question`→`title`, stringified `options`/`questions`, batch synthesis/title backfill, `{label,value}` → labels), and `execute` retains empty-options guardrails. See changes: ask-user-multiselect-polyfill, refactor(schema)-restructure-ask-user-tool-schemas, fix-multiselect-auto-cancel-on-dashboard. |
120
- | `src/extension/multiselect-polyfill.ts` | `polyfillMultiselect(ctx, title, options, opts)` — primary path delegates to bridge-patched `ctx.ui.multiselect` (PromptBus `DashboardDefaultAdapter` → client `MultiselectRenderer`). Legacy fallback uses `ctx.ui.custom<T>()` + `MultiSelectList` for older / non-bridge contexts; this fallback is **a no-op in pi 0.70 RPC mode** (dashboard headless sessions) — pi-coding-agent defines `custom` as `async () => undefined` there — and is only effective in pure-TUI sessions if a future pi version restores `ctx.ui.custom` in RPC mode. Resolves to `string[]` (confirmed, including empty `[]`) or `undefined` (cancelled). Used for both single-question and batch sub-question `multiselect` paths in `ask-user-tool.ts`. See changes: fix-multiselect-auto-cancel-on-dashboard, fix-multiselect-tui-arm-self-cancel. |
121
- | `src/extension/multiselect-list.ts` | `MultiSelectList` component implementing pi-tui's `Component` interface. Keyboard contract: `↑↓`/`k`/`j` navigate, `Space` toggles current, `Enter` confirms (selected values in original option order), `Escape` cancels. **No "select all" binding in TUI** — the dashboard adapter provides that affordance. |
122
- | `src/shared/openspec-activity-detector.ts` | Detects OpenSpec activity from tool events; auto-attach requires only changeName (phase optional). Bash arm rejects flag-shaped capture tokens (`name.startsWith("-")` → `null`) so `openspec archive --help` and similar discovery commands no longer rename unnamed sessions to `--help`. See change: fix-openspec-flag-rename-bug. |
123
- | `src/shared/openspec-poller.ts` | OpenSpec CLI polling (shared, used by server DirectoryService). `buildOpenSpecData(listResult, statusResults, designProbeFactory?, specsProbeFactory?)` accepts two optional probe factories: a `DesignProbeFactory` that promotes the `design` artifact's status from `"ready"` to `"done"` when local file evidence (R1/R2/R3) satisfies design (see change: fix-openspec-design-detection), and a `SpecsProbeFactory` that promotes the `specs` artifact from `"ready"` to `"done"` when at least one `specs/**/*.md` file exists locally (see change: fix-openspec-specs-mtime-gate-blind-spot). Both overrides are promote-only, single-artifact-only, never demote; the post-override loop re-derives change-level `isComplete`. `pollOpenSpec` (sync) and `pollOpenSpecAsync` (async) wire real-fs probes via `createFsProbeFactory(cwd)` and `createFsSpecsProbeFactory(cwd)`. `directory-service.ts` does the same on the mtime-gated path. |
124
- | `src/shared/openspec-design-evidence.ts` | Pure rule evaluator + real-fs probe factory for the OpenSpec design-artifact override. `evaluateLocalDesignSatisfaction(changeDir, probe)` short-circuits R1 (`^design.*\.md$` file present) → R2 (`design/` folder with `*.md`) → R3 (`tasks.md` contains a `^\s*-\s+\[[ xX]\]\s` checkbox). `createFsDesignEvidenceProbe()` returns a defensive sync probe (try/catch on every fs call). Probe-injection keeps `buildOpenSpecData` pure and unit-testable without filesystem mocks. See change: fix-openspec-design-detection. |
125
- | `src/shared/openspec-specs-evidence.ts` | Pure rule evaluator + real-fs probe factory for the OpenSpec specs-artifact override. `evaluateLocalSpecsSatisfaction(changeDir, probe)` returns true iff at least one `*.md` file exists under `<changeDir>/specs/` (iterative DFS, short-circuits on first match). `createFsSpecsEvidenceProbe()` returns a defensive sync probe — every `readdirSync` is wrapped in try/catch so missing dirs / permission errors / symlink loops yield `false` rather than throwing. Mirror-shape of `openspec-design-evidence.ts`; consumed by `buildOpenSpecData` as a defense-in-depth layer over the `directory-service.ts` mtime gate (which now also watches `specs/**`). See change: fix-openspec-specs-mtime-gate-blind-spot. |
126
- | `.pi/skills/openspec-shared/scripts/effective-status.sh` | Bash wrapper around `openspec status --change <name> --json` that applies the same R1/R2/R3 promotion as the dashboard so OpenSpec workflow skills (`openspec-{continue,ff,apply,verify}-change`) and dashboard session-card buttons cannot disagree about a change's next-ready artifact. Inlines the rule logic via `find` + `grep -E` and uses `jq` for JSON mutation; falls back to raw CLI output if `jq` is absent. **Repo-lint** `packages/shared/src/__tests__/no-raw-openspec-status-in-skills.test.ts` blocks raw `openspec status ... --json` calls in any of the four governed skills (opt-out: `ban:openspec-status-ok`). Parity test: `packages/shared/src/__tests__/openspec-effective-status-script.test.ts`. See change: fix-openspec-design-detection. |
254
+ | `src/client/hooks/useImagePaste.ts` | Reusable clipboard-image-paste hook (controlled/uncontrolled modes) |
255
+ | `src/extension/prompt-bus.ts` | PromptBus — unified prompt routing to registered adapters |
256
+ | `src/extension/dashboard-default-adapter.ts` | Built-in PromptBus adapter rendering prompts as dashboard chat dialogs |
257
+ | `src/extension/ui-modules.ts` | Extension UI System Phase 1+2: refresh, throttle, manage |
258
+ | `src/client/components/extension-ui/GenericExtensionDialog.tsx` | Phase-1 modal renderer for `ExtensionUiModule` (table/grid/form) |
259
+ | `src/client/components/extension-ui/decorator-utils.ts` | Phase-2 helper `decoratorsOfKind` filter over `Session.uiDecorators` |
260
+ | `src/client/components/extension-ui/FooterSegmentSlot.tsx` | Phase-2 slot rendering footer-segment descriptors as inline pills |
261
+ | `src/client/components/extension-ui/AgentMetricSlot.tsx` | Phase-2 slot rendering agent-metric descriptors inside FlowAgentCard |
262
+ | `src/client/components/extension-ui/BreadcrumbSlot.tsx` | Phase-2 slot rendering breadcrumb as step indicator at FlowDashboard top |
263
+ | `src/client/components/extension-ui/GateSlot.tsx` | Phase-2 slot aggregating gate descriptors per flowId (most-restrictive-wins) |
264
+ | `src/client/components/extension-ui/ToastSlot.tsx` | Phase-2 slot rendering toast descriptors top-right with auto-dismiss + cap-of-5 |
265
+ | `src/client/lib/mdi-icon-lookup.ts` | `resolveMdiIcon(key)` against `@mdi/js` exports; null on unknown key |
266
+ | `src/client/lib/prompt-component-registry.ts` | Client component registry for prompt types (placement, component) |
267
+ | `src/extension/ask-user-tool.ts` | `ask_user` tool registration (confirm/select/multiselect/input/batch via flat oneOf schema) |
268
+ | `src/extension/multiselect-polyfill.ts` | `polyfillMultiselect` — bridge-patched multiselect with TUI fallback |
269
+ | `src/extension/multiselect-list.ts` | `MultiSelectList` pi-tui Component (↑↓/Space/Enter/Esc keyboard contract) |
270
+ | `src/shared/openspec-activity-detector.ts` | Detects OpenSpec activity from tool events; rejects flag-shaped tokens |
271
+ | `src/shared/openspec-poller.ts` | OpenSpec CLI polling: `buildOpenSpecData` with optional design + specs probe factories |
272
+ | `src/shared/openspec-design-evidence.ts` | Pure rule evaluator + fs probe for OpenSpec design-artifact override (R1/R2/R3) |
273
+ | `src/shared/openspec-specs-evidence.ts` | Pure rule evaluator + fs probe for OpenSpec specs-artifact override |
274
+ | `.pi/skills/openspec-shared/scripts/effective-status.sh` | Bash wrapper around `openspec status` applying R1/R2/R3 promotion |
127
275
  | `src/shared/state-replay.ts` | Synthesizes events from pi entries (shared, used by server + bridge) |
128
- | `src/shared/dashboard-plugin/slot-types.ts` | Frozen slot taxonomy: `SlotId` union, `Multiplicity`, `PayloadTier` enums, `SLOT_DEFINITIONS` record. Adding a slot = minor; removing = major. |
129
- | `src/shared/dashboard-plugin/manifest-types.ts` | `PluginManifest` and `PluginClaim` interfaces; includes `tab` field for `settings-section` claims and `fixture` flag for test-only plugins. |
130
- | `src/shared/dashboard-plugin/slot-props.ts` | `SlotPropsMap` and `SlotProps<SlotId>` typed prop contracts per slot id. Type-level test asserts all slot ids are covered. |
131
- | `src/shared/dashboard-plugin/plugin-status.ts` | `PluginStatus` (for `/api/health`) and `PluginConfigUpdate` (WebSocket broadcast payload). |
132
- | `src/shared/plugin-bridge-register.ts` | Plugin bridge entry management: `registerPluginBridge`, `deregisterPluginBridge`, `registerAllPluginBridges`. Manages `dashboard-<plugin-id>` keys in `settings.json#dashboardPluginBridges`; never touches user-owned `packages[]` entries. Atomic write (tmp+rename). |
133
- | `packages/dashboard-plugin-runtime/src/slot-registry.ts` | `createSlotRegistry()` typed `Map<SlotId, ClaimEntry[]>` pre-sorted by `(priority, pluginId)`. Filter helpers: `forSession`, `forFolder`, `forCommand`, `forTab`, `forToolName`. |
134
- | `packages/dashboard-plugin-runtime/src/manifest-validator.ts` | Hand-rolled manifest validator. Throws `ManifestValidationError` with `pluginId` and `reason`. No Zod dependency. |
135
- | `packages/dashboard-plugin-runtime/src/plugin-context.tsx` | `PluginContextProvider`, `CurrentPluginLayer`, `usePluginConfig<T>()`, `useAllSessions`, `useSessionState`, `usePluginLogger`, `usePluginSend`, `usePluginRouter`, `useSlotRegistry`, `applyPluginConfigUpdate`. Per-plugin context layer scopes hooks to the contributing plugin's id. |
136
- | `packages/dashboard-plugin-runtime/src/slot-consumers.tsx` | One component per slot id: `SidebarFolderSectionSlot`, `SessionCardBadgeSlot`, `SessionCardActionBarSlot`, `ContentViewSlot`, `ContentHeaderStickySlot`, `ContentInlineFooterSlot`, `AnchoredPopoverSlot`, `CommandRouteSlot`, `SettingsSectionSlot`, `ToolRendererSlot`. Each wraps contributions in `SlotErrorBoundary`. |
137
- | `packages/dashboard-plugin-runtime/src/slot-error-boundary.tsx` | Per-claim React error boundary. Logs `[slot-error-boundary] Plugin "<id>" slot "<slot>" threw:` and renders nothing for the failing claim without suppressing siblings. |
138
- | `packages/dashboard-plugin-runtime/src/vite-plugin/index.ts` | `viteDashboardPluginsPlugin(repoRoot?)` — generates `packages/client/src/generated/plugin-registry.tsx` with named imports (for tree-shaking). Watches manifests during dev; regenerates + triggers HMR on changes. Filters `fixture:true` plugins in production. |
139
- | `packages/dashboard-plugin-runtime/src/server/loader.ts` | `discoverPlugins(repoRoot?)` (single-process-cache glob), `loadServerEntries(deps)` (per-plugin dynamic-import + `registerPlugin` invocation, failure-isolated), `getPluginStatusStore()`. |
140
- | `packages/dashboard-plugin-runtime/src/server/server-context.ts` | `createServerPluginContext(deps, pluginId)` factory creates a `ServerPluginContext` scoped to a plugin with namespaced logger and typed config accessors. |
141
- | `packages/dashboard-plugin-runtime/src/server/config-validator.ts` | `validatePluginConfig`, `applySchemaDefaults` — Ajv JSON-Schema 7 validation for plugin config writes. |
142
- | `packages/dashboard-plugin-runtime/src/server/plugin-status-store.ts` | In-memory `PluginStatusStore` consumed by `/api/health.plugins[]`. |
143
- | `src/server/routes/plugin-config-routes.ts` | `POST /api/config/plugins/:id` — validates `:id`, validates body against `configSchema`, merges into `plugins.<id>.*`, atomic-writes, broadcasts `plugin_config_update`. Auth-gated. |
144
- | `packages/demo-plugin/` | Private workspace (`"private": true`, `"fixture": true`). Exercises `settings-section` (DemoSettings form) and `tool-renderer` (DemoToolRenderer green box for `toolName: "DashboardDemo"`). Excluded from production bundles. |
276
+ | `src/shared/dashboard-plugin/slot-types.ts` | Frozen slot taxonomy: `SlotId`, `Multiplicity`, `PayloadTier`, `SLOT_DEFINITIONS` |
277
+ | `src/shared/dashboard-plugin/manifest-types.ts` | `PluginManifest` and `PluginClaim` interfaces |
278
+ | `src/shared/dashboard-plugin/slot-props.ts` | `SlotPropsMap` and `SlotProps<SlotId>` typed prop contracts per slot id |
279
+ | `src/shared/dashboard-plugin/plugin-status.ts` | `PluginStatus` (for `/api/health`) and `PluginConfigUpdate` (WS payload) |
280
+ | `src/shared/plugin-bridge-register.ts` | Plugin bridge entry registration in pi `settings.json#dashboardPluginBridges` |
281
+ | `packages/dashboard-plugin-runtime/src/slot-registry.ts` | `createSlotRegistry()` typed `Map<SlotId, ClaimEntry[]>` with filter helpers |
282
+ | `packages/dashboard-plugin-runtime/src/manifest-validator.ts` | Hand-rolled manifest validator throwing `ManifestValidationError` |
283
+ | `packages/dashboard-plugin-runtime/src/plugin-context.tsx` | PluginContextProvider + per-plugin hook layer (config/log/send/router/registry) |
284
+ | `packages/dashboard-plugin-runtime/src/slot-consumers.tsx` | One component per slot id, wrapping contributions in `SlotErrorBoundary` |
285
+ | `packages/dashboard-plugin-runtime/src/slot-error-boundary.tsx` | Per-claim React error boundary; isolates failing claim from siblings |
286
+ | `packages/dashboard-plugin-runtime/src/vite-plugin/index.ts` | `viteDashboardPluginsPlugin` — generates plugin-registry.tsx, watches manifests |
287
+ | `packages/dashboard-plugin-runtime/src/server/loader.ts` | `discoverPlugins` + `loadServerEntries` (failure-isolated) + `getPluginStatusStore` |
288
+ | `packages/dashboard-plugin-runtime/src/server/server-context.ts` | `createServerPluginContext` — per-plugin scoped logger + config |
289
+ | `packages/dashboard-plugin-runtime/src/server/config-validator.ts` | Ajv JSON-Schema 7 validation for plugin config writes |
290
+ | `packages/dashboard-plugin-runtime/src/server/plugin-status-store.ts` | In-memory `PluginStatusStore` for `/api/health.plugins[]` |
291
+ | `src/server/routes/plugin-config-routes.ts` | `POST /api/config/plugins/:id` — validates and merges plugin config (auth-gated) |
292
+ | `packages/demo-plugin/` | Private fixture plugin exercising settings-section + tool-renderer slots |
293
+ | `packages/dashboard-plugin-skill/` | Pi skill `dashboard-plugin-scaffold`. Modes: `new` (scaffold packages/<id>-plugin/), `augment` (inject manifest + src/dashboard/ into pi-extension at cwd). |
145
294
  | `src/shared/stats-extractor.ts` | Extracts token/cost stats from turn_end events |
146
- | `src/server/session-stats-reader.ts` | Reads cumulative stats + context usage from session JSONL files at startup |
147
- | `src/server/server.ts` | HTTP + WebSocket server (composes route modules + wiring). Exports two pure helpers consumed by the bootstrap-state subscribe callback: `makeBootstrapTransitionHandler({ flushQueue, onTransitionToReady })` (stateful gate — fires both callbacks once per `installing → ready` transition, ignores the initial `ready` snapshot) and `runPostInstallRepair({ registry, directoryService, browserGateway })` (full `registry.rescan()` + per-cwd `refreshOpenSpec` with selective `openspec_update` broadcast on prior-empty / data-differs + per-cwd `refreshPiResources`; per-cwd failures isolated). The centralized hook covers all three install entry points (`runDegradedModeBootstrap`, REST `triggerUpgradePi`, REST `triggerRetry`). DEBUG-gated single-line diagnostic on completion. See change: fix-openspec-buttons-after-bootstrap-install. |
295
+ | `src/server/session-stats-reader.ts` | Reads cumulative stats + context usage from session JSONL files |
296
+ | `src/server/server.ts` | HTTP + WebSocket server (composes route modules + wiring) |
148
297
  | `src/server/routes/session-routes.ts` | REST routes: sessions, events, session-diff |
149
298
  | `src/server/routes/git-routes.ts` | REST routes: git branches, checkout, init, stash-pop |
150
- | `src/server/routes/file-routes.ts` | REST routes: file read, browse (with `detect=0\|1` opt-in classifier), browse-flags (bulk classifier), browse-mkdir, readme, pinned-dirs. See change: split-browse-flags |
299
+ | `src/server/routes/file-routes.ts` | REST routes: file read, browse, browse-flags, browse-mkdir, readme, pinned-dirs |
151
300
  | `src/server/routes/openspec-routes.ts` | REST routes: openspec-archive, pi-resources, pi-resource-file |
152
301
  | `src/server/routes/system-routes.ts` | REST routes: config, health, shutdown, tunnel, editors |
153
- | `src/server/event-wiring.ts` | Pi gateway → browser gateway event forwarding (replay suppression with `skipReplayInsert` dedup, flows refresh dedup, context usage extraction). Phase-1 Extension UI System: caches `ui_modules_list` on `Session.uiModules` and broadcasts; caches `ui_data_list` on `Session.uiDataMap[event]` with a per-event item cap (default 1000, last-write-wins on overflow) and broadcasts. Phase-2: `ext_ui_decorator` switch arm caches descriptors under `Session.uiDecorators[`${kind}:${namespace}:${id}`]` (upsert, or delete when `removed: true`) and broadcasts the message verbatim to subscribers; deleting an absent key is a no-op but still broadcasts. **Last-activity stamping** (change: session-card-last-activity-badge): every live (non-replay) `event_forward` whose `eventType` passes `isActivityEvent(...)` (`event-status-extraction.ts` allowlist — `prompt_send`, `message_*`, `turn_end`, `tool_execution_*`, `agent_*`, `bash_output`, `flow_*`, `architect_*`) updates `session.lastActivityAt = Date.now()` in memory and broadcasts at most once per 30 s per session via `lastActivityBroadcastAt: Map<sessionId, ms>`; the map entry is dropped on `session_unregister` so a subsequent re-register does not silently suppress its first broadcast. See changes: add-extension-ui-modal, add-extension-ui-decorations, session-card-last-activity-badge. **Unread-trigger evaluation** (change: session-card-unread-stripes): right after the `extractSessionUpdates` block, snapshots `{status, currentTool}` before/after and calls `isUnreadTrigger(eventType, before, after, payload)`; if true AND `viewedSessionTracker.isViewedByAnyone(sessionId) === false` AND `!replayingSessions.has(sessionId)`, stamps `session.unread = true` and broadcasts `session_updated`. The `viewedSessionTracker` dep is optional on `EventWiringDeps` for backward compatibility — wiring is opt-in. |
302
+ | `src/server/event-wiring.ts` | Pi gateway → browser gateway event forwarding; UI cache + activity stamping + unread trigger |
154
303
  | `src/server/idle-timer.ts` | Auto-shutdown idle timer with sleep-wake resilience |
155
304
  | `src/server/session-bootstrap.ts` | Startup session discovery and OpenSpec polling init |
156
305
  | `src/server/pi-gateway.ts` | Extension WebSocket gateway (port 9999) |
157
306
  | `src/server/browser-gateway.ts` | Browser WebSocket gateway (dispatches to handler modules) |
158
307
  | `src/server/browser-handlers/handler-context.ts` | Shared context type for browser message handlers |
159
- | `src/server/browser-handlers/subscription-handler.ts` | Subscribe/unsubscribe with async batched replay, backpressure, lazy loading. Exports `replayUiState(ws, sessionId, ctx)` for the Extension UI System; called immediately after every `replayPendingUiRequests` site (4 sites). Replay sends the cached `ui_modules_list` (Phase 1) → one `ui_data_list` per `(event, items)` entry (Phase 1) → one `ext_ui_decorator` per `Session.uiDecorators` cache entry (Phase 2; never with `removed: true` since deleted entries are absent). Replay ordering: events → pending UI requests → ui_modules_list → ui_data_list → ext_ui_decorator. See changes: add-extension-ui-modal, add-extension-ui-decorations. |
160
- | `src/server/browser-handlers/session-action-handler.ts` | Send prompt, abort, resume, spawn, shutdown, force kill, flow control. `handleSpawnSession` accepts an optional `SpawnSessionBrowserMessage.attachProposal` and enqueues it into `pendingAttachRegistry` BEFORE awaiting `spawnPiSession(...)` so a fast `session_register` cannot lose the intent (see change: add-folder-task-checker-and-spawn-attach). `handleForceKill` delegates the SIGTERM→wait→SIGKILL escalation to `killProcess` from `platform/process.ts` so Windows gets `taskkill /F /T /PID` (genuine tree kill). No direct `process.kill()` anywhere (enforced by `no-direct-process-kill.test.ts`). `handleSendPrompt` also intercepts `/reload` on headless sessions (gated by `headlessPidRegistry.getPid`) and delegates to `handleHeadlessReload`, which SIGTERMs the pi process and respawns with `--session <file> --mode continue` — pi 0.68.0 has no extension-accessible reload path in RPC mode, so kill-and-respawn is the only way to reload settings/extensions/skills for headless sessions. See changes: route-kill-paths-through-platform, headless-reload-via-respawn. |
161
- | `src/server/browser-handlers/session-action-helpers.ts` | Pure helpers for session-action-handler. `shouldInterceptReload(msg, headlessPidRegistry)` gates the headless-reload interception: exact `/reload` text, no images, PID tracked in registry. Extracted for testability. |
162
- | `src/client/components/ImageLightbox.tsx` | Full-size image lightbox with zoom/pan (useZoomPan), Esc/backdrop close |
308
+ | `src/server/browser-handlers/subscription-handler.ts` | Subscribe/unsubscribe with batched replay; replays UI state |
309
+ | `src/server/browser-handlers/session-action-handler.ts` | Send prompt, abort, resume, spawn, shutdown, force kill, flow control |
310
+ | `src/server/browser-handlers/session-action-helpers.ts` | Pure helpers for session-action-handler (`shouldInterceptReload`) |
311
+ | `src/client/components/ImageLightbox.tsx` | Full-size image lightbox with zoom/pan, Esc/backdrop close |
163
312
  | `src/client/components/CollapsedToolGroup.tsx` | Collapsed group of repeated tool calls with expand toggle |
164
313
  | `src/client/lib/group-tool-calls.ts` | Groups consecutive identical tool calls for chat display |
165
- | `src/client/lib/collapse-retried-errors.ts` | Two pure helpers used by `ChatView` to remove duplicate tool cards: `findRetriedErrorIds(messages)` returns ids of error `toolResult`s immediately superseded by a successful retry of the same tool (skipping `assistant`/`thinking`/`turnSeparator`/`rawEvent`/`commandFeedback`); `findActiveInteractiveToolResultIds(messages)` returns ids of `running` `toolResult`s paired with a *pending* `interactiveUi` message that follows them, so the running tool card can be hidden while the `InteractiveUiCard` is the sole interactive surface. See change: collapse-duplicate-tool-cards |
166
- | `src/client/components/RetriedErrorBadge.tsx` | One-line `⚠ <toolName> failed — retried ›` pill rendered by `ChatView` in place of full `ToolCallStep` for ids returned by `findRetriedErrorIds`. Click toggles an expanded view that re-uses `<ToolCallStep status="error">` with a "Hide failed attempt" toggle so the original validation message + `Received arguments:` JSON is still recoverable. See change: collapse-duplicate-tool-cards |
167
- | `src/server/browser-handlers/session-meta-handler.ts` | Rename, hide, unhide, attach/detach proposal, fetch, list. Attach/detach apply an **idempotent auto-rename rule** via the pure helpers in `packages/server/src/proposal-attach-naming.ts`: attach renames when name is empty OR equals the current `attachedProposal` (auto-set witness); detach reverts the name only when that equality holds. The same helpers are reused by the REST endpoints in `session-api.ts` to keep WS and REST in lockstep. See change: fix-mobile-attach-proposal-display. |
168
- | `src/server/proposal-attach-naming.ts` | Pure decision-matrix helpers `attachRenameTarget(session, changeName)` and `detachShouldClearName(session)` used by both the WS handler and the REST endpoint to compute the idempotent auto-rename / auto-revert. Equality witness: `name === attachedProposal`. See change: fix-mobile-attach-proposal-display. |
314
+ | `src/client/lib/collapse-retried-errors.ts` | Pure helpers `findRetriedErrorIds` + `findActiveInteractiveToolResultIds` for chat dedup |
315
+ | `src/client/components/RetriedErrorBadge.tsx` | One-line "tool failed — retried" pill replacing collapsed errored ToolCallStep |
316
+ | `src/server/browser-handlers/session-meta-handler.ts` | Rename, hide, unhide, attach/detach proposal, fetch, list |
317
+ | `src/server/proposal-attach-naming.ts` | Pure helpers `attachRenameTarget` + `detachShouldClearName` (idempotent auto-rename) |
169
318
  | `src/server/browser-handlers/terminal-handler.ts` | Create, kill, rename terminals |
170
319
  | `src/server/browser-handlers/directory-handler.ts` | Pin/unpin dirs, reorder, openspec refresh, pi-gateway forwards |
171
320
  | `src/server/memory-event-store.ts` | In-memory event buffer with LRU eviction, per-session cap, payload truncation |
172
321
  | `src/server/memory-session-manager.ts` | Pure in-memory session registry |
173
- | `src/client/components/FolderOpenSpecSection.tsx` | Folder-level OpenSpec UI: collapsible change list, refresh, bulk archive, archive button. Each change row's `N/M tasks` indicator is a button that opens the existing `TasksPopover` (one popover at a time; opening another row swaps the popover); a `mdiPlay` icon button rendered after the artifact letters invokes `onSpawnAttached(cwd, changeName)` to spawn a pi session with the change pre-attached. Both buttons stop propagation so the surrounding folder collapse is unaffected. See change: add-folder-task-checker-and-spawn-attach. |
174
- | `src/server/pending-attach-registry.ts` | In-memory FIFO queue of pending `attachProposal` intents per cwd. `enqueue(cwd, changeName)` is called from `handleSpawnSession` when the browser sets `SpawnSessionBrowserMessage.attachProposal`; `consume(cwd)` is called from `event-wiring.ts`'s `pi-gateway.onSessionRegistered` hook on every `session_register`, and the resolved name is applied via the shared `applyAttachProposal(sessionId, changeName, ctx)` helper in `session-meta-handler.ts` (same code as `handleAttachProposal`). Per-cwd cap = 8 (silent drop + warn), 60 s TTL on every read/write to avoid stranded intents from failed spawns. Cwd is normalized via `safeRealpathSync` + trailing-sep strip so `/p`, `/p/`, and symlink variants share a queue. In-memory only — not persisted. See change: add-folder-task-checker-and-spawn-attach. |
175
- | `src/client/components/ArchiveBrowserView.tsx` | Searchable archive browser: date-grouped list, two-level nav to artifact reader |
176
- | `src/client/hooks/useArchiveListing.ts` | Fetch hook + pure helpers (groupByDate, filterEntries) for archive endpoint |
177
- | `src/server/openspec-archive.ts` | Scans `openspec/changes/archive/` and returns structured ArchiveEntry list |
322
+ | `src/client/components/FolderOpenSpecSection.tsx` | Folder-level OpenSpec UI: change list, refresh, bulk archive, attach-spawn |
323
+ | `src/server/pending-attach-registry.ts` | In-memory FIFO queue of pending `attachProposal` intents per cwd (60s TTL) |
324
+ | `src/client/components/ArchiveBrowserView.tsx` | Searchable archive browser: date-grouped list, two-level nav |
325
+ | `src/client/hooks/useArchiveListing.ts` | Fetch hook + pure helpers for archive endpoint |
326
+ | `src/server/openspec-archive.ts` | Scans `openspec/changes/archive/` and returns ArchiveEntry list |
178
327
  | `src/client/components/SessionOpenSpecActions.tsx` | Session-level OpenSpec: searchable attach dialog, action buttons, detach |
179
328
  | `src/client/components/DialogPortal.tsx` | Portal wrapper rendering dialogs at document.body with scroll lock |
180
329
  | `src/client/components/PinDirectoryDialog.tsx` | Dialog to pin a directory (wraps PathPicker) |
181
330
  | `src/client/components/PathPicker.tsx` | Reusable keyboard-first path picker with typeahead directory list |
182
331
  | `src/client/lib/browse-api.ts` | Client-side browse API helper for PathPicker |
183
- | `src/server/browse.ts` | Directory listing + classification for the browse API. Two responsibilities kept deliberately separate: `listDirectories(dir, q, { detect })` does enumeration (cheap, single `readdir`; per-entry `.git`/`.pi` probes only when `detect: true`), and `classifyPaths(paths)` does bulk classification (≤ `MAX_FLAG_PATHS = 100`, `fs.access` fan-out bounded by `createSemaphore(32)`, any error → `{ isGit: false, isPi: false }`). `parseFlagsQuery(rawPaths)` validates the `paths=<json-array>` query for `GET /api/browse/flags`. Worktree-safe: detection uses `fs.access` (never `readdir`) so `.git` regular-files in worktrees still classify correctly. See change: split-browse-flags |
184
- | `src/server/pi-resource-scanner.ts` | Discovers pi extensions, skills, prompts from local, global, and package sources |
185
- | `src/server/package-manager-wrapper.ts` | Thin adapter around pi's `DefaultPackageManager` with operation serialization, progress forwarding, and session reload; delegates module resolution to `ToolRegistry.resolveModule("pi-coding-agent")`. Includes `move(req)` for scope-to-scope package moves: hybrid execution — npm/git/https sources reinstall at dest then remove from origin (single `busy` lock, coalesced reload), filesystem-path sources settings-edit only (uses `settingsManager.getGlobalSettings/getProjectSettings/setPackages/setProjectPackages` to preserve filter objects). Identity preflight via `computeIdentity` before any side-effect; partial-success (install OK, remove failed) is delivered through the `package_operation_complete` WS event's `partialSuccess` field. See change: unify-package-management-ui. |
186
- | `src/server/package-source-helpers.ts` | Pure helpers `parseSourceKind(source)` (npm/git/https/abs-path/rel-path) and `computeIdentity(source, settingsDir?)` (npm = bare name, git/https = url-no-ref, path = resolved absolute) mirroring pi's source-kind and dedup rules from `docs/packages.md`. Used by `package-manager-wrapper`'s `move()` arm-selection and identity preflight; safe for cross-platform tests (no `process.platform` branching). See change: unify-package-management-ui. |
187
- | `src/shared/tool-registry/registry.ts` | `ToolRegistry` service — single-source resolver for every external binary/module (pi, pi-coding-agent, openspec, npm, node, tsx, git, zrok, pi-dashboard). Ordered strategy chain per tool, per-resolution diagnostic trail, in-memory cache, override-aware |
188
- | `src/shared/tool-registry/definitions.ts` | Registers the standard tool set. Each definition declares an ordered strategy chain (override → bare-import → managed → npm-global → where) and a classifier (strategy → source). Includes build-time tools `electron` and `node-pty` (registered as `kind: "module"` returning the package directory; resolved hoist-aware via `bareImportPackageDirStrategy` with optional `searchPaths`). See change: register-build-time-tools |
189
- | `packages/shared/bin/pi-dashboard-resolve-tool.cjs` | Shell-callable resolver wrapper for the tool registry. CommonJS, dependency-free, invokable as `node packages/shared/bin/pi-dashboard-resolve-tool.cjs <tool-name> [--json]`. Mirrors the `override` + `bare-import` strategy semantics for build-time tools (electron, node-pty) so CI workflows / Dockerfiles / postinstall scripts can resolve hoisted-vs-nested layouts without depending on the shared package's TS build. **Strategy chain MUST stay in sync with `tool-registry/definitions.ts`** — enforced socially via cross-reference comments and via `no-hardcoded-node-modules-paths.test.ts`. See change: register-build-time-tools |
190
- | `src/shared/__tests__/no-hardcoded-node-modules-paths.test.ts` | Repo-level lint: scans `.github/workflows/publish.yml`, `.github/workflows/ci.yml`, `packages/electron/scripts/Dockerfile.build`, and the two `fix-pty-permissions.cjs` postinstalls for hardcoded `node_modules/electron` / `node_modules/node-pty` substrings outside the explicit allowlist. Fails with `file:line:col` citation. Replaces hand-rolled inline `require.resolve(...)` patterns with the registered tool registry. Mirrors `no-direct-process-kill.test.ts` and `no-raw-node-import.test.ts`. See change: register-build-time-tools |
191
- | `src/shared/tool-registry/strategies.ts` | Reusable resolution strategies: `overrideStrategy`, `managedBinStrategy`, `managedModuleStrategy`, `npmGlobalStrategy`, `whereStrategy`, `bareImportStrategy` (uses `createRequire` for sync probe). All take injectable `StrategyDeps` for tests |
192
- | `src/shared/tool-registry/overrides.ts` | Read/write `~/.pi/dashboard/tool-overrides.json` (machine-local, separate from `config.json`). Lazy-loaded, atomic write via tmp+rename, malformed-file tolerant |
193
- | `src/shared/tool-registry/types.ts` | `ToolDefinition`, `Strategy`, `StrategyResult`, `Resolution`, `Source`, `UnknownToolError`, `ModuleResolutionError` |
332
+ | `src/server/browse.ts` | Directory listing + classification for the browse API |
333
+ | `src/server/pi-resource-scanner.ts` | Discovers pi extensions, skills, prompts from local/global/package sources |
334
+ | `src/server/package-manager-wrapper.ts` | Wraps pi's DefaultPackageManager; adds `move()` for scope-to-scope moves |
335
+ | `src/server/package-source-helpers.ts` | Pure `parseSourceKind` + `computeIdentity` (npm/git/https/path identity rules) |
336
+ | `src/shared/tool-registry/registry.ts` | `ToolRegistry` — single-source resolver for every external binary/module |
337
+ | `src/shared/tool-registry/definitions.ts` | Registers standard tool set with ordered strategy chains |
338
+ | `packages/shared/bin/pi-dashboard-resolve-tool.cjs` | Shell-callable resolver wrapper (CommonJS, no TS deps) for build-time tools |
339
+ | `src/shared/__tests__/no-hardcoded-node-modules-paths.test.ts` | Repo-lint: forbid hardcoded `node_modules/electron` / `node_modules/node-pty` |
340
+ | `src/shared/tool-registry/strategies.ts` | Reusable resolution strategies (override / managed / npm-global / where / bare-import) |
341
+ | `src/shared/tool-registry/overrides.ts` | Read/write `~/.pi/dashboard/tool-overrides.json` with atomic write |
342
+ | `src/shared/tool-registry/types.ts` | `ToolDefinition`, `Strategy`, `Resolution`, error classes |
194
343
  | `src/shared/tool-registry/index.ts` | Barrel export + `getDefaultRegistry()` singleton accessor |
195
- | `src/server/routes/tool-routes.ts` | REST routes: `GET /api/tools`, `GET /api/tools/:name`, `POST /api/tools/rescan`, `PUT/DELETE /api/tools/:name`, `POST /api/tools/diagnostics` (text/plain export) |
196
- | `packages/shared/src/bootstrap-install.ts` | Shared bootstrap installer (`bootstrapInstall({ packages, managedDir?, progress?, npmArgv?, env?, registry? })`, `bootstrapInstallDefaults`, `ensureManagedDir`, `resolveNpmArgv`). Single entry point for pi/openspec/tsx install into `~/.pi-dashboard/` — called from the Electron wizard (via `packages/electron/src/lib/dependency-installer.ts` wrapper that adds bundled-node + offline-cacache concerns) and from the CLI first-run path (`cli.ts runDegradedModeBootstrap`). See change: unified-bootstrap-install. |
197
- | `packages/server/src/bootstrap-state.ts` | In-memory bootstrap state store (`createBootstrapState()`, `BootstrapState { status, progress, error, version, compatibility, bridgeRegistrationError }`, `BootstrapStateStore` with get/set/subscribe/dispose plus side-channel `setLastInstallPackages` / `getLastInstallPackages` so `POST /api/bootstrap/retry` re-runs the exact failed set instead of a hard-coded default). Partial `set()` supports `undefined` = clear semantics; `setLastInstallPackages` does NOT trigger subscribers (it's not part of the broadcast snapshot). |
198
- | `packages/server/src/routes/bootstrap-routes.ts` | REST routes: `GET /api/bootstrap/status`, `POST /api/bootstrap/upgrade-pi` (202+ticketId or 409), `POST /api/bootstrap/retry` (202 if failed, else 409). Trigger callbacks are injected so CLI wires them to `bootstrapInstall` while tests wire them to spies. |
199
- | `packages/server/src/bootstrap-queue.ts` | In-memory ticket queue (`createBootstrapQueue()`, `enqueue(handler)`, `flushAll()`, `size()`, `clear(reason)`, `onTicketComplete(listener)`). `server.ts` flushes on bootstrap-state transition to "ready" and wires `onTicketComplete` → `bootstrap_ticket_complete` WS broadcast so browsers holding a 202 ticketId learn the queued op's outcome. `session-api.ts gateOrEnqueue` uses the queue to defer session spawn during installs. On `clear`, pending tickets are rejected directly (reject closure stored on the entry) so no caller hangs at shutdown. |
200
- | `packages/server/src/pi-version-skew.ts` | Pi compatibility range reader (`readPiCompatibility` reads `piCompatibility` from `packages/server/package.json`; `readCurrentPiVersion` via `createRequire`, with `fs.realpathSync` on the registry-resolved bin path so symlinked npm-global launchers resolve to the real module's `package.json`), comparator (`parseVersion`, `compareVersions`, `isBelow`, `isAbove` supporting `0.x` wildcard), + `updateBootstrapCompatibility(store, pkgPath)` that writes the result into `bootstrapState.compatibility` with a 60 s cache. Below-minimum adds a 503-blocking `error` message. **CLI surface**: `cli.ts` calls `logCompatibilityWarning(bootstrapState)` after each `updateBootstrapCompatibility(...)` site and emits a stderr warning — 3-line red block on below-minimum (`⚠ pi X is below the required minimum Y … Run: pi-dashboard upgrade-pi`), single-line advisory on below-recommended, silent when in range. **Currently pinned**: `minimum: "0.70.0"`, `recommended: "0.70.0"`, `maximum: null` (lockstep — no backward compatibility for older pi versions). See changes: pi-zero-seventy-compat, warn-pi-version-skew-in-cli. |
201
- | `packages/client/src/hooks/useBootstrapStatus.ts` | Client hook for bootstrap state. Fetches `/api/bootstrap/status` on mount, subscribes to `bootstrap-status` `CustomEvent` dispatched by `useMessageHandler` on `bootstrap_status_update` WS broadcasts. Exposes `{ state, isLoading, error, refresh, retry, upgradePi }`. |
202
- | `packages/client/src/components/BootstrapBanner.tsx` | Banner mounted in `App.tsx` above `<MobileShell>`. Hidden at status="ready" with no compatibility hints; blue "Installing pi…" when installing; red "pi install failed — [Retry]" when failed; amber upgrade hints when `compatibility.upgradeRecommended` or `upgradeDashboard` is true. |
203
- | `src/client/lib/tools-api.ts` | Client-side fetch helpers for `/api/tools*` (`fetchTools`, `rescanAll`, `rescanOne`, `setOverride`, `clearOverride`, `downloadDiagnostics`) |
204
- | `src/client/components/ToolsSection.tsx` | Settings → General → **Tools** section. One row per registered tool: status badge, source, truncated path, expand-to-trail, override input, per-row rescan. Top-level: Rescan all / Reset overrides / Export diagnostics |
205
- | `src/server/npm-search-proxy.ts` | Cached proxy for npm registry search (`keywords:pi-package`) and README fetch |
344
+ | `src/server/routes/tool-routes.ts` | REST routes for `/api/tools*` (list, rescan, override, diagnostics) |
345
+ | `packages/shared/src/bootstrap-install.ts` | Shared bootstrap installer for pi/openspec/tsx into `~/.pi-dashboard/` |
346
+ | `packages/server/src/bootstrap-state.ts` | In-memory bootstrap state store (status/progress/error/version/compatibility) |
347
+ | `packages/server/src/routes/bootstrap-routes.ts` | REST routes: bootstrap status, upgrade-pi, retry |
348
+ | `packages/server/src/bootstrap-queue.ts` | In-memory ticket queue, flushes on bootstrap-state ready transition |
349
+ | `packages/server/src/pi-version-skew.ts` | Pi compatibility range reader + comparator + bootstrap compatibility writer |
350
+ | `packages/client/src/hooks/useBootstrapStatus.ts` | Client hook for bootstrap state (fetch + WS subscribe) |
351
+ | `packages/client/src/components/BootstrapBanner.tsx` | Banner above MobileShell for installing/failed/upgrade states |
352
+ | `src/client/lib/tools-api.ts` | Client-side fetch helpers for `/api/tools*` |
353
+ | `src/client/components/ToolsSection.tsx` | Settings → General → Tools section (per-tool status/source/override UI) |
354
+ | `src/server/npm-search-proxy.ts` | Cached proxy for npm registry search (`keywords:pi-package`) and README |
206
355
  | `src/server/routes/package-routes.ts` | REST routes: search, readme, installed, install, remove, update, check-updates |
207
356
  | `src/client/components/SortablePinnedGroup.tsx` | Drag-to-reorder wrapper for pinned directory groups |
208
357
  | `src/server/preferences-store.ts` | Global UI preferences (pinned dirs, session order) in `preferences.json` |
209
358
  | `src/server/meta-persistence.ts` | Per-session debounced `.meta.json` writer |
210
- | `src/server/session-scanner.ts` | Startup session discovery by scanning `~/.pi/agent/sessions/`. Each restored `DashboardSession` has `lastActivityAt` cold-start-seeded from the `events.jsonl` mtime (one `fs.statSync` per session, defensive try/catch — returns `undefined` on error so the badge falls back to `startedAt`). See change: session-card-last-activity-badge. The restored session also carries `unread` from `meta.unread` so unread sessions stay flagged across server restart. The `server.ts:273-279` cold-start "force status=ended" override only mutates `status` and `endedAt`, leaving `unread` intact — a session that was unread when the server stopped is still unread when it starts back up, even before its bridge reattaches. See change: session-card-unread-stripes. |
359
+ | `src/server/session-scanner.ts` | Startup session discovery scanning `~/.pi/agent/sessions/` |
211
360
  | `src/server/migrate-persistence.ts` | One-time migration from `sessions.json` + `state.json` to `.meta.json` |
212
- | `src/server/session-order-manager.ts` | Per-cwd session ordering with persistence. **Move-to-front semantic** (change: top-of-tier-on-status-change): exports `moveToFront(cwd, sessionId)` idempotent `remove + unshift` used by `server.ts onChange`'s ended→alive user-intent branch so the just-resumed card always surfaces at the top of the alive tier, even on repeated end→resume cycles. Bridge auto-reattach short-circuits before any mutation via `pendingResumeIntents.consume()`. |
213
- | `src/server/directory-service.ts` | Server-side session discovery, event loading, and OpenSpec polling. Uses mtime-gated per-directory cache (`DirCache`), shared FIFO semaphore, and deterministic per-cwd `phaseOffsetMs(cwd, jitterSeconds)` jitter (FNV-1a 32-bit hash). **Per-change watch set** (`perChangeArtifactPaths`) covers `<change>/`, `tasks.md`, `proposal.md`, `design.md`, plus `specs/`, every immediate `specs/<cap>/`, and every `specs/<cap>/spec.md` — `readdirSync` of `specs/` is try/catch-wrapped so missing dirs yield an empty fan-out (change: fix-openspec-specs-mtime-gate-blind-spot, which also wires `createFsSpecsProbeFactory(cwd)` into `buildOpenSpecData` as a second probe-factory argument so multi-spec authoring lights up green even when the gate misfires). **TOCTOU-safe stamping** (change: fix-openspec-mtime-gate-toctou): the per-change status loop captures `preCallMtime` *before* awaiting `runOpenSpecStatus` and stamps THAT value into the cache; if the post-call effective mtime differs, the entry is racy and the cache is left untouched (next gated tick re-polls). DEBUG-gated `console.warn` cites this change name when the discard branch fires. **Refresh contract**: `refreshOpenSpec(cwd)` is the user-clicked path and bypasses the gate via `pollOne(cwd, true)` — it's the manual escape hatch when the gate's heuristic is wrong. `pollDirectoryGated`, `onDirectoryAdded`, and the post-archive refresh in `handleOpenSpecBulkArchive` all call `pollOne(cwd, false)` so periodic / internal paths stay O(1) status spawns. `reconfigurePolling(cfg)` applies live config changes without a restart. Pi-resources scan lives on its own 5×-interval timer so it doesn't stack with the openspec burst. See changes: fix-openspec-specs-mtime-gate-blind-spot, fix-openspec-mtime-gate-toctou, fix-openspec-mtime-gate-blind-spots, optimize-openspec-poll-burst |
361
+ | `src/server/session-order-manager.ts` | Per-cwd session ordering with persistence; `moveToFront` semantic |
362
+ | `src/server/directory-service.ts` | Server-side session discovery, event loading, OpenSpec polling (mtime-gated) |
214
363
  | `src/server/pending-fork-registry.ts` | Tracks pending fork operations for session placement |
215
364
  | `src/server/pending-resume-registry.ts` | Queues prompts for auto-resume of ended sessions |
216
- | `src/server/pending-resume-intent-registry.ts` | In-memory `Map<sessionId, timestamp>` (default 60 s TTL, lazy expiry on read). `record(id)` is called by `handleResumeSession` (WS) and the REST `POST /api/session/:id/resume` handler immediately before `spawnPiSession`; `consume(id)` is called by `server.ts`'s `sessionManager.onChange` hook in the ended→alive branch. **4-way intent contract** (change: reattach-move-to-front): when `consume` returns `null` (no user intent tagged) the branch checks `OnChangeContext.registerReason`; if `"reattach"` it applies the configured `reattachPlacement` policy via `reattach-placement.ts::applyReattachPolicy`, otherwise (legacy bridge or genuine spawn) it returns early without mutation. Registry intents (`"front"` / `"keep"`) always win over `registerReason` per spec. The `if (!order.includes(sessionId))` guard inside the branch keeps drag-to-resume's dropped slot intact when the intent is present. In-memory only — NOT persisted across server restarts. See changes: preserve-session-order-on-reboot, reattach-move-to-front. |
217
- | `src/server/reattach-placement.ts` | Pure `decideReattachAction(policy, status)` + I/O `applyReattachPolicy(sessionId, cwd, policy, deps, priorStatus?)` for the bridge-reattach placement policy. Mapping: `"always"` → `moveToFront`; `"streaming-only"` → `moveToFront` iff `effectiveStatus === "streaming"` where `effectiveStatus = priorStatus ?? session.status`; `"preserve"` → no-op. **`priorStatus` is required for `streaming-only` to work**: `memory-session-manager.ts::register` unconditionally sets `status: "active"`, so without the prior value the policy would silently behave as `"preserve"`. Captured by `register()` from `existing?.status` BEFORE assembling the new session and forwarded via `OnChangeContext.priorStatus`. `applyReattachPolicy` is wired from `server.ts onChange` at TWO sites: (a) the existing ended→alive branch when the consumed registry intent is `null` and `ctx.registerReason === "reattach"`, and (b) a new alive→alive branch (`!isEnded && !wasEnded && ctx?.registerReason === "reattach"`) that handles the common case where the session was persisted as `"active"` so neither end-state transition fires. See change: reattach-move-to-front. |
365
+ | `src/server/pending-resume-intent-registry.ts` | In-memory user-resume intent map (60s TTL); 4-way intent contract on reattach |
366
+ | `src/server/reattach-placement.ts` | Pure `decideReattachAction` + I/O `applyReattachPolicy` for bridge-reattach placement |
218
367
  | `src/server/json-store.ts` | Atomic JSON file read/write helpers |
219
- | `src/server/process-manager.ts` | Session spawning: dispatches via `platform/spawn-mechanism.ts` `selectMechanism` → `tmux` / `wt` / `wsl-tmux` / `headless`. All mechanisms forward `sessionFile`/`mode` uniformly via `sessionFlagsToArgv`. Windows headless uses `spawnDetached` primitive with `detached: true` (PGID-equivalent via libuv) and stderr-to-file-fd (crash-visible). |
220
- | `src/shared/platform/detached-spawn.ts` | Three primitives: `spawnDetached` (libuv-correct detached defaults on every OS), `waitForNoCrash` (negative liveness — did it survive the window?), `waitForReady` (positive liveness — did a probe turn true?). All spawn sites with long-lived detached children delegate here. `SpawnDetachedOptions` exposes optional `stdoutFd` (defaults to `"ignore"`) and `logFd` (stderr, defaults to `"ignore"`); the bridge server-launcher sets both to the same fd for parity with the CLI. |
221
- | `src/shared/platform/node-version-check.ts` | Pure predicate `isKnownBadNode(version)` + message builder `buildNodeVersionWarning(version)` for Node ranges affected by [nodejs/node#58515](https://github.com/nodejs/node/issues/58515) (22.0.0–22.17.x and 24.1.0–24.2.x). Consumed by `packages/server/src/cli.ts` (preflight warning), `packages/extension/src/server-launcher.ts` (ready-timeout hint), and `packages/electron/src/lib/doctor.ts` (Node runtime compatibility row). |
222
- | `src/shared/platform/preload-fastify.ts` | Shared resolver `resolvePreloadFastifyPath(): string \| null` — returns absolute native path (never `file://`) to `packages/server/preload-fastify.cjs`. Used by all four server-spawn sites (cli daemon, bridge launcher, Electron lifecycle, restart-helper orchestrator) to inject `--require <preloadPath>` BEFORE `--import <jitiLoader>` so Fastify's CJS chain is cached before any ESM hook runs — sidesteps nodejs/node#58515 race-independently. See change: `preload-fastify-cjs`. |
223
- | `packages/server/preload-fastify.cjs` | CommonJS preload file — loaded by Node's legacy synchronous CJS loader via `--require`. Populates `require.cache` with `fastify` + `@fastify/ajv-compiler` + `@fastify/ajv-compiler/standalone` so the ESM→CJS translator short-circuits on later imports. MUST stay `.cjs`; MUST NOT use ESM syntax. |
224
- | `src/shared/platform/spawn-mechanism.ts` | `SpawnMechanism` enum (`tmux`/`wt`/`wsl-tmux`/`headless`) + pure `selectMechanism({ platform, userStrategy, electronMode, available })` selector. `buildWtArgs` builds argv for Windows Terminal `new-tab`. `sessionFlagsToArgv` is the uniform flag builder every mechanism MUST call. |
225
- | `src/shared/platform/process-identify.ts` | `findPidByMarker` + `isProcessLikePi` + `isPiCommandLine` — consolidates the three `process.platform === "win32"` branches that previously lived inside `session-action-handler.ts`. Windows stubs are documented (command-line lookup goes via `headlessPidRegistry` instead). |
226
- | `src/shared/platform/process.ts` | **Sole source of process termination + liveness primitives**: `isProcessAlive(pid)` (signal 0), `killProcess(pid, {timeoutMs})` (Windows `taskkill /F /T /PID`, POSIX `SIGTERM` → wait → `SIGKILL` tree kill), `killPidWithGroup(pid, sig)` (POSIX `kill(-pid, sig)`, Windows direct kill). Every `process.kill(...)` call outside this file is banned by `no-direct-process-kill.test.ts`. See change: route-kill-paths-through-platform. |
227
- | `src/shared/platform/node-spawn.ts` | **Sole source of `node --import <loader> <entry>` argv construction**: `toFileUrl(pathOrUrl)` (idempotent path → file:// URL, handles Windows drive letters on POSIX hosts), `spawnNodeScript(opts)` (wraps both loader and entry positions as file:// URLs before delegating to `platform/exec.ts::spawn`). Every `spawn(process.execPath, ["--import", ...])` call outside this file with raw path arguments is banned by `no-raw-node-import.test.ts`. Fixes `ERR_UNSUPPORTED_ESM_URL_SCHEME` on non-C: Windows drives (e.g. `B:\`) where Node's drive-letter heuristic for entry-script paths has known gaps. See change: fix-windows-entry-script-url. **Jiti version contract** (change: fix-electron-windows-installer-and-server-bootstrap, Defect 2): `shouldUrlWrapEntry()`'s Windows-non-tsx arm assumes the jiti loader is from `@mariozechner/pi-coding-agent@0.70.x` (jiti 2.x with the `file:///` triple-slash URL handling fix). Newer jiti versions (e.g. jiti 2.6.5 in pi 0.71.x) misnormalize triple-slash URLs and break the contract. The contract is defended by Defect 1's fix (managed-dir population from the offline cacache pins pi to 0.70.0); regression-pinned by `node-spawn-jiti-contract.test.ts` which asserts `offline-packages.json` keeps the pin in the supported `0.70.x` range AND the docstring documents the contract. |
228
- | `src/shared/__tests__/no-raw-node-import.test.ts` | Repo-level lint: scans `packages/*/src/` (excluding `platform/node-spawn.ts` + `resolve-jiti.ts` + `__tests__/`) for `spawn(...)` argv with `"--import"` / `"--loader"` followed by raw identifiers not wrapped in `toFileUrl(...)` / `pathToFileURL(...)`. Mirrors `no-direct-process-kill.test.ts` pattern. |
229
- | `src/shared/__tests__/no-direct-process-kill.test.ts` | Repo-level lint: scans `packages/*/src/` (excluding platform/ and `__tests__/`) for `process.kill(` calls and fails with file:line if any are found. Mirrors `no-direct-child-process.test.ts`. |
230
- | `src/shared/__tests__/bootstrap/` | In-memory bootstrap resolution harness (memfs-backed). `harness.ts` (withFakeEnv + layer), `fixtures/` (managed/npm-g/electron/dev-monorepo/settings-json layouts), `assertions.ts` (snapshotTrail + snapshotSettingsDelta with `<HOME>` / `<NPM_ROOT>` normalization), `scenarios.ts` (1080-cell cube: platform × dash × pi × settings × env), `scenarios-skipped.ts` (bulk-skip manifest with documented reasons), `cube.test.ts` (fail-closed sweep), `families/*.test.ts` (30+ registered scenario cells across A-K). Run via `npm run test:bootstrap`. See change: bootstrap-resolution-harness. |
368
+ | `src/server/process-manager.ts` | Session spawning via `selectMechanism` → tmux/wt/wsl-tmux/headless |
369
+ | `src/shared/platform/detached-spawn.ts` | `spawnDetached` + `waitForNoCrash` + `waitForReady` primitives |
370
+ | `src/shared/platform/node-version-check.ts` | `isKnownBadNode` + `buildNodeVersionWarning` (nodejs/node#58515 ranges) |
371
+ | `src/shared/platform/preload-fastify.ts` | Resolver returning native path to `preload-fastify.cjs` for `--require` injection |
372
+ | `packages/server/preload-fastify.cjs` | CJS preload populating `require.cache` with fastify + ajv-compiler |
373
+ | `src/shared/platform/spawn-mechanism.ts` | `SpawnMechanism` enum + `selectMechanism` selector + `sessionFlagsToArgv` |
374
+ | `src/shared/platform/process-identify.ts` | `findPidByMarker` + `isProcessLikePi` + `isPiCommandLine` |
375
+ | `src/shared/platform/process.ts` | Sole source of process termination + liveness primitives (kill/alive/group) |
376
+ | `src/shared/platform/node-spawn.ts` | Sole source of `node --import <loader> <entry>` argv construction |
377
+ | `src/shared/__tests__/no-raw-node-import.test.ts` | Repo-lint: forbid raw `--import`/`--loader` argv outside `node-spawn.ts` |
378
+ | `src/shared/__tests__/no-direct-process-kill.test.ts` | Repo-lint: forbid `process.kill(` outside `platform/` |
379
+ | `src/shared/__tests__/bootstrap/` | In-memory bootstrap resolution harness (memfs-backed); 1080-cell scenario cube |
231
380
  | `src/server/editor-registry.ts` | Detects available native editors (running processes + CLI) |
232
- | `src/server/editor-manager.ts` | Lifecycle manager for code-server child processes (spawn, stop, idle, heartbeat) |
381
+ | `src/server/editor-manager.ts` | Lifecycle manager for code-server child processes |
233
382
  | `src/server/editor-proxy.ts` | Reverse proxy for `/editor/:id/*` to code-server instances |
234
383
  | `src/server/editor-detection.ts` | Auto-detect code-server/openvscode-server binary on PATH |
235
384
  | `src/server/routes/editor-routes.ts` | REST routes: editor start, stop, heartbeat, status, detect |
236
- | `src/server/event-status-extraction.ts` | Extracts session status/tool updates from events (incl. flow metadata). Hosts two pure classifiers consumed by `event-wiring.ts`: `isActivityEvent(eventType)` (allowlist driving `lastActivityAt` stamping; see change: session-card-last-activity-badge) and `isUnreadTrigger(eventType, before, after, payload)` (returns true on `streaming→idle\|active`, on `currentTool→"ask_user"`, and on `agent_end` with truthy `payload.error`; see change: session-card-unread-stripes). |
237
- | `src/server/viewed-session-tracker.ts` | In-memory `Map<sessionId, Set<WebSocket>>` registry of which browser has which session displayed (`/session/:id`). Created by `browser-gateway.ts`, exposed on `BrowserGateway.viewedSessionTracker`, and threaded into `wireEvents({ ..., viewedSessionTracker })`. `view`/`unview` are called from the `session_view`/`session_unview` switch arms; `unviewAll(ws)` is called on every WS `close` so disconnected browsers cannot hold sessions in the viewed state. `isViewedByAnyone(sessionId)` gates the unread-trigger stamp in `event-wiring.ts`. Read state is GLOBAL across browsers — mirrors mail/Slack. In-memory only. See change: session-card-unread-stripes. |
385
+ | `src/server/event-status-extraction.ts` | Extracts session status/tool updates; hosts `isActivityEvent` + `isUnreadTrigger` |
386
+ | `src/server/viewed-session-tracker.ts` | Per-browser viewed-session map; gates unread-trigger stamping |
238
387
  | `src/server/headless-pid-registry.ts` | Maps headless child PIDs to session IDs |
239
388
  | `src/server/auth.ts` | OAuth2 authentication: provider registry, JWT helpers, user allowlist |
240
389
  | `src/server/provider-auth-handlers.ts` | Pi provider OAuth handlers (Anthropic, Codex, GitHub Copilot, Gemini CLI, Antigravity) |
241
- | `src/server/provider-auth-storage.ts` | Read/write ~/.pi/agent/auth.json with lockfile for pi provider credentials |
390
+ | `src/server/provider-auth-storage.ts` | Read/write `~/.pi/agent/auth.json` with lockfile for pi provider credentials |
242
391
  | `src/server/routes/provider-auth-routes.ts` | REST routes: provider OAuth authorize/exchange/callback, device-code, API key CRUD |
243
- | `src/server/routes/provider-routes.ts` | REST routes: custom LLM provider CRUD (`GET/PUT /api/providers`) + **`POST /api/providers/test`** connection probe (reuses `provider-probe.ts`). See change: `hot-reload-custom-providers` |
244
- | `src/server/provider-probe.ts` | Pure per-API-type probe builders (`buildProbeRequest` for `openai-completions`/`openai-responses`/`anthropic-messages`/`google-generative-ai`) + `resolveProbeApiKey` (handles literal, `$ENV_VAR`, and `***` REDACTED sentinel via injected providers reader) + I/O-bearing `probeProvider` (8 s timeout, never echoes apiKey in error text, caps body excerpts at 500 chars). Used by the `/api/providers/test` route and shared with the bridge's discovery path |
245
- | `src/extension/provider-register.ts` | Reads `~/.pi/agent/providers.json`, calls `pi.registerProvider()` with auto-discovered models, exports `reloadProviders(pi)` + `onProviderChanged(cb)`. `reloadProviders` diffs the current file against a module-level `lastRegistered` snapshot and applies add/remove/change via `registerEntry` / `pi.unregisterProvider`. Called from the bridge's `credentials_updated` handler BEFORE `modelRegistry.refresh()` so new providers appear in `/model` without a session restart. Every discovered model is enriched via `enrichModelMetadata(id, api, probe)` where `probe` wraps pi's `modelRegistry.find()` (captured from `ctx.modelRegistry` at the first `session_start` event; `model_select` is a fallback capture point) — this resolves accurate `contextWindow`, `maxTokens`, `reasoning`, `cost`, and `input` for catalog-known models (e.g., `proxy/cc/claude-opus-4-7` → 1M ctx / reasoning / Opus pricing) and falls back to api-appropriate defaults otherwise (`anthropic-messages` → 200k/64k, `google-generative-ai` → 1M/65k, `openai-completions` → 128k/16k). The fallback path keeps `input: ["text", "image"]` so pasted images reach vision-capable models without pi-ai's `downgradeUnsupportedImages` stripping them client-side; text-only models either ignore the image silently or return a 400 that the user sees. See changes: enrich-custom-provider-model-metadata, enable-image-input-custom-providers |
246
- | `src/client/lib/providers-api.ts` | Client-side fetch helper: `testProvider({ name?, baseUrl, apiKey, api })` structured `{ ok, status?, modelCount?, sample?, error? }`. Used by the **Test** button on the LLM Providers card |
392
+ | `src/server/routes/provider-routes.ts` | REST routes: custom LLM provider CRUD + connection probe |
393
+ | `src/server/provider-probe.ts` | Pure per-API probe builders + I/O `probeProvider` (8s timeout, no apiKey echo) |
394
+ | `src/extension/provider-register.ts` | Reads `providers.json`, calls `pi.registerProvider`, hot-reload on credentials change |
395
+ | `src/client/lib/providers-api.ts` | Client fetch helper for `/api/providers/test` connection probe |
247
396
  | `src/client/components/ProviderAuthSection.tsx` | Settings section: OAuth login buttons, device-code modal, API key inputs |
248
397
  | `src/server/auth-plugin.ts` | Fastify plugin: auth routes, onRequest hook, WS upgrade validation |
249
- | `src/server/config-api.ts` | Config REST API: read (redacted), write (partial merge), secret preservation. `writeConfigPartial` auth-merge propagates `secret`, `providers`, `allowedUsers`, `bypassHosts`, `bypassUrls` (the last two added by change `fix-trusted-networks-no-oauth` — silently dropped before, breaking Trusted Networks saves for users without OAuth). |
398
+ | `src/server/config-api.ts` | Config REST API: read (redacted), write (partial merge), secret preservation |
250
399
  | `src/client/components/SettingsPanel.tsx` | Settings UI: all dashboard config fields, grouped form, save to server |
251
400
  | `src/client/hooks/useAuthStatus.ts` | Client auth status hook and login redirect helper |
252
- | `src/server/localhost-guard.ts` | Network access guard: `createNetworkGuard` (loopback/trusted/authenticated), `isBypassedHost` (CIDR/wildcard/exact), netmask-to-CIDR helpers |
401
+ | `src/server/localhost-guard.ts` | Network access guard (loopback/trusted/authenticated, CIDR/wildcard bypass) |
253
402
  | `src/server/server-pid.ts` | PID file management for daemon mode |
254
- | `src/client/components/ServerSelector.tsx` | Server selector dropdown showing persisted known servers. Availability probing runs **only on dropdown open** (once per open, not on mount, not on a timer, not while closed). Accepts `inFlightSwitchKey` to render a spinner on the entry being switched to. Unreachable entries are `disabled`, rendered with `opacity-50` + `cursor-not-allowed`, and clicks are no-ops. Reachable clicks delegate to the transactional staging-socket switch (see `server-switch.ts`). See change: safe-server-switch |
255
- | `packages/client/src/lib/staging-socket.ts` | `openStagingSocket(url, {timeoutMs}): Promise<WebSocket>` — single-settle helper that resolves on first `OPEN`, rejects on error/close/timeout, and closes the socket on timeout to avoid leaks. Used by the transactional server-switch. |
256
- | `packages/client/src/lib/server-switch.ts` | `performServerSwitch(target, deps)` — extracted two-phase transaction (stage → commit) from `App.tsx`'s `handleServerSwitch`. Guarantees the ordering `clearInMemoryState` → `setWsUrl` → `persistLastServer`, and never persists localStorage or clears state on staging failure. Fully unit-tested. |
257
- | `packages/client/src/components/ConnectionStatusBanner.tsx` | Disconnection banner: appears only after the active WebSocket has been non-`OPEN` for &gt;3s continuously; hidden immediately on reconnect; suppressed during in-flight staging switch. Mounted above `<MobileShell>` in `App.tsx`. |
403
+ | `src/client/components/ServerSelector.tsx` | Server selector dropdown (open-only probing, transactional staging-socket switch) |
404
+ | `packages/client/src/lib/staging-socket.ts` | `openStagingSocket(url, {timeoutMs})` — single-settle WS staging helper |
405
+ | `packages/client/src/lib/server-switch.ts` | `performServerSwitch` two-phase transaction (stage → commit) |
406
+ | `packages/client/src/components/ConnectionStatusBanner.tsx` | Disconnection banner (>3s non-OPEN, hidden during staging switch) |
258
407
  | `src/client/components/KnownServersSection.tsx` | Settings section: list/add/remove persisted known remote servers |
259
- | `src/client/components/NetworkDiscoverySection.tsx` | Settings section: mDNS network scan with "Add" action and label prompt. When the scan finds zero servers (common — Wi-Fi AP isolation, mesh routers, VLANs, VPN, firewall), renders a diagnostic block listing failure causes plus an inline manual-add form that accepts a free-form host input parsed via `parseHostInput()`. Surfaces scan errors instead of swallowing them. See change: diagnose-empty-mdns-scan |
260
- | `src/client/lib/parse-host-input.ts` | Pure helper `parseHostInput(input, defaultPort = 8000) → {host, port} \| null` accepting full URLs (`http://192.168.16.202:8000`), `host:port`, bare hostnames, and bracketed IPv6 (`[::1]:8000`). Rejects empty input, bare IPv6 (ambiguous with `host:port`), invalid ports, malformed URLs. 12 unit tests in `parse-host-input.test.ts`. See change: diagnose-empty-mdns-scan |
408
+ | `src/client/components/NetworkDiscoverySection.tsx` | Settings section: mDNS network scan with manual-add fallback on empty result |
409
+ | `src/client/lib/parse-host-input.ts` | Pure `parseHostInput(input, defaultPort)` accepting URLs/host:port/IPv6 |
261
410
  | `src/client/lib/known-servers-api.ts` | Client-side fetch helpers for known servers CRUD and discovery endpoints |
262
411
  | `src/server/routes/known-servers-routes.ts` | REST routes: known servers CRUD, on-demand mDNS discovery scan |
263
412
  | `src/server/terminal-manager.ts` | PTY lifecycle, ring buffer, spawn/attach/kill terminals |
264
413
  | `src/server/terminal-gateway.ts` | Binary WebSocket upgrade handler for `/ws/terminal/:id` |
265
414
  | `scripts/fix-pty-permissions.cjs` | Postinstall: fix node-pty spawn-helper execute permissions |
266
- | `src/server/tunnel.ts` | Zrok tunnel with reserved shares for persistent URLs, binary detection, PID tracking, stale cleanup |
267
- | `src/client/components/TunnelButton.tsx` | Unified tunnel/QR button — tunnel icon when not set up, QR icon when inactive, green QR icon when connected; opens QR dialog with disconnect/setup |
268
- | `src/client/components/QrCodeDialog.tsx` | QR code dialog showing tunnel URL as scannable QR code with copy, disconnect, and setup buttons |
415
+ | `src/server/tunnel.ts` | Zrok tunnel with reserved shares, binary detection, PID tracking |
416
+ | `src/client/components/TunnelButton.tsx` | Unified tunnel/QR button (icon state varies by tunnel status) |
417
+ | `src/client/components/QrCodeDialog.tsx` | QR code dialog showing tunnel URL with copy/disconnect/setup |
269
418
  | `public/manifest.json` | PWA web app manifest for installability |
270
419
  | `public/sw.js` | Minimal service worker for PWA installability |
271
- | `src/client/components/ZrokInstallGuide.tsx` | OS-aware zrok installation guide view (macOS/Linux/Windows) |
272
- | `src/server/cli.ts` | CLI entry point with subcommands (start/stop/restart/status); `findPortHolders` is cross-platform (netstat/taskkill on Windows, lsof on Unix) and `server.log` is opened append-mode with timestamped headers. The local post-install `registry.rescan("pi")` block was removed and ownership of the post-install rescan + force-refresh moved to the centralized `bootstrapState.subscribe` hook in `server.ts`. See change: fix-openspec-buttons-after-bootstrap-install. **`cmdRestart(config, injected?)`** (change: fix-restart-bridge-auto-start-race): probes `isDashboardRunning(port)`; if up, POSTs `/api/restart` with `{dev}` and exits, delegating the entire stop/start to `restart-helper.ts`'s detached orchestrator; if down or HTTP fails, falls back to local `cmdStop()` + `cmdStart()`. Eliminates the in-process race where `cmdStop` killed the daemon and bridges' auto-start beat the subsequent `cmdStart`'s `isServerRunning` check (silently early-returning, leaving the user offline). The optional `injected` arg is for unit tests — production callers always use the real defaults. |
273
- | `src/server/restart-helper.ts` | Cross-platform `/api/restart` orchestrator: spawns a detached `node -e` child using only Node built-ins (net, http) — no sh/lsof/curl dependency; exports pure `buildOrchestratorScript(params)` for testing. **Explicit prior-daemon kill** (change: fix-restart-bridge-auto-start-race): the embedded script reads `~/.pi/dashboard/dashboard.pid`, sends `SIGTERM` to the recorded PID if alive, polls for exit (3 s deadline), then `SIGKILL`. Removes the "wait for self-exit" ambiguity that let bridges race the orchestrator before this change. The `portFree` poll is reduced from 10 s to 5 s since step 0 already guarantees the previous server is dead. |
274
- | `src/shared/resolve-jiti.ts` | Resolves pi's jiti register hook as a `file://` URL (required for `node --import` on Windows); exports pure `buildJitiRegisterUrl(pkgJsonPath)` helper and `resolveJitiFromAnchor(anchorPath)` for managed-install/system-pi callers |
275
- | `src/shared/platform/paths.ts` | OS-aware path primitives: `normalizePath`, `samePath` (filesystem-level equality), `parsePathInput` (picker input), `withTrailingSep`, `isFilesystemRoot`. All accept optional trailing `platform: NodeJS.Platform` for testability. Windows multi-drive invariant: A:\, B:\, C:\ never merge; bare `B:` input treated as drive root, not cwd-relative. Exported from `platform/index.ts` as `paths.*` namespace alongside `git.*`, `openspec.*`, `npm.*` |
276
- | `src/client/lib/session-grouping.ts` | `inferPlatform(samples)` heuristic (backslash/drive-letter = Windows, leading `/` = POSIX) + `groupSessionsByDirectory` that uses `normalizePath`-keyed Maps so sessions group under their pinned folder across separator/case/trailing drift. **Per-session group-key precedence** (change: add-jj-workspace-plugin, Decision 15): exported pure helper `resolveSessionGroupPath(session, pinnedKeys, platform)` resolves the group key as **explicit pin > `jjState.workspaceRoot` > `cwd`** — a session inside a `.shadow/<name>/` jj workspace collapses under its parent repo's group, but an explicit pin on the workspace path still wins. Within a group, sessions are pre-sorted by `clusterByWorkspaceName` so all rows sharing the same `(jjState?.workspaceName ?? "")` cluster adjacently (empty / main-tree first, then ws-A, ws-B, …); the existing `sortSessionsByOrder` ranking still applies inside each cluster. Tests: `packages/client/src/lib/__tests__/session-grouping.test.ts` (5 tests) covers the four scenarios from the spec plus the default-workspace regression guard. |
277
- | `src/shared/platform/` | Unified cross-OS primitives (see `index.ts` barrel). Sub-modules: `exec.ts` (**the only module that imports `node:child_process`** — wraps `execSync`/`exec`/`execFile`/`spawn`/`spawnSync` with `windowsHide: true` by default; enforced by `no-direct-child-process.test.ts`), `runner.ts` (the Recipe engine — `run(recipe, input)` resolves binaries via `ToolResolver`, applies timeout / tolerate / error normalization, returns `Result<T>`), `git.ts` / `openspec.ts` / `npm.ts` (Recipe-based tool modules — typed functions like `git.diff(...)`, `openspec.list(...)`, `npm.rootGlobal()` that never touch `child_process` or `process.platform`), `binary-lookup.ts` (`where`/`which`, `.cmd` ext, `ToolResolver` class; **`isAppImageSelfHit(path, opts?)`** — pure helper that flags a candidate binary path as the running Electron AppImage launcher when `realpath(path) === realpath(process.execPath)`, when `path` lives under `process.env.APPDIR` (squashfs mount), or when `realpath(path) === realpath(process.env.APPIMAGE)`. Defensive try/catch around every `realpath`. Consumed by `whereStrategy` (Layer 2 — every registry-resolved tool inherits the guard) and `detectPiDashboardCli` / `detectPi` / `detectSystemNode` (Layer 1 — belt-and-braces). See change: fix-electron-appimage-cli-self-detection), `process.ts` (`findPortHolders`, `killProcess`, `isProcessAlive`, `killPidWithGroup`, `parseNetstatListeners`), `process-scan.ts` (`isProcessRunning` via pgrep/tasklist, `parseEtime`), `shell.ts` (`detectShell` for SHELL/COMSPEC, `getTerminalEnvHints`), `commands.ts` (`openBrowser`, `isVirtualMachine`). All exported helpers that depend on OS take an optional `platform: NodeJS.Platform` parameter so tests can exercise both branches without mutating `process.platform`. See changes: consolidate-platform-handlers, platform-command-executor. |
420
+ | `src/client/components/ZrokInstallGuide.tsx` | OS-aware zrok installation guide view |
421
+ | `src/server/cli.ts` | CLI entry: start/stop/restart/status; `cmdRestart` delegates to `/api/restart` when up |
422
+ | `src/server/restart-helper.ts` | Cross-platform `/api/restart` orchestrator (detached node-built-ins-only spawner) |
423
+ | `src/shared/resolve-jiti.ts` | Resolves pi's jiti register hook as a `file://` URL |
424
+ | `src/shared/platform/paths.ts` | OS-aware path primitives (`normalizePath`, `samePath`, `parsePathInput`) |
425
+ | `src/client/lib/session-grouping.ts` | Sessions grouped by directory; `resolveSessionGroupPath` (pin > jjState.workspaceRoot > cwd) |
426
+ | `src/shared/platform/` | Unified cross-OS primitives barrel (exec/runner/git/openspec/npm/process/binary-lookup/...) |
278
427
  | `src/shared/rest-api.ts` | REST API type definitions |
279
-
280
- | `.pi/skills/release-cut/SKILL.md` | Cuts a new release: promotes `## [Unreleased]` in CHANGELOG to a dated section, bumps every workspace package.json, commits, tags, pushes (which fires `publish.yml`). The skill's `Next steps (human)` block enumerates the **7 platform artifacts** the human releaser should expect on the draft GitHub Release: `PI-Dashboard-darwin-arm64-<ver>.dmg` (Apple Silicon), `PI-Dashboard-darwin-x64-<ver>.dmg` (Intel), Linux `.deb` × 2 (x64+arm64), Linux `.AppImage` (x64 only — appimagetool has no arm64 build), Windows NSIS+ZIP+portable (x64), Windows ZIP+portable (arm64, no NSIS cross-compile). Missing artifacts in the draft = a CI failure; do NOT click Publish. (change: add-darwin-x64-build updated the count from 6 → 7 and split the macOS DMG into two arches.) |
281
- | `.pi/skills/spec-coherence-check/SKILL.md` | Skill: sweep proposals for staleness, conflicts, obsolescence against codebase |
428
+ | `.pi/skills/release-cut/SKILL.md` | Release-cut skill: bump versions, promote CHANGELOG, tag, push (fires publish.yml) |
429
+ | `.pi/skills/spec-coherence-check/SKILL.md` | Skill: sweep proposals for staleness, conflicts, obsolescence |
282
430
  | `.pi/skills/spec-coherence-check/references/proposal-queue-schema.md` | JSON schema for `.pi/proposal-queue.json` |
283
- | `.pi/skills/code-review/SKILL.md` | Skill: comprehensive code review with severity labels, four-phase process, language-specific guides |
284
- | `.pi/skills/code-review/references/` | On-demand language guides (React, TypeScript, Vue, Rust, Go, Java, Python, C/C++, CSS, Qt) + architecture/performance/security reviews |
431
+ | `.pi/skills/code-review/SKILL.md` | Skill: comprehensive code review with severity labels |
432
+ | `.pi/skills/code-review/references/` | Language guides + architecture/performance/security review references |
285
433
  | `.pi/skills/nano-banana-imagegen/SKILL.md` | Skill: AI image generation/editing via Google Gemini (nano-banana CLI) |
286
- | `.pi/skills/nano-banana-imagegen/references/` | Prompting guide, example prompts (headers, icons, illustrations, photography) |
287
- | `.pi/skills/browser-visual-debug/SKILL.md` | Skill: visual debugging with a real browser (screenshots, interaction, responsive testing) via pi-agent-browser |
288
- | `.pi/skills/browser-visual-debug/references/` | Dashboard recipes, responsive testing presets, agent-browser commands cheatsheet |
289
- | `.pi/skills/browser-visual-debug/scripts/detect-dashboard.sh` | Auto-detect dashboard URL, mode, and Vite dev server status |
290
- | `packages/electron/src/main.ts` | Electron main process: single-instance, wizard, server launch, loading page, tray. **Power-user-mode managed install** (change: fix-electron-windows-installer-and-server-bootstrap, Defect 1): the firstRun `pi.found && bridge.found` auto-skip path no longer skips `installStandalone()`. Auto-skip removes the wizard UI; the managed install (tsx + pi@0.70.0 + openspec from the offline cacache) runs anyway so the bundled server's runtime can resolve its TS loader from `~/.pi-dashboard/`. Decision logic extracted into the pure `decideStartupAction(state)` helper in `lib/power-user-install.ts` and pinned by `wizard-power-user-managed-install.test.ts`. **External-link hardening (change: harden-external-link-handling, #13)**: `createMainWindow` registers `webContents.setWindowOpenHandler` + `will-navigate` BEFORE `loadURL` so `target="_blank"` / `window.open` and bare-`<a>` external navigation both route through `shell.openExternal` (system browser) instead of replacing the dashboard. The `will-navigate` callback is **OAuth-aware** via `decideWillNavigate(serverUrl, webContents.getURL(), url)` (change: fix-oauth-blocked-by-external-link-guard) so the trap-guard fires only when leaving the dashboard — while the user is mid-login on an OAuth provider page (Google, GitHub, generic OIDC), provider-internal multi-step navigation is allowed and the eventual callback redirect lands cleanly. `setWindowOpenHandler` is unchanged. |
291
- | `packages/electron/src/lib/link-handling.ts` | Pure helpers used by the Electron shell. **`isSameOriginUrl(href, serverOrigin)`** — classifies URLs as same-origin vs external (handles absolute, relative, fragment-only, and malformed inputs; see change: harden-external-link-handling, #13). **`decideWillNavigate(serverOrigin, currentUrl, targetUrl) → "allow" | "open-external" | "cancel"`** — OAuth-aware decision wrapper for the `will-navigate` callback: while the BrowserWindow shows a non-dashboard origin (mid-OAuth) it returns `"allow"` so provider login flows proceed; on the dashboard it composes `isSameOriginUrl` to decide between `"allow"` (same-origin) and `"open-external"` (external trap-guard); fail-closes to `"cancel"` on unparseable `serverOrigin` (see change: fix-oauth-blocked-by-external-link-guard). No Electron imports; 23 unit tests in `packages/electron/src/__tests__/link-handling.test.ts`. |
292
- | `packages/client/src/components/MarkdownContent.tsx` | ReactMarkdown-based renderer for chat bodies, thinking blocks, flow agent detail, package READMEs, markdown previews. **External-link hardening (change: harden-external-link-handling, #13)**: exports pure `isExternalHref(href)` and overrides the `a` component so external URLs render with `target="_blank" rel="noopener noreferrer"` while fragment-only and same-origin hrefs stay in-document. |
293
- | `packages/client/src/__tests__/no-bare-external-anchor.test.ts` | Repo-level lint: scans client `.tsx` files for `<a href="http(s)://...">` tags missing `target="_blank"`. Per-line opt-out via `// ban:bare-anchor-ok`. See change: harden-external-link-handling. |
294
- | `packages/electron/src/lib/server-lifecycle.ts` | Health check tsx binary spawn (inlined config/health, no shared pkg imports). **60-second startup deadline + cause-aware error wording** (change: fix-electron-windows-installer-and-server-bootstrap, Defect 4): exports `SERVER_READY_DEADLINE_MS = 60_000` (was inline `15_000`) and a pure helper `buildServerStartupError(...)` that renders "Server child process exited prematurely (...)" when `ready.error` mentions an exit, vs. "Server did not respond within 60 seconds (...)" when the deadline elapsed. The pre-fix message conflated both cases and was misleading when the child died in <1s. Both `launchViaCli` and `launchServer` use the same constant + helper. |
295
- | `packages/server/src/extension-register.ts` | Auto-registers bundled bridge extension in pi's global settings on startup |
434
+ | `.pi/skills/nano-banana-imagegen/references/` | Prompting guide, example prompts |
435
+ | `.pi/skills/browser-visual-debug/SKILL.md` | Skill: visual debugging with a real browser via pi-agent-browser |
436
+ | `.pi/skills/browser-visual-debug/references/` | Dashboard recipes, responsive presets, agent-browser cheatsheet |
437
+ | `.pi/skills/browser-visual-debug/scripts/detect-dashboard.sh` | Auto-detect dashboard URL, mode, Vite dev server status |
438
+ | `packages/electron/src/main.ts` | Electron main: single-instance, wizard, server launch, loading page, tray |
439
+ | `packages/electron/src/lib/link-handling.ts` | Pure `isSameOriginUrl` + OAuth-aware `decideWillNavigate` for external-link guard |
440
+ | `packages/client/src/components/MarkdownContent.tsx` | ReactMarkdown renderer (chat/thinking/READMEs/previews); external-link hardening + KaTeX math + `pi-asset:` image scheme |
441
+ | `packages/client/src/lib/SessionAssetsContext.tsx` | Per-session image-asset registry context resolving `pi-asset:<hash>` srcs in `MarkdownContent` |
442
+ | `packages/extension/src/markdown-image-inliner.ts` | Bridge helper rewriting assistant `![alt](path)` `![alt](pi-asset:<hash>)` (SHA-256/16, MIME allowlist, 5 MB/img + 20 MB/msg caps) |
443
+ | `packages/client/src/__tests__/no-bare-external-anchor.test.ts` | Repo-lint: forbid bare `<a href="http(s)://">` without `target="_blank"` |
444
+ | `packages/electron/src/lib/pick-node.ts` | Pure `pickNodeForServer` — prefer system Node when version-safe, else bundled |
445
+ | `packages/electron/src/lib/ensure-windows-path.ts` | `ensureWindowsSystemPath` — prepend System32/npm/Git dirs on Windows; no-op on POSIX |
446
+ | `packages/electron/src/lib/server-lifecycle.ts` | Health check → server spawn; `setSpawnedPid` + `decideShutdownOnQuit` for V2 ownership rule |
447
+ | `packages/electron/src/lib/launch-source.ts` | `selectLaunchSource()` resolver: attach→devMonorepo→piExtension→npmGlobal→extracted; `spawnFromSource` |
448
+ | `packages/electron/src/lib/bundle-extract.ts` | `needsExtraction`, `migrateConfigs`, `extractBundle` with survive-extract whitelist for `~/.pi-dashboard/` |
449
+ | `packages/shared/src/installable-list.ts` | `InstallablePackage`/`InstallableList` types; `readInstallableList`, `writeInstallableList`, `mergeInstallableList` |
450
+ | `packages/server/src/bootstrap-install-from-list.ts` | Per-package reconcile loop reading `~/.pi/dashboard/installable.json`; no-op when file absent |
451
+ | `packages/shared/src/bridge-register.ts` | Shared bridge registration: `findBundledExtension(baseDir)` + `registerBridgeExtension(path)`; non-destructive cleanup, AppImage guard. Used by server startup and Electron wizard. |
296
452
  | `packages/electron/src/lib/doctor.ts` | Doctor diagnostic: checks all binaries, versions, server status, offers setup |
453
+ | `packages/shared/src/doctor-core.ts` | Shared doctor primitives: types, SECTION_OF, SUGGESTIONS, safeExec/safeCheck/assumedMandatory, runSharedChecks, formatDoctorReportMarkdown |
454
+ | `packages/electron/src/lib/doctor-bridge-contract.ts` | Typed `DoctorBridge` interface + frozen `DOCTOR_IPC_CHANNELS` (channel-name-drift lint) |
455
+ | `packages/electron/src/lib/doctor-window.ts` | `openDoctorWindow()` factory + IPC handlers (`doctor:run` etc.); concurrent-run serialization; closed→null leak fix |
456
+ | `packages/electron/src/preload/doctor-preload.ts` | Preload bridge exposing `window.electron.doctor` to `doctor.html` |
457
+ | `packages/electron/src/renderer/doctor.html` | Hand-rolled Doctor renderer — sections, status pills, suggestion callouts, toolbar |
458
+ | `packages/server/src/routes/doctor-routes.ts` | `GET /api/doctor` route — auth-gated; runs `runSharedChecks`; 200 + fallback row on internal failure |
459
+ | `packages/client/src/lib/doctor-api.ts` | Client fetch helper for `/api/doctor` with `DoctorFetchError` typed envelope |
460
+ | `packages/client/src/components/DiagnosticsSection.tsx` | Settings → Diagnostics — fetch, sections, suggestions, copy-to-clipboard with textarea fallback |
297
461
  | `packages/electron/src/lib/app-menu.ts` | App menu with About dialog and Doctor on all platforms |
298
- | `packages/electron/src/lib/tray.ts` | System tray with platform-specific icons (template on macOS, ico/png on Win/Linux) |
299
- | `packages/electron/src/lib/dependency-installer.ts` | Async npm install of pi, openspec, tsx into ~/.pi-dashboard/ using bundled Node |
300
- | `packages/electron/src/lib/dependency-detector.ts` | Detects pi, openspec, Node.js on system PATH and managed install. **AppImage self-recursion guard** (change: fix-electron-appimage-cli-self-detection): `detectPiDashboardCli` rejects any candidate that matches `isAppImageSelfHit(path)` (in addition to the existing `_npx` filter) so power-user mode falls through to the standalone tsx + `cli.ts` path when the only `pi-dashboard` on PATH is the AppImage's own launcher (`packagerConfig.executableName: "pi-dashboard"` collides by design). `detectPi` and `detectSystemNode` apply the same guard symmetrically on the registry-resolved path — belt-and-braces beyond the `whereStrategy` filter. **Windows extension filter** (change: fix-electron-windows-installer-and-server-bootstrap, Defect 3): exports a pure helper `pickSpawnableShim(rawWhereOutput, platform)` that, on `win32`, prefers candidates ending in `.cmd`/`.exe`/`.bat`/`.ps1` over an extensionless POSIX shim from npm-global. `spawn()` without `shell:true` cannot invoke an extensionless shim on Windows, so the pre-fix `lines[0]` pick produced `ENOENT`. POSIX behaviour (single-line `which`) unchanged. Locked by `dependency-detector-windows-extensions.test.ts`. |
462
+ | `packages/electron/src/lib/tray.ts` | System tray with platform-specific icons |
463
+ | `packages/electron/src/lib/dependency-installer.ts` | Async npm install of pi/openspec/tsx into `~/.pi-dashboard/` (Windows-hardened) |
464
+ | `packages/electron/src/lib/dependency-detector.ts` | Detects pi/openspec/Node on PATH and managed install (AppImage + Win-ext guards) |
301
465
  | `packages/electron/src/lib/bundled-node.ts` | Resolves bundled Node.js/npm paths in Electron resources |
302
466
  | `packages/electron/src/lib/wizard-window.ts` | First-run setup wizard window with preload bridge |
303
- | `packages/electron/forge.config.ts` | Electron Forge config: DMG, DEB, AppImage, NSIS makers, icon, extraResources. **NSIS naming overrides** (change: fix-electron-windows-installer-and-server-bootstrap): the NSIS maker's `getAppBuilderConfig` callback explicitly pins `productName`, `appId`, `nsis.artifactName`, `nsis.shortcutName`, `nsis.uninstallDisplayName` all to `pi-dashboard`. Without this override, electron-builder's NSIS install-dir fallback chain reads npm `name` slash-stripped and produces `@blackbelt-technologypi-dashboard-electron`. Locked by `forge-config-naming.test.ts`. **macOS deployment-target floor** (change: add-darwin-x64-build, 6b): `packagerConfig.extendInfo.LSMinimumSystemVersion = "10.15"` pins the user-visible minimum macOS version in the produced `Info.plist`. Pairs with `MACOSX_DEPLOYMENT_TARGET=10.15` exported by the `Make Electron distributables` step in `publish.yml` (every Mach-O the build produces declares 10.15 as its `LC_BUILD_VERSION.minos`) and a CI verification step that mounts the DMG and asserts both values match — a future runner-image upgrade or source-built native module cannot silently raise the floor without failing the job. **Arch-tagged DMG basename** (change: fix-darwin-dmg-arch-collision): the `@electron-forge/maker-dmg` config's `name` field is composed at config-evaluation time as `` `PI-Dashboard-darwin-${process.arch}-${pkgVersion}` `` (with `pkgVersion` read from `packages/electron/package.json` once at module top). Both macOS matrix legs (`macos-14`/arm64, `macos-15-intel`/x64) previously emitted a static `PI Dashboard.dmg`, causing `softprops/action-gh-release@v2` to silently overwrite one arch with the other on release-asset upload (it dedups by basename). The arch-tagged basename also ensures parity with deb (`_amd64`/`_arm64`) and Windows portable (`-arm64`/`-x64`) artifact-naming conventions. The `title` field stays `"PI Dashboard"` so the mounted-volume window title remains friendly. Locked by `forge-config-dmg-naming.test.ts` (6 tests covering both arches, version interpolation, title preservation, exact pattern match). |
304
- | `packages/electron/scripts/build-installer.sh` | Build script: native + Docker cross-platform (--linux, --windows, --all). **`--mac-both`** (change: add-darwin-x64-build) builds BOTH macOS DMGs (arm64 + x64) on an Apple Silicon host in one invocation: orchestrates two sequential `build_native_one_arch` calls with sentinel-driven cache invalidation between them (`resources/.last-arch`); refuses on Intel hosts (Rosetta is one-way, x64 → arm64 cross-compile is not supported locally) and on non-darwin hosts. Cross-arch x64 builds on Apple Silicon hosts auto-detect Rosetta 2 via `arch -x86_64 /usr/bin/true` (fail-fast with `softwareupdate --install-rosetta --agree-to-license` hint if missing) and wrap `bundle-server.mjs` in `arch -x86_64` + sets `TARGET_ARCH=x64` env so node-pty's prebuilt x64 binary is downloaded. The `--mac-both` post-build smoke summary mounts each DMG and reports the inner Mach-O arch tag (`arm64` / `x86_64`) via `file` so silent arch-mismatch artifacts cannot ship undetected. The arch-aware cache wipe also fires on single-arch back-to-back invocations (`--arch arm64` then `--arch x64`) so manual cache cleanup is no longer required. Pure helpers: `maybe_wipe_arch_caches`, `verify_rosetta_or_fail`, `build_native_one_arch`. |
305
- | `packages/electron/scripts/docker-make.sh` | Docker entrypoint: platform-aware native module handling, ZIP for Windows |
306
- | `packages/electron/scripts/Dockerfile.build` | Docker image for cross-platform builds (node:22-bookworm-slim + build tools) |
307
- | `packages/electron/scripts/bundle-server.mjs` | Bundles dashboard server source + deps into resources/server/ (`--source-only` for cross-builds). Node-native ESM script (replaces `bundle-server.sh`) so Windows electron CI runs without MSYS/bash. Uses `fs.cpSync` / `fs.chmodSync` / recursive `readdir` instead of `cp -R`/`find`/`chmod`/`du`. Verified bit-parity with the old shell script: identical 2251-file layout, identical structure. See change: eliminate-bash-on-windows-runners. **Architectural lock**: synthetic `package.json` deliberately does NOT declare `@mariozechner/pi-coding-agent` (or any managed-dir-resident dep). The bundled tree only contains workspace deps (`fastify`, `ws`, `node-pty`, etc.) directly imported by the bundled `cli.ts`. pi/openspec/tsx live in the managed dir (`~/.pi-dashboard/`) and are installed there by `installStandalone()` from the offline cacache pinned in `offline-packages.json`. An earlier `/opsx:apply` session against `fix-electron-windows-installer-and-server-bootstrap` proposed adding a `dependencies: { "@mariozechner/pi-coding-agent": "0.70.0" }` block here — reverted as architecturally wrong (would duplicate ~10MB and create version-drift risk vs. the offline cacache). Bundle stays at ~80MB; if it ever climbs to ~160MB, that's the regression marker. See change: fix-electron-windows-installer-and-server-bootstrap (D5 reconsidered). |
308
- | `packages/electron/offline-packages.json` | Pinned versions of pi-coding-agent / openspec / tsx that get bundled as an offline npm cacache per release (see change: `electron-offline-bundled-packages`) |
309
- | `packages/electron/scripts/bundle-offline-packages.sh` | Build-time script that runs `npm install --os=<os> --cpu=<cpu> --ignore-scripts` for the pinned versions, tars the resulting `_cacache/` (pax format — ustar is too narrow), writes `manifest.json` with SHA-256, and enforces a 100 MB hard budget. Opt-in via `BUNDLE_OFFLINE_PACKAGES=1`. |
310
- | `packages/electron/resources/offline-packages/manifest.json` | Offline-cache manifest (`{bundledAt, targetPlatform, tarball, tarballBytes, sha256, packages}`). Consumed at runtime by `dependency-installer.ts` via `resolveOfflinePackages()`. |
311
- | `packages/electron/resources/offline-packages/npm-cache.tar.gz` | gzipped npm cacache used by the first-run offline install. Extracted to `~/.pi-dashboard/.offline-cache/`, verified by SHA-256, consumed by `npm install --offline`, then deleted to reclaim ~140 MB. |
312
- | `packages/electron/src/lib/offline-packages.ts` | Pure helpers: `parseOfflineManifest`, `resolveOfflinePackages`, `fileSha256`, `extractOfflineCache`, `buildOfflineInstallArgs`, `selectInstallStrategy`. 19 unit tests in `packages/electron/src/__tests__/offline-packages.test.ts`. |
313
- | `packages/electron/scripts/bundle-recommended-extensions.sh` | Opt-in (via `BUNDLE_RECOMMENDED_EXTENSIONS=1`) build-time bundler that clones each id in `BUNDLED_EXTENSION_IDS` into `packages/electron/resources/bundled-extensions/<id>/`, records `.bundled-sha`, enforces SPDX allowlist (MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC) and a 15 MB total size budget. See change: bundle-first-party-extensions. |
314
- | `packages/electron/src/lib/dependency-installer.ts` → `installBundledExtensions` | First-run activation of pre-bundled extensions. Copies `<resourcesPath>/bundled-extensions/<id>/` into pi's git cache (`~/.pi/agent/git/<host>/<path>/`), runs `npm install --omit=dev` if runtime deps declared, then calls `manager.addSourceToSettings(gitUrl)` + `settingsManager.flush()`. Returns activated ids so `installRecommendedExtensions` can skip them with `output: "Already installed (bundled)"`. |
315
- | `packages/electron/src/lib/wizard-badge.ts` | Pure `classifyProgressBadge(output)` helper (`bundled` / `system` / null) shared between wizard HTML inline JS and unit tests. |
316
- | `packages/shared/src/recommended-extensions.ts` → `BUNDLED_EXTENSION_IDS` | Single source of truth for which recommended ids ship bundled in the Electron installer. **Currently `["pi-anthropic-messages"]` only** — `"pi-flows"` was removed in commit b9b3d7e because the upstream repo (BlackBeltTechnology/pi-flows) declares no SPDX license (no `LICENSE` file, no `package.json#license`), and `bundle-recommended-extensions.sh`'s allowlist (MIT/Apache-2.0/BSD-2-Clause/BSD-3-Clause/ISC) correctly rejects it — which was blocking every electron build matrix variant. Re-add `"pi-flows"` once upstream declares a license. The npm publish path is unaffected (only electron build was broken). See `openspec/changes/archive/2026-04-21-bundle-first-party-extensions/design.md` for original design + license-blocker discussion. |
317
- | `packages/electron/scripts/docker-make.sh` | Docker entrypoint: bundles server, installs native deps, runs Forge make |
318
- | `packages/electron/scripts/Dockerfile.build` | Docker image for cross-platform builds (node:22-bookworm-slim + build tools) |
467
+ | `packages/electron/forge.config.ts` | Electron Forge config: DMG/DEB/AppImage/NSIS makers; arch-tagged DMG; macOS 10.15 floor |
468
+ | `packages/electron/scripts/build-installer.sh` | Build script: native + Docker cross-platform; `--mac-both` arm64+x64 sequence |
469
+ | `packages/electron/scripts/docker-make.sh` | Docker entrypoint: bundles server, native deps, runs Forge make |
470
+ | `packages/electron/scripts/Dockerfile.build` | Docker image for cross-platform builds (node:22-bookworm-slim) |
471
+ | `packages/electron/scripts/bundle-server.mjs` | Bundles dashboard server + workspace deps into `resources/server/` (Node-native ESM) |
472
+ | `packages/electron/offline-packages.json` | Pinned versions of pi/openspec/tsx for offline npm cacache |
473
+ | `packages/electron/scripts/bundle-offline-packages.sh` | Build-time script: pack pinned versions into cacache tarball with SHA-256 |
474
+ | `packages/electron/resources/offline-packages/manifest.json` | Offline-cache manifest consumed at runtime by `dependency-installer.ts` |
475
+ | `packages/electron/resources/offline-packages/npm-cache.tar.gz` | gzipped npm cacache for first-run offline install |
476
+ | `packages/electron/src/lib/offline-packages.ts` | Pure offline-cache helpers (parse, resolve, verify SHA-256, extract) |
477
+ | `packages/electron/scripts/bundle-recommended-extensions.sh` | Opt-in: clone bundled-extension ids with SPDX allowlist + 15MB budget |
478
+ | `packages/electron/src/lib/dependency-installer.ts` → `installBundledExtensions` | First-run activation of pre-bundled extensions into pi git cache |
479
+ | `packages/electron/src/lib/wizard-badge.ts` | Pure `classifyProgressBadge(output)` (`bundled`/`system`/null) |
480
+ | `packages/shared/src/recommended-extensions.ts` → `BUNDLED_EXTENSION_IDS` | Single source of truth for bundled extension ids in Electron installer |
319
481
  | `packages/electron/scripts/test-server-launch.sh` | Docker-based test for server launch on clean Linux |
320
482
  | `packages/electron/scripts/test-electron-install.sh` | Full e2e Docker test: install, wizard, server launch, health check |
321
483
  | `packages/electron/scripts/test-electron-install-inner.sh` | Inner test script run inside Docker container |
322
- | `packages/electron/resources/icon.png` | Master 1024×1024 app icon (π on dark navy) |
323
- | `.github/workflows/publish.yml` | CI: builds DMG × 2 (macOS arm64 + x64), DEB+AppImage (Linux), NSIS+ZIP+portable (Windows) on native runners; publishes npm + GitHub Release. **Build matrix covers 6 (platform, arch) tuples** — darwin/arm64 (`macos-14`), darwin/x64 (`macos-15-intel` — GitHub's last hosted Intel x86_64 image after `macos-13` was retired on 2025-12-08; EOL announced 2027-08; change: add-darwin-x64-build), linux/x64 (`ubuntu-latest`), linux/arm64 (`ubuntu-24.04-arm`), win32/x64 (`windows-latest`), win32/arm64 (`windows-latest`). Missing rows are a regression — the spec requirement `electron-build-pipeline > CI build matrix` enumerates each scenario explicitly to prevent silent drift. **Two triggers**: (a) push of any `v*` tag (the original path — release-cut skill / hand tag), (b) `workflow_dispatch` from the GitHub Actions UI with a single required `version` input (e.g. `"0.4.1"`). The `prepare` job branches on `github.event_name`: tag-push extracts the version from `GITHUB_REF_NAME`; dispatch validates the input as semver, checks tag uniqueness on origin, bumps every workspace `package.json` via `npm version -ws`, syncs cross-ref specifiers via `scripts/sync-versions.js`, promotes `## [Unreleased]` to a dated `## [<version>]` section in `CHANGELOG.md`, commits + tags + pushes the branch. The publish, electron, and github-release jobs all `needs: prepare` and check out `ref: ${{ needs.prepare.outputs.tag }}` so both trigger paths publish the same tree. **Idempotent ordered npm publish (commit b9fcea9)**: the publish step replaced the bulk `npm publish --workspaces --include-workspace-root` call with a per-package loop that (a) **skips** already-published versions via `npm view <pkg>@<ver>` (so a re-run after partial-publish failure resumes cleanly instead of aborting on "cannot publish over previously published"), (b) publishes the **4 stable sub-packages first** (`pi-dashboard-shared` → `extension` → `server` → `web`), then the brand-new `dashboard-plugin-runtime`, then the **root metapackage last** so the registry already serves matching sub-package versions before the root tarball lands and resolves dependents like `@blackbelt-technology/pi-dashboard-extension@^X.Y.Z`. v0.4.0 and v0.4.1 shipped broken because the bulk call aborted on the first error and only the root tarball landed — `npm install @blackbelt-technology/pi-agent-dashboard@0.4.1` returned ETARGET on the sub-deps. Single-failure isolation: any non-skip failure marks the step failed via a `FAIL=1` accumulator but lets the loop finish so logs show every package's outcome. Also (commit b9fcea9): `packages/server/package.json#dependencies` now declares `@blackbelt-technology/dashboard-plugin-runtime: ^<ver>` — previously imported via workspace symlinks but missing from the published tarball, so a clean `npm install` of just the server crashed with `MODULE_NOT_FOUND` on first start. Also (commit 2728c31): every workspace `package.json` (`shared`, `extension`, `server`, `client`, `dashboard-plugin-runtime`, plus `electron`) now declares a `repository` field — required for npm provenance attestation validation when publishing with `--provenance` from GitHub Actions OIDC. **Electron-publish dependency-graph contract** (change: publish-fix-macos): the `electron` matrix job declares `needs: [prepare, publish]` and `strategy.fail-fast: false`. The `needs: publish` gate closes the ETARGET race that broke release run #34 — `bundle-server.mjs` runs `npm install --omit=dev` against the live npm registry and resolves `@blackbelt-technology/dashboard-plugin-runtime@^<ver>` (added in commit b9fcea9 to fix `MODULE_NOT_FOUND` on clean server installs), so it must run AFTER publish has uploaded the just-bumped sub-packages. `fail-fast: false` keeps a single-OS failure from cancelling the other four matrix variants — release engineers see the full diagnostic per OS instead of one error and four cancellations. Locked by `packages/shared/src/__tests__/publish-workflow-contract.test.ts`. **No-bash-on-Windows invariant** (change: eliminate-bash-on-windows-runners): no step in `publish.yml` or `ci.yml` combines `shell: bash` with a runtime configuration that can run on a `windows-latest` runner. Cross-OS build orchestration lives in `.mjs` scripts invoked by `node`; POSIX-only steps use `shell: bash` gated by `if: matrix.platform != 'win32'`; Windows-only steps use `shell: pwsh`. The bundle scripts (`bundle-server.mjs`, `bundle-offline-packages.mjs`, `bundle-recommended-extensions.mjs`) are Node-native, eliminating the bash↔Node bridge that produced the `MODULE_NOT_FOUND` regression on Windows runners. Locked by `packages/shared/src/__tests__/no-bash-on-windows.test.ts`. |
324
- | `packages/shared/src/__tests__/publish-workflow-contract.test.ts` | Repo-level lint: parses `.github/workflows/publish.yml`, asserts the electron job's `needs:` array contains both `prepare` and `publish` AND `strategy.fail-fast` is the literal `false`. Failure messages cite change `publish-fix-macos` so the contributor knows where to look. Mirrors `no-direct-process-kill.test.ts` and `no-raw-node-import.test.ts`. See change: publish-fix-macos. |
325
- | `packages/shared/src/__tests__/no-bash-on-windows.test.ts` | Repo-level lint: parses every workflow YAML, computes per-step Windows reachability from `electron` matrix × each step's `if:` filter (small grammar: `matrix.platform == 'X'`, `matrix.platform != 'X'`, `&&`, `||`, `!(...)` , parens), and fails when any `shell: bash` step is reachable on a Windows runner. Failure messages cite change `eliminate-bash-on-windows-runners` plus the offending `file:line` + step name. Unrecognised `if:` expressions fail closed (force the contributor to write a recognisable form or extend the evaluator). See change: eliminate-bash-on-windows-runners. |
484
+ | `packages/electron/resources/icon.png` | Master 1024×1024 app icon |
485
+ | `.github/workflows/publish.yml` | CI: build matrix × 6 (platform,arch); idempotent ordered npm publish; no-bash-on-Windows |
486
+ | `packages/shared/src/__tests__/publish-workflow-contract.test.ts` | Repo-lint: pin electron job's `needs:` array and `fail-fast: false` |
487
+ | `packages/shared/src/__tests__/no-bash-on-windows.test.ts` | Repo-lint: forbid `shell: bash` on steps reachable on Windows runners |
326
488
 
327
489
  ## Build & Restart Workflow
328
490
 
@@ -389,17 +551,4 @@ When creating OpenSpec change artifacts, always place them at `openspec/changes/
389
551
 
390
552
  When creating diagrams, use Mermaid syntax (```mermaid blocks) instead of ASCII box drawings. This applies to explore mode, design documents, and all other artifacts.
391
553
 
392
- ## Code Instructions
393
-
394
- 1. First think through the problem, read the codebase for relevant files.
395
- 2. Before you make any major changes, check in with me and I will verify the plan.
396
- 3. Please every step of the way just give me a high level explanation of what changes you made.
397
- 4. Make every task and code change you do as simple as possible. We want to avoid making any massive or complex changes. Every change should impact as little code as possible. Everything is about simplicity.
398
- 5. Maintain a documentation file that describes how the architecture of the app works inside and out.
399
- 6. Never speculate about code you have not opened. If the user references a specific file, you MUST read the file before answering. Make sure to investigate and read relevant files BEFORE answering questions about the codebase. Never make any claims about code before investigating unless you are certain of the correct answer - give grounded and hallucination-free answers.
400
- 7. For implementation use TDD (Test-Driven Development): write or update tests first to define the expected behaviour, verify they fail, then write the minimal implementation to make them pass.
401
- 8. Use DRY (Don't Repeat Yourself): extract reusable logic into separate classes, utilities, or components. If the same pattern appears in multiple places, refactor it into a shared helper.
402
-
403
- ## Document changes
404
554
 
405
- When an implementation is ready, update AGENTS.md, README.md, and docs/architecture.md. AGENTS.md contains instructions for AI agents, key files, and commands needed to build and operate. README.md contains end-user and developer documentation with CI badges, prerequisites, configuration, and project structure. docs/architecture.md contains detailed data flows, persistence model, reconnection logic, and configuration reference.