tickmarkr 1.61.0 → 1.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/compile/native.js +71 -6
- package/dist/drivers/herdr.d.ts +1 -0
- package/dist/drivers/herdr.js +37 -3
- package/dist/run/daemon.js +16 -7
- package/package.json +1 -1
package/dist/compile/native.js
CHANGED
|
@@ -118,24 +118,89 @@ export function compileNative(file) {
|
|
|
118
118
|
throw new CompileError(`acceptance criteria are required on every task. Missing on: ${missing.join(", ")}.\n` +
|
|
119
119
|
`Add to each section in ${file}:\n- acceptance:\n - <observable outcome>`);
|
|
120
120
|
}
|
|
121
|
-
|
|
121
|
+
// v1.62 (OBS-97): commas inside {a,b} alternatives are part of one glob entry, not separators —
|
|
122
|
+
// split only at brace depth 0 so a brace glob reaches the lint (and the scope gate) intact.
|
|
123
|
+
const splitTop = (value) => {
|
|
124
|
+
const parts = [];
|
|
125
|
+
let depth = 0;
|
|
126
|
+
let current = "";
|
|
127
|
+
for (const ch of value) {
|
|
128
|
+
if (ch === "," && depth === 0) {
|
|
129
|
+
parts.push(current);
|
|
130
|
+
current = "";
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (ch === "{")
|
|
134
|
+
depth++;
|
|
135
|
+
else if (ch === "}" && depth > 0)
|
|
136
|
+
depth--;
|
|
137
|
+
current += ch;
|
|
138
|
+
}
|
|
139
|
+
return [...parts, current];
|
|
140
|
+
};
|
|
141
|
+
const csv = (value) => (value && value.toLowerCase() !== "none" ? splitTop(value).map((item) => item.trim()).filter(Boolean) : []);
|
|
122
142
|
// OBS-97: a typed test: oracle needs a collectable home. vitest only collects COLLECTABLE_TESTS
|
|
123
143
|
// paths, so a task whose non-empty files[] cannot host one makes scope-green and acceptance-green
|
|
124
144
|
// mutually exclusive by construction — run-20260719-210434 burned two dispatch attempts before a
|
|
125
145
|
// consult diagnosed exactly this. Empty files[] stays exempt: no file scope means unrestricted
|
|
126
146
|
// (src/gates/scope.ts). An entry hosts a collectable path iff its glob can produce one — probed by
|
|
127
|
-
// substituting each wildcard run with a test-shaped segment (also covers literal test-file paths)
|
|
147
|
+
// substituting each wildcard run with a test-shaped segment (also covers literal test-file paths);
|
|
148
|
+
// v1.62 extends the probe to {a,b} brace alternatives and ? single-character wildcards.
|
|
128
149
|
const collectable = picomatch(COLLECTABLE_TESTS, { dot: true });
|
|
129
150
|
// Single-token substitution is not a true glob-overlap test (tests/**/*.ts needs "probe.test",
|
|
130
151
|
// a bare ** needs the whole collectable path) — probe with several test-shaped tokens and accept
|
|
131
152
|
// if ANY candidate satisfies both globs. Scopes that truly cannot host one still fail every probe.
|
|
132
153
|
const PROBE_TOKENS = ["probe.test.ts", "probe.test", "tests/probe.test.ts"];
|
|
154
|
+
// v1.62: {a,b} alternatives expand to concrete branches before probing (innermost group first, so
|
|
155
|
+
// nesting resolves); an unbalanced brace is literal to picomatch and stays unexpanded. Expansion is
|
|
156
|
+
// BOUNDED: past BRANCH_CAP branches, further alternatives are dropped — dropping candidates can only
|
|
157
|
+
// reject, never accept (every accept needs a concrete witness), so the cap stays fail-closed while a
|
|
158
|
+
// pathological {a,b}{a,b}… entry can no longer balloon compile time.
|
|
159
|
+
const BRANCH_CAP = 64;
|
|
160
|
+
const expandBraces = (glob) => {
|
|
161
|
+
let branches = [glob];
|
|
162
|
+
for (;;) {
|
|
163
|
+
let changed = false;
|
|
164
|
+
const next = [];
|
|
165
|
+
for (const branch of branches) {
|
|
166
|
+
const inner = branch.match(/\{([^{}]*)\}/);
|
|
167
|
+
if (!inner) {
|
|
168
|
+
next.push(branch);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
changed = true;
|
|
172
|
+
for (const alt of inner[1].split(","))
|
|
173
|
+
next.push(branch.replace(inner[0], alt));
|
|
174
|
+
}
|
|
175
|
+
branches = next.slice(0, BRANCH_CAP);
|
|
176
|
+
if (!changed)
|
|
177
|
+
return branches;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
// v1.62: a ? never blocks self-match (it matches any one char), so hosting hinges on the probe
|
|
181
|
+
// clearing the collectable literals — search ? positions over that alphabet PER POSITION (a mixed
|
|
182
|
+
// scope like test?/unit.tes?.ts needs different chars at each ?). Every accepted probe is a concrete
|
|
183
|
+
// path satisfying BOTH globs (a witness), so expansion and substitution can only lift false
|
|
184
|
+
// rejections — a scope with no witness still fails every probe. Bounded: entries with more than
|
|
185
|
+
// QMARK_CAP ?s stay fail-closed (4^4 = 256 candidates is the ceiling per probe).
|
|
186
|
+
const QMARK_CHARS = ["t", "e", "s", "."];
|
|
187
|
+
const QMARK_CAP = 4;
|
|
188
|
+
const qmarkVariants = (probe) => {
|
|
189
|
+
const qCount = (probe.match(/\?/g) ?? []).length;
|
|
190
|
+
if (qCount === 0 || qCount > QMARK_CAP)
|
|
191
|
+
return [probe];
|
|
192
|
+
let variants = [probe];
|
|
193
|
+
for (let i = 0; i < qCount; i++) {
|
|
194
|
+
variants = variants.flatMap((v) => QMARK_CHARS.map((ch) => v.replace("?", ch)));
|
|
195
|
+
}
|
|
196
|
+
return [probe, ...variants];
|
|
197
|
+
};
|
|
133
198
|
const canHostTest = (entry) => {
|
|
134
199
|
const self = picomatch(entry, { dot: true });
|
|
135
|
-
return
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
200
|
+
return expandBraces(entry)
|
|
201
|
+
.flatMap((branch) => PROBE_TOKENS.map((token) => branch.replace(/\*+/g, token)))
|
|
202
|
+
.flatMap(qmarkVariants)
|
|
203
|
+
.some((probe) => self(probe) && collectable(probe));
|
|
139
204
|
};
|
|
140
205
|
const homeless = drafts
|
|
141
206
|
.filter((draft) => {
|
package/dist/drivers/herdr.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ExecutorDriver, type NotifyOpts, type Slot, type SlotOpts } from "./types.js";
|
|
2
2
|
export declare const TRAILER_SAFE_FLOOR_COLS = 108;
|
|
3
3
|
export declare const TRAILER_WIDTH_MARGIN = 2;
|
|
4
|
+
export declare const DELIVERY_ATTEMPTS = 3;
|
|
4
5
|
/** First-generation join direction from measured trailer-safe floor (43-MEASUREMENT.md). */
|
|
5
6
|
export declare function workerSplitDirection(paneCols: number | null, safeFloor?: number, margin?: number): "right" | "down";
|
|
6
7
|
export declare class HerdrDriver implements ExecutorDriver {
|
package/dist/drivers/herdr.js
CHANGED
|
@@ -6,6 +6,10 @@ import { canonicalizeLegacyName, formatOwnedName, panesToClose, parseOwnedName }
|
|
|
6
6
|
// VIS-09 P43-03: adopted safety floor from 43-MEASUREMENT.md (narrowest safe 53 → floor 108).
|
|
7
7
|
export const TRAILER_SAFE_FLOOR_COLS = 108;
|
|
8
8
|
export const TRAILER_WIDTH_MARGIN = 2; // cols below (floor + margin) refuse a rightward first split
|
|
9
|
+
// OBS-85 verified delivery: bounded type→read-back→enter attempts before failing closed.
|
|
10
|
+
export const DELIVERY_ATTEMPTS = 3;
|
|
11
|
+
const DELIVERY_VERIFY_TIMEOUT_MS = 2000; // per attempt — a paste that hasn't rendered in 2s is retyped
|
|
12
|
+
const DELIVERY_READ_LINES = 80;
|
|
9
13
|
/** First-generation join direction from measured trailer-safe floor (43-MEASUREMENT.md). */
|
|
10
14
|
export function workerSplitDirection(paneCols, safeFloor = TRAILER_SAFE_FLOOR_COLS, margin = TRAILER_WIDTH_MARGIN) {
|
|
11
15
|
if (paneCols == null || paneCols <= 0)
|
|
@@ -273,11 +277,41 @@ export class HerdrDriver {
|
|
|
273
277
|
await this.renameGroupTab(entry);
|
|
274
278
|
return { id: pane, name, cwd, tabId: entry.tabId, group };
|
|
275
279
|
}
|
|
280
|
+
// OBS-85 verified delivery: a pane paste can interleave a long dispatch line with itself (codex
|
|
281
|
+
// `$(git rev-parse…)` mashed into its trailing printf — v1.58 T2 attempts 2-4, v1.61 T10). Never
|
|
282
|
+
// the atomic `pane run` (text+Enter in one request, uninspectable between the two): type WITHOUT
|
|
283
|
+
// enter, read the pane back — `wait output --match` checks the same unwrapped transcript pane
|
|
284
|
+
// read exposes, event-driven so wrap and render timing can't race the check — and press Enter
|
|
285
|
+
// only when that read-back contains the typed command. A corrupted paste is captured (pane read),
|
|
286
|
+
// cleared (C-u), and retyped, bounded; persistent corruption fails closed WITH the captured
|
|
287
|
+
// transcript — the dispatch-time pincer the ledger asks for, not post-hoc `git:` archaeology.
|
|
276
288
|
async run(slot, cmd) {
|
|
277
289
|
const pane = await this.paneId(slot);
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
290
|
+
let transcript = "";
|
|
291
|
+
for (let attempt = 0; attempt < DELIVERY_ATTEMPTS; attempt++) {
|
|
292
|
+
if (attempt > 0) {
|
|
293
|
+
// clear the corrupted input line before retyping; a failed clear must NOT be retyped onto —
|
|
294
|
+
// corrupt-prefix + clean-retype would concatenate and false-verify by containment
|
|
295
|
+
const cleared = await this.herdr(`pane send-keys ${shq(pane)} C-u`, slot.cwd);
|
|
296
|
+
if (cleared.code !== 0)
|
|
297
|
+
throw new Error(`herdr delivery clear failed — refusing to retype onto a corrupted line (OBS-85); pane transcript:\n${transcript}`);
|
|
298
|
+
}
|
|
299
|
+
const typed = await this.herdr(`pane send-text ${shq(pane)} ${shq(cmd)}`, slot.cwd);
|
|
300
|
+
if (typed.code !== 0)
|
|
301
|
+
throw new Error(`herdr pane send-text failed: ${typed.stderr || typed.stdout}`);
|
|
302
|
+
const back = await this.herdr(`wait output ${shq(pane)} --match ${shq(cmd)} --timeout ${DELIVERY_VERIFY_TIMEOUT_MS}`, slot.cwd, DELIVERY_VERIFY_TIMEOUT_MS + 15_000);
|
|
303
|
+
// ponytail: transcript containment (scrollback included); anchor to the prompt line if a
|
|
304
|
+
// prior echo of the same command ever false-verifies. Dead pane → error envelope → unverified.
|
|
305
|
+
if (this.waitOk(back.code, back.stdout)) {
|
|
306
|
+
const enter = await this.herdr(`pane send-keys ${shq(pane)} Enter`, slot.cwd);
|
|
307
|
+
if (enter.code !== 0)
|
|
308
|
+
throw new Error(`herdr pane send-keys Enter failed: ${enter.stderr || enter.stdout}`);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
// capture the corrupted delivery BEFORE clearing it — the OBS-85 byte-level evidence
|
|
312
|
+
transcript = (await this.herdr(`pane read ${shq(pane)} --source recent-unwrapped --lines ${DELIVERY_READ_LINES}`, slot.cwd)).stdout;
|
|
313
|
+
}
|
|
314
|
+
throw new Error(`herdr delivery corrupted after ${DELIVERY_ATTEMPTS} attempts — enter never pressed (OBS-85); pane transcript:\n${transcript}`);
|
|
281
315
|
}
|
|
282
316
|
waitOk(code, stdout) {
|
|
283
317
|
if (code !== 0 || !stdout.trim())
|
package/dist/run/daemon.js
CHANGED
|
@@ -7,7 +7,7 @@ import { stringify } from "yaml";
|
|
|
7
7
|
import { trailerPattern, writePrompt } from "../adapters/prompt.js";
|
|
8
8
|
import { allAdapters, discoverChannels, getAdapter, probeAll, readDoctor } from "../adapters/registry.js";
|
|
9
9
|
import { addUsage, channelKey, matchesTrustDialog, QUOTA_RE } from "../adapters/types.js";
|
|
10
|
-
import { bannerShell } from "../brand.js";
|
|
10
|
+
import { bannerShell, paneDispatchCommand } from "../brand.js";
|
|
11
11
|
import { globalConfigDir, loadConfigWithMode, readOverlayFile, repoOverlayPath, } from "../config/config.js";
|
|
12
12
|
import { herdrSealShellPrefix, SubprocessDriver } from "../drivers/subprocess.js";
|
|
13
13
|
import { formatOwnedName } from "../drivers/types.js";
|
|
@@ -575,6 +575,19 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
575
575
|
journal.append("worker-mode-fallback", t.id, { reason: driver.interactive ? "adapter" : "driver" });
|
|
576
576
|
}
|
|
577
577
|
const interactive = icmd !== null;
|
|
578
|
+
// OBS-85 (v1.62 T1): both dispatch branches deliver ONE short script invocation — banner,
|
|
579
|
+
// adapter command, and nonce exit marker live in a per-attempt script beside the prompt
|
|
580
|
+
// artifact (the same paneDispatchCommand pattern judge/review/consult dispatches use). The
|
|
581
|
+
// delivered pane line carries no command substitution and no trailing shell text, so paste
|
|
582
|
+
// timing can never interleave a `$(…)` with what follows it (the codex corruption class).
|
|
583
|
+
const workerCmd = interactive ? icmd : adapter.invoke(t, wt, assignment, { promptFile }).command;
|
|
584
|
+
const dispatchScript = promptFile.replace(/\.md$/, ".sh");
|
|
585
|
+
writeFileSync(dispatchScript, [
|
|
586
|
+
"export BASH_SILENCE_DEPRECATION_WARNING=1",
|
|
587
|
+
bannerShell(),
|
|
588
|
+
workerCmd,
|
|
589
|
+
exitMarkerCmd,
|
|
590
|
+
].join("\n"));
|
|
578
591
|
// SPEND-01: this attempt's dispatch wall-clock — the usage collect cursor. Captured once here, the
|
|
579
592
|
// single site, so a test can reason about it; keep Date.now() out of profile.ts (still pure) and
|
|
580
593
|
// out of adapter module scope (the cursor is a parameter, threaded from the daemon).
|
|
@@ -617,9 +630,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
617
630
|
if (interactive) {
|
|
618
631
|
// v1.2 interactive: the TUI doesn't exit on completion — the trailer is the finish line.
|
|
619
632
|
// The exit wrapper still fires if the TUI dies (crash/quit): fast-fail instead of burning the timeout.
|
|
620
|
-
|
|
621
|
-
// judge/review/consult dispatch scripts; one printf one-liner, dispatch mechanics unchanged.
|
|
622
|
-
await driver.run(slot, `${bannerShell()}; ${icmd}; ${exitMarkerCmd}`);
|
|
633
|
+
await driver.run(slot, paneDispatchCommand(dispatchScript));
|
|
623
634
|
let paged = false;
|
|
624
635
|
// v1.22 T5 / OBS-19: auto-answer a fingerprint-matched trust dialog exactly once per slot.
|
|
625
636
|
// Any other blocked/idle dialog still pages the operator (paged latch below).
|
|
@@ -708,9 +719,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
708
719
|
}
|
|
709
720
|
}
|
|
710
721
|
else {
|
|
711
|
-
|
|
712
|
-
// T5: same brand header as the interactive path and the gate/consult dispatch scripts.
|
|
713
|
-
await driver.run(slot, `${bannerShell()}; ${inv.command}; ${exitMarkerCmd}`);
|
|
722
|
+
await driver.run(slot, paneDispatchCommand(dispatchScript));
|
|
714
723
|
// OBS-54: headless workers have the same output-inactivity budget as visible panes.
|
|
715
724
|
// OBS-82: same normalized-snapshot compare as the interactive site — spinner-only repaints
|
|
716
725
|
// exhaust the budget here too; harvest below still reads the raw pane.
|