frayui 0.1.0

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 (173) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +186 -0
  3. package/dist/claude-agent-broker.js +21641 -0
  4. package/dist/codex-app-server-daemon.js +354 -0
  5. package/dist/dev-child.js +40319 -0
  6. package/dist/frayui.js +8738 -0
  7. package/package.json +46 -0
  8. package/runtime/cc/scripts/fray/agent-bindings.mjs +287 -0
  9. package/runtime/cc/scripts/fray/agent-liveness.mjs +367 -0
  10. package/runtime/cc/scripts/fray/agent-status.mjs +178 -0
  11. package/runtime/cc/scripts/fray/config.mjs +982 -0
  12. package/runtime/cc/scripts/fray/decisions.mjs +97 -0
  13. package/runtime/cc/scripts/fray/index.mjs +699 -0
  14. package/runtime/cc/scripts/fray/notify-shared.mjs +90 -0
  15. package/runtime/cc/scripts/fray/notify.mjs +81 -0
  16. package/runtime/cc/scripts/fray/ownership.mjs +120 -0
  17. package/runtime/cc/scripts/fray/rest-detect.mjs +213 -0
  18. package/runtime/cc/scripts/fray/thread-excerpt.mjs +162 -0
  19. package/runtime/cc/scripts/fray/thread-update.mjs +285 -0
  20. package/runtime/cc-worker/.claude-plugin/plugin.json +10 -0
  21. package/runtime/cc-worker/DECISIONS.md +923 -0
  22. package/runtime/cc-worker/LICENSE +21 -0
  23. package/runtime/cc-worker/agents/fable-high.md +8 -0
  24. package/runtime/cc-worker/agents/fable-low.md +8 -0
  25. package/runtime/cc-worker/agents/fable-max.md +8 -0
  26. package/runtime/cc-worker/agents/fable-medium.md +8 -0
  27. package/runtime/cc-worker/agents/fable-xhigh.md +8 -0
  28. package/runtime/cc-worker/agents/haiku.md +7 -0
  29. package/runtime/cc-worker/agents/opus-high.md +8 -0
  30. package/runtime/cc-worker/agents/opus-low.md +8 -0
  31. package/runtime/cc-worker/agents/opus-max.md +8 -0
  32. package/runtime/cc-worker/agents/opus-medium.md +8 -0
  33. package/runtime/cc-worker/agents/opus-xhigh.md +8 -0
  34. package/runtime/cc-worker/agents/sonnet-high.md +8 -0
  35. package/runtime/cc-worker/agents/sonnet-low.md +8 -0
  36. package/runtime/cc-worker/agents/sonnet-max.md +8 -0
  37. package/runtime/cc-worker/agents/sonnet-medium.md +8 -0
  38. package/runtime/cc-worker/agents/sonnet-xhigh.md +8 -0
  39. package/runtime/cc-worker/bin/fray +17 -0
  40. package/runtime/cc-worker/bin/fray-mcp.mjs +248 -0
  41. package/runtime/cc-worker/bin/fray-update +18 -0
  42. package/runtime/cc-worker/hooks/agent-bind.mjs +40 -0
  43. package/runtime/cc-worker/hooks/agent-dispatch.mjs +74 -0
  44. package/runtime/cc-worker/hooks/bash-background.d.mts +6 -0
  45. package/runtime/cc-worker/hooks/bash-background.mjs +188 -0
  46. package/runtime/cc-worker/hooks/deny-ask.mjs +38 -0
  47. package/runtime/cc-worker/hooks/deny-plan.mjs +61 -0
  48. package/runtime/cc-worker/hooks/hooks.json +111 -0
  49. package/runtime/cc-worker/hooks/perm-policy.mjs +211 -0
  50. package/runtime/cc-worker/hooks/precompact-instructions.mjs +122 -0
  51. package/runtime/cc-worker/hooks/scratchpad-stop.mjs +125 -0
  52. package/runtime/cc-worker/hooks/scratchpad.mjs +419 -0
  53. package/runtime/cc-worker/hooks/session-seed.mjs +105 -0
  54. package/runtime/cc-worker/scripts/fray/agent-bindings.mjs +9 -0
  55. package/runtime/cc-worker/scripts/fray/config.mjs +12 -0
  56. package/runtime/cc-worker/skills/gh/SKILL.md +132 -0
  57. package/runtime/cc-worker/skills/gh/scripts/ci-watch.mjs +60 -0
  58. package/runtime/cc-worker/skills/gh/scripts/github-watch.mjs +130 -0
  59. package/runtime/cc-worker/skills/gh/scripts/review-watch.mjs +54 -0
  60. package/runtime/cc-worker/skills/handoff/SKILL.md +203 -0
  61. package/runtime/cc-worker/skills/waits/SKILL.md +83 -0
  62. package/web-dist/apple-touch-icon.png +0 -0
  63. package/web-dist/assets/TerminalPane-fMGrgB-a.js +7 -0
  64. package/web-dist/assets/abnfDiagram-VRR7QNED-BtlrDuYX.js +1 -0
  65. package/web-dist/assets/arc-BSyeo0Gb.js +1 -0
  66. package/web-dist/assets/architecture-TIHT7OUA-CuD8jpr-.js +1 -0
  67. package/web-dist/assets/architectureDiagram-ZJ3FMSHR-CdLQ71be.js +36 -0
  68. package/web-dist/assets/array-BifhSqXX.js +1 -0
  69. package/web-dist/assets/blockDiagram-677ZJIJ3-CBk2FHEj.js +132 -0
  70. package/web-dist/assets/c4Diagram-LMCZKHZV-mZ3HQ6WX.js +10 -0
  71. package/web-dist/assets/channel-C2eUWc74.js +1 -0
  72. package/web-dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  73. package/web-dist/assets/chunk-32BRIVSS-Daxvi7f5.js +1 -0
  74. package/web-dist/assets/chunk-52WLFC77-BGJoZvry.js +10 -0
  75. package/web-dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  76. package/web-dist/assets/chunk-7BUUIJ7U-Bb538aSH.js +1 -0
  77. package/web-dist/assets/chunk-C7G6YPKG-Ycd1yheh.js +1 -0
  78. package/web-dist/assets/chunk-EX3LRPZG-BPFhlsHp.js +231 -0
  79. package/web-dist/assets/chunk-FWX5IMBZ--xy8DTon.js +2 -0
  80. package/web-dist/assets/chunk-HOUHSVGY-Dq3zoygp.js +1 -0
  81. package/web-dist/assets/chunk-ICXQ74PX-FfKP-7yM.js +2 -0
  82. package/web-dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  83. package/web-dist/assets/chunk-KEIR6QF5-BfrZ3jm6.js +161 -0
  84. package/web-dist/assets/chunk-MOJQB5TN-BeiHnsLF.js +88 -0
  85. package/web-dist/assets/chunk-OGEWGWER-CjDU_9fC.js +1 -0
  86. package/web-dist/assets/chunk-PUDLZKDR-BO7tm5QX.js +156 -0
  87. package/web-dist/assets/chunk-Q4XR5HBZ-BPefovOg.js +70 -0
  88. package/web-dist/assets/chunk-RYQCIY6F-Cu_KplZW.js +1 -0
  89. package/web-dist/assets/chunk-V7JOEXUC-DWnebFHh.js +206 -0
  90. package/web-dist/assets/chunk-VAUOI2AC-BL6qWFhW.js +1 -0
  91. package/web-dist/assets/chunk-VR4S4FIN-Mfs__7L9.js +1 -0
  92. package/web-dist/assets/chunk-WYO6CB5R-Bf2IYbEU.js +127 -0
  93. package/web-dist/assets/chunk-XXDRQBXY-DYlTP5J-.js +1 -0
  94. package/web-dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  95. package/web-dist/assets/chunk-ZGVPDNZ5-CJ9ZHZgU.js +62 -0
  96. package/web-dist/assets/chunk-ZIRB5QZD-C6fEPe3t.js +32 -0
  97. package/web-dist/assets/classDiagram-OUVF2IWQ-GaOJNgfE.js +1 -0
  98. package/web-dist/assets/classDiagram-v2-EOCWNBFH-GaOJNgfE.js +1 -0
  99. package/web-dist/assets/cose-bilkent-JH36ORCC-BUIsLrGc.js +1 -0
  100. package/web-dist/assets/cynefin-VYW2F7L2-COSC0oNL.js +1 -0
  101. package/web-dist/assets/cynefinDiagram-TSTJHNR4-mInhJdg1.js +62 -0
  102. package/web-dist/assets/cytoscape.esm-B3I8pqwA.js +321 -0
  103. package/web-dist/assets/dagre-CXRCoUWR.js +1 -0
  104. package/web-dist/assets/dagre-VKFMJZFB-D-T5z05M.js +4 -0
  105. package/web-dist/assets/defaultLocale-C8Fc0cco.js +1 -0
  106. package/web-dist/assets/diagram-FQU43EPY-HbMkSb3j.js +3 -0
  107. package/web-dist/assets/diagram-G47NLZAW-DBwZLcZh.js +24 -0
  108. package/web-dist/assets/diagram-NH7WQ7WH-DG_Mhays.js +24 -0
  109. package/web-dist/assets/diagram-OA4YK3LP-hH39gqpu.js +30 -0
  110. package/web-dist/assets/diagram-WEI45ONY-DhKZuwxW.js +41 -0
  111. package/web-dist/assets/dist-DoH_9pyS.js +1 -0
  112. package/web-dist/assets/ebnfDiagram-CCIWWBDH-B7BiO-NR.js +1 -0
  113. package/web-dist/assets/erDiagram-Q63AITRT-DHjG5RDQ.js +85 -0
  114. package/web-dist/assets/eventmodeling-45OFAUF4-C0eV8RBx.js +1 -0
  115. package/web-dist/assets/flowDiagram-23GEKE2U-BbldRZUK.js +1 -0
  116. package/web-dist/assets/ganttDiagram-NO4QXBWP-BZ-98wTa.js +292 -0
  117. package/web-dist/assets/gitGraph-TEB2WS4Q-nF22R2jO.js +1 -0
  118. package/web-dist/assets/gitGraphDiagram-IHSO6WYX-CoYQQ22V.js +106 -0
  119. package/web-dist/assets/graphlib-B8gBHxth.js +1 -0
  120. package/web-dist/assets/index-BHzIN-tQ.js +357 -0
  121. package/web-dist/assets/index-Duiy4w7C.css +1 -0
  122. package/web-dist/assets/info-DKCQHKI2-Cy2BCbBW.js +1 -0
  123. package/web-dist/assets/infoDiagram-FWYZ7A6U-CmhR2R1x.js +2 -0
  124. package/web-dist/assets/init-D6jRqBbL.js +1 -0
  125. package/web-dist/assets/ishikawaDiagram-FXEZZL3T-CfG59afl.js +70 -0
  126. package/web-dist/assets/journeyDiagram-5HDEW3XC-BvFmG20q.js +139 -0
  127. package/web-dist/assets/kanban-definition-HUTT4EX6-BHGEecsY.js +89 -0
  128. package/web-dist/assets/katex-CddkPoXu.js +257 -0
  129. package/web-dist/assets/line-Ds4xvN3d.js +1 -0
  130. package/web-dist/assets/linear-z2V0wJk9.js +1 -0
  131. package/web-dist/assets/map-DsCK-0Cs.js +1 -0
  132. package/web-dist/assets/mermaid-parser.core-Gow4kgYJ.js +7 -0
  133. package/web-dist/assets/mermaid.core-B7Qc_kbm.js +11 -0
  134. package/web-dist/assets/mindmap-definition-LN4V7U3C-ayXKGcy_.js +96 -0
  135. package/web-dist/assets/ordinal-hYBb2elL.js +1 -0
  136. package/web-dist/assets/packet-7NZHBO7P-oLBdtkD8.js +1 -0
  137. package/web-dist/assets/path-BWPyau1x.js +1 -0
  138. package/web-dist/assets/pegDiagram-2B236MQR-DwJyc_1B.js +1 -0
  139. package/web-dist/assets/pie-RZYD4A2V-CXzad3fT.js +1 -0
  140. package/web-dist/assets/pieDiagram-ENE6RG2P-CZzgSGtM.js +39 -0
  141. package/web-dist/assets/quadrantDiagram-ABIIQ3AL--IPIp8yo.js +7 -0
  142. package/web-dist/assets/radar-I7S5WNFK-DldySvtZ.js +1 -0
  143. package/web-dist/assets/railroad-3IZDKUUU-CiWFZfXM.js +1 -0
  144. package/web-dist/assets/railroad-abnf-AHOZXSZD-bXMYUOCD.js +1 -0
  145. package/web-dist/assets/railroad-ebnf-EBAXGLYW-D11ZwZby.js +1 -0
  146. package/web-dist/assets/railroad-peg-LSFZ7HO6-DD1AC5X-.js +1 -0
  147. package/web-dist/assets/railroadDiagram-RFXS5EU6-DcCMNGGb.js +1 -0
  148. package/web-dist/assets/requirementDiagram-TGXJPOKE-C-6j8urk.js +84 -0
  149. package/web-dist/assets/rolldown-runtime-Bh1tDfsg.js +1 -0
  150. package/web-dist/assets/rough.esm-CSKSodPl.js +1 -0
  151. package/web-dist/assets/sankeyDiagram-HTMAVEWB-CPtiOpdz.js +40 -0
  152. package/web-dist/assets/sequenceDiagram-DBY2YBRQ-B8FnQTdn.js +162 -0
  153. package/web-dist/assets/sizeCapture-X5ZJPWSS-B0uUizjq.js +1 -0
  154. package/web-dist/assets/src-C4XfhTaE.js +1 -0
  155. package/web-dist/assets/stateDiagram-2N3HPSRC-CxG9nX2P.js +1 -0
  156. package/web-dist/assets/stateDiagram-v2-6OUMAXLB-SGarOFIy.js +1 -0
  157. package/web-dist/assets/swimlanes-5IMT3BWC-D49-t0hV.js +2 -0
  158. package/web-dist/assets/swimlanesDiagram-G3AALYLV-krF0bh4b.js +8 -0
  159. package/web-dist/assets/timeline-definition-FHXFAJF6-C9OEd9SE.js +120 -0
  160. package/web-dist/assets/treeView-QDETBFTQ-D1AKqVPa.js +1 -0
  161. package/web-dist/assets/treemap-6X3UGDF4-BRssB7hM.js +1 -0
  162. package/web-dist/assets/vennDiagram-L72KCM5P-CftkAbY0.js +34 -0
  163. package/web-dist/assets/wardley-OPB4EBWU-RLMgzRq4.js +1 -0
  164. package/web-dist/assets/wardleyDiagram-EHGQE667-BLytgXPS.js +78 -0
  165. package/web-dist/assets/xychartDiagram-FW5EYKEG-BYgK1Y3H.js +7 -0
  166. package/web-dist/favicon-16.png +0 -0
  167. package/web-dist/favicon-32.png +0 -0
  168. package/web-dist/favicon.svg +78 -0
  169. package/web-dist/icon-192.png +0 -0
  170. package/web-dist/icon-512.png +0 -0
  171. package/web-dist/icon-maskable-512.png +0 -0
  172. package/web-dist/index.html +33 -0
  173. package/web-dist/manifest.webmanifest +16 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Colin McDonnell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: fable-high
