spexcode 0.6.5 → 0.6.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.
Files changed (56) hide show
  1. package/node_modules/@spexcode/{spec-cli → session-core}/dist/delivery-queue.d.ts +1 -1
  2. package/node_modules/@spexcode/{spec-cli → session-core}/dist/delivery-queue.js +18 -2
  3. package/node_modules/@spexcode/session-core/dist/index.d.ts +5 -0
  4. package/node_modules/@spexcode/session-core/dist/index.js +5 -0
  5. package/node_modules/@spexcode/session-core/dist/internal.d.ts +3 -0
  6. package/node_modules/@spexcode/session-core/dist/internal.js +3 -0
  7. package/node_modules/@spexcode/session-core/dist/message.d.ts +22 -0
  8. package/node_modules/@spexcode/session-core/dist/message.js +52 -0
  9. package/node_modules/@spexcode/session-core/dist/record-lock.d.ts +7 -0
  10. package/node_modules/@spexcode/session-core/dist/record-lock.js +152 -0
  11. package/node_modules/@spexcode/session-core/dist/runtime-session.d.ts +50 -0
  12. package/node_modules/@spexcode/session-core/dist/runtime-session.js +286 -0
  13. package/node_modules/@spexcode/session-core/dist/session-timeline.d.ts +46 -0
  14. package/node_modules/@spexcode/session-core/dist/session-timeline.js +216 -0
  15. package/node_modules/@spexcode/session-core/package.json +33 -0
  16. package/node_modules/@spexcode/spec-cli/bin/spex.mjs +2 -1
  17. package/node_modules/@spexcode/spec-cli/dist/claude-headless.d.ts +4 -1
  18. package/node_modules/@spexcode/spec-cli/dist/claude-headless.js +13 -4
  19. package/node_modules/@spexcode/spec-cli/dist/cli.js +72 -23
  20. package/node_modules/@spexcode/spec-cli/dist/client.d.ts +2 -1
  21. package/node_modules/@spexcode/spec-cli/dist/client.js +13 -8
  22. package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.d.ts +5 -0
  23. package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.js +112 -0
  24. package/node_modules/@spexcode/spec-cli/dist/doctor.js +7 -1
  25. package/node_modules/@spexcode/spec-cli/dist/gateway-hub.js +7 -5
  26. package/node_modules/@spexcode/spec-cli/dist/gateway.d.ts +1 -0
  27. package/node_modules/@spexcode/spec-cli/dist/gateway.js +44 -20
  28. package/node_modules/@spexcode/spec-cli/dist/graphCache.js +2 -1
  29. package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.js +2 -1
  30. package/node_modules/@spexcode/spec-cli/dist/harness.d.ts +15 -2
  31. package/node_modules/@spexcode/spec-cli/dist/harness.js +174 -54
  32. package/node_modules/@spexcode/spec-cli/dist/help.d.ts +5 -0
  33. package/node_modules/@spexcode/spec-cli/dist/help.js +26 -6
  34. package/node_modules/@spexcode/spec-cli/dist/index.js +3 -3
  35. package/node_modules/@spexcode/spec-cli/dist/listen.d.ts +2 -1
  36. package/node_modules/@spexcode/spec-cli/dist/listen.js +10 -10
  37. package/node_modules/@spexcode/spec-cli/dist/opencode-headless.d.ts +1 -0
  38. package/node_modules/@spexcode/spec-cli/dist/opencode-headless.js +7 -0
  39. package/node_modules/@spexcode/spec-cli/dist/runtime-rotate.d.ts +1 -0
  40. package/node_modules/@spexcode/spec-cli/dist/runtime-rotate.js +58 -0
  41. package/node_modules/@spexcode/spec-cli/dist/session-follow.js +1 -1
  42. package/node_modules/@spexcode/spec-cli/dist/session-timeline.d.ts +6 -46
  43. package/node_modules/@spexcode/spec-cli/dist/session-timeline.js +8 -221
  44. package/node_modules/@spexcode/spec-cli/dist/sessions.d.ts +25 -4
  45. package/node_modules/@spexcode/spec-cli/dist/sessions.js +822 -394
  46. package/node_modules/@spexcode/spec-cli/dist/supervise.js +3 -3
  47. package/node_modules/@spexcode/spec-cli/package.json +6 -4
  48. package/node_modules/@spexcode/spec-core/dist/git.d.ts +4 -0
  49. package/node_modules/@spexcode/spec-core/dist/git.js +32 -0
  50. package/node_modules/@spexcode/spec-core/dist/layout.d.ts +4 -0
  51. package/node_modules/@spexcode/spec-core/package.json +1 -1
  52. package/node_modules/@spexcode/spec-eval/package.json +2 -2
  53. package/node_modules/@spexcode/spec-forge/package.json +2 -2
  54. package/package.json +3 -3
  55. /package/node_modules/@spexcode/{spec-cli → session-core}/dist/session-cursors.d.ts +0 -0
  56. /package/node_modules/@spexcode/{spec-cli → session-core}/dist/session-cursors.js +0 -0
