agentbox-app 0.1.6 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +209 -55
- package/bin/agentbox.mjs +6 -0
- package/briefs/message-rules.md +13 -3
- package/briefs/worker.md +63 -49
- package/briefs/writing-rules.md +90 -131
- package/main/account-discovery.mjs +21 -20
- package/main/account-tooling.mjs +9 -10
- package/main/agent-activity.mjs +45 -5
- package/main/agent-schedule.mjs +14 -14
- package/main/agent-sessions.mjs +94 -10
- package/main/agent-updates.mjs +114 -0
- package/main/agents.mjs +52 -46
- package/main/analytics.mjs +27 -0
- package/main/approval-prompt-server.mjs +2 -2
- package/main/artifact-path.mjs +11 -0
- package/main/claude-bin.mjs +31 -5
- package/main/claude-input.mjs +45 -3
- package/main/claude-plan.mjs +11 -11
- package/main/code-change.mjs +3 -3
- package/main/codex-account.mjs +1 -1
- package/main/codex-bin.mjs +17 -0
- package/main/codex-memory-gate.mjs +260 -0
- package/main/codex-models.mjs +12 -7
- package/main/codex-threads.mjs +46 -23
- package/main/codex-usage-file.mjs +7 -7
- package/main/codex-usage.mjs +4 -4
- package/main/codex-watch.mjs +12 -12
- package/main/config.mjs +68 -8
- package/main/dev-window.mjs +9 -9
- package/main/doc-scheme.mjs +2 -0
- package/main/engine-setup.mjs +193 -0
- package/main/feedback.mjs +55 -0
- package/main/folder-pictures.mjs +48 -0
- package/main/frame-scrollbars.mjs +90 -0
- package/main/fresh-user.mjs +6 -3
- package/main/git-change-offthread.mjs +70 -0
- package/main/git-change-thread.mjs +11 -0
- package/main/git-change.mjs +1 -1
- package/main/img-scheme.mjs +70 -1
- package/main/instruction-settings.mjs +92 -54
- package/main/ipc.mjs +316 -42
- package/main/leftovers.mjs +518 -0
- package/main/live-replies.mjs +43 -5
- package/main/machine.mjs +47 -0
- package/main/main.mjs +119 -15
- package/main/memory-gate-history.mjs +254 -0
- package/main/memory-gate-server.mjs +461 -0
- package/main/memory-gate.mjs +206 -0
- package/main/message-priority.mjs +87 -0
- package/main/rail-note.mjs +2 -2
- package/main/refresh-agent-models.mjs +20 -0
- package/main/repeats.mjs +4 -4
- package/main/row-label.mjs +114 -51
- package/main/serve.mjs +67 -5
- package/main/settings.mjs +223 -26
- package/main/ship-queue.mjs +279 -0
- package/main/sign-in-files.mjs +39 -0
- package/main/source-updater.mjs +298 -0
- package/main/store/home.mjs +47 -2
- package/main/store/work-items.mjs +150 -6
- package/main/store.mjs +235 -9
- package/main/supervisor.mjs +1444 -406
- package/main/task-commands.mjs +8 -10
- package/main/task-folders.mjs +74 -13
- package/main/task-remote-control.mjs +3 -3
- package/main/task-terminals.mjs +2 -1
- package/main/team/index.mjs +440 -0
- package/main/team/memory-cloud.mjs +247 -0
- package/main/team/projects.mjs +162 -0
- package/main/team/session.mjs +225 -0
- package/main/team/sign-in.mjs +106 -0
- package/main/team/supabase-backend.mjs +231 -0
- package/main/team/sync.mjs +163 -0
- package/main/updater.mjs +15 -5
- package/main/write-before-reload.mjs +50 -0
- package/mcp/core/page.mjs +9 -11
- package/mcp/core/work.mjs +91 -25
- package/mcp/tools.mjs +12 -6
- package/package.json +21 -9
- package/renderer/dist/assets/agentbox-icon-F1oN9nj_.svg +15 -0
- package/renderer/dist/assets/index-B-2Az952.css +32 -0
- package/renderer/dist/assets/index-D6qncgai.js +1168 -0
- package/renderer/dist/index.html +2 -2
- package/scripts/memory-gate-hook.sh +59 -0
- package/shared/agent-import.mjs +38 -15
- package/shared/agents.d.mts +26 -8
- package/shared/agents.mjs +100 -108
- package/shared/analytics-events.mjs +19 -2
- package/shared/answers.mjs +12 -0
- package/shared/bridge-map.d.mts +12 -0
- package/shared/bridge-map.mjs +37 -1
- package/shared/claude-commands.generated.mjs +2 -2
- package/shared/claude-models.generated.mjs +4 -4
- package/shared/claude-usage.mjs +6 -6
- package/shared/codex-import.mjs +7 -7
- package/shared/codex-modes.d.mts +50 -0
- package/shared/codex-usage.mjs +7 -8
- package/shared/demo-world.mjs +5 -5
- package/shared/engines.mjs +27 -10
- package/shared/feedback.d.mts +14 -0
- package/shared/feedback.mjs +44 -0
- package/shared/first-run-examples.mjs +4 -4
- package/shared/first-run-practice.d.mts +16 -0
- package/shared/first-run-practice.mjs +176 -70
- package/shared/fresh-user-home.mjs +22 -3
- package/shared/instruction-defaults.json +2 -2
- package/shared/product-name.mjs +4 -2
- package/shared/project-folder-check.mjs +6 -0
- package/shared/rank.d.mts +1 -0
- package/shared/rank.mjs +22 -0
- package/shared/referenced-files.mjs +1 -1
- package/shared/schemes.d.mts +11 -0
- package/shared/schemes.mjs +1 -1
- package/shared/shortcuts.d.mts +41 -0
- package/shared/shortcuts.mjs +236 -0
- package/shared/spawn-trouble.d.mts +2 -0
- package/shared/spawn-trouble.mjs +39 -18
- package/shared/team-rules.d.mts +17 -0
- package/shared/team-rules.mjs +230 -0
- package/shared/team-status.d.mts +11 -0
- package/shared/team-status.mjs +90 -0
- package/shared/terminal-links.d.mts +2 -0
- package/shared/terminal-links.mjs +55 -0
- package/shared/terminal-state.d.mts +1 -0
- package/shared/terminal-state.mjs +16 -0
- package/shared/thread-cards.d.mts +19 -0
- package/shared/thread-cards.mjs +254 -0
- package/shared/usage.d.mts +1 -1
- package/shared/usage.mjs +6 -7
- package/shared/work-items.mjs +255 -27
- package/shared/work-lines.mjs +393 -16
- package/worker-permissions.json +1 -1
- package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
- package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
- package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
- package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
- package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
- package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
- package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
- package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
- package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
- package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
- package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
- package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
- package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
- package/renderer/dist/assets/index-CwiDv5bL.css +0 -32
- package/renderer/dist/assets/index-D9hCAVPl.js +0 -684
- package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
- package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
- package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
- package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
- package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
- package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
- package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
- package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
- package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
- package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
- package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
- package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
- package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
- package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
- package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
- package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
- package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
- package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
- package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
- package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
- package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
- package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
- package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
- package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
- package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
- package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
- package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
- package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
- package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
- package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
- package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
- package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
- package/renderer/dist/emulator/audio-rules.mjs +0 -74
- package/renderer/dist/emulator/data/compression/README.md +0 -11
- package/renderer/dist/emulator/data/compression/extract7z.js +0 -1
- package/renderer/dist/emulator/data/compression/extractzip.js +0 -1
- package/renderer/dist/emulator/data/compression/libunrar.js +0 -519
- package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
- package/renderer/dist/emulator/data/cores/README.md +0 -22
- package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/package.json +0 -65
- package/renderer/dist/emulator/data/cores/reports/mgba.json +0 -1
- package/renderer/dist/emulator/data/emulator.css +0 -1614
- package/renderer/dist/emulator/data/loader.js +0 -170
- package/renderer/dist/emulator/data/localization/README.md +0 -63
- package/renderer/dist/emulator/data/localization/af-FR.json +0 -339
- package/renderer/dist/emulator/data/localization/ar-AR.json +0 -301
- package/renderer/dist/emulator/data/localization/ben-BEN.json +0 -301
- package/renderer/dist/emulator/data/localization/de-GER.json +0 -301
- package/renderer/dist/emulator/data/localization/el-GR.json +0 -301
- package/renderer/dist/emulator/data/localization/en-US.json +0 -339
- package/renderer/dist/emulator/data/localization/es-ES.json +0 -310
- package/renderer/dist/emulator/data/localization/fa-AF.json +0 -310
- package/renderer/dist/emulator/data/localization/hi-HI.json +0 -301
- package/renderer/dist/emulator/data/localization/it-IT.json +0 -302
- package/renderer/dist/emulator/data/localization/ja-JA.json +0 -301
- package/renderer/dist/emulator/data/localization/jv-JV.json +0 -301
- package/renderer/dist/emulator/data/localization/ko-KO.json +0 -336
- package/renderer/dist/emulator/data/localization/pt-BR.json +0 -361
- package/renderer/dist/emulator/data/localization/retroarch.json +0 -617
- package/renderer/dist/emulator/data/localization/ro-RO.json +0 -310
- package/renderer/dist/emulator/data/localization/ru-RU.json +0 -301
- package/renderer/dist/emulator/data/localization/tr-TR.json +0 -310
- package/renderer/dist/emulator/data/localization/vi-VN.json +0 -302
- package/renderer/dist/emulator/data/localization/zh-CN.json +0 -351
- package/renderer/dist/emulator/data/src/GameManager.js +0 -464
- package/renderer/dist/emulator/data/src/compression.js +0 -142
- package/renderer/dist/emulator/data/src/emulator.js +0 -6158
- package/renderer/dist/emulator/data/src/gamepad.js +0 -167
- package/renderer/dist/emulator/data/src/nipplejs.js +0 -1
- package/renderer/dist/emulator/data/src/shaders.js +0 -236
- package/renderer/dist/emulator/data/src/socket.io.min.js +0 -7
- package/renderer/dist/emulator/data/src/storage.js +0 -125
- package/renderer/dist/emulator/data/version.json +0 -5
- package/renderer/dist/emulator/host.js +0 -299
- package/renderer/dist/emulator/index.html +0 -28
- package/renderer/dist/emulator/provenance.json +0 -6
package/main/settings.mjs
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import fs from 'node:fs';
|
|
16
16
|
import os from 'node:os';
|
|
17
17
|
import path from 'node:path';
|
|
18
|
-
import { saveConfig } from './config.mjs';
|
|
18
|
+
import { codexChoiceToOpen, saveConfig } from './config.mjs';
|
|
19
19
|
import { resolveClaudeBin, forgetClaudeBin, INSTALL_URL } from './claude-bin.mjs';
|
|
20
20
|
import { AGENT_MODES } from '../shared/agents.mjs';
|
|
21
21
|
import { analyticsKey, diagnosticsOn } from './analytics.mjs';
|
|
@@ -23,7 +23,9 @@ import { CLAUDE_PERMISSION_MODES } from '../shared/work-items.mjs';
|
|
|
23
23
|
import { accountSentence, engineTroubleNote } from '../shared/spawn-trouble.mjs';
|
|
24
24
|
import { readPlan } from './claude-plan.mjs';
|
|
25
25
|
import { effectiveProfiles } from './account-discovery.mjs';
|
|
26
|
-
import { machineSlots, machineNote, MAX_SLOTS } from './machine.mjs';
|
|
26
|
+
import { machineSlots, machineNote, autoAgents, MAX_SLOTS } from './machine.mjs';
|
|
27
|
+
import { NAME } from '../shared/product-name.mjs';
|
|
28
|
+
import { autoSlots } from './memory-gate.mjs';
|
|
27
29
|
import { accountIdentity, claudeLoginCommand, duplicateAccountNote, linkAccountTooling, makeClaudeHome } from './account-tooling.mjs';
|
|
28
30
|
import { isEngine } from '../shared/engines.mjs';
|
|
29
31
|
import { codexAccount, codexLoginCommand, makeCodexHome } from './codex-account.mjs';
|
|
@@ -270,6 +272,10 @@ export function recheckClaude(config) {
|
|
|
270
272
|
// has just found Claude Code updates it rather than leaving the rest of the
|
|
271
273
|
// app pointing at where it used to not be.
|
|
272
274
|
if (state.claudeFound && state.claudeBin) config.claudeBin = state.claudeBin;
|
|
275
|
+
// And whether it is here at all, which is what decides that a Mac with only
|
|
276
|
+
// Codex runs on Codex (`Supervisor#_enginesFound`). Only a certain answer
|
|
277
|
+
// moves it, so an unsure search never takes Claude Code away from anybody.
|
|
278
|
+
if (state.claudeFound || state.claudeCertain) config.claudeFound = state.claudeFound;
|
|
273
279
|
return state;
|
|
274
280
|
}
|
|
275
281
|
|
|
@@ -319,6 +325,12 @@ export function codexState(config, where = {}) {
|
|
|
319
325
|
};
|
|
320
326
|
}
|
|
321
327
|
|
|
328
|
+
/** `codexState` under the names the walk's last card reads beside Claude's. */
|
|
329
|
+
export function codexFoundState(config, where = {}) {
|
|
330
|
+
const state = codexState(config, where);
|
|
331
|
+
return { codexFound: state.found, codexCertain: state.certain, codexInstallUrl: state.url };
|
|
332
|
+
}
|
|
333
|
+
|
|
322
334
|
/**
|
|
323
335
|
* LOOK AGAIN, PROPERLY, for the second engine. The twin of `recheckClaude`
|
|
324
336
|
* above, and it earns its place for a sharper reason: finding Codex here is
|
|
@@ -329,6 +341,10 @@ export function recheckCodex(config) {
|
|
|
329
341
|
forgetCodexBin();
|
|
330
342
|
const state = codexState(config);
|
|
331
343
|
config.codexBin = state.found ? state.bin : null;
|
|
344
|
+
// Found while the app is open, by this button or the plan setup: the choice
|
|
345
|
+
// opens now, as it would have at the next launch (main/config.mjs).
|
|
346
|
+
const opened = codexChoiceToOpen(config, state.found);
|
|
347
|
+
if (opened && config.appDir) saveConfig(config, { engineChoice: opened });
|
|
332
348
|
return state;
|
|
333
349
|
}
|
|
334
350
|
|
|
@@ -366,9 +382,9 @@ export function readSettings({ config, supervisor, store }) {
|
|
|
366
382
|
const trouble = troubles[profile] ?? null;
|
|
367
383
|
const resting = (supervisor._profileCooldown?.[profile] ?? 0) >= now;
|
|
368
384
|
// WHOSE SUBSCRIPTION THIS ROW IS, not merely which folder it lives in.
|
|
369
|
-
// The page named its rows after folders, so nothing on it could tell
|
|
370
|
-
// that
|
|
371
|
-
// main/account-tooling.mjs has
|
|
385
|
+
// The page named its rows after folders, so nothing on it could tell a
|
|
386
|
+
// person that one of their accounts had been replaced by the other;
|
|
387
|
+
// main/account-tooling.mjs has how that happens. Null when the
|
|
372
388
|
// folder holds no login, and then the row reads as it always did.
|
|
373
389
|
const identity = accountIdentity(profile === 'default' ? null : profile);
|
|
374
390
|
return {
|
|
@@ -421,9 +437,8 @@ export function readSettings({ config, supervisor, store }) {
|
|
|
421
437
|
// THE MARK, already a drawable url or null, resolved once by listProducts.
|
|
422
438
|
logo: product.logo,
|
|
423
439
|
// ONE SWITCH A PROJECT STILL HAS (w-d19d6d387c, 2026-09-22). It had five
|
|
424
|
-
// controls
|
|
425
|
-
//
|
|
426
|
-
// lean towards just having project-level instructions." The drive, the
|
|
440
|
+
// controls, and what they did was not clear even after reading their
|
|
441
|
+
// labels, so project-level instructions replace them. The drive, the
|
|
427
442
|
// per-project pause and personal mode are gone from the app entirely.
|
|
428
443
|
// `permission` stays readable because `projectSessionArgs` is still
|
|
429
444
|
// honoured for anyone who sets one in the config by hand, and the page
|
|
@@ -439,10 +454,31 @@ export function readSettings({ config, supervisor, store }) {
|
|
|
439
454
|
};
|
|
440
455
|
});
|
|
441
456
|
|
|
457
|
+
// ARCHIVED PROJECTS, only so the Projects page can bring one back. Name and
|
|
458
|
+
// folder, the two things that tell three projects called Daydream apart.
|
|
459
|
+
let archivedProjects = [];
|
|
460
|
+
try {
|
|
461
|
+
archivedProjects = store.listProducts({ includeArchived: true })
|
|
462
|
+
.filter((p) => p.archived === true)
|
|
463
|
+
.map((p) => ({ slug: p.slug, name: p.name, dir: p.dir }));
|
|
464
|
+
} catch {}
|
|
465
|
+
|
|
442
466
|
return {
|
|
467
|
+
archivedProjects,
|
|
443
468
|
workspace: {
|
|
444
469
|
agentsRunning: !supervisor.paused,
|
|
445
470
|
sessionsAtOnce: config.maxConcurrentSessions,
|
|
471
|
+
// AUTOMATIC, AND WHAT IT WORKS OUT TO (w-e5225b62ba). `agentsAuto` is who
|
|
472
|
+
// is deciding; `agentsTotal` is what the whole Mac is running at once,
|
|
473
|
+
// which is the number the page shows, because the per-account one is
|
|
474
|
+
// arithmetic nobody should have to do. `agentsNudge` is how many steps
|
|
475
|
+
// down "Something felt slow" has taken it.
|
|
476
|
+
agentsAuto: !!config.agentsAuto,
|
|
477
|
+
agentsTotal: status.capacity,
|
|
478
|
+
agentsNudge: Math.max(0, Math.round(Number(config.agentsNudge) || 0)),
|
|
479
|
+
// What Automatic would say right now, so a manual number can be compared
|
|
480
|
+
// with it without turning Automatic back on to find out.
|
|
481
|
+
agentsAutoTotal: autoAgents({ gated: !!config.memoryGate, nudge: config.agentsNudge }),
|
|
446
482
|
// THE PLAN THAT SET THE NUMBER ABOVE, when a plan set it. Null in the
|
|
447
483
|
// ordinary case: she picked the number herself, or it is the three every
|
|
448
484
|
// install has always had. It is only ever filled in when somebody would
|
|
@@ -457,6 +493,8 @@ export function readSettings({ config, supervisor, store }) {
|
|
|
457
493
|
slotsSuggested: slotsHere.slots,
|
|
458
494
|
slotsMax: MAX_SLOTS,
|
|
459
495
|
machineNote: machineNote(slotsHere, config.maxConcurrentSessions),
|
|
496
|
+
memoryGate: memoryGateSettings({ config, supervisor }),
|
|
497
|
+
leftovers: leftoverSettings({ config, supervisor }),
|
|
460
498
|
capacity: status.capacity,
|
|
461
499
|
running: sessions.length,
|
|
462
500
|
model: parseSessionArgs(workspaceArgs).model,
|
|
@@ -509,6 +547,10 @@ export function readSettings({ config, supervisor, store }) {
|
|
|
509
547
|
// Someone who reads "install it, then open this screen again" and does
|
|
510
548
|
// exactly that has to see it turn found, without restarting the app.
|
|
511
549
|
...claudeState(config),
|
|
550
|
+
// AND WHETHER CODEX IS, because the app needs one of the two and not
|
|
551
|
+
// both: the last card of the walk only shuts the inbox when neither is
|
|
552
|
+
// here (renderer/src/App.tsx).
|
|
553
|
+
...codexFoundState(config),
|
|
512
554
|
// AND WHAT THIS MAC'S CLAUDE CODE CALLS ITS MODELS.
|
|
513
555
|
//
|
|
514
556
|
// In the same trip, and for the same reason the line above is looked up
|
|
@@ -577,21 +619,26 @@ function engineSettings(supervisor, config) {
|
|
|
577
619
|
// and that stopped being true somewhere between codex-cli versions.
|
|
578
620
|
// main/codex-account.mjs holds the measurement. Null when nobody is signed
|
|
579
621
|
// in, and the card then reads exactly as it always did.
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
622
|
+
// A Mac that runs on Codex alone has asked about it by having nothing else.
|
|
623
|
+
//
|
|
624
|
+
// AND THE PAGE IS DRAWN ON EVERY MAC NOW (w-d5d632e503). A tester who only
|
|
625
|
+
// uses Codex looked for it in Settings, found nothing, and had no way to
|
|
626
|
+
// tell "not installed" from "not offered". Not found, the page says so and
|
|
627
|
+
// links the install, which is the question that person had.
|
|
628
|
+
const codexIsHome = engine === 'codex';
|
|
629
|
+
const codex = {
|
|
630
|
+
...codexState(config),
|
|
631
|
+
trouble: engineTroubleFor(supervisor, 'codex'),
|
|
632
|
+
account: codexAccount(supervisor._codexHome()),
|
|
633
|
+
// EVERY CODEX LOGIN ON THE MAC, NOT JUST THE FIRST. The supervisor has
|
|
634
|
+
// run one app-server per CODEX_HOME since a second login became real;
|
|
635
|
+
// what never existed was a screen that could see them. One entry per
|
|
636
|
+
// `codexProfiles` word, resolved through the supervisor's own mapping so
|
|
637
|
+
// the card and the fleet cannot disagree about which folder a name
|
|
638
|
+
// means.
|
|
639
|
+
accounts: codexAccountRows(supervisor),
|
|
640
|
+
};
|
|
641
|
+
if (choices.length < 2 && !codexIsHome) {
|
|
595
642
|
return { engine, engineChoices: choices, codex, codexModels: [], codexModelDefault: null, codexModel: null };
|
|
596
643
|
}
|
|
597
644
|
const home = supervisor._codexHome();
|
|
@@ -754,8 +801,8 @@ export function addCodexAccount({ config, supervisor }) {
|
|
|
754
801
|
* accounting while this was a product we sold. She withdrew that on
|
|
755
802
|
* w-3498e0cad2 ("we're no longer offering this as a commercially viable
|
|
756
803
|
* product... the rules have changed so I think you should allow
|
|
757
|
-
* multi-accounting"), and this is the door that decision asked for.
|
|
758
|
-
* for wanting it
|
|
804
|
+
* multi-accounting"), and this is the door that decision asked for. The
|
|
805
|
+
* ordinary reason for wanting it: a work login and a personal login, both
|
|
759
806
|
* live, on one Mac.
|
|
760
807
|
*
|
|
761
808
|
* Everything else is the Codex flow exactly. Main makes the folder and
|
|
@@ -781,19 +828,143 @@ export function addClaudeAccount({ config, supervisor }) {
|
|
|
781
828
|
return { home: made.home, profile: made.profile, command: claudeLoginCommand(made.home) };
|
|
782
829
|
}
|
|
783
830
|
|
|
831
|
+
/**
|
|
832
|
+
* The Agents page's memory rows: the switch, the number of heavy commands at
|
|
833
|
+
* once (null is Auto), what Auto works out to here, and one sentence about
|
|
834
|
+
* right now while it is on.
|
|
835
|
+
*/
|
|
836
|
+
export function memoryGateSettings({ config, supervisor }) {
|
|
837
|
+
const on = !!config.memoryGate;
|
|
838
|
+
const slots = Number.isFinite(config.memoryGateSlots) ? config.memoryGateSlots : null;
|
|
839
|
+
// What Auto works out to on this Mac, whether or not somebody picked.
|
|
840
|
+
const slotsAuto = autoSlots(os.totalmem());
|
|
841
|
+
let now = null;
|
|
842
|
+
if (on) {
|
|
843
|
+
let s = null;
|
|
844
|
+
try { s = supervisor.memoryGateStatus?.() ?? null; } catch {}
|
|
845
|
+
if (s?.role === 'standby') now = `Another ${NAME} on this Mac is coordinating.`;
|
|
846
|
+
else if (s) {
|
|
847
|
+
const heavy = (s.running ?? []).filter((r) => r.holdsSlot).length;
|
|
848
|
+
const waiting = (s.waiting ?? []).length;
|
|
849
|
+
const mem = s.pressure === 'critical' ? 'Memory is very short.' : s.pressure === 'tight' ? 'Memory is tight.' : 'Memory is fine.';
|
|
850
|
+
const run = heavy ? `${heavy} heavy command${heavy === 1 ? '' : 's'} running` : 'Nothing heavy running';
|
|
851
|
+
now = `${mem} ${run}${waiting ? `, ${waiting} waiting` : ''}.`;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
return { on, slots, slotsAuto, slotsMax: MAX_SLOTS, now, reading: memoryReading({ config, supervisor }) };
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* THE FIGURES THE RUNNING PAGE LEADS WITH (w-e5225b62ba), rather than the same
|
|
859
|
+
* facts tacked onto the end of three grey paragraphs: "6 running now", "Holding
|
|
860
|
+
* 3 commands now", "Nothing running now".
|
|
861
|
+
*
|
|
862
|
+
* Every field is null when it is not known, and the page leaves out what is
|
|
863
|
+
* null. A figure invented to fill a slot is worse than a gap: this card's whole
|
|
864
|
+
* job is to be the one place on the screen that is measured.
|
|
865
|
+
*
|
|
866
|
+
* `usedPct` is the kernel's own `kern.memorystatus_level` turned round, which is
|
|
867
|
+
* what the gate already polls, so the bar and the word above it cannot disagree.
|
|
868
|
+
*/
|
|
869
|
+
export function memoryReading({ config, supervisor }) {
|
|
870
|
+
const out = { heavy: null, waiting: null, pressure: null, usedPct: null, asked: null, waited: null, refused: null };
|
|
871
|
+
if (!config.memoryGate) return out;
|
|
872
|
+
let s = null;
|
|
873
|
+
try { s = supervisor.memoryGateStatus?.() ?? null; } catch { return out; }
|
|
874
|
+
if (!s || s.role === 'standby') return out;
|
|
875
|
+
out.heavy = (s.running ?? []).filter((r) => r.holdsSlot).length;
|
|
876
|
+
out.waiting = (s.waiting ?? []).length;
|
|
877
|
+
out.pressure = s.pressure ?? null;
|
|
878
|
+
if (Number.isFinite(s.freePct)) out.usedPct = Math.max(0, Math.min(100, 100 - s.freePct));
|
|
879
|
+
const c = s.counts ?? {};
|
|
880
|
+
if (Number.isFinite(c.asked)) out.asked = c.asked;
|
|
881
|
+
if (Number.isFinite(c.waited)) out.waited = c.waited;
|
|
882
|
+
if (Number.isFinite(c.refused)) out.refused = c.refused;
|
|
883
|
+
return out;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* The Agents page's leftovers row: the switch, and one sentence about what
|
|
888
|
+
* finished agents have left running while it is on.
|
|
889
|
+
*/
|
|
890
|
+
export function leftoverSettings({ config, supervisor }) {
|
|
891
|
+
const on = !!config.cleanupLeftovers;
|
|
892
|
+
let now = null;
|
|
893
|
+
let s = null;
|
|
894
|
+
try { s = supervisor.leftoverStatus?.() ?? null; } catch {}
|
|
895
|
+
const list = s?.leftovers ?? [];
|
|
896
|
+
const stoppable = list.reduce((n, l) => n + (l.programs ?? 0), 0);
|
|
897
|
+
const kept = list.reduce((n, l) => n + (l.kept ?? 0), 0);
|
|
898
|
+
const total = stoppable + kept;
|
|
899
|
+
// OFF, IT SAYS WHAT TURNING IT ON WOULD STOP, before anybody does: the
|
|
900
|
+
// consent is to these programs too, not only to future ones (agreed with
|
|
901
|
+
// Codex, 2026-10-05).
|
|
902
|
+
if (!on) {
|
|
903
|
+
if (total && stoppable) {
|
|
904
|
+
now = `Right now finished agents have left ${total} program${total === 1 ? '' : 's'} running; turning this on stops ${stoppable === total ? (total === 1 ? 'it' : 'them') : `${stoppable} of them`}, two hours from then.`;
|
|
905
|
+
}
|
|
906
|
+
return { on, now };
|
|
907
|
+
}
|
|
908
|
+
{
|
|
909
|
+
if (!total) now = 'Nothing left running by finished agents.';
|
|
910
|
+
else {
|
|
911
|
+
const next = list.filter((l) => l.programs && l.stopsAt).map((l) => l.stopsAt).sort((a, b) => a - b)[0];
|
|
912
|
+
const when = (ms) => {
|
|
913
|
+
const m = Math.max(1, Math.round((ms - Date.now()) / 60_000));
|
|
914
|
+
return m >= 60 ? `${Math.floor(m / 60)} h${m % 60 ? ` ${m % 60} m` : ''}` : `${m} m`;
|
|
915
|
+
};
|
|
916
|
+
now = `Finished agents have left ${total} program${total === 1 ? '' : 's'} running.`;
|
|
917
|
+
if (stoppable) now += ` ${stoppable} ${next && next > Date.now() ? `stop in ${when(next)}` : 'are stopping'}`;
|
|
918
|
+
if (kept) now += `${stoppable ? ';' : ''} ${kept} ${kept === 1 ? 'is' : 'are'} kept`;
|
|
919
|
+
now += '.';
|
|
920
|
+
const survivors = s?.survivors?.length ?? 0;
|
|
921
|
+
if (survivors) now += ` ${survivors} did not stop when asked.`;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
return { on, now };
|
|
925
|
+
}
|
|
926
|
+
|
|
784
927
|
export function setWorkspaceSetting({ config, supervisor }, { key, value }) {
|
|
785
928
|
switch (key) {
|
|
786
929
|
case 'agentsRunning':
|
|
787
930
|
supervisor.paused = !value;
|
|
788
931
|
supervisor.onChange?.();
|
|
789
932
|
break;
|
|
933
|
+
// WHO DECIDES THE NUMBER (w-e5225b62ba). Turning Automatic on does not
|
|
934
|
+
// throw away the number somebody set: it stays in the file, unread, so
|
|
935
|
+
// turning Automatic off again gives them their own number back rather than
|
|
936
|
+
// a fresh guess.
|
|
937
|
+
case 'agentsAuto':
|
|
938
|
+
saveConfig(config, { agentsAuto: !!value });
|
|
939
|
+
supervisor.wake?.();
|
|
940
|
+
supervisor.onChange?.();
|
|
941
|
+
break;
|
|
942
|
+
// SOMETHING FELT SLOW. The one lever a person has that no reading can
|
|
943
|
+
// replace: they are sitting in front of the Mac and the app is not. It only
|
|
944
|
+
// ever subtracts, and main/machine.mjs refuses to take it below one.
|
|
945
|
+
case 'agentsFeltSlow': {
|
|
946
|
+
const now = Math.max(0, Math.round(Number(config.agentsNudge) || 0));
|
|
947
|
+
saveConfig(config, { agentsNudge: value === 'reset' ? 0 : now + 1 });
|
|
948
|
+
supervisor.wake?.();
|
|
949
|
+
supervisor.onChange?.();
|
|
950
|
+
break;
|
|
951
|
+
}
|
|
790
952
|
case 'sessionsAtOnce': {
|
|
791
953
|
// THE MACHINE DOES NOT CLAMP THIS, and for one round it did. These are
|
|
792
954
|
// developers using the tool as they want, so the range is the control's
|
|
793
955
|
// own range, the same on every Mac. What the hardware does is SUGGEST a
|
|
794
956
|
// starting number, one row up.
|
|
795
957
|
const n = Math.max(1, Math.min(MAX_SLOTS, Math.round(Number(value) || 1)));
|
|
796
|
-
|
|
958
|
+
// Setting a number by hand IS taking the decision back, so the page never
|
|
959
|
+
// shows a stepper that the supervisor is quietly ignoring.
|
|
960
|
+
saveConfig(config, { maxConcurrentSessions: n, agentsAuto: false });
|
|
961
|
+
// An explicit choice replaces the startup plan suggestion for BOTH
|
|
962
|
+
// engines. Leaving this marker set makes Codex ignore the new number
|
|
963
|
+
// until loadConfig runs again at restart. It is derived, not persisted.
|
|
964
|
+
config.planSlotsFrom = null;
|
|
965
|
+
// Fill newly available slots through the usual queue and its guards.
|
|
966
|
+
// Lowering the cap leaves existing work running and gates new starts.
|
|
967
|
+
supervisor.wake?.();
|
|
797
968
|
break;
|
|
798
969
|
}
|
|
799
970
|
// Written as an opt-OUT (`diagnostics: false`) so that on is what a config
|
|
@@ -801,6 +972,32 @@ export function setWorkspaceSetting({ config, supervisor }, { key, value }) {
|
|
|
801
972
|
// see in her own file. `saveConfig` mutates the live config too, and
|
|
802
973
|
// main/analytics.mjs reads the switch at every send, so off takes effect
|
|
803
974
|
// from the moment she moves it and not at the next launch.
|
|
975
|
+
// HOLD HEAVY WORK WHEN MEMORY IS SHORT (w-3958c3753d). Takes effect now:
|
|
976
|
+
// the coordinator starts or stops this moment, and workers spawned from
|
|
977
|
+
// here on carry the check or do not. A worker already running keeps the
|
|
978
|
+
// hooks it was started with; with the switch off its hook finds no socket
|
|
979
|
+
// and lets every command straight through.
|
|
980
|
+
case 'memoryGate':
|
|
981
|
+
saveConfig(config, { memoryGate: !!value });
|
|
982
|
+
supervisor.setMemoryGate?.(!!value)?.catch?.((e) => console.warn('zero: memory gate:', e.message));
|
|
983
|
+
supervisor.onChange?.();
|
|
984
|
+
break;
|
|
985
|
+
// STOP WHAT FINISHED AGENTS LEAVE RUNNING (main/leftovers.mjs). Turning it
|
|
986
|
+
// on records the moment, which is the earliest the clock may start from.
|
|
987
|
+
case 'cleanupLeftovers':
|
|
988
|
+
saveConfig(config, value ? { cleanupLeftovers: true, cleanupLeftoversSince: Date.now() } : { cleanupLeftovers: false });
|
|
989
|
+
supervisor.setLeftoverCleanup?.(!!value)?.catch?.((e) => console.warn('zero: leftovers:', e.message));
|
|
990
|
+
supervisor.onChange?.();
|
|
991
|
+
break;
|
|
992
|
+
case 'memoryGateSlots': {
|
|
993
|
+
const n = value === null || value === undefined || value === 'auto'
|
|
994
|
+
? null
|
|
995
|
+
: Math.max(1, Math.min(MAX_SLOTS, Math.round(Number(value) || 1)));
|
|
996
|
+
saveConfig(config, { memoryGateSlots: n });
|
|
997
|
+
if (config.memoryGate) supervisor.setMemoryGate?.(true)?.catch?.(() => {});
|
|
998
|
+
supervisor.onChange?.();
|
|
999
|
+
break;
|
|
1000
|
+
}
|
|
804
1001
|
case 'diagnostics':
|
|
805
1002
|
saveConfig(config, { diagnostics: !!value });
|
|
806
1003
|
break;
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// THE APP SHIPS A TASK; THE AGENT ONLY SAYS IT IS READY (2026-10-02).
|
|
2
|
+
//
|
|
3
|
+
// Agents used to finish by pushing to main and updating the folder the app
|
|
4
|
+
// runs from. Over thirty hours on one Mac, 17 tasks stalled because Claude
|
|
5
|
+
// Code's auto-mode safety check refused one of those steps (a push to a public
|
|
6
|
+
// repo, a fast-forward of another checkout, a rebase), 16 because a folder was
|
|
7
|
+
// left mid-merge or committed into, 6 racing each other's pushes. A refusal is
|
|
8
|
+
// final and asks nobody, so nothing reached the approvals corner; the task
|
|
9
|
+
// just said "the permission check refused it" and waited.
|
|
10
|
+
//
|
|
11
|
+
// So for a project that turns this on, the agent stops at a branch: it commits
|
|
12
|
+
// in its task folder, adds the label `ship` to its row, and ends its turn.
|
|
13
|
+
// This queue then runs the project's own ship script, one task at a time
|
|
14
|
+
// across the whole Mac, so two tasks never race. A failure goes back to the
|
|
15
|
+
// agent that wrote the change, as a reply, with the script's own words.
|
|
16
|
+
//
|
|
17
|
+
// OFF UNLESS A PERSON TURNS IT ON. The setting lives in the app's own data
|
|
18
|
+
// folder (`ship.json`), never in the store or the repo: agents can write
|
|
19
|
+
// project files, and a ship command they could rewrite would be a command the
|
|
20
|
+
// app runs for them outside every check. The script it names is run FROM THE
|
|
21
|
+
// BASE CHECKOUT, which only ever fast-forwards to the remote, so a branch
|
|
22
|
+
// cannot change the code that ships it. With no `ship.json`, nothing here does
|
|
23
|
+
// anything at all.
|
|
24
|
+
|
|
25
|
+
import { spawn } from 'node:child_process';
|
|
26
|
+
import fs from 'node:fs';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
import { Name } from '../shared/product-name.mjs';
|
|
29
|
+
|
|
30
|
+
export const SHIP_LABEL = 'ship';
|
|
31
|
+
const SHIP_MS = 20 * 60 * 1000;
|
|
32
|
+
const KEEP = 4000;
|
|
33
|
+
|
|
34
|
+
export const shipSettingsFile = (userDir) => path.join(userDir, 'ship.json');
|
|
35
|
+
|
|
36
|
+
// WHICH "READY" EACH TASK HAS ALREADY BEEN SHIPPED FOR, by the moment its
|
|
37
|
+
// label was written. Taking the label off is not enough: a label the person
|
|
38
|
+
// put on outranks the app on the ledger, so the app's removal is ignored, and
|
|
39
|
+
// on the first real run that shipped a task and then ran it twice more
|
|
40
|
+
// (2026-10-02). A new label carries a new moment and ships again.
|
|
41
|
+
const handledFile = (userDir) => path.join(userDir, 'ship-handled.json');
|
|
42
|
+
const keyOf = (item) => `${item.product}/${item.id}`;
|
|
43
|
+
const markedAt = (item) => item.wrote?.labels?.ts ?? item.updatedAt ?? 0;
|
|
44
|
+
function readHandled(userDir) {
|
|
45
|
+
try { const raw = JSON.parse(fs.readFileSync(handledFile(userDir), 'utf8')); return raw && typeof raw === 'object' ? raw : {}; } catch { return {}; }
|
|
46
|
+
}
|
|
47
|
+
function writeHandled(userDir, all) {
|
|
48
|
+
try {
|
|
49
|
+
const tmp = `${handledFile(userDir)}.${process.pid}.tmp`;
|
|
50
|
+
fs.writeFileSync(tmp, `${JSON.stringify(all)}\n`);
|
|
51
|
+
fs.renameSync(tmp, handledFile(userDir));
|
|
52
|
+
} catch { /* the label removal still covers the ordinary case */ }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** `{ [projectSlug]: { script: 'scripts/ship.mjs' } }`, or `{}`. */
|
|
56
|
+
export function readShipSettings(userDir) {
|
|
57
|
+
try {
|
|
58
|
+
const raw = JSON.parse(fs.readFileSync(shipSettingsFile(userDir), 'utf8'));
|
|
59
|
+
return raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
|
|
60
|
+
} catch {
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The script that ships this project, inside its base checkout, or null. */
|
|
66
|
+
export function shipScriptFor(product, settings) {
|
|
67
|
+
const rel = settings?.[product?.slug]?.script;
|
|
68
|
+
if (!product?.repoPath || typeof rel !== 'string' || !rel || path.isAbsolute(rel)) return null;
|
|
69
|
+
const base = path.resolve(product.repoPath);
|
|
70
|
+
const script = path.resolve(base, rel);
|
|
71
|
+
if (!script.startsWith(base + path.sep)) return null;
|
|
72
|
+
return fs.existsSync(script) ? script : null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const labelsOf = (item) => (Array.isArray(item?.labels) ? item.labels : []);
|
|
76
|
+
export const wantsShipping = (item) => labelsOf(item).includes(SHIP_LABEL);
|
|
77
|
+
export const withoutShipLabel = (item) => labelsOf(item).filter((l) => l !== SHIP_LABEL);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The next task to ship, oldest ask first, or null.
|
|
81
|
+
*
|
|
82
|
+
* Only a row of a project that ships, marked `ship`, with no session on it
|
|
83
|
+
* (an agent still working may yet commit), and with a task folder to ship from.
|
|
84
|
+
*/
|
|
85
|
+
export function nextToShip(items, products, settings, { isLive = () => false, folderFor, handled = {} }) {
|
|
86
|
+
const bySlug = new Map(products.map((p) => [p.slug, p]));
|
|
87
|
+
const ready = items
|
|
88
|
+
.filter((i) => wantsShipping(i) && i.status !== 'done' && !isLive(i) && !(markedAt(i) <= (handled[keyOf(i)] ?? -1)))
|
|
89
|
+
.map((item) => {
|
|
90
|
+
const product = bySlug.get(item.product);
|
|
91
|
+
const script = product ? shipScriptFor(product, settings) : null;
|
|
92
|
+
const cwd = script ? folderFor(item, product) : null;
|
|
93
|
+
return script && cwd && fs.existsSync(cwd) ? { item, product, script, cwd } : null;
|
|
94
|
+
})
|
|
95
|
+
.filter(Boolean);
|
|
96
|
+
ready.sort((a, b) => markedAt(a.item) - markedAt(b.item));
|
|
97
|
+
return ready[0] ?? null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The commit the script reports, from its own `ship: shipped <sha>.` line. */
|
|
101
|
+
export function shippedSha(output) {
|
|
102
|
+
return String(output ?? '').match(/ship: shipped ([0-9a-f]{7,40})\./)?.[1] ?? null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// A LINE VITEST TICKED GREEN IS NEVER WHY A SHIP FAILED (w-c121bd85e6).
|
|
106
|
+
//
|
|
107
|
+
// Test files here are named after the behaviour they cover, so plenty of them
|
|
108
|
+
// carry "failed", "error" or "refused" inside the name, with a hyphen either
|
|
109
|
+
// side. A hyphen is a word boundary, so even the whole-word guard below
|
|
110
|
+
// matches inside "a-failed-hook-is-visible-while-the-agent-continues". On
|
|
111
|
+
// 2026-10-04 a ship that DIED was reported as that file, which had passed in
|
|
112
|
+
// 51 milliseconds. Vitest ticks every file it ran green, so the tick is the
|
|
113
|
+
// one reliable way to rule a line out.
|
|
114
|
+
const TICKED_GREEN = /^[✓✔]/;
|
|
115
|
+
// WHOLE WORDS (w-7ec8553e23): without \b, "red" matched inside "configured"
|
|
116
|
+
// and a harmless warning the tests print was shown as why a ship failed.
|
|
117
|
+
const NAMES_A_FAILURE = /\b(CONFLICT|error|red|refused|failed|FAIL)\b/i;
|
|
118
|
+
// Vitest's heading over the list of failures, "⎯⎯⎯ Failed Tests 3 ⎯⎯⎯". It
|
|
119
|
+
// carries the count and names nothing, and the FAIL lines that DO name
|
|
120
|
+
// something are printed directly under it, so taking the first match showed
|
|
121
|
+
// the heading and sent whoever read it to the GitHub run to find out which
|
|
122
|
+
// three (w-c121bd85e6, this branch's own first ship). Passed over while there
|
|
123
|
+
// is anything better, and still offered when it is all there is.
|
|
124
|
+
const ONLY_A_COUNT = /^⎯+ .* ⎯+$/;
|
|
125
|
+
// A LINE VITEST QUOTED IS NOT THE SCRIPT SPEAKING (w-560647d4db, five
|
|
126
|
+
// bounces). A failing text assertion prints the file's own text as a diff, so
|
|
127
|
+
// any source line carrying one of those words becomes a candidate — and the
|
|
128
|
+
// diff is printed long before the runner's list of failures. Twice the note
|
|
129
|
+
// read "+ than deleted: a bare corner here reads as a control
|
|
130
|
+
// that failed", which is a pre-existing code comment in a file the branch
|
|
131
|
+
// touched. It names no fault, and the real cause was three failing tests the
|
|
132
|
+
// note never mentioned. Both shapes of quotation are skipped: a diff line
|
|
133
|
+
// (`+`/`-`) and a code frame (`34| expect(...)`).
|
|
134
|
+
const QUOTED_SOURCE = /^([+-]|\d+\|)/;
|
|
135
|
+
// THE RUNNER'S OWN FAILURES OUTRANK A LOOSE WORD MATCH. These two name a
|
|
136
|
+
// file: `FAIL tests/x.test.mjs > ...` and `❯ tests/x.test.mjs (3 tests | 3
|
|
137
|
+
// failed)`. Anything else matching the words above — the script's closing
|
|
138
|
+
// line, a merge message, a warning — says less, wherever it sits in the
|
|
139
|
+
// output.
|
|
140
|
+
const NAMES_A_RED_TEST = /^(FAIL\b|❯ .*\|\s*\d+ failed)/;
|
|
141
|
+
// Vitest's totals, "Tests 3 failed | 1920 passed (1923)", and its heading
|
|
142
|
+
// "⎯⎯⎯ Failed Tests 3 ⎯⎯⎯". Not "Test Files 1 failed", which counts files.
|
|
143
|
+
const HOW_MANY = [/^Tests\s+(\d+) failed/, /Failed Tests\s+(\d+)/];
|
|
144
|
+
|
|
145
|
+
const DIED = 'the test run died without naming a failing test';
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Why the branch did not ship, out of the script's own output.
|
|
149
|
+
*
|
|
150
|
+
* A run that went RED says so somewhere. A run that DIED — killed on a busy
|
|
151
|
+
* Mac, or cut off — leaves output with no failure in it at all, and the two
|
|
152
|
+
* have to be told apart: offering the nearest line instead sends whoever
|
|
153
|
+
* reads it after a fault that is not there.
|
|
154
|
+
*
|
|
155
|
+
* @returns {{ died: boolean, why: string|null, last: string|null, failed: number|null }}
|
|
156
|
+
*/
|
|
157
|
+
export function whyItDidNotShip(output) {
|
|
158
|
+
const lines = String(output ?? '').split('\n').map((l) => l.trim()).filter(Boolean);
|
|
159
|
+
const said = lines.filter((l) => !TICKED_GREEN.test(l) && !QUOTED_SOURCE.test(l));
|
|
160
|
+
const named = said.filter((l) => NAMES_A_FAILURE.test(l));
|
|
161
|
+
const red = named.find((l) => NAMES_A_RED_TEST.test(l));
|
|
162
|
+
const why = red ?? named.find((l) => !ONLY_A_COUNT.test(l)) ?? named[0] ?? null;
|
|
163
|
+
// The count goes with a line that names ONE test, so that it reads as the
|
|
164
|
+
// first of several rather than as the whole story. On a line that is not a
|
|
165
|
+
// test — a conflict, a refused push — a number beside it would read as that
|
|
166
|
+
// line's own count, so it is left off.
|
|
167
|
+
const counted = red ? Number(said.map((l) => HOW_MANY.map((re) => re.exec(l)?.[1]).find(Boolean)).find(Boolean)) : NaN;
|
|
168
|
+
return { died: !why, why, last: said.at(-1) ?? null, failed: Number.isFinite(counted) ? counted : null };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** "3 tests failed, the first is FAIL tests/…", or just the line. */
|
|
172
|
+
function diagnosis({ why, failed }) {
|
|
173
|
+
if (!why || !failed) return why;
|
|
174
|
+
return `${failed} test${failed === 1 ? '' : 's'} failed, the first is ${why}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** What the agent is told when its branch did not ship. */
|
|
178
|
+
export function failureReply(output) {
|
|
179
|
+
const tail = String(output ?? '').trim().slice(-KEEP);
|
|
180
|
+
const { died, ...rest } = whyItDidNotShip(output);
|
|
181
|
+
return [
|
|
182
|
+
`${Name} tried to ship your branch and it did not go out. Nothing was pushed.`,
|
|
183
|
+
// THE SAME DIAGNOSIS THE THREAD SHOWS, above the tail (w-560647d4db). The
|
|
184
|
+
// tail holds everything, which is why two sessions read a quoted code
|
|
185
|
+
// comment in it as the fault. One line naming how many tests failed and
|
|
186
|
+
// which file is where to start.
|
|
187
|
+
...(died ? [] : ['', `It went red: ${diagnosis(rest)}`]),
|
|
188
|
+
...(died ? [
|
|
189
|
+
'',
|
|
190
|
+
'Its test run DIED rather than going red: nothing it printed names a failing',
|
|
191
|
+
'test. Look for what killed the run. This Mac runs a dozen agents at once, so',
|
|
192
|
+
'a worker that was starved or timed out is likelier than a bug in your change.',
|
|
193
|
+
'Run the tests for your change again before you go looking for one.',
|
|
194
|
+
] : []),
|
|
195
|
+
'',
|
|
196
|
+
'Fix what the script says below in your task folder, commit, then add the label',
|
|
197
|
+
`\`${SHIP_LABEL}\` to this row again and end your turn. Do not push or touch any`,
|
|
198
|
+
'other folder yourself; the app ships it.',
|
|
199
|
+
'',
|
|
200
|
+
'```',
|
|
201
|
+
tail,
|
|
202
|
+
'```',
|
|
203
|
+
].join('\n');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** The line the thread shows: the app's own, short, and the first error in it. */
|
|
207
|
+
export function failureNote(output) {
|
|
208
|
+
const { died, last, ...rest } = whyItDidNotShip(output);
|
|
209
|
+
const said = died ? `${DIED}. It last said: ${last ?? 'nothing at all'}` : diagnosis(rest);
|
|
210
|
+
return `It did not ship, and it went back to its agent: ${said.slice(0, 300)}`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function runScript(script, cwd, { timeout = SHIP_MS, env = process.env } = {}) {
|
|
214
|
+
const node = env.npm_node_execpath && fs.existsSync(env.npm_node_execpath) ? env.npm_node_execpath : 'node';
|
|
215
|
+
return new Promise((resolve) => {
|
|
216
|
+
let out = '';
|
|
217
|
+
let child;
|
|
218
|
+
try {
|
|
219
|
+
child = spawn(node, [script], { cwd, env: { ...env, GIT_TERMINAL_PROMPT: '0' }, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
220
|
+
} catch (e) {
|
|
221
|
+
resolve({ code: 1, out: String(e?.message ?? e) });
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const keep = (b) => { out = (out + b).slice(-64 * 1024); };
|
|
225
|
+
child.stdout.on('data', keep);
|
|
226
|
+
child.stderr.on('data', keep);
|
|
227
|
+
const timer = setTimeout(() => { keep('\nship: stopped after 20 minutes.'); child.kill('SIGTERM'); }, timeout);
|
|
228
|
+
child.on('error', (e) => keep(`\n${e.message}`));
|
|
229
|
+
child.on('close', (code) => { clearTimeout(timer); resolve({ code: code ?? 1, out }); });
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* One task at a time, for the whole Mac. `tick` is called from the
|
|
235
|
+
* supervisor's own tick and returns at once; the run happens beside it.
|
|
236
|
+
*/
|
|
237
|
+
export class ShipQueue {
|
|
238
|
+
constructor({ store, userDir, folderFor, isLive, handBack = () => {}, run = runScript, afterShip = () => {}, log = console }) {
|
|
239
|
+
Object.assign(this, { store, userDir, folderFor, isLive, handBack, run, afterShip, log });
|
|
240
|
+
this.busy = null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
tick(items, products) {
|
|
244
|
+
if (this.busy) return null;
|
|
245
|
+
const settings = readShipSettings(this.userDir);
|
|
246
|
+
if (!Object.keys(settings).length) return null;
|
|
247
|
+
const next = nextToShip(items, products, settings, { isLive: this.isLive, folderFor: this.folderFor, handled: readHandled(this.userDir) });
|
|
248
|
+
if (!next) return null;
|
|
249
|
+
// Marked handled only once the run has ENDED, pass or fail. Marked before
|
|
250
|
+
// it, a run cut off by an app restart was never tried again
|
|
251
|
+
// (w-f37a34def6, 2026-10-04). Within one app, `busy` already stops a
|
|
252
|
+
// second start; across a restart, trying again is the point.
|
|
253
|
+
const done = () => writeHandled(this.userDir, { ...readHandled(this.userDir), [keyOf(next.item)]: markedAt(next.item) });
|
|
254
|
+
this.busy = this.ship(next).then(done, done).finally(() => { this.busy = null; });
|
|
255
|
+
return this.busy;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async ship({ item, product, script, cwd }) {
|
|
259
|
+
this.log.info?.(`zero: shipping ${item.id} from ${cwd}`);
|
|
260
|
+
const { code, out } = await this.run(script, cwd);
|
|
261
|
+
try {
|
|
262
|
+
// Already on main: there is nothing to fix, so nobody is woken.
|
|
263
|
+
const already = /There is nothing to ship/.test(out);
|
|
264
|
+
if (code === 0 || already) {
|
|
265
|
+
this.store.shipped(product.slug, item.id, { sha: shippedSha(out), labels: withoutShipLabel(item) });
|
|
266
|
+
this.afterShip();
|
|
267
|
+
} else {
|
|
268
|
+
this.store.shipFailed(product.slug, item.id, { note: failureNote(out), labels: withoutShipLabel(item) });
|
|
269
|
+
// Straight back to the session that wrote the change. Not through the
|
|
270
|
+
// reply on the row: that field is hers, and the app's words written
|
|
271
|
+
// there would be drawn as hers and could not replace hers anyway.
|
|
272
|
+
this.handBack(item, failureReply(out));
|
|
273
|
+
}
|
|
274
|
+
} catch (e) {
|
|
275
|
+
this.log.warn?.(`zero: could not record the ship of ${item.id}:`, e.message);
|
|
276
|
+
}
|
|
277
|
+
return { code };
|
|
278
|
+
}
|
|
279
|
+
}
|