tickmarkr 1.45.0 → 1.47.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/fake.js +2 -2
- package/dist/adapters/model-lints.d.ts +19 -0
- package/dist/adapters/model-lints.js +74 -1
- package/dist/adapters/registry.js +12 -3
- package/dist/brand.js +2 -1
- package/dist/cli/commands/approve.js +6 -7
- package/dist/cli/commands/doctor.js +6 -2
- package/dist/cli/commands/fleet.d.ts +5 -0
- package/dist/cli/commands/fleet.js +249 -0
- package/dist/cli/commands/init.js +55 -3
- package/dist/cli/commands/plan.js +7 -1
- package/dist/cli/commands/profile.js +129 -7
- package/dist/cli/commands/run.js +34 -17
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +3 -1
- package/dist/compile/collateral.js +4 -2
- package/dist/compile/native.js +16 -1
- package/dist/config/config.d.ts +42 -0
- package/dist/config/config.js +288 -1
- package/dist/drivers/herdr.d.ts +1 -0
- package/dist/drivers/herdr.js +12 -2
- package/dist/gates/acceptance.js +7 -2
- package/dist/gates/scope.d.ts +2 -1
- package/dist/gates/scope.js +13 -3
- package/dist/plan/prompt.js +1 -1
- package/dist/route/profile.d.ts +25 -1
- package/dist/route/profile.js +47 -13
- package/dist/route/router.d.ts +10 -3
- package/dist/route/router.js +81 -15
- package/dist/run/consult.d.ts +5 -0
- package/dist/run/consult.js +35 -0
- package/dist/run/daemon.js +145 -25
- package/dist/run/git.d.ts +2 -0
- package/dist/run/git.js +20 -7
- package/dist/run/journal.d.ts +39 -2
- package/dist/run/journal.js +96 -4
- package/dist/run/merge.js +8 -8
- package/package.json +1 -1
package/dist/run/journal.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { type Assignment } from "../adapters/types.js";
|
|
3
3
|
import type { TickmarkrConfig } from "../config/config.js";
|
|
4
4
|
import { type TaskStatus } from "../graph/schema.js";
|
|
5
|
-
import { type RoutingProfile } from "../route/profile.js";
|
|
5
|
+
import { type ProfileDiscount, type RoutingProfile } from "../route/profile.js";
|
|
6
6
|
export interface JournalEvent {
|
|
7
7
|
ts: string;
|
|
8
8
|
event: string;
|
|
@@ -16,10 +16,21 @@ export interface ResumeState {
|
|
|
16
16
|
lastAssignment?: Assignment;
|
|
17
17
|
}
|
|
18
18
|
export declare const ATTEMPT_CAP_RELEASE: "attempt-cap";
|
|
19
|
-
export declare const PARK_KINDS: readonly ["ladder-exhausted", "attempt-cap", "gate-fail", "quota", "reroute-exhausted", "setup", "stall", "merge-conflict"];
|
|
19
|
+
export declare const PARK_KINDS: readonly ["human-gate", "ladder-exhausted", "attempt-cap", "gate-fail", "quota", "reroute-exhausted", "setup", "stall", "merge-conflict", "tip-moved"];
|
|
20
20
|
export type ParkKind = (typeof PARK_KINDS)[number];
|
|
21
21
|
export declare const RETRY_MODES: readonly ["resume", "fresh"];
|
|
22
22
|
export type RetryMode = (typeof RETRY_MODES)[number];
|
|
23
|
+
export declare const WORKER_RESULT_CAUSES: readonly ["provider-death", "stall-timeout", "malformed-trailer", "clean-exit-no-trailer"];
|
|
24
|
+
export type WorkerResultCause = (typeof WORKER_RESULT_CAUSES)[number];
|
|
25
|
+
/** OBS-53: classify worker-result failures so retries and routing see the true signal, not one lumped bucket. */
|
|
26
|
+
export declare function classifyWorkerResultCause(opts: {
|
|
27
|
+
output: string;
|
|
28
|
+
ok: boolean;
|
|
29
|
+
finished: boolean;
|
|
30
|
+
exitCode: number | null;
|
|
31
|
+
summary: string;
|
|
32
|
+
timedOut: boolean;
|
|
33
|
+
}): WorkerResultCause | undefined;
|
|
23
34
|
export declare const TelemetryRowSchema: z.ZodObject<{
|
|
24
35
|
taskId: z.ZodString;
|
|
25
36
|
shape: z.ZodString;
|
|
@@ -42,9 +53,11 @@ export declare const TelemetryRowSchema: z.ZodObject<{
|
|
|
42
53
|
"attempt-cap": "attempt-cap";
|
|
43
54
|
"gate-fail": "gate-fail";
|
|
44
55
|
quota: "quota";
|
|
56
|
+
"human-gate": "human-gate";
|
|
45
57
|
"reroute-exhausted": "reroute-exhausted";
|
|
46
58
|
stall: "stall";
|
|
47
59
|
"merge-conflict": "merge-conflict";
|
|
60
|
+
"tip-moved": "tip-moved";
|
|
48
61
|
}>>;
|
|
49
62
|
tokens: z.ZodCatch<z.ZodOptional<z.ZodObject<{
|
|
50
63
|
input: z.ZodNumber;
|
|
@@ -60,8 +73,29 @@ export declare const TelemetryRowSchema: z.ZodObject<{
|
|
|
60
73
|
resume: "resume";
|
|
61
74
|
fresh: "fresh";
|
|
62
75
|
}>>;
|
|
76
|
+
signalQuality: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<0>, z.ZodLiteral<0.25>, z.ZodLiteral<0.5>, z.ZodLiteral<0.75>, z.ZodLiteral<1>]>>;
|
|
77
|
+
signalBasis: z.ZodOptional<z.ZodEnum<{
|
|
78
|
+
proved: "proved";
|
|
79
|
+
"review-agree": "review-agree";
|
|
80
|
+
"judge-only": "judge-only";
|
|
81
|
+
legacy: "legacy";
|
|
82
|
+
vacuous: "vacuous";
|
|
83
|
+
skipped: "skipped";
|
|
84
|
+
}>>;
|
|
63
85
|
}, z.core.$strip>;
|
|
64
86
|
export type TelemetryRow = z.infer<typeof TelemetryRowSchema>;
|
|
87
|
+
export declare const SIGNAL_BASIS: readonly ["proved", "review-agree", "judge-only", "legacy", "vacuous", "skipped"];
|
|
88
|
+
export type SignalBasis = (typeof SIGNAL_BASIS)[number];
|
|
89
|
+
export declare const SIGNAL_QUALITY: Record<SignalBasis, 0 | 0.25 | 0.5 | 0.75 | 1>;
|
|
90
|
+
export declare function signalQualityFromBasis(basis: SignalBasis): 0 | 0.25 | 0.5 | 0.75 | 1;
|
|
91
|
+
export declare function deriveSignalBasis(gate: string, pass: boolean, details: string, meta?: Record<string, unknown>): SignalBasis;
|
|
92
|
+
export declare function gateResultJournalData(gate: string, pass: boolean, details: string, meta?: Record<string, unknown>): {
|
|
93
|
+
gate: string;
|
|
94
|
+
pass: boolean;
|
|
95
|
+
details: string;
|
|
96
|
+
signalBasis: SignalBasis;
|
|
97
|
+
signalQuality: number;
|
|
98
|
+
} & Record<string, unknown>;
|
|
65
99
|
export declare function recordedGraphDefinitionHash(events: JournalEvent[]): string | undefined;
|
|
66
100
|
export type EngagementCompare = {
|
|
67
101
|
comparable: true;
|
|
@@ -84,6 +118,9 @@ export declare function readAllTelemetry(repoRoot: string, lastK: number, opts?:
|
|
|
84
118
|
})[];
|
|
85
119
|
export declare const RUNS_WINDOW = 50;
|
|
86
120
|
export declare function readProfileCursor(repoRoot: string): string | undefined;
|
|
121
|
+
export declare function profileDiscountsPath(repoRoot: string): string;
|
|
122
|
+
export declare function readProfileDiscounts(repoRoot: string): ProfileDiscount[];
|
|
123
|
+
export declare function appendProfileDiscount(repoRoot: string, discount: ProfileDiscount): void;
|
|
87
124
|
export declare function loadRoutingProfile(repoRoot: string, cfg: TickmarkrConfig, opts?: {
|
|
88
125
|
preview?: boolean;
|
|
89
126
|
}): RoutingProfile | undefined;
|
package/dist/run/journal.js
CHANGED
|
@@ -2,7 +2,7 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync } from
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { channelKey, TokenUsageSchema } from "../adapters/types.js";
|
|
5
|
-
import { stateDirName } from "../graph/graph.js";
|
|
5
|
+
import { stateDirName, tickmarkrDir } from "../graph/graph.js";
|
|
6
6
|
import { TIERS } from "../graph/schema.js";
|
|
7
7
|
import { buildProfile } from "../route/profile.js";
|
|
8
8
|
export function formatJournalNarration({ event, taskId, data }) {
|
|
@@ -38,9 +38,28 @@ const DispatchAssignmentSchema = z.object({
|
|
|
38
38
|
channel: z.enum(["sub", "api"]),
|
|
39
39
|
tier: z.enum(TIERS),
|
|
40
40
|
});
|
|
41
|
-
export const PARK_KINDS = ["ladder-exhausted", "attempt-cap", "gate-fail", "quota",
|
|
42
|
-
"reroute-exhausted", "setup", "stall", "merge-conflict"];
|
|
41
|
+
export const PARK_KINDS = ["human-gate", "ladder-exhausted", "attempt-cap", "gate-fail", "quota",
|
|
42
|
+
"reroute-exhausted", "setup", "stall", "merge-conflict", "tip-moved"];
|
|
43
43
|
export const RETRY_MODES = ["resume", "fresh"];
|
|
44
|
+
export const WORKER_RESULT_CAUSES = ["provider-death", "stall-timeout", "malformed-trailer", "clean-exit-no-trailer"];
|
|
45
|
+
// OBS-53: provider-outage signatures in dead worker output ("Unable to reach the model provider" and kin).
|
|
46
|
+
const PROVIDER_OUTAGE_RE = /Unable to reach the model provider|cannot reach the model provider|model provider.*(?:unavailable|unreachable)/i;
|
|
47
|
+
/** OBS-53: classify worker-result failures so retries and routing see the true signal, not one lumped bucket. */
|
|
48
|
+
export function classifyWorkerResultCause(opts) {
|
|
49
|
+
if (opts.ok && opts.finished)
|
|
50
|
+
return undefined;
|
|
51
|
+
if (PROVIDER_OUTAGE_RE.test(opts.output))
|
|
52
|
+
return "provider-death";
|
|
53
|
+
if (opts.summary === "unparseable TICKMARKR_RESULT trailer")
|
|
54
|
+
return "malformed-trailer";
|
|
55
|
+
if (!opts.finished && opts.timedOut)
|
|
56
|
+
return "stall-timeout";
|
|
57
|
+
if (!opts.finished && opts.exitCode !== null)
|
|
58
|
+
return "clean-exit-no-trailer";
|
|
59
|
+
if (!opts.finished)
|
|
60
|
+
return "stall-timeout";
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
44
63
|
export const TelemetryRowSchema = z.object({
|
|
45
64
|
// v1.5 core — required; every old row has all eight (daemon.ts writes all eight)
|
|
46
65
|
taskId: z.string(), shape: z.string(), adapter: z.string(), model: z.string(), channel: z.string(),
|
|
@@ -71,7 +90,47 @@ export const TelemetryRowSchema = z.object({
|
|
|
71
90
|
overrun: z.literal(true).optional(),
|
|
72
91
|
// v1.29 additive: mode of the attempt represented by this row. Absent on old telemetry.
|
|
73
92
|
retryMode: z.enum(RETRY_MODES).optional(),
|
|
93
|
+
// v1.46 additive (T5): gate-signal quality for routing hygiene — OPTIONAL, absent = legacy (0.25 at fold).
|
|
94
|
+
signalQuality: z.union([z.literal(0), z.literal(0.25), z.literal(0.5), z.literal(0.75), z.literal(1)]).optional(),
|
|
95
|
+
signalBasis: z.enum(["proved", "review-agree", "judge-only", "legacy", "vacuous", "skipped"]).optional(),
|
|
74
96
|
});
|
|
97
|
+
// v1.46 T5 (Sol signal telemetry): gate-result rows carry explicit signalQuality so future defect windows
|
|
98
|
+
// are identifiable without forensics. Basis is the provenance claim; quality is the dyadic h-fold weight.
|
|
99
|
+
export const SIGNAL_BASIS = ["proved", "review-agree", "judge-only", "legacy", "vacuous", "skipped"];
|
|
100
|
+
export const SIGNAL_QUALITY = {
|
|
101
|
+
proved: 1,
|
|
102
|
+
"review-agree": 0.75,
|
|
103
|
+
"judge-only": 0.5,
|
|
104
|
+
legacy: 0.25,
|
|
105
|
+
vacuous: 0,
|
|
106
|
+
skipped: 0,
|
|
107
|
+
};
|
|
108
|
+
export function signalQualityFromBasis(basis) {
|
|
109
|
+
return SIGNAL_QUALITY[basis];
|
|
110
|
+
}
|
|
111
|
+
export function deriveSignalBasis(gate, pass, details, meta = {}) {
|
|
112
|
+
if (meta.skipped === true)
|
|
113
|
+
return "skipped";
|
|
114
|
+
if (meta.unparseable === true)
|
|
115
|
+
return "vacuous";
|
|
116
|
+
if (gate === "acceptance")
|
|
117
|
+
return "judge-only";
|
|
118
|
+
if (gate === "review")
|
|
119
|
+
return pass ? "review-agree" : "vacuous";
|
|
120
|
+
if (gate === "test" || gate === "build") {
|
|
121
|
+
if (!pass)
|
|
122
|
+
return "vacuous";
|
|
123
|
+
if (/no test files|0 tests|tests\s+0\s/i.test(details))
|
|
124
|
+
return "vacuous";
|
|
125
|
+
return "proved";
|
|
126
|
+
}
|
|
127
|
+
return pass ? "proved" : "vacuous";
|
|
128
|
+
}
|
|
129
|
+
// The canonical gate-result journal payload — daemon should spread this into append("gate-result", …).
|
|
130
|
+
export function gateResultJournalData(gate, pass, details, meta = {}) {
|
|
131
|
+
const signalBasis = deriveSignalBasis(gate, pass, details, meta);
|
|
132
|
+
return { gate, pass, details, ...meta, signalBasis, signalQuality: signalQualityFromBasis(signalBasis) };
|
|
133
|
+
}
|
|
75
134
|
// T3 (Sol #2 / Fable F2): one canonical engagement identity, shared by status AND resume. The run-start
|
|
76
135
|
// event records graphDefinitionHash (over compiled task definitions only — see graph.graphDefinitionHash);
|
|
77
136
|
// this is the single field both consumers read, and the single comparator below is the single place the
|
|
@@ -168,6 +227,38 @@ export function readProfileCursor(repoRoot) {
|
|
|
168
227
|
return undefined;
|
|
169
228
|
return readFileSync(path, "utf8").trim() || undefined;
|
|
170
229
|
}
|
|
230
|
+
// v1.46 T5 evidence hygiene state — one line per mark: `<runId> [<taskId>] <weight> # <reason>`.
|
|
231
|
+
// Follows the profile-since precedent: state file in .tickmarkr/, never config, never git.
|
|
232
|
+
const PROFILE_DISCOUNTS_RE = /^(run-[A-Za-z0-9][A-Za-z0-9_-]*)(?:\s+(\S+))?\s+(0|0\.5)\s+#\s+(.+)$/;
|
|
233
|
+
export function profileDiscountsPath(repoRoot) {
|
|
234
|
+
return join(repoRoot, stateDirName(repoRoot), "profile-discounts");
|
|
235
|
+
}
|
|
236
|
+
export function readProfileDiscounts(repoRoot) {
|
|
237
|
+
const path = profileDiscountsPath(repoRoot);
|
|
238
|
+
if (!existsSync(path))
|
|
239
|
+
return [];
|
|
240
|
+
const out = [];
|
|
241
|
+
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
242
|
+
const trimmed = line.trim();
|
|
243
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
244
|
+
continue;
|
|
245
|
+
const m = PROFILE_DISCOUNTS_RE.exec(trimmed);
|
|
246
|
+
if (!m)
|
|
247
|
+
continue;
|
|
248
|
+
out.push({
|
|
249
|
+
runId: m[1],
|
|
250
|
+
...(m[2] ? { taskId: m[2] } : {}),
|
|
251
|
+
weight: m[3] === "0" ? 0 : 0.5,
|
|
252
|
+
reason: m[4].trim(),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
export function appendProfileDiscount(repoRoot, discount) {
|
|
258
|
+
tickmarkrDir(repoRoot);
|
|
259
|
+
const line = `${discount.runId}${discount.taskId ? ` ${discount.taskId}` : ""} ${discount.weight} # ${discount.reason}\n`;
|
|
260
|
+
appendFileSync(profileDiscountsPath(repoRoot), line);
|
|
261
|
+
}
|
|
171
262
|
// The one shared profile builder (criterion 4: plan and daemon share ONE code path).
|
|
172
263
|
// preview:true bypasses the routing.learned:off short-circuit so `tickmarkr plan` can render the
|
|
173
264
|
// trust-ramp preview while the daemon (no preview) stays inert (VALIDATION 13-01-11).
|
|
@@ -176,7 +267,8 @@ export function loadRoutingProfile(repoRoot, cfg, opts = {}) {
|
|
|
176
267
|
return undefined; // never built, never passed
|
|
177
268
|
const rows = readAllTelemetry(repoRoot, RUNS_WINDOW, { after: readProfileCursor(repoRoot) });
|
|
178
269
|
// ROUTE-15: halfLifeRuns threads from config as a pure param; undefined ⇒ module default (byte-identical).
|
|
179
|
-
|
|
270
|
+
const discounts = readProfileDiscounts(repoRoot);
|
|
271
|
+
return rows.length ? buildProfile(rows, { halfLifeRuns: cfg.routing.learnedTuning?.halfLifeRuns, discounts }) : undefined; // cold ⇒ undefined ⇒ v1.5 dead-code path
|
|
180
272
|
}
|
|
181
273
|
export class Journal {
|
|
182
274
|
dir;
|
package/dist/run/merge.js
CHANGED
|
@@ -3,7 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { shq } from "../adapters/types.js";
|
|
4
4
|
import { fingerprint } from "../gates/baseline.js";
|
|
5
5
|
import { tickmarkrDir } from "../graph/graph.js";
|
|
6
|
-
import { gitHead, linkNodeModules, resolveIntegrationBranch, sh,
|
|
6
|
+
import { gitHead, linkNodeModules, resolveIntegrationBranch, sh, shGit, shGitOk, WORKTREES_DIR } from "./git.js";
|
|
7
7
|
export function integrationBranch(cfg, runId) {
|
|
8
8
|
return `${cfg.integrationBranchPrefix}${runId}`;
|
|
9
9
|
}
|
|
@@ -12,12 +12,12 @@ export async function ensureIntegration(repo, branch, baseRef) {
|
|
|
12
12
|
branch = await resolveIntegrationBranch(repo, branch);
|
|
13
13
|
const dir = join(tickmarkrDir(repo), WORKTREES_DIR, sanitize(branch));
|
|
14
14
|
if (!existsSync(join(dir, ".git"))) {
|
|
15
|
-
const exists = (await
|
|
15
|
+
const exists = (await shGit(`git rev-parse --verify refs/heads/${shq(branch)}`, repo)).code === 0;
|
|
16
16
|
if (exists) {
|
|
17
|
-
await
|
|
17
|
+
await shGitOk(`git worktree add ${shq(dir)} ${shq(branch)}`, repo);
|
|
18
18
|
}
|
|
19
19
|
else {
|
|
20
|
-
await
|
|
20
|
+
await shGitOk(`git worktree add -b ${shq(branch)} ${shq(dir)} ${shq(baseRef)}`, repo);
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
linkNodeModules(repo, dir);
|
|
@@ -27,21 +27,21 @@ export function integrationHead(intWt) {
|
|
|
27
27
|
return gitHead(intWt);
|
|
28
28
|
}
|
|
29
29
|
export async function mergeTask(intWt, taskBranch, message, gatedCommit) {
|
|
30
|
-
const tip = await
|
|
30
|
+
const tip = await shGit(`git rev-parse --verify ${shq(`refs/heads/${taskBranch}`)}`, intWt);
|
|
31
31
|
if (tip.code !== 0)
|
|
32
32
|
return { ok: false, conflict: tip.stderr || tip.stdout };
|
|
33
33
|
const branchTip = tip.stdout.trim();
|
|
34
34
|
if (branchTip !== gatedCommit)
|
|
35
35
|
return { ok: false, tipMoved: { gatedCommit, branchTip } };
|
|
36
36
|
// Merge the verified hash, not the mutable branch name: a move after the comparison cannot land ungated content.
|
|
37
|
-
const r = await
|
|
37
|
+
const r = await shGit(`git merge --no-ff ${shq(gatedCommit)} -m ${shq(message)}`, intWt);
|
|
38
38
|
if (r.code === 0)
|
|
39
39
|
return { ok: true };
|
|
40
|
-
const conflict = (await
|
|
40
|
+
const conflict = (await shGit("git status --porcelain", intWt)).stdout
|
|
41
41
|
.split("\n")
|
|
42
42
|
.filter((l) => l.startsWith("UU") || l.startsWith("AA"))
|
|
43
43
|
.join("\n") || r.stderr || r.stdout;
|
|
44
|
-
await
|
|
44
|
+
await shGit("git merge --abort", intWt);
|
|
45
45
|
return { ok: false, conflict };
|
|
46
46
|
}
|
|
47
47
|
// OBS-34: strict exit-code verify on the integration tip — no baseline forgiveness.
|