tickmarkr 1.61.0 → 1.63.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/cli/commands/status.js +15 -2
- package/dist/compile/native.js +75 -6
- package/dist/drivers/herdr.d.ts +1 -0
- package/dist/drivers/herdr.js +37 -3
- package/dist/gates/baseline.d.ts +7 -0
- package/dist/gates/baseline.js +34 -1
- package/dist/run/daemon.js +43 -9
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +2 -1
- package/skills/tickmarkr-loop/SKILL.md +2 -1
- package/skills/tickmarkr-overseer/SKILL.md +1 -1
|
@@ -103,14 +103,27 @@ const daemonPid = (events) => {
|
|
|
103
103
|
}
|
|
104
104
|
return undefined;
|
|
105
105
|
};
|
|
106
|
+
const terminalFailureCause = (events) => {
|
|
107
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
108
|
+
const e = events[i];
|
|
109
|
+
if (e.event !== "run-end" || typeof e.data.error !== "string")
|
|
110
|
+
continue;
|
|
111
|
+
const phase = typeof e.data.phase === "string" && e.data.phase.trim()
|
|
112
|
+
? `${e.data.phase.trim()} failed`
|
|
113
|
+
: "run failed";
|
|
114
|
+
return `${phase}: ${e.data.error.replace(/\s+/g, " ").trim()}`;
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
117
|
+
};
|
|
106
118
|
const liveness = (events) => {
|
|
107
119
|
const last = events.at(-1);
|
|
108
120
|
if (!last)
|
|
109
121
|
return "last event unknown · daemon pid unknown";
|
|
110
122
|
const age = fmtAge(Date.now() - Date.parse(last.ts));
|
|
111
123
|
const pid = daemonPid(events);
|
|
124
|
+
const cause = terminalFailureCause(events);
|
|
112
125
|
if (pid === undefined)
|
|
113
|
-
return `last event ${age} ago · daemon pid unknown`;
|
|
126
|
+
return `last event ${age} ago · daemon pid unknown${cause ? ` · ${cause}` : ""}`;
|
|
114
127
|
// a dead pid after run-end is a clean exit, not a crash — "dead" is only alarming (red) while
|
|
115
128
|
// the run is still incomplete (operator's crash indicator)
|
|
116
129
|
const ended = events.some((e) => e.event === "run-end");
|
|
@@ -122,7 +135,7 @@ const liveness = (events) => {
|
|
|
122
135
|
catch (k) {
|
|
123
136
|
state = k.code === "ESRCH" ? (ended ? "finished" : "dead") : "alive";
|
|
124
137
|
} // EPERM ⇒ alive
|
|
125
|
-
return `last event ${age} ago · daemon pid ${pid} ${state}`;
|
|
138
|
+
return `last event ${age} ago · daemon pid ${pid} ${state}${cause ? ` · ${cause}` : ""}`;
|
|
126
139
|
};
|
|
127
140
|
const renderFrame = (cwd) => {
|
|
128
141
|
const g = loadGraph(cwd);
|
package/dist/compile/native.js
CHANGED
|
@@ -21,6 +21,10 @@ export function compileNative(file) {
|
|
|
21
21
|
if (!existsSync(file))
|
|
22
22
|
throw new CompileError(`no such native spec file: ${file}`);
|
|
23
23
|
const content = readFileSync(file, "utf8");
|
|
24
|
+
// Exact shared-template check: init writes specTemplate(), and any edit changes the body.
|
|
25
|
+
if (content === specTemplate()) {
|
|
26
|
+
throw new CompileError(`${file} is the unedited tickmarkr init scaffold. Edit ${file} before compiling.`);
|
|
27
|
+
}
|
|
24
28
|
const drafts = [];
|
|
25
29
|
let plainCount = 0; // v1.19: plain-string acceptance items compiled as judge oracles (compat) — warn once
|
|
26
30
|
let specMode;
|
|
@@ -118,24 +122,89 @@ export function compileNative(file) {
|
|
|
118
122
|
throw new CompileError(`acceptance criteria are required on every task. Missing on: ${missing.join(", ")}.\n` +
|
|
119
123
|
`Add to each section in ${file}:\n- acceptance:\n - <observable outcome>`);
|
|
120
124
|
}
|
|
121
|
-
|
|
125
|
+
// v1.62 (OBS-97): commas inside {a,b} alternatives are part of one glob entry, not separators —
|
|
126
|
+
// split only at brace depth 0 so a brace glob reaches the lint (and the scope gate) intact.
|
|
127
|
+
const splitTop = (value) => {
|
|
128
|
+
const parts = [];
|
|
129
|
+
let depth = 0;
|
|
130
|
+
let current = "";
|
|
131
|
+
for (const ch of value) {
|
|
132
|
+
if (ch === "," && depth === 0) {
|
|
133
|
+
parts.push(current);
|
|
134
|
+
current = "";
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (ch === "{")
|
|
138
|
+
depth++;
|
|
139
|
+
else if (ch === "}" && depth > 0)
|
|
140
|
+
depth--;
|
|
141
|
+
current += ch;
|
|
142
|
+
}
|
|
143
|
+
return [...parts, current];
|
|
144
|
+
};
|
|
145
|
+
const csv = (value) => (value && value.toLowerCase() !== "none" ? splitTop(value).map((item) => item.trim()).filter(Boolean) : []);
|
|
122
146
|
// OBS-97: a typed test: oracle needs a collectable home. vitest only collects COLLECTABLE_TESTS
|
|
123
147
|
// paths, so a task whose non-empty files[] cannot host one makes scope-green and acceptance-green
|
|
124
148
|
// mutually exclusive by construction — run-20260719-210434 burned two dispatch attempts before a
|
|
125
149
|
// consult diagnosed exactly this. Empty files[] stays exempt: no file scope means unrestricted
|
|
126
150
|
// (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)
|
|
151
|
+
// substituting each wildcard run with a test-shaped segment (also covers literal test-file paths);
|
|
152
|
+
// v1.62 extends the probe to {a,b} brace alternatives and ? single-character wildcards.
|
|
128
153
|
const collectable = picomatch(COLLECTABLE_TESTS, { dot: true });
|
|
129
154
|
// Single-token substitution is not a true glob-overlap test (tests/**/*.ts needs "probe.test",
|
|
130
155
|
// a bare ** needs the whole collectable path) — probe with several test-shaped tokens and accept
|
|
131
156
|
// if ANY candidate satisfies both globs. Scopes that truly cannot host one still fail every probe.
|
|
132
157
|
const PROBE_TOKENS = ["probe.test.ts", "probe.test", "tests/probe.test.ts"];
|
|
158
|
+
// v1.62: {a,b} alternatives expand to concrete branches before probing (innermost group first, so
|
|
159
|
+
// nesting resolves); an unbalanced brace is literal to picomatch and stays unexpanded. Expansion is
|
|
160
|
+
// BOUNDED: past BRANCH_CAP branches, further alternatives are dropped — dropping candidates can only
|
|
161
|
+
// reject, never accept (every accept needs a concrete witness), so the cap stays fail-closed while a
|
|
162
|
+
// pathological {a,b}{a,b}… entry can no longer balloon compile time.
|
|
163
|
+
const BRANCH_CAP = 64;
|
|
164
|
+
const expandBraces = (glob) => {
|
|
165
|
+
let branches = [glob];
|
|
166
|
+
for (;;) {
|
|
167
|
+
let changed = false;
|
|
168
|
+
const next = [];
|
|
169
|
+
for (const branch of branches) {
|
|
170
|
+
const inner = branch.match(/\{([^{}]*)\}/);
|
|
171
|
+
if (!inner) {
|
|
172
|
+
next.push(branch);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
changed = true;
|
|
176
|
+
for (const alt of inner[1].split(","))
|
|
177
|
+
next.push(branch.replace(inner[0], alt));
|
|
178
|
+
}
|
|
179
|
+
branches = next.slice(0, BRANCH_CAP);
|
|
180
|
+
if (!changed)
|
|
181
|
+
return branches;
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
// v1.62: a ? never blocks self-match (it matches any one char), so hosting hinges on the probe
|
|
185
|
+
// clearing the collectable literals — search ? positions over that alphabet PER POSITION (a mixed
|
|
186
|
+
// scope like test?/unit.tes?.ts needs different chars at each ?). Every accepted probe is a concrete
|
|
187
|
+
// path satisfying BOTH globs (a witness), so expansion and substitution can only lift false
|
|
188
|
+
// rejections — a scope with no witness still fails every probe. Bounded: entries with more than
|
|
189
|
+
// QMARK_CAP ?s stay fail-closed (4^4 = 256 candidates is the ceiling per probe).
|
|
190
|
+
const QMARK_CHARS = ["t", "e", "s", "."];
|
|
191
|
+
const QMARK_CAP = 4;
|
|
192
|
+
const qmarkVariants = (probe) => {
|
|
193
|
+
const qCount = (probe.match(/\?/g) ?? []).length;
|
|
194
|
+
if (qCount === 0 || qCount > QMARK_CAP)
|
|
195
|
+
return [probe];
|
|
196
|
+
let variants = [probe];
|
|
197
|
+
for (let i = 0; i < qCount; i++) {
|
|
198
|
+
variants = variants.flatMap((v) => QMARK_CHARS.map((ch) => v.replace("?", ch)));
|
|
199
|
+
}
|
|
200
|
+
return [probe, ...variants];
|
|
201
|
+
};
|
|
133
202
|
const canHostTest = (entry) => {
|
|
134
203
|
const self = picomatch(entry, { dot: true });
|
|
135
|
-
return
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
204
|
+
return expandBraces(entry)
|
|
205
|
+
.flatMap((branch) => PROBE_TOKENS.map((token) => branch.replace(/\*+/g, token)))
|
|
206
|
+
.flatMap(qmarkVariants)
|
|
207
|
+
.some((probe) => self(probe) && collectable(probe));
|
|
139
208
|
};
|
|
140
209
|
const homeless = drafts
|
|
141
210
|
.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/gates/baseline.d.ts
CHANGED
|
@@ -4,7 +4,14 @@ export interface Baseline {
|
|
|
4
4
|
commands: Record<string, {
|
|
5
5
|
exitCode: number;
|
|
6
6
|
fingerprints: string[];
|
|
7
|
+
missingCommand?: boolean;
|
|
7
8
|
}>;
|
|
9
|
+
warnings?: BaselineWarning[];
|
|
10
|
+
}
|
|
11
|
+
export interface BaselineWarning {
|
|
12
|
+
kind: "wrong-environment";
|
|
13
|
+
commands: string[];
|
|
14
|
+
reason: string;
|
|
8
15
|
}
|
|
9
16
|
export declare function fingerprint(output: string): string[];
|
|
10
17
|
export declare function detectGateCommands(repoRoot: string, cfg: TickmarkrConfig): Record<string, string>;
|
package/dist/gates/baseline.js
CHANGED
|
@@ -41,12 +41,45 @@ export function detectGateCommands(repoRoot, cfg) {
|
|
|
41
41
|
}
|
|
42
42
|
return out;
|
|
43
43
|
}
|
|
44
|
+
const shellToken = (cmd) => {
|
|
45
|
+
for (const raw of cmd.trim().split(/\s+/)) {
|
|
46
|
+
if (!raw || /^[A-Za-z_][A-Za-z0-9_]*=/.test(raw))
|
|
47
|
+
continue;
|
|
48
|
+
if (raw === "env")
|
|
49
|
+
continue;
|
|
50
|
+
return raw.replace(/^['"]|['"]$/g, "");
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
};
|
|
54
|
+
const reEscape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
55
|
+
function missingConfiguredCommand(cmd, result) {
|
|
56
|
+
if (result.code !== 127)
|
|
57
|
+
return false;
|
|
58
|
+
const token = shellToken(cmd);
|
|
59
|
+
if (!token)
|
|
60
|
+
return false;
|
|
61
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
62
|
+
return new RegExp(`(?:^|[:\\s])${reEscape(token)}:\\s+(?:command not found|No such file or directory)`, "i").test(output);
|
|
63
|
+
}
|
|
44
64
|
export async function captureBaseline(cwd, commands) {
|
|
45
65
|
const base = { commands: {} };
|
|
46
66
|
for (const [name, cmd] of Object.entries(commands)) {
|
|
47
67
|
const r = await sh(cmd, cwd);
|
|
48
68
|
// ponytail: strip the executing cwd so repo-root capture and worktree compare fingerprint identically; /private-vs-/tmp symlink variance is out of scope
|
|
49
|
-
base.commands[name] = {
|
|
69
|
+
base.commands[name] = {
|
|
70
|
+
exitCode: r.code,
|
|
71
|
+
fingerprints: fingerprint((r.stdout + "\n" + r.stderr).split(cwd).join("")),
|
|
72
|
+
missingCommand: missingConfiguredCommand(cmd, r),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const names = Object.keys(commands);
|
|
76
|
+
const missing = names.filter((name) => base.commands[name]?.missingCommand === true);
|
|
77
|
+
if (names.length > 0 && missing.length === names.length) {
|
|
78
|
+
base.warnings = [{
|
|
79
|
+
kind: "wrong-environment",
|
|
80
|
+
commands: missing,
|
|
81
|
+
reason: `wrong environment: every configured baseline command was missing (${missing.join(", ")})`,
|
|
82
|
+
}];
|
|
50
83
|
}
|
|
51
84
|
return base;
|
|
52
85
|
}
|
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";
|
|
@@ -109,6 +109,10 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
109
109
|
// v1.54 T2: declared before the try so the finally can always deregister (a throw before
|
|
110
110
|
// registration leaves it undefined — the guard below covers that path).
|
|
111
111
|
let onTermination;
|
|
112
|
+
let journal;
|
|
113
|
+
let runStarted = false;
|
|
114
|
+
let taskLoopStarted = false;
|
|
115
|
+
let branch = "";
|
|
112
116
|
try {
|
|
113
117
|
let graph = loadGraph(repoRoot);
|
|
114
118
|
// v1.51 T2: the routing mode resolves BEFORE any routing input is built — run flag > spec front-matter
|
|
@@ -184,12 +188,12 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
184
188
|
};
|
|
185
189
|
process.on("SIGINT", onTermination);
|
|
186
190
|
process.on("SIGTERM", onTermination);
|
|
187
|
-
|
|
191
|
+
journal = opts.resume ? Journal.open(repoRoot, runId, opts.narrate) : Journal.create(repoRoot, runId, opts.narrate);
|
|
188
192
|
const branchEvent = opts.resume
|
|
189
193
|
? [...journal.read()].reverse().find((e) => (e.event === "run-start" || e.event === "run-end" || e.event === "merge") && typeof e.data.branch === "string")
|
|
190
194
|
: undefined;
|
|
191
195
|
const recordedBranch = typeof branchEvent?.data.branch === "string" ? branchEvent.data.branch : undefined;
|
|
192
|
-
|
|
196
|
+
branch = recordedBranch
|
|
193
197
|
? branchEvent.event === "merge" ? recordedBranch.slice(0, recordedBranch.lastIndexOf("--")) : recordedBranch
|
|
194
198
|
: integrationBranch(cfg, runId);
|
|
195
199
|
if (lock.reclaimed)
|
|
@@ -249,9 +253,12 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
249
253
|
baseline = await captureBaseline(repoRoot, commands);
|
|
250
254
|
writeFileSync(join(journal.dir, "baseline.json"), JSON.stringify(baseline, null, 2));
|
|
251
255
|
journal.append("run-start", undefined, { pid: process.pid, baseRef, commands, channels: channels.map(channelKey), branch, graphDefinitionHash: graphDefinitionHash(graph), mode: rm.mode.mode, modeSource: rm.source, ...(prior ? { supersedes: prior.runId } : {}) }); // graphDefinitionHash: T3 engagement identity (status+resume share it); pid: v1.13 (VIS-11) liveness; mode/modeSource: v1.51 T2; supersedes: v1.53 T5
|
|
256
|
+
runStarted = true;
|
|
252
257
|
// v1.53 T5: mark the prior run AFTER this run's run-start exists, so the prior journal never
|
|
253
258
|
// names a successor that has no journal. Append-only — the prior journal is never rewritten.
|
|
254
259
|
prior?.append("superseded", undefined, { by: runId });
|
|
260
|
+
for (const warning of baseline.warnings ?? [])
|
|
261
|
+
journal.append("baseline-warning", undefined, { ...warning });
|
|
255
262
|
}
|
|
256
263
|
// T6: open the narrator AFTER run-start/run-resume is journaled so the watch surface has a run to
|
|
257
264
|
// show. driver.narrator is undefined on subprocess → no-op (subprocess spawns nothing). Swallowed:
|
|
@@ -575,6 +582,19 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
575
582
|
journal.append("worker-mode-fallback", t.id, { reason: driver.interactive ? "adapter" : "driver" });
|
|
576
583
|
}
|
|
577
584
|
const interactive = icmd !== null;
|
|
585
|
+
// OBS-85 (v1.62 T1): both dispatch branches deliver ONE short script invocation — banner,
|
|
586
|
+
// adapter command, and nonce exit marker live in a per-attempt script beside the prompt
|
|
587
|
+
// artifact (the same paneDispatchCommand pattern judge/review/consult dispatches use). The
|
|
588
|
+
// delivered pane line carries no command substitution and no trailing shell text, so paste
|
|
589
|
+
// timing can never interleave a `$(…)` with what follows it (the codex corruption class).
|
|
590
|
+
const workerCmd = interactive ? icmd : adapter.invoke(t, wt, assignment, { promptFile }).command;
|
|
591
|
+
const dispatchScript = promptFile.replace(/\.md$/, ".sh");
|
|
592
|
+
writeFileSync(dispatchScript, [
|
|
593
|
+
"export BASH_SILENCE_DEPRECATION_WARNING=1",
|
|
594
|
+
bannerShell(),
|
|
595
|
+
workerCmd,
|
|
596
|
+
exitMarkerCmd,
|
|
597
|
+
].join("\n"));
|
|
578
598
|
// SPEND-01: this attempt's dispatch wall-clock — the usage collect cursor. Captured once here, the
|
|
579
599
|
// single site, so a test can reason about it; keep Date.now() out of profile.ts (still pure) and
|
|
580
600
|
// out of adapter module scope (the cursor is a parameter, threaded from the daemon).
|
|
@@ -617,9 +637,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
617
637
|
if (interactive) {
|
|
618
638
|
// v1.2 interactive: the TUI doesn't exit on completion — the trailer is the finish line.
|
|
619
639
|
// 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}`);
|
|
640
|
+
await driver.run(slot, paneDispatchCommand(dispatchScript));
|
|
623
641
|
let paged = false;
|
|
624
642
|
// v1.22 T5 / OBS-19: auto-answer a fingerprint-matched trust dialog exactly once per slot.
|
|
625
643
|
// Any other blocked/idle dialog still pages the operator (paged latch below).
|
|
@@ -708,9 +726,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
708
726
|
}
|
|
709
727
|
}
|
|
710
728
|
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}`);
|
|
729
|
+
await driver.run(slot, paneDispatchCommand(dispatchScript));
|
|
714
730
|
// OBS-54: headless workers have the same output-inactivity budget as visible panes.
|
|
715
731
|
// OBS-82: same normalized-snapshot compare as the interactive site — spinner-only repaints
|
|
716
732
|
// exhaust the budget here too; harvest below still reads the raw pane.
|
|
@@ -970,6 +986,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
970
986
|
return;
|
|
971
987
|
}
|
|
972
988
|
};
|
|
989
|
+
taskLoopStarted = true;
|
|
973
990
|
const inflight = new Map();
|
|
974
991
|
while (true) {
|
|
975
992
|
// v1.54 T2: a signal that landed while nothing was racing `aborted` (empty inflight window)
|
|
@@ -1060,6 +1077,23 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1060
1077
|
await driver.notify(`tickmarkr ${runId}: ${summary.done.length} done, ${summary.failed.length} failed, ${summary.human.length} awaiting human, ${summary.blocked.length} blocked, ${summary.pending.length} pending${attribution ? ` (${attribution})` : ""}${tipFail} — integration branch ${branch} (merge to main is yours)`, { tier: summary.tipVerify === "failed" ? "attention" : "routine" });
|
|
1061
1078
|
return summary;
|
|
1062
1079
|
}
|
|
1080
|
+
catch (err) {
|
|
1081
|
+
if (runStarted && !taskLoopStarted && !journal.read().some((e) => e.event === "run-end")) {
|
|
1082
|
+
journal.append("run-end", undefined, {
|
|
1083
|
+
runId,
|
|
1084
|
+
branch,
|
|
1085
|
+
done: [],
|
|
1086
|
+
failed: [],
|
|
1087
|
+
human: [],
|
|
1088
|
+
blocked: [],
|
|
1089
|
+
pending: [],
|
|
1090
|
+
phase: "setup",
|
|
1091
|
+
fatal: true,
|
|
1092
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
throw err;
|
|
1096
|
+
}
|
|
1063
1097
|
finally {
|
|
1064
1098
|
// v1.54 T2: deregister on EVERY exit (normal run end, throw, termination unwind) — the daemon
|
|
1065
1099
|
// test suite runs runDaemon dozens of times in one process; a leaked handler would close a
|
package/package.json
CHANGED
|
@@ -16,7 +16,8 @@ When working in a multi-agent terminal environment, decide your role before star
|
|
|
16
16
|
- **Supervisor with a live orchestrator:** do not start a second run. Relay the mission to the existing orchestrator with a [verified handoff](#verified-handoffs-agent-to-agent-messaging), then supervise it as OVERSEER.
|
|
17
17
|
- **Primary session without an orchestrator:** rename your own tab `OVERSEER · <version>` and your agent `overseer`, spawn one child orchestration session with your host's launch form, label its tab `ORCH · <version>` and name its agent, give it the mission and these rules verbatim, then supervise it. Do not drive a duplicate single-tier run yourself. Before spawning, confirm any PREVIOUS orchestrator has stood down (monitors stopped, input box empty — dim ghost-text suggestions are UI, not queued input; ANSI-verify before alarming) and close its tab — the journal, records, and ledger hold the story; scrollback is disposable.
|
|
18
18
|
- **Claude Code:** `herdr agent start orchestrator --cwd <repo> --no-focus -- claude --permission-mode bypassPermissions`
|
|
19
|
-
- **Codex:** `herdr agent start orchestrator --cwd <repo> --no-focus -- codex --
|
|
19
|
+
- **Codex:** `herdr agent start orchestrator --cwd <repo> --no-focus -- codex --dangerously-bypass-approvals-and-sandbox` — the unsandboxed flag is REQUIRED, not optional: codex's `workspace-write` sandbox keeps `.git` refs read-only, so a sandboxed orchestrator's `tickmarkr run` dies at integration-branch creation (`git worktree add` cannot lock the ref). Do not downgrade this flag; the herdr pane and repo scope are the containment.
|
|
20
|
+
- **Auxiliary agents you spawn (consultants, reviewers, scouts) follow the same forms.** Never launch a claude session in plan mode or default permission mode for autonomous work — both stall on per-command approval prompts nobody is watching; claude is always `--permission-mode bypassPermissions` (tickmarkr's own adapter uses exactly this for workers, judges, and consults). A read-only codex consultant may use `--sandbox read-only`; any codex session that must touch git needs the unsandboxed flag above.
|
|
20
21
|
|
|
21
22
|
Outside a multi-agent terminal environment, run the loop directly.
|
|
22
23
|
|
|
@@ -15,7 +15,8 @@ When working in a multi-agent terminal environment, decide your role before star
|
|
|
15
15
|
- **Supervisor with a live orchestrator:** do not start a second run. Relay the mission to the existing orchestrator with a [verified handoff](#verified-handoffs-agent-to-agent-messaging), then supervise it as OVERSEER.
|
|
16
16
|
- **Primary session without an orchestrator:** rename your own tab `OVERSEER · <version>` and your agent `overseer`, spawn one child orchestration session with your host's launch form, label its tab `ORCH · <version>` and name its agent, give it the mission and these rules verbatim, then supervise it. Do not drive a duplicate single-tier run yourself. Before spawning, confirm any PREVIOUS orchestrator has [stood down](#stand-down-mission-end-and-retirement) and close its tab.
|
|
17
17
|
- **Claude Code:** `herdr agent start orchestrator --cwd <repo> --no-focus -- claude --permission-mode bypassPermissions`
|
|
18
|
-
- **Codex:** `herdr agent start orchestrator --cwd <repo> --no-focus -- codex --
|
|
18
|
+
- **Codex:** `herdr agent start orchestrator --cwd <repo> --no-focus -- codex --dangerously-bypass-approvals-and-sandbox` — the unsandboxed flag is REQUIRED, not optional: codex's `workspace-write` sandbox keeps `.git` refs read-only, so a sandboxed orchestrator's `tickmarkr run` dies at integration-branch creation (`git worktree add` cannot lock the ref). Do not downgrade this flag; the herdr pane and repo scope are the containment.
|
|
19
|
+
- **Auxiliary agents you spawn (consultants, reviewers, scouts) follow the same forms.** Never launch a claude session in plan mode or default permission mode for autonomous work — both stall on per-command approval prompts nobody is watching; claude is always `--permission-mode bypassPermissions` (tickmarkr's own adapter uses exactly this for workers, judges, and consults). A read-only codex consultant may use `--sandbox read-only`; any codex session that must touch git needs the unsandboxed flag above.
|
|
19
20
|
|
|
20
21
|
Outside a multi-agent terminal environment, run the loop directly.
|
|
21
22
|
|
|
@@ -29,7 +29,7 @@ Requires `HERDR_ENV=1`; if unset, say so and stop.
|
|
|
29
29
|
main name plus at most ONE hot-state token. Vocabulary: ORCH carries the milestone and progress
|
|
30
30
|
fraction (`ORCH · v1.19 4/5`, updated on every task-done); WORKERS carries the task token (tickmarkr
|
|
31
31
|
updates it). Never long context strings or ✓-chains.
|
|
32
|
-
2. **Orchestrator**: Launch the orchestrator with your agent host. For Claude Code, use `herdr agent start orchestrator --cwd <repo> --no-focus -- claude --permission-mode bypassPermissions` (pin a strong model with `--model <m>` if the operator has a policy). For Codex, use `herdr agent start orchestrator --cwd <repo> --no-focus -- codex --
|
|
32
|
+
2. **Orchestrator**: Launch the orchestrator with your agent host. For Claude Code, use `herdr agent start orchestrator --cwd <repo> --no-focus -- claude --permission-mode bypassPermissions` (pin a strong model with `--model <m>` if the operator has a policy). For Codex, use `herdr agent start orchestrator --cwd <repo> --no-focus -- codex --dangerously-bypass-approvals-and-sandbox` (add `--model <m>` to specify the model). The unsandboxed flag is REQUIRED: codex's `workspace-write` sandbox keeps `.git` refs read-only, so a sandboxed orchestrator's `tickmarkr run` dies at integration-branch creation — do not downgrade it. Workers you never spawn — tickmarkr spawns its own visible worker panes. Auxiliary agents you do spawn (consultants, reviewers, scouts) follow the same forms: never launch a claude session in plan mode or default permission mode for autonomous work — both stall on per-command approval prompts nobody is watching; claude is always `--permission-mode bypassPermissions`, and a read-only codex consultant may use `--sandbox read-only`.
|
|
33
33
|
3. **Standing instructions travel as a brief FILE, never as pane text** — PTY input truncates at ~1024B and a
|
|
34
34
|
truncated brief silently drops policy. Write the full brief to `<repo>/.tickmarkr/overseer/ORCH-BRIEF.md`
|
|
35
35
|
(inside the tickmarkr state dir — already self-gitignored, no exclude step needed), then send one line:
|