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,481 @@
|
|
|
1
|
+
// The dashboard: the company's real state as one folded picture.
|
|
2
|
+
//
|
|
3
|
+
// This module is the PURE core, zero-dep ESM so main (the ledger store, the
|
|
4
|
+
// agent tools) and the renderer (the page) share exactly one set of rules. No
|
|
5
|
+
// filesystem, no network, no React, no Date.now except as an injected default,
|
|
6
|
+
// so every rule below is unit-testable without a DOM or a disk.
|
|
7
|
+
//
|
|
8
|
+
// Two halves, and the split is the whole design:
|
|
9
|
+
//
|
|
10
|
+
// DERIVED in-flight work, artifacts, folders, analytics numbers, how many
|
|
11
|
+
// days in. Computed at read time from state that already exists.
|
|
12
|
+
// Typed status rots; earned status stays true.
|
|
13
|
+
//
|
|
14
|
+
// WRITTEN the judgments a derivation cannot make: the one-liner, where the
|
|
15
|
+
// company is on the path, the bottleneck, the next move, which
|
|
16
|
+
// metrics matter. Appended to a ledger as partial patches by any
|
|
17
|
+
// agent (or the founder), folded newest-wins per field.
|
|
18
|
+
//
|
|
19
|
+
// The sharpest constraint in the whole feature, worth restating where the code
|
|
20
|
+
// lives: any pre-launch score that can be improved WITHOUT moving toward launch
|
|
21
|
+
// will be improved without moving toward launch. That is why the pre-launch
|
|
22
|
+
// headline is "steps to launch", which counts down and is derived from the
|
|
23
|
+
// path, rather than anything a founder can raise by interviewing forever.
|
|
24
|
+
|
|
25
|
+
/* ------------------------------- the stages ------------------------------ */
|
|
26
|
+
// Two states of one page, never two products. Pre-launch adds the path and the
|
|
27
|
+
// proportion pips; launched retires them and real metrics take over.). A
|
|
28
|
+
// founder who flips their own stage early is not a failure mode worth
|
|
29
|
+
// machinery.
|
|
30
|
+
export const STAGES = ['prelaunch', 'launched'];
|
|
31
|
+
|
|
32
|
+
/* -------------------------------- the path ------------------------------- */
|
|
33
|
+
// The default road to launch. Six steps, plain words, ending at the door: the
|
|
34
|
+
// point is showing it to people you do not know. An agent may replace this
|
|
35
|
+
// wholesale for a company these steps do not fit (a physical product, an
|
|
36
|
+
// existing business adding a product), which is why it is a ledger field and
|
|
37
|
+
// not a hard-coded ladder.
|
|
38
|
+
//
|
|
39
|
+
// Every step's `note` is a fact or a condition, never a target and never a
|
|
40
|
+
// countdown: comparison is motivating, prescription is not.
|
|
41
|
+
export const DEFAULT_PATH = [
|
|
42
|
+
{ name: 'Describe it', status: 'here', note: '' },
|
|
43
|
+
{ name: 'Meet your customers', status: 'ahead', note: '' },
|
|
44
|
+
{ name: 'Talk to them', status: 'ahead', note: '' },
|
|
45
|
+
{ name: 'Build it', status: 'ahead', note: '' },
|
|
46
|
+
{ name: 'Put it online, take payment', status: 'ahead', note: '' },
|
|
47
|
+
{ name: "Show it to people you don't know", status: 'ahead', note: 'the whole point' },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
export const STEP_STATUSES = ['done', 'here', 'ahead'];
|
|
51
|
+
|
|
52
|
+
// The last step is the door: it renders as wanted rather than as locked (full
|
|
53
|
+
// strength label, a lamp ring waiting). Position, not a flag on the data, so a
|
|
54
|
+
// replaced path gets the same treatment for free.
|
|
55
|
+
export function isDoor(path, index) {
|
|
56
|
+
return index === (path?.length ?? 0) - 1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// How many steps remain, the pre-launch headline. Counts every step that is not
|
|
60
|
+
// done, so standing still cannot improve it and finishing one always does.
|
|
61
|
+
export function stepsToLaunch(path) {
|
|
62
|
+
const steps = Array.isArray(path) ? path : [];
|
|
63
|
+
return steps.filter((s) => s?.status !== 'done').length;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* ------------------------------- the metrics ----------------------------- */
|
|
67
|
+
// A metric is one of three things, and the difference between the second and
|
|
68
|
+
// the third is a load-bearing honesty rule:
|
|
69
|
+
//
|
|
70
|
+
// a VALUE a real number we measured. `0` is a real number.
|
|
71
|
+
// ABSENT we are not collecting this. Says so, and says what would make
|
|
72
|
+
// it start. Renders over a dashed baseline.
|
|
73
|
+
// a PROPORTION "4 of 6", pre-launch, drawn as pips rather than a time series
|
|
74
|
+
// because the answer is out of six, not over days.
|
|
75
|
+
//
|
|
76
|
+
// Absence of data and a real zero are DIFFERENT FACTS and must never look the
|
|
77
|
+
// same. A page that draws "not measured" as a confident 0 is lying, and this
|
|
78
|
+
// product's whole claim is that its numbers are not lying.
|
|
79
|
+
export function makeMetric(input = {}) {
|
|
80
|
+
const value = Number.isFinite(input.value) ? input.value : null;
|
|
81
|
+
const of = Number.isFinite(input.of) ? input.of : null;
|
|
82
|
+
return {
|
|
83
|
+
id: String(input.id ?? '').trim() || slug(input.label),
|
|
84
|
+
label: String(input.label ?? '').trim(),
|
|
85
|
+
value,
|
|
86
|
+
of,
|
|
87
|
+
// A currency mark, and the only reason it exists: a big bare "1,240" reads
|
|
88
|
+
// as a count of things, not as money. Kept to a mark rather than a format
|
|
89
|
+
// so the number itself stays the number.
|
|
90
|
+
prefix: String(input.prefix ?? '').trim().slice(0, 4),
|
|
91
|
+
// the italic line beneath the number: what the number MEANS, in plain words
|
|
92
|
+
unit: String(input.unit ?? '').trim(),
|
|
93
|
+
// when there is no value: the short honest phrase ("not measured", "not
|
|
94
|
+
// yet") and the line that says what would fill it. Never a loading state.
|
|
95
|
+
absent: value == null ? (String(input.absent ?? '').trim() || 'not measured') : null,
|
|
96
|
+
fills: value == null ? String(input.fills ?? '').trim() : '',
|
|
97
|
+
series: Array.isArray(input.series) ? input.series.filter((n) => Number.isFinite(n)) : null,
|
|
98
|
+
source: input.source ?? null,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Which drawing a metric gets. A proportion is pips; anything else with history
|
|
103
|
+
// is a sparkline; anything else is the dashed absent baseline. Pure, so the
|
|
104
|
+
// component never decides this itself and the tests can pin it.
|
|
105
|
+
export function metricShape(metric) {
|
|
106
|
+
if (!metric) return 'absent';
|
|
107
|
+
if (metric.of != null) return 'pips';
|
|
108
|
+
if (metric.value == null) return 'absent';
|
|
109
|
+
// A series that is zero every day is drawn as a real LINE on a real baseline,
|
|
110
|
+
// not as a row of bars with no height. A flat line reads as a measurement;
|
|
111
|
+
// the dashed baseline reads as an absence.
|
|
112
|
+
if (metric.series && metric.series.length) {
|
|
113
|
+
return metric.series.some((n) => n > 0) ? 'series' : 'flat';
|
|
114
|
+
}
|
|
115
|
+
return 'flat';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// True when this metric is honestly measured. `0` is measured; null is not.
|
|
119
|
+
export function isMeasured(metric) {
|
|
120
|
+
return !!metric && metric.value != null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// `drillEvent` lived here: it answered which event a number could be opened
|
|
124
|
+
// into, and it did that by asking whether the metric's source kind was the
|
|
125
|
+
// other product's name. Removed on 2026-09-22 with the rest of that name. It
|
|
126
|
+
// had no callers in main, shared, renderer, tests or scripts, and nothing in
|
|
127
|
+
// this file ever wrote that kind (the only one written is 'derived'), so it
|
|
128
|
+
// returned null for every input it could ever have been given.
|
|
129
|
+
|
|
130
|
+
/* ------------------------------- the ledger ------------------------------ */
|
|
131
|
+
// Append-only, one JSON object per line:
|
|
132
|
+
// { ts, source: 'agent' | 'founder', chatId?, patch: { <field>: value } }
|
|
133
|
+
//
|
|
134
|
+
// Several conversations run at once and all of them may want to write, which is
|
|
135
|
+
// exactly why this is append-only: two chats appending concurrently cannot
|
|
136
|
+
// clobber each other the way two read-modify-writes of a single document would.
|
|
137
|
+
// Folding is newest-wins per TOP-LEVEL field, so a chat that only knows the
|
|
138
|
+
// next move never has to restate the one-liner to avoid erasing it.
|
|
139
|
+
export const LEDGER_FIELDS = [
|
|
140
|
+
'founderName', 'oneLiner', 'stage', 'path', 'bottleneck', 'nextMove',
|
|
141
|
+
'metrics', 'keyMetricId', 'analytics',
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
export const LEDGER_SOURCES = ['agent', 'founder'];
|
|
145
|
+
|
|
146
|
+
// THE FOUNDER'S WORD WINS.). Authority is per FIELD and only over fields the
|
|
147
|
+
// founder actually touched, so pinning the headline metric never freezes the
|
|
148
|
+
// agent out of updating the metric values themselves. A newer founder patch
|
|
149
|
+
// always beats an older founder patch: changing your mind is allowed.
|
|
150
|
+
export function foldLedger(lines, now = Date.now()) {
|
|
151
|
+
const picture = defaultDashboard(now);
|
|
152
|
+
const winner = new Map(); // field -> { ts, source }
|
|
153
|
+
|
|
154
|
+
for (const line of Array.isArray(lines) ? lines : []) {
|
|
155
|
+
const entry = normalizeLine(line);
|
|
156
|
+
if (!entry) continue; // a malformed line is skipped, never fatal
|
|
157
|
+
for (const [field, value] of Object.entries(entry.patch)) {
|
|
158
|
+
if (!LEDGER_FIELDS.includes(field)) continue;
|
|
159
|
+
const held = winner.get(field);
|
|
160
|
+
if (held && !beats(entry, held)) continue;
|
|
161
|
+
const clean = coerceField(field, value);
|
|
162
|
+
if (clean === undefined) continue; // a field that failed validation keeps the older value
|
|
163
|
+
picture[field] = clean;
|
|
164
|
+
winner.set(field, { ts: entry.ts, source: entry.source });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// who last spoke for each field, so the page can show the founder that their
|
|
169
|
+
// own choice is the one in force rather than silently agreeing with them
|
|
170
|
+
picture.authored = Object.fromEntries([...winner].map(([f, w]) => [f, w.source]));
|
|
171
|
+
return picture;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Does the incoming entry outrank the one already holding this field?
|
|
175
|
+
function beats(incoming, held) {
|
|
176
|
+
if (held.source === 'founder' && incoming.source !== 'founder') return false;
|
|
177
|
+
if (incoming.source === 'founder' && held.source !== 'founder') return true;
|
|
178
|
+
return incoming.ts >= held.ts;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// One ledger line, defensively read. Anything unparseable returns null so a
|
|
182
|
+
// corrupt ledger degrades to an older picture rather than a broken page.
|
|
183
|
+
export function normalizeLine(line) {
|
|
184
|
+
let raw = line;
|
|
185
|
+
if (typeof raw === 'string') {
|
|
186
|
+
const text = raw.trim();
|
|
187
|
+
if (!text) return null;
|
|
188
|
+
try { raw = JSON.parse(text); } catch { return null; }
|
|
189
|
+
}
|
|
190
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
191
|
+
const patch = raw.patch;
|
|
192
|
+
if (!patch || typeof patch !== 'object' || Array.isArray(patch)) return null;
|
|
193
|
+
const ts = Number.isFinite(raw.ts) ? raw.ts : 0;
|
|
194
|
+
const source = LEDGER_SOURCES.includes(raw.source) ? raw.source : 'agent';
|
|
195
|
+
return { ts, source, chatId: raw.chatId ?? null, patch };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Per-field validation. Returning undefined means "this patch had nothing
|
|
199
|
+
// usable for this field", and the previous value stands: a garbled write can
|
|
200
|
+
// never blank a good picture.
|
|
201
|
+
function coerceField(field, value) {
|
|
202
|
+
switch (field) {
|
|
203
|
+
case 'founderName':
|
|
204
|
+
// What to call them, as THEY said it. Never derived from an email local
|
|
205
|
+
// part: that produced "Morning, Sfeder." on the founder's own screen
|
|
206
|
+
// (2026-07-27) from a git-style handle, and a name is exactly the thing
|
|
207
|
+
// you do not guess. Absent, the page greets without a name, which is
|
|
208
|
+
// warm and correct; wrong, it is neither.
|
|
209
|
+
return typeof value === 'string' && value.trim() ? clean(value).slice(0, 40) : undefined;
|
|
210
|
+
|
|
211
|
+
case 'oneLiner':
|
|
212
|
+
return typeof value === 'string' && value.trim() ? clean(value) : undefined;
|
|
213
|
+
|
|
214
|
+
case 'stage':
|
|
215
|
+
return STAGES.includes(value) ? value : undefined;
|
|
216
|
+
|
|
217
|
+
case 'keyMetricId':
|
|
218
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
219
|
+
|
|
220
|
+
case 'path': {
|
|
221
|
+
if (!Array.isArray(value) || !value.length) return undefined;
|
|
222
|
+
const steps = value
|
|
223
|
+
.filter((s) => s && typeof s === 'object' && String(s.name ?? '').trim())
|
|
224
|
+
.map((s) => ({
|
|
225
|
+
name: clean(s.name),
|
|
226
|
+
status: STEP_STATUSES.includes(s.status) ? s.status : 'ahead',
|
|
227
|
+
note: clean(s.note ?? ''),
|
|
228
|
+
}));
|
|
229
|
+
return steps.length ? steps : undefined;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
case 'bottleneck':
|
|
233
|
+
case 'nextMove': {
|
|
234
|
+
if (value === null) return null; // an explicit clear is legitimate
|
|
235
|
+
if (!value || typeof value !== 'object') return undefined;
|
|
236
|
+
const body = clean(value.body ?? '');
|
|
237
|
+
if (!body) return undefined;
|
|
238
|
+
return { body, foot: clean(value.foot ?? '') };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
case 'metrics': {
|
|
242
|
+
if (!Array.isArray(value)) return undefined;
|
|
243
|
+
const metrics = value
|
|
244
|
+
.filter((m) => m && typeof m === 'object' && String(m.label ?? '').trim())
|
|
245
|
+
.map((m) => makeMetric({ ...m, label: clean(m.label), unit: clean(m.unit ?? ''), fills: clean(m.fills ?? ''), absent: clean(m.absent ?? '') }));
|
|
246
|
+
return metrics.length ? metrics : undefined;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
case 'analytics': {
|
|
250
|
+
if (value === null) return null;
|
|
251
|
+
if (!value || typeof value !== 'object') return undefined;
|
|
252
|
+
const provider = String(value.provider ?? '').trim();
|
|
253
|
+
if (!provider) return undefined;
|
|
254
|
+
return {
|
|
255
|
+
provider,
|
|
256
|
+
projectId: value.projectId != null ? String(value.projectId) : null,
|
|
257
|
+
host: typeof value.host === 'string' && value.host.trim() ? value.host.trim() : null,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
default:
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// The picture before anyone has written anything: a brand new company on step
|
|
267
|
+
// one.
|
|
268
|
+
export function defaultDashboard(now = Date.now()) {
|
|
269
|
+
return {
|
|
270
|
+
stage: 'prelaunch',
|
|
271
|
+
founderName: null,
|
|
272
|
+
oneLiner: null,
|
|
273
|
+
path: DEFAULT_PATH.map((s) => ({ ...s })),
|
|
274
|
+
bottleneck: null,
|
|
275
|
+
nextMove: null,
|
|
276
|
+
metrics: [],
|
|
277
|
+
keyMetricId: null,
|
|
278
|
+
analytics: null,
|
|
279
|
+
authored: {},
|
|
280
|
+
now,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/* ------------------------- the metric set on screen ---------------------- */
|
|
285
|
+
// One large number on the left, three to its right. Which one is large is the
|
|
286
|
+
// founder's if they said so, otherwise the first the agent listed, otherwise
|
|
287
|
+
// the derived pre-launch headline.
|
|
288
|
+
//
|
|
289
|
+
// Pre-launch ALWAYS has "steps to launch" available as the headline even when
|
|
290
|
+
// no agent has written a single metric, because the page has to be honest and
|
|
291
|
+
// useful on day one, and that number is derived from the path so it cannot be
|
|
292
|
+
// gamed.
|
|
293
|
+
export function metricRow(picture, { others = 3 } = {}) {
|
|
294
|
+
// NO PICTURE IS NOT A COMPANY. Day one is `defaultDashboard`, which has the
|
|
295
|
+
// six-step path and therefore a true headline; a null picture means the
|
|
296
|
+
// ledger has not been read yet (on the web the brain is a sandbox that takes
|
|
297
|
+
// 30-40 seconds to wake, and the workspace paints from cache meanwhile).
|
|
298
|
+
// Deriving the headline from an absent path made those seconds print a
|
|
299
|
+
// confident "0" over "nothing left before launch", which told the founder her
|
|
300
|
+
// company had launched (report 2026-08-06). Invent nothing; the page draws
|
|
301
|
+
// its loading state from this emptiness.
|
|
302
|
+
if (!picture) return { key: null, others: [] };
|
|
303
|
+
const metrics = [...(picture.metrics ?? [])];
|
|
304
|
+
const prelaunch = picture?.stage !== 'launched';
|
|
305
|
+
|
|
306
|
+
if (prelaunch && !metrics.some((m) => m.id === 'steps-to-launch')) {
|
|
307
|
+
metrics.unshift(stepsToLaunchMetric(picture?.path ?? []));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
let keyIndex = 0;
|
|
311
|
+
if (picture?.keyMetricId) {
|
|
312
|
+
const found = metrics.findIndex((m) => m.id === picture.keyMetricId);
|
|
313
|
+
if (found >= 0) keyIndex = found;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const key = metrics[keyIndex] ?? null;
|
|
317
|
+
const rest = metrics.filter((_, i) => i !== keyIndex).slice(0, others);
|
|
318
|
+
return { key, others: rest };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// The derived pre-launch headline. Its unit line NAMES the remaining steps, so
|
|
322
|
+
// the number is never a bare score: it says what it would take to move it.
|
|
323
|
+
export function stepsToLaunchMetric(path) {
|
|
324
|
+
const steps = Array.isArray(path) ? path : [];
|
|
325
|
+
const remaining = steps.filter((s) => s?.status !== 'done');
|
|
326
|
+
const names = remaining.map((s) => lowerFirst(s.name));
|
|
327
|
+
const unit = names.length === 0
|
|
328
|
+
? 'nothing left before launch.'
|
|
329
|
+
: names.length === 1
|
|
330
|
+
? `${names[0]}.`
|
|
331
|
+
: `${names.slice(0, -1).join(', ')}, then ${names[names.length - 1]}.`;
|
|
332
|
+
return makeMetric({
|
|
333
|
+
id: 'steps-to-launch',
|
|
334
|
+
label: 'Steps to launch',
|
|
335
|
+
value: remaining.length,
|
|
336
|
+
unit,
|
|
337
|
+
source: { kind: 'derived', of: 'path' },
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// The pips under a pre-launch figure: a proportion, not a series. The path's
|
|
342
|
+
// own pips mark where you are with the lamp; a plain proportion has no "now".
|
|
343
|
+
export function pipsFor(metric, path) {
|
|
344
|
+
if (metric?.id === 'steps-to-launch') {
|
|
345
|
+
const steps = Array.isArray(path) ? path : [];
|
|
346
|
+
return steps.map((s, i) => {
|
|
347
|
+
if (s?.status === 'done') return 'on';
|
|
348
|
+
if (s?.status === 'here') return 'now';
|
|
349
|
+
// the first not-done step counts as where you are when nothing says 'here'
|
|
350
|
+
const firstOpen = steps.findIndex((x) => x?.status !== 'done');
|
|
351
|
+
return i === firstOpen && !steps.some((x) => x?.status === 'here') ? 'now' : 'off';
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
if (metric?.of == null) return [];
|
|
355
|
+
const total = Math.max(0, Math.round(metric.of));
|
|
356
|
+
const filled = Math.max(0, Math.min(total, Math.round(metric.value ?? 0)));
|
|
357
|
+
return Array.from({ length: total }, (_, i) => (i < filled ? 'on' : 'off'));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/* ------------------------------ in flight -------------------------------- */
|
|
361
|
+
// Four glyphs and no more, because the founder-facing question is only ever one
|
|
362
|
+
// of four: does it need me, is it moving, did it stall, is it ready.
|
|
363
|
+
//
|
|
364
|
+
// wait a conversation is holding a question for you
|
|
365
|
+
// run a turn is working right now, or a dev server is up
|
|
366
|
+
// stop something was running and is not any more
|
|
367
|
+
// done finished and you have not looked at it yet
|
|
368
|
+
//
|
|
369
|
+
// Everything here is DERIVED from state the app already keeps and never showed
|
|
370
|
+
// on this page: the per-chat status ledger and the runtime map. The old home's
|
|
371
|
+
// "in progress" lamp was not liveness at all and does not survive into this
|
|
372
|
+
// grid.
|
|
373
|
+
export const GLYPHS = ['wait', 'run', 'stop', 'done'];
|
|
374
|
+
|
|
375
|
+
// Order on screen: what needs you, then what is moving, then what stopped, then
|
|
376
|
+
// what is ready. Never a drought counter, never a scolding; a stalled item says
|
|
377
|
+
// when it stopped and nothing more.
|
|
378
|
+
const GLYPH_RANK = { wait: 0, run: 1, stop: 2, done: 3 };
|
|
379
|
+
|
|
380
|
+
export function deriveInFlight({ chats = [], chatStatus = {}, runtime = {}, creations = [] } = {}, now = Date.now()) {
|
|
381
|
+
const items = [];
|
|
382
|
+
|
|
383
|
+
for (const chat of chats) {
|
|
384
|
+
const st = chatStatus?.[chat?.id];
|
|
385
|
+
if (!st) continue;
|
|
386
|
+
if (st.askPending) {
|
|
387
|
+
items.push({ key: `chat:${chat.id}`, chatId: chat.id, name: chat.title || 'Untitled', glyph: 'wait', state: 'waiting on you', detail: '', since: st.askSince ?? null });
|
|
388
|
+
} else if (st.working) {
|
|
389
|
+
items.push({ key: `chat:${chat.id}`, chatId: chat.id, name: chat.title || 'Untitled', glyph: 'run', state: 'working', detail: elapsed(st.workingSince, now), since: st.workingSince ?? null });
|
|
390
|
+
} else if (st.doneAt && !st.readAt) {
|
|
391
|
+
items.push({ key: `chat:${chat.id}`, chatId: chat.id, name: chat.title || 'Untitled', glyph: 'done', state: 'ready', detail: when(st.doneAt, now), since: st.doneAt });
|
|
392
|
+
} else if (st.cutAt) {
|
|
393
|
+
// interrupted by a sleep or a restart and never picked back up
|
|
394
|
+
items.push({ key: `chat:${chat.id}`, chatId: chat.id, name: chat.title || 'Untitled', glyph: 'stop', state: 'stopped', detail: when(st.cutAt, now), since: st.cutAt });
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const titleOf = (id) => creations.find((c) => c?.id === id)?.title;
|
|
399
|
+
for (const [creationId, rt] of Object.entries(runtime ?? {})) {
|
|
400
|
+
const name = titleOf(creationId);
|
|
401
|
+
if (!name) continue; // a runtime whose creation is gone is not a thing to show
|
|
402
|
+
if (rt?.status === 'running' || rt?.status === 'starting') {
|
|
403
|
+
items.push({ key: `run:${creationId}`, creationId, name, glyph: 'run', state: rt.status === 'starting' ? 'starting' : 'running', detail: rt.port ? `port ${rt.port}` : '', since: null });
|
|
404
|
+
} else if (rt?.status === 'error') {
|
|
405
|
+
items.push({ key: `run:${creationId}`, creationId, name, glyph: 'stop', state: 'stopped', detail: 'it errored', since: null });
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
items.sort((a, b) => (GLYPH_RANK[a.glyph] - GLYPH_RANK[b.glyph]) || (b.since ?? 0) - (a.since ?? 0));
|
|
410
|
+
return items;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/* --------------------------------- time ---------------------------------- */
|
|
414
|
+
// Elapsed, for something happening right now. Minutes until an hour, then
|
|
415
|
+
// hours: a working turn measured in seconds reads as a stopwatch, which invites
|
|
416
|
+
// watching it rather than doing something else.
|
|
417
|
+
export function elapsed(since, now = Date.now()) {
|
|
418
|
+
if (!Number.isFinite(since) || since <= 0) return '';
|
|
419
|
+
const mins = Math.floor((now - since) / 60_000);
|
|
420
|
+
if (mins < 1) return 'just now';
|
|
421
|
+
if (mins < 60) return `${mins} min`;
|
|
422
|
+
const hours = Math.floor(mins / 60);
|
|
423
|
+
return hours < 24 ? `${hours}h` : `${Math.floor(hours / 24)}d`;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// When something happened, in the same words the rest of the app uses.
|
|
427
|
+
export function when(ts, now = Date.now()) {
|
|
428
|
+
if (!Number.isFinite(ts) || ts <= 0) return '';
|
|
429
|
+
const days = Math.floor((now - ts) / 86_400_000);
|
|
430
|
+
if (days <= 0) return 'today';
|
|
431
|
+
if (days === 1) return 'yesterday';
|
|
432
|
+
if (days < 7) return `${days} days ago`;
|
|
433
|
+
return new Date(ts).toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// The date on the top bar, in the app's house form: "Mon · Jul 27".
|
|
437
|
+
export function dateLine(now = Date.now()) {
|
|
438
|
+
const d = new Date(now);
|
|
439
|
+
const day = d.toLocaleDateString('en-US', { weekday: 'short' });
|
|
440
|
+
const rest = d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
441
|
+
return `${day} · ${rest}`;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// NO PACE LINE, deliberately. The pre-launch design carried "day 4 · most
|
|
445
|
+
// launch by 9"; the founder cut it (2026-07-26) as incidental copy. Removing
|
|
446
|
+
// it also removes the only number on the page with nothing behind it: we have
|
|
447
|
+
// no launch cohort yet, so the comparison would have been invented, and a "day
|
|
448
|
+
// N" count standing alone is the drought counter the house rules already ban.
|
|
449
|
+
// If a real cohort ever exists, comparison is welcome back; a target is not.
|
|
450
|
+
|
|
451
|
+
/* ------------------------------- the greeting ---------------------------- */
|
|
452
|
+
// Time of day, for "Morning, the founder." Local to whoever is reading
|
|
453
|
+
// it.
|
|
454
|
+
export function greeting(now = Date.now()) {
|
|
455
|
+
const hour = new Date(now).getHours();
|
|
456
|
+
if (hour < 12) return 'Morning';
|
|
457
|
+
if (hour < 18) return 'Afternoon';
|
|
458
|
+
return 'Evening';
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/* -------------------------------- helpers -------------------------------- */
|
|
462
|
+
// House rule, enforced rather than remembered: no em or en dashes in anything a
|
|
463
|
+
// founder reads. An agent writing the ledger is a model, and models reach for
|
|
464
|
+
// them constantly, so the ledger cleans on the way in instead of hoping.
|
|
465
|
+
export function clean(text) {
|
|
466
|
+
return String(text ?? '')
|
|
467
|
+
.replace(/\s*[—–]\s*/g, ', ')
|
|
468
|
+
.replace(/\s+/g, ' ')
|
|
469
|
+
.trim();
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function slug(text) {
|
|
473
|
+
return String(text ?? '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'metric';
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function lowerFirst(text) {
|
|
477
|
+
const s = String(text ?? '').trim();
|
|
478
|
+
if (!s) return s;
|
|
479
|
+
// only lowercase a plain capitalized word, so a proper noun keeps its case
|
|
480
|
+
return /^[A-Z][a-z]/.test(s) ? s[0].toLowerCase() + s.slice(1) : s;
|
|
481
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// WHAT A SESSION'S OWN TRACE LOG SAYS ABOUT WHETHER IT EVER GOT GOING.
|
|
2
|
+
//
|
|
3
|
+
// The supervisor already knows this while a session is alive: it holds the
|
|
4
|
+
// object and can see whether an id ever arrived. The trace is the only place
|
|
5
|
+
// that fact survives the process, and surviving the process is the whole point
|
|
6
|
+
// here.
|
|
7
|
+
//
|
|
8
|
+
// She was right, and the reason is a gap rather than a bug. The sentence that
|
|
9
|
+
// says a run died was written at the MOMENT of the exit, by the handler
|
|
10
|
+
// holding the session. Her four remaining Codex rows had already died before
|
|
11
|
+
// that code existed — the last of them at 13:48 and 13:51 — and nothing in the
|
|
12
|
+
// app ever looks at a row again to notice it is sitting there silent. So they
|
|
13
|
+
// read exactly as they read on Tuesday: in progress, nothing running, no word.
|
|
14
|
+
//
|
|
15
|
+
// A trace looks like this. The header, then whatever came out, then the exit:
|
|
16
|
+
//
|
|
17
|
+
// # There's something wrong with our search field. # · spawned
|
|
18
|
+
// 2026-08-27T20:51:39.445Z
|
|
19
|
+
//
|
|
20
|
+
// stderr: Reading additional input from stdin...
|
|
21
|
+
// Not inside a trusted directory and --skip-git-repo-check was not specified.
|
|
22
|
+
//
|
|
23
|
+
// # exited (1) 2026-08-27T20:51:40.139Z
|
|
24
|
+
//
|
|
25
|
+
// A run that actually started looks nothing like it: from the first line the
|
|
26
|
+
// CLI streams, the supervisor writes a clock-stamped line for every step. That
|
|
27
|
+
// stamp is the tell, and it is a better one than the exit code, because a
|
|
28
|
+
// session can run for six minutes of real work and still exit non-zero.
|
|
29
|
+
|
|
30
|
+
// Every narration line the supervisor writes is `HH:MM:SS something`. The
|
|
31
|
+
// header lines start with '#', the tool's own output is prefixed 'stderr: ',
|
|
32
|
+
// and neither can be mistaken for it.
|
|
33
|
+
const NARRATED = /^\d{2}:\d{2}:\d{2}\s{2}\S/m;
|
|
34
|
+
|
|
35
|
+
const EXITED = /^# exited \((-?\d+)\)/m;
|
|
36
|
+
|
|
37
|
+
// THE DIAGNOSIS IS ON THE SECOND LINE, NOT THE FIRST, and reading only the
|
|
38
|
+
// prefixed line loses it entirely. What the tool prints arrives as one chunk
|
|
39
|
+
// and is written with one `stderr: ` in front of the whole chunk, so a refusal
|
|
40
|
+
// that comes after a newline sits in the file with no prefix at all:
|
|
41
|
+
//
|
|
42
|
+
// stderr: Reading additional input from stdin...
|
|
43
|
+
// Not inside a trusted directory and --skip-git-repo-check was not specified.
|
|
44
|
+
//
|
|
45
|
+
// Taking prefixed lines alone therefore came back with "Reading additional
|
|
46
|
+
// input from stdin", which classifies as 'unknown', and the row would have
|
|
47
|
+
// been told the generic sentence about her four Codex tasks whose cause we
|
|
48
|
+
// knew exactly. So a chunk runs from its `stderr:` line until the blank line
|
|
49
|
+
// that ends it, and nothing of ours can be swept up on the way: the header and
|
|
50
|
+
// the narration both end the chunk too.
|
|
51
|
+
function stderrChunks(raw) {
|
|
52
|
+
const out = [];
|
|
53
|
+
let inside = false;
|
|
54
|
+
for (const line of raw.split('\n')) {
|
|
55
|
+
if (line.startsWith('stderr:')) {
|
|
56
|
+
inside = true;
|
|
57
|
+
out.push(line.replace(/^stderr:\s?/, ''));
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (!inside) continue;
|
|
61
|
+
if (!line.trim() || line.startsWith('#') || /^\d{2}:\d{2}:\d{2}\s{2}/.test(line)) { inside = false; continue; }
|
|
62
|
+
out.push(line);
|
|
63
|
+
}
|
|
64
|
+
return out.join('\n').slice(-1000);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// AND THE OTHER PLACE A REASON HIDES, which is not stderr at all.
|
|
68
|
+
//
|
|
69
|
+
// A CLI that starts, authenticates and fails says so through its own protocol,
|
|
70
|
+
// and the supervisor writes that as the run's result with an ERROR flag on it
|
|
71
|
+
// (`== RESULT (success ERROR · 1 turns) ==`). Nothing about it is on stderr.
|
|
72
|
+
// Measured on her store 2026-08-27: had 238 traces and 197 recorded strikes,
|
|
73
|
+
// every one of them
|
|
74
|
+
//
|
|
75
|
+
// Failed to authenticate: OAuth session expired and could not be refreshed
|
|
76
|
+
//
|
|
77
|
+
// and the row had never carried a single word. Read from stderr alone the
|
|
78
|
+
// cause came back 'unknown' and she would have been told the row "stopped
|
|
79
|
+
// before it did anything", which is true and useless: the one sentence worth
|
|
80
|
+
// having here is the one that tells her to type /login.
|
|
81
|
+
//
|
|
82
|
+
// Only ever read when the flag is on the line. A result WITHOUT the ERROR flag
|
|
83
|
+
// is a worker's own prose about her product, and prose is exactly the thing
|
|
84
|
+
// that must not be pattern-matched for words like "unauthorized".
|
|
85
|
+
const ERROR_RESULT = /^\d{2}:\d{2}:\d{2}\s{2}== RESULT \([^)]*\bERROR\b[^)]*\) ==$/m;
|
|
86
|
+
|
|
87
|
+
function errorResult(raw) {
|
|
88
|
+
const at = ERROR_RESULT.exec(raw);
|
|
89
|
+
if (!at) return '';
|
|
90
|
+
const after = raw.slice(at.index + at[0].length);
|
|
91
|
+
const end = after.search(/^# exited \(/m);
|
|
92
|
+
return (end === -1 ? after : after.slice(0, end)).trim();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @param {string} text the whole trace file
|
|
97
|
+
* @returns {{everRan: boolean, exitCode: number|null, stderr: string}}
|
|
98
|
+
* `stderr` is what the tool said about itself, from either place it says it.
|
|
99
|
+
* `everRan` false means the CLI never produced a first line: the process
|
|
100
|
+
* started and died before it was a session. `exitCode` null means the trace
|
|
101
|
+
* has no exit line at all, which is what a run killed with the app looks
|
|
102
|
+
* like, and is deliberately NOT read as a death.
|
|
103
|
+
*/
|
|
104
|
+
export function readDeadRunTrace(text) {
|
|
105
|
+
const raw = String(text ?? '');
|
|
106
|
+
const exited = EXITED.exec(raw);
|
|
107
|
+
return {
|
|
108
|
+
everRan: NARRATED.test(raw),
|
|
109
|
+
exitCode: exited ? Number(exited[1]) : null,
|
|
110
|
+
stderr: [stderrChunks(raw), errorResult(raw)].filter(Boolean).join('\n').slice(-1000),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* IS THIS THE TRACE OF A RUN THAT LEFT HER NOTHING?
|
|
116
|
+
*
|
|
117
|
+
* Both halves have to be true. It has to have finished — a trace with no exit
|
|
118
|
+
* line belongs to a session that is either still going or was killed with the
|
|
119
|
+
* app, and neither is news. And it has to have finished BADLY: either it never
|
|
120
|
+
* narrated a line, so it was never a session at all, or it exited non-zero.
|
|
121
|
+
* A clean exit means a worker ran and made its own choices about the row, and
|
|
122
|
+
* the supervisor has no business speaking over it.
|
|
123
|
+
*/
|
|
124
|
+
export function traceShowsASilentDeath(trace) {
|
|
125
|
+
if (!trace || trace.exitCode == null) return false;
|
|
126
|
+
return !trace.everRan || trace.exitCode !== 0;
|
|
127
|
+
}
|