tickmarkr 2.2.1 → 2.4.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 +10 -9
- package/dist/adapters/catalog-remote.d.ts +1 -4
- package/dist/adapters/catalog-remote.js +52 -42
- package/dist/adapters/catalog.js +5 -3
- package/dist/adapters/claude-code.d.ts +1 -1
- package/dist/adapters/claude-code.js +8 -5
- package/dist/adapters/model-lints.d.ts +9 -5
- package/dist/adapters/model-lints.js +56 -15
- package/dist/adapters/model-windows.js +11 -0
- package/dist/adapters/prompt.js +1 -0
- package/dist/adapters/qwen.d.ts +5 -0
- package/dist/adapters/qwen.js +153 -0
- package/dist/adapters/types.d.ts +21 -1
- package/dist/adapters/types.js +43 -2
- package/dist/cli/commands/approve.js +5 -4
- package/dist/cli/commands/beat.js +7 -4
- package/dist/cli/commands/compile.js +32 -6
- package/dist/cli/commands/doctor.d.ts +9 -4
- package/dist/cli/commands/doctor.js +87 -13
- package/dist/cli/commands/fleet.d.ts +4 -0
- package/dist/cli/commands/fleet.js +53 -14
- package/dist/cli/commands/init.js +36 -21
- package/dist/cli/commands/plan.js +45 -7
- package/dist/cli/commands/report.js +37 -1
- package/dist/cli/commands/status.d.ts +1 -0
- package/dist/cli/commands/status.js +45 -1
- package/dist/cli/commands/verify.d.ts +6 -0
- package/dist/cli/commands/verify.js +145 -25
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +2 -2
- package/dist/compile/collateral.d.ts +2 -9
- package/dist/compile/collateral.js +17 -18
- package/dist/compile/index.d.ts +4 -1
- package/dist/compile/index.js +41 -7
- package/dist/compile/native.d.ts +4 -2
- package/dist/compile/native.js +58 -9
- package/dist/compile/ownership.js +34 -9
- package/dist/config/config.d.ts +1 -0
- package/dist/config/config.js +52 -6
- package/dist/drivers/herdr.d.ts +1 -0
- package/dist/drivers/herdr.js +11 -1
- package/dist/drivers/index.d.ts +6 -0
- package/dist/drivers/index.js +19 -4
- package/dist/drivers/orca.d.ts +35 -1
- package/dist/drivers/orca.js +260 -20
- package/dist/drivers/subprocess.d.ts +3 -3
- package/dist/drivers/subprocess.js +16 -9
- package/dist/drivers/types.d.ts +12 -0
- package/dist/gates/baseline.d.ts +2 -0
- package/dist/gates/baseline.js +47 -11
- package/dist/gates/llm.d.ts +6 -0
- package/dist/gates/llm.js +25 -9
- package/dist/gates/review.d.ts +7 -3
- package/dist/gates/review.js +61 -22
- package/dist/gates/run-gates.d.ts +5 -2
- package/dist/gates/run-gates.js +50 -26
- package/dist/gates/verdict-cause.d.ts +6 -2
- package/dist/gates/verdict-cause.js +8 -4
- package/dist/route/preference.d.ts +4 -0
- package/dist/route/preference.js +40 -0
- package/dist/route/router.js +15 -2
- package/dist/run/consult.d.ts +1 -0
- package/dist/run/consult.js +39 -8
- package/dist/run/daemon.d.ts +16 -0
- package/dist/run/daemon.js +345 -74
- package/dist/run/git.d.ts +3 -0
- package/dist/run/git.js +40 -5
- package/dist/run/journal.d.ts +15 -2
- package/dist/run/journal.js +73 -12
- package/dist/run/supervision.d.ts +6 -0
- package/dist/run/supervision.js +29 -1
- package/dist/tui/ink/fleet-app.d.ts +4 -0
- package/dist/tui/ink/fleet-app.js +45 -16
- package/dist/tui/ink/init-app.js +4 -4
- package/package.json +59 -1
- package/skills/tickmarkr-overseer/SKILL.md +77 -18
- package/skills/tickmarkr-overseer/scripts/seat-send.sh +88 -18
- package/skills/tickmarkr-overseer/scripts/watch-artifacts.sh +36 -2
- package/skills/tickmarkr-overseer/scripts/watch-contamination.sh +36 -15
- package/skills/tickmarkr-overseer/scripts/watch-context.sh +33 -7
- package/skills/tickmarkr-overseer/scripts/watch-pending-input.sh +32 -8
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
6
6
|
import { allAdapters, probeAll, readDoctor, rolePools } from "../../adapters/registry.js";
|
|
7
7
|
import { channelKey } from "../../adapters/types.js";
|
|
8
8
|
import { loadConfig } from "../../config/config.js";
|
|
9
9
|
import { captureBaseline, detectGateCommands } from "../../gates/baseline.js";
|
|
10
|
+
import { modelProvider } from "../../gates/review.js";
|
|
10
11
|
import { runGates } from "../../gates/run-gates.js";
|
|
11
12
|
import { getTask, loadGraph } from "../../graph/graph.js";
|
|
12
13
|
import { GATE_NAMES } from "../../graph/schema.js";
|
|
13
14
|
import { linkNodeModules, removeWorktree, shGit, shGitOk } from "../../run/git.js";
|
|
15
|
+
import { Journal } from "../../run/journal.js";
|
|
14
16
|
/**
|
|
15
17
|
* tickmarkr verify — the gate battery as a standalone command (OPERATING-MODEL-2026-08-11 item 3).
|
|
16
18
|
*
|
|
@@ -59,7 +61,76 @@ const DIRTY_WHY = `refusing to gate a dirty worktree: the shell gates run agains
|
|
|
59
61
|
export function verifyStateDir(cwd) {
|
|
60
62
|
return join(realpathSync(tmpdir()), "tickmarkr-verify", createHash("sha256").update(cwd).digest("hex").slice(0, 12));
|
|
61
63
|
}
|
|
64
|
+
const STATE_FILES = ["graph.json", "doctor.json", "config.yaml"];
|
|
65
|
+
const LOCKFILES = ["package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb"];
|
|
66
|
+
/** Linked worktrees share operational state with the repository that owns the common git dir. */
|
|
67
|
+
export async function verifyStateRoot(cwd) {
|
|
68
|
+
const commonDir = resolve(cwd, (await shGitOk("git rev-parse --git-common-dir", cwd)).trim());
|
|
69
|
+
const commonRoot = realpathSync(dirname(commonDir));
|
|
70
|
+
if (commonRoot === realpathSync(cwd))
|
|
71
|
+
return cwd;
|
|
72
|
+
const localState = join(cwd, ".tickmarkr");
|
|
73
|
+
const commonState = join(commonRoot, ".tickmarkr");
|
|
74
|
+
return STATE_FILES.some((file) => !existsSync(join(localState, file)) && existsSync(join(commonState, file)))
|
|
75
|
+
? commonRoot
|
|
76
|
+
: cwd;
|
|
77
|
+
}
|
|
78
|
+
const hashParts = (parts) => {
|
|
79
|
+
const hash = createHash("sha256");
|
|
80
|
+
for (const part of parts)
|
|
81
|
+
hash.update(part);
|
|
82
|
+
return hash.digest("hex").slice(0, 12);
|
|
83
|
+
};
|
|
84
|
+
/** Cache identity is repository content, never a checkout path. */
|
|
85
|
+
export function baselineCachePath(cwd, baseSha, commands) {
|
|
86
|
+
const lockParts = [];
|
|
87
|
+
for (const file of LOCKFILES) {
|
|
88
|
+
const path = join(cwd, file);
|
|
89
|
+
lockParts.push(file, existsSync(path) ? readFileSync(path) : "<absent>");
|
|
90
|
+
}
|
|
91
|
+
const commandParts = Object.entries(commands).sort(([a], [b]) => a.localeCompare(b)).map(([name, command]) => `${name}\0${command}\0`);
|
|
92
|
+
return join(realpathSync(tmpdir()), "tickmarkr-verify", "cache", `baseline-${baseSha.slice(0, 12)}-${hashParts(lockParts)}-${hashParts(commandParts)}.json`);
|
|
93
|
+
}
|
|
94
|
+
const verdictlessCommands = (baseline, commands) => Object.keys(commands).filter((name) => baseline.commands[name]?.exitCode === undefined);
|
|
95
|
+
export function excludeAuthorProvider(channels, author) {
|
|
96
|
+
const provider = modelProvider(author.model, author.vendor);
|
|
97
|
+
return [author, ...channels.filter((candidate) => candidate !== author && modelProvider(candidate.model, candidate.vendor) !== provider)];
|
|
98
|
+
}
|
|
99
|
+
function recordedMerges(repoRoot) {
|
|
100
|
+
const runs = join(repoRoot, ".tickmarkr", "runs");
|
|
101
|
+
if (!existsSync(runs))
|
|
102
|
+
return [];
|
|
103
|
+
const rows = [];
|
|
104
|
+
for (const runId of readdirSync(runs).filter((name) => name.startsWith("run-")).sort().reverse()) {
|
|
105
|
+
const path = join(runs, runId, "journal.jsonl");
|
|
106
|
+
if (!existsSync(path))
|
|
107
|
+
continue;
|
|
108
|
+
for (const line of readFileSync(path, "utf8").split("\n").filter(Boolean)) {
|
|
109
|
+
try {
|
|
110
|
+
const row = JSON.parse(line);
|
|
111
|
+
if (row.event === "merge" && row.taskId && row.data?.commit)
|
|
112
|
+
rows.push({ runId, taskId: row.taskId, commit: row.data.commit });
|
|
113
|
+
}
|
|
114
|
+
catch { /* torn journal tail is not a merge record */ }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return rows;
|
|
118
|
+
}
|
|
119
|
+
async function warnWideTaskRange(cwd, stateRoot, taskId, mergeBase) {
|
|
120
|
+
const inRange = new Set((await shGitOk(`git rev-list --merges '${mergeBase}..HEAD'`, cwd)).trim().split("\n").filter(Boolean));
|
|
121
|
+
const merges = recordedMerges(stateRoot).filter((row) => inRange.has(row.commit));
|
|
122
|
+
const own = merges.find((row) => row.taskId === taskId);
|
|
123
|
+
const others = own ? merges.filter((row) => row.taskId !== taskId) : [];
|
|
124
|
+
if (own && others.length) {
|
|
125
|
+
console.error(`verify: WARNING --task ${taskId} range also carries merge commit(s) for ${others.map((row) => `${row.taskId} ${row.commit.slice(0, 12)}`).join(", ")}; ${taskId}'s own merge ${own.commit} is the intended HEAD`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
export const VERIFY_HELP = `usage: tickmarkr verify [--base <ref>] [--criteria <file> | --task <id>] [--json]
|
|
129
|
+
The final verdict and JSON result are written to stdout; progress and diagnostics are written to stderr.
|
|
130
|
+
Do not merge stdout and stderr (for example with 2>&1): doing so corrupts the verdict stream.`;
|
|
62
131
|
export async function verify(argv, cwd = process.cwd()) {
|
|
132
|
+
if (argv.some((arg) => arg === "--help" || arg === "-h"))
|
|
133
|
+
return { out: VERIFY_HELP, code: 0 };
|
|
63
134
|
const { values } = parseArgs({
|
|
64
135
|
args: argv,
|
|
65
136
|
options: {
|
|
@@ -69,13 +140,18 @@ export async function verify(argv, cwd = process.cwd()) {
|
|
|
69
140
|
files: { type: "string", multiple: true },
|
|
70
141
|
author: { type: "string" },
|
|
71
142
|
baseline: { type: "string" },
|
|
143
|
+
record: { type: "string" },
|
|
72
144
|
json: { type: "boolean", default: false },
|
|
73
145
|
"no-review": { type: "boolean", default: false },
|
|
74
146
|
"no-acceptance": { type: "boolean", default: false },
|
|
75
147
|
},
|
|
76
148
|
allowPositionals: false,
|
|
77
149
|
});
|
|
78
|
-
const
|
|
150
|
+
const stateRoot = await verifyStateRoot(cwd);
|
|
151
|
+
if (resolve(stateRoot) !== resolve(cwd)) {
|
|
152
|
+
console.error(`verify: state files graph.json, doctor.json and config.yaml resolved read-only from ${join(stateRoot, ".tickmarkr")} (linked worktree)`);
|
|
153
|
+
}
|
|
154
|
+
const cfg = loadConfig(stateRoot);
|
|
79
155
|
const head = (await shGitOk("git rev-parse HEAD", cwd)).trim();
|
|
80
156
|
const baseTip = (await shGitOk(`git rev-parse '${values.base}'`, cwd).catch(() => {
|
|
81
157
|
throw new Error(`--base ${values.base} is not a resolvable ref — pass --base <ref> naming the branch this diff targets`);
|
|
@@ -89,9 +165,7 @@ export async function verify(argv, cwd = process.cwd()) {
|
|
|
89
165
|
let files = values.files ?? [];
|
|
90
166
|
let goal = `independent verification of the ${values.base}..HEAD diff`;
|
|
91
167
|
if (values.task) {
|
|
92
|
-
const t = getTask(loadGraph(
|
|
93
|
-
if (!t)
|
|
94
|
-
throw new Error(`--task ${values.task}: no such task in .tickmarkr/graph.json`);
|
|
168
|
+
const t = getTask(loadGraph(stateRoot), values.task);
|
|
95
169
|
acceptance = t.acceptance;
|
|
96
170
|
if (!files.length)
|
|
97
171
|
files = t.files;
|
|
@@ -102,6 +176,8 @@ export async function verify(argv, cwd = process.cwd()) {
|
|
|
102
176
|
if (!acceptance.length)
|
|
103
177
|
throw new Error(`--criteria ${values.criteria}: no criteria found (bullets or command:/test:/judge: lines)`);
|
|
104
178
|
}
|
|
179
|
+
if (values.task)
|
|
180
|
+
await warnWideTaskRange(cwd, stateRoot, values.task, mergeBase);
|
|
105
181
|
const wantAcceptance = acceptance.length > 0 && !values["no-acceptance"];
|
|
106
182
|
const wantReview = !values["no-review"];
|
|
107
183
|
// A gate that enforced nothing must not print a green row. `files` has exactly three sources —
|
|
@@ -145,7 +221,7 @@ export async function verify(argv, cwd = process.cwd()) {
|
|
|
145
221
|
let author = HUMAN_AUTHOR;
|
|
146
222
|
const adapters = allAdapters();
|
|
147
223
|
if (wantAcceptance || wantReview) {
|
|
148
|
-
const health = readDoctor(
|
|
224
|
+
const health = readDoctor(stateRoot) ?? (await probeAll(adapters));
|
|
149
225
|
const pools = rolePools(cfg, adapters, health);
|
|
150
226
|
judgeChannels = pools.judge;
|
|
151
227
|
channels = pools.review;
|
|
@@ -157,6 +233,7 @@ export async function verify(argv, cwd = process.cwd()) {
|
|
|
157
233
|
throw new Error(`--author ${values.author} does not name a discoverable review channel — one of: ${channels.map(channelKey).join(", ") || "(none)"}`);
|
|
158
234
|
}
|
|
159
235
|
author = { adapter: c.adapter, model: c.model, channel: c.channel, tier: c.tier };
|
|
236
|
+
channels = excludeAuthorProvider(channels, c);
|
|
160
237
|
}
|
|
161
238
|
else {
|
|
162
239
|
channels = [...channels, HUMAN_CHANNEL];
|
|
@@ -173,28 +250,56 @@ export async function verify(argv, cwd = process.cwd()) {
|
|
|
173
250
|
const stateDir = verifyStateDir(cwd);
|
|
174
251
|
mkdirSync(stateDir, { recursive: true });
|
|
175
252
|
let baseline;
|
|
176
|
-
const cachePath =
|
|
253
|
+
const cachePath = baselineCachePath(cwd, mergeBase, commands);
|
|
254
|
+
const verdictlessMarker = `${cachePath}.verdictless`;
|
|
255
|
+
mkdirSync(dirname(cachePath), { recursive: true });
|
|
177
256
|
if (values.baseline) {
|
|
178
257
|
baseline = JSON.parse(readFileSync(values.baseline, "utf8"));
|
|
179
258
|
}
|
|
180
|
-
else if (existsSync(cachePath)) {
|
|
181
|
-
console.error(`verify: reusing cached baseline for ${mergeBase.slice(0, 12)} (${cachePath})`);
|
|
182
|
-
baseline = JSON.parse(readFileSync(cachePath, "utf8"));
|
|
183
|
-
}
|
|
184
259
|
else {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
260
|
+
let cached;
|
|
261
|
+
if (existsSync(cachePath)) {
|
|
262
|
+
cached = JSON.parse(readFileSync(cachePath, "utf8"));
|
|
263
|
+
const missing = verdictlessCommands(cached, commands);
|
|
264
|
+
if (missing.length) {
|
|
265
|
+
console.error(`verify: cached baseline recorded no verdict for ${missing.join(", ")}; it was not reusable and will be recaptured`);
|
|
266
|
+
rmSync(cachePath, { force: true });
|
|
267
|
+
cached = undefined;
|
|
268
|
+
}
|
|
193
269
|
}
|
|
194
|
-
|
|
195
|
-
|
|
270
|
+
if (cached) {
|
|
271
|
+
console.error(`verify: reusing cached baseline for ${mergeBase.slice(0, 12)} (${cachePath})`);
|
|
272
|
+
baseline = cached;
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
if (existsSync(verdictlessMarker)) {
|
|
276
|
+
console.error(`verify: prior baseline recorded no verdict and was not cached; recapturing merge-base ${mergeBase.slice(0, 12)}`);
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
console.error(`verify: capturing baseline at merge-base ${mergeBase.slice(0, 12)} (cached by base, lockfile and command hashes at ${cachePath})`);
|
|
280
|
+
}
|
|
281
|
+
const baseDir = join(stateDir, `base-${mergeBase.slice(0, 12)}`);
|
|
282
|
+
await shGit(`git worktree remove --force '${baseDir}'`, cwd); // stale leftover from an interrupted run
|
|
283
|
+
await shGitOk(`git worktree add --detach '${baseDir}' '${mergeBase}'`, cwd);
|
|
284
|
+
try {
|
|
285
|
+
linkNodeModules(cwd, baseDir, { force: true });
|
|
286
|
+
baseline = await captureBaseline(baseDir, commands);
|
|
287
|
+
const missing = verdictlessCommands(baseline, commands);
|
|
288
|
+
if (missing.length) {
|
|
289
|
+
writeFileSync(verdictlessMarker, JSON.stringify({ base: mergeBase, commands: missing }) + "\n");
|
|
290
|
+
rmSync(cachePath, { force: true });
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
writeFileSync(cachePath, JSON.stringify(baseline, null, 2));
|
|
294
|
+
rmSync(verdictlessMarker, { force: true });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
finally {
|
|
298
|
+
await removeWorktree(cwd, baseDir);
|
|
299
|
+
}
|
|
196
300
|
}
|
|
197
301
|
}
|
|
302
|
+
const recordJournal = values.record ? Journal.open(stateRoot, values.record) : undefined;
|
|
198
303
|
const artifactDir = join(stateDir, new Date().toISOString().replace(/[:.]/g, "-"));
|
|
199
304
|
mkdirSync(artifactDir, { recursive: true });
|
|
200
305
|
const { results } = await runGates(task, {
|
|
@@ -212,12 +317,27 @@ export async function verify(argv, cwd = process.cwd()) {
|
|
|
212
317
|
},
|
|
213
318
|
});
|
|
214
319
|
const green = results.length > 0 && results.every((r) => r.pass || r.meta?.skipped === true);
|
|
320
|
+
const reviewRows = results.filter((result) => result.gate === "review");
|
|
321
|
+
const reviewFindings = reviewRows.flatMap((result) => result.details.split("\n").flatMap((line) => {
|
|
322
|
+
const match = /^- \[([^\]]+)\] (.*)$/.exec(line);
|
|
323
|
+
return match ? [{ classification: match[1], note: match[2], reviewer: result.meta?.reviewer }] : [];
|
|
324
|
+
}));
|
|
325
|
+
const artifactPath = join(artifactDir, "verify-results.json");
|
|
326
|
+
writeFileSync(artifactPath, JSON.stringify({ base: baseTip, head, mergeBase, green, gateRows: results, reviewFindings }, null, 2) + "\n");
|
|
327
|
+
console.error(`verify: artifacts written to ${artifactPath}`);
|
|
328
|
+
const review = reviewRows.at(-1);
|
|
329
|
+
if (recordJournal && review) {
|
|
330
|
+
recordJournal.append("review-leg2", values.task ?? "VERIFY", {
|
|
331
|
+
base: baseTip, head, mergeBase, author: channelKey(author), artifactPath,
|
|
332
|
+
...review,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
215
335
|
if (values.json) {
|
|
216
|
-
return { out: JSON.stringify({ base: baseTip, head, mergeBase, green, results }, null, 2), code: green ? 0 : 2 };
|
|
336
|
+
return { out: JSON.stringify({ base: baseTip, head, mergeBase, green, artifactPath, results }, null, 2), code: green ? 0 : 2 };
|
|
217
337
|
}
|
|
218
338
|
const lines = results.map((r) => `${r.pass ? "PASS" : "FAIL"} ${r.gate}\n${r.details.split("\n").map((l) => ` ${l}`).join("\n")}`);
|
|
219
339
|
const verdict = green
|
|
220
|
-
? `verify GREEN — ${results.length} gate(s) passed on ${mergeBase.slice(0, 12)}..${head.slice(0, 12)} (merge is a human decision)`
|
|
221
|
-
: `verify RED — first failure decides; artifacts
|
|
340
|
+
? `verify GREEN — ${results.length} gate(s) passed on ${mergeBase.slice(0, 12)}..${head.slice(0, 12)} (merge is a human decision; artifacts: ${artifactPath})`
|
|
341
|
+
: `verify RED — first failure decides; artifacts: ${artifactPath}`;
|
|
222
342
|
return { out: [...lines, "", verdict].join("\n"), code: green ? 0 : 2 };
|
|
223
343
|
}
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type CommandResult = string | {
|
|
|
5
5
|
};
|
|
6
6
|
export type CommandMap = Record<string, (argv: string[]) => Promise<CommandResult>>;
|
|
7
7
|
export declare const COMMANDS: CommandMap;
|
|
8
|
-
export declare const USAGE = "tickmarkr \u2014 spec-driven orchestration harness for AI coding agents\nusage: tickmarkr <command>\n init guided setup + doctor; init --agent [--force] [--docs] adds agent skills/docs\n doctor re-probe adapters, herdr, auth; print capability matrix (--fix writes the test-runner ignore when a safe edit exists)\n fleet interactive fleet editor (fleet --print for CI drift checks)\n compile <src> spec \u2192 .tickmarkr/graph.json (fails without acceptance criteria)\n scope <intent> draft a compiled native spec beside an answered intent (--force to overwrite)\n plan dry-run routing table + cost estimate + floor lints\n eval run checked-in fixtures against every channel in isolated temp repos\n run execute the graph (--concurrency N --driver auto|herdr|subprocess|orca --route-strict; orca runs only when named)\n status live run state\n stats all-run channel delivery, red, rescue, author and reviewer statistics\n verify run the gate battery standalone against merge-base(--base, HEAD)..HEAD \u2014
|
|
8
|
+
export declare const USAGE = "tickmarkr \u2014 spec-driven orchestration harness for AI coding agents\nusage: tickmarkr <command>\n init guided setup + doctor; init --agent [--force] [--docs] adds agent skills/docs\n doctor re-probe adapters, herdr, auth; print capability matrix (--fix writes the test-runner ignore when a safe edit exists)\n fleet interactive fleet editor (fleet --print for CI drift checks)\n compile <src> spec \u2192 .tickmarkr/graph.json (fails without acceptance criteria)\n scope <intent> draft a compiled native spec beside an answered intent (--force to overwrite)\n plan dry-run routing table + cost estimate + floor lints\n eval run checked-in fixtures against every channel in isolated temp repos\n run execute the graph (--concurrency N --driver auto|herdr|subprocess|orca --route-strict; orca runs only when named)\n status live run state (--watch --events: JSON documents on stdout, keepalives on stderr; 2>&1 corrupts the stream)\n stats all-run channel delivery, red, rescue, author and reviewer statistics\n verify run the gate battery standalone against merge-base(--base, HEAD)..HEAD \u2014 verdict/JSON on stdout, progress on stderr; 2>&1 corrupts the verdict stream (--base main --criteria <file> | --task <id> [--files <glob>] [--author adapter:model] [--no-review] [--json])\n resume <id> continue a run from its journal\n report <id> cost/quality report (--md for committable execution record)\n profile show learned routing profile (profile reset = forget history via cursor, keeps telemetry)\n ui open the Fleet Studio TUI (full-screen tabbed cockpit)\n unlock remove a stale/garbage run lock (refuses if the holder is alive)\n beat <tier> record one supervision beat for orchestrator|orchestrator-context|overseer|overseer-context|watch, --seat <identity> required (--stand-down to hand off); a supervising seat's own watcher loop calls it, and status reads the tier STALE once the beats stop\n approve <id> <task> release a park (--uphold sides with the reviewer and funds a fixed attempt; --by <name> --reason <text>); takes effect on resume";
|
|
9
9
|
export declare function dispatch(cmd: string | undefined, argv: string[], commands?: CommandMap): Promise<{
|
|
10
10
|
out: string;
|
|
11
11
|
code: number;
|
package/dist/cli/index.js
CHANGED
|
@@ -37,9 +37,9 @@ usage: tickmarkr <command>
|
|
|
37
37
|
plan dry-run routing table + cost estimate + floor lints
|
|
38
38
|
eval run checked-in fixtures against every channel in isolated temp repos
|
|
39
39
|
run execute the graph (--concurrency N --driver auto|herdr|subprocess|orca --route-strict; orca runs only when named)
|
|
40
|
-
status live run state
|
|
40
|
+
status live run state (--watch --events: JSON documents on stdout, keepalives on stderr; 2>&1 corrupts the stream)
|
|
41
41
|
stats all-run channel delivery, red, rescue, author and reviewer statistics
|
|
42
|
-
verify run the gate battery standalone against merge-base(--base, HEAD)..HEAD —
|
|
42
|
+
verify run the gate battery standalone against merge-base(--base, HEAD)..HEAD — verdict/JSON on stdout, progress on stderr; 2>&1 corrupts the verdict stream (--base main --criteria <file> | --task <id> [--files <glob>] [--author adapter:model] [--no-review] [--json])
|
|
43
43
|
resume <id> continue a run from its journal
|
|
44
44
|
report <id> cost/quality report (--md for committable execution record)
|
|
45
45
|
profile show learned routing profile (profile reset = forget history via cursor, keeps telemetry)
|
|
@@ -149,14 +149,7 @@ export declare function reviewParticipationErrors(tasks: ReadonlyArray<Pick<Task
|
|
|
149
149
|
* whose task objects carry no `context` gets byte-identical verdicts to the ones it got before the
|
|
150
150
|
* field existed, because an absent declaration grants no authority at all.
|
|
151
151
|
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
* PROGRAMMATIC compile whose target repo differs from process.cwd() resolves the wrong root and can
|
|
155
|
-
* miss that repo's own `review.criticalPaths`. Threading it is one line in src/compile/index.ts,
|
|
156
|
-
* outside this task's file scope. Two things bound the exposure meanwhile, both live: the critical
|
|
157
|
-
* set here is the UNION with the shipped defaults, so a wrong root can never lower enforcement below
|
|
158
|
-
* the shipped floor; and the review GATE — which is handed the run's real config — refuses to skip a
|
|
159
|
-
* critical path itself (src/gates/review.ts), so a lint this seam misses costs a late verdict rather
|
|
160
|
-
* than an unreviewed one. The compile lint is the early warning; the gate is the fail-closed backstop.
|
|
152
|
+
* The compile seam in src/compile/index.ts threads `compileSource`'s repo root here, so CLI tests
|
|
153
|
+
* and programmatic compiles check the same repository overlay the later run will use.
|
|
161
154
|
*/
|
|
162
155
|
export declare function taskUnitContractErrors(tasks: ReadonlyArray<Pick<Task, "id" | "goal" | "files" | "deps" | "acceptance" | "shape"> & Partial<Pick<Task, "context">>>, repoRoot?: string, review?: ReviewParticipation): string[];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { extname, join, relative } from "node:path";
|
|
3
3
|
import { filesGlob } from "../graph/files-glob.js";
|
|
4
4
|
import { criticalPathHits, DEFAULT_CONFIG, DEFAULT_REVIEW_CRITICAL_PATHS, effectiveReviewPolicy, loadConfig, } from "../config/config.js";
|
|
@@ -15,6 +15,7 @@ const MAX_WALK_FILES = 400;
|
|
|
15
15
|
const MAX_HITS_PER_TASK = 20;
|
|
16
16
|
/** Skip giant fixtures / snapshots. */
|
|
17
17
|
const MAX_READ_BYTES = 512 * 1024;
|
|
18
|
+
const EXPORT_MANIFEST_TEST = "tests/repo/export-manifest.test.ts";
|
|
18
19
|
const SRC_EXT = /\.(ts|tsx|js|jsx|mts|cts)$/;
|
|
19
20
|
const CODE_EXT = /\.(ts|tsx|js|jsx|mts|cts)$/;
|
|
20
21
|
function isSrcPath(p) {
|
|
@@ -112,15 +113,20 @@ export function collateralHits(tasks, repoRoot) {
|
|
|
112
113
|
const read = makeReader(repoRoot);
|
|
113
114
|
for (const t of tasks) {
|
|
114
115
|
// OBS-22: scopeGate accepts picomatch globs; advisory collateral warnings must agree.
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
const files = t.files.map((f) => f.replace(/^\.\//, ""));
|
|
117
|
+
const scoped = filesGlob(files);
|
|
118
|
+
const srcFiles = files.filter(isSrcPath);
|
|
119
|
+
const hits = [];
|
|
120
|
+
// Exact-enumerated scripts are an export-set contract: a new path has no name or symbol for the
|
|
121
|
+
// ordinary sweep to find, but the manifest test will reject it unless the task owns that oracle.
|
|
122
|
+
if (testFiles.includes(EXPORT_MANIFEST_TEST)
|
|
123
|
+
&& !scoped(EXPORT_MANIFEST_TEST)
|
|
124
|
+
&& files.some((path) => path.startsWith("scripts/") && !/[?*{[]/.test(path) && !existsSync(join(repoRoot, path))))
|
|
125
|
+
hits.push(EXPORT_MANIFEST_TEST);
|
|
119
126
|
// needles unioned across all src files in this task
|
|
120
127
|
const needles = [...new Set(srcFiles.flatMap(needlesFor))];
|
|
121
|
-
const hits = [];
|
|
122
128
|
for (const tf of testFiles) {
|
|
123
|
-
if (scoped(tf))
|
|
129
|
+
if (scoped(tf) || hits.includes(tf))
|
|
124
130
|
continue;
|
|
125
131
|
const text = read(tf);
|
|
126
132
|
if (text === null)
|
|
@@ -128,9 +134,9 @@ export function collateralHits(tasks, repoRoot) {
|
|
|
128
134
|
if (mentions(text, needles))
|
|
129
135
|
hits.push(tf);
|
|
130
136
|
}
|
|
131
|
-
// deterministic: walk
|
|
137
|
+
// deterministic: the special hit and walk are stable; sort their union for one canonical map.
|
|
132
138
|
if (hits.length)
|
|
133
|
-
map.set(t.id, hits);
|
|
139
|
+
map.set(t.id, hits.sort());
|
|
134
140
|
}
|
|
135
141
|
return map;
|
|
136
142
|
}
|
|
@@ -890,15 +896,8 @@ function activeReviewParticipation(repoRoot) {
|
|
|
890
896
|
* whose task objects carry no `context` gets byte-identical verdicts to the ones it got before the
|
|
891
897
|
* field existed, because an absent declaration grants no authority at all.
|
|
892
898
|
*
|
|
893
|
-
*
|
|
894
|
-
*
|
|
895
|
-
* PROGRAMMATIC compile whose target repo differs from process.cwd() resolves the wrong root and can
|
|
896
|
-
* miss that repo's own `review.criticalPaths`. Threading it is one line in src/compile/index.ts,
|
|
897
|
-
* outside this task's file scope. Two things bound the exposure meanwhile, both live: the critical
|
|
898
|
-
* set here is the UNION with the shipped defaults, so a wrong root can never lower enforcement below
|
|
899
|
-
* the shipped floor; and the review GATE — which is handed the run's real config — refuses to skip a
|
|
900
|
-
* critical path itself (src/gates/review.ts), so a lint this seam misses costs a late verdict rather
|
|
901
|
-
* than an unreviewed one. The compile lint is the early warning; the gate is the fail-closed backstop.
|
|
899
|
+
* The compile seam in src/compile/index.ts threads `compileSource`'s repo root here, so CLI tests
|
|
900
|
+
* and programmatic compiles check the same repository overlay the later run will use.
|
|
902
901
|
*/
|
|
903
902
|
export function taskUnitContractErrors(tasks, repoRoot = process.cwd(), review = activeReviewParticipation(repoRoot)) {
|
|
904
903
|
return [
|
package/dist/compile/index.d.ts
CHANGED
|
@@ -10,6 +10,9 @@ export type PlanIR = {
|
|
|
10
10
|
tasks: RunGraph["tasks"];
|
|
11
11
|
};
|
|
12
12
|
export type PlanFinalizationHook = (plan: PlanIR) => PlanIR;
|
|
13
|
+
export type CompileOptions = {
|
|
14
|
+
strict?: boolean;
|
|
15
|
+
};
|
|
13
16
|
export declare function finalizePlan(plan: PlanIR, src: string, repoRoot?: string): RunGraph;
|
|
14
|
-
export declare function compileSource(src: string, type?: SourceType, root?: string, beforeFinalize?: PlanFinalizationHook): RunGraph;
|
|
17
|
+
export declare function compileSource(src: string, type?: SourceType, root?: string, beforeFinalize?: PlanFinalizationHook, options?: CompileOptions): RunGraph;
|
|
15
18
|
export {};
|
package/dist/compile/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { parse } from "yaml";
|
|
3
4
|
import { validateGraph } from "../graph/schema.js";
|
|
4
5
|
import { taskUnitContractErrors } from "./collateral.js";
|
|
5
6
|
import { blocksCompile, ownershipFindings, renderOwnershipFinding } from "./ownership.js";
|
|
@@ -33,8 +34,40 @@ function detect(src) {
|
|
|
33
34
|
// tasks are too large to converge. A violation is a compile error, never a warning — the failures it
|
|
34
35
|
// prevents (silently dropped commits, a 28-dispatch task) are invisible until they have already cost
|
|
35
36
|
// hours, which is exactly the class of thing that has to fail at authoring time.
|
|
36
|
-
function
|
|
37
|
-
|
|
37
|
+
function repoOverlayMode(repoRoot) {
|
|
38
|
+
if (!repoRoot)
|
|
39
|
+
return undefined;
|
|
40
|
+
try {
|
|
41
|
+
const cfg = parse(readFileSync(join(repoRoot, ".tickmarkr", "config.yaml"), "utf8"));
|
|
42
|
+
return typeof cfg?.routing?.mode === "string" ? cfg.routing.mode : undefined;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function hasModeOverride(src) {
|
|
49
|
+
try {
|
|
50
|
+
for (const line of readFileSync(src, "utf8").split("\n")) {
|
|
51
|
+
if (/^##\s+T\d+:/i.test(line))
|
|
52
|
+
return false;
|
|
53
|
+
if (/^mode-override:\s*true\s*(?:#.*)?$/.test(line))
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch { /* unreadable specs fail elsewhere */ }
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
function enforceModeOverlay(graph, src, repoRoot) {
|
|
61
|
+
if (graph.spec.source !== "native" || graph.mode === undefined)
|
|
62
|
+
return;
|
|
63
|
+
const overlayMode = repoOverlayMode(repoRoot);
|
|
64
|
+
if (overlayMode === undefined || overlayMode === graph.mode || hasModeOverride(src))
|
|
65
|
+
return;
|
|
66
|
+
throw new CompileError(`${src} front-matter mode ${graph.mode} disagrees with repository routing.mode ${overlayMode}; `
|
|
67
|
+
+ `write mode-override: true beside the mode line to make the override explicit.`);
|
|
68
|
+
}
|
|
69
|
+
function enforceTaskUnitContract(g, src, repoRoot) {
|
|
70
|
+
const errors = taskUnitContractErrors(g.tasks, repoRoot);
|
|
38
71
|
if (errors.length > 0) {
|
|
39
72
|
throw new CompileError(`${src} violates the task unit contract (${errors.length} error${errors.length > 1 ? "s" : ""}):\n`
|
|
40
73
|
+ errors.map((e) => ` - ${e}`).join("\n"));
|
|
@@ -52,7 +85,8 @@ export function finalizePlan(plan, src, repoRoot) {
|
|
|
52
85
|
...(plan.base !== undefined ? { base: plan.base } : {}),
|
|
53
86
|
},
|
|
54
87
|
tasks: plan.tasks,
|
|
55
|
-
}), src);
|
|
88
|
+
}), src, repoRoot);
|
|
89
|
+
enforceModeOverlay(graph, src, repoRoot);
|
|
56
90
|
// overseer-217 removal condition, now paid: on this milestone's authored graph the conventional
|
|
57
91
|
// name map emitted 21 raw unowned-test findings; review found 1 real and 20 false, while intersecting
|
|
58
92
|
// with a direct import or command-entry spawn retained the real one and left 0 false positives. That
|
|
@@ -73,11 +107,11 @@ export function finalizePlan(plan, src, repoRoot) {
|
|
|
73
107
|
}
|
|
74
108
|
return graph;
|
|
75
109
|
}
|
|
76
|
-
function compilePlan(src, type, root) {
|
|
110
|
+
function compilePlan(src, type, root, options = {}) {
|
|
77
111
|
const kind = type ?? detect(src);
|
|
78
112
|
const graph = kind === "speckit" ? compileSpecKit(src)
|
|
79
113
|
: kind === "gsd" ? compileGsd(src, root)
|
|
80
|
-
: kind === "native" ? compileNative(src)
|
|
114
|
+
: kind === "native" ? compileNative(src, { strict: options.strict })
|
|
81
115
|
: kind === "prd" ? compilePrd(src)
|
|
82
116
|
: null;
|
|
83
117
|
if (!graph) {
|
|
@@ -93,6 +127,6 @@ function compilePlan(src, type, root) {
|
|
|
93
127
|
tasks: graph.tasks,
|
|
94
128
|
};
|
|
95
129
|
}
|
|
96
|
-
export function compileSource(src, type, root, beforeFinalize = (plan) => plan) {
|
|
97
|
-
return finalizePlan(beforeFinalize(compilePlan(src, type, root)), src, root);
|
|
130
|
+
export function compileSource(src, type, root, beforeFinalize = (plan) => plan, options = {}) {
|
|
131
|
+
return finalizePlan(beforeFinalize(compilePlan(src, type, root, options)), src, root);
|
|
98
132
|
}
|
package/dist/compile/native.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export declare const COLLECTABLE_TESTS = "tests/**/*.test.ts";
|
|
|
17
17
|
export declare const LEGACY_PREFIX: string;
|
|
18
18
|
export declare const TICKMARKR_NATIVE_MARKER: RegExp;
|
|
19
19
|
export declare const NATIVE_MARKER: RegExp;
|
|
20
|
-
export declare const AUTHORING_LINT_CODES: readonly ["criterion-scope", "dependency-closure", "dependency-coupling", "proxy-metric", "external-referent", "closed-enumeration", "one-behavior", "concern-bundle", "seam-exists"];
|
|
20
|
+
export declare const AUTHORING_LINT_CODES: readonly ["criterion-scope", "dependency-closure", "dependency-coupling", "proxy-metric", "external-referent", "closed-enumeration", "one-behavior", "concern-bundle", "seam-exists", "fence-symbol-absent"];
|
|
21
21
|
export type AuthoringLintCode = (typeof AUTHORING_LINT_CODES)[number];
|
|
22
22
|
export interface AuthoringLintFinding {
|
|
23
23
|
code: AuthoringLintCode;
|
|
@@ -31,5 +31,7 @@ export interface AuthoringLintFinding {
|
|
|
31
31
|
* compile boundary; the remaining checks are review findings, emitted by compileNative below.
|
|
32
32
|
*/
|
|
33
33
|
export declare function authoringLintFindings(tasks: readonly Task[], file: string): AuthoringLintFinding[];
|
|
34
|
-
export declare function compileNative(file: string
|
|
34
|
+
export declare function compileNative(file: string, options?: {
|
|
35
|
+
strict?: boolean;
|
|
36
|
+
}): RunGraph;
|
|
35
37
|
export declare function specTemplate(): string;
|
package/dist/compile/native.js
CHANGED
|
@@ -83,6 +83,7 @@ export const AUTHORING_LINT_CODES = [
|
|
|
83
83
|
"one-behavior",
|
|
84
84
|
"concern-bundle",
|
|
85
85
|
"seam-exists",
|
|
86
|
+
"fence-symbol-absent",
|
|
86
87
|
];
|
|
87
88
|
// The authoring frame's observable oracle is the branch-tip test corpus, never the mutable index or
|
|
88
89
|
// an author's untracked checkout. Load it once per repository: one bounded git read replaces a grep
|
|
@@ -217,6 +218,31 @@ function criterionScopeFinding(task, text, criterion, id, tests) {
|
|
|
217
218
|
detail: `criterion names asserting file${missing.length === 1 ? "" : "s"} outside expanded files[] and context[]: ${missing.join(", ")} — ${remedy}`,
|
|
218
219
|
};
|
|
219
220
|
}
|
|
221
|
+
// OBS-898: the fence lint's corpus is listed ONCE per compile by git (tracked plus untracked-not-ignored,
|
|
222
|
+
// the same set a checkout walk sees minus the ignored trees) and filtered per task. The walk it replaces
|
|
223
|
+
// descended every dot-directory (.tickmarkr/runs alone held 3 799 files here) once PER TASK, so a
|
|
224
|
+
// 20-task spec compiled in seconds and the committed-spec corpus test timed out. Fail-open like
|
|
225
|
+
// testsAtHead: no git answer means no corpus, and the lint stays quiet rather than wrong.
|
|
226
|
+
function repoFileList(root) {
|
|
227
|
+
const ls = spawnSync("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8", maxBuffer: 1 << 25 });
|
|
228
|
+
if (ls.status !== 0 || typeof ls.stdout !== "string")
|
|
229
|
+
return [];
|
|
230
|
+
return ls.stdout.split("\0").filter(Boolean).sort();
|
|
231
|
+
}
|
|
232
|
+
function repoFiles(files, entries) {
|
|
233
|
+
const scoped = filesGlob(entries.map((entry) => entry.replace(/^\.\//, "")));
|
|
234
|
+
return files.filter(scoped);
|
|
235
|
+
}
|
|
236
|
+
const PRESERVATION_RE = /\b(?:keeps?|preserves?|retains?|does\s+not\s+weaken|do\s+not\s+weaken|not\s+weaken)\b/i;
|
|
237
|
+
const IDENTIFIER_SHAPED_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
238
|
+
function fencedIdentifiers(text) {
|
|
239
|
+
if (!PRESERVATION_RE.test(text))
|
|
240
|
+
return [];
|
|
241
|
+
return [...new Set([...text.matchAll(/`([^`\n]{1,120})`/g)]
|
|
242
|
+
.map((match) => match[1].trim())
|
|
243
|
+
.filter((token) => IDENTIFIER_SHAPED_RE.test(token) && /[A-Z0-9_$]/.test(token)))]
|
|
244
|
+
.sort();
|
|
245
|
+
}
|
|
220
246
|
function exportedIdentifier(root, files, identifier) {
|
|
221
247
|
const escaped = identifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
222
248
|
const declaration = new RegExp(`\\bexport\\s+(?:(?:declare|default|async)\\s+)*(?:function|class|const|let|var|interface|type)\\s+${escaped}\\b|\\bexport\\s*\\{[^}]*\\b${escaped}\\b`);
|
|
@@ -240,14 +266,33 @@ export function authoringLintFindings(tasks, file) {
|
|
|
240
266
|
const root = repositoryRoot(file);
|
|
241
267
|
let indexedTests;
|
|
242
268
|
const testIndex = () => indexedTests ??= root ? testsAtHead(root) : [];
|
|
269
|
+
let indexedFiles;
|
|
270
|
+
const fileIndex = () => indexedFiles ??= root ? repoFileList(root) : [];
|
|
243
271
|
const findings = [];
|
|
244
272
|
for (const task of tasks) {
|
|
245
273
|
const criteria = criterionTexts(task);
|
|
274
|
+
const taskFiles = root && task.files.length ? repoFiles(fileIndex(), task.files) : [];
|
|
275
|
+
const taskTexts = new Map(taskFiles.map((path) => {
|
|
276
|
+
try {
|
|
277
|
+
return [path, readFileSync(join(root, path), "utf8")];
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return [path, ""];
|
|
281
|
+
}
|
|
282
|
+
}));
|
|
246
283
|
for (const [index, text] of criteria.entries()) {
|
|
247
284
|
const needsTestIndex = /`[^`\n]{2,120}`|\b\d+\s*(?:\/|of)\s*\d+\b|\b[A-Za-z0-9_.-]+\.test\.ts\b/.test(text);
|
|
248
285
|
const scope = criterionScopeFinding(task, text, index + 1, id, needsTestIndex ? testIndex() : []);
|
|
249
286
|
if (scope)
|
|
250
287
|
findings.push(scope);
|
|
288
|
+
for (const symbol of fencedIdentifiers(text)) {
|
|
289
|
+
if (!taskTexts.size || [...taskTexts.values()].some((body) => body.includes(symbol)))
|
|
290
|
+
continue;
|
|
291
|
+
findings.push({
|
|
292
|
+
code: "fence-symbol-absent", fixtureId: id, taskId: task.id, criterion: index + 1,
|
|
293
|
+
detail: `preservation fence cites \`${symbol}\` but that symbol has zero hits in this task's files[] (${taskFiles.join(", ")}) — OBS-604`,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
251
296
|
if (/line[- ]count|physical line|not greater than (?:the|\d)|no larger than|at most \d+ (?:lines|bytes)/i.test(text)) {
|
|
252
297
|
findings.push({ code: "proxy-metric", fixtureId: id, taskId: task.id, criterion: index + 1, detail: "criterion uses a proxy size metric; state the structural intent instead" });
|
|
253
298
|
}
|
|
@@ -308,7 +353,7 @@ function renderAuthoringFinding(finding) {
|
|
|
308
353
|
const criterion = finding.criterion === undefined ? "" : ` criterion ${finding.criterion}`;
|
|
309
354
|
return `tickmarkr: authoring-lint[${finding.code}] fixture ${finding.fixtureId} task ${finding.taskId}${criterion}: ${finding.detail}`;
|
|
310
355
|
}
|
|
311
|
-
export function compileNative(file) {
|
|
356
|
+
export function compileNative(file, options = {}) {
|
|
312
357
|
if (!existsSync(file))
|
|
313
358
|
throw new CompileError(`no such native spec file: ${file}`);
|
|
314
359
|
const content = readFileSync(file, "utf8");
|
|
@@ -638,13 +683,17 @@ export function compileNative(file) {
|
|
|
638
683
|
tasks,
|
|
639
684
|
});
|
|
640
685
|
const authoringFindings = authoringLintFindings(result.tasks, file);
|
|
641
|
-
|
|
686
|
+
const blockingCodes = new Set(["criterion-scope", "fence-symbol-absent"]);
|
|
687
|
+
const blockingFindings = authoringFindings.filter((finding) => options.strict || blockingCodes.has(finding.code));
|
|
688
|
+
for (const finding of authoringFindings.filter((finding) => !blockingFindings.includes(finding))) {
|
|
642
689
|
console.warn(renderAuthoringFinding(finding));
|
|
643
690
|
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
691
|
+
if (blockingFindings.length > 0) {
|
|
692
|
+
const strict = options.strict ? " under --strict" : "";
|
|
693
|
+
const onlyCriterionScope = blockingFindings.every(({ code }) => code === "criterion-scope");
|
|
694
|
+
const label = onlyCriterionScope && !options.strict ? "the criterion-scope authoring lint" : `authoring lints${strict}`;
|
|
695
|
+
throw new CompileError(`${file} violates ${label} (${blockingFindings.length} error${blockingFindings.length === 1 ? "" : "s"}):\n`
|
|
696
|
+
+ blockingFindings.map((finding) => ` - ${renderAuthoringFinding(finding)}`).join("\n"));
|
|
648
697
|
}
|
|
649
698
|
// v1.19 read-old/write-new: a plain-string acceptance item compiles as a judge oracle. This is the
|
|
650
699
|
// one-time nudge toward typed oracles (command/test/judge); PRD/Spec Kit/GSD stay silent (untouched).
|
|
@@ -805,9 +854,9 @@ acceptance is required on every task (a nested list of observable outcomes).
|
|
|
805
854
|
set closed — this milestone paid a halted run to learn that the two populations are not identical.
|
|
806
855
|
- SPIKE-THE-CONTRACT-THEN-SCOPE trigger question: COULD A TEST THIS TASK DOES NOT OWN BE ASSERTING THE
|
|
807
856
|
THING I AM CHANGING? "I'D HAVE TO GREP TO KNOW" IS YES. This applies to observable contracts:
|
|
808
|
-
execution order, event-stream order, diagnostics/output sets, CLI surface, serialised formats,
|
|
809
|
-
timing measurements. If yes, implement the change as a
|
|
810
|
-
reds, THEN scope files[].
|
|
857
|
+
execution order, event-stream order, diagnostics/output sets, CLI surface, serialised formats,
|
|
858
|
+
timing measurements, or adding or removing a shipped file. If yes, implement the change as a
|
|
859
|
+
throwaway spike, run the full suite, read the reds, THEN scope files[].
|
|
811
860
|
- Caveat: a spike measures ONE implementation. It converts unknown collateral into
|
|
812
861
|
measured-for-one-specimen collateral; it does NOT make its reds the closed blocker set for every
|
|
813
862
|
route. A worker taking a different route can still red on unowned collateral; that remains a PLAN
|