tickmarkr 2.1.9 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/model-lints.js +9 -0
- package/dist/adapters/pi.d.ts +1 -0
- package/dist/adapters/pi.js +15 -1
- package/dist/adapters/prompt.js +11 -3
- package/dist/adapters/registry.js +13 -4
- package/dist/adapters/types.d.ts +3 -0
- package/dist/adapters/types.js +1 -0
- package/dist/cli/commands/approve.d.ts +20 -0
- package/dist/cli/commands/approve.js +127 -35
- package/dist/cli/commands/doctor.d.ts +1 -1
- package/dist/cli/commands/doctor.js +14 -4
- package/dist/cli/commands/plan.js +4 -1
- package/dist/cli/commands/verify.js +2 -0
- package/dist/cli/commands/version.d.ts +2 -1
- package/dist/cli/commands/version.js +25 -4
- package/dist/compile/collateral.js +149 -4
- package/dist/compile/native.js +2 -0
- package/dist/drivers/index.d.ts +1 -0
- package/dist/drivers/index.js +25 -10
- package/dist/drivers/orca.d.ts +19 -0
- package/dist/drivers/orca.js +45 -9
- package/dist/gates/baseline.d.ts +2 -0
- package/dist/gates/baseline.js +49 -14
- package/dist/gates/llm.d.ts +1 -1
- package/dist/gates/llm.js +41 -26
- package/dist/gates/review.js +2 -0
- package/dist/gates/run-gates.d.ts +6 -0
- package/dist/gates/run-gates.js +9 -1
- package/dist/run/consult.d.ts +7 -0
- package/dist/run/consult.js +16 -2
- package/dist/run/daemon.d.ts +12 -6
- package/dist/run/daemon.js +306 -24
- package/dist/run/journal.d.ts +16 -0
- package/dist/run/journal.js +51 -6
- package/dist/tui/cockpit/setup-cockpit.d.ts +8 -10
- package/dist/tui/cockpit/setup-cockpit.js +92 -53
- package/package.json +1 -1
- package/skills/tickmarkr-loop/SKILL.md +1 -0
- package/skills/tickmarkr-overseer/SKILL.md +17 -0
- package/skills/tickmarkr-overseer/scripts/watch-context.sh +2 -2
|
@@ -262,6 +262,148 @@ export function criterionReadsOnly(text, target) {
|
|
|
262
262
|
});
|
|
263
263
|
}
|
|
264
264
|
const ARCH_PAGES = ["docs/codebase/ARCHITECTURE.md", "docs/codebase/STRUCTURE.md"];
|
|
265
|
+
// files[] is deliberately small, but a brace range can still be broad. If a pattern exceeds
|
|
266
|
+
// this advisory budget, fail open instead of printing an unexpanded brace fragment as a directory.
|
|
267
|
+
const MAX_BRACE_EXPANSIONS = 256;
|
|
268
|
+
const RANGE_CHAR_START = 32;
|
|
269
|
+
const RANGE_CHAR_END = 126;
|
|
270
|
+
function isEscaped(input, index) {
|
|
271
|
+
let backslashes = 0;
|
|
272
|
+
for (let i = index - 1; i >= 0 && input[i] === "\\"; i--)
|
|
273
|
+
backslashes++;
|
|
274
|
+
return backslashes % 2 === 1;
|
|
275
|
+
}
|
|
276
|
+
function unescapeGlobLiteral(input) {
|
|
277
|
+
return input.replace(/\\(.)/g, "$1");
|
|
278
|
+
}
|
|
279
|
+
function rangeParts(body) {
|
|
280
|
+
const parts = [];
|
|
281
|
+
let depth = 0;
|
|
282
|
+
let start = 0;
|
|
283
|
+
let sawRange = false;
|
|
284
|
+
for (let i = 0; i < body.length - 1; i++) {
|
|
285
|
+
if (isEscaped(body, i))
|
|
286
|
+
continue;
|
|
287
|
+
const ch = body[i];
|
|
288
|
+
if (ch === "{") {
|
|
289
|
+
depth++;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (ch === "}") {
|
|
293
|
+
depth = Math.max(0, depth - 1);
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (ch !== "." || body[i + 1] !== "." || isEscaped(body, i + 1) || depth !== 0)
|
|
297
|
+
continue;
|
|
298
|
+
parts.push(body.slice(start, i));
|
|
299
|
+
i++;
|
|
300
|
+
start = i + 1;
|
|
301
|
+
sawRange = true;
|
|
302
|
+
}
|
|
303
|
+
if (!sawRange)
|
|
304
|
+
return undefined;
|
|
305
|
+
parts.push(body.slice(start));
|
|
306
|
+
return parts;
|
|
307
|
+
}
|
|
308
|
+
function rangeAlternatives(body) {
|
|
309
|
+
const parts = rangeParts(body);
|
|
310
|
+
if (!parts)
|
|
311
|
+
return undefined;
|
|
312
|
+
const source = `[${parts.map(unescapeGlobLiteral).sort().join("-")}]`;
|
|
313
|
+
let re;
|
|
314
|
+
try {
|
|
315
|
+
re = new RegExp(`^${source}$`);
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
320
|
+
const out = [];
|
|
321
|
+
for (let code = RANGE_CHAR_START; code <= RANGE_CHAR_END; code++) {
|
|
322
|
+
const ch = String.fromCharCode(code);
|
|
323
|
+
if (re.test(ch))
|
|
324
|
+
out.push(ch);
|
|
325
|
+
if (out.length > MAX_BRACE_EXPANSIONS)
|
|
326
|
+
return [];
|
|
327
|
+
}
|
|
328
|
+
return out;
|
|
329
|
+
}
|
|
330
|
+
function braceAlternatives(body) {
|
|
331
|
+
const alternatives = [];
|
|
332
|
+
let depth = 0;
|
|
333
|
+
let start = 0;
|
|
334
|
+
let hasComma = false;
|
|
335
|
+
for (let i = 0; i < body.length; i++) {
|
|
336
|
+
if (isEscaped(body, i))
|
|
337
|
+
continue;
|
|
338
|
+
const ch = body[i];
|
|
339
|
+
if (ch === "{") {
|
|
340
|
+
depth++;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (ch === "}") {
|
|
344
|
+
depth = Math.max(0, depth - 1);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (ch !== "," || depth !== 0)
|
|
348
|
+
continue;
|
|
349
|
+
alternatives.push(body.slice(start, i));
|
|
350
|
+
start = i + 1;
|
|
351
|
+
hasComma = true;
|
|
352
|
+
}
|
|
353
|
+
if (hasComma) {
|
|
354
|
+
alternatives.push(body.slice(start));
|
|
355
|
+
return alternatives;
|
|
356
|
+
}
|
|
357
|
+
return rangeAlternatives(body);
|
|
358
|
+
}
|
|
359
|
+
function expandableBrace(pattern) {
|
|
360
|
+
const opens = [];
|
|
361
|
+
let unsupported;
|
|
362
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
363
|
+
if (isEscaped(pattern, i))
|
|
364
|
+
continue;
|
|
365
|
+
if (pattern[i] === "{") {
|
|
366
|
+
opens.push(i);
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
if (pattern[i] !== "}" || !opens.length)
|
|
370
|
+
continue;
|
|
371
|
+
const start = opens.pop();
|
|
372
|
+
const alternatives = braceAlternatives(pattern.slice(start + 1, i));
|
|
373
|
+
if (alternatives === undefined)
|
|
374
|
+
continue;
|
|
375
|
+
if (alternatives.length === 0) {
|
|
376
|
+
unsupported ??= { start, end: i, alternatives };
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
return { start, end: i, alternatives };
|
|
380
|
+
}
|
|
381
|
+
return unsupported;
|
|
382
|
+
}
|
|
383
|
+
function expandFilesPattern(pattern) {
|
|
384
|
+
const pending = [pattern];
|
|
385
|
+
const expanded = new Set();
|
|
386
|
+
let visited = 0;
|
|
387
|
+
while (pending.length) {
|
|
388
|
+
const candidate = pending.pop();
|
|
389
|
+
if (++visited > MAX_BRACE_EXPANSIONS)
|
|
390
|
+
return [];
|
|
391
|
+
const brace = expandableBrace(candidate);
|
|
392
|
+
if (!brace) {
|
|
393
|
+
expanded.add(unescapeGlobLiteral(candidate));
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (brace.alternatives.length === 0)
|
|
397
|
+
continue;
|
|
398
|
+
if (pending.length + brace.alternatives.length > MAX_BRACE_EXPANSIONS)
|
|
399
|
+
return [];
|
|
400
|
+
for (const alternative of brace.alternatives) {
|
|
401
|
+
pending.push(candidate.slice(0, brace.start) + alternative + candidate.slice(brace.end + 1));
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const scoped = filesGlob(pattern);
|
|
405
|
+
return [...expanded].filter((candidate) => scoped(candidate));
|
|
406
|
+
}
|
|
265
407
|
function topLevelSrcDir(file) {
|
|
266
408
|
const parts = file.replace(/^\.\//, "").replace(/\/+$/, "").split("/");
|
|
267
409
|
if (parts[0] !== "src" || parts.length < 2 || !parts[1])
|
|
@@ -293,10 +435,13 @@ export function newDirectoryLints(tasks, repoRoot) {
|
|
|
293
435
|
const scoped = filesGlob(t.files.map((f) => f.replace(/^\.\//, "")));
|
|
294
436
|
const newDirs = new Set();
|
|
295
437
|
for (const f of t.files) {
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
438
|
+
const normalized = f.replace(/^\.\//, "");
|
|
439
|
+
for (const expanded of expandFilesPattern(normalized)) {
|
|
440
|
+
const dir = topLevelSrcDir(expanded);
|
|
441
|
+
if (!dir || existing.has(dir))
|
|
442
|
+
continue;
|
|
443
|
+
newDirs.add(`src/${dir}/`);
|
|
444
|
+
}
|
|
300
445
|
}
|
|
301
446
|
if (!newDirs.size)
|
|
302
447
|
continue;
|
package/dist/compile/native.js
CHANGED
|
@@ -724,6 +724,7 @@ acceptance is required on every task (a nested list of observable outcomes).
|
|
|
724
724
|
- command: <shell> (oracle: command — exit code)
|
|
725
725
|
- test: <name> (oracle: test — named test)
|
|
726
726
|
- judge: <rubric> (oracle: judge — LLM-judged, free text)
|
|
727
|
+
A judge criterion carries ONE claim; a semicolon-joined criterion warns — split its clauses.
|
|
727
728
|
- <plain text> (compat: compiles as judge oracle, warns)
|
|
728
729
|
|
|
729
730
|
HARD BOUNDS — these FAIL the compile, they do not warn:
|
|
@@ -839,6 +840,7 @@ acceptance is required on every task (a nested list of observable outcomes).
|
|
|
839
840
|
ORDERING AND OWNERSHIP:
|
|
840
841
|
- Every path has exactly ONE owning task. Two tasks writing one file must be ORDERED by deps, or the
|
|
841
842
|
loser's work is silently dropped when the integration tip advances.
|
|
843
|
+
- A task changing what the daemon DOES must own every surface that TELLS the operator what the daemon does.
|
|
842
844
|
- A file one task CREATES cannot be "context:" for another — only deps: carries it, and that extends to
|
|
843
845
|
the task that PRODUCES a value, not just the file's existence.
|
|
844
846
|
- Deleting or renaming a symbol is a cross-task contract. Sweep for consumers by symbol AND by what the
|
package/dist/drivers/index.d.ts
CHANGED
|
@@ -4,4 +4,5 @@ export declare const DRIVER_CHOICES: readonly ["auto", "herdr", "subprocess", "o
|
|
|
4
4
|
export type DriverChoice = (typeof DRIVER_CHOICES)[number];
|
|
5
5
|
/** Validate argv at the CLI boundary rather than casting an arbitrary string into a driver choice. */
|
|
6
6
|
export declare function parseDriverOverride(override?: string): DriverChoice | undefined;
|
|
7
|
+
export declare function driverEvidence(cfg: TickmarkrConfig, driver: ExecutorDriver, override?: string): string;
|
|
7
8
|
export declare function pickDriver(cfg: TickmarkrConfig, override?: string): ExecutorDriver;
|
package/dist/drivers/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { HerdrDriver } from "./herdr.js";
|
|
|
2
2
|
import { OrcaDriver } from "./orca.js";
|
|
3
3
|
import { SubprocessDriver } from "./subprocess.js";
|
|
4
4
|
export const DRIVER_CHOICES = ["auto", "herdr", "subprocess", "orca"];
|
|
5
|
+
const overrideByDriver = new WeakMap();
|
|
5
6
|
/** Validate argv at the CLI boundary rather than casting an arbitrary string into a driver choice. */
|
|
6
7
|
export function parseDriverOverride(override) {
|
|
7
8
|
if (override === undefined)
|
|
@@ -11,18 +12,32 @@ export function parseDriverOverride(override) {
|
|
|
11
12
|
return choice;
|
|
12
13
|
throw new Error(`usage: --driver must be one of ${DRIVER_CHOICES.join(" | ")} (got ${override})`);
|
|
13
14
|
}
|
|
15
|
+
export function driverEvidence(cfg, driver, override) {
|
|
16
|
+
const selectedOverride = override ?? overrideByDriver.get(driver);
|
|
17
|
+
const want = parseDriverOverride(selectedOverride) ?? cfg.driver;
|
|
18
|
+
if (selectedOverride !== undefined)
|
|
19
|
+
return `${driver.id} (--driver)`;
|
|
20
|
+
if (want !== "auto")
|
|
21
|
+
return `${driver.id} (config)`;
|
|
22
|
+
const herdrAvailable = process.env.HERDR_ENV === "1";
|
|
23
|
+
if (driver.id === (herdrAvailable ? "herdr" : "subprocess")) {
|
|
24
|
+
return `auto → ${driver.id} (${herdrAvailable ? "HERDR_ENV=1" : "HERDR_ENV unset"})`;
|
|
25
|
+
}
|
|
26
|
+
return `auto → ${driver.id} (runtime)`;
|
|
27
|
+
}
|
|
14
28
|
export function pickDriver(cfg, override) {
|
|
15
|
-
const
|
|
29
|
+
const selectedOverride = parseDriverOverride(override);
|
|
30
|
+
const want = selectedOverride ?? cfg.driver;
|
|
16
31
|
// VIS-09 item 2: plumb the per-tab cap into the HerdrDriver — the driver takes it as a constructor
|
|
17
32
|
// param and never imports config (cfg is the only seam). Guaranteed present: DEFAULT_CONFIG seeds
|
|
18
33
|
// workersPerTab:3 and deepMerge overlays on top, so a missing overlay key still resolves.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
return
|
|
34
|
+
const driver = want === "herdr" ? new HerdrDriver("herdr", cfg.visibility.workersPerTab)
|
|
35
|
+
: want === "subprocess" ? new SubprocessDriver()
|
|
36
|
+
// Orca is an operator-selected execution surface. Its runtime failure stays on Orca; selection
|
|
37
|
+
// must never substitute a hidden subprocess worker after this explicit choice.
|
|
38
|
+
: want === "orca" ? new OrcaDriver()
|
|
39
|
+
: HerdrDriver.available() ? new HerdrDriver("herdr", cfg.visibility.workersPerTab) : new SubprocessDriver();
|
|
40
|
+
if (selectedOverride !== undefined)
|
|
41
|
+
overrideByDriver.set(driver, selectedOverride);
|
|
42
|
+
return driver;
|
|
28
43
|
}
|
package/dist/drivers/orca.d.ts
CHANGED
|
@@ -3,7 +3,12 @@ import { type ExecutorDriver, type NotifyOpts, type Slot, type SlotOpts } from "
|
|
|
3
3
|
/** The response families the ONE shared envelope parser serves. There is no second JSON seam. */
|
|
4
4
|
export declare const ORCA_RESPONSE_FAMILIES: readonly ["status", "create", "list", "read", "send", "wait", "show", "close"];
|
|
5
5
|
export type OrcaFamily = (typeof ORCA_RESPONSE_FAMILIES)[number];
|
|
6
|
+
export declare const ORCA_FIXTURE_VERSION = "1.4.195";
|
|
7
|
+
export declare const ORCA_CLI_COMMAND_ENV = "ORCA_CLI_COMMAND";
|
|
6
8
|
export declare const STALE_HANDLE_CODE = "terminal_handle_stale";
|
|
9
|
+
export declare const TERMINAL_GONE_CODE = "terminal_gone";
|
|
10
|
+
export declare const STALE_HANDLE_CODES: Set<string>;
|
|
11
|
+
export declare const WAIT_TIMEOUT_CODE = "timeout";
|
|
7
12
|
export declare const NOT_WRITABLE_CODE = "terminal_not_writable";
|
|
8
13
|
/** The ONLY terminal status that licenses reading a terminal's bytes or its agent state. */
|
|
9
14
|
export declare const RUNNING_STATUS = "running";
|
|
@@ -38,6 +43,18 @@ export interface OrcaEnvelope {
|
|
|
38
43
|
runtimeId: string;
|
|
39
44
|
raw: string;
|
|
40
45
|
}
|
|
46
|
+
export interface OrcaBinaryResolverOpts {
|
|
47
|
+
env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
|
|
48
|
+
platform?: NodeJS.Platform;
|
|
49
|
+
resolve?: (bin: string, cwd: string) => {
|
|
50
|
+
resolved?: string;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* One Orca CLI name decision for both the driver and doctor. Linux desktop systems can have
|
|
55
|
+
* GNOME's screen-reader `orca` on PATH, so outside an Orca terminal the app CLI is `orca-ide`.
|
|
56
|
+
*/
|
|
57
|
+
export declare function resolveOrcaCliBinary(cwd?: string, opts?: OrcaBinaryResolverOpts): string | undefined;
|
|
41
58
|
/**
|
|
42
59
|
* The one JSON seam. Fails CLOSED on every degenerate response — empty, unparseable (a truncated
|
|
43
60
|
* body lands here), non-object, no boolean `ok`, `ok:false`, `ok:true` with no result object, or a
|
|
@@ -76,6 +93,8 @@ export declare function mapAgentState(term: Record<string, unknown>, tuiIdle: bo
|
|
|
76
93
|
export declare function joinWrapped(raw: string): string;
|
|
77
94
|
export interface OrcaDriverOpts {
|
|
78
95
|
bin?: string;
|
|
96
|
+
env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
|
|
97
|
+
platform?: NodeJS.Platform;
|
|
79
98
|
exec?: OrcaExec;
|
|
80
99
|
time?: OrcaTimeSource;
|
|
81
100
|
pageLines?: number;
|
package/dist/drivers/orca.js
CHANGED
|
@@ -8,7 +8,7 @@ import { formatOwnedName, panesToClose } from "./types.js";
|
|
|
8
8
|
// worktrees, routing, gates, journal and merges; orca supplies visible terminals only. Everything
|
|
9
9
|
// here is bound by the 1.4.186 conformance spike
|
|
10
10
|
// (.planning/assessments/2026-08-21-orca-driver-conformance.md, CONFORMANCE-END) and the
|
|
11
|
-
//
|
|
11
|
+
// 1.4.195 drift capture (.planning/assessments/2026-09-02-orca-1.4.195-capture/):
|
|
12
12
|
//
|
|
13
13
|
// - reads arrive as `result.terminal.tail` line arrays with line-indexed cursors and a `status`
|
|
14
14
|
// field on the same object (C2); a CLOSED terminal answers ok:true with its own dead record
|
|
@@ -32,7 +32,12 @@ import { formatOwnedName, panesToClose } from "./types.js";
|
|
|
32
32
|
// an older run's leftover sits in a checkout this run never knew (T2).
|
|
33
33
|
/** The response families the ONE shared envelope parser serves. There is no second JSON seam. */
|
|
34
34
|
export const ORCA_RESPONSE_FAMILIES = ["status", "create", "list", "read", "send", "wait", "show", "close"];
|
|
35
|
+
export const ORCA_FIXTURE_VERSION = "1.4.195";
|
|
36
|
+
export const ORCA_CLI_COMMAND_ENV = "ORCA_CLI_COMMAND";
|
|
35
37
|
export const STALE_HANDLE_CODE = "terminal_handle_stale";
|
|
38
|
+
export const TERMINAL_GONE_CODE = "terminal_gone";
|
|
39
|
+
export const STALE_HANDLE_CODES = new Set([STALE_HANDLE_CODE, TERMINAL_GONE_CODE]);
|
|
40
|
+
export const WAIT_TIMEOUT_CODE = "timeout";
|
|
36
41
|
export const NOT_WRITABLE_CODE = "terminal_not_writable";
|
|
37
42
|
/** The ONLY terminal status that licenses reading a terminal's bytes or its agent state. */
|
|
38
43
|
export const RUNNING_STATUS = "running";
|
|
@@ -79,6 +84,19 @@ export class OrcaUnavailableError extends OrcaError {
|
|
|
79
84
|
function str(v) {
|
|
80
85
|
return typeof v === "string" && v ? v : undefined;
|
|
81
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* One Orca CLI name decision for both the driver and doctor. Linux desktop systems can have
|
|
89
|
+
* GNOME's screen-reader `orca` on PATH, so outside an Orca terminal the app CLI is `orca-ide`.
|
|
90
|
+
*/
|
|
91
|
+
export function resolveOrcaCliBinary(cwd = process.cwd(), opts = {}) {
|
|
92
|
+
const env = opts.env ?? process.env;
|
|
93
|
+
const explicit = env[ORCA_CLI_COMMAND_ENV]?.trim();
|
|
94
|
+
if (explicit)
|
|
95
|
+
return explicit;
|
|
96
|
+
const platform = opts.platform ?? process.platform;
|
|
97
|
+
const selected = platform === "linux" && env.TERM_PROGRAM !== "Orca" ? "orca-ide" : "orca";
|
|
98
|
+
return opts.resolve ? opts.resolve(selected, cwd).resolved : selected;
|
|
99
|
+
}
|
|
82
100
|
/**
|
|
83
101
|
* The one JSON seam. Fails CLOSED on every degenerate response — empty, unparseable (a truncated
|
|
84
102
|
* body lands here), non-object, no boolean `ok`, `ok:false`, `ok:true` with no result object, or a
|
|
@@ -235,7 +253,7 @@ export class OrcaDriver {
|
|
|
235
253
|
pollMs;
|
|
236
254
|
probeStalenessMs;
|
|
237
255
|
constructor(opts = {}) {
|
|
238
|
-
this.bin = opts.bin ?? "orca";
|
|
256
|
+
this.bin = opts.bin ?? resolveOrcaCliBinary(process.cwd(), { env: opts.env, platform: opts.platform }) ?? "orca";
|
|
239
257
|
// Config values flow into a shell here: every argv element is quoted, always.
|
|
240
258
|
this.exec = opts.exec ?? ((args, cwd, timeoutMs) => {
|
|
241
259
|
return sh([this.bin, ...args].map(shq).join(" "), cwd, timeoutMs);
|
|
@@ -257,14 +275,13 @@ export class OrcaDriver {
|
|
|
257
275
|
// diagnostic is never discarded before the one shared parser reports it.
|
|
258
276
|
const raw = [r.stdout, r.stderr ? `STDERR: ${r.stderr}` : ""].filter(Boolean).join("\n");
|
|
259
277
|
if (r.code !== 0) {
|
|
260
|
-
// Recorded
|
|
278
|
+
// Recorded refusal transport: the process exits rc 1 with the STRUCTURED ok:false
|
|
261
279
|
// body on stdout. Parse it so the refusal CODE survives (terminal_handle_stale,
|
|
262
280
|
// terminal_not_writable) — everything downstream that recovers on a code depends on this
|
|
263
|
-
// branch.
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
// transport failure (a shell/runtime crash can leave stale stdout behind).
|
|
281
|
+
// branch. Elapsed `terminal wait` has two recorded transports: 1.4.186's ok:true,
|
|
282
|
+
// `wait.satisfied:false` receipt and 1.4.195's ok:false/code:timeout refusal. Each remains
|
|
283
|
+
// scoped to wait only; any other ok:true body on a nonzero exit stays a transport failure (a
|
|
284
|
+
// shell/runtime crash can leave stale stdout behind).
|
|
268
285
|
try {
|
|
269
286
|
const env = parseEnvelope(family, r.stdout, raw);
|
|
270
287
|
const wait = env.result.wait;
|
|
@@ -279,6 +296,21 @@ export class OrcaDriver {
|
|
|
279
296
|
}
|
|
280
297
|
}
|
|
281
298
|
catch (e) {
|
|
299
|
+
if (family === "wait"
|
|
300
|
+
&& r.code === 1
|
|
301
|
+
&& !r.timedOut
|
|
302
|
+
&& e instanceof OrcaError
|
|
303
|
+
&& e.code === WAIT_TIMEOUT_CODE
|
|
304
|
+
&& e.runtimeId
|
|
305
|
+
&& e.runtimeId !== "none") {
|
|
306
|
+
const handle = args[args.indexOf("--terminal") + 1];
|
|
307
|
+
const condition = args[args.indexOf("--for") + 1];
|
|
308
|
+
return {
|
|
309
|
+
result: { wait: { handle, condition, satisfied: false, status: RUNNING_STATUS } },
|
|
310
|
+
runtimeId: e.runtimeId,
|
|
311
|
+
raw,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
282
314
|
// The refusal code lives in stdout alone, but the raw bytes propagated to the caller must
|
|
283
315
|
// still be the COMBINED stream — stderr can carry the diagnostic that explains the refusal.
|
|
284
316
|
if (e instanceof OrcaError && !(e instanceof OrcaUnavailableError) && e.code !== undefined) {
|
|
@@ -392,6 +424,10 @@ export class OrcaDriver {
|
|
|
392
424
|
st.handle = handle;
|
|
393
425
|
// The handle is bound to the runtime identity that ANSWERED its create.
|
|
394
426
|
st.runtimeId = env.runtimeId;
|
|
427
|
+
const surface = str(term.surface);
|
|
428
|
+
if (surface !== undefined && surface !== "visible") {
|
|
429
|
+
await this.notify(`tickmarkr orca terminal created on ${surface} surface`, { tier: "attention" });
|
|
430
|
+
}
|
|
395
431
|
}
|
|
396
432
|
// ---- handle identity and restart recovery ----------------------------------------------------
|
|
397
433
|
/**
|
|
@@ -450,7 +486,7 @@ export class OrcaDriver {
|
|
|
450
486
|
await relist(e.runtimeId, e.raw);
|
|
451
487
|
continue;
|
|
452
488
|
}
|
|
453
|
-
if (e.code
|
|
489
|
+
if (STALE_HANDLE_CODES.has(e.code)) {
|
|
454
490
|
await relist(e.runtimeId, e.raw);
|
|
455
491
|
continue;
|
|
456
492
|
}
|
package/dist/gates/baseline.d.ts
CHANGED
|
@@ -45,6 +45,8 @@ export interface BaselineCommand {
|
|
|
45
45
|
* verdict: no exit code, no fingerprints, nothing forgivable.
|
|
46
46
|
*/
|
|
47
47
|
infra?: true;
|
|
48
|
+
/** Runner-level exhaustion lines that invalidated a completed capture. */
|
|
49
|
+
invalidatingLines?: string[];
|
|
48
50
|
}
|
|
49
51
|
export interface BaselineFileDuration {
|
|
50
52
|
file: string;
|
package/dist/gates/baseline.js
CHANGED
|
@@ -85,7 +85,7 @@ const TURBO_FAIL_RE = /^\s*(?:[\w@./-]+:\s*)*ELIFECYCLE\s+Command failed\b|^\s*F
|
|
|
85
85
|
// at least TWO colon-joined segments (`<pkg>:<task>:`, tasks may nest — `pkg:test:unit:`), every
|
|
86
86
|
// segment after the first starting with a letter — so `Error: boom` (one segment), `src/x.ts:12:`
|
|
87
87
|
// (digit segment) and `12:34 error` (eslint stylish) are never stripped.
|
|
88
|
-
const TURBO_PREFIX_RE = /^\s*[\w@./-]+(?::[A-Za-z_][\w.-]*)
|
|
88
|
+
const TURBO_PREFIX_RE = /^\s*[\w@./-]+(?::[A-Za-z_][\w.-]*)+:(?:\s+|$)/;
|
|
89
89
|
const stripTurboPrefix = (l) => {
|
|
90
90
|
const m = TURBO_PREFIX_RE.exec(l);
|
|
91
91
|
return m ? l.slice(m[0].length) : undefined;
|
|
@@ -161,11 +161,36 @@ export function classifyFailureOutput(output) {
|
|
|
161
161
|
* no failure in that incomplete environment can safely become pre-existing forgiveness. Keep this
|
|
162
162
|
* separate from `classifyFailureOutput` so changing capture policy cannot move gate verdicts.
|
|
163
163
|
*/
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
164
|
+
const VITEST_ECHO_BLOCK_RE = /^\s*std(?:out|err)\s+\|\s+\S+\.(?:test|spec)\.[cm]?[jt]sx?\s+>\s+\S/;
|
|
165
|
+
/** Keep runner output while omitting Vitest's echoed test-owned stdout/stderr diagnostic blocks. */
|
|
166
|
+
const withoutVitestEchoBlocks = (output) => {
|
|
167
|
+
const outside = [];
|
|
168
|
+
let inVitestEchoBlock = false;
|
|
169
|
+
for (const line of output.split("\n")) {
|
|
170
|
+
const clean = line.replace(ANSI_RE, "");
|
|
171
|
+
const runnerLine = stripTurboPrefix(clean) ?? clean;
|
|
172
|
+
if (inVitestEchoBlock) {
|
|
173
|
+
if (runnerLine.trim() === "")
|
|
174
|
+
inVitestEchoBlock = false;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (VITEST_ECHO_BLOCK_RE.test(runnerLine)) {
|
|
178
|
+
inVitestEchoBlock = true;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
outside.push(line);
|
|
182
|
+
}
|
|
183
|
+
return outside;
|
|
184
|
+
};
|
|
185
|
+
const captureInvalidatingLines = (output) => {
|
|
186
|
+
const invalidating = [];
|
|
187
|
+
for (const line of withoutVitestEchoBlocks(output)) {
|
|
188
|
+
const clean = line.replace(ANSI_RE, "");
|
|
189
|
+
if (!PASS_LINE_RE.test(clean) && CAPTURE_EXHAUSTION_RE.test(clean))
|
|
190
|
+
invalidating.push(line);
|
|
191
|
+
}
|
|
192
|
+
return invalidating;
|
|
193
|
+
};
|
|
169
194
|
const normalizeLine = (l) => l.replace(/\d+/g, "#").replace(/\s+/g, " ").trim();
|
|
170
195
|
// Vitest's default reporter names file durations as
|
|
171
196
|
// `✓ |project| tests/example.test.ts (12 tests) 1.23s` (❯ for a red file). The runner may be
|
|
@@ -203,8 +228,7 @@ export const UNRECOGNIZED_FAILURE = "<unrecognized failure output>";
|
|
|
203
228
|
// renders "zone +3 · run attempt 2 · 1 failed" (what T9 is chartered to draw) contributes nothing, with
|
|
204
229
|
// or without box glyphs, so it cannot manufacture a fresh-fingerprint rejection on any future attempt.
|
|
205
230
|
export function fingerprint(output) {
|
|
206
|
-
const lines = output
|
|
207
|
-
.split("\n")
|
|
231
|
+
const lines = withoutVitestEchoBlocks(output)
|
|
208
232
|
.map((l) => l.replace(ANSI_RE, ""))
|
|
209
233
|
.filter((l) => !PASS_LINE_RE.test(l));
|
|
210
234
|
// GATE-FIX-4 DEFECT 4: every line is read twice — as printed, and with a turbo `<pkg>:<task>:`
|
|
@@ -403,7 +427,7 @@ export function ceilingKillResult(gate, r, ceilingMs) {
|
|
|
403
427
|
* shell asks it to finish faster than the thing it is measuring.
|
|
404
428
|
*/
|
|
405
429
|
export const CAPTURE_CEILING_MS = 1_800_000;
|
|
406
|
-
const invalidCaptureEntry = (durationMs) => ({
|
|
430
|
+
const invalidCaptureEntry = (durationMs, invalidatingLines = []) => ({
|
|
407
431
|
infra: true,
|
|
408
432
|
fingerprints: [],
|
|
409
433
|
durationMs,
|
|
@@ -411,6 +435,7 @@ const invalidCaptureEntry = (durationMs) => ({
|
|
|
411
435
|
impliedParallelism: null,
|
|
412
436
|
longestFile: null,
|
|
413
437
|
ceilingMs: effectiveCeilingMs({ durationMs }),
|
|
438
|
+
...(invalidatingLines.length ? { invalidatingLines } : {}),
|
|
414
439
|
});
|
|
415
440
|
export async function captureBaseline(cwd, commands) {
|
|
416
441
|
const base = { commands: {} };
|
|
@@ -441,17 +466,20 @@ export async function captureBaseline(cwd, commands) {
|
|
|
441
466
|
base.commands[name] = invalidCaptureEntry(durationMs);
|
|
442
467
|
continue;
|
|
443
468
|
}
|
|
444
|
-
const
|
|
469
|
+
const combinedOutput = r.stdout + "\n" + r.stderr;
|
|
470
|
+
const raw = combinedOutput.split(cwd).join("");
|
|
445
471
|
// Run 2137: the child exited and printed ordinary FAIL/AssertionError lines, so the gate-side
|
|
446
472
|
// discriminator correctly called the mixed output a regression. But the same output also said
|
|
447
473
|
// `spawn EAGAIN`: the machine had run out of processes while the pristine-tree measurement was
|
|
448
474
|
// being taken. A capture cannot know which red lines predated that shortage and which it caused,
|
|
449
475
|
// so none may become a fingerprint every later task gets to forgive.
|
|
450
|
-
|
|
476
|
+
const invalidatingLines = captureInvalidatingLines(combinedOutput);
|
|
477
|
+
if (invalidatingLines.length) {
|
|
451
478
|
console.error(`tickmarkr: baseline capture for "${name}" completed with process/resource-exhaustion evidence — `
|
|
452
479
|
+ `it recorded NO exit-code verdict and NO fingerprints, so nothing is forgiven for this command; `
|
|
453
|
-
+ `the measurement cannot distinguish a pre-existing failure from one caused by exhaustion
|
|
454
|
-
|
|
480
|
+
+ `the measurement cannot distinguish a pre-existing failure from one caused by exhaustion. `
|
|
481
|
+
+ `First invalidating line: ${invalidatingLines[0]}`);
|
|
482
|
+
base.commands[name] = invalidCaptureEntry(durationMs, invalidatingLines);
|
|
455
483
|
continue;
|
|
456
484
|
}
|
|
457
485
|
base.commands[name] = {
|
|
@@ -530,7 +558,14 @@ export async function compareToBaseline(cwd, commands, baseline, enabled) {
|
|
|
530
558
|
if (!cmd) {
|
|
531
559
|
// nothing detected for this gate in the target repo — journal an explicit skip instead of
|
|
532
560
|
// vanishing silently (a lint gate with no lint script rendered as forever-open in status)
|
|
533
|
-
|
|
561
|
+
const reason = `no ${name} command detected`;
|
|
562
|
+
const outcome = { kind: "skipped", reason };
|
|
563
|
+
results.push({
|
|
564
|
+
gate: name,
|
|
565
|
+
pass: true,
|
|
566
|
+
details: `${reason} — skipped`,
|
|
567
|
+
meta: { skipped: true, outcome },
|
|
568
|
+
});
|
|
534
569
|
continue;
|
|
535
570
|
}
|
|
536
571
|
const entry = baseline.commands[name];
|
package/dist/gates/llm.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type WorkerAdapter } from "../adapters/types.js";
|
|
2
2
|
import { type ExecutorDriver, type Slot } from "../drivers/types.js";
|
|
3
3
|
export declare const GATE_PANE_SEP = " \u00B7 ";
|
|
4
4
|
export declare const COMPLETION_FAKING_CHECKLIST = "## Completion-faking checklist\nHunt for these concrete completion-faking shortcuts before ruling on any criterion:\n- hardcoded-result: output or fixture hardcoded to satisfy the stated criterion instead of real logic\n- test-weakening: tests skipped, deleted, or assertions loosened until failing behavior looks green\n- vacuous-assertion: a test that cannot fail (asserts a constant, asserts its own setup, no assertion)\n- fixture-overfit: implementation narrowed to the exact test inputs rather than the described behavior\n- echo-not-implement: criterion text echoed in names, comments, or strings without the behavior itself\n- stub-left-behind: TODO, throw, or no-op stub where the real implementation should be\n- error-swallowing: catch or fallback that hides failures instead of handling them\n- self-mocking: the code under test mocked or faked so the test exercises the mock\n- check-bypass: lint, type, or CI checks disabled, relaxed, or excluded to get green\n- rename-as-work: code moved or renamed and presented as the requested change\n- scope-padding: unrelated edits padding the diff while the criterion's behavior is untouched\nWhen a criterion fails, the verdict MUST name which shortcut above it matches, or state that none does.";
|
package/dist/gates/llm.js
CHANGED
|
@@ -3,6 +3,7 @@ import { randomBytes } from "node:crypto";
|
|
|
3
3
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
+
import { matchesTrustDialog } from "../adapters/types.js";
|
|
6
7
|
import { formatOwnedName, parseOwnedName } from "../drivers/types.js";
|
|
7
8
|
import { bannerShell, paneDispatchCommand } from "../brand.js";
|
|
8
9
|
import { sh } from "../run/git.js";
|
|
@@ -161,6 +162,16 @@ export async function runViaDriver(adapter, model, prompt, cwd, via, timeoutMs =
|
|
|
161
162
|
slot = await via.driver.slot(cwd, rolePaneNameFromPrompt(prompt, via.name), via.label ? { label: via.label } : undefined);
|
|
162
163
|
via.onSlot?.(slot);
|
|
163
164
|
await via.driver.run(slot, paneDispatchCommand(scriptPath));
|
|
165
|
+
if (via.driver.sendKey) {
|
|
166
|
+
try {
|
|
167
|
+
if (matchesTrustDialog(await via.driver.read(slot, 400), adapter.trustDialog)) {
|
|
168
|
+
await via.driver.sendKey(slot, adapter.trustDialog.key);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
/* a failed pre-wait read must not replace the verdict wait */
|
|
173
|
+
}
|
|
174
|
+
}
|
|
164
175
|
// nonce-suffixed exit only: a displayed bare "TICKMARKR_EXIT:" or another call's marker must not
|
|
165
176
|
// false-complete — same guard the worker path uses (daemon.ts:330-331).
|
|
166
177
|
const exitPattern = `TICKMARKR_EXIT_${nonce}:\\d`;
|
|
@@ -369,37 +380,41 @@ export function extractVerdictJson(raw, nonce) {
|
|
|
369
380
|
/* fall through */
|
|
370
381
|
}
|
|
371
382
|
}
|
|
372
|
-
let
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
let
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
383
|
+
for (let start = raw.lastIndexOf("{"); start >= 0;) {
|
|
384
|
+
const nextStart = start === 0 ? -1 : raw.lastIndexOf("{", start - 1);
|
|
385
|
+
let depth = 0;
|
|
386
|
+
let quoted = false;
|
|
387
|
+
let escaped = false;
|
|
388
|
+
for (let i = start; i < raw.length; i++) {
|
|
389
|
+
const char = raw[i];
|
|
390
|
+
if (quoted) {
|
|
391
|
+
if (escaped)
|
|
392
|
+
escaped = false;
|
|
393
|
+
else if (char === "\\")
|
|
394
|
+
escaped = true;
|
|
395
|
+
else if (char === '"')
|
|
396
|
+
quoted = false;
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (char === '"')
|
|
400
|
+
quoted = true;
|
|
401
|
+
else if (char === "{")
|
|
381
402
|
depth++;
|
|
382
|
-
else if (
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
if (v && typeof v === "object" && v.nonce === nonce) {
|
|
389
|
-
const { nonce: _n, ...rest } = v;
|
|
390
|
-
return rest;
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
catch {
|
|
394
|
-
/* keep scanning */
|
|
403
|
+
else if (char === "}" && --depth === 0) {
|
|
404
|
+
try {
|
|
405
|
+
const v = JSON.parse(raw.slice(start, i + 1));
|
|
406
|
+
if (v && typeof v === "object" && v.nonce === nonce) {
|
|
407
|
+
const { nonce: _n, ...rest } = v;
|
|
408
|
+
return rest;
|
|
395
409
|
}
|
|
396
|
-
pos = i - 1;
|
|
397
|
-
break;
|
|
398
410
|
}
|
|
411
|
+
catch {
|
|
412
|
+
/* keep scanning */
|
|
413
|
+
}
|
|
414
|
+
break;
|
|
399
415
|
}
|
|
400
416
|
}
|
|
401
|
-
|
|
402
|
-
return null;
|
|
417
|
+
start = nextStart;
|
|
403
418
|
}
|
|
404
419
|
return null;
|
|
405
420
|
}
|
package/dist/gates/review.js
CHANGED
|
@@ -348,6 +348,7 @@ The top-level comments array is optional. Use it only for actionable line-anchor
|
|
|
348
348
|
// OBS-196: name the cause and persist the raw bytes — a ruled-on "unparseable" without its
|
|
349
349
|
// evidence cannot be audited, and a cutoff must never be indistinguishable from a parse defect.
|
|
350
350
|
const cause = classifyVerdictCause(raw, nonce, "approve");
|
|
351
|
+
const bytes = Buffer.byteLength(raw, "utf8");
|
|
351
352
|
let saved;
|
|
352
353
|
if (artifactDir) {
|
|
353
354
|
try {
|
|
@@ -372,6 +373,7 @@ The top-level comments array is optional. Use it only for actionable line-anchor
|
|
|
372
373
|
reviewer: channelKey(reviewer),
|
|
373
374
|
unparseable: true,
|
|
374
375
|
cause,
|
|
376
|
+
...(cause === "empty-output" ? { bytes } : {}),
|
|
375
377
|
...(concludedOnInactivity ? { classification: "infra", infra: true } : {}),
|
|
376
378
|
},
|
|
377
379
|
};
|
|
@@ -31,6 +31,12 @@ export type GateEvent = {
|
|
|
31
31
|
phase: "end";
|
|
32
32
|
gate: GateName;
|
|
33
33
|
result: GateResult;
|
|
34
|
+
} | {
|
|
35
|
+
phase: "note";
|
|
36
|
+
gate: GateName;
|
|
37
|
+
name: string;
|
|
38
|
+
payload: Record<string, unknown>;
|
|
39
|
+
result: GateResult;
|
|
34
40
|
};
|
|
35
41
|
export interface GateContext {
|
|
36
42
|
worktree: string;
|