tickmarkr 1.38.0 → 1.41.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 -18
- package/dist/adapters/fake.js +23 -5
- package/dist/adapters/registry.js +12 -12
- package/dist/adapters/types.d.ts +1 -1
- package/dist/adapters/types.js +3 -2
- package/dist/cli/commands/init.js +43 -10
- package/dist/cli/commands/plan.js +12 -1
- 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/version.d.ts +1 -0
- package/dist/cli/commands/version.js +8 -0
- package/dist/cli/index.d.ts +5 -1
- package/dist/cli/index.js +8 -3
- package/dist/compile/native.js +8 -2
- package/dist/config/config.d.ts +3 -0
- package/dist/config/config.js +7 -1
- package/dist/drivers/subprocess.js +9 -2
- package/dist/gates/acceptance.d.ts +1 -0
- package/dist/gates/acceptance.js +24 -4
- package/dist/gates/baseline.js +8 -6
- package/dist/gates/review.js +26 -7
- package/dist/gates/run-gates.js +2 -2
- package/dist/graph/schema.d.ts +2 -0
- package/dist/graph/schema.js +16 -1
- package/dist/run/consult.d.ts +3 -0
- package/dist/run/consult.js +26 -2
- package/dist/run/daemon.js +21 -7
- package/dist/run/git.d.ts +1 -0
- package/dist/run/git.js +1 -1
- package/dist/run/merge.d.ts +2 -1
- package/dist/run/merge.js +18 -13
- package/package.json +1 -1
- package/schema/rungraph.schema.json +4 -0
- package/skills/tickmarkr-auto/SKILL.md +29 -4
- package/skills/tickmarkr-loop/SKILL.md +31 -5
package/dist/gates/acceptance.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { channelKey, shq } from "../adapters/types.js";
|
|
2
|
+
import { DEFAULT_DIFF_CAP } from "../config/config.js";
|
|
2
3
|
import { renderAcceptanceItem } from "../graph/schema.js";
|
|
3
4
|
import { sh, shOk } from "../run/git.js";
|
|
4
5
|
import { extractJson, runLlm } from "./llm.js";
|
|
5
|
-
const DIFF_CAP = 60_000; // judge sees diff + criteria only, and not unboundedly (spec §12)
|
|
6
6
|
const isCommand = (a) => typeof a === "object" && a.oracle === "command";
|
|
7
7
|
const isTest = (a) => typeof a === "object" && a.oracle === "test";
|
|
8
8
|
const isJudge = (a) => typeof a === "string" || (typeof a === "object" && a.oracle === "judge");
|
|
@@ -57,7 +57,12 @@ export async function acceptanceGate(task, worktree, baseRef, judge, via, opts =
|
|
|
57
57
|
const warn = onlyJudge
|
|
58
58
|
? "WARNING: only judge oracles — no deterministic command/test oracle guards this task (deterministic preferred, spec §2).\n"
|
|
59
59
|
: "";
|
|
60
|
-
const diff =
|
|
60
|
+
const diff = await shOk(`git diff '${baseRef}..HEAD'`, worktree);
|
|
61
|
+
const diffCap = opts.diffCap ?? DEFAULT_DIFF_CAP;
|
|
62
|
+
if (diff.length > diffCap) {
|
|
63
|
+
return { gate: "acceptance", pass: false,
|
|
64
|
+
details: warn + detBlock + `diff exceeds verifiable cap (${diff.length} > ${diffCap}) — split the task, or raise gates.diffCap` };
|
|
65
|
+
}
|
|
61
66
|
const prompt = `TICKMARKR-JUDGE
|
|
62
67
|
You are a strict acceptance judge. Decide whether the diff satisfies EVERY acceptance criterion.
|
|
63
68
|
Judge only what the diff proves — plausible-but-wrong must fail. Do not award partial credit.
|
|
@@ -87,6 +92,21 @@ Respond with ONLY this JSON (no prose before or after):
|
|
|
87
92
|
details: warn + detBlock + "judge output unparseable — failing closed",
|
|
88
93
|
meta: { unparseable: true, judge: channelKey({ adapter: judge.adapter.id, model: judge.model }) } };
|
|
89
94
|
}
|
|
90
|
-
const
|
|
91
|
-
|
|
95
|
+
const inconsistencies = [];
|
|
96
|
+
if (v.criteria.length !== judgeItems.length) {
|
|
97
|
+
inconsistencies.push(`judge verdict inconsistent: criteria count mismatch — expected ${judgeItems.length}, received ${v.criteria.length}`);
|
|
98
|
+
}
|
|
99
|
+
v.criteria.forEach((c, i) => {
|
|
100
|
+
if (!c || typeof c !== "object" || c.met !== true) {
|
|
101
|
+
inconsistencies.push(`judge verdict inconsistent: criteria[${i}].met must be true`);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
const pass = v.pass === true && inconsistencies.length === 0;
|
|
105
|
+
const lines = v.criteria.map((c, i) => c && typeof c === "object"
|
|
106
|
+
? `${c.met ? "✓" : "✗"} ${c.criterion}: ${c.reason}`
|
|
107
|
+
: `✗ criteria[${i}]: invalid criterion`);
|
|
108
|
+
if (!v.pass)
|
|
109
|
+
lines.push("judge verdict pass=false");
|
|
110
|
+
lines.push(...inconsistencies);
|
|
111
|
+
return { gate: "acceptance", pass, details: warn + detBlock + (lines.join("\n") || "judge passed") };
|
|
92
112
|
}
|
package/dist/gates/baseline.js
CHANGED
|
@@ -6,20 +6,21 @@ import { sh } from "../run/git.js";
|
|
|
6
6
|
// a pass-marker line is never a failure. [\d;#] covers raw ANSI and digit-normalized ANSI ("\x1b[#m") from
|
|
7
7
|
// baselines stored by pre-hardening code.
|
|
8
8
|
const ANSI_RE = /\x1b\[[\d;#]*[A-Za-z]/g;
|
|
9
|
-
// ponytail: only leading ✓/✔ after optional "label:" prefixes (turbo/vitest)
|
|
10
|
-
// runners' pass markers (PASS, ok) stay fingerprintable
|
|
11
|
-
const PASS_LINE_RE = /^\s*(?:[\w@./-]+:\s*)*[✓✔]/;
|
|
9
|
+
// ponytail: only leading ✓/✔ after optional "label:" prefixes (turbo/vitest), or tickmarkr's own run
|
|
10
|
+
// summary, counts as a pass line — other runners' pass markers (PASS, ok) stay fingerprintable
|
|
11
|
+
const PASS_LINE_RE = /^\s*(?:(?:[\w@./-]+:\s*)*[✓✔]|(?:\[tickmarkr\]\s+)?(?:tickmarkr\s+[\w.-]+:\s+)?(?:\d+|#)\s+done,\s+(?:\d+|#)\s+failed(?:,\s+(?:\d+|#)\s+awaiting human)?\b)/;
|
|
12
12
|
// HYG-08 (D-01, incident run-20260711-154920): a failing test went unnamed for 3 attempts because details
|
|
13
13
|
// headlined benign fingerprint-diff noise. These anchors harvest the runner's OWN failure naming from fresh
|
|
14
14
|
// output to headline it. \s is fine in a TS regex — the BSD [[:space:]] rule binds shell grep only.
|
|
15
|
-
|
|
15
|
+
// OBS-42: vitest's diagnostic failure headings are shared anchors for baseline and tip verification.
|
|
16
|
+
const FAIL_ANCHOR_RE = /^\s*(?:FAIL\s+|[^\w]*(?:Unhandled Errors|Uncaught Exception)\b)/;
|
|
16
17
|
const SUMMARY_FAIL_RE = /^\s*Tests?\s+(?:Files?\s+)?\d+\s+failed/; // " Tests N failed | M passed (T)"
|
|
17
18
|
const normalizeLine = (l) => l.replace(/\d+/g, "#").replace(/\s+/g, " ").trim();
|
|
18
19
|
export function fingerprint(output) {
|
|
19
20
|
const lines = output
|
|
20
21
|
.split("\n")
|
|
21
22
|
.map((l) => l.replace(ANSI_RE, ""))
|
|
22
|
-
.filter((l) => !PASS_LINE_RE.test(l) && /\b(error|fail(ed|ure|ing)?)\b/i.test(l))
|
|
23
|
+
.filter((l) => !PASS_LINE_RE.test(l) && (FAIL_ANCHOR_RE.test(l) || /\b(error|fail(ed|ure|ing)?)\b/i.test(l)))
|
|
23
24
|
.map(normalizeLine);
|
|
24
25
|
return [...new Set(lines)];
|
|
25
26
|
}
|
|
@@ -84,7 +85,8 @@ export async function compareToBaseline(cwd, commands, baseline, enabled) {
|
|
|
84
85
|
}
|
|
85
86
|
const raw = (r.stdout + "\n" + r.stderr).split(cwd).join("");
|
|
86
87
|
const known = new Set((baseline.commands[name]?.fingerprints ?? []).map(renormalize));
|
|
87
|
-
|
|
88
|
+
// OBS-42: diagnostic headings enrich fingerprints but cannot invalidate legacy baselines.
|
|
89
|
+
const fresh = fingerprint(raw).filter((f) => !known.has(f) && (!FAIL_ANCHOR_RE.test(f) || f.startsWith("FAIL ")));
|
|
88
90
|
if (!fresh.length && (baseline.commands[name]?.exitCode ?? 1) === 0) {
|
|
89
91
|
results.push({
|
|
90
92
|
gate: name,
|
package/dist/gates/review.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { channelKey } from "../adapters/types.js";
|
|
2
|
-
import { TIER_RANK } from "../config/config.js";
|
|
2
|
+
import { DEFAULT_DIFF_CAP, TIER_RANK } from "../config/config.js";
|
|
3
3
|
import { renderAcceptanceItem } from "../graph/schema.js";
|
|
4
4
|
import { getAdapter } from "../adapters/registry.js";
|
|
5
5
|
import { shOk } from "../run/git.js";
|
|
@@ -27,7 +27,6 @@ export function pickReviewer(author, channels, exclude = []) {
|
|
|
27
27
|
.filter((c) => c.vendor !== authorChannel.vendor && modelId(c.model) !== modelId(author.model) && !exclude.includes(channelKey(c)))
|
|
28
28
|
.sort((a, b) => TIER_RANK[b.tier] - TIER_RANK[a.tier] || marginalCostRank(a) - marginalCostRank(b))[0] ?? null);
|
|
29
29
|
}
|
|
30
|
-
const DIFF_CAP = 60_000;
|
|
31
30
|
export async function reviewGate(task, worktree, baseRef, author, channels, adapters, cfg, via, excludeReviewers) {
|
|
32
31
|
if (task.complexity < cfg.review.complexityThreshold) {
|
|
33
32
|
return { gate: "review", pass: true, details: `skipped — complexity ${task.complexity} < threshold ${cfg.review.complexityThreshold}`, meta: { skipped: true } };
|
|
@@ -38,7 +37,12 @@ export async function reviewGate(task, worktree, baseRef, author, channels, adap
|
|
|
38
37
|
? { gate: "review", pass: false, details: "no cross-vendor reviewer available (diversity rule); set review.required:false to waive" }
|
|
39
38
|
: { gate: "review", pass: true, details: "WARNING: no cross-vendor reviewer available — review waived by config" };
|
|
40
39
|
}
|
|
41
|
-
const diff =
|
|
40
|
+
const diff = await shOk(`git diff '${baseRef}..HEAD'`, worktree);
|
|
41
|
+
const diffCap = cfg.gates.diffCap ?? DEFAULT_DIFF_CAP;
|
|
42
|
+
if (diff.length > diffCap) {
|
|
43
|
+
return { gate: "review", pass: false,
|
|
44
|
+
details: `diff exceeds verifiable cap (${diff.length} > ${diffCap}) — split the task, or raise gates.diffCap` };
|
|
45
|
+
}
|
|
42
46
|
const prompt = `TICKMARKR-REVIEW
|
|
43
47
|
You are a skeptical cross-vendor code reviewer. Another agent (vendor: ${author.adapter}) authored this diff.
|
|
44
48
|
Look for correctness bugs, security issues, and acceptance-criteria gaps. Approve only if you would merge it.
|
|
@@ -56,14 +60,14 @@ Respond with ONLY this JSON:
|
|
|
56
60
|
{"approve": true|false, "issues": ["..."]}
|
|
57
61
|
`;
|
|
58
62
|
const raw = await runLlm(getAdapter(reviewer.adapter, adapters), reviewer.model, prompt, worktree, via ? { driver: via.driver, keep: via.keep, onSlot: via.onSlot, name: via.nameFor("review", reviewer.adapter), label: via.labelFor("review") } : undefined,
|
|
59
|
-
// frontier reviewers routinely need >5min on a
|
|
63
|
+
// frontier reviewers routinely need >5min on a configured-cap-sized diff, and `claude -p` buffers all
|
|
60
64
|
// output until completion — runLlm's 300s default killed reviews mid-flight, returning empty
|
|
61
65
|
// stdout that read as "unparseable" and escalated to re-implementation of green code
|
|
62
66
|
// (run-20260709-104447 P87-09). ponytail: literal 15min; make it cfg.review.timeoutMs if a
|
|
63
67
|
// second knob-turner appears.
|
|
64
68
|
900_000);
|
|
65
69
|
const v = extractJson(raw);
|
|
66
|
-
if (!v || typeof v.approve !== "boolean") {
|
|
70
|
+
if (!v || typeof v.approve !== "boolean" || !Array.isArray(v.issues)) {
|
|
67
71
|
return {
|
|
68
72
|
gate: "review",
|
|
69
73
|
pass: false,
|
|
@@ -71,10 +75,25 @@ Respond with ONLY this JSON:
|
|
|
71
75
|
meta: { reviewer: channelKey(reviewer) },
|
|
72
76
|
};
|
|
73
77
|
}
|
|
78
|
+
const issues = v.issues;
|
|
79
|
+
const inconsistencies = [];
|
|
80
|
+
issues.forEach((issue, i) => {
|
|
81
|
+
if (typeof issue !== "string")
|
|
82
|
+
inconsistencies.push(`review verdict inconsistent: issues[${i}] must be a string`);
|
|
83
|
+
});
|
|
84
|
+
if (v.approve && issues.length) {
|
|
85
|
+
inconsistencies.push("review verdict inconsistent: approve=true requires issues to be empty");
|
|
86
|
+
}
|
|
87
|
+
else if (!v.approve && !issues.length) {
|
|
88
|
+
inconsistencies.push("review verdict inconsistent: approve=false requires at least one issue");
|
|
89
|
+
}
|
|
90
|
+
const pass = v.approve === true && inconsistencies.length === 0;
|
|
91
|
+
const lines = issues.map((issue) => `- ${typeof issue === "string" ? issue : JSON.stringify(issue)}`);
|
|
92
|
+
lines.push(...inconsistencies);
|
|
74
93
|
return {
|
|
75
94
|
gate: "review",
|
|
76
|
-
pass
|
|
77
|
-
details: `reviewer ${reviewer.adapter}:${reviewer.model} (${reviewer.vendor}): ${v.approve ? "
|
|
95
|
+
pass,
|
|
96
|
+
details: `reviewer ${reviewer.adapter}:${reviewer.model} (${reviewer.vendor}): ${pass ? "approved" : v.approve ? "approval rejected" : "requested changes"}${lines.length ? "\n" + lines.join("\n") : ""}`,
|
|
78
97
|
meta: { reviewer: channelKey(reviewer) },
|
|
79
98
|
};
|
|
80
99
|
}
|
package/dist/gates/run-gates.js
CHANGED
|
@@ -63,7 +63,7 @@ export async function runGates(task, ctx) {
|
|
|
63
63
|
: undefined;
|
|
64
64
|
// v1.19 (T2): testCmd threads the detected test runner to the gate so named-test oracles run
|
|
65
65
|
// deterministically (filtered via -t) before any LLM judge dispatch.
|
|
66
|
-
let a = await acceptanceGate(task, ctx.worktree, ctx.baseRef, { adapter: judgeAdapter, model: ctx.cfg.judge.model }, jvia, { testCmd: ctx.commands.test });
|
|
66
|
+
let a = await acceptanceGate(task, ctx.worktree, ctx.baseRef, { adapter: judgeAdapter, model: ctx.cfg.judge.model }, jvia, { testCmd: ctx.commands.test, diffCap: ctx.cfg.gates.diffCap });
|
|
67
67
|
// GATE-09: an unparseable judge verdict retries the JUDGE exactly once on a failover channel — never
|
|
68
68
|
// the worker (run-20260711-185020 P43-03 L70-72 billed a judge flake as a worker attempt). The flaked
|
|
69
69
|
// first verdict NEVER enters results (no false gate-result journal event, no operator notify, no stale
|
|
@@ -89,7 +89,7 @@ export async function runGates(task, ctx) {
|
|
|
89
89
|
? { driver: ctx.via.driver, keep: ctx.via.keep, onSlot: ctx.via.onSlot, name: ctx.via.nameFor("judge", retryAdapter.id) + "-r1", label: ctx.via.labelFor("judge") }
|
|
90
90
|
: undefined;
|
|
91
91
|
// the retry IS a second acceptanceGate call: one code path, one parser, zero new parse leniency.
|
|
92
|
-
a = await acceptanceGate(task, ctx.worktree, ctx.baseRef, { adapter: retryAdapter, model: retry.model }, retryJvia, { testCmd: ctx.commands.test });
|
|
92
|
+
a = await acceptanceGate(task, ctx.worktree, ctx.baseRef, { adapter: retryAdapter, model: retry.model }, retryJvia, { testCmd: ctx.commands.test, diffCap: ctx.cfg.gates.diffCap });
|
|
93
93
|
a = { ...a, meta: { ...a.meta, judgeRetry: { flaked: flakedKey, retried: channelKey({ adapter: retry.adapter, model: retry.model }) } } };
|
|
94
94
|
}
|
|
95
95
|
await record(a);
|
package/dist/graph/schema.d.ts
CHANGED
|
@@ -77,6 +77,7 @@ export declare const TaskSchema: z.ZodObject<{
|
|
|
77
77
|
escalate: z.ZodOptional<z.ZodBoolean>;
|
|
78
78
|
}, z.core.$strip>>;
|
|
79
79
|
humanGate: z.ZodDefault<z.ZodBoolean>;
|
|
80
|
+
timeoutMinutes: z.ZodOptional<z.ZodNumber>;
|
|
80
81
|
status: z.ZodDefault<z.ZodEnum<{
|
|
81
82
|
pending: "pending";
|
|
82
83
|
running: "running";
|
|
@@ -161,6 +162,7 @@ export declare const RunGraphSchema: z.ZodObject<{
|
|
|
161
162
|
escalate: z.ZodOptional<z.ZodBoolean>;
|
|
162
163
|
}, z.core.$strip>>;
|
|
163
164
|
humanGate: z.ZodDefault<z.ZodBoolean>;
|
|
165
|
+
timeoutMinutes: z.ZodOptional<z.ZodNumber>;
|
|
164
166
|
status: z.ZodDefault<z.ZodEnum<{
|
|
165
167
|
pending: "pending";
|
|
166
168
|
running: "running";
|
package/dist/graph/schema.js
CHANGED
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
export const SHAPES = ["plan", "spec", "implement", "tests", "docs", "migration", "ui", "refactor", "chore"];
|
|
3
3
|
export const STATUSES = ["pending", "running", "gated", "failed", "done", "human"];
|
|
4
4
|
export const GATE_NAMES = ["build", "test", "lint", "evidence", "scope", "acceptance", "review"];
|
|
5
|
+
const MANDATORY_GATES = ["build", "test", "lint", "evidence", "scope"];
|
|
5
6
|
export const TIERS = ["cheap", "mid", "frontier"];
|
|
6
7
|
// v1.19 acceptance oracles: command (exit code), test (named test), judge (LLM, free-text rubric).
|
|
7
8
|
// A plain string is the read-old/write-new compat form — semantically a judge oracle (spec §2).
|
|
@@ -51,7 +52,19 @@ export const TaskSchema = z.object({
|
|
|
51
52
|
reason: z.string().optional(),
|
|
52
53
|
}))
|
|
53
54
|
.optional(),
|
|
54
|
-
gates: z
|
|
55
|
+
gates: z
|
|
56
|
+
.array(z.enum(GATE_NAMES))
|
|
57
|
+
.default(["build", "test", "lint", "evidence", "scope", "acceptance", "review"])
|
|
58
|
+
.superRefine((gates, ctx) => {
|
|
59
|
+
for (const gate of MANDATORY_GATES) {
|
|
60
|
+
if (!gates.includes(gate)) {
|
|
61
|
+
ctx.addIssue({
|
|
62
|
+
code: "custom",
|
|
63
|
+
message: `${gate} is a mandatory fail-closed gate invariant and cannot be omitted from task gates`,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}),
|
|
55
68
|
routingHints: z
|
|
56
69
|
.object({
|
|
57
70
|
pin: z.object({ via: z.string(), model: z.string() }).optional(),
|
|
@@ -61,6 +74,8 @@ export const TaskSchema = z.object({
|
|
|
61
74
|
})
|
|
62
75
|
.optional(),
|
|
63
76
|
humanGate: z.boolean().default(false),
|
|
77
|
+
// v1.39 OBS-37b: per-task worker window override; absent ⇒ config taskTimeoutMinutes.
|
|
78
|
+
timeoutMinutes: z.number().positive().optional(),
|
|
64
79
|
status: z.enum(STATUSES).default("pending"),
|
|
65
80
|
evidence: z
|
|
66
81
|
.object({
|
package/dist/run/consult.d.ts
CHANGED
|
@@ -5,8 +5,11 @@ 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;
|
package/dist/run/consult.js
CHANGED
|
@@ -3,6 +3,21 @@ 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
23
|
return `TICKMARKR-CONSULT
|
|
@@ -32,7 +47,7 @@ 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;
|
|
@@ -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";
|
|
@@ -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") {
|
|
@@ -404,7 +407,7 @@ 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);
|
|
@@ -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, `TICKMARKR_EXIT_${nonce}:\\d`,
|
|
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;
|
|
@@ -710,7 +723,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
710
723
|
// OBS-34: post-merge integration-tip verify — strict exit codes, no baseline forgiveness.
|
|
711
724
|
const lastMergedTask = [...journal.read()].reverse().find((e) => e.event === "merge" && e.taskId)?.taskId;
|
|
712
725
|
if (summary.done.length > 0 && Object.keys(commands).length > 0) {
|
|
713
|
-
const tipResults = await verifyIntegrationTip(intWt, commands);
|
|
726
|
+
const tipResults = await verifyIntegrationTip(intWt, commands, journal.dir);
|
|
714
727
|
let tipFailed = false;
|
|
715
728
|
for (const r of tipResults) {
|
|
716
729
|
if (r.pass) {
|
|
@@ -722,6 +735,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
722
735
|
cmd: r.cmd,
|
|
723
736
|
exitCode: r.exitCode,
|
|
724
737
|
fingerprints: r.fingerprints,
|
|
738
|
+
artifact: r.artifact,
|
|
725
739
|
lastMergedTask,
|
|
726
740
|
});
|
|
727
741
|
tipFailed = true;
|
package/dist/run/git.d.ts
CHANGED
|
@@ -16,4 +16,5 @@ export declare function cleanupRunWorktrees(repo: string, branch: string, opts:
|
|
|
16
16
|
}): Promise<void>;
|
|
17
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
|
+
export declare function linkNodeModules(repo: string, dir: string): void;
|
|
19
20
|
export declare function removeWorktree(repo: string, dir: string): Promise<void>;
|
package/dist/run/git.js
CHANGED
|
@@ -78,7 +78,7 @@ export async function createWorktree(repo, branch, baseRef) {
|
|
|
78
78
|
}
|
|
79
79
|
// best-effort: devDep-based gates (tsx, vitest) shell out root-anchored and ENOENT in a bare
|
|
80
80
|
// fresh worktree (OBS-27); failure here must never fail worktree creation
|
|
81
|
-
function linkNodeModules(repo, dir) {
|
|
81
|
+
export function linkNodeModules(repo, dir) {
|
|
82
82
|
const src = join(repo, "node_modules");
|
|
83
83
|
const dest = join(dir, "node_modules");
|
|
84
84
|
if (!existsSync(src) || existsSync(dest))
|
package/dist/run/merge.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export interface TipVerifyResult {
|
|
|
6
6
|
exitCode: number;
|
|
7
7
|
fingerprints: string[];
|
|
8
8
|
details: string;
|
|
9
|
+
artifact?: string;
|
|
9
10
|
}
|
|
10
11
|
export declare function integrationBranch(cfg: TickmarkrConfig, runId: string): string;
|
|
11
12
|
export declare function ensureIntegration(repo: string, branch: string, baseRef: string): Promise<string>;
|
|
@@ -18,4 +19,4 @@ export declare function mergeTask(intWt: string, taskBranch: string, message: st
|
|
|
18
19
|
branchTip: string;
|
|
19
20
|
};
|
|
20
21
|
}>;
|
|
21
|
-
export declare function verifyIntegrationTip(intWt: string, commands: Record<string, string
|
|
22
|
+
export declare function verifyIntegrationTip(intWt: string, commands: Record<string, string>, runDir: string): Promise<TipVerifyResult[]>;
|
package/dist/run/merge.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { existsSync, writeFileSync } 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
5
|
import { tickmarkrDir } from "../graph/graph.js";
|
|
6
|
-
import { gitHead, resolveIntegrationBranch, sh, shOk } from "./git.js";
|
|
6
|
+
import { gitHead, linkNodeModules, resolveIntegrationBranch, sh, shOk } from "./git.js";
|
|
7
7
|
export function integrationBranch(cfg, runId) {
|
|
8
8
|
return `${cfg.integrationBranchPrefix}${runId}`;
|
|
9
9
|
}
|
|
@@ -11,15 +11,16 @@ const sanitize = (branch) => branch.replace(/[^\w.-]+/g, "-");
|
|
|
11
11
|
export async function ensureIntegration(repo, branch, baseRef) {
|
|
12
12
|
branch = await resolveIntegrationBranch(repo, branch);
|
|
13
13
|
const dir = join(tickmarkrDir(repo), "worktrees", sanitize(branch));
|
|
14
|
-
if (existsSync(join(dir, ".git")))
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
if (!existsSync(join(dir, ".git"))) {
|
|
15
|
+
const exists = (await sh(`git rev-parse --verify refs/heads/${shq(branch)}`, repo)).code === 0;
|
|
16
|
+
if (exists) {
|
|
17
|
+
await shOk(`git worktree add ${shq(dir)} ${shq(branch)}`, repo);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
await shOk(`git worktree add -b ${shq(branch)} ${shq(dir)} ${shq(baseRef)}`, repo);
|
|
21
|
+
}
|
|
22
22
|
}
|
|
23
|
+
linkNodeModules(repo, dir);
|
|
23
24
|
return dir;
|
|
24
25
|
}
|
|
25
26
|
export function integrationHead(intWt) {
|
|
@@ -44,18 +45,22 @@ export async function mergeTask(intWt, taskBranch, message, gatedCommit) {
|
|
|
44
45
|
return { ok: false, conflict };
|
|
45
46
|
}
|
|
46
47
|
// OBS-34: strict exit-code verify on the integration tip — no baseline forgiveness.
|
|
47
|
-
export async function verifyIntegrationTip(intWt, commands) {
|
|
48
|
+
export async function verifyIntegrationTip(intWt, commands, runDir) {
|
|
48
49
|
const results = [];
|
|
49
50
|
for (const [gate, cmd] of Object.entries(commands)) {
|
|
50
51
|
const r = await sh(cmd, intWt);
|
|
51
|
-
const raw =
|
|
52
|
+
const raw = r.stdout + "\n" + r.stderr;
|
|
53
|
+
const artifact = r.code !== 0 ? join(runDir, `tip-verify-${gate}.log`) : undefined;
|
|
54
|
+
if (artifact)
|
|
55
|
+
writeFileSync(artifact, raw);
|
|
52
56
|
results.push({
|
|
53
57
|
gate,
|
|
54
58
|
cmd,
|
|
55
59
|
pass: r.code === 0,
|
|
56
60
|
exitCode: r.code,
|
|
57
|
-
fingerprints: r.code !== 0 ? fingerprint(raw) : [],
|
|
61
|
+
fingerprints: r.code !== 0 ? fingerprint(raw.split(intWt).join("")) : [],
|
|
58
62
|
details: r.code === 0 ? "exit 0" : `exit ${r.code}`,
|
|
63
|
+
...(artifact ? { artifact } : {}),
|
|
59
64
|
});
|
|
60
65
|
}
|
|
61
66
|
return results;
|
package/package.json
CHANGED
|
@@ -12,12 +12,16 @@ Use this to execute a requested sequence of repository specs. It is SDD-agnostic
|
|
|
12
12
|
|
|
13
13
|
When working in a multi-agent terminal environment, decide your role before starting:
|
|
14
14
|
|
|
15
|
-
- **Orchestrator:** your session was started to execute the mission. Rename your own tab/pane
|
|
16
|
-
- **Supervisor with a live orchestrator:** do not start a second run. Relay the mission to the existing orchestrator with a verified
|
|
17
|
-
- **Primary session without an orchestrator:** rename your own tab OVERSEER
|
|
15
|
+
- **Orchestrator:** your session was started to execute the mission. Rename your own tab/pane `ORCH · <version>` (short labels: ≤20 chars, `ROLE · token`) 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 handoff](#verified-handoffs-agent-to-agent-messaging), then supervise it as OVERSEER.
|
|
17
|
+
- **Primary session without an orchestrator:** rename your own tab `OVERSEER · <version>` and your agent `overseer`, spawn one child orchestration session with `--permission-mode bypassPermissions`, label its tab `ORCH · <version>` and name its agent, give it the mission and these rules verbatim, then supervise it. Do not drive a duplicate single-tier run yourself. Before spawning, confirm any PREVIOUS orchestrator has stood down (monitors stopped, input box empty) and close its tab — the journal, records, and ledger hold the story; scrollback is disposable.
|
|
18
18
|
|
|
19
19
|
Outside a multi-agent terminal environment, run the loop directly.
|
|
20
20
|
|
|
21
|
+
## Stand-down (mission end and retirement)
|
|
22
|
+
|
|
23
|
+
On each mission's terminal state, after the record commit and operator notification: the orchestrator stops every monitor and background task it started, prints one final stand-down line, and leaves nothing queued in its input box. A finished session with an armed watcher or pre-filled input is a loaded gun.
|
|
24
|
+
|
|
21
25
|
## Invariants
|
|
22
26
|
|
|
23
27
|
- Never run two tickmarkr runs in the same repository concurrently.
|
|
@@ -30,9 +34,30 @@ Outside a multi-agent terminal environment, run the loop directly.
|
|
|
30
34
|
|
|
31
35
|
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
36
|
|
|
37
|
+
## Binary preflight (before compile or run)
|
|
38
|
+
|
|
39
|
+
Before `tickmarkr compile` or `tickmarkr run`, compare the installed binary against the repository's `package.json` version:
|
|
40
|
+
|
|
41
|
+
1. Run `tickmarkr version` (one line, machine-parseable).
|
|
42
|
+
2. Read the `version` field from the repository's `package.json`.
|
|
43
|
+
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.
|
|
44
|
+
|
|
45
|
+
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.
|
|
46
|
+
|
|
47
|
+
## Verified handoffs (agent-to-agent messaging)
|
|
48
|
+
|
|
49
|
+
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).
|
|
50
|
+
|
|
51
|
+
Use one of:
|
|
52
|
+
|
|
53
|
+
- `herdr pane run <pane> "<message>"` — text plus Enter in the target shell
|
|
54
|
+
- `herdr notification show "<message>"` — OS-level delivery for the operator
|
|
55
|
+
|
|
56
|
+
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.
|
|
57
|
+
|
|
33
58
|
## Per-spec loop
|
|
34
59
|
|
|
35
|
-
1. **Prepare** — confirm the target list
|
|
60
|
+
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
61
|
2. **Compile** — run `tickmarkr compile <spec-or-directory>`. Fix source-spec defects instead of editing the generated graph.
|
|
37
62
|
3. **Plan** — run `tickmarkr plan`. Review routes, capability-floor warnings, and human gates before execution.
|
|
38
63
|
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.
|
|
@@ -11,9 +11,9 @@ Use this for any spec that `tickmarkr compile` accepts. It is SDD-agnostic: use
|
|
|
11
11
|
|
|
12
12
|
When working in a multi-agent terminal environment, decide your role before starting:
|
|
13
13
|
|
|
14
|
-
- **Orchestrator:** your session was started to execute the mission. Rename your own tab/pane
|
|
15
|
-
- **Supervisor with a live orchestrator:** do not start a second run. Relay the mission to the existing orchestrator with a verified
|
|
16
|
-
- **Primary session without an orchestrator:** rename your own tab OVERSEER
|
|
14
|
+
- **Orchestrator:** your session was started to execute the mission. Rename your own tab/pane `ORCH · <version>` (short labels: ≤20 chars, `ROLE · token`) and run the loop below.
|
|
15
|
+
- **Supervisor with a live orchestrator:** do not start a second run. Relay the mission to the existing orchestrator with a [verified handoff](#verified-handoffs-agent-to-agent-messaging), then supervise it as OVERSEER.
|
|
16
|
+
- **Primary session without an orchestrator:** rename your own tab `OVERSEER · <version>` and your agent `overseer`, spawn one child orchestration session with `--permission-mode bypassPermissions`, label its tab `ORCH · <version>` and name its agent, give it the mission and these rules verbatim, then supervise it. Do not drive a duplicate single-tier run yourself. Before spawning, confirm any PREVIOUS orchestrator has [stood down](#stand-down-mission-end-and-retirement) and close its tab.
|
|
17
17
|
|
|
18
18
|
Outside a multi-agent terminal environment, run the loop directly.
|
|
19
19
|
|
|
@@ -29,11 +29,37 @@ Outside a multi-agent terminal environment, run the loop directly.
|
|
|
29
29
|
|
|
30
30
|
Proceed through the loop without seeking routine confirmation. Stop only for a blocked agent interaction, a genuinely unresolved stalled task, or a designed human gate. Diagnose from the journal and available evidence before escalating; if a harness defect is fixed and verified, resume the run.
|
|
31
31
|
|
|
32
|
+
## Binary preflight (before compile or run)
|
|
33
|
+
|
|
34
|
+
Before `tickmarkr compile` or `tickmarkr run`, compare the installed binary against the repository's `package.json` version:
|
|
35
|
+
|
|
36
|
+
1. Run `tickmarkr version` (one line, machine-parseable).
|
|
37
|
+
2. Read the `version` field from the repository's `package.json`.
|
|
38
|
+
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.
|
|
39
|
+
|
|
40
|
+
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.
|
|
41
|
+
|
|
42
|
+
## Verified handoffs (agent-to-agent messaging)
|
|
43
|
+
|
|
44
|
+
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).
|
|
45
|
+
|
|
46
|
+
Use one of:
|
|
47
|
+
|
|
48
|
+
- `herdr pane run <pane> "<message>"` — text plus Enter in the target shell
|
|
49
|
+
- `herdr notification show "<message>"` — OS-level delivery for the operator
|
|
50
|
+
|
|
51
|
+
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.
|
|
52
|
+
|
|
53
|
+
## Stand-down (mission end and retirement)
|
|
54
|
+
|
|
55
|
+
- **Orchestrator, on terminal state** (green, failed, or parked), after the record commit and operator notification: stop every monitor and background task you started, print one final stand-down line, and leave NOTHING queued in your input box. A finished session with an armed watcher or pre-filled input is a loaded gun — a retired v1.40 orchestrator sat idle with "merge … tag, publish" unsent in its input; one stray Enter would have shipped a duplicate release.
|
|
56
|
+
- **Supervisor, when a mission completes** (and always before spawning the next orchestrator): verify the orchestrator stood down, then close its tab. The journal, execution record, OBS ledger, and memory hold the story; pane scrollback is disposable. Never leave a retired agent idle with watchers armed.
|
|
57
|
+
|
|
32
58
|
## The loop
|
|
33
59
|
|
|
34
|
-
1. **Prepare** — start from the requested spec. Check `git status`, confirm no tickmarkr run is active, and work from a non-main branch.
|
|
60
|
+
1. **Prepare** — start from the requested spec. 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.
|
|
35
61
|
2. **Compile** — run `tickmarkr compile <spec>`. Correct compilation errors in the spec, never in the generated graph.
|
|
36
62
|
3. **Plan** — run `tickmarkr plan`. Review the routing table, capability-floor warnings, and every human gate, including work that each gate blocks.
|
|
37
63
|
4. **Run** — run `tickmarkr run`. Watch the run journal for its terminal event rather than repeatedly polling agents. Resolve blocked interactions in the agent session; do not turn them into proxy questions.
|
|
38
64
|
5. **Verify and consolidate** — accept only a green run. A run is green when the run-end event exists in the journal AND the tip verify is not "failed". Tickmarkr consolidates accepted task work on `tickmarkr/<runId>`; it never signs off to the main branch. A human may later merge that integration branch through the repository's normal release process.
|
|
39
|
-
6. **Record** — write `tickmarkr report <runId> --md` beside the source spec and commit the execution record when the repository tracks those records.
|
|
65
|
+
6. **Record** — write `tickmarkr report <runId> --md` beside the source spec and commit the execution record when the repository tracks those records. Then [stand down](#stand-down-mission-end-and-retirement).
|