tickmarkr 2.4.2 → 2.5.1
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/adapters/fake.d.ts +1 -0
- package/dist/adapters/fake.js +2 -1
- package/dist/adapters/opencode.d.ts +1 -0
- package/dist/adapters/opencode.js +4 -1
- package/dist/adapters/types.d.ts +14 -0
- package/dist/adapters/types.js +25 -0
- 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/init.d.ts +2 -0
- package/dist/cli/commands/init.js +45 -5
- package/dist/cli/commands/plan.js +58 -6
- package/dist/cli/commands/report.js +18 -45
- package/dist/cli/commands/run.js +3 -0
- 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/gates/llm.d.ts +5 -0
- package/dist/gates/llm.js +188 -3
- package/dist/gates/review.d.ts +6 -3
- package/dist/gates/review.js +79 -33
- package/dist/gates/run-gates.d.ts +3 -0
- package/dist/gates/run-gates.js +14 -4
- 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 +5 -5
- package/dist/run/daemon.js +166 -44
- 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/dist/tui/ink/init-app.d.ts +4 -0
- package/dist/tui/ink/init-app.js +18 -7
- 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/skills/tickmarkr-overseer/scripts/watch-context.sh +13 -2
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
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { type WorkerAdapter } from "../adapters/types.js";
|
|
2
2
|
import { type ModeResolution, type RoutingMode, type TickmarkrConfig } from "../config/config.js";
|
|
3
3
|
import { type DriverChoice } from "../drivers/index.js";
|
|
4
|
-
import { type ExecutorDriver } from "../drivers/types.js";
|
|
4
|
+
import { type ExecutorDriver, type Slot } from "../drivers/types.js";
|
|
5
5
|
import { type Baseline } from "../gates/baseline.js";
|
|
6
6
|
import type { GateResult } from "../gates/types.js";
|
|
7
7
|
import { Journal, type JournalEvent } from "./journal.js";
|
|
8
|
+
export declare function closeLiveSlot(liveSlots: Set<Slot>, driver: Pick<ExecutorDriver, "close">, slot: Slot): Promise<void>;
|
|
8
9
|
export { harvestCpuFlatWindowMs, resetHarvestCpuFlatMsForTests, setHarvestCpuFlatMsForTests, workerTreeCpuMs, } from "./stall.js";
|
|
9
10
|
export interface RunOptions {
|
|
10
11
|
runId?: string;
|
|
@@ -64,10 +65,9 @@ export interface RunSummary {
|
|
|
64
65
|
*/
|
|
65
66
|
export declare function outstandingApprovals(events: JournalEvent[]): string[];
|
|
66
67
|
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). */
|
|
68
|
+
/** The narrator enters the production Run cockpit for this exact run. The
|
|
69
|
+
* completed static/growing four-hour records precede this default cutover;
|
|
70
|
+
* an explicit ID prevents a newer journal from redirecting the owned board. */
|
|
71
71
|
export declare const daemonEntrypoint: string;
|
|
72
72
|
export declare const watchCommand: (runId: string) => string;
|
|
73
73
|
/**
|