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,285 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
// Structured fray-thread updater. A superset of the single-op Edit tool: structured
|
|
4
|
+
// frontmatter inputs (--status/--status-text/--set) + a multi-patch body editor
|
|
5
|
+
// (--patch, repeatable) + --append. Writes the project's .fray/<slug>.md atomically,
|
|
6
|
+
// preserving every byte outside the keys/regions it touches.
|
|
7
|
+
//
|
|
8
|
+
// Exposed as the `fray-update` command (bin/fray-update is on the Bash PATH while the
|
|
9
|
+
// plugin is enabled). The project root is resolved from CLAUDE_PROJECT_DIR (exported to
|
|
10
|
+
// bin/hook processes), matching how bin/fray + index.mjs find the project's .fray/.
|
|
11
|
+
//
|
|
12
|
+
// Enforced invariant: setting `status: blocked` on a HUMAN-blocked thread (no `blocking_threads`/
|
|
13
|
+
// `depends_on` and no `revalidate_at`) REQUIRES a non-empty status_text (the decision write-up).
|
|
14
|
+
// The ⚖ awaiting-you queue DERIVES from those status_text fields (see decisions.mjs), so a
|
|
15
|
+
// human-blocked thread without a write-up would surface as an empty queue row. A MACHINE/timer-
|
|
16
|
+
// blocked thread carries its mechanism field instead and needs no write-up. After every edit
|
|
17
|
+
// this tool prints the FULL queue (collectDecisions) so a pending decision is never silently
|
|
18
|
+
// buried. Legacy status spellings (todo/plan → planned; enqueued/needs-decision → blocked) are
|
|
19
|
+
// ACCEPTED and normalized to canonical on write.
|
|
20
|
+
import { readFileSync, writeFileSync, existsSync, renameSync } from 'node:fs';
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
import { collectDecisions } from './decisions.mjs';
|
|
23
|
+
import { STATUS, ACCEPTED_STATUSES, isValidStatus, normalizeStatus, parseDeps } from './config.mjs';
|
|
24
|
+
|
|
25
|
+
// The status vocabulary is the SINGLE shared source from config.mjs (the same set the
|
|
26
|
+
// board validates against) — importing it keeps the updater and the board from drifting.
|
|
27
|
+
// The usage line shows the CANONICAL set; the legacy aliases are accepted but not advertised.
|
|
28
|
+
const STATUSES = STATUS;
|
|
29
|
+
const PATCH_SEP = '===>>';
|
|
30
|
+
// The decision write-up lives in the `status_text` frontmatter key — the single canonical
|
|
31
|
+
// spelling, read and written everywhere (the board reads the same key).
|
|
32
|
+
const STATUS_TEXT_KEY = 'status_text';
|
|
33
|
+
|
|
34
|
+
// The project root comes from the environment, NOT this script's own path: the tool
|
|
35
|
+
// ships inside the fray PLUGIN, so a script-relative root would point at the PLUGIN,
|
|
36
|
+
// never the project. CLAUDE_PROJECT_DIR is exported to bin/hook processes; when run by
|
|
37
|
+
// hand from the repo root, process.cwd() is correct.
|
|
38
|
+
const root = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
39
|
+
|
|
40
|
+
function die(msg) {
|
|
41
|
+
console.error(`error: ${msg}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const usage = `usage: fray-update <slug> [options]
|
|
46
|
+
--status <s> set status; one of: ${STATUSES.join(' · ')}
|
|
47
|
+
(legacy todo/plan/enqueued/needs-decision accepted → normalized to canonical)
|
|
48
|
+
(human-blocked — blocked with no blocking_threads/revalidate_at — REQUIRES a status_text)
|
|
49
|
+
--status-text "<text>" set the status_text field (decision write-up / gloss)
|
|
50
|
+
--set key=value set any other frontmatter scalar (repeatable)
|
|
51
|
+
--patch "<find>${PATCH_SEP}<replace>" body find/replace, must match EXACTLY once (repeatable, applied in order, atomic)
|
|
52
|
+
--append "<text>" append text to the body`;
|
|
53
|
+
|
|
54
|
+
function parseArgs(argv) {
|
|
55
|
+
const out = { slug: undefined, status: undefined, status_text: undefined, sets: [], patches: [], appends: [] };
|
|
56
|
+
let i = 0;
|
|
57
|
+
for (; i < argv.length; i++) {
|
|
58
|
+
const a = argv[i];
|
|
59
|
+
const needVal = (name) => {
|
|
60
|
+
if (i + 1 >= argv.length) die(`${name} requires a value`);
|
|
61
|
+
return argv[++i];
|
|
62
|
+
};
|
|
63
|
+
switch (a) {
|
|
64
|
+
case '--status': out.status = needVal('--status'); break;
|
|
65
|
+
case '--status-text': out.status_text = needVal('--status-text'); break;
|
|
66
|
+
case '--set': out.sets.push(needVal('--set')); break;
|
|
67
|
+
case '--patch': out.patches.push(needVal('--patch')); break;
|
|
68
|
+
case '--append': out.appends.push(needVal('--append')); break;
|
|
69
|
+
case '-h': case '--help': console.log(usage); process.exit(0); break;
|
|
70
|
+
default:
|
|
71
|
+
if (a.startsWith('-')) die(`unknown flag: ${a}`);
|
|
72
|
+
if (out.slug !== undefined) die(`unexpected positional: ${a} (slug already set to "${out.slug}")`);
|
|
73
|
+
out.slug = a;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function today() {
|
|
80
|
+
const d = new Date();
|
|
81
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
82
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Quote a frontmatter scalar value the way the existing threads do: double-quoted
|
|
86
|
+
// with inner double-quotes escaped. Bare safe scalars (the empty-bracket list,
|
|
87
|
+
// plain dates/words) are left unquoted to match the on-disk convention.
|
|
88
|
+
function quoteValue(v) {
|
|
89
|
+
if (/^\[.*\]$/.test(v.trim())) return v.trim(); // list literal, e.g. depends_on: []
|
|
90
|
+
if (/^[\w./#:+-]+$/.test(v)) return v; // bare safe scalar (date, single word, slug)
|
|
91
|
+
return `"${v.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Split the file into [frontmatter-lines, body-string]. Frontmatter is the block
|
|
95
|
+
// between the leading `---` and the next `---`. Returns null fm if absent.
|
|
96
|
+
function splitFrontmatter(text) {
|
|
97
|
+
const lines = text.split('\n');
|
|
98
|
+
if (lines[0] !== '---') return { fm: null, fmEnd: 0, body: text };
|
|
99
|
+
let end = -1;
|
|
100
|
+
for (let i = 1; i < lines.length; i++) {
|
|
101
|
+
if (lines[i] === '---') { end = i; break; }
|
|
102
|
+
}
|
|
103
|
+
if (end === -1) return { fm: null, fmEnd: 0, body: text };
|
|
104
|
+
const fm = lines.slice(1, end);
|
|
105
|
+
const body = lines.slice(end + 1).join('\n');
|
|
106
|
+
return { fm, fmEnd: end, body };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function fmGet(fm, key) {
|
|
110
|
+
const re = new RegExp(`^${key}:\\s*(.*)$`);
|
|
111
|
+
for (const line of fm) {
|
|
112
|
+
const m = line.match(re);
|
|
113
|
+
if (m) return m[1];
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Set a frontmatter key in place (preserving line order); append if absent.
|
|
119
|
+
function fmSet(fm, key, rawValue) {
|
|
120
|
+
const value = quoteValue(rawValue);
|
|
121
|
+
const re = new RegExp(`^${key}:\\s*`);
|
|
122
|
+
for (let i = 0; i < fm.length; i++) {
|
|
123
|
+
if (re.test(fm[i])) { fm[i] = `${key}: ${value}`; return fm; }
|
|
124
|
+
}
|
|
125
|
+
fm.push(`${key}: ${value}`);
|
|
126
|
+
return fm;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Write the decision write-up to the canonical `status_text` key — the only key the board
|
|
130
|
+
// (index.mjs) reads to render the `» gloss` line and to gate the drop-risk warning.
|
|
131
|
+
function setStatusText(fm, rawValue) {
|
|
132
|
+
return fmSet(fm, STATUS_TEXT_KEY, rawValue);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function getStatusText(fm) {
|
|
136
|
+
return fmGet(fm, STATUS_TEXT_KEY);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// A status_text value counts as "present" only if it's a non-empty string.
|
|
140
|
+
function isStatusTextNonEmpty(raw) {
|
|
141
|
+
if (raw === undefined) return false;
|
|
142
|
+
let v = raw.trim();
|
|
143
|
+
const m = v.match(/^"((?:[^"\\]|\\.)*)"$/);
|
|
144
|
+
if (m) v = m[1].replace(/\\(.)/g, '$1');
|
|
145
|
+
return v.trim().length > 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function main() {
|
|
149
|
+
const args = parseArgs(process.argv.slice(2));
|
|
150
|
+
if (!args.slug) { console.log(usage); process.exit(args.slug === undefined && process.argv.length <= 2 ? 0 : 1); }
|
|
151
|
+
|
|
152
|
+
const path = join(root, '.fray', `${args.slug}.md`);
|
|
153
|
+
if (!existsSync(path)) die(`no thread at ${path}`);
|
|
154
|
+
|
|
155
|
+
const original = readFileSync(path, 'utf8');
|
|
156
|
+
const { fm, body } = splitFrontmatter(original);
|
|
157
|
+
if (fm === null) die(`thread ${args.slug}.md has no YAML frontmatter block`);
|
|
158
|
+
|
|
159
|
+
// --- Validate status + the needs-human/status_text invariant BEFORE writing ---
|
|
160
|
+
// Accept canonical OR a legacy alias, then NORMALIZE to canonical so the on-disk value is
|
|
161
|
+
// always canonical going forward (e.g. `--status needs-decision` writes `needs-human`,
|
|
162
|
+
// `--status enqueued` writes `blocked`).
|
|
163
|
+
if (args.status !== undefined && !isValidStatus(args.status)) {
|
|
164
|
+
die(`invalid status "${args.status}"; must be one of: ${ACCEPTED_STATUSES.join(' · ')}`);
|
|
165
|
+
}
|
|
166
|
+
const canonicalStatus = args.status !== undefined ? normalizeStatus(args.status) : undefined;
|
|
167
|
+
// A `needs-human` thread — OR a `blocked` thread with NO machine field, which READS as
|
|
168
|
+
// needs-human (config.effectiveStatus) — IS the ⚖ awaiting-you queue entry, so it REQUIRES a
|
|
169
|
+
// status_text ask. A `blocked` thread WITH a `blocking_threads`/`revalidate_at` field is a
|
|
170
|
+
// machine/timer wait and needs no write-up. We look at the fields being SET this call PLUS
|
|
171
|
+
// what's already on the thread.
|
|
172
|
+
if (canonicalStatus === 'needs-human' || canonicalStatus === 'blocked') {
|
|
173
|
+
// A key counts as a machine field only when its EFFECTIVE post-write value is non-empty —
|
|
174
|
+
// the `--set` value if this call sets the key, else what's already on disk. Checking mere
|
|
175
|
+
// key-PRESENCE would let `--set blocking_threads=[]` / `--set revalidate_at=` bypass the
|
|
176
|
+
// invariant. Empty string, `[]`, and `[ ]` are all "no machine field."
|
|
177
|
+
// Deps: a `--set` on this call wins (inline value); otherwise read the ON-DISK deps via the
|
|
178
|
+
// shared block-form-aware parseDeps (a flat fmGet would miss a YAML block-form list and falsely
|
|
179
|
+
// refuse a legitimately machine-blocked thread — matches the board/decisions readers).
|
|
180
|
+
const setDep = args.sets.find((kv) => {
|
|
181
|
+
const k = kv.slice(0, kv.indexOf('=')).trim();
|
|
182
|
+
return k === 'blocking_threads' || k === 'depends_on';
|
|
183
|
+
});
|
|
184
|
+
const hasDeps =
|
|
185
|
+
setDep !== undefined
|
|
186
|
+
? (() => {
|
|
187
|
+
const v = setDep.slice(setDep.indexOf('=') + 1).trim().replace(/^["']|["']$/g, '').trim();
|
|
188
|
+
return v !== '' && v.replace(/\s+/g, '') !== '[]';
|
|
189
|
+
})()
|
|
190
|
+
: parseDeps(original).length > 0;
|
|
191
|
+
const setRv = args.sets.find((kv) => kv.slice(0, kv.indexOf('=')).trim() === 'revalidate_at');
|
|
192
|
+
const rvRaw = setRv !== undefined ? setRv.slice(setRv.indexOf('=') + 1) : fmGet(fm, 'revalidate_at');
|
|
193
|
+
const hasTimerField = (rvRaw ?? '').trim().replace(/^["']|["']$/g, '').trim() !== '';
|
|
194
|
+
const hasMachineField = hasDeps || hasTimerField;
|
|
195
|
+
// Effective needs-human = the word `needs-human`, OR `blocked` with no machine field.
|
|
196
|
+
if (canonicalStatus === 'needs-human' || !hasMachineField) {
|
|
197
|
+
const willHaveStatusText = args.status_text !== undefined
|
|
198
|
+
? isStatusTextNonEmpty(`"${args.status_text}"`) || args.status_text.trim().length > 0
|
|
199
|
+
: isStatusTextNonEmpty(getStatusText(fm));
|
|
200
|
+
if (!willHaveStatusText) {
|
|
201
|
+
die(canonicalStatus === 'needs-human'
|
|
202
|
+
? 'a `needs-human` thread REQUIRES a status_text stating the ask — pass --status-text "<the decision/ask needed>"'
|
|
203
|
+
: 'a `blocked` thread with no `blocking_threads`/`revalidate_at` reads as needs-human and REQUIRES a status_text — pass --status-text "<the ask>", add a machine field to make it a real machine wait, or use --status needs-human');
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// --- Apply body patches (atomic: validate all, then apply) ---
|
|
209
|
+
const patchOps = args.patches.map((p, idx) => {
|
|
210
|
+
const sepAt = p.indexOf(PATCH_SEP);
|
|
211
|
+
if (sepAt === -1) die(`--patch #${idx + 1} missing "${PATCH_SEP}" separator`);
|
|
212
|
+
return { find: p.slice(0, sepAt), replace: p.slice(sepAt + PATCH_SEP.length), idx };
|
|
213
|
+
});
|
|
214
|
+
for (const op of patchOps) {
|
|
215
|
+
if (op.find === '') die(`--patch #${op.idx + 1} has an empty find string`);
|
|
216
|
+
const count = body.split(op.find).length - 1;
|
|
217
|
+
if (count === 0) die(`--patch #${op.idx + 1} find string not found in body (no patches applied)`);
|
|
218
|
+
if (count > 1) die(`--patch #${op.idx + 1} find string occurs ${count} times, must be unique (no patches applied)`);
|
|
219
|
+
}
|
|
220
|
+
let newBody = body;
|
|
221
|
+
for (const op of patchOps) newBody = newBody.replace(op.find, op.replace);
|
|
222
|
+
|
|
223
|
+
// --- Apply appends ---
|
|
224
|
+
for (const text of args.appends) {
|
|
225
|
+
const sep = newBody.endsWith('\n') ? '' : '\n';
|
|
226
|
+
newBody = `${newBody}${sep}${text}${text.endsWith('\n') ? '' : '\n'}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// --- Apply frontmatter edits ---
|
|
230
|
+
let lastUpdateExplicit = false;
|
|
231
|
+
if (args.status !== undefined) fmSet(fm, 'status', canonicalStatus); // write CANONICAL, never the legacy alias
|
|
232
|
+
if (args.status_text !== undefined) setStatusText(fm, args.status_text);
|
|
233
|
+
for (const kv of args.sets) {
|
|
234
|
+
const eq = kv.indexOf('=');
|
|
235
|
+
if (eq === -1) die(`--set requires key=value (got "${kv}")`);
|
|
236
|
+
const key = kv.slice(0, eq).trim();
|
|
237
|
+
const value = kv.slice(eq + 1);
|
|
238
|
+
if (!key) die(`--set has an empty key (got "${kv}")`);
|
|
239
|
+
if (key === 'last_update') lastUpdateExplicit = true;
|
|
240
|
+
fmSet(fm, key, value);
|
|
241
|
+
}
|
|
242
|
+
// Auto-stamp last_update unless explicitly set. Only when SOMETHING changed.
|
|
243
|
+
const mutated = args.status !== undefined || args.status_text !== undefined || args.sets.length || patchOps.length || args.appends.length;
|
|
244
|
+
if (mutated && !lastUpdateExplicit) fmSet(fm, 'last_update', today());
|
|
245
|
+
|
|
246
|
+
// body is everything after the closing `---` line (its join started at end+1),
|
|
247
|
+
// so it carries its own leading newline(s) — emit it verbatim after `---\n` to
|
|
248
|
+
// preserve the exact blank-line layout byte-for-byte.
|
|
249
|
+
const out = `---\n${fm.join('\n')}\n---\n${newBody}`;
|
|
250
|
+
|
|
251
|
+
// Atomic write: temp file in the same dir, then rename.
|
|
252
|
+
const tmp = `${path}.tmp.${process.pid}`;
|
|
253
|
+
writeFileSync(tmp, out);
|
|
254
|
+
renameSync(tmp, path);
|
|
255
|
+
|
|
256
|
+
// --- Report ---
|
|
257
|
+
const finalStatus = fmGet(fm, 'status') ?? '(unset)';
|
|
258
|
+
const finalStatusText = getStatusText(fm);
|
|
259
|
+
console.log(`updated .fray/${args.slug}.md`);
|
|
260
|
+
console.log(` status: ${finalStatus}`);
|
|
261
|
+
if (finalStatusText !== undefined) {
|
|
262
|
+
const display = finalStatusText.replace(/^"(.*)"$/, '$1');
|
|
263
|
+
console.log(` status_text: ${display.length > 120 ? display.slice(0, 117) + '…' : display}`);
|
|
264
|
+
}
|
|
265
|
+
if (patchOps.length) console.log(` patches applied: ${patchOps.length}`);
|
|
266
|
+
if (args.appends.length) console.log(` appended: ${args.appends.length} block(s)`);
|
|
267
|
+
|
|
268
|
+
// Always surface the FULL decisions queue after ANY thread edit — so every fray
|
|
269
|
+
// edit-tool call prints the current decision write-ups straight to the terminal,
|
|
270
|
+
// not just a one-line summary of the thread that changed.
|
|
271
|
+
const decisions = collectDecisions();
|
|
272
|
+
console.log('');
|
|
273
|
+
if (decisions.length === 0) {
|
|
274
|
+
console.log('⚖ no pending decisions');
|
|
275
|
+
} else {
|
|
276
|
+
console.log(`⚖ ${decisions.length} decision(s) awaiting you:\n`);
|
|
277
|
+
decisions.forEach((d, i) => {
|
|
278
|
+
console.log(`[${d.slug}]`);
|
|
279
|
+
console.log(d.status_text || '(no status_text written up)');
|
|
280
|
+
if (i < decisions.length - 1) console.log('');
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
main();
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fray",
|
|
3
|
+
"description": "Worker-side fray plugin for fray-ui-spawned sessions. Each session is a WORKER driving ONE effort (slug via env FRAY_UI_THREAD): it SIGNALS through its final message (done, human/timer awaiting, question), keeps automatable waits active with Claude wait tools, and PERSISTS through a scratchpad blackboard (.fray/threads/<session-id>/scratch.md) with an optional stop_hook reminder. Supplies the fray:<model>-<effort> sub-agent profiles (dispatch as fray:opus-high, fray:sonnet-medium, fray:haiku, …), the on-demand fray:gh skill, and hooks that seed the worker contract, enforce background/name-stripped sub-agent dispatch, and deny the interactive question/plan prompts (deny-ask, deny-plan) that would hang a dashboard worker. Hooks are inert unless FRAY_UI_THREAD is set.",
|
|
4
|
+
"version": "0.1.10",
|
|
5
|
+
"author": { "name": "Colin McDonnell" },
|
|
6
|
+
"homepage": "https://github.com/colinhacks/fray",
|
|
7
|
+
"repository": "https://github.com/colinhacks/fray",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"keywords": ["orchestration", "agents", "fray", "fray-ui", "worker", "claude-code"]
|
|
10
|
+
}
|