frayui 0.1.0 → 0.1.2

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 (112) hide show
  1. package/README.md +52 -174
  2. package/dist/dev-child.js +24 -5
  3. package/dist/frayui.js +8 -8
  4. package/package.json +27 -8
  5. package/runtime/cc-worker/DECISIONS.md +14 -14
  6. package/runtime/cc-worker/bin/fray +4 -4
  7. package/runtime/cc-worker/bin/fray-update +4 -4
  8. package/runtime/cc-worker/hooks/session-seed.mjs +1 -1
  9. package/runtime/cc-worker/scripts/fray/agent-bindings.mjs +3 -3
  10. package/runtime/cc-worker/scripts/fray/config.mjs +8 -8
  11. package/runtime/cc-worker/skills/gh/SKILL.md +22 -0
  12. package/web-dist/assets/{TerminalPane-fMGrgB-a.js → TerminalPane-DyK0ncN8.js} +1 -1
  13. package/web-dist/assets/{abnfDiagram-VRR7QNED-BtlrDuYX.js → abnfDiagram-VRR7QNED-DtotTgyK.js} +1 -1
  14. package/web-dist/assets/architecture-TIHT7OUA-B23bEjC4.js +1 -0
  15. package/web-dist/assets/{architectureDiagram-ZJ3FMSHR-CdLQ71be.js → architectureDiagram-ZJ3FMSHR-CVl8mUt0.js} +1 -1
  16. package/web-dist/assets/{blockDiagram-677ZJIJ3-CBk2FHEj.js → blockDiagram-677ZJIJ3-VUZEsYBW.js} +1 -1
  17. package/web-dist/assets/{c4Diagram-LMCZKHZV-mZ3HQ6WX.js → c4Diagram-LMCZKHZV-Dlg18lBF.js} +1 -1
  18. package/web-dist/assets/channel-BMFwXMMq.js +1 -0
  19. package/web-dist/assets/{chunk-32BRIVSS-Daxvi7f5.js → chunk-32BRIVSS-DG8gd151.js} +1 -1
  20. package/web-dist/assets/{chunk-52WLFC77-BGJoZvry.js → chunk-52WLFC77-DKlahFJ2.js} +1 -1
  21. package/web-dist/assets/{chunk-C7G6YPKG-Ycd1yheh.js → chunk-C7G6YPKG-Bk-BFDsQ.js} +1 -1
  22. package/web-dist/assets/{chunk-EX3LRPZG-BPFhlsHp.js → chunk-EX3LRPZG-DnXqd-jJ.js} +1 -1
  23. package/web-dist/assets/{chunk-FWX5IMBZ--xy8DTon.js → chunk-FWX5IMBZ-ujYyBebT.js} +2 -2
  24. package/web-dist/assets/{chunk-HOUHSVGY-Dq3zoygp.js → chunk-HOUHSVGY-Dk2XhPWQ.js} +1 -1
  25. package/web-dist/assets/{chunk-ICXQ74PX-FfKP-7yM.js → chunk-ICXQ74PX-DoseDoV4.js} +1 -1
  26. package/web-dist/assets/{chunk-MOJQB5TN-BeiHnsLF.js → chunk-MOJQB5TN-B8dog4zz.js} +1 -1
  27. package/web-dist/assets/{chunk-OGEWGWER-CjDU_9fC.js → chunk-OGEWGWER-D4GkHktS.js} +1 -1
  28. package/web-dist/assets/{chunk-PUDLZKDR-BO7tm5QX.js → chunk-PUDLZKDR-ChEIayRe.js} +1 -1
  29. package/web-dist/assets/{chunk-Q4XR5HBZ-BPefovOg.js → chunk-Q4XR5HBZ-BtzMOr97.js} +1 -1
  30. package/web-dist/assets/{chunk-V7JOEXUC-DWnebFHh.js → chunk-V7JOEXUC-DQ7c8QAd.js} +1 -1
  31. package/web-dist/assets/{chunk-VAUOI2AC-BL6qWFhW.js → chunk-VAUOI2AC-6jWzOU_J.js} +1 -1
  32. package/web-dist/assets/{chunk-VR4S4FIN-Mfs__7L9.js → chunk-VR4S4FIN-TB0TNuya.js} +1 -1
  33. package/web-dist/assets/{chunk-WYO6CB5R-Bf2IYbEU.js → chunk-WYO6CB5R-CY7fL6F5.js} +1 -1
  34. package/web-dist/assets/{chunk-ZGVPDNZ5-CJ9ZHZgU.js → chunk-ZGVPDNZ5-vZO6zPtA.js} +1 -1
  35. package/web-dist/assets/classDiagram-OUVF2IWQ-DiWatUgG.js +1 -0
  36. package/web-dist/assets/classDiagram-v2-EOCWNBFH-DiWatUgG.js +1 -0
  37. package/web-dist/assets/{cynefin-VYW2F7L2-COSC0oNL.js → cynefin-VYW2F7L2-D4NcAp7V.js} +1 -1
  38. package/web-dist/assets/{cynefinDiagram-TSTJHNR4-mInhJdg1.js → cynefinDiagram-TSTJHNR4-hLO8f53U.js} +1 -1
  39. package/web-dist/assets/{dagre-VKFMJZFB-D-T5z05M.js → dagre-VKFMJZFB-86z9AWRw.js} +1 -1
  40. package/web-dist/assets/{diagram-FQU43EPY-HbMkSb3j.js → diagram-FQU43EPY-DXw3bj8G.js} +1 -1
  41. package/web-dist/assets/{diagram-G47NLZAW-DBwZLcZh.js → diagram-G47NLZAW-BtH3_eiQ.js} +1 -1
  42. package/web-dist/assets/{diagram-NH7WQ7WH-DG_Mhays.js → diagram-NH7WQ7WH-BxWq2H6D.js} +1 -1
  43. package/web-dist/assets/{diagram-OA4YK3LP-hH39gqpu.js → diagram-OA4YK3LP-BoU3I6Xx.js} +1 -1
  44. package/web-dist/assets/{diagram-WEI45ONY-DhKZuwxW.js → diagram-WEI45ONY-CkPD-vrA.js} +1 -1
  45. package/web-dist/assets/{ebnfDiagram-CCIWWBDH-B7BiO-NR.js → ebnfDiagram-CCIWWBDH-BudQTnvf.js} +1 -1
  46. package/web-dist/assets/{erDiagram-Q63AITRT-DHjG5RDQ.js → erDiagram-Q63AITRT-CRFLgkaW.js} +1 -1
  47. package/web-dist/assets/eventmodeling-45OFAUF4-C7ZRnE7-.js +1 -0
  48. package/web-dist/assets/flowDiagram-23GEKE2U-DKS-kFcF.js +1 -0
  49. package/web-dist/assets/{ganttDiagram-NO4QXBWP-BZ-98wTa.js → ganttDiagram-NO4QXBWP-DFmOc45y.js} +1 -1
  50. package/web-dist/assets/{gitGraph-TEB2WS4Q-nF22R2jO.js → gitGraph-TEB2WS4Q-CMW6meDa.js} +1 -1
  51. package/web-dist/assets/{gitGraphDiagram-IHSO6WYX-CoYQQ22V.js → gitGraphDiagram-IHSO6WYX-CHelbJfg.js} +1 -1
  52. package/web-dist/assets/{index-BHzIN-tQ.js → index-C2Tf3Ihs.js} +65 -65
  53. package/web-dist/assets/index-CeAC3wUW.css +1 -0
  54. package/web-dist/assets/{info-DKCQHKI2-Cy2BCbBW.js → info-DKCQHKI2-DZT4W61r.js} +1 -1
  55. package/web-dist/assets/{infoDiagram-FWYZ7A6U-CmhR2R1x.js → infoDiagram-FWYZ7A6U-Dj0tLhVO.js} +1 -1
  56. package/web-dist/assets/{ishikawaDiagram-FXEZZL3T-CfG59afl.js → ishikawaDiagram-FXEZZL3T-D825ES0c.js} +1 -1
  57. package/web-dist/assets/{journeyDiagram-5HDEW3XC-BvFmG20q.js → journeyDiagram-5HDEW3XC-D53Igq5c.js} +1 -1
  58. package/web-dist/assets/{kanban-definition-HUTT4EX6-BHGEecsY.js → kanban-definition-HUTT4EX6-CJ-F0vAo.js} +1 -1
  59. package/web-dist/assets/{line-Ds4xvN3d.js → line-CuiC27jO.js} +1 -1
  60. package/web-dist/assets/{mermaid-parser.core-Gow4kgYJ.js → mermaid-parser.core-rG6qAIn0.js} +3 -3
  61. package/web-dist/assets/{mermaid.core-B7Qc_kbm.js → mermaid.core-DzNUOuDq.js} +3 -3
  62. package/web-dist/assets/{mindmap-definition-LN4V7U3C-ayXKGcy_.js → mindmap-definition-LN4V7U3C-DHfhQue9.js} +1 -1
  63. package/web-dist/assets/{packet-7NZHBO7P-oLBdtkD8.js → packet-7NZHBO7P-BiUlKhVG.js} +1 -1
  64. package/web-dist/assets/{pegDiagram-2B236MQR-DwJyc_1B.js → pegDiagram-2B236MQR-MQdiKZx3.js} +1 -1
  65. package/web-dist/assets/{pie-RZYD4A2V-CXzad3fT.js → pie-RZYD4A2V-dNHUvvE3.js} +1 -1
  66. package/web-dist/assets/{pieDiagram-ENE6RG2P-CZzgSGtM.js → pieDiagram-ENE6RG2P-BwFxe1x1.js} +1 -1
  67. package/web-dist/assets/{quadrantDiagram-ABIIQ3AL--IPIp8yo.js → quadrantDiagram-ABIIQ3AL-9dic-8vv.js} +1 -1
  68. package/web-dist/assets/{radar-I7S5WNFK-DldySvtZ.js → radar-I7S5WNFK-CK3dl20X.js} +1 -1
  69. package/web-dist/assets/{railroad-3IZDKUUU-CiWFZfXM.js → railroad-3IZDKUUU-BuB2LkFj.js} +1 -1
  70. package/web-dist/assets/railroad-abnf-AHOZXSZD-C1-MJAeY.js +1 -0
  71. package/web-dist/assets/railroad-ebnf-EBAXGLYW-crQCD9rJ.js +1 -0
  72. package/web-dist/assets/railroad-peg-LSFZ7HO6-Ba-_QGKi.js +1 -0
  73. package/web-dist/assets/{railroadDiagram-RFXS5EU6-DcCMNGGb.js → railroadDiagram-RFXS5EU6-CuuspS_3.js} +1 -1
  74. package/web-dist/assets/{requirementDiagram-TGXJPOKE-C-6j8urk.js → requirementDiagram-TGXJPOKE-DHAILLdh.js} +1 -1
  75. package/web-dist/assets/{sankeyDiagram-HTMAVEWB-CPtiOpdz.js → sankeyDiagram-HTMAVEWB-CztQ2t1b.js} +1 -1
  76. package/web-dist/assets/{sequenceDiagram-DBY2YBRQ-B8FnQTdn.js → sequenceDiagram-DBY2YBRQ-jk56eo0l.js} +1 -1
  77. package/web-dist/assets/{stateDiagram-2N3HPSRC-CxG9nX2P.js → stateDiagram-2N3HPSRC-uwqBWnrI.js} +1 -1
  78. package/web-dist/assets/stateDiagram-v2-6OUMAXLB-DOPbAEV3.js +1 -0
  79. package/web-dist/assets/{swimlanes-5IMT3BWC-D49-t0hV.js → swimlanes-5IMT3BWC-BzuJ77j6.js} +1 -1
  80. package/web-dist/assets/swimlanesDiagram-G3AALYLV-BRh1ca21.js +8 -0
  81. package/web-dist/assets/{timeline-definition-FHXFAJF6-C9OEd9SE.js → timeline-definition-FHXFAJF6-CVDNkzi9.js} +1 -1
  82. package/web-dist/assets/{treeView-QDETBFTQ-D1AKqVPa.js → treeView-QDETBFTQ-CdkPxaDl.js} +1 -1
  83. package/web-dist/assets/{treemap-6X3UGDF4-BRssB7hM.js → treemap-6X3UGDF4-lnksCHdn.js} +1 -1
  84. package/web-dist/assets/{vennDiagram-L72KCM5P-CftkAbY0.js → vennDiagram-L72KCM5P-B533fRM0.js} +1 -1
  85. package/web-dist/assets/{wardley-OPB4EBWU-RLMgzRq4.js → wardley-OPB4EBWU-DwKf-2YJ.js} +1 -1
  86. package/web-dist/assets/{wardleyDiagram-EHGQE667-BLytgXPS.js → wardleyDiagram-EHGQE667-BDDN72B1.js} +1 -1
  87. package/web-dist/assets/{xychartDiagram-FW5EYKEG-BYgK1Y3H.js → xychartDiagram-FW5EYKEG-CkvHmNVt.js} +1 -1
  88. package/web-dist/index.html +2 -2
  89. package/web-dist/assets/architecture-TIHT7OUA-CuD8jpr-.js +0 -1
  90. package/web-dist/assets/channel-C2eUWc74.js +0 -1
  91. package/web-dist/assets/classDiagram-OUVF2IWQ-GaOJNgfE.js +0 -1
  92. package/web-dist/assets/classDiagram-v2-EOCWNBFH-GaOJNgfE.js +0 -1
  93. package/web-dist/assets/eventmodeling-45OFAUF4-C0eV8RBx.js +0 -1
  94. package/web-dist/assets/flowDiagram-23GEKE2U-BbldRZUK.js +0 -1
  95. package/web-dist/assets/index-Duiy4w7C.css +0 -1
  96. package/web-dist/assets/railroad-abnf-AHOZXSZD-bXMYUOCD.js +0 -1
  97. package/web-dist/assets/railroad-ebnf-EBAXGLYW-D11ZwZby.js +0 -1
  98. package/web-dist/assets/railroad-peg-LSFZ7HO6-DD1AC5X-.js +0 -1
  99. package/web-dist/assets/stateDiagram-v2-6OUMAXLB-SGarOFIy.js +0 -1
  100. package/web-dist/assets/swimlanesDiagram-G3AALYLV-krF0bh4b.js +0 -8
  101. /package/runtime/{cc/scripts/fray → board}/agent-bindings.mjs +0 -0
  102. /package/runtime/{cc/scripts/fray → board}/agent-liveness.mjs +0 -0
  103. /package/runtime/{cc/scripts/fray → board}/agent-status.mjs +0 -0
  104. /package/runtime/{cc/scripts/fray → board}/config.mjs +0 -0
  105. /package/runtime/{cc/scripts/fray → board}/decisions.mjs +0 -0
  106. /package/runtime/{cc/scripts/fray → board}/index.mjs +0 -0
  107. /package/runtime/{cc/scripts/fray → board}/notify-shared.mjs +0 -0
  108. /package/runtime/{cc/scripts/fray → board}/notify.mjs +0 -0
  109. /package/runtime/{cc/scripts/fray → board}/ownership.mjs +0 -0
  110. /package/runtime/{cc/scripts/fray → board}/rest-detect.mjs +0 -0
  111. /package/runtime/{cc/scripts/fray → board}/thread-excerpt.mjs +0 -0
  112. /package/runtime/{cc/scripts/fray → board}/thread-update.mjs +0 -0
