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
|
@@ -0,0 +1,923 @@
|
|
|
1
|
+
# cc-worker — design decisions
|
|
2
|
+
|
|
3
|
+
The **fray** worker-side plugin (dir `cc-worker/`, manifest `name: "fray"` since 2026-07-08) is
|
|
4
|
+
consumed by fray-ui worker sessions: one interactive top-level
|
|
5
|
+
`claude` per `.fray/` thread, loaded via `claude --plugin-dir <repo>/cc-worker`. Each session is a
|
|
6
|
+
**worker bound to ONE thread** (slug in env `FRAY_UI_THREAD` + a `THREAD:` line in its prompt). The
|
|
7
|
+
human + the fray-ui app are the orchestrator; the worker just drives its one thread. This records
|
|
8
|
+
what was ported from the orchestrator `cc/` plugin, what was dropped, and why.
|
|
9
|
+
|
|
10
|
+
## Shared source, bundled runtime closure
|
|
11
|
+
|
|
12
|
+
- **`scripts/fray/config.mjs` and `scripts/fray/agent-bindings.mjs` are THIN SHIMS** that
|
|
13
|
+
`export *` from `../../../cc/scripts/fray/*.mjs`. cc-worker never copies config/vocab/binding
|
|
14
|
+
logic — there is exactly one source of truth (cc's). This assumes cc is a sibling dir (`../../cc/`
|
|
15
|
+
from the plugin root), the same assumption fray-ui's server makes (`ARCHITECTURE.md`: it imports
|
|
16
|
+
the board logic from `../../cc/scripts/fray/*.mjs`).
|
|
17
|
+
- **`bin/fray` and `bin/fray-update`** are cc's exact shim pattern, resolving cc's real scripts at
|
|
18
|
+
`../../cc/scripts/fray/{index,thread-update}.mjs` relative to the bin file (cwd-independent). They
|
|
19
|
+
land on the worker's Bash PATH the way cc's do. `fray-update` is the worker's primary tool for
|
|
20
|
+
owning its one thread file; `fray` lets it read/validate the board.
|
|
21
|
+
- **Portable artifact rule:** `packages/cli/src/artifacts.ts` copies the exact sibling
|
|
22
|
+
`cc/scripts/fray/` module closure to `runtime/cc/scripts/fray/`, beside `runtime/cc-worker/`.
|
|
23
|
+
The existing shims therefore resolve inside an immutable artifact when the source checkout is gone;
|
|
24
|
+
both the worker and cc closure are hashed in `manifest.runtimeFiles` and required at read time.
|
|
25
|
+
- **`agents/*.md`** are copied UNCHANGED from `cc/agents/` (16 profiles) — a worker dispatches its
|
|
26
|
+
own helpers at the same model/effort cells as the orchestrator.
|
|
27
|
+
|
|
28
|
+
## Hooks ported (all gated on `FRAY_UI_THREAD` — inert if the plugin is loaded anywhere else)
|
|
29
|
+
|
|
30
|
+
| Hook | Event | Ported from | What it does for a worker |
|
|
31
|
+
| --- | --- | --- | --- |
|
|
32
|
+
| `session-seed.mjs` | SessionStart (startup/resume/clear/compact) | cc `session-seed.mjs` | Injects the single-thread worker contract + the bound slug + its file path; re-grounds on compact. Also writes cc's `off` sentinel defensively (see interplay). |
|
|
33
|
+
| `precompact-instructions.mjs` | PreCompact (auto/manual) | new — no cc equivalent | Steers WHAT SURVIVES compaction: emits an editorial brief asking the summarizer to preserve the high-level approach (plan, alternatives rejected, rationale) at high fidelity, plus a closing "Re-grounding before continuing:" section naming the scratchpad path and the files to re-read. **PLAIN STDOUT is the channel** — cc's usual `hookSpecificOutput` JSON would be handed to the summarizer as its literal instructions. Skips sub-agent contexts (the derived scratchpad path is only guaranteed for the top-level worker). |
|
|
34
|
+
| `scratchpad.mjs` | SessionStart (startup/resume/clear/compact) + PreCompact (auto/manual) + UserPromptSubmit + PostToolUse | new — no cc equivalent | Keeps the ONE per-thread scratchpad (`.fray/threads/<sid>/scratch.md`) written and re-grounded across compaction. `--mode=session-start` injects the pad's HEAD back verbatim on the context-losing sources (compact/resume/clear) plus a re-read pointer, and teaches the contract when the pad is still an unwritten skeleton; `--mode=precompact` (**plain stdout**) hands the head to the summarizer; `--mode=nudge` fires on BOTH UserPromptSubmit and PostToolUse (mid-turn — the one that matters for long autonomous turns) once context has grown past `STALE_TOKENS` since the last write. Both nudge channels share one state file, so mid-turn firing does not multiply reminders. NOT gated on `FRAY_UI_THREAD`; `--via=project` marks the repo-local registration and defers to this one inside a fray worker. |
|
|
35
|
+
| `agent-dispatch.mjs` | PreToolUse(Agent) | cc `agent-dispatch.mjs` | Enforces `run_in_background:true`, strips `name`/`team_name`, appends a worker-flavored orchestration epilogue. |
|
|
36
|
+
| `bash-background.mjs` | PreToolUse(Bash) | new | Denies a local shell job that escapes through `&` without a later `wait` or EXIT trap. Shell job control bypasses Claude's task registry, so Fray cannot track or wake it; self-contained probe concurrency remains allowed. |
|
|
37
|
+
| `agent-bind.mjs` | PostToolUse(Agent) | cc `agent-bind.mjs` (verbatim behavior) | Records `agentId → thread` into `.fray/.agent-bindings.jsonl` in cc's exact format, so a worker's THREAD-tagged helper renders on the fray-ui board's per-thread liveness. |
|
|
38
|
+
| `stop-flush.mjs` | Stop | cc `fray-stop-reminder.mjs` (dirty-check idea only) | If the worker's ONE thread file wasn't edited since the last rest, nudges it to flush its state (mtime dirty-check, cooldown-limited, least-alarming `additionalContext` channel). |
|
|
39
|
+
|
|
40
|
+
## cc hooks DROPPED — one line each on why
|
|
41
|
+
|
|
42
|
+
- **`fray-reminder.mjs` (UserPromptSubmit per-turn pulse)** — DROPPED. It nags the *orchestrator*
|
|
43
|
+
about the whole board (pending-by-status, reconcile-stale, un-drained follow-ups, revalidate-due).
|
|
44
|
+
A worker owns one thread and does not orchestrate a board; a per-turn board pulse is pure noise.
|
|
45
|
+
- **`fray-stop-reminder.mjs` (board-wide Stop reconcile + pop-one decision queue)** — DROPPED as-is;
|
|
46
|
+
only its per-thread dirty-check idea is reused in `stop-flush.mjs`. The rest (reconcile every
|
|
47
|
+
rested agent, pop the next human-blocked thread and present it) is orchestrator decision-queue work
|
|
48
|
+
the fray-ui app + human own, not the worker.
|
|
49
|
+
- **`fray-notify-surface.mjs` (Stop) + `fray-notify` bin/`notify.mjs`** — DROPPED. The durable
|
|
50
|
+
WIN/DECISION/BLOCKER notification queue is an orchestrator surfacing channel; in fray-ui the UI
|
|
51
|
+
surfaces "awaiting you" from thread `status: blocked` + `status_text` directly, so the worker needs
|
|
52
|
+
no separate notify queue.
|
|
53
|
+
- **`fray-subagent-rest.mjs` (SubagentStop recorder) + `fray-rest-guard.mjs` (SubagentStop guard)**
|
|
54
|
+
— DROPPED. These feed the orchestrator's board-wide "reconcile every rested dispatched agent"
|
|
55
|
+
machinery (`.rested-agents.jsonl`, the `.dispatch-count` gate). A worker actively collects its own
|
|
56
|
+
handful of helpers before resting (contract in SKILL.md); it does not need the board-scale
|
|
57
|
+
rest-reconciliation guard. The worker-facing half of the rest-guard's lesson (run long ops inline,
|
|
58
|
+
don't rest on a waiter) is carried in the dispatch epilogue instead.
|
|
59
|
+
- **`fray-thread-edit-steer.mjs` (PostToolUse Edit/Write)** — DROPPED. It's an orchestrator
|
|
60
|
+
convenience that steers an in-flight agent when the orchestrator hand-edits a thread; a worker edits
|
|
61
|
+
its OWN thread and dispatches its OWN helpers, so there's nothing to cross-steer.
|
|
62
|
+
- **`session-end.mjs` (SessionEnd heartbeat clear)** — DROPPED. It clears the session-ownership
|
|
63
|
+
HEARTBEAT so a dead orchestrator's threads orphan. Workers don't participate in cc's multi-session
|
|
64
|
+
ownership model (no `owner_session` claims, no heartbeat) — the fray-ui app tracks which session
|
|
65
|
+
drives which thread — so there's no heartbeat to clear.
|
|
66
|
+
- **The `.dispatch-ledger.jsonl` write + THREAD-existence DENY gate + `.dispatch-count` bump**
|
|
67
|
+
(inside cc's `agent-dispatch.mjs`) — DROPPED from the worker's PreToolUse. The ledger is a
|
|
68
|
+
compaction-durable orchestrator record of which-agent-serves-which-thread across MANY threads; the
|
|
69
|
+
THREAD-existence gate enforces the orchestrator's "file the thread before dispatching" discipline;
|
|
70
|
+
the count only gates the (dropped) SubagentStop recorder. A worker owns exactly one, already-created
|
|
71
|
+
thread and its helpers own no thread, so none apply. The `.agent-bindings.jsonl` write — the piece
|
|
72
|
+
that actually renders sub-agent liveness on the board — IS kept (via `agent-bind.mjs`).
|
|
73
|
+
|
|
74
|
+
## Interplay with the orchestrator `cc` plugin (double-hook analysis)
|
|
75
|
+
|
|
76
|
+
**Question:** if the user has `cc` (the fray orchestrator plugin) enabled globally AND a fray-ui
|
|
77
|
+
worker session starts in the same repo, do both plugins' hooks fire (double-hook)?
|
|
78
|
+
|
|
79
|
+
**Finding — NO, not by default. cc is inert in a fresh worker session.** cc's every hook is gated on
|
|
80
|
+
`frayActive(projectDir, sessionId)` (`cc/scripts/fray/config.mjs`). That gate is **opt-IN per
|
|
81
|
+
session**: it requires `.fray/` to exist AND a per-session sentinel at
|
|
82
|
+
`.fray/.session-state/<session_id>` containing `on` (written by `fray on` / the orchestrator fray
|
|
83
|
+
skill's Step 0). With no sentinel it returns **false** — the documented default:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
// config.mjs frayActive(), final line:
|
|
87
|
+
return false; // DEFAULT: OPT-IN — dormant until this session runs `fray on`
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
A freshly-spawned fray-ui worker has a distinct `CLAUDE_CODE_SESSION_ID` and never runs `fray on`, so
|
|
91
|
+
cc's `frayActive()` is false for it → **all cc hooks are silent no-ops in the worker.** Meanwhile
|
|
92
|
+
cc-worker gates on the orthogonal `FRAY_UI_THREAD` env, so the two plugins key off different signals
|
|
93
|
+
and do not both activate. No double-hook by default.
|
|
94
|
+
|
|
95
|
+
**Residual risk:** if, inside a worker session, someone runs `fray on` or loads the orchestrator
|
|
96
|
+
`fray` skill (whose Step 0 runs `fray on`), cc's `frayActive()` flips true AND cc-worker is active →
|
|
97
|
+
both fire (you'd get orchestrator board nags inside a worker — wrong).
|
|
98
|
+
|
|
99
|
+
**Mitigation implemented (cheap + safe + reversible):** `session-seed.mjs` writes cc's OWN per-session
|
|
100
|
+
`off` sentinel for the worker's session id via cc's shared `setSessionOverride(dir, sid, 'off')` on
|
|
101
|
+
every worker SessionStart. `frayActive()` short-circuits to false on an `off` override
|
|
102
|
+
(`if (override === 'off') return false`), so cc is **guaranteed dormant** in a worker session even if
|
|
103
|
+
something later attempts to activate it — unless the human deliberately runs `fray on` afterward
|
|
104
|
+
(which overwrites the sentinel), the explicit "I want this session to orchestrate too" escape hatch.
|
|
105
|
+
This uses cc's own public API (identical to what `fray off` does), touches only gitignored runtime
|
|
106
|
+
state keyed on this worker's session id, and does NOT disable cc-worker (which gates on
|
|
107
|
+
`FRAY_UI_THREAD`, not the sentinel). It's the safest cheap option: it neutralizes the other plugin
|
|
108
|
+
without a UI-side plugin-disable flag (Claude Code has no per-invocation "disable plugin X" flag), and
|
|
109
|
+
the worker SKILL.md additionally tells the worker not to run `fray on` / load the orchestrator skill.
|
|
110
|
+
|
|
111
|
+
## plugin.json
|
|
112
|
+
|
|
113
|
+
`name: "fray"` (renamed from `fray-worker` on 2026-07-08 — see the follow-up note), `version: "0.1.2"`,
|
|
114
|
+
`license: "MIT"`. Hooks are auto-discovered from `hooks/hooks.json` (same as cc — plugin.json carries
|
|
115
|
+
no explicit hooks reference); every hook command is wired via `${CLAUDE_PLUGIN_ROOT}`.
|
|
116
|
+
|
|
117
|
+
## Claude settings-source isolation — deliberately deferred
|
|
118
|
+
|
|
119
|
+
The portable worker launch passes its per-session plugin with `--plugin-dir` on both spawn and
|
|
120
|
+
resume, and clears only `CLAUDE_CODE_SUBAGENT_MODEL` plus `CLAUDE_CODE_EFFORT_LEVEL`: those inherited
|
|
121
|
+
variables would silently defeat Fray's selected worker/profile. It deliberately does **not** replace
|
|
122
|
+
`HOME`, `CLAUDE_CONFIG_DIR`, or Claude's settings sources. Doing so would also change authentication,
|
|
123
|
+
user-approved permissions, MCP configuration, and global plugin behavior; that is a product-policy
|
|
124
|
+
decision, not an artifact-portability implementation detail. A future isolation policy must specify
|
|
125
|
+
which settings/auth surfaces are preserved before adding `--settings`, a config-home override, or a
|
|
126
|
+
global-plugin disable mechanism.
|
|
127
|
+
|
|
128
|
+
## 2026-07-02: Stop hook removed
|
|
129
|
+
stop-flush.mjs is no longer wired (script kept for reference). User call: under fray-ui the
|
|
130
|
+
tailer/board already surface worker state live, and the block-until-file-edited nag forced even
|
|
131
|
+
trivial workers into Read/Edit dances that render as noise in the chat UI. Thread-file discipline
|
|
132
|
+
remains a prompt-level contract (worker system prompt + SKILL), not a hook-enforced gate.
|
|
133
|
+
|
|
134
|
+
## 2026-07-08: Developer-experience port — doctrine, thread-type presets, dialectic
|
|
135
|
+
Goal: carry the old cc/ plugin's developer experience (minus the orchestrator machinery) into
|
|
136
|
+
fray-ui workers. What landed:
|
|
137
|
+
- **`skills/dialectic/SKILL.md`** — ported from `cc/skills/dialectic/` (self-contained dueling-
|
|
138
|
+
sub-agents methodology; no board/reconcile dependency). Its model-tier references use this
|
|
139
|
+
plugin's namespace (`fray:opus-high`, etc. — see the naming note below).
|
|
140
|
+
- **`skills/worker/SKILL.md`** — added two sections: "Choosing a helper's model + effort" (the
|
|
141
|
+
full Haiku/Sonnet/Opus tiering doctrine + effort ladder + bias-to-Opus corollary, adapted from
|
|
142
|
+
`cc/skills/fray/SKILL.md:218-234` for a worker dispatching its OWN helpers) and "Thread-type
|
|
143
|
+
presets" (research / audit / implementation / planning — deliverable shape + "done" bar each,
|
|
144
|
+
derived from that skill's `:92-150`/`:242-317`/`:463` framing). Also added a status-field
|
|
145
|
+
discipline section (later split into `activity` + `status_text` — see the follow-up) and an
|
|
146
|
+
"awaiting your OWN sub-agent is NOT blocked" clarification.
|
|
147
|
+
- **`ui/WORKER_PROMPT.md`** (fray-ui system prompt, not in this plugin) — carries the terse version
|
|
148
|
+
of the same three: a "Status discipline" block, the model/effort doctrine in the Sub-agents
|
|
149
|
+
section, and a "Thread types" section. This is the maintainer's explicit ask that the preset
|
|
150
|
+
vocabulary ride in the SYSTEM prompt passed to every worker. No dispatch.ts change, so no fray-ui
|
|
151
|
+
server restart is needed — `loadWorkerPrompt()` re-reads the file per dispatch and each spawned
|
|
152
|
+
`claude` rescans this plugin dir fresh.
|
|
153
|
+
|
|
154
|
+
### Naming: subagent_type is `fray:<model>-<effort>` (plugin renamed to `fray` on 2026-07-08)
|
|
155
|
+
The plugin's manifest `name` (`.claude-plugin/plugin.json`) drives the subagent-type NAMESPACE, and
|
|
156
|
+
each agent file's frontmatter `name` drives the agent name — verified empirically with a throwaway
|
|
157
|
+
plugin (`name:"fray"` + agent `name:opus-high` → `fray:opus-high`); the plugin DIRECTORY name does
|
|
158
|
+
not matter. So the profiles dispatch as `fray:opus-high` / `fray:sonnet-medium` / `fray:haiku`, and
|
|
159
|
+
the skills as `fray:worker` + `fray:dialectic`. A BARE name (`opus-high`, `haiku`) does NOT resolve
|
|
160
|
+
— the Agent tool returns "Agent type '…' not found. Available agents: … fray:haiku …" — so every
|
|
161
|
+
doctrine/dialectic reference uses the `fray:`-prefixed form (confirmed end-to-end: `subagent_type:
|
|
162
|
+
fray:haiku` returns PONG). See the follow-up note below for why the name is `fray`, not `fray-worker`.
|
|
163
|
+
|
|
164
|
+
## 2026-07-08 (follow-up): status split, validation hook, `fray:` namespace rename
|
|
165
|
+
Three maintainer refinements landed on top of the port:
|
|
166
|
+
|
|
167
|
+
**1. `status_text` split into `activity` + `status_text`.** The single overloaded field became two:
|
|
168
|
+
`activity` = the form-constrained LIVE label the UI renders beside the spinner (single line, ≤100
|
|
169
|
+
chars, present-progressive gerund); `status_text` = the classic 1–2-sentence human gloss that also
|
|
170
|
+
doubles as THE ask on a human-`blocked` thread (queue cards headline it; no gerund constraint). The
|
|
171
|
+
gerund/≤100 discipline moved OFF `status_text` and ONTO `activity` in `ui/WORKER_PROMPT.md` +
|
|
172
|
+
`skills/worker/SKILL.md`. (UI-side `activity` plumbing/rendering — board JSON → ThreadView → listing
|
|
173
|
+
row — is a SIBLING agent's scope; not touched here beyond `ui/WORKER_PROMPT.md`.)
|
|
174
|
+
|
|
175
|
+
**2. New PostToolUse validation hook — `hooks/thread-frontmatter-validate.mjs`** (matcher
|
|
176
|
+
`Edit|Write|MultiEdit`, wired in `hooks/hooks.json`). Gated on FRAY_UI_THREAD + a top-level
|
|
177
|
+
`.fray/<slug>.md` path (dotfiles + `.findings/` sidecars skipped via the vendored `threadSlug`). On
|
|
178
|
+
every thread-file edit it re-reads + validates the frontmatter and, on a HARD violation, returns
|
|
179
|
+
`{"decision":"block","reason":…}` (PostToolUse block — the worker sees the quoted reason and
|
|
180
|
+
re-edits); soft issues warn via `systemMessage`; a clean edit is silent; ANY error fails OPEN
|
|
181
|
+
(exit 0). Rules (mirroring cc's board validator `index.mjs:302-335`): required `title`+`status`;
|
|
182
|
+
`status` ∈ the vocab (legacy aliases warn, not block); `activity` single-line/≤100/gerund-heuristic
|
|
183
|
+
(first word ends in "ing"); `blocked` ⇒ at most ONE of `blocking_threads`/`revalidate_at`, and
|
|
184
|
+
human-blocked (neither) ⇒ `status_text` required; `status_text` >240 chars warns. SELF-CONTAINED: it
|
|
185
|
+
VENDORS minimal copies of cc's frontmatter parser (`index.mjs:82`), path matcher
|
|
186
|
+
(`fray-thread-edit-steer.mjs:67`), and vocab (`config.mjs:291`/`:301`) — cc-worker must not import cc
|
|
187
|
+
at runtime. Verified: 10 direct unit cases (pass/block/warn/inert) + a real headless worker whose bad
|
|
188
|
+
Write was blocked with the exact quoted reason.
|
|
189
|
+
|
|
190
|
+
**3. Plugin renamed `fray-worker` → `fray`; worker skill renamed `fray-worker` → `worker`.** The
|
|
191
|
+
maintainer disliked the old worker-prefixed dispatch namespace. Renamed the manifest `name` to `fray` and the 16 agent
|
|
192
|
+
files + their frontmatter from `fray-<model>-<effort>` to `<model>-<effort>` (bare `haiku`), so
|
|
193
|
+
dispatches read `fray:opus-high` etc. Renamed the worker skill dir + frontmatter `fray-worker` →
|
|
194
|
+
`worker` (giving `fray:worker`, not the stutter `fray:fray-worker`). The plugin DIRECTORY stays
|
|
195
|
+
`cc-worker/` (dispatch.ts points at that path — unchanged, no server restart). Every reference in
|
|
196
|
+
`ui/WORKER_PROMPT.md`, both skills, the hooks, and this file was updated; a grep for the old prefix token is clean.
|
|
197
|
+
|
|
198
|
+
**Accepted name collision.** The old GLOBAL orchestrator plugin (`cc/`) is ALSO named `fray`. This is
|
|
199
|
+
accepted as harmless: `cc/` is disabled in `~/.claude/settings.json` (`"fray@fray": false`) and, even
|
|
200
|
+
when enabled, loads only in ORCHESTRATOR sessions (a different process class) — never in a fray-ui
|
|
201
|
+
worker, which loads ONLY this plugin via `--plugin-dir cc-worker`. Verified: the headless worker env
|
|
202
|
+
(same settings.json) registers a clean `fray:*` set with no `cc/` agents present. If a user ever
|
|
203
|
+
loaded BOTH plugins in one session, Claude Code would namespace-collide two `fray` plugins — but that
|
|
204
|
+
combination does not occur on the worker path.
|
|
205
|
+
|
|
206
|
+
## 2026-07-08 (campaign): `needs-human` first-class status + interactive-prompt deny hooks
|
|
207
|
+
Part of the board-wide redesign (owned jointly with the fray-ui UI half): `needs-human` becomes a
|
|
208
|
+
first-class fray status — the declared "awaiting a human" state and the queue's definition — while
|
|
209
|
+
`blocked` narrows to MACHINE-waits only. The vocab/parser/validator changes live in the shared cc/
|
|
210
|
+
scripts (`config.mjs` STATUS/STATUS_ALIASES + new `effectiveStatus()`, `index.mjs`, `decisions.mjs`,
|
|
211
|
+
`statusline-fray.mjs`, `thread-update.mjs`, `fray-reminder.mjs`) — consumed by fray-ui's readBoard
|
|
212
|
+
shell-out, so they take effect on the next board rebuild with NO server restart. cc-worker-side:
|
|
213
|
+
|
|
214
|
+
- **`hooks/thread-frontmatter-validate.mjs`** — vendored vocab bumped: `needs-human` is canonical,
|
|
215
|
+
`needs-decision` aliases to it. New rules: a `needs-human` thread (incl. a legacy `blocked` with no
|
|
216
|
+
machine field, which reads as needs-human via the inlined `effectiveStatus`) REQUIRES a
|
|
217
|
+
`status_text` (hard BLOCK); a machine-`blocked` thread with no mechanism field is a WARN suggesting
|
|
218
|
+
needs-human (not a block — legacy tolerance); >1 mechanism stays a block.
|
|
219
|
+
- **`skills/worker/SKILL.md` + `ui/WORKER_PROMPT.md` + `hooks/session-seed.mjs`** — the worker status
|
|
220
|
+
guidance rewritten to the new contract: an ask OR a result needing review → `status: needs-human`
|
|
221
|
+
with a `status_text` ask ("Review: …"); `blocked` is machine-only; `done` means NOTHING is left for
|
|
222
|
+
the human (Mark-as-done is the human's acknowledgment — never jump straight to `done` when review
|
|
223
|
+
is pending).
|
|
224
|
+
- **`hooks/deny-ask.mjs`** (existing, PreToolUse `AskUserQuestion`) — deny reason re-pointed at
|
|
225
|
+
`--status needs-human`. `AskUserQuestion` is a real tool → cleanly deniable via PreToolUse; verified
|
|
226
|
+
by piping the hook its exact payload (deny + reason) and confirming inert when FRAY_UI_THREAD unset.
|
|
227
|
+
- **`hooks/deny-plan.mjs`** (NEW, PermissionRequest `ExitPlanMode`) — denies the plan-approval prompt.
|
|
228
|
+
MECHANISM (per the Claude Code hooks docs): `ExitPlanMode` is a PERMISSION surface, not a plain
|
|
229
|
+
tool, so it is denied via a **PermissionRequest** hook (matcher exactly `ExitPlanMode`), NOT
|
|
230
|
+
PreToolUse. The deny JSON is `{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":
|
|
231
|
+
{"behavior":"deny"}}, "additionalContext":"<redirect>"}` — the instructive redirect rides
|
|
232
|
+
**top-level `additionalContext`** (NOT `decision.message`), which Claude injects to the model as a
|
|
233
|
+
plain-text system-reminder; exit 0 with the JSON on stdout (exit 2 makes Claude ignore it). CAVEAT:
|
|
234
|
+
PermissionRequest hooks do NOT fire under `claude -p` (headless print mode), so the `-p` smoke test
|
|
235
|
+
can't exercise it — but fray-ui workers run as INTERACTIVE tmux `claude` sessions, where they DO
|
|
236
|
+
fire. Both deny hooks are FRAY_UI_THREAD-gated and fail-open. (`AskUserQuestion` was not surfaced as
|
|
237
|
+
an invokable tool in the `-p` harness either, so both hooks were verified by piping their exact hook
|
|
238
|
+
payloads rather than by driving a live prompt.)
|
|
239
|
+
|
|
240
|
+
### `hooks/perm-policy.mjs` — the worker's permission policy (2026-07-25)
|
|
241
|
+
|
|
242
|
+
Replaces the observe-only `perm-observe.mjs`. Same registration (PermissionRequest, matcher `*`), but
|
|
243
|
+
it now DECIDES: allow / deny / defer, first match wins.
|
|
244
|
+
|
|
245
|
+
WHY IT DECIDES. fray dispatches Claude workers at `--permission-mode auto`
|
|
246
|
+
(`WORKER_DISPATCH_PERMISSION`), and `auto` is **not** non-interactive — its classifier still raises a
|
|
247
|
+
prompt for anything it judges risky. With nobody at the keyboard that parks a thread invisibly; one sat
|
|
248
|
+
blocked on a `git push` for over a day. The blunt alternative was dispatching at `bypassPermissions`,
|
|
249
|
+
which was deliberately NOT chosen: bypass removes the decision POINT, so nothing can ever inspect a
|
|
250
|
+
request again. Keeping `auto` and deciding here preserves the seam — and Claude Code labels the outcome
|
|
251
|
+
in the pane ("Allowed by PermissionRequest hook"), so an auto-approval stays attributable rather than
|
|
252
|
+
being indistinguishable from bypass.
|
|
253
|
+
|
|
254
|
+
THE TABLE ships UNIVERSAL rules only; this plugin loads for every project fray drives, so a rule that
|
|
255
|
+
is right for one repo is wrong for the next. `catastrophic-delete` and `raw-disk-write` deny outright
|
|
256
|
+
(strictly safer than bypass, which would have allowed both). `restrictive-mode` DEFERS whenever
|
|
257
|
+
`permission_mode !== "auto"`, which is what makes a genuine lower-permission mode usable: move a thread
|
|
258
|
+
to `default` with the live permission control and its prompts come back. `FRAY_PERM_POLICY=review`
|
|
259
|
+
defers everything. Fail-safe INVERTS the old observer's fail-open — for a hook that can APPROVE, any
|
|
260
|
+
error must fall back to asking, never to allowing.
|
|
261
|
+
|
|
262
|
+
PAYLOAD (verified live, correcting an earlier note that claimed otherwise): the PermissionRequest hook
|
|
263
|
+
input carries `session_id`, `transcript_path`, `cwd`, `prompt_id`, `permission_mode`, `effort`,
|
|
264
|
+
`hook_event_name`, `tool_name`, `tool_input`, and `permission_suggestions`. That is enough for a real
|
|
265
|
+
per-tool/per-command policy. Allow JSON is
|
|
266
|
+
`{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow","updatedInput":<tool_input>}}}`.
|
|
267
|
+
|
|
268
|
+
KNOWN LIMIT: an explicit `ask` RULE outranks this hook. `"permissions":{"ask":["Bash(git push:*)"]}`
|
|
269
|
+
raises a prompt an `allow` here does not override — Claude Code says so on the prompt itself. Isolated
|
|
270
|
+
against a hook that allows unconditionally (it prompted too), so this is Claude Code precedence, not a
|
|
271
|
+
defect, and arguably correct: an explicit human rule should beat a blanket policy. A repo carrying
|
|
272
|
+
`ask` rules can therefore still park a worker; the fix is that repo's settings.
|
|
273
|
+
|
|
274
|
+
MARKER: `<stateDir>/perm-requests/<slug>.json` now carries `decision` / `rule` / `reason` / `command`
|
|
275
|
+
alongside the original fields. The tailer treats ONLY a deferred request as a human block (an
|
|
276
|
+
auto-resolved one must never card as "Needs you"), and surfaces the last allow/deny as `permPolicy` —
|
|
277
|
+
the only durable record an approval leaves anywhere, since allows are never written to the transcript.
|
|
278
|
+
A marker with no `decision` (an older plugin build) still blocks exactly as before.
|
|
279
|
+
|
|
280
|
+
VERIFICATION: `scripts/verify-perm-marker.mjs` runs the REAL hook the way Claude Code does and
|
|
281
|
+
asserts every branch. The allow path was additionally driven against a REAL interactive `claude` (it
|
|
282
|
+
cannot be exercised under `claude -p`, where PermissionRequest hooks do not fire).
|
|
283
|
+
|
|
284
|
+
### The `--settings` permission floor — BUILT, verified, then deliberately REMOVED (2026-07-08)
|
|
285
|
+
A `--settings` permission FLOOR was added to `ui/dispatch.ts` (`WORKER_DENY_SETTINGS =
|
|
286
|
+
{"permissions":{"deny":["AskUserQuestion","ExitPlanMode"]}}`, both command builders) and verified to
|
|
287
|
+
work: `claude -p --settings '{deny:[AskUserQuestion,ExitPlanMode,Bash]}'` → the model reports
|
|
288
|
+
`Bash: No` (control — a bare-name deny removes a normally-present tool from context), `Read/Write:
|
|
289
|
+
Yes`, `AskUserQuestion/ExitPlanMode: No`. It was then **removed on the maintainer's call**, and the
|
|
290
|
+
reasoning is worth keeping (it corrects the earlier "floor first, hooks failover" story):
|
|
291
|
+
|
|
292
|
+
- A bare-name deny removes the tool from the tool LIST, but a model **knows `AskUserQuestion` /
|
|
293
|
+
`ExitPlanMode` from TRAINING** and can still attempt them. With the floor in place, that attempt
|
|
294
|
+
hits a generic "no such tool" permission error — NOT the hook's instructive `needs-human` redirect
|
|
295
|
+
(a permission `deny` is evaluated regardless of hook output and takes precedence, so the model sees
|
|
296
|
+
the generic denial). So the floor can actually **prevent the tool-block education process**.
|
|
297
|
+
- Therefore: **hooks-only enforcement.** The deny HOOKS are themselves a hard deny AND they teach on
|
|
298
|
+
contact (their `needs-human` redirect reaches the model — verified: deny-ask emits a PreToolUse
|
|
299
|
+
`permissionDecisionReason`, deny-plan a PermissionRequest top-level `additionalContext`). The floor
|
|
300
|
+
is gone; `dispatch.ts` + `server.test.ts` are reverted to clean.
|
|
301
|
+
|
|
302
|
+
### Plan-mode softlock fix (2026-07-08)
|
|
303
|
+
A worker in plan mode that calls `ExitPlanMode` would be denied by deny-plan — but plan mode ALSO
|
|
304
|
+
blocks file edits, so the redirect ("write the plan into your thread") is impossible to follow: a
|
|
305
|
+
softlock. deny-plan does not branch on the session's mode, so it cannot pass plan mode through. Fixed
|
|
306
|
+
at the SOURCE instead: `ui/dispatch.ts` `workerPermissionMode()` coerces `--permission-mode plan` → `auto` inside
|
|
307
|
+
BOTH command builders (so dispatch, adopt, AND resume never spawn a worker in plan mode). Workers plan
|
|
308
|
+
by writing the plan into the thread + `status: needs-human` (the contract), which has no plan-mode
|
|
309
|
+
requirement — so nothing is lost. deny-plan then denies `ExitPlanMode` UNCONDITIONALLY when gated:
|
|
310
|
+
for a real fray-ui worker (never in plan mode) that is always a spurious call → deny + redirect is
|
|
311
|
+
correct. RESIDUAL GAP (accepted, documented): the deny could softlock only a FOREIGN session that is
|
|
312
|
+
simultaneously in plan mode AND running with FRAY_UI_THREAD set AND this plugin loaded — a combination
|
|
313
|
+
fray-ui never produces. A normal plan-mode session outside fray-ui is untouched (deny-plan is inert
|
|
314
|
+
without FRAY_UI_THREAD). Follow-up for UI honesty: `web/src/lib/options.ts` still offers "plan" in the
|
|
315
|
+
dispatch permission-mode dropdown (coerced to `auto` at spawn) — the sibling can drop it there.
|
|
316
|
+
|
|
317
|
+
### Malformed-thread one-click repair — read-side recovery (2026-07-09)
|
|
318
|
+
INCIDENT: a worker that spawned before the frontmatter-validation write-hook existed wrote
|
|
319
|
+
`nub/.fray/sandbox-windows-backend.md` with its metadata in **bold prose** instead of YAML
|
|
320
|
+
frontmatter. The board banner correctly reported "sandbox-windows-backend.md: no YAML frontmatter",
|
|
321
|
+
but the thread was INVISIBLE to the queue/status system (the parser can't read its title/status →
|
|
322
|
+
`status: ?`) until the orchestrator hand-edited YAML. Maintainer directive: "make sure that doesn't
|
|
323
|
+
happen again."
|
|
324
|
+
|
|
325
|
+
TWO-SIDED DEFENSE:
|
|
326
|
+
- **Write-side (already existed):** the frontmatter-validation file-tool hook blocks a compliant
|
|
327
|
+
worker from writing a thread `.md` with no frontmatter in the first place.
|
|
328
|
+
- **Read-side (this change):** RECOVERY for any straggler the write-hook can't catch — pre-hook
|
|
329
|
+
files, hand edits, and (the residual, by design) **Bash-written files that bypass the file-tool
|
|
330
|
+
hooks entirely**. The board now ships STRUCTURED, classified errors so the UI can offer one-click
|
|
331
|
+
repair.
|
|
332
|
+
|
|
333
|
+
MECHANISM:
|
|
334
|
+
- `cc/scripts/fray/index.mjs` `--json` gains a parallel `errorItems: [{file, kind, message}]`
|
|
335
|
+
(`kind: 'no-frontmatter'` = repairable, else `'other'`). The parser classifies — it knows exactly
|
|
336
|
+
why it rejected the file. The legacy `errors: string[]` array is emitted UNCHANGED alongside it.
|
|
337
|
+
- `errorItems` flows through `readBoard`/`fray.ts` → `board.ts assemble()` → `BoardSnapshot` +
|
|
338
|
+
`BoardMeta` (so the repair affordance survives a board delta, not just the keyframe) → the
|
|
339
|
+
TodosView banner, which renders a **Repair** button per `no-frontmatter` item.
|
|
340
|
+
- `repairThread({file})` RPC (`repair.ts`) validates the file is a real `.md` DIRECTLY under `.fray/`
|
|
341
|
+
(resolve + dirname===root guard; rejects `../`, `sub/`, absolute paths), refuses any file that
|
|
342
|
+
already has a `---` block (repair is ONLY the missing-frontmatter case), then PREPENDS minimal
|
|
343
|
+
frontmatter: `title` from the first `# H1` (else the filename slug), `status: active`, and a
|
|
344
|
+
standing `status_text` flag that the status is unverified. Then a board rebuild.
|
|
345
|
+
|
|
346
|
+
CONSERVATIVE ON PURPOSE: repair NEVER infers status from prose. This morning's file said "DONE" in
|
|
347
|
+
bold — guessing wrong silently is worse than surfacing. `status: active` makes the thread visible;
|
|
348
|
+
if its agent is gone, the runtime crash-net cards it for human attention — the correct escalation.
|
|
349
|
+
|
|
350
|
+
RESIDUAL (accepted, documented): a Bash-written `.fray/*.md` bypasses the file-tool hooks by design,
|
|
351
|
+
so the write-side can't prevent it — the read-side repair is the safety net that heals it in one
|
|
352
|
+
click.
|
|
353
|
+
|
|
354
|
+
## 2026-07-09: v2 worker contract — the session-first rebuild (fences + scratchpad; thread-file contract DELETED)
|
|
355
|
+
The maintainer settled fray-ui on a SESSION-FIRST model: threads ARE claude sessions and the human's
|
|
356
|
+
dashboard shows the session TRANSCRIPT. Queue membership is explicit: `question` hands off to the
|
|
357
|
+
human, `done` queues a checked completion, process-level blocks surface themselves, `awaiting`
|
|
358
|
+
excuses a machine wait, and bare rest stays quiet. The entire `.fray/<slug>.md` ownership contract is
|
|
359
|
+
GONE: no thread files, no frontmatter, no `status`/`activity`/`status_text`, no `needs-human`, no
|
|
360
|
+
`blocked` machine fields, no `hasPlan`/`## Plan`, no `fray-update`. Workers now SIGNAL through their
|
|
361
|
+
FINAL MESSAGE and PERSIST through a SCRATCHPAD. This is the cc-worker-side realignment.
|
|
362
|
+
|
|
363
|
+
**The new signal model (taught in `ui/WORKER_PROMPT.md` §"End-of-turn signals" + `skills/worker/SKILL.md`):**
|
|
364
|
+
- **Bare rest is quiet** — a rested thread with no fence does NOT enter Needs-you and is not a human
|
|
365
|
+
handoff. Human handoff is explicit via `question` (or a real process-level block).
|
|
366
|
+
- **` ```done `** — work complete + stands; body = 1–4 lines of what shipped + where. Renders a
|
|
367
|
+
checked success card in the queue until explicit Archive; the fence MUTATES NOTHING
|
|
368
|
+
(maintainer-settled), and a follow-up may still wake the worker.
|
|
369
|
+
- **` ```awaiting `** — waiting on a MACHINE (CI/PR/timer/session); body may lead with `kind: value`
|
|
370
|
+
hint lines, kind ∈ pr|ci|timer|session. NEVER for a human wait.
|
|
371
|
+
- **` ```question `** — unchanged grammar (question / approval / multi / danger; trailing `- A. …`
|
|
372
|
+
options + `Recommendation:`), now the ONLY handback-for-input; no status flip accompanies it.
|
|
373
|
+
- CONSISTENCY: the taught grammar matches the shared parser (`packages/shared/src/index.ts`
|
|
374
|
+
`ThreadFence` kind ∈ done|awaiting, `AwaitingHint` kind ∈ pr|ci|timer|session). Opening line is
|
|
375
|
+
exactly ` ```done `/` ```awaiting ` (nothing after the language word); exactly one fence, at the end.
|
|
376
|
+
|
|
377
|
+
**The scratchpad (`.fray/threads/<session-id>/scratch.md`) — new §"Scratchpad" in both docs:** free-form
|
|
378
|
+
markdown, NO schema, NO validation. It is the worker's compaction-proof working memory (survive-
|
|
379
|
+
compaction to-do lists / work queues / Ralph-style epic checklists live here, not in ephemeral
|
|
380
|
+
context) AND the shared blackboard for parallel sub-agents (shared state is written into it; its PATH
|
|
381
|
+
is passed into every sub-agent prompt; helpers READ it, the worker folds their results back in). The
|
|
382
|
+
path is server-established convention already wired through `shared` (`scratchpadPath`), `router.ts`
|
|
383
|
+
(reads `.fray/threads/<session_id>/scratch.md`), and `dispatch.ts`.
|
|
384
|
+
|
|
385
|
+
**Hooks changed:**
|
|
386
|
+
- **DELETED `hooks/thread-frontmatter-validate.mjs`** + its `hooks.json` PostToolUse `Edit|Write|
|
|
387
|
+
MultiEdit` entry. It validated thread-file frontmatter (status vocab / gerund `activity` / machine
|
|
388
|
+
fields) — a contract that no longer exists. Nothing left to validate on file edits.
|
|
389
|
+
- **DELETED `hooks/stop-flush.mjs`** (already UNWIRED since the 2026-07-02 Stop-hook removal; kept
|
|
390
|
+
"for reference" then). Its sole job was nudging the worker to flush state into its thread FILE —
|
|
391
|
+
dead with the thread-file contract gone. No `hooks.json` entry to remove (it was never re-wired).
|
|
392
|
+
- **`hooks/session-seed.mjs`** — reseeded to the v2 contract: signal via the final message (bare rest
|
|
393
|
+
is quiet; done queues checked completion; awaiting excuses a machine wait; question asks) + the
|
|
394
|
+
scratchpad, whose concrete path is derived from
|
|
395
|
+
the SessionStart `session_id` (`currentSessionId(input.session_id)` → `.fray/threads/<sid>/scratch.md`) and
|
|
396
|
+
named in the seed. FRAY_UI_THREAD gating + the cc double-hook `off`-sentinel defense KEPT verbatim;
|
|
397
|
+
the compact re-grounding now points at the scratchpad, not a thread file.
|
|
398
|
+
- **`hooks/agent-dispatch.mjs`** — epilogue no longer says "don't edit `.fray/` thread files or
|
|
399
|
+
config.yml"; now "don't edit the dispatcher's scratchpad (`.fray/threads/<session-id>/scratch.md`) — READ it for shared
|
|
400
|
+
context if its path is in your prompt, report in your FINAL MESSAGE." Background/name-strip
|
|
401
|
+
enforcement unchanged.
|
|
402
|
+
- **`hooks/deny-ask.mjs`** — redirect retargeted off `fray-update … --status needs-human`: now "ask
|
|
403
|
+
in your FINAL MESSAGE with ```question blocks, then rest; a question IS the handback (no extra
|
|
404
|
+
fence)." Still PreToolUse(AskUserQuestion), FRAY_UI_THREAD-gated, fail-open.
|
|
405
|
+
- **`hooks/deny-plan.mjs`** — redirect retargeted off "write the plan into your thread `.fray/<slug>.md`
|
|
406
|
+
+ status: needs-human": now "write the plan into `.fray/plans/<topic>.md` and/or the scratchpad;
|
|
407
|
+
ask via a ```question approval block." PermissionRequest(ExitPlanMode) mechanism + the plan-mode
|
|
408
|
+
softlock reasoning (dispatch.ts coerces plan→auto) unchanged.
|
|
409
|
+
- **`hooks/agent-bind.mjs`** — UNCHANGED (functionally). It records `agentId → thread` into
|
|
410
|
+
`.fray/.agent-bindings.jsonl` from a helper's `THREAD: <slug>` tag; that tag still rides the
|
|
411
|
+
per-thread dispatch and references no dead status/frontmatter contract. NOTE for the server/tailer
|
|
412
|
+
verticals: session-first sub-agent liveness is now TAILER-derived (`ThreadView.subAgents`), so the
|
|
413
|
+
`.agent-bindings.jsonl` binding this hook writes may be VESTIGIAL. Left in place (harmless,
|
|
414
|
+
fail-open, out of this vertical's delete scope) — a candidate for removal once the tailer path is
|
|
415
|
+
confirmed to fully supersede board-side `bindingsByThread`.
|
|
416
|
+
|
|
417
|
+
**`skills/worker/SKILL.md`** — rewritten to the same pillars (version 0.2.0): the signal model
|
|
418
|
+
replaces the status-vocabulary sections; a scratchpad section replaces the "own ONE thread file"
|
|
419
|
+
sections; thread-type presets keep the research/audit/implementation/planning taxonomy but strip
|
|
420
|
+
every status reference (planning now delivers a `.fray/plans/<topic>.md` artifact). The sub-agents
|
|
421
|
+
section adds "pass the scratchpad path into helper prompts." `skills/dialectic` untouched.
|
|
422
|
+
|
|
423
|
+
**`plugin.json`** description updated: no more "validate thread-file frontmatter"; now fence signals
|
|
424
|
+
+ scratchpad blackboard + sub-agent profiles + deny-ask/deny-plan.
|
|
425
|
+
|
|
426
|
+
**What the server/web verticals must know:** (a) FRAY_UI_THREAD must keep being passed at spawn — every
|
|
427
|
+
hook still gates on it. (b) The scratchpad path convention is `.fray/threads/<session-id>/scratch.md` where
|
|
428
|
+
`<session-id>` is the pinned `--session-id` (the same id the SessionStart hook sees as `session_id`);
|
|
429
|
+
the seed hook NAMES that concrete path to the worker, so dispatch must keep pinning `--session-id`.
|
|
430
|
+
(c) `packages/server/src/dispatch.ts` `composePrompt()` STILL emits the dead per-thread contract
|
|
431
|
+
("You own `.fray/<slug>.md` … set `status: blocked` … Set `status: done`") — that is server-vertical
|
|
432
|
+
scope (not editable here), but it now CONTRADICTS the v2 WORKER_PROMPT and must be rewritten to the
|
|
433
|
+
fence/scratchpad model (or dropped) by the dispatch owner. Flagged to server-core.
|
|
434
|
+
|
|
435
|
+
## 2026-07-12: awaiting reversal — park only human/timestamp gates; keep automation active
|
|
436
|
+
|
|
437
|
+
The v2 rule above made `awaiting` a broad machine-wait bucket. In practice workers emitted a fence
|
|
438
|
+
for CI, bots, releases, and merge progression, then returned with no process actually owning the
|
|
439
|
+
next transition. The rail's hourglass therefore implied a watcher that often did not exist. The
|
|
440
|
+
contract is now narrower:
|
|
441
|
+
|
|
442
|
+
- `awaiting` is a deliberate PARK for either `human: <actor + exact external review/approval>` or
|
|
443
|
+
`timer: <ISO-8601 instant>`. The dashboard operator's own decision remains `question`.
|
|
444
|
+
- CI, automated review, releases/deploys, merge queues, and already-authorized merge progression
|
|
445
|
+
stay ACTIVE. Claude workers use a background `Bash` one-shot or `Monitor`; Codex workers keep a
|
|
446
|
+
blocking exec session alive and poll it. Their completion/event re-invokes the worker.
|
|
447
|
+
- `pr:` / `ci:` / `session:` continue to parse so old transcripts do not break. The existing PR/CI
|
|
448
|
+
waker remains a compatibility bridge, but workers must not create new waits with those hints.
|
|
449
|
+
`timer:` remains the durable scheduler path across process/session restart. `human:` is descriptive
|
|
450
|
+
and intentionally not auto-fired.
|
|
451
|
+
- Every follow-up clears the old fence. “Back to awaiting” requires a fresh check: re-emit a current
|
|
452
|
+
human/timer fence, or re-arm automation and remain active.
|
|
453
|
+
|
|
454
|
+
Claude Code 2.1.207 was audited before teaching this. fray-ui does not pass `--tools`,
|
|
455
|
+
`--allowedTools`, or `--disallowedTools`, and its helper profiles only select model/effort, so wait
|
|
456
|
+
tools are available to top-level workers and helpers. `Monitor` defaults to 300,000 ms (maximum
|
|
457
|
+
3,600,000 ms); `persistent:true` runs until `TaskStop` or session end. Background Bash reports an
|
|
458
|
+
output path; `TaskOutput` exists but is deprecated, so workers should `Read` that path for diagnostics.
|
|
459
|
+
Both Monitor and background Bash are session/process-bound, which is why durable wall-clock checks
|
|
460
|
+
remain `timer:` fences. Helpers must not return a final handoff while they still own a live watcher;
|
|
461
|
+
the top-level worker owns long-lived CI/PR/merge progression.
|
|
462
|
+
|
|
463
|
+
## 2026-07-13: ordinary rest returns to Queue; human Snooze/Archive own triage
|
|
464
|
+
|
|
465
|
+
The quiet-bare-rest rule above was reversed after live use showed that an owned Fray worker could
|
|
466
|
+
come to rest without choosing a fence and disappear from the only surface the operator routinely
|
|
467
|
+
triages. Queue membership is now server-derived from process rest, not dependent on perfect worker
|
|
468
|
+
signaling:
|
|
469
|
+
|
|
470
|
+
- Every owned, open session whose top-level turn is genuinely at rest enters Queue by default.
|
|
471
|
+
- A live child/Monitor still counts as in-flight work. A truthful external-human or future-timer
|
|
472
|
+
`awaiting` fence remains dimmed in Held. Legacy CI/PR/session and hintless waits do not excuse rest.
|
|
473
|
+
- A human may durably Snooze an ordinary handoff (default one day, presets/custom exact instant) or
|
|
474
|
+
Archive it. Due snoozes automatically re-enter Queue; Archive never does.
|
|
475
|
+
- Questions, permissions/native approvals, typed interactions, and crashes break through Snooze so a
|
|
476
|
+
provider cannot be stranded behind an invisible hard gate. `done` remains the checked presentation,
|
|
477
|
+
but it is still a resting handoff and can be snoozed.
|
|
478
|
+
|
|
479
|
+
The worker contract still teaches explicit `question`, `done`, and narrow `awaiting` fences because
|
|
480
|
+
they improve priority and presentation. A fence is no longer required merely to make a rested worker
|
|
481
|
+
discoverable.
|
|
482
|
+
|
|
483
|
+
## 2026-07-12: runtime release gate — real CDP evidence plus independent review
|
|
484
|
+
|
|
485
|
+
Major UI, server, and control-plane work may no longer reach `done` from unit/integration/mocked
|
|
486
|
+
evidence alone. The canonical `ui/WORKER_PROMPT.md` contract now requires real Chrome CDP QA against
|
|
487
|
+
a disposable full stack, relevant active/idle/error/restart coverage, desktop+narrow screenshots,
|
|
488
|
+
console/network inspection, and an explicit correctness+aesthetics assessment. Chrome DevTools MCP is
|
|
489
|
+
preferred when it is available to the current provider; `agent-browser` or the repository Puppeteer
|
|
490
|
+
harness are explicit fallbacks. Mocked DOM/routes can supplement but cannot be the sole evidence.
|
|
491
|
+
|
|
492
|
+
Completion also requires two distinct review passes: the implementer's self-review of diff+evidence,
|
|
493
|
+
then an independent fresh-context adversarial review; confirmed findings are fixed and affected gates
|
|
494
|
+
rerun. The exception is proportional and narrow: trivial non-runtime docs-only or provably mechanical
|
|
495
|
+
changes may skip CDP/independent review, while uncertainty applies the gate. This rule is mirrored in
|
|
496
|
+
`skills/worker/SKILL.md` (v0.2.2) and the SessionStart seed; the backend-aware prompt contract test pins
|
|
497
|
+
all four delivered surfaces, and the Claude expansion golden changes intentionally. The Codex addendum
|
|
498
|
+
no longer mislabels an author's inline second read as independent review: use delegation when available,
|
|
499
|
+
or report the gate unmet.
|
|
500
|
+
|
|
501
|
+
## 2026-07-21: Plugin slim-down — one contract copy, gh is the only injected skill
|
|
502
|
+
|
|
503
|
+
The plugin stops shipping three of its four skills. `skills/worker` is DELETED: it was a second copy
|
|
504
|
+
of the worker contract whose single source is `packages/server/src/workerPrompt.ts` (the system
|
|
505
|
+
prompt, rebuilt on every dispatch/resume and compaction-immune) — every contract edit had to be made
|
|
506
|
+
twice, and the copies drifted. The session-seed pointer sentences that said "Load the `fray:worker`
|
|
507
|
+
skill for the full contract" now point at the system-prompt contract only, and the contract tests pin
|
|
508
|
+
the two backend prompts (not a skill copy). `skills/dialectic` is dropped from the plugin (generic
|
|
509
|
+
methodology nobody wired into the seed or prompt; workers on other people's projects never asked for
|
|
510
|
+
it). `skills/adhoc-cdp` MOVED to the fray repo's own `.agents/skills/adhoc-cdp` (agent-neutral; `.claude/skills/adhoc-cdp` is a symlink to it so Claude and Codex share one copy) — its content is
|
|
511
|
+
fray-ui-specific (adhoc-stack.mjs / shot.mjs), so it is a project skill, not global plugin cargo; the
|
|
512
|
+
generic "verify in a real browser" principle already lives in the prompt's runtime-gate section.
|
|
513
|
+
`skills/gh` remains the ONE injected skill: bulky, conditionally relevant, and its pointer is already
|
|
514
|
+
auth-gated in the seed — exactly the on-demand shape skills are for.
|
|
515
|
+
|
|
516
|
+
## 2026-07-22: The no-PR rule now also lives in AGENTS.md (docs, not a hook)
|
|
517
|
+
|
|
518
|
+
Root cause of "agents keep opening PRs despite FRAY.md": the worker contract + injected FRAY.md are a
|
|
519
|
+
SNAPSHOT frozen at session creation. The Codex worker that opened PR #17 and #18 (thread `862831cf`,
|
|
520
|
+
born 09:50 Jul 21) spawned minutes before FRAY.md injection first landed and hours before the no-PR
|
|
521
|
+
rule existed, so it never saw the rule and carried the base contract's "open a PR and report its URL"
|
|
522
|
+
on every turn. Editing FRAY.md does nothing for a session already in flight, and sub-agents never
|
|
523
|
+
receive FRAY.md at all.
|
|
524
|
+
|
|
525
|
+
Mitigation (docs only): the no-PR rule was added to `AGENTS.md` — the agent-neutral home Codex
|
|
526
|
+
re-reads FRESH every session and sub-agents load. That reaches NEW sessions of both backends without a
|
|
527
|
+
frozen snapshot. It does NOT retroactively reach an already-running frozen session; restart such a
|
|
528
|
+
session to pick up a rule change. A tool-layer enforcement hook (`deny-pr` PreToolUse) was built and
|
|
529
|
+
then deliberately reverted as overkill for a single-user repo — the doc reach is the intended fix.
|
|
530
|
+
|
|
531
|
+
## 2026-07-30: Carryover — a per-session brief the harness re-injects, so context survives compaction
|
|
532
|
+
|
|
533
|
+
The two existing mitigations for compaction loss both routed the context through a DECISION, and that
|
|
534
|
+
is where they leaked. The scratchpad survives on disk but only helps if the post-compaction turn
|
|
535
|
+
chooses to read it — a model decision, and it gets skipped. `precompact-instructions.mjs` steers the
|
|
536
|
+
summarizer, but the summary is a lossy retelling by a model that never saw the reasoning, and it is
|
|
537
|
+
regenerated from scratch every time. `carryover.mjs` removes the decision: whatever is in
|
|
538
|
+
`.fray/threads/<sid>/carryover.md` is spliced into the context window by the harness, before the
|
|
539
|
+
model's first token. The model cannot forget to read it and the summarizer cannot paraphrase it away.
|
|
540
|
+
|
|
541
|
+
**The file is authored by the AGENT, not by the hook** — it is a plain markdown file written with the
|
|
542
|
+
Write tool. Rejected: a CLI (`fray carryover set …`) and a dedicated MCP tool. Both add something to
|
|
543
|
+
learn and to keep installed, and neither buys anything over a file the agent already knows how to
|
|
544
|
+
write and a human can hand-edit mid-session (an edit is just an mtime change, which the nudge treats
|
|
545
|
+
exactly like an agent's write — pinned by a test).
|
|
546
|
+
|
|
547
|
+
**Keyed by session id**, taken from the hook's stdin `session_id` and falling back to
|
|
548
|
+
`CLAUDE_CODE_SESSION_ID` via the shared `currentSessionId`. Those two are equal (already relied on by
|
|
549
|
+
the activation sentinel), which is what lets the AGENT compute its own path from a Bash/Write call.
|
|
550
|
+
Stored beside the scratchpad rather than under `.fray/.session-state/` because the agent is already
|
|
551
|
+
told the thread dir. `.fray/` is gitignored in full, so a brief never reaches a commit, and nothing
|
|
552
|
+
enumerates `.fray/threads/*` to build the board (threads come from the DB), so writing a directory
|
|
553
|
+
there for a non-fray session cannot conjure a phantom thread card.
|
|
554
|
+
|
|
555
|
+
**Staleness is measured in context TOKENS, and as GROWTH, never an absolute.** The transcript's
|
|
556
|
+
newest usage record gives live context fill (`input + cache_creation + cache_read`); the hook reads
|
|
557
|
+
only the last 256 KB of the file, since transcripts reach tens of megabytes here. An absolute
|
|
558
|
+
threshold is unusable because the window is not knowable from a hook — a real compaction in this
|
|
559
|
+
project fired at preTokens 935,291 on a 1M-window session while a 200k session compacts near 160k.
|
|
560
|
+
Growth since the last write is window-independent and self-resetting. Wall clock and transcript BYTES
|
|
561
|
+
were both rejected: neither tracks context pressure (one large tool result adds megabytes to the file
|
|
562
|
+
without moving the window much).
|
|
563
|
+
|
|
564
|
+
**Why a nudge at all.** Without it the file is never written and the whole mechanism is decorative —
|
|
565
|
+
an agent that is never reminded does not stop to journal. It is the one part that is a heuristic, so
|
|
566
|
+
it is the one part with a cheap escape hatch: `FRAY_CARRYOVER_STALE_TOKENS` retunes it and
|
|
567
|
+
`FRAY_CARRYOVER=off` disables every mode.
|
|
568
|
+
|
|
569
|
+
**Registered twice, deduped deterministically.** The plugin registration ships to every fray worker
|
|
570
|
+
everywhere. The repo's own `.claude/settings.json` carries the same three registrations with
|
|
571
|
+
`--via=project` so plain (non-fray) `claude` sessions in this repo get the behavior too; that flag
|
|
572
|
+
exits when `FRAY_UI_THREAD` is set, because such a session already loads the plugin and would
|
|
573
|
+
otherwise inject the brief twice. A flag plus an env check — no lock file, no race.
|
|
574
|
+
|
|
575
|
+
VERIFIED LIVE against cli 2.1.220, in an isolated `/tmp` project, not by proxy:
|
|
576
|
+
- `SessionStart:startup` fired (exit 0, 63 ms) and a real session quoted a sentinel that existed ONLY
|
|
577
|
+
in the brief — with **zero tool calls** in the transcript, so it came from context, not a file read.
|
|
578
|
+
- A real `/compact` produced a `compact_boundary`, and `SessionStart:compact` then delivered a brief
|
|
579
|
+
whose sentinel had been swapped to a value that had NEVER appeared in that session's context — so
|
|
580
|
+
the post-compaction injection demonstrably re-reads the file rather than echoing the summary.
|
|
581
|
+
- `PreCompact:manual [carryover.mjs --mode=precompact] completed with status 0` in the hook debug log.
|
|
582
|
+
- The `UserPromptSubmit` nudge fired live and reported ~31k tokens computed from the real transcript,
|
|
583
|
+
confirming the usage parser works against Claude Code's actual format and not just a fixture.
|
|
584
|
+
|
|
585
|
+
Regression net: `packages/server/src/carryover-hook.test.ts` executes the real script over its
|
|
586
|
+
wire contract (argv + stdin JSON + stdout) rather than asserting on its source.
|
|
587
|
+
|
|
588
|
+
## 2026-07-30 (same day, revised): carryover COLLAPSED into the scratchpad; the nudge moved mid-turn
|
|
589
|
+
|
|
590
|
+
The `carryover.md` brief shipped earlier today was redundant with the scratchpad and made the worker
|
|
591
|
+
maintain two overlapping documents (maintainer's call, and correct). ONE doc per thread is the rule.
|
|
592
|
+
`carryover.mjs` is DELETED and replaced by `scratchpad.mjs`, which does the same three things to
|
|
593
|
+
`scratch.md` — the pad the dispatcher already provisions, already names in the system prompt, and
|
|
594
|
+
already forbids sub-agents from writing.
|
|
595
|
+
|
|
596
|
+
Two behavior changes fell out of the retarget:
|
|
597
|
+
|
|
598
|
+
**Injection is scoped to the context-losing sources.** A brand-new `startup` has lost nothing, so it
|
|
599
|
+
gets the contract text only; `compact`/`resume`/`clear` get the pad's head injected verbatim PLUS an
|
|
600
|
+
explicit "re-read the full file" pointer. Injecting the head rather than only pointing at the file is
|
|
601
|
+
deliberate: a bare reminder routes recovery through a decision the model can skip, which is the exact
|
|
602
|
+
failure being fixed. The head is the floor; the pointer is the ceiling. The cap (12k chars, was 24k)
|
|
603
|
+
keeps that floor affordable now that the target is unbounded working memory rather than a bounded brief.
|
|
604
|
+
|
|
605
|
+
**"Present" no longer means "written".** fray provisions scratch.md with a skeleton, so file-absence
|
|
606
|
+
is gone as the unwritten signal. `substanceLength()` strips headings, the provisioned orientation line
|
|
607
|
+
and empty task boxes, and measures what remains. It is a heuristic on purpose — it only decides
|
|
608
|
+
whether to NUDGE, so a wrong call costs one redundant reminder, never correctness.
|
|
609
|
+
|
|
610
|
+
**The nudge now also fires on PostToolUse, and that is the real fix.** UserPromptSubmit alone only
|
|
611
|
+
fires at turn boundaries, and a fray worker runs enormous autonomous turns — dozens of tool calls
|
|
612
|
+
between human prompts — so a whole session's work could compact unpersisted without a single nudge.
|
|
613
|
+
PostToolUse `additionalContext` was verified live against cli 2.1.220 (a real session quoted a
|
|
614
|
+
sentinel injected after a Bash call, and through the plugin the model received the nudge mid-turn and
|
|
615
|
+
said it would update the pad). Both channels share one state file, so the interval is global: firing
|
|
616
|
+
per tool call makes the existing budget land SOONER and mid-turn, it does not multiply reminders.
|
|
617
|
+
|
|
618
|
+
### Researched: there is NO context-pressure hook, on either backend
|
|
619
|
+
|
|
620
|
+
Measured, so do not go looking again. Claude Code 2.1.220 exposes **31 hook events** and not one
|
|
621
|
+
signals an approaching context limit; **no hook input carries a token count at all**, and the docs say
|
|
622
|
+
plainly to poll the transcript yourself. Codex is the same. So the fill is computed here from the
|
|
623
|
+
transcript's newest usage record (`input + cache_creation + cache_read`), reading only the file's tail
|
|
624
|
+
because transcripts reach tens of megabytes. Growth-since-last-write remains the metric because the
|
|
625
|
+
window is not knowable from a hook.
|
|
626
|
+
|
|
627
|
+
Useful events noticed while enumerating, none wired yet: `PostToolBatch` (fires after a batch of
|
|
628
|
+
parallel tool calls resolves, before the next model call), `SubagentStart` (could carry the
|
|
629
|
+
read-only-pad rule structurally instead of via the dispatch epilogue's prose), `PostCompact`
|
|
630
|
+
(receives the summary), and `SessionStart`'s `initialUserMessage` output (injects a VISIBLE user
|
|
631
|
+
message rather than a system reminder) and `fork` matcher.
|
|
632
|
+
|
|
633
|
+
### Rejected: a blocking Stop gate
|
|
634
|
+
|
|
635
|
+
A Stop hook could refuse to let the worker rest until it writes. That was tried and removed on
|
|
636
|
+
2026-07-02 (maintainer's call): the block-until-file-edited nag forced even trivial workers into
|
|
637
|
+
Read/Edit dances that render as noise in the chat UI. This nudges; it never blocks.
|
|
638
|
+
|
|
639
|
+
### Verified feasible, NOT yet built: the background checkpoint fork
|
|
640
|
+
|
|
641
|
+
`claude -p --resume <sid> --fork-session` was tested end to end. The fork carries the FULL original
|
|
642
|
+
conversation (it reproduced a human quote that existed only in that transcript), it wrote to the
|
|
643
|
+
ORIGINAL thread's scratchpad when handed the absolute path, the original transcript was
|
|
644
|
+
**byte-identical before and after** (15,078 → 15,078 — the live session is never interrupted), the
|
|
645
|
+
original continued normally afterwards, and it is cheap because it HITS THE PROMPT CACHE (28,904
|
|
646
|
+
cache-read vs 413 cache-create). `SessionStart` has a `fork` matcher so hooks can tell they are in one.
|
|
647
|
+
|
|
648
|
+
Open problems before this could ship: the fork gets a NEW session id, so it must be handed the
|
|
649
|
+
ORIGINAL pad path explicitly (its own hooks would derive a different one); concurrent writes against
|
|
650
|
+
the live worker need an ownership rule; the spawn needs a single-flight lock so one threshold crossing
|
|
651
|
+
cannot fan out into many forks; and fray's discover/tailer must not adopt the fork's transcript as a
|
|
652
|
+
board thread.
|
|
653
|
+
|
|
654
|
+
### Codex parity gap
|
|
655
|
+
|
|
656
|
+
Codex 0.144.6 (installed) HAS a hooks system with nearly the same schema — SessionStart, SessionEnd,
|
|
657
|
+
PreToolUse, PostToolUse, PermissionRequest, UserPromptSubmit, Stop, PreCompact, PostCompact,
|
|
658
|
+
SubagentStart, SubagentStop — plus `additionalContext`, all confirmed present in the installed binary.
|
|
659
|
+
Config lives at `~/.codex/hooks.json` or `[hooks]` in config.toml, repo-level `.codex/hooks.json`, or
|
|
660
|
+
plugin-bundled. SessionStart distinguishes a `compact` source. Codex is BETTER in one respect: it lets
|
|
661
|
+
you SET the compaction threshold (`model_auto_compact_token_limit`, with
|
|
662
|
+
`model_auto_compact_token_limit_scope` = `total` | `body_after_prefix`), so compaction can be made
|
|
663
|
+
predictable with headroom rather than merely detected. Wrinkle: codex enforces hook TRUST
|
|
664
|
+
(`--dangerously-bypass-hook-trust` exists for automation). **fray currently wires ZERO hooks for
|
|
665
|
+
codex**, so a codex worker has only prompt-level scratchpad discipline — the largest remaining gap.
|
|
666
|
+
|
|
667
|
+
## 2026-07-30 (third pass): reinforcement is OPT-IN, and the two backends are gated differently
|
|
668
|
+
|
|
669
|
+
> **SUPERSEDED the same day by the fourth pass below** — the opt-in gate was REMOVED. Re-grounding is
|
|
670
|
+
> unconditional now. The measured codex findings in this entry all still hold; only the gating does not.
|
|
671
|
+
|
|
672
|
+
Maintainer's call: the mechanism is opinionated, so it should be chosen rather than inherited. New
|
|
673
|
+
`scratchpadReinforcement` setting, **OFF by default** — the inverse of `runtimeGate` /
|
|
674
|
+
`autoResumeOnLimit`, which are opt-OUT. Toggle sits in the Settings drawer under "Auto-resume after
|
|
675
|
+
usage limits". `scratchpad.mjs` inverted its kill switch into an opt-in gate: absence means off.
|
|
676
|
+
|
|
677
|
+
**The two backends cannot be gated in the same place**, which is the whole design constraint here:
|
|
678
|
+
|
|
679
|
+
- **Claude → a worker ENV VAR.** `hooks.json` is static, so `FRAY_SCRATCHPAD_HOOK=on` is what decides
|
|
680
|
+
whether the registered hooks do anything. Stamped on the tmux spawn, and on the broker/SDK path via
|
|
681
|
+
a new per-fork `extraWorkerEnv` on `ClaudeAgentBrokerBridge` — evaluated per fork, so flipping the
|
|
682
|
+
setting reaches the next dispatch or cold-resume without a server restart.
|
|
683
|
+
- **Codex → per-conversation CONFIG**, on the `config` override `CodexAppServerBridge` already
|
|
684
|
+
supported, plus a `--enabled` flag. The env var is unusable there because the `codex app-server`
|
|
685
|
+
daemon is SHARED per project and its environment cannot express a per-conversation decision;
|
|
686
|
+
building the config at all already means the setting is on.
|
|
687
|
+
|
|
688
|
+
### Measured, so nobody re-derives it (codex-cli 0.144.6)
|
|
689
|
+
|
|
690
|
+
- **`codex exec` runs NO lifecycle hooks, from any discovery path** — not `<repo>/.codex/hooks.json`,
|
|
691
|
+
not `$CODEX_HOME/hooks.json`, not `-c hooks.…`, with or without `bypass_hook_trust=true`, inside a
|
|
692
|
+
git repo or outside one — even though the `hooks` feature flag reports as enabled. Probed with a
|
|
693
|
+
marker file so "did the hook RUN" stayed separate from "did its output reach the model".
|
|
694
|
+
- **`codex app-server` DOES run them** when they arrive as config overrides. Verified through the real
|
|
695
|
+
`CodexAppServerBridge` with the real `scratchpad.mjs`: SessionStart, UserPromptSubmit and Stop all
|
|
696
|
+
fired. Probe kept at `backend/_live_codex_hooks.mts`.
|
|
697
|
+
- **`bypass_hook_trust` is required** — codex SILENTLY SKIPS untrusted hook definitions, so without it
|
|
698
|
+
the config is delivered and ignored, which looks exactly like a broken feature.
|
|
699
|
+
- **Codex reports its OWN rollout session id to the hook** (e.g. `019fb427-…`, `transcript_path` under
|
|
700
|
+
`~/.codex/sessions`), NOT fray's thread id. Hence the mandatory `--session=<fray sessionId>`:
|
|
701
|
+
deriving the path would address a scratchpad that does not exist, and the worker would look
|
|
702
|
+
unreinforced for a reason nobody could see. Codex does send `source` (`"startup"`), same field name
|
|
703
|
+
as Claude, so the compact/resume branch works unchanged.
|
|
704
|
+
- **Codex has no PreCompact/PostCompact context-injection wire type** (only SessionStart /
|
|
705
|
+
UserPromptSubmit / PostToolUse / PreToolUse / PermissionRequest / SubagentStart), so the
|
|
706
|
+
summarizer-steering channel stays Claude-only.
|
|
707
|
+
|
|
708
|
+
### Known gap, deliberately not papered over
|
|
709
|
+
|
|
710
|
+
The codex NUDGE cannot fire yet: staleness is computed from Claude's transcript shape
|
|
711
|
+
(`message.usage`), and a codex rollout is a different format, so `contextTokens()` returns null and
|
|
712
|
+
the nudge degrades to SILENCE rather than to a wrong number — pinned by a test. Closing it needs a
|
|
713
|
+
rollout-aware token parser. The load-bearing channel (restoring the pad on SessionStart) works on both.
|
|
714
|
+
|
|
715
|
+
## 2026-07-30 (fourth pass): the gate comes OFF — the scratchpad is canonical, so re-grounding is not optional
|
|
716
|
+
|
|
717
|
+
Maintainer's correction, and it reverses the third pass's central decision: the thing that deserves an
|
|
718
|
+
opt-in is the FORK-based auto-updating (still unbuilt, and called "a little overkill"), NOT the
|
|
719
|
+
re-grounding. The scratchpad is the CANONICAL document for a thread, so reading it back after a
|
|
720
|
+
compaction is what makes the pad worth writing at all — a posture no project should have to opt into.
|
|
721
|
+
|
|
722
|
+
The bug this fixes is worse than a mis-scoped setting: `scratchpadReinforcement` defaulted to FALSE, so
|
|
723
|
+
the DEFAULT worker — every worker, in practice — got nothing back after a compaction. The mechanism was
|
|
724
|
+
shipped and inert. A feature that is off by default is a feature that does not exist.
|
|
725
|
+
|
|
726
|
+
Removed entirely: the `scratchpadReinforcement` setting (shared schema, server defaults, Settings
|
|
727
|
+
drawer toggle), the `FRAY_SCRATCHPAD_HOOK=on` env plumbing (`scratchpadHookEnv`, the tmux spawn stamp,
|
|
728
|
+
the broker bridge's `extraWorkerEnv` dep), and codex's `--enabled` flag. A dead toggle that gates
|
|
729
|
+
nothing is worse than no toggle; when the fork checkpointer is built it brings its own setting.
|
|
730
|
+
|
|
731
|
+
What survives: `--session=<fray sessionId>` on the codex path stays MANDATORY (codex reports its own
|
|
732
|
+
rollout session id, so the derived path would address a scratchpad that does not exist), and
|
|
733
|
+
`bypass_hook_trust` stays required (codex silently skips untrusted hook definitions). The escape hatch
|
|
734
|
+
is now `FRAY_SCRATCHPAD_HOOK=off` — env only, and only an explicit off value disables, because it is
|
|
735
|
+
for a one-off session, not a project posture.
|
|
736
|
+
|
|
737
|
+
Behavior change beyond ungating: **an EMPTY pad now re-grounds too.** Previously a compaction with an
|
|
738
|
+
unwritten pad fell through to the generic contract text; now it says plainly that context was just
|
|
739
|
+
lost, that the pad is the canonical record, to re-read it NOW, and that it currently has nothing in it
|
|
740
|
+
so it must be written. "You lost your context and your pad is empty" is the most actionable thing the
|
|
741
|
+
next turn can hear, and staying quiet about it left the worker with nothing at the exact moment it had
|
|
742
|
+
nothing.
|
|
743
|
+
|
|
744
|
+
VERIFIED against a real `/compact` with NO env var and NO flag set anywhere: `SessionStart:compact`
|
|
745
|
+
exit 0, delivering the re-grounding lead plus the pad head carrying a sentinel from the file. 17 hook
|
|
746
|
+
tests; full suite 2419 pass / 0 fail; typecheck clean.
|
|
747
|
+
|
|
748
|
+
## 2026-07-30 (fifth pass): the worker CONTRACT now explains what the scratchpad is FOR
|
|
749
|
+
|
|
750
|
+
The hooks made the scratchpad load-bearing, but the shipped worker contract still described it as a
|
|
751
|
+
place that "survives compaction" and handed over a path. That undersells it in the way that matters:
|
|
752
|
+
a worker told "here is a file that survives compaction" writes a task list; a worker told "this is the
|
|
753
|
+
canonical record of the thread and your compaction-survival mechanism" writes the REASONING, which is
|
|
754
|
+
precisely what compaction destroys and what a summary cannot reconstruct.
|
|
755
|
+
|
|
756
|
+
Rewritten in all four places a worker meets the pad, so the framing is consistent wherever it lands:
|
|
757
|
+
|
|
758
|
+
- `workerPrompt.ts` `SCRATCHPAD` (both backends) — now headed "the canonical record of this thread",
|
|
759
|
+
and says plainly WHY: compaction drops the reasoning first (the plan, the alternatives ruled out,
|
|
760
|
+
why the human chose what they chose), a summary preserves what you did and not why, so write that
|
|
761
|
+
here AS YOU GO and re-read the file after any compaction or resume. Enumerates what belongs in it.
|
|
762
|
+
The claude variant keeps the sub-agent blackboard job and now states that helpers READ but never
|
|
763
|
+
EDIT it — the one-scratchpad rule, said where the worker actually reads it.
|
|
764
|
+
- `dispatch.ts` `scratchpadOrientation()` (system-level, rebuilt on every resume) and the first
|
|
765
|
+
user-message line — same framing, one sentence each.
|
|
766
|
+
- `dispatch.ts` `scratchpadContent()` — the provisioned skeleton's own orientation line.
|
|
767
|
+
- `cc-worker/hooks/session-seed.mjs` — the runtime `SCRATCHPAD:` line.
|
|
768
|
+
|
|
769
|
+
**Deliberately NOT promised: automatic re-injection.** The prompt says fray "helps by feeding the head
|
|
770
|
+
of this file back into your context", and the IMPERATIVE it gives is unconditional — re-read the file
|
|
771
|
+
yourself after any compaction or resume. A contract that leans on a runtime guarantee degrades badly
|
|
772
|
+
wherever that channel is absent, and one such gap is known: codex's `thread/resume` does NOT re-send
|
|
773
|
+
the per-conversation `config`, so a COLD-resumed codex thread (fray restart, app-server daemon death)
|
|
774
|
+
may lose its hooks. Unverified either way — flagged, not assumed.
|
|
775
|
+
|
|
776
|
+
Fallout fixed in the same change (the hook's own template detector): `substanceLength()` recognised the
|
|
777
|
+
provisioned skeleton by a `^(Your|SCRATCHPAD:)` prefix plus the old wording. The new orientation line
|
|
778
|
+
starts differently, so a freshly provisioned pad read as WRITTEN — which made an empty pad skip its
|
|
779
|
+
re-grounding branch and made the summarizer swallow a skeleton. It now matches on the concept
|
|
780
|
+
(`compaction-survival mechanism|compaction-proof working memory`), which is also backward compatible
|
|
781
|
+
with pads already on disk under the old wording.
|
|
782
|
+
|
|
783
|
+
Goldens regenerated (a deliberate contract change); the diff is the scratchpad section and nothing
|
|
784
|
+
else. Two `dispatch.test.ts` assertions re-anchored — they pin the claude-keeps/codex-drops blackboard
|
|
785
|
+
asymmetry and were using the retired phrase as their anchor; the behavior they check is unchanged.
|
|
786
|
+
Suite 2413 pass / 0 fail, typecheck clean.
|
|
787
|
+
|
|
788
|
+
## 2026-07-30 (sixth pass): keep the helper epilogue universal
|
|
789
|
+
|
|
790
|
+
`hooks/agent-dispatch.mjs` appends its epilogue to every Claude `Agent` helper launched by a fray-ui
|
|
791
|
+
worker, regardless of the repository or kind of task. That makes it the wrong layer for requirements
|
|
792
|
+
about compilation, shared build locks, build/test ownership, or how long-running operations should
|
|
793
|
+
be managed.
|
|
794
|
+
|
|
795
|
+
The epilogue keeps only universal coordination: return a useful handoff, do not mutate the owning
|
|
796
|
+
worker's `.fray/` state unless explicitly assigned, and use the always-available `SendMessage`
|
|
797
|
+
upward channel when the dispatcher acting mid-flight could change the outcome. Task-specific
|
|
798
|
+
verification and process-lifecycle instructions belong in the dispatch prompt; repository-specific
|
|
799
|
+
ones belong in the repository's own guidance. Background dispatch enforcement and `name` /
|
|
800
|
+
`team_name` stripping are unchanged.
|
|
801
|
+
|
|
802
|
+
Correction during implementation: the first reduction also removed `SendMessage`. That was too
|
|
803
|
+
aggressive. Every helper receives the tool, and reaching the dispatcher before returning is
|
|
804
|
+
universally useful orchestration rather than repository policy. The restored wording treats it as
|
|
805
|
+
available directly and therefore drops the old deferred-tool / `ToolSearch` caveat.
|
|
806
|
+
|
|
807
|
+
## 2026-07-30 (sixth pass): the operator's Settings instructions move to the SYSTEM prompt
|
|
808
|
+
|
|
809
|
+
Question from the maintainer: is FRAY.md eagerly loaded, and did the Settings prompt get replaced by
|
|
810
|
+
it? Answer, from the code: **both exist, neither replaced the other** — and they were being treated
|
|
811
|
+
very differently.
|
|
812
|
+
|
|
813
|
+
- **FRAY.md** → `frayConfigBlock(projectDir)` → `extraSystemPrompt` at EVERY site (claude dispatch,
|
|
814
|
+
codex dispatch, adopt, resume, broker follow-up, router follow-up). System-level, so it already
|
|
815
|
+
survives compaction and is rebuilt on every resume. Nothing to fix.
|
|
816
|
+
- **The Settings preamble** (drawer label "Subagent instructions", schema field `dispatchPreamble`) →
|
|
817
|
+
`composePrompt()` → the **FIRST USER MESSAGE** only. Never re-applied on resume, and the first user
|
|
818
|
+
message is exactly what compaction replaces with a summary. So the repo's conventions survived while
|
|
819
|
+
the operator's own standing instructions quietly did not.
|
|
820
|
+
|
|
821
|
+
Fixed by giving it the same treatment as FRAY.md: new `operatorInstructionsBlock(preamble)`, added to
|
|
822
|
+
the system-prompt composition at all six sites, and REMOVED from `composePrompt` (which loses its
|
|
823
|
+
`customInstructions` parameter). Moved rather than duplicated — leaving it in both would carry a second
|
|
824
|
+
copy of a potentially long preamble in context for the whole pre-compaction window, and the block sits
|
|
825
|
+
ABOVE the task banner either way, so the visible chat bubble is unchanged.
|
|
826
|
+
|
|
827
|
+
Two things fall out for free: an edited setting now reaches a thread that is ALREADY RUNNING (the
|
|
828
|
+
system prompt is rebuilt on every resume), and the block states its relationship to FRAY.md — follow
|
|
829
|
+
both, prefer the more specific where they genuinely conflict — so a worker meeting two operator-authored
|
|
830
|
+
surfaces knows how to reconcile them.
|
|
831
|
+
|
|
832
|
+
VERIFIED with a real `/compact`: a sentinel placed in `--append-system-prompt` was still readable by the
|
|
833
|
+
model after the compaction boundary (`compact_boundary` present in the transcript, model returned
|
|
834
|
+
`SORREL-EGRET-4402`). That is the property the move depends on, tested rather than assumed.
|
|
835
|
+
|
|
836
|
+
One test needed real rework rather than a signature patch: `server.test.ts` asserted the preamble
|
|
837
|
+
appeared in the composed first message — the exact behavior being moved — so it now asserts the
|
|
838
|
+
opposite plus a dedicated `operatorInstructionsBlock` test. A second (`dispatch.test.ts`'s banner test)
|
|
839
|
+
used `PROJECT INSTRUCTIONS` as its above-the-banner anchor; on an ABSENT string `indexOf(...) < banner`
|
|
840
|
+
passes VACUOUSLY (-1 < banner), so it was re-anchored on the scratchpad line and given an explicit
|
|
841
|
+
`doesNotMatch` instead. Suite 2460 pass / 0 fail, typecheck clean.
|
|
842
|
+
|
|
843
|
+
## 2026-07-30 (seventh pass): converge on FRAY.md — the Settings preamble is GONE
|
|
844
|
+
|
|
845
|
+
Maintainer's call, one pass after making the preamble durable: rather than maintain two
|
|
846
|
+
operator-authored surfaces, keep the one that is already versioned with the repo. `dispatchPreamble`
|
|
847
|
+
(drawer label "Subagent instructions") is deleted outright — schema, server default, the drawer field
|
|
848
|
+
and its draft plumbing, `operatorInstructionsBlock` and all six of its call sites.
|
|
849
|
+
|
|
850
|
+
**FRAY.md is now the ONLY place project conventions live.** It was already the better surface and
|
|
851
|
+
needed no work: `frayConfigBlock()` puts it in `extraSystemPrompt` at every site (claude dispatch,
|
|
852
|
+
codex dispatch, adopt, resume, broker follow-up, router follow-up), so it survives compaction and is
|
|
853
|
+
rebuilt on every resume. It is also reviewable in a diff, travels with a clone, and can differ per
|
|
854
|
+
branch — none of which a value in a local SQLite settings blob can do.
|
|
855
|
+
|
|
856
|
+
Checked before deleting rather than after: `DEFAULT_PREAMBLE` shipped as `""`, and a read of every
|
|
857
|
+
`~/.fray/projects/*/ui.db` found no project with a non-empty `dispatchPreamble`. So there is nothing to
|
|
858
|
+
migrate and no operator text is silently dropped. `Settings` is a plain `z.object`, so an older blob
|
|
859
|
+
that still carries the key parses fine — zod strips the unknown field.
|
|
860
|
+
|
|
861
|
+
Fallout fixed in the same change, since it was this change that made them false: two dispatch.ts
|
|
862
|
+
header comments still described the preamble as the prompt's "orchestration wisdom", and
|
|
863
|
+
`SettingsDrawer.test.ts` iterated a help-key list containing `subagentInstructions`. The
|
|
864
|
+
`operatorInstructionsBlock` test added one pass earlier was removed with the function.
|
|
865
|
+
|
|
866
|
+
Suite 2419 pass / 0 fail (two timing-sensitive tests — `app-socket` coalescing and the tmux SIGKILL
|
|
867
|
+
buffer — flaked under parallel load and pass in isolation; neither touches this change), typecheck
|
|
868
|
+
clean, and the drawer was re-driven in a real browser: the field is gone, and the Prompts section
|
|
869
|
+
measures as exactly two children at the standard 24px `gap-6` with no orphaned container left behind.
|
|
870
|
+
|
|
871
|
+
## 2026-07-30 (eighth pass): native Codex children merge the shared scratchpad
|
|
872
|
+
|
|
873
|
+
An apparent post-compaction continuity failure in thread `9540edc3-4807-4bb0-8e36-5940f92b452b`
|
|
874
|
+
was not path loss and was not Claude's on-disk token broker. The affected thread was a Codex session.
|
|
875
|
+
At its first compaction the worker immediately read the correct exact path; at a later compaction it
|
|
876
|
+
again addressed the correct path and got `ENOENT`. The directory and sibling artifacts remained.
|
|
877
|
+
|
|
878
|
+
The missing file was child corruption. Native child `/root/auditstatus_prepare`, launched with
|
|
879
|
+
`fork_turns:"none"`, still inherited the parent's developer-level `SCRATCHPAD:` mandate. It reasoned
|
|
880
|
+
"Planning isolated scratch creation" and replaced the parent's exact pad with its own `# SEA corpus
|
|
881
|
+
preparation` notes. Two minutes later it noticed the mistake, reasoned "Reverting unauthorized root
|
|
882
|
+
scratch changes", and tried to remove its replacement. A shell `rm -f` was denied, but its fallback
|
|
883
|
+
`apply_patch` `*** Delete File` succeeded. It could not restore the content it had overwritten, so the
|
|
884
|
+
pad remained absent until the root compacted nearly an hour later. `fork_turns:"none"` removes
|
|
885
|
+
conversational turns; it does not remove the root worker's base/developer instructions.
|
|
886
|
+
|
|
887
|
+
The deliberately prompt-level fix preserves the useful part of that inheritance:
|
|
888
|
+
|
|
889
|
+
- The Codex worker contract, system orientation, and first task message now make the pad explicitly
|
|
890
|
+
collaborative. Children may read it and persist their own progress, but every edit is a merge:
|
|
891
|
+
re-read first, patch only a scoped agent/task section, and preserve all existing state. Every
|
|
892
|
+
native-child dispatch restates that rule.
|
|
893
|
+
- Codex registers `scratchpad.mjs --mode=subagent-start` as a `SubagentStart` hook. It injects the
|
|
894
|
+
child-only epilogue structurally on every native dispatch: the child may update its scoped progress,
|
|
895
|
+
but may never delete, truncate, reinitialize, move, or replace the whole file — including as cleanup
|
|
896
|
+
or rollback after a mistake. This fixes the observed failure without a filesystem guard or a
|
|
897
|
+
single-writer restriction.
|
|
898
|
+
- Newly provisioned pads recommend a light structure rather than enforce a machine schema: Goal,
|
|
899
|
+
Task list, Decisions, Shared context, per-agent progress, Verification, and Next action. A visible
|
|
900
|
+
legend uses `[ ]` pending, `[/]` in progress, `[x]` complete, `[-]` cancelled, and `[?]` blocked.
|
|
901
|
+
Fray's Markdown renderer recognizes all five while the source stays readable in Obsidian/plain text.
|
|
902
|
+
- If a pad is unexpectedly absent or empty after compaction/resume, the re-grounding injection says
|
|
903
|
+
the exact path is authoritative, forbids searching neighboring thread pads or broadly reloading repo
|
|
904
|
+
docs, and tells the root to reconstruct from the retained summary plus directly named handoffs.
|
|
905
|
+
|
|
906
|
+
The root's apparently confused recovery is now explained too. The post-compaction hook correctly
|
|
907
|
+
reported that the pad had no substantive content, and the root's first call addressed the correct
|
|
908
|
+
exact path and got `ENOENT`. Only then did it search worktrees, list the surviving thread directory,
|
|
909
|
+
and read older handoffs to reconstruct state. Its "Writing initial scratch file" reasoning label was
|
|
910
|
+
model narration for recreating a currently absent file, not evidence that no pad had existed before.
|
|
911
|
+
|
|
912
|
+
## 2026-07-31: shared scratchpad updates are expected, not merely permitted
|
|
913
|
+
|
|
914
|
+
The collaborative contract above said children *may* update the shared scratchpad. A root worker then
|
|
915
|
+
treated batch file ownership and a dynamic-orchestrator mega-doc's single-writer rule as reasons to
|
|
916
|
+
centralize scratchpad writes and even make the pad immutable. That was the opposite of the intended
|
|
917
|
+
blackboard pattern.
|
|
918
|
+
|
|
919
|
+
The worker contract, dispatch orientation, Codex child hook, and Claude dispatch epilogue now say each
|
|
920
|
+
sub-agent should merge its own scoped progress as it works. Deliverable file ownership never includes
|
|
921
|
+
the Fray scratchpad, and a root must reconcile concurrent scoped updates rather than act as sole
|
|
922
|
+
writer. The existing merge-safety rules remain unchanged: re-read first, preserve all other content,
|
|
923
|
+
and never delete, truncate, reinitialize, move, or replace the pad.
|