agentbox-app 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.md +176 -0
- package/bin/agentbox.mjs +103 -0
- package/briefs/adhd-mode.md +5 -0
- package/briefs/message-rules.md +95 -0
- package/briefs/worker.md +230 -0
- package/briefs/writing-rules.md +336 -0
- package/main/account-discovery.mjs +145 -0
- package/main/account-tooling.mjs +261 -0
- package/main/agent-activity.mjs +56 -0
- package/main/agent-files.mjs +422 -0
- package/main/agent-schedule.mjs +254 -0
- package/main/agent-sessions.mjs +435 -0
- package/main/agents.mjs +1095 -0
- package/main/analytics.mjs +206 -0
- package/main/answer-modes.mjs +206 -0
- package/main/approval-prompt-server.mjs +160 -0
- package/main/approvals.mjs +487 -0
- package/main/artifact-path.mjs +144 -0
- package/main/claude-bin.mjs +311 -0
- package/main/claude-input.mjs +117 -0
- package/main/claude-models.mjs +244 -0
- package/main/claude-plan.mjs +148 -0
- package/main/claude-usage.mjs +149 -0
- package/main/code-change.mjs +383 -0
- package/main/code-file.mjs +116 -0
- package/main/codex-account.mjs +146 -0
- package/main/codex-app-server.mjs +796 -0
- package/main/codex-approvals.mjs +712 -0
- package/main/codex-bin.mjs +261 -0
- package/main/codex-compaction.mjs +49 -0
- package/main/codex-models.mjs +194 -0
- package/main/codex-session.mjs +797 -0
- package/main/codex-threads.mjs +268 -0
- package/main/codex-usage-file.mjs +192 -0
- package/main/codex-usage.mjs +149 -0
- package/main/codex-watch.mjs +35 -0
- package/main/codex.mjs +757 -0
- package/main/config.mjs +345 -0
- package/main/copy-selection.mjs +55 -0
- package/main/crash-report.mjs +158 -0
- package/main/demo.mjs +254 -0
- package/main/dev-window.mjs +103 -0
- package/main/doc-file.mjs +99 -0
- package/main/doc-scheme.mjs +126 -0
- package/main/first-run.mjs +190 -0
- package/main/folders.mjs +91 -0
- package/main/fresh-user.mjs +120 -0
- package/main/git-change.mjs +323 -0
- package/main/img-scheme.mjs +90 -0
- package/main/instruction-settings.mjs +205 -0
- package/main/ipc.mjs +1558 -0
- package/main/live-replies.mjs +77 -0
- package/main/machine.mjs +113 -0
- package/main/main.mjs +972 -0
- package/main/menu.mjs +177 -0
- package/main/notify.mjs +155 -0
- package/main/open-source-file.mjs +8 -0
- package/main/project-identity.mjs +149 -0
- package/main/provider-commands.mjs +32 -0
- package/main/rail-note.mjs +74 -0
- package/main/repeats.mjs +261 -0
- package/main/row-label.mjs +185 -0
- package/main/screen-detail.mjs +44 -0
- package/main/serve.mjs +262 -0
- package/main/settings.mjs +912 -0
- package/main/staleness.mjs +76 -0
- package/main/store/chats.mjs +188 -0
- package/main/store/home.mjs +383 -0
- package/main/store/migrate.mjs +40 -0
- package/main/store/paths.mjs +82 -0
- package/main/store/pending-writes.mjs +122 -0
- package/main/store/project-lock.mjs +200 -0
- package/main/store/project.mjs +523 -0
- package/main/store/queues.mjs +31 -0
- package/main/store/work-items.mjs +299 -0
- package/main/store/write-queue.mjs +14 -0
- package/main/store-modules.mjs +23 -0
- package/main/store.mjs +1269 -0
- package/main/supervisor.mjs +6875 -0
- package/main/task-commands.mjs +161 -0
- package/main/task-folders-offthread.mjs +84 -0
- package/main/task-folders-thread.mjs +13 -0
- package/main/task-folders.mjs +409 -0
- package/main/task-remote-control.mjs +65 -0
- package/main/task-terminals.mjs +169 -0
- package/main/terminal-access.mjs +6 -0
- package/main/updater.mjs +240 -0
- package/main/zoom-keys.mjs +54 -0
- package/main/zoom-readout.mjs +20 -0
- package/mcp/agentbox-mcp.sh +34 -0
- package/mcp/core/account.mjs +77 -0
- package/mcp/core/chat.mjs +61 -0
- package/mcp/core/documents.mjs +238 -0
- package/mcp/core/page.mjs +152 -0
- package/mcp/core/products.mjs +110 -0
- package/mcp/core/skills.mjs +126 -0
- package/mcp/core/work.mjs +377 -0
- package/mcp/stdio.mjs +70 -0
- package/mcp/tools.mjs +257 -0
- package/package.json +186 -0
- package/renderer/dist/assets/Geist-Variable-Bj2R_7yk.woff2 +0 -0
- package/renderer/dist/assets/GeistMono-Variable-Dispecij.woff2 +0 -0
- package/renderer/dist/assets/addon-fit-YJmn1quW.js +12 -0
- package/renderer/dist/assets/addon-serialize-CsFQTvP_.js +13 -0
- 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/gouache-valley-ChSu14-t.webp +0 -0
- package/renderer/dist/assets/gouache-valley-d9qasI6k.webp +0 -0
- package/renderer/dist/assets/gouache-valley-sm-CdLjT5Ec.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-CwiDv5bL.css +32 -0
- package/renderer/dist/assets/index-D9hCAVPl.js +684 -0
- 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/source-sans-3-cyrillic-ext-wght-normal-DzyfIafT.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-cyrillic-wght-normal-BMDVbyM7.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-greek-ext-wght-normal-BWSLJLk6.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-greek-wght-normal-C9H9m1vD.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-latin-ext-wght-normal-C8iNium2.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-latin-wght-normal-BqRLTx4X.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-vietnamese-wght-normal-C1uRvKPU.woff2 +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/assets/xterm-BT7tBf8A.js +27 -0
- package/renderer/dist/emulator/audio-rules.mjs +74 -0
- package/renderer/dist/emulator/data/compression/README.md +11 -0
- package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
- package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
- package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
- package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
- package/renderer/dist/emulator/data/cores/README.md +22 -0
- package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/package.json +65 -0
- package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
- package/renderer/dist/emulator/data/emulator.css +1614 -0
- package/renderer/dist/emulator/data/loader.js +170 -0
- package/renderer/dist/emulator/data/localization/README.md +63 -0
- package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
- package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
- package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
- package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
- package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
- package/renderer/dist/emulator/data/localization/en-US.json +339 -0
- package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
- package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
- package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
- package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
- package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
- package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
- package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
- package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
- package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
- package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
- package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
- package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
- package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
- package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
- package/renderer/dist/emulator/data/src/GameManager.js +464 -0
- package/renderer/dist/emulator/data/src/compression.js +142 -0
- package/renderer/dist/emulator/data/src/emulator.js +6158 -0
- package/renderer/dist/emulator/data/src/gamepad.js +167 -0
- package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
- package/renderer/dist/emulator/data/src/shaders.js +236 -0
- package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
- package/renderer/dist/emulator/data/src/storage.js +125 -0
- package/renderer/dist/emulator/data/version.json +5 -0
- package/renderer/dist/emulator/host.js +299 -0
- package/renderer/dist/emulator/index.html +28 -0
- package/renderer/dist/emulator/provenance.json +6 -0
- package/renderer/dist/fixtures/onboard/designs/w-n13/the-first-row.html +1 -0
- package/renderer/dist/index.html +13 -0
- package/scripts/after-install.mjs +59 -0
- package/scripts/approval-server.sh +43 -0
- package/scripts/before-publish.mjs +133 -0
- package/shared/agent-import.d.mts +13 -0
- package/shared/agent-import.mjs +252 -0
- package/shared/agents.d.mts +190 -0
- package/shared/agents.mjs +937 -0
- package/shared/analytics-events.mjs +63 -0
- package/shared/answers.d.mts +12 -0
- package/shared/answers.mjs +80 -0
- package/shared/artifact-keys.d.mts +28 -0
- package/shared/artifact-keys.mjs +140 -0
- package/shared/bridge-map.mjs +128 -0
- package/shared/claude-commands.d.mts +20 -0
- package/shared/claude-commands.generated.mjs +149 -0
- package/shared/claude-commands.mjs +131 -0
- package/shared/claude-models.generated.d.mts +15 -0
- package/shared/claude-models.generated.mjs +26 -0
- package/shared/claude-usage.d.mts +16 -0
- package/shared/claude-usage.mjs +172 -0
- package/shared/codex-commands.d.mts +4 -0
- package/shared/codex-commands.mjs +17 -0
- package/shared/codex-import.d.mts +38 -0
- package/shared/codex-import.mjs +232 -0
- package/shared/codex-modes.mjs +159 -0
- package/shared/codex-usage.mjs +180 -0
- package/shared/contracts.mjs +412 -0
- package/shared/crash-scrub.mjs +182 -0
- package/shared/dashboard.mjs +481 -0
- package/shared/dead-run-trace.mjs +127 -0
- package/shared/demo-world.mjs +415 -0
- package/shared/effort-levels.d.mts +15 -0
- package/shared/effort-levels.mjs +76 -0
- package/shared/engines.d.mts +62 -0
- package/shared/engines.mjs +389 -0
- package/shared/first-run-examples.mjs +60 -0
- package/shared/first-run-line.mjs +149 -0
- package/shared/first-run-practice.d.mts +32 -0
- package/shared/first-run-practice.mjs +254 -0
- package/shared/first-run-shape.mjs +100 -0
- package/shared/fresh-user-home.mjs +189 -0
- package/shared/hook-failure.mjs +22 -0
- package/shared/install-id.mjs +39 -0
- package/shared/instruction-defaults.json +5 -0
- package/shared/library-folder.mjs +28 -0
- package/shared/notify-rules.mjs +120 -0
- package/shared/product-name.d.mts +6 -0
- package/shared/product-name.mjs +144 -0
- package/shared/project-folder-check.d.mts +22 -0
- package/shared/project-folder-check.mjs +152 -0
- package/shared/provider-commands.d.mts +8 -0
- package/shared/provider-commands.mjs +58 -0
- package/shared/rank.d.mts +19 -0
- package/shared/rank.mjs +146 -0
- package/shared/recovery.mjs +26 -0
- package/shared/referenced-files.d.mts +11 -0
- package/shared/referenced-files.mjs +122 -0
- package/shared/repeats.d.mts +45 -0
- package/shared/repeats.mjs +193 -0
- package/shared/schemes.mjs +33 -0
- package/shared/settings-terminal.d.mts +1 -0
- package/shared/settings-terminal.mjs +14 -0
- package/shared/side-build.mjs +57 -0
- package/shared/spawn-trouble.d.mts +8 -0
- package/shared/spawn-trouble.mjs +599 -0
- package/shared/terminal-state.d.mts +1 -0
- package/shared/terminal-state.mjs +58 -0
- package/shared/usage.d.mts +53 -0
- package/shared/usage.mjs +319 -0
- package/shared/work-items.mjs +561 -0
- package/shared/work-lines.mjs +360 -0
- package/worker-permissions.json +6 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
// WHICH CODING AGENT A TASK IS HANDED TO.
|
|
2
|
+
//
|
|
3
|
+
// AND THE PREMISE SHE GAVE FOR IT DID NOT HOLD. Measured head to head on her
|
|
4
|
+
// Mac the same day, identical task, four fresh repos each: Codex 16.8 / 45.3 /
|
|
5
|
+
// 96.9 / 18.6 seconds, Claude Code 19.0 / 19.8 / 19.8 / 21.2. Both correct
|
|
6
|
+
// every run. Codex is NOT faster; it is slower on median and it wanders. That
|
|
7
|
+
// is written here rather than in a report because it is the reason this file
|
|
8
|
+
// must never make Codex the default for anybody, including her. Claude Code is
|
|
9
|
+
// the default and stays the default until she says otherwise. What this file
|
|
10
|
+
// buys is a SECOND ENGINE on a second subscription, which is the thing that
|
|
11
|
+
// actually went wrong for her, and a choice she makes per task.
|
|
12
|
+
//
|
|
13
|
+
// THIS FILE WAS DELETED AND IS COMING BACK IN STAGES, WHICH IS WHY IT NOW HAS
|
|
14
|
+
// A GATE. Her own message on that commit says thirteen; the number here is the
|
|
15
|
+
// one the tree can be asked for.
|
|
16
|
+
//
|
|
17
|
+
// `engine` DID NOT GO. It is still a live name on the work-item contract
|
|
18
|
+
// (`WORK_ITEM_FIELDS`, shared/work-items.mjs:95, accepted by the patch
|
|
19
|
+
// validator at :290, stored as a free string and validated against nothing,
|
|
20
|
+
// because "nothing here reads them"). The fold has no migration and drops
|
|
21
|
+
// nothing, so a row marked between 08-25 and 08-27 still carries
|
|
22
|
+
// `engine: "codex"` UNLESS a later line overwrote that one field — which is
|
|
23
|
+
// possible: the fold lets an equal-or-stronger write replace a field held by
|
|
24
|
+
// an earlier one (`beats`, the authority comparison near shared/work-items.mjs
|
|
25
|
+
// :494). Nothing has ever been written to clear it on purpose, so the honest
|
|
26
|
+
// statement is "probably still there", not "always will be".
|
|
27
|
+
//
|
|
28
|
+
// HOW MANY SUCH ROWS SHE ACTUALLY HAS IS AN OPEN QUESTION, AND IT IS NOT ZERO
|
|
29
|
+
// UNTIL SOMEBODY LOOKS. It could not be measured from the machine this was
|
|
30
|
+
// written on: there is no ~/Zero there at all. On her Mac it is one command,
|
|
31
|
+
// `grep -rho '"engine":"[a-z]*"' ~/Zero | sort | uniq -c`, and the answer
|
|
32
|
+
// belongs in this paragraph when someone runs it. The gate below costs nothing
|
|
33
|
+
// if the count turns out to be zero, and is the whole story if it is not.
|
|
34
|
+
//
|
|
35
|
+
// So the 08-25 resolver, restored unchanged, would have been a silent reversal
|
|
36
|
+
// of her decision: those rows would start choosing Codex again the moment this
|
|
37
|
+
// module landed, with no picker, nothing in Settings, and no approval path
|
|
38
|
+
// built. The resolver is therefore CLOSED unless the caller hands it
|
|
39
|
+
// `ENGINE_CHOICE_ENABLED` below. A caller that passes nothing gets Claude
|
|
40
|
+
// Code, because the safe answer is the one that has to be the accident.
|
|
41
|
+
//
|
|
42
|
+
// This module holds the names and the one rule. Everything Codex-shaped —
|
|
43
|
+
// arguments, its event stream, where it writes its transcript — belongs in
|
|
44
|
+
// main/codex.mjs when that comes back, because the renderer must never import
|
|
45
|
+
// spawn code.
|
|
46
|
+
|
|
47
|
+
/** The engines, in the order the picker draws them. */
|
|
48
|
+
export const ENGINES = [
|
|
49
|
+
{ id: 'claude', label: 'Claude Code', word: 'Claude Code' },
|
|
50
|
+
{ id: 'codex', label: 'Codex', word: 'Codex' },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
export const ENGINE_IDS = ENGINES.map((e) => e.id);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* What a task runs on when nobody has chosen.
|
|
57
|
+
*
|
|
58
|
+
* Not a setting and not a coin flip. See the measurement above: Claude Code is
|
|
59
|
+
* faster on median and never wandered, and it is the only one of the two that
|
|
60
|
+
* is WIRED to stop and ask her for permission mid-run.
|
|
61
|
+
*
|
|
62
|
+
* That second clause used to say Codex CANNOT ask, and it is now wrong twice
|
|
63
|
+
* over. It was true of `codex exec`, which is what the removed 08-25 build
|
|
64
|
+
* drove and what the sentence was written about. `codex app-server` raises real
|
|
65
|
+
* approval requests, measured 2026-09-04, main/codex-app-server.mjs answers
|
|
66
|
+
* them, main/codex-approvals.mjs draws them as the card she already knows, and
|
|
67
|
+
* the spool they land in is the one the other engine uses. This paragraph went
|
|
68
|
+
* on listing all four of those as missing for a whole slice after two of them
|
|
69
|
+
* had been built.
|
|
70
|
+
*
|
|
71
|
+
* THE PICKER IS BUILT NOW TOO, 2026-09-04: a word in the composer's sentence
|
|
72
|
+
* ("With Codex."), a row in Settings, and the engine named in the byline --
|
|
73
|
+
* every one of them drawn only where `Supervisor#engineChoices` says this Mac
|
|
74
|
+
* really offers a choice, which means the gate is open and the binary is there.
|
|
75
|
+
* That paragraph used to say the missing picker was why the default had not
|
|
76
|
+
* moved, and it was the last of four such claims to stop being true.
|
|
77
|
+
*
|
|
78
|
+
* SO THE DEFAULT IS HELD BY THE MEASUREMENT AT THE TOP OF THIS FILE AND BY
|
|
79
|
+
* NOTHING ELSE. Codex is slower on median and it wanders; Claude Code stays the
|
|
80
|
+
* default until she says otherwise, and now that there is a picker that is a
|
|
81
|
+
* decision rather than a consequence.
|
|
82
|
+
*/
|
|
83
|
+
export const DEFAULT_ENGINE = 'claude';
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* THE CAPABILITY GATE: an explicit code opt-in, saying that whoever wrote this
|
|
87
|
+
* call site knew there was a second engine and meant to allow it.
|
|
88
|
+
*
|
|
89
|
+
* BE PRECISE ABOUT WHAT IT PROVES, because a gate believed to be stronger than
|
|
90
|
+
* it is, is worse than no gate. It is a hard barrier against SERIALIZED DATA:
|
|
91
|
+
* nothing that arrives as a ledger line, a config file or an IPC message can
|
|
92
|
+
* open it, ever. It is NOT proof that the approval path she needs was actually
|
|
93
|
+
* built, and it is not a defence against our own code — any module in this repo
|
|
94
|
+
* can import the token. It moves the decision from data to source, and that is
|
|
95
|
+
* all it does. It is the second half, the picker and her consent, that makes
|
|
96
|
+
* the choice real.
|
|
97
|
+
*
|
|
98
|
+
* It is a Symbol, and that shape is the whole design:
|
|
99
|
+
*
|
|
100
|
+
* - A SYMBOL CANNOT ARRIVE FROM OUTSIDE THE PROCESS. Every input that could
|
|
101
|
+
* carry a stale `codex` is serialized: the ledger line off disk and
|
|
102
|
+
* `zero.config.json` are JSON, which has no way to spell a Symbol and drops
|
|
103
|
+
* it in silence, and an Electron IPC payload goes through the structured
|
|
104
|
+
* clone algorithm, which does not drop a Symbol but THROWS DataCloneError on
|
|
105
|
+
* one. Neither can express the gate. The louder of the two failures is the
|
|
106
|
+
* better one and we get it where it matters least; the point is that the
|
|
107
|
+
* untrusted data and the permission live in different type systems.
|
|
108
|
+
* - IT IS `Symbol`, NEVER `Symbol.for`. The global registry would let any code
|
|
109
|
+
* anywhere mint an equal one out of a string. This one exists only where
|
|
110
|
+
* somebody imported this module and named it.
|
|
111
|
+
* - IT IS NOT A BOOLEAN. `enabled: true` is the kind of thing a caller spreads
|
|
112
|
+
* in by accident from some other options object. This is not.
|
|
113
|
+
*/
|
|
114
|
+
export const ENGINE_CHOICE_ENABLED = Symbol('the second engine is wired up in this build');
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* WHEN SHE OPENED THE GATE, read off `zero.config.json`, or null. This is the
|
|
118
|
+
* one value in the app that can cause the Symbol above to be handed over
|
|
119
|
+
* (main/supervisor.mjs `_engineFor`, the only holder of it).
|
|
120
|
+
*
|
|
121
|
+
* THE OPT-IN IS A MOMENT AND NOT A FLAG, and that is the whole design of it.
|
|
122
|
+
*
|
|
123
|
+
* `engine` survived the 2026-08-27 removal on the work-item contract and the
|
|
124
|
+
* fold drops nothing, so her store may still hold rows marked `codex` between
|
|
125
|
+
* 08-25 and 08-27. How many is unknown; on her Mac it is one command,
|
|
126
|
+
* `grep -rho '"engine":"[a-z]*"' ~/Zero | sort | uniq -c`, and the answer
|
|
127
|
+
* belongs here when somebody runs it. A boolean opt-in would mean "every row
|
|
128
|
+
* that ever said codex is live from now on", and the first thing it would do is
|
|
129
|
+
* route an unknown number of those onto a second engine she had marked before
|
|
130
|
+
* there was any way to choose. That is the exact hazard the gate was built for,
|
|
131
|
+
* so an opt-in that walks into it is not one worth having. The picker built on
|
|
132
|
+
* 2026-09-04 does not soften this: an August row is still not a choice she is
|
|
133
|
+
* making now, and the byline draws it as the engine that will really run it.
|
|
134
|
+
*
|
|
135
|
+
* THOSE TWO MEANINGS ARE SEPARABLE, AND THE LEDGER ALREADY HOLDS WHAT
|
|
136
|
+
* SEPARATES THEM. The fold records `wrote[field] = { ts, source }` per field
|
|
137
|
+
* and stopped deleting it on the way out precisely because readers needed it
|
|
138
|
+
* (shared/work-items.mjs:324; shared/answers.mjs reads `wrote.answer.ts` the
|
|
139
|
+
* same way). So "Codex may be chosen FROM NOW ON" is a checkable statement
|
|
140
|
+
* rather than a hope, and `engineChoiceOnRowIsStale` below is the check.
|
|
141
|
+
*
|
|
142
|
+
* WHY NOT A BOOLEAN PLUS A REMEMBERED "first seen at". Because that would live
|
|
143
|
+
* in the app's own state rather than in her file: a fresh install, a state file
|
|
144
|
+
* she deleted, or a second Mac would each invent a different moment, and the
|
|
145
|
+
* one thing this value must be is the same on every machine reading the same
|
|
146
|
+
* store. A date she typed is a fact about her decision; a date we noticed is a
|
|
147
|
+
* fact about a process.
|
|
148
|
+
*
|
|
149
|
+
* THE WORKSPACE DEFAULT (`config.engine`) IS TIME-SCOPED TOO, SINCE 2026-09-05,
|
|
150
|
+
* AND THIS PARAGRAPH USED TO SAY IT COULD NOT BE. Its words were: "JSON has no
|
|
151
|
+
* per-key write times, and `engine` sits in the same file as this, so opening
|
|
152
|
+
* that file to write the moment is the same act as reading the line above it."
|
|
153
|
+
* The first clause is true of JSON and irrelevant: the APP has write times, one
|
|
154
|
+
* module writes that key (`setWorkspaceSetting`, main/settings.mjs), and
|
|
155
|
+
* `saveConfig` takes a patch of several keys, so `engineAt` rides in the same
|
|
156
|
+
* write. Without it, closing the gate and reopening it with a NEWER moment
|
|
157
|
+
* re-armed a workspace default set weeks earlier -- every row that names
|
|
158
|
+
* nothing went straight back to Codex without her saying so again, which is the
|
|
159
|
+
* exact claim this value is supposed to make impossible, and the rollback
|
|
160
|
+
* lifecycle is the thing the gate is FOR.
|
|
161
|
+
*
|
|
162
|
+
* AND THE SECOND CLAUSE, READ CORRECTLY, IS WHY AN ABSENT STAMP MEANS HONOURED.
|
|
163
|
+
* `engineChoice` is hand-written into this file; a hand-written `engine` on the
|
|
164
|
+
* line above it was put there by the same hand in the same act, so the two ARE
|
|
165
|
+
* contemporaneous and there is nothing to be stale against. Only a default the
|
|
166
|
+
* app wrote can carry a stamp, and only a stamped default can go stale — which
|
|
167
|
+
* is also what keeps every config that exists today behaving exactly as it did.
|
|
168
|
+
* `engineDefaultIsStale` below is the check.
|
|
169
|
+
*
|
|
170
|
+
* IT IS PARSED STRICTLY, WHICH IS NOT THE SAME AS PARSED. `Date.parse` reads
|
|
171
|
+
* "1", "2026", "Sep 4 2026" and "2026/09/04" as real dates, so a bare parse
|
|
172
|
+
* would let `"engineChoice": "1"` open the second engine. The shape is checked
|
|
173
|
+
* first and everything else is off, because the safe answer is the one that has
|
|
174
|
+
* to be the accident.
|
|
175
|
+
*/
|
|
176
|
+
const A_MOMENT = /^\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* One strict parse, used by both moments in this file, so the two cannot come
|
|
180
|
+
* to disagree about what counts as a date somebody wrote down. */
|
|
181
|
+
function momentIn(written) {
|
|
182
|
+
if (typeof written !== 'string' || !A_MOMENT.test(written)) return null;
|
|
183
|
+
const at = Date.parse(written);
|
|
184
|
+
return Number.isFinite(at) ? at : null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function engineChoiceSince(config = {}) {
|
|
188
|
+
return momentIn(config?.engineChoice);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* WHEN THE WORKSPACE DEFAULT WAS SET, read off `zero.config.json`, or null.
|
|
193
|
+
*
|
|
194
|
+
* Null means no stamp, and no stamp means a hand-written default — see the long
|
|
195
|
+
* note above `engineChoiceSince`. It is the same strict parse for the same
|
|
196
|
+
* reason: a claim that cannot be read is not a claim, so it reads as no stamp,
|
|
197
|
+
* which is the hand-written case.
|
|
198
|
+
*/
|
|
199
|
+
export function engineDefaultSince(config = {}) {
|
|
200
|
+
return momentIn(config?.engineAt);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Whether the workspace default is one she set BEFORE the gate's current
|
|
205
|
+
* moment: a setting from the build before the rollback, rather than a decision
|
|
206
|
+
* she is making now.
|
|
207
|
+
*
|
|
208
|
+
* False when nobody has opted in, because there is no moment to be stale
|
|
209
|
+
* against. False when the default is Claude Code, which asks for nothing the
|
|
210
|
+
* gate withholds — the same rule `engineChoiceOnRowIsStale` follows below, and
|
|
211
|
+
* for the same reason: discarding it could only ever move work TO the second
|
|
212
|
+
* engine, which is the one direction the gate exists to prevent. False when
|
|
213
|
+
* there is no stamp, which is a default she typed beside the moment itself.
|
|
214
|
+
*/
|
|
215
|
+
export function engineDefaultIsStale(config = {}) {
|
|
216
|
+
const since = engineChoiceSince(config);
|
|
217
|
+
if (since === null) return false;
|
|
218
|
+
if (!enginePicked(config?.engine)) return false;
|
|
219
|
+
const at = engineDefaultSince(config);
|
|
220
|
+
return at !== null && at < since;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Whether the engine named on this row is one picked BEFORE she opened the
|
|
225
|
+
* gate: an August row, rather than a decision she is making now.
|
|
226
|
+
*
|
|
227
|
+
* False when nobody has opted in, because there is no moment to be stale
|
|
228
|
+
* against and the gate itself is what refuses then. False when the row names no
|
|
229
|
+
* engine, or names one nothing recognises, because neither is asking for
|
|
230
|
+
* anything and a warning on an ordinary row is noise on every row. True when
|
|
231
|
+
* the row names an engine and there is no `wrote.engine.ts` at or after the
|
|
232
|
+
* moment — which also covers an imported session and an old line with no
|
|
233
|
+
* `wrote` map at all, where there is simply no evidence she chose anything.
|
|
234
|
+
*
|
|
235
|
+
* AND FALSE FOR A ROW THAT NAMES CLAUDE CODE, WHICH THIS ASKED WITH `isEngine`
|
|
236
|
+
* AND GOT BACKWARDS. A row saying `engine: "claude"`, written before the moment,
|
|
237
|
+
* was discarded as stale — and `engineFor` then fell through to `config.engine`,
|
|
238
|
+
* which may be `codex`. So her explicit "run this on Claude Code" was read as no
|
|
239
|
+
* choice at all and answered with the second engine, which is the precise
|
|
240
|
+
* opposite of what staleness is for. `enginePicked` is the fix and it is the
|
|
241
|
+
* whole of it: naming the DEFAULT asks for nothing the gate withholds, so there
|
|
242
|
+
* is nothing to be stale about, and discarding it could only ever move work onto
|
|
243
|
+
* the engine the gate was built to hold back.
|
|
244
|
+
*
|
|
245
|
+
* `wrote.engine.source` is deliberately NOT consulted. would be the stronger
|
|
246
|
+
* rule, but CLAUDE.md is explicit that a ledger line names its own author as a
|
|
247
|
+
* plain string and nothing checks it, so a source check would guard against an
|
|
248
|
+
* accident and never against a hostile worker, while silently refusing whatever
|
|
249
|
+
* a future picker happens to write. Who may write `engine` at all belongs to
|
|
250
|
+
* the slice that draws one.
|
|
251
|
+
*/
|
|
252
|
+
export function engineChoiceOnRowIsStale(item, config = {}) {
|
|
253
|
+
const since = engineChoiceSince(config);
|
|
254
|
+
if (since === null) return false;
|
|
255
|
+
if (!enginePicked(item?.engine)) return false;
|
|
256
|
+
const ts = item?.wrote?.engine?.ts;
|
|
257
|
+
return !(Number.isFinite(ts) && ts >= since);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function isEngine(id) {
|
|
261
|
+
return typeof id === 'string' && ENGINE_IDS.includes(id);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function engineLabel(id) {
|
|
265
|
+
return ENGINES.find((e) => e.id === id)?.label ?? ENGINES[0].label;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Which engines this Mac can actually run, given what the finders found.
|
|
270
|
+
*
|
|
271
|
+
* `found` is a map of id to boolean. An engine whose binary is not on the
|
|
272
|
+
* machine is not offered, because a picker that offers something that cannot
|
|
273
|
+
* run is a picker that hands her a session that dies on arrival. Claude Code
|
|
274
|
+
* is always in the list even when it was not found: the app already refuses to
|
|
275
|
+
* open an inbox without it and says so in its own words (main/claude-bin.mjs),
|
|
276
|
+
* and a second sentence here saying the same thing differently is noise.
|
|
277
|
+
*
|
|
278
|
+
* UNGATED, AND THE INVARIANT IS THEREFORE SCOPED TO ROUTING. Say it plainly,
|
|
279
|
+
* because the file must not claim more than the code delivers: this function,
|
|
280
|
+
* `engineChoiceExists` and `enginePicked` will all happily describe a two-engine
|
|
281
|
+
* Mac without anyone holding the token. A future renderer could draw a picker
|
|
282
|
+
* out of them, and the gate would not stop it — she would be offered a choice
|
|
283
|
+
* that `engineFor` then refuses, which is its own kind of lie.
|
|
284
|
+
*
|
|
285
|
+
* That is a deliberate trade and not an oversight. These three answer "what is
|
|
286
|
+
* on this Mac" and "what did she pick"; they spawn nothing, and requiring a
|
|
287
|
+
* capability token to answer a question about the filesystem buys nothing real.
|
|
288
|
+
*
|
|
289
|
+
* AND THE SLICE THAT DREW A PICKER ANSWERED FOR IT, 2026-09-04. There is
|
|
290
|
+
* exactly one caller, `Supervisor#engineChoices`, and it asks
|
|
291
|
+
* `engineChoiceSince` FIRST -- the same expression `_engineFor` derives the gate
|
|
292
|
+
* token from -- so the offer and the routing are refused by the same fact and
|
|
293
|
+
* cannot come apart. A Mac with Codex installed and no moment written answers
|
|
294
|
+
* one engine here and Claude Code there. The renderer calls none of these: it
|
|
295
|
+
* is handed the list on the snapshot. tests/a-codex-row-from-august-still-runs-on-claude.test.mjs
|
|
296
|
+
* holds the caller list exactly, so the next one has to argue for itself here.
|
|
297
|
+
*/
|
|
298
|
+
export function availableEngines(found = {}) {
|
|
299
|
+
return ENGINES.filter((e) => e.id === DEFAULT_ENGINE || found[e.id]);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Whether there is a choice to make at all. One engine is not a choice. */
|
|
303
|
+
export function engineChoiceExists(found = {}) {
|
|
304
|
+
return availableEngines(found).length > 1;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* THE RULE. Which engine one work item runs on, and it is deliberately narrow.
|
|
309
|
+
*
|
|
310
|
+
* The gate answers first: without `enabled: ENGINE_CHOICE_ENABLED` this is
|
|
311
|
+
* Claude Code, whatever the row says and whatever the workspace says. That is
|
|
312
|
+
* what holds the August rows described at the top of this file, and it means
|
|
313
|
+
* the honest answer for any caller that has not been taught about the second
|
|
314
|
+
* engine — including every caller written before it comes back — is the engine
|
|
315
|
+
* she is running.
|
|
316
|
+
*
|
|
317
|
+
* Then her choice on the task, over the workspace default, which is hers to
|
|
318
|
+
* set in Settings, over Claude Code. An engine named on a row whose binary is
|
|
319
|
+
* not on this Mac falls back rather than dying: she may have chosen Codex on
|
|
320
|
+
* her laptop and be reading the row on a machine without it, and a session
|
|
321
|
+
* that never starts tells her nothing about why. That fallback is unchanged
|
|
322
|
+
* and the gate is IN ADDITION to it, not instead of it — the binary is a fact
|
|
323
|
+
* about this Mac, the gate is a fact about this build, and either one missing
|
|
324
|
+
* is enough to answer Claude Code.
|
|
325
|
+
*
|
|
326
|
+
* AND A CHOICE FROM BEFORE SHE OPENED THE GATE IS NOT A CHOICE SHE IS MAKING.
|
|
327
|
+
* When the config names the moment she opted in, a row whose `engine` predates
|
|
328
|
+
* it reads as no choice at all and the workspace default answers instead. That
|
|
329
|
+
* is `engineChoiceOnRowIsStale` above, and it is the whole of what makes the
|
|
330
|
+
* opt-in mean "Codex may be chosen from now on" rather than "every row that
|
|
331
|
+
* ever said codex is live". A caller that passes no such moment gets the old
|
|
332
|
+
* rule unchanged, which is why every assertion written before this one still
|
|
333
|
+
* holds -- but the single caller in the app that can open the gate derives the
|
|
334
|
+
* token FROM that same value, so in her app the two cannot come apart.
|
|
335
|
+
*/
|
|
336
|
+
export function engineFor(item, { config = {}, found = {}, enabled } = {}) {
|
|
337
|
+
if (enabled !== ENGINE_CHOICE_ENABLED) return DEFAULT_ENGINE;
|
|
338
|
+
const wanted = isEngine(item?.engine) && !engineChoiceOnRowIsStale(item, config) ? item.engine : null;
|
|
339
|
+
const fallback = isEngine(config?.engine) && !engineDefaultIsStale(config) ? config.engine : DEFAULT_ENGINE;
|
|
340
|
+
const pick = wanted ?? fallback;
|
|
341
|
+
if (pick === DEFAULT_ENGINE) return DEFAULT_ENGINE;
|
|
342
|
+
return found[pick] ? pick : DEFAULT_ENGINE;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Whether the engine on this row is one she chose, as opposed to the one it
|
|
347
|
+
* would run on anyway. What makes the composer's footer stay open, the same
|
|
348
|
+
* rule the level and the model follow (renderer/src/compose-footer.ts).
|
|
349
|
+
*/
|
|
350
|
+
export function enginePicked(id) {
|
|
351
|
+
return isEngine(id) && id !== DEFAULT_ENGINE;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* THE MODEL ON THIS ROW, OR NULL BECAUSE IT BELONGS TO THE OTHER HARNESS.
|
|
356
|
+
*
|
|
357
|
+
* THE MIRROR OF `codexModelRefusal`, WHICH ONLY EVER GUARDED ONE DIRECTION.
|
|
358
|
+
* That one stops a Claude alias reaching a Codex run. Nothing stopped the
|
|
359
|
+
* reverse, and the reverse happens BY ITSELF: `engineFor` above falls back to
|
|
360
|
+
* Claude Code when Codex is not on this Mac -- which is correct and stays --
|
|
361
|
+
* and `spawnPlan` then applied `item.model` without asking which engine wrote
|
|
362
|
+
* it. Measured 2026-09-05 by reading the resolved plan: a row saying `engine:
|
|
363
|
+
* "codex", model: "gpt-5.6-sol"` -- the slug her own config.toml names --
|
|
364
|
+
* produced `claude -p <brief> --model gpt-5.6-sol`, which Claude Code has never
|
|
365
|
+
* heard of. It dies on arrival and dies again on every retry.
|
|
366
|
+
*
|
|
367
|
+
* THE TWO FIELDS ARE ONE CHOICE AND THIS IS WHY. The composer's model list is
|
|
368
|
+
* scoped to the engine (`engineModelChoices`, renderer/src/models.ts) because
|
|
369
|
+
* the two vocabularies do not overlap at all: `opus` is an alias Claude Code
|
|
370
|
+
* resolves, `gpt-5.6-sol` is an exact slug Codex takes. So a model word means
|
|
371
|
+
* nothing without the engine beside it, and a row whose engine has changed
|
|
372
|
+
* under it has no model, rather than one that reads across.
|
|
373
|
+
*
|
|
374
|
+
* NULL IS NOT A STAND-IN AND THE CALLER MUST NOT TREAT IT AS ONE. This says
|
|
375
|
+
* "there is no model on this row for this run"; whether that means running on
|
|
376
|
+
* the workspace's own model or refusing the run outright is the caller's
|
|
377
|
+
* decision, and main/supervisor.mjs makes it -- it refuses, and says so on the
|
|
378
|
+
* row, for the reason `codexModelRefusal` gives on the other side.
|
|
379
|
+
*
|
|
380
|
+
* A ROW THAT NAMES NO ENGINE IS UNTOUCHED, which is every row she has ever
|
|
381
|
+
* written: there is no other harness in the sentence, so the model rides
|
|
382
|
+
* exactly as it did.
|
|
383
|
+
*/
|
|
384
|
+
export function modelForEngine(item, engine) {
|
|
385
|
+
const model = typeof item?.model === 'string' && item.model ? item.model : null;
|
|
386
|
+
if (!model) return null;
|
|
387
|
+
if (isEngine(item?.engine) && item.engine !== engine) return null;
|
|
388
|
+
return model;
|
|
389
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// BEAT EIGHT: THE THREE EXAMPLE ROWS, AND CLEARING THEM IS THE POINT.
|
|
2
|
+
//
|
|
3
|
+
// STAGED IS THE ONE SHE TOOK, off the round four page: "Build it, with the
|
|
4
|
+
// staged tasks on beat eight (recommended)", answered "perfect! build it". The
|
|
5
|
+
// alternative was really running three of the agents just imported, which
|
|
6
|
+
// spends somebody's tokens in their first minute and does nothing at all on a
|
|
7
|
+
// Mac that had no agent files on it.
|
|
8
|
+
//
|
|
9
|
+
// THE THREE SHAPES ARE FROM ROUND ONE: a question, a piece of finished work,
|
|
10
|
+
// and a decision an agent made without her. Between them they are the whole of
|
|
11
|
+
// what an inbox holds and the three different things clearing one can mean.
|
|
12
|
+
//
|
|
13
|
+
// EVERY ONE OF THEM SAYS "Example" FIRST, IN THE TITLE AS WELL AS THE RESULT.
|
|
14
|
+
// Somebody who clears three tasks in an onboarding and later finds them in
|
|
15
|
+
// their closed list should never have to wonder whether they were real.
|
|
16
|
+
//
|
|
17
|
+
// This file has no window and no filesystem in it so both halves can read it:
|
|
18
|
+
// the renderer draws the walk off ../renderer/src/onboarding.ts, and the main
|
|
19
|
+
// process writes the rows off here.
|
|
20
|
+
|
|
21
|
+
export const EXAMPLES = [
|
|
22
|
+
{
|
|
23
|
+
kind: 'question',
|
|
24
|
+
title: 'Example: which of these two names should the project use?',
|
|
25
|
+
result: 'Example. Both are free. Say either one and I will rename everything to match.',
|
|
26
|
+
agoMs: 4 * 60_000,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
kind: 'task',
|
|
30
|
+
title: 'Example: the sign in page is built and the tests are green.',
|
|
31
|
+
result: 'Example. Nothing here needs you. Close it and it is gone.',
|
|
32
|
+
agoMs: 11 * 60_000,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
kind: 'review',
|
|
36
|
+
title: 'Example: I took the smaller library, say if you disagree.',
|
|
37
|
+
result: 'Example. This is what an agent looks like when it made a call without you.',
|
|
38
|
+
agoMs: 26 * 60_000,
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* WHAT PUTS A ROW IN HER INBOX, and it is not the kind or the label: it is
|
|
44
|
+
* `answeredHerAsk` in renderer/src/list-rules.ts, which is her own open row
|
|
45
|
+
* carrying a result an agent wrote AFTER her last word on it. So each example
|
|
46
|
+
* is written twice, her half and then the agent's half, and the agent's half
|
|
47
|
+
* has to carry the later timestamp or the row is real and simply never drawn.
|
|
48
|
+
*
|
|
49
|
+
* This returns the two writes for one example, in order, with the timestamps
|
|
50
|
+
* already worked out. Pure, so the ordering is tested rather than hoped for:
|
|
51
|
+
* tests/the-walk-stages-three-examples.test.mjs. */
|
|
52
|
+
export function stampsFor(example, now) {
|
|
53
|
+
const at = now - example.agoMs;
|
|
54
|
+
return {
|
|
55
|
+
// the row exists and it is her ask, at the moment it landed.
|
|
56
|
+
hers: at,
|
|
57
|
+
// The agent's: strictly later, which is what the inbox rule asks for.
|
|
58
|
+
agent: at + 1_000,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// THE ONE LINE THE FIRST TASK ANSWERS WITH.
|
|
2
|
+
//
|
|
3
|
+
// So the walk's example task still has a real row, a real reading pane and a
|
|
4
|
+
// real answer, and the answer is read off the folder she just pointed at
|
|
5
|
+
// instead of being reasoned about by a session. The reason is measured and it
|
|
6
|
+
// is on the row: her own first task sat queued for thirteen seconds behind the
|
|
7
|
+
// directive the walk had just created, ran for another forty-seven, and came
|
|
8
|
+
// back in 59.6 seconds while the sentence on screen promised a few. A first
|
|
9
|
+
// task that depends on a live agent inherits the queue, the model, the network
|
|
10
|
+
// and the size of the folder, and a first run cannot promise anything about any
|
|
11
|
+
// of those.
|
|
12
|
+
//
|
|
13
|
+
// WHAT IT ANSWERS WITH still has to be TRUE, because it is also what goes in
|
|
14
|
+
// the sidebar under the project's name.So this never invents a sentence about a
|
|
15
|
+
// project. It reads one that is already written down, and when nothing is
|
|
16
|
+
// written down it says so.
|
|
17
|
+
//
|
|
18
|
+
// Pure on purpose: the reading of the disk is in main/first-run.mjs and the
|
|
19
|
+
// judgement about what a line is worth is here, where a test can hold it.
|
|
20
|
+
|
|
21
|
+
/** As long as a sentence in a list row can be before it is a paragraph. */
|
|
22
|
+
export const MAX_LINE = 180;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Under this a full stop is an abbreviation rather than the end of a thought.
|
|
26
|
+
* "e.g." and "v1.2" both live below it and no description does. */
|
|
27
|
+
const MIN_SENTENCE = 24;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A markdown badge line: `[](b)`, shields, and nothing else on it.
|
|
31
|
+
* Readmes open with rows of these and none of them says what the thing is. */
|
|
32
|
+
function isBadges(line) {
|
|
33
|
+
const stripped = line.replace(/\[?!\[[^\]]*\]\([^)]*\)\]?\([^)]*\)?/g, '').trim();
|
|
34
|
+
return stripped === '' && /!\[/.test(line);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A line that is furniture rather than prose.
|
|
39
|
+
*
|
|
40
|
+
* THE LAST FOUR ARE MEASURED, not imagined. Run over the 29 project folders in
|
|
41
|
+
* ~/Desktop/dev on 2026-08-21, the first cut of this returned "Run the
|
|
42
|
+
* application with the following command:" for one project, "To build the
|
|
43
|
+
* project, run:" for another, a shields.io link definition for a third and
|
|
44
|
+
* "Built with: Next.js 16 | FastAPI | DeepAgents | ..." for a fourth. Every one
|
|
45
|
+
* of those is a true line out of a readme and none of them says what the thing
|
|
46
|
+
* IS, which is the question the task asks. */
|
|
47
|
+
function isFurniture(line) {
|
|
48
|
+
if (!line) return true;
|
|
49
|
+
if (line.startsWith('<!--') || line.startsWith('<')) return true; // html comment, or a centred div
|
|
50
|
+
if (/^[-=*_]{3,}$/.test(line)) return true; // a rule
|
|
51
|
+
if (/^[-*+]\s/.test(line) || /^\d+[.)]\s/.test(line)) return true; // a list item is not a description
|
|
52
|
+
if (line.startsWith('```') || line.startsWith(' ')) return true; // code
|
|
53
|
+
if (line.startsWith('|') || line.startsWith('>')) return true; // a table row, a blockquote
|
|
54
|
+
if (isBadges(line)) return true;
|
|
55
|
+
if (/^\[[^\]]+\]:\s/.test(line)) return true; // a link definition at the foot of a readme
|
|
56
|
+
if (line.endsWith(':')) return true; // a lead-in to a command, not a description
|
|
57
|
+
if ((line.match(/\|/g) ?? []).length >= 2) return true; // a stack list, not a sentence
|
|
58
|
+
if (/^(install|usage|getting started|quick ?start|setup|license|contributing)\b/i.test(line)) return true;
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Markdown out, words left. Links keep their text, emphasis and code lose
|
|
64
|
+
* their marks, and a trailing full stop is kept because it is a sentence. */
|
|
65
|
+
export function plainText(line) {
|
|
66
|
+
return line
|
|
67
|
+
.replace(/!\[[^\]]*\]\([^)]*\)/g, '')
|
|
68
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
69
|
+
.replace(/`([^`]*)`/g, '$1')
|
|
70
|
+
.replace(/[*_]{1,3}([^*_]+)[*_]{1,3}/g, '$1')
|
|
71
|
+
.replace(/\s+/g, ' ')
|
|
72
|
+
.trim();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* THE FIRST REAL LINE OF A README. The heading at the top is the project's
|
|
77
|
+
* name, which she already typed on the screen before this one, so it is not
|
|
78
|
+
* the answer: what is wanted is the first line of prose under it. A readme
|
|
79
|
+
* with nothing but a heading in it returns null rather than the heading. */
|
|
80
|
+
export function fromReadme(text) {
|
|
81
|
+
if (!text) return null;
|
|
82
|
+
let inFence = false;
|
|
83
|
+
for (const raw of String(text).split(/\r?\n/)) {
|
|
84
|
+
const line = raw.trim();
|
|
85
|
+
if (line.startsWith('```')) { inFence = !inFence; continue; }
|
|
86
|
+
if (inFence) continue;
|
|
87
|
+
if (line.startsWith('#')) continue; // the name, and every heading after it
|
|
88
|
+
if (isFurniture(line)) continue;
|
|
89
|
+
const words = plainText(line);
|
|
90
|
+
// Two words is the floor. A one word line is a label, not a description.
|
|
91
|
+
if (words.split(' ').length < 2) continue;
|
|
92
|
+
return clip(words);
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* package.json says what a thing is in one field, and people fill it in. Same
|
|
99
|
+
* for Cargo.toml and pyproject.toml, which is why the shape is shared. */
|
|
100
|
+
export function fromDescription(value) {
|
|
101
|
+
if (typeof value !== 'string') return null;
|
|
102
|
+
const words = plainText(value);
|
|
103
|
+
return words.split(' ').filter(Boolean).length >= 2 ? clip(words) : null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* ONE SENTENCE, AND NOT A PARAGRAPH. The FIRST sentence end rather than the
|
|
108
|
+
* last one that fits, which is the opposite of how the inbox row clips: a row
|
|
109
|
+
* wants as much of the message as the width allows, and this wants the
|
|
110
|
+
* description and not the sentence after it.
|
|
111
|
+
*
|
|
112
|
+
* Only a first sentence long enough to say something counts. Under that, and
|
|
113
|
+
* when there is no sentence end at all, the cut is at a word with an ellipsis,
|
|
114
|
+
* because a line that stops mid-word reads as a bug rather than as a summary. */
|
|
115
|
+
export function clip(text, max = MAX_LINE) {
|
|
116
|
+
const first = text.search(/[.!?](\s|$)/);
|
|
117
|
+
if (first >= MIN_SENTENCE && first < max) return text.slice(0, first + 1);
|
|
118
|
+
if (text.length <= max) return text;
|
|
119
|
+
const cut = text.slice(0, max);
|
|
120
|
+
const space = cut.lastIndexOf(' ');
|
|
121
|
+
return (space > MIN_SENTENCE ? cut.slice(0, space) : cut).replace(/[,;:\s]+$/, '') + '…';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* WHAT THE TASK ANSWERS, given whatever the folder had in it.
|
|
126
|
+
*
|
|
127
|
+
* `readme` and `description` are what was found on disk, either of them null.
|
|
128
|
+
* `name` is what she called the project on the screen before, and `folder` is
|
|
129
|
+
* the short path she chose. The last case is the honest one: a folder with no
|
|
130
|
+
* readme and no description in it has nothing written down that says what it
|
|
131
|
+
* is, and saying so is better than guessing, because this same line goes in
|
|
132
|
+
* the sidebar and stays there. */
|
|
133
|
+
export function firstRunAnswer({ readme, description, name, folder }) {
|
|
134
|
+
const found = fromReadme(readme) ?? fromDescription(description);
|
|
135
|
+
if (found) return { line: found, found: true };
|
|
136
|
+
const where = folder ? ` in ${folder}` : '';
|
|
137
|
+
const who = name || 'This project';
|
|
138
|
+
// TWO DIFFERENT TRUE SENTENCES, because they are two different facts and the
|
|
139
|
+
// first cut said the wrong one. Measured over ~/Desktop/dev on 2026-08-21:
|
|
140
|
+
// agentbox-website HAS a readme, and every line in it is a build instruction,
|
|
141
|
+
// so "has no readme" was simply false about her own folder.
|
|
142
|
+
if (readme && String(readme).trim()) {
|
|
143
|
+
return { line: `${who} has a readme${where}, and no line in it says what the project is.`, found: false };
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
line: `${who} has no readme${where}, so there is nothing written down yet that says what it is.`,
|
|
147
|
+
found: false,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Types for shared/first-run-practice.mjs, which the renderer imports for the
|
|
2
|
+
// practice project's copy. Same pattern as agents.d.mts beside it: the module
|
|
3
|
+
// is plain JavaScript so both halves can read it, and this says what it is.
|
|
4
|
+
|
|
5
|
+
export const PRACTICE_NAME: string;
|
|
6
|
+
export const PRACTICE_SLUG: string;
|
|
7
|
+
export const PRACTICE_FLAG: string;
|
|
8
|
+
export const PRACTICE_TASK: { title: string; body: string };
|
|
9
|
+
export const PRACTICE_ANSWER: string;
|
|
10
|
+
export const PRACTICE_TASK_TRACE: string[];
|
|
11
|
+
/** The practice project's sidebar note, written to its `pinned.md` and drawn
|
|
12
|
+
* beside the third introduction slab. */
|
|
13
|
+
export const PRACTICE_NOTE: string;
|
|
14
|
+
export const PRACTICE_ROWS: {
|
|
15
|
+
kind: string;
|
|
16
|
+
title: string;
|
|
17
|
+
/** The sentence a person typed to start it. */
|
|
18
|
+
body: string;
|
|
19
|
+
/** The lines the run wrote while it worked, in the shape traceStreamLine
|
|
20
|
+
* writes a real one: `[Tool] argument`, or a plain sentence when the agent
|
|
21
|
+
* is talking. The store stamps the times. */
|
|
22
|
+
trace: string[];
|
|
23
|
+
result: string;
|
|
24
|
+
agoMs: number;
|
|
25
|
+
/** THE ONE THAT IS NOT FINISHED. An agent is stopped on it, waiting for an
|
|
26
|
+
* answer, and beat thirteen is about the difference between this row and the
|
|
27
|
+
* other two. Exactly one of the three carries it. */
|
|
28
|
+
waiting?: boolean;
|
|
29
|
+
/** THE ONE THAT IS NOT FOR TODAY. Real work, nobody's emergency, and the row
|
|
30
|
+
* beat fourteen snoozes. Exactly one of the four carries it. */
|
|
31
|
+
later?: boolean;
|
|
32
|
+
}[];
|