@@ -31,7 +31,12 @@ export type CodexGenerationLedger = Readonly<{
31
31
  export declare function codexGenerationSocketPath(root: string, generationId?: string): string;
32
32
  export declare function legacyCodexGenerationEndpoint(root: string): CodexGenerationEndpoint;
33
33
  export declare function readCodexGenerationLedger(root: string): CodexGenerationLedger;
34
+ export type CodexGenerationRotation = Readonly<{
35
+ previous: CodexGenerationEndpoint;
36
+ current: CodexGenerationEndpoint;
37
+ }>;
34
38
  export declare function ensureCodexCurrentGeneration(root: string, start: (endpoint: CodexGenerationEndpoint) => Promise<void>): Promise<CodexGenerationEndpoint>;
39
+ export declare function rotateCodexCurrentGeneration(root: string, start: (endpoint: CodexGenerationEndpoint) => Promise<void>): Promise<CodexGenerationRotation>;
35
40
  export declare function bindCodexGeneration(root: string, sessionId: string, threadId: string, generationId: string | null): void;
36
41
  export declare function prepareCodexGenerationRegistration(root: string, sessionId: string, threadId: string, generationId: string): void;
37
42
  export declare function commitCodexGenerationRegistration(root: string, sessionId: string, threadId: string, generationId: string): void;
@@ -497,6 +497,118 @@ export async function ensureCodexCurrentGeneration(root, start) {
497
497
  return publishPendingGeneration(root, action.endpoint);
498
498
  }
499
499
  }
