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.
- package/dist/adapters/kimi.d.ts +5 -0
- package/dist/adapters/kimi.js +79 -0
- package/dist/adapters/model-lints.d.ts +19 -0
- package/dist/adapters/model-lints.js +74 -1
- package/dist/adapters/registry.d.ts +11 -0
- package/dist/adapters/registry.js +41 -5
- package/dist/cli/commands/doctor.js +9 -3
- package/dist/cli/commands/fleet.d.ts +5 -0
- package/dist/cli/commands/fleet.js +249 -0
- package/dist/cli/commands/plan.js +7 -1
- package/dist/cli/commands/run.js +34 -17
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +3 -1
- package/dist/compile/native.js +16 -1
- package/dist/config/config.d.ts +42 -0
- package/dist/config/config.js +300 -1
- package/dist/gates/acceptance.js +7 -2
- package/dist/gates/scope.d.ts +2 -1
- package/dist/gates/scope.js +11 -1
- package/dist/route/profile.d.ts +3 -1
- package/dist/route/profile.js +5 -3
- package/dist/route/router.d.ts +9 -2
- package/dist/route/router.js +72 -14
- package/dist/run/daemon.js +1 -1
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +2 -2
- package/skills/tickmarkr-loop/SKILL.md +2 -2
package/dist/route/profile.js
CHANGED
|
@@ -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
|
-
|
|
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 /
|
|
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 * (
|
|
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
|
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 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
|
|
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;
|
package/dist/route/router.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 (
|
|
87
|
-
lints.push(`${task.id} (${task.shape}): ${what} routes ${tier}, below advisory 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
|
|
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
|
-
|
|
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,
|
|
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
|
|
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} < ${
|
|
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 =
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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) {
|
package/dist/run/daemon.js
CHANGED
|
@@ -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
|
@@ -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).
|