agentbox-app 0.1.7 → 0.1.12
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 +220 -58
- package/bin/agentbox.mjs +18 -3
- package/briefs/message-rules.md +24 -3
- package/briefs/worker.md +63 -49
- package/briefs/writing-rules.md +90 -131
- package/main/account-discovery.mjs +21 -20
- package/main/account-tooling.mjs +11 -11
- package/main/agent-activity.mjs +45 -5
- package/main/agent-schedule.mjs +14 -14
- package/main/agent-sessions.mjs +94 -10
- package/main/agent-updates.mjs +116 -0
- package/main/agents.mjs +52 -46
- package/main/analytics.mjs +27 -0
- package/main/approval-prompt-server.mjs +2 -2
- package/main/approvals.mjs +14 -5
- package/main/artifact-path.mjs +11 -0
- package/main/claude-bin.mjs +32 -5
- package/main/claude-input.mjs +45 -3
- package/main/claude-plan.mjs +25 -13
- package/main/code-change.mjs +3 -3
- package/main/codex-account.mjs +1 -1
- package/main/codex-bin.mjs +18 -0
- package/main/codex-launch-env.mjs +31 -0
- package/main/codex-memory-gate.mjs +260 -0
- package/main/codex-models.mjs +12 -7
- package/main/codex-threads.mjs +46 -23
- package/main/codex-usage-file.mjs +7 -7
- package/main/codex-usage.mjs +4 -4
- package/main/codex-watch.mjs +12 -12
- package/main/config.mjs +82 -17
- package/main/demo.mjs +17 -14
- package/main/dev-window.mjs +9 -9
- package/main/doc-scheme.mjs +2 -0
- package/main/engine-setup.mjs +204 -0
- package/main/feedback.mjs +55 -0
- package/main/folder-pictures.mjs +48 -0
- package/main/frame-scrollbars.mjs +90 -0
- package/main/fresh-user.mjs +7 -4
- package/main/git-change-offthread.mjs +70 -0
- package/main/git-change-thread.mjs +11 -0
- package/main/git-change.mjs +9 -3
- package/main/img-scheme.mjs +70 -1
- package/main/instruction-settings.mjs +134 -60
- package/main/ipc.mjs +335 -42
- package/main/leftovers.mjs +518 -0
- package/main/live-replies.mjs +43 -5
- package/main/machine.mjs +48 -1
- package/main/main.mjs +138 -15
- package/main/memory-gate-history.mjs +254 -0
- package/main/memory-gate-server.mjs +461 -0
- package/main/memory-gate.mjs +206 -0
- package/main/message-priority.mjs +87 -0
- package/main/notify.mjs +1 -1
- package/main/rail-note.mjs +2 -2
- package/main/refresh-agent-models.mjs +21 -0
- package/main/repeats.mjs +4 -4
- package/main/row-label.mjs +115 -51
- package/main/runs-on.mjs +93 -0
- package/main/serve.mjs +77 -5
- package/main/settings.mjs +245 -26
- package/main/ship-queue.mjs +279 -0
- package/main/sign-in-files.mjs +39 -0
- package/main/source-updater.mjs +298 -0
- package/main/store/home.mjs +84 -7
- package/main/store/project.mjs +15 -19
- package/main/store/work-items.mjs +150 -6
- package/main/store.mjs +245 -13
- package/main/supervisor.mjs +1634 -417
- package/main/task-commands.mjs +8 -10
- package/main/task-folders.mjs +94 -16
- package/main/task-remote-control.mjs +3 -3
- package/main/task-terminals.mjs +14 -2
- package/main/team/index.mjs +440 -0
- package/main/team/memory-cloud.mjs +247 -0
- package/main/team/projects.mjs +162 -0
- package/main/team/session.mjs +225 -0
- package/main/team/sign-in.mjs +106 -0
- package/main/team/supabase-backend.mjs +231 -0
- package/main/team/sync.mjs +163 -0
- package/main/updater.mjs +15 -5
- package/main/write-before-reload.mjs +50 -0
- package/mcp/core/page.mjs +10 -12
- package/mcp/core/work.mjs +123 -26
- package/mcp/tools.mjs +13 -6
- package/package.json +21 -9
- package/renderer/dist/assets/agentbox-icon-F1oN9nj_.svg +15 -0
- package/renderer/dist/assets/index-ChKsQkRw.js +1169 -0
- package/renderer/dist/assets/index-ClBXnq3B.css +32 -0
- package/renderer/dist/index.html +2 -2
- package/scripts/after-install.mjs +14 -0
- package/scripts/approval-server.sh +7 -2
- package/scripts/memory-gate-hook.sh +59 -0
- package/shared/agent-import.mjs +39 -16
- package/shared/agents.d.mts +26 -8
- package/shared/agents.mjs +100 -108
- package/shared/analytics-events.mjs +19 -2
- package/shared/answers.mjs +12 -0
- package/shared/bridge-map.d.mts +12 -0
- package/shared/bridge-map.mjs +38 -1
- package/shared/claude-commands.generated.mjs +2 -2
- package/shared/claude-models.generated.mjs +6 -6
- package/shared/claude-usage.mjs +6 -6
- package/shared/codex-import.mjs +7 -7
- package/shared/codex-modes.d.mts +50 -0
- package/shared/codex-modes.mjs +1 -1
- package/shared/codex-usage.mjs +7 -8
- package/shared/demo-world.mjs +6 -6
- package/shared/engines.mjs +27 -10
- package/shared/feedback.d.mts +14 -0
- package/shared/feedback.mjs +44 -0
- package/shared/first-run-examples.mjs +4 -4
- package/shared/first-run-practice.d.mts +16 -0
- package/shared/first-run-practice.mjs +176 -70
- package/shared/fresh-user-home.mjs +34 -4
- package/shared/instruction-defaults.json +2 -2
- package/shared/product-name.mjs +4 -2
- package/shared/project-folder-check.mjs +9 -3
- package/shared/rank.d.mts +1 -0
- package/shared/rank.mjs +22 -0
- package/shared/referenced-files.mjs +1 -1
- package/shared/runs-on.d.mts +20 -0
- package/shared/runs-on.mjs +79 -0
- package/shared/schemes.d.mts +11 -0
- package/shared/schemes.mjs +1 -1
- package/shared/shortcuts.d.mts +41 -0
- package/shared/shortcuts.mjs +236 -0
- package/shared/spawn-trouble.d.mts +2 -0
- package/shared/spawn-trouble.mjs +40 -19
- package/shared/team-rules.d.mts +17 -0
- package/shared/team-rules.mjs +230 -0
- package/shared/team-status.d.mts +11 -0
- package/shared/team-status.mjs +90 -0
- package/shared/terminal-links.d.mts +2 -0
- package/shared/terminal-links.mjs +55 -0
- package/shared/terminal-state.d.mts +1 -0
- package/shared/terminal-state.mjs +16 -0
- package/shared/thread-cards.d.mts +19 -0
- package/shared/thread-cards.mjs +254 -0
- package/shared/usage.d.mts +1 -1
- package/shared/usage.mjs +6 -7
- package/shared/work-items.mjs +258 -28
- package/shared/work-lines.mjs +393 -16
- package/worker-permissions.json +1 -1
- package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
- package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
- package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
- package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
- package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
- package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
- package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
- package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
- package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
- package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
- package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
- package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
- package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
- package/renderer/dist/assets/index-B7hhfHrT.js +0 -684
- package/renderer/dist/assets/index-CwiDv5bL.css +0 -32
- package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
- package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
- package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
- package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
- package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
- package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
- package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
- package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
- package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
- package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
- package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
- package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
- package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
- package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
- package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
- package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
- package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
- package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
- package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
- package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
- package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
- package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
- package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
- package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
- package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
- package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
- package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
- package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
- package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
- package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
- package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
- package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
- package/renderer/dist/emulator/audio-rules.mjs +0 -74
- package/renderer/dist/emulator/data/compression/README.md +0 -11
- package/renderer/dist/emulator/data/compression/extract7z.js +0 -1
- package/renderer/dist/emulator/data/compression/extractzip.js +0 -1
- package/renderer/dist/emulator/data/compression/libunrar.js +0 -519
- package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
- package/renderer/dist/emulator/data/cores/README.md +0 -22
- 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 +0 -65
- package/renderer/dist/emulator/data/cores/reports/mgba.json +0 -1
- package/renderer/dist/emulator/data/emulator.css +0 -1614
- package/renderer/dist/emulator/data/loader.js +0 -170
- package/renderer/dist/emulator/data/localization/README.md +0 -63
- package/renderer/dist/emulator/data/localization/af-FR.json +0 -339
- package/renderer/dist/emulator/data/localization/ar-AR.json +0 -301
- package/renderer/dist/emulator/data/localization/ben-BEN.json +0 -301
- package/renderer/dist/emulator/data/localization/de-GER.json +0 -301
- package/renderer/dist/emulator/data/localization/el-GR.json +0 -301
- package/renderer/dist/emulator/data/localization/en-US.json +0 -339
- package/renderer/dist/emulator/data/localization/es-ES.json +0 -310
- package/renderer/dist/emulator/data/localization/fa-AF.json +0 -310
- package/renderer/dist/emulator/data/localization/hi-HI.json +0 -301
- package/renderer/dist/emulator/data/localization/it-IT.json +0 -302
- package/renderer/dist/emulator/data/localization/ja-JA.json +0 -301
- package/renderer/dist/emulator/data/localization/jv-JV.json +0 -301
- package/renderer/dist/emulator/data/localization/ko-KO.json +0 -336
- package/renderer/dist/emulator/data/localization/pt-BR.json +0 -361
- package/renderer/dist/emulator/data/localization/retroarch.json +0 -617
- package/renderer/dist/emulator/data/localization/ro-RO.json +0 -310
- package/renderer/dist/emulator/data/localization/ru-RU.json +0 -301
- package/renderer/dist/emulator/data/localization/tr-TR.json +0 -310
- package/renderer/dist/emulator/data/localization/vi-VN.json +0 -302
- package/renderer/dist/emulator/data/localization/zh-CN.json +0 -351
- package/renderer/dist/emulator/data/src/GameManager.js +0 -464
- package/renderer/dist/emulator/data/src/compression.js +0 -142
- package/renderer/dist/emulator/data/src/emulator.js +0 -6158
- package/renderer/dist/emulator/data/src/gamepad.js +0 -167
- package/renderer/dist/emulator/data/src/nipplejs.js +0 -1
- package/renderer/dist/emulator/data/src/shaders.js +0 -236
- package/renderer/dist/emulator/data/src/socket.io.min.js +0 -7
- package/renderer/dist/emulator/data/src/storage.js +0 -125
- package/renderer/dist/emulator/data/version.json +0 -5
- package/renderer/dist/emulator/host.js +0 -299
- package/renderer/dist/emulator/index.html +0 -28
- package/renderer/dist/emulator/provenance.json +0 -6
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// A PAGE IN THE PANE KEEPS ITS SCROLLBAR OUT OF SIGHT UNTIL YOU SCROLL.
|
|
2
|
+
//
|
|
3
|
+
// The app's own scrollbars are invisible at rest (styles.css, "SCROLLBARS ARE
|
|
4
|
+
// NOT FURNITURE"), but a page drawn in the document pane is its own document
|
|
5
|
+
// and that stylesheet stops at the frame. With a mouse plugged in, macOS draws
|
|
6
|
+
// classic scrollbars, and a page that does not declare itself dark gets the
|
|
7
|
+
// light ones: a white track the full height of the pane with a grey thumb in
|
|
8
|
+
// it. That was the report, with a screenshot, on 2026-10-05.
|
|
9
|
+
//
|
|
10
|
+
// THE FILE IS STILL SERVED EXACTLY AS IT WAS WRITTEN (doc-scheme.mjs). What is
|
|
11
|
+
// added here is the app's own chrome, from outside, once the page's document
|
|
12
|
+
// exists: one adopted stylesheet that touches nothing but scrollbars, inside a
|
|
13
|
+
// cascade layer so a page that styled its own scrollbars keeps them, and two
|
|
14
|
+
// passive listeners that only ever swap that sheet. Nothing a page does is
|
|
15
|
+
// changed or stopped, and no key or gesture of hers is listened to.
|
|
16
|
+
//
|
|
17
|
+
// A frame whose sandbox has no allow-scripts cannot be handed this (measured:
|
|
18
|
+
// "Script not run"). The one such frame is the small preview under a message,
|
|
19
|
+
// and that one is told not to scroll at all (ArtifactThumbnail.tsx).
|
|
20
|
+
import { DOC_SCHEMES, isScheme } from '../shared/schemes.mjs';
|
|
21
|
+
|
|
22
|
+
const RULES = (thumb) => `@layer agentbox-scrollbars {
|
|
23
|
+
::-webkit-scrollbar { width: 10px; height: 10px; }
|
|
24
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
25
|
+
::-webkit-scrollbar-corner { background: transparent; }
|
|
26
|
+
::-webkit-scrollbar-thumb { background: ${thumb}; border-radius: 10px; border: 3px solid transparent; background-clip: content-box; }
|
|
27
|
+
::-webkit-scrollbar-thumb:hover { background: rgba(150, 150, 150, 0.7); background-clip: content-box; }
|
|
28
|
+
}`;
|
|
29
|
+
|
|
30
|
+
/** No track and no thumb: what a page looks like while nobody is scrolling it. */
|
|
31
|
+
export const AT_REST = RULES('transparent');
|
|
32
|
+
/** A thin soft thumb that reads on a dark page and a light one alike. */
|
|
33
|
+
export const SHOWN = RULES('rgba(140, 140, 140, 0.45)');
|
|
34
|
+
|
|
35
|
+
/** Is this a page of ours or a file, rather than a running app or the web. */
|
|
36
|
+
export function wantsQuietScrollbars(url) {
|
|
37
|
+
let parsed;
|
|
38
|
+
try { parsed = new URL(String(url ?? '')); } catch { return false; }
|
|
39
|
+
return parsed.protocol === 'file:' || isScheme(DOC_SCHEMES, parsed.protocol);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Runs INSIDE the page, so it is written to be serialised: everything it uses
|
|
44
|
+
* comes in through its arguments. Shown while the page or anything in it
|
|
45
|
+
* scrolls and for a moment after, and while the pointer is at the right edge
|
|
46
|
+
* where the thumb would be grabbed.
|
|
47
|
+
*/
|
|
48
|
+
export function quietScrollbars(page, atRest, shown) {
|
|
49
|
+
const doc = page.document;
|
|
50
|
+
if (doc.adoptedStyleSheets.some((s) => s.agentboxScrollbars)) return;
|
|
51
|
+
const sheet = new page.CSSStyleSheet();
|
|
52
|
+
sheet.agentboxScrollbars = true;
|
|
53
|
+
sheet.replaceSync(atRest);
|
|
54
|
+
doc.adoptedStyleSheets = [...doc.adoptedStyleSheets, sheet];
|
|
55
|
+
let showing = false;
|
|
56
|
+
let atEdge = false;
|
|
57
|
+
let timer = null;
|
|
58
|
+
const show = () => { if (!showing) { sheet.replaceSync(shown); showing = true; } };
|
|
59
|
+
const hideSoon = () => {
|
|
60
|
+
page.clearTimeout(timer);
|
|
61
|
+
timer = page.setTimeout(() => { timer = null; if (!atEdge) { sheet.replaceSync(atRest); showing = false; } }, 900);
|
|
62
|
+
};
|
|
63
|
+
page.addEventListener('scroll', () => { show(); if (!atEdge) hideSoon(); }, { capture: true, passive: true });
|
|
64
|
+
page.addEventListener('mousemove', (e) => {
|
|
65
|
+
const edge = e.clientX >= doc.documentElement.clientWidth - 14;
|
|
66
|
+
if (edge === atEdge) return;
|
|
67
|
+
atEdge = edge;
|
|
68
|
+
if (edge) { page.clearTimeout(timer); timer = null; show(); } else hideSoon();
|
|
69
|
+
}, { passive: true });
|
|
70
|
+
page.addEventListener('mouseout', (e) => {
|
|
71
|
+
if (e.relatedTarget || !atEdge) return;
|
|
72
|
+
atEdge = false;
|
|
73
|
+
hideSoon();
|
|
74
|
+
}, { passive: true });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const SCRIPT = `(${quietScrollbars.toString()})(window, ${JSON.stringify(AT_REST)}, ${JSON.stringify(SHOWN)})`;
|
|
78
|
+
|
|
79
|
+
function quiet(frame) {
|
|
80
|
+
if (!frame || !wantsQuietScrollbars(frame.url)) return;
|
|
81
|
+
frame.executeJavaScript(SCRIPT).catch(() => { /* a sandbox without scripts, or the frame went away */ });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Every page frame in this window gets the quiet scrollbar as its document is ready. */
|
|
85
|
+
export function quietTheFramesScrollbars(contents) {
|
|
86
|
+
contents.on('frame-created', (_event, { frame }) => {
|
|
87
|
+
if (!frame || frame === contents.mainFrame) return;
|
|
88
|
+
frame.on('dom-ready', () => quiet(frame));
|
|
89
|
+
});
|
|
90
|
+
}
|
package/main/fresh-user.mjs
CHANGED
|
@@ -57,6 +57,9 @@ export function relaunchCommand({ execPath = process.execPath, argv = process.ar
|
|
|
57
57
|
* It cannot break inbox zero: the walk shows only its own three examples. */
|
|
58
58
|
export function openFreshUser({
|
|
59
59
|
withAgents = true,
|
|
60
|
+
// False is a new user with no Claude Code or Codex of hers, who signs in
|
|
61
|
+
// for itself: the way to see the walk's plan question (w-9f6975906c).
|
|
62
|
+
withTools = true,
|
|
60
63
|
execPath = process.execPath,
|
|
61
64
|
argv = process.argv,
|
|
62
65
|
packaged = true,
|
|
@@ -65,7 +68,7 @@ export function openFreshUser({
|
|
|
65
68
|
} = {}) {
|
|
66
69
|
let prepared;
|
|
67
70
|
try {
|
|
68
|
-
prepared = prepareHome(makeHome(now), { withAgents });
|
|
71
|
+
prepared = prepareHome(makeHome(now), { withAgents: withTools && withAgents, withTools });
|
|
69
72
|
} catch (err) {
|
|
70
73
|
return { ok: false, error: `Could not make a throwaway home under ${HOMES}: ${err?.message ?? err}` };
|
|
71
74
|
}
|
|
@@ -80,7 +83,7 @@ export function openFreshUser({
|
|
|
80
83
|
child = spawnFn(bin, args, {
|
|
81
84
|
detached: true,
|
|
82
85
|
stdio: 'ignore',
|
|
83
|
-
env: freshEnv(prepared.home),
|
|
86
|
+
env: freshEnv(prepared.home, process.env, { ownSignIns: !withTools }),
|
|
84
87
|
});
|
|
85
88
|
child.unref?.();
|
|
86
89
|
} catch (err) {
|
|
@@ -91,9 +94,9 @@ export function openFreshUser({
|
|
|
91
94
|
// be missing without looking broken, and a test that quietly cannot run an
|
|
92
95
|
// agent is a test that lies. They are said rather than assumed.
|
|
93
96
|
const notes = [];
|
|
94
|
-
if (!prepared.claudeBin) notes.push('Claude Code is not at ~/.local/bin/claude, so agents will not run in it.');
|
|
97
|
+
if (withTools && !prepared.claudeBin) notes.push('Claude Code is not at ~/.local/bin/claude, so agents will not run in it.');
|
|
95
98
|
if (!prepared.keychain) notes.push('There is no ~/Library/Keychains here, so Claude Code will say it is not logged in.');
|
|
96
|
-
if (withAgents && !prepared.agents) notes.push('There is no ~/.claude on this
|
|
99
|
+
if (withAgents && !prepared.agents) notes.push('There is no ~/.claude on this computer, so it will have no agents to offer.');
|
|
97
100
|
|
|
98
101
|
return { ok: true, home: prepared.home, agents: prepared.agents, notes, pid: child?.pid ?? null };
|
|
99
102
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// THE CHECKOUT IS PHOTOGRAPHED OFF THE THREAD THAT DRAWS HER WINDOW.
|
|
2
|
+
//
|
|
3
|
+
// Measured 2026-10-01 on a test copy: one snapshot of the app took 2.56 seconds
|
|
4
|
+
// while an agent was starting, against a few milliseconds either side. The
|
|
5
|
+
// spawn ran `snapshotRepo` inline, which is four git commands and a read of
|
|
6
|
+
// every untracked file, and on a busy Mac each git command costs half a second.
|
|
7
|
+
// That is the "long delay" after pressing Send. The same shape as
|
|
8
|
+
// main/task-folders-offthread.mjs: main/git-change.mjs stays synchronous and
|
|
9
|
+
// runs whole inside a worker thread, and if the thread cannot start the job
|
|
10
|
+
// runs here, the old way.
|
|
11
|
+
import { Worker } from 'node:worker_threads';
|
|
12
|
+
import * as change from './git-change.mjs';
|
|
13
|
+
|
|
14
|
+
const JOBS = new Set(['snapshotRepo']);
|
|
15
|
+
|
|
16
|
+
let worker = null;
|
|
17
|
+
let broken = false;
|
|
18
|
+
let seq = 0;
|
|
19
|
+
const waiting = new Map();
|
|
20
|
+
|
|
21
|
+
function runHere({ name, args, resolve, reject }) {
|
|
22
|
+
try { resolve(change[name](...args)); } catch (error) { reject(error); }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function giveUp() {
|
|
26
|
+
broken = true;
|
|
27
|
+
worker = null;
|
|
28
|
+
const left = [...waiting.values()];
|
|
29
|
+
waiting.clear();
|
|
30
|
+
for (const job of left) runHere(job);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function thread() {
|
|
34
|
+
if (worker || broken) return worker;
|
|
35
|
+
try {
|
|
36
|
+
worker = new Worker(new URL('./git-change-thread.mjs', import.meta.url));
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.warn('zero: the checkout will be read on the main thread:', error.message);
|
|
39
|
+
broken = true;
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
worker.on('message', ({ id, ok, value, error }) => {
|
|
43
|
+
const job = waiting.get(id);
|
|
44
|
+
if (!job) return;
|
|
45
|
+
waiting.delete(id);
|
|
46
|
+
if (!waiting.size) worker?.unref();
|
|
47
|
+
if (ok) job.resolve(value); else job.reject(new Error(error));
|
|
48
|
+
});
|
|
49
|
+
worker.on('error', (error) => {
|
|
50
|
+
console.warn('zero: the checkout thread failed:', error.message);
|
|
51
|
+
giveUp();
|
|
52
|
+
});
|
|
53
|
+
worker.on('exit', () => { if (worker) giveUp(); });
|
|
54
|
+
worker.unref();
|
|
55
|
+
return worker;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Run one of git-change.mjs's exports without blocking this thread. */
|
|
59
|
+
export function gitJob(name, ...args) {
|
|
60
|
+
if (!JOBS.has(name)) return Promise.reject(new Error(`not a git job: ${name}`));
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const job = { name, args, resolve, reject };
|
|
63
|
+
const w = thread();
|
|
64
|
+
if (!w) { runHere(job); return; }
|
|
65
|
+
const id = ++seq;
|
|
66
|
+
waiting.set(id, job);
|
|
67
|
+
w.ref();
|
|
68
|
+
w.postMessage({ id, name, args });
|
|
69
|
+
});
|
|
70
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// THE OTHER END OF main/git-change-offthread.mjs. One job at a time, in order.
|
|
2
|
+
import { parentPort } from 'node:worker_threads';
|
|
3
|
+
import * as change from './git-change.mjs';
|
|
4
|
+
|
|
5
|
+
parentPort.on('message', ({ id, name, args }) => {
|
|
6
|
+
try {
|
|
7
|
+
parentPort.postMessage({ id, ok: true, value: change[name](...args) });
|
|
8
|
+
} catch (error) {
|
|
9
|
+
parentPort.postMessage({ id, ok: false, error: String(error?.message ?? error) });
|
|
10
|
+
}
|
|
11
|
+
});
|
package/main/git-change.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// the Edit or Write tool. Measured on this Mac 2026-08-24: of 105 runs that
|
|
6
6
|
// changed a file over two days, 4 would have shown the whole change. The other
|
|
7
7
|
// 101 rewrote their files with sed, a heredoc or a script, and a card drawn
|
|
8
|
-
// from the conversation has nothing in it. That is what
|
|
8
|
+
// from the conversation has nothing in it. That is what a tester was clicking.
|
|
9
9
|
//
|
|
10
10
|
// SO THE CARD STOPS ASKING HOW THE AGENT TYPED. A run's checkout is
|
|
11
11
|
// photographed when the session spawns and read again when it exits, and the
|
|
@@ -42,7 +42,10 @@ const MAX_ROWS_PER_FILE = 20_000;
|
|
|
42
42
|
const NOT_CODE = /(^|\/)(node_modules|\.git)\//;
|
|
43
43
|
|
|
44
44
|
function git(cwd, args, { maxBuffer = 64 * 1024 * 1024 } = {}) {
|
|
45
|
-
|
|
45
|
+
// A user's `diff.mnemonicPrefix` rewrites a/ and b/ into c/ i/ o/ w/.
|
|
46
|
+
// The reader below only knows one prefix. Turn the setting off for this
|
|
47
|
+
// process so a checkout reads the same on every machine.
|
|
48
|
+
return execFileSync('git', ['--no-pager', '-c', 'diff.mnemonicPrefix=false', ...args], {
|
|
46
49
|
cwd,
|
|
47
50
|
encoding: 'utf8',
|
|
48
51
|
maxBuffer,
|
|
@@ -189,7 +192,10 @@ export function filesFromPatch(patch) {
|
|
|
189
192
|
const nameOn = (line, side) => {
|
|
190
193
|
const name = line.slice(4).trim();
|
|
191
194
|
if (name === '/dev/null') return null;
|
|
192
|
-
|
|
195
|
+
// a/ and b/ are the ordinary prefixes. c/ i/ o/ w/ are mnemonicPrefix,
|
|
196
|
+
// and 1/ 2/ are `git diff --no-index` under that same setting. One
|
|
197
|
+
// prefix comes off; a file that is itself named `b/foo` stays `b/foo`.
|
|
198
|
+
return name.replace(/^[abiowc12]\//, '');
|
|
193
199
|
};
|
|
194
200
|
for (const line of String(patch ?? '').split('\n')) {
|
|
195
201
|
if (line.startsWith('diff --git ')) { closeHunk(); file = null; oldName = null; continue; }
|
package/main/img-scheme.mjs
CHANGED
|
@@ -36,7 +36,9 @@
|
|
|
36
36
|
// product's docs, its attachments, its designs and the `.staging` folder a
|
|
37
37
|
// paste lands in) or a product's own code repo, which is where a worker
|
|
38
38
|
// writes `shots/…` and then names it on a card.
|
|
39
|
+
import fs from 'node:fs';
|
|
39
40
|
import path from 'node:path';
|
|
41
|
+
import { Readable } from 'node:stream';
|
|
40
42
|
import { IMG_SCHEME, IMG_SCHEMES, isScheme } from '../shared/schemes.mjs';
|
|
41
43
|
|
|
42
44
|
// The name is read rather than typed, and every spelling this app has used is
|
|
@@ -51,6 +53,12 @@ export const IMG_HOST = 'file';
|
|
|
51
53
|
|
|
52
54
|
const PICTURES = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.avif', '.bmp', '.ico']);
|
|
53
55
|
|
|
56
|
+
// AND A SOUND OR A FILM, SINCE 2026-09-29, so a voice sample or a render a
|
|
57
|
+
// worker names plays in the message (ArtifactMedia in Focus.tsx) instead of
|
|
58
|
+
// sitting there as a grey path. Same two rules, same roots: this only widens
|
|
59
|
+
// what counts as something to look at, to include something to listen to.
|
|
60
|
+
const MEDIA = new Set(['.mp3', '.wav', '.m4a', '.aac', '.ogg', '.mp4', '.mov', '.m4v', '.webm']);
|
|
61
|
+
|
|
54
62
|
/** The url the renderer draws, for a picture already resolved on disk. */
|
|
55
63
|
export function imgUrl(file) {
|
|
56
64
|
if (typeof file !== 'string' || !path.isAbsolute(file)) return null;
|
|
@@ -82,9 +90,70 @@ function inside(dir, file) {
|
|
|
82
90
|
return f === d || f.startsWith(d + path.sep);
|
|
83
91
|
}
|
|
84
92
|
|
|
93
|
+
const MEDIA_TYPES = {
|
|
94
|
+
'.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.m4a': 'audio/mp4', '.aac': 'audio/aac', '.ogg': 'audio/ogg',
|
|
95
|
+
'.mp4': 'video/mp4', '.mov': 'video/quicktime', '.m4v': 'video/mp4', '.webm': 'video/webm',
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** The content type of a sound or a film, or null when `file` is neither. */
|
|
99
|
+
export function mediaType(file) {
|
|
100
|
+
return MEDIA_TYPES[path.extname(String(file ?? '')).toLowerCase()] ?? null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// A PLAYER ASKS FOR A SOUND IN PIECES, AND net.fetch IGNORES THE ASK.
|
|
104
|
+
// Measured in a hidden Electron window on 2026-09-29 against one of her voice
|
|
105
|
+
// samples, served on this scheme through net.fetch: it loaded and reported its
|
|
106
|
+
// 39.36 seconds, and could not be moved. `seekable` was [0, 0] and a jump to
|
|
107
|
+
// the middle stayed at 0, with or without the Range header passed through,
|
|
108
|
+
// because a file:// fetch answers every range with the whole file and a 200.
|
|
109
|
+
// So a sound or a film is answered here, in the byte range the player asked
|
|
110
|
+
// for, with a 206.
|
|
111
|
+
/**
|
|
112
|
+
* The bytes a Range header asks for, as { start, end } inclusive, or null for
|
|
113
|
+
* the whole file, or false when the range cannot be served. Only one range.
|
|
114
|
+
*/
|
|
115
|
+
export function byteRange(header, size) {
|
|
116
|
+
if (!header) return null;
|
|
117
|
+
const m = /^bytes=(\d*)-(\d*)$/.exec(String(header).trim());
|
|
118
|
+
if (!m || (m[1] === '' && m[2] === '')) return null;
|
|
119
|
+
let start;
|
|
120
|
+
let end;
|
|
121
|
+
if (m[1] === '') {
|
|
122
|
+
// The last N bytes.
|
|
123
|
+
start = Math.max(0, size - Number(m[2]));
|
|
124
|
+
end = size - 1;
|
|
125
|
+
} else {
|
|
126
|
+
start = Number(m[1]);
|
|
127
|
+
end = m[2] === '' ? size - 1 : Math.min(Number(m[2]), size - 1);
|
|
128
|
+
}
|
|
129
|
+
if (start >= size || start > end) return false;
|
|
130
|
+
return { start, end };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* A sound or a film off disk, answered in the range the player asked for.
|
|
135
|
+
* The caller has already decided the file may be served (`servable`).
|
|
136
|
+
*/
|
|
137
|
+
export async function mediaResponse(file, rangeHeader) {
|
|
138
|
+
const { size } = await fs.promises.stat(file);
|
|
139
|
+
const range = byteRange(rangeHeader, size);
|
|
140
|
+
const headers = new Headers({ 'content-type': mediaType(file) ?? 'application/octet-stream', 'accept-ranges': 'bytes' });
|
|
141
|
+
if (range === false) {
|
|
142
|
+
headers.set('content-range', `bytes */${size}`);
|
|
143
|
+
return new Response(null, { status: 416, headers });
|
|
144
|
+
}
|
|
145
|
+
if (size === 0) return new Response(null, { status: 200, headers });
|
|
146
|
+
const { start, end } = range ?? { start: 0, end: size - 1 };
|
|
147
|
+
headers.set('content-length', String(end - start + 1));
|
|
148
|
+
if (range) headers.set('content-range', `bytes ${start}-${end}/${size}`);
|
|
149
|
+
const body = Readable.toWeb(fs.createReadStream(file, { start, end }));
|
|
150
|
+
return new Response(body, { status: range ? 206 : 200, headers });
|
|
151
|
+
}
|
|
152
|
+
|
|
85
153
|
/** Is this a picture the window is allowed to draw? Pure, so it is testable. */
|
|
86
154
|
export function servable(file, roots) {
|
|
87
155
|
if (typeof file !== 'string' || !path.isAbsolute(file)) return false;
|
|
88
|
-
|
|
156
|
+
const ext = path.extname(file).toLowerCase();
|
|
157
|
+
if (!PICTURES.has(ext) && !MEDIA.has(ext)) return false;
|
|
89
158
|
return (roots ?? []).some((root) => inside(root, file));
|
|
90
159
|
}
|
|
@@ -1,17 +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
|
-
// expose some of them, or maybe we would best aggregate others."
|
|
8
|
-
//
|
|
9
|
-
// A person opening that page is answering two questions, what should agents do
|
|
10
|
-
// and how should they talk to me, so the writing rules and the finishing rules
|
|
11
|
-
// became ONE document: `message-rules.md`, "How agents write to you". The
|
|
12
|
-
// evidence they were one thing all along is her own rules file, where three of
|
|
13
|
-
// four paragraphs are about how agents write to her rather than what they do.
|
|
14
|
-
// `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.
|
|
15
7
|
import fs from 'node:fs';
|
|
16
8
|
import path from 'node:path';
|
|
17
9
|
import {fileURLToPath} from 'node:url';
|
|
@@ -19,56 +11,67 @@ import {fileURLToPath} from 'node:url';
|
|
|
19
11
|
// one place that holds it. Typing it here is what left three dead names in the
|
|
20
12
|
// code through two renames (tests/the-app-is-named-in-one-place).
|
|
21
13
|
import {nameSlug} from '../shared/product-name.mjs';
|
|
22
|
-
// ADHD MODE IS A FOURTH FILE, NOT A PARAGRAPH INSIDE THE THIRD (w-5737fe67cf
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// while the mode is off. Whether it is on lives in her config (`adhdMode`).
|
|
14
|
+
// ADHD MODE IS A FOURTH FILE, NOT A PARAGRAPH INSIDE THE THIRD (w-5737fe67cf).
|
|
15
|
+
// Its text is appended to the instructions while the mode is on and removed when
|
|
16
|
+
// it is off. Kept in its own file so edits to it never mix with the user's own
|
|
17
|
+
// rules, and survive while the mode is off. Whether it is on lives in the
|
|
18
|
+
// config (`adhdMode`).
|
|
28
19
|
const files = {rules:'founder.md',messages:'message-rules.md',adhd:'adhd-mode.md',system:'worker.md'};
|
|
29
20
|
// The two names the joined document replaced, read only by the migration.
|
|
30
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
|
+
}
|
|
31
30
|
const defaults = JSON.parse(fs.readFileSync(fileURLToPath(new URL('../shared/instruction-defaults.json',import.meta.url)),'utf8'));
|
|
32
31
|
function fileFor(id) {
|
|
33
32
|
if (!Object.hasOwn(files,id)) throw new Error('Unknown instruction section');
|
|
34
33
|
return files[id];
|
|
35
34
|
}
|
|
35
|
+
// WHAT THE BOX OPENS ON when the user has written nothing. The message rules
|
|
36
|
+
// box starts empty like the general one (w-3ec9f07978): the app's own message
|
|
37
|
+
// rules are not the user's to edit, because the inbox reads every message by
|
|
38
|
+
// them, so they ride on every run from the checkout and never sit in the box.
|
|
39
|
+
const boxDefault=(id)=>(id==='rules'||id==='messages') ? '' : defaults[id];
|
|
36
40
|
export function readInstruction(appDir,id,shippedDir=appDir) {
|
|
41
|
+
id=sectionFor(appDir,id);
|
|
37
42
|
const file=fileFor(id);
|
|
38
|
-
const defaultText=id
|
|
43
|
+
const defaultText=boxDefault(id);
|
|
39
44
|
let text=defaultText;
|
|
40
45
|
try {text=fs.readFileSync(path.join(appDir,'briefs',file),'utf8');} catch(e) {if(e.code!=='ENOENT') throw e; if(id==='system') text=readSystemTemplate(shippedDir,appDir);}
|
|
41
46
|
return {text,defaultText};
|
|
42
47
|
}
|
|
43
48
|
/**
|
|
44
49
|
* RESTORE POINTS, BECAUSE A SAVE USED TO BE THE END OF THE OLD TEXT
|
|
45
|
-
* (w-94b3af4e70
|
|
50
|
+
* (w-94b3af4e70).
|
|
46
51
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* app doesn't work the way they want it or they think, 'It worked better
|
|
50
|
-
* before. I want to restore it.'"
|
|
52
|
+
* Why the page needs this at all: a user can edit the agent instructions, lose
|
|
53
|
+
* something that mattered, and want an earlier version back.
|
|
51
54
|
*
|
|
52
|
-
* Until this, the only thing the page could hand back was our shipped text,
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
+
* Until this, the only thing the page could hand back was our shipped text, so
|
|
56
|
+
* recovering meant throwing away everything the user had written to get the
|
|
57
|
+
* default. The box saves 400ms after a keystroke, straight over
|
|
55
58
|
* the file, so a paragraph deleted at noon was gone at noon.
|
|
56
59
|
*
|
|
57
60
|
* WHAT COUNTS AS A VERSION: the text that was in the file before a save, kept
|
|
58
61
|
* under the time that text was itself saved, so the list reads as the versions
|
|
59
|
-
*
|
|
62
|
+
* the user wrote rather than as a log of keystrokes. A burst of typing is one
|
|
60
63
|
* version and not two hundred, because text that has been sitting untouched for
|
|
61
64
|
* less than QUIET_MS is still being written and is not a place to come back to.
|
|
62
65
|
*
|
|
63
66
|
* It never throws into a save. Losing a restore point is a small thing and
|
|
64
|
-
* failing to write what
|
|
67
|
+
* failing to write what the user just typed is not.
|
|
65
68
|
*/
|
|
66
69
|
const HISTORY='instruction-history';
|
|
67
70
|
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,18 +82,18 @@ 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')};
|
|
86
89
|
}
|
|
87
|
-
function keepVersion(appDir,id,now=Date.now()) {
|
|
90
|
+
function keepVersion(appDir,id,now=Date.now(),always=false) {
|
|
88
91
|
try {
|
|
89
92
|
const live=path.join(appDir,'briefs',fileFor(id));
|
|
90
93
|
let stat,text;
|
|
91
94
|
try {stat=fs.statSync(live); text=fs.readFileSync(live,'utf8');}
|
|
92
95
|
catch(e) {if(e.code!=='ENOENT') throw e; return;}
|
|
93
|
-
if(now-stat.mtimeMs<QUIET_MS) return;
|
|
96
|
+
if(!always&&now-stat.mtimeMs<QUIET_MS) return;
|
|
94
97
|
const ts=Math.floor(stat.mtimeMs);
|
|
95
98
|
const have=listVersions(appDir,id);
|
|
96
99
|
if(have.some(v=>v.ts===ts)) return;
|
|
@@ -102,6 +105,7 @@ function keepVersion(appDir,id,now=Date.now()) {
|
|
|
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);
|
|
@@ -123,21 +127,20 @@ export function readSystemTemplate(appDir,dataDir=appDir) {
|
|
|
123
127
|
}
|
|
124
128
|
|
|
125
129
|
/**
|
|
126
|
-
*
|
|
130
|
+
* THE USER'S OWN WRITING, CARRIED OUT OF THE CHECKOUT ONCE (w-3dc46f3a67).
|
|
127
131
|
*
|
|
128
132
|
* Until this ran, the four boxes in Settings read and wrote `dataDir/briefs`,
|
|
129
133
|
* and running from source `dataDir` is the git checkout the whole fleet works
|
|
130
|
-
* in.
|
|
131
|
-
*
|
|
132
|
-
* were being kept in the one folder every worker is allowed to `git checkout`.
|
|
134
|
+
* in. So the user's rules were being kept in the one folder every worker is
|
|
135
|
+
* allowed to `git checkout`, and agents had been committing to them.
|
|
133
136
|
*
|
|
134
|
-
* WHAT MOVES IS ONLY WHAT
|
|
137
|
+
* WHAT MOVES IS ONLY WHAT THE USER WROTE, and the test for that is the shipped
|
|
135
138
|
* default. A file whose text IS the default is our file sitting where we put
|
|
136
|
-
* it, and copying it into
|
|
137
|
-
* worker brief would never reach
|
|
138
|
-
* forever. So a file is carried across only when it differs from the
|
|
139
|
-
* which
|
|
140
|
-
* ship. Those two are the same pair package.json keeps out of the download.
|
|
139
|
+
* it, and copying it into the user's folder would freeze it there: the next
|
|
140
|
+
* release's worker brief would never reach them, because the "edit" would
|
|
141
|
+
* shadow it forever. So a file is carried across only when it differs from the
|
|
142
|
+
* default, which is typically founder.md and writing-rules.md and neither of the
|
|
143
|
+
* two we ship. Those two are the same pair package.json keeps out of the download.
|
|
141
144
|
*
|
|
142
145
|
* It is a COPY, not a move, and it never overwrites. The checkout keeps its
|
|
143
146
|
* copy so that merging this cannot lose a word, and a second run finds the
|
|
@@ -151,6 +154,7 @@ export function carryHerBriefsAcross(fromDir,toDir) {
|
|
|
151
154
|
if(!fromDir||!toDir||path.resolve(fromDir)===path.resolve(toDir)) return carried;
|
|
152
155
|
for(const [id,file] of Object.entries(files)) {
|
|
153
156
|
try {
|
|
157
|
+
if(id==='messages' && agentInstructionsUnified(toDir)) continue;
|
|
154
158
|
const target=path.join(toDir,'briefs',file);
|
|
155
159
|
if(fs.existsSync(target)) continue;
|
|
156
160
|
let text;
|
|
@@ -165,41 +169,111 @@ export function carryHerBriefsAcross(fromDir,toDir) {
|
|
|
165
169
|
}
|
|
166
170
|
|
|
167
171
|
/**
|
|
168
|
-
* THE TWO OLD MESSAGE FILES, JOINED INTO ONE, ONCE (w-3dc46f3a67
|
|
172
|
+
* THE TWO OLD MESSAGE FILES, JOINED INTO ONE, ONCE (w-3dc46f3a67).
|
|
169
173
|
*
|
|
170
|
-
* `writing-rules.md` said how an agent writes to
|
|
174
|
+
* `writing-rules.md` said how an agent writes to the user during a task and
|
|
171
175
|
* `finishing.md` said how it writes the last message. They are one box now, so
|
|
172
176
|
* they have to become one document, and this is where that happens: on the
|
|
173
177
|
* first boot after the change, before anything reads the new name.
|
|
174
178
|
*
|
|
175
|
-
* WHAT IT DOES WITH THE HALF
|
|
176
|
-
* anything in that file is
|
|
177
|
-
* of text, so most people have never touched them. When
|
|
178
|
-
* the other, the joined document takes the shipped
|
|
179
|
-
* half rather than leaving it out:
|
|
180
|
-
* and dropping it would quietly stop every
|
|
179
|
+
* WHAT IT DOES WITH THE HALF THE USER NEVER WROTE. The writing rules ship
|
|
180
|
+
* empty, so anything in that file is the user's. The finishing rules ship with
|
|
181
|
+
* 5,794 characters of text, so most people have never touched them. When the
|
|
182
|
+
* user has the one and not the other, the joined document takes the shipped
|
|
183
|
+
* finishing text as its second half rather than leaving it out: the user's copy
|
|
184
|
+
* shadows the default from here on, and dropping it would quietly stop every
|
|
185
|
+
* task ending the way it does today.
|
|
181
186
|
*
|
|
182
|
-
* AND WHEN
|
|
183
|
-
* install, and leaving
|
|
184
|
-
* release's default reaching
|
|
185
|
-
* never freeze our own text into
|
|
187
|
+
* AND WHEN THE USER WROTE NEITHER, IT WRITES NOTHING AT ALL. That is the fresh
|
|
188
|
+
* install, and leaving them with no file of their own is what keeps a later
|
|
189
|
+
* release's default reaching them. Same rule as `carryHerBriefsAcross` above:
|
|
190
|
+
* never freeze our own text into the user's folder.
|
|
191
|
+
*
|
|
192
|
+
* IT READS ONLY THE USER'S FOLDER, AND ADDS NOTHING OF OURS (w-3ec9f07978).
|
|
193
|
+
* It used to fall back to the checkout's copies and to put the shipped ending
|
|
194
|
+
* under whatever it found. In this repository `writing-rules.md` ships 16,000
|
|
195
|
+
* characters long, so every fresh copy run from source joined our two files
|
|
196
|
+
* and saved them as the user's, and the box showed nothing but our machinery.
|
|
197
|
+
* The shipped ending rides on every run anyway now (`messageRules` in the
|
|
198
|
+
* supervisor), so there is nothing of ours left to carry.
|
|
186
199
|
*
|
|
187
200
|
* Never throws, never overwrites. Returns what it joined, or null.
|
|
188
201
|
*/
|
|
189
202
|
export function joinMessageRules(shippedDir,toDir) {
|
|
190
203
|
try {
|
|
204
|
+
if(agentInstructionsUnified(toDir)) return null;
|
|
191
205
|
const target=path.join(toDir,'briefs',files.messages);
|
|
192
206
|
if(fs.existsSync(target)) return null;
|
|
193
|
-
const read=(
|
|
194
|
-
const
|
|
195
|
-
const [writing,finishing]=WAS_MESSAGES.map((name)=>(either(name) ?? '').trim());
|
|
207
|
+
const read=(name)=>{try {return fs.readFileSync(path.join(toDir,'briefs',name),'utf8');} catch {return null;}};
|
|
208
|
+
const [writing,finishing]=WAS_MESSAGES.map((name)=>(read(name) ?? '').trim());
|
|
196
209
|
if(!writing && !finishing) return null;
|
|
197
|
-
const
|
|
198
|
-
const joined=[writing,ending].filter(Boolean).join('\n\n');
|
|
210
|
+
const joined=[writing,finishing].filter(Boolean).join('\n\n');
|
|
199
211
|
writeInstruction(toDir,'messages',`${joined}\n`);
|
|
200
|
-
return {writing:writing.length,ending:
|
|
212
|
+
return {writing:writing.length,ending:finishing.length,total:joined.length+1};
|
|
201
213
|
} catch(e) {
|
|
202
214
|
console.warn(`${nameSlug}: could not join the message rules: ${e.message}`);
|
|
203
215
|
return null;
|
|
204
216
|
}
|
|
205
217
|
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* A MESSAGE BOX THAT HOLDS ONLY OUR TEXT IS NOT THE USER'S, ONCE (w-3ec9f07978).
|
|
221
|
+
*
|
|
222
|
+
* The old join above wrote the checkout's writing rules and ending rules into
|
|
223
|
+
* the user's folder as if they had typed them, so their box opened on 21,743
|
|
224
|
+
* characters of the app's own rules. A file whose text is EXACTLY one of the
|
|
225
|
+
* things we ship, alone or joined the way the old join joined them, is set
|
|
226
|
+
* aside: kept as a restore point and removed, so the box opens empty and our
|
|
227
|
+
* rules reach the run from the checkout as they should.
|
|
228
|
+
*
|
|
229
|
+
* Exact only. One line of the user's own makes the whole file theirs, and
|
|
230
|
+
* nothing here tries to cut it apart. Never throws. Returns whether it acted.
|
|
231
|
+
*/
|
|
232
|
+
export function setAsideShippedMessageRules(shippedDir,toDir) {
|
|
233
|
+
try {
|
|
234
|
+
if(agentInstructionsUnified(toDir)) return false;
|
|
235
|
+
const target=path.join(toDir,'briefs',files.messages);
|
|
236
|
+
if(path.resolve(shippedDir)===path.resolve(toDir)||!fs.existsSync(target)) return false;
|
|
237
|
+
const read=(name)=>{try {return fs.readFileSync(path.join(shippedDir,'briefs',name),'utf8').trim();} catch {return '';}};
|
|
238
|
+
const writing=read(WAS_MESSAGES[0]);
|
|
239
|
+
const endings=[...new Set([read(files.messages),defaults.messages.trim()])].filter(Boolean);
|
|
240
|
+
const ours=new Set([...endings,...endings.map((e)=>[writing,e].filter(Boolean).join('\n\n'))]);
|
|
241
|
+
const text=fs.readFileSync(target,'utf8').trim();
|
|
242
|
+
if(!text||!ours.has(text)) return false;
|
|
243
|
+
keepVersion(toDir,'messages',Date.now(),true);
|
|
244
|
+
fs.rmSync(target,{force:true});
|
|
245
|
+
return true;
|
|
246
|
+
} catch(e) {
|
|
247
|
+
console.warn(`${nameSlug}: could not set aside the shipped message rules: ${e.message}`);
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
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
|
+
}
|