tickmarkr 1.37.0 → 1.40.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 +25 -23
- package/dist/adapters/claude-code.js +4 -4
- package/dist/adapters/codex.js +1 -1
- package/dist/adapters/fake.d.ts +2 -2
- package/dist/adapters/fake.js +29 -11
- package/dist/adapters/grok.js +5 -5
- package/dist/adapters/model-lints.d.ts +4 -4
- package/dist/adapters/model-lints.js +1 -1
- package/dist/adapters/prompt.js +8 -13
- package/dist/adapters/registry.d.ts +8 -8
- package/dist/adapters/registry.js +17 -17
- package/dist/adapters/types.d.ts +4 -4
- package/dist/adapters/types.js +3 -2
- package/dist/brand.d.ts +2 -0
- package/dist/brand.js +17 -0
- package/dist/cli/commands/approve.js +3 -3
- package/dist/cli/commands/compile.js +1 -1
- package/dist/cli/commands/doctor.js +3 -3
- package/dist/cli/commands/init.js +57 -19
- package/dist/cli/commands/plan.js +12 -1
- package/dist/cli/commands/profile.js +6 -6
- package/dist/cli/commands/resume.d.ts +4 -1
- package/dist/cli/commands/resume.js +4 -1
- package/dist/cli/commands/run.d.ts +4 -1
- package/dist/cli/commands/run.js +9 -1
- package/dist/cli/commands/status.js +2 -1
- package/dist/cli/commands/unlock.js +1 -1
- package/dist/cli/commands/version.d.ts +1 -0
- package/dist/cli/commands/version.js +8 -0
- package/dist/cli/index.d.ts +6 -3
- package/dist/cli/index.js +13 -18
- package/dist/compile/gsd.js +1 -1
- package/dist/compile/index.js +9 -3
- package/dist/compile/native.d.ts +2 -0
- package/dist/compile/native.js +12 -4
- package/dist/config/config.d.ts +9 -6
- package/dist/config/config.js +15 -13
- package/dist/drivers/herdr.js +5 -5
- package/dist/drivers/index.d.ts +2 -2
- package/dist/drivers/subprocess.js +12 -5
- package/dist/drivers/types.js +4 -6
- package/dist/gates/acceptance.d.ts +1 -0
- package/dist/gates/acceptance.js +25 -5
- package/dist/gates/baseline.d.ts +2 -2
- package/dist/gates/llm.js +12 -9
- package/dist/gates/review.d.ts +2 -2
- package/dist/gates/review.js +27 -8
- package/dist/gates/run-gates.d.ts +2 -2
- package/dist/gates/run-gates.js +2 -2
- package/dist/graph/graph.d.ts +2 -2
- package/dist/graph/graph.js +4 -16
- package/dist/graph/schema.d.ts +2 -0
- package/dist/graph/schema.js +16 -1
- package/dist/plan/prompt.js +1 -1
- package/dist/plan/scope.d.ts +2 -2
- package/dist/plan/scope.js +5 -5
- package/dist/route/preference.d.ts +4 -4
- package/dist/route/router.d.ts +3 -3
- package/dist/run/consult.d.ts +5 -2
- package/dist/run/consult.js +31 -7
- package/dist/run/daemon.js +31 -18
- package/dist/run/git.d.ts +1 -1
- package/dist/run/git.js +5 -9
- package/dist/run/journal.d.ts +2 -2
- package/dist/run/journal.js +4 -4
- package/dist/run/lock.js +11 -11
- package/dist/run/merge.d.ts +2 -2
- package/dist/run/merge.js +3 -3
- package/dist/run/reconcile.js +1 -1
- package/package.json +2 -2
- package/schema/rungraph.schema.json +4 -0
- package/skills/tickmarkr-auto/SKILL.md +23 -2
- package/skills/tickmarkr-loop/SKILL.md +23 -2
package/dist/config/config.js
CHANGED
|
@@ -9,6 +9,7 @@ const TierEnum = z.enum(TIERS, {
|
|
|
9
9
|
error: (iss) => `Invalid option: expected one of "cheap"|"mid"|"frontier" (got ${JSON.stringify(iss.input)})`,
|
|
10
10
|
});
|
|
11
11
|
export const TIER_RANK = { cheap: 0, mid: 1, frontier: 2 };
|
|
12
|
+
export const DEFAULT_DIFF_CAP = 60_000;
|
|
12
13
|
export const MapEntrySchema = z.object({
|
|
13
14
|
pin: z.object({ via: z.string(), model: z.string() }).optional(),
|
|
14
15
|
tier: TierEnum.optional(),
|
|
@@ -63,7 +64,7 @@ const ShapeGateParticipationSchema = z
|
|
|
63
64
|
});
|
|
64
65
|
}
|
|
65
66
|
});
|
|
66
|
-
export const
|
|
67
|
+
export const TickmarkrConfigSchema = z.object({
|
|
67
68
|
concurrency: z.number().int().positive(),
|
|
68
69
|
driver: z.enum(["auto", "herdr", "subprocess"]),
|
|
69
70
|
integrationBranchPrefix: z
|
|
@@ -86,13 +87,15 @@ export const DrovrConfigSchema = z.object({
|
|
|
86
87
|
halfLifeRuns: z.number().positive().optional(),
|
|
87
88
|
availWeight: z.number().nonnegative().optional(),
|
|
88
89
|
}).optional(),
|
|
90
|
+
// Pre-v1.21 doctor.json lacks per-model verdicts. Keep legacy unknown-is-routable behavior opt-in.
|
|
91
|
+
allowUnverifiedModels: z.boolean(),
|
|
89
92
|
allow: PrefBlockSchema.optional(),
|
|
90
93
|
deny: PrefBlockSchema.optional(),
|
|
91
94
|
}),
|
|
92
95
|
tiers: z.record(z.string(), TierEntrySchema),
|
|
93
96
|
pricing: z.record(z.string(), z.number()),
|
|
94
97
|
// v1.20 REC-02: optional detailed price table for cost estimation. Distinct from `pricing` above
|
|
95
|
-
// (the coarse per-task tier estimate `
|
|
98
|
+
// (the coarse per-task tier estimate `tickmarkr plan` shows) — this one drives the usage/cost report.
|
|
96
99
|
// Absent ⇒ every channel reports "not measurable" (never $0). models keyed by model id (LiteLLM
|
|
97
100
|
// convention); subs keyed by adapter id (subscription plans are per-account, not per-token).
|
|
98
101
|
cost: z
|
|
@@ -105,6 +108,7 @@ export const DrovrConfigSchema = z.object({
|
|
|
105
108
|
build: z.string(),
|
|
106
109
|
test: z.string(),
|
|
107
110
|
lint: z.string(),
|
|
111
|
+
diffCap: z.number().int().positive(),
|
|
108
112
|
byShape: z.partialRecord(z.enum(SHAPES), ShapeGateParticipationSchema).optional(),
|
|
109
113
|
}).partial(),
|
|
110
114
|
scope: z.object({
|
|
@@ -136,7 +140,7 @@ export class ConfigError extends Error {
|
|
|
136
140
|
export const DEFAULT_CONFIG = {
|
|
137
141
|
concurrency: 3,
|
|
138
142
|
driver: "auto",
|
|
139
|
-
integrationBranchPrefix: "
|
|
143
|
+
integrationBranchPrefix: "tickmarkr/",
|
|
140
144
|
taskTimeoutMinutes: 30,
|
|
141
145
|
contextWarnTokens: 170_000, // v1.23 T2: overseer ctx-watch.sh proven threshold
|
|
142
146
|
routing: {
|
|
@@ -154,12 +158,13 @@ export const DEFAULT_CONFIG = {
|
|
|
154
158
|
},
|
|
155
159
|
// ROUTE-14 (2026-07-11, operator-adopted): learned reordering ON by default. Shipped OFF through v1.6/v1.7
|
|
156
160
|
// as a preview-then-adopt trust ramp (ROUTE-09); the operator adopts it here after v1.8 made learning
|
|
157
|
-
// auditable (VIS-05 `
|
|
161
|
+
// auditable (VIS-05 `tickmarkr report` learning section) and matured (decay ROUTE-11, scored utilization
|
|
158
162
|
// ROUTE-12, escalation tiebreak ROUTE-13). SAFE BY CONSTRUCTION: an empty/cold profile ⇒ every
|
|
159
163
|
// learnedScore returns exactly NEUTRAL ⇒ byte-identical v1.5 static routing, so ON only changes routing in
|
|
160
164
|
// a workspace that has accumulated ≥MIN_SAMPLES warm telemetry per cell. Preview any workspace's effect
|
|
161
|
-
// first with `
|
|
165
|
+
// first with `tickmarkr plan` / `tickmarkr report`; flip to "off" to pin exact static routing (the kill switch stands).
|
|
162
166
|
learned: "on",
|
|
167
|
+
allowUnverifiedModels: false,
|
|
163
168
|
},
|
|
164
169
|
// Seed table (spec §13). New models = edit this (or your config.yaml), never code.
|
|
165
170
|
tiers: {
|
|
@@ -185,7 +190,7 @@ export const DEFAULT_CONFIG = {
|
|
|
185
190
|
// promoted from overlay 2026-07-11 (MODEL-10).
|
|
186
191
|
// grok-4.5-fast → cheap: speed-optimized variant, no independent benchmark scores yet → floor tier.
|
|
187
192
|
// RETIRED 2026-07-13: cursor-agent seeds grok-4.5-xhigh / grok-4.5-fast-xhigh — CLI no longer reports
|
|
188
|
-
// either id (
|
|
193
|
+
// either id (tickmarkr plan lint, 2026-07-13). Tombstoned here; re-seed if cursor re-exposes them.
|
|
189
194
|
// (Benchmark provenance above still informs the native grok adapter seeds below.)
|
|
190
195
|
"cursor-agent": {
|
|
191
196
|
vendor: "cursor", channel: "sub",
|
|
@@ -230,7 +235,7 @@ export const DEFAULT_CONFIG = {
|
|
|
230
235
|
},
|
|
231
236
|
},
|
|
232
237
|
pricing: { cheap: 0.1, mid: 0.5, frontier: 2.5 },
|
|
233
|
-
gates: {},
|
|
238
|
+
gates: { diffCap: DEFAULT_DIFF_CAP },
|
|
234
239
|
judge: { adapter: "claude-code", model: "fable" },
|
|
235
240
|
review: { complexityThreshold: 7, required: true },
|
|
236
241
|
consult: { adapter: "claude-code", model: "fable", stallMinutes: 15 },
|
|
@@ -260,12 +265,8 @@ function readYaml(path) {
|
|
|
260
265
|
return parse(readFileSync(path, "utf8"));
|
|
261
266
|
}
|
|
262
267
|
export function globalConfigDir() {
|
|
263
|
-
// read-old/write-new (operator-ordered rename 2026-07-15): prefer ~/.config/tickmarkr; an
|
|
264
|
-
// existing ~/.config/drovr keeps working until the operator moves it. New scaffolds use tickmarkr.
|
|
265
268
|
const base = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
266
|
-
|
|
267
|
-
const legacy = join(base, "drovr");
|
|
268
|
-
return existsSync(join(next, "config.yaml")) || !existsSync(join(legacy, "config.yaml")) ? next : legacy;
|
|
269
|
+
return join(base, "tickmarkr");
|
|
269
270
|
}
|
|
270
271
|
export function overlayPreferShapes(repoRoot, opts = {}) {
|
|
271
272
|
const shapes = new Set();
|
|
@@ -284,7 +285,7 @@ export function loadConfig(repoRoot, opts = {}) {
|
|
|
284
285
|
const globalCfg = readYaml(join(opts.globalDir ?? globalConfigDir(), "config.yaml"));
|
|
285
286
|
const repoCfg = readYaml(join(repoRoot, stateDirName(repoRoot), "config.yaml"));
|
|
286
287
|
const merged = deepMerge(deepMerge(structuredClone(DEFAULT_CONFIG), globalCfg), repoCfg);
|
|
287
|
-
const r =
|
|
288
|
+
const r = TickmarkrConfigSchema.safeParse(merged);
|
|
288
289
|
if (!r.success)
|
|
289
290
|
throw new ConfigError(z.prettifyError(r.error));
|
|
290
291
|
return r.data;
|
|
@@ -320,6 +321,7 @@ export function configTemplate(overlay) {
|
|
|
320
321
|
# scope:
|
|
321
322
|
# allowDeviations: [] # globs an operator permits out-of-scope edits into, e.g. ["package-lock.json"]
|
|
322
323
|
# gates: # override auto-detected commands; per-shape participation may only skip LLM gates
|
|
324
|
+
# diffCap: 60000 # fail closed before an LLM gate; split the task or raise this positive integer
|
|
323
325
|
# test: npm test
|
|
324
326
|
# byShape:
|
|
325
327
|
# docs: { acceptance: false, review: false } # baseline, evidence, and scope are mandatory
|
package/dist/drivers/herdr.js
CHANGED
|
@@ -74,7 +74,7 @@ export class HerdrDriver {
|
|
|
74
74
|
}
|
|
75
75
|
async slot(cwd, name, opts) {
|
|
76
76
|
// T1 ownership contract: `opts.owned` (T2 call sites) names the pane canonically —
|
|
77
|
-
//
|
|
77
|
+
// tickmarkr:<role>:<taskId>:<attempt>:<runId>. Without it, `name` passes through byte-identical
|
|
78
78
|
// (today's legacy daemon/gates/consult shapes) — canonicalizeLegacyName (types.ts) is what lets
|
|
79
79
|
// reconcile.ts and this driver's own renameGroupTab/glyphFor decode role/taskId/attempt from
|
|
80
80
|
// those shapes without a call-site migration; T2 retires this branch by always passing `owned`.
|
|
@@ -92,7 +92,7 @@ export class HerdrDriver {
|
|
|
92
92
|
// named after a dead cursor worker and the operator read it as a mislabeled agent)
|
|
93
93
|
async tabSlot(cwd, name, label = name) {
|
|
94
94
|
// tab-per-slot: concurrent agents in one tab split it into sliver columns — TUIs exit or
|
|
95
|
-
// hard-wrap at COLUMNS≈2, shredding even the
|
|
95
|
+
// hard-wrap at COLUMNS≈2, shredding even the TICKMARKR_RESULT marker (v1.4 phase-1 incident).
|
|
96
96
|
// A dedicated named tab gives every agent a full-width pane; tab close() reaps it.
|
|
97
97
|
// VIS-10 (operator ruling 2026-07-11): "pane placed by focus heuristic" is a DEFECT CLASS.
|
|
98
98
|
// Fail closed at every step — env unset, tab-create non-zero, unparseable stdout, or a parsed
|
|
@@ -100,7 +100,7 @@ export class HerdrDriver {
|
|
|
100
100
|
if (!this.ws)
|
|
101
101
|
throw new Error("herdr placement requires HERDR_WORKSPACE_ID — refusing untargeted pane (VIS-10: fail closed, never place by focus)");
|
|
102
102
|
// pin the tab to the RUN's workspace, UNCONDITIONALLY (inherited via HERDR_WORKSPACE_ID), never the
|
|
103
|
-
// operator's focused one (Intl-Dossier run-20260709-104447 incident: worker tabs opened in the
|
|
103
|
+
// operator's focused one (Intl-Dossier run-20260709-104447 incident: worker tabs opened in the tickmarkr repo workspace)
|
|
104
104
|
const t = await this.herdr(`tab create --label ${shq(label)} --no-focus --workspace ${shq(this.ws)}`);
|
|
105
105
|
if (t.code !== 0)
|
|
106
106
|
throw new Error(`herdr tab create failed (exit ${t.code}, refusing untargeted placement): ${t.stderr || t.stdout}`);
|
|
@@ -139,7 +139,7 @@ export class HerdrDriver {
|
|
|
139
139
|
if (typeof id !== "string" || !id)
|
|
140
140
|
throw new Error(`herdr agent start returned no pane id: ${r.stdout}`);
|
|
141
141
|
// tab create auto-spawns a root shell pane and agent start --tab adds the agent as a SECOND
|
|
142
|
-
// pane — reap the idle shell so no tab shows a dead "
|
|
142
|
+
// pane — reap the idle shell so no tab shows a dead "tickmarkr git:" prompt beside its agent
|
|
143
143
|
// (VIS-04 orphan fix). Best-effort: a failed reap costs cosmetics only.
|
|
144
144
|
if (typeof rootPane === "string" && rootPane && rootPane !== id) {
|
|
145
145
|
await this.herdr(`pane close ${shq(rootPane)}`);
|
|
@@ -444,7 +444,7 @@ export class HerdrDriver {
|
|
|
444
444
|
return s;
|
|
445
445
|
});
|
|
446
446
|
}
|
|
447
|
-
// OBS-17 T2 / v1.22b T1: close every
|
|
447
|
+
// OBS-17 T2 / v1.22b T1: close every tickmarkr-owned pane that should not exist (superseded attempts,
|
|
448
448
|
// killed-daemon orphans, leftovers from OLDER runs) — in this run's workspace OR misplaced in any
|
|
449
449
|
// other one — then reap the tabs those closes emptied. Ownership is decided ONLY by parseOwnedName
|
|
450
450
|
// (drivers/types.ts panesToClose) — foreign names never become candidates, in any workspace; a pane
|
package/dist/drivers/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { TickmarkrConfig } from "../config/config.js";
|
|
2
2
|
import type { ExecutorDriver } from "./types.js";
|
|
3
|
-
export declare function pickDriver(cfg:
|
|
3
|
+
export declare function pickDriver(cfg: TickmarkrConfig, override?: "auto" | "herdr" | "subprocess"): ExecutorDriver;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { createWorktree } from "../run/git.js";
|
|
3
|
-
// HARD-03: cap retained worker output so a chatty worker can't grow
|
|
3
|
+
// HARD-03: cap retained worker output so a chatty worker can't grow tickmarkr's heap unbounded.
|
|
4
4
|
// 2MB is safe against BOTH consumers: the largest read() call site asks for 1000 lines
|
|
5
|
-
// (daemon.ts:217/247), and every marker (
|
|
5
|
+
// (daemon.ts:217/247), and every marker (TICKMARKR_RESULT, TICKMARKR_EXIT_<nonce>) is emitted at the
|
|
6
6
|
// END of output — a trailer would only be evicted if >2MB arrived AFTER it inside one 200ms
|
|
7
7
|
// waitOutput poll (~10MB/s sustained, far beyond agent-CLI rates). Tail-truncate, never head:
|
|
8
8
|
// consumers only ever tail-read.
|
|
@@ -43,7 +43,7 @@ export class SubprocessDriver {
|
|
|
43
43
|
}
|
|
44
44
|
async run(slot, cmd) {
|
|
45
45
|
const s = this.state(slot);
|
|
46
|
-
// HARD-05: interactive=false — no operator, so an open stdin pipe is a promise
|
|
46
|
+
// HARD-05: interactive=false — no operator, so an open stdin pipe is a promise tickmarkr can never
|
|
47
47
|
// keep; codex exec appends a piped stdin as a <stdin> block (`codex exec --help`) and blocks on a
|
|
48
48
|
// read that never EOFs. One spawn site covers every adapter (D-06).
|
|
49
49
|
// v1.22 T3: seal herdr control vars so worker/judge/review/consult children cannot reach the
|
|
@@ -52,6 +52,7 @@ export class SubprocessDriver {
|
|
|
52
52
|
cwd: slot.cwd,
|
|
53
53
|
stdio: ["ignore", "pipe", "pipe"],
|
|
54
54
|
env: sealHerdrEnv(process.env),
|
|
55
|
+
detached: true,
|
|
55
56
|
});
|
|
56
57
|
s.proc = p;
|
|
57
58
|
p.stdout.on("data", (d) => (s.buf = (s.buf + d).slice(-MAX_BUF)));
|
|
@@ -96,8 +97,14 @@ export class SubprocessDriver {
|
|
|
96
97
|
}
|
|
97
98
|
async close(slot) {
|
|
98
99
|
const s = this.slots.get(slot.id);
|
|
99
|
-
if (s?.proc && !s.exited)
|
|
100
|
-
|
|
100
|
+
if (s?.proc && !s.exited) {
|
|
101
|
+
try {
|
|
102
|
+
process.kill(-s.proc.pid, "SIGKILL");
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
s.proc.kill("SIGKILL");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
101
108
|
this.slots.delete(slot.id);
|
|
102
109
|
}
|
|
103
110
|
worktree(repo, branch, baseRef) {
|
package/dist/drivers/types.js
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
// ---- Ownership contract (T1: OBS-17 pane-hygiene) --------------------------------------------
|
|
2
|
-
// Every pane/tab
|
|
3
|
-
//
|
|
2
|
+
// Every pane/tab tickmarkr creates is identified by exactly one parseable token:
|
|
3
|
+
// tickmarkr:<role>:<taskId>:<attempt>:<runId>
|
|
4
4
|
// role ∈ OWNED_ROLES; attempt is a non-negative integer; taskId/runId never contain ":" (task and
|
|
5
5
|
// run ids are alphanumeric/dash by construction elsewhere). formatOwnedName/parseOwnedName round-trip
|
|
6
6
|
// exactly. parseOwnedName (or isForeignName) is the ONLY way reconcile.ts may decide a live pane name
|
|
7
|
-
// is
|
|
7
|
+
// is tickmarkr-owned — anything that doesn't parse is foreign and is never a candidate for closing.
|
|
8
8
|
export const OWNED_ROLES = ["worker", "judge", "review", "consult", "watch", "other"];
|
|
9
|
-
// read-old/write-new: new panes are named tickmarkr:*, but reconcile still recognizes (and may
|
|
10
|
-
// close/reuse) drovr:* panes left by pre-rename runs — resumed old runs keep working.
|
|
11
9
|
const OWNED_PREFIX = "tickmarkr";
|
|
12
|
-
const OWNED_RE = new RegExp(
|
|
10
|
+
const OWNED_RE = new RegExp(`^${OWNED_PREFIX}:(${OWNED_ROLES.join("|")}):([^:]+):(\\d+):([^:]+)$`);
|
|
13
11
|
export function formatOwnedName(o) {
|
|
14
12
|
return `${OWNED_PREFIX}:${o.role}:${o.taskId}:${o.attempt}:${o.runId}`;
|
|
15
13
|
}
|
package/dist/gates/acceptance.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { channelKey, shq } from "../adapters/types.js";
|
|
2
|
+
import { DEFAULT_DIFF_CAP } from "../config/config.js";
|
|
2
3
|
import { renderAcceptanceItem } from "../graph/schema.js";
|
|
3
4
|
import { sh, shOk } from "../run/git.js";
|
|
4
5
|
import { extractJson, runLlm } from "./llm.js";
|
|
5
|
-
const DIFF_CAP = 60_000; // judge sees diff + criteria only, and not unboundedly (spec §12)
|
|
6
6
|
const isCommand = (a) => typeof a === "object" && a.oracle === "command";
|
|
7
7
|
const isTest = (a) => typeof a === "object" && a.oracle === "test";
|
|
8
8
|
const isJudge = (a) => typeof a === "string" || (typeof a === "object" && a.oracle === "judge");
|
|
@@ -57,8 +57,13 @@ export async function acceptanceGate(task, worktree, baseRef, judge, via, opts =
|
|
|
57
57
|
const warn = onlyJudge
|
|
58
58
|
? "WARNING: only judge oracles — no deterministic command/test oracle guards this task (deterministic preferred, spec §2).\n"
|
|
59
59
|
: "";
|
|
60
|
-
const diff =
|
|
61
|
-
const
|
|
60
|
+
const diff = await shOk(`git diff '${baseRef}..HEAD'`, worktree);
|
|
61
|
+
const diffCap = opts.diffCap ?? DEFAULT_DIFF_CAP;
|
|
62
|
+
if (diff.length > diffCap) {
|
|
63
|
+
return { gate: "acceptance", pass: false,
|
|
64
|
+
details: warn + detBlock + `diff exceeds verifiable cap (${diff.length} > ${diffCap}) — split the task, or raise gates.diffCap` };
|
|
65
|
+
}
|
|
66
|
+
const prompt = `TICKMARKR-JUDGE
|
|
62
67
|
You are a strict acceptance judge. Decide whether the diff satisfies EVERY acceptance criterion.
|
|
63
68
|
Judge only what the diff proves — plausible-but-wrong must fail. Do not award partial credit.
|
|
64
69
|
Deterministic command/test oracles have already passed mechanically; judge ONLY the rubric items below.
|
|
@@ -87,6 +92,21 @@ Respond with ONLY this JSON (no prose before or after):
|
|
|
87
92
|
details: warn + detBlock + "judge output unparseable — failing closed",
|
|
88
93
|
meta: { unparseable: true, judge: channelKey({ adapter: judge.adapter.id, model: judge.model }) } };
|
|
89
94
|
}
|
|
90
|
-
const
|
|
91
|
-
|
|
95
|
+
const inconsistencies = [];
|
|
96
|
+
if (v.criteria.length !== judgeItems.length) {
|
|
97
|
+
inconsistencies.push(`judge verdict inconsistent: criteria count mismatch — expected ${judgeItems.length}, received ${v.criteria.length}`);
|
|
98
|
+
}
|
|
99
|
+
v.criteria.forEach((c, i) => {
|
|
100
|
+
if (!c || typeof c !== "object" || c.met !== true) {
|
|
101
|
+
inconsistencies.push(`judge verdict inconsistent: criteria[${i}].met must be true`);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
const pass = v.pass === true && inconsistencies.length === 0;
|
|
105
|
+
const lines = v.criteria.map((c, i) => c && typeof c === "object"
|
|
106
|
+
? `${c.met ? "✓" : "✗"} ${c.criterion}: ${c.reason}`
|
|
107
|
+
: `✗ criteria[${i}]: invalid criterion`);
|
|
108
|
+
if (!v.pass)
|
|
109
|
+
lines.push("judge verdict pass=false");
|
|
110
|
+
lines.push(...inconsistencies);
|
|
111
|
+
return { gate: "acceptance", pass, details: warn + detBlock + (lines.join("\n") || "judge passed") };
|
|
92
112
|
}
|
package/dist/gates/baseline.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { TickmarkrConfig } from "../config/config.js";
|
|
2
2
|
import type { GateResult } from "./types.js";
|
|
3
3
|
export interface Baseline {
|
|
4
4
|
commands: Record<string, {
|
|
@@ -7,6 +7,6 @@ export interface Baseline {
|
|
|
7
7
|
}>;
|
|
8
8
|
}
|
|
9
9
|
export declare function fingerprint(output: string): string[];
|
|
10
|
-
export declare function detectGateCommands(repoRoot: string, cfg:
|
|
10
|
+
export declare function detectGateCommands(repoRoot: string, cfg: TickmarkrConfig): Record<string, string>;
|
|
11
11
|
export declare function captureBaseline(cwd: string, commands: Record<string, string>): Promise<Baseline>;
|
|
12
12
|
export declare function compareToBaseline(cwd: string, commands: Record<string, string>, baseline: Baseline, enabled: string[]): Promise<GateResult[]>;
|
package/dist/gates/llm.js
CHANGED
|
@@ -2,6 +2,7 @@ import { mkdtempSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { formatOwnedName, parseOwnedName } from "../drivers/types.js";
|
|
5
|
+
import { bannerShell } from "../brand.js";
|
|
5
6
|
import { sh } from "../run/git.js";
|
|
6
7
|
export const GATE_PANE_SEP = " · ";
|
|
7
8
|
/** T8: role-first pane name for fleet visibility — judge · T4, review · T3, consult · T2. */
|
|
@@ -12,7 +13,7 @@ export function gatePaneName(role, taskId, suffix = "") {
|
|
|
12
13
|
// via.name remains the fallback for non-gate runViaDriver callers and carries -r1 retry suffixes.
|
|
13
14
|
// T2 ownership contract: a canonical owned fallback (the daemon's nameFor now emits one) passes
|
|
14
15
|
// through untouched; run-gates' "-r1" judge-retry suffix becomes attempt+1 so the retry pane's name
|
|
15
|
-
// stays contract-parseable (
|
|
16
|
+
// stays contract-parseable (tickmarkr:judge:<task>:1:<runId>) instead of a corrupted-runId shape.
|
|
16
17
|
export function rolePaneNameFromPrompt(prompt, fallback) {
|
|
17
18
|
const retry = fallback.endsWith("-r1");
|
|
18
19
|
const base = retry ? fallback.slice(0, -3) : fallback;
|
|
@@ -22,14 +23,14 @@ export function rolePaneNameFromPrompt(prompt, fallback) {
|
|
|
22
23
|
const id = prompt.match(/## Task ([^\n:]+):/)?.[1];
|
|
23
24
|
if (!id)
|
|
24
25
|
return fallback;
|
|
25
|
-
if (prompt.startsWith("
|
|
26
|
+
if (prompt.startsWith("TICKMARKR-JUDGE"))
|
|
26
27
|
return gatePaneName("judge", id, retry ? "-r1" : "");
|
|
27
|
-
if (prompt.startsWith("
|
|
28
|
+
if (prompt.startsWith("TICKMARKR-REVIEW"))
|
|
28
29
|
return gatePaneName("review", id, retry ? "-r1" : "");
|
|
29
30
|
return fallback;
|
|
30
31
|
}
|
|
31
32
|
export async function runHeadless(adapter, model, prompt, cwd, timeoutMs = 300000) {
|
|
32
|
-
const pf = join(mkdtempSync(join(tmpdir(), "
|
|
33
|
+
const pf = join(mkdtempSync(join(tmpdir(), "tickmarkr-llm-")), "prompt.md");
|
|
33
34
|
writeFileSync(pf, prompt);
|
|
34
35
|
const r = await sh(adapter.headlessCommand(pf, model), cwd, timeoutMs);
|
|
35
36
|
return r.stdout + "\n" + r.stderr;
|
|
@@ -37,15 +38,17 @@ export async function runHeadless(adapter, model, prompt, cwd, timeoutMs = 30000
|
|
|
37
38
|
// v1.1 default path: the same headless CLI call, but dispatched through the driver
|
|
38
39
|
// as a visible named agent (herdr pane), with the quote-split completion wrapper.
|
|
39
40
|
export async function runViaDriver(adapter, model, prompt, cwd, via, timeoutMs = 300000) {
|
|
40
|
-
const pf = join(mkdtempSync(join(tmpdir(), "
|
|
41
|
+
const pf = join(mkdtempSync(join(tmpdir(), "tickmarkr-llm-")), "prompt.md");
|
|
41
42
|
writeFileSync(pf, prompt);
|
|
42
43
|
const slot = await via.driver.slot(cwd, rolePaneNameFromPrompt(prompt, via.name), via.label ? { label: via.label } : undefined);
|
|
43
44
|
via.onSlot?.(slot);
|
|
44
|
-
|
|
45
|
-
//
|
|
45
|
+
// brand banner at pane top — gate panes run headless-print (no alt screen), so it stays visible;
|
|
46
|
+
// extractJson is anchored to JSON braces and the exit marker to TICKMARKR_EXIT:\d, neither matches banner text
|
|
47
|
+
await via.driver.run(slot, `${bannerShell()}; ${adapter.headlessCommand(pf, model)}; printf '\\nTICKMARKR_''EXIT:%s\\n' $?`);
|
|
48
|
+
// wait for the digit-suffixed marker (a real $? exit code), never a bare "TICKMARKR_EXIT:" that a
|
|
46
49
|
// self-referential diff under review merely DISPLAYS — same guard the worker path uses (daemon.ts:193).
|
|
47
|
-
// Without it, a judge/review pane echoing
|
|
48
|
-
await via.driver.waitOutput(slot, "
|
|
50
|
+
// Without it, a judge/review pane echoing tickmarkr's own source false-completes before its verdict lands.
|
|
51
|
+
await via.driver.waitOutput(slot, "TICKMARKR_EXIT:\\d", timeoutMs, { regex: true });
|
|
49
52
|
const out = await via.driver.read(slot, 400);
|
|
50
53
|
if (!via.keep)
|
|
51
54
|
await via.driver.close(slot);
|
package/dist/gates/review.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Assignment, type BillingChannel, type WorkerAdapter } from "../adapters/types.js";
|
|
2
|
-
import { type
|
|
2
|
+
import { type TickmarkrConfig } from "../config/config.js";
|
|
3
3
|
import { type Task } from "../graph/schema.js";
|
|
4
4
|
import { type GateVia } from "./llm.js";
|
|
5
5
|
import type { GateResult } from "./types.js";
|
|
@@ -9,4 +9,4 @@ export interface ReviewVerdict {
|
|
|
9
9
|
}
|
|
10
10
|
export declare function modelId(model: string): string;
|
|
11
11
|
export declare function pickReviewer(author: Assignment, channels: BillingChannel[], exclude?: string[]): BillingChannel | null;
|
|
12
|
-
export declare function reviewGate(task: Task, worktree: string, baseRef: string, author: Assignment, channels: BillingChannel[], adapters: WorkerAdapter[], cfg:
|
|
12
|
+
export declare function reviewGate(task: Task, worktree: string, baseRef: string, author: Assignment, channels: BillingChannel[], adapters: WorkerAdapter[], cfg: TickmarkrConfig, via?: GateVia, excludeReviewers?: string[]): Promise<GateResult>;
|
package/dist/gates/review.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { channelKey } from "../adapters/types.js";
|
|
2
|
-
import { TIER_RANK } from "../config/config.js";
|
|
2
|
+
import { DEFAULT_DIFF_CAP, TIER_RANK } from "../config/config.js";
|
|
3
3
|
import { renderAcceptanceItem } from "../graph/schema.js";
|
|
4
4
|
import { getAdapter } from "../adapters/registry.js";
|
|
5
5
|
import { shOk } from "../run/git.js";
|
|
@@ -27,7 +27,6 @@ export function pickReviewer(author, channels, exclude = []) {
|
|
|
27
27
|
.filter((c) => c.vendor !== authorChannel.vendor && modelId(c.model) !== modelId(author.model) && !exclude.includes(channelKey(c)))
|
|
28
28
|
.sort((a, b) => TIER_RANK[b.tier] - TIER_RANK[a.tier] || marginalCostRank(a) - marginalCostRank(b))[0] ?? null);
|
|
29
29
|
}
|
|
30
|
-
const DIFF_CAP = 60_000;
|
|
31
30
|
export async function reviewGate(task, worktree, baseRef, author, channels, adapters, cfg, via, excludeReviewers) {
|
|
32
31
|
if (task.complexity < cfg.review.complexityThreshold) {
|
|
33
32
|
return { gate: "review", pass: true, details: `skipped — complexity ${task.complexity} < threshold ${cfg.review.complexityThreshold}`, meta: { skipped: true } };
|
|
@@ -38,8 +37,13 @@ export async function reviewGate(task, worktree, baseRef, author, channels, adap
|
|
|
38
37
|
? { gate: "review", pass: false, details: "no cross-vendor reviewer available (diversity rule); set review.required:false to waive" }
|
|
39
38
|
: { gate: "review", pass: true, details: "WARNING: no cross-vendor reviewer available — review waived by config" };
|
|
40
39
|
}
|
|
41
|
-
const diff =
|
|
42
|
-
const
|
|
40
|
+
const diff = await shOk(`git diff '${baseRef}..HEAD'`, worktree);
|
|
41
|
+
const diffCap = cfg.gates.diffCap ?? DEFAULT_DIFF_CAP;
|
|
42
|
+
if (diff.length > diffCap) {
|
|
43
|
+
return { gate: "review", pass: false,
|
|
44
|
+
details: `diff exceeds verifiable cap (${diff.length} > ${diffCap}) — split the task, or raise gates.diffCap` };
|
|
45
|
+
}
|
|
46
|
+
const prompt = `TICKMARKR-REVIEW
|
|
43
47
|
You are a skeptical cross-vendor code reviewer. Another agent (vendor: ${author.adapter}) authored this diff.
|
|
44
48
|
Look for correctness bugs, security issues, and acceptance-criteria gaps. Approve only if you would merge it.
|
|
45
49
|
|
|
@@ -56,14 +60,14 @@ Respond with ONLY this JSON:
|
|
|
56
60
|
{"approve": true|false, "issues": ["..."]}
|
|
57
61
|
`;
|
|
58
62
|
const raw = await runLlm(getAdapter(reviewer.adapter, adapters), reviewer.model, prompt, worktree, via ? { driver: via.driver, keep: via.keep, onSlot: via.onSlot, name: via.nameFor("review", reviewer.adapter), label: via.labelFor("review") } : undefined,
|
|
59
|
-
// frontier reviewers routinely need >5min on a
|
|
63
|
+
// frontier reviewers routinely need >5min on a configured-cap-sized diff, and `claude -p` buffers all
|
|
60
64
|
// output until completion — runLlm's 300s default killed reviews mid-flight, returning empty
|
|
61
65
|
// stdout that read as "unparseable" and escalated to re-implementation of green code
|
|
62
66
|
// (run-20260709-104447 P87-09). ponytail: literal 15min; make it cfg.review.timeoutMs if a
|
|
63
67
|
// second knob-turner appears.
|
|
64
68
|
900_000);
|
|
65
69
|
const v = extractJson(raw);
|
|
66
|
-
if (!v || typeof v.approve !== "boolean") {
|
|
70
|
+
if (!v || typeof v.approve !== "boolean" || !Array.isArray(v.issues)) {
|
|
67
71
|
return {
|
|
68
72
|
gate: "review",
|
|
69
73
|
pass: false,
|
|
@@ -71,10 +75,25 @@ Respond with ONLY this JSON:
|
|
|
71
75
|
meta: { reviewer: channelKey(reviewer) },
|
|
72
76
|
};
|
|
73
77
|
}
|
|
78
|
+
const issues = v.issues;
|
|
79
|
+
const inconsistencies = [];
|
|
80
|
+
issues.forEach((issue, i) => {
|
|
81
|
+
if (typeof issue !== "string")
|
|
82
|
+
inconsistencies.push(`review verdict inconsistent: issues[${i}] must be a string`);
|
|
83
|
+
});
|
|
84
|
+
if (v.approve && issues.length) {
|
|
85
|
+
inconsistencies.push("review verdict inconsistent: approve=true requires issues to be empty");
|
|
86
|
+
}
|
|
87
|
+
else if (!v.approve && !issues.length) {
|
|
88
|
+
inconsistencies.push("review verdict inconsistent: approve=false requires at least one issue");
|
|
89
|
+
}
|
|
90
|
+
const pass = v.approve === true && inconsistencies.length === 0;
|
|
91
|
+
const lines = issues.map((issue) => `- ${typeof issue === "string" ? issue : JSON.stringify(issue)}`);
|
|
92
|
+
lines.push(...inconsistencies);
|
|
74
93
|
return {
|
|
75
94
|
gate: "review",
|
|
76
|
-
pass
|
|
77
|
-
details: `reviewer ${reviewer.adapter}:${reviewer.model} (${reviewer.vendor}): ${v.approve ? "
|
|
95
|
+
pass,
|
|
96
|
+
details: `reviewer ${reviewer.adapter}:${reviewer.model} (${reviewer.vendor}): ${pass ? "approved" : v.approve ? "approval rejected" : "requested changes"}${lines.length ? "\n" + lines.join("\n") : ""}`,
|
|
78
97
|
meta: { reviewer: channelKey(reviewer) },
|
|
79
98
|
};
|
|
80
99
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Assignment, type BillingChannel, type WorkerAdapter, type WorkerResult } from "../adapters/types.js";
|
|
2
|
-
import { type
|
|
2
|
+
import { type TickmarkrConfig } from "../config/config.js";
|
|
3
3
|
import { type GateName, type Task } from "../graph/schema.js";
|
|
4
4
|
import { type Baseline } from "./baseline.js";
|
|
5
5
|
import type { GateVia } from "./llm.js";
|
|
@@ -23,7 +23,7 @@ export interface GateContext {
|
|
|
23
23
|
baseline: Baseline;
|
|
24
24
|
channels: BillingChannel[];
|
|
25
25
|
adapters: WorkerAdapter[];
|
|
26
|
-
cfg:
|
|
26
|
+
cfg: TickmarkrConfig;
|
|
27
27
|
via?: GateVia;
|
|
28
28
|
excludeReviewers?: string[];
|
|
29
29
|
onGate?: (e: GateEvent) => void | Promise<void>;
|
package/dist/gates/run-gates.js
CHANGED
|
@@ -63,7 +63,7 @@ export async function runGates(task, ctx) {
|
|
|
63
63
|
: undefined;
|
|
64
64
|
// v1.19 (T2): testCmd threads the detected test runner to the gate so named-test oracles run
|
|
65
65
|
// deterministically (filtered via -t) before any LLM judge dispatch.
|
|
66
|
-
let a = await acceptanceGate(task, ctx.worktree, ctx.baseRef, { adapter: judgeAdapter, model: ctx.cfg.judge.model }, jvia, { testCmd: ctx.commands.test });
|
|
66
|
+
let a = await acceptanceGate(task, ctx.worktree, ctx.baseRef, { adapter: judgeAdapter, model: ctx.cfg.judge.model }, jvia, { testCmd: ctx.commands.test, diffCap: ctx.cfg.gates.diffCap });
|
|
67
67
|
// GATE-09: an unparseable judge verdict retries the JUDGE exactly once on a failover channel — never
|
|
68
68
|
// the worker (run-20260711-185020 P43-03 L70-72 billed a judge flake as a worker attempt). The flaked
|
|
69
69
|
// first verdict NEVER enters results (no false gate-result journal event, no operator notify, no stale
|
|
@@ -89,7 +89,7 @@ export async function runGates(task, ctx) {
|
|
|
89
89
|
? { driver: ctx.via.driver, keep: ctx.via.keep, onSlot: ctx.via.onSlot, name: ctx.via.nameFor("judge", retryAdapter.id) + "-r1", label: ctx.via.labelFor("judge") }
|
|
90
90
|
: undefined;
|
|
91
91
|
// the retry IS a second acceptanceGate call: one code path, one parser, zero new parse leniency.
|
|
92
|
-
a = await acceptanceGate(task, ctx.worktree, ctx.baseRef, { adapter: retryAdapter, model: retry.model }, retryJvia, { testCmd: ctx.commands.test });
|
|
92
|
+
a = await acceptanceGate(task, ctx.worktree, ctx.baseRef, { adapter: retryAdapter, model: retry.model }, retryJvia, { testCmd: ctx.commands.test, diffCap: ctx.cfg.gates.diffCap });
|
|
93
93
|
a = { ...a, meta: { ...a.meta, judgeRetry: { flaked: flakedKey, retried: channelKey({ adapter: retry.adapter, model: retry.model }) } } };
|
|
94
94
|
}
|
|
95
95
|
await record(a);
|
package/dist/graph/graph.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type RunGraph, type Task, type TaskStatus } from "./schema.js";
|
|
2
|
-
export declare function stateDirName(
|
|
2
|
+
export declare function stateDirName(_repoRoot: string): string;
|
|
3
3
|
export declare function graphPath(repoRoot: string): string;
|
|
4
|
-
export declare function
|
|
4
|
+
export declare function tickmarkrDir(repoRoot: string): string;
|
|
5
5
|
export declare function loadGraph(repoRoot: string): RunGraph;
|
|
6
6
|
export declare function saveGraph(repoRoot: string, g: RunGraph): void;
|
|
7
7
|
export declare function getTask(g: RunGraph, id: string): Task;
|
package/dist/graph/graph.js
CHANGED
|
@@ -1,25 +1,13 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { validateGraph } from "./schema.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const cached = stateDirs.get(repoRoot);
|
|
7
|
-
if (cached)
|
|
8
|
-
return cached;
|
|
9
|
-
const name = existsSync(join(repoRoot, ".tickmarkr"))
|
|
10
|
-
? ".tickmarkr"
|
|
11
|
-
: existsSync(join(repoRoot, ".drovr"))
|
|
12
|
-
? ".drovr"
|
|
13
|
-
: existsSync(join(repoRoot, ".drover"))
|
|
14
|
-
? ".drover"
|
|
15
|
-
: ".tickmarkr";
|
|
16
|
-
stateDirs.set(repoRoot, name);
|
|
17
|
-
return name;
|
|
4
|
+
export function stateDirName(_repoRoot) {
|
|
5
|
+
return ".tickmarkr";
|
|
18
6
|
}
|
|
19
7
|
export function graphPath(repoRoot) {
|
|
20
8
|
return join(repoRoot, stateDirName(repoRoot), "graph.json");
|
|
21
9
|
}
|
|
22
|
-
export function
|
|
10
|
+
export function tickmarkrDir(repoRoot) {
|
|
23
11
|
const dir = join(repoRoot, stateDirName(repoRoot));
|
|
24
12
|
mkdirSync(dir, { recursive: true });
|
|
25
13
|
const gi = join(dir, ".gitignore");
|
|
@@ -34,7 +22,7 @@ export function loadGraph(repoRoot) {
|
|
|
34
22
|
return validateGraph(JSON.parse(readFileSync(p, "utf8")));
|
|
35
23
|
}
|
|
36
24
|
export function saveGraph(repoRoot, g) {
|
|
37
|
-
|
|
25
|
+
tickmarkrDir(repoRoot);
|
|
38
26
|
const p = graphPath(repoRoot);
|
|
39
27
|
// Temp file MUST be a sibling of graph.json: rename(2) is atomic only within one filesystem
|
|
40
28
|
// (never os.tmpdir()). pid-suffix so a racing writer can't clobber our in-flight temp (HARD-04).
|
package/dist/graph/schema.d.ts
CHANGED
|
@@ -77,6 +77,7 @@ export declare const TaskSchema: z.ZodObject<{
|
|
|
77
77
|
escalate: z.ZodOptional<z.ZodBoolean>;
|
|
78
78
|
}, z.core.$strip>>;
|
|
79
79
|
humanGate: z.ZodDefault<z.ZodBoolean>;
|
|
80
|
+
timeoutMinutes: z.ZodOptional<z.ZodNumber>;
|
|
80
81
|
status: z.ZodDefault<z.ZodEnum<{
|
|
81
82
|
pending: "pending";
|
|
82
83
|
running: "running";
|
|
@@ -161,6 +162,7 @@ export declare const RunGraphSchema: z.ZodObject<{
|
|
|
161
162
|
escalate: z.ZodOptional<z.ZodBoolean>;
|
|
162
163
|
}, z.core.$strip>>;
|
|
163
164
|
humanGate: z.ZodDefault<z.ZodBoolean>;
|
|
165
|
+
timeoutMinutes: z.ZodOptional<z.ZodNumber>;
|
|
164
166
|
status: z.ZodDefault<z.ZodEnum<{
|
|
165
167
|
pending: "pending";
|
|
166
168
|
running: "running";
|
package/dist/graph/schema.js
CHANGED
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
export const SHAPES = ["plan", "spec", "implement", "tests", "docs", "migration", "ui", "refactor", "chore"];
|
|
3
3
|
export const STATUSES = ["pending", "running", "gated", "failed", "done", "human"];
|
|
4
4
|
export const GATE_NAMES = ["build", "test", "lint", "evidence", "scope", "acceptance", "review"];
|
|
5
|
+
const MANDATORY_GATES = ["build", "test", "lint", "evidence", "scope"];
|
|
5
6
|
export const TIERS = ["cheap", "mid", "frontier"];
|
|
6
7
|
// v1.19 acceptance oracles: command (exit code), test (named test), judge (LLM, free-text rubric).
|
|
7
8
|
// A plain string is the read-old/write-new compat form — semantically a judge oracle (spec §2).
|
|
@@ -51,7 +52,19 @@ export const TaskSchema = z.object({
|
|
|
51
52
|
reason: z.string().optional(),
|
|
52
53
|
}))
|
|
53
54
|
.optional(),
|
|
54
|
-
gates: z
|
|
55
|
+
gates: z
|
|
56
|
+
.array(z.enum(GATE_NAMES))
|
|
57
|
+
.default(["build", "test", "lint", "evidence", "scope", "acceptance", "review"])
|
|
58
|
+
.superRefine((gates, ctx) => {
|
|
59
|
+
for (const gate of MANDATORY_GATES) {
|
|
60
|
+
if (!gates.includes(gate)) {
|
|
61
|
+
ctx.addIssue({
|
|
62
|
+
code: "custom",
|
|
63
|
+
message: `${gate} is a mandatory fail-closed gate invariant and cannot be omitted from task gates`,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}),
|
|
55
68
|
routingHints: z
|
|
56
69
|
.object({
|
|
57
70
|
pin: z.object({ via: z.string(), model: z.string() }).optional(),
|
|
@@ -61,6 +74,8 @@ export const TaskSchema = z.object({
|
|
|
61
74
|
})
|
|
62
75
|
.optional(),
|
|
63
76
|
humanGate: z.boolean().default(false),
|
|
77
|
+
// v1.39 OBS-37b: per-task worker window override; absent ⇒ config taskTimeoutMinutes.
|
|
78
|
+
timeoutMinutes: z.number().positive().optional(),
|
|
64
79
|
status: z.enum(STATUSES).default("pending"),
|
|
65
80
|
evidence: z
|
|
66
81
|
.object({
|
package/dist/plan/prompt.js
CHANGED
package/dist/plan/scope.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { WorkerAdapter } from "../adapters/types.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { TickmarkrConfig } from "../config/config.js";
|
|
3
3
|
import type { ExecutorDriver } from "../drivers/types.js";
|
|
4
4
|
export declare function clarificationGate(intent: string): string[];
|
|
5
5
|
export interface ScopeOptions {
|
|
6
|
-
cfg:
|
|
6
|
+
cfg: TickmarkrConfig;
|
|
7
7
|
adapters: WorkerAdapter[];
|
|
8
8
|
driver?: ExecutorDriver;
|
|
9
9
|
force?: boolean;
|