tickmarkr 2.8.6 → 2.8.7
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/grok.js +5 -9
- package/dist/adapters/prompt.js +9 -1
- package/dist/adapters/types.d.ts +1 -0
- package/dist/adapters/types.js +12 -0
- package/dist/cli/commands/init.d.ts +10 -0
- package/dist/cli/commands/init.js +13 -3
- package/dist/cli/commands/version.d.ts +1 -1
- package/dist/cli/commands/version.js +36 -2
- package/dist/cli/help.d.ts +1 -0
- package/dist/cli/help.js +6 -3
- package/dist/drivers/subprocess.js +9 -4
- package/dist/gates/baseline.js +47 -7
- package/dist/run/daemon.js +94 -32
- package/dist/run/git.d.ts +5 -0
- package/dist/run/git.js +51 -1
- package/dist/run/repair-disposition.d.ts +1 -1
- package/dist/run/repair-disposition.js +2 -2
- package/dist/run/stall.d.ts +2 -0
- package/dist/run/stall.js +12 -4
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +10 -8
- package/skills/tickmarkr-loop/SKILL.md +10 -8
- package/skills/tickmarkr-overseer/SKILL.md +10 -8
package/dist/adapters/grok.js
CHANGED
|
@@ -115,16 +115,12 @@ export const grok = {
|
|
|
115
115
|
return { command: this.headlessCommand(ctx.promptFile, a.model) };
|
|
116
116
|
},
|
|
117
117
|
parse: parseWorkerResult, // prompt.ts — last-valid-trailer, hard-wrap tolerant. No grok fork.
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
// has stalled before launch (OBS-414's own sweep lists grok's gap as MCP suppression, not trust).
|
|
122
|
-
// FALSIFIER, and it is cheap: dispatch grok into a never-seen worktree and read the pane. One
|
|
123
|
-
// capture of a trust prompt replaces this with {fingerprint, key} — the capture is the only
|
|
124
|
-
// admissible evidence, and a guess here would press a key on whatever it happened to match.
|
|
118
|
+
// Queue row 215 (D-2118 add.1, D-2125): recorded Grok Build 1.0.62 and 1.0.67 directory-trust
|
|
119
|
+
// modal, quoted in GROK_TRUST_PANE. Enter is the measured acceptance key (y also accepts).
|
|
120
|
+
// Accepted residual: answering persists grok's trust for the main checkout root.
|
|
125
121
|
trustDialog: {
|
|
126
|
-
|
|
127
|
-
|
|
122
|
+
fingerprint: "Grok Build may run or modify contents in this directory",
|
|
123
|
+
key: "Enter",
|
|
128
124
|
},
|
|
129
125
|
// v1.5 MODEL-01: fail OPEN to [] (advisory detection, unlike gates). Parses the model LIST only —
|
|
130
126
|
// the auth banner on line 1 is ignored BY CONSTRUCTION (parseGrokModels anchors on the header).
|
package/dist/adapters/prompt.js
CHANGED
|
@@ -12,13 +12,21 @@ import { CAPACITY_RE } from "./types.js";
|
|
|
12
12
|
// and the allowlist stays exactly as immutable during a run as it was before this text changed.
|
|
13
13
|
export function buildTaskPrompt(task, feedback = "", nonce = "") {
|
|
14
14
|
const list = (xs) => xs.map((x) => `- ${x}`).join("\n");
|
|
15
|
+
// Queue row 234 (D-2130 add.1, D-2131): only the worker brief carries the suite;
|
|
16
|
+
// the judge and the review brief keep the shared criterion text byte-equal.
|
|
17
|
+
const criteria = task.acceptance.map((item) => typeof item !== "string" && item.oracle === "test" && item.landing !== undefined
|
|
18
|
+
? `${renderAcceptanceItem(item)} | suite: ${item.landing}`
|
|
19
|
+
: renderAcceptanceItem(item));
|
|
20
|
+
const suiteNote = task.acceptance.some((item) => typeof item !== "string" && item.oracle === "test" && item.landing !== undefined)
|
|
21
|
+
? '\n\nFor test criteria with ` | suite: `, the test title is the text before the marker, and the test must pass in the named file.'
|
|
22
|
+
: "";
|
|
15
23
|
return `You are an autonomous coding worker dispatched by tickmarkr into an isolated git worktree.
|
|
16
24
|
|
|
17
25
|
## Task ${task.id}: ${task.title}
|
|
18
26
|
Goal: ${task.goal}
|
|
19
27
|
|
|
20
28
|
## Acceptance criteria (you will be judged against these, verbatim)
|
|
21
|
-
${list(
|
|
29
|
+
${list(criteria)}${suiteNote}
|
|
22
30
|
${task.files.length ? `\n## File scope — touch ONLY paths matching:\n${list(task.files)}\n` : ""}${task.context.length ? `\n## Context (read these first)\n${list(task.context)}\n` : ""}${task.outOfScope?.length ? `\n## Out of scope\nStay within the task’s declared bounds; do not pursue these items:\n${list(task.outOfScope)}\n` : ""}
|
|
23
31
|
## Rules
|
|
24
32
|
- Work only inside the current directory (your isolated worktree). Never push. Never switch branches.
|
package/dist/adapters/types.d.ts
CHANGED
|
@@ -117,6 +117,7 @@ export declare const TRUST_DIALOG_BLANK_MESSAGE = "trust-dialog fingerprint must
|
|
|
117
117
|
export declare const CLAUDE_TRUST_PANE: string;
|
|
118
118
|
export declare const CODEX_TRUST_PANE: string;
|
|
119
119
|
export declare const CURSOR_TRUST_PANE = "Workspace Trust Required\nTrust this folder?";
|
|
120
|
+
export declare const GROK_TRUST_PANE: string;
|
|
120
121
|
export declare const KIMI_TRUST_PANE: string;
|
|
121
122
|
export declare const KIMI_TRUST_PANE_029: string;
|
|
122
123
|
export declare const KIMI_MCP_TRUST_PANE: string;
|
package/dist/adapters/types.js
CHANGED
|
@@ -59,6 +59,16 @@ export const CODEX_TRUST_PANE = [
|
|
|
59
59
|
].join("\n");
|
|
60
60
|
// v1.22 T5 / OBS-19, cursor-agent's per-worktree dialog.
|
|
61
61
|
export const CURSOR_TRUST_PANE = "Workspace Trust Required\nTrust this folder?";
|
|
62
|
+
// Queue row 215 (D-2118 add.1, D-2125): quoted from the recorded Grok Build 1.0.62 modal in
|
|
63
|
+
// tests/fixtures/grok-trust/grok-1.0.62.screen.txt; 1.0.67 renders these same modal rows centered.
|
|
64
|
+
export const GROK_TRUST_PANE = [
|
|
65
|
+
"Do you trust the contents of this directory?",
|
|
66
|
+
"/Users/…/repo",
|
|
67
|
+
"Grok Build may run or modify contents in this directory,",
|
|
68
|
+
"posing security risks.",
|
|
69
|
+
"Yes, proceed y",
|
|
70
|
+
"No, quit n",
|
|
71
|
+
].join("\n");
|
|
62
72
|
// Queue row 198, kimi 2.2.0's folder-trust modal on a fresh task worktree (run 1394 T3, read off the
|
|
63
73
|
// Orca screen before its one Enter). The worktree path is shortened as above; every other row is as
|
|
64
74
|
// rendered. 2.2.0 renamed the selected option, so the 0.29.0 fingerprint below never matched it and the
|
|
@@ -92,6 +102,7 @@ export const KIMI_MCP_TRUST_PANE = [
|
|
|
92
102
|
].join("\n");
|
|
93
103
|
export const RECORDED_TRUST_PANES = [
|
|
94
104
|
CLAUDE_TRUST_PANE, CODEX_TRUST_PANE, CURSOR_TRUST_PANE, KIMI_TRUST_PANE, KIMI_TRUST_PANE_029, KIMI_MCP_TRUST_PANE,
|
|
105
|
+
GROK_TRUST_PANE,
|
|
95
106
|
];
|
|
96
107
|
// v1.89 T1 / OBS-414 round 4 — EXACT, not "leading bytes of a recorded line". Round 3 accepted any
|
|
97
108
|
// prefix of a captured line, and every prefix of a trust prompt is also a substring of unrelated
|
|
@@ -111,6 +122,7 @@ export const APPROVED_TRUST_FINGERPRINTS = [
|
|
|
111
122
|
"Workspace Trust Required", // cursor-agent, OBS-19
|
|
112
123
|
"❯ Trust this folder", // kimi 0.29.0, OBS-358 + OBS-406 — the cursor glyph is load-bearing
|
|
113
124
|
"❯ Trust and continue", // kimi 2.2.0, queue row 198 — the same gate with its option renamed
|
|
125
|
+
"Grok Build may run or modify contents in this directory", // queue row 215, D-2125
|
|
114
126
|
];
|
|
115
127
|
const isApprovedFingerprint = (fingerprint) => APPROVED_TRUST_FINGERPRINTS.includes(fingerprint)
|
|
116
128
|
&& RECORDED_TRUST_PANES.some((pane) => pane.includes(fingerprint));
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
interface SkillDrift {
|
|
2
|
+
skill: string;
|
|
3
|
+
dir: string;
|
|
4
|
+
missing: string[];
|
|
5
|
+
modified: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare function skillDrift(cwd: string): SkillDrift[];
|
|
8
|
+
export declare const docsDrift: (cwd: string) => string[];
|
|
9
|
+
export declare function installedAgentGuidance(cwd: string): string[];
|
|
1
10
|
export type DetectedHost = "herdr" | "orca" | "subprocess";
|
|
2
11
|
export declare function detectedHost(env?: NodeJS.ProcessEnv): DetectedHost;
|
|
3
12
|
/** Test seam mirroring FleetIO: production callers omit it and get the process streams. */
|
|
@@ -6,3 +15,4 @@ export type InitIO = {
|
|
|
6
15
|
output?: NodeJS.WriteStream;
|
|
7
16
|
};
|
|
8
17
|
export declare function init(argv: string[], cwd?: string, io?: InitIO): Promise<string>;
|
|
18
|
+
export {};
|
|
@@ -107,7 +107,11 @@ Outside multi-agent environments, run the loop directly.
|
|
|
107
107
|
|
|
108
108
|
### Version preflight
|
|
109
109
|
|
|
110
|
-
Before \`tickmarkr compile\` or \`tickmarkr run\`: run \`tickmarkr version
|
|
110
|
+
Before \`tickmarkr compile\` or \`tickmarkr run\`: run \`tickmarkr version --check\` and stop on a non-zero exit. In the tickmarkr source repository (\`package.json\` named \`tickmarkr\`), stop if the versions differ anywhere (major, minor, or patch); the binary and repository must agree on the entire version, so binary \`2.1.0\` versus repository \`2.1.1\` is a stop. In a consumer repository, the check compares installed agent guidance with this binary's packaged block and skills, never the app's manifest version; refresh drifted guidance with \`tickmarkr init --agent --force\`. Tell the operator to update or link the correct binary when the source version differs. Never proceed on hope — stale binaries silently skip daemon gates. Also verify no run is live in this repository before starting one: lead with this repository's \`.tickmarkr/graph.lock\`, read its holder pid, and treat it as live until \`kill -0 <pid>\` proves that holder dead. Never require a machine-wide process pattern to be empty: a lawful run in another repository — or the probing shell's own argv — can match. If you use a process probe as secondary evidence, exclude the probing process, resolve every candidate's own cwd (for example with \`lsof -a -p <pid> -d cwd\`), and count only candidates whose cwd is this repository root.
|
|
111
|
+
|
|
112
|
+
### Seat hygiene
|
|
113
|
+
|
|
114
|
+
In a multi-tier session, the tiers clear each other at 50% context or more, after writing a handoff. A new spec or run is a mission boundary: write the handoff and clear before taking the next mission. Copy this duty into every brief and handoff. On a single-tier plain host, write your handoff and ask the operator to clear the session.
|
|
111
115
|
|
|
112
116
|
### Tip-verify-before-green
|
|
113
117
|
|
|
@@ -184,7 +188,7 @@ function skillFileList(root, prefix = "") {
|
|
|
184
188
|
}
|
|
185
189
|
// Drift is only computed for skills that ARE installed. A skill never installed is not drift — `--agent`
|
|
186
190
|
// installs it and says so, and nagging about it would bury the case this exists for.
|
|
187
|
-
function skillDrift(cwd) {
|
|
191
|
+
export function skillDrift(cwd) {
|
|
188
192
|
const drifted = [];
|
|
189
193
|
for (const { skillsDir } of hostTargets(cwd)) {
|
|
190
194
|
for (const skill of AGENT_SKILLS) {
|
|
@@ -215,7 +219,7 @@ function skillDrift(cwd) {
|
|
|
215
219
|
// superseded preflight rules (patch-version drift tolerated, a machine-wide process match required)
|
|
216
220
|
// through the very release that fixed the tracked copy, and a consultation was launched under the stale
|
|
217
221
|
// text with nobody able to see it. Same rule as skillDrift: a doc that was never written is not drift.
|
|
218
|
-
const docsDrift = (cwd) => hostTargets(cwd).map(({ docPath }) => docPath).filter((docPath) => {
|
|
222
|
+
export const docsDrift = (cwd) => hostTargets(cwd).map(({ docPath }) => docPath).filter((docPath) => {
|
|
219
223
|
if (!existsSync(docPath))
|
|
220
224
|
return false;
|
|
221
225
|
const current = readFileSync(docPath, "utf8");
|
|
@@ -227,6 +231,12 @@ const docsDrift = (cwd) => hostTargets(cwd).map(({ docPath }) => docPath).filter
|
|
|
227
231
|
return true; // a truncated block is stale by definition
|
|
228
232
|
return current.slice(begin, end + DOCS_END.length) !== AGENT_DOCS.trimEnd();
|
|
229
233
|
});
|
|
234
|
+
export function installedAgentGuidance(cwd) {
|
|
235
|
+
return hostTargets(cwd).flatMap(({ skillsDir, docPath }) => [
|
|
236
|
+
...AGENT_SKILLS.map((skill) => join(skillsDir, skill, "SKILL.md")).filter((path) => existsSync(path)),
|
|
237
|
+
...(existsSync(docPath) && readFileSync(docPath, "utf8").includes(DOCS_BEGIN) ? [docPath] : []),
|
|
238
|
+
]);
|
|
239
|
+
}
|
|
230
240
|
const describeDrift = (d) => [
|
|
231
241
|
d.missing.length ? `missing ${d.missing.join(", ")}` : "",
|
|
232
242
|
d.modified.length ? `modified ${d.modified.join(", ")}` : "",
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export declare function distFingerprint(dir?: string): string;
|
|
2
|
-
export declare function version(argv?: string[], dir?: string): Promise<string>;
|
|
2
|
+
export declare function version(argv?: string[], dir?: string, cwd?: string): Promise<string>;
|
|
@@ -23,7 +23,41 @@ export function distFingerprint(dir = distDir) {
|
|
|
23
23
|
hash.update(relative(dir, file)).update("\0").update(readFileSync(file)).update("\0");
|
|
24
24
|
return hash.digest("hex");
|
|
25
25
|
}
|
|
26
|
-
export async function version(argv = [], dir = distDir) {
|
|
26
|
+
export async function version(argv = [], dir = distDir, cwd = process.cwd()) {
|
|
27
27
|
const { version: v } = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
28
|
-
|
|
28
|
+
const stamp = argv.includes("--dist") ? `${v} dist:${resolve(dir)} fingerprint:${distFingerprint(dir)}` : v;
|
|
29
|
+
if (!argv.includes("--check"))
|
|
30
|
+
return stamp;
|
|
31
|
+
let manifest;
|
|
32
|
+
try {
|
|
33
|
+
manifest = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
|
|
34
|
+
}
|
|
35
|
+
catch { /* no source manifest — consumer guidance check */ }
|
|
36
|
+
if (manifest?.name === "tickmarkr") {
|
|
37
|
+
const repoVersion = manifest.version ?? "missing";
|
|
38
|
+
const equal = v === repoVersion;
|
|
39
|
+
const out = `tickmarkr binary ${stamp}: compared tickmarkr source repository package.json version ${repoVersion} — ${equal ? "entire version matches" : "versions differ; update or link the correct binary"}`;
|
|
40
|
+
if (!equal)
|
|
41
|
+
throw new Error(out);
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
const { docsDrift, installedAgentGuidance, skillDrift } = await import("./init.js");
|
|
45
|
+
const installed = installedAgentGuidance(cwd);
|
|
46
|
+
if (!installed.length)
|
|
47
|
+
return `tickmarkr binary ${stamp}: no installed agent guidance to compare`;
|
|
48
|
+
const drift = [
|
|
49
|
+
...skillDrift(cwd).flatMap(({ dir: skillDir, missing, modified }) => [
|
|
50
|
+
...missing.map((file) => `missing ${relative(cwd, join(skillDir, file))}`),
|
|
51
|
+
...modified.map((file) => `modified ${relative(cwd, join(skillDir, file))}`),
|
|
52
|
+
]),
|
|
53
|
+
...docsDrift(cwd).map((file) => `stale agent-docs block in ${relative(cwd, file)}`),
|
|
54
|
+
];
|
|
55
|
+
const out = [
|
|
56
|
+
`tickmarkr binary ${stamp}: compared installed agent guidance with this binary's packaged block and skills:`,
|
|
57
|
+
...installed.map((file) => ` ${relative(cwd, file)}`),
|
|
58
|
+
...(drift.length ? [...drift, "refresh: tickmarkr init --agent --force"] : ["installed guidance matches"]),
|
|
59
|
+
].join("\n");
|
|
60
|
+
if (drift.length)
|
|
61
|
+
throw new Error(out);
|
|
62
|
+
return out;
|
|
29
63
|
}
|
package/dist/cli/help.d.ts
CHANGED
package/dist/cli/help.js
CHANGED
|
@@ -169,9 +169,12 @@ export const COMMAND_HELP = {
|
|
|
169
169
|
],
|
|
170
170
|
},
|
|
171
171
|
version: {
|
|
172
|
-
usage: "version [--dist]", description: "Print the installed package version. Top-level aliases: --version and -v.",
|
|
173
|
-
options: {
|
|
174
|
-
|
|
172
|
+
usage: "version [--dist] [--check]", description: "Print the installed package version. Top-level aliases: --version and -v.",
|
|
173
|
+
options: {
|
|
174
|
+
"--dist": "Also print the resolved build directory and distribution fingerprint.",
|
|
175
|
+
"--check": "Compare the entire source version when package.json is named tickmarkr; otherwise check installed agent guidance against this binary. Exit non-zero on drift; no installed guidance exits zero.",
|
|
176
|
+
},
|
|
177
|
+
examples: ["version", "version --dist", "version --check"],
|
|
175
178
|
},
|
|
176
179
|
verify: {
|
|
177
180
|
usage: "verify [--base <ref>] [--criteria <file> | --task <id>] [options]",
|
|
@@ -49,7 +49,7 @@ export class SubprocessDriver {
|
|
|
49
49
|
n = 0;
|
|
50
50
|
async slot(cwd, name) {
|
|
51
51
|
const id = `sp-${++this.n}`;
|
|
52
|
-
this.slots.set(id, { buf: "", exited: false });
|
|
52
|
+
this.slots.set(id, { buf: "", receivedBytes: 0, exited: false });
|
|
53
53
|
return { id, name, cwd };
|
|
54
54
|
}
|
|
55
55
|
state(slot) {
|
|
@@ -72,8 +72,13 @@ export class SubprocessDriver {
|
|
|
72
72
|
detached: true,
|
|
73
73
|
});
|
|
74
74
|
s.proc = p;
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
// Queue row 233 (D-2130): count both received streams before retention truncates the tail.
|
|
76
|
+
const receive = (d) => {
|
|
77
|
+
s.receivedBytes += d.length;
|
|
78
|
+
s.buf = (s.buf + d).slice(-MAX_BUF);
|
|
79
|
+
};
|
|
80
|
+
p.stdout.on("data", receive);
|
|
81
|
+
p.stderr.on("data", receive);
|
|
77
82
|
p.on("close", () => (s.exited = true));
|
|
78
83
|
p.on("error", (e) => { s.buf = (s.buf + `\n[tickmarkr subprocess error] ${e}\n`).slice(-MAX_BUF); s.exited = true; });
|
|
79
84
|
// Queue row 108 (D-1622): THIS run's transport state. The slot's `exited` outlives an invocation (a reused slot) and
|
|
@@ -117,7 +122,7 @@ export class SubprocessDriver {
|
|
|
117
122
|
async transportState(slot) {
|
|
118
123
|
const s = this.state(slot);
|
|
119
124
|
const run = s.invocation;
|
|
120
|
-
return { closed: run?.closed ?? false, bytes:
|
|
125
|
+
return { closed: run?.closed ?? false, bytes: s.receivedBytes, exitCode: run?.exitCode ?? null, signal: run?.signal ?? null };
|
|
121
126
|
}
|
|
122
127
|
async read(slot, lines) {
|
|
123
128
|
return this.state(slot).buf.split("\n").slice(-lines).join("\n");
|
package/dist/gates/baseline.js
CHANGED
|
@@ -428,11 +428,30 @@ export function classifyFailureOutput(output) {
|
|
|
428
428
|
// and `stderr | <task id>` (digits and underscores) when the reporter no longer knows the task
|
|
429
429
|
// (dist/chunks/index.*.js, `headerText`). The stripper knew only the first form.
|
|
430
430
|
const VITEST_ECHO_BLOCK_RE = /^\s*std(?:out|err)\s+\|\s+(?:unknown test\s*$|\d+_[\d_]+\s*$|\S+\.(?:test|spec)\.[cm]?[jt]sx?(?:\s*$|\s+>\s+\S))/;
|
|
431
|
-
|
|
431
|
+
// Queue row 211 (D-2118): only Jest's complete, column-zero recovery warning is noise. A missing
|
|
432
|
+
// neighbour, changed hint, indented copy or multi-line Watchman error remains failure evidence.
|
|
433
|
+
const JEST_WATCHMAN_HEADER = "jest-haste-map: Watchman crawl failed. Retrying once with node crawler.";
|
|
434
|
+
const JEST_WATCHMAN_HINT = " Usually this happens when watchman isn't running. Create an empty `.watchmanconfig` file in your project's root folder or initialize a git or hg repository in your project.";
|
|
435
|
+
const JEST_WATCHMAN_ERROR_RE = /^ {2}Error: Watchman error: [^\r\n]+\. Make sure watchman is running for this project\. See https:\/\/facebook\.github\.io\/watchman\/docs\/troubleshooting\.$/;
|
|
436
|
+
/** Queue row 211 (D-2118): display readers retain raw Vitest echo lines while dropping this warning. */
|
|
437
|
+
const withoutJestWatchmanBlocks = (output) => {
|
|
438
|
+
const kept = [];
|
|
439
|
+
const lines = output.split("\n");
|
|
440
|
+
for (let i = 0; i < lines.length; i++) {
|
|
441
|
+
const line = lines[i];
|
|
442
|
+
if (line === JEST_WATCHMAN_HEADER && lines[i + 1] === JEST_WATCHMAN_HINT && JEST_WATCHMAN_ERROR_RE.test(lines[i + 2] ?? "")) {
|
|
443
|
+
i += 2;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
kept.push(line);
|
|
447
|
+
}
|
|
448
|
+
return kept;
|
|
449
|
+
};
|
|
450
|
+
/** Queue row 211 (D-2118): omit the recovered Jest warning alongside Vitest's test-owned echo blocks. */
|
|
432
451
|
const withoutVitestEchoBlocks = (output) => {
|
|
433
452
|
const outside = [];
|
|
434
453
|
let inVitestEchoBlock = false;
|
|
435
|
-
for (const line of output
|
|
454
|
+
for (const line of withoutJestWatchmanBlocks(output)) {
|
|
436
455
|
const clean = line.replace(ANSI_RE, "");
|
|
437
456
|
const runnerLine = stripTurboPrefix(clean) ?? clean;
|
|
438
457
|
if (inVitestEchoBlock) {
|
|
@@ -625,8 +644,7 @@ function fingerprintOccurrences(output) {
|
|
|
625
644
|
}
|
|
626
645
|
// The diagnostic channel for a runner we cannot read: raw output lines, never fingerprints, never a
|
|
627
646
|
// verdict. They exist so the operator sees SOMETHING when a green command turns red unrecognizably.
|
|
628
|
-
const unrecognizedEvidence = (raw) => raw
|
|
629
|
-
.split("\n")
|
|
647
|
+
const unrecognizedEvidence = (raw) => withoutJestWatchmanBlocks(raw)
|
|
630
648
|
.map((l) => l.replace(ANSI_RE, "").trimEnd())
|
|
631
649
|
.filter((l) => VOCAB_RE.test(l))
|
|
632
650
|
.slice(0, 10)
|
|
@@ -1028,11 +1046,33 @@ export async function detectVacuousOracles(cwd, tasks) {
|
|
|
1028
1046
|
// drop a FAIL line that fingerprint-collides with baseline noise). No headline anchors → byte-identical
|
|
1029
1047
|
// fallback to today's text (non-vitest runners lose nothing). RED-pinned by tests/gates/baseline.test.ts
|
|
1030
1048
|
// "HYG-08: details headlines the failing test, not the noise".
|
|
1049
|
+
// Queue row 224(a) (D-2118): Jest identities and their first message are display-only; Console and
|
|
1050
|
+
// column-zero validation bullets are not identities. Keep the existing metadata extraction separate.
|
|
1051
|
+
const JEST_IDENTITY_RE = /^ {2}● (?!Console\s*$)\S.*$/;
|
|
1031
1052
|
function headlineDetails(raw, fresh, freshLines) {
|
|
1032
|
-
const
|
|
1053
|
+
const namedHeadlines = raw
|
|
1033
1054
|
.split("\n")
|
|
1034
1055
|
.map((l) => l.replace(ANSI_RE, ""))
|
|
1035
1056
|
.filter((l) => namesFailureEitherForm(l) || SUMMARY_FAIL_RE.test(l));
|
|
1057
|
+
const outputLines = withoutJestWatchmanBlocks(raw).map((l) => l.replace(ANSI_RE, ""));
|
|
1058
|
+
let inVitestEchoBlock = false;
|
|
1059
|
+
const headlines = outputLines.flatMap((line, index) => {
|
|
1060
|
+
const runnerLine = stripTurboPrefix(line) ?? line;
|
|
1061
|
+
const echoed = inVitestEchoBlock || VITEST_ECHO_BLOCK_RE.test(runnerLine);
|
|
1062
|
+
if (inVitestEchoBlock) {
|
|
1063
|
+
if (runnerLine.trim() === "")
|
|
1064
|
+
inVitestEchoBlock = false;
|
|
1065
|
+
}
|
|
1066
|
+
else if (echoed)
|
|
1067
|
+
inVitestEchoBlock = true;
|
|
1068
|
+
// Queue row 224(a) (D-2118): preserve raw FAIL/summary headlines; only Jest identities exclude echoes.
|
|
1069
|
+
if (namesFailureEitherForm(line) || SUMMARY_FAIL_RE.test(line))
|
|
1070
|
+
return [line];
|
|
1071
|
+
if (echoed || !JEST_IDENTITY_RE.test(line))
|
|
1072
|
+
return [];
|
|
1073
|
+
const message = withoutVitestEchoBlocks(outputLines.slice(index + 1).join("\n")).find((l) => l.trim() !== "");
|
|
1074
|
+
return message === undefined ? [line] : [line, message];
|
|
1075
|
+
});
|
|
1036
1076
|
// S22: the ORIGINAL runner lines behind v2 comparison keys are the diagnostic; the keys are secondary.
|
|
1037
1077
|
// A legacy entry's lines ARE its keys, so its details stay byte-identical.
|
|
1038
1078
|
const lines = [...new Set(freshLines)].join("\n");
|
|
@@ -1042,7 +1082,7 @@ function headlineDetails(raw, fresh, freshLines) {
|
|
|
1042
1082
|
return { details: same ? `new failures vs baseline:\n${lines}` : `new failures vs baseline:\n${lines}\n\n${keys}` };
|
|
1043
1083
|
return {
|
|
1044
1084
|
details: `failing tests:\n${headlines.join("\n")}\n\n${same ? "" : `new failure lines vs baseline:\n${lines}\n\n`}${keys}`,
|
|
1045
|
-
meta: { failingTests:
|
|
1085
|
+
...(namedHeadlines.length ? { meta: { failingTests: namedHeadlines.filter(namesFailureEitherForm) } } : {}),
|
|
1046
1086
|
};
|
|
1047
1087
|
}
|
|
1048
1088
|
export async function compareToBaseline(cwd, commands, baseline, enabled, opts = {}) {
|
|
@@ -1225,7 +1265,7 @@ export async function compareToBaseline(cwd, commands, baseline, enabled, opts =
|
|
|
1225
1265
|
if (classification === "infra") {
|
|
1226
1266
|
const evidence = failingLines.length
|
|
1227
1267
|
? failingLines.slice(0, 10).join("\n")
|
|
1228
|
-
: unrecognizedEvidence(raw) || raw.trim().split("\n").slice(0, 10).join("\n");
|
|
1268
|
+
: unrecognizedEvidence(raw) || withoutJestWatchmanBlocks(raw).join("\n").trim().split("\n").slice(0, 10).join("\n");
|
|
1229
1269
|
record({
|
|
1230
1270
|
gate: name,
|
|
1231
1271
|
pass: false,
|
package/dist/run/daemon.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { HOST_PROBE_SAMPLE_MS, HOST_PROBE_SAMPLES, HostDegradedError, hostDegraded, observeHost } from "./host-health.js";
|
|
2
2
|
import { runWithInterruptionSink, VITEST_CACHE_ENV, worktreeVitestCache } from "../gates/test-manifest.js";
|
|
3
3
|
import { COMMAND_LEASE_TOKEN_ENV, commandLeaseEnvironment, CommandLeases, currentCommandLeaseToken, isRunnerCommand, runWithCommandLease, withCommandLease } from "./lease.js";
|
|
4
|
-
import { execFileSync, spawn } from "node:child_process";
|
|
4
|
+
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
5
5
|
import { createHash, randomBytes } from "node:crypto";
|
|
6
6
|
import { shq } from "../adapters/types.js";
|
|
7
7
|
import { appendFileSync, closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, readlinkSync, readSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
@@ -34,10 +34,10 @@ import { executionSignal, remainingExecutionMs, withExecutionBudget, withoutExec
|
|
|
34
34
|
import { repairSelectionDecision, repairSelectionEnabled } from "./repair-selection.js";
|
|
35
35
|
import { failureDisposition, reserveInfrastructureRetry } from "./recovery.js";
|
|
36
36
|
import { runEnvironment } from "./environment.js";
|
|
37
|
-
import { addDetachedWorktree, changeRepresented, cleanupRunWorktrees, declaredBaseContainment, deriveForkCap, FORK_CAP_ENV, gitHead, linkNodeModules, npmDependencyInstallCommand, npmDependencyManifestChanged, PRESERVE_COMMIT_SUBJECT, PRESERVE_PRODUCER_TRAILER, preserveWorktree, producerFields, reflogWitness, removeWorktree, resolvedCapacity, runWithForkBudget, runWithVerificationBudget, sameCapacity, sameVerification, sh, shell, shGit, SUITE_PARENT_ENV, verificationProtocol, WORKTREE_LAYOUT_CONTRACT, WORKTREES_DIR, worktreePath } from "./git.js";
|
|
37
|
+
import { addDetachedWorktree, changeRepresented, cleanupRunWorktrees, declaredBaseContainment, deriveForkCap, FORK_CAP_ENV, gitHead, linkNodeModules, npmDependencyInstallCommand, npmDependencyManifestChanged, PRESERVE_COMMIT_SUBJECT, PRESERVE_PRODUCER_TRAILER, preserveWorktree, producerFields, reflogWitness, removeWorktree, resolvedCapacity, runWithForkBudget, runWithVerificationBudget, sameCapacity, sameVerification, sh, shell, shGit, SUITE_PARENT_ENV, verificationProtocol, WorktreeAdminRaceError, WORKTREE_LAYOUT_CONTRACT, WORKTREES_DIR, worktreePath } from "./git.js";
|
|
38
38
|
import { MASK } from "./redact.js";
|
|
39
39
|
import { runInteractiveSeed } from "./interactive-seed.js";
|
|
40
|
-
import { classifyRepairDisposition, resolveScopeHints } from "./repair-disposition.js";
|
|
40
|
+
import { classifyRepairDisposition, namedPaths, resolveScopeHints } from "./repair-disposition.js";
|
|
41
41
|
import { applyScopeAmendments, activeRetryBan, interruptedAttempt, RESUME_HARVEST_SOURCE, resumeHarvestAuthor, approvalAction, APPROVAL_REFUSED, bindingToken, effectiveEvents, foldDecisions, physicalLine, staleApprovals, classifyTaskFailure, classifyWorkerResultCause, deferredReviewFindings, engagementComparable, formatPriorFindingEvidence, GATE_FINGERPRINT_CAP, GATE_SATISFIED_RELEASE, identicalGateFailures, isDeferredFinding, journaledFailureBrief, Journal, loadRoutingProfile, newRunId, normalizeGateFailure, outstandingConsultGuidance, readableExcerpt, receiptOrigin, outstandingReviewFindings, pendingApprovalActions, pendingRechecks, pendingRepairFindings, phaseForGate, priorJudgments, readPriorRunEvidence, recordedTaskFailureKind, reraisedReviewChains, RECHECK_RELEASE, renderStructuredReviewFinding, repairReachSinceApproval, repairsSinceApproval, reviewRoundsSinceApproval, runHasEnded, standingRulings, structuredFindings, upheldFeedbackByTask } from "./journal.js";
|
|
42
42
|
import { carriedAuthorVendors, gateReviewerFloor, isDiffCapPark, isGarbageReview, pickReviewer } from "../gates/review.js";
|
|
43
43
|
import { acquireApprovalSerialization, acquireRunLock, isPidLive, releaseRunLock } from "./lock.js";
|
|
@@ -855,7 +855,11 @@ export function resetDeadChannelFastKillMsForTests() {
|
|
|
855
855
|
// monotonic-tracker silence — CONCLUDES the wait. Conclude, never kill: the pane is harvested by
|
|
856
856
|
// the same tail a window expiry uses, and the carried worktree goes straight to gates. Set at the
|
|
857
857
|
// fast-kill's window on purpose (the OBS-264 arithmetic is "a ~36m stall + ~15m redo becomes a
|
|
858
|
-
// ~5m gate pass")
|
|
858
|
+
// ~5m gate pass"). Queue row 231 (D-2119): a headless worker waiting on its remote provider can be
|
|
859
|
+
// silent with flat CPU. Print mode also requires a fresh reading of no ESTABLISHED TCP connection
|
|
860
|
+
// in its owned process group; connected or unreadable holds the harvest. Interactive workers keep
|
|
861
|
+
// the triad: an idle pane holds connections too. Residual (D-2125): a truly hung headless worker
|
|
862
|
+
// holding a connection rides the stall window instead.
|
|
859
863
|
const HARVEST_SILENT_MS = 5 * 60_000;
|
|
860
864
|
let harvestSilentMs = HARVEST_SILENT_MS;
|
|
861
865
|
/** Test seam — shrink the harvest silence gate without minute-long sleeps. */
|
|
@@ -3235,11 +3239,6 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3235
3239
|
];
|
|
3236
3240
|
return `${identity} — release with ${commands.map((command) => `\`${command}\``).join(" or ")}`;
|
|
3237
3241
|
};
|
|
3238
|
-
// Lexing never starts in the middle of a word. Resolution is separate so missing hints
|
|
3239
|
-
// remain visible evidence without becoming executable approval advice.
|
|
3240
|
-
const namedPaths = (text) => [...text.matchAll(/(?:^|[\s`'"])((?:[A-Za-z0-9_@.()[\]-]+\/)+[A-Za-z0-9_@.[\]-]+|[A-Za-z0-9_@-]+(?:\.[A-Za-z0-9_-]+)+)(?=$|[\s`'"),:;.!?])/g)]
|
|
3241
|
-
.map((match) => match[1].replace(/^\.\//, "").replace(/\.$/, ""))
|
|
3242
|
-
.filter((path) => !path.split("/").includes(".."));
|
|
3243
3242
|
const diffPaths = async (base, wt) => {
|
|
3244
3243
|
const out = await shGit(`git diff --name-only -z ${shq(base)}..HEAD`, wt);
|
|
3245
3244
|
return new Set(out.code === 0 ? out.stdout.split("\0").filter(Boolean) : []);
|
|
@@ -3526,7 +3525,20 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3526
3525
|
const ref = await preserveWorktree(priorWt, producer);
|
|
3527
3526
|
if (ref)
|
|
3528
3527
|
journal.append("worktree-preserved", t.id, { ref, ...producerFields(producer) });
|
|
3529
|
-
|
|
3528
|
+
// Queue row 223 (D-2118): only this typed add race buys retries, with three adds total.
|
|
3529
|
+
const addWorktree = () => {
|
|
3530
|
+
return driver.worktree(repoRoot, taskBranch, taskBase);
|
|
3531
|
+
};
|
|
3532
|
+
for (let attempt = 1;; attempt++) {
|
|
3533
|
+
try {
|
|
3534
|
+
return await addWorktree();
|
|
3535
|
+
}
|
|
3536
|
+
catch (error) {
|
|
3537
|
+
if (!(error instanceof WorktreeAdminRaceError) || attempt >= 3)
|
|
3538
|
+
throw error;
|
|
3539
|
+
journal.append("worktree-add-retry", t.id, { attempt, nextAttempt: attempt + 1, evidence: error.evidence });
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3530
3542
|
};
|
|
3531
3543
|
// A dead worker with a clean checkout still needs a durable recovery handle: there may be no
|
|
3532
3544
|
// pane left to identify even the commit it was dispatched from. preserveWorktree deliberately
|
|
@@ -5310,17 +5322,19 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
5310
5322
|
let cpuFlat;
|
|
5311
5323
|
let cpuAccountant;
|
|
5312
5324
|
let cpuGapCount = 0;
|
|
5313
|
-
|
|
5314
|
-
//
|
|
5325
|
+
const unmeasurableNoted = new Set();
|
|
5326
|
+
// Queue row 231 (D-2119): one line per reading per attempt, including the connection leg.
|
|
5327
|
+
// One CPU line per attempt, whichever way that leg turns out to be unmeasurable. A triad that
|
|
5315
5328
|
// can never conclude is this feature silently ABSENT — on a host whose `ps` the probe cannot
|
|
5316
5329
|
// read, every stall would ride its whole window out again with nothing saying why. Named once
|
|
5317
5330
|
// per attempt, not per slice: the condition is structural, and a per-slice line would bury it.
|
|
5318
|
-
const noteUnmeasurable = (reason) => {
|
|
5319
|
-
if (unmeasurableNoted)
|
|
5331
|
+
const noteUnmeasurable = (reason, reading = "cpu") => {
|
|
5332
|
+
if (unmeasurableNoted.has(reading))
|
|
5320
5333
|
return;
|
|
5321
|
-
unmeasurableNoted
|
|
5334
|
+
unmeasurableNoted.add(reading);
|
|
5322
5335
|
journal.append("worker-harvest-unmeasurable", t.id, { slot: slot.name, attempt, reason });
|
|
5323
5336
|
};
|
|
5337
|
+
let harvestHeldNoted = false;
|
|
5324
5338
|
// The CPU leg needs a marker in the worker's own argv, and every launch path puts this
|
|
5325
5339
|
// attempt's dispatch script there EXCEPT interactiveSeed: runInteractiveSeed launches the
|
|
5326
5340
|
// TUI directly, by a command the ADAPTER owns (seed.launch(model)) which tickmarkr cannot
|
|
@@ -5382,6 +5396,39 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
5382
5396
|
const carried = await commitsAheadOf(taskBase, wt);
|
|
5383
5397
|
if (carried.length === 0)
|
|
5384
5398
|
return false; // nothing landed: not this branch's population
|
|
5399
|
+
if (!interactive) {
|
|
5400
|
+
// Queue row 231 (D-2119): query afresh only after the other legs hold. lsof ORs selections
|
|
5401
|
+
// without -a; -g includes connector children in the owned group, not just the CLI root.
|
|
5402
|
+
// No environment inspection. One async PATH lookup, bounded from invocation like the
|
|
5403
|
+
// reap census (15 s); SIGKILL also retires a probe that ignores SIGTERM.
|
|
5404
|
+
const group = readOwnedProcessGroup(groupFile);
|
|
5405
|
+
const connection = group === undefined ? "unreadable" : await new Promise((resolve) => {
|
|
5406
|
+
try {
|
|
5407
|
+
execFile("lsof", ["-nP", "-a", "-g", String(group), "-iTCP", "-sTCP:ESTABLISHED", "-Fp"], { encoding: "utf8", timeout: 15_000, killSignal: "SIGKILL", maxBuffer: 4 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
5408
|
+
if (!error && stdout.trim())
|
|
5409
|
+
resolve("connected");
|
|
5410
|
+
else if (error && error.code === 1 && !stdout && !stderr)
|
|
5411
|
+
resolve("none");
|
|
5412
|
+
else
|
|
5413
|
+
resolve("unreadable");
|
|
5414
|
+
});
|
|
5415
|
+
}
|
|
5416
|
+
catch {
|
|
5417
|
+
resolve("unreadable");
|
|
5418
|
+
}
|
|
5419
|
+
});
|
|
5420
|
+
if (connection === "unreadable") {
|
|
5421
|
+
noteUnmeasurable("the worker ESTABLISHED TCP connection reading could not be read", "connection");
|
|
5422
|
+
return false;
|
|
5423
|
+
}
|
|
5424
|
+
if (connection === "connected") {
|
|
5425
|
+
if (!harvestHeldNoted) {
|
|
5426
|
+
harvestHeldNoted = true;
|
|
5427
|
+
journal.append("worker-harvest-held", t.id, { slot: slot.name, attempt, reason: "established-tcp-connection", processGroup: group });
|
|
5428
|
+
}
|
|
5429
|
+
return false;
|
|
5430
|
+
}
|
|
5431
|
+
}
|
|
5385
5432
|
journal.append("worker-harvest", t.id, {
|
|
5386
5433
|
slot: slot.name, attempt, commits: carried.length,
|
|
5387
5434
|
silentMs, cpuMs: leg.cpu.ms, cpuResolutionMs: leg.cpu.resolutionMs,
|
|
@@ -5860,13 +5907,17 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
5860
5907
|
lastStatus = st;
|
|
5861
5908
|
journal.append("worker-status", t.id, { slot: slot.name, status: st, attempt });
|
|
5862
5909
|
}
|
|
5863
|
-
|
|
5910
|
+
// Queue row 215 (D-2118 add.1): an unhooked CLI can report unknown at its startup modal.
|
|
5911
|
+
// Bound that status to worker-launch's startup window, including time spent reading.
|
|
5912
|
+
const withinTrustStartup = () => workerLaunchedAt !== undefined
|
|
5913
|
+
&& Date.now() - workerLaunchedAt <= workerStartupWindowMs;
|
|
5914
|
+
if (st === "blocked" || st === "idle" || (st === "unknown" && withinTrustStartup())) {
|
|
5864
5915
|
// T5: once-per-slot auto-answer when the adapter declares a trust dialog and the pane
|
|
5865
5916
|
// text matches. tickmarkr created the worktree from the operator's own repo — safe by construction.
|
|
5866
5917
|
if (!trustAnswered && adapter.trustDialog && driver.sendKey) {
|
|
5867
5918
|
try {
|
|
5868
|
-
const paneText = await workerTransport.read(slot, 80);
|
|
5869
|
-
if (matchesTrustDialog(paneText, adapter.trustDialog)) {
|
|
5919
|
+
const paneText = await (slot.readScreen ? slot.readScreen() : workerTransport.read(slot, 80));
|
|
5920
|
+
if ((st !== "unknown" || withinTrustStartup()) && matchesTrustDialog(paneText, adapter.trustDialog)) {
|
|
5870
5921
|
trustAnswered = true;
|
|
5871
5922
|
trustAnsweredSlots.add(slot.id);
|
|
5872
5923
|
// v1.25 T1: audit trail for live runs — prove the dialog appeared and was answered.
|
|
@@ -6092,9 +6143,9 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
6092
6143
|
}
|
|
6093
6144
|
// OBS-548: the FOURTH leg, and the one the daemon already measured. The three legs above
|
|
6094
6145
|
// are all channel-side: they say a pane stopped talking. The tree's CPU says whether
|
|
6095
|
-
// anything is still WORKING, and the harvest triad forty lines up refuses
|
|
6096
|
-
// conclusion without it
|
|
6097
|
-
//
|
|
6146
|
+
// anything is still WORKING, and the harvest triad forty lines up refuses this
|
|
6147
|
+
// conclusion without it. Queue row 231 (D-2119): print-mode harvest additionally checks
|
|
6148
|
+
// connections because a remote model turn can have flat CPU. An UNMEASURABLE
|
|
6098
6149
|
// reading keeps the fail-open contract the probe already states (undefined is never
|
|
6099
6150
|
// evidence a worker stopped), so on a host whose `ps` cannot be read the fast-kill stands
|
|
6100
6151
|
// down and the fixed attempt ceiling bounds the hold.
|
|
@@ -6281,13 +6332,15 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
6281
6332
|
}
|
|
6282
6333
|
await noteLaunched();
|
|
6283
6334
|
// OBS-54: headless workers have the same output-inactivity budget as visible panes.
|
|
6284
|
-
//
|
|
6335
|
+
// Queue row 233 (D-2130): print progress is the driver's received-byte count, never a row
|
|
6336
|
+
// high-water over the bounded tail; a driver reporting no count keeps the row measure.
|
|
6285
6337
|
const stallWindowMs = taskTimeoutMinutes * 60_000;
|
|
6338
|
+
const hardDeadline = attemptStart + (attemptHardTimeoutMs ?? stallWindowMs * 4);
|
|
6286
6339
|
const initialPane = await workerTransport.read(slot, 500);
|
|
6287
6340
|
startupFailure = startupFailureInWindow(initialPane, workerLaunchedAt);
|
|
6288
6341
|
let everHadOutput = initialPane.length > 0;
|
|
6289
6342
|
const stallProgress = new StallProgressTracker();
|
|
6290
|
-
stallProgress.observe({ paneText: initialPane, seedSubmitted: true, contextTokens });
|
|
6343
|
+
stallProgress.observe({ paneText: initialPane, receivedBytes: (await workerTransport.transportState?.(slot))?.bytes, seedSubmitted: true, contextTokens });
|
|
6291
6344
|
let lastProgressAt = Date.now();
|
|
6292
6345
|
finished = false;
|
|
6293
6346
|
// T2 review (material): the exit marker proves the PROCESS EXITED, never that the worker
|
|
@@ -6297,10 +6350,10 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
6297
6350
|
// worker-result-harvested row. The interactive site has always kept them apart (`finished`
|
|
6298
6351
|
// there is the trailer regex; the exit marker only sets exitCode), and the cause taxonomy
|
|
6299
6352
|
// already names this shape "clean-exit-no-trailer" — unreachable in print mode until now.
|
|
6300
|
-
while (!startupFailure && Date.now() - lastProgressAt < stallWindowMs) {
|
|
6353
|
+
while (!startupFailure && Date.now() - lastProgressAt < stallWindowMs && Date.now() < hardDeadline) {
|
|
6301
6354
|
fatalStop.signal.throwIfAborted();
|
|
6302
6355
|
executionSignal()?.throwIfAborted();
|
|
6303
|
-
const remaining = stallWindowMs - (Date.now() - lastProgressAt);
|
|
6356
|
+
const remaining = Math.min(stallWindowMs - (Date.now() - lastProgressAt), hardDeadline - Date.now());
|
|
6304
6357
|
let slice = Math.min(BLOCKED_POLL_MS, Math.max(100, Math.min(stallWindowMs / 2, remaining)));
|
|
6305
6358
|
const startupLeft = workerLaunchedAt === undefined
|
|
6306
6359
|
? 0
|
|
@@ -6331,11 +6384,10 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
6331
6384
|
break;
|
|
6332
6385
|
}
|
|
6333
6386
|
await sampleContext();
|
|
6334
|
-
if (stallProgress.observe({ paneText, contextTokens }))
|
|
6387
|
+
if (stallProgress.observe({ paneText, receivedBytes: (await workerTransport.transportState?.(slot))?.bytes, contextTokens }))
|
|
6335
6388
|
lastProgressAt = Date.now();
|
|
6336
|
-
//
|
|
6337
|
-
//
|
|
6338
|
-
// window out before anything looked at its commits.
|
|
6389
|
+
// Queue row 231 (D-2119): print mode adds a fresh owned-group connection reading to the
|
|
6390
|
+
// silence/commits/CPU triad; a quiet remote model turn can hold a connection with flat CPU.
|
|
6339
6391
|
// No nudge hold here, deliberately: print mode has no nudge surface at all (no pane to
|
|
6340
6392
|
// steer, driver.nudge is never consulted on this path), so there is no pending daemon
|
|
6341
6393
|
// action for the triad to preempt — the asymmetry with the interactive call site above is
|
|
@@ -6350,7 +6402,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
6350
6402
|
// without one is finished:false with a non-null exitCode — the harvest synthesis then owns
|
|
6351
6403
|
// it when the worktree carries work, and classifyWorkerResultCause names it otherwise.
|
|
6352
6404
|
finished = new RegExp(trailerPattern(nonce)).test(output);
|
|
6353
|
-
timedOut = !processExited && !finished && Date.now() - lastProgressAt >= stallWindowMs;
|
|
6405
|
+
timedOut = !processExited && !finished && (Date.now() - lastProgressAt >= stallWindowMs || Date.now() >= hardDeadline);
|
|
6354
6406
|
}
|
|
6355
6407
|
}
|
|
6356
6408
|
finally {
|
|
@@ -7101,7 +7153,8 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
7101
7153
|
// Budget spent means the FRESH LADDER owns this failure — including a review-only one, whose
|
|
7102
7154
|
// same-channel fix retry is exactly the round the budget just declared too expensive to repeat.
|
|
7103
7155
|
const repairExhausted = repairable && !repair;
|
|
7104
|
-
|
|
7156
|
+
const fundedRepair = repair && !capStep;
|
|
7157
|
+
if (fundedRepair) {
|
|
7105
7158
|
journal.append("repair-attempt", t.id, {
|
|
7106
7159
|
repair: repairHistory.length + 1, charge: repairsDrawn + 1, of: MAX_REPAIRS,
|
|
7107
7160
|
gates: failing.map((g) => g.gate),
|
|
@@ -7126,8 +7179,13 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
7126
7179
|
});
|
|
7127
7180
|
await driver.notify(`tickmarkr ${runId}: ${t.id} escalation: ${step}`, { tier: "attention" });
|
|
7128
7181
|
}
|
|
7182
|
+
// Queue row 224(b) (D-2118): pinned tasks park on the first unfunded owned deterministic red
|
|
7183
|
+
// at or after the second, so this climb fires at most once. Unpinned tasks climb on exactly
|
|
7184
|
+
// the second owned deterministic red, even when it funds a repair.
|
|
7129
7185
|
const climbCause = repairExhausted ? "repair-exhausted"
|
|
7130
|
-
: deterministic &&
|
|
7186
|
+
: deterministic && (t.routingHints?.pin
|
|
7187
|
+
? ownedGateFailures >= 2 && !fundedRepair
|
|
7188
|
+
: ownedGateFailures === 2) ? "gate-fail" : undefined;
|
|
7131
7189
|
if (climbCause) {
|
|
7132
7190
|
const climbed = await climb(climbCause, deterministic ?? failing[0], attempt + 1);
|
|
7133
7191
|
if (climbed === "parked")
|
|
@@ -7240,6 +7298,10 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
7240
7298
|
if (fatalStop.signal.aborted)
|
|
7241
7299
|
return;
|
|
7242
7300
|
const cleanupEvidence = cleanupErrors.length ? { cleanupErrors } : {};
|
|
7301
|
+
if (err instanceof WorktreeAdminRaceError) {
|
|
7302
|
+
await park(t, err.message, "infra", null, 0, Date.now(), 0, 0, undefined, 0, "fresh", { evidence: err.evidence, ...cleanupEvidence });
|
|
7303
|
+
return;
|
|
7304
|
+
}
|
|
7243
7305
|
if (err instanceof HostDegradedError) {
|
|
7244
7306
|
await park(t, err.message, "infra", null, 0, Date.now(), 0, 0, undefined, 0, "fresh", { disposition: "host-degraded", ...cleanupEvidence });
|
|
7245
7307
|
return;
|
package/dist/run/git.d.ts
CHANGED
|
@@ -199,6 +199,11 @@ export declare function shGitOk(cmd: string, cwd: string, own?: OwnGitOptions):
|
|
|
199
199
|
export declare function gitSync(dir: string, args: readonly string[], { env }?: {
|
|
200
200
|
env?: NodeJS.ProcessEnv;
|
|
201
201
|
}): SpawnSyncReturns<string>;
|
|
202
|
+
/** Queue row 223 (D-2118): git could not read a sibling's partially written admin file during add. */
|
|
203
|
+
export declare class WorktreeAdminRaceError extends Error {
|
|
204
|
+
readonly evidence: string;
|
|
205
|
+
constructor(evidence: string);
|
|
206
|
+
}
|
|
202
207
|
/**
|
|
203
208
|
* v2.6.8 T2: the DETACHED worktree capability, declared by calling it (the daemon's baseline recapture and standalone
|
|
204
209
|
* verify's base worktree). Under the ref-store pin git stubs a detached add's new HEAD as `ref: refs/heads/.invalid`
|
package/dist/run/git.js
CHANGED
|
@@ -621,6 +621,41 @@ export function gitSync(dir, args, { env = process.env } = {}) {
|
|
|
621
621
|
pin?.release();
|
|
622
622
|
}
|
|
623
623
|
}
|
|
624
|
+
// Queue row 223 (D-2118): one process-wide administration queue; recreation and rollback retain
|
|
625
|
+
// their caller's hold, including trust rechecks and branch restoration, without queuing behind it.
|
|
626
|
+
let worktreeAdminTail = Promise.resolve();
|
|
627
|
+
const worktreeAdminHold = new AsyncLocalStorage();
|
|
628
|
+
async function withWorktreeAdmin(operation) {
|
|
629
|
+
if (worktreeAdminHold.getStore())
|
|
630
|
+
return operation();
|
|
631
|
+
const prior = worktreeAdminTail;
|
|
632
|
+
let release;
|
|
633
|
+
worktreeAdminTail = new Promise((done) => { release = done; });
|
|
634
|
+
await prior;
|
|
635
|
+
try {
|
|
636
|
+
return await worktreeAdminHold.run(true, operation);
|
|
637
|
+
}
|
|
638
|
+
finally {
|
|
639
|
+
release();
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
/** Queue row 223 (D-2118): git could not read a sibling's partially written admin file during add. */
|
|
643
|
+
export class WorktreeAdminRaceError extends Error {
|
|
644
|
+
evidence;
|
|
645
|
+
constructor(evidence) {
|
|
646
|
+
super(`worktree add encountered a sibling admin read race (queue row 223, D-2118): ${evidence}`);
|
|
647
|
+
this.evidence = evidence;
|
|
648
|
+
this.name = "WorktreeAdminRaceError";
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
const siblingAdminReadFailed = (stderr) => {
|
|
652
|
+
for (const line of stderr.split("\n")) {
|
|
653
|
+
const read = /^(?:fatal: |error: )?failed to read (?:'([^'\r\n]+)'|"([^"\r\n]+)"|([^:\r\n]+))(?::|$)/.exec(line);
|
|
654
|
+
if (read && /(?:^|\/)worktrees\/[^/\r\n]+\/(?:commondir|gitdir|HEAD)$/.test(read[1] ?? read[2] ?? read[3]))
|
|
655
|
+
return true;
|
|
656
|
+
}
|
|
657
|
+
return false;
|
|
658
|
+
};
|
|
624
659
|
/**
|
|
625
660
|
* v2.6.8 T2: the DETACHED worktree capability, declared by calling it (the daemon's baseline recapture and standalone
|
|
626
661
|
* verify's base worktree). Under the ref-store pin git stubs a detached add's new HEAD as `ref: refs/heads/.invalid`
|
|
@@ -633,6 +668,9 @@ export function gitSync(dir, args, { env = process.env } = {}) {
|
|
|
633
668
|
* back through the trusted common directory derived here once (dropWorktree), even while that commondir stays hostile.
|
|
634
669
|
*/
|
|
635
670
|
export async function addDetachedWorktree(cwd, dir, commitish, timeoutMs = DEFAULT_SHELL_TIMEOUT_MS, { logHead = false } = {}) {
|
|
671
|
+
return withWorktreeAdmin(() => addDetachedWorktreeHeld(cwd, dir, commitish, timeoutMs, logHead));
|
|
672
|
+
}
|
|
673
|
+
async function addDetachedWorktreeHeld(cwd, dir, commitish, timeoutMs, logHead) {
|
|
636
674
|
// 2.8.1 T1 (ANL-023 §7.1): `logHead` writes the new checkout's HEAD reflog whatever the repository's
|
|
637
675
|
// core.logAllRefUpdates says, so a later HEAD move by any command is appended to an established witness.
|
|
638
676
|
const git = logHead ? "git -c core.logAllRefUpdates=always" : "git";
|
|
@@ -682,6 +720,9 @@ function rollbackFailed(error, failure) {
|
|
|
682
720
|
* step bounded by `timeoutMs`; every step is tried, and any that failed is thrown, never swallowed.
|
|
683
721
|
*/
|
|
684
722
|
async function dropWorktree(store, { dir, branch }, timeoutMs = DEFAULT_SHELL_TIMEOUT_MS) {
|
|
723
|
+
return withWorktreeAdmin(() => dropWorktreeHeld(store, { dir, branch }, timeoutMs));
|
|
724
|
+
}
|
|
725
|
+
async function dropWorktreeHeld(store, { dir, branch }, timeoutMs) {
|
|
685
726
|
const failed = await withoutExecutionBudget(async () => {
|
|
686
727
|
const run = async (cmd) => {
|
|
687
728
|
try {
|
|
@@ -859,6 +900,9 @@ export function checkoutIncarnation(worktree) {
|
|
|
859
900
|
}
|
|
860
901
|
}
|
|
861
902
|
export async function createWorktree(repo, branch, baseRef) {
|
|
903
|
+
return withWorktreeAdmin(() => createWorktreeHeld(repo, branch, baseRef));
|
|
904
|
+
}
|
|
905
|
+
async function createWorktreeHeld(repo, branch, baseRef) {
|
|
862
906
|
branch = await resolveTaskBranch(repo, branch);
|
|
863
907
|
const dir = join(tickmarkrDir(repo), WORKTREES_DIR, sanitize(branch));
|
|
864
908
|
if (existsSync(dir))
|
|
@@ -866,7 +910,13 @@ export async function createWorktree(repo, branch, baseRef) {
|
|
|
866
910
|
// the worktree capability: a branch add, whose symbolic HEAD keeps the ref-store pin (see OwnGitCapability); a raced
|
|
867
911
|
// add is rolled back, its branch restored to the value read here or deleted when it is new
|
|
868
912
|
const prior = (await shGit(`git rev-parse --verify -q ${shq(`refs/heads/${branch}`)}`, repo)).stdout.trim() || undefined;
|
|
869
|
-
|
|
913
|
+
const cmd = `git worktree add -B ${shq(branch)} ${shq(dir)} ${shq(baseRef)}`;
|
|
914
|
+
const added = await shGit(cmd, repo, DEFAULT_SHELL_TIMEOUT_MS, { createsWorktree: "branch", rollback: { dir, branch: { name: branch, prior } } });
|
|
915
|
+
if (added.code !== 0) {
|
|
916
|
+
if (siblingAdminReadFailed(added.stderr))
|
|
917
|
+
throw new WorktreeAdminRaceError(added.stderr.trim());
|
|
918
|
+
throw new Error(`command failed (${added.code}): ${cmd}\n${added.stderr || added.stdout}`);
|
|
919
|
+
}
|
|
870
920
|
linkNodeModules(repo, dir);
|
|
871
921
|
return dir;
|
|
872
922
|
}
|
|
@@ -33,7 +33,7 @@ export interface RepairDispositionInput {
|
|
|
33
33
|
/** task tree plus diff, deleted tracked paths included */
|
|
34
34
|
inventory: Iterable<string>;
|
|
35
35
|
}
|
|
36
|
-
/**
|
|
36
|
+
/** Queue row 230: tsc's position, numeric or baseline-normalized (#), ends a path; named parentheses stay. */
|
|
37
37
|
export declare const namedPaths: (text: string) => string[];
|
|
38
38
|
/** Binds candidates to the SUPPLIED inventory and nothing else: an exact path, else a unique
|
|
39
39
|
* path-suffix. Anything unbound or bound twice comes back as a non-executable diagnostic. */
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { filesGlob } from "../graph/files-glob.js";
|
|
2
2
|
const failed = (g) => !(g.pass || g.meta?.skipped === true) || g.meta?.infra === true;
|
|
3
3
|
const REFUSAL_RE = /outside|out.of.scope|allowlist|scope expansion|not (?:in|own)|unowned/i;
|
|
4
|
-
/**
|
|
5
|
-
export const namedPaths = (text) => [...text.matchAll(/(?:^|[\s`'"])((?:[A-Za-z0-9_@.()[\]-]+\/)+[A-Za-z0-9_@.[\]-]+|[A-Za-z0-9_@-]+(?:\.[A-Za-z0-9_-]+)+)(?=$|[\s`'"),:;.!?])/g)]
|
|
4
|
+
/** Queue row 230: tsc's position, numeric or baseline-normalized (#), ends a path; named parentheses stay. */
|
|
5
|
+
export const namedPaths = (text) => [...text.matchAll(/(?:^|[\s`'"])((?:[A-Za-z0-9_@.()[\]-]+\/)+(?:[A-Za-z0-9_@.[\]-]|\([A-Za-z0-9_@.[\]-]*\))+|(?:[A-Za-z0-9_@-]|\([A-Za-z0-9_@.[\]-]*\))+(?:\.(?:[A-Za-z0-9_-]|\([A-Za-z0-9_@.[\]-]*\))+)+)(?=$|[\s`'"),:;.!?]|\((?:\d+|#),\s*(?:\d+|#)\))/g)]
|
|
6
6
|
.map((match) => match[1].replace(/^\.\//, "").replace(/\.$/, ""))
|
|
7
7
|
.filter((path) => !path.split("/").includes(".."));
|
|
8
8
|
/** Binds candidates to the SUPPLIED inventory and nothing else: an exact path, else a unique
|
package/dist/run/stall.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
export declare function normalizeStallSnapshot(text: string): string;
|
|
4
4
|
export interface StallProgressSample {
|
|
5
5
|
paneText: string;
|
|
6
|
+
receivedBytes?: number;
|
|
6
7
|
seedSubmitted?: boolean;
|
|
7
8
|
contextTokens?: number;
|
|
8
9
|
}
|
|
@@ -102,6 +103,7 @@ export declare const PANE_READ_ROWS = 1000;
|
|
|
102
103
|
*/
|
|
103
104
|
export declare class StallProgressTracker {
|
|
104
105
|
private transcriptRows;
|
|
106
|
+
private receivedBytes;
|
|
105
107
|
private rawWindowLines;
|
|
106
108
|
private seedSubmitted;
|
|
107
109
|
private contextTokens;
|
package/dist/run/stall.js
CHANGED
|
@@ -613,6 +613,7 @@ export const PANE_READ_ROWS = 1000;
|
|
|
613
613
|
*/
|
|
614
614
|
export class StallProgressTracker {
|
|
615
615
|
transcriptRows = 0; // non-empty high-water over the bounded read — the growth signal
|
|
616
|
+
receivedBytes = 0;
|
|
616
617
|
rawWindowLines = 0; // raw-line high-water — the SATURATION signal (see the getter)
|
|
617
618
|
seedSubmitted = false;
|
|
618
619
|
contextTokens;
|
|
@@ -654,6 +655,14 @@ export class StallProgressTracker {
|
|
|
654
655
|
rowsAdvanced = true;
|
|
655
656
|
this.rowGrowthAt = now; // raw signal — advances even when the report below is suppressed
|
|
656
657
|
}
|
|
658
|
+
// Queue row 233 (D-2130): print-mode progress is the driver's received-byte count, never a
|
|
659
|
+
// row high-water over the bounded tail. A driver reporting no count keeps the row measure.
|
|
660
|
+
const bytes = sample.receivedBytes;
|
|
661
|
+
let outputAdvanced = rowsAdvanced;
|
|
662
|
+
if (bytes !== undefined && Number.isFinite(bytes) && bytes >= 0) {
|
|
663
|
+
outputAdvanced = bytes > this.receivedBytes;
|
|
664
|
+
this.receivedBytes = Math.max(this.receivedBytes, bytes);
|
|
665
|
+
}
|
|
657
666
|
let seedAdvanced = false;
|
|
658
667
|
if (sample.seedSubmitted && !this.seedSubmitted) {
|
|
659
668
|
this.seedSubmitted = true;
|
|
@@ -674,10 +683,9 @@ export class StallProgressTracker {
|
|
|
674
683
|
}
|
|
675
684
|
if (tokensAdvanced || seedAdvanced)
|
|
676
685
|
return true;
|
|
677
|
-
if (
|
|
678
|
-
//
|
|
679
|
-
//
|
|
680
|
-
// (unmetered adapter) keeps the old row-growth behavior.
|
|
686
|
+
if (outputAdvanced) {
|
|
687
|
+
// Queue row 233 (D-2130): both output measures obey the existing flat-token cap;
|
|
688
|
+
// an unmetered adapter can keep re-arming on output indefinitely, up to its hard deadline.
|
|
681
689
|
if (this.lastTokenGrowthAt !== undefined && now - this.lastTokenGrowthAt >= rowRearmTokenFlatMs)
|
|
682
690
|
return false;
|
|
683
691
|
return true;
|
package/package.json
CHANGED
|
@@ -20,16 +20,18 @@ Keep `name: tickmarkr-auto` and the installed links to canonical `skills/` intac
|
|
|
20
20
|
|
|
21
21
|
## Two-tier by default — role check before the loop
|
|
22
22
|
|
|
23
|
+
`<label>` means the version only in the tickmarkr source repository (`package.json` named `tickmarkr`); elsewhere it means the mission or run, never the installed binary version.
|
|
24
|
+
|
|
23
25
|
When working in a multi-agent terminal environment (herdr or Orca), decide your role before starting:
|
|
24
26
|
|
|
25
|
-
- **Orchestrator:** your session was started to execute the mission. Rename your own tab/pane `ORCH · <
|
|
27
|
+
- **Orchestrator:** your session was started to execute the mission. Rename your own tab/pane `ORCH · <label>` (short labels: ≤20 chars, `ROLE · token`) and run the loop below.
|
|
26
28
|
- **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.
|
|
27
|
-
- **Primary session without an orchestrator:** rename your own tab `OVERSEER · <
|
|
28
|
-
- **On herdr (`HERDR_ENV=1`)**: **spawning on current herdr is two-step** — the one-shot `agent start --cwd/--tab/--no-focus` form was removed in the herdr CLI redesign and now fails with `unknown option` (OBS-138). First create the pane: `herdr tab create --workspace <ws> --cwd <repo> --label "ORCH · <
|
|
29
|
+
- **Primary session without an orchestrator:** rename your own tab `OVERSEER · <label>` and your agent `overseer`, spawn one child orchestration session with your host's launch form, label its tab `ORCH · <label>` 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.
|
|
30
|
+
- **On herdr (`HERDR_ENV=1`)**: **spawning on current herdr is two-step** — the one-shot `agent start --cwd/--tab/--no-focus` form was removed in the herdr CLI redesign and now fails with `unknown option` (OBS-138). First create the pane: `herdr tab create --workspace <ws> --cwd <repo> --label "ORCH · <label>"` (tab create does not steal focus unless `--focus` is passed; parse `result.root_pane.pane_id` from its JSON), then start the agent in it:
|
|
29
31
|
- **Claude Code:** `herdr agent start orchestrator --kind claude --pane <root-pane-id> -- --permission-mode bypassPermissions`
|
|
30
32
|
- **Codex:** `herdr agent start orchestrator --kind codex --pane <root-pane-id> -- --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.
|
|
31
33
|
- **Auxiliary seats run as the CLI's interactive TUI in their visible pane — never headless** (`claude -p` / `codex exec`): headless buffers output until exit so the pane renders idle for the entire run, is blind to SessionStart hook errors (a broken and a fixed hook both return green), and gives a stall watcher no midpoint — silent-time equals lifetime. Headless is for exit-code probes only (a quota check that wants `rc`), never for work anyone must watch.
|
|
32
|
-
- **On Orca (`TERM_PROGRAM=Orca` and non-empty `ORCA_TERMINAL_HANDLE`)**: spawn it with `orca terminal create --worktree path:<repo> --title "ORCH · <
|
|
34
|
+
- **On Orca (`TERM_PROGRAM=Orca` and non-empty `ORCA_TERMINAL_HANDLE`)**: spawn it with `orca terminal create --worktree path:<repo> --title "ORCH · <label>" --command "<agent-cmd>" --json` and parse `result.terminal.handle` from the create receipt. **Auxiliary seats run as the CLI's interactive TUI in their visible terminal — never headless**, for the reasons the herdr branch gives.
|
|
33
35
|
- **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.
|
|
34
36
|
|
|
35
37
|
With neither herdr nor Orca there is no role to decide and no seat to spawn: follow [On a plain host (neither herdr nor Orca)](#on-a-plain-host-neither-herdr-nor-orca) and run the loop directly, single-tier.
|
|
@@ -83,11 +85,11 @@ Run every requested target in order without seeking routine confirmation. Stop o
|
|
|
83
85
|
|
|
84
86
|
## Binary preflight (before compile or run)
|
|
85
87
|
|
|
86
|
-
Before `tickmarkr compile` or `tickmarkr run
|
|
88
|
+
Before `tickmarkr compile` or `tickmarkr run`:
|
|
87
89
|
|
|
88
|
-
1. Run `tickmarkr version`
|
|
89
|
-
2.
|
|
90
|
-
3. If the binary and repository do not **agree on the entire version** (including the patch; e.g. binary `2.1.0` vs repo `2.1.1`), **stop immediately** and tell the operator to update the global install (`npm i -g tickmarkr@latest`), or to install this repository's build as a REAL COPY — `npm pack`, then `npm i -g ./<tarball>`. Do not compile, plan, or run on hope.
|
|
90
|
+
1. Run `tickmarkr version --check` and **stop immediately on a non-zero exit**.
|
|
91
|
+
2. In a consumer repository, it compares installed agent guidance with this binary's packaged block and skills, never the app's manifest version. Refresh drifted guidance with `tickmarkr init --agent --force`.
|
|
92
|
+
3. Only in the tickmarkr source repository (`package.json` named `tickmarkr`), the check compares its version with the binary. If the binary and repository do not **agree on the entire version** (including the patch; e.g. binary `2.1.0` vs repo `2.1.1`), **stop immediately** and tell the operator to update the global install (`npm i -g tickmarkr@latest`), or to install this repository's build as a REAL COPY — `npm pack`, then `npm i -g ./<tarball>`. Do not compile, plan, or run on hope.
|
|
91
93
|
> ⚠ **Never `npm i -g .` on the repository directory, and never link it.** npm SYMLINKS a directory install, which makes the working tree itself the machine-wide binary: every later build — including a gate's own `npm run build` — silently hot-swaps the CLI for every repository on the machine, with no version change to notice it by. Measured 2026-08-29: a verify build gate rewrote the shared binary while another repository's daemon was mid-run against it, and a positive control that rebuilds at a pre-fix ref would have installed the very defect it was proving fixed, machine-wide (OBS-771). Verify an install by comparing the global and repo **inodes** — they must DIFFER — never by `tickmarkr version`, which cannot go red when nothing is bumped.
|
|
92
94
|
|
|
93
95
|
A stale binary silently skips daemon gates shipped in newer releases — the v1.38 run exposed this when a global `1.36.0` binary missed the daemon tip-verify gate entirely (OBS-38). Preflight failure is always stop-and-report; never proceed-and-hope.
|
|
@@ -9,16 +9,18 @@ Use this for any spec that `tickmarkr compile` accepts. It is SDD-agnostic: use
|
|
|
9
9
|
|
|
10
10
|
## Two-tier by default — role check before the loop
|
|
11
11
|
|
|
12
|
+
`<label>` means the version only in the tickmarkr source repository (`package.json` named `tickmarkr`); elsewhere it means the mission or run, never the installed binary version.
|
|
13
|
+
|
|
12
14
|
When working in a multi-agent terminal environment (herdr or Orca), decide your role before starting:
|
|
13
15
|
|
|
14
|
-
- **Orchestrator:** your session was started to execute the mission. Rename your own tab/pane `ORCH · <
|
|
16
|
+
- **Orchestrator:** your session was started to execute the mission. Rename your own tab/pane `ORCH · <label>` (short labels: ≤20 chars, `ROLE · token`) and run the loop below.
|
|
15
17
|
- **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
|
-
- **Primary session without an orchestrator:** rename your own tab `OVERSEER · <
|
|
17
|
-
- **On herdr (`HERDR_ENV=1`)**: **spawning on current herdr is two-step** — the one-shot `agent start --cwd/--tab/--no-focus` form was removed in the herdr CLI redesign and now fails with `unknown option` (OBS-138). First create the pane: `herdr tab create --workspace <ws> --cwd <repo> --label "ORCH · <
|
|
18
|
+
- **Primary session without an orchestrator:** rename your own tab `OVERSEER · <label>` and your agent `overseer`, spawn one child orchestration session with your host's launch form, label its tab `ORCH · <label>` 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.
|
|
19
|
+
- **On herdr (`HERDR_ENV=1`)**: **spawning on current herdr is two-step** — the one-shot `agent start --cwd/--tab/--no-focus` form was removed in the herdr CLI redesign and now fails with `unknown option` (OBS-138). First create the pane: `herdr tab create --workspace <ws> --cwd <repo> --label "ORCH · <label>"` (tab create does not steal focus unless `--focus` is passed; parse `result.root_pane.pane_id` from its JSON), then start the agent in it:
|
|
18
20
|
- **Claude Code:** `herdr agent start orchestrator --kind claude --pane <root-pane-id> -- --permission-mode bypassPermissions`
|
|
19
21
|
- **Codex:** `herdr agent start orchestrator --kind codex --pane <root-pane-id> -- --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
22
|
- **Auxiliary seats run as the CLI's interactive TUI in their visible pane — never headless** (`claude -p` / `codex exec`): headless buffers output until exit so the pane renders idle for the entire run, is blind to SessionStart hook errors (a broken and a fixed hook both return green), and gives a stall watcher no midpoint — silent-time equals lifetime. Headless is for exit-code probes only (a quota check that wants `rc`), never for work anyone must watch.
|
|
21
|
-
- **On Orca (`TERM_PROGRAM=Orca` and non-empty `ORCA_TERMINAL_HANDLE`)**: spawn it with `orca terminal create --worktree path:<repo> --title "ORCH · <
|
|
23
|
+
- **On Orca (`TERM_PROGRAM=Orca` and non-empty `ORCA_TERMINAL_HANDLE`)**: spawn it with `orca terminal create --worktree path:<repo> --title "ORCH · <label>" --command "<agent-cmd>" --json` and parse `result.terminal.handle` from the create receipt. **Auxiliary seats run as the CLI's interactive TUI in their visible terminal — never headless**, for the reasons the herdr branch gives.
|
|
22
24
|
- **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.
|
|
23
25
|
|
|
24
26
|
With neither herdr nor Orca there is no role to decide and no seat to spawn: follow [On a plain host (neither herdr nor Orca)](#on-a-plain-host-neither-herdr-nor-orca) and run the loop directly, single-tier.
|
|
@@ -69,11 +71,11 @@ Proceed through the loop without seeking routine confirmation. Stop only for a b
|
|
|
69
71
|
|
|
70
72
|
## Binary preflight (before compile or run)
|
|
71
73
|
|
|
72
|
-
Before `tickmarkr compile` or `tickmarkr run
|
|
74
|
+
Before `tickmarkr compile` or `tickmarkr run`:
|
|
73
75
|
|
|
74
|
-
1. Run `tickmarkr version`
|
|
75
|
-
2.
|
|
76
|
-
3. If the binary and repository do not **agree on the entire version** (including the patch; e.g. binary `2.1.0` vs repo `2.1.1`), **stop immediately** and tell the operator to update the global install (`npm i -g tickmarkr@latest`), or to install this repository's build as a REAL COPY — `npm pack`, then `npm i -g ./<tarball>`. Do not compile, plan, or run on hope.
|
|
76
|
+
1. Run `tickmarkr version --check` and **stop immediately on a non-zero exit**.
|
|
77
|
+
2. In a consumer repository, it compares installed agent guidance with this binary's packaged block and skills, never the app's manifest version. Refresh drifted guidance with `tickmarkr init --agent --force`.
|
|
78
|
+
3. Only in the tickmarkr source repository (`package.json` named `tickmarkr`), the check compares its version with the binary. If the binary and repository do not **agree on the entire version** (including the patch; e.g. binary `2.1.0` vs repo `2.1.1`), **stop immediately** and tell the operator to update the global install (`npm i -g tickmarkr@latest`), or to install this repository's build as a REAL COPY — `npm pack`, then `npm i -g ./<tarball>`. Do not compile, plan, or run on hope.
|
|
77
79
|
> ⚠ **Never `npm i -g .` on the repository directory, and never link it.** npm SYMLINKS a directory install, which makes the working tree itself the machine-wide binary: every later build — including a gate's own `npm run build` — silently hot-swaps the CLI for every repository on the machine, with no version change to notice it by. Measured 2026-08-29: a verify build gate rewrote the shared binary while another repository's daemon was mid-run against it, and a positive control that rebuilds at a pre-fix ref would have installed the very defect it was proving fixed, machine-wide (OBS-771). Verify an install by comparing the global and repo **inodes** — they must DIFFER — never by `tickmarkr version`, which cannot go red when nothing is bumped.
|
|
78
80
|
|
|
79
81
|
A stale binary silently skips daemon gates shipped in newer releases — the v1.38 run exposed this when a global `1.36.0` binary missed the daemon tip-verify gate entirely (OBS-38). Preflight failure is always stop-and-report; never proceed-and-hope.
|
|
@@ -5,6 +5,8 @@ description: "Use when the user asks to oversee/supervise/babysit an autonomous
|
|
|
5
5
|
|
|
6
6
|
# Overseer (tickmarkr)
|
|
7
7
|
|
|
8
|
+
`<label>` means the version only in the tickmarkr source repository (`package.json` named `tickmarkr`); elsewhere it means the mission or run, never the installed binary version.
|
|
9
|
+
|
|
8
10
|
Become the OVERSEER for this workspace or session. Do no heavy work directly — build and supervise a two-tier
|
|
9
11
|
hierarchy of VISIBLE agents (you → orchestrator → tickmarkr's own worker fleet), and route human decisions
|
|
10
12
|
to the user with evidence.
|
|
@@ -203,15 +205,15 @@ replaces the two-tier setup.
|
|
|
203
205
|
**Live tab labels (standing operator rule, 2026-07-12):** on every decision or state change (role
|
|
204
206
|
handoff, task done/merged, run end) rename the affected tabs — and keep labels SHORT: the role as the
|
|
205
207
|
main name plus at most ONE hot-state token. Vocabulary: ORCH carries the milestone and progress
|
|
206
|
-
fraction (`ORCH ·
|
|
208
|
+
fraction (`ORCH · <label> 4/5`, updated on every task-done); tickmarkr opens ONE TAB PER TASK, labelled
|
|
207
209
|
with the task id and holding that task's worker plus its judge/review/consult panes (tickmarkr
|
|
208
210
|
updates it). Never long context strings or ✓-chains.
|
|
209
|
-
- **On Orca (`TERM_PROGRAM=Orca` and non-empty `ORCA_TERMINAL_HANDLE`)**: Do not load the `herdr` skill and do not map a Herdr workspace. Work from the current Orca terminal context. Keep the overseer in the launching terminal, name it in the same act — `orca terminal rename --terminal "$ORCA_TERMINAL_HANDLE" --title "OVERSEER · <
|
|
211
|
+
- **On Orca (`TERM_PROGRAM=Orca` and non-empty `ORCA_TERMINAL_HANDLE`)**: Do not load the `herdr` skill and do not map a Herdr workspace. Work from the current Orca terminal context. Keep the overseer in the launching terminal, name it in the same act — `orca terminal rename --terminal "$ORCA_TERMINAL_HANDLE" --title "OVERSEER · <label>"` (tab title; see the seat-name law under Seat-spawn recipes) — and inspect terminals with `orca terminal list --json`. Launch the daemon detached at the orchestrator terminal's recorded handle using the detached launch below — no pane of its own; its self-placed watch board stays in the ORCH tab.
|
|
210
212
|
2. **Orchestrator**: First consume a successful `tickmarkr fleet --pick consult` in the seat repository, per the Fleet selection contract below; then launch the returned adapter/model with your agent host.
|
|
211
|
-
- **On herdr (`HERDR_ENV=1`)**: Spawning on current herdr is two-step — the one-shot `agent start --cwd` form was removed in the herdr CLI redesign and now fails with `unknown option` (OBS-138): first create the pane with `herdr tab create --workspace <ws> --cwd <repo> --label "ORCH · <
|
|
213
|
+
- **On herdr (`HERDR_ENV=1`)**: Spawning on current herdr is two-step — the one-shot `agent start --cwd` form was removed in the herdr CLI redesign and now fails with `unknown option` (OBS-138): first create the pane with `herdr tab create --workspace <ws> --cwd <repo> --label "ORCH · <label>"` and parse `result.root_pane.pane_id` from its JSON, then start the agent in it. For Claude Code, use `herdr agent start orchestrator --kind claude --pane <root-pane-id> -- --permission-mode bypassPermissions` (append `--model <picked-model>` after the `--` from the Fleet receipt). For Codex, use `herdr agent start orchestrator --kind codex --pane <root-pane-id> -- --dangerously-bypass-approvals-and-sandbox` (add `--model <picked-model>` from the Fleet receipt). 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 --settings '{"promptSuggestionEnabled":false}'`. **For a codex consultant, use `-a never --sandbox workspace-write` — NOT `--sandbox read-only`.** ⚠ **`--sandbox read-only` CONTRADICTS this skill's own completion protocol and will hang the seat.** Every seat you spawn is told to deliver an ARTIFACT ending in a terminal MARKER, because that is the only completion signal the artifact watcher can key on (`done` is turn end). A read-only sandbox cannot write that artifact, so codex blocks on `Would you like to make the following edits?` for its OWN report — and the report exists ONLY in the pending edit, so abandoning the prompt destroys the work rather than merely delaying it. Measured 2026-08-28: a consultant spawned `--sandbox read-only` finished a 14,604-byte verdict, sat blocked on the write, and the operator saw the prompt before the supervising tier did. `read-only` is correct ONLY for a seat that writes nothing at all — which, under the artifact+marker rule, is no seat this skill tells you to spawn. When the prompt does appear, answer **"Yes, and don't ask again for these files"** rather than plain yes: plain yes re-blocks on the next write of the same file. **That `--settings` pair is not cosmetic and it is not optional:** claude-code's AUTOSUGGEST renders context-plausible ghost text into an idle seat's prompt line that is BYTE-IDENTICAL to a typed draft in text-format reads (OBS-482), so a supervising tier cannot tell a seat's own unsent work from a rendering artifact without `agent read --format ansi`. Turning the suggester off at spawn removes the ambiguity at its source instead of paying for the discrimination at every read. Verified against the shipped binary: `claude --settings '{"promptSuggestionEnabled":false}' -p …` exits 0 with a real response, and the key appears in the binary's own settings schema. **For kimi, pass `-y`** (`herdr agent start <name> --kind kimi --pane <id> -- -y`) — the adapter already launches its own workers that way (`src/adapters/kimi.ts:204`), and a kimi seat spawned without it sits on an approval prompt having done nothing. **Herdr cannot see that state**: it reports a kimi pane as `agent_status: working` with `screen_detection_skipped: true` while the prompt is up, so the BLOCKED-STATE watcher below is blind on this vendor and the spawn flag is the ONLY control. Every vendor you spawn needs its auto-approve form named here; a vendor absent from this list is a seat that will hang.
|
|
212
214
|
- **On Orca (`TERM_PROGRAM=Orca` and non-empty `ORCA_TERMINAL_HANDLE`)**: Seats spawn with `orca terminal create` on a path worktree selector with a command:
|
|
213
|
-
`orca terminal create --worktree path:<repo> --title "ORCH · <
|
|
214
|
-
For Claude Code: `orca terminal create --worktree path:<repo> --title "ORCH · <
|
|
215
|
+
`orca terminal create --worktree path:<repo> --title "ORCH · <label>" --command "<agent-cmd>" --json`
|
|
216
|
+
For Claude Code: `orca terminal create --worktree path:<repo> --title "ORCH · <label>" --command "claude --model <picked-model> --permission-mode bypassPermissions --settings '{\"promptSuggestionEnabled\":false}'" --json`. For Codex: `orca terminal create --worktree path:<repo> --title "ORCH · <label>" --command "codex --model <picked-model> --dangerously-bypass-approvals-and-sandbox" --json`. Parse `result.terminal.handle` from the create receipt.
|
|
215
217
|
Require the orchestrator to use the host-owned daemon and detached-beat lifecycle below for both run and resume, and to report the lock pid ancestry check.
|
|
216
218
|
3. **Standing instructions travel as a brief FILE, never as pane text** — PTY input truncates at ~1024B and a
|
|
217
219
|
truncated brief silently drops policy. Write the full brief to `<repo>/.tickmarkr/overseer/ORCH-BRIEF.md`
|
|
@@ -221,7 +223,7 @@ replaces the two-tier setup.
|
|
|
221
223
|
- **On Orca (`TERM_PROGRAM=Orca` and non-empty `ORCA_TERMINAL_HANDLE`)**: Briefs travel as a file announced by `terminal send` with enter and a submit wait whose receipt is read for observed submission rather than input acceptance:
|
|
222
224
|
`orca terminal send --terminal <handle> --text "Read .tickmarkr/overseer/ORCH-BRIEF.md and follow it exactly." --enter --wait-submit 15 --json`
|
|
223
225
|
Read `result.send.prompt.stages` and treat the brief as delivered only when a `turn_started` stage is present; `accepted: true` proves input acceptance, not a started turn; never resend on `accepted` alone.
|
|
224
|
-
The brief MUST contain: the mission, the five-tab canon (on herdr) or seat layout (on Orca), the pane mechanics below, rules 1–2, the GSD-leg rules
|
|
226
|
+
The brief MUST contain: **Seat hygiene — in a multi-tier session the tiers clear each other at 50% context or more after writing a handoff; a new spec or run is a mission boundary, so hand off and clear before the next mission; copy this duty into every brief and handoff; on a single-tier plain host write your handoff and ask the operator to clear the session**, then the mission, the five-tab canon (on herdr) or seat layout (on Orca), the pane mechanics below, rules 1–2, the GSD-leg rules
|
|
225
227
|
(below) whenever the mission dispatches `/gsd:*` legs, and require a verbatim one-sentence
|
|
226
228
|
acknowledgment of the human-checkpoint rule before anything is dispatched.
|
|
227
229
|
*method guards* in that brief (how to know a thing, not what is true of this spec), and deleting them
|
|
@@ -329,7 +331,7 @@ Kimi `-y`, artifact plus terminal marker, and verified file-brief delivery. Flee
|
|
|
329
331
|
a `git worktree add` the overseer made for a spec branch, a throwaway clone — `terminal create` hangs and
|
|
330
332
|
returns "Timed out waiting for terminal handle after creation". The lawful form for such a checkout is a
|
|
331
333
|
terminal in a managed worktree with a cwd change in the command:
|
|
332
|
-
`orca terminal create --worktree path:<managed-repo> --title "ORCH · <
|
|
334
|
+
`orca terminal create --worktree path:<managed-repo> --title "ORCH · <label>" --command "cd <checkout> && <agent-cmd>" --json`.
|
|
333
335
|
**An Orca-hosted run's checkout is created by Orca** (`orca worktree create --repo id:<repo> --base-branch <spec-branch>`),
|
|
334
336
|
never by `git worktree add`: the orca driver asks `orca worktree current` from inside every task worktree and refuses
|
|
335
337
|
(`selector_not_found`) on an unmanaged checkout — every task fails pre-launch and the board cannot place (OBS-1061,
|
|
@@ -353,7 +355,7 @@ Kimi `-y`, artifact plus terminal marker, and verified file-brief delivery. Flee
|
|
|
353
355
|
Code's turn summary, Codex's last prompt) and is NOT the seat's name. This is vendor-neutral by
|
|
354
356
|
construction — no per-vendor env var, no agent command, no launch-form ritual — which is what makes
|
|
355
357
|
it the same law as Herdr's tab label. So: **the overseer names its OWN seat at Setup 1** with
|
|
356
|
-
`orca terminal rename --terminal "$ORCA_TERMINAL_HANDLE" --title "OVERSEER · <
|
|
358
|
+
`orca terminal rename --terminal "$ORCA_TERMINAL_HANDLE" --title "OVERSEER · <label>"` (the seat
|
|
357
359
|
it was invoked in is the one seat nobody else launches), every seat it spawns is named by
|
|
358
360
|
`create --title`, and a live-label change (progress fraction, hot-state token) is `rename` again.
|
|
359
361
|
**Prove a name by the field that holds:** `tabs[].title` in
|