tickmarkr 2.4.1 → 2.5.0

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 (75) hide show
  1. package/README.md +110 -24
  2. package/dist/adapters/claude-code.js +7 -2
  3. package/dist/adapters/codex.d.ts +1 -0
  4. package/dist/adapters/codex.js +66 -5
  5. package/dist/adapters/types.d.ts +4 -0
  6. package/dist/adapters/types.js +33 -0
  7. package/dist/cli/commands/approve.d.ts +42 -0
  8. package/dist/cli/commands/approve.js +80 -13
  9. package/dist/cli/commands/doctor.d.ts +234 -0
  10. package/dist/cli/commands/doctor.js +139 -5
  11. package/dist/cli/commands/report.js +15 -44
  12. package/dist/cli/commands/scope.js +36 -6
  13. package/dist/cli/commands/stats.d.ts +2 -0
  14. package/dist/cli/commands/stats.js +42 -23
  15. package/dist/cli/commands/status.js +20 -4
  16. package/dist/cli/commands/ui.js +42 -53
  17. package/dist/cli/commands/unlock.d.ts +1 -1
  18. package/dist/cli/commands/unlock.js +59 -9
  19. package/dist/cli/help.d.ts +219 -0
  20. package/dist/cli/help.js +212 -0
  21. package/dist/cli/index.d.ts +42 -2
  22. package/dist/cli/index.js +23 -8
  23. package/dist/drivers/herdr.d.ts +7 -2
  24. package/dist/drivers/herdr.js +78 -48
  25. package/dist/drivers/orca.d.ts +3 -2
  26. package/dist/drivers/orca.js +43 -4
  27. package/dist/drivers/subprocess.d.ts +1 -0
  28. package/dist/drivers/subprocess.js +3 -0
  29. package/dist/drivers/types.d.ts +14 -0
  30. package/dist/gates/artifact-manifest.d.ts +50 -0
  31. package/dist/gates/artifact-manifest.js +23 -0
  32. package/dist/plan/scope.d.ts +25 -0
  33. package/dist/plan/scope.js +92 -12
  34. package/dist/report/operator-record.d.ts +49 -0
  35. package/dist/report/operator-record.js +137 -0
  36. package/dist/run/daemon.d.ts +3 -4
  37. package/dist/run/daemon.js +18 -10
  38. package/dist/run/lock.d.ts +59 -2
  39. package/dist/run/lock.js +184 -26
  40. package/dist/run/operator-state.d.ts +86 -0
  41. package/dist/run/operator-state.js +165 -0
  42. package/dist/run/supervision.d.ts +32 -0
  43. package/dist/run/supervision.js +138 -17
  44. package/dist/tui/cockpit/capture.d.ts +19 -0
  45. package/dist/tui/cockpit/capture.js +89 -1
  46. package/dist/tui/cockpit/components.d.ts +15 -1
  47. package/dist/tui/cockpit/components.js +79 -9
  48. package/dist/tui/cockpit/decision-actions.d.ts +147 -0
  49. package/dist/tui/cockpit/decision-actions.js +315 -0
  50. package/dist/tui/cockpit/derive.d.ts +1 -1
  51. package/dist/tui/cockpit/derive.js +2 -0
  52. package/dist/tui/cockpit/evidence-view.d.ts +119 -0
  53. package/dist/tui/cockpit/evidence-view.js +210 -0
  54. package/dist/tui/cockpit/home-view.d.ts +88 -0
  55. package/dist/tui/cockpit/home-view.js +240 -0
  56. package/dist/tui/cockpit/keys.d.ts +125 -0
  57. package/dist/tui/cockpit/keys.js +31 -0
  58. package/dist/tui/cockpit/layout.d.ts +14 -0
  59. package/dist/tui/cockpit/layout.js +15 -0
  60. package/dist/tui/cockpit/live-runtime.d.ts +46 -0
  61. package/dist/tui/cockpit/live-runtime.js +683 -0
  62. package/dist/tui/cockpit/live-store.d.ts +289 -0
  63. package/dist/tui/cockpit/live-store.js +308 -0
  64. package/dist/tui/cockpit/live.d.ts +21 -1
  65. package/dist/tui/cockpit/live.js +12 -1
  66. package/dist/tui/cockpit/run-view.d.ts +100 -0
  67. package/dist/tui/cockpit/run-view.js +202 -0
  68. package/dist/tui/cockpit/shell.d.ts +50 -0
  69. package/dist/tui/cockpit/shell.js +74 -0
  70. package/dist/tui/cockpit/theme.d.ts +27 -0
  71. package/dist/tui/cockpit/theme.js +21 -0
  72. package/package.json +1 -1
  73. package/skills/tickmarkr-auto/SKILL.md +10 -1
  74. package/skills/tickmarkr-loop/SKILL.md +72 -1
  75. package/skills/tickmarkr-overseer/SKILL.md +12 -0
