spexcode 0.6.8 → 0.7.0-next.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.
Files changed (72) 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/dist/locality.d.ts +12 -1
  6. package/node_modules/@spexcode/session-selflaunch/dist/locality.js +62 -10
  7. package/node_modules/@spexcode/session-selflaunch/package.json +3 -3
  8. package/node_modules/@spexcode/session-topology/package.json +2 -2
  9. package/node_modules/@spexcode/spec-cli/bin/spex.mjs +127 -10
  10. package/node_modules/@spexcode/spec-cli/dist/cli.js +19 -0
  11. package/node_modules/@spexcode/spec-cli/dist/file-write.js +14 -2
  12. package/node_modules/@spexcode/spec-cli/dist/guide.js +9 -11
  13. package/node_modules/@spexcode/spec-cli/dist/harness.d.ts +11 -5
  14. package/node_modules/@spexcode/spec-cli/dist/harness.js +185 -28
  15. package/node_modules/@spexcode/spec-cli/dist/host.js +5 -1
  16. package/node_modules/@spexcode/spec-cli/dist/index.js +21 -65
  17. package/node_modules/@spexcode/spec-cli/dist/machine-peer.d.ts +2 -1
  18. package/node_modules/@spexcode/spec-cli/dist/machine-peer.js +45 -14
  19. package/node_modules/@spexcode/spec-cli/dist/opencode.js +10 -2
  20. package/node_modules/@spexcode/spec-cli/dist/pi-harness.js +18 -5
  21. package/node_modules/@spexcode/spec-cli/dist/pi-headless.d.ts +2 -0
  22. package/node_modules/@spexcode/spec-cli/dist/pi-headless.js +27 -0
  23. package/node_modules/@spexcode/spec-cli/dist/session-transcript.d.ts +57 -0
  24. package/node_modules/@spexcode/spec-cli/dist/session-transcript.js +137 -0
  25. package/node_modules/@spexcode/spec-cli/dist/sessions.d.ts +4 -0
  26. package/node_modules/@spexcode/spec-cli/dist/sessions.js +104 -36
  27. package/node_modules/@spexcode/spec-cli/dist/shim-runtime.js +14 -0
  28. package/node_modules/@spexcode/spec-cli/dist/supervise.js +1 -1
  29. package/node_modules/@spexcode/spec-cli/package.json +8 -6
  30. package/node_modules/@spexcode/spec-core/dist/layout.d.ts +0 -1
  31. package/node_modules/@spexcode/spec-core/package.json +1 -1
  32. package/node_modules/@spexcode/spec-core/templates/spexcode.json +36 -13
  33. package/node_modules/@spexcode/spec-eval/package.json +2 -2
  34. package/node_modules/@spexcode/spec-forge/package.json +2 -2
  35. package/node_modules/@spexcode/transcript/dist/frames.d.ts +46 -0
  36. package/node_modules/@spexcode/transcript/dist/frames.js +87 -0
  37. package/node_modules/@spexcode/transcript/dist/index.d.ts +5 -0
  38. package/node_modules/@spexcode/transcript/dist/index.js +5 -0
  39. package/node_modules/@spexcode/transcript/dist/live.d.ts +16 -0
  40. package/node_modules/@spexcode/transcript/dist/live.js +65 -0
  41. package/node_modules/@spexcode/transcript/dist/parsers.d.ts +50 -0
  42. package/node_modules/@spexcode/transcript/dist/parsers.js +395 -0
  43. package/node_modules/@spexcode/transcript/dist/readers.d.ts +16 -0
  44. package/node_modules/@spexcode/transcript/dist/readers.js +375 -0
  45. package/node_modules/@spexcode/{spec-cli/dist/transcript-reader.d.ts → transcript/dist/turns.d.ts} +11 -5
  46. package/node_modules/@spexcode/transcript/dist/turns.js +11 -0
  47. package/node_modules/@spexcode/transcript/package.json +30 -0
  48. package/node_modules/smol-toml/LICENSE +24 -0
  49. package/node_modules/smol-toml/README.md +418 -0
  50. package/node_modules/smol-toml/dist/date.d.ts +41 -0
  51. package/node_modules/smol-toml/dist/date.js +127 -0
  52. package/node_modules/smol-toml/dist/error.d.ts +38 -0
  53. package/node_modules/smol-toml/dist/error.js +63 -0
  54. package/node_modules/smol-toml/dist/extract.js +69 -0
  55. package/node_modules/smol-toml/dist/index.cjs +734 -0
  56. package/node_modules/smol-toml/dist/index.d.ts +43 -0
  57. package/node_modules/smol-toml/dist/index.js +33 -0
  58. package/node_modules/smol-toml/dist/parse.d.ts +36 -0
  59. package/node_modules/smol-toml/dist/parse.js +149 -0
  60. package/node_modules/smol-toml/dist/primitive.js +238 -0
  61. package/node_modules/smol-toml/dist/stringify.d.ts +31 -0
  62. package/node_modules/smol-toml/dist/stringify.js +181 -0
  63. package/node_modules/smol-toml/dist/struct.js +179 -0
  64. package/node_modules/smol-toml/dist/util.d.ts +38 -0
  65. package/node_modules/smol-toml/dist/util.js +89 -0
  66. package/node_modules/smol-toml/package.json +68 -0
  67. package/package.json +3 -3
  68. package/node_modules/@spexcode/spec-cli/dist/execution-trace.d.ts +0 -27
  69. package/node_modules/@spexcode/spec-cli/dist/execution-trace.js +0 -475
  70. package/node_modules/@spexcode/spec-cli/dist/session-execution.d.ts +0 -10
  71. package/node_modules/@spexcode/spec-cli/dist/session-execution.js +0 -70
  72. package/node_modules/@spexcode/spec-cli/dist/transcript-reader.js +0 -251
