tickmarkr 2.6.4 → 2.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/catalog-remote.d.ts +23 -0
- package/dist/adapters/catalog-remote.js +38 -0
- package/dist/adapters/claude-code.d.ts +1 -1
- package/dist/adapters/claude-code.js +4 -1
- package/dist/adapters/codex-commit-check.d.ts +25 -0
- package/dist/adapters/codex-commit-check.js +108 -0
- package/dist/adapters/model-discovery-check.d.ts +33 -0
- package/dist/adapters/model-discovery-check.js +123 -0
- package/dist/adapters/model-lints.d.ts +1 -1
- package/dist/adapters/model-lints.js +4 -0
- package/dist/adapters/prompt.js +3 -2
- package/dist/adapters/types.d.ts +7 -0
- package/dist/cli/commands/approve.js +10 -4
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.js +14 -2
- package/dist/cli/commands/fleet.d.ts +10 -1
- package/dist/cli/commands/fleet.js +55 -38
- package/dist/cli/commands/init.js +5 -1
- package/dist/cli/commands/plan.js +65 -4
- package/dist/cli/commands/verify.d.ts +1 -1
- package/dist/cli/commands/verify.js +28 -6
- package/dist/config/fleet-overlay.d.ts +2 -1
- package/dist/config/fleet-overlay.js +77 -4
- package/dist/drivers/orca.d.ts +19 -1
- package/dist/drivers/orca.js +161 -31
- package/dist/gates/cache.d.ts +4 -0
- package/dist/gates/cache.js +10 -2
- package/dist/gates/llm.d.ts +4 -1
- package/dist/gates/llm.js +6 -1
- package/dist/gates/review.js +8 -3
- package/dist/gates/run-gates.d.ts +3 -2
- package/dist/gates/run-gates.js +91 -50
- package/dist/run/daemon.js +150 -19
- package/dist/run/git.js +9 -0
- package/dist/run/journal.d.ts +6 -2
- package/dist/run/journal.js +22 -7
- package/dist/run/launchservices-check.d.ts +56 -0
- package/dist/run/launchservices-check.js +117 -0
- package/dist/run/operator-state.d.ts +22 -2
- package/dist/run/operator-state.js +20 -3
- package/dist/run/title-environment.d.ts +45 -0
- package/dist/run/title-environment.js +127 -0
- package/dist/run/title-preload.cjs +38 -0
- package/dist/run/title-preload.d.cts +1 -0
- package/dist/tui/cockpit/board.js +12 -6
- package/dist/tui/cockpit/keys.d.ts +8 -0
- package/dist/tui/cockpit/keys.js +4 -1
- package/dist/tui/cockpit/layout.d.ts +2 -2
- package/dist/tui/cockpit/layout.js +2 -2
- package/dist/tui/cockpit/live-runtime.js +46 -20
- package/dist/tui/cockpit/live-store.d.ts +24 -1
- package/dist/tui/cockpit/live-store.js +99 -8
- package/dist/tui/cockpit/log-view.d.ts +28 -0
- package/dist/tui/cockpit/log-view.js +60 -0
- package/dist/tui/cockpit/run-view.js +4 -0
- package/dist/tui/cockpit/shell.d.ts +2 -1
- package/dist/tui/cockpit/shell.js +6 -4
- package/dist/tui/ink/fleet-app.d.ts +12 -6
- package/dist/tui/ink/fleet-app.js +17 -11
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +32 -2
- package/skills/tickmarkr-loop/SKILL.md +46 -2
- package/skills/tickmarkr-overseer/SKILL.md +83 -8
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type JournalEvent } from "../../run/journal.js";
|
|
2
|
-
import { type OperatorRecord } from "../../run/operator-state.js";
|
|
2
|
+
import { type OperatorRecord, type OwedAuthority } from "../../run/operator-state.js";
|
|
3
3
|
export declare const OBSERVATION_INTERVAL_MS = 1000;
|
|
4
4
|
/** Stat-to-fstat observations one poll spends on a journal a writer keeps appending to. */
|
|
5
5
|
export declare const APPEND_RACE_OBSERVATIONS = 3;
|
|
@@ -27,9 +27,12 @@ export interface JournalLine extends Omit<OperatorRecord, "event"> {
|
|
|
27
27
|
endOffset: number;
|
|
28
28
|
}
|
|
29
29
|
export interface TailSnapshot {
|
|
30
|
+
/** `digest` (I2): sha256 of every byte the tail consumed, [0, offset) — the bytes its fold was built from. */
|
|
30
31
|
source: string;
|
|
31
32
|
generation: number;
|
|
32
33
|
identity?: string;
|
|
34
|
+
version?: string;
|
|
35
|
+
digest: string;
|
|
33
36
|
offset: number;
|
|
34
37
|
lines: number;
|
|
35
38
|
history: readonly JournalLine[];
|
|
@@ -64,11 +67,17 @@ export declare class JournalTail {
|
|
|
64
67
|
private lastSuccessfulReadAt?;
|
|
65
68
|
private failure?;
|
|
66
69
|
private stale;
|
|
70
|
+
private hash;
|
|
71
|
+
private invalid;
|
|
67
72
|
constructor(source: string, hooks?: {
|
|
68
73
|
record?: (record: OperatorRecord) => void;
|
|
69
74
|
reset?: () => void;
|
|
70
75
|
});
|
|
71
76
|
private reset;
|
|
77
|
+
/** I2: the consumed prefix was found rewritten (an in-place rewrite then growth reads as an append), so the
|
|
78
|
+
* next poll treats the file as replaced and refolds it from its first byte. */
|
|
79
|
+
invalidate(): void;
|
|
80
|
+
get invalidated(): boolean;
|
|
72
81
|
/** A same-inode append between stat and fstat is retried; exhaustion keeps the last good snapshot, pending. */
|
|
73
82
|
poll(now?: number): TailSnapshot;
|
|
74
83
|
/** One stat-to-fstat observation. False when an append raced it; nothing was consumed or reset. */
|
|
@@ -86,6 +95,18 @@ export interface CachedSource<T = unknown> {
|
|
|
86
95
|
error?: string;
|
|
87
96
|
observedAt: number;
|
|
88
97
|
}
|
|
98
|
+
/** I2: the journal basis an owed-check fold read — the observed file version, its generation and the bytes consumed. */
|
|
99
|
+
export declare const journalBasis: (journal: TailSnapshot) => string;
|
|
100
|
+
/** I2: the bytes the tail consumed are no longer the file's prefix — rewritten in place, then grown. */
|
|
101
|
+
export declare class JournalRewritten extends Error {
|
|
102
|
+
constructor();
|
|
103
|
+
}
|
|
104
|
+
/** I2 (D-857): the ONE way any reader beside the tail gets `journal`'s bytes — every consumed byte of the very
|
|
105
|
+
* file version the tail observed (an in-place rewrite, even an equal-length one, changes it), stamped before
|
|
106
|
+
* and after the read, and proved by digest to be the very bytes the tail's fold consumed (a rewrite followed by
|
|
107
|
+
* growth keeps the tail appending). Any change throws, so no second read is ever combined with this
|
|
108
|
+
* observation's lifecycle, owed or basis. */
|
|
109
|
+
export declare function readObservedJournal(journal: TailSnapshot): Buffer;
|
|
89
110
|
export interface LiveStoreOptions {
|
|
90
111
|
cwd: string;
|
|
91
112
|
runId: string;
|
|
@@ -102,6 +123,7 @@ export declare function createLiveStore(options: LiveStoreOptions): {
|
|
|
102
123
|
delayed: boolean;
|
|
103
124
|
freshness: string;
|
|
104
125
|
operator: import("../../run/operator-state.js").OperatorSnapshot;
|
|
126
|
+
owed: OwedAuthority | undefined;
|
|
105
127
|
journal: TailSnapshot;
|
|
106
128
|
graph: CachedSource<{
|
|
107
129
|
version: 1;
|
|
@@ -204,6 +226,7 @@ export declare function createLiveStore(options: LiveStoreOptions): {
|
|
|
204
226
|
delayed: boolean;
|
|
205
227
|
freshness: string;
|
|
206
228
|
operator: import("../../run/operator-state.js").OperatorSnapshot;
|
|
229
|
+
owed: OwedAuthority | undefined;
|
|
207
230
|
journal: TailSnapshot;
|
|
208
231
|
graph: CachedSource<{
|
|
209
232
|
version: 1;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { closeSync, fstatSync, openSync, readSync, statSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { graphPath, stateDirName } from "../../graph/graph.js";
|
|
4
5
|
import { validateGraph } from "../../graph/schema.js";
|
|
5
|
-
import { parseRunId } from "../../run/journal.js";
|
|
6
|
+
import { foldOwedChecks, parseJournalText, parseRunId } from "../../run/journal.js";
|
|
6
7
|
import { isPidLive, STALE_MS } from "../../run/lock.js";
|
|
7
8
|
import { readTierLiveness, SUPERVISION_TIERS } from "../../run/supervision.js";
|
|
8
9
|
import { OperatorStateFold } from "../../run/operator-state.js";
|
|
@@ -14,6 +15,12 @@ export const STORE_LIMITS = { graphBytes: 16 * 1024 * 1024, history: 256, histor
|
|
|
14
15
|
const errorText = (e) => e instanceof Error ? e.message : String(e);
|
|
15
16
|
const fileIdentity = (st) => `${st.dev}:${st.ino}`;
|
|
16
17
|
const stamp = (st) => `${fileIdentity(st)}:${st.size}:${st.mtimeNs}:${st.ctimeNs}`;
|
|
18
|
+
/** The one record shape the tail accepts; the complete-journal (owed) read validates against it too. */
|
|
19
|
+
const isJournalRecord = (e) => {
|
|
20
|
+
const r = e;
|
|
21
|
+
return !!r && typeof r === "object" && typeof r.ts === "string" && Number.isFinite(Date.parse(r.ts)) && typeof r.event === "string"
|
|
22
|
+
&& !!r.data && typeof r.data === "object" && !Array.isArray(r.data) && (r.taskId === undefined || typeof r.taskId === "string");
|
|
23
|
+
};
|
|
17
24
|
function decodeLine(bytes, source, line, offset, endOffset, generation, oversized = false) {
|
|
18
25
|
const base = { source, line, id: `${source}#L${line}`, offset, endOffset, generation };
|
|
19
26
|
let raw = "";
|
|
@@ -24,7 +31,7 @@ function decodeLine(bytes, source, line, offset, endOffset, generation, oversize
|
|
|
24
31
|
if (!raw.trim())
|
|
25
32
|
return { ...base, raw };
|
|
26
33
|
const e = JSON.parse(raw);
|
|
27
|
-
if (!
|
|
34
|
+
if (!isJournalRecord(e))
|
|
28
35
|
throw new Error("invalid journal record");
|
|
29
36
|
return { ...base, raw, event: e };
|
|
30
37
|
}
|
|
@@ -51,6 +58,8 @@ export class JournalTail {
|
|
|
51
58
|
lastSuccessfulReadAt;
|
|
52
59
|
failure;
|
|
53
60
|
stale = false;
|
|
61
|
+
hash = createHash("sha256");
|
|
62
|
+
invalid = false;
|
|
54
63
|
constructor(source, hooks = {}) {
|
|
55
64
|
this.source = source;
|
|
56
65
|
this.hooks = hooks;
|
|
@@ -66,8 +75,14 @@ export class JournalTail {
|
|
|
66
75
|
this.errors = [];
|
|
67
76
|
this.malformedCount = 0;
|
|
68
77
|
this.generation++;
|
|
78
|
+
this.hash = createHash("sha256");
|
|
79
|
+
this.invalid = false;
|
|
69
80
|
this.hooks.reset?.();
|
|
70
81
|
}
|
|
82
|
+
/** I2: the consumed prefix was found rewritten (an in-place rewrite then growth reads as an append), so the
|
|
83
|
+
* next poll treats the file as replaced and refolds it from its first byte. */
|
|
84
|
+
invalidate() { this.invalid = true; }
|
|
85
|
+
get invalidated() { return this.invalid; }
|
|
71
86
|
/** A same-inode append between stat and fstat is retried; exhaustion keeps the last good snapshot, pending. */
|
|
72
87
|
poll(now = Date.now()) {
|
|
73
88
|
try {
|
|
@@ -94,7 +109,7 @@ export class JournalTail {
|
|
|
94
109
|
const st = statSync(this.source, { bigint: true });
|
|
95
110
|
if (!st.isFile())
|
|
96
111
|
throw new Error("journal source is not a regular file");
|
|
97
|
-
const replaced = !this.st || fileIdentity(st) !== fileIdentity(this.st) || st.size < this.st.size || (st.size === this.st.size && stamp(st) !== stamp(this.st));
|
|
112
|
+
const replaced = this.invalid || !this.st || fileIdentity(st) !== fileIdentity(this.st) || st.size < this.st.size || (st.size === this.st.size && stamp(st) !== stamp(this.st));
|
|
98
113
|
if (Number(st.size) > (replaced ? 0 : this.offset)) {
|
|
99
114
|
fd = openSync(this.source, "r");
|
|
100
115
|
const opened = fstatSync(fd, { bigint: true });
|
|
@@ -115,6 +130,7 @@ export class JournalTail {
|
|
|
115
130
|
throw new Error("journal truncated during read");
|
|
116
131
|
this.bytesRead += count;
|
|
117
132
|
remaining -= count;
|
|
133
|
+
this.hash.update(buffer.subarray(0, count));
|
|
118
134
|
let begin = 0;
|
|
119
135
|
for (let index = 0; index < count; index++)
|
|
120
136
|
if (buffer[index] === 10) {
|
|
@@ -158,7 +174,7 @@ export class JournalTail {
|
|
|
158
174
|
}
|
|
159
175
|
snapshot() {
|
|
160
176
|
return {
|
|
161
|
-
source: this.source, generation: this.generation, identity: this.st && fileIdentity(this.st), offset: this.offset, lines: this.line,
|
|
177
|
+
source: this.source, generation: this.generation, identity: this.st && fileIdentity(this.st), version: this.st && stamp(this.st), digest: this.hash.copy().digest("hex"), offset: this.offset, lines: this.line,
|
|
162
178
|
history: this.history.slice(), errors: this.errors.slice(), malformedCount: this.malformedCount,
|
|
163
179
|
pending: this.carryBytes ? { line: this.line + 1, bytes: this.carryBytes } : undefined,
|
|
164
180
|
backlogBytes: Math.max(0, Number(this.st?.size ?? 0) - this.offset),
|
|
@@ -262,6 +278,48 @@ class JsonSource {
|
|
|
262
278
|
}
|
|
263
279
|
}
|
|
264
280
|
}
|
|
281
|
+
/** I2: the journal basis an owed-check fold read — the observed file version, its generation and the bytes consumed. */
|
|
282
|
+
export const journalBasis = (journal) => `${journal.source}:${journal.generation}:${journal.version}:${journal.offset}`;
|
|
283
|
+
const unknownOwed = (reason) => ({ known: false, debt: "unknown", outstanding: [], acceptedRisk: [], discharged: [], unknown: [{ reason }] });
|
|
284
|
+
/** I2: the bytes the tail consumed are no longer the file's prefix — rewritten in place, then grown. */
|
|
285
|
+
export class JournalRewritten extends Error {
|
|
286
|
+
constructor() { super("journal prefix rewritten since the tail consumed it"); }
|
|
287
|
+
}
|
|
288
|
+
/** I2 (D-857): the ONE way any reader beside the tail gets `journal`'s bytes — every consumed byte of the very
|
|
289
|
+
* file version the tail observed (an in-place rewrite, even an equal-length one, changes it), stamped before
|
|
290
|
+
* and after the read, and proved by digest to be the very bytes the tail's fold consumed (a rewrite followed by
|
|
291
|
+
* growth keeps the tail appending). Any change throws, so no second read is ever combined with this
|
|
292
|
+
* observation's lifecycle, owed or basis. */
|
|
293
|
+
export function readObservedJournal(journal) {
|
|
294
|
+
const fd = openSync(journal.source, "r");
|
|
295
|
+
try {
|
|
296
|
+
const observed = () => { if (stamp(fstatSync(fd, { bigint: true })) !== journal.version)
|
|
297
|
+
throw new Error("journal changed since its observation"); };
|
|
298
|
+
observed();
|
|
299
|
+
const bytes = Buffer.alloc(journal.offset);
|
|
300
|
+
for (let length = 0; length < bytes.length;) {
|
|
301
|
+
const n = readSync(fd, bytes, length, bytes.length - length, length);
|
|
302
|
+
if (!n)
|
|
303
|
+
throw new Error("journal truncated since its observation");
|
|
304
|
+
length += n;
|
|
305
|
+
}
|
|
306
|
+
observed();
|
|
307
|
+
if (createHash("sha256").update(bytes).digest("hex") !== journal.digest)
|
|
308
|
+
throw new JournalRewritten();
|
|
309
|
+
return bytes;
|
|
310
|
+
}
|
|
311
|
+
finally {
|
|
312
|
+
closeSync(fd);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
/** The complete journal behind `journal`'s basis: its observed bytes, every non-blank line a valid record. */
|
|
316
|
+
function readLedger(journal) {
|
|
317
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(readObservedJournal(journal));
|
|
318
|
+
const events = parseJournalText(text);
|
|
319
|
+
if (events.length !== text.split("\n").filter(line => line.trim()).length || !events.every(isJournalRecord))
|
|
320
|
+
throw new Error("complete journal holds an invalid line");
|
|
321
|
+
return events;
|
|
322
|
+
}
|
|
265
323
|
/** C1 mount/C6 reader boundary. No timers or presence writes: the mount owns scheduling and disarm. */
|
|
266
324
|
export function createLiveStore(options) {
|
|
267
325
|
const now = options.now ?? Date.now;
|
|
@@ -285,10 +343,41 @@ export function createLiveStore(options) {
|
|
|
285
343
|
let disposed = false;
|
|
286
344
|
let queued;
|
|
287
345
|
const metrics = [];
|
|
346
|
+
// I2: the complete journal is re-read only when its basis moves; its owed fold re-reads every discharge
|
|
347
|
+
// artifact on every observation, and only the immutable git re-proof is reused through this store's memo.
|
|
348
|
+
// ponytail: an ended run's whole parsed journal stays resident; fold a debt-only projection if that measures large.
|
|
349
|
+
const owedMemo = new Set();
|
|
350
|
+
let ledger;
|
|
351
|
+
const owedAuthority = (journal) => {
|
|
352
|
+
const basis = journalBasis(journal);
|
|
353
|
+
if (ledger?.basis !== basis) {
|
|
354
|
+
// Never cache a failure: only a successful read is reused; a failed one is unknown debt now and re-read next observation.
|
|
355
|
+
try {
|
|
356
|
+
ledger = { basis, events: readLedger(journal) };
|
|
357
|
+
}
|
|
358
|
+
catch (e) {
|
|
359
|
+
ledger = undefined;
|
|
360
|
+
if (e instanceof JournalRewritten)
|
|
361
|
+
tail.invalidate();
|
|
362
|
+
return { basis, fold: unknownOwed(`complete journal unreadable: ${errorText(e)}`) };
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return { basis, fold: foldOwedChecks(ledger.events, options.cwd, owedMemo) };
|
|
366
|
+
};
|
|
288
367
|
function observe() {
|
|
289
368
|
const observedAt = now();
|
|
290
369
|
const delayed = lastObservation !== undefined && observedAt - lastObservation > 2 * OBSERVATION_INTERVAL_MS;
|
|
291
|
-
const
|
|
370
|
+
const isReadable = (j) => j.status === "readable" && j.backlogBytes === 0;
|
|
371
|
+
// Debt decides only an ended run's headline; a running or unreadable journal never pays for the fold.
|
|
372
|
+
const owedOf = (j) => isReadable(j) && fold.ended ? owedAuthority(j) : undefined;
|
|
373
|
+
let journal = tail.poll(observedAt);
|
|
374
|
+
let owed = owedOf(journal);
|
|
375
|
+
// I2: the owed read found the tail's consumed prefix rewritten, so its fold (lifecycle) is stale. Refold the
|
|
376
|
+
// whole file now: lifecycle and debt then come from the same bytes. A repeat rewrite is unknown debt.
|
|
377
|
+
if (tail.invalidated) {
|
|
378
|
+
journal = tail.poll(observedAt);
|
|
379
|
+
owed = owedOf(journal);
|
|
380
|
+
}
|
|
292
381
|
const graphReading = graph.read(observedAt);
|
|
293
382
|
const configReading = config.read(observedAt);
|
|
294
383
|
const cacheReading = cache.read(observedAt);
|
|
@@ -301,11 +390,13 @@ export function createLiveStore(options) {
|
|
|
301
390
|
if (metrics.length > STORE_LIMITS.metrics)
|
|
302
391
|
metrics.shift();
|
|
303
392
|
lastObservation = observedAt;
|
|
304
|
-
const readable = journal
|
|
393
|
+
const readable = isReadable(journal);
|
|
305
394
|
return {
|
|
306
395
|
sequence: ++sequence, observedAt, delayed, freshness: errors.length ? "failed" : delayed ? "delayed" : "fresh",
|
|
307
|
-
|
|
308
|
-
|
|
396
|
+
// I1: only this run's own lock naming a provably-dead (ESRCH) holder settles unfinished cells; an alive,
|
|
397
|
+
// EPERM, foreign, unreadable or absent lock is not proof the work stopped.
|
|
398
|
+
operator: fold.snapshot({ graph: graphReading.status === "readable" ? graphReading.value : undefined, sequence, observedAt, readable, graphAvailability: { status: graphReading.status, error: graphReading.error }, daemonDead: lockState === "dead", owed, basis: journalBasis(journal) }),
|
|
399
|
+
owed, journal, graph: graphReading, config: configReading, cache: cacheReading,
|
|
309
400
|
lock: { ...owner, state: lockState, alive, expired: owner.identity ? observedAt - Number(owner.identity.split(":")[3]) / 1e6 > STALE_MS : undefined },
|
|
310
401
|
supervision, errors, actionsEnabled: readable && !errors.length && !delayed,
|
|
311
402
|
viewport, inputSequence, metrics: metrics.slice(),
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { JournalEvent } from "../../run/journal.js";
|
|
2
|
+
import { type JournalLine, type TailSnapshot } from "./live-store.js";
|
|
3
|
+
/** The board uses the daemon's TTY narration, including its suppression and sanitization. */
|
|
4
|
+
export declare const renderLogLines: (events: readonly JournalEvent[], runId: string, columns: number) => string[];
|
|
5
|
+
export interface LogRow {
|
|
6
|
+
line: number;
|
|
7
|
+
text: string;
|
|
8
|
+
}
|
|
9
|
+
export interface LogPage {
|
|
10
|
+
rows: readonly LogRow[];
|
|
11
|
+
error?: string;
|
|
12
|
+
}
|
|
13
|
+
type PageReader = (firstLine: number, count?: number, generation?: number) => readonly JournalLine[];
|
|
14
|
+
interface LogSource {
|
|
15
|
+
journal: TailSnapshot;
|
|
16
|
+
page: PageReader;
|
|
17
|
+
runId: string;
|
|
18
|
+
columns: number;
|
|
19
|
+
}
|
|
20
|
+
/** Demand-page backwards to fill the viewport, including events outside the store's retained tail.
|
|
21
|
+
* Only the requested visible rows survive; suppressed events never consume viewport rows. */
|
|
22
|
+
export declare function readLogPage(source: LogSource, count: number, end?: number): LogPage;
|
|
23
|
+
/** Move by visible narration rows. Undefined means follow the journal tail again. */
|
|
24
|
+
export declare function moveLogEnd(source: LogSource, end: number | undefined, delta: number): number | undefined;
|
|
25
|
+
export declare function LogView({ page }: {
|
|
26
|
+
page: LogPage;
|
|
27
|
+
}): import("react").JSX.Element;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { narrationRow } from "../../cli/commands/run.js";
|
|
4
|
+
import { STORE_LIMITS } from "./live-store.js";
|
|
5
|
+
/** The board uses the daemon's TTY narration, including its suppression and sanitization. */
|
|
6
|
+
export const renderLogLines = (events, runId, columns) => events.map(event => narrationRow(event, runId, columns)).filter((line) => line !== null);
|
|
7
|
+
const narrate = (rows, source) => rows.flatMap(row => row.event
|
|
8
|
+
? renderLogLines([row.event], source.runId, source.columns).map(text => ({ line: row.line, text })) : []);
|
|
9
|
+
/** Demand-page backwards to fill the viewport, including events outside the store's retained tail.
|
|
10
|
+
* Only the requested visible rows survive; suppressed events never consume viewport rows. */
|
|
11
|
+
export function readLogPage(source, count, end = source.journal.lines) {
|
|
12
|
+
let cursor = Math.min(end, source.journal.lines);
|
|
13
|
+
let rows = [];
|
|
14
|
+
try {
|
|
15
|
+
while (cursor > 0 && rows.length < count) {
|
|
16
|
+
const first = Math.max(1, cursor - STORE_LIMITS.history + 1);
|
|
17
|
+
const retained = source.journal.history.filter(row => row.line >= first && row.line <= cursor);
|
|
18
|
+
const page = retained.length === cursor - first + 1 ? retained
|
|
19
|
+
: source.page(first, cursor - first + 1, source.journal.generation);
|
|
20
|
+
rows = [...narrate(page, source), ...rows].slice(-count);
|
|
21
|
+
cursor = first - 1;
|
|
22
|
+
}
|
|
23
|
+
return { rows };
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
return { rows: [], error: error instanceof Error ? error.message : String(error) };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Move by visible narration rows. Undefined means follow the journal tail again. */
|
|
30
|
+
export function moveLogEnd(source, end, delta) {
|
|
31
|
+
const cursor = Math.min(end ?? source.journal.lines, source.journal.lines);
|
|
32
|
+
if (delta < 0) {
|
|
33
|
+
const rows = readLogPage(source, -delta + 1, cursor).rows;
|
|
34
|
+
return rows[Math.max(0, rows.length - 1 + delta)]?.line ?? end;
|
|
35
|
+
}
|
|
36
|
+
let first = cursor + 1, remaining = delta;
|
|
37
|
+
while (first <= source.journal.lines) {
|
|
38
|
+
const last = Math.min(source.journal.lines, first + STORE_LIMITS.history - 1);
|
|
39
|
+
let rows;
|
|
40
|
+
try {
|
|
41
|
+
rows = narrate(source.page(first, last - first + 1, source.journal.generation), source);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return end;
|
|
45
|
+
} // A stale journal observation must never terminate the board's key handler.
|
|
46
|
+
if (rows.length >= remaining) {
|
|
47
|
+
const line = rows[remaining - 1].line;
|
|
48
|
+
return line === source.journal.lines ? undefined : line;
|
|
49
|
+
}
|
|
50
|
+
remaining -= rows.length;
|
|
51
|
+
first = last + 1;
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
export function LogView({ page }) {
|
|
56
|
+
return _jsx(Box, { flexDirection: "column", children: page.error
|
|
57
|
+
? _jsxs(Text, { children: ["Log unavailable: ", page.error] })
|
|
58
|
+
: page.rows.length ? page.rows.map(row => _jsx(Text, { wrap: "truncate-end", children: row.text }, row.line))
|
|
59
|
+
: _jsx(Text, { children: "No narrated events" }) });
|
|
60
|
+
}
|
|
@@ -98,6 +98,10 @@ export function runGateCells(task, evidence, rows = []) {
|
|
|
98
98
|
else if (cell.state === "queued") {
|
|
99
99
|
labels.push(`queued — ${row?.event?.event ?? "wait"}${typeof data.count === "number" ? ` (${data.count} suites)` : ""}`);
|
|
100
100
|
}
|
|
101
|
+
else if (cell.state === "unknown" && row?.event?.event !== "gate-result") {
|
|
102
|
+
// I1: a start or wait row settled at a terminal boundary — no verdict ever measured it.
|
|
103
|
+
labels.push(`unknown — ${row?.event?.event ?? "start"} never finished`);
|
|
104
|
+
}
|
|
101
105
|
else if (data.disabled === true) {
|
|
102
106
|
labels.push("disabled by policy");
|
|
103
107
|
}
|
|
@@ -2,13 +2,14 @@ import { type ReactNode } from "react";
|
|
|
2
2
|
import { type ShellPlan } from "./layout.js";
|
|
3
3
|
import { SHELL_BINDINGS } from "./keys.js";
|
|
4
4
|
import type { LiveStoreSnapshot } from "./live-store.js";
|
|
5
|
-
export type ShellView = "home" | "run" | "evidence";
|
|
5
|
+
export type ShellView = "home" | "run" | "evidence" | "log";
|
|
6
6
|
export type ShellFocus = ShellPlan["focus"][number];
|
|
7
7
|
export interface ShellState {
|
|
8
8
|
view: ShellView;
|
|
9
9
|
focus: ShellFocus;
|
|
10
10
|
scroll: number;
|
|
11
11
|
help: boolean;
|
|
12
|
+
logEnd?: number;
|
|
12
13
|
evidenceSection?: number;
|
|
13
14
|
canOpen?: boolean;
|
|
14
15
|
shortcutColumns?: number;
|
|
@@ -13,7 +13,8 @@ export const shellHelp = (bindings = SHELL_BINDINGS) => [
|
|
|
13
13
|
"Esc closes deepest overlay; it never quits.",
|
|
14
14
|
"Text: q1? is literal; Enter applies; Esc cancels.",
|
|
15
15
|
"Confirm: y alone confirms; n/Esc cancels.",
|
|
16
|
-
"Tab now moves focus; view numbers are 1/4/5.",
|
|
16
|
+
"Tab now moves focus; view numbers are 1/4/5/7.",
|
|
17
|
+
"Log: arrows/page keys scroll; f toggles tail follow.",
|
|
17
18
|
"Fleet: tickmarkr fleet", "Plan: tickmarkr plan", "Health: tickmarkr doctor",
|
|
18
19
|
];
|
|
19
20
|
/** Read the Yoga geometry that actually painted, including clipped/scrolled leaves.
|
|
@@ -65,10 +66,11 @@ export function CockpitShell({ snapshot, state, columns, rows, version, children
|
|
|
65
66
|
const overlay = state.editor !== undefined ? [`${state.editorKind === "export" ? "Export destination" : "Filter"}: ${state.editor}`, "Enter Apply | Esc Cancel"] : state.overlay ?? (state.help ? shellHelp(bindings) : undefined);
|
|
66
67
|
const totalRows = overlay?.length ?? contentRows;
|
|
67
68
|
const displayOffset = overlay ? Math.min(state.overlayOffset, Math.max(0, totalRows - plan.bodyRows)) : scroll;
|
|
68
|
-
const
|
|
69
|
+
const logMode = state.view === "log" ? `${state.logEnd === undefined ? "FOLLOW" : "PAUSED"} | ` : "";
|
|
70
|
+
const position = `${logMode}${totalRows ? displayOffset + 1 : 0}–${Math.min(totalRows, displayOffset + plan.bodyRows)}/${totalRows} | ${Math.max(0, totalRows - plan.bodyRows)} hidden`;
|
|
69
71
|
const keybar = bindings.filter(b => ["focus", "open", "actions", "help", "quit"].includes(b.action) && (columns >= 60 || b.action !== "focus")).map(b => `${b.key} ${columns < 60 && b.action === "actions" ? "Act" : b.label}`).join(" | ");
|
|
70
72
|
const status = columns < 60 ? `${op.label} | tip ${op.currentTip.toUpperCase()}` : `${op.label} | MERGED ${op.merged}/${op.planned ?? "?"} | CURRENT TIP ${op.currentTip.toUpperCase()} | ${snapshot.freshness}`;
|
|
71
73
|
return _jsx(ShellPresentation.Provider, { value: true, children: _jsx(ShellTheme.Provider, { value: { mode: colour, palette }, children: _jsxs(Box, { ref: root, flexDirection: "column", width: columns, height: rows, flexShrink: 0, backgroundColor: ink(palette.surface), children: [line(`tickmarkr ${version} | ${state.view.toUpperCase()} | ${snapshot.journal.source.split("/").at(-2) ?? "run identity unknown"}`), line(columns < 60
|
|
72
|
-
? `1H 4R 5E | ${position}`
|
|
73
|
-
: `1 Home 4 Run 5 Evidence | ${state.focus} | ${position}`), line("─".repeat(columns), columns, palette.chrome), _jsxs(Box, { height: plan.bodyRows, flexShrink: 0, flexDirection: "row", overflow: "hidden", children: [_jsx(Box, { width: 1, flexDirection: "column", children: Array.from({ length: plan.bodyRows }, (_, i) => _jsx(Text, { color: ink(palette.chrome), children: "\u2502" }, i)) }), plan.rail > 0 && _jsxs(_Fragment, { children: [_jsx(Box, { width: plan.rail, flexDirection: "column", children: ["1 Home", "4 Run", "5 Evidence", "", "CLI:", "fleet", "plan", "doctor"].map((t, i) => _jsx(Text, { color: ink(palette.chrome), children: fitCells(t, plan.rail) }, i)) }), _jsx(Box, { width: 1, flexDirection: "column", children: Array.from({ length: plan.bodyRows }, (_, i) => _jsx(Text, { color: ink(palette.chrome), children: "\u2502" }, i)) })] }), _jsxs(Box, { width: plan.bodyColumns, height: plan.bodyRows, flexDirection: "column", overflow: "hidden", flexShrink: 0, children: [overlay?.slice(displayOffset, displayOffset + plan.bodyRows).map((t, i) => _jsx(Text, { wrap: "truncate-end", color: ink(palette.text), children: fitCells(t, plan.bodyColumns) }, i)), _jsx(Box, { ref: leaf, display: overlay ? "none" : "flex", flexDirection: "column", flexShrink: 0, marginTop: -scroll, children: children })] }), plan.shortcuts > 0 && _jsxs(_Fragment, { children: [_jsx(Box, { width: 1, flexDirection: "column", children: Array.from({ length: plan.bodyRows }, (_, i) => _jsx(Text, { color: ink(palette.chrome), children: "\u2502" }, i)) }), _jsx(Box, { width: plan.shortcuts, flexDirection: "column", children: bindings.map(b => _jsx(Text, { color: ink(palette.chrome), wrap: "truncate-end", children: fitCells(`${b.key} ${b.label}`, plan.shortcuts) }, b.key)) })] }), _jsx(Box, { width: 1, flexDirection: "column", children: Array.from({ length: plan.bodyRows }, (_, i) => _jsx(Text, { color: ink(palette.chrome), children: "\u2502" }, i)) })] }), line("─".repeat(columns), columns, palette.chrome), line(status), line(keybar), line("─".repeat(columns), columns, palette.chrome)] }) }) });
|
|
74
|
+
? `1H 4R 5E 7L | ${position}`
|
|
75
|
+
: `1 Home 4 Run 5 Evidence 7 Log | ${state.focus} | ${position}`), line("─".repeat(columns), columns, palette.chrome), _jsxs(Box, { height: plan.bodyRows, flexShrink: 0, flexDirection: "row", overflow: "hidden", children: [_jsx(Box, { width: 1, flexDirection: "column", children: Array.from({ length: plan.bodyRows }, (_, i) => _jsx(Text, { color: ink(palette.chrome), children: "\u2502" }, i)) }), plan.rail > 0 && _jsxs(_Fragment, { children: [_jsx(Box, { width: plan.rail, flexDirection: "column", children: ["1 Home", "4 Run", "5 Evidence", "7 Log", "", "CLI:", "fleet", "plan", "doctor"].map((t, i) => _jsx(Text, { color: ink(palette.chrome), children: fitCells(t, plan.rail) }, i)) }), _jsx(Box, { width: 1, flexDirection: "column", children: Array.from({ length: plan.bodyRows }, (_, i) => _jsx(Text, { color: ink(palette.chrome), children: "\u2502" }, i)) })] }), _jsxs(Box, { width: plan.bodyColumns, height: plan.bodyRows, flexDirection: "column", overflow: "hidden", flexShrink: 0, children: [overlay?.slice(displayOffset, displayOffset + plan.bodyRows).map((t, i) => _jsx(Text, { wrap: "truncate-end", color: ink(palette.text), children: fitCells(t, plan.bodyColumns) }, i)), _jsx(Box, { ref: leaf, display: overlay ? "none" : "flex", flexDirection: "column", flexShrink: 0, marginTop: -scroll, children: children })] }), plan.shortcuts > 0 && _jsxs(_Fragment, { children: [_jsx(Box, { width: 1, flexDirection: "column", children: Array.from({ length: plan.bodyRows }, (_, i) => _jsx(Text, { color: ink(palette.chrome), children: "\u2502" }, i)) }), _jsx(Box, { width: plan.shortcuts, flexDirection: "column", children: bindings.map(b => _jsx(Text, { color: ink(palette.chrome), wrap: "truncate-end", children: fitCells(`${b.key} ${b.label}`, plan.shortcuts) }, b.key)) })] }), _jsx(Box, { width: 1, flexDirection: "column", children: Array.from({ length: plan.bodyRows }, (_, i) => _jsx(Text, { color: ink(palette.chrome), children: "\u2502" }, i)) })] }), line("─".repeat(columns), columns, palette.chrome), line(status), line(keybar), line("─".repeat(columns), columns, palette.chrome)] }) }) });
|
|
74
76
|
}
|
|
@@ -16,6 +16,12 @@ export type FleetEditorState = Record<DenyScopeKey, string[]> & {
|
|
|
16
16
|
};
|
|
17
17
|
steering: Record<FleetSteeringKey, string[] | undefined>;
|
|
18
18
|
};
|
|
19
|
+
/** T8: the staged classifications and efforts every preview folds — the same metadata a review stages,
|
|
20
|
+
* so a preview ranks what w would write before the first review and after an escaped one. */
|
|
21
|
+
export type FleetStagedMetadata = Pick<FleetEditorState, "classifications" | "efforts">;
|
|
22
|
+
/** OBS-525: auto reverts the shape's WHOLE routing declaration — pool included, not just the pin.
|
|
23
|
+
* prefer survives: it biases auto routing and is valid without pin/pool. */
|
|
24
|
+
export declare function autoMapEntry(entry: MapEntry | undefined): MapEntry;
|
|
19
25
|
export type FleetOverlayReview = {
|
|
20
26
|
kind: "empty";
|
|
21
27
|
} | {
|
|
@@ -136,9 +142,9 @@ export declare function FleetApp({ ageMs, agents, initialAllowOut, initialEffort
|
|
|
136
142
|
initialMode: RoutingMode;
|
|
137
143
|
modeOptions: FleetModeOption[];
|
|
138
144
|
initialMap: Record<string, MapEntry>;
|
|
139
|
-
modePreview: (mode: RoutingMode, map: Record<string, MapEntry>, deny: FleetStagedDeny) => string[];
|
|
140
|
-
shapeRows: (mode: RoutingMode, map: Record<string, MapEntry>, deny: FleetStagedDeny) => FleetShapeRow[];
|
|
141
|
-
candidatesForShape: (shape: Shape, mode: RoutingMode, map: Record<string, MapEntry>, deny: FleetStagedDeny) => {
|
|
145
|
+
modePreview: (mode: RoutingMode, map: Record<string, MapEntry>, deny: FleetStagedDeny, stage?: FleetStagedMetadata) => string[];
|
|
146
|
+
shapeRows: (mode: RoutingMode, map: Record<string, MapEntry>, deny: FleetStagedDeny, stage?: FleetStagedMetadata) => FleetShapeRow[];
|
|
147
|
+
candidatesForShape: (shape: Shape, mode: RoutingMode, map: Record<string, MapEntry>, deny: FleetStagedDeny, stage?: FleetStagedMetadata) => {
|
|
142
148
|
rows: FleetCandidateOption[];
|
|
143
149
|
excludedNote?: string;
|
|
144
150
|
ledger?: string[];
|
|
@@ -177,9 +183,9 @@ export declare function runFleetInkEditor({ ageMs, adapters, health, initialAllo
|
|
|
177
183
|
initialMode: RoutingMode;
|
|
178
184
|
modeOptions: FleetModeOption[];
|
|
179
185
|
initialMap: Record<string, MapEntry>;
|
|
180
|
-
modePreview: (mode: RoutingMode, map: Record<string, MapEntry>, deny: FleetStagedDeny) => string[];
|
|
181
|
-
shapeRows: (mode: RoutingMode, map: Record<string, MapEntry>, deny: FleetStagedDeny) => FleetShapeRow[];
|
|
182
|
-
candidatesForShape: (shape: Shape, mode: RoutingMode, map: Record<string, MapEntry>, deny: FleetStagedDeny) => {
|
|
186
|
+
modePreview: (mode: RoutingMode, map: Record<string, MapEntry>, deny: FleetStagedDeny, stage?: FleetStagedMetadata) => string[];
|
|
187
|
+
shapeRows: (mode: RoutingMode, map: Record<string, MapEntry>, deny: FleetStagedDeny, stage?: FleetStagedMetadata) => FleetShapeRow[];
|
|
188
|
+
candidatesForShape: (shape: Shape, mode: RoutingMode, map: Record<string, MapEntry>, deny: FleetStagedDeny, stage?: FleetStagedMetadata) => {
|
|
183
189
|
rows: FleetCandidateOption[];
|
|
184
190
|
excludedNote?: string;
|
|
185
191
|
ledger?: string[];
|
|
@@ -12,6 +12,12 @@ import { windowRows } from "./components.js";
|
|
|
12
12
|
import { wrapCells } from "../cockpit/width.js";
|
|
13
13
|
import { clip, clipPathTail, ElisionMark, enterAltScreen, fmtCtx, fmtMs, fmtUsdPair, Glyph, INK, inkInput, inkOutput, KeyBar, OverlayPanel, padCell, padCellStart, Pointer, SearchRow, } from "./frame.js";
|
|
14
14
|
const STEERING_KEYS = ["review", "consult"];
|
|
15
|
+
/** OBS-525: auto reverts the shape's WHOLE routing declaration — pool included, not just the pin.
|
|
16
|
+
* prefer survives: it biases auto routing and is valid without pin/pool. */
|
|
17
|
+
export function autoMapEntry(entry) {
|
|
18
|
+
const { pin: _pin, pool: _pool, ...rest } = entry ?? {};
|
|
19
|
+
return rest;
|
|
20
|
+
}
|
|
15
21
|
/** every scope's staged list, empty — the "every policy scope open" probe the picker lifts against */
|
|
16
22
|
export const openStagedDeny = () => ({
|
|
17
23
|
...Object.fromEntries(DENY_SCOPES.map((scope) => [stagedDenyKeyOf(scope), []])),
|
|
@@ -346,7 +352,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
346
352
|
...Object.fromEntries(DENY_SCOPES.map((scope) => [stagedDenyKeyOf(scope), [...ui[scope.key]].sort()])),
|
|
347
353
|
allowOut: [...ui.allowOut].sort(),
|
|
348
354
|
});
|
|
349
|
-
const shapeList = () => shapeRows(ui.selectedMode, ui.map, stagedDeny());
|
|
355
|
+
const shapeList = () => shapeRows(ui.selectedMode, ui.map, stagedDeny(), stagedMetadata());
|
|
350
356
|
const steeringList = () => [
|
|
351
357
|
...STEERING_KEYS.map((key) => ({
|
|
352
358
|
id: key,
|
|
@@ -369,6 +375,11 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
369
375
|
...modelGroups.map((_, index) => ({ kind: "adapter", index })),
|
|
370
376
|
];
|
|
371
377
|
// ── state transitions ──────────────────────────────────────────────────────
|
|
378
|
+
// T8: the previews fold the CURRENT staged metadata — never only what the last review saw
|
|
379
|
+
const stagedMetadata = () => {
|
|
380
|
+
const { classifications, efforts } = editorState();
|
|
381
|
+
return { classifications, efforts };
|
|
382
|
+
};
|
|
372
383
|
const editorState = () => ({
|
|
373
384
|
...denyLists(),
|
|
374
385
|
allowOut: [...ui.allowOut].sort(),
|
|
@@ -733,7 +744,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
733
744
|
// channel must be OFFERED at or above the floor once every policy scope is open — a failed
|
|
734
745
|
// model probe (dropped by discovery), a pin or a pool keep it out of that list and a floor
|
|
735
746
|
// marks it belowFloor, so nothing here lifts it.
|
|
736
|
-
const eligible = candidatesForShape(overlay.shape, ui.selectedMode, ui.map, openStagedDeny()).rows
|
|
747
|
+
const eligible = candidatesForShape(overlay.shape, ui.selectedMode, ui.map, openStagedDeny(), stagedMetadata()).rows
|
|
737
748
|
.some((row) => row.id === id && !row.belowFloor);
|
|
738
749
|
if (!eligible) {
|
|
739
750
|
// the ledger line spells its reasons; drop the row label and the reach segment for the notice
|
|
@@ -756,7 +767,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
756
767
|
const text = `lift: cleared ${found.entry} from ${found.scope}${after.reach === "in" ? "" : ` — still out: ${after.reasons.join("; ")}`}`;
|
|
757
768
|
ui.lastEdit = { id, text };
|
|
758
769
|
ui.notice = text;
|
|
759
|
-
const picked = candidatesForShape(overlay.shape, ui.selectedMode, ui.map, stagedDeny());
|
|
770
|
+
const picked = candidatesForShape(overlay.shape, ui.selectedMode, ui.map, stagedDeny(), stagedMetadata());
|
|
760
771
|
ui.overlay = { ...overlay, rows: picked.rows, excludedNote: picked.excludedNote, ledger: picked.ledger ?? [], at: 0 };
|
|
761
772
|
bump();
|
|
762
773
|
};
|
|
@@ -1590,17 +1601,12 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
1590
1601
|
if (!shape)
|
|
1591
1602
|
return;
|
|
1592
1603
|
if (hotkey === "a") {
|
|
1593
|
-
|
|
1594
|
-
// the pin. prefer survives: it biases auto routing and is valid without pin/pool.
|
|
1595
|
-
const nextEntry = { ...ui.map[shape] };
|
|
1596
|
-
delete nextEntry.pin;
|
|
1597
|
-
delete nextEntry.pool;
|
|
1598
|
-
ui.map = { ...ui.map, [shape]: nextEntry };
|
|
1604
|
+
ui.map = { ...ui.map, [shape]: autoMapEntry(ui.map[shape]) };
|
|
1599
1605
|
bump();
|
|
1600
1606
|
return;
|
|
1601
1607
|
}
|
|
1602
1608
|
if (key.return || hotkey === "p") {
|
|
1603
|
-
const picked = candidatesForShape(shape, ui.selectedMode, ui.map, stagedDeny());
|
|
1609
|
+
const picked = candidatesForShape(shape, ui.selectedMode, ui.map, stagedDeny(), stagedMetadata());
|
|
1604
1610
|
setOverlay({
|
|
1605
1611
|
kind: "candidates",
|
|
1606
1612
|
shape,
|
|
@@ -1791,7 +1797,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
1791
1797
|
if (!overlay)
|
|
1792
1798
|
return null;
|
|
1793
1799
|
if (overlay.kind === "presets") {
|
|
1794
|
-
const details = modeOptions[overlay.at] ? modePreview(modeOptions[overlay.at].id, ui.map, stagedDeny()) : [];
|
|
1800
|
+
const details = modeOptions[overlay.at] ? modePreview(modeOptions[overlay.at].id, ui.map, stagedDeny(), stagedMetadata()) : [];
|
|
1795
1801
|
return (_jsxs(OverlayPanel, { title: overlay.home ? "routing preset" : "routing mode", width: bodyW, children: [_jsx(Text, { dimColor: true, children: overlay.home ? "a preset routes every shape — custom opens the browser" : "floors move with the mode; the browser edits the rest" }), _jsx(Text, { children: " " }), modeOptions.map((option, index) => (_jsxs(Text, { children: [_jsx(Pointer, { on: overlay.at === index }), option.id === ui.selectedMode ? _jsx(Glyph, { kind: "on" }) : _jsx(Text, { children: " " }), _jsx(Text, { bold: overlay.at === index, children: ` ${padCell(option.id, 12)}` }), _jsx(Text, { dimColor: true, children: clip(option.gloss, bodyW - 20) })] }, option.id))), overlay.home && (_jsxs(Text, { children: [_jsx(Pointer, { on: overlay.at === modeOptions.length }), _jsx(Text, { children: " " }), _jsx(Text, { bold: overlay.at === modeOptions.length, children: ` ${padCell("custom", 12)}` }), _jsx(Text, { dimColor: true, children: clip("open the fleet browser (models · shapes · steering)", bodyW - 20) })] })), _jsx(Text, { children: " " }), details.map((line) => _jsx(Text, { dimColor: true, children: line }, line))] }));
|
|
1796
1802
|
}
|
|
1797
1803
|
if (overlay.kind === "review") {
|
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@ Use the canonical loop skill's [cockpit, parked decisions and printed twins](../
|
|
|
12
12
|
for each target: delivered views are **1 Home, 4 Run, 5 Evidence**; Fleet/Bootstrap and
|
|
13
13
|
Plan/Health remain follow-ons with CLI entries. The recorded 1/3 merged, human T2, blocked
|
|
14
14
|
T3 case stays PARTIAL despite tip pass. Review the Run confirmation, read back the appended
|
|
15
|
-
`task-approved` receipt, and explicitly `tickmarkr resume <runId>` when no live owner exists;
|
|
15
|
+
`task-approved` receipt, and explicitly launch `tickmarkr resume <runId>` with the host-owned split form below when no live owner exists;
|
|
16
16
|
approval records permission and never dispatches work itself. Non-TTY readers keep `status`,
|
|
17
17
|
`report` and default-watch line output, with `--watch --plain` as the TTY fallback.
|
|
18
18
|
Keep `name: tickmarkr-auto` and the installed links to canonical `skills/` intact.
|
|
@@ -92,7 +92,37 @@ After sending, **confirm delivery** by reading the target pane and verifying the
|
|
|
92
92
|
1. **Prepare** — confirm the target list. Run the [binary preflight](#binary-preflight-before-compile-or-run). Check `git status`, confirm no tickmarkr run is active, and work from a non-main branch.
|
|
93
93
|
2. **Compile** — run `tickmarkr compile <spec-or-directory>`. Fix source-spec defects instead of editing the generated graph.
|
|
94
94
|
3. **Plan** — run `tickmarkr plan`. Review routes, capability-floor warnings, and human gates before execution.
|
|
95
|
-
4. **Run** —
|
|
95
|
+
4. **Run** — launch `tickmarkr run` using the [host-owned split form](#host-owned-daemon-and-detached-beats), and start detached beats with their run-end stop items. A watch ending the seat's turn is no watch: keep a **blocking journal consumer** alive for the run's terminal events — the shipped watcher below, or a foreground `until grep` on the run's terminal events — and ensure it is re-armed at most every twenty minutes. Never rely on a `Monitor`-only wake. Watch the run journal rather than polling agents, using the shipped watcher — `.claude/skills/tickmarkr-overseer/scripts/watch-journal.sh <state-dir>/runs 20 28800` — which takes a line baseline at arm time, then wakes ONCE on `run-end`, `task-human`, `task-failed` or `consult-verdict` and grades the run-end summary against every execution clause for you; the debt clause is yours — read CURRENT `tickmarkr status <runId>` (step 5). Re-arm after every wake. ⛔ Never `tail -F | grep -m1` (run-end is the journal's last line, so tail never notices the broken pipe and the watcher hangs forever) and never a pane-level done wait (it fires on every agent turn end, not mission end). ⚠ A bare whole-file `grep -q '"event":"run-end"'` is the trap the watcher exists to avoid: on a resume it matches the PREVIOUS run's run-end and returns instantly, so a re-armed watcher reads as coverage that does not exist. Resolve blocked interactions in the relevant agent session.
|
|
96
96
|
5. **Verify and consolidate** — continue only after a green run. A run is green when the run-end event exists in the journal, the tip verify is not "failed", the summary's `failed`, `human`, `blocked` and `pending` buckets are all empty, and CURRENT `tickmarkr status <runId>` reads its owed checks outstanding empty AND known (`outstanding 0`) — a run with a parked task is partial, not green. Empty execution buckets alone are not green either (D-660): every bucket empty and the tip passed, but an operator waived one review, leaves one accepted-risk review check owed — status reads `outstanding 1 (T7 review)` and `run`/`resume` still exit 0 on execution alone, so the run is execution complete, not green. It turns green only when a `tickmarkr verify --record <runId>` discharge moves CURRENT status to `outstanding 0`, including a discharge landing after run-end — the historical run-end record keeps the old count, so never read debt from it; `outstanding unknown` is never green. Tickmarkr consolidates accepted work on `tickmarkr/<runId>` and never signs off to the main branch. A human controls any later release merge.
|
|
97
97
|
6. **Record** — `tickmarkr report <runId> --md` prints Markdown to stdout; redirect explicitly beside the spec (for example `tickmarkr report <runId> --md > feature.record.md`) and commit the execution record when the repository tracks those records.
|
|
98
98
|
7. **Continue** — move to the next requested target. If a target fails or is parked, stop with the journal evidence rather than silently skipping it.
|
|
99
|
+
|
|
100
|
+
## Host-owned daemon and detached beats
|
|
101
|
+
|
|
102
|
+
Launch the daemon as a SPLIT of the orchestrator's own pane in the ORCH tab, owned by the
|
|
103
|
+
host PTY. Never launch it as an agent harness background task or in a separate tab. The daemon
|
|
104
|
+
self-places its board from that split, keeping both daemon and board in the ORCH tab.
|
|
105
|
+
|
|
106
|
+
- **On Orca (`TERM_PROGRAM=Orca` and non-empty `ORCA_TERMINAL_HANDLE`)**:
|
|
107
|
+
`orca terminal split --terminal "$ORCA_TERMINAL_HANDLE" --direction vertical --command "tickmarkr run"`.
|
|
108
|
+
- **On herdr (`HERDR_ENV=1`)**: use `herdr pane split` on the orchestrator's own pane, then
|
|
109
|
+
`herdr pane run <new> "tickmarkr run"` in the returned split.
|
|
110
|
+
|
|
111
|
+
For `resume`, use the same host-owned split form with `tickmarkr resume <runId>` in place of
|
|
112
|
+
`tickmarkr run`. After either launch, read this repository's lock pid and walk its ppid chain
|
|
113
|
+
in the process table to the host PTY; verify no agent session is an ancestor. If an agent is
|
|
114
|
+
an ancestor, stop that launch and relaunch through the host split before continuing.
|
|
115
|
+
|
|
116
|
+
Start `tickmarkr beat <tier> --seat <seat> --loop` as a DETACHED background process, one per
|
|
117
|
+
(tier, seat), from the repository root. Use setsid plus nohup (or the host's equivalent detached
|
|
118
|
+
session facility), redirect output to a file, and record a pidfile under the repository's state
|
|
119
|
+
dir. Verify ppid 1 and no agent-session ancestor. Never put beats in a harness background task
|
|
120
|
+
or a visible tab. The lifecycle must be idempotent: start checks the pidfile against the live
|
|
121
|
+
process payload and returns an "already running" no-op for the same (repo, tier, seat); status
|
|
122
|
+
checks that payload and beat freshness; stop sends `--stand-down` for the same tier and seat,
|
|
123
|
+
verifies the loop exits within one interval, then removes its pidfile. A stale or reused pid
|
|
124
|
+
must never be killed without matching its live payload. Include a run-end stop item for every
|
|
125
|
+
beat started, also on failure, park or handoff; verify DISARMED and remove retired beat files.
|
|
126
|
+
|
|
127
|
+
Keep only watchers that must WAKE the seat in the harness. Re-arm them on each wake and at
|
|
128
|
+
their harness cap; daemon and beat lifetimes must not depend on that cap.
|