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.
Files changed (239) hide show
  1. package/README.md +24 -16
  2. package/bin/agentbox.mjs +18 -9
  3. package/briefs/message-rules.md +15 -4
  4. package/main/account-tooling.mjs +2 -1
  5. package/main/acp.mjs +296 -0
  6. package/main/agent-homes.mjs +158 -0
  7. package/main/agent-sessions.mjs +57 -20
  8. package/main/agent-updates.mjs +9 -20
  9. package/main/analytics.mjs +30 -0
  10. package/main/approvals.mjs +14 -5
  11. package/main/claude-bin.mjs +45 -310
  12. package/main/claude-input.mjs +9 -3
  13. package/main/claude-launch-env.mjs +54 -0
  14. package/main/claude-models.mjs +39 -7
  15. package/main/claude-plan.mjs +14 -2
  16. package/main/claude-usage.mjs +82 -25
  17. package/main/codex-bin.mjs +51 -248
  18. package/main/codex-launch-env.mjs +31 -0
  19. package/main/codex-session.mjs +38 -3
  20. package/main/codex-threads.mjs +41 -8
  21. package/main/codex-watch.mjs +67 -3
  22. package/main/config.mjs +40 -13
  23. package/main/corner-tag.mjs +489 -0
  24. package/main/crash-report.mjs +26 -3
  25. package/main/demo.mjs +17 -14
  26. package/main/engine-bin.mjs +595 -0
  27. package/main/engine-setup.mjs +27 -27
  28. package/main/feedback.mjs +11 -6
  29. package/main/fresh-user.mjs +1 -1
  30. package/main/git-change.mjs +8 -2
  31. package/main/harnesses/claude-stream.mjs +208 -0
  32. package/main/harnesses/claude.mjs +84 -0
  33. package/main/harnesses/codex.mjs +65 -0
  34. package/main/harnesses/common.mjs +78 -0
  35. package/main/harnesses/instructions-file.mjs +116 -0
  36. package/main/harnesses/opencode.mjs +234 -0
  37. package/main/harnesses.mjs +65 -0
  38. package/main/importable-threads.mjs +38 -0
  39. package/main/instruction-settings.mjs +47 -11
  40. package/main/ipc.mjs +175 -22
  41. package/main/local-app-frames.mjs +82 -0
  42. package/main/machine.mjs +1 -1
  43. package/main/main.mjs +120 -5
  44. package/main/memory-gate-history.mjs +51 -12
  45. package/main/memory-gate-server.mjs +67 -11
  46. package/main/memory-gate.mjs +49 -8
  47. package/main/memory-log.mjs +124 -0
  48. package/main/notify.mjs +48 -5
  49. package/main/opencode-server.mjs +271 -0
  50. package/main/opencode.mjs +287 -0
  51. package/main/phone-link.mjs +551 -0
  52. package/main/refresh-agent-models.mjs +2 -1
  53. package/main/relay-address.mjs +28 -0
  54. package/main/relay-client.mjs +246 -0
  55. package/main/row-label.mjs +8 -7
  56. package/main/runs-on.mjs +90 -0
  57. package/main/serve.mjs +124 -10
  58. package/main/settings.mjs +171 -11
  59. package/main/shell-path.mjs +86 -0
  60. package/main/ship-queue.mjs +25 -7
  61. package/main/sign-in-files.mjs +3 -7
  62. package/main/source-updater.mjs +27 -1
  63. package/main/start-hold.mjs +73 -0
  64. package/main/store/home.mjs +37 -5
  65. package/main/store/project.mjs +15 -19
  66. package/main/store.mjs +10 -4
  67. package/main/supervisor.mjs +1030 -447
  68. package/main/task-commands.mjs +13 -17
  69. package/main/task-folders-offthread.mjs +6 -1
  70. package/main/task-folders.mjs +491 -82
  71. package/main/task-terminals.mjs +49 -8
  72. package/main/team/index.mjs +2 -2
  73. package/main/team/projects.mjs +1 -1
  74. package/main/updater.mjs +71 -9
  75. package/main/worktree-include.mjs +494 -0
  76. package/mcp/core/page.mjs +1 -1
  77. package/mcp/core/work.mjs +34 -3
  78. package/mcp/tools.mjs +2 -1
  79. package/package.json +10 -1
  80. package/renderer/dist/assets/abnfDiagram-YUKMZFIV-DPPuMWt-.js +1 -0
  81. package/renderer/dist/assets/arc-HmFfdseH.js +1 -0
  82. package/renderer/dist/assets/architectureDiagram-47P4ROYG-DxJ3zHrH.js +36 -0
  83. package/renderer/dist/assets/blockDiagram-E7TT5TSB-CbTXzOqQ.js +140 -0
  84. package/renderer/dist/assets/c4Diagram-CRA5TL53-X5ItFyLn.js +38 -0
  85. package/renderer/dist/assets/channel-BlT2g9Sz.js +1 -0
  86. package/renderer/dist/assets/chunk-2Q5K7J3B-DC12Ho_L.js +1 -0
  87. package/renderer/dist/assets/chunk-5VM5RSS4-ClVtfPd0.js +15 -0
  88. package/renderer/dist/assets/chunk-7TKQ45FW-DgcOm0HT.js +1 -0
  89. package/renderer/dist/assets/chunk-AW2ZBBNX-C01s4yoF.js +88 -0
  90. package/renderer/dist/assets/chunk-FVRAUYC3-Bj0zxhR4.js +2 -0
  91. package/renderer/dist/assets/chunk-HTAEGDNF-DWDeEhLU.js +1 -0
  92. package/renderer/dist/assets/chunk-JWPE2WC7-Dsqwv3eW.js +1 -0
  93. package/renderer/dist/assets/chunk-KQW6MTUR-CE86ANL2.js +1 -0
  94. package/renderer/dist/assets/chunk-XXDRQBXY-CoNTASfs.js +1 -0
  95. package/renderer/dist/assets/classDiagram-v2-K4WV4PDN-2wprLqtw.js +217 -0
  96. package/renderer/dist/assets/cornerTag-BLrxiDE5.css +1 -0
  97. package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +1 -0
  98. package/renderer/dist/assets/cose-bilkent-JH36ORCC-enqMiYbn.js +1 -0
  99. package/renderer/dist/assets/cynefin-EF2NZ3EQ-DHngLxvQ.js +141 -0
  100. package/renderer/dist/assets/cynefinDiagram-3GCD6N5R-C0rZZIl3.js +62 -0
  101. package/renderer/dist/assets/cytoscape.esm-C5JX3QLV.js +331 -0
  102. package/renderer/dist/assets/dagre-W4DXFKR2-DQnaEWO7.js +4 -0
  103. package/renderer/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  104. package/renderer/dist/assets/diagram-2UJZ2QOL-CS8AlGPx.js +200 -0
  105. package/renderer/dist/assets/diagram-OVF4WLC6-yiDMHIkY.js +24 -0
  106. package/renderer/dist/assets/diagram-PFPMY2P6-XbQKzbBA.js +30 -0
  107. package/renderer/dist/assets/diagram-UMYRVEAY-dhVvlfl_.js +24 -0
  108. package/renderer/dist/assets/diagram-YEKJPTXX-DZHe8SCf.js +41 -0
  109. package/renderer/dist/assets/diagram-ZIFT7M5P-B5h6aXVR.js +3 -0
  110. package/renderer/dist/assets/ebnfDiagram-VR2GEFS7-PR9ScDco.js +1 -0
  111. package/renderer/dist/assets/elk-IJKZMXRS-Cg-yQXPd.js +27 -0
  112. package/renderer/dist/assets/erDiagram-O2IAPWRE-omOrJMxu.js +99 -0
  113. package/renderer/dist/assets/flowDiagram-OXPTDLAJ-aOX2hzvT.js +213 -0
  114. package/renderer/dist/assets/ganttDiagram-R7TSEDQI-B1H3pWyI.js +292 -0
  115. package/renderer/dist/assets/gitGraphDiagram-XJZIOB7I-Cvw304HC.js +106 -0
  116. package/renderer/dist/assets/graph-DGbsTEsI.js +1 -0
  117. package/renderer/dist/assets/infoDiagram-5W2HQ5XZ-_ZXcNViH.js +2 -0
  118. package/renderer/dist/assets/init-Gi6I4Gst.js +1 -0
  119. package/renderer/dist/assets/ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js +70 -0
  120. package/renderer/dist/assets/journeyDiagram-COXZFDF6-fr62QoAt.js +139 -0
  121. package/renderer/dist/assets/kanban-definition-P3RFRI5V-J8zsabCe.js +89 -0
  122. package/renderer/dist/assets/katex-C5jXJg4s.js +257 -0
  123. package/renderer/dist/assets/linear-BmNdEtIi.js +1 -0
  124. package/renderer/dist/assets/main-ChNdnUvt.js +1132 -0
  125. package/renderer/dist/assets/main-nj0QR5kg.css +32 -0
  126. package/renderer/dist/assets/mermaid.core-CnYzvFkz.js +314 -0
  127. package/renderer/dist/assets/mindmap-definition-LZFPQGKD-Bfcxc7PM.js +96 -0
  128. package/renderer/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  129. package/renderer/dist/assets/pegDiagram-BGZESJAR-CwUJOq6X.js +1 -0
  130. package/renderer/dist/assets/pieDiagram-CAPJLFHJ-BrwMmqfC.js +39 -0
  131. package/renderer/dist/assets/product-name-CTKLYNWY.js +40 -0
  132. package/renderer/dist/assets/quadrantDiagram-GDMTTRAM-BN4Stoj-.js +7 -0
  133. package/renderer/dist/assets/railroadDiagram-SM67HX2B-CPZ5bbeR.js +1 -0
  134. package/renderer/dist/assets/requirementDiagram-X7JNWC4A-CgRnimVe.js +84 -0
  135. package/renderer/dist/assets/sankeyDiagram-UM26HJRW-Bt2rej__.js +40 -0
  136. package/renderer/dist/assets/sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js +169 -0
  137. package/renderer/dist/assets/sizeCapture-INFHLROL-usyNyXyh.js +1 -0
  138. package/renderer/dist/assets/stateDiagram-v2-TBUQTH76-CRzUBb0u.js +291 -0
  139. package/renderer/dist/assets/swimlanes-N4OXWK64-C1GJvMS0.js +1 -0
  140. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +8 -0
  141. package/renderer/dist/assets/timeline-definition-YOQAKGHF-caeyPlcc.js +120 -0
  142. package/renderer/dist/assets/usecaseDiagram-VIAY4XPW-BN_SV6PC.js +370 -0
  143. package/renderer/dist/assets/vennDiagram-BLWOH2XV-BLGuaUX-.js +34 -0
  144. package/renderer/dist/assets/wardleyDiagram-YMQ3BMBF-Da0cJeNc.js +78 -0
  145. package/renderer/dist/assets/xychartDiagram-TAQBALBS-cGfUffMk.js +7 -0
  146. package/renderer/dist/corner-tag.html +13 -0
  147. package/renderer/dist/emulator/audio-rules.mjs +74 -0
  148. package/renderer/dist/emulator/data/compression/README.md +11 -0
  149. package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
  150. package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
  151. package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
  152. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  153. package/renderer/dist/emulator/data/cores/README.md +22 -0
  154. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  155. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  156. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  157. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  158. package/renderer/dist/emulator/data/cores/package.json +65 -0
  159. package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
  160. package/renderer/dist/emulator/data/emulator.css +1614 -0
  161. package/renderer/dist/emulator/data/loader.js +170 -0
  162. package/renderer/dist/emulator/data/localization/README.md +63 -0
  163. package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
  164. package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
  165. package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
  166. package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
  167. package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
  168. package/renderer/dist/emulator/data/localization/en-US.json +339 -0
  169. package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
  170. package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
  171. package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
  172. package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
  173. package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
  174. package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
  175. package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
  176. package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
  177. package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
  178. package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
  179. package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
  180. package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
  181. package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
  182. package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
  183. package/renderer/dist/emulator/data/src/GameManager.js +464 -0
  184. package/renderer/dist/emulator/data/src/compression.js +142 -0
  185. package/renderer/dist/emulator/data/src/emulator.js +6158 -0
  186. package/renderer/dist/emulator/data/src/gamepad.js +167 -0
  187. package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
  188. package/renderer/dist/emulator/data/src/shaders.js +236 -0
  189. package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
  190. package/renderer/dist/emulator/data/src/storage.js +125 -0
  191. package/renderer/dist/emulator/data/version.json +5 -0
  192. package/renderer/dist/emulator/host.js +297 -0
  193. package/renderer/dist/emulator/index.html +28 -0
  194. package/renderer/dist/emulator/provenance.json +6 -0
  195. package/renderer/dist/index.html +16 -3
  196. package/scripts/after-install.mjs +14 -0
  197. package/scripts/approval-server.sh +31 -7
  198. package/scripts/before-publish.mjs +12 -10
  199. package/shared/agent-import.mjs +3 -1
  200. package/shared/agents.d.mts +1 -0
  201. package/shared/bridge-map.mjs +19 -0
  202. package/shared/claude-commands.generated.mjs +8 -6
  203. package/shared/claude-models.generated.mjs +10 -8
  204. package/shared/codex-modes.mjs +1 -1
  205. package/shared/corner-tag.d.mts +14 -0
  206. package/shared/corner-tag.mjs +141 -0
  207. package/shared/demo-world.mjs +1 -1
  208. package/shared/engines.mjs +20 -5
  209. package/shared/fresh-user-home.mjs +12 -1
  210. package/shared/harness-definitions.mjs +103 -0
  211. package/shared/instruction-defaults.json +1 -1
  212. package/shared/notify-rules.d.mts +25 -0
  213. package/shared/notify-rules.mjs +19 -0
  214. package/shared/project-folder-check.mjs +3 -3
  215. package/shared/provider-commands.mjs +10 -2
  216. package/shared/rank.d.mts +2 -0
  217. package/shared/rank.mjs +50 -0
  218. package/shared/relay-address.mjs +6 -0
  219. package/shared/relay-crypto.mjs +165 -0
  220. package/shared/runs-on.d.mts +10 -0
  221. package/shared/runs-on.mjs +55 -0
  222. package/shared/shortcuts.mjs +11 -5
  223. package/shared/spawn-trouble.mjs +58 -7
  224. package/shared/terminal-input.d.mts +1 -0
  225. package/shared/terminal-input.mjs +30 -0
  226. package/shared/terminal-keys.d.mts +1 -0
  227. package/shared/terminal-keys.mjs +16 -0
  228. package/shared/terminal-size.d.mts +4 -0
  229. package/shared/terminal-size.mjs +29 -0
  230. package/shared/usage.d.mts +3 -0
  231. package/shared/usage.mjs +7 -0
  232. package/shared/where-it-runs-from.d.mts +43 -0
  233. package/shared/where-it-runs-from.mjs +155 -0
  234. package/shared/work-items.mjs +28 -2
  235. package/shared/work-lines.mjs +1 -1
  236. package/shared/xterm-invalid-range.d.mts +2 -0
  237. package/shared/xterm-invalid-range.mjs +58 -0
  238. package/renderer/dist/assets/index-B-2Az952.css +0 -32
  239. 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
- // THERE ARE THREE OF THEM NOW, NOT FOUR (w-3dc46f3a67). Five editable layers of
5
- // prompts was too many for a person to manage, so some were merged.
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
- fileFor(id);
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
- fileFor(id);
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
+ }