tickmarkr 1.37.0 → 1.40.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/README.md +25 -23
- package/dist/adapters/claude-code.js +4 -4
- package/dist/adapters/codex.js +1 -1
- package/dist/adapters/fake.d.ts +2 -2
- package/dist/adapters/fake.js +29 -11
- package/dist/adapters/grok.js +5 -5
- package/dist/adapters/model-lints.d.ts +4 -4
- package/dist/adapters/model-lints.js +1 -1
- package/dist/adapters/prompt.js +8 -13
- package/dist/adapters/registry.d.ts +8 -8
- package/dist/adapters/registry.js +17 -17
- package/dist/adapters/types.d.ts +4 -4
- package/dist/adapters/types.js +3 -2
- package/dist/brand.d.ts +2 -0
- package/dist/brand.js +17 -0
- package/dist/cli/commands/approve.js +3 -3
- package/dist/cli/commands/compile.js +1 -1
- package/dist/cli/commands/doctor.js +3 -3
- package/dist/cli/commands/init.js +57 -19
- package/dist/cli/commands/plan.js +12 -1
- package/dist/cli/commands/profile.js +6 -6
- package/dist/cli/commands/resume.d.ts +4 -1
- package/dist/cli/commands/resume.js +4 -1
- package/dist/cli/commands/run.d.ts +4 -1
- package/dist/cli/commands/run.js +9 -1
- package/dist/cli/commands/status.js +2 -1
- package/dist/cli/commands/unlock.js +1 -1
- package/dist/cli/commands/version.d.ts +1 -0
- package/dist/cli/commands/version.js +8 -0
- package/dist/cli/index.d.ts +6 -3
- package/dist/cli/index.js +13 -18
- package/dist/compile/gsd.js +1 -1
- package/dist/compile/index.js +9 -3
- package/dist/compile/native.d.ts +2 -0
- package/dist/compile/native.js +12 -4
- package/dist/config/config.d.ts +9 -6
- package/dist/config/config.js +15 -13
- package/dist/drivers/herdr.js +5 -5
- package/dist/drivers/index.d.ts +2 -2
- package/dist/drivers/subprocess.js +12 -5
- package/dist/drivers/types.js +4 -6
- package/dist/gates/acceptance.d.ts +1 -0
- package/dist/gates/acceptance.js +25 -5
- package/dist/gates/baseline.d.ts +2 -2
- package/dist/gates/llm.js +12 -9
- package/dist/gates/review.d.ts +2 -2
- package/dist/gates/review.js +27 -8
- package/dist/gates/run-gates.d.ts +2 -2
- package/dist/gates/run-gates.js +2 -2
- package/dist/graph/graph.d.ts +2 -2
- package/dist/graph/graph.js +4 -16
- package/dist/graph/schema.d.ts +2 -0
- package/dist/graph/schema.js +16 -1
- package/dist/plan/prompt.js +1 -1
- package/dist/plan/scope.d.ts +2 -2
- package/dist/plan/scope.js +5 -5
- package/dist/route/preference.d.ts +4 -4
- package/dist/route/router.d.ts +3 -3
- package/dist/run/consult.d.ts +5 -2
- package/dist/run/consult.js +31 -7
- package/dist/run/daemon.js +31 -18
- package/dist/run/git.d.ts +1 -1
- package/dist/run/git.js +5 -9
- package/dist/run/journal.d.ts +2 -2
- package/dist/run/journal.js +4 -4
- package/dist/run/lock.js +11 -11
- package/dist/run/merge.d.ts +2 -2
- package/dist/run/merge.js +3 -3
- package/dist/run/reconcile.js +1 -1
- package/package.json +2 -2
- package/schema/rungraph.schema.json +4 -0
- package/skills/tickmarkr-auto/SKILL.md +23 -2
- package/skills/tickmarkr-loop/SKILL.md +23 -2
package/dist/plan/scope.js
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "no
|
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { basename, dirname, extname, join } from "node:path";
|
|
4
4
|
import { discoverChannels, getAdapter, probeAll } from "../adapters/registry.js";
|
|
5
|
-
import { compileNative,
|
|
5
|
+
import { compileNative, LEGACY_PREFIX } from "../compile/native.js";
|
|
6
6
|
import { pickDriver } from "../drivers/index.js";
|
|
7
7
|
import { extractJson, runLlm } from "../gates/llm.js";
|
|
8
8
|
import { TaskSchema } from "../graph/schema.js";
|
|
@@ -48,7 +48,7 @@ function section(source, name) {
|
|
|
48
48
|
return lines.slice(start + 1, end === -1 ? undefined : end).join("\n");
|
|
49
49
|
}
|
|
50
50
|
function extractDraft(raw) {
|
|
51
|
-
const clean = raw.replace(/\
|
|
51
|
+
const clean = raw.replace(/\nTICKMARKR_EXIT:\d[\s\S]*$/, "").trim();
|
|
52
52
|
try {
|
|
53
53
|
const value = JSON.parse(clean);
|
|
54
54
|
if (typeof value === "string")
|
|
@@ -64,18 +64,18 @@ function extractDraft(raw) {
|
|
|
64
64
|
const fenced = [...clean.matchAll(/```(?:markdown|md)?\s*\n([\s\S]*?)```/gi)].at(-1)?.[1];
|
|
65
65
|
if (fenced)
|
|
66
66
|
return fenced;
|
|
67
|
-
const marker = clean.search(
|
|
67
|
+
const marker = clean.search(new RegExp(`<!--\\s*(?:tickmarkr|${LEGACY_PREFIX}):spec`));
|
|
68
68
|
return (marker === -1 ? clean : clean.slice(marker)).trimEnd() + "\n";
|
|
69
69
|
}
|
|
70
70
|
function validateDraft(draft) {
|
|
71
|
-
if (
|
|
71
|
+
if (!/^<!--\s*tickmarkr:spec/.test(draft))
|
|
72
72
|
throw new Error("draft is missing the tickmarkr native marker");
|
|
73
73
|
if (!section(draft, "Assumptions").trim())
|
|
74
74
|
throw new Error("draft is missing explicit assumptions");
|
|
75
75
|
const requirements = [...new Set(section(draft, "Requirements").match(/\bREQ-\d{2}\b/g) ?? [])];
|
|
76
76
|
if (!requirements.length)
|
|
77
77
|
throw new Error("draft has no REQ-nn requirements");
|
|
78
|
-
const dir = mkdtempSync(join(tmpdir(), "
|
|
78
|
+
const dir = mkdtempSync(join(tmpdir(), "tickmarkr-scope-compile-"));
|
|
79
79
|
const file = join(dir, "draft.spec.md");
|
|
80
80
|
try {
|
|
81
81
|
writeFileSync(file, draft);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { type AuthHealth } from "../adapters/types.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { TickmarkrConfig } from "../config/config.js";
|
|
3
3
|
export interface Disallowed {
|
|
4
4
|
by: "deny" | "allow";
|
|
5
5
|
entry: string;
|
|
6
6
|
}
|
|
7
|
-
export declare function excludedChannels(cfg:
|
|
7
|
+
export declare function excludedChannels(cfg: TickmarkrConfig, adapters: {
|
|
8
8
|
id: string;
|
|
9
9
|
}[] | string[], health: Record<string, AuthHealth>): {
|
|
10
10
|
key: string;
|
|
@@ -17,11 +17,11 @@ export declare function exclusionLine(excluded: {
|
|
|
17
17
|
export declare function preferRanks(c: {
|
|
18
18
|
adapter: string;
|
|
19
19
|
model: string;
|
|
20
|
-
}, cfg:
|
|
20
|
+
}, cfg: TickmarkrConfig): {
|
|
21
21
|
shape: string;
|
|
22
22
|
rank: number;
|
|
23
23
|
}[];
|
|
24
24
|
export declare function disallowedBy(c: {
|
|
25
25
|
adapter: string;
|
|
26
26
|
model: string;
|
|
27
|
-
}, routing:
|
|
27
|
+
}, routing: TickmarkrConfig["routing"]): Disallowed | null;
|
package/dist/route/router.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Assignment, type BillingChannel } from "../adapters/types.js";
|
|
2
|
-
import { type
|
|
2
|
+
import { type TickmarkrConfig } from "../config/config.js";
|
|
3
3
|
import type { Task } from "../graph/schema.js";
|
|
4
4
|
import { type RoutingProfile } from "./profile.js";
|
|
5
5
|
export type LadderStep = "retry" | "escalate" | "consult" | "human";
|
|
@@ -30,5 +30,5 @@ export declare class RoutingError extends Error {
|
|
|
30
30
|
constructor(msg: string);
|
|
31
31
|
}
|
|
32
32
|
export declare function marginalCostRank(c: BillingChannel): number;
|
|
33
|
-
export declare function route(task: Task, cfg:
|
|
34
|
-
export declare function nextChannel(current: Assignment, task: Task, cfg:
|
|
33
|
+
export declare function route(task: Task, cfg: TickmarkrConfig, channels: BillingChannel[], profile?: RoutingProfile, preferCtx?: RoutingPreferContext): Route;
|
|
34
|
+
export declare function nextChannel(current: Assignment, task: Task, cfg: TickmarkrConfig, channels: BillingChannel[], tried: string[], profile?: RoutingProfile): Assignment | null;
|
package/dist/run/consult.d.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import type { WorkerAdapter } from "../adapters/types.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { TickmarkrConfig } from "../config/config.js";
|
|
3
3
|
import type { ExecutorDriver, Slot } from "../drivers/types.js";
|
|
4
4
|
import type { GateResult } from "../gates/types.js";
|
|
5
5
|
export interface ConsultVerdict {
|
|
6
6
|
action: "retry" | "reroute" | "decompose" | "human";
|
|
7
7
|
notes: string;
|
|
8
|
+
reason?: string;
|
|
9
|
+
guidance?: string;
|
|
8
10
|
excludeAdapter?: string;
|
|
9
11
|
}
|
|
12
|
+
export declare function renderRetryGuidance(v: ConsultVerdict): string;
|
|
10
13
|
export interface Dossier {
|
|
11
14
|
taskId: string;
|
|
12
15
|
trigger: string;
|
|
@@ -16,7 +19,7 @@ export interface Dossier {
|
|
|
16
19
|
gates: GateResult[];
|
|
17
20
|
}
|
|
18
21
|
export declare function buildDossierPrompt(d: Dossier): string;
|
|
19
|
-
export declare function consult(d: Dossier, cfg:
|
|
22
|
+
export declare function consult(d: Dossier, cfg: TickmarkrConfig, adapters: WorkerAdapter[], driver: ExecutorDriver, cwd: string, runDir: string, opts?: {
|
|
20
23
|
keep?: boolean;
|
|
21
24
|
onSlot?: (slot: Slot) => void;
|
|
22
25
|
runId?: string;
|
package/dist/run/consult.js
CHANGED
|
@@ -3,9 +3,24 @@ import { join } from "node:path";
|
|
|
3
3
|
import { getAdapter } from "../adapters/registry.js";
|
|
4
4
|
import { extractJson, gatePaneName } from "../gates/llm.js";
|
|
5
5
|
import { sh } from "./git.js";
|
|
6
|
+
const MAX_RETRY_GUIDANCE_LINES = 10;
|
|
7
|
+
function guidanceParts(text) {
|
|
8
|
+
return text.split(/\n+/).flatMap((line) => line.split(/(?<=[.!?])\s+/)).map((s) => s.trim()).filter(Boolean);
|
|
9
|
+
}
|
|
10
|
+
// OBS-37a: retry worker prompts get structured bullets only — never the consult's raw notes prose
|
|
11
|
+
// (herdr false-blocked when consult verdict text was echoed verbatim in a cursor worker prompt).
|
|
12
|
+
export function renderRetryGuidance(v) {
|
|
13
|
+
const lines = [`Action: ${v.action}`];
|
|
14
|
+
if (v.reason)
|
|
15
|
+
lines.push(`Reason: ${v.reason}`);
|
|
16
|
+
const body = v.guidance ?? (v.reason ? "" : v.notes);
|
|
17
|
+
for (const part of guidanceParts(body))
|
|
18
|
+
lines.push(part);
|
|
19
|
+
return lines.slice(0, MAX_RETRY_GUIDANCE_LINES).map((l) => `- ${l}`).join("\n");
|
|
20
|
+
}
|
|
6
21
|
const ACTIONS = ["retry", "reroute", "decompose", "human"];
|
|
7
22
|
export function buildDossierPrompt(d) {
|
|
8
|
-
return `
|
|
23
|
+
return `TICKMARKR-CONSULT
|
|
9
24
|
You are a senior engineering consult for the tickmarkr orchestrator. A worker task hit trouble.
|
|
10
25
|
Read the dossier and return a verdict. Be decisive; cost is the lowest priority, quality the highest.
|
|
11
26
|
|
|
@@ -32,12 +47,12 @@ trust dialog, broken install) — not when a single model produced bad code. Omi
|
|
|
32
47
|
reroutes so other models of the same adapter remain eligible.
|
|
33
48
|
|
|
34
49
|
Respond with ONLY this JSON:
|
|
35
|
-
{"action": "retry" | "reroute" | "decompose" | "human", "
|
|
50
|
+
{"action": "retry" | "reroute" | "decompose" | "human", "reason": "why this action", "guidance": "imperative steps for the worker (newline-separated ok)", "excludeAdapter"?: "<adapter-id>"}
|
|
36
51
|
`;
|
|
37
52
|
}
|
|
38
53
|
let consultSeq = 0;
|
|
39
54
|
export async function consult(d, cfg, adapters, driver, cwd, runDir,
|
|
40
|
-
// T2 ownership contract: runId names the consult pane canonically (
|
|
55
|
+
// T2 ownership contract: runId names the consult pane canonically (tickmarkr:consult:<task>:0:<runId>)
|
|
41
56
|
// via SlotOpts.owned; without it the legacy gatePaneName shape survives (non-daemon callers/tests).
|
|
42
57
|
opts = {}) {
|
|
43
58
|
const n = ++consultSeq;
|
|
@@ -58,10 +73,10 @@ opts = {}) {
|
|
|
58
73
|
...(opts.runId ? { owned: { role: "consult", taskId: d.taskId, attempt: 0, runId: opts.runId } } : {}),
|
|
59
74
|
});
|
|
60
75
|
opts.onSlot?.(slot);
|
|
61
|
-
await driver.run(slot, `${adapter.headlessCommand(promptFile, cfg.consult.model)}; printf '\\
|
|
62
|
-
// digit-suffixed marker only: a dossier quoting
|
|
76
|
+
await driver.run(slot, `${adapter.headlessCommand(promptFile, cfg.consult.model)}; printf '\\nTICKMARKR_''EXIT:%s\\n' $?`);
|
|
77
|
+
// digit-suffixed marker only: a dossier quoting tickmarkr's own "TICKMARKR_EXIT:" literal must not
|
|
63
78
|
// false-complete the consult pane before its verdict (same guard as daemon.ts:193 / llm.ts).
|
|
64
|
-
await driver.waitOutput(slot, "
|
|
79
|
+
await driver.waitOutput(slot, "TICKMARKR_EXIT:\\d", cfg.consult.stallMinutes * 60_000, { regex: true });
|
|
65
80
|
out = await driver.read(slot, 300);
|
|
66
81
|
if (!opts.keep)
|
|
67
82
|
await driver.close(slot);
|
|
@@ -76,5 +91,14 @@ opts = {}) {
|
|
|
76
91
|
// zero-match expansion as channel-level reroute.
|
|
77
92
|
const raw = v.excludeAdapter;
|
|
78
93
|
const excludeAdapter = typeof raw === "string" && raw.length > 0 ? raw : undefined;
|
|
79
|
-
|
|
94
|
+
const reason = typeof v.reason === "string" ? v.reason : undefined;
|
|
95
|
+
const guidance = typeof v.guidance === "string" ? v.guidance : undefined;
|
|
96
|
+
const notes = String(v.notes ?? guidance ?? reason ?? "");
|
|
97
|
+
return {
|
|
98
|
+
action: v.action,
|
|
99
|
+
notes,
|
|
100
|
+
...(reason ? { reason } : {}),
|
|
101
|
+
...(guidance ? { guidance } : {}),
|
|
102
|
+
...(excludeAdapter ? { excludeAdapter } : {}),
|
|
103
|
+
};
|
|
80
104
|
}
|
package/dist/run/daemon.js
CHANGED
|
@@ -10,7 +10,7 @@ import { formatOwnedName } from "../drivers/types.js";
|
|
|
10
10
|
import { captureBaseline, detectGateCommands } from "../gates/baseline.js";
|
|
11
11
|
import { runGates } from "../gates/run-gates.js";
|
|
12
12
|
import { addEvidence, attributeBlocked, blockedTasks, getTask, loadGraph, pendingTasks, readyTasks, saveGraph, setStatus } from "../graph/graph.js";
|
|
13
|
-
import { consult } from "./consult.js";
|
|
13
|
+
import { consult, renderRetryGuidance } from "./consult.js";
|
|
14
14
|
import { cleanupRunWorktrees, gitHead, sh } from "./git.js";
|
|
15
15
|
import { Journal, loadRoutingProfile, newRunId } from "./journal.js";
|
|
16
16
|
import { acquireRunLock, releaseRunLock } from "./lock.js";
|
|
@@ -53,7 +53,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
53
53
|
const recordedBranch = typeof branchEvent?.data.branch === "string" ? branchEvent.data.branch : undefined;
|
|
54
54
|
const branch = recordedBranch
|
|
55
55
|
? branchEvent.event === "merge" ? recordedBranch.slice(0, recordedBranch.lastIndexOf("--")) : recordedBranch
|
|
56
|
-
:
|
|
56
|
+
: integrationBranch(cfg, runId);
|
|
57
57
|
if (lock.reclaimed)
|
|
58
58
|
journal.append("lock-reclaimed", undefined, lock.reclaimed); // HARD-02 audit trail
|
|
59
59
|
// GATE-08 (v1.12): the humanGate guard consults this run's journaled approvals, not just the compiled
|
|
@@ -127,14 +127,14 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
127
127
|
};
|
|
128
128
|
await reconcile(); // run start/resume boundary: nothing in flight — full sweep, incl. older runs' leftovers
|
|
129
129
|
// v1.4 self-reference guard: a random nonce on the worker trailer AND exit marker. Displayed
|
|
130
|
-
// source/diffs (e.g. a worker editing
|
|
131
|
-
//
|
|
130
|
+
// source/diffs (e.g. a worker editing tickmarkr's own prompt.ts/daemon.ts) can't know it, so an echoed
|
|
131
|
+
// TICKMARKR_RESULT/TICKMARKR_EXIT literal can never premature-harvest the worker. Quote-split keeps the
|
|
132
132
|
// echoed command line itself from matching the marker it prints.
|
|
133
133
|
//
|
|
134
134
|
// v1.13 (VIS-09 safety, 43-02): the nonce is per-ATTEMPT, declared at the top of the attempt loop
|
|
135
135
|
// below — NOT here at run scope. A run-scoped nonce is a latent hazard: HerdrDriver.read() is
|
|
136
136
|
// `pane read --lines 1000` over scrollback and SubprocessDriver never clears s.buf, so any transcript
|
|
137
|
-
// retention across attempts would let attempt N harvest attempt N-1's
|
|
137
|
+
// retention across attempts would let attempt N harvest attempt N-1's TICKMARKR_RESULT out of scrollback
|
|
138
138
|
// as its OWN completion — silently LYING about a worker's outcome. Pinned by the stale-trailer oracle
|
|
139
139
|
// in tests/run/daemon.test.ts ("a retained prior-attempt trailer cannot complete a retry"); a future
|
|
140
140
|
// hoist back to run scope reddens it.
|
|
@@ -159,6 +159,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
159
159
|
};
|
|
160
160
|
const execTask = async (t) => {
|
|
161
161
|
const startMs = Date.now();
|
|
162
|
+
const taskTimeoutMinutes = t.timeoutMinutes ?? cfg.taskTimeoutMinutes;
|
|
162
163
|
if (t.humanGate && !approved.has(t.id)) {
|
|
163
164
|
// GATE-08: the condition is the APPROVAL, never the code path. `!opts.resume` (or any run-phase
|
|
164
165
|
// term) would silently dispatch every unapproved gate that becomes ready during a resume — pinned
|
|
@@ -252,11 +253,13 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
252
253
|
const applyVerdict = async (v, attempts, trigger) => {
|
|
253
254
|
journal.append("consult-verdict", t.id, {
|
|
254
255
|
action: v.action, notes: v.notes,
|
|
256
|
+
...(v.reason ? { reason: v.reason } : {}),
|
|
257
|
+
...(v.guidance ? { guidance: v.guidance } : {}),
|
|
255
258
|
...(v.excludeAdapter ? { excludeAdapter: v.excludeAdapter } : {}),
|
|
256
259
|
});
|
|
257
260
|
await driver.notify(`tickmarkr ${runId}: ${t.id} consult verdict: ${v.action}`, { tier: "attention" });
|
|
258
261
|
if (v.action === "retry") {
|
|
259
|
-
feedback = v
|
|
262
|
+
feedback = renderRetryGuidance(v) || feedback;
|
|
260
263
|
return true;
|
|
261
264
|
}
|
|
262
265
|
if (v.action === "reroute") {
|
|
@@ -294,8 +297,8 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
294
297
|
// v1.13 (VIS-09 safety): one FRESH nonce per attempt — see the run-scope comment above. A retained
|
|
295
298
|
// prior-attempt trailer (herdr scrollback / subprocess buffer) must never satisfy this attempt.
|
|
296
299
|
const nonce = randomBytes(4).toString("hex");
|
|
297
|
-
const exitMarkerCmd = `printf '\\
|
|
298
|
-
const exitRe = new RegExp(`
|
|
300
|
+
const exitMarkerCmd = `printf '\\nTICKMARKR_''EXIT_${nonce}:%s\\n' $?`;
|
|
301
|
+
const exitRe = new RegExp(`TICKMARKR_EXIT_${nonce}:(\\d+)`);
|
|
299
302
|
if (attempt >= MAX_ATTEMPTS) {
|
|
300
303
|
await park(t, `attempt cap (${MAX_ATTEMPTS}) reached`, "attempt-cap", assignment, attempt, startMs, gateFails, consults, tokens, metered, retryMode);
|
|
301
304
|
return;
|
|
@@ -404,13 +407,13 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
404
407
|
finished = false;
|
|
405
408
|
exitCode = null;
|
|
406
409
|
output = "";
|
|
407
|
-
const deadline = Date.now() +
|
|
410
|
+
const deadline = Date.now() + taskTimeoutMinutes * 60_000;
|
|
408
411
|
while (Date.now() < deadline) {
|
|
409
412
|
const sliceStart = Date.now();
|
|
410
413
|
const slice = Math.min(BLOCKED_POLL_MS, deadline - sliceStart);
|
|
411
|
-
if (await driver.waitOutput(slot, `(${trailerPattern(nonce)})|
|
|
412
|
-
// verify before accepting: a worker that merely DISPLAYS a marker (e.g. editing
|
|
413
|
-
// own source, where "
|
|
414
|
+
if (await driver.waitOutput(slot, `(${trailerPattern(nonce)})|TICKMARKR_EXIT_${nonce}:\\d`, slice, { regex: true })) {
|
|
415
|
+
// verify before accepting: a worker that merely DISPLAYS a marker (e.g. editing tickmarkr's
|
|
416
|
+
// own source, where "TICKMARKR_EXIT:" is a string literal) must not end the wait. Only a
|
|
414
417
|
// parseable trailer or a digit-suffixed exit marker in the harvest is completion.
|
|
415
418
|
output = await driver.read(slot, 1000); // TUI transcripts carry chrome — read deeper than print's 500
|
|
416
419
|
finished = new RegExp(trailerPattern(nonce)).test(output);
|
|
@@ -429,7 +432,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
429
432
|
const st = paged ? "" : await driver.status(slot);
|
|
430
433
|
if (!paged && (st === "blocked" || st === "idle")) {
|
|
431
434
|
// T5: once-per-slot auto-answer when the adapter declares a trust dialog and the pane
|
|
432
|
-
// text matches.
|
|
435
|
+
// text matches. tickmarkr created the worktree from the operator's own repo — safe by construction.
|
|
433
436
|
if (!trustAnswered && adapter.trustDialog && driver.sendKey) {
|
|
434
437
|
try {
|
|
435
438
|
const paneText = await driver.read(slot, 80);
|
|
@@ -473,11 +476,21 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
473
476
|
else {
|
|
474
477
|
const inv = adapter.invoke(t, wt, assignment, { promptFile });
|
|
475
478
|
await driver.run(slot, `${inv.command}; ${exitMarkerCmd}`);
|
|
476
|
-
finished = await driver.waitOutput(slot, `
|
|
479
|
+
finished = await driver.waitOutput(slot, `TICKMARKR_EXIT_${nonce}:\\d`, taskTimeoutMinutes * 60_000, { regex: true });
|
|
477
480
|
output = await driver.read(slot, 500);
|
|
478
481
|
exitCode = Number(exitRe.exec(output)?.[1] ?? 1);
|
|
479
482
|
}
|
|
480
|
-
|
|
483
|
+
// SPEND-01 interactive metering race: the harvest loop breaks on the trailer, but the worker
|
|
484
|
+
// shell may still be running post-trailer bookkeeping (session-store flush, fake usage stamp,
|
|
485
|
+
// exit wrapper). Print mode already waits for TICKMARKR_EXIT, which follows that tail; drain
|
|
486
|
+
// interactive attempts to the same exit marker before close and the post-hoc usage disk read
|
|
487
|
+
// so a writer never races the reader (real CLIs can flush usage asynchronously after the trailer).
|
|
488
|
+
if (interactive && finished && !exitRe.test(output)) {
|
|
489
|
+
await driver.waitOutput(slot, `TICKMARKR_EXIT_${nonce}:\\d`, 2_000, { regex: true });
|
|
490
|
+
}
|
|
491
|
+
// keepPanes retains visible context, not a timed-out subprocess tree. Close before consult/retry
|
|
492
|
+
// can recreate the worktree; Herdr and subprocesses that reached their exit marker stay unchanged.
|
|
493
|
+
if (keepOpen && (finished || driver.id !== "subprocess"))
|
|
481
494
|
keptSlots.push(slot);
|
|
482
495
|
else
|
|
483
496
|
await driver.close(slot);
|
|
@@ -536,7 +549,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
536
549
|
journal.telemetry({ taskId: t.id, shape: t.shape, adapter: assignment.adapter, model: assignment.model, channel: assignment.channel, attempts: attempt + 1, outcome: "failed", durationMs: 0, overrun: true, retryMode });
|
|
537
550
|
const v = await runConsult("stall", output, exitCode !== null && interactive
|
|
538
551
|
? `worker process exited (code ${exitCode}) without a trailer`
|
|
539
|
-
: `no completion marker within ${
|
|
552
|
+
: `no completion marker within ${taskTimeoutMinutes}m`, []);
|
|
540
553
|
if (await applyVerdict(v, attempt + 1, "stall"))
|
|
541
554
|
continue;
|
|
542
555
|
return;
|
|
@@ -548,7 +561,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
548
561
|
return;
|
|
549
562
|
const g = e.result;
|
|
550
563
|
// GATE-09 (ROADMAP SC-4): journal every judge retry as an attributable event — which gate flaked,
|
|
551
|
-
// which channel flaked, which channel retried — so `
|
|
564
|
+
// which channel flaked, which channel retried — so `tickmarkr journal`/report can distinguish "judge
|
|
552
565
|
// flaked, retried" from "worker failed" (run-20260711-185020 P43-03 L70-72 billed a judge flake as
|
|
553
566
|
// a worker attempt; 47-01 fixed WHO retries, this closes the audit-trail half). The condition is
|
|
554
567
|
// META-ONLY (D-03): gate === "acceptance" + typeof-shape guards on meta.judgeRetry — never a
|
|
@@ -583,7 +596,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
583
596
|
// D-07: judge/review panes self-clean when their verdict is read (keepLlm) — only "forever" keeps them.
|
|
584
597
|
keep: keepLlm,
|
|
585
598
|
onSlot: keepLlm ? (s) => keptSlots.push(s) : undefined,
|
|
586
|
-
// T2 ownership contract: canonical names (
|
|
599
|
+
// T2 ownership contract: canonical names (tickmarkr:<role>:<task>:0:<runId>) so reconcile
|
|
587
600
|
// owns judge/review panes; run-gates' -r1 retry suffix becomes attempt 1 in llm.ts.
|
|
588
601
|
// Same-name reuse across worker attempts is safe: panes self-clean when read (keepLlm),
|
|
589
602
|
// and herdr's DEFECT-01 reclaim covers a kept holdover under keepPanes:forever.
|
package/dist/run/git.d.ts
CHANGED
|
@@ -14,6 +14,6 @@ export declare function cleanupRunWorktrees(repo: string, branch: string, opts:
|
|
|
14
14
|
removeIntegration: boolean;
|
|
15
15
|
removeTaskIds: string[];
|
|
16
16
|
}): Promise<void>;
|
|
17
|
-
export declare function resolveIntegrationBranch(
|
|
17
|
+
export declare function resolveIntegrationBranch(_repo: string, branch: string): Promise<string>;
|
|
18
18
|
export declare function createWorktree(repo: string, branch: string, baseRef: string): Promise<string>;
|
|
19
19
|
export declare function removeWorktree(repo: string, dir: string): Promise<void>;
|
package/dist/run/git.js
CHANGED
|
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { existsSync, symlinkSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { shq } from "../adapters/types.js";
|
|
5
|
-
import {
|
|
5
|
+
import { tickmarkrDir } from "../graph/graph.js";
|
|
6
6
|
// stdin "ignore": same class as HARD-05 / SubprocessDriver — never leave an open pipe a child can block on
|
|
7
7
|
// (pi -p / codex exec wait for stdin EOF). timedOut distinguishes SIGKILL-timeout from a real nonzero exit.
|
|
8
8
|
export function sh(cmd, cwd, timeoutMs = 600000) {
|
|
@@ -49,7 +49,7 @@ export async function gitHead(cwd) {
|
|
|
49
49
|
}
|
|
50
50
|
export const sanitizeBranch = (branch) => branch.replace(/[^\w.-]+/g, "-");
|
|
51
51
|
const sanitize = sanitizeBranch;
|
|
52
|
-
export const worktreePath = (repo, branch) => join(
|
|
52
|
+
export const worktreePath = (repo, branch) => join(tickmarkrDir(repo), "worktrees", sanitize(branch));
|
|
53
53
|
/** OBS-28: remove this run's recorded worktrees; tolerates already-gone paths. */
|
|
54
54
|
export async function cleanupRunWorktrees(repo, branch, opts) {
|
|
55
55
|
if (opts.removeIntegration)
|
|
@@ -57,12 +57,8 @@ export async function cleanupRunWorktrees(repo, branch, opts) {
|
|
|
57
57
|
for (const id of opts.removeTaskIds)
|
|
58
58
|
await removeWorktree(repo, worktreePath(repo, `${branch}--${id}`));
|
|
59
59
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (!branch.startsWith("drovr/"))
|
|
63
|
-
return branch;
|
|
64
|
-
const legacy = `drover/${branch.slice("drovr/".length)}`;
|
|
65
|
-
return !(await branchExists(repo, branch)) && await branchExists(repo, legacy) ? legacy : branch;
|
|
60
|
+
export async function resolveIntegrationBranch(_repo, branch) {
|
|
61
|
+
return branch;
|
|
66
62
|
}
|
|
67
63
|
const resolveTaskBranch = async (repo, branch) => {
|
|
68
64
|
const split = branch.lastIndexOf("--");
|
|
@@ -73,7 +69,7 @@ const resolveTaskBranch = async (repo, branch) => {
|
|
|
73
69
|
};
|
|
74
70
|
export async function createWorktree(repo, branch, baseRef) {
|
|
75
71
|
branch = await resolveTaskBranch(repo, branch);
|
|
76
|
-
const dir = join(
|
|
72
|
+
const dir = join(tickmarkrDir(repo), "worktrees", sanitize(branch));
|
|
77
73
|
if (existsSync(dir))
|
|
78
74
|
await removeWorktree(repo, dir);
|
|
79
75
|
await shOk(`git worktree add -B ${shq(branch)} ${shq(dir)} ${shq(baseRef)}`, repo);
|
package/dist/run/journal.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { type Assignment } from "../adapters/types.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { TickmarkrConfig } from "../config/config.js";
|
|
4
4
|
import { type TaskStatus } from "../graph/schema.js";
|
|
5
5
|
import { type RoutingProfile } from "../route/profile.js";
|
|
6
6
|
export interface JournalEvent {
|
|
@@ -70,7 +70,7 @@ export declare function readAllTelemetry(repoRoot: string, lastK: number, opts?:
|
|
|
70
70
|
})[];
|
|
71
71
|
export declare const RUNS_WINDOW = 50;
|
|
72
72
|
export declare function readProfileCursor(repoRoot: string): string | undefined;
|
|
73
|
-
export declare function loadRoutingProfile(repoRoot: string, cfg:
|
|
73
|
+
export declare function loadRoutingProfile(repoRoot: string, cfg: TickmarkrConfig, opts?: {
|
|
74
74
|
preview?: boolean;
|
|
75
75
|
}): RoutingProfile | undefined;
|
|
76
76
|
export declare class Journal {
|
package/dist/run/journal.js
CHANGED
|
@@ -123,8 +123,8 @@ export function readAllTelemetry(repoRoot, lastK, opts = {}) {
|
|
|
123
123
|
}
|
|
124
124
|
// ponytail: fixed 50-run window; promote to a routing.learned.* config knob only if operators need to tune it.
|
|
125
125
|
export const RUNS_WINDOW = 50;
|
|
126
|
-
// VIS-03 reset cursor — one trimmed runId line at .
|
|
127
|
-
// Opaque: used ONLY in the runId > comparison above, never a shell or path join beyond .
|
|
126
|
+
// VIS-03 reset cursor — one trimmed runId line at .tickmarkr/profile-since; absent/empty ⇒ undefined.
|
|
127
|
+
// Opaque: used ONLY in the runId > comparison above, never a shell or path join beyond .tickmarkr/.
|
|
128
128
|
export function readProfileCursor(repoRoot) {
|
|
129
129
|
const path = join(repoRoot, stateDirName(repoRoot), "profile-since");
|
|
130
130
|
if (!existsSync(path))
|
|
@@ -132,7 +132,7 @@ export function readProfileCursor(repoRoot) {
|
|
|
132
132
|
return readFileSync(path, "utf8").trim() || undefined;
|
|
133
133
|
}
|
|
134
134
|
// The one shared profile builder (criterion 4: plan and daemon share ONE code path).
|
|
135
|
-
// preview:true bypasses the routing.learned:off short-circuit so `
|
|
135
|
+
// preview:true bypasses the routing.learned:off short-circuit so `tickmarkr plan` can render the
|
|
136
136
|
// trust-ramp preview while the daemon (no preview) stays inert (VALIDATION 13-01-11).
|
|
137
137
|
export function loadRoutingProfile(repoRoot, cfg, opts = {}) {
|
|
138
138
|
if (cfg.routing.learned === "off" && !opts.preview)
|
|
@@ -218,7 +218,7 @@ export class Journal {
|
|
|
218
218
|
// lastAssignment} from EXISTING events only (task-dispatch + consult-verdict + optional v1.24
|
|
219
219
|
// task-approved{release:attempt-cap}). Additive-only: no new required event, no schema change, the
|
|
220
220
|
// status replay above is byte-untouched (corpus criterion 3 is git-diff-provable). Motivated by the
|
|
221
|
-
// 2026-07-11 incident (run-20260711-185020, P43-03): `
|
|
221
|
+
// 2026-07-11 incident (run-20260711-185020, P43-03): `tickmarkr resume` re-dispatched at attempt 0 on
|
|
222
222
|
// pi:zai/glm-5.2, the exact channel a frontier consult had just banned, because execTask's
|
|
223
223
|
// attempt/tried/assignment state is loop-local and dies with the process while the journal held every fact needed.
|
|
224
224
|
//
|
package/dist/run/lock.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { linkSync, readFileSync, statSync, unlinkSync, utimesSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import {
|
|
5
|
-
// HARD-01/02: coarse per-run advisory lock over .
|
|
4
|
+
import { tickmarkrDir, stateDirName } from "../graph/graph.js";
|
|
5
|
+
// HARD-01/02: coarse per-run advisory lock over .tickmarkr/graph.json. LOCK-02: the lock is created by
|
|
6
6
|
// the link(2) idiom — write the full payload to graph.lock.<pid>.tmp, then linkSync(tmp, lockPath),
|
|
7
7
|
// which is atomic and throws EEXIST if the lock already exists (the mutual-exclusion primitive).
|
|
8
8
|
// rename() was REJECTED: it silently clobbers an existing destination, destroying that mutual
|
|
9
9
|
// exclusion (two daemons would both "acquire"). LOCK-02 (OBS-05): a PROVABLY-dead holder (ESRCH)
|
|
10
10
|
// self-clears immediately — expiry is NOT required. ESRCH is proof-positive death; the PID-reuse
|
|
11
|
-
// hazard runs the OTHER way (a reused pid reads ALIVE and refuses until `
|
|
11
|
+
// hazard runs the OTHER way (a reused pid reads ALIVE and refuses until `tickmarkr unlock`). mtime stays
|
|
12
12
|
// the heartbeat — it feeds the reclaim race guard (ino+mtime re-stat) and the reclaimed audit value.
|
|
13
13
|
// Zero new deps — node:fs stdlib.
|
|
14
14
|
export const HEARTBEAT_MS = 10_000;
|
|
@@ -16,7 +16,7 @@ export const STALE_MS = 60_000; // 6× heartbeat headroom; PITFALLS floor is ≥
|
|
|
16
16
|
// T-10-01: the payload is a trust boundary — parse with zod, fail closed on garbage. Only
|
|
17
17
|
// pid/runId/startedAt are ever read; anything else is ignored (info-disclosure surface).
|
|
18
18
|
const PayloadSchema = z.object({ pid: z.number().int().positive(), runId: z.string(), startedAt: z.number() });
|
|
19
|
-
const lockPath = (repoRoot) => join(
|
|
19
|
+
const lockPath = (repoRoot) => join(tickmarkrDir(repoRoot), "graph.lock");
|
|
20
20
|
let heartbeat;
|
|
21
21
|
let heldPath;
|
|
22
22
|
// Best-effort release if the daemon exits without hitting its finally (crash mid-body). NO
|
|
@@ -33,12 +33,12 @@ process.once("exit", () => { if (heldPath)
|
|
|
33
33
|
// `expired` for the race guard + reclaim audit); the heartbeat mechanism itself is untouched.
|
|
34
34
|
// ponytail: a pid REUSED by an unrelated live process reads ALIVE (kill(pid,0) succeeds) and now
|
|
35
35
|
// refuses indefinitely instead of expiring out in ≤60s — that is the fail-closed direction; the
|
|
36
|
-
// escape hatch is `
|
|
36
|
+
// escape hatch is `tickmarkr unlock`. Acceptable for a single-machine tool (ESRCH is the only
|
|
37
37
|
// proof-positive death; narrowing further needs pid-start-time correlation, out of scope here).
|
|
38
38
|
// LOCK-01: garbage ⇒ always refuse (mtime irrelevant). It short-circuits before dead can matter —
|
|
39
39
|
// so inspect()'s `dead = pid === undefined` fallback for the garbage row stays harmless. Safe post-16-01:
|
|
40
|
-
// the atomic link(2) write means
|
|
41
|
-
// come from external corruption, which is exactly what must refuse. Only `
|
|
40
|
+
// the atomic link(2) write means tickmarkr can no longer mint garbage itself; a garbage payload can only
|
|
41
|
+
// come from external corruption, which is exactly what must refuse. Only `tickmarkr unlock` removes it
|
|
42
42
|
// — a self-heal reclaim would silently overwrite whatever corrupted the file.
|
|
43
43
|
export function shouldRefuse(i) {
|
|
44
44
|
return i.garbage || !i.dead;
|
|
@@ -49,7 +49,7 @@ function inspect(p) {
|
|
|
49
49
|
const mtimeMs = st.mtimeMs;
|
|
50
50
|
const expired = Date.now() - mtimeMs > STALE_MS;
|
|
51
51
|
const parsed = PayloadSchema.safeParse(readPayload(p));
|
|
52
|
-
const garbage = !parsed.success; // LOCK-01: its own state — shouldRefuse refuses it unconditionally; only `
|
|
52
|
+
const garbage = !parsed.success; // LOCK-01: its own state — shouldRefuse refuses it unconditionally; only `tickmarkr unlock` removes it
|
|
53
53
|
const pid = parsed.success ? parsed.data.pid : undefined;
|
|
54
54
|
let dead = pid === undefined; // harmless fallback for the garbage row — garbage short-circuits shouldRefuse before this is read
|
|
55
55
|
if (pid !== undefined) {
|
|
@@ -81,7 +81,7 @@ function unlinkIfOurs(p) {
|
|
|
81
81
|
}
|
|
82
82
|
export function acquireRunLock(repoRoot, runId) {
|
|
83
83
|
const p = lockPath(repoRoot);
|
|
84
|
-
const tmp = join(
|
|
84
|
+
const tmp = join(tickmarkrDir(repoRoot), `graph.lock.${process.pid}.tmp`);
|
|
85
85
|
try {
|
|
86
86
|
try {
|
|
87
87
|
unlinkSync(tmp);
|
|
@@ -127,10 +127,10 @@ export function acquireRunLock(repoRoot, runId) {
|
|
|
127
127
|
}
|
|
128
128
|
const stateDir = stateDirName(repoRoot);
|
|
129
129
|
if (garbage)
|
|
130
|
-
throw new Error(`${stateDir}/graph.lock holds an unreadable/garbage payload — refusing to reclaim it; run \`
|
|
130
|
+
throw new Error(`${stateDir}/graph.lock holds an unreadable/garbage payload — refusing to reclaim it; run \`tickmarkr unlock\` to remove it`);
|
|
131
131
|
// LOCK-02: shouldRefuse is false whenever dead, so this throw is reached only for a LIVE holder
|
|
132
132
|
// (incl. EPERM = alive-but-not-ours). The dead-but-fresh case self-clears via the reclaim branch.
|
|
133
|
-
throw new Error(`${stateDir}/graph.lock held by pid ${pid ?? "?"}${heldRun ? ` (run ${heldRun})` : ""} — another
|
|
133
|
+
throw new Error(`${stateDir}/graph.lock held by pid ${pid ?? "?"}${heldRun ? ` (run ${heldRun})` : ""} — another tickmarkr run? (operator escape: \`tickmarkr unlock\`)`);
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
export function releaseRunLock(repoRoot) {
|
package/dist/run/merge.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { TickmarkrConfig } from "../config/config.js";
|
|
2
2
|
export interface TipVerifyResult {
|
|
3
3
|
gate: string;
|
|
4
4
|
cmd: string;
|
|
@@ -7,7 +7,7 @@ export interface TipVerifyResult {
|
|
|
7
7
|
fingerprints: string[];
|
|
8
8
|
details: string;
|
|
9
9
|
}
|
|
10
|
-
export declare function integrationBranch(cfg:
|
|
10
|
+
export declare function integrationBranch(cfg: TickmarkrConfig, runId: string): string;
|
|
11
11
|
export declare function ensureIntegration(repo: string, branch: string, baseRef: string): Promise<string>;
|
|
12
12
|
export declare function integrationHead(intWt: string): Promise<string>;
|
|
13
13
|
export declare function mergeTask(intWt: string, taskBranch: string, message: string, gatedCommit: string): Promise<{
|
package/dist/run/merge.js
CHANGED
|
@@ -2,15 +2,15 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { shq } from "../adapters/types.js";
|
|
4
4
|
import { fingerprint } from "../gates/baseline.js";
|
|
5
|
-
import {
|
|
5
|
+
import { tickmarkrDir } from "../graph/graph.js";
|
|
6
6
|
import { gitHead, resolveIntegrationBranch, sh, shOk } from "./git.js";
|
|
7
7
|
export function integrationBranch(cfg, runId) {
|
|
8
|
-
return
|
|
8
|
+
return `${cfg.integrationBranchPrefix}${runId}`;
|
|
9
9
|
}
|
|
10
10
|
const sanitize = (branch) => branch.replace(/[^\w.-]+/g, "-");
|
|
11
11
|
export async function ensureIntegration(repo, branch, baseRef) {
|
|
12
12
|
branch = await resolveIntegrationBranch(repo, branch);
|
|
13
|
-
const dir = join(
|
|
13
|
+
const dir = join(tickmarkrDir(repo), "worktrees", sanitize(branch));
|
|
14
14
|
if (existsSync(join(dir, ".git")))
|
|
15
15
|
return dir; // resume: keep existing tip
|
|
16
16
|
const exists = (await sh(`git rev-parse --verify refs/heads/${shq(branch)}`, repo)).code === 0;
|
package/dist/run/reconcile.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { formatOwnedName, isForeignName, panesToClose, parseOwnedName } from "../drivers/types.js";
|
|
2
2
|
export { isForeignName, panesToClose, parseOwnedName };
|
|
3
|
-
// T1: pure fold over journal rows — the exact set of
|
|
3
|
+
// T1: pure fold over journal rows — the exact set of tickmarkr-owned pane/tab names that SHOULD exist
|
|
4
4
|
// right now for a live run. No I/O: the caller supplies rows (Journal.read()) and a live pane listing
|
|
5
5
|
// separately. Anything owned (parseOwnedName succeeds) but not in this set is garbage to close;
|
|
6
6
|
// anything foreign (parseOwnedName fails) is never a candidate, no matter what state it's in.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tickmarkr",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.40.0",
|
|
4
4
|
"description": "Spec in, verified work out.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"test": "vitest run",
|
|
23
23
|
"test:coverage": "vitest run --coverage",
|
|
24
24
|
"schema": "tsx scripts/emit-schema.ts",
|
|
25
|
-
"e2e": "
|
|
25
|
+
"e2e": "TICKMARKR_E2E=1 vitest run tests/e2e --testTimeout 900000"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"picomatch": "^4.0.2",
|
|
@@ -13,7 +13,7 @@ Use this to execute a requested sequence of repository specs. It is SDD-agnostic
|
|
|
13
13
|
When working in a multi-agent terminal environment, decide your role before starting:
|
|
14
14
|
|
|
15
15
|
- **Orchestrator:** your session was started to execute the mission. Rename your own tab/pane ORCHESTRATOR and run the loop below.
|
|
16
|
-
- **Supervisor with a live orchestrator:** do not start a second run. Relay the mission to the existing orchestrator with a verified
|
|
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, spawn one child orchestration session with `--permission-mode bypassPermissions`, label its tab ORCHESTRATOR, give it the mission and these rules verbatim, then supervise it. Do not drive a duplicate single-tier run yourself.
|
|
18
18
|
|
|
19
19
|
Outside a multi-agent terminal environment, run the loop directly.
|
|
@@ -30,9 +30,30 @@ Outside a multi-agent terminal environment, run the loop directly.
|
|
|
30
30
|
|
|
31
31
|
Run every requested target in order without seeking routine confirmation. Stop only for a blocked agent interaction, a genuinely unresolved stalled task, a designed human gate, or a failed run that needs a human decision. Diagnose from the journal and evidence first; self-release and resume only after fixing and verifying a harness defect.
|
|
32
32
|
|
|
33
|
+
## Binary preflight (before compile or run)
|
|
34
|
+
|
|
35
|
+
Before `tickmarkr compile` or `tickmarkr run`, compare the installed binary against the repository's `package.json` version:
|
|
36
|
+
|
|
37
|
+
1. Run `tickmarkr version` (one line, machine-parseable).
|
|
38
|
+
2. Read the `version` field from the repository's `package.json`.
|
|
39
|
+
3. If the binary is **older on major.minor** than the repo (e.g. binary `1.36.x` vs repo `1.38.x`), **stop immediately** and tell the operator to update the global install (`npm i -g tickmarkr@latest`) or link the repo binary. Do not compile, plan, or run on hope.
|
|
40
|
+
|
|
41
|
+
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.
|
|
42
|
+
|
|
43
|
+
## Verified handoffs (agent-to-agent messaging)
|
|
44
|
+
|
|
45
|
+
When relaying missions between agents in a multi-agent terminal, **never use bare send-text** (`herdr agent send` / pane send-text) — it writes text without pressing Enter, so handoffs sit unsubmitted (OBS-39).
|
|
46
|
+
|
|
47
|
+
Use one of:
|
|
48
|
+
|
|
49
|
+
- `herdr pane run <pane> "<message>"` — text plus Enter in the target shell
|
|
50
|
+
- `herdr notification show "<message>"` — OS-level delivery for the operator
|
|
51
|
+
|
|
52
|
+
After sending, **confirm delivery** by reading the target pane and verifying the message landed (input empty, agent status `working`, or notification acknowledged). Never report "briefed" or "relayed" without read-back confirmation.
|
|
53
|
+
|
|
33
54
|
## Per-spec loop
|
|
34
55
|
|
|
35
|
-
1. **Prepare** — confirm the target list
|
|
56
|
+
1. **Prepare** — confirm the target list. Run the [binary preflight](#binary-preflight-before-compile-or-run). Check `git status`, confirm no tickmarkr run is active, and work from a non-main branch.
|
|
36
57
|
2. **Compile** — run `tickmarkr compile <spec-or-directory>`. Fix source-spec defects instead of editing the generated graph.
|
|
37
58
|
3. **Plan** — run `tickmarkr plan`. Review routes, capability-floor warnings, and human gates before execution.
|
|
38
59
|
4. **Run** — run `tickmarkr run`. Watch the run journal for its terminal event rather than polling agents. Resolve blocked interactions in the relevant agent session.
|