tickmarkr 2.5.4 → 2.5.6
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 +3 -1
- package/dist/adapters/registry.js +6 -1
- package/dist/adapters/types.d.ts +3 -0
- package/dist/cli/commands/approve.d.ts +1 -0
- package/dist/cli/commands/approve.js +118 -9
- package/dist/cli/commands/doctor.d.ts +10 -0
- package/dist/cli/commands/doctor.js +44 -29
- package/dist/cli/commands/fleet.js +199 -33
- package/dist/cli/commands/init.js +196 -6
- package/dist/cli/commands/plan.js +20 -4
- package/dist/cli/commands/resume.js +4 -2
- package/dist/cli/commands/run.js +11 -2
- package/dist/cli/commands/verify.js +102 -81
- package/dist/cli/help.d.ts +2 -0
- package/dist/cli/help.js +3 -1
- package/dist/config/config.d.ts +25 -8
- package/dist/config/config.js +41 -29
- package/dist/config/fleet-overlay.d.ts +3 -9
- package/dist/config/fleet-overlay.js +114 -19
- package/dist/config/fleet-why.d.ts +7 -0
- package/dist/config/fleet-why.js +5 -0
- package/dist/drivers/index.d.ts +15 -1
- package/dist/drivers/index.js +39 -10
- package/dist/drivers/orca.d.ts +119 -10
- package/dist/drivers/orca.js +781 -110
- package/dist/gates/baseline.d.ts +17 -3
- package/dist/gates/baseline.js +63 -15
- package/dist/gates/cache.d.ts +101 -0
- package/dist/gates/cache.js +401 -0
- package/dist/gates/llm.d.ts +3 -0
- package/dist/gates/llm.js +11 -0
- package/dist/gates/review.d.ts +28 -3
- package/dist/gates/review.js +106 -14
- package/dist/gates/run-gates.d.ts +9 -1
- package/dist/gates/run-gates.js +407 -102
- package/dist/gates/test-manifest.d.ts +128 -0
- package/dist/gates/test-manifest.js +463 -0
- package/dist/gates/test-reporter.d.ts +4 -0
- package/dist/gates/test-reporter.js +57 -0
- package/dist/graph/graph.d.ts +2 -0
- package/dist/graph/graph.js +45 -1
- package/dist/route/preference.d.ts +22 -1
- package/dist/route/preference.js +123 -25
- package/dist/route/router.js +31 -6
- package/dist/run/consult.js +5 -4
- package/dist/run/daemon.d.ts +15 -0
- package/dist/run/daemon.js +638 -167
- package/dist/run/execution-budget.d.ts +25 -0
- package/dist/run/execution-budget.js +142 -0
- package/dist/run/git.d.ts +50 -1
- package/dist/run/git.js +131 -12
- package/dist/run/journal.d.ts +16 -2
- package/dist/run/journal.js +115 -25
- package/dist/run/lease.d.ts +58 -0
- package/dist/run/lease.js +310 -0
- package/dist/run/merge.d.ts +2 -0
- package/dist/run/merge.js +91 -3
- package/dist/run/operator-state.d.ts +11 -0
- package/dist/run/operator-state.js +17 -3
- package/dist/run/recovery.d.ts +8 -0
- package/dist/run/recovery.js +25 -0
- package/dist/run/repair-selection.d.ts +12 -0
- package/dist/run/repair-selection.js +56 -0
- package/dist/run/stall.d.ts +6 -1
- package/dist/run/stall.js +60 -3
- package/dist/tui/cockpit/board.d.ts +96 -0
- package/dist/tui/cockpit/board.js +346 -0
- package/dist/tui/cockpit/decision-actions.js +2 -0
- package/dist/tui/cockpit/layout.d.ts +5 -1
- package/dist/tui/cockpit/layout.js +8 -3
- package/dist/tui/cockpit/live-runtime.js +71 -21
- package/dist/tui/cockpit/run-view.d.ts +7 -5
- package/dist/tui/cockpit/run-view.js +12 -11
- package/dist/tui/ink/fleet-app.d.ts +49 -27
- package/dist/tui/ink/fleet-app.js +229 -38
- package/package.json +2 -2
- package/skills/tickmarkr-overseer/SKILL.md +173 -113
- package/skills/tickmarkr-overseer/scripts/grade-ci.sh +29 -2
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ prefer, every example below works with either.
|
|
|
52
52
|
|
|
53
53
|
- **macOS or Linux.** Every command shells out through `bash`, so native Windows is not
|
|
54
54
|
supported — use WSL (untested).
|
|
55
|
-
- Node ≥ 20 and `git` on PATH.
|
|
55
|
+
- Node ≥ 20.3 and `git` on PATH.
|
|
56
56
|
- At least one agent CLI on PATH (`claude`, `codex`, `cursor-agent`, `opencode`, `grok`, `pi`,
|
|
57
57
|
or `kimi`), authenticated through its own login. tickmarkr never handles vendor API keys itself.
|
|
58
58
|
|
|
@@ -476,6 +476,8 @@ accepted contributions are credited via `Co-authored-by:` on the release commit.
|
|
|
476
476
|
|
|
477
477
|
## Documentation
|
|
478
478
|
|
|
479
|
+
- [Experimental execution policy](docs/experimental-execution-policy.md) — bounded recovery, cumulative task time, and repair selection.
|
|
480
|
+
|
|
479
481
|
- **[FLEET.md](https://github.com/alzahrani-khalid/tickmarkr/blob/main/FLEET.md)** — routing modes, steering syntax, tier provenance, and run flags (advanced reference)
|
|
480
482
|
- **[LICENSE](LICENSE)** — MIT license
|
|
481
483
|
- **[CONTRIBUTING.md](CONTRIBUTING.md)** — development setup and contribution guidelines
|
|
@@ -578,7 +578,12 @@ export function discoverChannels(cfg, adapters, health, role = "worker") {
|
|
|
578
578
|
// channel — routing an unknown or 403 into dispatch is fail-closed. Operators with pre-v1.21 doctor.json
|
|
579
579
|
// can opt into the prior unknown-is-routable behavior with routing.allowUnverifiedModels.
|
|
580
580
|
// OBS-117: configured aliases absent from the adapter CLI's own model list are not dispatchable.
|
|
581
|
-
return a.channels(cfg)
|
|
581
|
+
return a.channels(cfg)
|
|
582
|
+
.filter((c) => !invalid.has(c.model) && modelAuthed(h, c.model, cfg.routing.allowUnverifiedModels) && (!s || s.includes(c.model)))
|
|
583
|
+
.map((c) => {
|
|
584
|
+
const identity = h?.modelAuth?.[c.model]?.identity ?? h?.modelIdentities?.[c.model];
|
|
585
|
+
return identity ? { ...c, identity } : c;
|
|
586
|
+
});
|
|
582
587
|
});
|
|
583
588
|
if (!cfg.routing.allow && !cfg.routing.deny)
|
|
584
589
|
return base;
|
package/dist/adapters/types.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ export interface BillingChannel {
|
|
|
22
22
|
model: string;
|
|
23
23
|
channel: "sub" | "api";
|
|
24
24
|
tier: Tier;
|
|
25
|
+
identity?: string;
|
|
25
26
|
}
|
|
26
27
|
export declare const MODEL_PROBE_ERRORS: readonly ["EMFILE", "EAGAIN", "ENFILE", "ENOMEM", "ENOSPC"];
|
|
27
28
|
export type ModelProbeError = typeof MODEL_PROBE_ERRORS[number];
|
|
@@ -30,6 +31,7 @@ export interface ModelAuth {
|
|
|
30
31
|
reason?: string;
|
|
31
32
|
probeError?: ModelProbeError;
|
|
32
33
|
probedAt: string;
|
|
34
|
+
identity?: string;
|
|
33
35
|
}
|
|
34
36
|
export interface AuthHealth {
|
|
35
37
|
installed: boolean;
|
|
@@ -40,6 +42,7 @@ export interface AuthHealth {
|
|
|
40
42
|
modelsDetectedAt?: string;
|
|
41
43
|
servable?: string[];
|
|
42
44
|
modelAuth?: Record<string, ModelAuth>;
|
|
45
|
+
modelIdentities?: Record<string, string>;
|
|
43
46
|
}
|
|
44
47
|
export declare function modelAuthed(health: AuthHealth | undefined, model: string, allowUnverifiedModels?: boolean): boolean;
|
|
45
48
|
export interface Invocation {
|
|
@@ -21,6 +21,7 @@ export interface NewestPark {
|
|
|
21
21
|
/** The daemon-recorded kind (task-human data.kind), never inferred from prose. */
|
|
22
22
|
kind: string | undefined;
|
|
23
23
|
reason: string | undefined;
|
|
24
|
+
approveCommand?: string;
|
|
24
25
|
/** The newest failed gate before the park — the gate a waive would satisfy. */
|
|
25
26
|
failedGate: string | undefined;
|
|
26
27
|
/** A pre-dispatch human gate whose reason marks it permanent by design (see isTombstonePark). */
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import { graphDefinitionHash, loadGraph, saveGraph } from "../../graph/graph.js";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
1
3
|
import { userInfo } from "node:os";
|
|
4
|
+
import { loadConfig } from "../../config/config.js";
|
|
5
|
+
import { integrationBranch } from "../../run/merge.js";
|
|
6
|
+
import { separabilityErrors } from "../../compile/collateral.js";
|
|
2
7
|
import { GATE_NAMES } from "../../graph/schema.js";
|
|
3
|
-
import { ATTEMPT_CAP_RELEASE, GATE_SATISFIED_RELEASE, Journal, RECHECK_RELEASE, REVIEW_UPHELD_RELEASE } from "../../run/journal.js";
|
|
8
|
+
import { applyScopeAmendments, engagementComparable, ATTEMPT_CAP_RELEASE, GATE_SATISFIED_RELEASE, Journal, RECHECK_RELEASE, REVIEW_UPHELD_RELEASE } from "../../run/journal.js";
|
|
4
9
|
export const APPROVAL_DISPOSITIONS = ["dispatch", "waive-gate", "re-dispatch", "fund-fixed-attempt", "fresh-budget"];
|
|
5
10
|
/**
|
|
6
11
|
* What each disposition's release actually buys — the clause every operator-facing sentence about
|
|
@@ -52,6 +57,7 @@ sourceIndexes) {
|
|
|
52
57
|
const reason = typeof e.data.reason === "string" ? e.data.reason : undefined;
|
|
53
58
|
return {
|
|
54
59
|
index: i, line: (sourceIndexes?.[i] ?? i) + 1, ts: typeof e.ts === "string" ? e.ts : undefined, kind, reason,
|
|
60
|
+
approveCommand: typeof e.data.approveCommand === "string" ? e.data.approveCommand : undefined,
|
|
55
61
|
failedGate: failedGateForNewestPark(events, taskId, i), tombstone: isTombstonePark(kind, reason),
|
|
56
62
|
};
|
|
57
63
|
}
|
|
@@ -73,7 +79,7 @@ export function readJournalEvents(journal) {
|
|
|
73
79
|
* surface may read it from, so what a menu offers and what the command accepts cannot drift.
|
|
74
80
|
*/
|
|
75
81
|
export function permittedDecisionVerbs(park) {
|
|
76
|
-
if (!park || park.tombstone)
|
|
82
|
+
if (!park || park.tombstone || park.kind === "scope-request")
|
|
77
83
|
return [];
|
|
78
84
|
if (park.kind === "gate-fail") {
|
|
79
85
|
if (park.failedGate === undefined)
|
|
@@ -82,6 +88,8 @@ export function permittedDecisionVerbs(park) {
|
|
|
82
88
|
}
|
|
83
89
|
if (park.kind === "infra")
|
|
84
90
|
return ["approve", "recheck"];
|
|
91
|
+
if (park.kind === "diff-cap")
|
|
92
|
+
return ["recheck"]; // OBS-1007: a cap trip is re-gated under a raised cap, never re-bought
|
|
85
93
|
return ["approve"];
|
|
86
94
|
}
|
|
87
95
|
/** The release marker this command appends for a verb on a park — the fact a read-back must match. */
|
|
@@ -123,7 +131,7 @@ export function approvalEnactment(token, run) {
|
|
|
123
131
|
}
|
|
124
132
|
/** The production command registered in COMMANDS; its returned bytes are what the CLI prints. */
|
|
125
133
|
export async function approve(argv, cwd = process.cwd()) {
|
|
126
|
-
const { runId, taskId, by, reason, waive, uphold, recheck, reviewRoundCeiling } = parseArgs(argv);
|
|
134
|
+
const { runId, taskId, by, reason, waive, uphold, recheck, reviewRoundCeiling, files } = parseArgs(argv);
|
|
127
135
|
const decisions = [waive, uphold, recheck].filter(Boolean).length;
|
|
128
136
|
if (decisions > 1)
|
|
129
137
|
throw new Error("--waive, --uphold and --recheck are different decisions — pass one");
|
|
@@ -135,6 +143,21 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
135
143
|
if (status === undefined) {
|
|
136
144
|
throw new Error(`task ${taskId} has no events in run ${runId} — unknown task or never dispatched`);
|
|
137
145
|
}
|
|
146
|
+
// OBS-1022: a task-failed task whose preserved ref (or task branch) carries commits ahead of the
|
|
147
|
+
// integration base owns verified-able work; --recheck re-gates that tree with no worker. Any other
|
|
148
|
+
// verb on a failed task, or a recheck over nothing landed, is refused naming why.
|
|
149
|
+
if (status === "failed" && recheck) {
|
|
150
|
+
const ahead = failedTaskCommitsAhead(cwd, journal, runId, taskId);
|
|
151
|
+
if (ahead.count === 0) {
|
|
152
|
+
throw new Error(`task ${taskId} is failed and ${ahead.ref} carries no commits ahead of ${ahead.base} — nothing to re-gate; use resume --retry-failed to fund a worker`);
|
|
153
|
+
}
|
|
154
|
+
journal.append("task-approved", taskId, {
|
|
155
|
+
by, ...(reason ? { reason } : {}), via: "cli", release: RECHECK_RELEASE,
|
|
156
|
+
recheckedRef: ahead.ref, commitsAhead: ahead.count,
|
|
157
|
+
...(reviewRoundCeiling === undefined ? {} : { reviewRoundCeiling }),
|
|
158
|
+
});
|
|
159
|
+
return disposition(cwd, runId, "re-dispatch", `re-checking failed ${taskId} in ${runId} — by ${by}; ${ahead.count} commit(s) on ${ahead.ref} ahead of ${ahead.base}; no worker funded`, serialization.contended);
|
|
160
|
+
}
|
|
138
161
|
if (status !== "human") {
|
|
139
162
|
// a silent no-op would be worse than a loud refusal — name the actual status (D-05)
|
|
140
163
|
throw new Error(`task ${taskId} is ${status}, not a parked human gate — refusing (a silent no-op would be worse)`);
|
|
@@ -146,8 +169,43 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
146
169
|
const lastHuman = park === undefined ? undefined : events[park.index];
|
|
147
170
|
const capPark = park?.kind === ATTEMPT_CAP_RELEASE;
|
|
148
171
|
const gateFailPark = park?.kind === "gate-fail";
|
|
149
|
-
const infraPark = park?.kind === "infra";
|
|
172
|
+
const infraPark = park?.kind === "infra" || park?.kind === "diff-cap";
|
|
150
173
|
const failedGate = gateFailPark ? park?.failedGate : undefined;
|
|
174
|
+
if ((park?.kind === "scope-request" && !decisions) || files !== undefined) {
|
|
175
|
+
if (park?.kind !== "scope-request")
|
|
176
|
+
throw new Error("--files requires a scope-request park");
|
|
177
|
+
if (!files?.length)
|
|
178
|
+
throw new Error(`scope-request for ${taskId} requires --files <glob,…>`);
|
|
179
|
+
if (decisions)
|
|
180
|
+
throw new Error("--files cannot be combined with --waive, --uphold or --recheck");
|
|
181
|
+
const graph = applyScopeAmendments(loadGraph(cwd), journal);
|
|
182
|
+
const from = graphDefinitionHash(graph);
|
|
183
|
+
if (!engagementComparable(journal.read(), from).comparable
|
|
184
|
+
|| (lastHuman?.data.graphDefinitionHash !== undefined && lastHuman.data.graphDefinitionHash !== from)) {
|
|
185
|
+
throw new Error(`refusing stale scope-request approval for ${taskId}: graph revision changed`);
|
|
186
|
+
}
|
|
187
|
+
const task = graph.tasks.find((t) => t.id === taskId);
|
|
188
|
+
if (!task)
|
|
189
|
+
throw new Error(`unknown task ${taskId}`);
|
|
190
|
+
const amendedFiles = [...new Set([...task.files, ...files])];
|
|
191
|
+
if (amendedFiles.length === task.files.length)
|
|
192
|
+
throw new Error("--files must extend the parked files[] revision");
|
|
193
|
+
const amended = { ...graph, tasks: graph.tasks.map((t) => t.id === taskId ? { ...t, files: amendedFiles } : t) };
|
|
194
|
+
const conflicts = separabilityErrors(amended.tasks);
|
|
195
|
+
if (conflicts.length)
|
|
196
|
+
throw new Error(`refusing scope-request approval for ${taskId}: ${conflicts.join("\n")}`);
|
|
197
|
+
journal.append("task-approved", taskId, {
|
|
198
|
+
by, ...(reason ? { reason } : {}), via: "cli", release: "scope-request",
|
|
199
|
+
amendment: { from, to: graphDefinitionHash(amended), beforeFiles: task.files, files: amendedFiles, parkLine: park.line },
|
|
200
|
+
});
|
|
201
|
+
// Do not write graph.json from this process while the daemon owns it: its sweep materializes
|
|
202
|
+
// the amendment without replacing a sibling's running state with this command's snapshot.
|
|
203
|
+
const projected = applyScopeAmendments(graph, journal);
|
|
204
|
+
const owner = approvalRunOwner(cwd, runId);
|
|
205
|
+
if (!owner.live && !owner.blockingRunId)
|
|
206
|
+
saveGraph(cwd, projected);
|
|
207
|
+
return disposition(cwd, runId, "dispatch", `approved files[] for ${taskId} in ${runId} — by ${by}`, serialization.contended);
|
|
208
|
+
}
|
|
151
209
|
if (gateFailPark && !failedGate) {
|
|
152
210
|
throw new Error(`task ${taskId} is parked on gate-fail but has no failed gate result on the newest park — refusing to infer one`);
|
|
153
211
|
}
|
|
@@ -167,7 +225,7 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
167
225
|
}
|
|
168
226
|
if (recheck) {
|
|
169
227
|
if ((!gateFailPark || !failedGate) && !infraPark) {
|
|
170
|
-
throw new Error(`--recheck applies to a gate-fail or
|
|
228
|
+
throw new Error(`--recheck applies to a gate-fail, infra or diff-cap park; ${taskId}'s newest park is ${String(lastHuman?.data.kind ?? "none")} with failed gate ${failedGate ?? "none"} — refusing`);
|
|
171
229
|
}
|
|
172
230
|
journal.append("task-approved", taskId, {
|
|
173
231
|
by,
|
|
@@ -176,7 +234,7 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
176
234
|
release: RECHECK_RELEASE,
|
|
177
235
|
...(reviewRoundCeiling === undefined ? {} : { reviewRoundCeiling }),
|
|
178
236
|
});
|
|
179
|
-
return disposition(cwd, runId, "re-dispatch", `re-checking ${taskId} in ${runId} — by ${by}; ${failedGate ? `failed gate ${failedGate}` :
|
|
237
|
+
return disposition(cwd, runId, "re-dispatch", `re-checking ${taskId} in ${runId} — by ${by}; ${failedGate ? `failed gate ${failedGate}` : `${park.kind} park`}; no gate marked satisfied`, serialization.contended);
|
|
180
238
|
}
|
|
181
239
|
if (waive) {
|
|
182
240
|
if (!gateFailPark || !failedGate) {
|
|
@@ -204,6 +262,11 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
204
262
|
if (park?.tombstone) {
|
|
205
263
|
throw new Error(`task ${taskId}'s newest park is a tombstone (${park.reason ?? "no reason"}) — permanent by design; no verb releases it`);
|
|
206
264
|
}
|
|
265
|
+
// OBS-1007: a cap trip is re-gated under a raised cap, never re-bought — plain approve would fund a
|
|
266
|
+
// worker the production oracle (permittedDecisionVerbs) says this park cannot buy.
|
|
267
|
+
if (park?.kind === "diff-cap") {
|
|
268
|
+
throw new Error(`task ${taskId} is parked on a diff cap; plain approve would fund another worker — raise gates.diffCap and pass --recheck (disposition re-dispatch)`);
|
|
269
|
+
}
|
|
207
270
|
journal.append("task-approved", taskId, {
|
|
208
271
|
by,
|
|
209
272
|
...(reason ? { reason } : {}),
|
|
@@ -218,6 +281,42 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
218
281
|
serialization.release();
|
|
219
282
|
}
|
|
220
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* OBS-1022: what a failed task has landed. A preserved snapshot is authoritative only when it contains
|
|
286
|
+
* the current task-branch tip; an older preservation can remain in the journal after a later retry has
|
|
287
|
+
* advanced/rebuilt the branch. Missing task branches may still recover from a preserved ref. The selected
|
|
288
|
+
* ref is counted against the run's integration branch; unreadable refs count as zero and fail closed.
|
|
289
|
+
*/
|
|
290
|
+
function failedTaskCommitsAhead(cwd, journal, runId, taskId) {
|
|
291
|
+
const cfg = loadConfig(cwd);
|
|
292
|
+
const base = integrationBranch(cfg, runId);
|
|
293
|
+
const taskRef = `${base}--${taskId}`;
|
|
294
|
+
const preserved = [...journal.read()].reverse().find((e) => e.taskId === taskId && e.event === "worktree-preserved" && typeof e.data.ref === "string");
|
|
295
|
+
const preservedRef = preserved?.data.ref;
|
|
296
|
+
let ref = taskRef;
|
|
297
|
+
if (preservedRef) {
|
|
298
|
+
try {
|
|
299
|
+
execFileSync("git", ["rev-parse", "--verify", taskRef], { cwd, stdio: "ignore" });
|
|
300
|
+
execFileSync("git", ["merge-base", "--is-ancestor", taskRef, preservedRef], { cwd, stdio: "ignore" });
|
|
301
|
+
ref = preservedRef;
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
try {
|
|
305
|
+
execFileSync("git", ["rev-parse", "--verify", taskRef], { cwd, stdio: "ignore" });
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
ref = preservedRef;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
const out = execFileSync("git", ["rev-list", "--count", `${base}..${ref}`], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
314
|
+
return { ref, base, count: Number.parseInt(out.trim(), 10) || 0 };
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
return { ref, base, count: 0 };
|
|
318
|
+
}
|
|
319
|
+
}
|
|
221
320
|
// The status is command OUTPUT, not a typed sibling result: the registered approve function returns
|
|
222
321
|
// one primitive string and the dispatcher prints those bytes unchanged. A compact sentinel followed
|
|
223
322
|
// by JSON makes the contract unambiguous to machines without widening the shared command result type.
|
|
@@ -247,11 +346,12 @@ function disposition(cwd, runId, token, message, contended) {
|
|
|
247
346
|
};
|
|
248
347
|
return `${out}\nTICKMARKR_APPROVAL ${JSON.stringify(record)}`;
|
|
249
348
|
}
|
|
250
|
-
const USAGE = "usage: tickmarkr approve <run-id> <task-id> [--waive|--uphold|--recheck] [--review-rounds <positive-integer>] [--by <name>] [--reason <text>]";
|
|
349
|
+
const USAGE = "usage: tickmarkr approve <run-id> <task-id> [--files <glob,…>] [--waive|--uphold|--recheck] [--review-rounds <positive-integer>] [--by <name>] [--reason <text>]";
|
|
251
350
|
// hand-parsed argv — no CLI framework (house style). Positionals are runId then taskId; decision,
|
|
252
351
|
// ceiling, actor and reason are flags. Throws usage on missing positionals (mirrors resume.ts/unlock.ts).
|
|
253
352
|
function parseArgs(argv) {
|
|
254
353
|
const positionals = [];
|
|
354
|
+
let files;
|
|
255
355
|
let by;
|
|
256
356
|
let reason;
|
|
257
357
|
let waive = false;
|
|
@@ -260,7 +360,16 @@ function parseArgs(argv) {
|
|
|
260
360
|
let reviewRoundCeiling;
|
|
261
361
|
for (let i = 0; i < argv.length; i++) {
|
|
262
362
|
const a = argv[i];
|
|
263
|
-
if (a === "--
|
|
363
|
+
if (a === "--files") {
|
|
364
|
+
const value = argv[++i];
|
|
365
|
+
if (!value || value.startsWith("--"))
|
|
366
|
+
throw new Error("--files requires <glob,…>");
|
|
367
|
+
files = value.split(",").map((file) => file.trim());
|
|
368
|
+
if (files.some((file) => !file || file.startsWith("/") || file.startsWith("!") || file.split("/").includes(".."))) {
|
|
369
|
+
throw new Error("--files requires non-empty repository-relative globs");
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
else if (a === "--by") {
|
|
264
373
|
by = argv[++i];
|
|
265
374
|
if (!by)
|
|
266
375
|
throw new Error(USAGE);
|
|
@@ -296,7 +405,7 @@ function parseArgs(argv) {
|
|
|
296
405
|
if (!runId || !taskId) {
|
|
297
406
|
throw new Error(USAGE);
|
|
298
407
|
}
|
|
299
|
-
return { runId, taskId, by: by ?? userInfo().username, reason, waive, uphold, recheck, reviewRoundCeiling };
|
|
408
|
+
return { runId, taskId, by: by ?? userInfo().username, reason, waive, uphold, recheck, reviewRoundCeiling, files };
|
|
300
409
|
}
|
|
301
410
|
function failedGateForNewestPark(events, taskId, lastHumanIndex) {
|
|
302
411
|
for (let i = lastHumanIndex - 1; i >= 0; i -= 1) {
|
|
@@ -203,6 +203,11 @@ export declare function doctorProbePreflight(cwd?: string, cfg?: {
|
|
|
203
203
|
worker: "interactive" | "print";
|
|
204
204
|
workersPerTab: number;
|
|
205
205
|
};
|
|
206
|
+
executionPolicy?: {
|
|
207
|
+
boundedInfrastructure: boolean;
|
|
208
|
+
repairSelection: boolean;
|
|
209
|
+
taskExecutionLimitMs: number;
|
|
210
|
+
} | undefined;
|
|
206
211
|
setup?: string | undefined;
|
|
207
212
|
cost?: {
|
|
208
213
|
models?: Record<string, {
|
|
@@ -322,6 +327,11 @@ export declare function cachedDoctorDiagnostics(cwd?: string, adapters?: WorkerA
|
|
|
322
327
|
worker: "interactive" | "print";
|
|
323
328
|
workersPerTab: number;
|
|
324
329
|
};
|
|
330
|
+
executionPolicy?: {
|
|
331
|
+
boundedInfrastructure: boolean;
|
|
332
|
+
repairSelection: boolean;
|
|
333
|
+
taskExecutionLimitMs: number;
|
|
334
|
+
} | undefined;
|
|
325
335
|
setup?: string | undefined;
|
|
326
336
|
cost?: {
|
|
327
337
|
models?: Record<string, {
|
|
@@ -613,6 +613,46 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
613
613
|
if (probeProgressTTY && probeOk + probeBad > 0) {
|
|
614
614
|
process.stderr.write(`\r\x1b[2K probed ${probeOk + probeBad} channels · ${probeOk} ok${probeBad ? ` · ${probeBad} failed` : ""}\n`);
|
|
615
615
|
}
|
|
616
|
+
const identityResolver = opts.resolveClaudeAliasIdentity
|
|
617
|
+
?? (adapters.some((a) => a.id === claudeCode.id) ? resolveClaudeAliasIdentity : undefined);
|
|
618
|
+
const aliasDriftFindings = [];
|
|
619
|
+
// Single pre-write snapshot of the resolver's outcome per alias (including undefined/failure).
|
|
620
|
+
// Catalog rendering below reads only this map — never calls the resolver again — so a transient
|
|
621
|
+
// resolver that succeeds on a later call can never produce a result that stays display-only
|
|
622
|
+
// while doctor.json and discovery keep the earlier undefined/failed outcome.
|
|
623
|
+
const resolvedIdentitySnapshot = {};
|
|
624
|
+
if (identityResolver && health["claude-code"]?.installed) {
|
|
625
|
+
const configured = cfg.tiers["claude-code"]?.models ?? {};
|
|
626
|
+
for (const [alias, stampedIdentity] of Object.entries(CLAUDE_ALIAS_IDENTITY_STAMPS)) {
|
|
627
|
+
if (!(alias in configured))
|
|
628
|
+
continue;
|
|
629
|
+
let resolvedIdentity;
|
|
630
|
+
try {
|
|
631
|
+
resolvedIdentity = identityResolver(cwd, alias);
|
|
632
|
+
}
|
|
633
|
+
catch {
|
|
634
|
+
resolvedIdentitySnapshot[alias] = undefined;
|
|
635
|
+
continue; // advisory source failure is unknown, never a doctor failure
|
|
636
|
+
}
|
|
637
|
+
resolvedIdentitySnapshot[alias] = resolvedIdentity;
|
|
638
|
+
if (resolvedIdentity) {
|
|
639
|
+
if (!health["claude-code"].modelAuth) {
|
|
640
|
+
health["claude-code"].modelAuth = {};
|
|
641
|
+
}
|
|
642
|
+
const existing = health["claude-code"].modelAuth[alias] ?? {
|
|
643
|
+
authed: true,
|
|
644
|
+
probedAt: new Date().toISOString(),
|
|
645
|
+
};
|
|
646
|
+
health["claude-code"].modelAuth[alias] = {
|
|
647
|
+
...existing,
|
|
648
|
+
identity: resolvedIdentity,
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
if (resolvedIdentity && resolvedIdentity !== stampedIdentity) {
|
|
652
|
+
aliasDriftFindings.push(`resolved-identity drift: claude-code:${alias} resolved to ${resolvedIdentity}, stamped identity ${stampedIdentity} — reclassify per benchmark policy (advisory — routing unchanged)`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
616
656
|
writeDoctor(cwd, health);
|
|
617
657
|
const rows = adapters.map((a) => {
|
|
618
658
|
const h = health[a.id];
|
|
@@ -752,37 +792,12 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
752
792
|
// v1.65 T3: hardcoded-flag drift — advisory warn rows only. Runs AFTER writeDoctor so the verdicts
|
|
753
793
|
// can never leak into doctor.json, and discoverChannels/routing never read them.
|
|
754
794
|
lintRows.push(...flagDriftWarnings(adapters, health).map(attentionRow));
|
|
755
|
-
// OBS-145: resolved-identity drift is
|
|
756
|
-
|
|
757
|
-
// never enters health/doctor.json, config, channel discovery, learned profiles, or route().
|
|
758
|
-
const identityResolver = opts.resolveClaudeAliasIdentity
|
|
759
|
-
?? (adapters.includes(claudeCode) ? resolveClaudeAliasIdentity : undefined);
|
|
760
|
-
if (identityResolver && health["claude-code"]?.installed) {
|
|
761
|
-
const configured = cfg.tiers["claude-code"]?.models ?? {};
|
|
762
|
-
for (const [alias, stampedIdentity] of Object.entries(CLAUDE_ALIAS_IDENTITY_STAMPS)) {
|
|
763
|
-
if (!(alias in configured))
|
|
764
|
-
continue;
|
|
765
|
-
let resolvedIdentity;
|
|
766
|
-
try {
|
|
767
|
-
resolvedIdentity = identityResolver(cwd, alias);
|
|
768
|
-
}
|
|
769
|
-
catch {
|
|
770
|
-
continue; // advisory source failure is unknown, never a doctor failure
|
|
771
|
-
}
|
|
772
|
-
if (resolvedIdentity && resolvedIdentity !== stampedIdentity) {
|
|
773
|
-
lintRows.push(attentionRow(`resolved-identity drift: claude-code:${alias} resolved to ${resolvedIdentity}, stamped identity ${stampedIdentity} — reclassify per benchmark policy (advisory — routing unchanged)`));
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
}
|
|
795
|
+
// OBS-145: resolved-identity drift is advisory display warning.
|
|
796
|
+
lintRows.push(...aliasDriftFindings.map(attentionRow));
|
|
777
797
|
const resolvedCatalogModel = (adapter, model) => {
|
|
778
|
-
if (adapter !== "claude-code" || !
|
|
798
|
+
if (adapter !== "claude-code" || !(model in CLAUDE_ALIAS_IDENTITY_STAMPS))
|
|
779
799
|
return undefined;
|
|
780
|
-
|
|
781
|
-
return identityResolver(cwd, model);
|
|
782
|
-
}
|
|
783
|
-
catch {
|
|
784
|
-
return undefined;
|
|
785
|
-
}
|
|
800
|
+
return resolvedIdentitySnapshot[model];
|
|
786
801
|
};
|
|
787
802
|
// MODEL-05/06: print-only drift fragment; advisory, whole-line-commented additions, tickmarkr NEVER applies it.
|
|
788
803
|
// TTY gets a one-line summary + the fragment as a file (the full dump drowned everything else,
|