package/README.md CHANGED
@@ -1,186 +1,64 @@
1
- # fray-ui
2
-
3
- fray-ui is a workspace-scoped orchestration surface for [fray](https://github.com/colinhacks/fray): a localhost server plus a web
4
- client (opened as a normal tab in your default browser) that shows a sidebar of your repo's `.fray/`
5
- threads and, for the selected thread, a live embedded Claude Code terminal. You dispatch agents,
6
- watch them work, follow up, and clear the "awaiting you" queue — one server per repo. The UI itself
7
- has zero orchestration intelligence: all the judgment lives in the user-editable dispatch preamble
8
- (Settings) and in the [`cc-worker`](https://github.com/colinhacks/fray/tree/main/cc-worker) plugin that every dispatched agent loads.
1
+ <p align="center">
2
+ <h1 align="center">🧵<br/><code>fray</code></h1>
3
+ <p align="center">A local dashboard for running many coding agents at once.
4
+ <br/>
5
+ by <a href="https://x.com/colinhacks">@colinhacks</a>
6
+ </p>
7
+ </p>
8
+ <br/>
9
+
10
+ <p align="center">
11
+ <a href="https://opensource.org/licenses/MIT" rel="nofollow"><img src="https://img.shields.io/github/license/colinhacks/fray" alt="License"></a>
12
+ <a href="https://www.npmjs.com/package/frayui" rel="nofollow"><img src="https://img.shields.io/npm/dw/frayui.svg" alt="npm"></a>
13
+ <a href="https://github.com/colinhacks/fray" rel="nofollow"><img src="https://img.shields.io/github/stars/colinhacks/fray" alt="stars"></a>
14
+ </p>
15
+
16
+ ## What is fray?
17
+
18
+ Run one agent and you babysit a terminal. Run ten and you lose track of which ones are working, which
19
+ are stuck, and which are waiting on you.
20
+
21
+ fray is a web UI that runs on your machine, scoped to one repo. You describe a task, fray dispatches a
22
+ real [Claude Code](https://claude.com/claude-code) or [Codex](https://developers.openai.com/codex)
23
+ session to drive it, and every session shows up as a thread you can watch, steer, answer, or drop into
24
+ a live terminal. Threads that need you sort to the top; the rest get out of the way.
25
+
26
+ There is no cloud, no account, and no agent of its own — fray is a window onto the CLIs you already
27
+ have installed.
9
28
 
10
29
  ## Quickstart
11
30
 
12
- ```bash
13
- cd /path/to/fray
14
- nub install
15
- nub run fray-dev:install # one-time: writes ~/.local/bin/fray-dev → this checkout's CLI source
16
- ```
17
-
18
- Then, from any Git repository:
31
+ Requires **Node 26+**, `tmux`, and the [Claude Code](https://claude.com/claude-code) or
32
+ [Codex](https://developers.openai.com/codex) CLI signed in.
19
33
 
20
- ```bash
21
- cd /path/to/a/workspace
22
- fray-dev # foreground immutable server + default-browser request; Ctrl-C stops it
23
- fray-dev /path/to/other/repo # explicitly select a repository
24
- fray-dev --app # opt in to the legacy dedicated app window
25
- fray-dev --no-app # reuse/start and print its URL without opening a browser
26
- fray-dev --status # workspace, source checkout, port, supervisor PID
27
- fray-dev --stop # stop only the UI server; agent tmux sessions survive
28
- fray-dev --foreground # compatibility spelling; Fray already runs in the foreground
34
+ ```sh
35
+ cd /path/to/your/repo
36
+ npx frayui
29
37
  ```
30
38
 
31
- ## Production npm launcher
32
-
33
- The development command above is deliberately source-backed. The intended production command is a
34
- separate registry package: `npx frayui` will run the npm-resolved `frayui` package and never follow
35
- this checkout. The current package metadata still uses the working name `fray`; migrating it to the
36
- single-file `frayui` bundle is a separate release-packaging slice. When a published release is
37
- available, **Update & Restart** will ask npm for a fresh immutable package execution cache, drain only
38
- Fray's disposable HTTP control plane, and start that cache with the
39
- same project identity, port, SQLite state, tmux socket, and provider sessions. It does not edit the
40
- currently executing `npx` cache or replace an arbitrary global installation.
41
-
42
- This repository has **not** published the package or verified ownership/availability of the unscoped
43
- `frayui` npm name. The current workspace dependencies still need a maintainer-selected release closure (publish the
44
- `@fray-ui/*` runtime packages, or build one audited bundled runtime tarball) before `npx frayui` can
45
- be released; the npm publisher account and that packaging decision require maintainer action.
46
- Until then, `fray-dev` is the supported command for this checkout.
47
-
48
- `fray-dev` is source-backed only at launch: the installed shim contains an absolute, shell-safe pointer
49
- to this checkout's `packages/cli/src/index.ts`, not a copied build. On each fresh launch after that
50
- workspace's Fray supervisor has stopped, it selects a verified immutable artifact matching the current
51
- source fingerprint, reuses an identical global artifact when one exists, or builds and promotes one
52
- automatically. No manual `build`/`promote` step or repeated shim install is required. The running server
53
- never watches this checkout and never runs HMR: edits made while it is running do nothing until you stop
54
- Fray and run `fray-dev` again. SQLite state, persisted port, lock, legacy app profile, and tmux socket
55
- are keyed by a stable checkout UUID. An ordinary/main worktree retains the repo's
56
- `git config --local fray.id`; each linked Git worktree keeps a different UUID in its private Git
57
- administrative directory. Canonical real paths make a checkout opened through a symlink reuse the
58
- same instance, while sibling worktrees remain isolated even when their paths contain spaces.
59
- Managed ordinary repositories use `tmux -L fray-repo-<full UUID>`; a durable migration record keeps
60
- an attributable historical `fray-<8 chars>` server pinned until its sessions are gone, then switches
61
- permanently without moving them. An explicit `FRAY_TMUX_SOCKET` is an unmanaged escape hatch and is
62
- used verbatim—Fray does not inspect, migrate, or add ownership markers to that server.
63
-
64
- `fray-dev` stays in the foreground while it serves its immutable snapshot; Ctrl-C stops only that
65
- workspace's UI server. A second `fray-dev` verifies `/health` has both this UUID and exact canonical
66
- worktree root, then asks the OS to open its URL in the configured default browser instead of starting
67
- a competitor. New workspaces serialize only port allocation/startup, then run concurrently on isolated
68
- ports and tmux sockets. `--port <n>` is an explicit new-launch request and fails clearly on a conflict.
69
- Broken live supervisors are never silently replaced; inspect `fray-dev --status`, fix source, or use
70
- `fray-dev --stop`.
71
-
72
- If `~/.local/bin` is not on `PATH`, the installer prints the exact export to add. Use
73
- `FRAY_BIN_DIR=/another/bin nub run fray-dev:install` to choose another directory; it refuses to overwrite
74
- an unrelated `fray-dev` executable unless passed `--force`. `nub run fray-dev:check` verifies the owned
75
- shim without changing it, and `nub run fray-dev:uninstall` removes only that owned shim. `fray-ui` remains
76
- a package-bin alias for compatibility, but `fray-dev` is the canonical source-checkout command.
77
-
78
- ## Browser launch modes
79
-
80
- The default `fray-dev` launch makes one standard OS request to open its localhost URL in the configured
81
- default browser. The launcher waits for the OS handler to accept or reject that request; the browser
82
- decides which window or process receives it. In particular, macOS may choose among running browser
83
- instances that share one bundle identity. Fray does not scan, reuse, focus, or privately address
84
- browser tabs. External links use native safe new-tab navigation and internal links stay in the local
85
- browser context. `fray-dev --no-app` remains the print-only option.
86
-
87
- `fray-dev --app` preserves the legacy dedicated/chromeless window as an explicit compatibility opt-in.
88
- On macOS that window gets its **own Dock name ("Fray") and icon**. On the first opt-in app launch,
89
- the launcher silently installs the Fray PWA into the project's browser profile
90
- over the Chrome DevTools Protocol (`--remote-debugging-pipe` → `PWA.install` +
91
- `PWA.changeAppUserSettings(displayMode: standalone)`; windowless, ~3-4s, once per machine). Chrome
92
- then generates a real app-shim bundle at `~/Applications/Chrome Apps.localized/fray.app` — own
93
- `CFBundleName`, icon rasterized from our web-app manifest — and every launch goes through that shim
94
- (`open fray.app`), whose `app_mode_loader` hosts the window under its own bundle identity. Verified:
95
- `lsappinfo` shows `LSDisplayName="fray"`, `Arch=ARM64`, and the Dock shows fray's own tile.
96
- Subsequent launches detect the existing shim (~60ms) and skip the install.
97
-
98
- Why it works this way (all verified empirically on Chrome 150 / macOS):
99
-
100
- - A plain `--app=` window is owned by the Chrome browser process — the Dock shows "Google Chrome",
101
- no launch flag changes it, and a hand-rolled `.app` that `exec`s Chrome loses its identity the
102
- moment Chrome's Cocoa startup re-registers the process (and risks a Rosetta launch besides).
103
- Chrome's generated app-shim is the only mechanism that yields an own Dock identity.
104
- - The CDP `PWA.*` domain is only exposed on `--remote-debugging-pipe` connections (port-based
105
- websocket clients lack `AllowUnsafeOperations`), and a CDP install defaults the app to
106
- open-in-a-tab — `changeAppUserSettings(displayMode: "standalone")` is the required second half;
107
- without it the browser answers the shim `kSuccessAndDisconnect` and windows stay Chrome-branded.
108
- - Shim detection is stateless: the launcher scans shim `Info.plist`s for `CrAppModeShortcutURL` ==
109
- the launch URL and `CrAppModeUserDataDir` under the project profile. (Chrome's generated app id is
110
- not reproducible as a hash of the URL — don't try.)
111
-
112
- Failure at any opt-in app step (profile busy, install error, no shim found) falls back silently to
113
- the plain `--app` window. [`packages/web/public/favicon.svg`](https://github.com/colinhacks/fray/tree/main/packages/web/public/favicon.svg) is
114
- the canonical icon artwork; `nub scripts/generate-icons.mjs` regenerates its six tracked PNG
115
- derivatives, while `nub scripts/generate-icons.mjs --check` detects drift. On macOS,
116
- `nub scripts/generate-icons.mjs --refresh-app-icons` also refreshes the ICNS resource in idle,
117
- metadata-verified Fray PWA shims without launching Chrome; it stages and verifies each ad-hoc-signed
118
- bundle before swapping it into place. The manifest lives in
119
- [`packages/web/public/`](https://github.com/colinhacks/fray/tree/main/packages/web/public); the window title is set at runtime to
120
- `fray · <owner/repo>` from the board snapshot.
121
-
122
- *Windows/Linux follow-ups (not yet wired):* the launcher only brands the Dock on macOS. Windows would
123
- set an `AppUserModelID` (+ icon) on a generated `.lnk`; Linux (X11) would pass `--class=fray` and ship
124
- a `.desktop` file whose `StartupWMClass=fray` matches it. Both are documented TODOs.
125
-
126
- ## Architecture (10 lines)
127
-
128
- 1. Workspace-scoped: one server per repo, launched from the repo root, watching only that repo's `.fray/`.
129
- 2. `.fray/` thread files are the source of truth for thread status — the server imports fray's own
130
- board parser (`cc/scripts/fray/*.mjs`), never re-implements it.
131
- 3. Session JSONL (`~/.claude/projects/<slug>/*.jsonl`) is telemetry only: liveness + previews, parsed defensively.
132
- 4. Agents are top-level processes in detached tmux sessions on the workspace's private, UUID-keyed socket.
133
- 5. The web terminal attaches per-viewer via node-pty (`tmux attach`); closing a tab kills only that attach client.
134
- 6. A single `/events` SSE channel pushes full board snapshots (no diff protocol) plus `notify` events.
135
- 7. RPC is a typed query/mutation layer over Hono at `/rpc`; the terminal is a WebSocket at `/term/:slug`.
136
- 8. `web` is React 19 + Vite + Tailwind v4 + valtio + TanStack Query + xterm.js.
137
- 9. UI state (unread, settings, session registry) lives in `~/.fray/projects/<id>/ui.db` (SQLite), never in `.fray/`.
138
- 10. Packages: `shared` (zod contract), `rpc`, `server`, `web`, `cli`. Read `ARCHITECTURE.md` before touching any.
139
-
140
- ## Settings & the dispatch preamble
141
-
142
- Everything the UI deliberately does NOT know lives in **Settings** (gear icon): the
143
- **dispatch preamble** is injected verbatim ahead of every dispatched agent's task, carrying the
144
- orchestration wisdom (sub-agent discipline, git hygiene, the stop criterion). It ships from
145
- [`DEFAULT_PREAMBLE.md`](https://github.com/colinhacks/fray/blob/main/packages/server/src/DEFAULT_PREAMBLE.md); edit it per project and use **Reset preamble to
146
- default** to clear your overrides back to the shipped defaults. Settings also carry the default
147
- model, effort, and a desktop-notifications toggle (which requests browser
148
- permission and only fires when the Fray tab or window is hidden).
39
+ That's the whole setup. It opens a browser tab, and one server runs per repo.
149
40
 
150
- ## `FRAY.md` a repo's own worker norms
41
+ ## How it works
151
42
 
152
- The dispatch preamble is per-**operator** state that lives in the UI's database. For norms that should
153
- travel **with the repo** and be shared by the whole team, drop a **`FRAY.md`** at the project root.
154
- When present, its contents are injected into every dispatched worker's system prompt (dispatch, adopt,
155
- and resume, on both the Claude and Codex backends) under a header stating that they **override fray's
156
- built-in worker defaults** wherever they conflict. Fray's worker contract is deliberately
157
- defer-by-default: its engineering-process guidance (review depth, git/PR conventions, the quality bar)
158
- is a default for when the project is silent, and `FRAY.md` is how a project speaks. Use it to say
159
- things like "our gates are `pnpm check`", "skip the independent adversarial review on small UI diffs —
160
- implementer self-review plus a browser screenshot is enough", or "push to a branch + PR, never merge
161
- your own". The fray-mechanical contract (signal fences, scratchpad, the browser runtime gate) is not
162
- overridable. Keep it concise — it's read fresh on every spawn and bounded to stay within the
163
- system-prompt budget.
43
+ - **One thread, one effort.** Each thread owns a single top-level agent session in a detached `tmux`
44
+ session, so closing the tab never kills the work. `tmux -L fray attach` is always there if you want
45
+ the raw terminal.
46
+ - **Agents signal, they don't just stop.** A worker ends its turn with a fenced ` ```done `,
47
+ ` ```awaiting `, or ` ```question ` block. fray renders those as cards — a question becomes
48
+ answerable choices, an `awaiting pr-watch:` parks the thread and wakes it when the PR moves.
49
+ - **Nothing is hidden in the app.** All the orchestration judgment lives in text you can edit: the
50
+ **dispatch preamble** in Settings, and a **`FRAY.md`** at your repo root whose contents are injected
51
+ into every worker and override fray's defaults.
52
+ - **State stays out of your repo.** Unread counts, settings, and the session registry live in
53
+ `~/.fray/projects/<id>/ui.db`.
164
54
 
165
- ## The cc-worker plugin
55
+ ## Docs
166
56
 
167
- Every agent fray-ui spawns loads the [`cc-worker`](https://github.com/colinhacks/fray/tree/main/cc-worker) Claude Code plugin
168
- (`--plugin-dir`), which supplies the single-thread worker contract and hooks. Its hooks gate on the
169
- `FRAY_UI_THREAD` env var, so passing it is harmless even in a non-fray repo. In the monorepo it is
170
- found automatically as a sibling of `cc/`; a standalone install points at it with
171
- `FRAY_WORKER_PLUGIN_DIR`.
57
+ - [`ARCHITECTURE.md`](ARCHITECTURE.md) the invariants, layout, and design decisions. Read it before
58
+ changing anything.
59
+ - [`cc-worker/`](cc-worker) the Claude Code plugin every dispatched agent loads.
60
+ - [`board/`](board) the zero-dep `.fray/` board parser the server shells out to.
172
61
 
173
- ## Troubleshooting
62
+ ## License
174
63
 
175
- - **Terminal never attaches / `spawn-helper` errors.** node-pty prebuilds lose the exec bit when
176
- npm/pnpm unpack them. The server package's `postinstall` re-chmods it; if it didn't run, do it
177
- manually: `chmod +x node_modules/node-pty/prebuilds/*/spawn-helper`. PTY code cannot run inside a
178
- sandboxed shell.
179
- - **Escape hatch into a stuck agent.** The sessions live on the workspace socket recorded in
180
- `~/.fray/projects/<id>/project-launch.owner` (or `tmux-socket-migration.json` while stopped).
181
- Attach with `tmux -L <socket> attach -t fray-<slug>` and detach with `Ctrl-b d`.
182
- - **Non-monorepo install** (fray-ui run against a repo outside this monorepo): set
183
- `FRAY_SCRIPTS_DIR` to the installed plugin's `scripts/fray` directory (the board parser) and
184
- `FRAY_WORKER_PLUGIN_DIR` to the `cc-worker` plugin directory.
185
- - **Port already in use.** Omit `--port` to let `fray-dev` allocate and persist an isolated port. An
186
- explicit occupied port is rejected; `fray-dev --status` shows the workspace's current owner.
64
+ MIT
package/dist/dev-child.js CHANGED
@@ -6551,21 +6551,38 @@ function parseAgentMessage(text) {
6551
6551
  function wakeItemTail(item) {
6552
6552
  return `${item.at ? ` at ${item.at}` : ""}${item.url ? `: ${item.url}` : ""}`;
6553
6553
  }
6554
+ function wakeReviewReads({ ref, items }) {
6555
+ const [repo, number] = ref.split("#");
6556
+ const ids = /* @__PURE__ */ new Set();
6557
+ for (const item of items) {
6558
+ const id = /#pullrequestreview-(\d+)$/.exec(item.url ?? "")?.[1];
6559
+ if (id) ids.add(id);
6560
+ }
6561
+ return [...ids].map((id) => `gh api --paginate repos/${repo}/pulls/${number}/reviews/${id}/comments`);
6562
+ }
6563
+ function wakeReviewTail(steer) {
6564
+ const reads = wakeReviewReads(steer);
6565
+ return reads.length ? `
6566
+
6567
+ ${WAKE_REVIEW_LEAD}
6568
+ ${reads.join("\n")}` : "";
6569
+ }
6554
6570
  function formatGithubWakeSteer({ ref, items, omitted }) {
6555
6571
  const icon = items.some((i) => !i.bot) ? "\u{1F464}" : "\u{1F916}";
6572
+ const reviewTail = wakeReviewTail({ ref, items, omitted });
6556
6573
  if (items.length === 1 && omitted === 0) {
6557
6574
  const item = items[0];
6558
6575
  const url = item.url ? `: ${item.url}` : ".";
6559
- return `${icon} New GitHub ${item.label} on ${ref} from @${item.actor}${item.at ? ` at ${item.at}` : ""}. Read that exact ${item.label} \u2014 ${WAKE_SCOPE} \u2014 and continue${url}`;
6576
+ return `${icon} New GitHub ${item.label} on ${ref} from @${item.actor}${item.at ? ` at ${item.at}` : ""}. Read that exact ${item.label} \u2014 ${WAKE_SCOPE} \u2014 and continue${url}${reviewTail}`;
6560
6577
  }
6561
6578
  const more = omitted > 0 ? `
6562
6579
  - \u2026and ${omitted} more not listed \u2014 check ${ref} for the rest` : "";
6563
6580
  const lines = items.map((i) => `- ${i.bot ? "\u{1F916}" : "\u{1F464}"} ${i.label} from @${i.actor}${wakeItemTail(i)}`).join("\n");
6564
6581
  return `${icon} ${items.length + omitted} new GitHub items on ${ref}. Read exactly these \u2014 ${WAKE_SCOPE} \u2014 and continue:
6565
6582
 
6566
- ${lines}${more}`;
6583
+ ${lines}${more}${reviewTail}`;
6567
6584
  }
6568
- var ATTACHMENT_IMAGE_EXTENSIONS, ATTACHMENT_DOC_EXTENSIONS, ATTACHMENT_EXTENSIONS, ATTACHMENT_MAX_BASE64_CHARS, ATTACHMENT_MAX_BYTES, ATTACHMENT_EXT_SET, ATTACHMENT_ACCEPT, FrayStatus, BlockMechanism, RuntimeState, Backend, CodexModel, ThreadProfileOption, ThreadAgent, SubAgentView, BgShellView, CompletionHoldOp, CompletionHold, AskOption, AskQuestion, PendingAsk, NativeInputRequired, AwaitingHint, AWAITING_TIMER_RE, SnoozeUntil, SNOOZE_PROMPT_MAX, SnoozePrompt, ThreadFence, PlanView, LimitWindow, LimitPause, ThreadView, BoardErrorItem, BoardSnapshot, QuotaWindow, ProviderQuota, QuotaSnapshot, ProviderAuth, AuthSnapshot, AccountLogoutInput, AccountLogoutResult, AccountLoginStartInput, AccountLoginStartResult, AccountLoginStatusInput, AccountLoginStatusResult, PermissionMode, LocalFileOpener, Settings, DispatchProviderPreferences, DispatchPreferences, DispatchProfileSnapshot, SetDispatchPreferenceInput, DispatchInput, ADOPT_THREAD_MESSAGE_MAX_CHARS, AdoptThreadInput, AdoptThreadResult, FollowUpInput, UnqueueFollowUpInput, UnqueueFollowUpResult, SetThreadSnoozeInput, ConfirmAwaitingInput, RenameThreadInput, AiRenameThreadInput, AiRenameThreadResult, SetThreadPermissionInput, SetThreadPermissionResult, ThreadProfileOptionsInput, ThreadProfileOptionsResult, SetThreadProfileInput, SetThreadProfileResult, DISPATCH_TASK_BANNER, DISPATCH_TASK_BANNER_MARKER, GITHUB_DISPATCH_UI_BOUNDARY, WAKE_DELIVERY_TOKEN_TAIL, AGENT_MESSAGE_WRAPPER, WAKE_SCOPE, WAKE_REF, WAKE_SINGLE, WAKE_MULTI_HEAD, GithubStatus, GithubItem, GithubListInput, GithubBatchInput, GithubBatchResult, BoardMeta, ServerEvent, TranscriptEdit, TranscriptTodo, TranscriptToolCall, THOUGHT_EVENT_PREFIX, TranscriptPart, TranscriptMessage, TranscriptPageCursor, TranscriptPage, TranscriptEarlierInput, SocketTranscriptSlug, SocketClientMsg, DEFAULT_PORT, tmuxSessionName;
6585
+ var ATTACHMENT_IMAGE_EXTENSIONS, ATTACHMENT_DOC_EXTENSIONS, ATTACHMENT_EXTENSIONS, ATTACHMENT_MAX_BASE64_CHARS, ATTACHMENT_MAX_BYTES, ATTACHMENT_EXT_SET, ATTACHMENT_ACCEPT, FrayStatus, BlockMechanism, RuntimeState, Backend, CodexModel, ThreadProfileOption, ThreadAgent, SubAgentView, BgShellView, CompletionHoldOp, CompletionHold, AskOption, AskQuestion, PendingAsk, NativeInputRequired, AwaitingHint, AWAITING_TIMER_RE, SnoozeUntil, SNOOZE_PROMPT_MAX, SnoozePrompt, ThreadFence, PlanView, LimitWindow, LimitPause, ThreadView, BoardErrorItem, BoardSnapshot, QuotaWindow, ProviderQuota, QuotaSnapshot, ProviderAuth, AuthSnapshot, AccountLogoutInput, AccountLogoutResult, AccountLoginStartInput, AccountLoginStartResult, AccountLoginStatusInput, AccountLoginStatusResult, PermissionMode, LocalFileOpener, Settings, DispatchProviderPreferences, DispatchPreferences, DispatchProfileSnapshot, SetDispatchPreferenceInput, DispatchInput, ADOPT_THREAD_MESSAGE_MAX_CHARS, AdoptThreadInput, AdoptThreadResult, FollowUpInput, UnqueueFollowUpInput, UnqueueFollowUpResult, SetThreadSnoozeInput, ConfirmAwaitingInput, RenameThreadInput, AiRenameThreadInput, AiRenameThreadResult, SetThreadPermissionInput, SetThreadPermissionResult, ThreadProfileOptionsInput, ThreadProfileOptionsResult, SetThreadProfileInput, SetThreadProfileResult, DISPATCH_TASK_BANNER, DISPATCH_TASK_BANNER_MARKER, GITHUB_DISPATCH_UI_BOUNDARY, WAKE_DELIVERY_TOKEN_TAIL, AGENT_MESSAGE_WRAPPER, WAKE_SCOPE, WAKE_REVIEW_LEAD, WAKE_REF, WAKE_SINGLE, WAKE_MULTI_HEAD, GithubStatus, GithubItem, GithubListInput, GithubBatchInput, GithubBatchResult, BoardMeta, ServerEvent, TranscriptEdit, TranscriptTodo, TranscriptToolCall, THOUGHT_EVENT_PREFIX, TranscriptPart, TranscriptMessage, TranscriptPageCursor, TranscriptPage, TranscriptEarlierInput, SocketTranscriptSlug, SocketClientMsg, DEFAULT_PORT, tmuxSessionName;
6569
6586
  var init_src = __esm({
6570
6587
  "packages/shared/src/index.ts"() {
6571
6588
  "use strict";
@@ -7271,6 +7288,7 @@ ${DISPATCH_TASK_BANNER}
7271
7288
  WAKE_DELIVERY_TOKEN_TAIL = /\n*<!-- fray-wake:[A-Za-z0-9_-]+ -->\s*$/;
7272
7289
  AGENT_MESSAGE_WRAPPER = /^<agent-message from="([^"]*)">\n?([\s\S]*?)\n?<\/agent-message>\s*$/;
7273
7290
  WAKE_SCOPE = "ignore older activity you have already handled";
7291
+ WAKE_REVIEW_LEAD = "A review's body is often empty because its substance is inline comments. Read them, one call each:";
7274
7292
  WAKE_REF = String.raw`[A-Za-z0-9][\w.-]*\/[A-Za-z0-9][\w.-]*#\d+`;
7275
7293
  WAKE_SINGLE = new RegExp(
7276
7294
  String.raw`^(👤|🤖) New GitHub (.+?) on (${WAKE_REF}) from @(\S+?)(?: at (\S+?))?\. Read that exact .+? — ` + WAKE_SCOPE + String.raw` — and continue(?::\s*(\S+)|\.)$`
@@ -13378,7 +13396,7 @@ import { basename as basename3, dirname as dirname5, join as join10, resolve as
13378
13396
  import { promisify as promisify4 } from "node:util";
13379
13397
  function frayScriptsDir() {
13380
13398
  if (process.env.FRAY_SCRIPTS_DIR) return process.env.FRAY_SCRIPTS_DIR;
13381
- return resolve5(import.meta.dirname, "..", "..", "..", "cc", "scripts", "fray");
13399
+ return resolve5(import.meta.dirname, "..", "..", "..", "board");
13382
13400
  }
13383
13401
  function directFrayRoot(projectDir) {
13384
13402
  try {
@@ -13416,7 +13434,7 @@ async function readBoard(projectDir, scriptsDir = frayScriptsDir()) {
13416
13434
  threads: rawThreads.filter((thread) => ThreadSlug.safeParse(thread?.id).success),
13417
13435
  errors: [...Array.isArray(parsed.errors) ? parsed.errors : [], ...invalidErrors],
13418
13436
  warnings: Array.isArray(parsed.warnings) ? parsed.warnings : [],
13419
- // Additive: absent on a pre-update fray script (older cc/scripts) → [] (the board just loses the
13437
+ // Additive: absent on a pre-update fray script (older board scripts) → [] (the board just loses the
13420
13438
  // repair affordance, never the plain error strings).
13421
13439
  errorItems: [...Array.isArray(parsed.errorItems) ? parsed.errorItems : [], ...invalidErrorItems]
13422
13440
  };
@@ -33978,6 +33996,7 @@ function isDirectHookExecution(argv1, moduleUrl) {
33978
33996
  }
33979
33997
  var init_bash_background = __esm({
33980
33998
  "cc-worker/hooks/bash-background.mjs"() {
33999
+ "use strict";
33981
34000
  if (isDirectHookExecution(process.argv[1], import.meta.url)) {
33982
34001
  try {
33983
34002
  const env = process.argv.includes("--fray-ui-thread") ? { ...process.env, FRAY_UI_THREAD: process.env.FRAY_UI_THREAD || "codex-worker" } : process.env;
package/dist/frayui.js CHANGED
@@ -7214,14 +7214,14 @@ var init_local_image = __esm({
7214
7214
  }
7215
7215
  });
7216
7216
 
7217
- // packages/cli/src/production.ts
7217
+ // src/production.ts
7218
7218
  import { spawn as spawn2 } from "node:child_process";
7219
7219
  import { closeSync as closeSync4, openSync as openSync4 } from "node:fs";
7220
7220
  import { fileURLToPath as fileURLToPath2 } from "node:url";
7221
7221
  import { join as join7 } from "node:path";
7222
7222
  import { setTimeout as delay3 } from "node:timers/promises";
7223
7223
 
7224
- // packages/cli/src/launcher.ts
7224
+ // src/launcher.ts
7225
7225
  init_project_identity();
7226
7226
  init_project_launch();
7227
7227
  init_tmux_socket();
@@ -7515,7 +7515,7 @@ async function waitForWorkspace(port, expected2, timeoutMs = LAUNCH_TIMEOUT_MS,
7515
7515
  }
7516
7516
  }
7517
7517
 
7518
- // packages/cli/src/production.ts
7518
+ // src/production.ts
7519
7519
  init_project_launch();
7520
7520
 
7521
7521
  // packages/server/src/dev-supervisor.ts
@@ -7847,9 +7847,9 @@ function classifyDevChange(path, roots = defaultDevWatchRoots()) {
7847
7847
  return "launcher";
7848
7848
  }
7849
7849
  if (!SOURCE_EXTENSIONS.has(extname2(name))) return null;
7850
+ if (parts[0] === "src") return "launcher";
7850
7851
  if (parts[0] !== "packages" || parts[2] !== "src") return null;
7851
7852
  const pkg = packageName;
7852
- if (pkg === "cli") return "launcher";
7853
7853
  if (pkg === "server") {
7854
7854
  if (name === "dev-supervisor.ts" || name === "dev.ts") return "launcher";
7855
7855
  return "child";
@@ -8446,7 +8446,7 @@ async function startDevSupervisor(opts) {
8446
8446
  return supervisor;
8447
8447
  }
8448
8448
 
8449
- // packages/cli/src/production-update.ts
8449
+ // src/production-update.ts
8450
8450
  import { execFile, spawn } from "node:child_process";
8451
8451
  var PRODUCTION_REEXEC_FLAG = "--_fray-production-reexec";
8452
8452
  function compareReleaseVersions(a, b) {
@@ -8517,7 +8517,7 @@ var npmRegistryReleaseAdapter = {
8517
8517
  }
8518
8518
  };
8519
8519
 
8520
- // packages/cli/src/preflight.ts
8520
+ // src/preflight.ts
8521
8521
  import { spawnSync } from "node:child_process";
8522
8522
  import { chmodSync, statSync as statSync5 } from "node:fs";
8523
8523
  import { createRequire } from "node:module";
@@ -8566,7 +8566,7 @@ function ensureNativeHelperPermissions(options2 = {}) {
8566
8566
  }
8567
8567
  }
8568
8568
 
8569
- // packages/cli/src/production.ts
8569
+ // src/production.ts
8570
8570
  var ABANDON_GRACE_MS = 3e3;
8571
8571
  var PACKAGE_NAME = process.env.FRAY_REGISTRY_PACKAGE ?? "frayui";
8572
8572
  var PACKAGE_VERSION = process.env.npm_package_version ?? "0.0.1";
@@ -8651,7 +8651,7 @@ async function runSupervisor(port, token) {
8651
8651
  const env = projectLaunchEnvironment({ ...process.env, FRAY_PRODUCTION_SUPERVISOR: "1" }, target, owner.token);
8652
8652
  const webDist = join7(import.meta.dirname, "..", "web-dist");
8653
8653
  const runtimeDir = join7(import.meta.dirname, "..", "runtime");
8654
- const scriptsDir = join7(runtimeDir, "cc", "scripts", "fray");
8654
+ const scriptsDir = join7(runtimeDir, "board");
8655
8655
  const workerPluginDir = join7(runtimeDir, "cc-worker");
8656
8656
  const childEntry = fileURLToPath2(new URL("./dev-child.js", import.meta.url));
8657
8657
  let plannedUpdate;
package/package.json CHANGED
@@ -1,23 +1,23 @@
1
1
  {
2
2
  "name": "frayui",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "private": false,
5
- "description": "Local orchestration surface for Fray projects a per-repo web UI over your .fray/ agent threads",
5
+ "description": "A local dashboard for running many coding agents at once",
6
6
  "keywords": [
7
7
  "fray",
8
- "claude",
9
- "claude-code",
10
8
  "agents",
9
+ "claude-code",
10
+ "codex",
11
11
  "orchestration",
12
- "tui"
12
+ "dashboard",
13
+ "ai"
13
14
  ],
14
15
  "license": "MIT",
15
16
  "author": "Colin McDonnell",
16
17
  "homepage": "https://github.com/colinhacks/fray",
17
18
  "repository": {
18
19
  "type": "git",
19
- "url": "git+https://github.com/colinhacks/fray.git",
20
- "directory": "packages/cli"
20
+ "url": "git+https://github.com/colinhacks/fray.git"
21
21
  },
22
22
  "engines": {
23
23
  "node": ">=26"
@@ -32,6 +32,13 @@
32
32
  "runtime"
33
33
  ],
34
34
  "scripts": {
35
+ "dev": "nub packages/server/src/dev.ts",
36
+ "fray-dev:install": "nub scripts/install-global-cli.mjs",
37
+ "fray-dev:check": "nub scripts/install-global-cli.mjs --check",
38
+ "fray-dev:uninstall": "nub scripts/install-global-cli.mjs --uninstall",
39
+ "install:global": "nub scripts/install-global-cli.mjs",
40
+ "typecheck": "tsc -b packages/shared packages/rpc packages/server . && pnpm --filter @fray-ui/web typecheck",
41
+ "test": "nub --test --test-force-exit 'board/*.test.mjs' 'scripts/**/*.test.mjs' 'src/**/*.test.ts' 'packages/shared/src/**/*.test.ts' 'packages/server/src/**/*.test.ts' 'packages/web/src/**/*.test.ts'",
35
42
  "prepack": "node ./scripts/prepare-package.mjs && node ./scripts/build-package.mjs && node ./scripts/publish-manifest.mjs --strip",
36
43
  "postpack": "node ./scripts/publish-manifest.mjs --restore"
37
44
  },
@@ -41,6 +48,18 @@
41
48
  "node-pty": "^1.1"
42
49
  },
43
50
  "devDependencies": {
44
- "esbuild": "^0.25.0"
51
+ "@types/node": "^24",
52
+ "esbuild": "^0.25.0",
53
+ "puppeteer": "^25.3.0",
54
+ "typescript": "^5.7"
55
+ },
56
+ "pnpm": {
57
+ "onlyBuiltDependencies": [
58
+ "better-sqlite3",
59
+ "node-pty",
60
+ "@parcel/watcher",
61
+ "esbuild",
62
+ "@fray-ui/server"
63
+ ]
45
64
  }
46
65
  }
@@ -10,16 +10,16 @@ what was ported from the orchestrator `cc/` plugin, what was dropped, and why.
10
10
  ## Shared source, bundled runtime closure
11
11
 
12
12
  - **`scripts/fray/config.mjs` and `scripts/fray/agent-bindings.mjs` are THIN SHIMS** that
13
- `export *` from `../../../cc/scripts/fray/*.mjs`. cc-worker never copies config/vocab/binding
13
+ `export *` from `../../../board/*.mjs`. cc-worker never copies config/vocab/binding
14
14
  logic — there is exactly one source of truth (cc's). This assumes cc is a sibling dir (`../../cc/`
15
15
  from the plugin root), the same assumption fray-ui's server makes (`ARCHITECTURE.md`: it imports
16
- the board logic from `../../cc/scripts/fray/*.mjs`).
16
+ the board logic from `../../board/*.mjs`).
17
17
  - **`bin/fray` and `bin/fray-update`** are cc's exact shim pattern, resolving cc's real scripts at
18
- `../../cc/scripts/fray/{index,thread-update}.mjs` relative to the bin file (cwd-independent). They
18
+ `../../board/{index,thread-update}.mjs` relative to the bin file (cwd-independent). They
19
19
  land on the worker's Bash PATH the way cc's do. `fray-update` is the worker's primary tool for
20
20
  owning its one thread file; `fray` lets it read/validate the board.
21
- - **Portable artifact rule:** `packages/cli/src/artifacts.ts` copies the exact sibling
22
- `cc/scripts/fray/` module closure to `runtime/cc/scripts/fray/`, beside `runtime/cc-worker/`.
21
+ - **Portable artifact rule:** `src/artifacts.ts` copies the exact sibling
22
+ `board/` module closure to `runtime/board/`, beside `runtime/cc-worker/`.
23
23
  The existing shims therefore resolve inside an immutable artifact when the source checkout is gone;
24
24
  both the worker and cc closure are hashed in `manifest.runtimeFiles` and required at read time.
25
25
  - **`agents/*.md`** are copied UNCHANGED from `cc/agents/` (16 profiles) — a worker dispatches its
@@ -77,7 +77,7 @@ what was ported from the orchestrator `cc/` plugin, what was dropped, and why.
77
77
  worker session starts in the same repo, do both plugins' hooks fire (double-hook)?
78
78
 
79
79
  **Finding — NO, not by default. cc is inert in a fresh worker session.** cc's every hook is gated on
80
- `frayActive(projectDir, sessionId)` (`cc/scripts/fray/config.mjs`). That gate is **opt-IN per
80
+ `frayActive(projectDir, sessionId)` (`board/config.mjs`). That gate is **opt-IN per
81
81
  session**: it requires `.fray/` to exist AND a per-session sentinel at
82
82
  `.fray/.session-state/<session_id>` containing `on` (written by `fray on` / the orchestrator fray
83
83
  skill's Step 0). With no sentinel it returns **false** — the documented default:
@@ -144,7 +144,7 @@ fray-ui workers. What landed:
144
144
  derived from that skill's `:92-150`/`:242-317`/`:463` framing). Also added a status-field
145
145
  discipline section (later split into `activity` + `status_text` — see the follow-up) and an
146
146
  "awaiting your OWN sub-agent is NOT blocked" clarification.
147
- - **`ui/WORKER_PROMPT.md`** (fray-ui system prompt, not in this plugin) — carries the terse version
147
+ - **`packages/server/src/workerPrompt.ts`** (fray-ui system prompt, not in this plugin) — carries the terse version
148
148
  of the same three: a "Status discipline" block, the model/effort doctrine in the Sub-agents
149
149
  section, and a "Thread types" section. This is the maintainer's explicit ask that the preset
150
150
  vocabulary ride in the SYSTEM prompt passed to every worker. No dispatch.ts change, so no fray-ui
@@ -168,9 +168,9 @@ Three maintainer refinements landed on top of the port:
168
168
  `activity` = the form-constrained LIVE label the UI renders beside the spinner (single line, ≤100
169
169
  chars, present-progressive gerund); `status_text` = the classic 1–2-sentence human gloss that also
170
170
  doubles as THE ask on a human-`blocked` thread (queue cards headline it; no gerund constraint). The
171
- gerund/≤100 discipline moved OFF `status_text` and ONTO `activity` in `ui/WORKER_PROMPT.md` +
171
+ gerund/≤100 discipline moved OFF `status_text` and ONTO `activity` in `packages/server/src/workerPrompt.ts` +
172
172
  `skills/worker/SKILL.md`. (UI-side `activity` plumbing/rendering — board JSON → ThreadView → listing
173
- row — is a SIBLING agent's scope; not touched here beyond `ui/WORKER_PROMPT.md`.)
173
+ row — is a SIBLING agent's scope; not touched here beyond `packages/server/src/workerPrompt.ts`.)
174
174
 
175
175
  **2. New PostToolUse validation hook — `hooks/thread-frontmatter-validate.mjs`** (matcher
176
176
  `Edit|Write|MultiEdit`, wired in `hooks/hooks.json`). Gated on FRAY_UI_THREAD + a top-level
@@ -193,7 +193,7 @@ files + their frontmatter from `fray-<model>-<effort>` to `<model>-<effort>` (ba
193
193
  dispatches read `fray:opus-high` etc. Renamed the worker skill dir + frontmatter `fray-worker` →
194
194
  `worker` (giving `fray:worker`, not the stutter `fray:fray-worker`). The plugin DIRECTORY stays
195
195
  `cc-worker/` (dispatch.ts points at that path — unchanged, no server restart). Every reference in
196
- `ui/WORKER_PROMPT.md`, both skills, the hooks, and this file was updated; a grep for the old prefix token is clean.
196
+ `packages/server/src/workerPrompt.ts`, both skills, the hooks, and this file was updated; a grep for the old prefix token is clean.
197
197
 
198
198
  **Accepted name collision.** The old GLOBAL orchestrator plugin (`cc/`) is ALSO named `fray`. This is
199
199
  accepted as harmless: `cc/` is disabled in `~/.claude/settings.json` (`"fray@fray": false`) and, even
@@ -216,7 +216,7 @@ shell-out, so they take effect on the next board rebuild with NO server restart.
216
216
  machine field, which reads as needs-human via the inlined `effectiveStatus`) REQUIRES a
217
217
  `status_text` (hard BLOCK); a machine-`blocked` thread with no mechanism field is a WARN suggesting
218
218
  needs-human (not a block — legacy tolerance); >1 mechanism stays a block.
219
- - **`skills/worker/SKILL.md` + `ui/WORKER_PROMPT.md` + `hooks/session-seed.mjs`** — the worker status
219
+ - **`skills/worker/SKILL.md` + `packages/server/src/workerPrompt.ts` + `hooks/session-seed.mjs`** — the worker status
220
220
  guidance rewritten to the new contract: an ask OR a result needing review → `status: needs-human`
221
221
  with a `status_text` ask ("Review: …"); `blocked` is machine-only; `done` means NOTHING is left for
222
222
  the human (Mark-as-done is the human's acknowledgment — never jump straight to `done` when review
@@ -331,7 +331,7 @@ TWO-SIDED DEFENSE:
331
331
  repair.
332
332
 
333
333
  MECHANISM:
334
- - `cc/scripts/fray/index.mjs` `--json` gains a parallel `errorItems: [{file, kind, message}]`
334
+ - `board/index.mjs` `--json` gains a parallel `errorItems: [{file, kind, message}]`
335
335
  (`kind: 'no-frontmatter'` = repairable, else `'other'`). The parser classifies — it knows exactly
336
336
  why it rejected the file. The legacy `errors: string[]` array is emitted UNCHANGED alongside it.
337
337
  - `errorItems` flows through `readBoard`/`fray.ts` → `board.ts assemble()` → `BoardSnapshot` +
@@ -360,7 +360,7 @@ GONE: no thread files, no frontmatter, no `status`/`activity`/`status_text`, no
360
360
  `blocked` machine fields, no `hasPlan`/`## Plan`, no `fray-update`. Workers now SIGNAL through their
361
361
  FINAL MESSAGE and PERSIST through a SCRATCHPAD. This is the cc-worker-side realignment.
362
362
 
363
- **The new signal model (taught in `ui/WORKER_PROMPT.md` §"End-of-turn signals" + `skills/worker/SKILL.md`):**
363
+ **The new signal model (taught in `packages/server/src/workerPrompt.ts` §"End-of-turn signals" + `skills/worker/SKILL.md`):**
364
364
  - **Bare rest is quiet** — a rested thread with no fence does NOT enter Needs-you and is not a human
365
365
  handoff. Human handoff is explicit via `question` (or a real process-level block).
366
366
  - **` ```done `** — work complete + stands; body = 1–4 lines of what shipped + where. Renders a
@@ -483,7 +483,7 @@ discoverable.
483
483
  ## 2026-07-12: runtime release gate — real CDP evidence plus independent review
484
484
 
485
485
  Major UI, server, and control-plane work may no longer reach `done` from unit/integration/mocked
486
- evidence alone. The canonical `ui/WORKER_PROMPT.md` contract now requires real Chrome CDP QA against
486
+ evidence alone. The canonical `packages/server/src/workerPrompt.ts` contract now requires real Chrome CDP QA against
487
487
  a disposable full stack, relevant active/idle/error/restart coverage, desktop+narrow screenshots,
488
488
  console/network inspection, and an explicit correctness+aesthetics assessment. Chrome DevTools MCP is
489
489
  preferred when it is available to the current provider; `agent-browser` or the repository Puppeteer
@@ -3,15 +3,15 @@
3
3
  /**
4
4
  * fray — the board command, on the worker's Bash PATH while the fray-worker plugin is enabled.
5
5
  * A worker owns ONE thread but can still run `fray` (validate/read the board for its repo). This
6
- * shim execs cc's bundled board script directly (NOT a copy) — cc-worker never forks board logic.
6
+ * shim execs the repo board's bundled script directly (NOT a copy) — cc-worker never forks board logic.
7
7
  *
8
- * Resolving the board: this file is <plugin>/bin/fray, cc is a sibling of the plugin root, so the
9
- * board is at ../../cc/scripts/fray/index.mjs relative to THIS file — cwd-independent. Imported
8
+ * Resolving the board: this file is <plugin>/bin/fray, board/ is a sibling of the plugin root, so the
9
+ * board is at ../../board/index.mjs relative to THIS file — cwd-independent. Imported
10
10
  * via a file:// URL so argv/process.env (incl. CLAUDE_PROJECT_DIR) flow straight through.
11
11
  */
12
12
  import { fileURLToPath, pathToFileURL } from 'node:url';
13
13
  import { dirname, join } from 'node:path';
14
14
 
15
15
  const here = dirname(fileURLToPath(import.meta.url));
16
- const board = join(here, '..', '..', 'cc', 'scripts', 'fray', 'index.mjs');
16
+ const board = join(here, '..', '..', 'board', 'index.mjs');
17
17
  await import(pathToFileURL(board).href);
@@ -4,15 +4,15 @@
4
4
  * fray-update — the structured thread updater, on the worker's Bash PATH while the fray-worker
5
5
  * plugin is enabled. This is the worker's PRIMARY tool for owning its one thread file: it writes
6
6
  * `.fray/<slug>.md` through the same code path fray-ui uses (never hand-rolled markdown edits).
7
- * Execs cc's bundled updater directly (NOT a copy) — cc-worker never forks updater logic.
7
+ * Execs the repo board's bundled updater directly (NOT a copy) — cc-worker never forks updater logic.
8
8
  *
9
- * Resolving the script: this file is <plugin>/bin/fray-update, cc is a sibling of the plugin root,
10
- * so the updater is at ../../cc/scripts/fray/thread-update.mjs relative to THIS file. Imported via
9
+ * Resolving the script: this file is <plugin>/bin/fray-update, board/ is a sibling of the plugin root,
10
+ * so the updater is at ../../board/thread-update.mjs relative to THIS file. Imported via
11
11
  * a file:// URL so argv/process.env flow straight through.
12
12
  */
13
13
  import { fileURLToPath, pathToFileURL } from 'node:url';
14
14
  import { dirname, join } from 'node:path';
15
15
 
16
16
  const here = dirname(fileURLToPath(import.meta.url));
17
- const updater = join(here, '..', '..', 'cc', 'scripts', 'fray', 'thread-update.mjs');
17
+ const updater = join(here, '..', '..', 'board', 'thread-update.mjs');
18
18
  await import(pathToFileURL(updater).href);
@@ -21,7 +21,7 @@
21
21
  // worker plugin, and cc's hooks/skills/agents are gone. But a machine can still carry a CACHED cc
22
22
  // install from before the retirement, whose hooks fire in every repo gated on cc's opt-IN sentinel.
23
23
  // A fresh worker never runs `fray on`, so such a cc is already dormant; we write cc's own per-session
24
- // `off` sentinel anyway via the shared config API (cc/scripts/fray survives as cc-worker's board
24
+ // `off` sentinel anyway via the shared config API (board survives as cc-worker's board
25
25
  // implementation) so a stale install is guaranteed inert. Cheap belt-and-suspenders. See DECISIONS.md.
26
26
  import { readFileSync } from 'node:fs';
27
27
  import { execFileSync } from 'node:child_process';