tickmarkr 1.78.0 → 1.80.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/dist/adapters/claude-code.d.ts +11 -0
- package/dist/adapters/claude-code.js +105 -16
- package/dist/adapters/kimi.d.ts +6 -0
- package/dist/adapters/kimi.js +88 -26
- package/dist/adapters/registry.js +6 -1
- package/dist/brand.d.ts +4 -0
- package/dist/brand.js +4 -0
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.js +23 -0
- package/dist/cli/commands/status.d.ts +4 -0
- package/dist/cli/commands/status.js +237 -18
- package/dist/cli/commands/ui.d.ts +1 -1
- package/dist/cli/commands/ui.js +11 -1
- package/dist/drivers/herdr.js +36 -7
- package/dist/drivers/types.d.ts +22 -1
- package/dist/gates/acceptance.d.ts +5 -0
- package/dist/gates/acceptance.js +5 -3
- package/dist/gates/llm.d.ts +9 -0
- package/dist/gates/llm.js +74 -0
- package/dist/gates/review.d.ts +5 -0
- package/dist/gates/review.js +5 -3
- package/dist/run/journal.d.ts +1 -1
- package/dist/tui/cockpit/capture.d.ts +117 -0
- package/dist/tui/cockpit/capture.js +329 -0
- package/dist/tui/cockpit/components.d.ts +179 -0
- package/dist/tui/cockpit/components.js +280 -0
- package/dist/tui/cockpit/demo.d.ts +41 -0
- package/dist/tui/cockpit/demo.js +144 -0
- package/dist/tui/cockpit/derive.d.ts +39 -0
- package/dist/tui/cockpit/derive.js +390 -0
- package/dist/tui/cockpit/layout.d.ts +76 -0
- package/dist/tui/cockpit/layout.js +84 -0
- package/dist/tui/cockpit/run-cockpit.d.ts +19 -0
- package/dist/tui/cockpit/run-cockpit.js +194 -0
- package/dist/tui/cockpit/setup-cockpit.d.ts +41 -0
- package/dist/tui/cockpit/setup-cockpit.js +488 -0
- package/dist/tui/cockpit/theme.d.ts +161 -0
- package/dist/tui/cockpit/theme.js +170 -0
- package/package.json +2 -1
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { type AuthHealth, type TrustDialog, type WorkerAdapter } from "./types.js";
|
|
2
|
+
export declare const CLAUDE_ALIAS_IDENTITY_STAMPS: {
|
|
3
|
+
readonly fable: "claude-fable-5";
|
|
4
|
+
readonly opus: "claude-opus-4-8";
|
|
5
|
+
readonly sonnet: "claude-sonnet-5";
|
|
6
|
+
readonly haiku: "claude-haiku-4-5-20251001";
|
|
7
|
+
};
|
|
8
|
+
export type ClaudeAlias = keyof typeof CLAUDE_ALIAS_IDENTITY_STAMPS;
|
|
9
|
+
export type ClaudeAliasIdentityProbe = (cwd: string, alias: ClaudeAlias) => string | undefined;
|
|
2
10
|
export declare const CLAUDE_TRUST_DIALOG: TrustDialog;
|
|
3
11
|
export declare function claudeSlug(real: string): string;
|
|
12
|
+
export declare function readClaudeAliasIdentity(cwd: string, alias: ClaudeAlias): string | undefined;
|
|
13
|
+
export declare function probeClaudeAliasIdentity(cwd: string, alias: ClaudeAlias): string | undefined;
|
|
14
|
+
export declare function resolveClaudeAliasIdentity(cwd: string, alias: ClaudeAlias, probe?: ClaudeAliasIdentityProbe): string | undefined;
|
|
4
15
|
export declare function probeVersion(bin: string): AuthHealth;
|
|
5
16
|
export declare const claudeCode: WorkerAdapter;
|
|
@@ -3,7 +3,7 @@ import { readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { parseWorkerResult } from "./prompt.js";
|
|
6
|
-
import { channelsFromConfig, shq, TokenUsageSchema } from "./types.js";
|
|
6
|
+
import { channelsFromConfig, MODEL_ID_RE, shq, TokenUsageSchema } from "./types.js";
|
|
7
7
|
// SPEND-01/SPEND-11: claude writes a per-session JSONL to ~/.claude/projects/<slug>/ where slug is the
|
|
8
8
|
// realpath'd cwd with every non-alphanumeric char replaced by "-" (verified 114/114 — 36-DIAGNOSIS.md).
|
|
9
9
|
// The old `/`-only formula missed the "." in `.tickmarkr/worktrees/…` — ENOENT on every worktree dispatch.
|
|
@@ -20,6 +20,20 @@ import { channelsFromConfig, shq, TokenUsageSchema } from "./types.js";
|
|
|
20
20
|
// Phase 18's operator-price × tokens derivation, not a CLI claim.
|
|
21
21
|
const MAX_SESSION_FILES = 20; // newest-first; a long-lived project dir can hold many sessions
|
|
22
22
|
const MAX_SESSION_BYTES = 8_000_000; // per-file cap; a runaway JSONL cannot make the read unbounded
|
|
23
|
+
// OBS-145: these aliases float at the Claude CLI layer, while their tier/pricing decisions were
|
|
24
|
+
// made for the dated identities below (claude-code seeds stamped 2026-07-09 — doctor's own lint).
|
|
25
|
+
// Updating a stamp is a deliberate benchmark-policy act; doctor only compares against it and never
|
|
26
|
+
// edits tiers, pricing, learned scores, or routing. `opus` is deliberately left at 4-8: the OBS-145
|
|
27
|
+
// drill probed the alias at claude-opus-4-8 matching its stamp, then captured it re-pointing to
|
|
28
|
+
// claude-opus-5 ~30 min later with zero events fired. The 2026-07-24 fit added an EXPLICIT
|
|
29
|
+
// claude-opus-5 channel to the repo overlay — it did not re-date this alias's stamps, so the
|
|
30
|
+
// alias channel still carries 4-8-dated tier/pricing while serving 5. That warning is true.
|
|
31
|
+
export const CLAUDE_ALIAS_IDENTITY_STAMPS = {
|
|
32
|
+
fable: "claude-fable-5",
|
|
33
|
+
opus: "claude-opus-4-8",
|
|
34
|
+
sonnet: "claude-sonnet-5",
|
|
35
|
+
haiku: "claude-haiku-4-5-20251001",
|
|
36
|
+
};
|
|
23
37
|
// v1.75 T2 / OBS-137: current Claude Code workspace-trust prompt (2.1.218). The full question
|
|
24
38
|
// distinguishes this startup gate from routine agent text; Enter accepts the selected trust option.
|
|
25
39
|
export const CLAUDE_TRUST_DIALOG = {
|
|
@@ -29,6 +43,95 @@ export const CLAUDE_TRUST_DIALOG = {
|
|
|
29
43
|
export function claudeSlug(real) {
|
|
30
44
|
return real.replace(/[^A-Za-z0-9]/g, "-");
|
|
31
45
|
}
|
|
46
|
+
// newest-first by mtime, bounded — mtime picks WHICH files to scan, never a record's cursor.
|
|
47
|
+
// Shared by collectUsage (spend) and readClaudeAliasIdentity (OBS-145): same store, same bounds.
|
|
48
|
+
function newestSessionFiles(dir) {
|
|
49
|
+
return readdirSync(dir)
|
|
50
|
+
.filter((f) => f.endsWith(".jsonl"))
|
|
51
|
+
.map((f) => {
|
|
52
|
+
try {
|
|
53
|
+
return { f, m: statSync(join(dir, f)).mtimeMs };
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return undefined; // a stat failure just drops that file
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
.filter((x) => x !== undefined)
|
|
60
|
+
.sort((a, b) => b.m - a.m)
|
|
61
|
+
.slice(0, MAX_SESSION_FILES)
|
|
62
|
+
.map((x) => x.f);
|
|
63
|
+
}
|
|
64
|
+
const belongsToAliasFamily = (identity, alias) => identity === alias || identity.split(/[^A-Za-z0-9]+/).includes(alias);
|
|
65
|
+
// Zero-token first choice: Claude assistant records state the resolved message.model. The newest
|
|
66
|
+
// matching family record in this cwd wins. Every failure is advisory/unknown, never an exception.
|
|
67
|
+
export function readClaudeAliasIdentity(cwd, alias) {
|
|
68
|
+
try {
|
|
69
|
+
const real = realpathSync(cwd);
|
|
70
|
+
const dir = join(homedir(), ".claude", "projects", claudeSlug(real));
|
|
71
|
+
let newest;
|
|
72
|
+
for (const f of newestSessionFiles(dir)) {
|
|
73
|
+
let text;
|
|
74
|
+
try {
|
|
75
|
+
text = readFileSync(join(dir, f), "utf8").slice(0, MAX_SESSION_BYTES);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
for (const line of text.split("\n")) {
|
|
81
|
+
if (!line.trim())
|
|
82
|
+
continue;
|
|
83
|
+
let raw;
|
|
84
|
+
try {
|
|
85
|
+
raw = JSON.parse(line);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const rec = raw;
|
|
91
|
+
if (rec.cwd !== real || typeof rec.message?.model !== "string")
|
|
92
|
+
continue;
|
|
93
|
+
const identity = rec.message.model;
|
|
94
|
+
const timestamp = Date.parse(String(rec.timestamp));
|
|
95
|
+
if (!MODEL_ID_RE.test(identity) || !belongsToAliasFamily(identity, alias) || !Number.isFinite(timestamp))
|
|
96
|
+
continue;
|
|
97
|
+
if (!newest || timestamp > newest.timestamp)
|
|
98
|
+
newest = { identity, timestamp };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return newest?.identity;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Store silence earns one minimal stated-identity turn. The output contract is intentionally strict:
|
|
108
|
+
// one safe model id from the requested alias family, otherwise unknown/fail-open. Vitest never spawns
|
|
109
|
+
// a real agent CLI; tests inject the probe callback at resolveClaudeAliasIdentity instead.
|
|
110
|
+
export function probeClaudeAliasIdentity(cwd, alias) {
|
|
111
|
+
if (process.env.VITEST)
|
|
112
|
+
return undefined;
|
|
113
|
+
try {
|
|
114
|
+
const r = spawnSync("claude", [
|
|
115
|
+
"-p",
|
|
116
|
+
"State the exact model identifier serving this request. Reply with only that identifier.",
|
|
117
|
+
"--model", alias,
|
|
118
|
+
"--permission-mode", "bypassPermissions",
|
|
119
|
+
"--strict-mcp-config",
|
|
120
|
+
"--mcp-config", '{"mcpServers":{}}',
|
|
121
|
+
"--output-format", "text",
|
|
122
|
+
], { cwd, encoding: "utf8", timeout: 60000, stdio: ["ignore", "pipe", "pipe"] });
|
|
123
|
+
if (r.error || r.status !== 0)
|
|
124
|
+
return undefined;
|
|
125
|
+
const identity = (r.stdout || "").trim();
|
|
126
|
+
return MODEL_ID_RE.test(identity) && belongsToAliasFamily(identity, alias) ? identity : undefined;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
export function resolveClaudeAliasIdentity(cwd, alias, probe = probeClaudeAliasIdentity) {
|
|
133
|
+
return readClaudeAliasIdentity(cwd, alias) ?? probe(cwd, alias);
|
|
134
|
+
}
|
|
32
135
|
export function probeVersion(bin) {
|
|
33
136
|
const r = spawnSync(bin, ["--version"], { encoding: "utf8", timeout: 10000 });
|
|
34
137
|
if (r.error || r.status !== 0)
|
|
@@ -75,24 +178,10 @@ export const claudeCode = {
|
|
|
75
178
|
const real = realpathSync(cwd); // resolve symlinks (darwin /tmp → /private/tmp)
|
|
76
179
|
const slug = claudeSlug(real);
|
|
77
180
|
const dir = join(homedir(), ".claude", "projects", slug);
|
|
78
|
-
// newest-first by mtime, bounded — mtime picks WHICH files to scan, never a record's cursor.
|
|
79
|
-
const files = readdirSync(dir)
|
|
80
|
-
.filter((f) => f.endsWith(".jsonl"))
|
|
81
|
-
.map((f) => {
|
|
82
|
-
try {
|
|
83
|
-
return { f, m: statSync(join(dir, f)).mtimeMs };
|
|
84
|
-
}
|
|
85
|
-
catch {
|
|
86
|
-
return undefined; // a stat failure just drops that file
|
|
87
|
-
}
|
|
88
|
-
})
|
|
89
|
-
.filter((x) => x !== undefined)
|
|
90
|
-
.sort((a, b) => b.m - a.m)
|
|
91
|
-
.slice(0, MAX_SESSION_FILES);
|
|
92
181
|
let input = 0, output = 0, kept = false;
|
|
93
182
|
let cacheRead, cacheWrite;
|
|
94
183
|
const seen = new Set(); // message.id is globally unique across session files in one call
|
|
95
|
-
for (const
|
|
184
|
+
for (const f of newestSessionFiles(dir)) {
|
|
96
185
|
let text;
|
|
97
186
|
try {
|
|
98
187
|
text = readFileSync(join(dir, f), "utf8").slice(0, MAX_SESSION_BYTES);
|
package/dist/adapters/kimi.d.ts
CHANGED
|
@@ -13,9 +13,15 @@ export declare function kimiBannerModel(banner: string): string | undefined;
|
|
|
13
13
|
export declare function kimiBannerSessionId(banner: string): string | undefined;
|
|
14
14
|
export type KimiBannerConfirm = {
|
|
15
15
|
ok: true;
|
|
16
|
+
status: "confirmed";
|
|
17
|
+
sessionId?: string;
|
|
18
|
+
} | {
|
|
19
|
+
status: "unknown";
|
|
20
|
+
printedModel?: string;
|
|
16
21
|
sessionId?: string;
|
|
17
22
|
} | {
|
|
18
23
|
ok: false;
|
|
24
|
+
status: "mismatch";
|
|
19
25
|
error: string;
|
|
20
26
|
};
|
|
21
27
|
export declare function confirmKimiSeedBanner(banner: string, assignedModel: string): KimiBannerConfirm;
|
package/dist/adapters/kimi.js
CHANGED
|
@@ -96,11 +96,26 @@ export async function probeKimiDoctorTurn(cwd) {
|
|
|
96
96
|
// Prefer a banner Session line when present so seed-mode captures the id from the launch banner
|
|
97
97
|
// itself rather than waiting for a completion-time resume trailer (which the TUI may never emit).
|
|
98
98
|
const RESUME_TRAILER_RE = /^\s*To resume this session: kimi -r (session_[0-9a-f-]+)\s*$/;
|
|
99
|
-
const
|
|
99
|
+
const BANNER_SESSION_RE = /^Session:\s*(session_[0-9a-f-]+)$/;
|
|
100
|
+
const BANNER_MODEL_RE = /^Model:\s*(.+)$/;
|
|
101
|
+
function kimiBannerLines(banner) {
|
|
102
|
+
// The live TUI renders banner fields as indented rows inside `│ ... │`. Strip only that
|
|
103
|
+
// per-line margin/chrome, following prompt.ts's pane-text precedent; preserve field content.
|
|
104
|
+
return banner.split("\n")
|
|
105
|
+
.map((line) => line.replace(/^[\s│|]+/, "").replace(/[\s│|]+$/, ""));
|
|
106
|
+
}
|
|
107
|
+
function kimiBannerPrintedModel(banner) {
|
|
108
|
+
for (const line of kimiBannerLines(banner)) {
|
|
109
|
+
const printedModel = BANNER_MODEL_RE.exec(line)?.[1]?.trim();
|
|
110
|
+
if (printedModel)
|
|
111
|
+
return printedModel;
|
|
112
|
+
}
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
100
115
|
export function kimiSessionId(output) {
|
|
101
116
|
let id;
|
|
102
|
-
for (const line of output
|
|
103
|
-
const banner =
|
|
117
|
+
for (const line of kimiBannerLines(output)) {
|
|
118
|
+
const banner = BANNER_SESSION_RE.exec(line);
|
|
104
119
|
if (banner) {
|
|
105
120
|
id = banner[1];
|
|
106
121
|
continue;
|
|
@@ -113,44 +128,84 @@ export function kimiSessionId(output) {
|
|
|
113
128
|
}
|
|
114
129
|
// v1.69 T7: the cold-start banner prints the model alias and session id. Parse them from the
|
|
115
130
|
// banner text already captured for the readiness match — no new probe, no extra dispatch. Kimi
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
const
|
|
119
|
-
|
|
131
|
+
// may print a display name, the full config key, or its legacy suffix. Resolve only names with a
|
|
132
|
+
// known channel mapping: inventing `kimi-code/<future display>` would turn unknown into mismatch.
|
|
133
|
+
const KIMI_BANNER_MODEL_CHANNELS = new Map([
|
|
134
|
+
["K3", "kimi-code/k3"],
|
|
135
|
+
["k3", "kimi-code/k3"],
|
|
136
|
+
["kimi-code/k3", "kimi-code/k3"],
|
|
137
|
+
["K2.7 Coding", "kimi-code/kimi-for-coding"],
|
|
138
|
+
["kimi-for-coding", "kimi-code/kimi-for-coding"],
|
|
139
|
+
["kimi-code/kimi-for-coding", "kimi-code/kimi-for-coding"],
|
|
140
|
+
["K2.7 Coding Highspeed", "kimi-code/kimi-for-coding-highspeed"],
|
|
141
|
+
["kimi-for-coding-highspeed", "kimi-code/kimi-for-coding-highspeed"],
|
|
142
|
+
["kimi-code/kimi-for-coding-highspeed", "kimi-code/kimi-for-coding-highspeed"],
|
|
143
|
+
]);
|
|
120
144
|
export function kimiBannerModel(banner) {
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
return undefined;
|
|
124
|
-
const printedModel = m[1].trim();
|
|
125
|
-
if (!printedModel)
|
|
126
|
-
return undefined;
|
|
127
|
-
return printedModel.startsWith("kimi-code/") ? printedModel : `kimi-code/${printedModel}`;
|
|
145
|
+
const printedModel = kimiBannerPrintedModel(banner);
|
|
146
|
+
return printedModel === undefined ? undefined : KIMI_BANNER_MODEL_CHANNELS.get(printedModel);
|
|
128
147
|
}
|
|
129
148
|
export function kimiBannerSessionId(banner) {
|
|
130
|
-
|
|
149
|
+
for (const line of kimiBannerLines(banner)) {
|
|
150
|
+
const sessionId = BANNER_SESSION_RE.exec(line)?.[1];
|
|
151
|
+
if (sessionId)
|
|
152
|
+
return sessionId;
|
|
153
|
+
}
|
|
154
|
+
return undefined;
|
|
131
155
|
}
|
|
132
|
-
// Fail closed on a
|
|
156
|
+
// Fail closed on a mapped-model mismatch. Unknown/missing names remain non-blocking for forward
|
|
157
|
+
// compatibility, but carry an explicit unknown status rather than masquerading as confirmation.
|
|
133
158
|
export function confirmKimiSeedBanner(banner, assignedModel) {
|
|
159
|
+
const sessionId = kimiBannerSessionId(banner);
|
|
160
|
+
const printedModel = kimiBannerPrintedModel(banner);
|
|
134
161
|
const saw = kimiBannerModel(banner);
|
|
135
|
-
if (
|
|
136
|
-
return {
|
|
162
|
+
if (printedModel === undefined || saw === undefined) {
|
|
163
|
+
return {
|
|
164
|
+
status: "unknown",
|
|
165
|
+
...(printedModel === undefined ? {} : { printedModel }),
|
|
166
|
+
...(sessionId === undefined ? {} : { sessionId }),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
if (saw !== assignedModel) {
|
|
170
|
+
return { ok: false, status: "mismatch", error: `model mismatch: expected ${assignedModel}, saw ${saw}` };
|
|
137
171
|
}
|
|
138
|
-
return { ok: true, sessionId:
|
|
172
|
+
return { ok: true, status: "confirmed", ...(sessionId === undefined ? {} : { sessionId }) };
|
|
139
173
|
}
|
|
140
174
|
const kimiTuiLaunch = (model) => `kimi -y -m ${shq(model)}`;
|
|
141
175
|
const KIMI_ANSI_SGR_RE = /\u001B\[[0-9;]*m/g;
|
|
142
176
|
const KIMI_EDITOR_TOP_RE = /^╭─+╮$/;
|
|
143
177
|
const KIMI_EDITOR_INPUT_RE = /^│ > .*│$/;
|
|
144
178
|
const KIMI_EDITOR_EMPTY_RE = /^│ > +│$/;
|
|
179
|
+
// OBS-181: any interior row of the box. An editor carrying a prompt renders one or more blank
|
|
180
|
+
// continuation rows beneath it, so the bottom border is not always the very next line.
|
|
181
|
+
const KIMI_EDITOR_ROW_RE = /^│.*│$/;
|
|
145
182
|
const KIMI_EDITOR_BOTTOM_RE = /^╰─+╯$/;
|
|
146
183
|
function matchesKimiEditorBox(paneText, empty) {
|
|
147
|
-
|
|
184
|
+
// OBS-152: kimi renders its whole TUI indented one column, so `^`-anchored row matches never fire
|
|
185
|
+
// on a LIVE editor box — probe 6 timed out readiness against a frame whose box was fully painted
|
|
186
|
+
// and interactive. Trim per line: the left margin is chrome, and what actually distinguishes the
|
|
187
|
+
// editor from the welcome panel (also a bordered box) is the border adjacency asserted below.
|
|
188
|
+
//
|
|
189
|
+
// `match` means THE EDITOR BOX IS PAINTED (readiness), and is deliberately true for an empty box.
|
|
190
|
+
// `emptyMatch` is the stricter "painted AND carrying nothing". Callers deciding submission must
|
|
191
|
+
// therefore test emptyMatch FIRST — see HerdrDriver.submissionRegistered.
|
|
192
|
+
//
|
|
193
|
+
// OBS-181: the adjacency used to demand the bottom border on the line directly below the prompt.
|
|
194
|
+
// A wedged worker pane rendered FOUR rows — top, prompt, a blank continuation row, bottom — so
|
|
195
|
+
// NEITHER matcher fired, and the driver's submission check fell through to a fallback that always
|
|
196
|
+
// answers "delivered". Walk the interior to the border instead of assuming a three-row box.
|
|
197
|
+
const lines = paneText.replace(KIMI_ANSI_SGR_RE, "").split("\n").map((line) => line.trim());
|
|
148
198
|
const input = empty ? KIMI_EDITOR_EMPTY_RE : KIMI_EDITOR_INPUT_RE;
|
|
149
|
-
return lines.some((line, index) =>
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
199
|
+
return lines.some((line, index) => {
|
|
200
|
+
if (!input.test(line))
|
|
201
|
+
return false;
|
|
202
|
+
if (index === 0 || !KIMI_EDITOR_TOP_RE.test(lines[index - 1]))
|
|
203
|
+
return false;
|
|
204
|
+
let below = index + 1;
|
|
205
|
+
while (below < lines.length && KIMI_EDITOR_ROW_RE.test(lines[below]))
|
|
206
|
+
below++;
|
|
207
|
+
return below < lines.length && KIMI_EDITOR_BOTTOM_RE.test(lines[below]);
|
|
208
|
+
});
|
|
154
209
|
}
|
|
155
210
|
// v1.75 T1 / OBS-136: Kimi's actual editor is a three-line bordered box whose content row starts
|
|
156
211
|
// `│ > `. The welcome panel separately contains "Send /help..." and is only launch-liveness
|
|
@@ -170,7 +225,14 @@ const KIMI_SEED = {
|
|
|
170
225
|
launch: kimiTuiLaunch,
|
|
171
226
|
readinessMatch: "Send /help for help information.",
|
|
172
227
|
seedLine: (promptFile) => `Read ${promptFile} and do exactly what it says.`,
|
|
173
|
-
confirmBanner:
|
|
228
|
+
confirmBanner: (banner, assignedModel) => {
|
|
229
|
+
const confirmation = confirmKimiSeedBanner(banner, assignedModel);
|
|
230
|
+
// The generic seed contract is binary: unknown display names remain admissible while the
|
|
231
|
+
// Kimi-specific result above truthfully keeps them distinct from a confirmed identity.
|
|
232
|
+
return confirmation.status === "unknown"
|
|
233
|
+
? { ok: true, status: confirmation.status, sessionId: confirmation.sessionId }
|
|
234
|
+
: confirmation;
|
|
235
|
+
},
|
|
174
236
|
};
|
|
175
237
|
// v1.69 T7 / v1.71 T2: thin wrapper over the daemon's generic dispatch path for kimi-only tests.
|
|
176
238
|
export async function runKimiInteractiveSeed(opts) {
|
|
@@ -203,7 +203,12 @@ const MODEL_PROBE_PROMPT = "Reply with exactly OK and nothing else.";
|
|
|
203
203
|
// 60s: a healthy MCP-free codex probe measured 29.9s round-trip (2026-07-15) — 30s had zero headroom.
|
|
204
204
|
const MODEL_PROBE_TIMEOUT_MS = 60000;
|
|
205
205
|
// Auth-words only match when tied to a failure word; bare "auth"/"OAuth"/"authored" never fail (v1.27 T2).
|
|
206
|
-
|
|
206
|
+
// OBS-150: the 4xx alternative must refuse digit-group context. Bare `\b4\d\d\b` matched "485" inside the
|
|
207
|
+
// token footer "tokens used 26,485" and failed a HEALTHY probe closed — a ~1-in-10 dice roll on EVERY model
|
|
208
|
+
// probe of any adapter that prints a comma-grouped token count. Lookarounds reject a group that is part of a
|
|
209
|
+
// larger number (26,485 · 1.400 · 400.5) while keeping real bare codes (401 · "error 429" · "403 Forbidden."
|
|
210
|
+
// — trailing sentence punctuation still matches, only a following DIGIT means "decimal, not status code").
|
|
211
|
+
const AUTH_FAILURE_RE = /\b(?<![\d.,])4\d\d\b(?![.,]\d)|\bauth(?:entication|orization)?\s+(?:error|failed|failure|denied)|unauthori[sz]ed|forbidden|access denied|credit(?:s)?\s+(?:exhausted|error|denied)/i;
|
|
207
212
|
const PROBE_REASON_CAP = 240;
|
|
208
213
|
// v1.55 T3: the tail must open at a word boundary — a mid-word slice ("odel 'grok-…'") reads as
|
|
209
214
|
// corruption in operator-facing diagnostics. Input is already space-normalized, so " " is the only
|
package/dist/brand.d.ts
CHANGED
|
@@ -2,6 +2,10 @@ import type { OwnedName } from "./drivers/types.js";
|
|
|
2
2
|
export declare const BANNER: string;
|
|
3
3
|
/** ANSI-stripped, trailing-space-trimmed twin of BANNER — README hero and other plain surfaces. */
|
|
4
4
|
export declare const PLAIN_BANNER: string;
|
|
5
|
+
/** Two-line run-cockpit lockup, sliced from BANNER so its mark and product name cannot drift. */
|
|
6
|
+
export declare const COMPACT_LOCKUP: string;
|
|
7
|
+
/** ANSI-stripped, trailing-space-trimmed twin of COMPACT_LOCKUP for colourless surfaces. */
|
|
8
|
+
export declare const PLAIN_COMPACT_LOCKUP: string;
|
|
5
9
|
export declare const PANE_IDENTITY_ENV = "TICKMARKR_PANE_IDENTITY";
|
|
6
10
|
/** One-line pane identity through the T1 ownership contract: role · task · attempt · run. */
|
|
7
11
|
export declare function paneIdentityLine(o: OwnedName): string;
|
package/dist/brand.js
CHANGED
|
@@ -12,6 +12,10 @@ export const BANNER = [
|
|
|
12
12
|
].join("\n");
|
|
13
13
|
/** ANSI-stripped, trailing-space-trimmed twin of BANNER — README hero and other plain surfaces. */
|
|
14
14
|
export const PLAIN_BANNER = BANNER.replace(/\x1b\[[0-9;]*m/g, "").replace(/[ \t]+$/gm, "");
|
|
15
|
+
/** Two-line run-cockpit lockup, sliced from BANNER so its mark and product name cannot drift. */
|
|
16
|
+
export const COMPACT_LOCKUP = BANNER.split("\n").slice(1, 3).join("\n");
|
|
17
|
+
/** ANSI-stripped, trailing-space-trimmed twin of COMPACT_LOCKUP for colourless surfaces. */
|
|
18
|
+
export const PLAIN_COMPACT_LOCKUP = COMPACT_LOCKUP.replace(/\x1b\[[0-9;]*m/g, "").replace(/[ \t]+$/gm, "");
|
|
15
19
|
// T5: the pane identity every visible pane announces under the logo. HerdrDriver seeds this env var
|
|
16
20
|
// into each pane shell at slot() time (paneIdentityLine of the pane's T1 owned name); the banner's
|
|
17
21
|
// identity line reads it at pane runtime, so every role — worker/judge/review/consult — wears the
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { type ClaudeAlias } from "../../adapters/claude-code.js";
|
|
1
2
|
import type { WorkerAdapter } from "../../adapters/types.js";
|
|
2
3
|
import { type KimiDoctorTurnResult } from "../../adapters/kimi.js";
|
|
3
4
|
export type DoctorOpts = {
|
|
4
5
|
banner?: boolean;
|
|
5
6
|
kimiTurnProbe?: (cwd: string) => Promise<KimiDoctorTurnResult>;
|
|
7
|
+
resolveClaudeAliasIdentity?: (cwd: string, alias: ClaudeAlias) => string | undefined;
|
|
6
8
|
};
|
|
7
9
|
export declare function doctor(_argv: string[], cwd?: string, adapters?: WorkerAdapter[], opts?: DoctorOpts): Promise<string>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { allAdapters, binaryShadowWarnings, detectCandidateClis, flagDriftWarnings, modelAliasExclusions, modelAliasLine, probeAll, probeModels, readAutoPrefer, servableExclusions, servabilityLine, writeDoctor } from "../../adapters/registry.js";
|
|
4
|
+
import { CLAUDE_ALIAS_IDENTITY_STAMPS, claudeCode, resolveClaudeAliasIdentity } from "../../adapters/claude-code.js";
|
|
4
5
|
import { BANNER, dim, fail, kvRow, legend, ok, rule, statusRow, title } from "../../brand.js";
|
|
5
6
|
import { tickmarkrDir, stateDirName } from "../../graph/graph.js";
|
|
6
7
|
import { declaredModelWindow, hasWindowsConfig, modelLints, suggestOverlay, ttyVisual } from "../../adapters/model-lints.js";
|
|
@@ -120,6 +121,28 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
120
121
|
// v1.65 T3: hardcoded-flag drift — advisory warn rows only. Runs AFTER writeDoctor so the verdicts
|
|
121
122
|
// can never leak into doctor.json, and discoverChannels/routing never read them.
|
|
122
123
|
rows.push(...flagDriftWarnings(adapters, health).map(attentionRow));
|
|
124
|
+
// OBS-145: resolved-identity drift is the same class of display-only doctor warning. Stamps live
|
|
125
|
+
// beside the alias-owning adapter; the comparison runs only for configured floating aliases and
|
|
126
|
+
// never enters health/doctor.json, config, channel discovery, learned profiles, or route().
|
|
127
|
+
const identityResolver = opts.resolveClaudeAliasIdentity
|
|
128
|
+
?? (adapters.includes(claudeCode) ? resolveClaudeAliasIdentity : undefined);
|
|
129
|
+
if (identityResolver && health["claude-code"]?.installed) {
|
|
130
|
+
const configured = cfg.tiers["claude-code"]?.models ?? {};
|
|
131
|
+
for (const [alias, stampedIdentity] of Object.entries(CLAUDE_ALIAS_IDENTITY_STAMPS)) {
|
|
132
|
+
if (!(alias in configured))
|
|
133
|
+
continue;
|
|
134
|
+
let resolvedIdentity;
|
|
135
|
+
try {
|
|
136
|
+
resolvedIdentity = identityResolver(cwd, alias);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
continue; // advisory source failure is unknown, never a doctor failure
|
|
140
|
+
}
|
|
141
|
+
if (resolvedIdentity && resolvedIdentity !== stampedIdentity) {
|
|
142
|
+
rows.push(attentionRow(`resolved-identity drift: claude-code:${alias} resolved to ${resolvedIdentity}, stamped identity ${stampedIdentity} — reclassify per benchmark policy (advisory — routing unchanged)`));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
123
146
|
// MODEL-05/06: print-only drift fragment; advisory, whole-line-commented additions, tickmarkr NEVER applies it.
|
|
124
147
|
// TTY gets a one-line summary + the fragment as a file (the full dump drowned everything else,
|
|
125
148
|
// v1.33.1 onboarding); machine/CI surface keeps the inline dump — layout is pinned by tests.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type DecisionEvent, type DecisionWebhookPost } from "../../drivers/types.js";
|
|
1
2
|
import { type Task, type TaskStatus } from "../../graph/schema.js";
|
|
2
3
|
import { type JournalEvent } from "../../run/journal.js";
|
|
3
4
|
export type StatusOpts = {
|
|
@@ -5,6 +6,8 @@ export type StatusOpts = {
|
|
|
5
6
|
sleep?: (ms: number) => Promise<void>;
|
|
6
7
|
now?: () => number;
|
|
7
8
|
readWorkerOutput?: (taskId: string, attempt: number, runId: string) => Promise<string | undefined>;
|
|
9
|
+
webhookUrl?: string;
|
|
10
|
+
postWebhook?: DecisionWebhookPost;
|
|
8
11
|
};
|
|
9
12
|
export declare const GATE_KEYS: {
|
|
10
13
|
readonly build: "B";
|
|
@@ -15,6 +18,7 @@ export declare const GATE_KEYS: {
|
|
|
15
18
|
readonly acceptance: "A";
|
|
16
19
|
readonly review: "R";
|
|
17
20
|
};
|
|
21
|
+
export declare const decisionEventsFromJournal: (events: JournalEvent[], runId: string, stateDir?: string) => DecisionEvent[];
|
|
18
22
|
export declare const taskBox: (status: TaskStatus) => string;
|
|
19
23
|
export declare const gateBox: (state: "open" | "pass" | "fail" | "skip", unicode: boolean) => string;
|
|
20
24
|
export type GateState = "open" | "pass" | "fail" | "skip";
|