@ctrl-spc/cs 0.7.14 → 0.7.16
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 +50 -3
- package/dist/agents.js +175 -1
- package/dist/autostart.js +103 -122
- package/dist/codex-home.js +14 -11
- package/dist/companion-ui.js +54 -6
- package/dist/companion.js +86 -169
- package/dist/config.js +199 -17
- package/dist/daemon-lifecycle.js +575 -0
- package/dist/daemon-lock.js +149 -42
- package/dist/daemon-processes.js +860 -0
- package/dist/daemon.js +14 -46
- package/dist/darwin-coalition.js +340 -0
- package/dist/failure-reason.js +76 -16
- package/dist/index.js +75 -76
- package/dist/login.js +9 -9
- package/dist/mcp.js +55 -56
- package/dist/native/darwin-coalition +0 -0
- package/dist/native/darwin-coalition.build.json +1 -0
- package/dist/native/darwin-coalition.c +145 -0
- package/dist/orchestrator.js +892 -575
- package/dist/panel3/coordinator.js +3 -1
- package/dist/panel3/presence.js +1 -1
- package/dist/panel3/run.js +996 -558
- package/dist/panel3/spawn.js +85 -23
- package/dist/panel3/tools.js +5 -0
- package/dist/presence-heartbeat.js +3 -0
- package/dist/presence.js +271 -135
- package/dist/supabase.js +173 -37
- package/dist/win-shell.js +464 -1
- package/dist/windows-job.js +312 -0
- package/package.json +4 -3
package/dist/panel3/spawn.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { nativeFailureKind, failureMessage } from '../failure-reason.js';
|
|
2
|
+
import { CodexHomeFailure } from '../codex-home.js';
|
|
1
3
|
/**
|
|
2
4
|
* ═══ AGENT PANEL v3: running one headless agent and reading what it said. ═══
|
|
3
5
|
*
|
|
@@ -123,7 +125,7 @@ import { spawn as spawnChild } from 'node:child_process';
|
|
|
123
125
|
import { randomUUID } from 'node:crypto';
|
|
124
126
|
import { agentPath } from '../agents.js';
|
|
125
127
|
import { ensureCodexRunHome, ensurePanel3CodexOwnerHome, removeCodexRunHome, } from '../codex-home.js';
|
|
126
|
-
import { windowsSafeSpawn } from '../win-shell.js';
|
|
128
|
+
import { windowsSafeSpawn, spawnOwnedProcess, releaseOwnedProcess } from '../win-shell.js';
|
|
127
129
|
import { modelChoiceRules } from './prompt.js';
|
|
128
130
|
const AGENT_VAR = 'CTRL_SPC_V3_AGENT';
|
|
129
131
|
/**
|
|
@@ -181,6 +183,7 @@ export function agentArgs(level, toolsUrl, agent = harness(), platform = process
|
|
|
181
183
|
return [
|
|
182
184
|
// `-p` with the prompt on stdin. See the header.
|
|
183
185
|
'-p',
|
|
186
|
+
'--output-format', 'json',
|
|
184
187
|
'--mcp-config',
|
|
185
188
|
JSON.stringify({ mcpServers: { [SERVER]: { type: 'http', url: toolsUrl } } }),
|
|
186
189
|
'--strict-mcp-config',
|
|
@@ -297,6 +300,9 @@ function runKey(toolsUrl) {
|
|
|
297
300
|
* present.
|
|
298
301
|
*/
|
|
299
302
|
export function codexAnswer(stdout, exitCode = 0, stderr = '') {
|
|
303
|
+
const failureKind = nativeFailureKind(stdout, stderr);
|
|
304
|
+
if (failureKind !== 'unknown')
|
|
305
|
+
return { ok: false, failureKind, retryable: failureKind === 'transient', reason: failureMessage('codex', failureKind) };
|
|
300
306
|
let text = null;
|
|
301
307
|
let failure = null;
|
|
302
308
|
for (const line of stdout.split('\n')) {
|
|
@@ -341,16 +347,26 @@ export function codexAnswer(stdout, exitCode = 0, stderr = '') {
|
|
|
341
347
|
}
|
|
342
348
|
return { ok: true, text: text.trim() };
|
|
343
349
|
}
|
|
344
|
-
/**
|
|
350
|
+
/** The native result envelope distinguishes a provider failure from answer prose. */
|
|
345
351
|
export function claudeAnswer(stdout, exitCode = 0, stderr = '') {
|
|
352
|
+
const failureKind = nativeFailureKind(stdout, stderr);
|
|
353
|
+
if (failureKind !== 'unknown')
|
|
354
|
+
return { ok: false, failureKind, retryable: failureKind === 'transient', reason: failureMessage('claude', failureKind) };
|
|
346
355
|
if (/^\[claude-code:unrecognized_model\]/m.test(stdout + '\n' + stderr)) {
|
|
347
|
-
return { ok: false, retryable: false, reason: 'Claude rejected the selected model. Choose an available model and retry.' };
|
|
356
|
+
return { ok: false, failureKind: 'invalid-model', retryable: false, reason: 'Claude rejected the selected model. Choose an available model and retry.' };
|
|
348
357
|
}
|
|
349
358
|
if (exitCode !== 0)
|
|
350
359
|
return { ok: false, reason: `claude exited ${exitCode}${tail(stderr) || tail(stdout)}` };
|
|
351
360
|
if (stdout.trim() === '')
|
|
352
361
|
return { ok: false, reason: `claude exited 0 and said nothing${tail(stderr)}` };
|
|
353
|
-
|
|
362
|
+
try {
|
|
363
|
+
const result = JSON.parse(stdout);
|
|
364
|
+
if (result?.type === 'result' && result.is_error === false && typeof result.result === 'string' && result.result.trim()) {
|
|
365
|
+
return { ok: true, text: result.result.trim() };
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
catch { /* A malformed native result is not a completed answer. */ }
|
|
369
|
+
return { ok: false, reason: 'Claude did not return a completed result.' };
|
|
354
370
|
}
|
|
355
371
|
/** Enough for any answer a person reads, and a ceiling so a runaway process
|
|
356
372
|
* cannot exhaust this daemon's memory. */
|
|
@@ -405,11 +421,16 @@ function tail(text, chars = 500) {
|
|
|
405
421
|
* process still alive" after the daemon that started it has been killed. So the
|
|
406
422
|
* caller gets the pid immediately, writes it, and then waits.
|
|
407
423
|
*/
|
|
408
|
-
export function startAgent(prompt, level, toolsUrl, cwd, ownerSession, settings = {}) {
|
|
409
|
-
|
|
424
|
+
export function startAgent(prompt, level, toolsUrl, cwd, ownerSession, settings = {}, lifecycle) {
|
|
425
|
+
if (lifecycle?.interrupted())
|
|
426
|
+
return { pid: null, session: Promise.resolve(null),
|
|
427
|
+
answered: Promise.resolve({ ok: false, reason: 'Work was interrupted by the service command.' }),
|
|
428
|
+
interrupted: lifecycle.interrupted, completed: lifecycle.complete };
|
|
429
|
+
const failed = (reason, failureKind = 'unknown') => ({
|
|
410
430
|
pid: null,
|
|
411
431
|
session: Promise.resolve(null),
|
|
412
|
-
answered: Promise.resolve({ ok: false, reason }),
|
|
432
|
+
answered: Promise.resolve({ ok: false, reason, failureKind, retryable: false }),
|
|
433
|
+
...(lifecycle ? { interrupted: lifecycle.interrupted, completed: lifecycle.complete } : {}),
|
|
413
434
|
});
|
|
414
435
|
let agent;
|
|
415
436
|
try {
|
|
@@ -429,7 +450,7 @@ export function startAgent(prompt, level, toolsUrl, cwd, ownerSession, settings
|
|
|
429
450
|
prompt = `${modelChoiceRules(agent)}\n\n${prompt}`;
|
|
430
451
|
const bin = agentPath(agent);
|
|
431
452
|
if (!bin) {
|
|
432
|
-
return failed(`${agent} is not installed on this machine
|
|
453
|
+
return failed(`${agent} is not installed on this machine`, 'missing-binary');
|
|
433
454
|
}
|
|
434
455
|
/* macOS uses the proven per-run home. Windows keeps its installed home so the
|
|
435
456
|
Desktop runtime's ACL-bound sandbox helpers remain valid; `codexArgs`
|
|
@@ -443,17 +464,17 @@ export function startAgent(prompt, level, toolsUrl, cwd, ownerSession, settings
|
|
|
443
464
|
// pass this and keep today's behaviour; see `codex-home.ts`.
|
|
444
465
|
: ensureCodexRunHome({ url: toolsUrl }, null, runKey(toolsUrl), false)
|
|
445
466
|
: null;
|
|
446
|
-
if (
|
|
447
|
-
return failed(
|
|
448
|
-
+ 'and nothing else. Sign codex in on the machine running this, and start it again.');
|
|
449
|
-
}
|
|
467
|
+
if (home instanceof CodexHomeFailure)
|
|
468
|
+
return failed(home.message, home.kind);
|
|
450
469
|
const { args, shell } = windowsSafeSpawn(bin, ARGS);
|
|
451
470
|
let child;
|
|
452
471
|
try {
|
|
453
|
-
|
|
472
|
+
lifecycle?.beforeSpawn();
|
|
473
|
+
const options = {
|
|
454
474
|
// The repo-wide invariant for ANY child process (MEMORY: "Windows silence
|
|
455
475
|
// decision"). A user must never see a console flash.
|
|
456
476
|
windowsHide: true,
|
|
477
|
+
detached: process.platform !== 'win32',
|
|
457
478
|
shell,
|
|
458
479
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
459
480
|
// NEVER the daemon's inherited cwd, which under a launchd login item is
|
|
@@ -465,14 +486,17 @@ export function startAgent(prompt, level, toolsUrl, cwd, ownerSession, settings
|
|
|
465
486
|
said. Claude is spawned with the environment untouched, exactly as
|
|
466
487
|
before. */
|
|
467
488
|
...(home ? { env: { ...process.env, CODEX_HOME: home } } : {}),
|
|
468
|
-
}
|
|
489
|
+
};
|
|
490
|
+
child = lifecycle
|
|
491
|
+
? spawnOwnedProcess(bin, args, options, lifecycle.id)
|
|
492
|
+
: spawnChild(bin, args, options);
|
|
469
493
|
}
|
|
470
494
|
catch (err) {
|
|
471
495
|
// NOT `err.message`, which names the binary's absolute path. See
|
|
472
496
|
// `couldNotStart`.
|
|
473
497
|
if (home && !ownerSession)
|
|
474
498
|
removeCodexRunHome(home);
|
|
475
|
-
return failed(couldNotStart(err, agent));
|
|
499
|
+
return failed(couldNotStart(err, agent), 'preparation-unavailable');
|
|
476
500
|
}
|
|
477
501
|
let resolveSession;
|
|
478
502
|
let sessionSettled = false;
|
|
@@ -488,6 +512,7 @@ export function startAgent(prompt, level, toolsUrl, cwd, ownerSession, settings
|
|
|
488
512
|
else if (agent === 'claude') {
|
|
489
513
|
observeSession(launchedOwnerSession?.resumeSessionId ?? launchedOwnerSession?.freshSessionId ?? null);
|
|
490
514
|
}
|
|
515
|
+
let registration = Promise.resolve();
|
|
491
516
|
const answered = new Promise((resolve) => {
|
|
492
517
|
let stdout = '';
|
|
493
518
|
let stderr = '';
|
|
@@ -512,11 +537,32 @@ export function startAgent(prompt, level, toolsUrl, cwd, ownerSession, settings
|
|
|
512
537
|
/* THE CREDENTIAL COPY GOES WHEN THE RUN DOES. `codex-home.ts` calls this
|
|
513
538
|
the primary reclaim and the startup sweep the backstop; a home left
|
|
514
539
|
behind holds a copy of the user's codex credential. */
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
540
|
+
void (async () => {
|
|
541
|
+
await registration.catch(() => { });
|
|
542
|
+
if (lifecycle && child.pid) {
|
|
543
|
+
// A wrapper's close is not tree exit. Keep the run owned until the
|
|
544
|
+
// actual descendants end; lifecycle commands have their own deadline.
|
|
545
|
+
let warned = false;
|
|
546
|
+
while (true) {
|
|
547
|
+
try {
|
|
548
|
+
await lifecycle.exited();
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
catch (error) {
|
|
552
|
+
if (!warned) {
|
|
553
|
+
console.warn(`Owned agent execution has not ended: ${error.message}`);
|
|
554
|
+
warned = true;
|
|
555
|
+
}
|
|
556
|
+
await new Promise((done) => setTimeout(done, 250));
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (home && !ownerSession)
|
|
561
|
+
removeCodexRunHome(home);
|
|
562
|
+
if (!sessionSettled)
|
|
563
|
+
observeSession(null);
|
|
564
|
+
resolve(answer);
|
|
565
|
+
})();
|
|
520
566
|
};
|
|
521
567
|
child.stdout?.on('data', (d) => {
|
|
522
568
|
stdout = collect(stdout, String(d));
|
|
@@ -543,7 +589,7 @@ export function startAgent(prompt, level, toolsUrl, cwd, ownerSession, settings
|
|
|
543
589
|
WITHOUT the message, which is where node puts the binary's absolute path.
|
|
544
590
|
The same failure as the throw above, arriving asynchronously. */
|
|
545
591
|
child.on('error', (err) => {
|
|
546
|
-
finish({ ok: false, reason: couldNotStart(err, agent) });
|
|
592
|
+
finish({ ok: false, reason: couldNotStart(err, agent), failureKind: 'preparation-unavailable', retryable: false });
|
|
547
593
|
});
|
|
548
594
|
child.on('close', (code, signal) => {
|
|
549
595
|
/* ═══ THREE OUTCOMES, AND ONLY ONE OF THEM IS AN ANSWER. ═══ A non-zero
|
|
@@ -582,7 +628,23 @@ export function startAgent(prompt, level, toolsUrl, cwd, ownerSession, settings
|
|
|
582
628
|
handler the EPIPE is an unhandled stream error and takes the daemon down
|
|
583
629
|
with it. */
|
|
584
630
|
child.stdin?.on('error', () => { });
|
|
585
|
-
|
|
631
|
+
if (lifecycle) {
|
|
632
|
+
registration = lifecycle.register(child, agent);
|
|
633
|
+
void registration.then(() => {
|
|
634
|
+
if (lifecycle.interrupted())
|
|
635
|
+
return;
|
|
636
|
+
releaseOwnedProcess(child);
|
|
637
|
+
child.stdin?.end(prompt);
|
|
638
|
+
}).catch((error) => {
|
|
639
|
+
// No prompt was delivered. The held native handle may be cancelled
|
|
640
|
+
// even when durable ownership registration itself failed.
|
|
641
|
+
child.stdin?.destroy();
|
|
642
|
+
child.kill('SIGKILL');
|
|
643
|
+
finish({ ok: false, reason: `The agent could not be safely registered: ${error.message}` });
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
else
|
|
647
|
+
child.stdin?.end(prompt);
|
|
586
648
|
});
|
|
587
|
-
return { pid: child.pid ?? null, session, answered };
|
|
649
|
+
return { pid: child.pid ?? null, session, answered, ...(lifecycle ? { interrupted: lifecycle.interrupted, completed: lifecycle.complete } : {}) };
|
|
588
650
|
}
|
package/dist/panel3/tools.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { clientSessionCurrent } from '../supabase.js';
|
|
1
2
|
import { createEpicHandler, createSprintHandler, listStructureHandler, updateStructureHandler, listStructureArtifactsHandler, getStructureArtifactHandler, searchAgentCardsHandler, createStructureArtifactHandler, updateStructureArtifactHandler } from '../product-tools.js';
|
|
2
3
|
import { listArtifactFoldersHandler, setArtifactFolderHandler, workItemDependencyHandler } from '../product-tools.js';
|
|
3
4
|
/**
|
|
@@ -3472,6 +3473,10 @@ export async function startToolsServer(client, dispatch, recoverLanding) {
|
|
|
3472
3473
|
res.writeHead(status, { 'Content-Type': 'text/plain' }).end(why);
|
|
3473
3474
|
};
|
|
3474
3475
|
async function handle(req, res) {
|
|
3476
|
+
if (!clientSessionCurrent(client)) {
|
|
3477
|
+
fail(res, 401, 'This connection no longer owns the local sign-in.');
|
|
3478
|
+
return;
|
|
3479
|
+
}
|
|
3475
3480
|
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
3476
3481
|
const parts = url.pathname.startsWith('/mcp/')
|
|
3477
3482
|
? url.pathname.slice('/mcp/'.length).split('/').filter(Boolean)
|
|
@@ -12,5 +12,8 @@ export function buildPresenceHeartbeatPayload(input, seenAt = new Date()) {
|
|
|
12
12
|
// A beating heart is not stopped: clears the stamp a clean shutdown left,
|
|
13
13
|
// so a machine that comes back reads online again.
|
|
14
14
|
stopped_at: null,
|
|
15
|
+
session_state: 'authenticated',
|
|
16
|
+
session_observed_at: seenAt.toISOString(),
|
|
17
|
+
harness_auth: input.harnessAuth ?? {},
|
|
15
18
|
};
|
|
16
19
|
}
|