@@ -0,0 +1,289 @@
1
+ import { type JournalEvent } from "../../run/journal.js";
2
+ import { type OperatorRecord } from "../../run/operator-state.js";
3
+ export declare const OBSERVATION_INTERVAL_MS = 1000;
4
+ export declare const STORE_LIMITS: {
5
+ readonly history: 256;
6
+ readonly historyBytes: number;
7
+ readonly recordBytes: number;
8
+ readonly readBytes: number;
9
+ readonly subscribers: 64;
10
+ readonly metrics: 12;
11
+ readonly errors: 32;
12
+ };
13
+ export interface SourceError {
14
+ source: string;
15
+ error: string;
16
+ line?: number;
17
+ id?: string;
18
+ }
19
+ export interface JournalLine extends Omit<OperatorRecord, "event"> {
20
+ event?: JournalEvent;
21
+ raw: string;
22
+ error?: string;
23
+ offset: number;
24
+ endOffset: number;
25
+ }
26
+ export interface TailSnapshot {
27
+ source: string;
28
+ generation: number;
29
+ identity?: string;
30
+ offset: number;
31
+ lines: number;
32
+ history: readonly JournalLine[];
33
+ errors: readonly SourceError[];
34
+ malformedCount: number;
35
+ pending: {
36
+ line: number;
37
+ bytes: number;
38
+ } | undefined;
39
+ backlogBytes: number;
40
+ status: "readable" | "pending" | "corrupt" | "unreadable";
41
+ error?: SourceError;
42
+ lastSuccessfulReadAt?: number;
43
+ bytesRead: number;
44
+ }
45
+ /** Byte-offset tail with a fixed read budget. Idle observations stat, but read no journal bytes. */
46
+ export declare class JournalTail {
47
+ readonly source: string;
48
+ private readonly hooks;
49
+ private st?;
50
+ private offset;
51
+ private line;
52
+ private generation;
53
+ private carry;
54
+ private carryBytes;
55
+ private lineOffset;
56
+ private history;
57
+ private historyBytes;
58
+ private errors;
59
+ private malformedCount;
60
+ private bytesRead;
61
+ private lastSuccessfulReadAt?;
62
+ private failure?;
63
+ constructor(source: string, hooks?: {
64
+ record?: (record: OperatorRecord) => void;
65
+ reset?: () => void;
66
+ });
67
+ private reset;
68
+ poll(now?: number): TailSnapshot;
69
+ private appendCarry;
70
+ snapshot(): TailSnapshot;
71
+ /** Demand paging scans with fixed buffers, independent of the retained history; no growing line index. */
72
+ page(firstLine: number, count?: number, generation?: number): readonly JournalLine[];
73
+ }
74
+ export interface CachedSource<T = unknown> {
75
+ source: string;
76
+ identity?: string;
77
+ value?: T;
78
+ status: "readable" | "absent" | "unreadable";
79
+ error?: string;
80
+ observedAt: number;
81
+ }
82
+ export interface LiveStoreOptions {
83
+ cwd: string;
84
+ runId: string;
85
+ now?: () => number;
86
+ configPath?: string;
87
+ cachePath?: string;
88
+ isDaemonAlive?: (pid: number) => boolean;
89
+ }
90
+ /** C1 mount/C6 reader boundary. No timers or presence writes: the mount owns scheduling and disarm. */
91
+ export declare function createLiveStore(options: LiveStoreOptions): {
92
+ snapshot: () => {
93
+ sequence: number;
94
+ observedAt: number;
95
+ delayed: boolean;
96
+ freshness: string;
97
+ operator: import("../../run/operator-state.js").OperatorSnapshot;
98
+ journal: TailSnapshot;
99
+ graph: CachedSource<{
100
+ version: 1;
101
+ spec: {
102
+ source: "speckit" | "gsd" | "prd" | "native";
103
+ paths: string[];
104
+ hash: string;
105
+ base?: string | undefined;
106
+ };
107
+ tasks: {
108
+ id: string;
109
+ title: string;
110
+ goal: string;
111
+ shape: "plan" | "spec" | "implement" | "tests" | "docs" | "migration" | "ui" | "refactor" | "chore";
112
+ complexity: number;
113
+ deps: string[];
114
+ files: string[];
115
+ context: string[];
116
+ acceptance: (string | {
117
+ oracle: "command";
118
+ command: string;
119
+ text?: string | undefined;
120
+ } | {
121
+ oracle: "test";
122
+ test: string;
123
+ text?: string | undefined;
124
+ } | {
125
+ oracle: "judge";
126
+ text: string;
127
+ })[];
128
+ gates: ("build" | "test" | "lint" | "evidence" | "scope" | "acceptance" | "review")[];
129
+ humanGate: boolean;
130
+ status: "pending" | "running" | "gated" | "failed" | "done" | "human";
131
+ evidence: {
132
+ commits: string[];
133
+ artifacts: string[];
134
+ gateResults: unknown[];
135
+ };
136
+ scopeHints?: {
137
+ paths: string[];
138
+ confidence?: number | undefined;
139
+ reason?: string | undefined;
140
+ }[] | undefined;
141
+ routingHints?: {
142
+ pin?: {
143
+ via: string;
144
+ model: string;
145
+ } | undefined;
146
+ floor?: "cheap" | "mid" | "frontier" | undefined;
147
+ source?: string | undefined;
148
+ escalate?: boolean | undefined;
149
+ } | undefined;
150
+ timeoutMinutes?: number | undefined;
151
+ }[];
152
+ mode?: "partner-led" | "risk-based" | "staff-led" | undefined;
153
+ }>;
154
+ config: CachedSource<string>;
155
+ cache: CachedSource<any>;
156
+ lock: {
157
+ state: string;
158
+ alive: boolean | undefined;
159
+ expired: boolean | undefined;
160
+ source: string;
161
+ identity?: string;
162
+ value?: {
163
+ pid: number;
164
+ runId: string;
165
+ startedAt: number;
166
+ } | undefined;
167
+ status: "readable" | "absent" | "unreadable";
168
+ error?: string;
169
+ observedAt: number;
170
+ };
171
+ supervision: import("../../run/supervision.js").TierLiveness[];
172
+ errors: SourceError[];
173
+ actionsEnabled: boolean;
174
+ viewport: {
175
+ columns: number;
176
+ rows: number;
177
+ };
178
+ inputSequence: number;
179
+ metrics: {
180
+ observedAt: number;
181
+ bytesRead: number;
182
+ }[];
183
+ };
184
+ refresh: () => {
185
+ sequence: number;
186
+ observedAt: number;
187
+ delayed: boolean;
188
+ freshness: string;
189
+ operator: import("../../run/operator-state.js").OperatorSnapshot;
190
+ journal: TailSnapshot;
191
+ graph: CachedSource<{
192
+ version: 1;
193
+ spec: {
194
+ source: "speckit" | "gsd" | "prd" | "native";
195
+ paths: string[];
196
+ hash: string;
197
+ base?: string | undefined;
198
+ };
199
+ tasks: {
200
+ id: string;
201
+ title: string;
202
+ goal: string;
203
+ shape: "plan" | "spec" | "implement" | "tests" | "docs" | "migration" | "ui" | "refactor" | "chore";
204
+ complexity: number;
205
+ deps: string[];
206
+ files: string[];
207
+ context: string[];
208
+ acceptance: (string | {
209
+ oracle: "command";
210
+ command: string;
211
+ text?: string | undefined;
212
+ } | {
213
+ oracle: "test";
214
+ test: string;
215
+ text?: string | undefined;
216
+ } | {
217
+ oracle: "judge";
218
+ text: string;
219
+ })[];
220
+ gates: ("build" | "test" | "lint" | "evidence" | "scope" | "acceptance" | "review")[];
221
+ humanGate: boolean;
222
+ status: "pending" | "running" | "gated" | "failed" | "done" | "human";
223
+ evidence: {
224
+ commits: string[];
225
+ artifacts: string[];
226
+ gateResults: unknown[];
227
+ };
228
+ scopeHints?: {
229
+ paths: string[];
230
+ confidence?: number | undefined;
231
+ reason?: string | undefined;
232
+ }[] | undefined;
233
+ routingHints?: {
234
+ pin?: {
235
+ via: string;
236
+ model: string;
237
+ } | undefined;
238
+ floor?: "cheap" | "mid" | "frontier" | undefined;
239
+ source?: string | undefined;
240
+ escalate?: boolean | undefined;
241
+ } | undefined;
242
+ timeoutMinutes?: number | undefined;
243
+ }[];
244
+ mode?: "partner-led" | "risk-based" | "staff-led" | undefined;
245
+ }>;
246
+ config: CachedSource<string>;
247
+ cache: CachedSource<any>;
248
+ lock: {
249
+ state: string;
250
+ alive: boolean | undefined;
251
+ expired: boolean | undefined;
252
+ source: string;
253
+ identity?: string;
254
+ value?: {
255
+ pid: number;
256
+ runId: string;
257
+ startedAt: number;
258
+ } | undefined;
259
+ status: "readable" | "absent" | "unreadable";
260
+ error?: string;
261
+ observedAt: number;
262
+ };
263
+ supervision: import("../../run/supervision.js").TierLiveness[];
264
+ errors: SourceError[];
265
+ actionsEnabled: boolean;
266
+ viewport: {
267
+ columns: number;
268
+ rows: number;
269
+ };
270
+ inputSequence: number;
271
+ metrics: {
272
+ observedAt: number;
273
+ bytesRead: number;
274
+ }[];
275
+ };
276
+ requestRefresh: () => Promise<void>;
277
+ subscribe: (listener: () => void) => () => void;
278
+ input: () => void;
279
+ resize: (columns: number, rows: number) => void;
280
+ page: (firstLine: number, count?: number, generation?: number) => readonly JournalLine[];
281
+ diagnostics: () => {
282
+ subscriptions: number;
283
+ pendingReads: number;
284
+ metrics: number;
285
+ };
286
+ dispose: () => void;
287
+ };
288
+ export type LiveStore = ReturnType<typeof createLiveStore>;
289
+ export type LiveStoreSnapshot = ReturnType<LiveStore["snapshot"]>;
@@ -0,0 +1,308 @@
1
+ import { closeSync, fstatSync, openSync, readFileSync, readSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { graphPath, stateDirName } from "../../graph/graph.js";
4
+ import { validateGraph } from "../../graph/schema.js";
5
+ import { parseRunId } from "../../run/journal.js";
6
+ import { isPidLive, STALE_MS } from "../../run/lock.js";
7
+ import { readTierLiveness, SUPERVISION_TIERS } from "../../run/supervision.js";
8
+ import { OperatorStateFold } from "../../run/operator-state.js";
9
+ export const OBSERVATION_INTERVAL_MS = 1_000;
10
+ export const STORE_LIMITS = { history: 256, historyBytes: 2 * 1024 * 1024, recordBytes: 1024 * 1024, readBytes: 1024 * 1024, subscribers: 64, metrics: 12, errors: 32 };
11
+ const errorText = (e) => e instanceof Error ? e.message : String(e);
12
+ const fileIdentity = (st) => `${st.dev}:${st.ino}`;
13
+ const stamp = (st) => `${fileIdentity(st)}:${st.size}:${st.mtimeNs}:${st.ctimeNs}`;
14
+ function decodeLine(bytes, source, line, offset, endOffset, generation, oversized = false) {
15
+ const base = { source, line, id: `${source}#L${line}`, offset, endOffset, generation };
16
+ let raw = "";
17
+ try {
18
+ if (oversized)
19
+ throw new Error(`record exceeds ${STORE_LIMITS.recordBytes} bytes; page raw evidence from disk`);
20
+ raw = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
21
+ if (!raw.trim())
22
+ return { ...base, raw };
23
+ const e = JSON.parse(raw);
24
+ if (!e || typeof e !== "object" || typeof e.ts !== "string" || !Number.isFinite(Date.parse(e.ts)) || typeof e.event !== "string" || !e.data || typeof e.data !== "object" || Array.isArray(e.data) || (e.taskId !== undefined && typeof e.taskId !== "string"))
25
+ throw new Error("invalid journal record");
26
+ return { ...base, raw, event: e };
27
+ }
28
+ catch (e) {
29
+ return { ...base, raw: raw || bytes.toString("utf8"), error: errorText(e) };
30
+ }
31
+ }
32
+ /** Byte-offset tail with a fixed read budget. Idle observations stat, but read no journal bytes. */
33
+ export class JournalTail {
34
+ source;
35
+ hooks;
36
+ st;
37
+ offset = 0;
38
+ line = 0;
39
+ generation = 0;
40
+ carry = Buffer.alloc(0);
41
+ carryBytes = 0;
42
+ lineOffset = 0;
43
+ history = [];
44
+ historyBytes = 0;
45
+ errors = [];
46
+ malformedCount = 0;
47
+ bytesRead = 0;
48
+ lastSuccessfulReadAt;
49
+ failure;
50
+ constructor(source, hooks = {}) {
51
+ this.source = source;
52
+ this.hooks = hooks;
53
+ }
54
+ reset() {
55
+ this.offset = 0;
56
+ this.line = 0;
57
+ this.carry = Buffer.alloc(0);
58
+ this.carryBytes = 0;
59
+ this.lineOffset = 0;
60
+ this.history = [];
61
+ this.historyBytes = 0;
62
+ this.errors = [];
63
+ this.malformedCount = 0;
64
+ this.generation++;
65
+ this.hooks.reset?.();
66
+ }
67
+ poll(now = Date.now()) {
68
+ let fd;
69
+ try {
70
+ const st = statSync(this.source, { bigint: true });
71
+ if (!st.isFile())
72
+ throw new Error("journal source is not a regular file");
73
+ if (!this.st || fileIdentity(st) !== fileIdentity(this.st) || st.size < this.st.size || (st.size === this.st.size && stamp(st) !== stamp(this.st)))
74
+ this.reset();
75
+ if (Number(st.size) > this.offset) {
76
+ fd = openSync(this.source, "r");
77
+ if (stamp(fstatSync(fd, { bigint: true })) !== stamp(st))
78
+ throw new Error("journal changed before read; retry observation");
79
+ let remaining = STORE_LIMITS.readBytes;
80
+ const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, Number(st.size) - this.offset));
81
+ while (this.offset < Number(st.size) && remaining > 0) {
82
+ const count = readSync(fd, buffer, 0, Math.min(buffer.length, Number(st.size) - this.offset, remaining), this.offset);
83
+ if (!count)
84
+ throw new Error("journal truncated during read");
85
+ this.bytesRead += count;
86
+ remaining -= count;
87
+ let begin = 0;
88
+ for (let index = 0; index < count; index++)
89
+ if (buffer[index] === 10) {
90
+ this.appendCarry(buffer.subarray(begin, index));
91
+ const endOffset = this.offset + index + 1;
92
+ const row = decodeLine(this.carry, this.source, ++this.line, this.lineOffset, endOffset, this.generation, this.carryBytes > STORE_LIMITS.recordBytes);
93
+ this.history.push(row);
94
+ this.historyBytes += Buffer.byteLength(row.raw);
95
+ while (this.history.length > STORE_LIMITS.history || this.historyBytes > STORE_LIMITS.historyBytes)
96
+ this.historyBytes -= Buffer.byteLength(this.history.shift().raw);
97
+ if (row.error) {
98
+ this.malformedCount++;
99
+ this.errors.push({ source: row.source, error: row.error, line: row.line, id: row.id });
100
+ if (this.errors.length > STORE_LIMITS.errors)
101
+ this.errors.shift();
102
+ }
103
+ if (row.event)
104
+ this.hooks.record?.({ ...row, event: row.event });
105
+ this.carry = Buffer.alloc(0);
106
+ this.carryBytes = 0;
107
+ this.lineOffset = endOffset;
108
+ begin = index + 1;
109
+ }
110
+ this.appendCarry(buffer.subarray(begin, count));
111
+ this.offset += count;
112
+ }
113
+ }
114
+ this.st = st;
115
+ this.failure = undefined;
116
+ this.lastSuccessfulReadAt = now;
117
+ }
118
+ catch (e) {
119
+ this.failure = { source: this.source, error: errorText(e) };
120
+ }
121
+ finally {
122
+ if (fd !== undefined)
123
+ closeSync(fd);
124
+ }
125
+ return this.snapshot();
126
+ }
127
+ appendCarry(bytes) {
128
+ this.carryBytes += bytes.length;
129
+ const available = Math.max(0, STORE_LIMITS.recordBytes - this.carry.length);
130
+ if (available && bytes.length)
131
+ this.carry = Buffer.concat([this.carry, bytes.subarray(0, available)]);
132
+ }
133
+ snapshot() {
134
+ return {
135
+ source: this.source, generation: this.generation, identity: this.st && fileIdentity(this.st), offset: this.offset, lines: this.line,
136
+ history: this.history.slice(), errors: this.errors.slice(), malformedCount: this.malformedCount,
137
+ pending: this.carryBytes ? { line: this.line + 1, bytes: this.carryBytes } : undefined,
138
+ backlogBytes: Math.max(0, Number(this.st?.size ?? 0) - this.offset),
139
+ status: this.failure ? "unreadable" : this.malformedCount ? "corrupt" : this.carryBytes ? "pending" : "readable",
140
+ error: this.failure, lastSuccessfulReadAt: this.lastSuccessfulReadAt, bytesRead: this.bytesRead,
141
+ };
142
+ }
143
+ /** Demand paging scans with fixed buffers, independent of the retained history; no growing line index. */
144
+ page(firstLine, count = 32, generation = this.generation) {
145
+ if (!Number.isInteger(firstLine) || firstLine < 1)
146
+ throw new Error("evidence line must be a positive integer");
147
+ if (generation !== this.generation)
148
+ throw new Error("evidence belongs to a replaced journal");
149
+ const rows = [];
150
+ const reader = new JournalTail(this.source, { record: undefined });
151
+ // Read only enough for the requested page. The retained scanner window is bounded, even on huge files.
152
+ const fd = openSync(this.source, "r");
153
+ try {
154
+ const st = fstatSync(fd, { bigint: true });
155
+ if (this.st && stamp(st) !== stamp(this.st))
156
+ throw new Error("journal changed; refresh before paging");
157
+ const buffer = Buffer.allocUnsafe(64 * 1024);
158
+ let position = 0, line = 0, start = 0;
159
+ const limit = Math.max(1, Math.min(STORE_LIMITS.history, Math.floor(count)));
160
+ while (position < Number(st.size) && rows.length < limit) {
161
+ const n = readSync(fd, buffer, 0, buffer.length, position);
162
+ if (!n)
163
+ break;
164
+ let begin = 0;
165
+ for (let i = 0; i < n; i++)
166
+ if (buffer[i] === 10) {
167
+ if (line + 1 >= firstLine)
168
+ reader.appendCarry(buffer.subarray(begin, i));
169
+ line++;
170
+ if (line >= firstLine)
171
+ rows.push(decodeLine(reader.carry, this.source, line, start, position + i + 1, generation, reader.carryBytes > STORE_LIMITS.recordBytes));
172
+ reader.carry = Buffer.alloc(0);
173
+ reader.carryBytes = 0;
174
+ start = position + i + 1;
175
+ begin = i + 1;
176
+ if (rows.length >= limit)
177
+ break;
178
+ }
179
+ if (line + 1 >= firstLine && rows.length < limit)
180
+ reader.appendCarry(buffer.subarray(begin, n));
181
+ position += n;
182
+ }
183
+ if (stamp(fstatSync(fd, { bigint: true })) !== stamp(st))
184
+ throw new Error("journal changed during evidence read");
185
+ return rows;
186
+ }
187
+ finally {
188
+ closeSync(fd);
189
+ }
190
+ }
191
+ }
192
+ class JsonSource {
193
+ path;
194
+ parse;
195
+ cached;
196
+ constructor(path, parse) {
197
+ this.path = path;
198
+ this.parse = parse;
199
+ }
200
+ read(now) {
201
+ try {
202
+ const st = statSync(this.path, { bigint: true });
203
+ const id = stamp(st);
204
+ if (this.cached?.identity === id && this.cached.status === "readable")
205
+ return this.cached = { ...this.cached, observedAt: now };
206
+ if (!st.isFile() || st.size > BigInt(STORE_LIMITS.recordBytes))
207
+ throw new Error("source is not a bounded regular file");
208
+ const value = this.parse(readFileSync(this.path, "utf8"));
209
+ return this.cached = { source: this.path, identity: id, value, status: "readable", observedAt: now };
210
+ }
211
+ catch (e) {
212
+ const absent = e.code === "ENOENT";
213
+ return this.cached = { source: this.path, status: absent ? "absent" : "unreadable", error: absent ? undefined : errorText(e), observedAt: now };
214
+ }
215
+ }
216
+ }
217
+ /** C1 mount/C6 reader boundary. No timers or presence writes: the mount owns scheduling and disarm. */
218
+ export function createLiveStore(options) {
219
+ const now = options.now ?? Date.now;
220
+ const state = join(options.cwd, stateDirName(options.cwd));
221
+ let fold = new OperatorStateFold();
222
+ const tail = new JournalTail(join(state, "runs", parseRunId(options.runId), "journal.jsonl"), {
223
+ reset: () => { fold = new OperatorStateFold(); }, record: record => fold.apply(record),
224
+ });
225
+ const graph = new JsonSource(graphPath(options.cwd), raw => validateGraph(JSON.parse(raw)));
226
+ const config = new JsonSource(options.configPath ?? join(state, "config.yaml"), raw => raw);
227
+ const cache = new JsonSource(options.cachePath ?? join(state, "doctor.json"), JSON.parse);
228
+ const lock = new JsonSource(join(state, "graph.lock"), raw => {
229
+ const v = JSON.parse(raw);
230
+ if (!v || !Number.isInteger(v.pid) || v.pid < 1 || typeof v.runId !== "string" || typeof v.startedAt !== "number")
231
+ throw new Error("invalid lock payload");
232
+ return v;
233
+ });
234
+ const listeners = new Set();
235
+ let sequence = 0, lastObservation, inputSequence = 0;
236
+ let viewport = { columns: 120, rows: 40 };
237
+ let disposed = false;
238
+ let queued;
239
+ const metrics = [];
240
+ function observe() {
241
+ const observedAt = now();
242
+ const delayed = lastObservation !== undefined && observedAt - lastObservation > 2 * OBSERVATION_INTERVAL_MS;
243
+ const journal = tail.poll(observedAt);
244
+ const graphReading = graph.read(observedAt);
245
+ const configReading = config.read(observedAt);
246
+ const cacheReading = cache.read(observedAt);
247
+ const owner = lock.read(observedAt);
248
+ const alive = owner.status === "readable" && owner.value ? (options.isDaemonAlive ?? isPidLive)(owner.value.pid) : undefined;
249
+ const lockState = owner.status === "absent" ? "absent" : owner.status === "unreadable" ? "unreadable" : owner.value?.runId !== options.runId ? "foreign" : alive ? "alive" : "dead";
250
+ const supervision = SUPERVISION_TIERS.map(t => readTierLiveness(options.cwd, t, observedAt));
251
+ const errors = [journal.error, ...journal.errors, ...[graphReading, configReading, cacheReading, owner].filter(s => s.error).map(s => ({ source: s.source, error: s.error })), ...supervision.filter(s => s.state === "UNREADABLE").map(s => ({ source: join(state, "supervision", s.tier), error: "unreadable supervision" }))].filter((e) => !!e);
252
+ metrics.push({ observedAt, bytesRead: journal.bytesRead });
253
+ if (metrics.length > STORE_LIMITS.metrics)
254
+ metrics.shift();
255
+ lastObservation = observedAt;
256
+ const readable = journal.status === "readable" && journal.backlogBytes === 0;
257
+ return {
258
+ sequence: ++sequence, observedAt, delayed, freshness: errors.length ? "failed" : delayed ? "delayed" : "fresh",
259
+ operator: fold.snapshot({ graph: graphReading.status === "readable" ? graphReading.value : undefined, sequence, observedAt, readable }),
260
+ journal, graph: graphReading, config: configReading, cache: cacheReading,
261
+ lock: { ...owner, state: lockState, alive, expired: owner.identity ? observedAt - Number(owner.identity.split(":")[3]) / 1e6 > STALE_MS : undefined },
262
+ supervision, errors, actionsEnabled: readable && !errors.length && !delayed,
263
+ viewport, inputSequence, metrics: metrics.slice(),
264
+ };
265
+ }
266
+ let snapshot = observe();
267
+ const publish = () => { for (const listener of listeners)
268
+ listener(); };
269
+ const refresh = () => { if (!disposed) {
270
+ snapshot = observe();
271
+ publish();
272
+ } return snapshot; };
273
+ return {
274
+ snapshot: () => snapshot, refresh,
275
+ requestRefresh: () => {
276
+ if (disposed)
277
+ return Promise.resolve();
278
+ return queued ??= Promise.resolve().then(() => { refresh(); }).finally(() => { queued = undefined; });
279
+ },
280
+ subscribe: (listener) => {
281
+ if (disposed)
282
+ throw new Error("store is disposed");
283
+ if (listeners.size >= STORE_LIMITS.subscribers)
284
+ throw new Error("store subscription limit reached");
285
+ listeners.add(listener);
286
+ return () => { listeners.delete(listener); };
287
+ },
288
+ input: () => {
289
+ if (!disposed) {
290
+ inputSequence++;
291
+ const nextSequence = ++sequence;
292
+ snapshot = { ...snapshot, sequence: nextSequence, operator: { ...snapshot.operator, sequence: nextSequence }, inputSequence };
293
+ publish();
294
+ }
295
+ },
296
+ resize: (columns, rows) => {
297
+ if (!disposed) {
298
+ viewport = { columns, rows };
299
+ const nextSequence = ++sequence;
300
+ snapshot = { ...snapshot, sequence: nextSequence, operator: { ...snapshot.operator, sequence: nextSequence }, viewport };
301
+ publish();
302
+ }
303
+ },
304
+ page: (firstLine, count, generation) => tail.page(firstLine, count, generation),
305
+ diagnostics: () => ({ subscriptions: listeners.size, pendingReads: queued ? 1 : 0, metrics: metrics.length }),
306
+ dispose: () => { disposed = true; listeners.clear(); },
307
+ };
308
+ }
@@ -1,3 +1,4 @@
1
+ import { type ShellDelivery } from "./live-runtime.js";
1
2
  import type { CockpitGraph, RunCockpitData, RunCockpitSource } from "./derive.js";
