tickmarkr 2.6.3 → 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/README.md +10 -2
- 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/codex.d.ts +2 -1
- package/dist/adapters/codex.js +26 -5
- 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 +2 -1
- package/dist/adapters/model-lints.js +52 -1
- package/dist/adapters/model-windows.d.ts +2 -1
- package/dist/adapters/model-windows.js +18 -7
- package/dist/adapters/prompt.js +3 -2
- package/dist/adapters/types.d.ts +13 -0
- package/dist/cli/commands/approve.js +37 -6
- package/dist/cli/commands/doctor.d.ts +4 -0
- package/dist/cli/commands/doctor.js +20 -3
- package/dist/cli/commands/fleet.d.ts +10 -1
- package/dist/cli/commands/fleet.js +279 -95
- package/dist/cli/commands/init.js +10 -4
- package/dist/cli/commands/plan.js +65 -4
- package/dist/cli/commands/report.d.ts +39 -2
- package/dist/cli/commands/report.js +227 -10
- package/dist/cli/commands/resume.js +2 -4
- package/dist/cli/commands/run.d.ts +11 -0
- package/dist/cli/commands/run.js +33 -5
- package/dist/cli/commands/status.js +30 -3
- package/dist/cli/commands/verify.d.ts +1 -1
- package/dist/cli/commands/verify.js +356 -123
- package/dist/compile/native.js +130 -0
- package/dist/config/config.d.ts +25 -7
- package/dist/config/config.js +92 -11
- 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/route/router.d.ts +11 -0
- package/dist/route/router.js +29 -8
- package/dist/run/daemon.js +158 -22
- package/dist/run/git.js +9 -0
- package/dist/run/journal.d.ts +101 -1
- package/dist/run/journal.js +352 -2
- 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 +30 -1
- package/dist/tui/cockpit/live-store.js +134 -18
- 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 +33 -3
- package/skills/tickmarkr-loop/SKILL.md +55 -5
- package/skills/tickmarkr-overseer/SKILL.md +83 -8
|
@@ -1,6 +1,8 @@
|
|
|
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
|
+
/** Stat-to-fstat observations one poll spends on a journal a writer keeps appending to. */
|
|
5
|
+
export declare const APPEND_RACE_OBSERVATIONS = 3;
|
|
4
6
|
export declare const STORE_LIMITS: {
|
|
5
7
|
readonly graphBytes: number;
|
|
6
8
|
readonly history: 256;
|
|
@@ -25,9 +27,12 @@ export interface JournalLine extends Omit<OperatorRecord, "event"> {
|
|
|
25
27
|
endOffset: number;
|
|
26
28
|
}
|
|
27
29
|
export interface TailSnapshot {
|
|
30
|
+
/** `digest` (I2): sha256 of every byte the tail consumed, [0, offset) — the bytes its fold was built from. */
|
|
28
31
|
source: string;
|
|
29
32
|
generation: number;
|
|
30
33
|
identity?: string;
|
|
34
|
+
version?: string;
|
|
35
|
+
digest: string;
|
|
31
36
|
offset: number;
|
|
32
37
|
lines: number;
|
|
33
38
|
history: readonly JournalLine[];
|
|
@@ -61,12 +66,22 @@ export declare class JournalTail {
|
|
|
61
66
|
private bytesRead;
|
|
62
67
|
private lastSuccessfulReadAt?;
|
|
63
68
|
private failure?;
|
|
69
|
+
private stale;
|
|
70
|
+
private hash;
|
|
71
|
+
private invalid;
|
|
64
72
|
constructor(source: string, hooks?: {
|
|
65
73
|
record?: (record: OperatorRecord) => void;
|
|
66
74
|
reset?: () => void;
|
|
67
75
|
});
|
|
68
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;
|
|
81
|
+
/** A same-inode append between stat and fstat is retried; exhaustion keeps the last good snapshot, pending. */
|
|
69
82
|
poll(now?: number): TailSnapshot;
|
|
83
|
+
/** One stat-to-fstat observation. False when an append raced it; nothing was consumed or reset. */
|
|
84
|
+
private observe;
|
|
70
85
|
private appendCarry;
|
|
71
86
|
snapshot(): TailSnapshot;
|
|
72
87
|
/** Demand paging scans with fixed buffers, independent of the retained history; no growing line index. */
|
|
@@ -80,6 +95,18 @@ export interface CachedSource<T = unknown> {
|
|
|
80
95
|
error?: string;
|
|
81
96
|
observedAt: number;
|
|
82
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;
|
|
83
110
|
export interface LiveStoreOptions {
|
|
84
111
|
cwd: string;
|
|
85
112
|
runId: string;
|
|
@@ -96,6 +123,7 @@ export declare function createLiveStore(options: LiveStoreOptions): {
|
|
|
96
123
|
delayed: boolean;
|
|
97
124
|
freshness: string;
|
|
98
125
|
operator: import("../../run/operator-state.js").OperatorSnapshot;
|
|
126
|
+
owed: OwedAuthority | undefined;
|
|
99
127
|
journal: TailSnapshot;
|
|
100
128
|
graph: CachedSource<{
|
|
101
129
|
version: 1;
|
|
@@ -198,6 +226,7 @@ export declare function createLiveStore(options: LiveStoreOptions): {
|
|
|
198
226
|
delayed: boolean;
|
|
199
227
|
freshness: string;
|
|
200
228
|
operator: import("../../run/operator-state.js").OperatorSnapshot;
|
|
229
|
+
owed: OwedAuthority | undefined;
|
|
201
230
|
journal: TailSnapshot;
|
|
202
231
|
graph: CachedSource<{
|
|
203
232
|
version: 1;
|
|
@@ -1,17 +1,26 @@
|
|
|
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";
|
|
9
10
|
export const OBSERVATION_INTERVAL_MS = 1_000;
|
|
11
|
+
/** Stat-to-fstat observations one poll spends on a journal a writer keeps appending to. */
|
|
12
|
+
export const APPEND_RACE_OBSERVATIONS = 3;
|
|
10
13
|
// Graph declarations have a separate 16 MiB bound; journal retention remains unchanged.
|
|
11
14
|
export const STORE_LIMITS = { graphBytes: 16 * 1024 * 1024, history: 256, historyBytes: 2 * 1024 * 1024, recordBytes: 1024 * 1024, readBytes: 1024 * 1024, subscribers: 64, metrics: 12, errors: 32 };
|
|
12
15
|
const errorText = (e) => e instanceof Error ? e.message : String(e);
|
|
13
16
|
const fileIdentity = (st) => `${st.dev}:${st.ino}`;
|
|
14
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
|
+
};
|
|
15
24
|
function decodeLine(bytes, source, line, offset, endOffset, generation, oversized = false) {
|
|
16
25
|
const base = { source, line, id: `${source}#L${line}`, offset, endOffset, generation };
|
|
17
26
|
let raw = "";
|
|
@@ -22,7 +31,7 @@ function decodeLine(bytes, source, line, offset, endOffset, generation, oversize
|
|
|
22
31
|
if (!raw.trim())
|
|
23
32
|
return { ...base, raw };
|
|
24
33
|
const e = JSON.parse(raw);
|
|
25
|
-
if (!
|
|
34
|
+
if (!isJournalRecord(e))
|
|
26
35
|
throw new Error("invalid journal record");
|
|
27
36
|
return { ...base, raw, event: e };
|
|
28
37
|
}
|
|
@@ -48,6 +57,9 @@ export class JournalTail {
|
|
|
48
57
|
bytesRead = 0;
|
|
49
58
|
lastSuccessfulReadAt;
|
|
50
59
|
failure;
|
|
60
|
+
stale = false;
|
|
61
|
+
hash = createHash("sha256");
|
|
62
|
+
invalid = false;
|
|
51
63
|
constructor(source, hooks = {}) {
|
|
52
64
|
this.source = source;
|
|
53
65
|
this.hooks = hooks;
|
|
@@ -63,20 +75,53 @@ export class JournalTail {
|
|
|
63
75
|
this.errors = [];
|
|
64
76
|
this.malformedCount = 0;
|
|
65
77
|
this.generation++;
|
|
78
|
+
this.hash = createHash("sha256");
|
|
79
|
+
this.invalid = false;
|
|
66
80
|
this.hooks.reset?.();
|
|
67
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; }
|
|
86
|
+
/** A same-inode append between stat and fstat is retried; exhaustion keeps the last good snapshot, pending. */
|
|
68
87
|
poll(now = Date.now()) {
|
|
88
|
+
try {
|
|
89
|
+
for (let observation = 1; !this.observe(); observation++) {
|
|
90
|
+
if (observation === APPEND_RACE_OBSERVATIONS) {
|
|
91
|
+
this.stale = true;
|
|
92
|
+
this.failure = undefined;
|
|
93
|
+
return this.snapshot();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
this.stale = false;
|
|
97
|
+
this.failure = undefined;
|
|
98
|
+
this.lastSuccessfulReadAt = now;
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
this.failure = { source: this.source, error: errorText(e) };
|
|
102
|
+
}
|
|
103
|
+
return this.snapshot();
|
|
104
|
+
}
|
|
105
|
+
/** One stat-to-fstat observation. False when an append raced it; nothing was consumed or reset. */
|
|
106
|
+
observe() {
|
|
69
107
|
let fd;
|
|
70
108
|
try {
|
|
71
109
|
const st = statSync(this.source, { bigint: true });
|
|
72
110
|
if (!st.isFile())
|
|
73
111
|
throw new Error("journal source is not a regular file");
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (Number(st.size) > this.offset) {
|
|
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));
|
|
113
|
+
if (Number(st.size) > (replaced ? 0 : this.offset)) {
|
|
77
114
|
fd = openSync(this.source, "r");
|
|
78
|
-
|
|
115
|
+
const opened = fstatSync(fd, { bigint: true });
|
|
116
|
+
if (stamp(opened) !== stamp(st)) {
|
|
117
|
+
if (fileIdentity(opened) === fileIdentity(st) && opened.size > st.size)
|
|
118
|
+
return false;
|
|
79
119
|
throw new Error("journal changed before read; retry observation");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (replaced)
|
|
123
|
+
this.reset();
|
|
124
|
+
if (fd !== undefined) {
|
|
80
125
|
let remaining = STORE_LIMITS.readBytes;
|
|
81
126
|
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, Number(st.size) - this.offset));
|
|
82
127
|
while (this.offset < Number(st.size) && remaining > 0) {
|
|
@@ -85,6 +130,7 @@ export class JournalTail {
|
|
|
85
130
|
throw new Error("journal truncated during read");
|
|
86
131
|
this.bytesRead += count;
|
|
87
132
|
remaining -= count;
|
|
133
|
+
this.hash.update(buffer.subarray(0, count));
|
|
88
134
|
let begin = 0;
|
|
89
135
|
for (let index = 0; index < count; index++)
|
|
90
136
|
if (buffer[index] === 10) {
|
|
@@ -113,17 +159,12 @@ export class JournalTail {
|
|
|
113
159
|
}
|
|
114
160
|
}
|
|
115
161
|
this.st = st;
|
|
116
|
-
|
|
117
|
-
this.lastSuccessfulReadAt = now;
|
|
118
|
-
}
|
|
119
|
-
catch (e) {
|
|
120
|
-
this.failure = { source: this.source, error: errorText(e) };
|
|
162
|
+
return true;
|
|
121
163
|
}
|
|
122
164
|
finally {
|
|
123
165
|
if (fd !== undefined)
|
|
124
166
|
closeSync(fd);
|
|
125
167
|
}
|
|
126
|
-
return this.snapshot();
|
|
127
168
|
}
|
|
128
169
|
appendCarry(bytes) {
|
|
129
170
|
this.carryBytes += bytes.length;
|
|
@@ -133,11 +174,11 @@ export class JournalTail {
|
|
|
133
174
|
}
|
|
134
175
|
snapshot() {
|
|
135
176
|
return {
|
|
136
|
-
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,
|
|
137
178
|
history: this.history.slice(), errors: this.errors.slice(), malformedCount: this.malformedCount,
|
|
138
179
|
pending: this.carryBytes ? { line: this.line + 1, bytes: this.carryBytes } : undefined,
|
|
139
180
|
backlogBytes: Math.max(0, Number(this.st?.size ?? 0) - this.offset),
|
|
140
|
-
status: this.failure ? "unreadable" : this.malformedCount ? "corrupt" : this.carryBytes ? "pending" : "readable",
|
|
181
|
+
status: this.failure ? "unreadable" : this.malformedCount ? "corrupt" : this.carryBytes || this.stale ? "pending" : "readable",
|
|
141
182
|
error: this.failure, lastSuccessfulReadAt: this.lastSuccessfulReadAt, bytesRead: this.bytesRead,
|
|
142
183
|
};
|
|
143
184
|
}
|
|
@@ -237,6 +278,48 @@ class JsonSource {
|
|
|
237
278
|
}
|
|
238
279
|
}
|
|
239
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
|
+
}
|
|
240
323
|
/** C1 mount/C6 reader boundary. No timers or presence writes: the mount owns scheduling and disarm. */
|
|
241
324
|
export function createLiveStore(options) {
|
|
242
325
|
const now = options.now ?? Date.now;
|
|
@@ -260,10 +343,41 @@ export function createLiveStore(options) {
|
|
|
260
343
|
let disposed = false;
|
|
261
344
|
let queued;
|
|
262
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
|
+
};
|
|
263
367
|
function observe() {
|
|
264
368
|
const observedAt = now();
|
|
265
369
|
const delayed = lastObservation !== undefined && observedAt - lastObservation > 2 * OBSERVATION_INTERVAL_MS;
|
|
266
|
-
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
|
+
}
|
|
267
381
|
const graphReading = graph.read(observedAt);
|
|
268
382
|
const configReading = config.read(observedAt);
|
|
269
383
|
const cacheReading = cache.read(observedAt);
|
|
@@ -276,11 +390,13 @@ export function createLiveStore(options) {
|
|
|
276
390
|
if (metrics.length > STORE_LIMITS.metrics)
|
|
277
391
|
metrics.shift();
|
|
278
392
|
lastObservation = observedAt;
|
|
279
|
-
const readable = journal
|
|
393
|
+
const readable = isReadable(journal);
|
|
280
394
|
return {
|
|
281
395
|
sequence: ++sequence, observedAt, delayed, freshness: errors.length ? "failed" : delayed ? "delayed" : "fresh",
|
|
282
|
-
|
|
283
|
-
|
|
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,
|
|
284
400
|
lock: { ...owner, state: lockState, alive, expired: owner.identity ? observedAt - Number(owner.identity.split(":")[3]) / 1e6 > STALE_MS : undefined },
|
|
285
401
|
supervision, errors, actionsEnabled: readable && !errors.length && !delayed,
|
|
286
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") {
|