@@ -1,9 +1,8 @@
1
1
  {
2
2
  "lint": {
3
- "governedRoots": ["."]
4
- },
5
- "dashboard": {
6
- "showHeadlessLaunchers": false
3
+ "governedRoots": [
4
+ "."
5
+ ]
7
6
  },
8
7
  "uploads": {
9
8
  "maxBytes": 2147483648,
@@ -19,15 +18,39 @@
19
18
  },
20
19
  "sessions": {
21
20
  "launchers": {
22
- "claude": { "harness": "claude", "cmd": "claude" },
23
- "claude-headless": { "harness": "claude-headless", "cmd": "claude" },
24
- "codex": { "harness": "codex", "cmd": "codex" },
25
- "codex-headless": { "harness": "codex-headless", "cmd": "codex --yolo" },
26
- "opencode": { "harness": "opencode", "cmd": "opencode" },
27
- "opencode-headless": { "harness": "opencode-headless", "cmd": "opencode --auto" },
28
- "pi": { "harness": "pi", "cmd": "pi" },
29
- "pi-headless": { "harness": "pi-headless", "cmd": "pi" }
21
+ "claude-headless": {
22
+ "harness": "claude-headless",
23
+ "cmd": "claude"
24
+ },
25
+ "claude": {
26
+ "harness": "claude",
27
+ "cmd": "claude"
28
+ },
29
+ "codex-headless": {
30
+ "harness": "codex-headless",
31
+ "cmd": "codex --yolo"
32
+ },
33
+ "codex": {
34
+ "harness": "codex",
35
+ "cmd": "codex"
36
+ },
37
+ "opencode-headless": {
38
+ "harness": "opencode-headless",
39
+ "cmd": "opencode --auto"
40
+ },
41
+ "opencode": {
42
+ "harness": "opencode",
43
+ "cmd": "opencode"
44
+ },
45
+ "pi-headless": {
46
+ "harness": "pi-headless",
47
+ "cmd": "pi"
48
+ },
49
+ "pi": {
50
+ "harness": "pi",
51
+ "cmd": "pi"
52
+ }
30
53
  },
31
- "defaultLauncher": "claude"
54
+ "defaultLauncher": "claude-headless"
32
55
  }
33
56
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/spec-eval",
3
- "version": "0.6.8",
3
+ "version": "0.7.0-next.1",
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.6.8"
29
+ "@spexcode/spec-core": "0.7.0-next.1"
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.6.8",
3
+ "version": "0.7.0-next.1",
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.6.8"
25
+ "@spexcode/spec-core": "0.7.0-next.1"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^20.16.0",
@@ -0,0 +1,46 @@
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';
4
+ export type StreamTool = Readonly<Omit<TranscriptTool, 'output'> & {
5
+ output?: null;
6
+ }>;
7
+ export type StreamTurn = Readonly<Omit<TranscriptTurn, 'tools'> & {
8
+ tools?: readonly StreamTool[];
9
+ }>;
10
+ export type StreamFrame = Readonly<Omit<TranscriptRead, 'turns'> & {
11
+ kind: 'full' | 'delta';
12
+ turns: readonly StreamTurn[];
13
+ removed?: readonly string[];
14
+ }>;
15
+ export type TranscriptErrorFrame = Readonly<{
16
+ revision: string;
17
+ from: number;
18
+ to: number;
19
+ error: string;
20
+ reason: string;
21
+ }>;
22
+ export type TranscriptFrame = StreamFrame | TranscriptErrorFrame;
23
+ export declare const isErrorFrame: (frame: TranscriptFrame) => frame is TranscriptErrorFrame;
24
+ export declare const ABSENT_REVISION = "absent";
25
+ export declare const withheld: (read: TranscriptRead) => Omit<StreamFrame, "kind">;
26
+ export declare class FrameProducer {
27
+ private primed;
28
+ private sent;
29
+ private counters;
30
+ next(read: TranscriptRead): StreamFrame | null;
31
+ reset(): void;
32
+ }
33
+ export declare const absentFrame: (from: number, to: number) => StreamFrame;
34
+ export type FrameStream = Readonly<{
35
+ publish(now?: number): Promise<TranscriptFrame | null>;
36
+ close(): void;
37
+ }>;
38
+ export declare function openFrameStream(reader: TranscriptReader, threadId: string, from: number, clock?: () => number): FrameStream;
39
+ export type HeldTranscript = Readonly<{
40
+ turns: readonly StreamTurn[];
41
+ }>;
42
+ export type MergedFrame = Readonly<{
43
+ state: HeldTranscript;
44
+ payload: TranscriptErrorFrame | (Omit<StreamFrame, 'kind' | 'removed'>);
45
+ }>;
46
+ export declare function mergeTranscriptFrame(state: HeldTranscript, frame: TranscriptFrame): MergedFrame;
@@ -0,0 +1,87 @@
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';
4
+ export const isErrorFrame = (frame) => 'error' in frame;
5
+ // The revision an absent source reports: the thread has not started writing. Not an error.
6
+ export const ABSENT_REVISION = 'absent';
7
+ export const withheld = (read) => ({
8
+ ...read,
9
+ turns: read.turns.map((turn) => turn.tools
10
+ ? { ...turn, tools: turn.tools.map((tool) => tool.output === undefined ? tool : { ...tool, output: null }) }
11
+ : turn),
12
+ });
13
+ // THE PRODUCER'S HALF. Feed it every read of the open interval; it returns the frame that read is worth — the
14
+ // first one is the whole interval (`full`), every later one a `delta` holding only the turns that are new or
15
+ // changed since the previous frame (a turn changes when a call in it gains its result) and the ids the turn cap
16
+ // evicted; the counters are always absolute — or `null` when the read changed nothing the subscriber holds.
17
+ export class FrameProducer {
18
+ primed = false; // a `full` frame has been delivered on this stream
19
+ sent = new Map(); // turn id → the serialized turn the subscriber holds
20
+ counters = ''; // the absolute counters as last sent
21
+ next(read) {
22
+ const held = withheld(read);
23
+ const next = new Map(held.turns.map((turn) => [turn.id, JSON.stringify(turn)]));
24
+ const changed = held.turns.filter((turn) => this.sent.get(turn.id) !== next.get(turn.id));
25
+ const removed = [...this.sent.keys()].filter((turnId) => !next.has(turnId));
26
+ const counters = `${held.truncated}:${held.omittedTurns}:${held.omittedBytes}:${held.outOfOrderEvents}`;
27
+ if (this.primed && !changed.length && !removed.length && counters === this.counters)
28
+ return null;
29
+ const frame = this.primed ? { ...held, kind: 'delta', turns: changed, removed } : { ...held, kind: 'full' };
30
+ this.sent = next;
31
+ this.counters = counters;
32
+ this.primed = true;
33
+ return frame;
34
+ }
35
+ // the next frame is `full` again — a subscriber that reconnects, or a source that vanished, holds nothing
36
+ reset() { this.primed = false; this.sent = new Map(); this.counters = ''; }
37
+ }
38
+ export const absentFrame = (from, to) => ({ kind: 'full', revision: ABSENT_REVISION, from, to, turns: [], truncated: false, omittedTurns: 0, omittedBytes: 0, outOfOrderEvents: 0 });
39
+ export function openFrameStream(reader, threadId, from, clock = () => Date.now()) {
40
+ let last;
41
+ let tail = null; // opened on the first non-absent revision
42
+ const producer = new FrameProducer();
43
+ return {
44
+ async publish(now = clock()) {
45
+ const revision = reader.revision(threadId) ?? ABSENT_REVISION;
46
+ if (revision === last)
47
+ return null;
48
+ last = revision;
49
+ const to = Math.max(from + 1, now);
50
+ if (revision === ABSENT_REVISION) {
51
+ producer.reset();
52
+ return absentFrame(from, to);
53
+ }
54
+ try {
55
+ return producer.next(await (tail ??= reader.tail(threadId, from)).advance(to));
56
+ }
57
+ catch (error) {
58
+ if (!(error instanceof TranscriptReadError))
59
+ throw error;
60
+ return { revision, from, to, error: error.message, reason: error.reason };
61
+ }
62
+ },
63
+ close() { tail?.close(); tail = null; },
64
+ };
65
+ }
66
+ export function mergeTranscriptFrame(state, frame) {
67
+ if (isErrorFrame(frame))
68
+ return { state, payload: frame };
69
+ const { kind, removed, turns: incoming, ...rest } = frame;
70
+ if (kind !== 'delta') {
71
+ const turns = [...(incoming || [])];
72
+ return { state: { turns }, payload: { ...rest, turns } };
73
+ }
74
+ const gone = new Set(removed || []);
75
+ const turns = state.turns.filter((turn) => !gone.has(turn.id));
76
+ const index = new Map(turns.map((turn, at) => [turn.id, at]));
77
+ for (const turn of incoming || []) {
78
+ const at = index.get(turn.id);
79
+ if (at === undefined) {
80
+ index.set(turn.id, turns.length);
81
+ turns.push(turn);
82
+ }
83
+ else
84
+ turns[at] = turn;
85
+ }
86
+ return { state: { turns }, payload: { ...rest, turns } };
87
+ }
@@ -0,0 +1,5 @@
1
+ export * from './turns.js';
2
+ export * from './parsers.js';
3
+ export * from './readers.js';
4
+ export * from './frames.js';
5
+ export * from './live.js';
@@ -0,0 +1,5 @@
1
+ export * from './turns.js';
2
+ export * from './parsers.js';
3
+ export * from './readers.js';
4
+ export * from './frames.js';
5
+ export * from './live.js';
@@ -0,0 +1,16 @@
1
+ import { type TranscriptRange, type TranscriptRead, type TranscriptReader, type TranscriptTail } from './turns.js';
2
+ import { type Parse } from './parsers.js';
3
+ export declare class LiveTranscript implements TranscriptReader {
4
+ private readonly parse;
5
+ readonly threadId: string;
6
+ private readonly events;
7
+ private writes;
8
+ private readonly listeners;
9
+ constructor(parse: Parse, threadId: string);
10
+ push(native: unknown): boolean;
11
+ onChange(listener: () => void): () => void;
12
+ private own;
13
+ revision(threadId: string): string | null;
14
+ read(threadId: string, range: TranscriptRange): Promise<TranscriptRead>;
15
+ tail(threadId: string, from: number): TranscriptTail;
16
+ }
@@ -0,0 +1,65 @@
1
+ import { TranscriptReadError } from './turns.js';
2
+ import { IntervalCollector } from './parsers.js';
3
+ // THE IN-MEMORY SOURCE. A headless controller already holds the harness's native events as they stream past —
4
+ // Claude's `stream-json` lines, an app-server's notifications — so a transcript need not be re-read from the
5
+ // file the harness also writes. Push each native event here, through the same parser the file reader uses, and
6
+ // this is a `TranscriptReader` like any other: the frame protocol, the interval read, and every renderer work
7
+ // unchanged, and `onChange` lets a producer publish on arrival instead of on a tick. One instance is one thread.
8
+ export class LiveTranscript {
9
+ parse;
10
+ threadId;
11
+ events = [];
12
+ writes = 0;
13
+ listeners = new Set();
14
+ constructor(parse, threadId) {
15
+ this.parse = parse;
16
+ this.threadId = threadId;
17
+ }
18
+ // `true` when the record meant something to the parser; an unrecognized record is not an error — a native
19
+ // stream carries plenty the transcript does not show (control replies, usage, the result envelope)
20
+ push(native) {
21
+ const event = this.parse(native);
22
+ if (!event)
23
+ return false;
24
+ this.events.push(event);
25
+ this.writes++;
26
+ for (const listener of this.listeners)
27
+ listener();
28
+ return true;
29
+ }
30
+ onChange(listener) {
31
+ this.listeners.add(listener);
32
+ return () => { this.listeners.delete(listener); };
33
+ }
34
+ own(threadId) {
35
+ if (threadId !== this.threadId)
36
+ throw new TranscriptReadError('missing', `live transcript holds thread ${this.threadId}, not ${threadId}`);
37
+ }
38
+ // `null` until the first event lands: the thread has not started, which the frame protocol reads as absent
39
+ revision(threadId) {
40
+ this.own(threadId);
41
+ return this.writes ? `${this.writes}` : null;
42
+ }
43
+ async read(threadId, range) {
44
+ this.own(threadId);
45
+ const collector = new IntervalCollector(range);
46
+ for (const event of this.events)
47
+ collector.add(event);
48
+ return collector.finish(`${this.writes}`, 'live');
49
+ }
50
+ // the cursor is an index into the event list: each advance collects only what was pushed since the last one
51
+ tail(threadId, from) {
52
+ this.own(threadId);
53
+ const collector = new IntervalCollector({ from, to: from });
54
+ let consumed = 0;
55
+ return {
56
+ advance: async (to) => {
57
+ collector.extend(to);
58
+ for (; consumed < this.events.length; consumed++)
59
+ collector.add(this.events[consumed]);
60
+ return collector.finish(`${this.writes}`, 'live');
61
+ },
62
+ close: () => { },
63
+ };
64
+ }
65
+ }
@@ -0,0 +1,50 @@
1
+ import { type TranscriptRange, type TranscriptRead } from './turns.js';
2
+ export declare const MAX_TURNS = 200;
3
+ export declare const MAX_OUTPUT_BYTES: number;
4
+ export type MutableTool = {
5
+ id: string;
6
+ name: string;
7
+ input?: string;
8
+ output?: string;
9
+ outputLines: number;
10
+ outputBytes: number;
11
+ };
12
+ export type MutableTurn = {
13
+ id: string | null;
14
+ at: number;
15
+ role: 'user' | 'assistant';
16
+ text?: string;
17
+ tools: MutableTool[];
18
+ };
19
+ export type ParsedEvent = {
20
+ at: number | null;
21
+ turn: MutableTurn | null;
22
+ toolOutputs?: readonly {
23
+ id: string;
24
+ text: string;
25
+ }[];
26
+ };
27
+ export type Parse = (value: unknown) => ParsedEvent | null;
28
+ export declare function claudeEvent(value: unknown): ParsedEvent | null;
29
+ export declare function codexEvent(value: unknown): ParsedEvent | null;
30
+ export declare function piEvent(value: unknown): ParsedEvent | null;
31
+ export declare function geminiEvent(value: unknown): ParsedEvent | null;
32
+ export declare function openclawEvent(value: unknown): ParsedEvent | null;
33
+ export declare function hermesEvents(value: unknown): ParsedEvent[];
34
+ export declare function opencodeEvents(value: unknown): ParsedEvent[];
35
+ export declare class IntervalCollector {
36
+ readonly turns: MutableTurn[];
37
+ private readonly byTool;
38
+ private readonly evicted;
39
+ private readonly synthesized;
40
+ sawTimestamp: boolean;
41
+ omittedTurns: number;
42
+ omittedBytes: number;
43
+ outOfOrderEvents: number;
44
+ private pastRange;
45
+ private readonly range;
46
+ constructor(range: TranscriptRange);
47
+ extend(to: number): void;
48
+ add(event: ParsedEvent): boolean;
49
+ finish(revision: string, harness: string): TranscriptRead;
50
+ }