agentbox-app 0.1.12 → 0.1.13
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/main/acp.mjs +296 -0
- package/main/agent-homes.mjs +158 -0
- package/main/agent-sessions.mjs +57 -20
- package/main/agent-updates.mjs +9 -22
- package/main/analytics.mjs +30 -0
- package/main/claude-bin.mjs +45 -311
- package/main/claude-input.mjs +9 -3
- package/main/claude-launch-env.mjs +54 -0
- package/main/claude-models.mjs +39 -7
- package/main/claude-plan.mjs +16 -16
- package/main/claude-usage.mjs +82 -25
- package/main/codex-bin.mjs +51 -249
- package/main/codex-session.mjs +38 -3
- package/main/codex-threads.mjs +41 -8
- package/main/codex-watch.mjs +67 -3
- package/main/config.mjs +26 -4
- package/main/corner-tag.mjs +489 -0
- package/main/crash-report.mjs +26 -3
- package/main/engine-bin.mjs +595 -0
- package/main/engine-setup.mjs +4 -15
- package/main/feedback.mjs +11 -6
- package/main/harnesses/claude-stream.mjs +208 -0
- package/main/harnesses/claude.mjs +84 -0
- package/main/harnesses/codex.mjs +65 -0
- package/main/harnesses/common.mjs +78 -0
- package/main/harnesses/instructions-file.mjs +116 -0
- package/main/harnesses/opencode.mjs +234 -0
- package/main/harnesses.mjs +65 -0
- package/main/importable-threads.mjs +38 -0
- package/main/ipc.mjs +165 -31
- package/main/local-app-frames.mjs +82 -0
- package/main/main.mjs +100 -4
- package/main/memory-gate-history.mjs +51 -12
- package/main/memory-gate-server.mjs +67 -11
- package/main/memory-gate.mjs +49 -8
- package/main/memory-log.mjs +124 -0
- package/main/notify.mjs +47 -4
- package/main/opencode-server.mjs +271 -0
- package/main/opencode.mjs +287 -0
- package/main/phone-link.mjs +551 -0
- package/main/relay-address.mjs +28 -0
- package/main/relay-client.mjs +246 -0
- package/main/row-label.mjs +6 -6
- package/main/runs-on.mjs +32 -35
- package/main/serve.mjs +113 -9
- package/main/settings.mjs +148 -10
- package/main/shell-path.mjs +86 -0
- package/main/ship-queue.mjs +24 -6
- package/main/sign-in-files.mjs +3 -7
- package/main/source-updater.mjs +27 -1
- package/main/start-hold.mjs +73 -0
- package/main/store.mjs +1 -1
- package/main/supervisor.mjs +832 -428
- package/main/task-commands.mjs +13 -17
- package/main/task-folders-offthread.mjs +6 -1
- package/main/task-folders.mjs +490 -98
- package/main/task-terminals.mjs +37 -7
- package/main/updater.mjs +71 -9
- package/main/worktree-include.mjs +494 -0
- package/package.json +10 -1
- package/renderer/dist/assets/abnfDiagram-YUKMZFIV-DPPuMWt-.js +1 -0
- package/renderer/dist/assets/arc-HmFfdseH.js +1 -0
- package/renderer/dist/assets/architectureDiagram-47P4ROYG-DxJ3zHrH.js +36 -0
- package/renderer/dist/assets/blockDiagram-E7TT5TSB-CbTXzOqQ.js +140 -0
- package/renderer/dist/assets/c4Diagram-CRA5TL53-X5ItFyLn.js +38 -0
- package/renderer/dist/assets/channel-BlT2g9Sz.js +1 -0
- package/renderer/dist/assets/chunk-2Q5K7J3B-DC12Ho_L.js +1 -0
- package/renderer/dist/assets/chunk-5VM5RSS4-ClVtfPd0.js +15 -0
- package/renderer/dist/assets/chunk-7TKQ45FW-DgcOm0HT.js +1 -0
- package/renderer/dist/assets/chunk-AW2ZBBNX-C01s4yoF.js +88 -0
- package/renderer/dist/assets/chunk-FVRAUYC3-Bj0zxhR4.js +2 -0
- package/renderer/dist/assets/chunk-HTAEGDNF-DWDeEhLU.js +1 -0
- package/renderer/dist/assets/chunk-JWPE2WC7-Dsqwv3eW.js +1 -0
- package/renderer/dist/assets/chunk-KQW6MTUR-CE86ANL2.js +1 -0
- package/renderer/dist/assets/chunk-XXDRQBXY-CoNTASfs.js +1 -0
- package/renderer/dist/assets/classDiagram-v2-K4WV4PDN-2wprLqtw.js +217 -0
- package/renderer/dist/assets/cornerTag-BLrxiDE5.css +1 -0
- package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +1 -0
- package/renderer/dist/assets/cose-bilkent-JH36ORCC-enqMiYbn.js +1 -0
- package/renderer/dist/assets/cynefin-EF2NZ3EQ-DHngLxvQ.js +141 -0
- package/renderer/dist/assets/cynefinDiagram-3GCD6N5R-C0rZZIl3.js +62 -0
- package/renderer/dist/assets/cytoscape.esm-C5JX3QLV.js +331 -0
- package/renderer/dist/assets/dagre-W4DXFKR2-DQnaEWO7.js +4 -0
- package/renderer/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/renderer/dist/assets/diagram-2UJZ2QOL-CS8AlGPx.js +200 -0
- package/renderer/dist/assets/diagram-OVF4WLC6-yiDMHIkY.js +24 -0
- package/renderer/dist/assets/diagram-PFPMY2P6-XbQKzbBA.js +30 -0
- package/renderer/dist/assets/diagram-UMYRVEAY-dhVvlfl_.js +24 -0
- package/renderer/dist/assets/diagram-YEKJPTXX-DZHe8SCf.js +41 -0
- package/renderer/dist/assets/diagram-ZIFT7M5P-B5h6aXVR.js +3 -0
- package/renderer/dist/assets/ebnfDiagram-VR2GEFS7-PR9ScDco.js +1 -0
- package/renderer/dist/assets/elk-IJKZMXRS-Cg-yQXPd.js +27 -0
- package/renderer/dist/assets/erDiagram-O2IAPWRE-omOrJMxu.js +99 -0
- package/renderer/dist/assets/flowDiagram-OXPTDLAJ-aOX2hzvT.js +213 -0
- package/renderer/dist/assets/ganttDiagram-R7TSEDQI-B1H3pWyI.js +292 -0
- package/renderer/dist/assets/gitGraphDiagram-XJZIOB7I-Cvw304HC.js +106 -0
- package/renderer/dist/assets/graph-DGbsTEsI.js +1 -0
- package/renderer/dist/assets/infoDiagram-5W2HQ5XZ-_ZXcNViH.js +2 -0
- package/renderer/dist/assets/init-Gi6I4Gst.js +1 -0
- package/renderer/dist/assets/ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js +70 -0
- package/renderer/dist/assets/journeyDiagram-COXZFDF6-fr62QoAt.js +139 -0
- package/renderer/dist/assets/kanban-definition-P3RFRI5V-J8zsabCe.js +89 -0
- package/renderer/dist/assets/katex-C5jXJg4s.js +257 -0
- package/renderer/dist/assets/linear-BmNdEtIi.js +1 -0
- package/renderer/dist/assets/main-ChNdnUvt.js +1132 -0
- package/renderer/dist/assets/main-nj0QR5kg.css +32 -0
- package/renderer/dist/assets/mermaid.core-CnYzvFkz.js +314 -0
- package/renderer/dist/assets/mindmap-definition-LZFPQGKD-Bfcxc7PM.js +96 -0
- package/renderer/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/renderer/dist/assets/pegDiagram-BGZESJAR-CwUJOq6X.js +1 -0
- package/renderer/dist/assets/pieDiagram-CAPJLFHJ-BrwMmqfC.js +39 -0
- package/renderer/dist/assets/product-name-CTKLYNWY.js +40 -0
- package/renderer/dist/assets/quadrantDiagram-GDMTTRAM-BN4Stoj-.js +7 -0
- package/renderer/dist/assets/railroadDiagram-SM67HX2B-CPZ5bbeR.js +1 -0
- package/renderer/dist/assets/requirementDiagram-X7JNWC4A-CgRnimVe.js +84 -0
- package/renderer/dist/assets/sankeyDiagram-UM26HJRW-Bt2rej__.js +40 -0
- package/renderer/dist/assets/sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js +169 -0
- package/renderer/dist/assets/sizeCapture-INFHLROL-usyNyXyh.js +1 -0
- package/renderer/dist/assets/stateDiagram-v2-TBUQTH76-CRzUBb0u.js +291 -0
- package/renderer/dist/assets/swimlanes-N4OXWK64-C1GJvMS0.js +1 -0
- package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +8 -0
- package/renderer/dist/assets/timeline-definition-YOQAKGHF-caeyPlcc.js +120 -0
- package/renderer/dist/assets/usecaseDiagram-VIAY4XPW-BN_SV6PC.js +370 -0
- package/renderer/dist/assets/vennDiagram-BLWOH2XV-BLGuaUX-.js +34 -0
- package/renderer/dist/assets/wardleyDiagram-YMQ3BMBF-Da0cJeNc.js +78 -0
- package/renderer/dist/assets/xychartDiagram-TAQBALBS-cGfUffMk.js +7 -0
- package/renderer/dist/corner-tag.html +13 -0
- package/renderer/dist/emulator/audio-rules.mjs +74 -0
- package/renderer/dist/emulator/data/compression/README.md +11 -0
- package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
- package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
- package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
- package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
- package/renderer/dist/emulator/data/cores/README.md +22 -0
- package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/package.json +65 -0
- package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
- package/renderer/dist/emulator/data/emulator.css +1614 -0
- package/renderer/dist/emulator/data/loader.js +170 -0
- package/renderer/dist/emulator/data/localization/README.md +63 -0
- package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
- package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
- package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
- package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
- package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
- package/renderer/dist/emulator/data/localization/en-US.json +339 -0
- package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
- package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
- package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
- package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
- package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
- package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
- package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
- package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
- package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
- package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
- package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
- package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
- package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
- package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
- package/renderer/dist/emulator/data/src/GameManager.js +464 -0
- package/renderer/dist/emulator/data/src/compression.js +142 -0
- package/renderer/dist/emulator/data/src/emulator.js +6158 -0
- package/renderer/dist/emulator/data/src/gamepad.js +167 -0
- package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
- package/renderer/dist/emulator/data/src/shaders.js +236 -0
- package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
- package/renderer/dist/emulator/data/src/storage.js +125 -0
- package/renderer/dist/emulator/data/version.json +5 -0
- package/renderer/dist/emulator/host.js +297 -0
- package/renderer/dist/emulator/index.html +28 -0
- package/renderer/dist/emulator/provenance.json +6 -0
- package/renderer/dist/index.html +16 -3
- package/scripts/approval-server.sh +24 -5
- package/scripts/before-publish.mjs +12 -10
- package/shared/agent-import.mjs +2 -0
- package/shared/agents.d.mts +1 -0
- package/shared/bridge-map.mjs +18 -0
- package/shared/claude-commands.generated.mjs +6 -4
- package/shared/claude-models.generated.mjs +6 -4
- package/shared/corner-tag.d.mts +14 -0
- package/shared/corner-tag.mjs +141 -0
- package/shared/engines.mjs +20 -5
- package/shared/harness-definitions.mjs +103 -0
- package/shared/notify-rules.d.mts +25 -0
- package/shared/notify-rules.mjs +19 -0
- package/shared/provider-commands.mjs +10 -2
- package/shared/rank.d.mts +2 -0
- package/shared/rank.mjs +50 -0
- package/shared/relay-address.mjs +6 -0
- package/shared/relay-crypto.mjs +165 -0
- package/shared/runs-on.d.mts +0 -10
- package/shared/runs-on.mjs +25 -49
- package/shared/shortcuts.mjs +10 -4
- package/shared/spawn-trouble.mjs +57 -6
- package/shared/terminal-input.d.mts +1 -0
- package/shared/terminal-input.mjs +30 -0
- package/shared/terminal-keys.d.mts +1 -0
- package/shared/terminal-keys.mjs +16 -0
- package/shared/terminal-size.d.mts +4 -0
- package/shared/terminal-size.mjs +29 -0
- package/shared/usage.d.mts +3 -0
- package/shared/usage.mjs +7 -0
- package/shared/where-it-runs-from.d.mts +43 -0
- package/shared/where-it-runs-from.mjs +155 -0
- package/shared/work-items.mjs +25 -1
- package/shared/work-lines.mjs +1 -1
- package/shared/xterm-invalid-range.d.mts +2 -0
- package/shared/xterm-invalid-range.mjs +58 -0
- package/renderer/dist/assets/index-ChKsQkRw.js +0 -1169
- package/renderer/dist/assets/index-ClBXnq3B.css +0 -32
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { hookFailure } from '../../shared/hook-failure.mjs';
|
|
2
|
+
import { SAYING_CAP } from '../codex.mjs';
|
|
3
|
+
|
|
4
|
+
// The stream's own metadata, kept on the session object: the session id (what
|
|
5
|
+
// a personal reply resumes) and the final result message (what the founder
|
|
6
|
+
// reads). Recorded for every session because it is one parse we are already
|
|
7
|
+
// paying for; acted on where someone needs it.
|
|
8
|
+
export function captureStream(session, line) {
|
|
9
|
+
try {
|
|
10
|
+
const obj = JSON.parse(line);
|
|
11
|
+
if (obj.session_id) session.sessionId = obj.session_id;
|
|
12
|
+
if (obj.type === 'result') {
|
|
13
|
+
session.result = String(obj.result ?? '');
|
|
14
|
+
session.resultIsError = !!obj.is_error;
|
|
15
|
+
// A run cannot end with helpers still out. Cleared here as well as on
|
|
16
|
+
// exit so the number can never outlive the thing it counts. Unless
|
|
17
|
+
// something is still out in the background: then this was a turn
|
|
18
|
+
// ending, not the run, and the session stays open for it.
|
|
19
|
+
if (!session.child?.waitingOn?.().length) session.helperIds?.clear();
|
|
20
|
+
}
|
|
21
|
+
noteClaimAnswer(session, obj);
|
|
22
|
+
countHelpers(session, obj);
|
|
23
|
+
} catch {}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Whether this run was refused its own row, off the claim call and its reply.
|
|
27
|
+
// `closingMessageIsTheAnswer` reads it: a refused worker is told to end saying
|
|
28
|
+
// nothing, and whatever it does say must not land on a row someone else holds.
|
|
29
|
+
// A later claim that succeeds clears it.
|
|
30
|
+
function noteClaimAnswer(session, obj) {
|
|
31
|
+
const blocks = Array.isArray(obj?.message?.content) ? obj.message.content : [];
|
|
32
|
+
for (const b of blocks) {
|
|
33
|
+
if (obj.type === 'assistant' && b?.type === 'tool_use' && /claim_work_item$/.test(b.name ?? '')
|
|
34
|
+
&& (!b.input?.id || b.input.id === session.itemId)) {
|
|
35
|
+
(session.claimCalls ??= new Set()).add(b.id);
|
|
36
|
+
}
|
|
37
|
+
if (obj.type === 'user' && b?.type === 'tool_result' && session.claimCalls?.has(b.tool_use_id)) {
|
|
38
|
+
const text = typeof b.content === 'string' ? b.content
|
|
39
|
+
: (b.content ?? []).map((c) => c?.text ?? '').join('');
|
|
40
|
+
if (/"claimed"\s*:\s*false/.test(text)) session.claimRefused = true;
|
|
41
|
+
else if (/"claimed"\s*:\s*true/.test(text)) session.claimRefused = false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* `--include-partial-messages` puts `stream_event` frames on the same stdout
|
|
48
|
+
* the finished messages come down. This keeps the CURRENT text block on the
|
|
49
|
+
* session as it grows, and nothing else: no thinking, no tool arguments, and
|
|
50
|
+
* nothing written to disk. It dies with the process, exactly like `tail`.
|
|
51
|
+
*
|
|
52
|
+
* ONE BLOCK AT A TIME, AND THAT IS THE POINT. A finished message becomes ONE
|
|
53
|
+
* trace line per text block (`traceStreamLine`), so holding exactly one block
|
|
54
|
+
* here means the live string and the traced string are the same string, and
|
|
55
|
+
* the thread can drop the live copy the moment the traced one arrives without
|
|
56
|
+
* guessing (`itemThread`, renderer/src/item-thread.ts).
|
|
57
|
+
*
|
|
58
|
+
* IT IS NOT CLEARED WHEN THE MESSAGE FINISHES, and that is deliberate. The
|
|
59
|
+
* trace is written on one channel and read on another, so clearing it here
|
|
60
|
+
* would blink the sentence off her screen for however long the read takes and
|
|
61
|
+
* then put it back. The renderer suppresses it the moment the thread has it,
|
|
62
|
+
* which is a comparison rather than a race.
|
|
63
|
+
*
|
|
64
|
+
* Returns true when the string moved, which is what earns a push.
|
|
65
|
+
*/
|
|
66
|
+
export function streamingText(session, line) {
|
|
67
|
+
try {
|
|
68
|
+
const obj = JSON.parse(line);
|
|
69
|
+
if (obj.type !== 'stream_event') return false;
|
|
70
|
+
const event = obj.event ?? {};
|
|
71
|
+
// A new message. Whatever the last one ended on is the last one's, and the
|
|
72
|
+
// trace has it by now.
|
|
73
|
+
if (event.type === 'message_start') {
|
|
74
|
+
session.saying = '';
|
|
75
|
+
session.sayingAt = 0;
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
if (event.type === 'content_block_start') {
|
|
79
|
+
// Only a text block resets it. A tool call or a thought starting means
|
|
80
|
+
// this message has moved on from prose, and the block already typed
|
|
81
|
+
// stays on screen until the trace delivers the same words.
|
|
82
|
+
if (event.content_block?.type !== 'text') return false;
|
|
83
|
+
session.saying = '';
|
|
84
|
+
session.sayingAt = Date.now();
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
if (event.type === 'content_block_delta' && event.delta?.type === 'text_delta') {
|
|
88
|
+
const piece = String(event.delta.text ?? '');
|
|
89
|
+
if (!piece) return false;
|
|
90
|
+
if (!session.sayingAt) session.sayingAt = Date.now();
|
|
91
|
+
const next = (session.saying ?? '') + piece;
|
|
92
|
+
// Past the cap this stops being a sentence she is reading. Dropped
|
|
93
|
+
// rather than cut, because a cut string never matches the traced one and
|
|
94
|
+
// would sit under it on her screen forever.
|
|
95
|
+
session.saying = next.length > SAYING_CAP ? '' : next;
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
} catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// HOW MANY HELPERS THIS SESSION HAS OUT RIGHT NOW.
|
|
105
|
+
//
|
|
106
|
+
// THE ONLY TWO EVENTS THAT MATTER, and they bracket a helper exactly. Measured
|
|
107
|
+
// on Claude Code 2.1.246 over a real four-helper run (the probe is written up
|
|
108
|
+
// in decisions.md, 08-26): `task_started` carries a fresh `task_id`, and
|
|
109
|
+
// `task_updated` carries a `patch.status` of `completed` for that same id. Four
|
|
110
|
+
// starts, four completions, no id seen twice and none left open. So a set of
|
|
111
|
+
// live ids, and its size is the number.
|
|
112
|
+
//
|
|
113
|
+
// A SET AND NOT A COUNTER, because a counter cannot survive a duplicate: the
|
|
114
|
+
// stream is read line by line off a pipe, and one repeated line would leave the
|
|
115
|
+
// page saying five helpers are working for the rest of the run with nothing
|
|
116
|
+
// able to correct it. Adding an id twice is free.
|
|
117
|
+
//
|
|
118
|
+
// NESTED HELPERS ARE COUNTED THE SAME. `spawn_depth` says how deep a helper
|
|
119
|
+
// sits, and this deliberately ignores it: a helper's helper is still a
|
|
120
|
+
// subagent working on her task.
|
|
121
|
+
//
|
|
122
|
+
// THE LEAD IS NOT IN THIS NUMBER AND NEVER HAS BEEN, which is what the line
|
|
123
|
+
// drawn off it was getting wrong until 2026-08-27. Only a `task_started` id is
|
|
124
|
+
// ever added, and the lead has no `task_started` of its own.It is, so
|
|
125
|
+
// `shortWord` in renderer/src/live-line.ts now says Subagents, unhyphenated on
|
|
126
|
+
// her call of 2026-08-27 and on a count of the spelling in the Claude Code
|
|
127
|
+
// build she runs. If this ever starts counting the lead as well, that word has
|
|
128
|
+
// to move with it. DID THIS RUN LEAVE HER ANYTHING TO READ?
|
|
129
|
+
//
|
|
130
|
+
// The rule that divides the two writers, kept in one named place because
|
|
131
|
+
// getting it wrong is silence, and silence is the failure that matters most:
|
|
132
|
+
//
|
|
133
|
+
// - said nothing at all -> the supervisor speaks for it (sayTheRunDied)
|
|
134
|
+
// - said something wrong -> the supervisor speaks for it, in OUR words
|
|
135
|
+
// - said something real -> it speaks for itself, untouched
|
|
136
|
+
//
|
|
137
|
+
// The middle line is the one that was missing. Both writers used to exclude an
|
|
138
|
+
// error result: `speaksForTheSession` because raw CLI text must never reach her
|
|
139
|
+
// row, and this one because it only looked for a result that was absent. Each
|
|
140
|
+
// was right on its own and together they left the case that matters most with
|
|
141
|
+
// no writer at all.
|
|
142
|
+
export function saidNothingSheCanUse(session) {
|
|
143
|
+
return session?.result == null || !!session.resultIsError;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function countHelpers(session, obj) {
|
|
147
|
+
if (obj?.type !== 'system') return;
|
|
148
|
+
if (obj.subtype === 'task_started' && obj.task_id) {
|
|
149
|
+
(session.helperIds ??= new Set()).add(obj.task_id);
|
|
150
|
+
} else if (obj.subtype === 'task_updated' && obj.task_id) {
|
|
151
|
+
// Anything that is not still running ends it. The measured value is
|
|
152
|
+
// `completed`, and a helper that fails or is cancelled has equally stopped
|
|
153
|
+
// working, so the test is what it is NOT rather than a list of endings we
|
|
154
|
+
// would have to keep in step with the CLI.
|
|
155
|
+
const status = obj.patch?.status;
|
|
156
|
+
if (status && status !== 'in_progress' && status !== 'running') session.helperIds?.delete(obj.task_id);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// The persisted trace line: fuller than the tail. Tool calls carry their key
|
|
161
|
+
// input (the file written, the command run), because "tool: Bash" answers
|
|
162
|
+
// nothing when the founder asks what a session actually did.
|
|
163
|
+
export function traceStreamLine(line) {
|
|
164
|
+
try {
|
|
165
|
+
const obj = JSON.parse(line);
|
|
166
|
+
const at = new Date().toISOString().slice(11, 19);
|
|
167
|
+
const hook = hookFailure(obj);
|
|
168
|
+
if (hook) return `${at} ${hook}`;
|
|
169
|
+
if (obj.type === 'assistant' && obj.message?.content) {
|
|
170
|
+
const parts = [];
|
|
171
|
+
for (const c of obj.message.content) {
|
|
172
|
+
if (c.type === 'text' && c.text?.trim()) parts.push(c.text.trim());
|
|
173
|
+
if (c.type === 'tool_use') {
|
|
174
|
+
const input = c.input ?? {};
|
|
175
|
+
const hint = input.file_path ?? input.path ?? input.command ?? input.title ?? input.description ?? input.id ?? '';
|
|
176
|
+
parts.push(`[${c.name}] ${String(hint).slice(0, 200)}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return parts.length ? parts.map((p) => `${at} ${p}`).join('\n') : null;
|
|
180
|
+
}
|
|
181
|
+
if (obj.type === 'result') {
|
|
182
|
+
const flavor = `${obj.subtype ?? 'ok'}${obj.is_error ? ' ERROR' : ''} · ${obj.num_turns ?? '?'} turns`;
|
|
183
|
+
return `\n${at} == RESULT (${flavor}) ==\n${String(obj.result ?? '').slice(0, 4000)}`;
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
} catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Stream-json lines are verbose; the In Progress tail wants the gist.
|
|
192
|
+
export function summarizeStreamLine(line) {
|
|
193
|
+
try {
|
|
194
|
+
const obj = JSON.parse(line);
|
|
195
|
+
const hook = hookFailure(obj);
|
|
196
|
+
if (hook) return hook;
|
|
197
|
+
if (obj.type === 'assistant' && obj.message?.content) {
|
|
198
|
+
const text = obj.message.content.filter((c) => c.type === 'text').map((c) => c.text).join(' ');
|
|
199
|
+
const tools = obj.message.content.filter((c) => c.type === 'tool_use').map((c) => c.name);
|
|
200
|
+
if (tools.length) return `tool: ${tools.join(', ')}`;
|
|
201
|
+
if (text) return text.slice(0, 300);
|
|
202
|
+
}
|
|
203
|
+
if (obj.type === 'result') return `done: ${String(obj.result ?? '').slice(0, 200)}`;
|
|
204
|
+
return null;
|
|
205
|
+
} catch {
|
|
206
|
+
return line.slice(0, 200);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { harnessDefinition } from '../../shared/harness-definitions.mjs';
|
|
6
|
+
import { EFFORT_LEVELS } from '../../shared/effort-levels.mjs';
|
|
7
|
+
import { storeRootEnv } from '../store/home.mjs';
|
|
8
|
+
import { resolveClaudeBin } from '../claude-bin.mjs';
|
|
9
|
+
import { claudeModelRows } from '../claude-models.mjs';
|
|
10
|
+
import { linkAccountTooling } from '../account-tooling.mjs';
|
|
11
|
+
import { streamInputArgs, attachClaudeInput } from '../claude-input.mjs';
|
|
12
|
+
import { claudeLaunchEnv } from '../claude-launch-env.mjs';
|
|
13
|
+
import { claudeActivity } from '../agent-activity.mjs';
|
|
14
|
+
import { captureStream, streamingText, summarizeStreamLine, traceStreamLine } from './claude-stream.mjs';
|
|
15
|
+
import { definitionMethods, commonUpdatePlan } from './common.mjs';
|
|
16
|
+
const definition = harnessDefinition('claude');
|
|
17
|
+
|
|
18
|
+
export const claudeHarness = {
|
|
19
|
+
...definitionMethods(definition),
|
|
20
|
+
discover: resolveClaudeBin,
|
|
21
|
+
launchEnv: claudeLaunchEnv,
|
|
22
|
+
models: ({ bin } = {}) => claudeModelRows({ bin: bin ?? null }).map(m => ({ id: m.alias, label: m.label })),
|
|
23
|
+
defaultModel: () => null,
|
|
24
|
+
workspaceModel: () => null,
|
|
25
|
+
profileHome: () => null,
|
|
26
|
+
effortLevels: () => EFFORT_LEVELS.map(e => e.id),
|
|
27
|
+
profiles: sup => sup._profiles(),
|
|
28
|
+
signedIn: () => true,
|
|
29
|
+
profileEnv: (env, profile) => profile === 'default' ? env : { ...env, [definition.homeEnv]: profile },
|
|
30
|
+
signInFiles({ folder, home }) {
|
|
31
|
+
if (folder) return definition.credentialFiles.map(file => path.join(folder, file));
|
|
32
|
+
const dir = path.join(home, '.claude');
|
|
33
|
+
return [path.join(home, '.claude.json'), ...definition.credentialFiles.map(file => path.join(dir, file))];
|
|
34
|
+
},
|
|
35
|
+
tooling: profile => linkAccountTooling(profile),
|
|
36
|
+
spawn(sup, plan, { cwd, item, profile, env }) {
|
|
37
|
+
// The binary is the one this definition names, not a field spelled out
|
|
38
|
+
// here: that is the last place an engine of this family needed code.
|
|
39
|
+
return spawn(sup.config[definition.binKey], streamInputArgs(definition.run, plan.args), {
|
|
40
|
+
cwd,
|
|
41
|
+
env: { ...claudeLaunchEnv(sup.config[definition.binKey], env), STORE_ACCOUNT_ID: sup.config.accountId, ...storeRootEnv(sup.config.storeRoot),
|
|
42
|
+
...(profile !== 'default' ? { [definition.homeEnv]: profile } : {}),
|
|
43
|
+
ZERO_PRODUCT: item.product, ZERO_ITEM: item.id, ...sup.memoryGateEnv(item) },
|
|
44
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
readers: () => ({ capture: captureStream, stream: streamingText, summarize: summarizeStreamLine, trace: traceStreamLine, activity: claudeActivity }),
|
|
48
|
+
subscribe(child, _session, absorb) {
|
|
49
|
+
let buffer = '';
|
|
50
|
+
child.stdout.on('data', data => {
|
|
51
|
+
buffer += data.toString();
|
|
52
|
+
let index;
|
|
53
|
+
while ((index = buffer.indexOf('\n')) >= 0) {
|
|
54
|
+
const line = buffer.slice(0, index); buffer = buffer.slice(index + 1);
|
|
55
|
+
if (line.trim()) absorb(line);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
attachInput: child => attachClaudeInput(child),
|
|
60
|
+
transcript(sup, rec) {
|
|
61
|
+
if (!rec?.sessionId) return null;
|
|
62
|
+
const home = rec.profile && rec.profile !== 'default' ? rec.profile : path.join(os.homedir(), '.claude');
|
|
63
|
+
const projects = path.join(home, 'projects');
|
|
64
|
+
const slug = String(rec.cwd ?? '').replace(/[^a-zA-Z0-9]/g, '-');
|
|
65
|
+
const direct = path.join(projects, slug, `${rec.sessionId}.jsonl`);
|
|
66
|
+
if (fs.existsSync(direct)) return direct;
|
|
67
|
+
try { for (const dir of fs.readdirSync(projects)) {
|
|
68
|
+
const file = path.join(projects, dir, `${rec.sessionId}.jsonl`);
|
|
69
|
+
if (fs.existsSync(file)) return file;
|
|
70
|
+
} } catch {}
|
|
71
|
+
return null;
|
|
72
|
+
},
|
|
73
|
+
change: () => null,
|
|
74
|
+
release: () => {},
|
|
75
|
+
memoryGate: (sup, item) => sup.memoryGateEnv(item),
|
|
76
|
+
smallModelArgs: (prompt, model) => ['-p', prompt, '--model', model],
|
|
77
|
+
updateSettings: ({ home }) => {
|
|
78
|
+
try { return JSON.parse(fs.readFileSync(path.join(process.env.CLAUDE_CONFIG_DIR || path.join(home, '.claude'), 'settings.json'), 'utf8')); }
|
|
79
|
+
catch { return {}; }
|
|
80
|
+
},
|
|
81
|
+
updatePlan: options => commonUpdatePlan(definition, options, ({ bin, real, home, channel, name }) =>
|
|
82
|
+
real.startsWith(path.join(home, '.local/share/claude/versions') + '/')
|
|
83
|
+
? { name, file: bin, args: ['update'], feed: `https://downloads.claude.ai/claude-code-releases/${channel}`, format: 'text' } : null),
|
|
84
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { harnessDefinition } from '../../shared/harness-definitions.mjs';
|
|
3
|
+
import { resolveCodexBin } from '../codex-bin.mjs';
|
|
4
|
+
import { codexLaunchEnv } from '../codex-launch-env.mjs';
|
|
5
|
+
import { codexModels, codexDefaultModel, codexModelLevels } from '../codex-models.mjs';
|
|
6
|
+
import { codexTranscriptFile, workerThreadParams } from '../codex-session.mjs';
|
|
7
|
+
import { compactCodexThread } from '../codex-compaction.mjs';
|
|
8
|
+
import { captureCodexEvent, codexStreamingText, summarizeCodexEvent, traceCodexEvent, rememberCodexChange, changeFromCodexTurn } from '../codex.mjs';
|
|
9
|
+
import { codexActivity } from '../agent-activity.mjs';
|
|
10
|
+
import { runCodexCommand } from '../provider-commands.mjs';
|
|
11
|
+
import { definitionMethods, commonUpdatePlan } from './common.mjs';
|
|
12
|
+
const definition = harnessDefinition('codex');
|
|
13
|
+
|
|
14
|
+
export const codexHarness = {
|
|
15
|
+
...definitionMethods(definition),
|
|
16
|
+
discover: resolveCodexBin,
|
|
17
|
+
launchEnv: codexLaunchEnv,
|
|
18
|
+
models: (options = {}) => codexModels(options),
|
|
19
|
+
defaultModel: (options = {}) => codexDefaultModel(options),
|
|
20
|
+
workspaceModel: sup => sup._codexWorkspaceModel(),
|
|
21
|
+
profileHome: (sup, profile) => sup._codexProfileHome(profile),
|
|
22
|
+
effortLevels: (model, options = {}) => codexModelLevels(model, options),
|
|
23
|
+
profiles: sup => sup._codexProfiles(),
|
|
24
|
+
signedIn: (sup, profile) => sup._codexSignedIn(profile),
|
|
25
|
+
profileEnv: (env, profile, sup) => ({ ...env, [definition.homeEnv]: sup._codexProfileHome(profile) }),
|
|
26
|
+
tooling: () => null,
|
|
27
|
+
// Codex carries the person's standing instructions as a thread's
|
|
28
|
+
// `developerInstructions`, so it needs neither a flag nor a file. It says so
|
|
29
|
+
// rather than inheriting the refusal meant for an engine that has nowhere to
|
|
30
|
+
// put them (main/harnesses/instructions-file.mjs).
|
|
31
|
+
instructions: () => ({ by: 'thread' }),
|
|
32
|
+
spawn: (sup, plan, context) => sup._spawnCodexWorker(plan, context),
|
|
33
|
+
readers: () => ({ capture: captureCodexEvent, stream: codexStreamingText, summarize: summarizeCodexEvent, trace: traceCodexEvent, activity: codexActivity }),
|
|
34
|
+
subscribe(child, session, absorb) {
|
|
35
|
+
child.on('event', (method, params) => { rememberCodexChange(session, method, params); absorb(method, params); });
|
|
36
|
+
},
|
|
37
|
+
attachInput: () => {},
|
|
38
|
+
transcript: (sup, rec) => rec?.sessionId ? codexTranscriptFile(rec.sessionId, { home: sup._codexProfileHome(rec.profile) }) : null,
|
|
39
|
+
change: (session, options) => changeFromCodexTurn(session.codexChange ?? [], options),
|
|
40
|
+
release: (sup, profile) => sup._releaseIdleCodex(profile),
|
|
41
|
+
usage: sup => sup.codexUsage(),
|
|
42
|
+
async compact(sup, rec, cwd) {
|
|
43
|
+
const { client, handshake } = sup._codexServer(sup._codexProfileHome(rec.profile ?? 'default'));
|
|
44
|
+
const mcpServers = await sup._codexIsolation(client, handshake);
|
|
45
|
+
const params = workerThreadParams({ cwd, mcpServers, storeServer: null });
|
|
46
|
+
return compactCodexThread({ server: client, threadId: rec.sessionId, threadParams: params });
|
|
47
|
+
},
|
|
48
|
+
settleApproval: (sup, ...args) => sup.settleCodexApproval(...args),
|
|
49
|
+
approvalChanged: (sup, ...args) => sup.codexCardChanged(...args),
|
|
50
|
+
memoryGate: sup => sup.codexMemoryGateEnv(),
|
|
51
|
+
async runCommand(sup, { home, ...operation }) {
|
|
52
|
+
const { client, handshake } = sup._codexServer(home);
|
|
53
|
+
await handshake;
|
|
54
|
+
return runCodexCommand({ server: client, ...operation });
|
|
55
|
+
},
|
|
56
|
+
updateSettings: () => ({}),
|
|
57
|
+
updatePlan: options => commonUpdatePlan(definition, options, ({ bin, real, name }) => {
|
|
58
|
+
const standalone = real.match(/^(.*)\/packages\/standalone\/(?:releases|current)\//);
|
|
59
|
+
return standalone ? { name, file: '/bin/bash',
|
|
60
|
+
args: ['-o', 'pipefail', '-c', '/usr/bin/curl -fsSL --connect-timeout 10 --max-time 60 https://chatgpt.com/codex/install.sh | /bin/sh'],
|
|
61
|
+
env: { CODEX_HOME: standalone[1], CODEX_INSTALL_DIR: path.dirname(bin) },
|
|
62
|
+
feed: 'https://releases.openai.com/codex/channels/latest', format: 'codex' } : null;
|
|
63
|
+
}),
|
|
64
|
+
smallModelArgs: prompt => ['exec', '--skip-git-repo-check', '--ephemeral', '--ignore-user-config', '--ignore-rules', '-s', 'read-only', '-c', 'model_reasoning_effort=low', prompt],
|
|
65
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { instructionsFor } from './instructions-file.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* THE COMMAND LINE THAT STARTS A WORKER, BUILT FROM THE DEFINITION'S `run`.
|
|
6
|
+
*
|
|
7
|
+
* Every flag name here used to be a literal in main/supervisor.mjs, so the
|
|
8
|
+
* supervisor knew one engine's CLI by heart and a second one of the same family
|
|
9
|
+
* could not be added without editing it. The order is the order that shipped,
|
|
10
|
+
* and `tests/a-second-stream-json-engine-is-only-a-definition` holds it there,
|
|
11
|
+
* because flag order is the kind of thing a refactor silently rewrites.
|
|
12
|
+
*
|
|
13
|
+
* ABSENT MEANS CANNOT, not "default". An engine whose `run` has no `effort` is
|
|
14
|
+
* handed no effort word even when the task names one, which is how a harness
|
|
15
|
+
* says it lacks a capability without the call site asking who it is. `fork` is
|
|
16
|
+
* the one pair: a fork is a fork OF something, so it needs a resume as well.
|
|
17
|
+
*/
|
|
18
|
+
export function workerArgsFrom(run, { prompt, grants = [], effort = null, system = null, resumeId = null, fork = false } = {}) {
|
|
19
|
+
if (!run) return [];
|
|
20
|
+
const args = [];
|
|
21
|
+
if (run.print) args.push(run.print, prompt);
|
|
22
|
+
if (run.outputFormat) args.push(run.outputFormat.flag, run.outputFormat.value);
|
|
23
|
+
if (run.stream) args.push(...run.stream);
|
|
24
|
+
args.push(...grants);
|
|
25
|
+
if (run.effort && effort) args.push(run.effort, effort);
|
|
26
|
+
if (run.system && system) args.push(run.system, system);
|
|
27
|
+
if (run.resume && resumeId) args.push(run.resume, resumeId);
|
|
28
|
+
if (run.fork && fork && resumeId) args.push(run.fork);
|
|
29
|
+
return args;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function definitionMethods(definition) {
|
|
33
|
+
const scrub = definition.scrubPatterns.map(pattern => new RegExp(pattern));
|
|
34
|
+
return {
|
|
35
|
+
...definition,
|
|
36
|
+
binary: config => config?.[definition.binKey] ?? null,
|
|
37
|
+
workerArgs: options => workerArgsFrom(definition.run, options),
|
|
38
|
+
// WHERE THE PERSON'S STANDING INSTRUCTIONS GO on this engine. A flag for
|
|
39
|
+
// the one that has one, a file named in the definition for the ones that
|
|
40
|
+
// do not, and a refusal for an engine with neither — because that engine
|
|
41
|
+
// would answer against rules it was never given, and nothing on screen
|
|
42
|
+
// would say so.
|
|
43
|
+
instructions: context => instructionsFor(definition, context),
|
|
44
|
+
launchEnv: (_bin, env) => ({ ...env }),
|
|
45
|
+
install: () => ({ file: definition.installer.shell, args: ['-c', `curl -fsSL ${definition.installer.url} | ${definition.installer.pipe}`] }),
|
|
46
|
+
signIn: bin => ({ file: bin, args: [...definition.loginArgs] }),
|
|
47
|
+
signInStatus: bin => ({ file: bin, args: [...definition.statusArgs] }),
|
|
48
|
+
accountKey: profile => `${definition.accountPrefix}${profile}`,
|
|
49
|
+
workerEnv(input = process.env) {
|
|
50
|
+
const env = { ...input };
|
|
51
|
+
for (const key of Object.keys(env)) if (scrub.some(pattern => pattern.test(key))) delete env[key];
|
|
52
|
+
return env;
|
|
53
|
+
},
|
|
54
|
+
signInFiles: ({ folder }) => folder ? definition.credentialFiles.map(file => path.join(folder, file)) : [],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Installation-specific work remains in each adapter. The common installers
|
|
59
|
+
// use the same prefix and cask checks for every registered tool.
|
|
60
|
+
export function commonUpdatePlan(definition, { bin, real, home, channel = 'latest' }, native) {
|
|
61
|
+
if (!path.isAbsolute(bin || '') || !real) return null;
|
|
62
|
+
const name = definition.label;
|
|
63
|
+
const brew = real.match(/^(\/opt\/homebrew|\/usr\/local)\/Caskroom\/([^/]+)\//);
|
|
64
|
+
if (brew && definition.casks.includes(brew[2])) return {
|
|
65
|
+
name, file: `${brew[1]}/bin/brew`, args: ['upgrade', '--cask', brew[2]],
|
|
66
|
+
feed: `https://formulae.brew.sh/api/cask/${brew[2]}.json`, format: 'brew',
|
|
67
|
+
};
|
|
68
|
+
const tail = `/lib/node_modules/${definition.package}/`;
|
|
69
|
+
const at = real.indexOf(tail);
|
|
70
|
+
if (at > 0) {
|
|
71
|
+
const prefix = real.slice(0, at);
|
|
72
|
+
return { name, file: path.join(prefix, 'bin/npm'),
|
|
73
|
+
args: ['install', '--global', '--prefix', prefix, `${definition.package}@${channel}`],
|
|
74
|
+
env: { PATH: `${prefix}/bin:${process.env.PATH || ''}` },
|
|
75
|
+
feed: `https://registry.npmjs.org/${definition.package}/${channel}`, format: 'npm' };
|
|
76
|
+
}
|
|
77
|
+
return native?.({ bin, real, home, channel, name }) ?? null;
|
|
78
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE PERSON'S STANDING INSTRUCTIONS, FOR AN ENGINE WITH NO SYSTEM PROMPT.
|
|
3
|
+
*
|
|
4
|
+
* Claude Code takes them on `--append-system-prompt` and Codex takes them as a
|
|
5
|
+
* thread's `developerInstructions`. Every other engine we have looked at takes
|
|
6
|
+
* neither: ACP has no system prompt in the protocol, and OpenCode's server
|
|
7
|
+
* takes none. So the instructions are the one thing that would arrive nowhere
|
|
8
|
+
* while everything else about a run looked perfect, and a worker that never
|
|
9
|
+
* read them is worse than one that failed to start, because it works and
|
|
10
|
+
* answers against rules it was never given.
|
|
11
|
+
*
|
|
12
|
+
* Decided on w-380b06bf53: they go into the file the tool itself reads, and
|
|
13
|
+
* which file that is lives in the definition as data — AGENTS.md for most of
|
|
14
|
+
* them, GEMINI.md for Gemini CLI. Same shape as every other name in there.
|
|
15
|
+
*
|
|
16
|
+
* TWO RULES THIS FILE IS BUILT AROUND, both of them other people's property.
|
|
17
|
+
*
|
|
18
|
+
* NEVER A TRACKED FILE. A task folder is a worktree of somebody's own
|
|
19
|
+
* repository, and AGENTS.md is frequently committed in one. This codebase has
|
|
20
|
+
* already paid for writing a tracked file once: every folder that had run the
|
|
21
|
+
* app reported itself dirty, `releaseTaskFolder` would not reclaim a folder,
|
|
22
|
+
* and change cards carried files nobody had touched (CLAUDE.md, 2026-10-07).
|
|
23
|
+
* So the preferred home is the tool's own config directory, which is per-task
|
|
24
|
+
* and ours; and if a definition does ask for the working folder, a path the
|
|
25
|
+
* repository tracks is refused rather than written.
|
|
26
|
+
*
|
|
27
|
+
* NEVER SOMEBODY ELSE'S WORDS. If the file is already there, what is in it
|
|
28
|
+
* stays. Only the block between our two markers is rewritten, so resuming a
|
|
29
|
+
* thread fifty times leaves one block and their own text untouched.
|
|
30
|
+
*/
|
|
31
|
+
import fs from 'node:fs';
|
|
32
|
+
import path from 'node:path';
|
|
33
|
+
import { execFileSync } from 'node:child_process';
|
|
34
|
+
|
|
35
|
+
export const INSTRUCTIONS_BEGIN = '<!-- agentbox:instructions -->';
|
|
36
|
+
export const INSTRUCTIONS_END = '<!-- /agentbox:instructions -->';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* HOW THIS ENGINE CARRIES INSTRUCTIONS: 'flag', 'file', or null for neither.
|
|
40
|
+
*
|
|
41
|
+
* Null is the answer that matters. It is not a default to be filled in later;
|
|
42
|
+
* it is an engine that cannot be offered, because the person's rules would not
|
|
43
|
+
* reach it and nothing on screen would say so.
|
|
44
|
+
*/
|
|
45
|
+
export function carriesInstructions(definition) {
|
|
46
|
+
if (definition?.run?.system) return 'flag';
|
|
47
|
+
if (definition?.instructions?.file) return 'file';
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The absolute path this engine's instructions belong at, or null. */
|
|
52
|
+
export function instructionsTarget(definition, { cwd, home } = {}) {
|
|
53
|
+
const spec = definition?.instructions;
|
|
54
|
+
if (!spec?.file) return null;
|
|
55
|
+
const root = spec.in === 'cwd' ? cwd : home;
|
|
56
|
+
return root ? path.join(root, spec.file) : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Whether this repository has the file under version control. */
|
|
60
|
+
export function fileIsTracked(file, cwd) {
|
|
61
|
+
const where = cwd ?? path.dirname(file);
|
|
62
|
+
try {
|
|
63
|
+
execFileSync('git', ['ls-files', '--error-unmatch', '--', file], { cwd: where, stdio: 'ignore' });
|
|
64
|
+
return true;
|
|
65
|
+
} catch {
|
|
66
|
+
return false; // not tracked, or not a repository at all
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Put the instructions in the file, and nothing else in it out.
|
|
72
|
+
*
|
|
73
|
+
* Idempotent on purpose: a thread resumes many times and each run writes the
|
|
74
|
+
* block again, so the test that matters is that there is exactly one of them
|
|
75
|
+
* afterwards. Returns the path written.
|
|
76
|
+
*/
|
|
77
|
+
export function writeInstructions({ file, text, cwd = null, tracked = null }) {
|
|
78
|
+
if (!file) throw Error('There is nowhere to write these instructions');
|
|
79
|
+
const isTracked = tracked ?? fileIsTracked(file, cwd);
|
|
80
|
+
if (isTracked) {
|
|
81
|
+
// Their file, under their version control. Writing it would dirty the
|
|
82
|
+
// worktree and could ride into the change this task is for.
|
|
83
|
+
throw Error(`${file} is tracked by this repository, so the instructions cannot be written there`);
|
|
84
|
+
}
|
|
85
|
+
const block = `${INSTRUCTIONS_BEGIN}\n${text}\n${INSTRUCTIONS_END}`;
|
|
86
|
+
let before = '';
|
|
87
|
+
try { before = fs.readFileSync(file, 'utf8'); } catch {}
|
|
88
|
+
let next;
|
|
89
|
+
const begins = before.indexOf(INSTRUCTIONS_BEGIN);
|
|
90
|
+
const ends = before.indexOf(INSTRUCTIONS_END);
|
|
91
|
+
if (begins >= 0 && ends > begins) {
|
|
92
|
+
next = before.slice(0, begins) + block + before.slice(ends + INSTRUCTIONS_END.length);
|
|
93
|
+
} else {
|
|
94
|
+
next = before ? `${before.replace(/\s*$/, '')}\n\n${block}\n` : `${block}\n`;
|
|
95
|
+
}
|
|
96
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
97
|
+
fs.writeFileSync(file, next);
|
|
98
|
+
return file;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The `instructions` operation every harness answers.
|
|
103
|
+
*
|
|
104
|
+
* `{ by: 'flag' }` for an engine whose command line carries them, so nothing
|
|
105
|
+
* is written and the existing path is untouched. `{ by: 'file', file }` once
|
|
106
|
+
* they are on disk. An engine that carries them neither way has no business
|
|
107
|
+
* being offered, so this throws rather than returning a shrug.
|
|
108
|
+
*/
|
|
109
|
+
export function instructionsFor(definition, { cwd, home, text } = {}) {
|
|
110
|
+
const how = carriesInstructions(definition);
|
|
111
|
+
if (how === 'flag') return { by: 'flag' };
|
|
112
|
+
if (how !== 'file') throw Error(`${definition?.label ?? 'This engine'} has nowhere to put standing instructions`);
|
|
113
|
+
const file = instructionsTarget(definition, { cwd, home });
|
|
114
|
+
if (!text) return { by: 'file', file, written: false };
|
|
115
|
+
return { by: 'file', file: writeInstructions({ file, text, cwd }), written: true };
|
|
116
|
+
}
|