tickmarkr 1.46.0 → 1.48.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.
@@ -59,10 +59,11 @@ export function resolveHygieneWeight(row, discounts = []) {
59
59
  // yielded no quality obs (quota parks) has spent its budget — stop probing it, don't probe it forever.
60
60
  // ponytail: rank-decay ships in v1.8 (buildProfile below); remaining ceiling = doneMedianMs staleness
61
61
  // (perf term is undecayed) and per-cell EWMA as the finer upgrade path.
62
- export function explorationBonus(cell) {
62
+ // ROUTE-15 PARAM-SHAPE: optional cap threads from routing.explore.cap; undefined ⇒ module default (byte-identical).
63
+ export function explorationBonus(cell, cap = EXPLORE_CAP) {
63
64
  if (!cell)
64
65
  return 0;
65
- return Math.max(0, 1 - cell.dispatches / EXPLORE_CAP); // ∈ [0,1]; exactly 0 once dispatches ≥ cap
66
+ return Math.max(0, 1 - cell.dispatches / cap); // ∈ [0,1]; exactly 0 once dispatches ≥ cap
66
67
  }
67
68
  const QUALITY_FAIL_PARKS = new Set(["ladder-exhausted", "attempt-cap", "gate-fail"]);
68
69
  // Quality observation per row: 1 clean, 0.5 degraded, 0 verified failure, null excluded/unobserved.
@@ -183,6 +184,7 @@ export function cellSummary(cell) {
183
184
  // Per-term decomposition of learnedScore — the single arithmetic source for profile --explain.
184
185
  export function learnedScoreTerms(profile, shape, chKey, channel, opts = {}) {
185
186
  const availWeight = opts.availWeight ?? AVAIL_WEIGHT;
187
+ const refMs = opts.slaMinutes !== undefined ? opts.slaMinutes * 60_000 : REF_MS;
186
188
  const cell = cellOf(profile, shape, chKey, channel);
187
189
  if (!cell)
188
190
  return { quality: NEUTRAL, perf: NEUTRAL, avail: NEUTRAL, overrun: NEUTRAL };
@@ -198,7 +200,7 @@ export function learnedScoreTerms(profile, shape, chKey, channel, opts = {}) {
198
200
  // 0 — a silent ROUTE-07 break. With it, every quotaHits=0 cell is byte-identical to pre-ROUTE-12.
199
201
  const perf = cell.n < MIN_SAMPLES || cell.doneMedianMs === undefined || cell.doneCount < MIN_SAMPLES
200
202
  ? 0
201
- : PERF_WEIGHT * (REF_MS / (REF_MS + cell.doneMedianMs) - 0.5);
203
+ : PERF_WEIGHT * (refMs / (refMs + cell.doneMedianMs) - 0.5);
202
204
  // PENALTY-ONLY availability (ROUTE-12): a throttling channel is less available — deprioritize it without ever
203
205
  // treating quota as a quality signal, ejecting it, or predicting quota. quotaHits=0 ⇒ avail 0 (no-throttle
204
206
  // byte-identity). Gated on dispatches (≥ MIN_SAMPLES > 0 ⇒ positive denominator, no divide-by-zero) INDEPENDENTLY
@@ -1,5 +1,5 @@
1
1
  import { type Assignment, type BillingChannel } from "../adapters/types.js";
2
- import { type TickmarkrConfig } from "../config/config.js";
2
+ import { type TickmarkrConfig, type Tier } 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";
@@ -26,9 +26,16 @@ export interface RoutingPreferContext {
26
26
  doctorFresh: boolean;
27
27
  overlayPreferShapes: ReadonlySet<string>;
28
28
  }
29
+ export interface ExploreContext {
30
+ noExplore?: boolean;
31
+ quality?: boolean;
32
+ }
33
+ export declare const NO_EXPLORE_ENV = "TICKMARKR_NO_EXPLORE";
34
+ export declare const QUALITY_ENV = "TICKMARKR_QUALITY";
35
+ export declare const raiseTier: (tier: Tier) => Tier;
29
36
  export declare class RoutingError extends Error {
30
37
  constructor(msg: string);
31
38
  }
32
39
  export declare function marginalCostRank(c: BillingChannel): number;
33
- export declare function route(task: Task, cfg: TickmarkrConfig, channels: BillingChannel[], profile?: RoutingProfile, preferCtx?: RoutingPreferContext, exclude?: ReadonlySet<string>): Route;
40
+ export declare function route(task: Task, cfg: TickmarkrConfig, channels: BillingChannel[], profile?: RoutingProfile, preferCtx?: RoutingPreferContext, exclude?: ReadonlySet<string>, exploreCtx?: ExploreContext): Route;
34
41
  export declare function nextChannel(current: Assignment, task: Task, cfg: TickmarkrConfig, channels: BillingChannel[], tried: string[], profile?: RoutingProfile, exclude?: ReadonlySet<string>): Assignment | null;
@@ -1,7 +1,27 @@
1
1
  import { channelKey, channelsFromConfig } from "../adapters/types.js";
2
2
  import { TIER_RANK } from "../config/config.js";
3
3
  import { disallowedBy } from "./preference.js";
4
- import { cellOf, EXPLORE_CAP, explorationBonus, learnedScore } from "./profile.js";
4
+ import { cellOf, EXPLORE_CAP, explorationBonus, learnedScore, MIN_SAMPLES } from "./profile.js";
5
+ export const NO_EXPLORE_ENV = "TICKMARKR_NO_EXPLORE";
6
+ export const QUALITY_ENV = "TICKMARKR_QUALITY";
7
+ const exploreCap = (cfg) => cfg.routing.explore?.cap ?? EXPLORE_CAP;
8
+ const qualityOn = (exploreCtx) => !!exploreCtx?.quality || process.env[QUALITY_ENV] === "1";
9
+ export const raiseTier = (tier) => tier === "cheap" ? "mid" : tier === "mid" ? "frontier" : "frontier";
10
+ const exploreOff = (task, cfg, exploreCtx) => {
11
+ if (qualityOn(exploreCtx) || exploreCtx?.noExplore || process.env[NO_EXPLORE_ENV] === "1")
12
+ return true;
13
+ const e = cfg.routing.explore;
14
+ if (!e)
15
+ return false;
16
+ if (e.mode === "off")
17
+ return true;
18
+ if (e.excludeShapes?.includes(task.shape))
19
+ return true;
20
+ const thr = e.excludeComplexityAtOrAbove;
21
+ if (thr != null && task.complexity >= thr)
22
+ return true;
23
+ return false;
24
+ };
5
25
  const autoPreferList = (doc, shape) => {
6
26
  const v = doc?.[shape];
7
27
  return Array.isArray(v) ? v : undefined;
@@ -50,10 +70,36 @@ function withoutExcluded(channels, exclude) {
50
70
  // OBS-57: in-run demotion after consecutive no-trailer windows — route around poisoned channels for the rest of the run.
51
71
  return channels.filter((c) => !exclude.has(channelKey(c)));
52
72
  }
53
- export function route(task, cfg, channels, profile, preferCtx, exclude) {
73
+ const qualityBound = (quality, configFloor, taskFloor) => {
74
+ if (!quality)
75
+ return undefined;
76
+ if (configFloor)
77
+ return `floor ${configFloor}→${raiseTier(configFloor)} (--quality)`;
78
+ if (taskFloor)
79
+ return `floor ${taskFloor}→${raiseTier(taskFloor)} (--quality)`;
80
+ return undefined;
81
+ };
82
+ const maybeSlaLint = (lints, task, profile, slaMinutes, c) => {
83
+ if (slaMinutes === undefined || !profile)
84
+ return;
85
+ const cell = cellOf(profile, task.shape, channelKey(c), c.channel);
86
+ if (!cell?.doneMedianMs || cell.doneCount < MIN_SAMPLES)
87
+ return;
88
+ const slaMs = slaMinutes * 60_000;
89
+ if (cell.doneMedianMs <= slaMs)
90
+ return;
91
+ const medianMin = Math.round(cell.doneMedianMs / 60_000);
92
+ lints.push(`${task.id} (${task.shape}): median ${medianMin}m exceeds sla ${slaMinutes}m — learned perf term references sla ${slaMinutes}m ref`);
93
+ };
94
+ export function route(task, cfg, channels, profile, preferCtx, exclude, exploreCtx) {
54
95
  channels = withoutExcluded(channels, exclude);
55
96
  const lints = [];
56
- const floor = cfg.routing.floors[task.shape];
97
+ const advisoryFloor = cfg.routing.floors[task.shape];
98
+ const quality = qualityOn(exploreCtx);
99
+ const floor = advisoryFloor;
100
+ const slaMinutes = cfg.routing.sla?.[task.shape];
101
+ // ponytail: sla is plan-time advisory only — never thread into learnedScore (would reroute warm rivals).
102
+ const scoreOpts = { availWeight: cfg.routing.learnedTuning?.availWeight };
57
103
  const entry = cfg.routing.map[task.shape];
58
104
  const prefer = effectivePrefer(task.shape, entry, preferCtx);
59
105
  const prefActive = !!(cfg.routing.allow || cfg.routing.deny);
@@ -83,11 +129,12 @@ export function route(task, cfg, channels, profile, preferCtx, exclude) {
83
129
  }
84
130
  };
85
131
  const lintFloor = (tier, what) => {
86
- if (floor && TIER_RANK[tier] < TIER_RANK[floor]) {
87
- lints.push(`${task.id} (${task.shape}): ${what} routes ${tier}, below advisory floor ${floor}`);
132
+ if (advisoryFloor && TIER_RANK[tier] < TIER_RANK[advisoryFloor]) {
133
+ lints.push(`${task.id} (${task.shape}): ${what} routes ${tier}, below advisory floor ${advisoryFloor}`);
88
134
  }
89
135
  };
90
- const taskFloor = task.routingHints?.floor;
136
+ const taskFloorRaw = task.routingHints?.floor;
137
+ const taskFloor = taskFloorRaw && quality ? raiseTier(taskFloorRaw) : taskFloorRaw;
91
138
  const source = task.routingHints?.source;
92
139
  const src = source ? `, ${source}` : ""; // never interpolate a possibly-undefined source
93
140
  // task pin: planner-authored, try-first — degrades on miss or below-floor (D-05, research A3), never throws
@@ -98,6 +145,7 @@ export function route(task, cfg, channels, profile, preferCtx, exclude) {
98
145
  const c = channels.find((c) => c.adapter === taskPin.via && c.model === taskPin.model);
99
146
  if (c && (!taskFloor || TIER_RANK[c.tier] >= TIER_RANK[taskFloor])) {
100
147
  lintFloor(c.tier, "task pin");
148
+ maybeSlaLint(lints, task, profile, slaMinutes, c);
101
149
  return { assignment: toAssignment(c), ladder: ladderFor(task, entry), lints, provenance: `pin ${taskPin.via}:${taskPin.model} (task hint${src})` };
102
150
  }
103
151
  const why = c ? `below task floor ${taskFloor}` : "unavailable";
@@ -109,10 +157,16 @@ export function route(task, cfg, channels, profile, preferCtx, exclude) {
109
157
  disallowedPin(entry.pin.via, entry.pin.model, "map pin (config routing.map)");
110
158
  const c = resolvePin(entry.pin, channels);
111
159
  lintFloor(c.tier, "map pin");
160
+ maybeSlaLint(lints, task, profile, slaMinutes, c);
112
161
  return { assignment: toAssignment(c), ladder: ladderFor(task, entry), lints, provenance: `${degraded}pin ${entry.pin.via}:${entry.pin.model} (config routing.map)` };
113
162
  }
114
163
  const baseTier = entry?.tier ?? floor ?? "cheap";
115
- const minTier = taskFloor && TIER_RANK[taskFloor] > TIER_RANK[baseTier] ? taskFloor : baseTier; // task floor is a hard >= constraint in all paths (D-04)
164
+ let minTier = taskFloor && TIER_RANK[taskFloor] > TIER_RANK[baseTier] ? taskFloor : baseTier; // task floor is a hard >= constraint in all paths (D-04)
165
+ if (quality && advisoryFloor) {
166
+ const raised = raiseTier(advisoryFloor);
167
+ if (TIER_RANK[raised] > TIER_RANK[minTier])
168
+ minTier = raised;
169
+ }
116
170
  if (entry?.tier)
117
171
  lintFloor(entry.tier, "map tier");
118
172
  if (prefActive)
@@ -145,13 +199,15 @@ export function route(task, cfg, channels, profile, preferCtx, exclude) {
145
199
  // is keyed by channelKey alone, which would collide only if a fleet exposed the same adapter:model on
146
200
  // both classes at once — impossible under today's scalar TierEntrySchema.channel.
147
201
  // ROUTE-15: availWeight threads from config as a pure param; undefined ⇒ module default (byte-identical).
148
- const scores = new Map(eligibleRaw.map((c) => [channelKey(c), learnedScore(profile, task.shape, channelKey(c), c.channel, { availWeight: cfg.routing.learnedTuning?.availWeight })]));
202
+ const scores = new Map(eligibleRaw.map((c) => [channelKey(c), learnedScore(profile, task.shape, channelKey(c), c.channel, scoreOpts)]));
149
203
  const scoreOf = (c) => scores.get(channelKey(c));
150
204
  // v1.6 Phase 14: exploration bonus precomputed ONCE (Pitfall 5), a new key ABOVE the score. EXP-01 needs
151
205
  // it above the score (a warm-good incumbent can't permanently outrank an under-cap rival in a static tie);
152
206
  // magnitude-free as its own lexicographic key. EXP-02 holds structurally: pins returned at :67/:78 and the
153
207
  // floor filtered eligibleRaw at :89, all upstream of this block, so the bonus decides only within a static tie.
154
- const bonuses = new Map(eligibleRaw.map((c) => [channelKey(c), explorationBonus(cellOf(profile, task.shape, channelKey(c), c.channel))]));
208
+ const cap = exploreCap(cfg);
209
+ const off = exploreOff(task, cfg, exploreCtx);
210
+ const bonuses = new Map(eligibleRaw.map((c) => [channelKey(c), off ? 0 : explorationBonus(cellOf(profile, task.shape, channelKey(c), c.channel), cap)]));
155
211
  const bonusOf = (c) => bonuses.get(channelKey(c));
156
212
  const staticWinner = [...eligibleRaw].sort(staticCmp)[0];
157
213
  // Phase 34 ROUTE-17: prefer becomes a BAND + group-rep keys so exploration fires ACROSS prefer
@@ -182,7 +238,7 @@ export function route(task, cfg, channels, profile, preferCtx, exclude) {
182
238
  const diffKey = ru ? firstDiff(w, ru) : -1;
183
239
  const probe = diffKey === 1 || diffKey === 6;
184
240
  if (probe) {
185
- learnedChosen = `via exploration probe (dispatches=${cellOf(profile, task.shape, channelKey(w), w.channel)?.dispatches ?? 0} < ${EXPLORE_CAP})`;
241
+ learnedChosen = `via exploration probe (dispatches=${cellOf(profile, task.shape, channelKey(w), w.channel)?.dispatches ?? 0} < ${cap})`;
186
242
  }
187
243
  else if (diffKey === 2 || diffKey === 7) {
188
244
  const n = cellOf(profile, task.shape, channelKey(w), w.channel)?.n ?? 0;
@@ -195,10 +251,11 @@ export function route(task, cfg, channels, profile, preferCtx, exclude) {
195
251
  else {
196
252
  eligible = eligibleRaw.sort(staticCmp); // ROUTE-07/09: literally the v1.5 code path — dead code cannot deviate
197
253
  }
198
- const bound = taskFloor && TIER_RANK[taskFloor] >= TIER_RANK[baseTier] ? `floor ${taskFloor} (task hint${src})` :
199
- entry?.tier ? `tier ${entry.tier} (config routing.map)` :
200
- floor ? `floor ${floor} (config floors)` :
201
- "tier cheap (default)";
254
+ const bound = qualityBound(quality, advisoryFloor, taskFloorRaw) ??
255
+ (taskFloor && TIER_RANK[taskFloor] >= TIER_RANK[baseTier] ? `floor ${taskFloor} (task hint${src})` :
256
+ entry?.tier ? `tier ${entry.tier} (config routing.map)` :
257
+ floor ? `floor ${floor} (config floors)` :
258
+ "tier cheap (default)");
202
259
  // name the key that actually broke the tie: prefer outranks the marginal-cost/tier keys, so if the
203
260
  // winner matched a prefer entry, prefer decided it — not "cheapest sufficient tier" (ROUTE-03, WR-01)
204
261
  const preferVia = preferFromAuto(task.shape, preferCtx)
@@ -206,6 +263,7 @@ export function route(task, cfg, channels, profile, preferCtx, exclude) {
206
263
  : "via prefer";
207
264
  const chosenBy = learnedChosen || (prefer && preferIndex(eligible[0], prefer) < prefer.length
208
265
  ? preferVia : "cheapest sufficient tier");
266
+ maybeSlaLint(lints, task, profile, slaMinutes, eligible[0]);
209
267
  return { assignment: toAssignment(eligible[0]), ladder: ladderFor(task, entry), lints, provenance: `${degraded}${bound}, marginal-cost auto (${chosenBy})`, ...(deviation ? { deviation } : {}) };
210
268
  }
211
269
  export function nextChannel(current, task, cfg, channels, tried, profile, exclude) {
@@ -427,7 +427,7 @@ export async function runDaemon(repoRoot, opts = {}) {
427
427
  const promptFile = writePrompt(journal.dir, t, attempt, feedback, nonce);
428
428
  // OBS-56: state the non-interactive, one-pass finish contract and the OBS-54 stall budget in every
429
429
  // worker prompt, not only consult retry guidance. Prepended so prompt.ts's completion trailer stays last.
430
- const workerContract = `## Harness contract\n- This harness is non-interactive: make one continuous pass; do not stop for questions or follow-up input.\n- You have a ${taskTimeoutMinutes} minute stall window. Budget the full suite once, then commit and emit the completion trailer before it expires.`;
430
+ const workerContract = `## Harness contract\n- This harness is non-interactive: make one continuous pass; do not stop for questions or follow-up input.\n- You have a ${taskTimeoutMinutes} minute stall window. Budget the full suite once, then commit and emit the completion trailer before it expires.\n- Each test: acceptance criterion must exist as a vitest test whose title matches the criterion string verbatim.`; // OBS-64
431
431
  // OBS-47: state the worktree layout contract in the worker prompt (cheap-tier workers were
432
432
  // committing/deleting node_modules and tripping the scope gate). The harness re-asserts the link
433
433
  // itself before gates regardless of what the worker does with it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tickmarkr",
3
- "version": "1.46.0",
3
+ "version": "1.48.0",
4
4
  "description": "Spec in, verified work out.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -14,7 +14,7 @@ When working in a multi-agent terminal environment, decide your role before star
14
14
 
15
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
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.
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 — dim ghost-text suggestions are UI, not queued input; ANSI-verify before alarming) and close its tab — the journal, records, and ledger hold the story; scrollback is disposable.
18
18
 
19
19
  Outside a multi-agent terminal environment, run the loop directly.
20
20
 
@@ -60,7 +60,7 @@ After sending, **confirm delivery** by reading the target pane and verifying the
60
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.
61
61
  2. **Compile** — run `tickmarkr compile <spec-or-directory>`. Fix source-spec defects instead of editing the generated graph.
62
62
  3. **Plan** — run `tickmarkr plan`. Review routes, capability-floor warnings, and human gates before execution.
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.
63
+ 4. **Run** — run `tickmarkr run`. Watch the run journal for its terminal event rather than polling agents — a self-terminating poll (`until grep -q '"event":"run-end"' <state-dir>/runs/<runId>/journal.jsonl; do sleep 20; done`), never `tail -F | grep -m1` (wedges on the journal's final line) and never a pane-level done wait (turn-end flaps). Resolve blocked interactions in the relevant agent session.
64
64
  5. **Verify and consolidate** — continue only after 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 work on `tickmarkr/<runId>` and never signs off to the main branch. A human controls any later release merge.
65
65
  6. **Record** — write `tickmarkr report <runId> --md` beside the source spec and commit the execution record when the repository tracks those records.
66
66
  7. **Continue** — move to the next requested target. If a target fails or is parked, stop with the journal evidence rather than silently skipping it.
@@ -53,13 +53,13 @@ After sending, **confirm delivery** by reading the target pane and verifying the
53
53
  ## Stand-down (mission end and retirement)
54
54
 
55
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.
56
+ - **Supervisor, when a mission completes** (and always before spawning the next orchestrator): verify the orchestrator stood down, then close its tab. Seeming input-box text in a retired pane can be the TUI's dim ghost-text suggestion, not queued input — confirm with an ANSI read (dim escape around the text) or type-one-char-and-read-back before treating it as the loaded gun; close the tab either way. 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
57
 
58
58
  ## The loop
59
59
 
60
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.
61
61
  2. **Compile** — run `tickmarkr compile <spec>`. Correct compilation errors in the spec, never in the generated graph.
62
62
  3. **Plan** — run `tickmarkr plan`. Review the routing table, capability-floor warnings, and every human gate, including work that each gate blocks.
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.
63
+ 4. **Run** — run `tickmarkr run`. Watch the run journal for its terminal event rather than repeatedly polling agents. Use a self-terminating poll — `until grep -q '"event":"run-end"' <state-dir>/runs/<runId>/journal.jsonl; do sleep 20; done` — never `tail -F | grep -m1` (run-end is the journal's last line, so tail never notices the broken pipe and the watcher hangs forever) and never a pane-level done wait (it fires on every agent turn end, not mission end). Resolve blocked interactions in the agent session; do not turn them into proxy questions.
64
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.
65
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).