spexcode 0.7.0-next.0 → 0.7.0-next.10

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 (32) hide show
  1. package/node_modules/@spexcode/session-application/package.json +5 -5
  2. package/node_modules/@spexcode/session-events/package.json +2 -2
  3. package/node_modules/@spexcode/session-protocol/package.json +1 -1
  4. package/node_modules/@spexcode/session-runtime/package.json +2 -2
  5. package/node_modules/@spexcode/session-selflaunch/package.json +3 -3
  6. package/node_modules/@spexcode/session-topology/package.json +2 -2
  7. package/node_modules/@spexcode/spec-cli/dist/client.js +7 -5
  8. package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.d.ts +4 -0
  9. package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.js +12 -0
  10. package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.js +4 -1
  11. package/node_modules/@spexcode/spec-cli/dist/harness.d.ts +1 -0
  12. package/node_modules/@spexcode/spec-cli/dist/harness.js +48 -8
  13. package/node_modules/@spexcode/spec-cli/dist/host-resources.js +55 -3
  14. package/node_modules/@spexcode/spec-cli/dist/materialize.js +54 -1
  15. package/node_modules/@spexcode/spec-cli/dist/session-transcript.d.ts +1 -0
  16. package/node_modules/@spexcode/spec-cli/dist/sessions.js +116 -13
  17. package/node_modules/@spexcode/spec-cli/package.json +7 -7
  18. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/skills/merge/spec.md +9 -3
  19. package/node_modules/@spexcode/spec-core/dist/git.d.ts +1 -0
  20. package/node_modules/@spexcode/spec-core/dist/git.js +25 -0
  21. package/node_modules/@spexcode/spec-core/package.json +1 -1
  22. package/node_modules/@spexcode/spec-eval/package.json +2 -2
  23. package/node_modules/@spexcode/spec-forge/package.json +2 -2
  24. package/node_modules/@spexcode/transcript/dist/frames.d.ts +2 -0
  25. package/node_modules/@spexcode/transcript/dist/frames.js +2 -0
  26. package/node_modules/@spexcode/transcript/dist/parsers.d.ts +8 -0
  27. package/node_modules/@spexcode/transcript/dist/parsers.js +320 -22
  28. package/node_modules/@spexcode/transcript/dist/readers.d.ts +7 -1
  29. package/node_modules/@spexcode/transcript/dist/readers.js +87 -4
  30. package/node_modules/@spexcode/transcript/dist/turns.d.ts +1 -0
  31. package/node_modules/@spexcode/transcript/package.json +1 -1
  32. package/package.json +2 -2
@@ -1,4 +1,5 @@
1
1
  import { execFile, execFileSync, spawn } from 'node:child_process';
2
+ import { createConnection } from 'node:net';
2
3
  import { promisify } from 'node:util';
3
4
  import { createHash, randomUUID } from 'node:crypto';
4
5
  import { readFileSync, writeFileSync, existsSync, renameSync, linkSync, mkdirSync, rmSync, readdirSync, realpathSync, statSync, unlinkSync } from 'node:fs';
@@ -15,6 +16,7 @@ import { readSessionFiles } from './session-files.js';
15
16
  import { readSessionWebs } from './session-web.js';
16
17
  import { acquireFreshSessionApplicationForCreate, configuredSessionApplicationIfCutover, initializeFreshSessionApplication, releaseFreshSessionApplicationForCreate, sessionApplicationCutoverState, setSessionApplicationCommitWake } from './session-application.js';
17
18
  import { jsonMigrationFencePath } from '@spexcode/session-application';
19
+ import { decodeEventJson } from '@spexcode/session-events';
18
20
  import { withDeliveryLocks } from './delivery-lock.js';
19
21
  import { withSessionRecordLock, withSessionRecordLockSync as coreWithSessionRecordLockSync } from './session-record-lock.js';
20
22
  import { stripRefSigil } from './mentions.js';