500
+ // An operator can move NEW traffic off a current root that is still exactly identifiable but unhealthy. This is
501
+ // deliberately not a restart: bindings remain on the old endpoint, which keeps serving them as draining until
502
+ // its ordinary zero-reference reclamation proof succeeds.
503
+ async function publishRotatedGeneration(root, endpoint) {
504
+ return withLedgerLock(root, async () => {
505
+ let previous = readCodexGenerationLedger(root);
506
+ if (previous.pending !== endpoint.id || !endpointIdentity(endpoint))
507
+ throw new Error('Codex generation rotation CAS lost or candidate identity changed before publication');
508
+ const previousId = previous.current;
509
+ const old = previousId ? previous.generations[previousId] : null;
510
+ if (!previousId || !old || old.state !== 'current')
511
+ throw new Error('canonical Codex generation changed during rotation; retry');
512
+ if (!endpointIdentity(old.endpoint)) {
513
+ // A root proven gone during the finite start window is safe to retire. Ambiguity remains a refusal: a
514
+ // ready candidate stays durably pending for a later explicit retry, rather than guessing at ownership.
515
+ const retired = retireGoneGenerationLocked(root, previous, previousId);
516
+ if (!retired)
517
+ throw new Error('canonical Codex generation became unproven during rotation; refusing to switch traffic');
518
+ previous = retired;
519
+ }
520
+ const generations = {
521
+ ...previous.generations,
522
+ [endpoint.id]: { state: 'current', endpoint },
523
+ };
524
+ if (previous.current)
525
+ generations[previous.current] = { state: 'draining', endpoint: old.endpoint };
526
+ writeLedger(root, previous, { current: endpoint.id, pending: null, generations, bindings: previous.bindings });
527
+ return { previous: old.endpoint, current: endpoint };
528
+ });
529
+ }
530
+ export async function rotateCodexCurrentGeneration(root, start) {
531
+ const deadline = Date.now() + 30_000;
532
+ for (;;) {
533
+ const action = await withLedgerLock(root, async () => {
534
+ let previous = readCodexGenerationLedger(root);
535
+ if (previous.revision === 0 && !existsSync(ledgerPath(root))) {
536
+ const bootstrapped = bootstrapLedger(root);
537
+ previous = writeLedger(root, previous, bootstrapped);
538
+ }
539
+ const current = previous.current ? previous.generations[previous.current] : null;
540
+ if (!current || current.state !== 'current')
541
+ throw new Error('there is no proven canonical app-server generation to switch; launch a Codex session first');
542
+ if (!endpointIdentity(current.endpoint)) {
543
+ if (goneGeneration(current.endpoint))
544
+ throw new Error('canonical Codex generation is already dead; a normal Codex launch will replace it');
545
+ throw new Error('canonical app-server generation is unproven; refusing to switch traffic');
546
+ }
547
+ if (previous.pending) {
548
+ const pending = previous.generations[previous.pending];
549
+ if (!pending || pending.state !== 'starting')
550
+ throw new Error('Codex generation ledger pending rotation is malformed');
551
+ // A prior coordinator may have completed the detached spawn but crashed before the pointer CAS.
552
+ if (endpointIdentity(pending.endpoint))
553
+ return { kind: 'publish', endpoint: pending.endpoint };
554
+ if (pending.reservation && processStartToken(pending.reservation.pid) === pending.reservation.startToken)
555
+ return { kind: 'wait' };
556
+ const generations = { ...previous.generations };
557
+ delete generations[pending.endpoint.id];
558
+ writeLedger(root, previous, { current: previous.current, pending: null, generations, bindings: previous.bindings });
559
+ return { kind: 'retry' };
560
+ }
561
+ const endpoint = newEndpoint(root);
562
+ const startToken = processStartToken(process.pid);
563
+ if (!startToken)
564
+ throw new Error('cannot prove coordinator process identity for Codex generation rotation reservation');
565
+ mkdirSync(dirname(endpoint.pidFile), { recursive: true, mode: 0o700 });
566
+ writeLedger(root, previous, {
567
+ current: previous.current,
568
+ pending: endpoint.id,
569
+ generations: { ...previous.generations, [endpoint.id]: { state: 'starting', endpoint, reservation: { pid: process.pid, startToken } } },
570
+ bindings: previous.bindings,
571
+ });
572
+ return { kind: 'start', endpoint };
573
+ });
574
+ if (action.kind === 'publish')
575
+ return publishRotatedGeneration(root, action.endpoint);
576
+ if (action.kind === 'retry')
577
+ continue;
578
+ if (action.kind === 'wait') {
579
+ if (Date.now() >= deadline)
580
+ throw new Error('Codex generation rotation reservation owner is still live but did not publish; retry after it exits');
581
+ await sleep(50);
582
+ continue;
583
+ }
584
+ try {
585
+ await start(action.endpoint);
586
+ }
587
+ catch (error) {
588
+ await withLedgerLock(root, async () => {
589
+ const previous = readCodexGenerationLedger(root);
590
+ if (previous.pending !== action.endpoint.id || endpointIdentity(action.endpoint))
591
+ return;
592
+ const generations = { ...previous.generations };
593
+ delete generations[action.endpoint.id];
594
+ writeLedger(root, previous, { current: previous.current, pending: null, generations, bindings: previous.bindings });
595
+ });
596
+ throw error;
597
+ }
598
+ if (!await waitForEndpoint(action.endpoint)) {
599
+ await withLedgerLock(root, async () => {
600
+ const previous = readCodexGenerationLedger(root);
601
+ if (previous.pending !== action.endpoint.id || endpointIdentity(action.endpoint))
602
+ return;
603
+ const generations = { ...previous.generations };
604
+ delete generations[action.endpoint.id];
605
+ writeLedger(root, previous, { current: previous.current, pending: null, generations, bindings: previous.bindings });
606
+ });
607
+ throw new Error(`candidate Codex generation ${action.endpoint.id} did not prove a live detached endpoint; current pointer was not switched`);
608
+ }
609
+ return publishRotatedGeneration(root, action.endpoint);
610
+ }
611
+ }
500
612
  export function bindCodexGeneration(root, sessionId, threadId, generationId) {
501
613
  withLedgerLockSync(root, () => {
502
614
  const previous = readCodexGenerationLedger(root);
@@ -518,7 +518,9 @@ function usage() {
518
518
  console.error(`spex doctor — diagnose spec health and how the SpexCode workflow reaches your agent
519
519
  (bare) spec-health findings + delivery report: preconditions · git-hook floor · contract · hooks(+handlers) · backend · footprint
520
520
  --contract print the surface:system contract text (hand it to any agent)
521
- --conflicts detect double-delivery — the same agent reached via loose native delivery AND a plugin bundle (exits non-zero on conflict)`);
521
+ --conflicts detect double-delivery — the same agent reached via loose native delivery AND a plugin bundle (exits non-zero on conflict)
522
+ repair app-server [--launcher <name>]
523
+ prove a fresh app-server, then switch new sessions to it without moving existing sessions`);
522
524
  return 0;
523
525
  }
524
526
  export async function runDoctor(args) {
@@ -530,6 +532,10 @@ export async function runDoctor(args) {
530
532
  return contract();
531
533
  if (args.includes('--conflicts'))
532
534
  return await conflicts();
535
+ if (args[0] === 'repair') {
536
+ const { runDoctorRepairAppServer } = await import('./runtime-rotate.js');
537
+ return await runDoctorRepairAppServer(args);
538
+ }
533
539
  switch (args[0]) {
534
540
  case undefined: return await doctor();
535
541
  case 'contract':
@@ -327,11 +327,13 @@ export function startHubGateway(opts) {
327
327
  socket.once('close', () => upstream.destroy());
328
328
  upstream.once('close', () => socket.destroy());
329
329
  });
330
- const onListen = () => {
331
- const scheme = secure ? 'https' : 'http';
332
- console.log(`[hub] multi-project gateway on ${scheme}://${opts.host ?? '0.0.0.0'}:${port} — /projects + /p/:projectId/*`);
333
- };
334
- listenOrExit(server, port, { host: opts.host, label: opts.label ?? 'hub gateway', cleanup: opts.onBindFail, onListen });
330
+ const scheme = secure ? 'https' : 'http';
331
+ listenOrExit(server, port, {
332
+ host: opts.host,
333
+ label: opts.label ?? 'hub gateway',
334
+ cleanup: opts.onBindFail,
335
+ ready: `[hub] multi-project gateway on ${scheme}://${opts.host ?? '0.0.0.0'}:${port} — /projects + /p/:projectId/*`,
336
+ });
335
337
  return server;
336
338
  }
337
339
  // replay an upgrade's headers with the Cookie header rewritten to exclude the gateway's own cookies.
@@ -22,6 +22,7 @@ export type GatewayOpts = {
22
22
  label?: string;
23
23
  onBindFail?: () => void;
24
24
  projectRoot?: string;
25
+ readyLines?: string[];
25
26
  };
26
27
  export declare function startGateway(opts: GatewayOpts): void;
27
28
  export declare function rawHeaders(req: http.IncomingMessage): string;
@@ -178,17 +178,15 @@ export function startGateway(opts) {
178
178
  // narrows the public gateway's reach. The gate note keys on LOOPBACK, not on host-being-explicit:
179
179
  // an ungated loopback bind is normal, an ungated wide bind is announced — never silent.
180
180
  const isLoopback = opts.host === '127.0.0.1' || opts.host === 'localhost' || opts.host === '::1';
181
- const onListen = () => {
182
- const scheme = secure ? 'https' : 'http';
183
- const label = opts.label ?? 'public mode';
184
- const gate = isLoopback ? '' : ` ${gated ? 'password-gated' : 'OPEN (no password)'}`;
185
- console.log(`[gateway] ${label} on ${scheme}://${isLoopback ? 'localhost' : (opts.host ?? '0.0.0.0')}:${opts.publicPort}${gate}, proxying /api to :${opts.upstreamPort}`);
186
- if (!secure && !isLoopback && !opts.host)
187
- console.log('[gateway] (TLS off — --http)');
188
- };
181
+ const scheme = secure ? 'https' : 'http';
182
+ const label = opts.label ?? 'public mode';
183
+ const gate = isLoopback ? '' : ` — ${gated ? 'password-gated' : 'OPEN (no password)'}`;
184
+ const ready = [...(opts.readyLines ?? []), `[gateway] ${label} on ${scheme}://${isLoopback ? 'localhost' : (opts.host ?? '0.0.0.0')}:${opts.publicPort}${gate}, proxying /api to :${opts.upstreamPort}`];
185
+ if (!secure && !isLoopback && !opts.host)
186
+ ready.push('[gateway] (TLS off --http)');
189
187
  // a busy public port is a hard, loud, non-zero exit — the SAME contract as the supervisor's proxy
190
188
  // (see [[spec-cli]] / listen.ts), so `spex serve` and `spex serve ui` fail a port clash identically.
191
- listenOrExit(server, opts.publicPort, { host: opts.host, label: opts.label ?? 'gateway', cleanup: opts.onBindFail, onListen });
189
+ listenOrExit(server, opts.publicPort, { host: opts.host, label: opts.label ?? 'gateway', cleanup: opts.onBindFail, ready });
192
190
  }
193
191
  // re-serialize an upgrade request's headers for replay against the upstream (exported for the host
194
192
  // gateway's per-project WS pipe, which replays the same way).
@@ -223,6 +221,16 @@ function doLogin(req, res, password, setCookie) {
223
221
  // and would fight Range requests.
224
222
  const COMPRESSIBLE = /^(text\/|application\/(json|javascript|xml)|image\/svg)/;
225
223
  const wantsGzip = (req) => /\bgzip\b/.test(String(req.headers['accept-encoding'] || ''));
224
+ // zlib's larger default table can be counterproductive for minified text: memLevel 5 both compresses it
225
+ // further and lowers each stream's working memory. One policy drives buffered and streamed gzip.
226
+ const GZIP_OPTIONS = { level: 9, memLevel: 5 };
227
+ function appendVary(current, token) {
228
+ const values = (Array.isArray(current) ? current : [current ?? ''])
229
+ .flatMap((value) => value.split(',')).map((value) => value.trim()).filter(Boolean);
230
+ if (!values.some((value) => value === '*' || value.toLowerCase() === token.toLowerCase()))
231
+ values.push(token);
232
+ return values.join(', ');
233
+ }
226
234
  // reverse-proxy an /api request to the loopback supervisor (which forwards to the live child) —
227
235
  // stream-gzipping compressible bodies (measured: the board JSON rides down at under a third).
228
236
  // `path` and `headers` optionally override routing inputs (the host gateway strips its /p/:projectId
@@ -322,16 +330,19 @@ export function proxyHttp(req, res, upstreamPort, path, headers = req.headers, u
322
330
  received.once('close', () => { if (!received.complete)
323
331
  failFromUpstream(); });
324
332
  const type = String(received.headers['content-type'] || '');
325
- const skip = !wantsGzip(req) || received.headers['content-encoding'] || !COMPRESSIBLE.test(type) || type.startsWith('text/event-stream');
326
- if (skip) {
327
- res.writeHead(received.statusCode || 502, received.headers);
333
+ const eligible = !received.headers['content-encoding'] && COMPRESSIBLE.test(type) && !type.startsWith('text/event-stream');
334
+ const responseHeaders = eligible
335
+ ? { ...received.headers, vary: appendVary(received.headers.vary, 'Accept-Encoding') }
336
+ : received.headers;
337
+ if (!eligible || !wantsGzip(req)) {
338
+ res.writeHead(received.statusCode || 502, responseHeaders);
328
339
  received.pipe(res);
329
340
  return;
330
341
  }
331
- const headers = { ...received.headers, 'content-encoding': 'gzip', vary: 'Accept-Encoding' };
342
+ const headers = { ...responseHeaders, 'content-encoding': 'gzip' };
332
343
  delete headers['content-length']; // streamed; the encoded length isn't knowable up front
333
344
  res.writeHead(received.statusCode || 502, headers);
334
- transform = createGzip();
345
+ transform = createGzip(GZIP_OPTIONS);
335
346
  transform.once('error', failFromUpstream);
336
347
  received.pipe(transform).pipe(res);
337
348
  });
@@ -473,17 +484,21 @@ export function serveStatic(req, res, distDir, urlPath) {
473
484
  const type = MIME[extname(file)] || 'application/octet-stream';
474
485
  const cacheControl = /[\\/]assets[\\/]/.test(file) ? 'public, max-age=31536000, immutable' : 'no-cache';
475
486
  const raw = readFileSync(file);
476
- if (wantsGzip(req) && COMPRESSIBLE.test(type)) {
487
+ const compressible = COMPRESSIBLE.test(type);
488
+ const headers = { 'Content-Type': type, 'Cache-Control': cacheControl };
489
+ if (compressible)
490
+ headers.Vary = appendVary(undefined, 'Accept-Encoding');
491
+ if (wantsGzip(req) && compressible) {
477
492
  const mtime = statSync(file).mtimeMs;
478
493
  let hit = gzMemo.get(file);
479
494
  if (!hit || hit.mtime !== mtime) {
480
- hit = { mtime, gz: gzipSync(raw) };
495
+ hit = { mtime, gz: gzipSync(raw, GZIP_OPTIONS) };
481
496
  gzMemo.set(file, hit);
482
497
  }
483
- res.writeHead(200, { 'Content-Type': type, 'Content-Encoding': 'gzip', Vary: 'Accept-Encoding', 'Cache-Control': cacheControl });
498
+ res.writeHead(200, { ...headers, 'Content-Encoding': 'gzip' });
484
499
  return res.end(hit.gz);
485
500
  }
486
- res.writeHead(200, { 'Content-Type': type, 'Cache-Control': cacheControl });
501
+ res.writeHead(200, headers);
487
502
  res.end(raw);
488
503
  }
489
504
  function sendHtml(res, status, html) {
@@ -498,6 +513,15 @@ function sendHtml(res, status, html) {
498
513
  // installed user has no source tree for). See [[packaging]].
499
514
  export function serveDashboardLocal(opts) {
500
515
  const distDir = resolveDistDir();
501
- console.log(`[dashboard] serving ${distDir}, /api → backend :${opts.apiPort}`);
502
- startGateway({ host: opts.host ?? '127.0.0.1', publicPort: opts.port, upstreamPort: opts.apiPort, password: '', tls: null, distDir, label: 'dashboard', projectRoot: opts.projectRoot });
516
+ startGateway({
517
+ host: opts.host ?? '127.0.0.1',
518
+ publicPort: opts.port,
519
+ upstreamPort: opts.apiPort,
520
+ password: '',
521
+ tls: null,
522
+ distDir,
523
+ label: 'dashboard',
524
+ projectRoot: opts.projectRoot,
525
+ readyLines: [`[dashboard] serving ${distDir}, /api → backend :${opts.apiPort}`],
526
+ });
503
527
  }
@@ -3,7 +3,7 @@ import { readFileSync, readdirSync, statSync } from 'node:fs';
3
3
  import { isAbsolute, join, resolve } from 'node:path';
4
4
  import { rebasePublishedSessions } from '@spexcode/spec-core';
5
5
  import { buildBoard, spliceSessions } from './graphSnapshot.js';
6
- import { headSha, repoRoot, withGitAbortSignal } from '@spexcode/spec-core';
6
+ import { headSha, repoRoot, requireGitWorkspace, withGitAbortSignal } from '@spexcode/spec-core';
7
7
  import { listSessionIds, mainBranch, mainCheckout, readPublicRecordEntry, sessionArtifactPath, sessionRecordPath } from '@spexcode/spec-core';
8
8
  import { boardThreads } from './issues.js';
9
9
  import { resolveForgeHost } from '@spexcode/spec-forge/drivers';
@@ -162,6 +162,7 @@ function sessionInputRevision() {
162
162
  }
163
163
  function boardInputRevision(board) {
164
164
  const root = repoRoot();
165
+ requireGitWorkspace(root);
165
166
  const session = sessionInputRevision();
166
167
  // Durable active records are the current root set; a cached ordinary row may be stale and must not replace
167
168
  // them. The one projection-only addition is explicit: listSessions republishes an archived-runtime hazard
@@ -1,4 +1,4 @@
1
- import { loadSpecs } from '@spexcode/spec-core';
1
+ import { loadSpecs, requireGitWorkspace } from '@spexcode/spec-core';
2
2
  import { resolveLayout } from '@spexcode/spec-core';
3
3
  import { listSessions } from './sessions.js';
4
4
  import { driftIndex, historyIndex, repoRoot } from '@spexcode/spec-core';
@@ -12,6 +12,7 @@ import { sessionEvalProjections } from '@spexcode/spec-eval/sessioneval';
12
12
  // The application adapter is the sole reader of runtime/forge state. graph.ts only receives this result.
13
13
  export async function boardSnapshot() {
14
14
  const root = repoRoot();
15
+ requireGitWorkspace(root);
15
16
  const [specs, sessions] = await Promise.all([loadSpecs(), listSessions()]);
16
17
  const layout = await resolveLayout({ activeSessionIds: sessions.map((session) => session.id) });
17
18
  const nodeIds = [...new Set([
@@ -24,6 +24,14 @@ export type FailureSubscription = {
24
24
  close(): void;
25
25
  readonly closed: Promise<string | null>;
26
26
  };
27
+ export type DeliveryTransportState = {
28
+ kind: 'reachable';
29
+ } | {
30
+ kind: 'unproven';
31
+ } | {
32
+ kind: 'unreachable';
33
+ reason: string;
34
+ };
27
35
  export type ProcTable = Map<number, {
28
36
  ppid: number;
29
37
  comm: string;
@@ -137,10 +145,12 @@ export interface Harness {
137
145
  slashCommands(): SlashCommand[];
138
146
  liveness(rec: HarnessLivenessRecord, tmuxAlive: boolean, runtimeDir?: string, pane?: PaneProbe, socketLive?: boolean): 'online' | 'offline';
139
147
  launchReady?(current: () => HarnessLaunchReadyRecord | null, deadline: number): Promise<HarnessLaunchReadinessFence | null>;
140
- leafOwnerNeedle?(rec: HarnessLivenessRecord & {
148
+ launchPayloadProof?: true;
149
+ exactNativeTargetId(rec: HarnessLivenessRecord & {
141
150
  harnessSessionId?: string | null;
142
151
  }): string | null;
143
152
  deliver(rec: HarnessDeliveryRecord, text: string): Promise<DispatchResult>;
153
+ deliveryTransport?(rec: HarnessDeliveryRecord): Promise<DeliveryTransportState>;
144
154
  observeTurnFailures?(rec: HarnessDeliveryRecord, onFailure: (failure: TurnFailure) => void): FailureSubscription;
145
155
  interrupt?(rec: HarnessDeliveryRecord): Promise<DispatchResult>;
146
156
  cleanupRuntime(rec: HarnessLivenessRecord): Promise<void>;
@@ -185,7 +195,7 @@ export interface Harness {
185
195
  resumeArg(rec: {
186
196
  session: string;
187
197
  harnessSessionId?: string | null;
188
- }): string;
198
+ }, pendingLaunchPayload?: string | null): string;
189
199
  }
190
200
  export type DispatchResult = {
191
201
  ok: boolean;
@@ -193,6 +203,8 @@ export type DispatchResult = {
193
203
  };
194
204
  export type HarnessDeliveryRecord = {
195
205
  session: string;
206
+ stopped?: boolean;
207
+ archived?: boolean;
196
208
  worktreePath?: string;
197
209
  harnessSessionId?: string | null;
198
210
  runtimeDir?: string;
@@ -205,6 +217,7 @@ export type HarnessArtifacts = {
205
217
  };
206
218
  export declare const legacyRvSock: (id: string) => string;
207
219
  export declare const scopedRvSock: (id: string, dir?: string) => string;
220
+ export declare function assertRvSockPath(id: string, dir?: string): string;
208
221
  export declare const rvSock: (id: string) => string;
209
222
  export declare function stampRvSock(id: string, dir?: string): string;
210
223
  export type ListenerProbe = 'live' | 'dead' | 'unproven';