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.
- package/LICENSE +21 -0
- package/README.md +186 -0
- package/dist/claude-agent-broker.js +21641 -0
- package/dist/codex-app-server-daemon.js +354 -0
- package/dist/dev-child.js +40319 -0
- package/dist/frayui.js +8738 -0
- package/package.json +46 -0
- package/runtime/cc/scripts/fray/agent-bindings.mjs +287 -0
- package/runtime/cc/scripts/fray/agent-liveness.mjs +367 -0
- package/runtime/cc/scripts/fray/agent-status.mjs +178 -0
- package/runtime/cc/scripts/fray/config.mjs +982 -0
- package/runtime/cc/scripts/fray/decisions.mjs +97 -0
- package/runtime/cc/scripts/fray/index.mjs +699 -0
- package/runtime/cc/scripts/fray/notify-shared.mjs +90 -0
- package/runtime/cc/scripts/fray/notify.mjs +81 -0
- package/runtime/cc/scripts/fray/ownership.mjs +120 -0
- package/runtime/cc/scripts/fray/rest-detect.mjs +213 -0
- package/runtime/cc/scripts/fray/thread-excerpt.mjs +162 -0
- package/runtime/cc/scripts/fray/thread-update.mjs +285 -0
- package/runtime/cc-worker/.claude-plugin/plugin.json +10 -0
- package/runtime/cc-worker/DECISIONS.md +923 -0
- package/runtime/cc-worker/LICENSE +21 -0
- package/runtime/cc-worker/agents/fable-high.md +8 -0
- package/runtime/cc-worker/agents/fable-low.md +8 -0
- package/runtime/cc-worker/agents/fable-max.md +8 -0
- package/runtime/cc-worker/agents/fable-medium.md +8 -0
- package/runtime/cc-worker/agents/fable-xhigh.md +8 -0
- package/runtime/cc-worker/agents/haiku.md +7 -0
- package/runtime/cc-worker/agents/opus-high.md +8 -0
- package/runtime/cc-worker/agents/opus-low.md +8 -0
- package/runtime/cc-worker/agents/opus-max.md +8 -0
- package/runtime/cc-worker/agents/opus-medium.md +8 -0
- package/runtime/cc-worker/agents/opus-xhigh.md +8 -0
- package/runtime/cc-worker/agents/sonnet-high.md +8 -0
- package/runtime/cc-worker/agents/sonnet-low.md +8 -0
- package/runtime/cc-worker/agents/sonnet-max.md +8 -0
- package/runtime/cc-worker/agents/sonnet-medium.md +8 -0
- package/runtime/cc-worker/agents/sonnet-xhigh.md +8 -0
- package/runtime/cc-worker/bin/fray +17 -0
- package/runtime/cc-worker/bin/fray-mcp.mjs +248 -0
- package/runtime/cc-worker/bin/fray-update +18 -0
- package/runtime/cc-worker/hooks/agent-bind.mjs +40 -0
- package/runtime/cc-worker/hooks/agent-dispatch.mjs +74 -0
- package/runtime/cc-worker/hooks/bash-background.d.mts +6 -0
- package/runtime/cc-worker/hooks/bash-background.mjs +188 -0
- package/runtime/cc-worker/hooks/deny-ask.mjs +38 -0
- package/runtime/cc-worker/hooks/deny-plan.mjs +61 -0
- package/runtime/cc-worker/hooks/hooks.json +111 -0
- package/runtime/cc-worker/hooks/perm-policy.mjs +211 -0
- package/runtime/cc-worker/hooks/precompact-instructions.mjs +122 -0
- package/runtime/cc-worker/hooks/scratchpad-stop.mjs +125 -0
- package/runtime/cc-worker/hooks/scratchpad.mjs +419 -0
- package/runtime/cc-worker/hooks/session-seed.mjs +105 -0
- package/runtime/cc-worker/scripts/fray/agent-bindings.mjs +9 -0
- package/runtime/cc-worker/scripts/fray/config.mjs +12 -0
- package/runtime/cc-worker/skills/gh/SKILL.md +132 -0
- package/runtime/cc-worker/skills/gh/scripts/ci-watch.mjs +60 -0
- package/runtime/cc-worker/skills/gh/scripts/github-watch.mjs +130 -0
- package/runtime/cc-worker/skills/gh/scripts/review-watch.mjs +54 -0
- package/runtime/cc-worker/skills/handoff/SKILL.md +203 -0
- package/runtime/cc-worker/skills/waits/SKILL.md +83 -0
- package/web-dist/apple-touch-icon.png +0 -0
- package/web-dist/assets/TerminalPane-fMGrgB-a.js +7 -0
- package/web-dist/assets/abnfDiagram-VRR7QNED-BtlrDuYX.js +1 -0
- package/web-dist/assets/arc-BSyeo0Gb.js +1 -0
- package/web-dist/assets/architecture-TIHT7OUA-CuD8jpr-.js +1 -0
- package/web-dist/assets/architectureDiagram-ZJ3FMSHR-CdLQ71be.js +36 -0
- package/web-dist/assets/array-BifhSqXX.js +1 -0
- package/web-dist/assets/blockDiagram-677ZJIJ3-CBk2FHEj.js +132 -0
- package/web-dist/assets/c4Diagram-LMCZKHZV-mZ3HQ6WX.js +10 -0
- package/web-dist/assets/channel-C2eUWc74.js +1 -0
- package/web-dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/web-dist/assets/chunk-32BRIVSS-Daxvi7f5.js +1 -0
- package/web-dist/assets/chunk-52WLFC77-BGJoZvry.js +10 -0
- package/web-dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/web-dist/assets/chunk-7BUUIJ7U-Bb538aSH.js +1 -0
- package/web-dist/assets/chunk-C7G6YPKG-Ycd1yheh.js +1 -0
- package/web-dist/assets/chunk-EX3LRPZG-BPFhlsHp.js +231 -0
- package/web-dist/assets/chunk-FWX5IMBZ--xy8DTon.js +2 -0
- package/web-dist/assets/chunk-HOUHSVGY-Dq3zoygp.js +1 -0
- package/web-dist/assets/chunk-ICXQ74PX-FfKP-7yM.js +2 -0
- package/web-dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/web-dist/assets/chunk-KEIR6QF5-BfrZ3jm6.js +161 -0
- package/web-dist/assets/chunk-MOJQB5TN-BeiHnsLF.js +88 -0
- package/web-dist/assets/chunk-OGEWGWER-CjDU_9fC.js +1 -0
- package/web-dist/assets/chunk-PUDLZKDR-BO7tm5QX.js +156 -0
- package/web-dist/assets/chunk-Q4XR5HBZ-BPefovOg.js +70 -0
- package/web-dist/assets/chunk-RYQCIY6F-Cu_KplZW.js +1 -0
- package/web-dist/assets/chunk-V7JOEXUC-DWnebFHh.js +206 -0
- package/web-dist/assets/chunk-VAUOI2AC-BL6qWFhW.js +1 -0
- package/web-dist/assets/chunk-VR4S4FIN-Mfs__7L9.js +1 -0
- package/web-dist/assets/chunk-WYO6CB5R-Bf2IYbEU.js +127 -0
- package/web-dist/assets/chunk-XXDRQBXY-DYlTP5J-.js +1 -0
- package/web-dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/web-dist/assets/chunk-ZGVPDNZ5-CJ9ZHZgU.js +62 -0
- package/web-dist/assets/chunk-ZIRB5QZD-C6fEPe3t.js +32 -0
- package/web-dist/assets/classDiagram-OUVF2IWQ-GaOJNgfE.js +1 -0
- package/web-dist/assets/classDiagram-v2-EOCWNBFH-GaOJNgfE.js +1 -0
- package/web-dist/assets/cose-bilkent-JH36ORCC-BUIsLrGc.js +1 -0
- package/web-dist/assets/cynefin-VYW2F7L2-COSC0oNL.js +1 -0
- package/web-dist/assets/cynefinDiagram-TSTJHNR4-mInhJdg1.js +62 -0
- package/web-dist/assets/cytoscape.esm-B3I8pqwA.js +321 -0
- package/web-dist/assets/dagre-CXRCoUWR.js +1 -0
- package/web-dist/assets/dagre-VKFMJZFB-D-T5z05M.js +4 -0
- package/web-dist/assets/defaultLocale-C8Fc0cco.js +1 -0
- package/web-dist/assets/diagram-FQU43EPY-HbMkSb3j.js +3 -0
- package/web-dist/assets/diagram-G47NLZAW-DBwZLcZh.js +24 -0
- package/web-dist/assets/diagram-NH7WQ7WH-DG_Mhays.js +24 -0
- package/web-dist/assets/diagram-OA4YK3LP-hH39gqpu.js +30 -0
- package/web-dist/assets/diagram-WEI45ONY-DhKZuwxW.js +41 -0
- package/web-dist/assets/dist-DoH_9pyS.js +1 -0
- package/web-dist/assets/ebnfDiagram-CCIWWBDH-B7BiO-NR.js +1 -0
- package/web-dist/assets/erDiagram-Q63AITRT-DHjG5RDQ.js +85 -0
- package/web-dist/assets/eventmodeling-45OFAUF4-C0eV8RBx.js +1 -0
- package/web-dist/assets/flowDiagram-23GEKE2U-BbldRZUK.js +1 -0
- package/web-dist/assets/ganttDiagram-NO4QXBWP-BZ-98wTa.js +292 -0
- package/web-dist/assets/gitGraph-TEB2WS4Q-nF22R2jO.js +1 -0
- package/web-dist/assets/gitGraphDiagram-IHSO6WYX-CoYQQ22V.js +106 -0
- package/web-dist/assets/graphlib-B8gBHxth.js +1 -0
- package/web-dist/assets/index-BHzIN-tQ.js +357 -0
- package/web-dist/assets/index-Duiy4w7C.css +1 -0
- package/web-dist/assets/info-DKCQHKI2-Cy2BCbBW.js +1 -0
- package/web-dist/assets/infoDiagram-FWYZ7A6U-CmhR2R1x.js +2 -0
- package/web-dist/assets/init-D6jRqBbL.js +1 -0
- package/web-dist/assets/ishikawaDiagram-FXEZZL3T-CfG59afl.js +70 -0
- package/web-dist/assets/journeyDiagram-5HDEW3XC-BvFmG20q.js +139 -0
- package/web-dist/assets/kanban-definition-HUTT4EX6-BHGEecsY.js +89 -0
- package/web-dist/assets/katex-CddkPoXu.js +257 -0
- package/web-dist/assets/line-Ds4xvN3d.js +1 -0
- package/web-dist/assets/linear-z2V0wJk9.js +1 -0
- package/web-dist/assets/map-DsCK-0Cs.js +1 -0
- package/web-dist/assets/mermaid-parser.core-Gow4kgYJ.js +7 -0
- package/web-dist/assets/mermaid.core-B7Qc_kbm.js +11 -0
- package/web-dist/assets/mindmap-definition-LN4V7U3C-ayXKGcy_.js +96 -0
- package/web-dist/assets/ordinal-hYBb2elL.js +1 -0
- package/web-dist/assets/packet-7NZHBO7P-oLBdtkD8.js +1 -0
- package/web-dist/assets/path-BWPyau1x.js +1 -0
- package/web-dist/assets/pegDiagram-2B236MQR-DwJyc_1B.js +1 -0
- package/web-dist/assets/pie-RZYD4A2V-CXzad3fT.js +1 -0
- package/web-dist/assets/pieDiagram-ENE6RG2P-CZzgSGtM.js +39 -0
- package/web-dist/assets/quadrantDiagram-ABIIQ3AL--IPIp8yo.js +7 -0
- package/web-dist/assets/radar-I7S5WNFK-DldySvtZ.js +1 -0
- package/web-dist/assets/railroad-3IZDKUUU-CiWFZfXM.js +1 -0
- package/web-dist/assets/railroad-abnf-AHOZXSZD-bXMYUOCD.js +1 -0
- package/web-dist/assets/railroad-ebnf-EBAXGLYW-D11ZwZby.js +1 -0
- package/web-dist/assets/railroad-peg-LSFZ7HO6-DD1AC5X-.js +1 -0
- package/web-dist/assets/railroadDiagram-RFXS5EU6-DcCMNGGb.js +1 -0
- package/web-dist/assets/requirementDiagram-TGXJPOKE-C-6j8urk.js +84 -0
- package/web-dist/assets/rolldown-runtime-Bh1tDfsg.js +1 -0
- package/web-dist/assets/rough.esm-CSKSodPl.js +1 -0
- package/web-dist/assets/sankeyDiagram-HTMAVEWB-CPtiOpdz.js +40 -0
- package/web-dist/assets/sequenceDiagram-DBY2YBRQ-B8FnQTdn.js +162 -0
- package/web-dist/assets/sizeCapture-X5ZJPWSS-B0uUizjq.js +1 -0
- package/web-dist/assets/src-C4XfhTaE.js +1 -0
- package/web-dist/assets/stateDiagram-2N3HPSRC-CxG9nX2P.js +1 -0
- package/web-dist/assets/stateDiagram-v2-6OUMAXLB-SGarOFIy.js +1 -0
- package/web-dist/assets/swimlanes-5IMT3BWC-D49-t0hV.js +2 -0
- package/web-dist/assets/swimlanesDiagram-G3AALYLV-krF0bh4b.js +8 -0
- package/web-dist/assets/timeline-definition-FHXFAJF6-C9OEd9SE.js +120 -0
- package/web-dist/assets/treeView-QDETBFTQ-D1AKqVPa.js +1 -0
- package/web-dist/assets/treemap-6X3UGDF4-BRssB7hM.js +1 -0
- package/web-dist/assets/vennDiagram-L72KCM5P-CftkAbY0.js +34 -0
- package/web-dist/assets/wardley-OPB4EBWU-RLMgzRq4.js +1 -0
- package/web-dist/assets/wardleyDiagram-EHGQE667-BLytgXPS.js +78 -0
- package/web-dist/assets/xychartDiagram-FW5EYKEG-BYgK1Y3H.js +7 -0
- package/web-dist/favicon-16.png +0 -0
- package/web-dist/favicon-32.png +0 -0
- package/web-dist/favicon.svg +78 -0
- package/web-dist/icon-192.png +0 -0
- package/web-dist/icon-512.png +0 -0
- package/web-dist/icon-maskable-512.png +0 -0
- package/web-dist/index.html +33 -0
- package/web-dist/manifest.webmanifest +16 -0
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "frayui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Local orchestration surface for Fray projects — a per-repo web UI over your .fray/ agent threads",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"fray",
|
|
8
|
+
"claude",
|
|
9
|
+
"claude-code",
|
|
10
|
+
"agents",
|
|
11
|
+
"orchestration",
|
|
12
|
+
"tui"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"author": "Colin McDonnell",
|
|
16
|
+
"homepage": "https://github.com/colinhacks/fray",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/colinhacks/fray.git",
|
|
20
|
+
"directory": "packages/cli"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=26"
|
|
24
|
+
},
|
|
25
|
+
"type": "module",
|
|
26
|
+
"bin": {
|
|
27
|
+
"frayui": "./dist/frayui.js"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"web-dist",
|
|
32
|
+
"runtime"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"prepack": "node ./scripts/prepare-package.mjs && node ./scripts/build-package.mjs && node ./scripts/publish-manifest.mjs --strip",
|
|
36
|
+
"postpack": "node ./scripts/publish-manifest.mjs --restore"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@parcel/watcher": "^2.5",
|
|
40
|
+
"better-sqlite3": "^12",
|
|
41
|
+
"node-pty": "^1.1"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"esbuild": "^0.25.0"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* fray — the AUTOMATIC, ephemeral thread↔agent binding.
|
|
4
|
+
*
|
|
5
|
+
* REPLACES the old hand-maintained `agents: [{id, label}]` thread frontmatter (a
|
|
6
|
+
* drift-prone ledger the orchestrator had to write by hand on every dispatch). The
|
|
7
|
+
* binding is now captured AUTOMATICALLY by the `agent-bind` hook (PostToolUse on the
|
|
8
|
+
* `Agent` tool): the Agent tool's RESULT carries the new `agentId`, the resolved
|
|
9
|
+
* `prompt` (with the `THREAD: <slug>` tag), and the `description` (a human label) — so
|
|
10
|
+
* the moment a background sub-agent is launched, the hook records `agentId → thread`
|
|
11
|
+
* here. The orchestrator records NOTHING by hand; there is no frontmatter to drift.
|
|
12
|
+
*
|
|
13
|
+
* EPHEMERAL ROUTING STATE, NOT DUPLICATED TRUTH. This file (`.fray/.agent-bindings.jsonl`)
|
|
14
|
+
* is transient routing info under the already-gitignored `.fray/` — the same class as
|
|
15
|
+
* `.dispatch-ledger.jsonl` / `.rested-agents.jsonl`. It maps an instance id to the thread
|
|
16
|
+
* it serves so consumers can RECONNECT a return/rest to its thread; it is never the source
|
|
17
|
+
* of a thread's truth (that's the thread's own `status:` + body). Append-only; a binding is
|
|
18
|
+
* immutable once written. Liveness/doneness is still DERIVED from ground truth (output-file
|
|
19
|
+
* mtime + thread status) — the binding only supplies the id↔thread mapping, never a state.
|
|
20
|
+
*
|
|
21
|
+
* FAIL-OPEN ABSOLUTELY: every reader/writer swallows errors. A missing/corrupt bindings
|
|
22
|
+
* file simply yields no bindings — the board and hooks degrade to "no agents to surface,"
|
|
23
|
+
* never an error.
|
|
24
|
+
*/
|
|
25
|
+
import { appendFileSync, readFileSync } from 'node:fs';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
|
|
28
|
+
/** @param {string} projectDir */
|
|
29
|
+
export function bindingsPath(projectDir) {
|
|
30
|
+
return join(projectDir, '.fray', '.agent-bindings.jsonl');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Extract the `THREAD: <slug>` tag from a dispatch prompt — the SAME shape the dispatch
|
|
35
|
+
* hook enforces (a `.fray/<slug>.md`-backed tag at the top of the prompt). Returns the
|
|
36
|
+
* normalized slug (no `.fray/` prefix, no `.md` suffix) or null for an untagged one-shot.
|
|
37
|
+
* @param {string} prompt
|
|
38
|
+
* @returns {string|null}
|
|
39
|
+
*/
|
|
40
|
+
export function threadFromPrompt(prompt) {
|
|
41
|
+
if (typeof prompt !== 'string') return null;
|
|
42
|
+
const m = prompt.match(/^THREAD:\s*([\w./-]+)/m);
|
|
43
|
+
return m ? m[1].replace(/^\.fray\//, '').replace(/\.md$/, '') : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Record one binding. Called by the `agent-bind` PostToolUse hook. No-ops (returns false)
|
|
48
|
+
* unless BOTH an agent id and a thread slug are present — an untagged one-shot has no thread
|
|
49
|
+
* to bind, so nothing is written. Fail-open: a write error never throws.
|
|
50
|
+
* @param {string} projectDir
|
|
51
|
+
* @param {{ agentId?: string|null, thread?: string|null, label?: string|null, session?: string|null }} b
|
|
52
|
+
* @returns {boolean} whether a line was written
|
|
53
|
+
*/
|
|
54
|
+
export function recordBinding(projectDir, { agentId, thread, label, session }) {
|
|
55
|
+
try {
|
|
56
|
+
if (!agentId || !thread) return false;
|
|
57
|
+
appendFileSync(
|
|
58
|
+
bindingsPath(projectDir),
|
|
59
|
+
JSON.stringify({
|
|
60
|
+
ts: new Date().toISOString(),
|
|
61
|
+
agent_id: String(agentId),
|
|
62
|
+
thread: String(thread),
|
|
63
|
+
label: label ? String(label) : null,
|
|
64
|
+
session: session ? String(session) : null,
|
|
65
|
+
}) + '\n',
|
|
66
|
+
);
|
|
67
|
+
return true;
|
|
68
|
+
} catch {
|
|
69
|
+
return false; // fail-open — a binding write must never block a dispatch
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Read every recorded binding (newest last). Fail-open: any error → [].
|
|
75
|
+
* @param {string} projectDir
|
|
76
|
+
* @returns {{ts?:string, agent_id:string, thread:string, label:string|null, session:string|null}[]}
|
|
77
|
+
*/
|
|
78
|
+
export function readBindings(projectDir) {
|
|
79
|
+
/** @type {{ts?:string, agent_id:string, thread:string, label:string|null, session:string|null}[]} */
|
|
80
|
+
const out = [];
|
|
81
|
+
try {
|
|
82
|
+
const raw = readFileSync(bindingsPath(projectDir), 'utf8');
|
|
83
|
+
for (const line of raw.split('\n')) {
|
|
84
|
+
if (!line.trim()) continue;
|
|
85
|
+
try {
|
|
86
|
+
const r = JSON.parse(line);
|
|
87
|
+
if (r && r.agent_id && r.thread) out.push(r);
|
|
88
|
+
} catch {
|
|
89
|
+
/* skip a malformed line */
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
/* no file → no bindings */
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Group bindings by thread slug into the `{id, label}[]` shape the board + liveness hook
|
|
100
|
+
* consume — the AUTOMATIC replacement for parsing `agents:` frontmatter. Dedupes by
|
|
101
|
+
* agent_id (one entry per instance), keeping the LATEST label seen for that id.
|
|
102
|
+
* @param {string} projectDir
|
|
103
|
+
* @returns {Map<string, {id:string, label:string|null}[]>}
|
|
104
|
+
*/
|
|
105
|
+
export function bindingsByThread(projectDir) {
|
|
106
|
+
/** @type {Map<string, Map<string, string|null>>} thread → (agentId → label) */
|
|
107
|
+
const byThread = new Map();
|
|
108
|
+
for (const b of readBindings(projectDir)) {
|
|
109
|
+
let m = byThread.get(b.thread);
|
|
110
|
+
if (!m) byThread.set(b.thread, (m = new Map()));
|
|
111
|
+
m.set(b.agent_id, b.label ?? m.get(b.agent_id) ?? null); // latest non-null label wins
|
|
112
|
+
}
|
|
113
|
+
/** @type {Map<string, {id:string, label:string|null}[]>} */
|
|
114
|
+
const out = new Map();
|
|
115
|
+
for (const [thread, m] of byThread) {
|
|
116
|
+
out.set(thread, [...m].map(([id, label]) => ({ id, label })));
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The thread a given agent id serves (latest binding wins), or null. Used to name the
|
|
123
|
+
* thread when a rest/return is surfaced, so reconciliation points at the right file.
|
|
124
|
+
* @param {string} projectDir
|
|
125
|
+
* @param {string|null|undefined} agentId
|
|
126
|
+
* @returns {string|null}
|
|
127
|
+
*/
|
|
128
|
+
export function threadForAgent(projectDir, agentId) {
|
|
129
|
+
if (!agentId) return null;
|
|
130
|
+
let found = null;
|
|
131
|
+
for (const b of readBindings(projectDir)) if (b.agent_id === agentId) found = b.thread;
|
|
132
|
+
return found;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Per thread, the SINGLE NEWEST binding — the agent CURRENTLY serving the thread.
|
|
137
|
+
*
|
|
138
|
+
* Liveness must key on this, NOT on every agent ever bound: a thread is frequently
|
|
139
|
+
* re-dispatched (an agent dies/rests and a FRESH agent picks the same thread up), and a
|
|
140
|
+
* SUPERSEDED older agent staying quiet is EXPECTED, never a drop. Flagging the old agent
|
|
141
|
+
* after a new one took over was the #1 false positive (e.g. dead `ac067e883` kept firing on
|
|
142
|
+
* `gvs-warning-silent` long after live `a56d5f1` replaced it). Keying on the newest binding
|
|
143
|
+
* makes that class structurally impossible.
|
|
144
|
+
*
|
|
145
|
+
* Newest = max binding `ts` (ISO), with append-order (file index) as the tiebreaker — so an
|
|
146
|
+
* unparseable/absent ts still resolves deterministically to the later-written line.
|
|
147
|
+
* @param {string} projectDir
|
|
148
|
+
* @returns {Map<string, {id:string, label:string|null, ts:string|null}>}
|
|
149
|
+
*/
|
|
150
|
+
export function newestBindingByThread(projectDir) {
|
|
151
|
+
/** @type {Map<string, {id:string, label:string|null, ts:string|null, _ms:number, _idx:number}>} */
|
|
152
|
+
const out = new Map();
|
|
153
|
+
const bindings = readBindings(projectDir);
|
|
154
|
+
for (let idx = 0; idx < bindings.length; idx++) {
|
|
155
|
+
const b = bindings[idx];
|
|
156
|
+
const ms = Date.parse(b.ts ?? '');
|
|
157
|
+
const key = Number.isFinite(ms) ? ms : -Infinity;
|
|
158
|
+
const cur = out.get(b.thread);
|
|
159
|
+
if (!cur || key > cur._ms || (key === cur._ms && idx > cur._idx)) {
|
|
160
|
+
out.set(b.thread, { id: b.agent_id, label: b.label ?? null, ts: b.ts ?? null, _ms: key, _idx: idx });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** @type {Map<string, {id:string, label:string|null, ts:string|null}>} */
|
|
164
|
+
const clean = new Map();
|
|
165
|
+
for (const [thread, v] of out) clean.set(thread, { id: v.id, label: v.label, ts: v.ts });
|
|
166
|
+
return clean;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Threads with a PR landing in flight via the merge cascade (`.fray/merge-queue.jsonl`).
|
|
171
|
+
*
|
|
172
|
+
* Such a thread is LEGITIMATELY `active` while its PR merges — the agent that produced the
|
|
173
|
+
* work has finished and its output is reconciled; the thread stays active purely for the
|
|
174
|
+
* DOWNSTREAM merge, not because an agent is stuck. Suppressing liveness for these kills the
|
|
175
|
+
* second false-positive class (a completed agent flagged because its thread is mid-merge).
|
|
176
|
+
* Fail-open: no file / unparseable → empty set (suppress nothing).
|
|
177
|
+
* @param {string} projectDir
|
|
178
|
+
* @returns {Set<string>}
|
|
179
|
+
*/
|
|
180
|
+
export function downstreamThreads(projectDir) {
|
|
181
|
+
/** @type {Set<string>} */
|
|
182
|
+
const out = new Set();
|
|
183
|
+
try {
|
|
184
|
+
const raw = readFileSync(join(projectDir, '.fray', 'merge-queue.jsonl'), 'utf8');
|
|
185
|
+
for (const line of raw.split('\n')) {
|
|
186
|
+
if (!line.trim()) continue;
|
|
187
|
+
try {
|
|
188
|
+
const r = JSON.parse(line);
|
|
189
|
+
if (r && r.thread) out.add(String(r.thread));
|
|
190
|
+
} catch {
|
|
191
|
+
/* skip a malformed line */
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
} catch {
|
|
195
|
+
/* no merge-queue → nothing downstream */
|
|
196
|
+
}
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* agent_ids that have recorded at least one SubagentStop (rest) in `.fray/.rested-agents.jsonl`
|
|
202
|
+
* — i.e. reached a clean stopping point at least once (ended a turn).
|
|
203
|
+
*
|
|
204
|
+
* This is the signal that separates a TERMINATED/parked agent from one still grinding inside a
|
|
205
|
+
* single long tool call. An agent mid-build (one 40-min `cargo build`, no rest yet) has stale
|
|
206
|
+
* output but is ALIVE — and must never be called "dropped". Combined with stale output, a rest
|
|
207
|
+
* record means the agent ended its turn and has been quiet since: the conservative "no live
|
|
208
|
+
* agent" condition. Fail-open: no file / unparseable → empty set.
|
|
209
|
+
* @param {string} projectDir
|
|
210
|
+
* @returns {Set<string>}
|
|
211
|
+
*/
|
|
212
|
+
export function restedAgentIds(projectDir) {
|
|
213
|
+
/** @type {Set<string>} */
|
|
214
|
+
const out = new Set();
|
|
215
|
+
try {
|
|
216
|
+
const raw = readFileSync(join(projectDir, '.fray', '.rested-agents.jsonl'), 'utf8');
|
|
217
|
+
for (const line of raw.split('\n')) {
|
|
218
|
+
if (!line.trim()) continue;
|
|
219
|
+
try {
|
|
220
|
+
const r = JSON.parse(line);
|
|
221
|
+
if (r && r.agent_id) out.add(String(r.agent_id));
|
|
222
|
+
} catch {
|
|
223
|
+
/* skip a malformed line */
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
} catch {
|
|
227
|
+
/* no rest log → no rested agents */
|
|
228
|
+
}
|
|
229
|
+
return out;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Count sub-agent rests recorded strictly after `sinceMs` that the ORCHESTRATOR actually
|
|
234
|
+
* dispatched — i.e. THREAD-BOUND agents — and have NOT yet been surfaced. This is what the
|
|
235
|
+
* Stop-hook rest guard nags on.
|
|
236
|
+
*
|
|
237
|
+
* THE FILTER (the fix for the over-aggressive nag): a rest counts ONLY if its `agent_id`
|
|
238
|
+
* appears in the bindings ledger (`.agent-bindings.jsonl`), which the agent-bind hook writes
|
|
239
|
+
* ONLY for a `THREAD:`-tagged orchestrator dispatch. The real predicate is "has a THREAD
|
|
240
|
+
* binding"; everything without one is excluded — none of it is orchestrator-facing reconcile work:
|
|
241
|
+
* - NESTED / worker-spawned sub-agents (e.g. a landing agent's own fresh-context self-review
|
|
242
|
+
* sub-agents): no THREAD tag → no binding → unbound. Their results fold into the parent's
|
|
243
|
+
* report; they are the worker's internal business. This is the class that drove the nag.
|
|
244
|
+
* - UNTAGGED orchestrator one-shots: also unbound (no thread), and they have no `.fray/<slug>.md`
|
|
245
|
+
* to fold into — the result returns as the tool result, with nothing to reconcile.
|
|
246
|
+
* - ANON rests (no `agent_id`): inherently unbindable, so never the orchestrator's to reconcile.
|
|
247
|
+
* The bound, THREAD-owned agent that genuinely rested and hasn't been folded STILL counts — the
|
|
248
|
+
* legitimate signal is preserved, just made precise.
|
|
249
|
+
*
|
|
250
|
+
* DEDUPE: an `agent_id` already in `surfacedAgents` is skipped, so a resuming bound agent's
|
|
251
|
+
* repeat rests don't re-nag; only genuinely-new bound ids count. Returns the count plus the
|
|
252
|
+
* DISTINCT new bound agent-ids and their thread slugs, so the caller can nag with a real number,
|
|
253
|
+
* grow the seen-set, and name the threads. Fail-open: any error → zero count.
|
|
254
|
+
* @param {string} projectDir
|
|
255
|
+
* @param {number} sinceMs
|
|
256
|
+
* @param {string[]} [surfacedAgents]
|
|
257
|
+
* @returns {{ count: number, agents: string[], threads: string[] }}
|
|
258
|
+
*/
|
|
259
|
+
export function newBoundRestsSince(projectDir, sinceMs, surfacedAgents = []) {
|
|
260
|
+
const seen = new Set(surfacedAgents);
|
|
261
|
+
const bound = new Set(readBindings(projectDir).map((b) => b.agent_id));
|
|
262
|
+
/** @type {Set<string>} */
|
|
263
|
+
const newAgents = new Set();
|
|
264
|
+
/** @type {Set<string>} */
|
|
265
|
+
const threads = new Set();
|
|
266
|
+
try {
|
|
267
|
+
const raw = readFileSync(join(projectDir, '.fray', '.rested-agents.jsonl'), 'utf8');
|
|
268
|
+
for (const line of raw.split('\n')) {
|
|
269
|
+
if (!line.trim()) continue;
|
|
270
|
+
try {
|
|
271
|
+
const rec = JSON.parse(line);
|
|
272
|
+
const ts = Date.parse(rec.ts);
|
|
273
|
+
if (!Number.isFinite(ts) || ts <= sinceMs) continue;
|
|
274
|
+
const id = rec.agent_id;
|
|
275
|
+
// bound-only: skip anon (no id), already-surfaced, and unbound (nested/worker-spawned).
|
|
276
|
+
if (!id || seen.has(id) || !bound.has(id)) continue;
|
|
277
|
+
newAgents.add(id);
|
|
278
|
+
if (rec.thread) threads.add(String(rec.thread));
|
|
279
|
+
} catch {
|
|
280
|
+
/* skip a malformed line */
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
} catch {
|
|
284
|
+
/* no rest log → no rests */
|
|
285
|
+
}
|
|
286
|
+
return { count: newAgents.size, agents: [...newAgents], threads: [...threads] };
|
|
287
|
+
}
|