tickmarkr 1.85.0 → 1.86.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 +4 -2
- package/dist/adapters/catalog-remote.d.ts +64 -0
- package/dist/adapters/catalog-remote.js +287 -0
- package/dist/adapters/catalog.d.ts +96 -0
- package/dist/adapters/catalog.js +176 -0
- package/dist/adapters/claude-code.js +5 -3
- package/dist/adapters/fake.js +33 -4
- package/dist/adapters/model-lints.d.ts +25 -5
- package/dist/adapters/model-lints.js +184 -50
- package/dist/adapters/model-windows.d.ts +31 -0
- package/dist/adapters/model-windows.js +69 -0
- package/dist/adapters/prompt.d.ts +5 -1
- package/dist/adapters/prompt.js +13 -4
- package/dist/adapters/registry.d.ts +25 -26
- package/dist/adapters/registry.js +173 -110
- package/dist/adapters/types.js +15 -3
- package/dist/brand.d.ts +5 -1
- package/dist/brand.js +18 -2
- package/dist/cli/commands/doctor.d.ts +3 -0
- package/dist/cli/commands/doctor.js +43 -21
- package/dist/cli/commands/fleet.d.ts +7 -0
- package/dist/cli/commands/fleet.js +94 -74
- package/dist/cli/commands/init.js +118 -5
- package/dist/cli/commands/status.js +44 -18
- package/dist/compile/gsd.d.ts +2 -1
- package/dist/compile/gsd.js +68 -2
- package/dist/compile/native.d.ts +14 -0
- package/dist/compile/native.js +161 -12
- package/dist/config/config.d.ts +20 -5
- package/dist/config/config.js +96 -64
- package/dist/config/fleet-overlay.d.ts +25 -20
- package/dist/config/fleet-overlay.js +195 -77
- package/dist/config/fleet-why.d.ts +23 -0
- package/dist/config/fleet-why.js +42 -0
- package/dist/drivers/herdr.d.ts +1 -0
- package/dist/drivers/herdr.js +70 -19
- package/dist/gates/acceptance.js +7 -2
- package/dist/gates/llm.d.ts +0 -1
- package/dist/gates/llm.js +5 -30
- package/dist/gates/review.d.ts +2 -1
- package/dist/gates/review.js +9 -7
- package/dist/gates/verdict-cause.d.ts +4 -0
- package/dist/gates/verdict-cause.js +63 -0
- package/dist/graph/schema.d.ts +6 -0
- package/dist/graph/schema.js +8 -5
- package/dist/route/router.d.ts +0 -5
- package/dist/route/router.js +16 -20
- package/dist/run/consult.d.ts +6 -0
- package/dist/run/consult.js +35 -25
- package/dist/run/daemon.js +71 -15
- package/dist/run/journal.js +87 -7
- package/dist/tui/cockpit/capture.d.ts +12 -0
- package/dist/tui/cockpit/capture.js +37 -1
- package/dist/tui/cockpit/components.js +8 -8
- package/dist/tui/cockpit/theme.d.ts +32 -26
- package/dist/tui/cockpit/theme.js +11 -5
- package/dist/tui/ink/components.d.ts +0 -15
- package/dist/tui/ink/components.js +0 -17
- package/dist/tui/ink/fleet-app.d.ts +4 -1
- package/dist/tui/ink/fleet-app.js +134 -13
- package/fixtures/sample.native.md +1 -1
- package/package.json +1 -1
- package/skills/tickmarkr-overseer/SKILL.md +354 -34
- package/skills/tickmarkr-overseer/scripts/watch-artifacts.sh +70 -0
- package/skills/tickmarkr-overseer/scripts/watch-panes.sh +1 -1
- package/dist/tui/ink/studio-app.d.ts +0 -59
- package/dist/tui/ink/studio-app.js +0 -320
- package/dist/tui/save.d.ts +0 -38
- package/dist/tui/save.js +0 -96
- package/dist/tui/staging.d.ts +0 -29
- package/dist/tui/staging.js +0 -78
package/dist/compile/gsd.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, isAbsolute, join, relative } from "node:path";
|
|
3
3
|
import { parse as parseYaml } from "yaml";
|
|
4
|
-
import { TIERS, validateGraph } from "../graph/schema.js";
|
|
4
|
+
import { AcceptanceItemSchema, TIERS, validateGraph } from "../graph/schema.js";
|
|
5
5
|
import { CompileError, assertWriteScope, inferShape, sha256 } from "./common.js";
|
|
6
6
|
// GSD artifact front-end (spec v1.3): one GSD *plan* is one tickmarkr *task* — a plan is
|
|
7
7
|
// worktree-sized; its inner <task> steps stay in the worker prompt via context[0] = the plan file.
|
|
@@ -16,6 +16,72 @@ export function isGsdPhaseDir(dir) {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
const strings = (v) => (Array.isArray(v) ? v.map(String) : []);
|
|
19
|
+
// Cycle-safe by construction: a recursive YAML alias (`&a [*a]`) is a legal value the parser hands
|
|
20
|
+
// back as a circular object, and a bare JSON.stringify throws a native TypeError on it — which would
|
|
21
|
+
// REPLACE the indexed CompileError this module owes the author with an unrelated crash. Every value
|
|
22
|
+
// the YAML parser can produce renders here. (A repeated non-recursive alias also renders [circular];
|
|
23
|
+
// this is a diagnostic string, not a round-trippable encoding.)
|
|
24
|
+
const show = (v) => {
|
|
25
|
+
const seen = new WeakSet();
|
|
26
|
+
const cycleSafe = (_k, x) => {
|
|
27
|
+
if (typeof x !== "object" || x === null)
|
|
28
|
+
return x;
|
|
29
|
+
if (seen.has(x))
|
|
30
|
+
return "[circular]";
|
|
31
|
+
seen.add(x);
|
|
32
|
+
return x;
|
|
33
|
+
};
|
|
34
|
+
return JSON.stringify(v, cycleSafe) ?? String(v);
|
|
35
|
+
};
|
|
36
|
+
// A truth IS an acceptance item, not a string to coerce. strings()' `.map(String)` turned a typed
|
|
37
|
+
// object truth into the literal "[object Object]" — schema-legal in the judge-compat string form, so
|
|
38
|
+
// it passed validateGraph, reached the worker prompt verbatim and became a judge rubric item; a YAML
|
|
39
|
+
// scalar `- 2.0` silently became "2". Nothing downstream catches either. So fail closed (house
|
|
40
|
+
// precedent: the routing block in compileOne) on anything that is not text and not a typed oracle.
|
|
41
|
+
// The CONTAINER is reported, not collapsed: an absent key returns undefined (nothing was declared) and
|
|
42
|
+
// a present-but-empty list returns [] (declared, contributed nothing) — strings() flattened both, plus
|
|
43
|
+
// a non-list, into the same silent []. Only the non-list fails closed; `truths: []` is real producer
|
|
44
|
+
// output whose plan can still be carried by its <done> lines, and compileOne already rejects a task
|
|
45
|
+
// whose acceptance ends up genuinely empty.
|
|
46
|
+
// Parser liberal, template strict: a string truth stays exactly what strings() made of it — the GSD
|
|
47
|
+
// planner template teaches prose-only and lives in another repo, so this parser must never be
|
|
48
|
+
// stricter than the thing that produced its input. A non-string must satisfy AcceptanceItemSchema
|
|
49
|
+
// EXACTLY — accepted with nothing dropped — which is where the dual-key `text:` beside
|
|
50
|
+
// `command:`/`test:` is DECLARED (schema.ts:25-30); being declared is what makes it survive
|
|
51
|
+
// loadGraph's revalidation, so nothing is ever reattached after.
|
|
52
|
+
export function parseTruths(file, raw) {
|
|
53
|
+
if (raw === undefined)
|
|
54
|
+
return undefined; // key absent: nothing was declared
|
|
55
|
+
if (!Array.isArray(raw)) {
|
|
56
|
+
throw new CompileError(`${file} has a must_haves.truths that is not a list (got ${show(raw)}).\n` +
|
|
57
|
+
` remedy: write each truth as a "- " list entry, or drop the key.`);
|
|
58
|
+
}
|
|
59
|
+
return raw.map((v, i) => {
|
|
60
|
+
if (typeof v === "string")
|
|
61
|
+
return v;
|
|
62
|
+
const parsed = AcceptanceItemSchema.safeParse(v);
|
|
63
|
+
if (!parsed.success) {
|
|
64
|
+
throw new CompileError(`${file} has a must_haves.truths[${i}] that is neither prose nor a typed acceptance oracle: ${show(v)}\n` +
|
|
65
|
+
` remedy: write it as plain text, or as {oracle: command, command: <shell>} / {oracle: test, test: <name>} /\n` +
|
|
66
|
+
` {oracle: judge, text: <rubric>} — command and test may carry a "text:" beside the oracle.`);
|
|
67
|
+
}
|
|
68
|
+
// A SUCCESSFUL parse is lossy too: z.object strips undeclared keys, so {oracle, command, text,
|
|
69
|
+
// severity} validates and compiles with `severity` gone — the same silent coercion in the other
|
|
70
|
+
// direction, and invisible because nothing downstream ever saw the key. Accepting less than the
|
|
71
|
+
// author supplied is not acceptance, so fail closed on whatever validation dropped.
|
|
72
|
+
// Object.hasOwn, never `k in`: the YAML parser hands back `constructor`, `toString`,
|
|
73
|
+
// `hasOwnProperty` and `__proto__` as OWN keys, but `in` finds all four on Object.prototype of the
|
|
74
|
+
// validated output, so each read as "kept" and was stripped in silence — the exact coercion this
|
|
75
|
+
// check exists to stop.
|
|
76
|
+
const dropped = Object.keys(v).filter((k) => !Object.hasOwn(parsed.data, k));
|
|
77
|
+
if (dropped.length) {
|
|
78
|
+
throw new CompileError(`${file} has a must_haves.truths[${i}] carrying key(s) no acceptance oracle declares: ${dropped.join(", ")}\n` +
|
|
79
|
+
` remedy: drop ${dropped.length === 1 ? "that key" : "those keys"}, or fold the intent into "text:" — a\n` +
|
|
80
|
+
` compile that kept ${dropped.length === 1 ? "it" : "them"} would discard ${dropped.length === 1 ? "it" : "them"} at the next graph load, unread.`);
|
|
81
|
+
}
|
|
82
|
+
return parsed.data;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
19
85
|
const WRITE_DIRECTIVE = /\b(?:Create|Write|Add|Emit|Generate)\s+`([^`\s]+)`/gi;
|
|
20
86
|
const isPathish = (s) => /^[\w./*-]+$/.test(s) && /\.[a-z]{2,4}$/i.test(s);
|
|
21
87
|
const stripFences = (s) => s.replace(/^```[\s\S]*?^```/gm, "");
|
|
@@ -89,7 +155,7 @@ function compileOne(file, storedPath) {
|
|
|
89
155
|
.trim();
|
|
90
156
|
const title = firstSentence || key;
|
|
91
157
|
const dones = [...body.matchAll(/<done>\s*([\s\S]*?)\s*<\/done>/g)].map((m) => m[1].replace(/\s+/g, " ").trim());
|
|
92
|
-
const truths =
|
|
158
|
+
const truths = parseTruths(file, fm.must_haves?.truths) ?? [];
|
|
93
159
|
const acceptance = [...dones, ...truths].filter(Boolean);
|
|
94
160
|
if (!acceptance.length) {
|
|
95
161
|
throw new CompileError(`${file} has no acceptance criteria — every tickmarkr task needs them.\n` +
|
package/dist/compile/native.d.ts
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
import { type RunGraph } from "../graph/schema.js";
|
|
2
|
+
/**
|
|
3
|
+
* Classify one `context:` entry against the tree a worker's worktree is built from.
|
|
4
|
+
* ok — reachable
|
|
5
|
+
* untracked — in the author's checkout, invisible to every worker. WARNS, permanently: per-file
|
|
6
|
+
* force-add is the deliberate opt-in, so this is a standing prompt, not a migration.
|
|
7
|
+
* missing — absent from both. FAILS compile; carries a repair when exactly one tracked file
|
|
8
|
+
* shares the basename (ambiguous matches suggest nothing rather than guess).
|
|
9
|
+
* RULING 2026-08-03 (.overseer/OVERSEER-RULING-20260803-context-visibility.md): OBS-170's
|
|
10
|
+
* fail-closed-on-untracked is retired — oracle upheld, policy overruled (it blocked 11 of 13).
|
|
11
|
+
*/
|
|
12
|
+
export declare function classifyContextPath(entry: string, tracked: Set<string>, repoDir: string): {
|
|
13
|
+
kind: "ok" | "untracked" | "missing";
|
|
14
|
+
suggestion?: string;
|
|
15
|
+
};
|
|
2
16
|
export declare const COLLECTABLE_TESTS = "tests/**/*.test.ts";
|
|
3
17
|
export declare const LEGACY_PREFIX: string;
|
|
4
18
|
export declare const TICKMARKR_NATIVE_MARKER: RegExp;
|
package/dist/compile/native.js
CHANGED
|
@@ -1,7 +1,61 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
1
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { basename, dirname } from "node:path";
|
|
2
4
|
import picomatch from "picomatch";
|
|
3
5
|
import { GATE_NAMES, GRAPH_ROUTING_MODES, ORACLES, SHAPES, TIERS, validateGraph } from "../graph/schema.js";
|
|
4
6
|
import { CompileError, inferShape, sha256 } from "./common.js";
|
|
7
|
+
// OBS-170/OBS-184: `context:` is a promise to the worker, and nothing ever checked it could be kept.
|
|
8
|
+
// Workers run in `git worktree add <baseRef>` (run/git.ts:169-176), which materialises the base
|
|
9
|
+
// TREE and copies nothing in — so the oracle is that tree, and NEVER two things that look like it:
|
|
10
|
+
// · existsSync reads the AUTHOR's checkout. Measured on v1.85: 10 of 16 unreachable entries are
|
|
11
|
+
// plainly present there (.overseer/ is 21 tracked files out of 480 on disk).
|
|
12
|
+
// · `git ls-files` reads the INDEX. `git add -f` writes the index, so a staged-uncommitted file
|
|
13
|
+
// reports present while still being invisible to every worker — the lint would then certify
|
|
14
|
+
// exactly the failure it exists to catch (RULING 2026-08-03 rider; the absent-vs-empty shape
|
|
15
|
+
// again, cf. fileBytes). Hence the opt-in is "add AND commit", and the resolver reads HEAD.
|
|
16
|
+
// `ls-tree -r HEAD` enumerates that tree once; per-path `cat-file -e HEAD:<path>` is the same oracle
|
|
17
|
+
// one path at a time, and one spawn beats N.
|
|
18
|
+
function trackedAtHead(dir) {
|
|
19
|
+
const git = (...args) => spawnSync("git", ["-C", dir, ...args], { encoding: "utf8", maxBuffer: 1 << 28 });
|
|
20
|
+
const top = git("rev-parse", "--show-toplevel");
|
|
21
|
+
// --full-tree is load-bearing: without it `ls-tree` scopes to the CWD's subtree and prints paths
|
|
22
|
+
// relative to it, so a spec in specs/ would see only specs/-relative names and misjudge every
|
|
23
|
+
// path outside it. Caught only by a fixture whose spec is NOT at the repo root.
|
|
24
|
+
const r = git("ls-tree", "--full-tree", "-r", "--name-only", "HEAD");
|
|
25
|
+
// not a repo, or no commits yet — fail open, so non-repo fixtures and fresh inits stay silent
|
|
26
|
+
if (top.status !== 0 || r.status !== 0 || typeof r.stdout !== "string")
|
|
27
|
+
return undefined;
|
|
28
|
+
return { root: top.stdout.trim(), tracked: new Set(r.stdout.split("\n").filter(Boolean)) };
|
|
29
|
+
}
|
|
30
|
+
const GLOB_CHARS = /[*?{[]/;
|
|
31
|
+
/**
|
|
32
|
+
* Classify one `context:` entry against the tree a worker's worktree is built from.
|
|
33
|
+
* ok — reachable
|
|
34
|
+
* untracked — in the author's checkout, invisible to every worker. WARNS, permanently: per-file
|
|
35
|
+
* force-add is the deliberate opt-in, so this is a standing prompt, not a migration.
|
|
36
|
+
* missing — absent from both. FAILS compile; carries a repair when exactly one tracked file
|
|
37
|
+
* shares the basename (ambiguous matches suggest nothing rather than guess).
|
|
38
|
+
* RULING 2026-08-03 (.overseer/OVERSEER-RULING-20260803-context-visibility.md): OBS-170's
|
|
39
|
+
* fail-closed-on-untracked is retired — oracle upheld, policy overruled (it blocked 11 of 13).
|
|
40
|
+
*/
|
|
41
|
+
export function classifyContextPath(entry, tracked, repoDir) {
|
|
42
|
+
if (GLOB_CHARS.test(entry))
|
|
43
|
+
return { kind: "ok" }; // a glob is not a promise about one file
|
|
44
|
+
const q = entry.replace(/\/+$/, "");
|
|
45
|
+
if (tracked.has(q))
|
|
46
|
+
return { kind: "ok" };
|
|
47
|
+
for (const t of tracked)
|
|
48
|
+
if (t.startsWith(`${q}/`))
|
|
49
|
+
return { kind: "ok" }; // directory with tracked children
|
|
50
|
+
if (existsSync(`${repoDir}/${q}`))
|
|
51
|
+
return { kind: "untracked" };
|
|
52
|
+
// Measured: 5 of v1.85's 6 absent entries are a tracked file written without its directory
|
|
53
|
+
// (`V185-SEEDS.md` for `.overseer/V185-SEEDS.md`) — the opt-in had worked, only the check was
|
|
54
|
+
// missing. Suggest ONLY on an unambiguous basename: two candidates is a guess, not a repair.
|
|
55
|
+
const base = basename(q);
|
|
56
|
+
const hits = [...tracked].filter((t) => basename(t) === base);
|
|
57
|
+
return hits.length === 1 ? { kind: "missing", suggestion: hits[0] } : { kind: "missing" };
|
|
58
|
+
}
|
|
5
59
|
// OBS-97: mirror of the vitest.config.ts suite include — the only path class vitest collects.
|
|
6
60
|
export const COLLECTABLE_TESTS = "tests/**/*.test.ts";
|
|
7
61
|
export const LEGACY_PREFIX = ["dro", "vr"].join("");
|
|
@@ -31,7 +85,7 @@ export function compileNative(file) {
|
|
|
31
85
|
for (const line of content.split("\n")) {
|
|
32
86
|
const heading = line.match(HEAD_RE);
|
|
33
87
|
if (heading) {
|
|
34
|
-
drafts.push({ id: heading[1], title: heading[2].trim(), fields: {}, acceptance: [], gates: [], hasGates: false, list: null,
|
|
88
|
+
drafts.push({ id: heading[1], title: heading[2].trim(), fields: {}, acceptance: [], gates: [], hasGates: false, list: null, continuationField: null });
|
|
35
89
|
continue;
|
|
36
90
|
}
|
|
37
91
|
const draft = drafts.at(-1);
|
|
@@ -59,29 +113,42 @@ export function compileNative(file) {
|
|
|
59
113
|
draft.list = name;
|
|
60
114
|
if (name === "gates")
|
|
61
115
|
draft.hasGates = true;
|
|
62
|
-
draft.
|
|
116
|
+
draft.continuationField = null;
|
|
63
117
|
}
|
|
64
118
|
else {
|
|
65
119
|
if (name === "goal" && !value)
|
|
66
120
|
invalid(draft.id, field[1], "must not be empty");
|
|
67
121
|
draft.fields[name] = value;
|
|
68
|
-
// OBS-60: carry
|
|
69
|
-
|
|
122
|
+
// OBS-60/OBS-308: carry wrapped physical lines for prose and comma-separated fields. A
|
|
123
|
+
// worker must receive the whole logical field, regardless of the author's editor width.
|
|
124
|
+
draft.continuationField = name === "goal" || name === "deps" || name === "files" || name === "context"
|
|
125
|
+
? name
|
|
126
|
+
: null;
|
|
70
127
|
draft.list = null;
|
|
71
128
|
}
|
|
72
129
|
continue;
|
|
73
130
|
}
|
|
74
|
-
// OBS-60:
|
|
75
|
-
|
|
131
|
+
// OBS-60/OBS-308: indented physical lines continue the preceding logical field until the next
|
|
132
|
+
// field/list/heading. List continuations join with a space so the existing comma parser sees the
|
|
133
|
+
// exact same value it would have received on one line.
|
|
134
|
+
if (draft.continuationField) {
|
|
135
|
+
const continuation = draft.continuationField;
|
|
76
136
|
if ((line.startsWith(" ") || line.startsWith("\t")) && !NESTED_RE.test(line)) {
|
|
77
|
-
|
|
78
|
-
|
|
137
|
+
const value = line.trim();
|
|
138
|
+
if (continuation === "goal") {
|
|
139
|
+
draft.fields.goal += `\n${value}`;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (value) {
|
|
143
|
+
draft.fields[continuation] += ` ${value}`;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
79
146
|
}
|
|
80
|
-
if (!line.trim()) {
|
|
147
|
+
if (continuation === "goal" && !line.trim()) {
|
|
81
148
|
draft.fields.goal += "\n";
|
|
82
149
|
continue;
|
|
83
150
|
}
|
|
84
|
-
draft.
|
|
151
|
+
draft.continuationField = null;
|
|
85
152
|
}
|
|
86
153
|
const nested = line.match(NESTED_RE);
|
|
87
154
|
if (nested && draft.list) {
|
|
@@ -124,12 +191,16 @@ export function compileNative(file) {
|
|
|
124
191
|
}
|
|
125
192
|
// v1.62 (OBS-97): commas inside {a,b} alternatives are part of one glob entry, not separators —
|
|
126
193
|
// split only at brace depth 0 so a brace glob reaches the lint (and the scope gate) intact.
|
|
194
|
+
// OBS-170/OBS-184: the same is true of a citation annotation. `.planning/OBSERVATIONS.md (OBS-262,
|
|
195
|
+
// OBS-263)` split at the inner comma into two bullets, and workers were dispatched a context line
|
|
196
|
+
// reading `OBS-263)`. Track paren depth alongside brace depth for exactly the OBS-97 reason.
|
|
127
197
|
const splitTop = (value) => {
|
|
128
198
|
const parts = [];
|
|
129
199
|
let depth = 0;
|
|
200
|
+
let paren = 0;
|
|
130
201
|
let current = "";
|
|
131
202
|
for (const ch of value) {
|
|
132
|
-
if (ch === "," && depth === 0) {
|
|
203
|
+
if (ch === "," && depth === 0 && paren === 0) {
|
|
133
204
|
parts.push(current);
|
|
134
205
|
current = "";
|
|
135
206
|
continue;
|
|
@@ -138,11 +209,34 @@ export function compileNative(file) {
|
|
|
138
209
|
depth++;
|
|
139
210
|
else if (ch === "}" && depth > 0)
|
|
140
211
|
depth--;
|
|
212
|
+
else if (ch === "(")
|
|
213
|
+
paren++;
|
|
214
|
+
else if (ch === ")" && paren > 0)
|
|
215
|
+
paren--;
|
|
141
216
|
current += ch;
|
|
142
217
|
}
|
|
143
218
|
return [...parts, current];
|
|
144
219
|
};
|
|
145
|
-
|
|
220
|
+
// Keeping the comma is only half of it: the merged entry still carries the annotation, so it
|
|
221
|
+
// resolves no better than the fragments did (measured: 24 → 19 unresolvable, zero rescued).
|
|
222
|
+
// Strip one trailing parenthetical so the entry is the path its author meant.
|
|
223
|
+
const stripAnnotation = (item) => {
|
|
224
|
+
const trimmed = item.trim();
|
|
225
|
+
if (!trimmed.endsWith(")"))
|
|
226
|
+
return trimmed;
|
|
227
|
+
let paren = 0;
|
|
228
|
+
for (let i = trimmed.length - 1; i >= 0; i--) {
|
|
229
|
+
if (trimmed[i] === ")")
|
|
230
|
+
paren++;
|
|
231
|
+
else if (trimmed[i] === "(") {
|
|
232
|
+
paren--;
|
|
233
|
+
if (paren === 0)
|
|
234
|
+
return trimmed.slice(0, i).trim();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return trimmed;
|
|
238
|
+
};
|
|
239
|
+
const csv = (value) => value && value.toLowerCase() !== "none" ? splitTop(value).map((item) => stripAnnotation(item.trim())).filter(Boolean) : [];
|
|
146
240
|
// OBS-97: a typed test: oracle needs a collectable home. vitest only collects COLLECTABLE_TESTS
|
|
147
241
|
// paths, so a task whose non-empty files[] cannot host one makes scope-green and acceptance-green
|
|
148
242
|
// mutually exclusive by construction — run-20260719-210434 burned two dispatch attempts before a
|
|
@@ -281,6 +375,31 @@ export function compileNative(file) {
|
|
|
281
375
|
}
|
|
282
376
|
}
|
|
283
377
|
}
|
|
378
|
+
// OBS-170/OBS-184: warn per unreachable context: entry, classified by the action its author must
|
|
379
|
+
// take. Warn-only by operator ruling (2026-08-03) — fail-closed would have refused to compile the
|
|
380
|
+
// spec that shipped green. Fails open when git cannot answer, so non-repo fixtures stay silent.
|
|
381
|
+
const repo = trackedAtHead(dirname(file));
|
|
382
|
+
if (repo) {
|
|
383
|
+
const unreachable = [];
|
|
384
|
+
for (const t of tasks) {
|
|
385
|
+
for (const entry of t.context) {
|
|
386
|
+
const { kind, suggestion } = classifyContextPath(entry, repo.tracked, repo.root);
|
|
387
|
+
if (kind === "untracked") {
|
|
388
|
+
// Permanent, by ruling: the per-file force-add IS the opt-in, so this stays a standing
|
|
389
|
+
// prompt. "and commit" is load-bearing — staging alone leaves it out of the base tree.
|
|
390
|
+
console.warn(`tickmarkr: OBS-170: task ${t.id} context ${JSON.stringify(entry)} exists in your checkout but is NOT in a worker's worktree. To make it worker context: git add -f ${entry} && git commit. Staging alone is not enough.`);
|
|
391
|
+
}
|
|
392
|
+
else if (kind === "missing") {
|
|
393
|
+
unreachable.push(` ${t.id}: ${JSON.stringify(entry)}${suggestion ? `\n → did you mean ${suggestion} ?` : "\n → not found in the repository, and not a path."}`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (unreachable.length) {
|
|
398
|
+
throw new CompileError(`context: paths that do not exist in ${file}:\n${unreachable.join("\n")}\n\n` +
|
|
399
|
+
`A context: entry is a promise the worker can read it. These resolve to nothing in the repository,\n` +
|
|
400
|
+
`so the worker would be told to read a file that is not there. Fix the paths and recompile.`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
284
403
|
return result;
|
|
285
404
|
}
|
|
286
405
|
// Commented native spec written to tickmarkr.spec.md by `tickmarkr init`. Documented via HTML comments (which the
|
|
@@ -319,6 +438,36 @@ acceptance is required on every task (a nested list of observable outcomes).
|
|
|
319
438
|
- test: <name> (oracle: test — named test)
|
|
320
439
|
- judge: <rubric> (oracle: judge — LLM-judged, free text)
|
|
321
440
|
- <plain text> (compat: compiles as judge oracle, warns)
|
|
441
|
+
|
|
442
|
+
HARD BOUNDS — these FAIL the compile, they do not warn:
|
|
443
|
+
- at most 6 acceptance items per task (no exception path)
|
|
444
|
+
- at most 8 files[] patterns; a {a,b} brace group counts as ONE pattern
|
|
445
|
+
- acceptance items x files[] patterns must be <= 24 ("surface")
|
|
446
|
+
- at most 60 goal words per acceptance criterion ("density")
|
|
447
|
+
Density is goal words DIVIDED BY items, so REMOVING a criterion RAISES it. Compress the goal in the
|
|
448
|
+
same edit, or a task that was comfortably inside the bound breaches it while you are tidying up.
|
|
449
|
+
|
|
450
|
+
WHAT MAKES A CRITERION REAL:
|
|
451
|
+
- "test:" must name a real test asserting on recorded state, journal lines, or drawn frames, and its
|
|
452
|
+
title must match the criterion string verbatim. It also needs a collectable test path in files[].
|
|
453
|
+
- NO criterion may be satisfiable by an absence, a rename, a source-text grep, or an empty collection.
|
|
454
|
+
"no file references X" is not a criterion — it passes in a repo where the feature was never built.
|
|
455
|
+
- "goal:" is NEVER verification. Prose in the goal enforces nothing: every obligation needs an
|
|
456
|
+
acceptance item, or a NAMED independent gate that actually applies. A gate is not independent of a
|
|
457
|
+
task that owns both the thing it checks and the fixture it checks against.
|
|
458
|
+
- A source-only obligation (a comment or doc that a change makes false) has no lawful "test:" — verify
|
|
459
|
+
it with "judge:", which reads the DIFF and must cite a changed line in a file the task owns.
|
|
460
|
+
|
|
461
|
+
ORDERING AND OWNERSHIP:
|
|
462
|
+
- Every path has exactly ONE owning task. Two tasks writing one file must be ORDERED by deps, or the
|
|
463
|
+
loser's work is silently dropped when the integration tip advances.
|
|
464
|
+
- A file one task CREATES cannot be "context:" for another — only deps: carries it, and that extends to
|
|
465
|
+
the task that PRODUCES a value, not just the file's existence.
|
|
466
|
+
- Deleting or renaming a symbol is a cross-task contract. Sweep for consumers by symbol AND by what the
|
|
467
|
+
mechanism DOES for them; the most dangerous consumer is a file no task owns, because nothing fixes it
|
|
468
|
+
and no edge can order it.
|
|
469
|
+
|
|
470
|
+
"timeout:" IS A KILL CEILING, NOT AN ESTIMATE. Never read a sum of timeouts as a predicted duration.
|
|
322
471
|
-->
|
|
323
472
|
|
|
324
473
|
## T1: Scaffold the feature
|
package/dist/config/config.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ declare const TierEnum: z.ZodEnum<{
|
|
|
7
7
|
export type Tier = z.infer<typeof TierEnum>;
|
|
8
8
|
export declare const TIER_RANK: Record<Tier, number>;
|
|
9
9
|
export declare const DEFAULT_DIFF_CAP = 60000;
|
|
10
|
+
export declare const MODEL_WINDOW_PLAUSIBILITY_FLOOR = 1024;
|
|
10
11
|
export declare const ROUTING_MODES: readonly ["partner-led", "risk-based", "staff-led"];
|
|
11
12
|
declare const ModeEnum: z.ZodEnum<{
|
|
12
13
|
"partner-led": "partner-led";
|
|
@@ -26,7 +27,7 @@ export declare const MapEntrySchema: z.ZodObject<{
|
|
|
26
27
|
}, z.core.$strip>;
|
|
27
28
|
export type MapEntry = z.infer<typeof MapEntrySchema>;
|
|
28
29
|
export declare const TierEntrySchema: z.ZodObject<{
|
|
29
|
-
vendor: z.ZodString
|
|
30
|
+
vendor: z.ZodNullable<z.ZodString>;
|
|
30
31
|
channel: z.ZodEnum<{
|
|
31
32
|
sub: "sub";
|
|
32
33
|
api: "api";
|
|
@@ -36,6 +37,13 @@ export declare const TierEntrySchema: z.ZodObject<{
|
|
|
36
37
|
mid: "mid";
|
|
37
38
|
frontier: "frontier";
|
|
38
39
|
}>>;
|
|
40
|
+
modelOverrides: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
41
|
+
vendor: z.ZodOptional<z.ZodString>;
|
|
42
|
+
channel: z.ZodOptional<z.ZodEnum<{
|
|
43
|
+
sub: "sub";
|
|
44
|
+
api: "api";
|
|
45
|
+
}>>;
|
|
46
|
+
}, z.core.$strip>>>;
|
|
39
47
|
windows: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
40
48
|
}, z.core.$strip>;
|
|
41
49
|
export type TierEntry = z.infer<typeof TierEntrySchema>;
|
|
@@ -173,7 +181,7 @@ export declare const TickmarkrConfigSchema: z.ZodObject<{
|
|
|
173
181
|
}, z.core.$strip>>;
|
|
174
182
|
}, z.core.$strip>;
|
|
175
183
|
tiers: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
176
|
-
vendor: z.ZodString
|
|
184
|
+
vendor: z.ZodNullable<z.ZodString>;
|
|
177
185
|
channel: z.ZodEnum<{
|
|
178
186
|
sub: "sub";
|
|
179
187
|
api: "api";
|
|
@@ -183,6 +191,13 @@ export declare const TickmarkrConfigSchema: z.ZodObject<{
|
|
|
183
191
|
mid: "mid";
|
|
184
192
|
frontier: "frontier";
|
|
185
193
|
}>>;
|
|
194
|
+
modelOverrides: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
195
|
+
vendor: z.ZodOptional<z.ZodString>;
|
|
196
|
+
channel: z.ZodOptional<z.ZodEnum<{
|
|
197
|
+
sub: "sub";
|
|
198
|
+
api: "api";
|
|
199
|
+
}>>;
|
|
200
|
+
}, z.core.$strip>>>;
|
|
186
201
|
windows: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
187
202
|
}, z.core.$strip>>;
|
|
188
203
|
pricing: z.ZodRecord<z.ZodString, z.ZodNumber>;
|
|
@@ -301,8 +316,8 @@ export type InitConfigOverlay = {
|
|
|
301
316
|
};
|
|
302
317
|
};
|
|
303
318
|
export declare function configTemplate(overlay?: InitConfigOverlay): string;
|
|
304
|
-
export { FLEET_OVERLAY_KEYS, fleetEditableEquals, fleetRepoOverlayFromDelta,
|
|
305
|
-
export type {
|
|
319
|
+
export { FLEET_OVERLAY_KEYS, fleetEditableEquals, fleetRepoOverlayFromDelta, renderFleetOverlayWrite, repoOverlayYaml, serializeFleetOverlay, unifiedYamlDiff, } from "./fleet-overlay.js";
|
|
320
|
+
export type { FleetOverlayWrite } from "./fleet-overlay.js";
|
|
306
321
|
export type FleetTierAssignment = {
|
|
307
322
|
tier: Tier;
|
|
308
323
|
provenance?: string;
|
|
@@ -316,7 +331,7 @@ export type FleetEditable = {
|
|
|
316
331
|
};
|
|
317
332
|
export declare function repoOverlayPath(repoRoot: string): string;
|
|
318
333
|
export declare function readOverlayFile(path: string): Record<string, unknown>;
|
|
319
|
-
export declare function fleetEditableFromConfig(cfg: TickmarkrConfig
|
|
334
|
+
export declare function fleetEditableFromConfig(cfg: TickmarkrConfig): FleetEditable;
|
|
320
335
|
/** Resolve which layer last set a dotted fleet path (defaults < global < repo). */
|
|
321
336
|
export declare function fleetKeyLayer(repoRoot: string, dotted: string, opts?: {
|
|
322
337
|
globalDir?: string;
|