2
3
  import { type RunInteractionState, type RunKeyEvent } from "./keys.js";
3
4
  import { type PointerReport, type PointerSurface } from "./pointer.js";
@@ -78,7 +79,11 @@ export type LiveCockpitDelivery = {
78
79
  readonly pointer: (report: PointerReport) => boolean;
79
80
  readonly batch: <Result>(deliver: () => Result) => Result;
80
81
  };
81
- export declare function runLiveCockpit({ input, output, cwd, runId, binaryVersion, refreshMs, now, debug, onDelivery, }: {
82
+ /**
83
+ * The retired single-view mount remains exported only for its historical
84
+ * compatibility suite. Product callers must use {@link runLiveCockpit}.
85
+ */
86
+ export declare function runLegacyCockpit({ input, output, cwd, runId, binaryVersion, refreshMs, now, debug, onDelivery, }: {
82
87
  input: NodeJS.ReadStream;
83
88
  output: NodeJS.WriteStream;
84
89
  cwd: string;
@@ -91,3 +96,18 @@ export declare function runLiveCockpit({ input, output, cwd, runId, binaryVersio
91
96
  /** Observe and control the production delivery boundary. */
92
97
  onDelivery?: (delivery: LiveCockpitDelivery) => void;
93
98
  }): Promise<void>;
99
+ /** The production cockpit entry: one measured FINAL shell mounting C3–C5. */
100
+ export declare function runLiveCockpit(options: {
101
+ input: NodeJS.ReadStream;
102
+ output: NodeJS.WriteStream;
103
+ cwd: string;
104
+ runId: string;
105
+ binaryVersion: string;
106
+ refreshMs?: number;
107
+ now?: () => number;
108
+ debug?: boolean;
109
+ /** Compatibility observer retained for the pre-existing production-mount harness. */
110
+ onDelivery?: (delivery: ShellDelivery) => void;
111
+ onShellDelivery?: (delivery: ShellDelivery) => void;
112
+ environment?: NodeJS.ProcessEnv;
113
+ }): Promise<void>;
@@ -1,3 +1,4 @@
1
+ import { borrowRuntimeTimeline, runConsolidatedCockpit } from "./live-runtime.js";
1
2
  import { existsSync, readFileSync } from "node:fs";
2
3
  import { join } from "node:path";
3
4
  import { render, useApp, useInput } from "ink";
@@ -441,7 +442,12 @@ function LiveApp({ delivery, size, refreshMs, }) {
441
442
  onCommittedFrame: commitFrame,
442
443
  });
443
444
  }
444
- export async function runLiveCockpit({ input, output, cwd, runId, binaryVersion, refreshMs = REFRESH_MS, now = Date.now, debug = false, onDelivery, }) {
445
+ /**
446
+ * The retired single-view mount remains exported only for its historical
447
+ * compatibility suite. Product callers must use {@link runLiveCockpit}.
448
+ */
449
+ export async function runLegacyCockpit({ input, output, cwd, runId, binaryVersion, refreshMs = REFRESH_MS, now = Date.now, debug = false, onDelivery, }) {
450
+ const releaseTimeline = borrowRuntimeTimeline();
445
451
  // Subscribe before Ink mounts so no stale-size repaint can overtake the
446
452
  // surface's newly planned frame — and before the delivery exists, which
447
453
  // routes every input against this measurement.
@@ -498,8 +504,13 @@ export async function runLiveCockpit({ input, output, cwd, runId, binaryVersion,
498
504
  }
499
505
  finally {
500
506
  size.close();
507
+ releaseTimeline();
501
508
  }
502
509
  }
503
510
  }
504
511
  }
505
512
  }
513
+ /** The production cockpit entry: one measured FINAL shell mounting C3–C5. */
514
+ export async function runLiveCockpit(options) {
515
+ return runConsolidatedCockpit(options);
516
+ }