3
+ description: fray worker profile — Fable at high effort. NOT the default reach for hard work — Opus 5 matches Fable and costs less, so dispatch fray:opus-high and buy extra rigor with EFFORT rather than a pricier model, unless the human explicitly asked for Fable. Dispatch via subagent_type to pin this model+effort cell for the self-contained task in the prompt.
4
+ model: fable
5
+ effort: high
6
+ ---
7
+
8
+ Execute the self-contained task in your dispatch prompt.
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: fable-low
3
+ description: fray worker profile — Fable at low effort. NOT the default reach for hard work — Opus 5 matches Fable and costs less, so dispatch fray:opus-low and buy extra rigor with EFFORT rather than a pricier model, unless the human explicitly asked for Fable. Dispatch via subagent_type to pin this model+effort cell for the self-contained task in the prompt.
4
+ model: fable
5
+ effort: low
6
+ ---
7
+
8
+ Execute the self-contained task in your dispatch prompt.
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: fable-max
3
+ description: fray worker profile — Fable at max effort. NOT the default reach for hard work — Opus 5 matches Fable and costs less, so dispatch fray:opus-max and buy extra rigor with EFFORT rather than a pricier model, unless the human explicitly asked for Fable. Dispatch via subagent_type to pin this model+effort cell for the self-contained task in the prompt.
4
+ model: fable
5
+ effort: max
6
+ ---
7
+
8
+ Execute the self-contained task in your dispatch prompt.
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: fable-medium
3
+ description: fray worker profile — Fable at medium effort. NOT the default reach for hard work — Opus 5 matches Fable and costs less, so dispatch fray:opus-medium and buy extra rigor with EFFORT rather than a pricier model, unless the human explicitly asked for Fable. Dispatch via subagent_type to pin this model+effort cell for the self-contained task in the prompt.
4
+ model: fable
5
+ effort: medium
6
+ ---
7
+
8
+ Execute the self-contained task in your dispatch prompt.
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: fable-xhigh
3
+ description: fray worker profile — Fable at xhigh effort. NOT the default reach for hard work — Opus 5 matches Fable and costs less, so dispatch fray:opus-xhigh and buy extra rigor with EFFORT rather than a pricier model, unless the human explicitly asked for Fable. Dispatch via subagent_type to pin this model+effort cell for the self-contained task in the prompt.
4
+ model: fable
5
+ effort: xhigh
6
+ ---
7
+
8
+ Execute the self-contained task in your dispatch prompt.
@@ -0,0 +1,7 @@
1
+ ---
2
+ name: haiku
3
+ description: fray worker profile — Haiku (no effort param). Dispatch via subagent_type for fully-scripted, mechanical harvest tasks only — give it a script, not a question.
4
+ model: haiku
5
+ ---
6
+
7
+ Execute the self-contained task in your dispatch prompt.
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: opus-high
3
+ description: fray worker profile — Opus at high effort. Dispatch via subagent_type to pin this model+effort cell for the self-contained task in the prompt.
4
+ model: opus
5
+ effort: high
6
+ ---
7
+
8
+ Execute the self-contained task in your dispatch prompt.
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: opus-low
3
+ description: fray worker profile — Opus at low effort. Dispatch via subagent_type to pin this model+effort cell for the self-contained task in the prompt.
4
+ model: opus
5
+ effort: low
6
+ ---
7
+
8
+ Execute the self-contained task in your dispatch prompt.
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: opus-max
3
+ description: fray worker profile — Opus at max effort. Dispatch via subagent_type to pin this model+effort cell for the self-contained task in the prompt.
4
+ model: opus
5
+ effort: max
6
+ ---
7
+
8
+ Execute the self-contained task in your dispatch prompt.
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: opus-medium
3
+ description: fray worker profile — Opus at medium effort. Dispatch via subagent_type to pin this model+effort cell for the self-contained task in the prompt.
4
+ model: opus
5
+ effort: medium
6
+ ---
7
+
8
+ Execute the self-contained task in your dispatch prompt.
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: opus-xhigh
3
+ description: fray worker profile — Opus at xhigh effort. Dispatch via subagent_type to pin this model+effort cell for the self-contained task in the prompt.
4
+ model: opus
5
+ effort: xhigh
6
+ ---
7
+
8
+ Execute the self-contained task in your dispatch prompt.
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: sonnet-high
3
+ description: fray worker profile — Sonnet at high effort. Dispatch via subagent_type to pin this model+effort cell for the self-contained task in the prompt.
4
+ model: sonnet
5
+ effort: high
6
+ ---
7
+
8
+ Execute the self-contained task in your dispatch prompt.
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: sonnet-low
3
+ description: fray worker profile — Sonnet at low effort. Dispatch via subagent_type to pin this model+effort cell for the self-contained task in the prompt.
4
+ model: sonnet
5
+ effort: low
6
+ ---
7
+
8
+ Execute the self-contained task in your dispatch prompt.
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: sonnet-max
3
+ description: fray worker profile — Sonnet at max effort. Dispatch via subagent_type to pin this model+effort cell for the self-contained task in the prompt.
4
+ model: sonnet
5
+ effort: max
6
+ ---
7
+
8
+ Execute the self-contained task in your dispatch prompt.
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: sonnet-medium
3
+ description: fray worker profile — Sonnet at medium effort. Dispatch via subagent_type to pin this model+effort cell for the self-contained task in the prompt.
4
+ model: sonnet
5
+ effort: medium
6
+ ---
7
+
8
+ Execute the self-contained task in your dispatch prompt.
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: sonnet-xhigh
3
+ description: fray worker profile — Sonnet at xhigh effort. Dispatch via subagent_type to pin this model+effort cell for the self-contained task in the prompt.
4
+ model: sonnet
5
+ effort: xhigh
6
+ ---
7
+
8
+ Execute the self-contained task in your dispatch prompt.
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ // @ts-check
3
+ /**
4
+ * fray — the board command, on the worker's Bash PATH while the fray-worker plugin is enabled.
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.
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
10
+ * via a file:// URL so argv/process.env (incl. CLAUDE_PROJECT_DIR) flow straight through.
11
+ */
12
+ import { fileURLToPath, pathToFileURL } from 'node:url';
13
+ import { dirname, join } from 'node:path';
14
+
15
+ const here = dirname(fileURLToPath(import.meta.url));
16
+ const board = join(here, '..', '..', 'cc', 'scripts', 'fray', 'index.mjs');
17
+ await import(pathToFileURL(board).href);
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env node
2
+ // @ts-check
3
+ /**
4
+ * fray-mcp — THE fray MCP server: one unified, dependency-free MCP stdio server (mounted as `fray`,
5
+ * so its tools are `mcp__fray__<tool>`) carrying every capability fray hands its own WORKERS. Today
6
+ * that is exactly one tool, `spawn_thread`, which dispatches a brand-new TOP-LEVEL fray board thread
7
+ * (its own session + scratchpad + independent drive — NOT an in-session Agent/Task helper). Future
8
+ * worker-facing fray tools join the TOOLS registry below rather than mounting a second server: one
9
+ * server keeps the worker's tool namespace coherent and the server-level pre-approval single.
10
+ *
11
+ * spawn_thread wraps fray's own dispatch RPC: it reads the running server's port from
12
+ * `<state-dir>/server.lock` and POSTs `/rpc/dispatch`. The `/rpc` surface has no token auth — only a
13
+ * loopback-origin CSRF gate — so a headerless local POST with `sec-fetch-site: same-origin` (undici
14
+ * sends no Origin) satisfies it.
15
+ *
16
+ * Mounted by the server (dispatch.ts) into the Claude backend via `--mcp-config`. The server passes
17
+ * FRAY_STATE_DIR in this process's env so we can locate server.lock without recomputing the project id.
18
+ *
19
+ * Protocol: MCP over stdio = newline-delimited JSON-RPC 2.0. We implement exactly the four methods a
20
+ * client drives (initialize, tools/list, tools/call, ping) plus the initialized notification. Hand-
21
+ * rolled rather than pulling @modelcontextprotocol/sdk: the surface is tiny, it ships as one loose
22
+ * .mjs next to bin/fray (no build/bundle/resolution concerns), and it matches this repo's own
23
+ * hand-rolled-RPC aesthetic. The server NEVER crashes on a bad tool call: failures come back as an
24
+ * isError tool result so the worker sees a message instead of a dead tool.
25
+ */
26
+ import { readFileSync } from "node:fs"
27
+ import { join } from "node:path"
28
+
29
+ const PROTOCOL_FALLBACK = "2025-06-18"
30
+ // Comfortably above a codex dispatch's bounded rollout-discovery wait (~15s) so a legitimate slow
31
+ // dispatch is never aborted client-side (which would make the worker think it failed and retry,
32
+ // double-spawning). The server completes regardless; this is only the client's patience.
33
+ const DISPATCH_TIMEOUT_MS = 30_000
34
+
35
+ const SPAWN_THREAD = {
36
+ name: "spawn_thread",
37
+ description:
38
+ "Spawn a brand-new, separate top-level fray thread — its own board card, session, and scratchpad, " +
39
+ "driving INDEPENDENTLY. This is FIRE-AND-FORGET: the new thread reports to the HUMAN on the board via " +
40
+ "its own final message, and its results NEVER come back to you, the caller. It is NOT an in-session " +
41
+ "sub-agent. It returns only the new thread's slug and a ready-to-paste markdown link " +
42
+ "`[title](/thread/<slug>)` that opens the thread in the fray drawer — put that link in your handoff. " +
43
+ "USE IT ONLY for a distinct, self-contained effort that belongs on the board in its own right and whose " +
44
+ "output you do NOT need to read. Do NOT use it for a helper whose result you must COLLECT and fold into " +
45
+ "your own work — a self-review, a verification pass, a research prong, a critic, any collect-back helper: " +
46
+ "those are in-session sub-agents (Claude: the Agent tool with `run_in_background`; Codex: native " +
47
+ "delegation), which return their findings to you. Spawning such a helper here STRANDS it — its work lands " +
48
+ "on another card and never reaches you, so you gain nothing. " +
49
+ "You MUST deliberately choose `model` and `effort` to match the NEW thread's task complexity — they are " +
50
+ "required, there is NO default. Do not reflexively pick the cheapest; a hard task on a weak model/effort " +
51
+ "wastes the whole thread.",
52
+ inputSchema: {
53
+ type: "object",
54
+ properties: {
55
+ prompt: {
56
+ type: "string",
57
+ description: "The full task/prompt for the new thread's worker. Be self-contained — the new thread starts with empty context.",
58
+ },
59
+ model: {
60
+ type: "string",
61
+ description:
62
+ "REQUIRED — pick by the NEW task's complexity; there is no default. For the `claude` backend: " +
63
+ "`opus` (the TOP tier — hardest reasoning, architecture, subtle correctness/security, adversarial " +
64
+ "review, the fix that must land), `sonnet` (ordinary substantive implementation/research), `haiku` " +
65
+ "(simple, fully-specified mechanical work). Do NOT pick `fable`: Opus 5 is just as good and cheaper, " +
66
+ "so a high-intensity task takes `opus` at a higher `effort`, not a different model — `fable` only " +
67
+ "when the human explicitly asks for it. For " +
68
+ "the `codex` backend use a codex model id instead (e.g. `gpt-5.6-sol`/`gpt-5.6-terra`/`gpt-5.6-luna`). " +
69
+ "Match the model to the backend you choose. Bias toward Opus/a strong model when the task is " +
70
+ "non-trivial or its outcome is load-bearing.",
71
+ },
72
+ effort: {
73
+ type: "string",
74
+ enum: ["low", "medium", "high", "xhigh", "max"],
75
+ description:
76
+ "REQUIRED — reasoning effort, pick by complexity; no default. `low` only for trivial tasks; " +
77
+ "`medium` for routine work; `high` for ordinary substantive work; `xhigh` for hard coding/agentic " +
78
+ "work; `max` for the single hardest problems. (Codex also accepts `ultra`.)",
79
+ },
80
+ backend: {
81
+ type: "string",
82
+ enum: ["claude", "codex"],
83
+ description: "Optional agent backend (default `claude`). If `codex`, `model` must be a codex model id.",
84
+ },
85
+ title: { type: "string", description: "Optional short title for the new thread (else derived from the prompt)." },
86
+ },
87
+ required: ["prompt", "model", "effort"],
88
+ },
89
+ }
90
+
91
+ // The unified server's tool registry: `tools/list` returns these and `tools/call` routes by name.
92
+ // Adding a worker-facing fray tool = one entry here + one handler in `HANDLERS` — never a second
93
+ // MCP server, so every fray tool stays under the same `mcp__fray__*` namespace and the same
94
+ // server-level pre-approval the dispatch layer already grants.
95
+ const TOOLS = [SPAWN_THREAD]
96
+
97
+ /** @type {Record<string, (args: Record<string, unknown>) => Promise<string>>} */
98
+ const HANDLERS = {
99
+ [SPAWN_THREAD.name]: spawnThread,
100
+ }
101
+
102
+ /** @param {unknown} obj */
103
+ function send(obj) {
104
+ process.stdout.write(JSON.stringify(obj) + "\n")
105
+ }
106
+ /** @param {string|number} id @param {unknown} result */
107
+ function reply(id, result) {
108
+ send({ jsonrpc: "2.0", id, result })
109
+ }
110
+ /** @param {string|number} id @param {number} code @param {string} message */
111
+ function replyError(id, code, message) {
112
+ send({ jsonrpc: "2.0", id, error: { code, message } })
113
+ }
114
+ /** @param {string|number} id @param {string} text @param {boolean} [isError] */
115
+ function replyTool(id, text, isError) {
116
+ reply(id, { content: [{ type: "text", text }], ...(isError ? { isError: true } : {}) })
117
+ }
118
+
119
+ function serverLockPort() {
120
+ const lock = process.env.FRAY_SERVER_LOCK
121
+ || (process.env.FRAY_STATE_DIR ? join(process.env.FRAY_STATE_DIR, "server.lock") : undefined)
122
+ if (!lock) throw new Error("FRAY_STATE_DIR / FRAY_SERVER_LOCK not set — cannot locate the fray server")
123
+ let parsed
124
+ try {
125
+ parsed = JSON.parse(readFileSync(lock, "utf8"))
126
+ } catch (err) {
127
+ throw new Error(`could not read the fray server lock at ${lock} (is the server running?): ${err instanceof Error ? err.message : err}`)
128
+ }
129
+ const port = parsed?.port
130
+ if (!Number.isInteger(port)) throw new Error(`fray server lock at ${lock} has no valid port`)
131
+ return port
132
+ }
133
+
134
+ /** The `spawn_thread` handler: POST /rpc/dispatch, return the worker-facing result text.
135
+ * @param {Record<string, unknown>} args @returns {Promise<string>} */
136
+ async function spawnThread(args) {
137
+ const prompt = typeof args.prompt === "string" ? args.prompt.trim() : ""
138
+ if (!prompt) throw new Error("`prompt` is required and must be a non-empty string")
139
+ // model + effort are REQUIRED (no default) so the caller must choose by task complexity — a defaulted
140
+ // model (e.g. the project's cheap default) is exactly the bug this guards. Enforced server-side too,
141
+ // not only in the tool schema, so a lenient client can't skip the decision.
142
+ const model = typeof args.model === "string" ? args.model.trim() : ""
143
+ if (!model) throw new Error("`model` is required — choose one by the new task's complexity (claude: opus/sonnet/haiku, opus being the top tier; codex: a gpt-5.6 model id). There is no default.")
144
+ const effort = typeof args.effort === "string" ? args.effort.trim() : ""
145
+ if (!effort) throw new Error("`effort` is required — choose one by complexity (low/medium/high/xhigh/max). There is no default.")
146
+
147
+ /** @type {Record<string, unknown>} */
148
+ const body = { prompt, model, effort }
149
+ if (typeof args.title === "string" && args.title.trim()) body.title = args.title.trim()
150
+ if (args.backend === "claude" || args.backend === "codex") body.backend = args.backend
151
+
152
+ const port = serverLockPort()
153
+ const controller = new AbortController()
154
+ const timer = setTimeout(() => controller.abort(), DISPATCH_TIMEOUT_MS)
155
+ let res
156
+ try {
157
+ res = await fetch(`http://127.0.0.1:${port}/rpc/dispatch`, {
158
+ method: "POST",
159
+ // No Origin header (undici omits it for non-browser fetch); `sec-fetch-site: same-origin`
160
+ // satisfies the server's loopback-origin gate (app.ts isTrustedLocalHttpRequest).
161
+ headers: { "content-type": "application/json", "sec-fetch-site": "same-origin" },
162
+ body: JSON.stringify(body),
163
+ signal: controller.signal,
164
+ })
165
+ } catch (err) {
166
+ throw new Error(`dispatch request failed: ${err instanceof Error ? err.message : err}`)
167
+ } finally {
168
+ clearTimeout(timer)
169
+ }
170
+ if (!res.ok) {
171
+ const detail = await res.text().catch(() => "")
172
+ throw new Error(`dispatch returned HTTP ${res.status}${detail ? `: ${detail.slice(0, 500)}` : ""}`)
173
+ }
174
+ const payload = await res.json().catch(() => null)
175
+ const slug = payload?.result?.slug
176
+ if (typeof slug !== "string" || !slug) throw new Error(`dispatch response missing a slug: ${JSON.stringify(payload)?.slice(0, 300)}`)
177
+ const label = typeof body.title === "string" ? body.title : slug
178
+ return (
179
+ `Spawned a new fray thread \`${slug}\`. It is now on the board driving independently — it reports ` +
180
+ `to the human via its own final message, NOT back to you, so do not wait on a result from it.\n\n` +
181
+ `Paste this link to let the human open it in the drawer:\n\n[${label}](/thread/${slug})`
182
+ )
183
+ }
184
+
185
+ /** @param {any} msg */
186
+ async function handle(msg) {
187
+ const { id, method, params } = msg ?? {}
188
+ const isNotification = id === undefined || id === null
189
+
190
+ switch (method) {
191
+ case "initialize": {
192
+ const requested = params?.protocolVersion
193
+ reply(id, {
194
+ protocolVersion: typeof requested === "string" ? requested : PROTOCOL_FALLBACK,
195
+ capabilities: { tools: {} },
196
+ serverInfo: { name: "fray", version: "0.1.0" },
197
+ })
198
+ return
199
+ }
200
+ case "notifications/initialized":
201
+ case "initialized":
202
+ return // notification — no reply
203
+ case "ping":
204
+ if (!isNotification) reply(id, {})
205
+ return
206
+ case "tools/list":
207
+ reply(id, { tools: TOOLS })
208
+ return
209
+ case "tools/call": {
210
+ const name = typeof params?.name === "string" ? params.name : ""
211
+ const handler = HANDLERS[name]
212
+ if (!handler) {
213
+ replyError(id, -32602, `unknown tool: ${params?.name}`)
214
+ return
215
+ }
216
+ try {
217
+ replyTool(id, await handler(params?.arguments ?? {}))
218
+ } catch (err) {
219
+ replyTool(id, `\`${name}\` failed: ${err instanceof Error ? err.message : String(err)}`, true)
220
+ }
221
+ return
222
+ }
223
+ default:
224
+ if (!isNotification) replyError(id, -32601, `method not found: ${method}`)
225
+ return
226
+ }
227
+ }
228
+
229
+ // NDJSON reader: buffer stdin, dispatch each complete line. Messages never contain raw newlines.
230
+ let buf = ""
231
+ process.stdin.setEncoding("utf8")
232
+ process.stdin.on("data", (chunk) => {
233
+ buf += chunk
234
+ let nl
235
+ while ((nl = buf.indexOf("\n")) >= 0) {
236
+ const line = buf.slice(0, nl).trim()
237
+ buf = buf.slice(nl + 1)
238
+ if (!line) continue
239
+ let msg
240
+ try {
241
+ msg = JSON.parse(line)
242
+ } catch {
243
+ continue // ignore unparseable lines
244
+ }
245
+ void handle(msg)
246
+ }
247
+ })
248
+ process.stdin.on("end", () => process.exit(0))
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ // @ts-check
3
+ /**
4
+ * fray-update — the structured thread updater, on the worker's Bash PATH while the fray-worker
5
+ * plugin is enabled. This is the worker's PRIMARY tool for owning its one thread file: it writes
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.
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
11
+ * a file:// URL so argv/process.env flow straight through.
12
+ */
13
+ import { fileURLToPath, pathToFileURL } from 'node:url';
14
+ import { dirname, join } from 'node:path';
15
+
16
+ const here = dirname(fileURLToPath(import.meta.url));
17
+ const updater = join(here, '..', '..', 'cc', 'scripts', 'fray', 'thread-update.mjs');
18
+ await import(pathToFileURL(updater).href);
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ // @ts-check
3
+ // PostToolUse hook on the `Agent` tool (fray-worker) — the AUTOMATIC thread↔agent binding, kept
4
+ // COMPATIBLE with cc's board. When a worker dispatches a helper tagged `THREAD: <slug>` at the top
5
+ // of the prompt, this records `agentId → thread` into `.fray/.agent-bindings.jsonl` in the exact
6
+ // shape cc's board (`bindingsByThread`) reads for per-thread sub-agent liveness — so fray-ui renders
7
+ // a worker's helper activity the same way the cc board does. An untagged helper writes nothing.
8
+ //
9
+ // GATE: inert unless FRAY_UI_THREAD is set. FAIL-OPEN ABSOLUTELY: any error → exit 0, no output.
10
+ // A missed binding just means one helper isn't surfaced; a PostToolUse hook must never disturb the turn.
11
+ import { readFileSync } from 'node:fs';
12
+ import { recordBinding, threadFromPrompt } from '../scripts/fray/agent-bindings.mjs';
13
+
14
+ try {
15
+ // WORKER GATE.
16
+ if (!(process.env.FRAY_UI_THREAD ?? '').trim()) process.exit(0);
17
+
18
+ const input = JSON.parse(readFileSync(0, 'utf8'));
19
+ const dir = process.env.CLAUDE_PROJECT_DIR ?? '.';
20
+
21
+ const ti = input.tool_input ?? {};
22
+ const tr = input.tool_response ?? {};
23
+ const trInner = tr.toolUseResult ?? tr;
24
+
25
+ const agentId = trInner.agentId ?? tr.agentId ?? null;
26
+ const prompt =
27
+ (typeof trInner.prompt === 'string' && trInner.prompt) ||
28
+ (typeof tr.prompt === 'string' && tr.prompt) ||
29
+ (typeof ti.prompt === 'string' && ti.prompt) ||
30
+ '';
31
+ const thread = threadFromPrompt(prompt);
32
+ const label = (typeof ti.description === 'string' && ti.description) || trInner.description || null;
33
+
34
+ if (agentId && thread) {
35
+ recordBinding(dir, { agentId, thread, label, session: input.session_id ?? null });
36
+ }
37
+ } catch {
38
+ /* fail-open — never disturb the turn */
39
+ }
40
+ process.exit(0);
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+ // @ts-check
3
+ // PreToolUse hook on the `Agent` tool (fray-worker). A worker MAY spin up its own helper
4
+ // sub-agents; this hook holds them to the same rules cc enforces for the orchestrator:
5
+ // 1) ENFORCE background dispatch — deny any Agent call lacking run_in_background:true (a
6
+ // foreground agent blocks the worker's turn; a human interjection orphans it).
7
+ // 2) STRIP `name`/`team_name` — setting either strands a nested dispatch (its result routes
8
+ // wrong and never returns cleanly), so scrub both silently.
9
+ // 3) AUTO-APPEND a repo-neutral ORCHESTRATION EPILOGUE so helpers return a useful handoff
10
+ // and know how to reach their dispatcher mid-flight without imposing build, test, git,
11
+ // compilation, or process-lifecycle policy on arbitrary repos.
12
+ //
13
+ // GATE: inert unless FRAY_UI_THREAD is set (not a fray-ui worker → allow every dispatch unmodified).
14
+ //
15
+ // DROPPED vs cc's agent-dispatch.mjs (see DECISIONS.md): the `.dispatch-count` bump (it only gates
16
+ // cc's SubagentStop rest-recorder, which cc-worker does not ship) and the THREAD:-ledger write +
17
+ // thread-existence DENY gate (that guards the orchestrator's "file the thread before dispatching"
18
+ // discipline; a worker owns exactly one already-existing thread and its helpers own no thread).
19
+ //
20
+ // FAIL OPEN: any parse error → allow unmodified. A broken dispatch hook must never halt work.
21
+ import { readFileSync } from 'node:fs';
22
+
23
+ const EPILOGUE = `
24
+
25
+ ---
26
+ [ORCHESTRATION EPILOGUE — auto-appended by the fray worker dispatch hook] You are a helper sub-agent for a fray-ui worker. Your final message is the handoff: report your outcome/status; what you did; changed files, artifacts, and commit SHA when applicable; verification or evidence and its result; caveats or unresolved work; and the next action when one remains. A bare "done" or progress-only message is not a complete handoff.
27
+ The worker's scratchpad is shared: merge your own scoped progress into it as you work rather than leaving the root as its sole writer. Re-read before every edit and preserve every other agent's content. This exact scratchpad is Fray coordination state, not a project deliverable or source edit: a scoped merge is an explicit exception to delegated phrases such as "write only <path>" or "do not modify the repo"; never classify it as unauthorized or roll it back. Never delete, truncate, reinitialize, move, or replace the whole file. Other project files, including repository-root files, remain governed by your delegated authority; their location alone neither permits nor forbids editing. Do not edit other \`.fray/\` state unless your prompt explicitly asks you to.
28
+ You have an upward channel while you work: \`SendMessage({to: "main", summary: "<5-10 words>", message: "…"})\` delivers to your dispatcher. Use it when the dispatcher acting before you finish could change the outcome—for example, when you hit an unresolved blocker, complete a milestone another task needs, or discover that your instructions should change. Do not use it for routine progress updates.`;
29
+
30
+ /** @param {unknown} obj @returns {never} */
31
+ function emit(obj) {
32
+ process.stdout.write(JSON.stringify(obj));
33
+ process.exit(0);
34
+ }
35
+
36
+ try {
37
+ // WORKER GATE — inert outside a fray-ui worker session.
38
+ if (!(process.env.FRAY_UI_THREAD ?? '').trim()) emit({});
39
+
40
+ const input = JSON.parse(readFileSync(0, 'utf8'));
41
+ const ti = input.tool_input ?? {};
42
+
43
+ if (ti.run_in_background !== true) {
44
+ emit({
45
+ hookSpecificOutput: {
46
+ hookEventName: 'PreToolUse',
47
+ permissionDecision: 'deny',
48
+ permissionDecisionReason:
49
+ 'fray worker (hook-enforced): Agent sub-agents MUST be dispatched with run_in_background:true — never foreground/blocking. A foreground agent blocks the worker turn and a human interjection orphans its work. Re-send this Agent call with run_in_background:true.',
50
+ },
51
+ });
52
+ }
53
+
54
+ // Strip name/team_name (they strand nested dispatches), then append the epilogue once.
55
+ const { name: _droppedName, team_name: _droppedTeam, ...tiStripped } = ti;
56
+ // Idempotence is "this prompt ALREADY ENDS WITH the epilogue", not "this prompt mentions the
57
+ // marker anywhere". A substring test silently ate the epilogue for any prompt that merely QUOTED
58
+ // the marker — e.g. a worker asking a helper to report whether the epilogue reached it, which is
59
+ // exactly how this was caught. endsWith still catches a genuine double-fire (the only real case).
60
+ const prompt = typeof ti.prompt === 'string' ? ti.prompt : '';
61
+ const updatedInput = prompt.endsWith(EPILOGUE)
62
+ ? tiStripped
63
+ : { ...tiStripped, prompt: prompt + EPILOGUE };
64
+
65
+ emit({
66
+ hookSpecificOutput: {
67
+ hookEventName: 'PreToolUse',
68
+ permissionDecision: 'allow',
69
+ updatedInput,
70
+ },
71
+ });
72
+ } catch {
73
+ emit({}); // fail open — allow unmodified
74
+ }
@@ -0,0 +1,6 @@
1
+ export function hasEscapingBackgroundJob(raw: unknown): boolean
2
+ export function evaluateBashBackgroundHook(
3
+ input: unknown,
4
+ env?: Record<string, string | undefined>,
5
+ ): Record<string, unknown>
6
+ export function isDirectHookExecution(argv1: unknown, moduleUrl: string): boolean