tickmarkr 2.5.5 → 2.5.6
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 +3 -1
- package/dist/cli/commands/approve.js +64 -3
- package/dist/cli/commands/doctor.d.ts +10 -0
- package/dist/cli/commands/fleet.js +4 -0
- package/dist/cli/commands/plan.js +20 -4
- package/dist/cli/commands/verify.js +102 -81
- package/dist/config/config.d.ts +11 -0
- package/dist/config/config.js +21 -12
- package/dist/config/fleet-overlay.js +55 -17
- package/dist/drivers/index.js +2 -1
- package/dist/drivers/orca.d.ts +21 -1
- package/dist/drivers/orca.js +209 -27
- package/dist/gates/baseline.d.ts +14 -3
- package/dist/gates/baseline.js +61 -14
- package/dist/gates/cache.d.ts +14 -13
- package/dist/gates/cache.js +17 -5
- package/dist/gates/llm.d.ts +3 -0
- package/dist/gates/llm.js +11 -0
- package/dist/gates/review.d.ts +28 -3
- package/dist/gates/review.js +106 -14
- package/dist/gates/run-gates.d.ts +6 -1
- package/dist/gates/run-gates.js +299 -101
- package/dist/gates/test-manifest.d.ts +30 -1
- package/dist/gates/test-manifest.js +113 -39
- package/dist/gates/test-reporter.js +14 -6
- package/dist/graph/graph.d.ts +2 -0
- package/dist/graph/graph.js +45 -1
- package/dist/run/consult.js +5 -4
- package/dist/run/daemon.d.ts +2 -0
- package/dist/run/daemon.js +480 -99
- package/dist/run/execution-budget.d.ts +25 -0
- package/dist/run/execution-budget.js +142 -0
- package/dist/run/git.d.ts +40 -0
- package/dist/run/git.js +89 -4
- package/dist/run/journal.d.ts +3 -1
- package/dist/run/journal.js +38 -16
- package/dist/run/lease.d.ts +44 -0
- package/dist/run/lease.js +226 -3
- package/dist/run/recovery.d.ts +8 -0
- package/dist/run/recovery.js +25 -0
- package/dist/run/repair-selection.d.ts +12 -0
- package/dist/run/repair-selection.js +56 -0
- package/dist/run/stall.d.ts +6 -1
- package/dist/run/stall.js +60 -3
- package/dist/tui/ink/fleet-app.d.ts +10 -2
- package/dist/tui/ink/fleet-app.js +33 -15
- package/package.json +2 -2
- package/skills/tickmarkr-overseer/scripts/grade-ci.sh +29 -2
|
@@ -23,7 +23,8 @@ function allowFormFromExclusions(universe, edited) {
|
|
|
23
23
|
}
|
|
24
24
|
// LEG2-T3 round 2 finding 1: every staged entry excludes what it NAMES — a bare model id every
|
|
25
25
|
// adapter serving it, an identity its alias — never only an adapter id or an adapter:model key.
|
|
26
|
-
|
|
26
|
+
// OBS-1046: the staged allow complement is a reason of its own, beside the authored deny lists.
|
|
27
|
+
const entries = [...edited.denyAdapters, ...edited.denyModels, ...(edited.allowOut ?? [])];
|
|
27
28
|
const adapters = [];
|
|
28
29
|
const models = [];
|
|
29
30
|
let excluded = false;
|
|
@@ -56,9 +57,12 @@ function residualDeny(universe, entries) {
|
|
|
56
57
|
// list keeps its node. A staged entry the allow form cannot express as a membership key (outside the
|
|
57
58
|
// probe universe, or a bare-model/identity spelling) is written verbatim too; canonical keys the
|
|
58
59
|
// session added ride the allow form alone.
|
|
59
|
-
function
|
|
60
|
+
function authoredEntries(doc, path) {
|
|
60
61
|
const node = doc.getIn(path, true);
|
|
61
|
-
|
|
62
|
+
return isSeq(node) ? node.items.flatMap((item) => (isScalar(item) ? [String(item.value)] : [])) : [];
|
|
63
|
+
}
|
|
64
|
+
function flatDenyAfterWrite(doc, path, after, universe) {
|
|
65
|
+
const authored = authoredEntries(doc, path);
|
|
62
66
|
const canonical = (entry) => universe.some((row) => entry === row.adapter || row.models.some((m) => entry === `${row.adapter}:${m}`));
|
|
63
67
|
const kept = authored.filter((entry) => after.includes(entry));
|
|
64
68
|
const verbatim = sortedUnique(after.filter((entry) => !kept.includes(entry)
|
|
@@ -183,9 +187,12 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
183
187
|
if (doc.errors.length)
|
|
184
188
|
throw doc.errors[0];
|
|
185
189
|
const { initial, edited } = write;
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
190
|
+
// OBS-1046: each flat scope (and the allow complement) is compared on its own — a scope the
|
|
191
|
+
// session never edited keeps its node byte for byte, whatever its sibling did.
|
|
192
|
+
const changed = (before = [], after = []) => sortedUnique(before).join() !== sortedUnique(after).join();
|
|
193
|
+
const adaptersChanged = changed(initial.denyAdapters, edited.denyAdapters);
|
|
194
|
+
const modelsChanged = changed(initial.denyModels, edited.denyModels);
|
|
195
|
+
if (adaptersChanged || modelsChanged || changed(initial.allowOut, edited.allowOut)) {
|
|
189
196
|
if (write.universe) {
|
|
190
197
|
// Membership write: the allow form IS the fleet; deny adapters/models scopes are tombstoned
|
|
191
198
|
// so a lower layer can never re-exclude behind the operator's back (workers untouched).
|
|
@@ -215,18 +222,44 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
215
222
|
// Whole fleet in: no restriction to express — the allow block goes away entirely.
|
|
216
223
|
deleteAt(doc, ["routing", "allow"]);
|
|
217
224
|
}
|
|
218
|
-
// Authored and non-canonical entries stay in deny (LEG2-T3 finding 4, round 2 finding 2)
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
|
|
225
|
+
// Authored and non-canonical entries stay in deny (LEG2-T3 finding 4, round 2 finding 2).
|
|
226
|
+
// OBS-1046: only an EDITED scope is rewritten, and only when its bytes must change — an
|
|
227
|
+
// addition the allow form carries leaves the list (an explicit `[]` included) untouched; a
|
|
228
|
+
// scope the press CLEARED down to nothing is tombstoned so a lower layer can never
|
|
229
|
+
// re-exclude behind the operator's back (workers untouched).
|
|
230
|
+
// LEG2-T3 finding 4: an authored entry the edit admits (staged nowhere any more) must go
|
|
231
|
+
// even from a scope whose own set did not change, or the admitted channel stays excluded
|
|
232
|
+
// behind the preserved bytes.
|
|
233
|
+
const stagedAfter = new Set([...edited.denyAdapters, ...edited.denyModels, ...(edited.allowOut ?? [])]);
|
|
234
|
+
const admitted = [...initial.denyAdapters, ...initial.denyModels, ...(initial.allowOut ?? [])]
|
|
235
|
+
.filter((entry) => !stagedAfter.has(entry));
|
|
236
|
+
const scopes = [
|
|
237
|
+
["adapters", initial.denyAdapters, edited.denyAdapters, adaptersChanged],
|
|
238
|
+
["models", initial.denyModels, edited.denyModels, modelsChanged],
|
|
239
|
+
];
|
|
240
|
+
for (const [scope, before, after, touched] of scopes) {
|
|
222
241
|
const path = ["routing", "deny", scope];
|
|
242
|
+
const authored = authoredEntries(doc, path);
|
|
243
|
+
const stale = authored.some((entry) => admitted.includes(entry));
|
|
244
|
+
if (!touched && !stale)
|
|
245
|
+
continue;
|
|
223
246
|
const remaining = flatDenyAfterWrite(doc, path, after, write.universe);
|
|
224
|
-
|
|
247
|
+
if (remaining.length) {
|
|
248
|
+
if (remaining.join("\n") !== authored.join("\n"))
|
|
249
|
+
setStringSequencePreservingComments(doc, path, remaining);
|
|
250
|
+
}
|
|
251
|
+
else if (stale || before.some((entry) => !after.includes(entry))) {
|
|
252
|
+
setStringSequencePreservingComments(doc, path, null);
|
|
253
|
+
}
|
|
225
254
|
}
|
|
226
255
|
}
|
|
227
256
|
else {
|
|
228
|
-
|
|
229
|
-
|
|
257
|
+
if (adaptersChanged) {
|
|
258
|
+
setStringSequencePreservingComments(doc, ["routing", "deny", "adapters"], edited.denyAdapters.length ? sortedUnique(edited.denyAdapters) : null);
|
|
259
|
+
}
|
|
260
|
+
if (modelsChanged) {
|
|
261
|
+
setStringSequencePreservingComments(doc, ["routing", "deny", "models"], edited.denyModels.length ? sortedUnique(edited.denyModels) : null);
|
|
262
|
+
}
|
|
230
263
|
}
|
|
231
264
|
}
|
|
232
265
|
// OBS-994/FL-1: routing.deny.workers is a literal deny list, never a universe-derived
|
|
@@ -360,15 +393,19 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
360
393
|
// operator reviews. commentString has no position context, but this writer owns the document:
|
|
361
394
|
// scalar-trailing single-line comments (the only inline form fleet emits) are marked with a
|
|
362
395
|
// private-use sentinel (the FIRST_TOUCH envelope precedent above), everything else renders
|
|
363
|
-
// byte-identical to yaml's own stringifyComment.
|
|
396
|
+
// byte-identical to yaml's own stringifyComment. OBS-1046: a scalar parsed from the prior bytes
|
|
397
|
+
// keeps the exact whitespace it had before its hash sign (yaml itself emits one space, so the
|
|
398
|
+
// sentinel carries the rest); only a comment fleet authored gets the two-space style.
|
|
364
399
|
visit(doc, (_key, node) => {
|
|
365
400
|
if (isScalar(node) && typeof node.comment === "string" && !node.comment.includes("\n")) {
|
|
366
|
-
node.
|
|
401
|
+
const tail = node.range ? priorBytes.slice(node.range[1], node.range[2]) : "";
|
|
402
|
+
const gap = /^([ \t]+)#/.exec(tail)?.[1] ?? " ";
|
|
403
|
+
node.comment = `${INLINE_COMMENT_SENTINEL}${gap.slice(1)}#${node.comment}`;
|
|
367
404
|
}
|
|
368
405
|
});
|
|
369
406
|
return doc.toString({
|
|
370
407
|
commentString: (comment) => comment.startsWith(INLINE_COMMENT_SENTINEL)
|
|
371
|
-
?
|
|
408
|
+
? comment.slice(1)
|
|
372
409
|
: comment.replace(/^(?!$)(?: $)?/gm, "#"),
|
|
373
410
|
// OBS-518: yaml's default pads flow collections (`[kimi]` → `[ kimi ]`), churning untouched
|
|
374
411
|
// lines on the one confirmation surface an operator reviews. Hand-written overlays use the
|
|
@@ -384,7 +421,8 @@ export function fleetRepoOverlayFromDelta(initial, edited, existingRepo = {}, fi
|
|
|
384
421
|
const routing = { ...out.routing };
|
|
385
422
|
let routingTouched = false;
|
|
386
423
|
const denyChanged = sortedUnique(initial.denyAdapters).join() !== sortedUnique(edited.denyAdapters).join()
|
|
387
|
-
|| sortedUnique(initial.denyModels).join() !== sortedUnique(edited.denyModels).join()
|
|
424
|
+
|| sortedUnique(initial.denyModels).join() !== sortedUnique(edited.denyModels).join()
|
|
425
|
+
|| sortedUnique(initial.allowOut ?? []).join() !== sortedUnique(edited.allowOut ?? []).join();
|
|
388
426
|
if (denyChanged) {
|
|
389
427
|
if (universe) {
|
|
390
428
|
const form = allowFormFromExclusions(universe, edited);
|
package/dist/drivers/index.js
CHANGED
|
@@ -15,7 +15,8 @@ const hostByDriver = new WeakMap();
|
|
|
15
15
|
export function classifyHost(env = process.env) {
|
|
16
16
|
if (env.HERDR_ENV === "1")
|
|
17
17
|
return "herdr";
|
|
18
|
-
|
|
18
|
+
// A whitespace-only handle is no handle: the narrator trims it and would refuse the split.
|
|
19
|
+
if (env.TERM_PROGRAM === "Orca" && (env.ORCA_TERMINAL_HANDLE ?? "").trim() !== "")
|
|
19
20
|
return "orca";
|
|
20
21
|
return "none";
|
|
21
22
|
}
|
package/dist/drivers/orca.d.ts
CHANGED
|
@@ -124,19 +124,28 @@ export declare function mapAgentState(term: Record<string, unknown>, tuiIdle: bo
|
|
|
124
124
|
* is proven gone. Everything else — a reservation or claim with no bound pane, a failed cleanup, a
|
|
125
125
|
* record another driver holds with a live observer — refuses and keeps the record exactly as it is.
|
|
126
126
|
*/
|
|
127
|
+
/** `placer` is the pid of the narrator that wrote the reservation: a reserved or claimed record is a
|
|
128
|
+
* normal intermediate state while that pid lives (another driver awaiting listing or bind) and a
|
|
129
|
+
* crash to recover only once it is dead. */
|
|
127
130
|
type BoardRecord = WatchBoardOwner & {
|
|
128
131
|
retired?: true;
|
|
129
132
|
runtimeId?: string;
|
|
133
|
+
placer?: number;
|
|
130
134
|
};
|
|
131
135
|
/**
|
|
132
136
|
* Whole-record compare-and-swap. The canonical path stays readable until commit: a mkdir lock
|
|
133
137
|
* excludes other writers, then create-only `link`s the new inode (fails if anything exists) or
|
|
134
138
|
* `rename`s the new file over the live path (POSIX atomic replace — readers see old or new, never
|
|
135
139
|
* absence). A crash that leaves a `.tmp` or `.lock` does not drop the previous record.
|
|
140
|
+
* Each lock generation publishes its pid and nonce atomically in a symlink target. A holder that died
|
|
141
|
+
* between taking and releasing it is recovered at once (its lock is taken over in place); a live
|
|
142
|
+
* holder bounds the wait on injected time and is then refused with the record untouched. A legacy
|
|
143
|
+
* pid-less or malformed generation is recoverable too: a contender atomically creates the next owner
|
|
144
|
+
* generation, and release removes the directory only while that exact generation is still current.
|
|
136
145
|
* ponytail: observeNamedRun (supervision.ts) renames without CAS. It cannot interleave with a swap
|
|
137
146
|
* because the narrator writes nothing between reserve and claim, and no transition here swaps one.
|
|
138
147
|
*/
|
|
139
|
-
export declare function casBoard(family: string, path: string, expected: string | undefined, next: BoardRecord): string
|
|
148
|
+
export declare function casBoard(family: string, path: string, expected: string | undefined, next: BoardRecord, time?: OrcaTimeSource): Promise<string>;
|
|
140
149
|
/**
|
|
141
150
|
* The renderer hard-wraps long lines, paints margin chrome, and a cursor page boundary splits a
|
|
142
151
|
* marker exactly like a wrap does. `parseWorkerResult` (src/adapters/prompt.ts) already de-wraps
|
|
@@ -146,6 +155,8 @@ export declare function casBoard(family: string, path: string, expected: string
|
|
|
146
155
|
* never depends on this.
|
|
147
156
|
*/
|
|
148
157
|
export declare function joinWrapped(raw: string): string;
|
|
158
|
+
/** The OBS-1011 add.1 capture: `status:"exited", tail:[], returnedLineCount 0` from a stream read. */
|
|
159
|
+
export declare function isBlindStreamPage(term: Record<string, unknown>): boolean;
|
|
149
160
|
export interface OrcaDriverOpts {
|
|
150
161
|
bin?: string;
|
|
151
162
|
env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
|
|
@@ -242,6 +253,15 @@ export declare class OrcaDriver implements ExecutorDriver {
|
|
|
242
253
|
private liveShowTerm;
|
|
243
254
|
private tailText;
|
|
244
255
|
private readPage;
|
|
256
|
+
/**
|
|
257
|
+
* OBS-1011 add.1 / OBS-1016: the captured incident shape — a stream page answering status exited
|
|
258
|
+
* with an empty tail on a terminal that accepted a send seconds earlier — is BLIND, not dead, when
|
|
259
|
+
* the same handle's show record on the same runtime reports connected and not orphaned (show carries
|
|
260
|
+
* no status field; none is demanded) and its screen read reports running. Then the rendered frame is
|
|
261
|
+
* the terminal's bytes. Anything less — disconnected, orphaned, another handle or runtime, a screen
|
|
262
|
+
* that is unavailable or exited — is refused as unavailable, exactly as the dead record would be.
|
|
263
|
+
*/
|
|
264
|
+
private screenBehindBlindStream;
|
|
245
265
|
/** A rendered-frame liveness read. `--screen` and `--cursor` are mutually exclusive in Orca. */
|
|
246
266
|
private readScreen;
|
|
247
267
|
/** A single UNPAGED tail read — exactly what the caller asked for and nothing more. Markers split
|
package/dist/drivers/orca.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, linkSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, linkSync, mkdirSync, readdirSync, readFileSync, readlinkSync, realpathSync, renameSync, rmdirSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { shq } from "../adapters/types.js";
|
|
@@ -300,26 +300,57 @@ function readBoard(path, repo, runId) {
|
|
|
300
300
|
* excludes other writers, then create-only `link`s the new inode (fails if anything exists) or
|
|
301
301
|
* `rename`s the new file over the live path (POSIX atomic replace — readers see old or new, never
|
|
302
302
|
* absence). A crash that leaves a `.tmp` or `.lock` does not drop the previous record.
|
|
303
|
+
* Each lock generation publishes its pid and nonce atomically in a symlink target. A holder that died
|
|
304
|
+
* between taking and releasing it is recovered at once (its lock is taken over in place); a live
|
|
305
|
+
* holder bounds the wait on injected time and is then refused with the record untouched. A legacy
|
|
306
|
+
* pid-less or malformed generation is recoverable too: a contender atomically creates the next owner
|
|
307
|
+
* generation, and release removes the directory only while that exact generation is still current.
|
|
303
308
|
* ponytail: observeNamedRun (supervision.ts) renames without CAS. It cannot interleave with a swap
|
|
304
309
|
* because the narrator writes nothing between reserve and claim, and no transition here swaps one.
|
|
305
310
|
*/
|
|
306
|
-
export function casBoard(family, path, expected, next) {
|
|
311
|
+
export async function casBoard(family, path, expected, next, time = SYSTEM_TIME) {
|
|
307
312
|
const raw = JSON.stringify(next) + "\n";
|
|
308
313
|
const refused = () => new OrcaError(family, `watch owner record ${path} changed underneath; swap refused and the current record kept`, "");
|
|
309
314
|
mkdirSync(dirname(path), { recursive: true });
|
|
310
315
|
const lock = `${path}.lock`;
|
|
311
|
-
const deadline =
|
|
316
|
+
const deadline = time.now() + LOCK_WAIT_MS;
|
|
317
|
+
const ownerTarget = `${process.pid}:${randomUUID()}`;
|
|
318
|
+
let ownerEntry = "";
|
|
319
|
+
let acquiredEntries = [];
|
|
312
320
|
for (;;) {
|
|
313
321
|
try {
|
|
314
322
|
mkdirSync(lock);
|
|
315
|
-
break;
|
|
316
323
|
}
|
|
317
324
|
catch (error) {
|
|
318
325
|
if (error.code !== "EEXIST")
|
|
319
326
|
throw error;
|
|
320
|
-
|
|
327
|
+
}
|
|
328
|
+
// The marker's directory entry and target appear as one filesystem operation. If the creator
|
|
329
|
+
// dies after mkdir but before this call, a contender claims owner.0; if both race, only one
|
|
330
|
+
// symlink wins and the loser judges that published owner before entering the CAS section.
|
|
331
|
+
const holder = lockHolder(lock);
|
|
332
|
+
if (holder?.pid !== undefined && pidLive(holder.pid)) {
|
|
333
|
+
if (time.now() >= deadline)
|
|
321
334
|
throw new OrcaError(family, `watch owner record ${path} lock not acquired; current record kept`, "");
|
|
322
|
-
|
|
335
|
+
await time.sleep(10);
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
ownerEntry = `owner.${(holder?.gen ?? -1) + 1}`;
|
|
339
|
+
try {
|
|
340
|
+
symlinkSync(ownerTarget, join(lock, ownerEntry));
|
|
341
|
+
// Snapshot only generations that this takeover superseded, with our marker last. Release must
|
|
342
|
+
// never recursively scan: after our final unlink a contender may publish a replacement marker
|
|
343
|
+
// before rmdir, and that marker belongs to the contender, not to this generation.
|
|
344
|
+
acquiredEntries = readdirSync(lock)
|
|
345
|
+
.filter((entry) => entry === ownerEntry || lockEntryGeneration(entry) !== undefined)
|
|
346
|
+
.sort((a, b) => Number(a === ownerEntry) - Number(b === ownerEntry));
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
const code = error.code;
|
|
351
|
+
if (code !== "EEXIST" && code !== "ENOENT")
|
|
352
|
+
throw error;
|
|
353
|
+
continue; // another contender published this generation, or the prior holder just released
|
|
323
354
|
}
|
|
324
355
|
}
|
|
325
356
|
try {
|
|
@@ -354,9 +385,69 @@ export function casBoard(family, path, expected, next) {
|
|
|
354
385
|
return raw;
|
|
355
386
|
}
|
|
356
387
|
finally {
|
|
357
|
-
|
|
388
|
+
releaseBoardLock(lock, ownerEntry, ownerTarget, acquiredEntries);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const LOCK_WAIT_MS = 2_000;
|
|
392
|
+
/** Best-effort release cannot invalidate a committed CAS. Superseded entries go first and our marker
|
|
393
|
+
* goes last; after that unlink, a non-recursive rmdir either removes the empty old generation or
|
|
394
|
+
* leaves a contender's newly published marker untouched. Any abnormal filesystem error leaves the
|
|
395
|
+
* lock for the next generation's stale-owner recovery instead of escaping from the caller's finally. */
|
|
396
|
+
function releaseBoardLock(lock, ownerEntry, ownerTarget, acquiredEntries) {
|
|
397
|
+
const holder = lockHolder(lock);
|
|
398
|
+
if (holder?.entry !== ownerEntry || holder.target !== ownerTarget)
|
|
399
|
+
return;
|
|
400
|
+
for (const entry of acquiredEntries) {
|
|
401
|
+
try {
|
|
402
|
+
unlinkSync(join(lock, entry));
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
if (error.code !== "ENOENT")
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
try {
|
|
410
|
+
rmdirSync(lock);
|
|
411
|
+
}
|
|
412
|
+
catch { /* ENOTEMPTY means a contender owns it; any other failure is recoverable stale state. */ }
|
|
413
|
+
}
|
|
414
|
+
/** The highest published generation. `pid*` files are accepted for recovery compatibility; new
|
|
415
|
+
* owners use atomic `owner.N -> pid:nonce` symlinks. Missing or malformed contents return a
|
|
416
|
+
* generation with no pid, which is stale and can only be superseded by creating the next marker. */
|
|
417
|
+
function lockHolder(lock) {
|
|
418
|
+
try {
|
|
419
|
+
const candidates = readdirSync(lock).flatMap((entry) => {
|
|
420
|
+
const owner = /^owner\.(\d+)$/.exec(entry);
|
|
421
|
+
const gen = lockEntryGeneration(entry);
|
|
422
|
+
return gen === undefined ? [] : [{ gen, entry, owner: owner !== null }];
|
|
423
|
+
}).sort((a, b) => b.gen - a.gen || Number(b.owner) - Number(a.owner));
|
|
424
|
+
const current = candidates[0];
|
|
425
|
+
if (!current)
|
|
426
|
+
return undefined;
|
|
427
|
+
let target;
|
|
428
|
+
try {
|
|
429
|
+
target = current.owner
|
|
430
|
+
? readlinkSync(join(lock, current.entry))
|
|
431
|
+
: readFileSync(join(lock, current.entry), "utf8").trim();
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
return { gen: current.gen, entry: current.entry };
|
|
435
|
+
}
|
|
436
|
+
const pidText = current.owner ? /^([1-9]\d*):.+$/.exec(target)?.[1] : target;
|
|
437
|
+
const pid = Number(pidText);
|
|
438
|
+
return Number.isInteger(pid) && pid > 0
|
|
439
|
+
? { gen: current.gen, entry: current.entry, target, pid }
|
|
440
|
+
: { gen: current.gen, entry: current.entry, target };
|
|
441
|
+
}
|
|
442
|
+
catch {
|
|
443
|
+
return undefined;
|
|
358
444
|
}
|
|
359
445
|
}
|
|
446
|
+
function lockEntryGeneration(entry) {
|
|
447
|
+
const owner = /^owner\.(\d+)$/.exec(entry);
|
|
448
|
+
const legacy = /^pid(?:\.(\d+))?$/.exec(entry);
|
|
449
|
+
return owner ? Number(owner[1]) : legacy ? Number(legacy[1] ?? 0) : undefined;
|
|
450
|
+
}
|
|
360
451
|
function pidLive(pid) {
|
|
361
452
|
try {
|
|
362
453
|
process.kill(pid, 0);
|
|
@@ -463,6 +554,13 @@ function collectTabs(node, out) {
|
|
|
463
554
|
export function joinWrapped(raw) {
|
|
464
555
|
return raw.split("\n").map((l) => l.replace(/^[\s│|]+/, "").replace(/[\s│|]+$/, "")).join("");
|
|
465
556
|
}
|
|
557
|
+
/** The OBS-1011 add.1 capture: `status:"exited", tail:[], returnedLineCount 0` from a stream read. */
|
|
558
|
+
export function isBlindStreamPage(term) {
|
|
559
|
+
return str(term.status) === "exited"
|
|
560
|
+
&& term.source !== "screen"
|
|
561
|
+
&& Array.isArray(term.tail) && term.tail.length === 0
|
|
562
|
+
&& (term.returnedLineCount === undefined || term.returnedLineCount === 0);
|
|
563
|
+
}
|
|
466
564
|
export class OrcaDriver {
|
|
467
565
|
id = "orca";
|
|
468
566
|
interactive = true; // a visible terminal the operator can watch and answer
|
|
@@ -637,8 +735,8 @@ export class OrcaDriver {
|
|
|
637
735
|
// terminalOp proves the runtime binding before the handle goes on the wire.
|
|
638
736
|
await this.sendText(st, cmd);
|
|
639
737
|
}
|
|
640
|
-
async sendText(st, text) {
|
|
641
|
-
const env = await this.terminalOp("send", st, (h) => this.call("send", ["terminal", "send", "--terminal", h, "--text", text, "--enter"], this.cliCwd(st)), { mutating: true });
|
|
738
|
+
async sendText(st, text, waitSubmit = false) {
|
|
739
|
+
const env = await this.terminalOp("send", st, (h) => this.call("send", ["terminal", "send", "--terminal", h, "--text", text, "--enter", ...(waitSubmit ? ["--wait-submit", "15"] : [])], this.cliCwd(st)), { mutating: true });
|
|
642
740
|
// Recorded 1.4.195 send receipt: result.send = {handle, accepted, bytesWritten}. `ok:true`
|
|
643
741
|
// alone is not delivery: the accepted receipt must name this handle and account for the bytes.
|
|
644
742
|
const receipt = this.sendReceipt(env, st);
|
|
@@ -646,6 +744,7 @@ export class OrcaDriver {
|
|
|
646
744
|
if (receipt.accepted !== true || typeof receipt.bytesWritten !== "number" || receipt.bytesWritten !== expectedBytes) {
|
|
647
745
|
throw new OrcaError("send", `send receipt does not report expected byte delivery (accepted: ${JSON.stringify(receipt.accepted)}, bytesWritten: ${receipt.bytesWritten}, expected: ${expectedBytes})`, env.raw);
|
|
648
746
|
}
|
|
747
|
+
return receipt;
|
|
649
748
|
}
|
|
650
749
|
sendReceipt(env, st) {
|
|
651
750
|
const receipt = env.result.send;
|
|
@@ -989,8 +1088,35 @@ export class OrcaDriver {
|
|
|
989
1088
|
...(cursor === undefined || recovered ? [] : ["--cursor", cursor]),
|
|
990
1089
|
"--limit", String(lines),
|
|
991
1090
|
], this.cliCwd(st)), { onRecovered: () => { recovered = true; } });
|
|
1091
|
+
if (isBlindStreamPage(requireTerminal(family, env))) {
|
|
1092
|
+
return { term: await this.screenBehindBlindStream(family, st, env), raw: env.raw, recovered };
|
|
1093
|
+
}
|
|
992
1094
|
return { term: this.validated(family, st, env), raw: env.raw, recovered };
|
|
993
1095
|
}
|
|
1096
|
+
/**
|
|
1097
|
+
* OBS-1011 add.1 / OBS-1016: the captured incident shape — a stream page answering status exited
|
|
1098
|
+
* with an empty tail on a terminal that accepted a send seconds earlier — is BLIND, not dead, when
|
|
1099
|
+
* the same handle's show record on the same runtime reports connected and not orphaned (show carries
|
|
1100
|
+
* no status field; none is demanded) and its screen read reports running. Then the rendered frame is
|
|
1101
|
+
* the terminal's bytes. Anything less — disconnected, orphaned, another handle or runtime, a screen
|
|
1102
|
+
* that is unavailable or exited — is refused as unavailable, exactly as the dead record would be.
|
|
1103
|
+
*/
|
|
1104
|
+
async screenBehindBlindStream(family, st, blind) {
|
|
1105
|
+
const handle = str(requireTerminal(family, blind).handle);
|
|
1106
|
+
if (handle !== st.handle) {
|
|
1107
|
+
throw new OrcaUnavailableError(family, `terminal record names ${handle ?? "no handle"}, not the addressed ${st.handle}`, blind.raw);
|
|
1108
|
+
}
|
|
1109
|
+
const show = await this.call("show", ["terminal", "show", "--terminal", st.handle], this.cliCwd(st));
|
|
1110
|
+
if (show.runtimeId !== st.runtimeId) {
|
|
1111
|
+
throw new OrcaUnavailableError(family, `exited-shaped stream page: show answered by runtime ${show.runtimeId}, not the bound ${st.runtimeId}`, show.raw, "exited");
|
|
1112
|
+
}
|
|
1113
|
+
this.liveShowTerm(family, st, show);
|
|
1114
|
+
const screen = await this.readScreen(st);
|
|
1115
|
+
if (screen.source !== "screen") {
|
|
1116
|
+
throw new OrcaUnavailableError(family, `exited-shaped stream page and no rendered screen for ${st.handle}`, blind.raw, "exited");
|
|
1117
|
+
}
|
|
1118
|
+
return screen.term;
|
|
1119
|
+
}
|
|
994
1120
|
/** A rendered-frame liveness read. `--screen` and `--cursor` are mutually exclusive in Orca. */
|
|
995
1121
|
async readScreen(st) {
|
|
996
1122
|
const env = await this.terminalOp("status", st, (h) => this.call("read", [
|
|
@@ -1238,17 +1364,26 @@ export class OrcaDriver {
|
|
|
1238
1364
|
const st = this.state(slot);
|
|
1239
1365
|
if (!await this.waitCondition(st, "tui-idle", 1))
|
|
1240
1366
|
return false;
|
|
1241
|
-
|
|
1242
|
-
//
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1367
|
+
const receipt = await this.sendText(st, message, true);
|
|
1368
|
+
// OBS-1016: delivery is proven by the receipt — a prompt stage of turn_started — or by the
|
|
1369
|
+
// composer emptying on a screen read, never by stream echo: the stream is blind on a live Orca
|
|
1370
|
+
// terminal (OBS-1011 add.1), so an echo sweep fails every nudge and latches the harvest hold.
|
|
1371
|
+
const prompt = typeof receipt.prompt === "object" && receipt.prompt !== null && !Array.isArray(receipt.prompt)
|
|
1372
|
+
? receipt.prompt
|
|
1373
|
+
: {};
|
|
1374
|
+
const stages = Array.isArray(prompt.stages) ? prompt.stages : [];
|
|
1375
|
+
if (stages.includes("turn_started"))
|
|
1376
|
+
return true;
|
|
1247
1377
|
const deadline = this.time.now() + NUDGE_ECHO_TIMEOUT_MS;
|
|
1248
1378
|
for (;;) {
|
|
1249
|
-
const
|
|
1250
|
-
|
|
1251
|
-
|
|
1379
|
+
const screen = await this.readScreen(st);
|
|
1380
|
+
// ponytail: "composer emptied" is "the text is no longer on the frame" — a TUI that keeps the
|
|
1381
|
+
// submitted turn on screen reads as undelivered until the timeout; the receipt stage is primary.
|
|
1382
|
+
if (screen.source === "screen") {
|
|
1383
|
+
const frame = this.tailText("status", screen.term, "");
|
|
1384
|
+
if (!frame.includes(message) && !joinWrapped(frame).includes(message))
|
|
1385
|
+
return true;
|
|
1386
|
+
}
|
|
1252
1387
|
const left = deadline - this.time.now();
|
|
1253
1388
|
if (left <= 0)
|
|
1254
1389
|
return false;
|
|
@@ -1280,7 +1415,36 @@ export class OrcaDriver {
|
|
|
1280
1415
|
else if (current) {
|
|
1281
1416
|
const state = boardState(current.record);
|
|
1282
1417
|
if (state === "reserved" || state === "claimed") {
|
|
1283
|
-
|
|
1418
|
+
// A reservation or claim with no bound pane is the placing narrator's normal intermediate
|
|
1419
|
+
// state while that narrator lives (awaiting its receipt, listing, claim or bind): refused
|
|
1420
|
+
// untouched, its observer never asked to stop. Only a DEAD placer (or one unrecorded) makes
|
|
1421
|
+
// it a crash to recover.
|
|
1422
|
+
const { placer } = current.record;
|
|
1423
|
+
if (typeof placer !== "number" || pidLive(placer)) {
|
|
1424
|
+
throw new OrcaError("split", `Orca narrator placement remains unresolved for ${name} (${state}, no bound pane) while placing narrator ${placer ?? "unrecorded"} lives; ${kept}`, "");
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
if (state === "claimed") {
|
|
1428
|
+
// The placing driver crashed between the observer's claim and its bind. The record is
|
|
1429
|
+
// replaced only once that observer is proven gone: dead outright, or live and stopped with
|
|
1430
|
+
// its acknowledgement awaited on injected time — unacknowledged, it is refused untouched.
|
|
1431
|
+
const pid = current.record.pid;
|
|
1432
|
+
if (pidLive(pid)) {
|
|
1433
|
+
try {
|
|
1434
|
+
await stopWatchBoard(current.record, this.time);
|
|
1435
|
+
}
|
|
1436
|
+
catch (error) {
|
|
1437
|
+
throw new OrcaError("split", `Orca narrator placement refused for ${name}: claimed board's live observer unacknowledged; ${kept}; ${errorText(error)}`, "");
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
const retired = { ...current.record, retired: true };
|
|
1441
|
+
current = { raw: await casBoard("split", path, current.raw, retired, this.time), record: retired };
|
|
1442
|
+
}
|
|
1443
|
+
if (state === "reserved") {
|
|
1444
|
+
// The placer died before any claim: no observer exists, its split pane (if any) is unknown
|
|
1445
|
+
// to the record. Tombstoned in place so the generic tail below replaces it.
|
|
1446
|
+
const retired = { ...current.record, retired: true };
|
|
1447
|
+
current = { raw: await casBoard("split", path, current.raw, retired, this.time), record: retired };
|
|
1284
1448
|
}
|
|
1285
1449
|
if (state === "bound") {
|
|
1286
1450
|
// The bound record is the durable truth of placement: the child handle PLUS the split
|
|
@@ -1290,7 +1454,7 @@ export class OrcaDriver {
|
|
|
1290
1454
|
}
|
|
1291
1455
|
// Lost: this runtime no longer has that pane. Tombstone before anything else.
|
|
1292
1456
|
const retired = { ...current.record, retired: true };
|
|
1293
|
-
current = { raw: casBoard("split", path, current.raw, retired), record: retired };
|
|
1457
|
+
current = { raw: await casBoard("split", path, current.raw, retired, this.time), record: retired };
|
|
1294
1458
|
}
|
|
1295
1459
|
// A tombstone can be an acknowledgement timeout, not completed observer cleanup. A live
|
|
1296
1460
|
// observer must stop and ack on injected time before the handle-bound close; timeout keeps
|
|
@@ -1316,7 +1480,19 @@ export class OrcaDriver {
|
|
|
1316
1480
|
// Reserve before any command can read the token: create-only, or a swap of the exact record
|
|
1317
1481
|
// judged replaceable above.
|
|
1318
1482
|
const token = randomUUID();
|
|
1319
|
-
casBoard("split", path, current?.raw, { repo: realpathSync(cwd), runId, driver: this.id, workspace: ORCA_SPACE, pane: "", name, token });
|
|
1483
|
+
await casBoard("split", path, current?.raw, { repo: realpathSync(cwd), runId, driver: this.id, workspace: ORCA_SPACE, pane: "", name, token, placer: process.pid }, this.time);
|
|
1484
|
+
// A reservation that can never be bound — no usable receipt, or no claim — is tombstoned so the
|
|
1485
|
+
// next narrator call splits afresh instead of refusing forever. Only THIS reservation, and only
|
|
1486
|
+
// while it is still the untouched reservation; a record anyone else moved is left to them.
|
|
1487
|
+
const tombstone = async (extra = {}) => {
|
|
1488
|
+
try {
|
|
1489
|
+
const now = readBoard(path, cwd, runId);
|
|
1490
|
+
if (now?.record.token === token && boardState(now.record) === "reserved") {
|
|
1491
|
+
await casBoard("split", path, now.raw, { ...now.record, ...extra, retired: true }, this.time);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
catch { /* the record is kept as it stands; the next narrator judges it */ }
|
|
1495
|
+
};
|
|
1320
1496
|
// Exactly one terminal split of the launching handle, horizontal, carrying token and command.
|
|
1321
1497
|
// An unknown receipt (transport failure, refusal, unparseable output) after the verb was issued
|
|
1322
1498
|
// is as indeterminate as a handle-less one: the pane may exist, so the reservation stays.
|
|
@@ -1325,7 +1501,8 @@ export class OrcaDriver {
|
|
|
1325
1501
|
"--terminal", launchingHandle,
|
|
1326
1502
|
"--direction", "horizontal",
|
|
1327
1503
|
"--command", `${WATCH_OWNER_ENV}=${shq(token)} ${command}`,
|
|
1328
|
-
], cwd).catch((error) => {
|
|
1504
|
+
], cwd).catch(async (error) => {
|
|
1505
|
+
await tombstone();
|
|
1329
1506
|
throw new OrcaError("split", `Orca narrator placement failed for ${name}: split receipt is unknown (${error instanceof OrcaError ? error.reason : errorText(error)}); ${kept}`, error instanceof OrcaError ? error.raw : "");
|
|
1330
1507
|
});
|
|
1331
1508
|
let child;
|
|
@@ -1333,6 +1510,7 @@ export class OrcaDriver {
|
|
|
1333
1510
|
child = splitReceipt(env);
|
|
1334
1511
|
}
|
|
1335
1512
|
catch {
|
|
1513
|
+
await tombstone();
|
|
1336
1514
|
throw new OrcaError("split", `Orca narrator placement failed for ${name}: split receipt is unknown, malformed or handle-less; ${kept}`, env.raw);
|
|
1337
1515
|
}
|
|
1338
1516
|
// Parent tabId must name the launching terminal's tab, and the child must actually appear
|
|
@@ -1343,6 +1521,7 @@ export class OrcaDriver {
|
|
|
1343
1521
|
listing = await this.listAll(cwd);
|
|
1344
1522
|
}
|
|
1345
1523
|
catch (error) {
|
|
1524
|
+
await tombstone();
|
|
1346
1525
|
throw new OrcaError("split", `Orca narrator placement failed for ${name}: split receipt is unknown, malformed or handle-less; ${kept}`, error instanceof OrcaError ? error.raw : env.raw);
|
|
1347
1526
|
}
|
|
1348
1527
|
const launchingTabId = terminalTabId(listing, launchingHandle);
|
|
@@ -1350,6 +1529,7 @@ export class OrcaDriver {
|
|
|
1350
1529
|
|| child.tabId !== launchingTabId
|
|
1351
1530
|
|| child.handle === launchingHandle
|
|
1352
1531
|
|| terminalTabId(listing, child.handle) !== launchingTabId) {
|
|
1532
|
+
await tombstone();
|
|
1353
1533
|
throw new OrcaError("split", `Orca narrator placement failed for ${name}: split receipt is unknown, malformed or handle-less; ${kept}`, env.raw);
|
|
1354
1534
|
}
|
|
1355
1535
|
const childHandle = child.handle;
|
|
@@ -1370,7 +1550,9 @@ export class OrcaDriver {
|
|
|
1370
1550
|
await this.time.sleep(20);
|
|
1371
1551
|
}
|
|
1372
1552
|
if (!claim) {
|
|
1373
|
-
// The receipt's handle may be closed
|
|
1553
|
+
// The receipt's handle may be closed; the reservation is tombstoned naming that pane so the
|
|
1554
|
+
// next narrator can prove it gone (or close it) and split afresh.
|
|
1555
|
+
await tombstone({ pane: childHandle, runtimeId: env.runtimeId });
|
|
1374
1556
|
let pane;
|
|
1375
1557
|
try {
|
|
1376
1558
|
await this.closeRecordedPane(cwd, name, childHandle, env.runtimeId);
|
|
@@ -1381,7 +1563,7 @@ export class OrcaDriver {
|
|
|
1381
1563
|
}
|
|
1382
1564
|
throw new OrcaError("split", `Orca narrator board unclaimed for ${name}: the observer never claimed the record (unclaimed board); ${kept}; ${pane}`, env.raw);
|
|
1383
1565
|
}
|
|
1384
|
-
casBoard("split", path, claim.raw, { ...claim.record, pane: childHandle, runtimeId: env.runtimeId });
|
|
1566
|
+
await casBoard("split", path, claim.raw, { ...claim.record, pane: childHandle, runtimeId: env.runtimeId }, this.time);
|
|
1385
1567
|
return watchSlot(cwd, name, childHandle);
|
|
1386
1568
|
});
|
|
1387
1569
|
}
|
|
@@ -1401,7 +1583,7 @@ export class OrcaDriver {
|
|
|
1401
1583
|
}
|
|
1402
1584
|
/** bound → retired, decided from the record alone: it must be this driver's board for exactly this
|
|
1403
1585
|
* slot's pane. Already retired is returned as it is. */
|
|
1404
|
-
retireBoard(family, slot) {
|
|
1586
|
+
async retireBoard(family, slot) {
|
|
1405
1587
|
const runId = parseOwnedName(slot.name)?.runId;
|
|
1406
1588
|
const path = runId ? this.boardPath(slot.cwd, runId) : undefined;
|
|
1407
1589
|
const current = path && runId ? readBoard(path, slot.cwd, runId) : undefined;
|
|
@@ -1412,7 +1594,7 @@ export class OrcaDriver {
|
|
|
1412
1594
|
if (r.retired)
|
|
1413
1595
|
return r;
|
|
1414
1596
|
const retired = { ...r, retired: true };
|
|
1415
|
-
casBoard(family, path, current.raw, retired);
|
|
1597
|
+
await casBoard(family, path, current.raw, retired, this.time);
|
|
1416
1598
|
return retired;
|
|
1417
1599
|
}
|
|
1418
1600
|
/** bound → retired first: whatever fails below, no later call answers this board again. A live
|
|
@@ -1421,7 +1603,7 @@ export class OrcaDriver {
|
|
|
1421
1603
|
* so it is only asked. */
|
|
1422
1604
|
async retireAndClose(slot) {
|
|
1423
1605
|
await this.serial(async () => {
|
|
1424
|
-
const retired = this.retireBoard("close", slot);
|
|
1606
|
+
const retired = await this.retireBoard("close", slot);
|
|
1425
1607
|
if (typeof retired.pid === "number" && pidLive(retired.pid))
|
|
1426
1608
|
await stopWatchBoard(retired, this.time);
|
|
1427
1609
|
else
|
package/dist/gates/baseline.d.ts
CHANGED
|
@@ -31,8 +31,12 @@ export interface BaselineCommand {
|
|
|
31
31
|
durationMs?: number;
|
|
32
32
|
/** Sum of the per-file durations named by the runner; null when its output names none. */
|
|
33
33
|
fileDurationSumMs?: number | null;
|
|
34
|
-
/** Total files
|
|
34
|
+
/** Total files the runner would collect; null when unavailable. */
|
|
35
35
|
fileCount?: number | null;
|
|
36
|
+
/** OBS-1044: `manifest` when `fileCount` came from the runner's own listing (test-manifest.ts's
|
|
37
|
+
* discovery seam) rather than a stdout summary sum. Absent on every pre-2.5.6 entry and on every
|
|
38
|
+
* scripted non-runner command, whose summary count keeps the stdout deficit rule byte-identically. */
|
|
39
|
+
fileCountSource?: "manifest";
|
|
36
40
|
/** fileDurationSumMs / durationMs — average implied file concurrency, not a configured fork count. */
|
|
37
41
|
impliedParallelism?: number | null;
|
|
38
42
|
/** The slowest per-file entry named by the runner; null when per-file timing is unavailable. */
|
|
@@ -150,6 +154,9 @@ export declare function ceilingKillResult(gate: string, r: ShResult, ceilingMs:
|
|
|
150
154
|
* shell asks it to finish faster than the thing it is measuring.
|
|
151
155
|
*/
|
|
152
156
|
export declare const CAPTURE_CEILING_MS = 1800000;
|
|
157
|
+
/** OBS-1044: a cached vitest entry whose count is a stdout sum may be inflated; only a manifest-derived
|
|
158
|
+
* count is safe to apply as a deficit floor. A null count compares nothing and is safe as-is. */
|
|
159
|
+
export declare function staleFileCountCommands(baseline: Baseline, commands: Record<string, string>, cwd: string): string[];
|
|
153
160
|
export declare function captureBaseline(cwd: string, commands: Record<string, string>): Promise<Baseline>;
|
|
154
161
|
export interface VacuousOracleWarning {
|
|
155
162
|
kind: "vacuous-oracle";
|
|
@@ -161,7 +168,10 @@ export declare function detectVacuousOracles(cwd: string, tasks: ReadonlyArray<{
|
|
|
161
168
|
id: string;
|
|
162
169
|
acceptance: AcceptanceItem[];
|
|
163
170
|
}>): Promise<VacuousOracleWarning[]>;
|
|
164
|
-
export
|
|
171
|
+
export interface RetryOptions {
|
|
172
|
+
authorizeRetry?: (cause: "infra" | "host-starved") => boolean;
|
|
173
|
+
}
|
|
174
|
+
export declare function compareToBaseline(cwd: string, commands: Record<string, string>, baseline: Baseline, enabled: string[], opts?: RetryOptions & {
|
|
165
175
|
rerunOf?: HostStarvedRerun;
|
|
166
176
|
infraRerun?: HostStarvedRerun;
|
|
167
177
|
selected?: readonly string[];
|
|
@@ -185,7 +195,8 @@ interface CalmWindow {
|
|
|
185
195
|
}
|
|
186
196
|
export declare function setCalmWindowForTests(over: Partial<CalmWindow>): void;
|
|
187
197
|
export declare function resetCalmWindowForTests(): void;
|
|
188
|
-
export declare function
|
|
198
|
+
export declare function calmWindowReady(): boolean;
|
|
199
|
+
export declare function waitForCalmWindow(signal?: AbortSignal): Promise<number>;
|
|
189
200
|
/** Summary totals survive digit-normalized fingerprints and exclude test-owned echoed output. */
|
|
190
201
|
export declare function runnerFileCount(raw: string): number | null;
|
|
191
202
|
export declare function fileCountDeficit(entry: BaselineCommand | undefined, raw: string, opts?: {
|