tickmarkr 2.4.1 → 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/adapters/claude-code.js +7 -2
- package/dist/adapters/codex.d.ts +1 -0
- package/dist/adapters/codex.js +66 -5
- package/dist/adapters/types.d.ts +4 -0
- package/dist/adapters/types.js +33 -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/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/drivers/orca.js
CHANGED
|
@@ -997,12 +997,51 @@ export class OrcaDriver {
|
|
|
997
997
|
return false;
|
|
998
998
|
}
|
|
999
999
|
}
|
|
1000
|
-
async narrator(
|
|
1000
|
+
async narrator(_cwd, _command, runId) {
|
|
1001
1001
|
if (!runId)
|
|
1002
1002
|
throw new OrcaError("create", "Orca narrator requires a run identity", "");
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1003
|
+
// The recorded Orca API can create a tab but provides no right/no-focus
|
|
1004
|
+
// placement receipt. Creating one would advertise a board we did not place.
|
|
1005
|
+
throw new OrcaError("create", "Orca narrator placement unsupported: right/no-focus board placement is not available", "");
|
|
1006
|
+
}
|
|
1007
|
+
async focus(target) {
|
|
1008
|
+
const { slot, runId, taskId, attempt } = target;
|
|
1009
|
+
const cwd = canonicalWorktreePath(slot.cwd);
|
|
1010
|
+
if (slot.name !== formatOwnedName({ role: "worker", taskId, attempt, runId })) {
|
|
1011
|
+
return { status: "foreign", reason: "Recorded run/task/attempt ownership does not match" };
|
|
1012
|
+
}
|
|
1013
|
+
try {
|
|
1014
|
+
const env = await this.listAll(cwd);
|
|
1015
|
+
const rows = env.result.terminals;
|
|
1016
|
+
const layouts = env.result.visualLayouts;
|
|
1017
|
+
if (!Array.isArray(rows) || !Array.isArray(layouts))
|
|
1018
|
+
return { status: "unsupported", reason: "Cannot verify Orca terminal ownership" };
|
|
1019
|
+
const handles = [];
|
|
1020
|
+
for (const layout of layouts) {
|
|
1021
|
+
if (typeof layout !== "object" || layout === null)
|
|
1022
|
+
continue;
|
|
1023
|
+
const lo = layout;
|
|
1024
|
+
if (!sameWorktree(terminalWorktree(lo), cwd))
|
|
1025
|
+
continue;
|
|
1026
|
+
const tabs = [];
|
|
1027
|
+
collectTabs(lo.root, tabs);
|
|
1028
|
+
for (const tab of tabs) {
|
|
1029
|
+
if (typeof tab === "object" && tab !== null && str(tab.title) === slot.name)
|
|
1030
|
+
collectPaneHandles(tab.panes, handles);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
if (handles.length !== 1)
|
|
1034
|
+
return { status: rows.length ? "foreign" : "closed", reason: "No unique owned terminal in the recorded worktree" };
|
|
1035
|
+
const matches = rows.filter(row => typeof row === "object" && row !== null && str(row.handle) === handles[0] && sameWorktree(terminalWorktree(row), cwd));
|
|
1036
|
+
if (matches.length !== 1)
|
|
1037
|
+
return { status: "foreign", reason: "Terminal worktree ownership is unverified" };
|
|
1038
|
+
if (matches[0].connected === false || matches[0].orphaned === true)
|
|
1039
|
+
return { status: "closed", reason: "Recorded terminal is no longer running; open task evidence" };
|
|
1040
|
+
return { status: "unsupported", reason: "Owned terminal verified; this Orca API has no focus operation. Open task evidence with Enter" };
|
|
1041
|
+
}
|
|
1042
|
+
catch (error) {
|
|
1043
|
+
return { status: "unsupported", reason: `Cannot verify Orca focus target: ${String(error)}` };
|
|
1044
|
+
}
|
|
1006
1045
|
}
|
|
1007
1046
|
async project(taskId, state) {
|
|
1008
1047
|
const worktree = this.taskWorktrees.get(taskId);
|
|
@@ -24,6 +24,7 @@ export declare class SubprocessDriver implements ExecutorDriver {
|
|
|
24
24
|
waitAgentStatus(slot: Slot, status: string, timeoutMs: number): Promise<boolean>;
|
|
25
25
|
status(_slot: Slot): Promise<string>;
|
|
26
26
|
read(slot: Slot, lines: number): Promise<string>;
|
|
27
|
+
focus(): Promise<import("./types.js").FocusResult>;
|
|
27
28
|
notify(msg: string, _opts?: NotifyOpts): Promise<void>;
|
|
28
29
|
close(slot: Slot): Promise<void>;
|
|
29
30
|
worktree(repo: string, branch: string, baseRef: string): Promise<string>;
|
|
@@ -107,6 +107,9 @@ export class SubprocessDriver {
|
|
|
107
107
|
async read(slot, lines) {
|
|
108
108
|
return this.state(slot).buf.split("\n").slice(-lines).join("\n");
|
|
109
109
|
}
|
|
110
|
+
async focus() {
|
|
111
|
+
return { status: "unsupported", reason: "Subprocess workers have no visible pane; open task evidence with Enter" };
|
|
112
|
+
}
|
|
110
113
|
async notify(msg, _opts) {
|
|
111
114
|
if (_opts?.tier === "routine")
|
|
112
115
|
return;
|
package/dist/drivers/types.d.ts
CHANGED
|
@@ -66,6 +66,18 @@ export declare function panesToClose(agents: FleetAgent[], desired: Set<string>,
|
|
|
66
66
|
tabId?: string;
|
|
67
67
|
}[];
|
|
68
68
|
export declare function canonicalizeLegacyName(name: string, runId: string): OwnedName;
|
|
69
|
+
export interface FocusTarget {
|
|
70
|
+
repo: string;
|
|
71
|
+
runId: string;
|
|
72
|
+
taskId: string;
|
|
73
|
+
attempt: number;
|
|
74
|
+
slot: Slot;
|
|
75
|
+
workspace?: string;
|
|
76
|
+
}
|
|
77
|
+
export type FocusResult = {
|
|
78
|
+
status: "focused" | "foreign" | "closed" | "unsupported";
|
|
79
|
+
reason: string;
|
|
80
|
+
};
|
|
69
81
|
export interface SlotPlacement {
|
|
70
82
|
surface?: string;
|
|
71
83
|
hostPlatform?: string;
|
|
@@ -76,6 +88,8 @@ export interface ExecutorDriver {
|
|
|
76
88
|
/** The exact terminal read surface used for liveness evidence. */
|
|
77
89
|
readSource?: string;
|
|
78
90
|
/** Placement facts returned by drivers whose terminal host exposes them. */
|
|
91
|
+
/** Verify the recorded identity against the live host before any focus mutation. */
|
|
92
|
+
focus?(target: FocusTarget): Promise<FocusResult>;
|
|
79
93
|
describe?(slot: Slot): SlotPlacement | Promise<SlotPlacement> | undefined;
|
|
80
94
|
slot(cwd: string, name: string, opts?: SlotOpts): Promise<Slot>;
|
|
81
95
|
run(slot: Slot, cmd: string): Promise<void>;
|
|
@@ -23,6 +23,27 @@ export type CaptureArtifactManifest = {
|
|
|
23
23
|
readonly artifacts: readonly CaptureArtifactManifestEntry[];
|
|
24
24
|
};
|
|
25
25
|
export declare const CAPTURE_PRODUCERS: readonly [{
|
|
26
|
+
readonly id: "screen-soak";
|
|
27
|
+
readonly provenance: {
|
|
28
|
+
readonly source: "tests/fixtures/screen-soak/soak.mjs";
|
|
29
|
+
readonly entrypoint: "sample";
|
|
30
|
+
readonly revision: "C6-four-hour-production-v1";
|
|
31
|
+
};
|
|
32
|
+
}, {
|
|
33
|
+
readonly id: "screen-soak-archive";
|
|
34
|
+
readonly provenance: {
|
|
35
|
+
readonly source: "tests/fixtures/screen-soak/archive.mjs";
|
|
36
|
+
readonly entrypoint: "archiveRecord";
|
|
37
|
+
readonly revision: "C6-lossless-gzip-v1";
|
|
38
|
+
};
|
|
39
|
+
}, {
|
|
40
|
+
readonly id: "cockpit-final-shell";
|
|
41
|
+
readonly provenance: {
|
|
42
|
+
readonly source: "src/tui/cockpit/capture.ts";
|
|
43
|
+
readonly entrypoint: "captureShellOutput";
|
|
44
|
+
readonly revision: "final-shell-v1";
|
|
45
|
+
};
|
|
46
|
+
}, {
|
|
26
47
|
readonly id: "cockpit-golden-frames";
|
|
27
48
|
readonly provenance: {
|
|
28
49
|
readonly source: "src/tui/cockpit/capture.ts";
|
|
@@ -40,6 +61,27 @@ export declare const CAPTURE_PRODUCERS: readonly [{
|
|
|
40
61
|
export declare const CAPTURE_ARTIFACT_MANIFEST: {
|
|
41
62
|
readonly version: 1;
|
|
42
63
|
readonly producers: readonly [{
|
|
64
|
+
readonly id: "screen-soak";
|
|
65
|
+
readonly provenance: {
|
|
66
|
+
readonly source: "tests/fixtures/screen-soak/soak.mjs";
|
|
67
|
+
readonly entrypoint: "sample";
|
|
68
|
+
readonly revision: "C6-four-hour-production-v1";
|
|
69
|
+
};
|
|
70
|
+
}, {
|
|
71
|
+
readonly id: "screen-soak-archive";
|
|
72
|
+
readonly provenance: {
|
|
73
|
+
readonly source: "tests/fixtures/screen-soak/archive.mjs";
|
|
74
|
+
readonly entrypoint: "archiveRecord";
|
|
75
|
+
readonly revision: "C6-lossless-gzip-v1";
|
|
76
|
+
};
|
|
77
|
+
}, {
|
|
78
|
+
readonly id: "cockpit-final-shell";
|
|
79
|
+
readonly provenance: {
|
|
80
|
+
readonly source: "src/tui/cockpit/capture.ts";
|
|
81
|
+
readonly entrypoint: "captureShellOutput";
|
|
82
|
+
readonly revision: "final-shell-v1";
|
|
83
|
+
};
|
|
84
|
+
}, {
|
|
43
85
|
readonly id: "cockpit-golden-frames";
|
|
44
86
|
readonly provenance: {
|
|
45
87
|
readonly source: "src/tui/cockpit/capture.ts";
|
|
@@ -62,6 +104,14 @@ export declare const CAPTURE_ARTIFACT_MANIFEST: {
|
|
|
62
104
|
path: string;
|
|
63
105
|
producer: string;
|
|
64
106
|
provenance: CaptureProducerProvenance;
|
|
107
|
+
} | {
|
|
108
|
+
path: string;
|
|
109
|
+
producer: string;
|
|
110
|
+
provenance: CaptureProducerProvenance;
|
|
111
|
+
} | {
|
|
112
|
+
path: string;
|
|
113
|
+
producer: string;
|
|
114
|
+
provenance: CaptureProducerProvenance;
|
|
65
115
|
})[];
|
|
66
116
|
};
|
|
67
117
|
/** Compatibility name for the pre-manifest gate API; now derived from one manifest. */
|
|
@@ -13,7 +13,19 @@ const COLOUR_PROVENANCE = {
|
|
|
13
13
|
entrypoint: "regenerateColourFrames",
|
|
14
14
|
revision: "colour-frame-v1",
|
|
15
15
|
};
|
|
16
|
+
const SHELL_PROVENANCE = {
|
|
17
|
+
source: "src/tui/cockpit/capture.ts", entrypoint: "captureShellOutput", revision: "final-shell-v1",
|
|
18
|
+
};
|
|
19
|
+
const SOAK_PROVENANCE = {
|
|
20
|
+
source: "tests/fixtures/screen-soak/soak.mjs", entrypoint: "sample", revision: "C6-four-hour-production-v1",
|
|
21
|
+
};
|
|
22
|
+
const SOAK_ARCHIVE_PROVENANCE = {
|
|
23
|
+
source: "tests/fixtures/screen-soak/archive.mjs", entrypoint: "archiveRecord", revision: "C6-lossless-gzip-v1",
|
|
24
|
+
};
|
|
16
25
|
export const CAPTURE_PRODUCERS = [
|
|
26
|
+
{ id: "screen-soak", provenance: SOAK_PROVENANCE },
|
|
27
|
+
{ id: "screen-soak-archive", provenance: SOAK_ARCHIVE_PROVENANCE },
|
|
28
|
+
{ id: "cockpit-final-shell", provenance: SHELL_PROVENANCE },
|
|
17
29
|
{ id: "cockpit-golden-frames", provenance: GOLDEN_PROVENANCE },
|
|
18
30
|
{ id: "cockpit-colour-frames", provenance: COLOUR_PROVENANCE },
|
|
19
31
|
];
|
|
@@ -49,6 +61,17 @@ export const CAPTURE_ARTIFACT_MANIFEST = {
|
|
|
49
61
|
version: 1,
|
|
50
62
|
producers: CAPTURE_PRODUCERS,
|
|
51
63
|
artifacts: [
|
|
64
|
+
// Exact measured artifacts only. Neighbouring harnesses, notes and unknown
|
|
65
|
+
// records remain logic; duration evidence never grants a directory exemption.
|
|
66
|
+
...["static", "growth", "cutover-static", "cutover-growth", "final-static", "final-growth", "retry-static", "retry-growth"].flatMap(attempt => ["build.json", "metadata.json", "samples.jsonl", "last-frame.ansi", "journal.jsonl.gz", "result.json.gz"].map(file => ({
|
|
67
|
+
path: `tests/fixtures/screen-soak/records/${attempt}/${file}`,
|
|
68
|
+
producer: file.endsWith(".gz") ? "screen-soak-archive" : "screen-soak",
|
|
69
|
+
provenance: provenanceCopy(file.endsWith(".gz") ? SOAK_ARCHIVE_PROVENANCE : SOAK_PROVENANCE),
|
|
70
|
+
}))),
|
|
71
|
+
...["home", "run", "evidence"].flatMap(view => ["120x40", "80x24"].map(size => ({
|
|
72
|
+
path: `tests/fixtures/cockpit/final/${view}.${size}.txt`,
|
|
73
|
+
producer: "cockpit-final-shell", provenance: provenanceCopy(SHELL_PROVENANCE),
|
|
74
|
+
}))),
|
|
52
75
|
...goldenFrameNames.map((fixture) => ({
|
|
53
76
|
path: `tests/fixtures/cockpit/frames/${fixture}`,
|
|
54
77
|
producer: "cockpit-golden-frames",
|
package/dist/plan/scope.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type { WorkerAdapter } from "../adapters/types.js";
|
|
2
2
|
import type { TickmarkrConfig } from "../config/config.js";
|
|
3
3
|
import type { ExecutorDriver } from "../drivers/types.js";
|
|
4
|
+
export declare const MAX_SCOPE_ATTEMPTS = 3;
|
|
4
5
|
export declare function clarificationGate(intent: string): string[];
|
|
5
6
|
export interface ScopeOptions {
|
|
6
7
|
cfg: TickmarkrConfig;
|
|
7
8
|
adapters: WorkerAdapter[];
|
|
8
9
|
driver?: ExecutorDriver;
|
|
9
10
|
force?: boolean;
|
|
11
|
+
candidate?: ScopeCandidate;
|
|
10
12
|
}
|
|
11
13
|
export interface ScopeResult {
|
|
12
14
|
specFile: string;
|
|
@@ -14,4 +16,27 @@ export interface ScopeResult {
|
|
|
14
16
|
attempts: number;
|
|
15
17
|
}
|
|
16
18
|
export declare function specPathForIntent(intentFile: string): string;
|
|
19
|
+
export interface ScopeCandidate {
|
|
20
|
+
adapter: string;
|
|
21
|
+
model: string;
|
|
22
|
+
}
|
|
23
|
+
export interface ScopePreview {
|
|
24
|
+
intentFile: string;
|
|
25
|
+
specFile: string;
|
|
26
|
+
specExists: boolean;
|
|
27
|
+
cached: boolean;
|
|
28
|
+
candidate?: ScopeCandidate;
|
|
29
|
+
authoringBudget: number;
|
|
30
|
+
probeCalls: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* R11/R45 (C10): local-only disclosure — intent/clarification checks and a candidate read off the
|
|
34
|
+
* doctor cache, never a fresh probe or a model turn. `readDoctor` and `discoverChannels`/`route` are
|
|
35
|
+
* pure reads over that cache, so this never touches an adapter.
|
|
36
|
+
*/
|
|
37
|
+
export declare function previewScope(intentFile: string, repoRoot: string, options: {
|
|
38
|
+
cfg: TickmarkrConfig;
|
|
39
|
+
adapters: WorkerAdapter[];
|
|
40
|
+
}): ScopePreview;
|
|
41
|
+
export declare function formatScopePreview(preview: ScopePreview): string;
|
|
17
42
|
export declare function scopeIntent(intentFile: string, repoRoot: string, options: ScopeOptions): Promise<ScopeResult>;
|
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
|
/**
|