tickmarkr 2.4.2 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -24
- package/dist/cli/commands/approve.d.ts +42 -0
- package/dist/cli/commands/approve.js +80 -13
- package/dist/cli/commands/doctor.d.ts +234 -0
- package/dist/cli/commands/doctor.js +139 -5
- package/dist/cli/commands/report.js +15 -44
- package/dist/cli/commands/scope.js +36 -6
- package/dist/cli/commands/stats.d.ts +2 -0
- package/dist/cli/commands/stats.js +42 -23
- package/dist/cli/commands/status.js +20 -4
- package/dist/cli/commands/ui.js +42 -53
- package/dist/cli/commands/unlock.d.ts +1 -1
- package/dist/cli/commands/unlock.js +59 -9
- package/dist/cli/help.d.ts +219 -0
- package/dist/cli/help.js +212 -0
- package/dist/cli/index.d.ts +42 -2
- package/dist/cli/index.js +23 -8
- package/dist/drivers/herdr.d.ts +7 -2
- package/dist/drivers/herdr.js +78 -48
- package/dist/drivers/orca.d.ts +3 -2
- package/dist/drivers/orca.js +43 -4
- package/dist/drivers/subprocess.d.ts +1 -0
- package/dist/drivers/subprocess.js +3 -0
- package/dist/drivers/types.d.ts +14 -0
- package/dist/gates/artifact-manifest.d.ts +50 -0
- package/dist/gates/artifact-manifest.js +23 -0
- package/dist/plan/scope.d.ts +25 -0
- package/dist/plan/scope.js +92 -12
- package/dist/report/operator-record.d.ts +49 -0
- package/dist/report/operator-record.js +137 -0
- package/dist/run/daemon.d.ts +3 -4
- package/dist/run/daemon.js +18 -10
- package/dist/run/lock.d.ts +59 -2
- package/dist/run/lock.js +184 -26
- package/dist/run/operator-state.d.ts +86 -0
- package/dist/run/operator-state.js +165 -0
- package/dist/run/supervision.d.ts +32 -0
- package/dist/run/supervision.js +138 -17
- package/dist/tui/cockpit/capture.d.ts +19 -0
- package/dist/tui/cockpit/capture.js +89 -1
- package/dist/tui/cockpit/components.d.ts +15 -1
- package/dist/tui/cockpit/components.js +79 -9
- package/dist/tui/cockpit/decision-actions.d.ts +147 -0
- package/dist/tui/cockpit/decision-actions.js +315 -0
- package/dist/tui/cockpit/derive.d.ts +1 -1
- package/dist/tui/cockpit/derive.js +2 -0
- package/dist/tui/cockpit/evidence-view.d.ts +119 -0
- package/dist/tui/cockpit/evidence-view.js +210 -0
- package/dist/tui/cockpit/home-view.d.ts +88 -0
- package/dist/tui/cockpit/home-view.js +240 -0
- package/dist/tui/cockpit/keys.d.ts +125 -0
- package/dist/tui/cockpit/keys.js +31 -0
- package/dist/tui/cockpit/layout.d.ts +14 -0
- package/dist/tui/cockpit/layout.js +15 -0
- package/dist/tui/cockpit/live-runtime.d.ts +46 -0
- package/dist/tui/cockpit/live-runtime.js +683 -0
- package/dist/tui/cockpit/live-store.d.ts +289 -0
- package/dist/tui/cockpit/live-store.js +308 -0
- package/dist/tui/cockpit/live.d.ts +21 -1
- package/dist/tui/cockpit/live.js +12 -1
- package/dist/tui/cockpit/run-view.d.ts +100 -0
- package/dist/tui/cockpit/run-view.js +202 -0
- package/dist/tui/cockpit/shell.d.ts +50 -0
- package/dist/tui/cockpit/shell.js +74 -0
- package/dist/tui/cockpit/theme.d.ts +27 -0
- package/dist/tui/cockpit/theme.js +21 -0
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +10 -1
- package/skills/tickmarkr-loop/SKILL.md +72 -1
- package/skills/tickmarkr-overseer/SKILL.md +12 -0
package/dist/plan/scope.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { basename, dirname, extname, join } from "node:path";
|
|
4
|
-
import { discoverChannels, getAdapter, probeAll } from "../adapters/registry.js";
|
|
4
|
+
import { discoverChannels, getAdapter, probeAll, readDoctor } from "../adapters/registry.js";
|
|
5
5
|
import { compileNative, LEGACY_PREFIX } from "../compile/native.js";
|
|
6
6
|
import { pickDriver } from "../drivers/index.js";
|
|
7
7
|
import { extractJson, runLlm } from "../gates/llm.js";
|
|
8
8
|
import { TaskSchema } from "../graph/schema.js";
|
|
9
9
|
import { route } from "../route/router.js";
|
|
10
10
|
import { scopePrompt } from "./prompt.js";
|
|
11
|
+
// R11/R45 (C10): the drafting loop's hard ceiling — shared by the loop itself and the preview
|
|
12
|
+
// disclosure so the two can never state different budgets.
|
|
13
|
+
export const MAX_SCOPE_ATTEMPTS = 3;
|
|
11
14
|
const HEADING_RE = /^#{1,6}\s+(.+?)\s*$/;
|
|
12
15
|
const ITEM_RE = /^\s*(?:[-*+]\s+|\d+[.)]\s+)(?:\[[ xX]\]\s*)?(?:[QA]\d*:\s*)?(.+?)\s*$/;
|
|
13
16
|
function sectionItems(source, heading) {
|
|
@@ -95,7 +98,13 @@ function validateDraft(draft) {
|
|
|
95
98
|
rmSync(dir, { recursive: true, force: true });
|
|
96
99
|
}
|
|
97
100
|
}
|
|
98
|
-
|
|
101
|
+
function scopePlanningTask() {
|
|
102
|
+
return TaskSchema.parse({
|
|
103
|
+
id: "SCOPE", title: "Draft native spec", goal: "Draft a compiled native spec", shape: "spec", complexity: 7,
|
|
104
|
+
acceptance: [{ oracle: "judge", text: "Every requirement maps to a task with typed acceptance oracles" }],
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function localValidate(intentFile) {
|
|
99
108
|
if (!existsSync(intentFile))
|
|
100
109
|
throw new Error(`no such intent file: ${intentFile}`);
|
|
101
110
|
const intent = readFileSync(intentFile, "utf8");
|
|
@@ -103,21 +112,92 @@ export async function scopeIntent(intentFile, repoRoot, options) {
|
|
|
103
112
|
if (unanswered.length) {
|
|
104
113
|
throw new Error(`unanswered blocking questions (${unanswered.length}):\n${unanswered.map((q, i) => `${i + 1}. ${q}`).join("\n")}`);
|
|
105
114
|
}
|
|
106
|
-
|
|
115
|
+
return { intent, specFile: specPathForIntent(intentFile) };
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* R11/R45 (C10): local-only disclosure — intent/clarification checks and a candidate read off the
|
|
119
|
+
* doctor cache, never a fresh probe or a model turn. `readDoctor` and `discoverChannels`/`route` are
|
|
120
|
+
* pure reads over that cache, so this never touches an adapter.
|
|
121
|
+
*/
|
|
122
|
+
export function previewScope(intentFile, repoRoot, options) {
|
|
123
|
+
const { specFile } = localValidate(intentFile);
|
|
124
|
+
const cachedHealth = readDoctor(repoRoot);
|
|
125
|
+
let candidate;
|
|
126
|
+
if (cachedHealth) {
|
|
127
|
+
const channels = discoverChannels(options.cfg, options.adapters, cachedHealth);
|
|
128
|
+
if (channels.length) {
|
|
129
|
+
try {
|
|
130
|
+
const assignment = route(scopePlanningTask(), options.cfg, channels).assignment;
|
|
131
|
+
// Review fix (finding 1): routing.allowUnverifiedModels lets discoverChannels/route pick a
|
|
132
|
+
// channel whose modelAuth is simply absent (unknown) — that is routing PERMISSION, not proof
|
|
133
|
+
// of health. Only disclose a candidate when the doctor cache actually marked this exact model
|
|
134
|
+
// authed; otherwise this stays the "unknown" case below, never "installed/authed".
|
|
135
|
+
if (cachedHealth[assignment.adapter]?.modelAuth?.[assignment.model]?.authed === true) {
|
|
136
|
+
candidate = { adapter: assignment.adapter, model: assignment.model };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// no eligible candidate in the cached snapshot — stays unknown, never "unreachable"
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
intentFile, specFile, specExists: existsSync(specFile), cached: cachedHealth !== null,
|
|
146
|
+
candidate, authoringBudget: MAX_SCOPE_ATTEMPTS, probeCalls: options.adapters.length,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
export function formatScopePreview(preview) {
|
|
150
|
+
const candidateLine = preview.candidate
|
|
151
|
+
? `cached candidate: ${preview.candidate.adapter}:${preview.candidate.model} (installed/authed at last doctor run)`
|
|
152
|
+
: `cached candidate: unknown (${preview.cached ? "no eligible channel in the doctor cache" : "no doctor cache — run tickmarkr doctor"})`;
|
|
153
|
+
return [
|
|
154
|
+
`tickmarkr scope --preview ${preview.intentFile}:`,
|
|
155
|
+
candidateLine,
|
|
156
|
+
`output destination: ${preview.specFile}${preview.specExists ? " (exists — active scope needs --force)" : ""}`,
|
|
157
|
+
`authoring-call budget: up to ${preview.authoringBudget} call${preview.authoringBudget === 1 ? "" : "s"} if confirmed`,
|
|
158
|
+
`probe calls: ${preview.probeCalls} (disclosed separately — one per configured adapter, only on confirmed active scope)`,
|
|
159
|
+
"cache policy: read-only; no adapter was probed and no model was called",
|
|
160
|
+
].join("\n");
|
|
161
|
+
}
|
|
162
|
+
// R11/R45 (C10 repair): a confirmed candidate is a promise made to the operator — find that exact
|
|
163
|
+
// adapter:model in the freshly probed channels, or fail loud. Never let a stale-cache candidate
|
|
164
|
+
// silently fall through to a fresh route() that could reroute to a different channel unconfirmed.
|
|
165
|
+
function bindCandidate(candidate, channels) {
|
|
166
|
+
const c = channels.find((ch) => ch.adapter === candidate.adapter && ch.model === candidate.model);
|
|
167
|
+
if (!c) {
|
|
168
|
+
throw new Error(`confirmed candidate ${candidate.adapter}:${candidate.model} is no longer available after a fresh probe ` +
|
|
169
|
+
`(doctor found: ${channels.map((ch) => `${ch.adapter}:${ch.model}`).join(", ") || "(nothing)"}) — ` +
|
|
170
|
+
"re-run scope --preview and confirm again");
|
|
171
|
+
}
|
|
172
|
+
return { adapter: c.adapter, model: c.model, channel: c.channel, tier: c.tier };
|
|
173
|
+
}
|
|
174
|
+
// Adapter-level probes establish the current installation/auth state, but shipped adapters do not
|
|
175
|
+
// return per-model verdicts from probe(). Preserve cached verdicts only where that fresh snapshot is
|
|
176
|
+
// silent; an explicit fresh per-model verdict still wins, as do fresh adapter-level failures.
|
|
177
|
+
function mergeCachedModelAuth(freshHealth, cachedHealth) {
|
|
178
|
+
if (!cachedHealth)
|
|
179
|
+
return freshHealth;
|
|
180
|
+
return Object.fromEntries(Object.entries(freshHealth).map(([adapter, fresh]) => {
|
|
181
|
+
const cachedModelAuth = cachedHealth[adapter]?.modelAuth;
|
|
182
|
+
if (!cachedModelAuth)
|
|
183
|
+
return [adapter, fresh];
|
|
184
|
+
return [adapter, { ...fresh, modelAuth: { ...cachedModelAuth, ...fresh.modelAuth } }];
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
export async function scopeIntent(intentFile, repoRoot, options) {
|
|
188
|
+
const { intent, specFile } = localValidate(intentFile);
|
|
107
189
|
if (existsSync(specFile) && !options.force)
|
|
108
190
|
throw new Error(`${specFile} already exists; pass --force to overwrite it`);
|
|
109
|
-
const health = await probeAll(options.adapters);
|
|
191
|
+
const health = mergeCachedModelAuth(await probeAll(options.adapters), readDoctor(repoRoot));
|
|
110
192
|
const channels = discoverChannels(options.cfg, options.adapters, health);
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
});
|
|
115
|
-
const assignment = route(planningTask, options.cfg, channels).assignment;
|
|
193
|
+
const assignment = options.candidate
|
|
194
|
+
? bindCandidate(options.candidate, channels)
|
|
195
|
+
: route(scopePlanningTask(), options.cfg, channels).assignment;
|
|
116
196
|
const adapter = getAdapter(assignment.adapter, options.adapters);
|
|
117
197
|
const driver = options.cfg.visibility.llm === "pane" ? options.driver ?? pickDriver(options.cfg) : undefined;
|
|
118
198
|
const name = basename(specFile, ".spec.md");
|
|
119
199
|
let prompt = scopePrompt(intent);
|
|
120
|
-
for (let attempts = 1; attempts <=
|
|
200
|
+
for (let attempts = 1; attempts <= MAX_SCOPE_ATTEMPTS; attempts++) {
|
|
121
201
|
const via = driver ? {
|
|
122
202
|
driver, name: `scope-${name}-${attempts}-${adapter.id}`, label: "SCOPE",
|
|
123
203
|
keep: options.cfg.visibility.keepPanes === "forever",
|
|
@@ -129,8 +209,8 @@ export async function scopeIntent(intentFile, repoRoot, options) {
|
|
|
129
209
|
}
|
|
130
210
|
catch (error) {
|
|
131
211
|
const message = error.message;
|
|
132
|
-
if (attempts ===
|
|
133
|
-
throw new Error(`scope draft failed after
|
|
212
|
+
if (attempts === MAX_SCOPE_ATTEMPTS)
|
|
213
|
+
throw new Error(`scope draft failed after ${MAX_SCOPE_ATTEMPTS - 1} repair retries:\n${message}`);
|
|
134
214
|
prompt = scopePrompt(intent, { draft, error: message });
|
|
135
215
|
continue;
|
|
136
216
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { TokenUsage } from "../adapters/types.js";
|
|
2
|
+
import type { JournalEvent } from "../run/journal.js";
|
|
3
|
+
import type { ChannelCost } from "./cost.js";
|
|
4
|
+
export declare function formatTokenUsage(u: TokenUsage): string;
|
|
5
|
+
export declare function totalTokens(u: TokenUsage): number;
|
|
6
|
+
/** The channel a journal event's `assignment` names, or absent — never coalesced to a placeholder
|
|
7
|
+
* string here, so a caller decides its own absent-channel reading. */
|
|
8
|
+
export declare function assignmentChannel(data: Record<string, unknown>): string | undefined;
|
|
9
|
+
/** Worker token coverage: "unmetered" when the adapter reported no usage at all — distinct from a
|
|
10
|
+
* channel with no telemetry row whatsoever, which `labelChannelUsage` below calls "unknown". */
|
|
11
|
+
export declare function formatChannelTokens(row: ChannelCost): string;
|
|
12
|
+
export declare function formatRateBasis(row: ChannelCost): string | undefined;
|
|
13
|
+
/** Nonmeasurable money always says so explicitly (`row.reason`, or "not recorded") — never a $0. */
|
|
14
|
+
export declare function formatChannelMoney(row: ChannelCost): {
|
|
15
|
+
readonly prices: string[];
|
|
16
|
+
readonly bases: string[];
|
|
17
|
+
};
|
|
18
|
+
export interface ChannelUsageLabel {
|
|
19
|
+
readonly channel: string;
|
|
20
|
+
/** "unmetered" (metered run, adapter reported nothing), a real total, or "unknown" (no telemetry
|
|
21
|
+
* row exists for this channel at all — missing metadata, never invented). */
|
|
22
|
+
readonly tokens: string;
|
|
23
|
+
/** "not measurable" (metered but unpriced/partial), a real dollar figure, or "unknown" (no row). */
|
|
24
|
+
readonly money: string;
|
|
25
|
+
}
|
|
26
|
+
/** Labels one channel's metered/priced facts. `row` absent means journal evidence names this
|
|
27
|
+
* channel (e.g. a pre-telemetry run) but no telemetry row was ever recorded for it — missing
|
|
28
|
+
* metadata, distinct from a recorded-but-unpriced/unmetered row. */
|
|
29
|
+
export declare function labelChannelUsage(channel: string, row: ChannelCost | undefined): ChannelUsageLabel;
|
|
30
|
+
export interface ChannelRoleCounts {
|
|
31
|
+
readonly channel: string;
|
|
32
|
+
/** Dispatches recorded for this channel as the task's worker. */
|
|
33
|
+
readonly worker: number;
|
|
34
|
+
/** Review gate-results whose reviewer identity resolved to this channel. */
|
|
35
|
+
readonly review: number;
|
|
36
|
+
/** Consult verdicts recorded against the task this channel was last dispatched on. */
|
|
37
|
+
readonly consult: number;
|
|
38
|
+
}
|
|
39
|
+
/** Recorded worker/review/consult appearances per channel, read only from journal evidence — no
|
|
40
|
+
* extrapolation to an all-role invoice. A channel that only ever reviewed still gets a row with
|
|
41
|
+
* worker:0, never a fabricated dispatch. */
|
|
42
|
+
export declare function channelRoleCounts(events: readonly JournalEvent[]): ChannelRoleCounts[];
|
|
43
|
+
export interface OperatorRecordRow extends ChannelRoleCounts, Omit<ChannelUsageLabel, "channel"> {
|
|
44
|
+
}
|
|
45
|
+
/** The shared record: one row per channel journal evidence actually names (role counts), joined
|
|
46
|
+
* with that channel's metered/priced facts. A channel `costs` prices but journal evidence never
|
|
47
|
+
* names gets no row here — this never invents a role from cost data alone. */
|
|
48
|
+
export declare function buildOperatorRecord(events: readonly JournalEvent[], costs?: readonly ChannelCost[]): OperatorRecordRow[];
|
|
49
|
+
export declare function formatOperatorRecordRow(row: OperatorRecordRow): string;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
const n = (x) => x.toLocaleString("en-US");
|
|
2
|
+
export function formatTokenUsage(u) {
|
|
3
|
+
const parts = [`in ${n(u.input)}`, `out ${n(u.output)}`];
|
|
4
|
+
if (u.cacheRead !== undefined && u.cacheWrite !== undefined)
|
|
5
|
+
parts.push(`cache r/w ${n(u.cacheRead)}/${n(u.cacheWrite)}`);
|
|
6
|
+
if (u.reasoning !== undefined)
|
|
7
|
+
parts.push(`reasoning ${n(u.reasoning)}`);
|
|
8
|
+
return parts.join(" ");
|
|
9
|
+
}
|
|
10
|
+
export function totalTokens(u) {
|
|
11
|
+
return [u.input, u.output, u.cacheRead, u.cacheWrite, u.reasoning].filter((x) => x !== undefined).reduce((a, b) => a + b, 0);
|
|
12
|
+
}
|
|
13
|
+
/** The channel a journal event's `assignment` names, or absent — never coalesced to a placeholder
|
|
14
|
+
* string here, so a caller decides its own absent-channel reading. */
|
|
15
|
+
export function assignmentChannel(data) {
|
|
16
|
+
const assignment = data.assignment;
|
|
17
|
+
if (!assignment || typeof assignment !== "object")
|
|
18
|
+
return undefined;
|
|
19
|
+
const { adapter, model } = assignment;
|
|
20
|
+
return typeof adapter === "string" && typeof model === "string" ? `${adapter}:${model}` : undefined;
|
|
21
|
+
}
|
|
22
|
+
/** Worker token coverage: "unmetered" when the adapter reported no usage at all — distinct from a
|
|
23
|
+
* channel with no telemetry row whatsoever, which `labelChannelUsage` below calls "unknown". */
|
|
24
|
+
export function formatChannelTokens(row) {
|
|
25
|
+
return row.tokens
|
|
26
|
+
? `${row.partialMetering ? "≥ " : ""}${formatTokenUsage(row.tokens)} (${n(totalTokens(row.tokens))} tokens)`
|
|
27
|
+
: "unmetered";
|
|
28
|
+
}
|
|
29
|
+
export function formatRateBasis(row) {
|
|
30
|
+
if (!row.rate)
|
|
31
|
+
return undefined;
|
|
32
|
+
const cache = row.rate.cacheReadPerMtok === undefined ? "" : `; cache-read $${row.rate.cacheReadPerMtok}/Mtok`;
|
|
33
|
+
const date = row.rate.rateDate === undefined ? "" : `; rate date ${row.rate.rateDate}`;
|
|
34
|
+
return `in/out $${row.rate.inPerMtok}/$${row.rate.outPerMtok}/Mtok${cache}${date}`;
|
|
35
|
+
}
|
|
36
|
+
/** Nonmeasurable money always says so explicitly (`row.reason`, or "not recorded") — never a $0. */
|
|
37
|
+
export function formatChannelMoney(row) {
|
|
38
|
+
const prices = [];
|
|
39
|
+
const bases = [];
|
|
40
|
+
if (row.apiUsd !== undefined)
|
|
41
|
+
prices.push(`price: $${row.apiUsd.toFixed(6)}`);
|
|
42
|
+
if (row.amortizedUsd !== undefined && row.subPlan !== undefined) {
|
|
43
|
+
const [low, high] = row.amortizedUsd;
|
|
44
|
+
prices.push(`price: $${low.toFixed(6)}–$${high.toFixed(6)} amortized`);
|
|
45
|
+
bases.push(`${n(row.attempts)} windows × $${row.subPlan.planMonthly}/month ÷ ${row.subPlan.windowsPerMonthHigh}–${row.subPlan.windowsPerMonthLow} windows/month`);
|
|
46
|
+
}
|
|
47
|
+
if (row.counterfactualUsd !== undefined)
|
|
48
|
+
prices.push(`API-equivalent: $${row.counterfactualUsd.toFixed(6)}`);
|
|
49
|
+
const basis = formatRateBasis(row);
|
|
50
|
+
if (basis)
|
|
51
|
+
bases.push(basis);
|
|
52
|
+
if (!prices.length)
|
|
53
|
+
prices.push("price: not measurable");
|
|
54
|
+
if (!bases.length)
|
|
55
|
+
bases.push(row.reason || "not recorded");
|
|
56
|
+
return { prices, bases };
|
|
57
|
+
}
|
|
58
|
+
/** Labels one channel's metered/priced facts. `row` absent means journal evidence names this
|
|
59
|
+
* channel (e.g. a pre-telemetry run) but no telemetry row was ever recorded for it — missing
|
|
60
|
+
* metadata, distinct from a recorded-but-unpriced/unmetered row. */
|
|
61
|
+
export function labelChannelUsage(channel, row) {
|
|
62
|
+
if (!row)
|
|
63
|
+
return { channel, tokens: "unknown", money: "unknown" };
|
|
64
|
+
const { prices } = formatChannelMoney(row);
|
|
65
|
+
return { channel, tokens: formatChannelTokens(row), money: prices.join("; ") };
|
|
66
|
+
}
|
|
67
|
+
const REVIEWER_PATTERN = /reviewer\s+([^\s;()]+:[^\s;()]+)/i;
|
|
68
|
+
function reviewerChannel(data) {
|
|
69
|
+
if (typeof data.reviewer === "string" && data.reviewer.includes(":"))
|
|
70
|
+
return data.reviewer;
|
|
71
|
+
const meta = typeof data.meta === "object" && data.meta !== null ? data.meta : undefined;
|
|
72
|
+
if (meta && "reviewer" in meta && typeof meta.reviewer === "string" && meta.reviewer.includes(":"))
|
|
73
|
+
return meta.reviewer;
|
|
74
|
+
return typeof data.details === "string" ? REVIEWER_PATTERN.exec(data.details)?.[1] : undefined;
|
|
75
|
+
}
|
|
76
|
+
/** The channel a `consult-verdict` event names as ITS OWN consultant identity (daemon.ts always
|
|
77
|
+
* appends `adapter`/`model` on every verdict) — never the task's worker dispatch, which is a
|
|
78
|
+
* different channel a consult can (and typically does) disagree with. */
|
|
79
|
+
function consultChannel(data) {
|
|
80
|
+
const { adapter, model } = data;
|
|
81
|
+
return typeof adapter === "string" && typeof model === "string" ? `${adapter}:${model}` : undefined;
|
|
82
|
+
}
|
|
83
|
+
/** Recorded worker/review/consult appearances per channel, read only from journal evidence — no
|
|
84
|
+
* extrapolation to an all-role invoice. A channel that only ever reviewed still gets a row with
|
|
85
|
+
* worker:0, never a fabricated dispatch. */
|
|
86
|
+
export function channelRoleCounts(events) {
|
|
87
|
+
const counts = new Map();
|
|
88
|
+
const ensure = (channel) => {
|
|
89
|
+
let row = counts.get(channel);
|
|
90
|
+
if (!row) {
|
|
91
|
+
row = { worker: 0, review: 0, consult: 0 };
|
|
92
|
+
counts.set(channel, row);
|
|
93
|
+
}
|
|
94
|
+
return row;
|
|
95
|
+
};
|
|
96
|
+
for (const e of events) {
|
|
97
|
+
if (e.event === "task-dispatch") {
|
|
98
|
+
const channel = assignmentChannel(e.data);
|
|
99
|
+
if (channel)
|
|
100
|
+
ensure(channel).worker++;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (e.event === "gate-result" && e.data.gate === "review") {
|
|
104
|
+
const channel = reviewerChannel(e.data);
|
|
105
|
+
if (channel)
|
|
106
|
+
ensure(channel).review++;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (e.event === "review-leg2") {
|
|
110
|
+
const channel = reviewerChannel(e.data);
|
|
111
|
+
if (channel)
|
|
112
|
+
ensure(channel).review++;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (e.event === "consult-verdict") {
|
|
116
|
+
const channel = consultChannel(e.data);
|
|
117
|
+
if (channel)
|
|
118
|
+
ensure(channel).consult++;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return [...counts.entries()]
|
|
122
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
123
|
+
.map(([channel, role]) => ({ channel, ...role }));
|
|
124
|
+
}
|
|
125
|
+
/** The shared record: one row per channel journal evidence actually names (role counts), joined
|
|
126
|
+
* with that channel's metered/priced facts. A channel `costs` prices but journal evidence never
|
|
127
|
+
* names gets no row here — this never invents a role from cost data alone. */
|
|
128
|
+
export function buildOperatorRecord(events, costs = []) {
|
|
129
|
+
const byChannel = new Map(costs.map((c) => [`${c.adapter}:${c.model}`, c]));
|
|
130
|
+
return channelRoleCounts(events).map((role) => {
|
|
131
|
+
const usage = labelChannelUsage(role.channel, byChannel.get(role.channel));
|
|
132
|
+
return { ...role, tokens: usage.tokens, money: usage.money };
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
export function formatOperatorRecordRow(row) {
|
|
136
|
+
return `${row.channel} — worker: ${row.worker}, review: ${row.review}, consult: ${row.consult}; tokens: ${row.tokens}; money: ${row.money}`;
|
|
137
|
+
}
|
package/dist/run/daemon.d.ts
CHANGED
|
@@ -64,10 +64,9 @@ export interface RunSummary {
|
|
|
64
64
|
*/
|
|
65
65
|
export declare function outstandingApprovals(events: JournalEvent[]): string[];
|
|
66
66
|
export declare function formatSummary(s: RunSummary): string;
|
|
67
|
-
/** The narrator
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* the wrong run is a recorded incident (skills/tickmarkr-overseer/SKILL.md). */
|
|
67
|
+
/** The narrator enters the production Run cockpit for this exact run. The
|
|
68
|
+
* completed static/growing four-hour records precede this default cutover;
|
|
69
|
+
* an explicit ID prevents a newer journal from redirecting the owned board. */
|
|
71
70
|
export declare const daemonEntrypoint: string;
|
|
72
71
|
export declare const watchCommand: (runId: string) => string;
|
|
73
72
|
/**
|
package/dist/run/daemon.js
CHANGED
|
@@ -134,12 +134,11 @@ export function formatSummary(s) {
|
|
|
134
134
|
: "";
|
|
135
135
|
return `done: ${s.done.length}, failed: ${s.failed.length}, human: ${s.human.length}, blocked: ${s.blocked.length}, pending: ${s.pending.length}\nintegration branch: ${s.branch}${tip}${outstanding}`;
|
|
136
136
|
}
|
|
137
|
-
/** The narrator
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* the wrong run is a recorded incident (skills/tickmarkr-overseer/SKILL.md). */
|
|
137
|
+
/** The narrator enters the production Run cockpit for this exact run. The
|
|
138
|
+
* completed static/growing four-hour records precede this default cutover;
|
|
139
|
+
* an explicit ID prevents a newer journal from redirecting the owned board. */
|
|
141
140
|
export const daemonEntrypoint = fileURLToPath(new URL("../cli/index.js", import.meta.url));
|
|
142
|
-
export const watchCommand = (runId) => `${shq(process.execPath)} ${shq(daemonEntrypoint)}
|
|
141
|
+
export const watchCommand = (runId) => `${shq(process.execPath)} ${shq(daemonEntrypoint)} ui ${shq(runId)} --view run`;
|
|
143
142
|
const MAX_ATTEMPTS = 10; // ponytail: hard cap so a pathological ladder can never loop forever
|
|
144
143
|
// v1.85 T3 (retry economics): two repairs per engagement, then the fresh ladder. A repair re-uses the
|
|
145
144
|
// findings and the landed diff instead of re-buying onboarding; when two of them have not closed the
|
|
@@ -1266,15 +1265,17 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1266
1265
|
// slot closed once (task-done, quota reroute, gate self-clean) can never be closed twice.
|
|
1267
1266
|
const liveSlots = new Set();
|
|
1268
1267
|
const closeSlot = async (s) => {
|
|
1269
|
-
if (!liveSlots.
|
|
1268
|
+
if (!liveSlots.has(s))
|
|
1270
1269
|
return; // already closed — never twice
|
|
1271
1270
|
await driver.close(s);
|
|
1271
|
+
liveSlots.delete(s);
|
|
1272
1272
|
};
|
|
1273
1273
|
const trackedDriver = {
|
|
1274
1274
|
id: driver.id,
|
|
1275
1275
|
interactive: driver.interactive,
|
|
1276
1276
|
...(driver.readSource ? { readSource: driver.readSource } : {}),
|
|
1277
1277
|
...(driver.describe ? { describe: driver.describe.bind(driver) } : {}),
|
|
1278
|
+
...(driver.focus ? { focus: driver.focus.bind(driver) } : {}),
|
|
1278
1279
|
slot: async (cwd, name, o) => { const s = await driver.slot(cwd, name, o); liveSlots.add(s); return s; },
|
|
1279
1280
|
run: (s, cmd) => driver.run(s, cmd),
|
|
1280
1281
|
waitOutput: (s, p, ms, o) => driver.waitOutput(s, p, ms, o),
|
|
@@ -1392,7 +1393,9 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1392
1393
|
watchSlot = await trackedDriver.narrator(repoRoot, watchCommand(runId), runId);
|
|
1393
1394
|
}
|
|
1394
1395
|
catch (error) {
|
|
1395
|
-
|
|
1396
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1397
|
+
journal.append("watch-placement-failed", undefined, { error: reason });
|
|
1398
|
+
console.error(`tickmarkr: narrator not opened: ${reason}`);
|
|
1396
1399
|
}
|
|
1397
1400
|
};
|
|
1398
1401
|
let baseRef;
|
|
@@ -1550,12 +1553,14 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1550
1553
|
// (no run identity) is never this run's to sweep.
|
|
1551
1554
|
if (watchSlot && watchSlot.name === watchName && !desired.has(watchName)) {
|
|
1552
1555
|
const w = watchSlot;
|
|
1553
|
-
watchSlot = undefined;
|
|
1554
1556
|
await trackedDriver.close(w);
|
|
1557
|
+
watchSlot = undefined;
|
|
1555
1558
|
}
|
|
1556
1559
|
}
|
|
1557
|
-
catch {
|
|
1558
|
-
|
|
1560
|
+
catch (error) {
|
|
1561
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1562
|
+
journal.append("watch-cleanup-failed", undefined, { error: reason });
|
|
1563
|
+
console.error(`tickmarkr: ${reason}`);
|
|
1559
1564
|
}
|
|
1560
1565
|
};
|
|
1561
1566
|
// run start/resume boundary: nothing in flight, so the sweep takes this run's judge/review/consult
|
|
@@ -2725,6 +2730,9 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
2725
2730
|
journal.append("worker-launch", t.id, {
|
|
2726
2731
|
attempt,
|
|
2727
2732
|
retryMode,
|
|
2733
|
+
driver: trackedDriver.id,
|
|
2734
|
+
slot: { ...slot },
|
|
2735
|
+
workspace: driver.id === "herdr" ? process.env.HERDR_WORKSPACE_ID : undefined,
|
|
2728
2736
|
...(trackedDriver.describe ? await trackedDriver.describe(slot) : {}),
|
|
2729
2737
|
});
|
|
2730
2738
|
};
|
package/dist/run/lock.d.ts
CHANGED
|
@@ -36,12 +36,69 @@ export declare function runLockOwner(repoRoot: string): {
|
|
|
36
36
|
export declare function runLockRunId(repoRoot: string): string | undefined;
|
|
37
37
|
export declare function runStatusLine(repoRoot: string, runId: string, events: readonly RunLineEvent[]): string | null;
|
|
38
38
|
export declare function isRunLockLive(repoRoot: string): boolean;
|
|
39
|
-
export
|
|
39
|
+
export interface LockSnapshot {
|
|
40
|
+
pid?: number;
|
|
41
|
+
runId?: string;
|
|
42
|
+
garbage: boolean;
|
|
43
|
+
unreadable: boolean;
|
|
44
|
+
dead: boolean;
|
|
45
|
+
ino: number;
|
|
46
|
+
mtimeMs: number;
|
|
47
|
+
raw: Buffer;
|
|
48
|
+
}
|
|
49
|
+
export type UnlockPreview = {
|
|
40
50
|
held: false;
|
|
41
51
|
} | {
|
|
42
52
|
held: true;
|
|
53
|
+
eligible: false;
|
|
54
|
+
reason: string;
|
|
55
|
+
pid?: number;
|
|
56
|
+
runId?: string;
|
|
57
|
+
} | {
|
|
58
|
+
held: true;
|
|
59
|
+
eligible: true;
|
|
60
|
+
pid: number;
|
|
61
|
+
runId: string;
|
|
62
|
+
ino: number;
|
|
63
|
+
mtimeMs: number;
|
|
64
|
+
raw: Buffer;
|
|
65
|
+
};
|
|
66
|
+
export declare function previewUnlock(repoRoot: string, runId: string): UnlockPreview;
|
|
67
|
+
export declare function commitUnlock(repoRoot: string, target: {
|
|
68
|
+
pid: number;
|
|
69
|
+
runId: string;
|
|
70
|
+
ino: number;
|
|
71
|
+
mtimeMs: number;
|
|
72
|
+
raw: Buffer;
|
|
73
|
+
}): {
|
|
43
74
|
removed: true;
|
|
75
|
+
pid: number;
|
|
76
|
+
runId: string;
|
|
77
|
+
} | {
|
|
78
|
+
removed: false;
|
|
79
|
+
reason: string;
|
|
80
|
+
};
|
|
81
|
+
export type UnlockGarbagePreview = {
|
|
82
|
+
held: false;
|
|
83
|
+
} | {
|
|
84
|
+
held: true;
|
|
85
|
+
eligible: false;
|
|
86
|
+
reason: string;
|
|
44
87
|
pid?: number;
|
|
45
88
|
runId?: string;
|
|
46
|
-
|
|
89
|
+
} | {
|
|
90
|
+
held: true;
|
|
91
|
+
eligible: true;
|
|
92
|
+
ino: number;
|
|
93
|
+
raw: Buffer;
|
|
94
|
+
};
|
|
95
|
+
export declare function previewGarbageUnlock(repoRoot: string): UnlockGarbagePreview;
|
|
96
|
+
export declare function commitGarbageUnlock(repoRoot: string, target: {
|
|
97
|
+
ino: number;
|
|
98
|
+
raw: Buffer;
|
|
99
|
+
}): {
|
|
100
|
+
removed: true;
|
|
101
|
+
} | {
|
|
102
|
+
removed: false;
|
|
103
|
+
reason: string;
|
|
47
104
|
};
|