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
package/shared/rank.mjs
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// The founder's running order over her companies, and the one scoring rule
|
|
2
|
+
// derived from it.
|
|
3
|
+
//
|
|
4
|
+
// This file exists because the rule was written twice: once in the supervisor
|
|
5
|
+
// (which decides what to spawn) and once in the renderer (which decides what
|
|
6
|
+
// the inbox shows first). Two copies of "what matters most" is two answers to
|
|
7
|
+
// the same question, and the day they disagree is the day the top of the inbox
|
|
8
|
+
// stops being what the agents are actually working on. There is one copy now,
|
|
9
|
+
// imported by both.
|
|
10
|
+
//
|
|
11
|
+
// The model is an ORDER, not a set of buckets (founder's call, 2026-08-06).
|
|
12
|
+
// Buckets leave every tie inside a bucket unanswered, and "which of these two
|
|
13
|
+
// comes first" is the question the fleet needs settled.
|
|
14
|
+
|
|
15
|
+
// A place in the order is worth this much, so it dominates any item's own
|
|
16
|
+
// priority (which runs 0..9). A project one step higher in the order beats
|
|
17
|
+
// anything filed under a project one step lower, whatever its tag says.
|
|
18
|
+
export const RANK_STEP = 100;
|
|
19
|
+
|
|
20
|
+
/** Clean a proposed order: strings only, no blanks, no duplicates. */
|
|
21
|
+
export function normalizeOrder(slugs) {
|
|
22
|
+
if (!Array.isArray(slugs)) return [];
|
|
23
|
+
const seen = new Set();
|
|
24
|
+
const out = [];
|
|
25
|
+
for (const slug of slugs) {
|
|
26
|
+
if (typeof slug !== 'string' || !slug || seen.has(slug)) continue;
|
|
27
|
+
seen.add(slug);
|
|
28
|
+
out.push(slug);
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The score contributed by a product's place in the order. Higher is sooner.
|
|
35
|
+
*
|
|
36
|
+
* An UNRANKED product scores zero, which puts it below every ranked one and
|
|
37
|
+
* ties it with the other unranked. That is deliberate: a product the founder
|
|
38
|
+
* has never placed must not silently outrank one she has, and an empty order
|
|
39
|
+
* has to behave exactly like the flat inbox that existed before ranking did.
|
|
40
|
+
*/
|
|
41
|
+
export function productRankScore(order, slug) {
|
|
42
|
+
const idx = order.indexOf(slug);
|
|
43
|
+
if (idx === -1) return 0;
|
|
44
|
+
return (order.length - idx) * RANK_STEP;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The number an item carries into that score.
|
|
49
|
+
*
|
|
50
|
+
* An AGENT'S tag counts for nothing. Agents used to set this themselves, on a
|
|
51
|
+
* rule in the worker brief that told them 7 meant blocking; of the 442 items
|
|
52
|
+
* agents filed, 127 came in at 7 or higher, which is not an order, it is
|
|
53
|
+
* noise. She asked for the ordering to be the app's job, the rule is out of
|
|
54
|
+
* the brief, and this is the half that also covers the items already sitting
|
|
55
|
+
* in the store carrying an agent's 8.
|
|
56
|
+
*
|
|
57
|
+
* Everything else counts, and the test for "hers" is deliberately wide: her
|
|
58
|
+
* compose box writes the number as 'system' rather than 'founder' (the split
|
|
59
|
+
* in store.composeItem is words-versus-lifecycle, not authority), and a repeat
|
|
60
|
+
* rule's number arrives the same way. Reading only 'founder' here would have
|
|
61
|
+
* thrown away the Urgent tag on the very row she complained about
|
|
62
|
+
* (tests/urgent-does-not-wait-behind-medium.test.mjs).
|
|
63
|
+
*
|
|
64
|
+
* Nought means untagged, not bottom. It is the seed the fold starts an item
|
|
65
|
+
* at and the default her compose sends when she picks no number, and her own
|
|
66
|
+
* scale starts at Low, which is 2. An untagged row therefore sits in the
|
|
67
|
+
* middle with everything else and her order over the products, then age,
|
|
68
|
+
* decides it.
|
|
69
|
+
*/
|
|
70
|
+
export const DEFAULT_PRIORITY = 5;
|
|
71
|
+
|
|
72
|
+
export function itemPriority(item) {
|
|
73
|
+
if (!item) return DEFAULT_PRIORITY;
|
|
74
|
+
if (item.wrote?.priority?.source === 'agent') return DEFAULT_PRIORITY;
|
|
75
|
+
const n = item.priority;
|
|
76
|
+
return Number.isFinite(n) && n > 0 ? n : DEFAULT_PRIORITY;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The top of her own scale, and the only level that INTERRUPTS.
|
|
81
|
+
*
|
|
82
|
+
* The number is the renderer's (renderer/src/priority.ts: Urgent is 9), and it
|
|
83
|
+
* is named here because the supervisor needs the same answer and importing a
|
|
84
|
+
* .ts module into the main process is not available to it. Same reason the
|
|
85
|
+
* whole file exists: the day the two copies disagree is the day Urgent stops
|
|
86
|
+
* meaning urgent in one half of the app.
|
|
87
|
+
*/
|
|
88
|
+
export const URGENT_PRIORITY = 9;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Is this HER urgent row?
|
|
92
|
+
*
|
|
93
|
+
* itemPriority already throws away an agent's number, so this cannot be
|
|
94
|
+
* reached by a worker tagging its own proposal a nine. That matters more here
|
|
95
|
+
* than anywhere else in this file: everything downstream of this predicate
|
|
96
|
+
* takes a running session away from something else.
|
|
97
|
+
*/
|
|
98
|
+
export function isUrgent(item) {
|
|
99
|
+
return itemPriority(item) >= URGENT_PRIORITY;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Migration from the three-bucket model. Only ever runs against state written
|
|
104
|
+
* before the order existed, and preserves the founder's intent: everything she
|
|
105
|
+
* called high comes first, then untagged, then everything she called low.
|
|
106
|
+
*/
|
|
107
|
+
export function orderFromTiers(tiers) {
|
|
108
|
+
if (!tiers || typeof tiers !== 'object') return [];
|
|
109
|
+
const slugs = Object.keys(tiers);
|
|
110
|
+
const rank = (s) => (tiers[s] > 0 ? 0 : tiers[s] < 0 ? 2 : 1);
|
|
111
|
+
return normalizeOrder(slugs.filter((s) => tiers[s] !== 0).sort((a, b) => rank(a) - rank(b)));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Where a dragged chip would land, in READING ORDER, given the chips' boxes and
|
|
116
|
+
* the pointer. Returns an index to insert BEFORE; the length means "at the end".
|
|
117
|
+
*
|
|
118
|
+
* This is pure geometry and lives here rather than in the component because it
|
|
119
|
+
* is the half of ranking that was wrong. The first version compared the pointer
|
|
120
|
+
* against chip centres on the X axis alone, which is right for one row and
|
|
121
|
+
* useless for four: with twenty two projects the chips WRAP, every row spans
|
|
122
|
+
* the same X range, and dragging up or down moved nothing at all (founder,
|
|
123
|
+
* 2026-08-06). Row first, then position within the row.
|
|
124
|
+
*/
|
|
125
|
+
export function landingIndex(rects, x, y) {
|
|
126
|
+
if (!Array.isArray(rects)) return 0;
|
|
127
|
+
for (let i = 0; i < rects.length; i += 1) {
|
|
128
|
+
const r = rects[i];
|
|
129
|
+
if (!r) continue;
|
|
130
|
+
if (y < r.top) return i; // above this row entirely
|
|
131
|
+
if (y <= r.bottom && x < r.left + r.width / 2) return i; // this row, left of centre
|
|
132
|
+
}
|
|
133
|
+
return rects.length;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Move one product to a new index, returning a fresh order. Products that were
|
|
138
|
+
* never in the order join it on their first move, so dragging an unranked chip
|
|
139
|
+
* is how it gets ranked at all.
|
|
140
|
+
*/
|
|
141
|
+
export function moveProduct(order, slug, toIndex) {
|
|
142
|
+
const base = normalizeOrder(order).filter((s) => s !== slug);
|
|
143
|
+
const at = Math.max(0, Math.min(base.length, Math.trunc(toIndex)));
|
|
144
|
+
base.splice(at, 0, slug);
|
|
145
|
+
return base;
|
|
146
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// What the app says after it has swept up a night's stranded work.
|
|
2
|
+
//
|
|
3
|
+
// The two numbers mean different things and must never blur:
|
|
4
|
+
// resumed — sessions handed back their own context. Nothing for her to do.
|
|
5
|
+
// waiting — rows whose session is gone. Her rule was that these wait for her
|
|
6
|
+
// rather than being restarted, so the line has to say where they
|
|
7
|
+
// are, which is the palette command that already exists.
|
|
8
|
+
//
|
|
9
|
+
// Slots are not mentioned. A row that only ran out of capacity resumes itself
|
|
10
|
+
// on the next tick, and a toast that lasts 2.5 seconds has no room for a
|
|
11
|
+
// number that will be wrong before she has finished reading it.
|
|
12
|
+
|
|
13
|
+
export function recoveryToast(r) {
|
|
14
|
+
if (!r) return null;
|
|
15
|
+
const resumed = Number(r.resumed) || 0;
|
|
16
|
+
const waiting = Number(r.waiting) || 0;
|
|
17
|
+
if (!resumed && !waiting) return null;
|
|
18
|
+
const agents = (n) => (n === 1 ? '1 agent' : `${n} agents`);
|
|
19
|
+
if (!waiting) {
|
|
20
|
+
return `${agents(resumed)} picked up where ${resumed === 1 ? 'it' : 'they'} left off`;
|
|
21
|
+
}
|
|
22
|
+
if (!resumed) {
|
|
23
|
+
return `${agents(waiting)} could not be resumed · waiting for you in ⌘K`;
|
|
24
|
+
}
|
|
25
|
+
return `${agents(resumed)} resumed · ${waiting} waiting for you in ⌘K`;
|
|
26
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Types for shared/referenced-files.mjs, which is plain ESM so that main (the
|
|
2
|
+
// spawn path's brief) and the renderer (her attachment row) share one copy of
|
|
3
|
+
// the rule about what counts as a file a message refers to.
|
|
4
|
+
|
|
5
|
+
// The last argument may be `{ dir }`, the product's own folder, which is what
|
|
6
|
+
// lets a path written out in full from the root be read as one of ours.
|
|
7
|
+
export function referencedFiles(
|
|
8
|
+
...texts: Array<string | null | undefined | { dir?: string | null }>
|
|
9
|
+
): string[];
|
|
10
|
+
export function fileLabel(path: string): { name: string; where: string };
|
|
11
|
+
export function isImagePath(path: string | null | undefined): boolean;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// PURE. Every file a message refers to, in the order it refers to them.
|
|
2
|
+
//
|
|
3
|
+
// The last clause is the design. Nothing here asks a worker to remember to
|
|
4
|
+
// attach anything: the message text IS the manifest, so a path a worker merely
|
|
5
|
+
// mentioned in passing lands at the foot of the message like any other
|
|
6
|
+
// attachment. Three ways a worker names a file, all of them already in use:
|
|
7
|
+
//
|
|
8
|
+
// [the report](reports/week-32.md) a markdown link or image
|
|
9
|
+
// `designs/round-3.html` a path in a code span
|
|
10
|
+
// designs/round-3.html a bare path in prose
|
|
11
|
+
//
|
|
12
|
+
// Absolute paths, urls and anchors are not files of this product's, so they are
|
|
13
|
+
// left alone. The extension list is what decides: a bare word with a dot in it
|
|
14
|
+
// is not a reference, and guessing wider would put "e.g" and "v1.2" in her
|
|
15
|
+
// attachment row.
|
|
16
|
+
//
|
|
17
|
+
// IT LIVES IN shared/ BECAUSE THE BRIEF NEEDS IT TOO. This was the renderer's
|
|
18
|
+
// own module, which meant the only reader of a message's manifest was the
|
|
19
|
+
// screen. The spawn path had none, so a task carrying her screenshot was handed
|
|
20
|
+
// to a worker as a relative link with nothing anywhere telling it to open one:
|
|
21
|
+
// 155 of the 320 sessions on Agentbox that were handed one never did. Same rule,
|
|
22
|
+
// one copy, both processes; renderer/src/referenced-files.ts re-exports it.
|
|
23
|
+
// `change` is the newest one and it is the whole answer to David's call.A run
|
|
24
|
+
// that touched the repository writes what it changed as a file, the file lands
|
|
25
|
+
// in the row at the foot of the card like every drawing already does, and
|
|
26
|
+
// clicking it opens the change in the pane.
|
|
27
|
+
const EXT = 'png|jpe?g|gif|webp|svg|html?|pdf|md|markdown|csv|txt|json|ya?ml|sql|sh|css|jsx?|tsx?|mjs|cjs|py|rb|go|rs|toml|change';
|
|
28
|
+
|
|
29
|
+
// A path, as it appears in prose or inside a link's parentheses. Deliberately
|
|
30
|
+
// no leading slash and no scheme: this is a path relative to the product, which
|
|
31
|
+
// is the only thing the app can resolve and open.
|
|
32
|
+
// The lookbehind is what keeps a url or an absolute path from being read as a
|
|
33
|
+
// relative one: without it "https://example.com/a.png" contributes
|
|
34
|
+
// "example.com/a.png" and "/etc/hosts.md" contributes "etc/hosts.md", and both
|
|
35
|
+
// would sit in her attachment row pointing at nothing.
|
|
36
|
+
const PATH = new RegExp(String.raw`(?<![\w./:@-])(?:\.\/)?[\w-][\w./-]*\.(?:${EXT})\b`, 'gi');
|
|
37
|
+
const LINK = new RegExp(String.raw`!?\[[^\]\n]*\]\(\s*([^)\s]+?)\s*(?:\s+"[^"]*")?\)`, 'g');
|
|
38
|
+
|
|
39
|
+
// THE WHOLE PATH, from the root, as a worker that pasted one writes it. Kept
|
|
40
|
+
// separate from PATH above because it is read by a different rule: a relative
|
|
41
|
+
// path is a reference on sight, an absolute one only when `dir` says it lands
|
|
42
|
+
// inside this product's own folder (see `take`).
|
|
43
|
+
//
|
|
44
|
+
// She was reading a card whose one document was written out in full,
|
|
45
|
+
// /Users/…/astral/designs//the-first-row.html. Everything the pane can do with
|
|
46
|
+
// a document hangs off this function: the chips at the foot of the message, the
|
|
47
|
+
// page drawn in a frame under it, the document it opens on its own, and the
|
|
48
|
+
// brief a worker is spawned with. All four were silent, because the path never
|
|
49
|
+
// got past the leading slash. Counted over her Agentbox store the same day: 14 of
|
|
50
|
+
// the 224 cards carrying any text name a file in full like this, and 8 of those
|
|
51
|
+
// mentions point at a file in her own project folder that she should have been
|
|
52
|
+
// able to open and could not.
|
|
53
|
+
//
|
|
54
|
+
// The other 10 are /tmp scratch and the app's own source, which is why the
|
|
55
|
+
// leading slash is not simply allowed. Inside her folder it is the document she
|
|
56
|
+
// was told to look at. Outside it, it is our machinery and stays plain text.
|
|
57
|
+
const ABS_PATH = new RegExp(String.raw`(?<![\w.:@/-])/[\w-][\w./-]*\.(?:${EXT})\b`, 'gi');
|
|
58
|
+
|
|
59
|
+
const ABSOLUTE = /^([a-z]+:|\/|#)/i;
|
|
60
|
+
|
|
61
|
+
const IMAGE = /\.(png|jpe?g|gif|webp)$/i;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Every file referenced across some pieces of a message, deduped, in order.
|
|
65
|
+
*
|
|
66
|
+
* Pass `{ dir }` last, the product's own folder, and a path written out in full
|
|
67
|
+
* from the root that lands inside it is read as the relative path it is. Every
|
|
68
|
+
* caller downstream keeps receiving relative paths and nothing else changes.
|
|
69
|
+
* Leave it out and only relative paths count, which is what this did before.
|
|
70
|
+
*/
|
|
71
|
+
export function referencedFiles(...texts) {
|
|
72
|
+
const last = texts[texts.length - 1];
|
|
73
|
+
const opts = last && typeof last === 'object' ? texts.pop() : null;
|
|
74
|
+
const dir = String(opts?.dir ?? '').replace(/\/+$/, '');
|
|
75
|
+
|
|
76
|
+
const found = [];
|
|
77
|
+
const seen = new Set();
|
|
78
|
+
|
|
79
|
+
const take = (raw) => {
|
|
80
|
+
if (!raw) return;
|
|
81
|
+
let path = raw.replace(/^\.\//, '').trim();
|
|
82
|
+
// Written out in full. It is one of ours only when it sits inside this
|
|
83
|
+
// product's folder, and then it is kept as the short path everything
|
|
84
|
+
// downstream already understands.
|
|
85
|
+
if (dir && path.startsWith(dir + '/')) path = path.slice(dir.length + 1);
|
|
86
|
+
if (!path || ABSOLUTE.test(path)) return;
|
|
87
|
+
// a link target that is not a file of ours (an id, a query) is not an
|
|
88
|
+
// attachment; the extension is what makes it one
|
|
89
|
+
if (!new RegExp(String.raw`\.(?:${EXT})$`, 'i').test(path)) return;
|
|
90
|
+
const key = path.toLowerCase();
|
|
91
|
+
if (seen.has(key)) return;
|
|
92
|
+
seen.add(key);
|
|
93
|
+
found.push(path);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
for (const text of texts) {
|
|
97
|
+
if (!text) continue;
|
|
98
|
+
// links first, so a target that also matches the bare-path scan is recorded
|
|
99
|
+
// once, in the order a reader meets it
|
|
100
|
+
const source = String(text);
|
|
101
|
+
const hits = [];
|
|
102
|
+
for (const m of source.matchAll(LINK)) hits.push({ at: m.index ?? 0, path: m[1] });
|
|
103
|
+
for (const m of source.matchAll(PATH)) hits.push({ at: m.index ?? 0, path: m[0] });
|
|
104
|
+
if (dir) for (const m of source.matchAll(ABS_PATH)) hits.push({ at: m.index ?? 0, path: m[0] });
|
|
105
|
+
hits.sort((a, b) => a.at - b.at);
|
|
106
|
+
for (const hit of hits) take(hit.path);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return found;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** What a chip says: the file's own name, with its folder as the quiet part. */
|
|
113
|
+
export function fileLabel(path) {
|
|
114
|
+
const parts = path.split('/');
|
|
115
|
+
const name = parts.pop() ?? path;
|
|
116
|
+
return { name, where: parts.join('/') };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Whether a referenced path is a picture, which is what a worker must OPEN. */
|
|
120
|
+
export function isImagePath(path) {
|
|
121
|
+
return IMAGE.test(String(path ?? ''));
|
|
122
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Types for shared/repeats.mjs, which is plain ESM so that main (Node) and the
|
|
2
|
+
// renderer (Vite) can share one copy of the scheduling rule.
|
|
3
|
+
|
|
4
|
+
// The SCHEDULE, apart from the rule that carries it: the composer and the reply
|
|
5
|
+
// box hand this around before any rule exists.
|
|
6
|
+
export interface RepeatShape {
|
|
7
|
+
every: 'day' | 'weekday' | 'week';
|
|
8
|
+
on?: number; // 0..6 when every is 'week'
|
|
9
|
+
at: string; // 24h local, "09:00"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface RepeatRule extends RepeatShape {
|
|
13
|
+
id: string;
|
|
14
|
+
title: string;
|
|
15
|
+
body?: string;
|
|
16
|
+
priority?: number;
|
|
17
|
+
createdAt: number;
|
|
18
|
+
updatedAt?: number;
|
|
19
|
+
endedAt?: number;
|
|
20
|
+
served: string;
|
|
21
|
+
misses: number;
|
|
22
|
+
alerted: number;
|
|
23
|
+
lastOccurrence?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const CLEAN_LABEL: string;
|
|
27
|
+
export const REPEAT_LABEL_PREFIX: string;
|
|
28
|
+
export function repeatLabel(ruleId: string): string;
|
|
29
|
+
export function ruleIdOf(item: { labels?: string[] } | null | undefined): string | null;
|
|
30
|
+
export function isCleanRun(item: { labels?: string[] } | null | undefined): boolean;
|
|
31
|
+
export const DAY_NAMES: string[];
|
|
32
|
+
export function isRepeatRule(rule: unknown): boolean;
|
|
33
|
+
export function ruleLabel(rule: unknown): string;
|
|
34
|
+
export function clockLabel(at: string): string;
|
|
35
|
+
export function dateKey(now?: number): string;
|
|
36
|
+
export function periodKey(rule: Partial<RepeatRule>, now?: number): string | null;
|
|
37
|
+
export function nextRunAt(rule: Partial<RepeatRule>, now?: number): number | null;
|
|
38
|
+
export function sameRule(a: unknown, b: unknown): boolean;
|
|
39
|
+
export function servedOf(rule: Partial<RepeatRule>): string;
|
|
40
|
+
export function isOwed(rule: Partial<RepeatRule>, now?: number): boolean;
|
|
41
|
+
export function occurrenceId(ruleId: string, key: string): string;
|
|
42
|
+
export function foldRepeats(
|
|
43
|
+
lines: Array<{ id: string; ts: number; patch: Record<string, unknown> } | null>,
|
|
44
|
+
now?: number,
|
|
45
|
+
): Map<string, RepeatRule>;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// Repeating tasks: the pure rules, shared by the main process and the renderer
|
|
2
|
+
// the way shared/rank.mjs is, so the tick and the list cannot come to disagree
|
|
3
|
+
// about when something is due.
|
|
4
|
+
//
|
|
5
|
+
// A repeating task is a RULE, not a work item. The spec
|
|
6
|
+
// (docs/superpowers/specs/2026-08-12-repeating-tasks-design.md) carries the six
|
|
7
|
+
// reasons; the short one is that a work-item ledger is read from a bounded
|
|
8
|
+
// trailing window and a template has to outlive that.
|
|
9
|
+
|
|
10
|
+
// A period is a local CALENDAR date, never `now - 86_400_000`, which is wrong by
|
|
11
|
+
// an hour twice a year. One date is one period, which is what makes DST, travel
|
|
12
|
+
// and clock correction boring here instead of a source of double runs.
|
|
13
|
+
const KEY_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
14
|
+
|
|
15
|
+
export const CLEAN_LABEL = 'clean';
|
|
16
|
+
export const REPEAT_LABEL_PREFIX = 'repeat:';
|
|
17
|
+
|
|
18
|
+
export function repeatLabel(ruleId) {
|
|
19
|
+
return `${REPEAT_LABEL_PREFIX}${ruleId}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function ruleIdOf(item) {
|
|
23
|
+
const found = (item?.labels ?? []).find((l) => typeof l === 'string' && l.startsWith(REPEAT_LABEL_PREFIX));
|
|
24
|
+
return found ? found.slice(REPEAT_LABEL_PREFIX.length) : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// A clean run is one the worker EXPLICITLY marked clean. Everything else is news
|
|
28
|
+
// and reaches her, including a run that died without saying anything: this
|
|
29
|
+
// predicate exists to hide, never to show, so it fails open by construction.
|
|
30
|
+
export function isCleanRun(item) {
|
|
31
|
+
return !!ruleIdOf(item) && (item?.labels ?? []).includes(CLEAN_LABEL);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const DAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
35
|
+
|
|
36
|
+
// Three shapes, because they are the three a founder actually asks for:
|
|
37
|
+
// { every: 'day', at } every morning
|
|
38
|
+
// { every: 'weekday', at } Monday to Friday, which is most work
|
|
39
|
+
// { every: 'week', on: 0..6, at } Thursdays
|
|
40
|
+
// The `every` field was an object from the start so these would be additions
|
|
41
|
+
// rather than a migration, and this is that.
|
|
42
|
+
export function isRepeatRule(rule) {
|
|
43
|
+
if (!rule) return false;
|
|
44
|
+
if (typeof rule.at !== 'string' || !/^([01]\d|2[0-3]):[0-5]\d$/.test(rule.at)) return false;
|
|
45
|
+
if (rule.every === 'day' || rule.every === 'weekday') return true;
|
|
46
|
+
if (rule.every === 'week') return Number.isInteger(rule.on) && rule.on >= 0 && rule.on <= 6;
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// the tag, the row and the picker's preview all print this, so they cannot
|
|
51
|
+
// drift.
|
|
52
|
+
export function ruleLabel(rule) {
|
|
53
|
+
if (!isRepeatRule(rule)) return 'Once';
|
|
54
|
+
const time = clockLabel(rule.at);
|
|
55
|
+
if (rule.every === 'day') return `daily at ${time}`;
|
|
56
|
+
if (rule.every === 'weekday') return `weekdays at ${time}`;
|
|
57
|
+
return `${DAY_NAMES[rule.on]}s at ${time}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function clockLabel(at) {
|
|
61
|
+
const [h, m] = at.split(':').map(Number);
|
|
62
|
+
const d = new Date();
|
|
63
|
+
d.setHours(h, m, 0, 0);
|
|
64
|
+
return d.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' })
|
|
65
|
+
.toLowerCase().replace(/\s/g, '');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Does this calendar date belong to the rule at all? A weekday rule skips the
|
|
69
|
+
// weekend and a weekly rule wants one day, which is the only thing that changes
|
|
70
|
+
// about the period: it is still a date, still one per rule, still never
|
|
71
|
+
// arithmetic.
|
|
72
|
+
function servesDay(rule, d) {
|
|
73
|
+
if (rule.every === 'day') return true;
|
|
74
|
+
if (rule.every === 'weekday') return d.getDay() >= 1 && d.getDay() <= 5;
|
|
75
|
+
return d.getDay() === rule.on;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function dateKey(now = Date.now()) {
|
|
79
|
+
const d = new Date(now);
|
|
80
|
+
const m = String(d.getMonth() + 1).padStart(2, '0');
|
|
81
|
+
const day = String(d.getDate()).padStart(2, '0');
|
|
82
|
+
return `${d.getFullYear()}-${m}-${day}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// The period `now` falls in: today's date once the hour has passed, yesterday's
|
|
86
|
+
// before it. Built with calendar operations so a clock change cannot shift it. A
|
|
87
|
+
// time that does not exist on a spring-forward date (02:30) normalizes to the
|
|
88
|
+
// first minute that does, which is exactly when that period should be served.
|
|
89
|
+
export function periodKey(rule, now = Date.now()) {
|
|
90
|
+
if (!isRepeatRule(rule)) return null;
|
|
91
|
+
const [h, m] = rule.at.split(':').map(Number);
|
|
92
|
+
const d = new Date(now);
|
|
93
|
+
const boundary = new Date(d.getFullYear(), d.getMonth(), d.getDate(), h, m, 0, 0);
|
|
94
|
+
if (boundary.getTime() > now) boundary.setDate(boundary.getDate() - 1);
|
|
95
|
+
// Walk back to the most recent day this rule actually serves. A week is the
|
|
96
|
+
// furthest that can ever be, so the loop is bounded and a rule whose day has
|
|
97
|
+
// not come round yet correctly reports the LAST one it served.
|
|
98
|
+
for (let back = 0; back < 8; back++) {
|
|
99
|
+
if (servesDay(rule, boundary)) return dateKey(boundary.getTime());
|
|
100
|
+
boundary.setDate(boundary.getDate() - 1);
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// The moment this rule next runs, for the Scheduled row.
|
|
106
|
+
export function nextRunAt(rule, now = Date.now()) {
|
|
107
|
+
if (!isRepeatRule(rule) || rule.endedAt) return null;
|
|
108
|
+
const [h, m] = rule.at.split(':').map(Number);
|
|
109
|
+
const d = new Date(now);
|
|
110
|
+
const next = new Date(d.getFullYear(), d.getMonth(), d.getDate(), h, m, 0, 0);
|
|
111
|
+
if (next.getTime() <= now) next.setDate(next.getDate() + 1);
|
|
112
|
+
for (let ahead = 0; ahead < 8; ahead++) {
|
|
113
|
+
if (servesDay(rule, next)) return next.getTime();
|
|
114
|
+
next.setDate(next.getDate() + 1);
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Two rules, same schedule? Used where typing drives the control: adopting a
|
|
120
|
+
// rule the words already say would otherwise fight her own pick every keystroke.
|
|
121
|
+
export function sameRule(a, b) {
|
|
122
|
+
if (!a || !b) return a === b;
|
|
123
|
+
return a.every === b.every && a.at === b.at && (a.on ?? null) === (b.on ?? null);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function servedOf(rule) {
|
|
127
|
+
return KEY_RE.test(rule?.served ?? '') ? rule.served : '';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function isOwed(rule, now = Date.now()) {
|
|
131
|
+
if (rule?.endedAt) return false;
|
|
132
|
+
const key = periodKey(rule, now);
|
|
133
|
+
return !!key && key > servedOf(rule);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Deterministic, so two writers racing describe the SAME item rather than two
|
|
137
|
+
// rows nothing can merge. Create-if-absent under the lock is what makes serving
|
|
138
|
+
// idempotent; this is what makes that check possible at all.
|
|
139
|
+
export function occurrenceId(ruleId, key) {
|
|
140
|
+
return `${ruleId}-${key.replace(/-/g, '')}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// `engine` and `model` are the LAST two and they are the reason this list is
|
|
144
|
+
// worth reading twice: a field written by `setRule` and missing from here is
|
|
145
|
+
// folded away, so it reads correctly once and is gone at the next app start.
|
|
146
|
+
// That is exactly how `on` disappeared off every weekly rule.
|
|
147
|
+
const FIELDS = [
|
|
148
|
+
'title', 'body', 'priority', 'every', 'at', 'on', 'engine', 'model',
|
|
149
|
+
'createdAt', 'endedAt', 'served', 'misses', 'alerted', 'lastOccurrence',
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
// Newest-wins per field, EXCEPT served, which folds by maximum period and
|
|
153
|
+
// refuses a date later than today. Same append-only shape as dashboard.jsonl,
|
|
154
|
+
// and the same reason: several writers, no read-modify-write, nothing lost.
|
|
155
|
+
//
|
|
156
|
+
// The two exceptions are one guard each against a watermark that lies. A
|
|
157
|
+
// delayed or clock-skewed writer landing an older date last would re-run a
|
|
158
|
+
// period already served, so the maximum wins rather than the latest line. And a
|
|
159
|
+
// date in the FUTURE cannot be legitimate (a period key is never later than
|
|
160
|
+
// today), so it is rejected outright: accepted, it would silence the task until
|
|
161
|
+
// the real calendar caught up, which could be years, and no later correct write
|
|
162
|
+
// could undo it because the maximum would keep it forever.
|
|
163
|
+
export function foldRepeats(lines, now = Date.now()) {
|
|
164
|
+
const today = dateKey(now);
|
|
165
|
+
const rules = new Map();
|
|
166
|
+
for (const line of lines) {
|
|
167
|
+
if (!line || typeof line !== 'object') continue;
|
|
168
|
+
const { id, ts, patch } = line;
|
|
169
|
+
if (typeof id !== 'string' || !id || !patch || typeof patch !== 'object') continue;
|
|
170
|
+
if (!Number.isFinite(ts)) continue;
|
|
171
|
+
let rule = rules.get(id);
|
|
172
|
+
if (!rule) {
|
|
173
|
+
rule = { id, createdAt: ts, updatedAt: ts, served: '', misses: 0, alerted: 0, _ts: {} };
|
|
174
|
+
rules.set(id, rule);
|
|
175
|
+
}
|
|
176
|
+
for (const field of FIELDS) {
|
|
177
|
+
if (!(field in patch)) continue;
|
|
178
|
+
const value = patch[field];
|
|
179
|
+
if (field === 'served') {
|
|
180
|
+
if (KEY_RE.test(value ?? '') && value <= today && value > rule.served) rule.served = value;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const held = rule._ts[field];
|
|
184
|
+
if (held !== undefined && ts < held) continue;
|
|
185
|
+
rule[field] = value;
|
|
186
|
+
rule._ts[field] = ts;
|
|
187
|
+
}
|
|
188
|
+
rule.createdAt = Math.min(rule.createdAt, ts);
|
|
189
|
+
rule.updatedAt = Math.max(rule.updatedAt, ts);
|
|
190
|
+
}
|
|
191
|
+
for (const rule of rules.values()) delete rule._ts;
|
|
192
|
+
return rules;
|
|
193
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// THE APP'S TWO PRIVATE URL SCHEMES, NAMED FROM THE ONE PLACE THE APP IS NAMED.
|
|
2
|
+
//
|
|
3
|
+
// These were typed out as `astral-doc` and `astral-img`, two renames after the
|
|
4
|
+
// app stopped being called that, in main, in the renderer and in the tests. An
|
|
5
|
+
// agent reading any of them learns the wrong name for the app it is working
|
|
6
|
+
// inside, which is the fault the founder opened the row about, and the next
|
|
7
|
+
// rename would have had to find them all by hand.
|
|
8
|
+
//
|
|
9
|
+
// EVERY OLD SPELLING IS STILL SERVED, AND THAT IS NOT POLITENESS. A url on one
|
|
10
|
+
// of these schemes is STORED: a product's logo is kept in the store as
|
|
11
|
+
// `<scheme>://file/…` and a message she has already been sent holds picture
|
|
12
|
+
// urls in its body. Dropping the old scheme would not degrade those, it would
|
|
13
|
+
// break them, and the symptom is a broken image in a row she wrote months ago.
|
|
14
|
+
// So the current name is what new urls are built with, and every name this app
|
|
15
|
+
// has had is still registered and still answered.
|
|
16
|
+
|
|
17
|
+
import { WAS, nameSlug } from './product-name.mjs';
|
|
18
|
+
|
|
19
|
+
const under = (was) => was.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
20
|
+
|
|
21
|
+
/** What a url built today is on. */
|
|
22
|
+
export const DOC_SCHEME = `${nameSlug}-doc`;
|
|
23
|
+
export const IMG_SCHEME = `${nameSlug}-img`;
|
|
24
|
+
|
|
25
|
+
/** Every spelling that must still resolve, current name first. */
|
|
26
|
+
export const DOC_SCHEMES = Object.freeze([DOC_SCHEME, ...WAS.map((was) => `${under(was)}-doc`)]);
|
|
27
|
+
export const IMG_SCHEMES = Object.freeze([IMG_SCHEME, ...WAS.map((was) => `${under(was)}-img`)]);
|
|
28
|
+
|
|
29
|
+
/** Both, for the one place that registers all of them at once. */
|
|
30
|
+
export const ALL_SCHEMES = Object.freeze([...DOC_SCHEMES, ...IMG_SCHEMES]);
|
|
31
|
+
|
|
32
|
+
/** True when `protocol` (as `URL` gives it, with its colon) is one of them. */
|
|
33
|
+
export const isScheme = (schemes, protocol) => schemes.some((s) => `${s}:` === protocol);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const SETTINGS_TERMINAL: { product: string; id: string };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// THE ONE TERMINAL THAT BELONGS TO NO TASK.
|
|
2
|
+
//
|
|
3
|
+
// Every other shell in this app is opened against a work item, and
|
|
4
|
+
// `terminalFolder` refuses a key whose item does not exist. Signing a second
|
|
5
|
+
// Codex account in needs a shell and has no item, so it gets this one reserved
|
|
6
|
+
// key, and main resolves it to the folder main itself just made rather than to
|
|
7
|
+
// anything the window asked for.
|
|
8
|
+
//
|
|
9
|
+
// THE PRODUCT WORD CARRIES A COLON ON PURPOSE. A key is `{product, id}`, and a
|
|
10
|
+
// product here is a slug taken from a folder name; a colon never survives that,
|
|
11
|
+
// so no project anybody creates can collide with this and be handed a shell
|
|
12
|
+
// pointed somewhere else. Both sides import this rather than spelling it,
|
|
13
|
+
// because the whole value of a reserved key is that there is one of it.
|
|
14
|
+
export const SETTINGS_TERMINAL = { product: 'zero:settings', id: 'codex-login' };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// A SIDE BUILD IS A SECOND BUNDLE AND IT IS NOT A SECOND AGENTBOX.
|
|
2
|
+
//
|
|
3
|
+
// 2026-08-27.
|
|
4
|
+
//
|
|
5
|
+
// The bundle is separate. The DATA FOLDER MUST NOT BE, and this file is the one
|
|
6
|
+
// place that says so, because two files saying it is how one of them goes
|
|
7
|
+
// quietly wrong.
|
|
8
|
+
//
|
|
9
|
+
// Why it matters, in main.mjs's own words at the single-instance lock: "Two
|
|
10
|
+
// instances mean two supervisors, and two supervisors spawn twin workers for
|
|
11
|
+
// every answered item (observed 2026-08-04: nine items double-spawned one
|
|
12
|
+
// second apart, four agents told the founder the same thing)." That lock lives
|
|
13
|
+
// INSIDE the data folder — measured on this Mac 2026-08-27, the running Agentbox
|
|
14
|
+
// holds SingletonLock in ~/Library/Application Support/Astral — so two folders
|
|
15
|
+
// would be two locks and the protection would simply not be there.
|
|
16
|
+
//
|
|
17
|
+
// Sharing the folder also means the side build is not a stranger's copy: her
|
|
18
|
+
// store, her settings, her theme, her place in everything, plus the branch.
|
|
19
|
+
|
|
20
|
+
import { NAME } from '../shared/product-name.mjs';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* THE BUNDLE NAME, not a sentence, so it is the plain spelling however the
|
|
24
|
+
* name is capitalised. Everything here compares against what macOS calls the
|
|
25
|
+
* bundle, which is `productName` in package.json, which is this. */
|
|
26
|
+
export const REAL_NAME = NAME;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The app's own name and a word after it, yes. The bare name itself, no, that
|
|
30
|
+
* is the real one. A bare word, or the name run into a word with no space, no:
|
|
31
|
+
* a name that does not read as the app plus a word is a name
|
|
32
|
+
* nobody will recognise in the Dock, and pack-side refuses it rather than
|
|
33
|
+
* quietly building a second supervisor. */
|
|
34
|
+
export function isSideBuild(name) {
|
|
35
|
+
const word = REAL_NAME.replace(/[.*+?^${}()|[\]\\]/g, (c) => `\\${c}`);
|
|
36
|
+
return new RegExp(`^${word} .+`).test(name ?? '');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// THE ONE SIDE BUILD THAT MUST NOT SHARE HER FOLDER.
|
|
40
|
+
//
|
|
41
|
+
// That folder has to be its OWN, or the hand-over would take the real
|
|
42
|
+
// Agentbox's single-instance lock and bring her inbox to the front instead of
|
|
43
|
+
// opening a stranger's.
|
|
44
|
+
export const NEW_USER_NAME = `${REAL_NAME} new user`;
|
|
45
|
+
|
|
46
|
+
export function isNewUserBuild(name) {
|
|
47
|
+
return name === NEW_USER_NAME;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The folder Chromium should name after this app, whatever the bundle is
|
|
52
|
+
* called. Everything downstream keys off app.getName, so a side build sets
|
|
53
|
+
* its name back to this before anything reads a path. */
|
|
54
|
+
export function dataFolderName(name) {
|
|
55
|
+
if (isNewUserBuild(name)) return name;
|
|
56
|
+
return isSideBuild(name) ? REAL_NAME : name;
|
|
57
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Types for shared/spawn-trouble.mjs, which is plain ESM so that the
|
|
2
|
+
// supervisor (Node) and the renderer (Vite) share one copy of every sentence
|
|
3
|
+
// this app is allowed to say about a task that could not run.
|
|
4
|
+
//
|
|
5
|
+
// Only what the renderer imports is declared. The rest of the module is read by
|
|
6
|
+
// main and by the tests, which are both plain JS and need nothing here.
|
|
7
|
+
|
|
8
|
+
export function strandedTitle(count: number): string;
|