@ctrl-spc/cs 0.6.0 → 0.7.1
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/dist/agents.js +70 -5
- package/dist/autostart.js +15 -1
- package/dist/browser.js +386 -0
- package/dist/codebases.js +15 -0
- package/dist/codex-home.js +562 -0
- package/dist/companion.js +9 -12
- package/dist/config.js +23 -0
- package/dist/daemon.js +33 -3
- package/dist/env.js +42 -0
- package/dist/failure-reason.js +98 -0
- package/dist/index.js +1 -1
- package/dist/mcp.js +7627 -298
- package/dist/orchestrator.js +6011 -0
- package/dist/panel3/answer.js +166 -0
- package/dist/panel3/checkout.js +29 -0
- package/dist/panel3/cli.js +83 -0
- package/dist/panel3/client.js +181 -0
- package/dist/panel3/coordinator.js +18 -0
- package/dist/panel3/presence.js +162 -0
- package/dist/panel3/prompt.js +945 -0
- package/dist/panel3/run.js +2516 -0
- package/dist/panel3/say.js +262 -0
- package/dist/panel3/secrets.js +98 -0
- package/dist/panel3/session.js +128 -0
- package/dist/panel3/show.js +997 -0
- package/dist/panel3/spawn.js +565 -0
- package/dist/panel3/tools.js +1906 -0
- package/dist/presence.js +178 -6
- package/dist/win-shell.js +162 -0
- package/dist/work-context.js +1484 -0
- package/dist/workflows.js +68 -0
- package/package.json +3 -2
package/dist/daemon.js
CHANGED
|
@@ -1,23 +1,53 @@
|
|
|
1
1
|
import { ensureAutostart } from './autostart.js';
|
|
2
2
|
import { startPresence, stopPresence } from './presence.js';
|
|
3
|
+
/* ═══ THE ONE IMPORT ANYTHING OUTSIDE `panel3/` MAKES INTO IT. ═══ Named in
|
|
4
|
+
`.implementations/19-agent-panel-v3/conventions.md` and enforced by
|
|
5
|
+
`test/panel3-isolation.contract.test.mjs`, whose MOUNTS map allows this file
|
|
6
|
+
exactly this specifier. A second one fails the suite. See `startPanel`. */
|
|
7
|
+
import { startPanel } from './panel3/run.js';
|
|
3
8
|
/**
|
|
4
9
|
* The terminal presence daemon (`cs start`). Comes online and heartbeats until
|
|
5
10
|
* the process is signalled. Under launchd/KeepAlive (autostart) a hard crash is
|
|
6
11
|
* restarted by the OS. The companion server (`cs open`) shares the same presence
|
|
7
12
|
* loop via presence.ts, so the two front-ends never diverge.
|
|
13
|
+
*
|
|
14
|
+
* ═══ AND SINCE recovery-1 SLICE 4 IT ALSO ANSWERS AGENT PANEL CARDS. ═══ Lane's
|
|
15
|
+
* ruling (2026-08-21): *the user experience must never require them to launch or
|
|
16
|
+
* authenticate multiple CLIs. They launch the CLI with `cs start`.* Before this
|
|
17
|
+
* the panel was a second binary with a second sign-in, so a stranded card could
|
|
18
|
+
* name the right machine and print a command that started the wrong daemon.
|
|
19
|
+
*
|
|
20
|
+
* THE PANEL GETS THE SESSION THIS ALREADY HOLDS, never one of its own. Two
|
|
21
|
+
* clients in one process is two refresh loops on one rotating refresh token,
|
|
22
|
+
* both writing `session.json`.
|
|
23
|
+
*
|
|
24
|
+
* ═══ THE COMPANION DELIBERATELY DOES NOT DO THIS. ═══ Two panel loops on one
|
|
25
|
+
* machine id and one harness contend on a single `panel3_machines` row and each
|
|
26
|
+
* would sweep the other's live runs. `cs start` is the launch the ruling names,
|
|
27
|
+
* and `ensureAutostart` below installs exactly it, so an existing login item
|
|
28
|
+
* starts doing the right thing with nothing else typed.
|
|
8
29
|
*/
|
|
9
30
|
export async function runDaemon() {
|
|
10
31
|
ensureAutostart(); // default-on: install the login item unless the user opted out
|
|
11
|
-
const { machineName, agents } = await startPresence();
|
|
32
|
+
const { machineName, agents, client } = await startPresence();
|
|
33
|
+
const panel = startPanel(client);
|
|
12
34
|
console.log(`CTRL+SPC — this computer: ${machineName}`);
|
|
13
35
|
console.log(`Agents detected: ${agents.length ? agents.join(', ') : 'none'}`);
|
|
14
|
-
console.log('Online. Heartbeating presence. Ctrl-C to stop.');
|
|
36
|
+
console.log('Online. Heartbeating presence and answering cards. Ctrl-C to stop.');
|
|
15
37
|
let stopping = false;
|
|
16
38
|
async function shutdown() {
|
|
17
39
|
if (stopping)
|
|
18
40
|
return;
|
|
19
41
|
stopping = true;
|
|
20
|
-
|
|
42
|
+
/* ═══ ONE HANDLER, BOTH WRITES, AND THEN ONE EXIT. ═══ A machine that has
|
|
43
|
+
gone has to read gone in both places: `cliv2_agents` for the title bar's
|
|
44
|
+
machines chip, and `panel3_machines.stopped_at` for the card that would
|
|
45
|
+
otherwise go on saying an agent is working. Two handlers each calling
|
|
46
|
+
`process.exit` would let whichever finished first kill the other's write,
|
|
47
|
+
so the panel's own signal handling stands down when it is started from
|
|
48
|
+
here (see `run()`). Both are best-effort and neither throws, which is why
|
|
49
|
+
they can settle together. */
|
|
50
|
+
await Promise.all([panel.stop(), stopPresence({ markOffline: true })]);
|
|
21
51
|
process.exit(0);
|
|
22
52
|
}
|
|
23
53
|
process.on('SIGINT', () => void shutdown());
|
package/dist/env.js
CHANGED
|
@@ -10,6 +10,48 @@ export const SUPABASE_KEY = process.env.CTRL_SPC_SUPABASE_KEY || 'sb_publishable
|
|
|
10
10
|
export const HEARTBEAT_INTERVAL_MS = 10_000;
|
|
11
11
|
/** Command (ping) poll cadence. */
|
|
12
12
|
export const COMMAND_POLL_INTERVAL_MS = 3_000;
|
|
13
|
+
/** Orchestrator listener cadence (feature 16, Slice 3). The interval a designated
|
|
14
|
+
* machine waits before looking for a new loose todo — and therefore the worst
|
|
15
|
+
* case for ux.md mock state E step 1, "Working with no agent named. Present for
|
|
16
|
+
* at most one poll interval". Matched to the command poll: both are one small
|
|
17
|
+
* indexed read against the user's own rows, and a second cadence to reason
|
|
18
|
+
* about would buy nothing. Not faster — the panel is realtime, so the user sees
|
|
19
|
+
* their own todo appear instantly; this interval only bounds how long before an
|
|
20
|
+
* agent is NAMED on it. */
|
|
21
|
+
export const ORCHESTRATOR_POLL_INTERVAL_MS = 3_000;
|
|
22
|
+
/** !Cleanup Phase 7 Slice 1 (I21, I24) — HOW MANY AGENTS MAY RUN AT ONCE.
|
|
23
|
+
*
|
|
24
|
+
* Was structurally ONE (a boolean `busy` on the listener state), which is the
|
|
25
|
+
* defect behind the user's first requirement: a second request typed seconds
|
|
26
|
+
* after the first sat untouched until the first finished, for up to the whole
|
|
27
|
+
* spawn timeout.
|
|
28
|
+
*
|
|
29
|
+
* THREE, and bounded on purpose. Each run is a real headless `claude` process
|
|
30
|
+
* with real memory and real API cost, so a burst of twenty requests must not
|
|
31
|
+
* fork twenty agents. Three is enough that a question is never stuck behind a
|
|
32
|
+
* build — the case the user named — while staying comfortable on a laptop that
|
|
33
|
+
* is also running the user's editor and browser.
|
|
34
|
+
*
|
|
35
|
+
* Overridable per machine, because the right number on a 64 GB desktop is not
|
|
36
|
+
* the right number on a laptop.
|
|
37
|
+
*
|
|
38
|
+
* THE REAL CEILING TURNED OUT NOT TO BE THE MACHINE. The Phase 7 Slice 1 walk
|
|
39
|
+
* ran three concurrent Opus workers and two of them came back `claude exited 1`
|
|
40
|
+
* while the third answered normally — the agent binary was fine when run by
|
|
41
|
+
* hand, and the stream carried a `rate_limit_event` for the five-hour window.
|
|
42
|
+
* So the binding constraint on concurrency is the user's API rate limit, not
|
|
43
|
+
* RAM or CPU, and raising this number past what the account allows converts
|
|
44
|
+
* work that would have queued into work that FAILS and burns attempts. A
|
|
45
|
+
* rate-limited run is currently indistinguishable from a crashed one, which is
|
|
46
|
+
* worth fixing before this default goes any higher. */
|
|
47
|
+
export const ORCHESTRATOR_MAX_CONCURRENT = Number(process.env.CTRL_SPC_V2_MAX_CONCURRENT) || 3;
|
|
48
|
+
/** Hard ceiling on one headless agent run before it is killed and the todo is
|
|
49
|
+
* released for retry. Generous on purpose: the spawned agent does REAL WORK
|
|
50
|
+
* (ux.md § Purpose) — it may cut a worktree, run a build, read a codebase — so a
|
|
51
|
+
* short timeout would kill legitimate work and hand the user a released todo
|
|
52
|
+
* that keeps failing the same way. It exists only so a wedged process cannot
|
|
53
|
+
* hold the single in-flight slot forever. */
|
|
54
|
+
export const ORCHESTRATOR_SPAWN_TIMEOUT_MS = 30 * 60_000;
|
|
13
55
|
/** How long a work session stays "working" after the agent's last ctrl-spc tool
|
|
14
56
|
* call, with no further activity. The companion keeps last_seen_at fresh within
|
|
15
57
|
* this window; once it lapses (agent finished, Ctrl-C'd, crashed, or asleep) the
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 18a SLICE 5: WHAT THE USER READS WHEN AN ATTEMPT FAILS.
|
|
3
|
+
*
|
|
4
|
+
* .implementations/18-orchestrator/18a-you-answer-and-the-work-continues/ux.md
|
|
5
|
+
* § "Slice 5. A failure you did not cause reads like the machine"
|
|
6
|
+
*
|
|
7
|
+
* The daemon's internal error strings are exit statuses: `claude exited 1`,
|
|
8
|
+
* `claude exited ENOENT`, `codex produced no output`. They are the right thing
|
|
9
|
+
* to LOG, and they are what every existing test asserts on, so they are left
|
|
10
|
+
* exactly as they are. What was wrong is that the same string reached the card.
|
|
11
|
+
*
|
|
12
|
+
* An exit code is not an explanation. It is identical whether the binary is
|
|
13
|
+
* missing, the account is rate limited, or the agent genuinely crashed, and the
|
|
14
|
+
* user cannot act on any of them from the number. This translates at the point
|
|
15
|
+
* the reason is STORED, so the log keeps the diagnosis and the card gets the
|
|
16
|
+
* sentence.
|
|
17
|
+
*
|
|
18
|
+
* THE FALLBACK IS HONEST, not a guess. An error this does not recognise becomes
|
|
19
|
+
* "the agent stopped without finishing", which is true of every one of them, and
|
|
20
|
+
* the technical string is still in the daemon log for whoever needs it. Inventing
|
|
21
|
+
* a cause from an exit code would be the same defect wearing better words.
|
|
22
|
+
*/
|
|
23
|
+
/** How many times the orchestrator dispatches before giving up.
|
|
24
|
+
* `MAX_ATTEMPTS` in orchestrator.ts is the definition; this module does not
|
|
25
|
+
* need it, but the web app's copy of the number is documented against the same
|
|
26
|
+
* fact: it is deliberately not configurable, so a constant cannot drift into a
|
|
27
|
+
* lie the way a configurable one would. */
|
|
28
|
+
/**
|
|
29
|
+
* The agent's name as the user picked it in the machine popover.
|
|
30
|
+
*
|
|
31
|
+
* Shared rather than inlined because 18a Slice 7 composes its own sentence: the
|
|
32
|
+
* liveness watch does not have an error string to translate, it has a FACT (the
|
|
33
|
+
* process is gone), and a sentence written from that fact is more honest than
|
|
34
|
+
* one matched out of a string nobody wrote.
|
|
35
|
+
*
|
|
36
|
+
* An agent this does not know about is passed through rather than mangled.
|
|
37
|
+
*/
|
|
38
|
+
export function agentDisplayName(agent) {
|
|
39
|
+
return agent === 'claude' ? 'Claude' : agent === 'codex' ? 'Codex' : agent;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Plain words for a failed attempt, from the daemon's own error string plus
|
|
43
|
+
* whatever the child printed to stderr.
|
|
44
|
+
*
|
|
45
|
+
* `agent` is capitalised into the sentence because the user picked it by name
|
|
46
|
+
* ("Claude", "Codex") and a sentence about "the agent" reads as being about
|
|
47
|
+
* something they did not choose.
|
|
48
|
+
*/
|
|
49
|
+
export function plainFailureReason(agent, error, stderr = '') {
|
|
50
|
+
const name = agentDisplayName(agent);
|
|
51
|
+
const haystack = `${error}\n${stderr}`.toLowerCase();
|
|
52
|
+
/* THE COMMAND IS NOT THERE. `ENOENT` from spawn is the fixture the scenario
|
|
53
|
+
uses (the agent removed from PATH) and the single most likely real cause on
|
|
54
|
+
a fresh machine. It is also the one the user can fix in a minute, which is
|
|
55
|
+
why it is worth naming rather than folding into the fallback. */
|
|
56
|
+
if (haystack.includes('enoent') || haystack.includes('command not found')) {
|
|
57
|
+
return `${name} is not installed on this machine, or is not on its PATH.`;
|
|
58
|
+
}
|
|
59
|
+
/* RATE LIMITED. ux.md records the measurement this came from: on 2026-08-07
|
|
60
|
+
two of three concurrent runs died on the account's rate limit while the same
|
|
61
|
+
work succeeded alone. It is the reason the cap stays at three, and until now
|
|
62
|
+
it was indistinguishable from a crash. */
|
|
63
|
+
if (haystack.includes('rate limit') || haystack.includes('rate_limit')
|
|
64
|
+
|| haystack.includes('429') || haystack.includes('overloaded')) {
|
|
65
|
+
return `${name}'s usage limit is in effect on this account, so the run could not start.`;
|
|
66
|
+
}
|
|
67
|
+
/* SIGNED OUT. Recoverable by the user, and silent otherwise: the run just
|
|
68
|
+
fails, over and over, for as long as the session is stale.
|
|
69
|
+
|
|
70
|
+
18a SLICE 7 ADDED THE OAUTH AND 401 SIGNATURES, from the real thing rather
|
|
71
|
+
than from imagination. The Windows machine's Claude Code had a stale token
|
|
72
|
+
and said, on STDOUT as a stream-json result: `"error":
|
|
73
|
+
"authentication_failed"` and `API Error: 401 OAuth access token has
|
|
74
|
+
expired.` The word "authentication" above would have matched that, but only
|
|
75
|
+
once stdout was being read at all, which is the other half of the fix. `401`
|
|
76
|
+
and `oauth` are here so the same failure is caught when it arrives in
|
|
77
|
+
shorter words. */
|
|
78
|
+
if (haystack.includes('unauthorized') || haystack.includes('not logged in')
|
|
79
|
+
|| haystack.includes('authentication') || haystack.includes('oauth')
|
|
80
|
+
|| haystack.includes('401')) {
|
|
81
|
+
return `${name} is not signed in on this machine.`;
|
|
82
|
+
}
|
|
83
|
+
/* THE RUN WAS KILLED, by the timeout or by a signal from outside. Not the
|
|
84
|
+
user's Stop, which never reaches here: a stopped run is settled with its own
|
|
85
|
+
flag well before this. */
|
|
86
|
+
if (haystack.includes('timed out')) {
|
|
87
|
+
return `The run was stopped for taking too long.`;
|
|
88
|
+
}
|
|
89
|
+
if (haystack.includes('killed by')) {
|
|
90
|
+
return `${name} was killed on this machine before it finished.`;
|
|
91
|
+
}
|
|
92
|
+
/* IT RAN AND SAID NOTHING. A clean exit with no answer, which is a real state
|
|
93
|
+
and reads very differently from a crash. */
|
|
94
|
+
if (haystack.includes('produced no output')) {
|
|
95
|
+
return `${name} finished without producing an answer.`;
|
|
96
|
+
}
|
|
97
|
+
return `${name} stopped without finishing.`;
|
|
98
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ const HELP = `cs — CTRL+SPC
|
|
|
11
11
|
cs Open the Companion app (the front door)
|
|
12
12
|
cs open Open the Companion app in your browser
|
|
13
13
|
cs login Sign in from the terminal and link this computer
|
|
14
|
-
cs start Come online
|
|
14
|
+
cs start Come online and answer cards, no window (used by auto-start)
|
|
15
15
|
cs status Show sign-in state, computer, and detected agents
|
|
16
16
|
cs autostart on Come online automatically at login
|
|
17
17
|
cs autostart off Stop coming online at login
|