agentbox-app 0.1.11 → 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/README.md +24 -16
- package/bin/agentbox.mjs +18 -9
- package/briefs/message-rules.md +15 -4
- package/main/account-tooling.mjs +2 -1
- 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 -20
- package/main/analytics.mjs +30 -0
- package/main/approvals.mjs +14 -5
- package/main/claude-bin.mjs +45 -310
- 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 +14 -2
- package/main/claude-usage.mjs +82 -25
- package/main/codex-bin.mjs +51 -248
- package/main/codex-launch-env.mjs +31 -0
- 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 +40 -13
- package/main/corner-tag.mjs +489 -0
- package/main/crash-report.mjs +26 -3
- package/main/demo.mjs +17 -14
- package/main/engine-bin.mjs +595 -0
- package/main/engine-setup.mjs +27 -27
- package/main/feedback.mjs +11 -6
- package/main/fresh-user.mjs +1 -1
- package/main/git-change.mjs +8 -2
- 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/instruction-settings.mjs +47 -11
- package/main/ipc.mjs +175 -22
- package/main/local-app-frames.mjs +82 -0
- package/main/machine.mjs +1 -1
- package/main/main.mjs +120 -5
- 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 +48 -5
- package/main/opencode-server.mjs +271 -0
- package/main/opencode.mjs +287 -0
- package/main/phone-link.mjs +551 -0
- package/main/refresh-agent-models.mjs +2 -1
- package/main/relay-address.mjs +28 -0
- package/main/relay-client.mjs +246 -0
- package/main/row-label.mjs +8 -7
- package/main/runs-on.mjs +90 -0
- package/main/serve.mjs +124 -10
- package/main/settings.mjs +171 -11
- package/main/shell-path.mjs +86 -0
- package/main/ship-queue.mjs +25 -7
- 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/home.mjs +37 -5
- package/main/store/project.mjs +15 -19
- package/main/store.mjs +10 -4
- package/main/supervisor.mjs +1030 -447
- package/main/task-commands.mjs +13 -17
- package/main/task-folders-offthread.mjs +6 -1
- package/main/task-folders.mjs +491 -82
- package/main/task-terminals.mjs +49 -8
- package/main/team/index.mjs +2 -2
- package/main/team/projects.mjs +1 -1
- package/main/updater.mjs +71 -9
- package/main/worktree-include.mjs +494 -0
- package/mcp/core/page.mjs +1 -1
- package/mcp/core/work.mjs +34 -3
- package/mcp/tools.mjs +2 -1
- 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/after-install.mjs +14 -0
- package/scripts/approval-server.sh +31 -7
- package/scripts/before-publish.mjs +12 -10
- package/shared/agent-import.mjs +3 -1
- package/shared/agents.d.mts +1 -0
- package/shared/bridge-map.mjs +19 -0
- package/shared/claude-commands.generated.mjs +8 -6
- package/shared/claude-models.generated.mjs +10 -8
- package/shared/codex-modes.mjs +1 -1
- package/shared/corner-tag.d.mts +14 -0
- package/shared/corner-tag.mjs +141 -0
- package/shared/demo-world.mjs +1 -1
- package/shared/engines.mjs +20 -5
- package/shared/fresh-user-home.mjs +12 -1
- package/shared/harness-definitions.mjs +103 -0
- package/shared/instruction-defaults.json +1 -1
- package/shared/notify-rules.d.mts +25 -0
- package/shared/notify-rules.mjs +19 -0
- package/shared/project-folder-check.mjs +3 -3
- 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 +10 -0
- package/shared/runs-on.mjs +55 -0
- package/shared/shortcuts.mjs +11 -5
- package/shared/spawn-trouble.mjs +58 -7
- 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 +28 -2
- 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-B-2Az952.css +0 -32
- package/renderer/dist/assets/index-D6qncgai.js +0 -1168
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// THE OPENCODE ADAPTER, AND IT IS THE POINT OF THE ENGINE ROW.
|
|
2
|
+
//
|
|
3
|
+
// The engine proposal (w-380b06bf53) counted 96 `=== 'codex'` or
|
|
4
|
+
// `=== 'claude'` branch points across 12 files and claimed one interface would
|
|
5
|
+
// turn the next harness into one new module. This is the test of that claim,
|
|
6
|
+
// and the answer is nearly yes: OpenCode is this file, main/opencode.mjs,
|
|
7
|
+
// main/opencode-server.mjs, one entry of data in shared/harness-definitions,
|
|
8
|
+
// one line registering it in main/harnesses.mjs -- and one filter in
|
|
9
|
+
// shared/engines.mjs, which was NOT free and is written up there. No existing
|
|
10
|
+
// file grew an engine branch.
|
|
11
|
+
//
|
|
12
|
+
// WHAT IS DECLARED MISSING RATHER THAN FAKED. `usage` is absent, so the
|
|
13
|
+
// registry answers `{ supported: false }` for it: OpenCode bills through
|
|
14
|
+
// whichever provider the user connected and has no endpoint that reports a
|
|
15
|
+
// plan limit. Per-run tokens and cost do arrive and are read
|
|
16
|
+
// (main/opencode.mjs), but "what is left" is a question nothing can answer, and
|
|
17
|
+
// docs/harnesses.md says never to mark missing usage as zero. `attachInput` is
|
|
18
|
+
// absent too: there is a `/tui/append-prompt` endpoint but it drives the
|
|
19
|
+
// terminal UI, not a headless run, so remoteControl is false.
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { spawn } from 'node:child_process';
|
|
23
|
+
import { harnessDefinition } from '../../shared/harness-definitions.mjs';
|
|
24
|
+
import { openCodeClient, createOpenCodeWorker, splitModel } from '../opencode-server.mjs';
|
|
25
|
+
import {
|
|
26
|
+
captureOpenCodeEvent, openCodeStreamingText, summarizeOpenCodeEvent,
|
|
27
|
+
traceOpenCodeEvent, openCodeActivity, rememberOpenCodeProsePart,
|
|
28
|
+
openCodePermissionConfig,
|
|
29
|
+
} from '../opencode.mjs';
|
|
30
|
+
import { definitionMethods, commonUpdatePlan } from './common.mjs';
|
|
31
|
+
|
|
32
|
+
const definition = harnessDefinition('opencode');
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* WHERE THIS WORKER'S PERMISSION POLICY LIVES ON DISK.
|
|
36
|
+
*
|
|
37
|
+
* One file per task folder, inside the folder, so two workers cannot read each
|
|
38
|
+
* other's policy and nothing is written into the user's own OpenCode config.
|
|
39
|
+
* `OPENCODE_CONFIG` points at it. It is rewritten on every start rather than
|
|
40
|
+
* reused, because a stale one is how a run silently stops asking.
|
|
41
|
+
*/
|
|
42
|
+
export function writePermissionConfig(cwd) {
|
|
43
|
+
const file = path.join(cwd, '.agentbox-opencode.json');
|
|
44
|
+
fs.writeFileSync(file, `${JSON.stringify(openCodePermissionConfig(), null, 2)}\n`, 'utf8');
|
|
45
|
+
return file;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The models OpenCode itself says are available, as `provider/model` rows. */
|
|
49
|
+
export function openCodeModelRows(providers) {
|
|
50
|
+
const list = Array.isArray(providers?.providers) ? providers.providers : [];
|
|
51
|
+
const rows = [];
|
|
52
|
+
for (const provider of list) {
|
|
53
|
+
for (const [id, model] of Object.entries(provider?.models ?? {})) {
|
|
54
|
+
rows.push({ id: `${provider.id}/${id}`, label: `${provider.name ?? provider.id} · ${model?.name ?? id}` });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return rows;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const openCodeHarness = {
|
|
61
|
+
...definitionMethods(definition),
|
|
62
|
+
|
|
63
|
+
// BINARY DISCOVERY. No bespoke resolver: the binary is whatever the config
|
|
64
|
+
// names, the same question `common.mjs` answers for the other two.
|
|
65
|
+
discover: (config = {}) => config?.[definition.binKey] ?? null,
|
|
66
|
+
|
|
67
|
+
// SIGN-IN. `opencode auth login` writes to OpenCode's own store and
|
|
68
|
+
// `opencode auth list` reports it. Agentbox launches those two and reads
|
|
69
|
+
// neither file, which is the whole of its involvement with credentials.
|
|
70
|
+
//
|
|
71
|
+
// AND SIGNED IN IS NOT REQUIRED. The other two harnesses cannot run without
|
|
72
|
+
// a login; OpenCode can, on the Zen free tier (measured: zero credentials,
|
|
73
|
+
// ten models, a finished run at cost 0). So this answers true rather than
|
|
74
|
+
// gating the run, and a provider that does need a key fails loudly at
|
|
75
|
+
// `session.error` with its own words, which the readers surface.
|
|
76
|
+
signedIn: () => true,
|
|
77
|
+
profiles: () => ['default'],
|
|
78
|
+
profileHome: () => null,
|
|
79
|
+
|
|
80
|
+
// THE WORKER'S ENVIRONMENT. `profileEnv` points OPENCODE_CONFIG at the
|
|
81
|
+
// policy file written for this folder; `workerEnv` (from the definition)
|
|
82
|
+
// strips every provider key first, so nothing we hold can reach it.
|
|
83
|
+
profileEnv: (env, profile) => (profile && profile !== 'default' ? { ...env, [definition.homeEnv]: profile } : env),
|
|
84
|
+
accountKey: () => `${definition.accountPrefix}default`,
|
|
85
|
+
signInFiles: ({ folder, home }) => (folder
|
|
86
|
+
? definition.credentialFiles.map(file => path.join(folder, file))
|
|
87
|
+
: [path.join(home, '.local/share/opencode/auth.json')]),
|
|
88
|
+
|
|
89
|
+
// MODELS. Asked of the running server rather than kept in a list here, for
|
|
90
|
+
// the reason Conductor's own docs give for doing the same: the set depends
|
|
91
|
+
// entirely on what the user connected, and we do not know it.
|
|
92
|
+
async models({ client } = {}) {
|
|
93
|
+
if (!client) return { supported: false, engine: definition.id, capability: 'models' };
|
|
94
|
+
try { return openCodeModelRows(await client.providers()); }
|
|
95
|
+
catch { return { supported: false, engine: definition.id, capability: 'models' }; }
|
|
96
|
+
},
|
|
97
|
+
defaultModel: () => null,
|
|
98
|
+
workspaceModel: () => null,
|
|
99
|
+
effortLevels: () => ({ supported: false, engine: definition.id, capability: 'effortLevels' }),
|
|
100
|
+
|
|
101
|
+
tooling: () => null,
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* ONE RUN. A server for this folder, then a worker on a session in it.
|
|
105
|
+
*
|
|
106
|
+
* The server is started per folder and killed with the worker. `--port 0`
|
|
107
|
+
* is not used: OpenCode picks a random port when none is given and prints
|
|
108
|
+
* the address it chose, which is what `openCodeServerAddress` waits for.
|
|
109
|
+
*/
|
|
110
|
+
spawn(sup, plan, { cwd, item, profile = 'default', env }) {
|
|
111
|
+
const configFile = writePermissionConfig(cwd);
|
|
112
|
+
const password = sup._openCodePassword?.() ?? null;
|
|
113
|
+
const server = spawn(sup.config[definition.binKey], ['serve', '--hostname', '127.0.0.1', '--port', '0'], {
|
|
114
|
+
cwd,
|
|
115
|
+
env: {
|
|
116
|
+
...env,
|
|
117
|
+
[definition.homeEnv]: configFile,
|
|
118
|
+
...(password ? { OPENCODE_SERVER_PASSWORD: password } : {}),
|
|
119
|
+
ZERO_PRODUCT: item.product, ZERO_ITEM: item.id,
|
|
120
|
+
},
|
|
121
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const worker = createOpenCodeWorker({
|
|
125
|
+
client: openCodeServerClient(server, password),
|
|
126
|
+
plan,
|
|
127
|
+
cwd,
|
|
128
|
+
resumeSessionId: plan.resumeId ?? null,
|
|
129
|
+
title: item.id,
|
|
130
|
+
onApproval: sup._openCodeApprovals?.(item) ?? null,
|
|
131
|
+
});
|
|
132
|
+
worker.once('exit', () => { try { server.kill(); } catch { /* already gone */ } });
|
|
133
|
+
worker.server = server;
|
|
134
|
+
return worker;
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
readers: () => ({
|
|
138
|
+
capture: captureOpenCodeEvent,
|
|
139
|
+
stream: openCodeStreamingText,
|
|
140
|
+
summarize: summarizeOpenCodeEvent,
|
|
141
|
+
trace: traceOpenCodeEvent,
|
|
142
|
+
activity: openCodeActivity,
|
|
143
|
+
}),
|
|
144
|
+
|
|
145
|
+
// Native names through, untranslated, as the Codex adapter does. The prose
|
|
146
|
+
// part is remembered first so a reasoning delta is never streamed as prose.
|
|
147
|
+
subscribe(child, session, absorb) {
|
|
148
|
+
child.on('event', (type, properties) => {
|
|
149
|
+
rememberOpenCodeProsePart(session, type, properties);
|
|
150
|
+
absorb(type, properties);
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
transcript: () => null,
|
|
155
|
+
change: session => (session?.opencodeChange?.length ? { files: [...session.opencodeChange] } : null),
|
|
156
|
+
release: () => {},
|
|
157
|
+
|
|
158
|
+
// COMPACTION. `POST /session/:id/summarize` -- measured, returns true.
|
|
159
|
+
compact: (sup, rec) => (rec?.sessionId && rec.client
|
|
160
|
+
? rec.client.summarize(rec.sessionId, rec.model ?? null)
|
|
161
|
+
: Promise.resolve(false)),
|
|
162
|
+
|
|
163
|
+
// SLASH COMMANDS. `GET /command` lists them and
|
|
164
|
+
// `POST /session/:id/command` runs one, both first-party.
|
|
165
|
+
runCommand: (sup, { client, sessionId, name, args = '' } = {}) => (client && sessionId
|
|
166
|
+
? client.runCommand(sessionId, { command: name, arguments: args })
|
|
167
|
+
: Promise.resolve(null)),
|
|
168
|
+
|
|
169
|
+
// VERSION UPDATES. `opencode upgrade` is its own updater and knows how it
|
|
170
|
+
// was installed; brew and npm installs are still caught by the common plan
|
|
171
|
+
// first so the user's package manager stays in charge where it is the owner.
|
|
172
|
+
updatePlan: options => commonUpdatePlan(definition, options, ({ bin, real, name }) => (
|
|
173
|
+
path.isAbsolute(bin) && real
|
|
174
|
+
? { name, file: bin, args: ['upgrade'], feed: 'https://api.github.com/repos/sst/opencode/releases/latest', format: 'text' }
|
|
175
|
+
: null)),
|
|
176
|
+
updateSettings: () => ({}),
|
|
177
|
+
|
|
178
|
+
smallModelArgs: (prompt, model) => ['run', '--format', 'json', ...(model ? ['--model', model] : []), prompt],
|
|
179
|
+
memoryGate: () => ({}),
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/** A client bound to a server whose port is only known once it says so. */
|
|
183
|
+
export function openCodeServerClient(server, password = null) {
|
|
184
|
+
const address = openCodeServerAddress(server, { password });
|
|
185
|
+
const bind = method => async (...args) => {
|
|
186
|
+
const client = await address;
|
|
187
|
+
return client[method](...args);
|
|
188
|
+
};
|
|
189
|
+
const client = {};
|
|
190
|
+
for (const method of ['createSession', 'prompt', 'abort', 'replyPermission', 'summarize', 'runCommand', 'providers', 'commands', 'config', 'health']) {
|
|
191
|
+
client[method] = bind(method);
|
|
192
|
+
}
|
|
193
|
+
client.events = ({ signal } = {}) => ({
|
|
194
|
+
close: () => {},
|
|
195
|
+
async *[Symbol.asyncIterator]() {
|
|
196
|
+
const real = await address;
|
|
197
|
+
const stream = real.events({ signal });
|
|
198
|
+
client.events.close = () => stream.close();
|
|
199
|
+
yield* stream;
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
return client;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* THE ADDRESS THE SERVER CHOSE, READ OFF ITS OWN FIRST LINE.
|
|
207
|
+
*
|
|
208
|
+
* `opencode serve` prints "opencode server listening on http://127.0.0.1:PORT"
|
|
209
|
+
* and nothing else before it is ready -- measured. Asking for port 0 and
|
|
210
|
+
* reading the answer is how two workers on one Mac never collide, which
|
|
211
|
+
* guessing a port does not survive.
|
|
212
|
+
*/
|
|
213
|
+
export function openCodeServerAddress(server, { password = null, timeoutMs = 30_000 } = {}) {
|
|
214
|
+
return new Promise((resolve, reject) => {
|
|
215
|
+
let text = '';
|
|
216
|
+
const timer = setTimeout(() => reject(Error('OpenCode did not say which port it was listening on')), timeoutMs);
|
|
217
|
+
const done = outcome => {
|
|
218
|
+
clearTimeout(timer);
|
|
219
|
+
server.stdout?.off?.('data', look);
|
|
220
|
+
server.stderr?.off?.('data', look);
|
|
221
|
+
outcome();
|
|
222
|
+
};
|
|
223
|
+
const look = data => {
|
|
224
|
+
text += data.toString();
|
|
225
|
+
const found = text.match(/https?:\/\/[0-9.]+:\d+/);
|
|
226
|
+
if (found) done(() => resolve(openCodeClient({ baseUrl: found[0], password })));
|
|
227
|
+
};
|
|
228
|
+
server.stdout?.on?.('data', look);
|
|
229
|
+
server.stderr?.on?.('data', look);
|
|
230
|
+
server.once('exit', code => done(() => reject(Error(`OpenCode's server stopped before it was ready (${code})`))));
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export { splitModel };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// One registry, no user-installed plugin execution or automatic enrollment.
|
|
2
|
+
// Optional reads report absence; unsupported actions fail before side effects.
|
|
3
|
+
import { claudeHarness } from './harnesses/claude.mjs';
|
|
4
|
+
import { codexHarness } from './harnesses/codex.mjs';
|
|
5
|
+
import { openCodeHarness } from './harnesses/opencode.mjs';
|
|
6
|
+
|
|
7
|
+
export const HARNESS_OPERATIONS = Object.freeze([
|
|
8
|
+
'discover', 'binary', 'workerArgs', 'install', 'signIn', 'signInStatus', 'signInFiles',
|
|
9
|
+
'workerEnv', 'launchEnv', 'profileEnv', 'profiles', 'signedIn', 'accountKey',
|
|
10
|
+
'models', 'defaultModel', 'workspaceModel', 'profileHome', 'effortLevels', 'usage', 'spawn', 'readers',
|
|
11
|
+
'subscribe', 'attachInput', 'tooling', 'transcript', 'change', 'release',
|
|
12
|
+
'compact', 'settleApproval', 'approvalChanged', 'memoryGate', 'updatePlan',
|
|
13
|
+
'smallModelArgs', 'updateSettings', 'runCommand', 'instructions',
|
|
14
|
+
]);
|
|
15
|
+
const OPTIONAL_READS = new Set(['usage', 'models', 'effortLevels']);
|
|
16
|
+
export function createHarnessRegistry(adapters) {
|
|
17
|
+
const entries = new Map();
|
|
18
|
+
for (const adapter of adapters) {
|
|
19
|
+
if (!adapter || typeof adapter.id !== 'string' || !/^[a-z][a-z0-9-]*$/.test(adapter.id) || !adapter.label) throw Error('A harness needs an id and label.');
|
|
20
|
+
if (entries.has(adapter.id)) throw Error(`Duplicate harness: ${adapter.id}`);
|
|
21
|
+
for (const [name, value] of Object.entries(adapter.capabilities ?? {})) {
|
|
22
|
+
if (typeof value !== 'boolean') throw Error(`${adapter.id}.${name} must be a boolean.`);
|
|
23
|
+
}
|
|
24
|
+
if (adapter.capabilities?.remoteControl && typeof adapter.attachInput !== 'function') throw Error(`${adapter.id}.remoteControl requires attachInput.`);
|
|
25
|
+
if (adapter.available != null && typeof adapter.available !== 'function') throw Error(`${adapter.id}.available must be a function or absent.`);
|
|
26
|
+
// WHAT IT CAN DO IS WHAT IT SAYS, NOT WHAT IT HAS A METHOD FOR. Having the
|
|
27
|
+
// method was the whole truth while every adapter was written by hand for
|
|
28
|
+
// one tool. One adapter serving several tools over a shared protocol
|
|
29
|
+
// breaks it: it implements every operation for all of them, and what THIS
|
|
30
|
+
// tool can do is what it negotiated when the session opened. So an adapter
|
|
31
|
+
// may declare an operation unavailable, and a declined one is refused
|
|
32
|
+
// exactly as a missing one is — before it does anything.
|
|
33
|
+
const declines = name => adapter.available ? adapter.available(name) !== true : false;
|
|
34
|
+
const complete = { ...adapter };
|
|
35
|
+
for (const name of HARNESS_OPERATIONS) {
|
|
36
|
+
if (adapter[name] != null && typeof adapter[name] !== 'function') throw Error(`${adapter.id}.${name} must be a function or absent.`);
|
|
37
|
+
const absent = (..._args) => {
|
|
38
|
+
if (OPTIONAL_READS.has(name)) return { supported: false, engine: adapter.id, capability: name };
|
|
39
|
+
throw Error(`${adapter.label} does not support ${name}.`);
|
|
40
|
+
};
|
|
41
|
+
const own = complete[name];
|
|
42
|
+
complete[name] = own ? ((...args) => declines(name) ? absent(...args) : own(...args)) : absent;
|
|
43
|
+
}
|
|
44
|
+
entries.set(adapter.id, { adapter, complete: Object.freeze(complete) });
|
|
45
|
+
}
|
|
46
|
+
return Object.freeze({
|
|
47
|
+
get(id = 'claude') {
|
|
48
|
+
const entry = entries.get(id);
|
|
49
|
+
if (!entry) throw Error(`Unknown coding agent: ${String(id)}`);
|
|
50
|
+
return entry.complete;
|
|
51
|
+
},
|
|
52
|
+
supports(id, capability) {
|
|
53
|
+
const adapter = entries.get(id)?.adapter;
|
|
54
|
+
if (typeof adapter?.[capability] !== 'function') return false;
|
|
55
|
+
return adapter.available ? adapter.available(capability) === true : true;
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
// REGISTERED IS NOT OFFERED. OpenCode is here so anything that asks for it by
|
|
60
|
+
// name gets the real adapter, and it is deliberately absent from `ENGINES`
|
|
61
|
+
// (shared/engines.mjs) so no row can choose it and no picker draws it until
|
|
62
|
+
// that is turned on deliberately. The two decisions are separate on purpose.
|
|
63
|
+
const registry = createHarnessRegistry([claudeHarness, codexHarness, openCodeHarness]);
|
|
64
|
+
export const harnessFor = id => registry.get(id);
|
|
65
|
+
export const supportsHarnessOperation = (id, name) => registry.supports(id, name);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// WHAT THE IMPORT CARD IS HANDED, in one function, so the read setup's last
|
|
2
|
+
// card depends on can be tested whole rather than through a copy of it
|
|
3
|
+
// (w-e46f2e9cea, 2026-10-11: "confirm this works during onboarding, that's the
|
|
4
|
+
// critical location"). `zero:agent-threads` in main/ipc.mjs is this and the
|
|
5
|
+
// `imported` marks; the walk's finish card and the ⌘K card both read it.
|
|
6
|
+
//
|
|
7
|
+
// Every folder either tool keeps sessions in (main/agent-homes.mjs), both
|
|
8
|
+
// readers, newest first. A reader that throws costs its own half, never the
|
|
9
|
+
// other one.
|
|
10
|
+
//
|
|
11
|
+
// AND WHEN THE LAST TEN DAYS HOLD NOTHING, THE NEWEST OLDER ONES. Setup skips
|
|
12
|
+
// its last card when it has nothing to offer, so somebody whose newest session
|
|
13
|
+
// was eleven days old was never told importing exists. The ten days stay the
|
|
14
|
+
// list whenever they hold anything; only an empty one reaches back, ninety days
|
|
15
|
+
// at most and the twenty newest, and the card says so (`listLine` in
|
|
16
|
+
// renderer/src/agent-import-card.ts).
|
|
17
|
+
|
|
18
|
+
import os from 'node:os';
|
|
19
|
+
import { appSessionFolders } from './agent-homes.mjs';
|
|
20
|
+
import { RECENT_DAYS, readSessionThreads } from './agent-sessions.mjs';
|
|
21
|
+
import { readCodexThreads } from './codex-threads.mjs';
|
|
22
|
+
|
|
23
|
+
export const OLDER_DAYS = 90;
|
|
24
|
+
export const OLDER_MOST = 20;
|
|
25
|
+
|
|
26
|
+
export function readImportableThreads({ config = null, supervisor = null, env = process.env, shellEnv, home = os.homedir(), now = Date.now() } = {}) {
|
|
27
|
+
const { dirs, codexDirs } = appSessionFolders({ config, supervisor, env, home, ...(shellEnv ? { shellEnv } : {}) });
|
|
28
|
+
const read = (days, max) => {
|
|
29
|
+
let claude = [];
|
|
30
|
+
let codex = [];
|
|
31
|
+
try { ({ threads: claude } = readSessionThreads({ home, dirs, now, days, ...(max ? { max } : {}) })); } catch { claude = []; }
|
|
32
|
+
try { ({ threads: codex } = readCodexThreads({ home, codexDirs, now, days })); } catch { codex = []; }
|
|
33
|
+
return [...claude, ...codex].sort((a, b) => (b.when ?? 0) - (a.when ?? 0));
|
|
34
|
+
};
|
|
35
|
+
const recent = read(RECENT_DAYS);
|
|
36
|
+
if (recent.length) return recent;
|
|
37
|
+
return read(OLDER_DAYS, OLDER_MOST).slice(0, OLDER_MOST);
|
|
38
|
+
}
|
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
// A fixed allowlist exposes the instruction files, never arbitrary paths.
|
|
2
2
|
// Defaults are shipped separately so an edited live file can always be reset.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// A person opening that page is answering two questions, what should agents do
|
|
8
|
-
// and how should they talk to me, so the writing rules and the finishing rules
|
|
9
|
-
// became ONE document: `message-rules.md`, "How agents write to you". In
|
|
10
|
-
// practice a rules file is mostly about how agents write to the person rather
|
|
11
|
-
// than what they do, which is why the two were one thing all along.
|
|
12
|
-
// `joinMessageRules` below carries the two old files into the new one.
|
|
4
|
+
// General and writing instructions are one user document (w-fb0fe5e85a).
|
|
5
|
+
// The old message id remains available for callers from an older renderer;
|
|
6
|
+
// once migrated, it reads, saves and restores the general instructions.
|
|
13
7
|
import fs from 'node:fs';
|
|
14
8
|
import path from 'node:path';
|
|
15
9
|
import {fileURLToPath} from 'node:url';
|
|
@@ -25,6 +19,14 @@ import {nameSlug} from '../shared/product-name.mjs';
|
|
|
25
19
|
const files = {rules:'founder.md',messages:'message-rules.md',adhd:'adhd-mode.md',system:'worker.md'};
|
|
26
20
|
// The two names the joined document replaced, read only by the migration.
|
|
27
21
|
const WAS_MESSAGES = ['writing-rules.md','finishing.md'];
|
|
22
|
+
const UNIFIED = '.unified-agent-instructions';
|
|
23
|
+
export function agentInstructionsUnified(appDir) {
|
|
24
|
+
return fs.existsSync(path.join(appDir,'briefs',UNIFIED));
|
|
25
|
+
}
|
|
26
|
+
function sectionFor(appDir,id) {
|
|
27
|
+
fileFor(id);
|
|
28
|
+
return id==='messages' && agentInstructionsUnified(appDir) ? 'rules' : id;
|
|
29
|
+
}
|
|
28
30
|
const defaults = JSON.parse(fs.readFileSync(fileURLToPath(new URL('../shared/instruction-defaults.json',import.meta.url)),'utf8'));
|
|
29
31
|
function fileFor(id) {
|
|
30
32
|
if (!Object.hasOwn(files,id)) throw new Error('Unknown instruction section');
|
|
@@ -36,6 +38,7 @@ function fileFor(id) {
|
|
|
36
38
|
// them, so they ride on every run from the checkout and never sit in the box.
|
|
37
39
|
const boxDefault=(id)=>(id==='rules'||id==='messages') ? '' : defaults[id];
|
|
38
40
|
export function readInstruction(appDir,id,shippedDir=appDir) {
|
|
41
|
+
id=sectionFor(appDir,id);
|
|
39
42
|
const file=fileFor(id);
|
|
40
43
|
const defaultText=boxDefault(id);
|
|
41
44
|
let text=defaultText;
|
|
@@ -68,7 +71,7 @@ const KEEP=20;
|
|
|
68
71
|
const QUIET_MS=10*60*1000;
|
|
69
72
|
function historyDir(appDir,id) {return path.join(appDir,HISTORY,id);}
|
|
70
73
|
export function listVersions(appDir,id) {
|
|
71
|
-
|
|
74
|
+
id=sectionFor(appDir,id);
|
|
72
75
|
const dir=historyDir(appDir,id);
|
|
73
76
|
let names;
|
|
74
77
|
try {names=fs.readdirSync(dir);} catch(e) {if(e.code!=='ENOENT') throw e; return [];}
|
|
@@ -79,7 +82,7 @@ export function listVersions(appDir,id) {
|
|
|
79
82
|
}).filter(Boolean).sort((a,b)=>b.ts-a.ts);
|
|
80
83
|
}
|
|
81
84
|
export function readVersion(appDir,id,ts) {
|
|
82
|
-
|
|
85
|
+
id=sectionFor(appDir,id);
|
|
83
86
|
const at=Number(ts);
|
|
84
87
|
if(!Number.isFinite(at)) throw new Error('Unknown version');
|
|
85
88
|
return {text:fs.readFileSync(path.join(historyDir(appDir,id),`${at}.md`),'utf8')};
|
|
@@ -102,6 +105,7 @@ function keepVersion(appDir,id,now=Date.now(),always=false) {
|
|
|
102
105
|
} catch(e) {console.warn(`${nameSlug}: could not keep a restore point for ${id}: ${e.message}`);}
|
|
103
106
|
}
|
|
104
107
|
export function writeInstruction(appDir,id,text,now=Date.now()) {
|
|
108
|
+
id=sectionFor(appDir,id);
|
|
105
109
|
const file=fileFor(id);
|
|
106
110
|
if(typeof text!=='string') throw new Error('Instructions must be text');
|
|
107
111
|
keepVersion(appDir,id,now);
|
|
@@ -150,6 +154,7 @@ export function carryHerBriefsAcross(fromDir,toDir) {
|
|
|
150
154
|
if(!fromDir||!toDir||path.resolve(fromDir)===path.resolve(toDir)) return carried;
|
|
151
155
|
for(const [id,file] of Object.entries(files)) {
|
|
152
156
|
try {
|
|
157
|
+
if(id==='messages' && agentInstructionsUnified(toDir)) continue;
|
|
153
158
|
const target=path.join(toDir,'briefs',file);
|
|
154
159
|
if(fs.existsSync(target)) continue;
|
|
155
160
|
let text;
|
|
@@ -196,6 +201,7 @@ export function carryHerBriefsAcross(fromDir,toDir) {
|
|
|
196
201
|
*/
|
|
197
202
|
export function joinMessageRules(shippedDir,toDir) {
|
|
198
203
|
try {
|
|
204
|
+
if(agentInstructionsUnified(toDir)) return null;
|
|
199
205
|
const target=path.join(toDir,'briefs',files.messages);
|
|
200
206
|
if(fs.existsSync(target)) return null;
|
|
201
207
|
const read=(name)=>{try {return fs.readFileSync(path.join(toDir,'briefs',name),'utf8');} catch {return null;}};
|
|
@@ -225,6 +231,7 @@ export function joinMessageRules(shippedDir,toDir) {
|
|
|
225
231
|
*/
|
|
226
232
|
export function setAsideShippedMessageRules(shippedDir,toDir) {
|
|
227
233
|
try {
|
|
234
|
+
if(agentInstructionsUnified(toDir)) return false;
|
|
228
235
|
const target=path.join(toDir,'briefs',files.messages);
|
|
229
236
|
if(path.resolve(shippedDir)===path.resolve(toDir)||!fs.existsSync(target)) return false;
|
|
230
237
|
const read=(name)=>{try {return fs.readFileSync(path.join(shippedDir,'briefs',name),'utf8').trim();} catch {return '';}};
|
|
@@ -241,3 +248,32 @@ export function setAsideShippedMessageRules(shippedDir,toDir) {
|
|
|
241
248
|
return false;
|
|
242
249
|
}
|
|
243
250
|
}
|
|
251
|
+
|
|
252
|
+
// Runs after the older migrations, before the supervisor or editors read.
|
|
253
|
+
// Original message files and their histories remain on disk for recovery.
|
|
254
|
+
// The completion marker prevents later boots reimporting them after an edit
|
|
255
|
+
// or an intentional empty save. A failed save leaves both originals active.
|
|
256
|
+
export function unifyAgentInstructions(shippedDir,toDir) {
|
|
257
|
+
try {
|
|
258
|
+
if(path.resolve(shippedDir)===path.resolve(toDir)||agentInstructionsUnified(toDir)) return null;
|
|
259
|
+
const general=readInstruction(toDir,'rules').text;
|
|
260
|
+
const writing=readInstruction(toDir,'messages').text;
|
|
261
|
+
if(writing.trim()) {
|
|
262
|
+
// If the combined save succeeded but completion was interrupted, the exact
|
|
263
|
+
// old text is already at the end. Retrying must not append it again.
|
|
264
|
+
const combined=general.endsWith(writing) ? general : general ? `${general}\n\n${writing}` : writing;
|
|
265
|
+
keepVersion(toDir,'rules',Date.now(),true);
|
|
266
|
+
keepVersion(toDir,'messages',Date.now(),true);
|
|
267
|
+
if(combined!==general) writeInstruction(toDir,'rules',combined);
|
|
268
|
+
}
|
|
269
|
+
const marker=path.join(toDir,'briefs',UNIFIED);
|
|
270
|
+
fs.mkdirSync(path.dirname(marker),{recursive:true});
|
|
271
|
+
const temporary=`${marker}.tmp-${process.pid}`;
|
|
272
|
+
fs.writeFileSync(temporary,'1\n','utf8');
|
|
273
|
+
fs.renameSync(temporary,marker);
|
|
274
|
+
return {general:general.length,writing:writing.length};
|
|
275
|
+
} catch(e) {
|
|
276
|
+
console.warn(`${nameSlug}: could not combine the agent instructions: ${e.message}`);
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
}
|