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.
- package/README.md +52 -174
- package/dist/dev-child.js +24 -5
- package/dist/frayui.js +8 -8
- package/package.json +27 -8
- package/runtime/cc-worker/DECISIONS.md +14 -14
- package/runtime/cc-worker/bin/fray +4 -4
- package/runtime/cc-worker/bin/fray-update +4 -4
- package/runtime/cc-worker/hooks/session-seed.mjs +1 -1
- package/runtime/cc-worker/scripts/fray/agent-bindings.mjs +3 -3
- package/runtime/cc-worker/scripts/fray/config.mjs +8 -8
- package/runtime/cc-worker/skills/gh/SKILL.md +22 -0
- package/web-dist/assets/{TerminalPane-fMGrgB-a.js → TerminalPane-DyK0ncN8.js} +1 -1
- package/web-dist/assets/{abnfDiagram-VRR7QNED-BtlrDuYX.js → abnfDiagram-VRR7QNED-DtotTgyK.js} +1 -1
- package/web-dist/assets/architecture-TIHT7OUA-B23bEjC4.js +1 -0
- package/web-dist/assets/{architectureDiagram-ZJ3FMSHR-CdLQ71be.js → architectureDiagram-ZJ3FMSHR-CVl8mUt0.js} +1 -1
- package/web-dist/assets/{blockDiagram-677ZJIJ3-CBk2FHEj.js → blockDiagram-677ZJIJ3-VUZEsYBW.js} +1 -1
- package/web-dist/assets/{c4Diagram-LMCZKHZV-mZ3HQ6WX.js → c4Diagram-LMCZKHZV-Dlg18lBF.js} +1 -1
- package/web-dist/assets/channel-BMFwXMMq.js +1 -0
- package/web-dist/assets/{chunk-32BRIVSS-Daxvi7f5.js → chunk-32BRIVSS-DG8gd151.js} +1 -1
- package/web-dist/assets/{chunk-52WLFC77-BGJoZvry.js → chunk-52WLFC77-DKlahFJ2.js} +1 -1
- package/web-dist/assets/{chunk-C7G6YPKG-Ycd1yheh.js → chunk-C7G6YPKG-Bk-BFDsQ.js} +1 -1
- package/web-dist/assets/{chunk-EX3LRPZG-BPFhlsHp.js → chunk-EX3LRPZG-DnXqd-jJ.js} +1 -1
- package/web-dist/assets/{chunk-FWX5IMBZ--xy8DTon.js → chunk-FWX5IMBZ-ujYyBebT.js} +2 -2
- package/web-dist/assets/{chunk-HOUHSVGY-Dq3zoygp.js → chunk-HOUHSVGY-Dk2XhPWQ.js} +1 -1
- package/web-dist/assets/{chunk-ICXQ74PX-FfKP-7yM.js → chunk-ICXQ74PX-DoseDoV4.js} +1 -1
- package/web-dist/assets/{chunk-MOJQB5TN-BeiHnsLF.js → chunk-MOJQB5TN-B8dog4zz.js} +1 -1
- package/web-dist/assets/{chunk-OGEWGWER-CjDU_9fC.js → chunk-OGEWGWER-D4GkHktS.js} +1 -1
- package/web-dist/assets/{chunk-PUDLZKDR-BO7tm5QX.js → chunk-PUDLZKDR-ChEIayRe.js} +1 -1
- package/web-dist/assets/{chunk-Q4XR5HBZ-BPefovOg.js → chunk-Q4XR5HBZ-BtzMOr97.js} +1 -1
- package/web-dist/assets/{chunk-V7JOEXUC-DWnebFHh.js → chunk-V7JOEXUC-DQ7c8QAd.js} +1 -1
- package/web-dist/assets/{chunk-VAUOI2AC-BL6qWFhW.js → chunk-VAUOI2AC-6jWzOU_J.js} +1 -1
- package/web-dist/assets/{chunk-VR4S4FIN-Mfs__7L9.js → chunk-VR4S4FIN-TB0TNuya.js} +1 -1
- package/web-dist/assets/{chunk-WYO6CB5R-Bf2IYbEU.js → chunk-WYO6CB5R-CY7fL6F5.js} +1 -1
- package/web-dist/assets/{chunk-ZGVPDNZ5-CJ9ZHZgU.js → chunk-ZGVPDNZ5-vZO6zPtA.js} +1 -1
- package/web-dist/assets/classDiagram-OUVF2IWQ-DiWatUgG.js +1 -0
- package/web-dist/assets/classDiagram-v2-EOCWNBFH-DiWatUgG.js +1 -0
- package/web-dist/assets/{cynefin-VYW2F7L2-COSC0oNL.js → cynefin-VYW2F7L2-D4NcAp7V.js} +1 -1
- package/web-dist/assets/{cynefinDiagram-TSTJHNR4-mInhJdg1.js → cynefinDiagram-TSTJHNR4-hLO8f53U.js} +1 -1
- package/web-dist/assets/{dagre-VKFMJZFB-D-T5z05M.js → dagre-VKFMJZFB-86z9AWRw.js} +1 -1
- package/web-dist/assets/{diagram-FQU43EPY-HbMkSb3j.js → diagram-FQU43EPY-DXw3bj8G.js} +1 -1
- package/web-dist/assets/{diagram-G47NLZAW-DBwZLcZh.js → diagram-G47NLZAW-BtH3_eiQ.js} +1 -1
- package/web-dist/assets/{diagram-NH7WQ7WH-DG_Mhays.js → diagram-NH7WQ7WH-BxWq2H6D.js} +1 -1
- package/web-dist/assets/{diagram-OA4YK3LP-hH39gqpu.js → diagram-OA4YK3LP-BoU3I6Xx.js} +1 -1
- package/web-dist/assets/{diagram-WEI45ONY-DhKZuwxW.js → diagram-WEI45ONY-CkPD-vrA.js} +1 -1
- package/web-dist/assets/{ebnfDiagram-CCIWWBDH-B7BiO-NR.js → ebnfDiagram-CCIWWBDH-BudQTnvf.js} +1 -1
- package/web-dist/assets/{erDiagram-Q63AITRT-DHjG5RDQ.js → erDiagram-Q63AITRT-CRFLgkaW.js} +1 -1
- package/web-dist/assets/eventmodeling-45OFAUF4-C7ZRnE7-.js +1 -0
- package/web-dist/assets/flowDiagram-23GEKE2U-DKS-kFcF.js +1 -0
- package/web-dist/assets/{ganttDiagram-NO4QXBWP-BZ-98wTa.js → ganttDiagram-NO4QXBWP-DFmOc45y.js} +1 -1
- package/web-dist/assets/{gitGraph-TEB2WS4Q-nF22R2jO.js → gitGraph-TEB2WS4Q-CMW6meDa.js} +1 -1
- package/web-dist/assets/{gitGraphDiagram-IHSO6WYX-CoYQQ22V.js → gitGraphDiagram-IHSO6WYX-CHelbJfg.js} +1 -1
- package/web-dist/assets/{index-BHzIN-tQ.js → index-C2Tf3Ihs.js} +65 -65
- package/web-dist/assets/index-CeAC3wUW.css +1 -0
- package/web-dist/assets/{info-DKCQHKI2-Cy2BCbBW.js → info-DKCQHKI2-DZT4W61r.js} +1 -1
- package/web-dist/assets/{infoDiagram-FWYZ7A6U-CmhR2R1x.js → infoDiagram-FWYZ7A6U-Dj0tLhVO.js} +1 -1
- package/web-dist/assets/{ishikawaDiagram-FXEZZL3T-CfG59afl.js → ishikawaDiagram-FXEZZL3T-D825ES0c.js} +1 -1
- package/web-dist/assets/{journeyDiagram-5HDEW3XC-BvFmG20q.js → journeyDiagram-5HDEW3XC-D53Igq5c.js} +1 -1
- package/web-dist/assets/{kanban-definition-HUTT4EX6-BHGEecsY.js → kanban-definition-HUTT4EX6-CJ-F0vAo.js} +1 -1
- package/web-dist/assets/{line-Ds4xvN3d.js → line-CuiC27jO.js} +1 -1
- package/web-dist/assets/{mermaid-parser.core-Gow4kgYJ.js → mermaid-parser.core-rG6qAIn0.js} +3 -3
- package/web-dist/assets/{mermaid.core-B7Qc_kbm.js → mermaid.core-DzNUOuDq.js} +3 -3
- package/web-dist/assets/{mindmap-definition-LN4V7U3C-ayXKGcy_.js → mindmap-definition-LN4V7U3C-DHfhQue9.js} +1 -1
- package/web-dist/assets/{packet-7NZHBO7P-oLBdtkD8.js → packet-7NZHBO7P-BiUlKhVG.js} +1 -1
- package/web-dist/assets/{pegDiagram-2B236MQR-DwJyc_1B.js → pegDiagram-2B236MQR-MQdiKZx3.js} +1 -1
- package/web-dist/assets/{pie-RZYD4A2V-CXzad3fT.js → pie-RZYD4A2V-dNHUvvE3.js} +1 -1
- package/web-dist/assets/{pieDiagram-ENE6RG2P-CZzgSGtM.js → pieDiagram-ENE6RG2P-BwFxe1x1.js} +1 -1
- package/web-dist/assets/{quadrantDiagram-ABIIQ3AL--IPIp8yo.js → quadrantDiagram-ABIIQ3AL-9dic-8vv.js} +1 -1
- package/web-dist/assets/{radar-I7S5WNFK-DldySvtZ.js → radar-I7S5WNFK-CK3dl20X.js} +1 -1
- package/web-dist/assets/{railroad-3IZDKUUU-CiWFZfXM.js → railroad-3IZDKUUU-BuB2LkFj.js} +1 -1
- package/web-dist/assets/railroad-abnf-AHOZXSZD-C1-MJAeY.js +1 -0
- package/web-dist/assets/railroad-ebnf-EBAXGLYW-crQCD9rJ.js +1 -0
- package/web-dist/assets/railroad-peg-LSFZ7HO6-Ba-_QGKi.js +1 -0
- package/web-dist/assets/{railroadDiagram-RFXS5EU6-DcCMNGGb.js → railroadDiagram-RFXS5EU6-CuuspS_3.js} +1 -1
- package/web-dist/assets/{requirementDiagram-TGXJPOKE-C-6j8urk.js → requirementDiagram-TGXJPOKE-DHAILLdh.js} +1 -1
- package/web-dist/assets/{sankeyDiagram-HTMAVEWB-CPtiOpdz.js → sankeyDiagram-HTMAVEWB-CztQ2t1b.js} +1 -1
- package/web-dist/assets/{sequenceDiagram-DBY2YBRQ-B8FnQTdn.js → sequenceDiagram-DBY2YBRQ-jk56eo0l.js} +1 -1
- package/web-dist/assets/{stateDiagram-2N3HPSRC-CxG9nX2P.js → stateDiagram-2N3HPSRC-uwqBWnrI.js} +1 -1
- package/web-dist/assets/stateDiagram-v2-6OUMAXLB-DOPbAEV3.js +1 -0
- package/web-dist/assets/{swimlanes-5IMT3BWC-D49-t0hV.js → swimlanes-5IMT3BWC-BzuJ77j6.js} +1 -1
- package/web-dist/assets/swimlanesDiagram-G3AALYLV-BRh1ca21.js +8 -0
- package/web-dist/assets/{timeline-definition-FHXFAJF6-C9OEd9SE.js → timeline-definition-FHXFAJF6-CVDNkzi9.js} +1 -1
- package/web-dist/assets/{treeView-QDETBFTQ-D1AKqVPa.js → treeView-QDETBFTQ-CdkPxaDl.js} +1 -1
- package/web-dist/assets/{treemap-6X3UGDF4-BRssB7hM.js → treemap-6X3UGDF4-lnksCHdn.js} +1 -1
- package/web-dist/assets/{vennDiagram-L72KCM5P-CftkAbY0.js → vennDiagram-L72KCM5P-B533fRM0.js} +1 -1
- package/web-dist/assets/{wardley-OPB4EBWU-RLMgzRq4.js → wardley-OPB4EBWU-DwKf-2YJ.js} +1 -1
- package/web-dist/assets/{wardleyDiagram-EHGQE667-BLytgXPS.js → wardleyDiagram-EHGQE667-BDDN72B1.js} +1 -1
- package/web-dist/assets/{xychartDiagram-FW5EYKEG-BYgK1Y3H.js → xychartDiagram-FW5EYKEG-CkvHmNVt.js} +1 -1
- package/web-dist/index.html +2 -2
- package/web-dist/assets/architecture-TIHT7OUA-CuD8jpr-.js +0 -1
- package/web-dist/assets/channel-C2eUWc74.js +0 -1
- package/web-dist/assets/classDiagram-OUVF2IWQ-GaOJNgfE.js +0 -1
- package/web-dist/assets/classDiagram-v2-EOCWNBFH-GaOJNgfE.js +0 -1
- package/web-dist/assets/eventmodeling-45OFAUF4-C0eV8RBx.js +0 -1
- package/web-dist/assets/flowDiagram-23GEKE2U-BbldRZUK.js +0 -1
- package/web-dist/assets/index-Duiy4w7C.css +0 -1
- package/web-dist/assets/railroad-abnf-AHOZXSZD-bXMYUOCD.js +0 -1
- package/web-dist/assets/railroad-ebnf-EBAXGLYW-D11ZwZby.js +0 -1
- package/web-dist/assets/railroad-peg-LSFZ7HO6-DD1AC5X-.js +0 -1
- package/web-dist/assets/stateDiagram-v2-6OUMAXLB-SGarOFIy.js +0 -1
- package/web-dist/assets/swimlanesDiagram-G3AALYLV-krF0bh4b.js +0 -8
- /package/runtime/{cc/scripts/fray → board}/agent-bindings.mjs +0 -0
- /package/runtime/{cc/scripts/fray → board}/agent-liveness.mjs +0 -0
- /package/runtime/{cc/scripts/fray → board}/agent-status.mjs +0 -0
- /package/runtime/{cc/scripts/fray → board}/config.mjs +0 -0
- /package/runtime/{cc/scripts/fray → board}/decisions.mjs +0 -0
- /package/runtime/{cc/scripts/fray → board}/index.mjs +0 -0
- /package/runtime/{cc/scripts/fray → board}/notify-shared.mjs +0 -0
- /package/runtime/{cc/scripts/fray → board}/notify.mjs +0 -0
- /package/runtime/{cc/scripts/fray → board}/ownership.mjs +0 -0
- /package/runtime/{cc/scripts/fray → board}/rest-detect.mjs +0 -0
- /package/runtime/{cc/scripts/fray → board}/thread-excerpt.mjs +0 -0
- /package/runtime/{cc/scripts/fray → board}/thread-update.mjs +0 -0
package/README.md
CHANGED
|
@@ -1,186 +1,64 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
```
|
|
21
|
-
cd /path/to/
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
41
|
+
## How it works
|
|
151
42
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
##
|
|
55
|
+
## Docs
|
|
166
56
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
`
|
|
170
|
-
|
|
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
|
-
##
|
|
62
|
+
## License
|
|
174
63
|
|
|
175
|
-
|
|
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, "..", "..", "..", "
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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, "
|
|
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.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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 `../../../
|
|
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 `../../
|
|
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
|
-
`../../
|
|
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:** `
|
|
22
|
-
`
|
|
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)` (`
|
|
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
|
-
- **`
|
|
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 `
|
|
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 `
|
|
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
|
-
`
|
|
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` + `
|
|
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
|
-
- `
|
|
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 `
|
|
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 `
|
|
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
|
|
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,
|
|
9
|
-
* board is at ../../
|
|
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, '..', '..', '
|
|
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
|
|
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,
|
|
10
|
-
* so the updater is at ../../
|
|
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, '..', '..', '
|
|
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 (
|
|
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';
|