@@ -1100,7 +1102,7 @@ export function toSession(rec, status, lv, activity = null) {
1100
1102
  const pp = prompt ? oneLinePreview(prompt) : null;
1101
1103
  const parts = { id: rec.session, name: rec.name, node: rec.node, title: rec.title, branch: rec.branch, activity: act, note: rec.note, promptPreview: pp };
1102
1104
  const harness = harnessById(rec.harness || defaultHarness.id);
1103
- return { id: rec.session, node: rec.node, branch: rec.branch, label: deriveLabel(parts), title: deriveTitle(parts), raw: { name: rec.name, title: rec.title }, path: rec.worktreePath, parent: rec.parent, harness: harness.id, capabilities: { headless: harness.headless }, launcher: rec.launcher, lifecycle: rec.status, proposal: rec.proposal, merges: rec.merges, note: rec.note, status, liveness: lv, archived: rec.archived, closedAt: rec.archived ? rec.closedAt : null, archiveHazard: null, prompt, promptPreview: pp, created: rec.createdAt, activity: act, sortKey: rec.sortKey, files: readSessionFiles(rec.session), web: readSessionWebs(rec.session), ...(rec.zcodeChildSessionIds?.length ? { zcodeChildSessionIds: [...rec.zcodeChildSessionIds] } : {}) };
1105
+ return { id: rec.session, node: rec.node, branch: rec.branch, label: deriveLabel(parts), title: deriveTitle(parts), raw: { name: rec.name, title: rec.title }, path: rec.worktreePath, parent: rec.parent, harness: harness.id, capabilities: { headless: harness.headless }, launcher: rec.launcher, lifecycle: rec.closedAt ? 'archived' : rec.status, proposal: rec.closedAt ? null : rec.proposal, merges: rec.merges, note: rec.note, status, liveness: lv, archived: rec.archived || !!rec.closedAt, closedAt: rec.closedAt, archiveHazard: null, prompt, promptPreview: pp, created: rec.createdAt, activity: act, sortKey: rec.sortKey, files: readSessionFiles(rec.session), web: readSessionWebs(rec.session), ...(rec.zcodeChildSessionIds?.length ? { zcodeChildSessionIds: [...rec.zcodeChildSessionIds] } : {}) };
1104
1106
  }
1105
1107
  // @@@zcode child identity - ZCode owns the child id and SpexCode owns the session record. The writer accepts
1106
1108
  // only their exact declared pair; names, worktrees, branches, and timestamps are deliberately not candidates.
