tickmarkr 2.6.0 → 2.6.2
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 +14 -3
- package/dist/adapters/catalog-remote.js +89 -47
- package/dist/adapters/claude-code.js +9 -6
- package/dist/adapters/codex.js +7 -4
- package/dist/adapters/prompt.d.ts +3 -1
- package/dist/adapters/prompt.js +21 -3
- package/dist/adapters/registry.js +3 -3
- package/dist/adapters/types.d.ts +13 -4
- package/dist/adapters/types.js +16 -0
- package/dist/cli/commands/approve.d.ts +5 -1
- package/dist/cli/commands/approve.js +66 -23
- package/dist/cli/commands/compile.js +13 -3
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.js +11 -3
- package/dist/cli/commands/fleet.js +71 -8
- package/dist/cli/commands/plan.js +7 -3
- package/dist/cli/commands/report.js +18 -2
- package/dist/cli/commands/resume.js +4 -2
- package/dist/cli/commands/status.js +37 -22
- package/dist/cli/help.d.ts +4 -0
- package/dist/cli/help.js +11 -2
- package/dist/config/config.d.ts +20 -0
- package/dist/config/config.js +47 -8
- package/dist/config/fleet-overlay.d.ts +1 -0
- package/dist/config/fleet-overlay.js +56 -0
- package/dist/drivers/orca.d.ts +26 -1
- package/dist/drivers/orca.js +199 -61
- package/dist/eval/canary.d.ts +2 -1
- package/dist/eval/canary.js +2 -2
- package/dist/eval/dispatch.js +1 -0
- package/dist/gates/acceptance.d.ts +2 -1
- package/dist/gates/acceptance.js +7 -2
- package/dist/gates/baseline.d.ts +12 -1
- package/dist/gates/baseline.js +11 -4
- package/dist/gates/cache.d.ts +3 -1
- package/dist/gates/cache.js +10 -3
- package/dist/gates/llm.d.ts +5 -4
- package/dist/gates/llm.js +17 -14
- package/dist/gates/review.d.ts +8 -0
- package/dist/gates/review.js +47 -15
- package/dist/gates/run-gates.d.ts +6 -1
- package/dist/gates/run-gates.js +34 -17
- package/dist/gates/test-manifest.d.ts +14 -0
- package/dist/gates/test-manifest.js +33 -6
- package/dist/graph/graph.d.ts +6 -2
- package/dist/graph/graph.js +15 -4
- package/dist/graph/schema.d.ts +2 -0
- package/dist/graph/schema.js +2 -0
- package/dist/plan/scope.js +2 -2
- package/dist/route/preference.d.ts +20 -2
- package/dist/route/preference.js +48 -13
- package/dist/route/role-pick.d.ts +16 -0
- package/dist/route/role-pick.js +15 -0
- package/dist/route/router.d.ts +14 -0
- package/dist/route/router.js +39 -16
- package/dist/run/consult.d.ts +13 -1
- package/dist/run/consult.js +19 -14
- package/dist/run/daemon.d.ts +46 -2
- package/dist/run/daemon.js +964 -190
- package/dist/run/git.d.ts +44 -1
- package/dist/run/git.js +103 -5
- package/dist/run/host-health.d.ts +20 -0
- package/dist/run/host-health.js +64 -0
- package/dist/run/journal.d.ts +126 -3
- package/dist/run/journal.js +418 -36
- package/dist/run/merge.d.ts +3 -1
- package/dist/run/merge.js +3 -2
- package/dist/run/operator-state.d.ts +24 -2
- package/dist/run/operator-state.js +41 -5
- package/dist/run/operator-summary.d.ts +3 -0
- package/dist/run/operator-summary.js +3 -1
- package/dist/run/protocol.d.ts +31 -1
- package/dist/run/protocol.js +3 -1
- package/dist/run/stall.d.ts +38 -2
- package/dist/run/stall.js +276 -6
- package/dist/run/supervision.d.ts +7 -1
- package/dist/run/supervision.js +5 -2
- package/dist/tui/cockpit/board.d.ts +1 -1
- package/dist/tui/cockpit/board.js +30 -22
- package/dist/tui/cockpit/decision-actions.d.ts +8 -5
- package/dist/tui/cockpit/decision-actions.js +55 -32
- package/dist/tui/cockpit/derive.d.ts +2 -0
- package/dist/tui/cockpit/derive.js +17 -2
- package/dist/tui/cockpit/live-runtime.d.ts +10 -0
- package/dist/tui/cockpit/live-runtime.js +50 -3
- package/dist/tui/cockpit/live-store.d.ts +1 -0
- package/dist/tui/cockpit/live-store.js +31 -8
- package/dist/tui/cockpit/run-cockpit.d.ts +3 -0
- package/dist/tui/cockpit/run-cockpit.js +28 -2
- package/dist/tui/cockpit/run-view.d.ts +11 -7
- package/dist/tui/cockpit/run-view.js +69 -15
- package/dist/tui/cockpit/setup-cockpit.d.ts +4 -0
- package/dist/tui/cockpit/setup-cockpit.js +6 -3
- package/dist/tui/ink/fleet-app.d.ts +15 -3
- package/dist/tui/ink/fleet-app.js +91 -22
- package/package.json +2 -1
- package/schema/config.schema.json +818 -0
- package/skills/tickmarkr-loop/SKILL.md +8 -2
- package/skills/tickmarkr-overseer/SKILL.md +85 -6
- package/skills/tickmarkr-overseer/scripts/classify-vitest-log.sh +88 -0
- package/skills/tickmarkr-overseer/scripts/context-statusline.sh +81 -0
- package/skills/tickmarkr-overseer/scripts/grade-ci.sh +36 -34
- package/skills/tickmarkr-overseer/scripts/watch-journal.sh +6 -4
|
@@ -8,10 +8,11 @@ import { formatOwnedName, parseOwnedName, } from "../../drivers/types.js";
|
|
|
8
8
|
import { blockedTasks, graphDefinitionHash, loadGraph, stateDirName } from "../../graph/graph.js";
|
|
9
9
|
import { GATE_NAMES } from "../../graph/schema.js";
|
|
10
10
|
import { projectActivity } from "../../run/activity.js";
|
|
11
|
+
import { authorsNote, doneAuthors } from "../../run/operator-state.js";
|
|
11
12
|
import { projectOperatorSummary } from "../../run/operator-summary.js";
|
|
12
13
|
import { trackJournalRows } from "../../run/protocol.js";
|
|
13
14
|
import { newestPark, permittedDecisionVerbs } from "./approve.js";
|
|
14
|
-
import { Journal, formatJournalNarration, engagementComparable, isQualityFailureParkKind, parseRunId, preservedRefsByTask, recordedTaskFailureKind, runHasEnded, upheldFeedbackByTask, } from "../../run/journal.js";
|
|
15
|
+
import { Journal, bindingToken, effectiveEvents, formatJournalNarration, parseJournalText, physicalLine, engagementComparable, isQualityFailureParkKind, parseRunId, preservedRefsByTask, recordedTaskFailureKind, runHasEnded, upheldFeedbackByTask, } from "../../run/journal.js";
|
|
15
16
|
import { isPidLive, runLockRunId, runStatusLine } from "../../run/lock.js";
|
|
16
17
|
import { normalizeGateOutcome } from "../../run/outcome.js";
|
|
17
18
|
import { desiredPanes } from "../../run/reconcile.js";
|
|
@@ -166,7 +167,7 @@ export const decisionEventsFromJournal = (events, runId, stateDir = ".tickmarkr"
|
|
|
166
167
|
...base,
|
|
167
168
|
type: "human-decision-required",
|
|
168
169
|
tier: "decision",
|
|
169
|
-
approvalCommand: `tickmarkr approve ${runId} ${event.taskId}`,
|
|
170
|
+
approvalCommand: `tickmarkr approve ${runId} ${event.taskId} --park ${bindingToken({ line: physicalLine(events, index), ts: event.ts })}`,
|
|
170
171
|
...(typeof event.data.kind === "string" ? { kind: event.data.kind } : {}),
|
|
171
172
|
...(typeof event.data.reason === "string" ? { reason: event.data.reason } : {}),
|
|
172
173
|
}];
|
|
@@ -840,18 +841,6 @@ const statusEngagement = (events, loadedHash) => {
|
|
|
840
841
|
}
|
|
841
842
|
return { comparable: true };
|
|
842
843
|
};
|
|
843
|
-
// The journal's own reader rule (src/run/journal.ts readJsonl), applied to bytes already in hand:
|
|
844
|
-
// skip blanks, drop a line that will not parse (a torn trailing write after a crash), keep the rest.
|
|
845
|
-
const parseJournalSnapshot = (raw) => raw.split("\n").flatMap((line) => {
|
|
846
|
-
if (!line.trim())
|
|
847
|
-
return [];
|
|
848
|
-
try {
|
|
849
|
-
return [JSON.parse(line)];
|
|
850
|
-
}
|
|
851
|
-
catch {
|
|
852
|
-
return [];
|
|
853
|
-
}
|
|
854
|
-
});
|
|
855
844
|
// runLockOwner is the single lock payload/liveness reader. Its current path helper also initializes
|
|
856
845
|
// .tickmarkr metadata; an engine-written lock necessarily passed through that initializer already, so
|
|
857
846
|
// status avoids invoking it in stripped reader-purity fixtures where no engine lock can exist.
|
|
@@ -891,7 +880,7 @@ const readRunRecord = (cwd, graph, namedRunId) => {
|
|
|
891
880
|
if (!runId)
|
|
892
881
|
return undefined;
|
|
893
882
|
const raw = readRunJournalRaw(cwd, runId, lockedRunId === runId);
|
|
894
|
-
const events =
|
|
883
|
+
const events = parseJournalText(raw);
|
|
895
884
|
// The resume comparator is the fail-closed baseline; a matching graph-rehash is the daemon's
|
|
896
885
|
// append-only audit that authorizes this status replay after stop-amend-resume.
|
|
897
886
|
const engagement = statusEngagement(events, graphDefinitionHash(graph));
|
|
@@ -917,7 +906,8 @@ const recordTaskRows = (record, graph, isDaemonAlive) => record.comparable && re
|
|
|
917
906
|
const recordProjection = (record, graph, isDaemonAlive) => {
|
|
918
907
|
const rows = record ? recordTaskRows(record, graph, isDaemonAlive) : new Map();
|
|
919
908
|
const events = record?.comparable ? record.events : [];
|
|
920
|
-
|
|
909
|
+
// OBS-1178: a refused or unsound decision released nothing, so the activity fold never reads it.
|
|
910
|
+
const activity = projectActivity(record?.runId ?? "", trackJournalRows(record?.runId ?? "", effectiveEvents(events).map((raw, sourceIndex) => ({ raw, sourceIndex }))), graph.tasks);
|
|
921
911
|
const tasks = graph.tasks.map(task => {
|
|
922
912
|
const row = rows.get(task.id);
|
|
923
913
|
const evidence = events.filter(event => event.taskId === task.id);
|
|
@@ -988,6 +978,7 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
988
978
|
const comparable = record?.comparable ?? false;
|
|
989
979
|
const rehashAt = record?.rehashAt;
|
|
990
980
|
const assignments = new Map();
|
|
981
|
+
const authorsByTask = new Map();
|
|
991
982
|
const contexts = new Map();
|
|
992
983
|
// v1.53 T5: this run is dead — a newer run replaced it
|
|
993
984
|
const supersededBy = [...events].reverse()
|
|
@@ -1000,6 +991,11 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
1000
991
|
assignments.set(e.taskId, `${a.adapter}:${a.model}`);
|
|
1001
992
|
}
|
|
1002
993
|
}
|
|
994
|
+
if (e.event === "task-done" && e.taskId) {
|
|
995
|
+
const authors = doneAuthors(e.data);
|
|
996
|
+
if (authors)
|
|
997
|
+
authorsByTask.set(e.taskId, authors);
|
|
998
|
+
}
|
|
1003
999
|
if (e.event === "context-sample" && e.taskId && typeof e.data.tokens === "number" && Number.isFinite(e.data.tokens)) {
|
|
1004
1000
|
contexts.set(e.taskId, e.data.tokens); // last write wins
|
|
1005
1001
|
}
|
|
@@ -1061,12 +1057,28 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
1061
1057
|
// OBS-738: recovery facts stay on the journal's two established reducers. The prefix fold keeps an
|
|
1062
1058
|
// older journal equally readable by asking upheldFeedbackByTask what was active at that exact
|
|
1063
1059
|
// restore boundary; current resume-restore rows additionally carry the same task for narration.
|
|
1060
|
+
// OBS-1178: both folds read decisions through the one decision fold over the whole journal.
|
|
1061
|
+
const decided = effectiveEvents(events);
|
|
1064
1062
|
const preservedRefs = preservedRefsByTask(events);
|
|
1065
|
-
const restoredUpheldFeedback = new Set(
|
|
1066
|
-
&& upheldFeedbackByTask(
|
|
1063
|
+
const restoredUpheldFeedback = new Set(decided.flatMap((event, index) => event.event === "resume-restore" && event.taskId
|
|
1064
|
+
&& upheldFeedbackByTask(decided.slice(0, index)).has(event.taskId)
|
|
1067
1065
|
? [event.taskId]
|
|
1068
1066
|
: []));
|
|
1067
|
+
// OBS-1178: a failed task's newest task-failed row is the token its recheck binds to. Only an
|
|
1068
|
+
// effective decision consumes it — a refused or unsound recheck leaves the task failed and the token shown.
|
|
1069
|
+
const failureTokens = new Map();
|
|
1070
|
+
decided.forEach((event, index) => {
|
|
1071
|
+
if (!event.taskId || !["task-dispatch", "task-done", "task-failed", "task-human", "task-approved"].includes(event.event))
|
|
1072
|
+
return;
|
|
1073
|
+
if (event.event === "task-failed")
|
|
1074
|
+
failureTokens.set(event.taskId, bindingToken({ line: physicalLine(decided, index), ts: event.ts }));
|
|
1075
|
+
else
|
|
1076
|
+
failureTokens.delete(event.taskId);
|
|
1077
|
+
});
|
|
1069
1078
|
const recoveryLinesForTask = (taskId) => [
|
|
1079
|
+
...(failureTokens.has(taskId) && runId
|
|
1080
|
+
? [`failed — ${taskId} — failure ${failureTokens.get(taskId)} — re-gate landed work with \`tickmarkr approve ${runId} ${taskId} --recheck --park ${failureTokens.get(taskId)}\``]
|
|
1081
|
+
: []),
|
|
1070
1082
|
...(preservedRefs.get(taskId) ?? []).flatMap(({ ref, diffCommand }) => [
|
|
1071
1083
|
`preserved worktree — ${taskId} — ${ref}`,
|
|
1072
1084
|
` ${diffCommand}`,
|
|
@@ -1106,12 +1118,14 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
1106
1118
|
const label = isStarved ? " starved" : phrase ? ` ${phrase}` : "";
|
|
1107
1119
|
const channel = journalRowsOnly ? folded?.actor ?? "-" : assignments.get(t.id) ?? "-";
|
|
1108
1120
|
const ctx = contexts.get(t.id);
|
|
1109
|
-
|
|
1121
|
+
// Merged authors are their own fact beside the last dispatch: a carry-only seat authored nothing.
|
|
1122
|
+
const authors = authorsNote(authorsByTask.get(t.id), channel);
|
|
1123
|
+
const assignCol = [channel, ctx !== undefined ? `ctx ${ctx}` : "", authors ?? ""].filter(Boolean).join(divider);
|
|
1110
1124
|
const gates = comparable
|
|
1111
1125
|
? gateSnapshot(t, events, rehashAt)
|
|
1112
1126
|
: { states: defaultGateStates(t), priorGraph: false };
|
|
1113
1127
|
const pane = panes.get(t.id);
|
|
1114
|
-
return { t, st, merged, failureKind, redTier, label, assignCol, isStarved, phrase, channel, ctx, livePhase, pane, summary, ...gates };
|
|
1128
|
+
return { t, st, merged, failureKind, redTier, label, assignCol, isStarved, phrase, channel, ctx, authors, livePhase, pane, summary, ...gates };
|
|
1115
1129
|
});
|
|
1116
1130
|
if (!unicode) {
|
|
1117
1131
|
// Keep the machine task-title columns intact; projection details occupy their own line.
|
|
@@ -1215,7 +1229,7 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
1215
1229
|
}).join("");
|
|
1216
1230
|
const gateHeader = GATE_NAMES.map((gate) => fitCells(gate.slice(0, 2), 3)).join("");
|
|
1217
1231
|
const noteFor = (cell) => {
|
|
1218
|
-
const { t, st, merged, failureKind, redTier, states, priorGraph, isStarved, phrase, ctx, livePhase, pane } = cell;
|
|
1232
|
+
const { t, st, merged, failureKind, redTier, states, priorGraph, isStarved, phrase, ctx, livePhase, pane, authors } = cell;
|
|
1219
1233
|
const effort = effortByTask.get(t.id);
|
|
1220
1234
|
const blocking = graphTaskStatus(st, t.status) === "done" ? [] : dependents.get(t.id) ?? [];
|
|
1221
1235
|
const failed = failedGates(states);
|
|
@@ -1242,6 +1256,7 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
1242
1256
|
blocking.length ? `blocks ${blocking.join(", ")}` : "",
|
|
1243
1257
|
pane ? `pane ${pane}` : "",
|
|
1244
1258
|
ctx !== undefined ? `ctx ${ctx}` : "",
|
|
1259
|
+
authors ?? "",
|
|
1245
1260
|
].filter(Boolean).join(" · ");
|
|
1246
1261
|
};
|
|
1247
1262
|
const staleWorker = (cell) => cell.livePhase?.phase === "worker"
|
|
@@ -1491,7 +1506,7 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
|
1491
1506
|
decisionRunId = runId;
|
|
1492
1507
|
journalCursor = 0;
|
|
1493
1508
|
}
|
|
1494
|
-
const journalEvents =
|
|
1509
|
+
const journalEvents = parseJournalText(readRunJournalRaw(cwd, runId, lockedRunId === runId));
|
|
1495
1510
|
if (journalEvents.length < journalCursor)
|
|
1496
1511
|
journalCursor = 0;
|
|
1497
1512
|
const fresh = decisionEventsFromJournal(journalEvents, runId, stateDirName(cwd))
|
package/dist/cli/help.d.ts
CHANGED
|
@@ -39,6 +39,8 @@ export declare const COMMAND_HELP: {
|
|
|
39
39
|
usage: string;
|
|
40
40
|
description: string;
|
|
41
41
|
options: {
|
|
42
|
+
"--pick <role>": string;
|
|
43
|
+
"--exclude-vendor <vendor>": string;
|
|
42
44
|
"--print": string;
|
|
43
45
|
"--why": string;
|
|
44
46
|
"--global-dir <path>": string;
|
|
@@ -161,6 +163,8 @@ export declare const COMMAND_HELP: {
|
|
|
161
163
|
usage: string;
|
|
162
164
|
description: string;
|
|
163
165
|
options: {
|
|
166
|
+
"--park <line>@<ts>": string;
|
|
167
|
+
"--gate <gate>": string;
|
|
164
168
|
"--files <glob,\u2026>": string;
|
|
165
169
|
"--by <name>": string;
|
|
166
170
|
"--reason <text>": string;
|
package/dist/cli/help.js
CHANGED
|
@@ -30,6 +30,8 @@ export const COMMAND_HELP = {
|
|
|
30
30
|
fleet: {
|
|
31
31
|
usage: "fleet [options]", description: "Edit fleet configuration interactively, or print it for scripts.",
|
|
32
32
|
options: {
|
|
33
|
+
"--pick <role>": "Resolve one role preference as JSON without launching a seat (review or consult; roles without prefer refuse).",
|
|
34
|
+
"--exclude-vendor <vendor>": "Exclude a vendor from --pick; repeat for multiple vendors.",
|
|
33
35
|
"--print": "Print fleet configuration without opening the editor.",
|
|
34
36
|
"--why": "Include routing eligibility explanations in printed output.",
|
|
35
37
|
"--global-dir <path>": "Use this global configuration directory.",
|
|
@@ -129,14 +131,21 @@ export const COMMAND_HELP = {
|
|
|
129
131
|
approve: {
|
|
130
132
|
usage: "approve <run-id> <task-id> [options]", description: "Append a validated park decision. A running owner may enact it; a closed run requires a separate resume. Decisions cannot be undone.",
|
|
131
133
|
options: {
|
|
134
|
+
"--park <line>@<ts>": "Bind the decision to the park token status prints (a failed task's recheck binds its failure token); refused once a newer park opens.",
|
|
135
|
+
"--gate <gate>": "Bind a waive to the park's failed gate; refused when the park failed another gate.",
|
|
132
136
|
"--files <glob,…>": "Extend files[] for a scope-request park with comma-separated repository-relative globs; required for scope approval.",
|
|
133
137
|
"--by <name>": "Name the actor (default: current OS user).",
|
|
134
138
|
"--reason <text>": "Record the decision reason.",
|
|
135
139
|
"--waive": "Waive only the identified failed gate.",
|
|
136
140
|
"--uphold": "Uphold a review failure and fund a fixed attempt.",
|
|
137
141
|
"--recheck": "Request rechecking an infra or failed-gate park; satisfies no gate.",
|
|
138
|
-
"--review-rounds <N>": "Set a positive integer review-round ceiling
|
|
139
|
-
}, examples: [
|
|
142
|
+
"--review-rounds <N>": "Set a positive integer review-round ceiling that binds only this approval engagement; a later approval without --review-rounds restores the built-in default.",
|
|
143
|
+
}, examples: [
|
|
144
|
+
"approve run-example T1 --park 42@2026-09-26T08:00:00.000Z --by operator --reason 'ready to proceed'",
|
|
145
|
+
"approve run-example T1 --waive --park 42@2026-09-26T08:00:00.000Z --gate review",
|
|
146
|
+
"approve run-example T1 --recheck --park 42@2026-09-26T08:00:00.000Z --reason 'infra recovered'",
|
|
147
|
+
"approve run-example T3 --recheck --park 57@2026-09-26T08:05:00.000Z --reason 'failed task: re-gate its landed commits'",
|
|
148
|
+
],
|
|
140
149
|
},
|
|
141
150
|
beat: {
|
|
142
151
|
usage: "beat <orchestrator|orchestrator-context|overseer|overseer-context|watch> --seat <identity> [options]",
|
package/dist/config/config.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { type Effort } from "../graph/schema.js";
|
|
2
3
|
declare const TierEnum: z.ZodEnum<{
|
|
3
4
|
cheap: "cheap";
|
|
4
5
|
mid: "mid";
|
|
@@ -41,6 +42,7 @@ export declare const MapEntrySchema: z.ZodObject<{
|
|
|
41
42
|
escalate: z.ZodOptional<z.ZodBoolean>;
|
|
42
43
|
}, z.core.$strip>;
|
|
43
44
|
export type MapEntry = z.infer<typeof MapEntrySchema>;
|
|
45
|
+
export declare const EFFORT_ADAPTERS: readonly ["claude-code", "codex"];
|
|
44
46
|
export declare const TierEntrySchema: z.ZodObject<{
|
|
45
47
|
vendor: z.ZodNullable<z.ZodString>;
|
|
46
48
|
channel: z.ZodEnum<{
|
|
@@ -58,6 +60,11 @@ export declare const TierEntrySchema: z.ZodObject<{
|
|
|
58
60
|
sub: "sub";
|
|
59
61
|
api: "api";
|
|
60
62
|
}>>;
|
|
63
|
+
effort: z.ZodOptional<z.ZodEnum<{
|
|
64
|
+
low: "low";
|
|
65
|
+
medium: "medium";
|
|
66
|
+
high: "high";
|
|
67
|
+
}>>;
|
|
61
68
|
}, z.core.$strip>>>;
|
|
62
69
|
windows: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
63
70
|
}, z.core.$strip>;
|
|
@@ -274,6 +281,11 @@ export declare const TickmarkrConfigSchema: z.ZodObject<{
|
|
|
274
281
|
sub: "sub";
|
|
275
282
|
api: "api";
|
|
276
283
|
}>>;
|
|
284
|
+
effort: z.ZodOptional<z.ZodEnum<{
|
|
285
|
+
low: "low";
|
|
286
|
+
medium: "medium";
|
|
287
|
+
high: "high";
|
|
288
|
+
}>>;
|
|
277
289
|
}, z.core.$strip>>>;
|
|
278
290
|
windows: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
279
291
|
}, z.core.$strip>>;
|
|
@@ -383,6 +395,12 @@ export declare function loadConfigWithMode(repoRoot: string, opts?: {
|
|
|
383
395
|
cfg: TickmarkrConfig;
|
|
384
396
|
mode: ModeResolution;
|
|
385
397
|
};
|
|
398
|
+
/** OBS-1182: tiers.<adapter>.modelOverrides as the layers under the repo overlay (defaults + global)
|
|
399
|
+
* merge them. Read raw, never schema-validated: a lower layer may only validate beside repo fields
|
|
400
|
+
* (global `vendor: null` completed by a repo vendor) and its override metadata is still inherited. */
|
|
401
|
+
export declare function lowerLayerModelOverrides(opts?: {
|
|
402
|
+
globalDir?: string;
|
|
403
|
+
}): Record<string, Record<string, Record<string, unknown>>>;
|
|
386
404
|
export declare function loadConfig(repoRoot: string, opts?: {
|
|
387
405
|
globalDir?: string;
|
|
388
406
|
}): TickmarkrConfig;
|
|
@@ -416,6 +434,8 @@ export type FleetEditable = Partial<Record<DenyScopeKey, string[]>> & {
|
|
|
416
434
|
denyModels: string[];
|
|
417
435
|
allowOut?: string[];
|
|
418
436
|
tiers: Record<string, Record<string, FleetTierAssignment | null>>;
|
|
437
|
+
/** OBS-1182: tiers.<adapter>.modelOverrides.<model>.effort — absent = CLI default; present only when one is set */
|
|
438
|
+
efforts?: Record<string, Record<string, Effort>>;
|
|
419
439
|
map: Record<string, MapEntry>;
|
|
420
440
|
floors: Record<string, Tier>;
|
|
421
441
|
};
|
package/dist/config/config.js
CHANGED
|
@@ -2,11 +2,11 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { filesGlob } from "../graph/files-glob.js";
|
|
5
|
-
import { Document, parse } from "yaml";
|
|
5
|
+
import { Document, isScalar, parse } from "yaml";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { CITED_MODEL_WINDOWS } from "../adapters/model-windows.js";
|
|
8
8
|
import { stateDirName } from "../graph/graph.js";
|
|
9
|
-
import { SHAPES, TIERS } from "../graph/schema.js";
|
|
9
|
+
import { EFFORTS, SHAPES, TIERS } from "../graph/schema.js";
|
|
10
10
|
const TierEnum = z.enum(TIERS, {
|
|
11
11
|
error: (iss) => `Invalid option: expected one of "cheap"|"mid"|"frontier" (got ${JSON.stringify(iss.input)})`,
|
|
12
12
|
});
|
|
@@ -51,21 +51,32 @@ export const MapEntrySchema = z.object({
|
|
|
51
51
|
escalate: z.boolean().optional(),
|
|
52
52
|
});
|
|
53
53
|
const DeclaredVendorSchema = z.string().trim().min(1, "vendor must be nonempty");
|
|
54
|
-
|
|
54
|
+
// OBS-1182: effort is launch metadata only these CLIs accept (claude --effort, codex -c
|
|
55
|
+
// model_reasoning_effort=). Every other adapter's override refuses the key outright, so config
|
|
56
|
+
// load and the emitted JSON schema both reject an effort that could never be launched.
|
|
57
|
+
export const EFFORT_ADAPTERS = ["claude-code", "codex"];
|
|
58
|
+
const EffortEnum = z.enum(EFFORTS, {
|
|
59
|
+
error: (iss) => `Invalid effort: expected one of "low"|"medium"|"high" (got ${JSON.stringify(iss.input)})`,
|
|
60
|
+
});
|
|
61
|
+
const NoEffort = z.never({
|
|
62
|
+
error: () => `effort is launch metadata only ${EFFORT_ADAPTERS.join(" and ")} accept — this adapter's CLI takes no effort level`,
|
|
63
|
+
});
|
|
64
|
+
const modelOverrideSchema = (effort) => z
|
|
55
65
|
.object({
|
|
56
66
|
vendor: DeclaredVendorSchema.optional(),
|
|
57
67
|
channel: z.enum(["sub", "api"]).optional(),
|
|
68
|
+
effort: effort.optional(),
|
|
58
69
|
})
|
|
59
|
-
.refine((override) => override.vendor !== undefined || override.channel !== undefined, {
|
|
60
|
-
message: "a model override must declare vendor, channel, or
|
|
70
|
+
.refine((override) => override.vendor !== undefined || override.channel !== undefined || override.effort !== undefined, {
|
|
71
|
+
message: "a model override must declare vendor, channel, effort, or a combination",
|
|
61
72
|
});
|
|
62
|
-
|
|
73
|
+
const tierEntrySchema = (effort) => z.object({
|
|
63
74
|
vendor: DeclaredVendorSchema.nullable(),
|
|
64
75
|
channel: z.enum(["sub", "api"]),
|
|
65
76
|
models: z.record(z.string(), TierEnum),
|
|
66
77
|
// T19: keep model values scalar for existing consumers. Optional sibling metadata resolves before
|
|
67
78
|
// parent metadata in channelsFromConfig; neither provider prefixes nor adapter ids manufacture it.
|
|
68
|
-
modelOverrides: z.record(z.string(),
|
|
79
|
+
modelOverrides: z.record(z.string(), modelOverrideSchema(effort)).optional(),
|
|
69
80
|
// v1.47 T3: optional per-model context-window sizes (tokens). Absent block ⇒ no doctor column, no plan lint.
|
|
70
81
|
windows: z.record(z.string(), z.number().int().positive()).optional(),
|
|
71
82
|
}).superRefine((entry, ctx) => {
|
|
@@ -81,6 +92,13 @@ export const TierEntrySchema = z.object({
|
|
|
81
92
|
});
|
|
82
93
|
}
|
|
83
94
|
});
|
|
95
|
+
export const TierEntrySchema = tierEntrySchema(EffortEnum);
|
|
96
|
+
// Per-adapter object + catchall rather than a record, so the emitted JSON schema carries the same
|
|
97
|
+
// adapter restriction as the loader; typed as the record every consumer already reads.
|
|
98
|
+
// ponytail: catchall parses the EFFORT_ADAPTERS keys first — the seed table already lists them first.
|
|
99
|
+
const TiersSchema = z
|
|
100
|
+
.object(Object.fromEntries(EFFORT_ADAPTERS.map((id) => [id, TierEntrySchema.optional()])))
|
|
101
|
+
.catchall(tierEntrySchema(NoEffort));
|
|
84
102
|
// v1.10 FLEET-06: optional routing.allow/deny fleet preference; absent blocks ⇒ byte-identical routing/discovery.
|
|
85
103
|
// deny wins on conflict; presence of allow (even {} / empty arrays) activates allowlist (fail-closed).
|
|
86
104
|
const PrefBlockSchema = z.object({
|
|
@@ -372,7 +390,7 @@ export const TickmarkrConfigSchema = z.object({
|
|
|
372
390
|
allow: PrefBlockSchema.optional(),
|
|
373
391
|
deny: DenyBlockSchema.optional(),
|
|
374
392
|
}),
|
|
375
|
-
tiers:
|
|
393
|
+
tiers: TiersSchema,
|
|
376
394
|
pricing: z.record(z.string(), z.number()),
|
|
377
395
|
// v1.20 REC-02: optional detailed price table for cost estimation. Distinct from `pricing` above
|
|
378
396
|
// (the coarse per-task tier estimate `tickmarkr plan` shows) — this one drives the usage/cost report.
|
|
@@ -798,6 +816,15 @@ export function loadConfigWithMode(repoRoot, opts = {}) {
|
|
|
798
816
|
assertSeededModelWindows();
|
|
799
817
|
return { cfg: r.data, mode: resolveRoutingMode(r.data, [globalCfg, repoCfg]) };
|
|
800
818
|
}
|
|
819
|
+
/** OBS-1182: tiers.<adapter>.modelOverrides as the layers under the repo overlay (defaults + global)
|
|
820
|
+
* merge them. Read raw, never schema-validated: a lower layer may only validate beside repo fields
|
|
821
|
+
* (global `vendor: null` completed by a repo vendor) and its override metadata is still inherited. */
|
|
822
|
+
export function lowerLayerModelOverrides(opts = {}) {
|
|
823
|
+
const isMap = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
824
|
+
const merged = deepMerge(structuredClone(DEFAULT_CONFIG), readYaml(join(opts.globalDir ?? globalConfigDir(), "config.yaml")));
|
|
825
|
+
const tiers = isMap(merged) && isMap(merged.tiers) ? merged.tiers : {};
|
|
826
|
+
return Object.fromEntries(Object.entries(tiers).map(([adapter, entry]) => [adapter, Object.fromEntries(Object.entries(isMap(entry) && isMap(entry.modelOverrides) ? entry.modelOverrides : {}).filter(([, o]) => isMap(o)))]));
|
|
827
|
+
}
|
|
801
828
|
export function loadConfig(repoRoot, opts = {}) {
|
|
802
829
|
return loadConfigWithMode(repoRoot, opts).cfg;
|
|
803
830
|
}
|
|
@@ -959,10 +986,14 @@ export function universeCovers(universe, entry) {
|
|
|
959
986
|
// Absent universe ⇒ deny arrays verbatim and no allowOut, so non-UI callers stay byte-identical.
|
|
960
987
|
export function fleetEditableFromConfig(cfg, universe) {
|
|
961
988
|
const tiers = {};
|
|
989
|
+
const efforts = {};
|
|
962
990
|
for (const [adapter, entry] of Object.entries(cfg.tiers)) {
|
|
963
991
|
tiers[adapter] = {};
|
|
964
992
|
for (const [model, tier] of Object.entries(entry.models)) {
|
|
965
993
|
tiers[adapter][model] = { tier };
|
|
994
|
+
const effort = entry.modelOverrides?.[model]?.effort;
|
|
995
|
+
if (effort)
|
|
996
|
+
(efforts[adapter] ??= {})[model] = effort;
|
|
966
997
|
}
|
|
967
998
|
}
|
|
968
999
|
// every enumerated deny scope rides verbatim (sorted) under its own key
|
|
@@ -991,6 +1022,7 @@ export function fleetEditableFromConfig(cfg, universe) {
|
|
|
991
1022
|
...denyLists,
|
|
992
1023
|
...(allowOut !== undefined ? { allowOut } : {}),
|
|
993
1024
|
tiers,
|
|
1025
|
+
...(Object.keys(efforts).length ? { efforts } : {}),
|
|
994
1026
|
map: structuredClone(cfg.routing.map),
|
|
995
1027
|
floors: { ...cfg.routing.floors },
|
|
996
1028
|
};
|
|
@@ -1037,6 +1069,13 @@ export function formatFleetPrint(repoRoot, opts = {}) {
|
|
|
1037
1069
|
for (const [adapter, entry] of Object.entries(effective.tiers)) {
|
|
1038
1070
|
for (const model of Object.keys(entry.models))
|
|
1039
1071
|
annotate(["tiers", adapter, "models", model]);
|
|
1072
|
+
// OBS-1182: the effort value prints in place; its note names the layer that set it and the edit
|
|
1073
|
+
for (const [model, override] of Object.entries(entry.modelOverrides ?? {})) {
|
|
1074
|
+
const path = ["tiers", adapter, "modelOverrides", model, "effort"];
|
|
1075
|
+
const node = doc.getIn(path, true);
|
|
1076
|
+
if (override.effort && isScalar(node))
|
|
1077
|
+
node.comment = ` ${fleetKeyLayer(repoRoot, path.join("."), opts)} — fleet e edits`;
|
|
1078
|
+
}
|
|
1040
1079
|
}
|
|
1041
1080
|
return String(doc);
|
|
1042
1081
|
}
|
|
@@ -360,6 +360,62 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
360
360
|
setScalarPreservingComment(doc, path, after.tier, unpackFleetProvenance(after.provenance).provenance);
|
|
361
361
|
}
|
|
362
362
|
}
|
|
363
|
+
// OBS-1182: effort lands in tiers.<adapter>.modelOverrides.<model>.effort — the scalar tier and
|
|
364
|
+
// every sibling override key (vendor, channel) keep their bytes.
|
|
365
|
+
const efforts = (e) => e.efforts ?? {};
|
|
366
|
+
for (const adapter of new Set([...Object.keys(efforts(initial)), ...Object.keys(efforts(edited))])) {
|
|
367
|
+
for (const model of new Set([...Object.keys(efforts(initial)[adapter] ?? {}), ...Object.keys(efforts(edited)[adapter] ?? {})])) {
|
|
368
|
+
const after = efforts(edited)[adapter]?.[model];
|
|
369
|
+
if (efforts(initial)[adapter]?.[model] === after)
|
|
370
|
+
continue;
|
|
371
|
+
const override = ["tiers", adapter, "modelOverrides", model];
|
|
372
|
+
if (after !== undefined) {
|
|
373
|
+
// Setting beneath a tombstone (the override, or modelOverrides above it) lifts it, and
|
|
374
|
+
// deepMerge would restore what it masked: re-mask each lower-layer key it covered — sibling
|
|
375
|
+
// models, then this override's vendor/channel (unknown lower ⇒ both, blind).
|
|
376
|
+
// ponytail: tiers.<adapter> itself tombstoned is unreachable here — its models, and so a tier, are masked too.
|
|
377
|
+
const tombstoned = (depth) => {
|
|
378
|
+
const node = doc.getIn(override.slice(0, depth), true);
|
|
379
|
+
return node !== undefined && !isMap(node);
|
|
380
|
+
};
|
|
381
|
+
const lowerModels = write.lowerOverrides?.[adapter] ?? {};
|
|
382
|
+
const remask = [];
|
|
383
|
+
if (tombstoned(3))
|
|
384
|
+
for (const other of Object.keys(lowerModels))
|
|
385
|
+
if (other !== model)
|
|
386
|
+
remask.push([...override.slice(0, 3), other]);
|
|
387
|
+
if (tombstoned(3) || tombstoned(4)) {
|
|
388
|
+
for (const key of write.lowerOverrides ? Object.keys(lowerModels[model] ?? {}) : ["vendor", "channel"]) {
|
|
389
|
+
if (key !== "effort")
|
|
390
|
+
remask.push([...override, key]);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
setScalarPreservingComment(doc, [...override, "effort"], after);
|
|
394
|
+
for (const path of remask)
|
|
395
|
+
setScalarPreservingComment(doc, path, null);
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
// Clearing means no effort in the MERGED config: drop the repo key, then mask an effort the
|
|
399
|
+
// lower layers still declare (unknown lower ⇒ a repo that never held the key inherited it).
|
|
400
|
+
const held = doc.getIn([...override, "effort"]) !== undefined;
|
|
401
|
+
deleteAt(doc, [...override, "effort"]);
|
|
402
|
+
const lower = write.lowerOverrides?.[adapter]?.[model];
|
|
403
|
+
const maskEffort = write.lowerOverrides ? lower?.effort !== undefined : !held;
|
|
404
|
+
const repoNode = doc.getIn(override, true);
|
|
405
|
+
const { effort: _masked, ...rest } = { ...lower, ...(isMap(repoNode) ? repoNode.toJSON() : {}) };
|
|
406
|
+
if (Object.values(rest).some((v) => v !== null && v !== undefined)) {
|
|
407
|
+
if (maskEffort)
|
|
408
|
+
setScalarPreservingComment(doc, [...override, "effort"], null);
|
|
409
|
+
}
|
|
410
|
+
else if (maskEffort || (isMap(repoNode) && repoNode.items.length > 0)) {
|
|
411
|
+
// Tombstone-only siblings must still suppress lower metadata when effort is cleared,
|
|
412
|
+
// even without inherited effort. Their merged {} would fail the nonempty refinement.
|
|
413
|
+
setScalarPreservingComment(doc, override, null);
|
|
414
|
+
}
|
|
415
|
+
for (const depth of [4, 3, 2, 1])
|
|
416
|
+
deleteEmptyMap(doc, override.slice(0, depth));
|
|
417
|
+
}
|
|
418
|
+
}
|
|
363
419
|
if (write.mode !== undefined) {
|
|
364
420
|
setScalarPreservingComment(doc, ["routing", "mode"], write.mode);
|
|
365
421
|
}
|
package/dist/drivers/orca.d.ts
CHANGED
|
@@ -123,12 +123,13 @@ export declare function mapAgentState(term: Record<string, unknown>, tuiIdle: bo
|
|
|
123
123
|
* A retired record is replaced by a new reservation (CAS on the tombstone bytes) only once its pane
|
|
124
124
|
* is proven gone. Everything else — a reservation or claim with no bound pane, a failed cleanup, a
|
|
125
125
|
* record another driver holds with a live observer — refuses and keeps the record exactly as it is.
|
|
126
|
+
* One reservation or claim is HELD rather than in flight: its placer is this process after an
|
|
127
|
+
* indeterminate split receipt (OBS-1172). It is reconciled again on every call, never split over.
|
|
126
128
|
*/
|
|
127
129
|
/** `placer` is the pid of the narrator that wrote the reservation: a reserved or claimed record is a
|
|
128
130
|
* normal intermediate state while that pid lives (another driver awaiting listing or bind) and a
|
|
129
131
|
* crash to recover only once it is dead. */
|
|
130
132
|
type BoardRecord = WatchBoardOwner & {
|
|
131
|
-
retired?: true;
|
|
132
133
|
runtimeId?: string;
|
|
133
134
|
placer?: number;
|
|
134
135
|
};
|
|
@@ -198,6 +199,16 @@ export declare class OrcaDriver implements ExecutorDriver {
|
|
|
198
199
|
/** Explicit runtime probe. Also T3's doctor probe. */
|
|
199
200
|
probeRuntime(cwd?: string): Promise<string>;
|
|
200
201
|
slot(cwd: string, name: string, opts?: SlotOpts): Promise<Slot>;
|
|
202
|
+
/**
|
|
203
|
+
* OBS-1109: bind THIS instance to a terminal an earlier daemon created for an OWNED slot, so a
|
|
204
|
+
* resumed daemon can read the interrupted attempt's pane. Binding needs ownership evidence, never
|
|
205
|
+
* a title or handle alone: exactly one tab on the tracked worktree carries the FULL journaled owned
|
|
206
|
+
* title, it resolves to exactly one terminal, and that terminal's scrollback proves the exact task
|
|
207
|
+
* checkout `slot.cwd` (the frame only tickmarkr's own create command prints). Zero, ambiguous or
|
|
208
|
+
* foreign candidates leave the slot unbound: the throw is the caller's decline. The journaled
|
|
209
|
+
* nonce is then checked by the caller against the trailer this binding reads.
|
|
210
|
+
*/
|
|
211
|
+
adopt(slot: Slot): Promise<Slot>;
|
|
201
212
|
/** Where to invoke the CLI for this slot's calls (see OrcaSlotState.dir). */
|
|
202
213
|
private cliCwd;
|
|
203
214
|
private state;
|
|
@@ -290,6 +301,15 @@ export declare class OrcaDriver implements ExecutorDriver {
|
|
|
290
301
|
sendKey(slot: Slot, key: string): Promise<void>;
|
|
291
302
|
nudge(slot: Slot, message: string): Promise<boolean>;
|
|
292
303
|
narrator(cwd: string, command: string, runId?: string): Promise<Slot>;
|
|
304
|
+
/** OBS-1172: what an indeterminate split left behind, reconciled from the owner record and Orca's
|
|
305
|
+
* terminal table — never from the receipt that did not arrive. The reservation's token reaches no
|
|
306
|
+
* process but the split's own command, so a claim proves that split started a board; the owned
|
|
307
|
+
* record (this driver, the watch title) is ours by that same token. The claimant must be live (pid
|
|
308
|
+
* and arm presence), and it must name the terminal it runs in — a listed pane in the launching tab.
|
|
309
|
+
* A claim that names none proves a board started, not WHICH pane hosts it: a sole neighbouring pane
|
|
310
|
+
* (row titles are shell-controlled, never ownership keys) is not adopted, so the claim stays held.
|
|
311
|
+
* No claim and no such pane: the split started nothing the table shows. Anything else is held. */
|
|
312
|
+
private settleIndeterminate;
|
|
293
313
|
private boardPath;
|
|
294
314
|
/** A recorded pane (a receipt's handle bound to the split envelope's runtime, never a guess) is
|
|
295
315
|
* gone when that runtime no longer lists it, or when a handle-bound close receipt names it.
|
|
@@ -303,6 +323,11 @@ export declare class OrcaDriver implements ExecutorDriver {
|
|
|
303
323
|
* handle-bound close (timeout keeps the tombstone and the pane); a dead one never acknowledges,
|
|
304
324
|
* so it is only asked. */
|
|
305
325
|
private retireAndClose;
|
|
326
|
+
/** Stop a retired board's observer before its pane is closed. A live observer acknowledges on
|
|
327
|
+
* injected time; a dead one never can, so it is only asked. OBS-1131: an acknowledgement timeout
|
|
328
|
+
* is not proof of death — while the pid lives the owned pane stays protected — but a pid that died
|
|
329
|
+
* during the wait is, and its pane is closed instead of leaked. */
|
|
330
|
+
private stopObserver;
|
|
306
331
|
/** WB-1 seam: the daemon reports this board lost. "Lost" can be a stale beat or missing presence
|
|
307
332
|
* under a still-live owner pid, so it is not proof of a dead observer — retirement keeps close's
|
|
308
333
|
* acknowledgement discipline (Leg-2 T9 P1). */
|