@@ -1174,7 +1176,7 @@ export async function listArchivedSessionIndex(probe) {
1174
1176
  if (entry.kind !== 'ok')
1175
1177
  continue;
1176
1178
  const rec = fromRaw(entry.raw);
1177
- if (!rec.governed || !rec.archived)
1179
+ if (!rec.governed || (!rec.archived && !rec.closedAt))
1178
1180
  continue;
1179
1181
  const parts = {
1180
1182
  id: rec.session, name: rec.name, node: rec.node, title: rec.title, branch: rec.branch,
@@ -1303,7 +1305,11 @@ export async function listSessions(includeArchived = false) {
1303
1305
  // missing durable cold proof is also legacy: leaf liveness alone cannot prove a Codex loaded thread was
1304
1306
  // unloaded, so it remains visible until an explicit archive repair.
1305
1307
  const cleanCold = projectedRecord.archived && !changedDuringCensus.has(id) && hasValidColdProof(projectedRecord) && physical === 'offline' && (!residentRequired || resident?.healthy === true);
1306
- const projected = projectedRecord.archived && !cleanCold ? { ...projectedRecord, archived: false, stopped: false } : projectedRecord;
1308
+ // A published close is terminal public history even if a later census cannot prove the old adapter fully
1309
+ // unloaded. Only legacy archived rows without closedAt may be exposed as a working hazard for repair.
1310
+ const projected = projectedRecord.archived && !cleanCold && !projectedRecord.closedAt
1311
+ ? { ...projectedRecord, archived: false, stopped: false }
1312
+ : projectedRecord;
1307
1313
  const projectedLv = projected === projectedRecord
1308
1314
  ? sessionHarness.runtimeOwnership === 'adapter'
1309
1315
  ? adapterResidentLiveness(projectedRecord, resident)
@@ -1704,11 +1710,45 @@ let draining = false; // re-entrancy guard: only one drain pass runs at a time (
1704
1710
  // A native receipt is bound before the readiness fence validates it. Suppress only that immediate wake so
1705
1711
  // queued prompts cannot drain during the candidate window; the successful publication path drains normally.
1706
1712
  const readinessWakeSuppressed = new Set();
1713
+ // A readiness timeout is launch-phase evidence only until the session produces another durable event. The
1714
+ // first event at/after the readiness marker is the launch transition itself; anything after that proves the
1715
+ // worker progressed (including a declaration), so a late diagnostic is moot and must not replace its note.
1716
+ function hasLaterLaunchReadinessEvent(rec) {
1717
+ const startedAt = rec.launchReadinessStartedAt;
1718
+ if (!Number.isFinite(startedAt))
1719
+ return false;
1720
+ const application = configuredSessionApplicationIfCutover();
1721
+ if (!application?.readState(rec.session))
1722
+ return false;
1723
+ const events = application.readEvents(rec.session);
1724
+ const statusPayload = (event) => {
1725
+ const payload = decodeEventJson(event.payload);
1726
+ return payload && typeof payload === 'object' && !Array.isArray(payload) && 'status' in payload
1727
+ ? payload : null;
1728
+ };
1729
+ const baseline = events.find((event) => {
1730
+ if (event.occurredAtMs < Number(startedAt))
1731
+ return false;
1732
+ const payload = statusPayload(event);
1733
+ return payload?.status === 'active';
1734
+ });
1735
+ return baseline ? events.some((event) => {
1736
+ if (event.eventSeq <= baseline.eventSeq)
1737
+ return false;
1738
+ const payload = statusPayload(event);
1739
+ if (!payload)
1740
+ return false;
1741
+ const note = payload.note;
1742
+ return !(typeof note === 'string' && /^(?:queued launch readiness failed|launch readiness warning):/.test(note));
1743
+ }) : false;
1744
+ }
1707
1745
  function noteQueuedLaunchFailureUnlocked(id, error, terminal = true, label, live = false) {
1746
+ const rec = readRecord(id);
1747
+ if (rec && (terminal || label === 'launch readiness warning') && hasLaterLaunchReadinessEvent(rec))
1748
+ return;
1708
1749
  const reason = error instanceof Error ? error.message : String(error);
1709
1750
  const note = `${label ?? (terminal ? 'queued launch readiness failed' : 'launch readiness warning')}: ${reason}`;
1710
1751
  console.error(`spex: session ${id}: ${note}`);
1711
- const rec = readRecord(id);
1712
1752
  if (rec && !retirementReason(rec) && (rec.note !== note
1713
1753
  || (terminal && (rec.status !== 'error' || !rec.stopped || rec.launchReadinessStartedAt != null))
1714
1754
  || (!terminal && live && (rec.status === 'error' || rec.stopped)))) {
@@ -1788,12 +1828,16 @@ export function canonicalRecordProjection(rec, canonical) {
1788
1828
  // The application row is the only lifecycle fact after cutover. A JSON status is historical envelope data,
1789
1829
  // so it must not win merely because it says waiting/error/archived while the canonical row says otherwise.
1790
1830
  if (!canonical) {
1791
- return rec;
1831
+ return ('closedAt' in rec && rec.closedAt
1832
+ ? { ...rec, archived: true }
1833
+ : rec);
1792
1834
  }
1835
+ const closed = 'closedAt' in rec && !!rec.closedAt;
1793
1836
  return {
1794
1837
  ...rec,
1795
- status: canonical.status,
1796
- proposal: canonical.proposal,
1838
+ archived: closed ? true : rec.archived,
1839
+ status: (closed ? 'archived' : canonical.status),
1840
+ proposal: (closed ? null : canonical.proposal),
1797
1841
  note: canonical.note,
1798
1842
  parent: canonical.parentSessionId,
1799
1843
  };
@@ -2488,9 +2532,53 @@ function isExplicitConnectionRefused(error) {
2488
2532
  return errors.length > 0 && errors.every(isExplicitConnectionRefused);
2489
2533
  return isExplicitConnectionRefused(error.cause);
2490
2534
  }
2535
+ async function establishBackendConnection(target) {
2536
+ const parsed = new URL(target.url);
2537
+ const port = Number(parsed.port) || (parsed.protocol === 'https:' ? 443 : 80);
2538
+ return await new Promise((resolve, reject) => {
2539
+ const socket = createConnection({ host: parsed.hostname, port });
2540
+ let settled = false;
2541
+ const finish = (fn) => {
2542
+ if (settled)
2543
+ return;
2544
+ settled = true;
2545
+ clearTimeout(timer);
2546
+ socket.destroy();
2547
+ fn();
2548
+ };
2549
+ const timer = setTimeout(() => {
2550
+ // The event loop may have been blocked past the wall after the kernel completed the handshake. In that
2551
+ // case Node has not emitted `connect` yet, but `connecting` is already false; acceptance still proves
2552
+ // presence and must not be relabelled as an unavailable backend.
2553
+ if (!socket.connecting || socket.readyState === 'open')
2554
+ return finish(() => resolve(false));
2555
+ finish(() => {
2556
+ const error = new Error(`backend connection was not accepted at ${target.url} within 1500ms`);
2557
+ error.name = 'BackendError';
2558
+ Object.assign(error, { code: 'backend_availability_indeterminate' });
2559
+ reject(error);
2560
+ });
2561
+ }, 1500);
2562
+ timer.unref?.();
2563
+ socket.once('connect', () => finish(() => resolve(false)));
2564
+ socket.once('error', (error) => finish(() => {
2565
+ if (isExplicitConnectionRefused(error))
2566
+ return resolve(true);
2567
+ const failed = new Error(`backend availability is indeterminate at ${target.url}; refusing in-process session creation (${error instanceof Error ? error.message : error})`);
2568
+ failed.name = 'BackendError';
2569
+ Object.assign(failed, { code: 'backend_availability_indeterminate', cause: error });
2570
+ reject(failed);
2571
+ }));
2572
+ });
2573
+ }
2491
2574
  async function probeSessionCreateAuthority(target) {
2575
+ // TCP acceptance establishes presence. The identity route can be delayed by a busy backend event loop,
2576
+ // so it gets the ordinary create request deadline instead of a short availability deadline.
2577
+ const refused = await establishBackendConnection(target);
2578
+ if (refused)
2579
+ return true;
2492
2580
  const controller = new AbortController();
2493
- const timer = setTimeout(() => controller.abort(), 1500);
2581
+ const timer = setTimeout(() => controller.abort(new Error('backend authority request timed out')), sessionCreateTimeoutMs() + 5_000);
2494
2582
  timer.unref?.();
2495
2583
  let response;
2496
2584
  try {
@@ -2498,11 +2586,9 @@ async function probeSessionCreateAuthority(target) {
2498
2586
  }
2499
2587
  catch (error) {
2500
2588
  clearTimeout(timer);
2501
- if (isExplicitConnectionRefused(error))
2502
- return true;
2503
- const failed = new Error(`backend availability is indeterminate at ${target.url}; refusing in-process session creation (${error instanceof Error ? error.message : error})`);
2589
+ const failed = new Error(`backend authority read failed after connection at ${target.url}; refusing in-process session creation (${error instanceof Error ? error.message : error})`);
2504
2590
  failed.name = 'BackendError';
2505
- Object.assign(failed, { code: 'backend_availability_indeterminate', cause: error });
2591
+ Object.assign(failed, { code: 'backend_authority_read_failed', cause: error });
2506
2592
  throw failed;
2507
2593
  }
2508
2594
  try {
@@ -2988,7 +3074,7 @@ async function prepareSession(prompt, parent, launcher, name, context) {
2988
3074
  try {
2989
3075
  gitMutationStarted = true;
2990
3076
  traceSessionCreate(id, requestDigest, phase, 'start', 'worktree-add');
2991
- const added = await withGitAbortSignal(signal, () => gitTry(['-C', root, 'worktree', 'add', '-b', branch, path, startPoint], { extraEnv: DEFER_FOOTPRINT_REFRESH }));
3077
+ const added = await withGitAbortSignal(signal, () => gitTry(['-C', root, 'worktree', 'add', '--no-track', '-b', branch, path, startPoint], { extraEnv: DEFER_FOOTPRINT_REFRESH }));
2992
3078
  traceSessionCreate(id, requestDigest, phase, 'finish', 'worktree-add');
2993
3079
  if (added.ok)
2994
3080
  Object.assign(owned, { path: true, worktree: true, branch: true });
@@ -4711,12 +4797,25 @@ async function closeOwnedSessionUnlocked(id, wt, _source, unboundStopped = false
4711
4797
  throw new ResourceConflict(`refusing to finish close for ${id}: session record disappeared before publication`);
4712
4798
  writeRecord({
4713
4799
  ...latest,
4800
+ proposal: null,
4714
4801
  archived: true,
4715
4802
  closedAt: latest.closedAt || new Date().toISOString(),
4716
4803
  stopped: true,
4717
4804
  coldProof: latest.coldProof || coldProofFor(latest),
4718
4805
  adapterRecovery: null,
4719
4806
  });
4807
+ // The canonical lifecycle must settle at the same terminal boundary as the durable close fact. `archived`
4808
+ // is an internal terminal marker; public projections render its closed record as `retired`.
4809
+ const application = configuredSessionApplicationIfCutover();
4810
+ if (application?.readState(id)) {
4811
+ application.transitionSession(id, {
4812
+ status: 'archived',
4813
+ proposal: null,
4814
+ note: latest.note,
4815
+ parentSessionId: latest.parent,
4816
+ recipientSessionIds: canonicalWatchRecipients(application, id, 'archived'),
4817
+ });
4818
+ }
4720
4819
  let slot = null;
4721
4820
  try {
4722
4821
  slot = existsSync(wt.path) ? treeSlotDir(wt.path) : null;
@@ -5289,6 +5388,8 @@ export async function drainSession(id) {
5289
5388
  const removed = application.dequeuePendingMessage(id, msg.messageId);
5290
5389
  if (!removed || removed.messageId !== msg.messageId)
5291
5390
  throw new ResourceConflict(`canonical queue head changed while delivering ${id}`);
5391
+ if (!msg.senderSessionId)
5392
+ markHumanPromptActive(id);
5292
5393
  }
5293
5394
  });
5294
5395
  return;
@@ -5315,6 +5416,8 @@ export async function drainSession(id) {
5315
5416
  const removed = application.dequeueForRuntime(id, 'spex-governed', binding.bindingGeneration, msg.messageId);
5316
5417
  if (!removed || removed.messageId !== msg.messageId)
5317
5418
  throw new ResourceConflict(`canonical queue head changed while delivering ${id}`);
5419
+ if (!msg.senderSessionId)
5420
+ markHumanPromptActive(id);
5318
5421
  }
5319
5422
  });
5320
5423
  return;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/spec-cli",
3
- "version": "0.7.0-next.0",
3
+ "version": "0.7.0-next.10",
4
4
  "type": "module",
5
5
  "description": "SpexCode CLI + server. The root spexcode package delegates to this compiled package; dashboard assets live in @spexcode/spec-dashboard.",
6
6
  "bin": {
@@ -35,12 +35,12 @@
35
35
  "test": "tsx --import ../scripts/test-home.mjs --test src/*.test.ts"
36
36
  },
37
37
  "dependencies": {
38
- "@spexcode/session-application": "0.7.0-next.0",
39
- "@spexcode/session-selflaunch": "0.7.0-next.0",
40
- "@spexcode/spec-core": "0.7.0-next.0",
41
- "@spexcode/spec-eval": "0.7.0-next.0",
42
- "@spexcode/spec-forge": "0.7.0-next.0",
43
- "@spexcode/transcript": "0.7.0-next.0",
38
+ "@spexcode/session-application": "0.7.0-next.10",
39
+ "@spexcode/session-selflaunch": "0.7.0-next.10",
40
+ "@spexcode/spec-core": "0.7.0-next.10",
41
+ "@spexcode/spec-eval": "0.7.0-next.10",
42
+ "@spexcode/spec-forge": "0.7.0-next.10",
43
+ "@spexcode/transcript": "0.7.0-next.10",
44
44
  "smol-toml": "^1.8.0"
45
45
  },
46
46
  "devDependencies": {
@@ -19,9 +19,15 @@ Land the current SpexCode session's branch; do not dispatch another merge reques
19
19
  3. Immediately before landing, verify
20
20
  `git merge-base --is-ancestor <source-head> <session-head>`. If it fails, sync again. A clean textual merge
21
21
  is not product proof.
22
- 4. In the source-of-truth checkout, make one `--no-ff` merge of the already-synced session tip. Do not resolve
23
- conflicts there. If unrelated dirty work prevents the merge, preserve it byte-for-byte and report the exact
24
- overlap rather than forcing, resetting, or committing it.
22
+ 4. Land with one `--no-ff` merge of the already-synced session tip, without touching the source checkout's
23
+ dirty work. Git refuses a merge over a dirty index, so do NOT clear it: add a temporary detached worktree of
24
+ the source head (`git worktree add --detach <tmp> <source-branch>`), make the `--no-ff` merge there, then
25
+ fast-forward the source checkout to that commit (`git merge --ff-only <tmp-head>`) and remove the temporary
26
+ worktree. A fast-forward never rewrites a path the merge did not change, so user-owned dirty files keep
27
+ their bytes and their staged/unstaged state. Never `git restore`, `reset`, `checkout`, `stash`, or overwrite
28
+ a user-owned path — an unstaged edit has no blob anywhere, so "save and put back" loses it, and a copy of the
29
+ same path from another worktree is a different version, not a restoration. Do not resolve conflicts in the
30
+ source checkout; if the merge genuinely overlaps a user-owned path, stop and report the exact overlap.
25
31
  5. Verify the source checkout has no `MERGE_HEAD`, the session tip is its ancestor, unrelated dirty
26
32
  fingerprints are unchanged, and the post-merge gates pass. Push the source-of-truth branch only after
27
33
  those checks.
@@ -122,6 +122,7 @@ export declare function historyCacheStats(): {
122
122
  historyRoots: number;
123
123
  driftRoots: number;
124
124
  };
125
+ export declare function pruneHistoryCaches(activeRoots: Iterable<string>): void;
125
126
  export declare function resetHistoryCachesForTests(): void;
126
127
  export declare function historyEventCachePathForTests(root: string): string;
127
128
  export declare function ancestorsOf(idx: Reachability, sha: string): Uint8Array | undefined;
@@ -2270,6 +2270,31 @@ export function sourceIndexes(root, tip = 'HEAD') {
2270
2270
  export function historyCacheStats() {
2271
2271
  return { historyHeads: indexCache.size, driftHeads: driftIdxCache.size, historyRoots: indexRoots.size, driftRoots: driftRoots.size };
2272
2272
  }
2273
+ // A backend snapshot is the authority for which checkout roots are live. Release cache ownership for roots
2274
+ // that disappeared from that snapshot so closed session worktrees do not keep immutable history warm until
2275
+ // the generic slot bound happens to evict them. Shared entries survive while another live root still names
2276
+ // the same immutable key; an in-flight promise remains usable by its current caller even after its cache key
2277
+ // is released.
2278
+ export function pruneHistoryCaches(activeRoots) {
2279
+ const active = new Set([...activeRoots].map(rootKey));
2280
+ for (const [root] of indexRoots)
2281
+ if (!active.has(root))
2282
+ indexRoots.delete(root);
2283
+ for (const [root] of driftRoots)
2284
+ if (!active.has(root))
2285
+ driftRoots.delete(root);
2286
+ const referencedHistory = new Set(indexRoots.values());
2287
+ const referencedDrift = new Set(driftRoots.values());
2288
+ for (const key of indexCache.keys())
2289
+ if (!referencedHistory.has(key))
2290
+ indexCache.delete(key);
2291
+ for (const key of driftIdxCache.keys())
2292
+ if (!referencedDrift.has(key))
2293
+ driftIdxCache.delete(key);
2294
+ for (const root of eventPathMemo.keys())
2295
+ if (!active.has(root))
2296
+ eventPathMemo.delete(root);
2297
+ }
2273
2298
  // Tests deliberately clear process-local promises and path identity to exercise both cold and read-back paths.
2274
2299
  // Production callers retain the bounded index caches; this is not a correctness escape hatch.
2275
2300
  export function resetHistoryCachesForTests() {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/spec-core",
3
- "version": "0.7.0-next.0",
3
+ "version": "0.7.0-next.10",
4
4
  "type": "module",
5
5
  "description": "SpexCode's dependency-minimal spec graph core.",
6
6
  "files": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/spec-eval",
3
- "version": "0.7.0-next.0",
3
+ "version": "0.7.0-next.10",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dist/index.js",
@@ -26,7 +26,7 @@
26
26
  "test": "tsx --import ../scripts/test-home.mjs --test src/*.test.ts"
27
27
  },
28
28
  "dependencies": {
29
- "@spexcode/spec-core": "0.7.0-next.0"
29
+ "@spexcode/spec-core": "0.7.0-next.10"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^20.16.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/spec-forge",
3
- "version": "0.7.0-next.0",
3
+ "version": "0.7.0-next.10",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dist/index.js",
@@ -22,7 +22,7 @@
22
22
  "test": "tsx --test src/*.test.ts"
23
23
  },
24
24
  "dependencies": {
25
- "@spexcode/spec-core": "0.7.0-next.0"
25
+ "@spexcode/spec-core": "0.7.0-next.10"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^20.16.0",
@@ -1,4 +1,6 @@
1
1
  import { type TranscriptRead, type TranscriptReader, type TranscriptTool, type TranscriptTurn } from './turns.js';
2
+ export { TranscriptReadError } from './turns.js';
3
+ export type { TranscriptRange, TranscriptRead, TranscriptReader, TranscriptTail, TranscriptTool, TranscriptTurn } from './turns.js';
2
4
  export type StreamTool = Readonly<Omit<TranscriptTool, 'output'> & {
3
5
  output?: null;
4
6
  }>;
@@ -1,4 +1,6 @@
1
1
  import { TranscriptReadError } from './turns.js';
2
+ // the browser-safe entry is complete on its own: the normalized shape travels with the frames that carry it
3
+ export { TranscriptReadError } from './turns.js';
2
4
  export const isErrorFrame = (frame) => 'error' in frame;
3
5
  // The revision an absent source reports: the thread has not started writing. Not an error.
4
6
  export const ABSENT_REVISION = 'absent';
@@ -8,6 +8,7 @@ export type MutableTool = {
8
8
  output?: string;
9
9
  outputLines: number;
10
10
  outputBytes: number;
11
+ outcome?: ToolOutcome;
11
12
  };
12
13
  export type MutableTurn = {
13
14
  id: string | null;
@@ -16,18 +17,25 @@ export type MutableTurn = {
16
17
  text?: string;
17
18
  tools: MutableTool[];
18
19
  };
20
+ export type ToolOutcome = 'failed' | 'rejected';
19
21
  export type ParsedEvent = {
20
22
  at: number | null;
21
23
  turn: MutableTurn | null;
22
24
  toolOutputs?: readonly {
23
25
  id: string;
24
26
  text: string;
27
+ outcome?: ToolOutcome;
25
28
  }[];
26
29
  };
27
30
  export type Parse = (value: unknown) => ParsedEvent | null;
28
31
  export declare function claudeEvent(value: unknown): ParsedEvent | null;
29
32
  export declare function codexEvent(value: unknown): ParsedEvent | null;
33
+ export declare function codexAppServerEvent(value: unknown): ParsedEvent | null;
34
+ export declare function codexAppServerStream(): Parse;
30
35
  export declare function piEvent(value: unknown): ParsedEvent | null;
36
+ export declare function geminiEvent(value: unknown): ParsedEvent | null;
37
+ export declare function openclawEvent(value: unknown): ParsedEvent | null;
38
+ export declare function hermesEvents(value: unknown): ParsedEvent[];
31
39
  export declare function opencodeEvents(value: unknown): ParsedEvent[];
32
40
  export declare class IntervalCollector {
33
41
  readonly turns: MutableTurn[];