tickmarkr 1.89.0 → 1.90.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/doctor.d.ts +27 -0
- package/dist/cli/commands/doctor.js +76 -0
- package/dist/cli/commands/init.js +1 -0
- package/dist/cli/commands/verify.d.ts +9 -0
- package/dist/cli/commands/verify.js +177 -0
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +3 -1
- package/dist/compile/collateral.js +7 -7
- package/dist/compile/common.js +2 -2
- package/dist/compile/native.d.ts +15 -1
- package/dist/compile/native.js +209 -3
- package/dist/config/config.js +2 -2
- package/dist/gates/baseline.d.ts +39 -5
- package/dist/gates/baseline.js +67 -8
- package/dist/gates/scope.js +3 -3
- package/dist/graph/files-glob.d.ts +18 -0
- package/dist/graph/files-glob.js +22 -0
- package/dist/run/daemon.d.ts +2 -0
- package/dist/run/daemon.js +14 -7
- package/dist/run/git.d.ts +3 -0
- package/dist/run/git.js +6 -3
- package/dist/run/merge.d.ts +4 -1
- package/dist/run/merge.js +26 -7
- package/fixtures/authoring-lints/01-awk-range-self-pass.spec.md +12 -0
- package/fixtures/authoring-lints/02-judge-text-key-miss.spec.md +7 -0
- package/fixtures/authoring-lints/03-c1-t41-rendered-observable.spec.md +8 -0
- package/fixtures/authoring-lints/04-c1-t24-named-file.spec.md +8 -0
- package/fixtures/authoring-lints/05-c2-t24-t28-dep-inversion.spec.md +7 -0
- package/fixtures/authoring-lints/06-c2-denumbered-coupling.spec.md +7 -0
- package/fixtures/authoring-lints/07-c3a-t41-line-count-proxy.spec.md +7 -0
- package/fixtures/authoring-lints/08-c3b-t41-governance-referent.spec.md +7 -0
- package/fixtures/authoring-lints/09-c4-universals-without-pointer.spec.md +7 -0
- package/fixtures/authoring-lints/10-c5-t34-conjunct-flood.spec.md +7 -0
- package/fixtures/authoring-lints/11-c6-t34-q3-q9-q20-bundle.spec.md +7 -0
- package/fixtures/authoring-lints/12-c7-t24-prose-seam.spec.md +8 -0
- package/package.json +1 -1
- package/skills/tickmarkr-overseer/SKILL.md +5 -0
- package/skills/tickmarkr-overseer/scripts/watch-contamination.sh +4 -1
- package/skills/tickmarkr-overseer/scripts/watch-pending-input.sh +18 -0
|
@@ -9,4 +9,31 @@ export type DoctorOpts = {
|
|
|
9
9
|
catalog?: CatalogReadResult;
|
|
10
10
|
catalogNow?: () => Date;
|
|
11
11
|
};
|
|
12
|
+
/**
|
|
13
|
+
* Q122s (TRIAL T-OBS-3): tickmarkr provisions run worktrees INSIDE the repo
|
|
14
|
+
* (.tickmarkr/worktrees.noindex/). A target repo whose test runner collects with
|
|
15
|
+
* repo-wide globs (jest's default `**/__tests__/**` shape, vitest's default include)
|
|
16
|
+
* scans every suite once per live worktree — duplicate-suite interference red the
|
|
17
|
+
* baseline AND tip-verify on SentioQ's first external run, a class invisible to
|
|
18
|
+
* dogfooding (this repo's own includes are rooted at tests/).
|
|
19
|
+
*
|
|
20
|
+
* Q130s (TRIAL T-OBS-6): the ignore must be <rootDir>-anchored. Jest
|
|
21
|
+
* testPathIgnorePatterns are REGEXES matched against the ABSOLUTE test path; an
|
|
22
|
+
* unanchored "/.tickmarkr/" also matches every suite INSIDE a worktree provisioned at
|
|
23
|
+
* <repo>/.tickmarkr/worktrees.noindex/… — jest discovers nothing there and the whole
|
|
24
|
+
* gate battery fails closed (SentioQ run 2: 10 dispatches, 0 merges). Anchored
|
|
25
|
+
* patterns resolve <rootDir> to the worktree itself and stay inert. Vitest
|
|
26
|
+
* include/exclude globs match root-relative paths, so they cannot self-match, but the
|
|
27
|
+
* prescribed shape is root-anchored anyway.
|
|
28
|
+
*
|
|
29
|
+
* Text-level heuristic, advisory only — never enters health/doctor.json or routing.
|
|
30
|
+
* ponytail: ceilings — comments are stripped before judgment, but an include built
|
|
31
|
+
* from variables is still judged by its literal text, and a pattern string containing
|
|
32
|
+
* `//` would be truncated by the line-comment strip. Each costs one advisory row,
|
|
33
|
+
* nothing more.
|
|
34
|
+
*/
|
|
35
|
+
export declare function runnerIgnoreFinding(cwd: string): {
|
|
36
|
+
verdict: "pass" | "warn";
|
|
37
|
+
detail: string;
|
|
38
|
+
} | undefined;
|
|
12
39
|
export declare function doctor(_argv: string[], cwd?: string, adapters?: WorkerAdapter[], opts?: DoctorOpts): Promise<string>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { allAdapters, binaryShadowWarnings, detectCandidateClis, flagDriftWarnings, modelAliasExclusions, modelAliasLine, probeAll, probeModels, servableExclusions, servabilityLine, writeDoctor } from "../../adapters/registry.js";
|
|
4
5
|
import { CLAUDE_ALIAS_IDENTITY_STAMPS, claudeCode, resolveClaudeAliasIdentity } from "../../adapters/claude-code.js";
|
|
@@ -13,6 +14,77 @@ import { readCachedCatalog, refreshCatalogCommand } from "../../adapters/catalog
|
|
|
13
14
|
const visual = () => process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
|
|
14
15
|
const alignedStatusRow = (verdict, key, value) => ` ${statusRow(verdict, kvRow(key, value).slice(2))}`;
|
|
15
16
|
const attentionRow = (text) => ` ${statusRow("warn", text)}`;
|
|
17
|
+
/**
|
|
18
|
+
* Q122s (TRIAL T-OBS-3): tickmarkr provisions run worktrees INSIDE the repo
|
|
19
|
+
* (.tickmarkr/worktrees.noindex/). A target repo whose test runner collects with
|
|
20
|
+
* repo-wide globs (jest's default `**/__tests__/**` shape, vitest's default include)
|
|
21
|
+
* scans every suite once per live worktree — duplicate-suite interference red the
|
|
22
|
+
* baseline AND tip-verify on SentioQ's first external run, a class invisible to
|
|
23
|
+
* dogfooding (this repo's own includes are rooted at tests/).
|
|
24
|
+
*
|
|
25
|
+
* Q130s (TRIAL T-OBS-6): the ignore must be <rootDir>-anchored. Jest
|
|
26
|
+
* testPathIgnorePatterns are REGEXES matched against the ABSOLUTE test path; an
|
|
27
|
+
* unanchored "/.tickmarkr/" also matches every suite INSIDE a worktree provisioned at
|
|
28
|
+
* <repo>/.tickmarkr/worktrees.noindex/… — jest discovers nothing there and the whole
|
|
29
|
+
* gate battery fails closed (SentioQ run 2: 10 dispatches, 0 merges). Anchored
|
|
30
|
+
* patterns resolve <rootDir> to the worktree itself and stay inert. Vitest
|
|
31
|
+
* include/exclude globs match root-relative paths, so they cannot self-match, but the
|
|
32
|
+
* prescribed shape is root-anchored anyway.
|
|
33
|
+
*
|
|
34
|
+
* Text-level heuristic, advisory only — never enters health/doctor.json or routing.
|
|
35
|
+
* ponytail: ceilings — comments are stripped before judgment, but an include built
|
|
36
|
+
* from variables is still judged by its literal text, and a pattern string containing
|
|
37
|
+
* `//` would be truncated by the line-comment strip. Each costs one advisory row,
|
|
38
|
+
* nothing more.
|
|
39
|
+
*/
|
|
40
|
+
export function runnerIgnoreFinding(cwd) {
|
|
41
|
+
const readIf = (p) => (existsSync(join(cwd, p)) ? readFileSync(join(cwd, p), "utf8") : "");
|
|
42
|
+
const pkgText = readIf("package.json");
|
|
43
|
+
let pkg = {};
|
|
44
|
+
try {
|
|
45
|
+
pkg = JSON.parse(pkgText || "{}");
|
|
46
|
+
}
|
|
47
|
+
catch { /* unparseable manifest: judge config files alone */ }
|
|
48
|
+
const jestConfig = ["jest.config.js", "jest.config.cjs", "jest.config.mjs", "jest.config.ts", "jest.config.json"].find((p) => existsSync(join(cwd, p)));
|
|
49
|
+
const vitestConfig = ["vitest.config.ts", "vitest.config.js", "vitest.config.mts", "vitest.config.mjs", "vitest.config.cts"].find((p) => existsSync(join(cwd, p)));
|
|
50
|
+
const testScript = pkg.scripts?.test ?? "";
|
|
51
|
+
const isJest = jestConfig !== undefined || pkg.jest !== undefined || /\bjest\b/.test(testScript);
|
|
52
|
+
const isVitest = !isJest && (vitestConfig !== undefined || /\bvitest\b/.test(testScript));
|
|
53
|
+
if (!isJest && !isVitest)
|
|
54
|
+
return undefined;
|
|
55
|
+
// Comments are stripped before judgment: a commented-out ignore must not false-pass,
|
|
56
|
+
// and a prose comment mentioning .tickmarkr must not false-warn the anchor probe.
|
|
57
|
+
const stripComments = (s) => s.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, "");
|
|
58
|
+
const configText = stripComments(isJest
|
|
59
|
+
? `${jestConfig ? readIf(jestConfig) : ""}${pkg.jest ? JSON.stringify(pkg.jest) : ""}`
|
|
60
|
+
: vitestConfig ? readIf(vitestConfig) : "");
|
|
61
|
+
if (/\.tickmarkr|worktrees\.noindex/.test(configText)) {
|
|
62
|
+
// Q130s self-match probe: erase every <rootDir>-anchored token (through the end of
|
|
63
|
+
// its quoted string); any surviving mention is a pattern that would also match
|
|
64
|
+
// inside a provisioned worktree (jest regex-vs-absolute-path semantics only).
|
|
65
|
+
const unanchored = configText.replace(/<rootDir>[^"'`\s]*/g, "");
|
|
66
|
+
if (isJest && /\.tickmarkr|worktrees\.noindex/.test(unanchored)) {
|
|
67
|
+
return {
|
|
68
|
+
verdict: "warn",
|
|
69
|
+
detail: `jest ignore for .tickmarkr/ is not <rootDir>-anchored — testPathIgnorePatterns are regexes against ABSOLUTE paths, so inside run worktrees (provisioned under .tickmarkr/worktrees.noindex/) it matches EVERY suite and the test gate fails closed; use "<rootDir>/\\\\.tickmarkr/" in ${jestConfig ?? "the package.json jest block"}`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return { verdict: "pass", detail: `${isJest ? "jest" : "vitest"} config ignores .tickmarkr/ — run worktrees stay out of the suite` };
|
|
73
|
+
}
|
|
74
|
+
// Rooted collection globs never reach .tickmarkr/…; only repo-wide `**/`-style patterns (or
|
|
75
|
+
// runner defaults, which are repo-wide) can collect worktree copies.
|
|
76
|
+
const repoWide = configText === "" || /["'`]\*\*\//.test(configText);
|
|
77
|
+
if (!repoWide) {
|
|
78
|
+
return { verdict: "pass", detail: `${isJest ? "jest" : "vitest"} collection globs are rooted — run worktrees are not collectable` };
|
|
79
|
+
}
|
|
80
|
+
const remedy = isJest
|
|
81
|
+
? `add "<rootDir>/\\\\.tickmarkr/" to testPathIgnorePatterns in ${jestConfig ?? "the package.json jest block"}`
|
|
82
|
+
: `add ".tickmarkr/**" to test.exclude in ${vitestConfig ?? "a vitest config"}`;
|
|
83
|
+
return {
|
|
84
|
+
verdict: "warn",
|
|
85
|
+
detail: `${isJest ? "jest" : "vitest"} collects repo-wide and will scan .tickmarkr/ run worktrees (duplicate suites, false reds) — ${remedy}`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
16
88
|
export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(), opts = {}) {
|
|
17
89
|
if (_argv.length === 1 && _argv[0] === "--refresh-catalog") {
|
|
18
90
|
const refreshed = await refreshCatalogCommand({ repoRoot: cwd, now: opts.catalogNow });
|
|
@@ -90,6 +162,10 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
90
162
|
rows.push(...detectCandidateClis({ cwd }).map(({ binary, path }) => alignedStatusRow("warn", binary, `detected at ${path} (no drive contract — not routable)`)));
|
|
91
163
|
const herdr = HerdrDriver.available();
|
|
92
164
|
rows.push(alignedStatusRow(herdr ? "pass" : "fail", "herdr", herdr ? "driver available (HERDR_ENV=1)" : "not detected — subprocess driver will be used"));
|
|
165
|
+
// Q122s: the target repo's runner must not scan tickmarkr's own worktrees (advisory row).
|
|
166
|
+
const runnerIgnore = runnerIgnoreFinding(cwd);
|
|
167
|
+
if (runnerIgnore)
|
|
168
|
+
rows.push(alignedStatusRow(runnerIgnore.verdict, "test-runner", runnerIgnore.detail));
|
|
93
169
|
// v1.22 T5: workspace-trust pre-flight — per installed adapter: trusted | seeded | action-required | n/a.
|
|
94
170
|
// action-required names the exact one-time command (or dialog) the operator must run once.
|
|
95
171
|
rows.push(legend("workspace trust:"));
|
|
@@ -87,6 +87,7 @@ tickmarkr compiles repository specs into isolated, independently verified agent
|
|
|
87
87
|
- \`tickmarkr resume <runId>\` — continue a paused or failed run
|
|
88
88
|
- \`tickmarkr approve <runId> <taskId>\` — release a human gate
|
|
89
89
|
- \`tickmarkr report <runId> --md\` — execution record beside the spec
|
|
90
|
+
- \`tickmarkr verify --base <ref>\` — run the gate battery standalone against merge-base(base, HEAD)..HEAD: no daemon, no retries, one fail-closed verdict (\`--criteria <file>\` or \`--task <id>\` adds the semantic gates; \`--no-review\` for deterministic-only)
|
|
90
91
|
|
|
91
92
|
Loop: compile → plan → run → report. Watch the journal for run-end rather than polling workers.
|
|
92
93
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type Assignment, type BillingChannel } from "../../adapters/types.js";
|
|
2
|
+
import { type AcceptanceItem } from "../../graph/schema.js";
|
|
3
|
+
export declare function parseCriteria(text: string): AcceptanceItem[];
|
|
4
|
+
export declare const HUMAN_CHANNEL: BillingChannel;
|
|
5
|
+
export declare const HUMAN_AUTHOR: Assignment;
|
|
6
|
+
export declare function verify(argv: string[], cwd?: string): Promise<{
|
|
7
|
+
out: string;
|
|
8
|
+
code: number;
|
|
9
|
+
}>;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
6
|
+
import { allAdapters, probeAll, readDoctor, rolePools } from "../../adapters/registry.js";
|
|
7
|
+
import { channelKey } from "../../adapters/types.js";
|
|
8
|
+
import { loadConfig } from "../../config/config.js";
|
|
9
|
+
import { captureBaseline, detectGateCommands } from "../../gates/baseline.js";
|
|
10
|
+
import { runGates } from "../../gates/run-gates.js";
|
|
11
|
+
import { getTask, loadGraph } from "../../graph/graph.js";
|
|
12
|
+
import { GATE_NAMES } from "../../graph/schema.js";
|
|
13
|
+
import { linkNodeModules, removeWorktree, shGit, shGitOk } from "../../run/git.js";
|
|
14
|
+
/**
|
|
15
|
+
* tickmarkr verify — the gate battery as a standalone command (OPERATING-MODEL-2026-08-11 item 3).
|
|
16
|
+
*
|
|
17
|
+
* Runs the existing seven-gate pipeline (build/test/lint against a captured base baseline, then
|
|
18
|
+
* evidence, scope, acceptance ‖ review) against `merge-base(--base, HEAD)..HEAD` of the CURRENT
|
|
19
|
+
* checkout. No daemon, no worktree lifecycle, no retries, no approvals, no resumable state: one
|
|
20
|
+
* invocation, one immutable candidate, one machine-readable verdict. A verifier failure costs one
|
|
21
|
+
* rerun. Same fail-closed guarantees — this is a thin caller of runGates, not a second pipeline.
|
|
22
|
+
*/
|
|
23
|
+
// "- test: x" / "test: x" → typed oracle; plain lines → judge criterion (native.ts's exact grammar).
|
|
24
|
+
const ORACLE_LINE = /^(command|test|judge):\s*(.+)$/;
|
|
25
|
+
export function parseCriteria(text) {
|
|
26
|
+
return text
|
|
27
|
+
.split("\n")
|
|
28
|
+
.map((l) => l.trim().replace(/^-\s+/, ""))
|
|
29
|
+
.filter((l) => l && !l.startsWith("#"))
|
|
30
|
+
.map((l) => {
|
|
31
|
+
const m = ORACLE_LINE.exec(l);
|
|
32
|
+
if (!m)
|
|
33
|
+
return l;
|
|
34
|
+
const body = m[2].trim();
|
|
35
|
+
return m[1] === "command" ? { oracle: "command", command: body }
|
|
36
|
+
: m[1] === "test" ? { oracle: "test", test: body }
|
|
37
|
+
: { oracle: "judge", text: body };
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
// The human-author sentinel: pickReviewer resolves the author IN the channel list and excludes that
|
|
41
|
+
// vendor (fail-closed when unresolvable). A human/plain-session diff has no vendor to exclude, so
|
|
42
|
+
// verify models the author as a "human" vendor channel — resolvable, excludes nothing real.
|
|
43
|
+
export const HUMAN_CHANNEL = { adapter: "human", vendor: "human", model: "human", channel: "sub", tier: "frontier" };
|
|
44
|
+
export const HUMAN_AUTHOR = { adapter: "human", model: "human", channel: "sub", tier: "frontier" };
|
|
45
|
+
export async function verify(argv, cwd = process.cwd()) {
|
|
46
|
+
const { values } = parseArgs({
|
|
47
|
+
args: argv,
|
|
48
|
+
options: {
|
|
49
|
+
base: { type: "string", default: "main" },
|
|
50
|
+
criteria: { type: "string" },
|
|
51
|
+
task: { type: "string" },
|
|
52
|
+
files: { type: "string", multiple: true },
|
|
53
|
+
author: { type: "string" },
|
|
54
|
+
baseline: { type: "string" },
|
|
55
|
+
json: { type: "boolean", default: false },
|
|
56
|
+
"no-review": { type: "boolean", default: false },
|
|
57
|
+
"no-acceptance": { type: "boolean", default: false },
|
|
58
|
+
},
|
|
59
|
+
allowPositionals: false,
|
|
60
|
+
});
|
|
61
|
+
const cfg = loadConfig(cwd);
|
|
62
|
+
const head = (await shGitOk("git rev-parse HEAD", cwd)).trim();
|
|
63
|
+
const baseTip = (await shGitOk(`git rev-parse '${values.base}'`, cwd).catch(() => {
|
|
64
|
+
throw new Error(`--base ${values.base} is not a resolvable ref — pass --base <ref> naming the branch this diff targets`);
|
|
65
|
+
})).trim();
|
|
66
|
+
const mergeBase = (await shGitOk(`git merge-base '${baseTip}' HEAD`, cwd)).trim();
|
|
67
|
+
if (mergeBase === head) {
|
|
68
|
+
throw new Error(`nothing to verify — HEAD is contained in ${values.base} (merge-base == HEAD). Commit work on a branch first.`);
|
|
69
|
+
}
|
|
70
|
+
// Criteria: an explicit compiled task, a criteria file, or none (deterministic gates + review only).
|
|
71
|
+
let acceptance = [];
|
|
72
|
+
let files = values.files ?? [];
|
|
73
|
+
let goal = `independent verification of the ${values.base}..HEAD diff`;
|
|
74
|
+
if (values.task) {
|
|
75
|
+
const t = getTask(loadGraph(cwd), values.task);
|
|
76
|
+
if (!t)
|
|
77
|
+
throw new Error(`--task ${values.task}: no such task in .tickmarkr/graph.json`);
|
|
78
|
+
acceptance = t.acceptance;
|
|
79
|
+
if (!files.length)
|
|
80
|
+
files = t.files;
|
|
81
|
+
goal = t.goal;
|
|
82
|
+
}
|
|
83
|
+
else if (values.criteria) {
|
|
84
|
+
acceptance = parseCriteria(readFileSync(values.criteria, "utf8"));
|
|
85
|
+
if (!acceptance.length)
|
|
86
|
+
throw new Error(`--criteria ${values.criteria}: no criteria found (bullets or command:/test:/judge: lines)`);
|
|
87
|
+
}
|
|
88
|
+
const wantAcceptance = acceptance.length > 0 && !values["no-acceptance"];
|
|
89
|
+
const wantReview = !values["no-review"];
|
|
90
|
+
const gates = GATE_NAMES.filter((g) => (g !== "acceptance" || wantAcceptance) && (g !== "review" || wantReview));
|
|
91
|
+
const task = {
|
|
92
|
+
id: "VERIFY", title: "standalone verification", goal, shape: "implement", complexity: 5,
|
|
93
|
+
deps: [], files, context: [], acceptance: acceptance.length ? acceptance : ["(deterministic verification only)"],
|
|
94
|
+
gates, humanGate: false, status: "pending",
|
|
95
|
+
evidence: { commits: [], artifacts: [], gateResults: [] },
|
|
96
|
+
};
|
|
97
|
+
const commands = detectGateCommands(cwd, cfg);
|
|
98
|
+
// Baseline: --baseline file > cached capture for this merge-base > fresh capture on a detached
|
|
99
|
+
// temp worktree of the merge-base (so pre-existing failures on base are forgiven, exactly as a run).
|
|
100
|
+
// ALL verify state (cache, base worktree, artifacts) lives OUTSIDE the repo: verify gates the repo
|
|
101
|
+
// root itself, so any file it wrote there would trip the battery's own dirty-worktree refusal.
|
|
102
|
+
// ponytail: tmpdir means the baseline cache dies on reboot/cleanup — worst case is one re-capture.
|
|
103
|
+
const stateDir = join(tmpdir(), "tickmarkr-verify", createHash("sha256").update(cwd).digest("hex").slice(0, 12));
|
|
104
|
+
mkdirSync(stateDir, { recursive: true });
|
|
105
|
+
let baseline;
|
|
106
|
+
const cachePath = join(stateDir, `baseline-${mergeBase.slice(0, 12)}.json`);
|
|
107
|
+
if (values.baseline) {
|
|
108
|
+
baseline = JSON.parse(readFileSync(values.baseline, "utf8"));
|
|
109
|
+
}
|
|
110
|
+
else if (existsSync(cachePath)) {
|
|
111
|
+
console.error(`verify: reusing cached baseline for ${mergeBase.slice(0, 12)} (${cachePath})`);
|
|
112
|
+
baseline = JSON.parse(readFileSync(cachePath, "utf8"));
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
const baseDir = join(stateDir, `base-${mergeBase.slice(0, 12)}`);
|
|
116
|
+
console.error(`verify: capturing baseline at merge-base ${mergeBase.slice(0, 12)} (one-time per base; cached at ${cachePath})`);
|
|
117
|
+
await shGit(`git worktree remove --force '${baseDir}'`, cwd); // stale leftover from an interrupted run
|
|
118
|
+
await shGitOk(`git worktree add --detach '${baseDir}' '${mergeBase}'`, cwd);
|
|
119
|
+
try {
|
|
120
|
+
linkNodeModules(cwd, baseDir, { force: true });
|
|
121
|
+
baseline = await captureBaseline(baseDir, commands);
|
|
122
|
+
writeFileSync(cachePath, JSON.stringify(baseline, null, 2));
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
await removeWorktree(cwd, baseDir);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// LLM seats only when a semantic gate will run.
|
|
129
|
+
let channels = [];
|
|
130
|
+
let judgeChannels;
|
|
131
|
+
let author = HUMAN_AUTHOR;
|
|
132
|
+
const adapters = allAdapters();
|
|
133
|
+
if (wantAcceptance || wantReview) {
|
|
134
|
+
const health = readDoctor(cwd) ?? (await probeAll(adapters));
|
|
135
|
+
const pools = rolePools(cfg, adapters, health);
|
|
136
|
+
judgeChannels = pools.judge;
|
|
137
|
+
channels = pools.review;
|
|
138
|
+
if (values.author && values.author !== "human") {
|
|
139
|
+
const [adapter, ...rest] = values.author.split(":");
|
|
140
|
+
const model = rest.join(":");
|
|
141
|
+
const c = channels.find((ch) => ch.adapter === adapter && ch.model === model);
|
|
142
|
+
if (!c) {
|
|
143
|
+
throw new Error(`--author ${values.author} does not name a discoverable review channel — one of: ${channels.map(channelKey).join(", ") || "(none)"}`);
|
|
144
|
+
}
|
|
145
|
+
author = { adapter: c.adapter, model: c.model, channel: c.channel, tier: c.tier };
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
channels = [...channels, HUMAN_CHANNEL];
|
|
149
|
+
}
|
|
150
|
+
if (wantReview && !channels.some((c) => c.vendor !== "human")) {
|
|
151
|
+
throw new Error("review gate needs at least one authed LLM channel (run `tickmarkr doctor`) — or pass --no-review");
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const artifactDir = join(stateDir, new Date().toISOString().replace(/[:.]/g, "-"));
|
|
155
|
+
mkdirSync(artifactDir, { recursive: true });
|
|
156
|
+
const { results } = await runGates(task, {
|
|
157
|
+
worktree: cwd, baseRef: mergeBase,
|
|
158
|
+
result: { ok: true, summary: "standalone verify — no worker claims to trust", deviations: [], raw: "" },
|
|
159
|
+
author, commands, baseline, channels, ...(judgeChannels ? { judgeChannels } : {}), adapters, cfg,
|
|
160
|
+
pipeline: "v185", artifactDir,
|
|
161
|
+
onGate: (e) => {
|
|
162
|
+
if (e.phase === "start")
|
|
163
|
+
console.error(`verify: → ${e.gate} (${e.index}/${e.total})`);
|
|
164
|
+
else
|
|
165
|
+
console.error(`verify: ${e.result.pass ? "✓" : "✗"} ${e.result.gate} — ${e.result.details.split("\n")[0] ?? ""}`);
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
const green = results.length > 0 && results.every((r) => r.pass || r.meta?.skipped === true);
|
|
169
|
+
if (values.json) {
|
|
170
|
+
return { out: JSON.stringify({ base: baseTip, head, mergeBase, green, results }, null, 2), code: green ? 0 : 2 };
|
|
171
|
+
}
|
|
172
|
+
const lines = results.map((r) => `${r.pass ? "PASS" : "FAIL"} ${r.gate}\n${r.details.split("\n").map((l) => ` ${l}`).join("\n")}`);
|
|
173
|
+
const verdict = green
|
|
174
|
+
? `verify GREEN — ${results.length} gate(s) passed on ${mergeBase.slice(0, 12)}..${head.slice(0, 12)} (merge is a human decision)`
|
|
175
|
+
: `verify RED — first failure decides; artifacts in ${artifactDir}`;
|
|
176
|
+
return { out: [...lines, "", verdict].join("\n"), code: green ? 0 : 2 };
|
|
177
|
+
}
|
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\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 herdr|subprocess --route-strict)\n status live run state\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 approve <id> <task> release a park (--uphold sides with the reviewer and funds a fixed attempt; --by <name> --reason <text>); takes effect on resume";
|
|
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\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 herdr|subprocess --route-strict)\n status live run state\n verify run the gate battery standalone against merge-base(--base, HEAD)..HEAD \u2014 no daemon, one verdict (--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 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
|
@@ -16,10 +16,11 @@ import { scope } from "./commands/scope.js";
|
|
|
16
16
|
import { status } from "./commands/status.js";
|
|
17
17
|
import { ui } from "./commands/ui.js";
|
|
18
18
|
import { unlock } from "./commands/unlock.js";
|
|
19
|
+
import { verify } from "./commands/verify.js";
|
|
19
20
|
import { version } from "./commands/version.js";
|
|
20
21
|
const normalize = (r) => typeof r === "string" ? { out: r, code: 0 } : r;
|
|
21
22
|
export const COMMANDS = {
|
|
22
|
-
init, doctor, fleet, compile, scope, plan, run, status, resume, report, profile, ui, unlock, approve, version, eval: evalCommand,
|
|
23
|
+
init, doctor, fleet, compile, scope, plan, run, status, resume, report, profile, ui, unlock, approve, version, verify, eval: evalCommand,
|
|
23
24
|
};
|
|
24
25
|
const VERSION_FLAGS = new Set(["version", "--version", "-v"]);
|
|
25
26
|
const HELP_CMDS = new Set(["help", "-h", "--help"]);
|
|
@@ -35,6 +36,7 @@ usage: tickmarkr <command>
|
|
|
35
36
|
eval run checked-in fixtures against every channel in isolated temp repos
|
|
36
37
|
run execute the graph (--concurrency N --driver herdr|subprocess --route-strict)
|
|
37
38
|
status live run state
|
|
39
|
+
verify run the gate battery standalone against merge-base(--base, HEAD)..HEAD — no daemon, one verdict (--base main --criteria <file> | --task <id> [--files <glob>] [--author adapter:model] [--no-review] [--json])
|
|
38
40
|
resume <id> continue a run from its journal
|
|
39
41
|
report <id> cost/quality report (--md for committable execution record)
|
|
40
42
|
profile show learned routing profile (profile reset = forget history via cursor, keeps telemetry)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { extname, join, relative } from "node:path";
|
|
3
|
-
import
|
|
3
|
+
import { filesGlob } from "../graph/files-glob.js";
|
|
4
4
|
import { criticalPathHits, DEFAULT_CONFIG, DEFAULT_REVIEW_CRITICAL_PATHS, effectiveReviewPolicy, loadConfig, } from "../config/config.js";
|
|
5
5
|
import { renderAcceptanceItem } from "../graph/schema.js";
|
|
6
6
|
// Advisory plan-time scan only (OBS-12/13/14/21, OBS-76). NEVER expands files[], fails compile,
|
|
@@ -106,7 +106,7 @@ export function collateralLints(tasks, repoRoot) {
|
|
|
106
106
|
const lines = [];
|
|
107
107
|
for (const t of tasks) {
|
|
108
108
|
// OBS-22: scopeGate accepts picomatch globs; advisory collateral warnings must agree.
|
|
109
|
-
const scoped =
|
|
109
|
+
const scoped = filesGlob(t.files.map((f) => f.replace(/^\.\//, "")));
|
|
110
110
|
const srcFiles = t.files.map((f) => f.replace(/^\.\//, "")).filter(isSrcPath);
|
|
111
111
|
if (!srcFiles.length)
|
|
112
112
|
continue;
|
|
@@ -175,7 +175,7 @@ export function newDirectoryLints(tasks, repoRoot) {
|
|
|
175
175
|
const lines = [];
|
|
176
176
|
for (const t of tasks) {
|
|
177
177
|
// OBS-22: scopeGate accepts picomatch globs; advisory warnings must agree.
|
|
178
|
-
const scoped =
|
|
178
|
+
const scoped = filesGlob(t.files.map((f) => f.replace(/^\.\//, "")));
|
|
179
179
|
const newDirs = new Set();
|
|
180
180
|
for (const f of t.files) {
|
|
181
181
|
const dir = topLevelSrcDir(f);
|
|
@@ -213,7 +213,7 @@ export function sourceScopeLints(tasks, repoRoot) {
|
|
|
213
213
|
const lines = [];
|
|
214
214
|
for (const { t, needles } of perTask) {
|
|
215
215
|
// OBS-22: scopeGate accepts picomatch globs; advisory warnings must agree.
|
|
216
|
-
const scoped =
|
|
216
|
+
const scoped = filesGlob(t.files.map((f) => f.replace(/^\.\//, "")));
|
|
217
217
|
// needles are word-chars by construction — no regex escaping needed; whole-word match only
|
|
218
218
|
const res = needles.map((n) => new RegExp(`\\b${n}\\b`));
|
|
219
219
|
const hits = [];
|
|
@@ -267,9 +267,9 @@ function overlappingPatterns(a, b) {
|
|
|
267
267
|
hits.add(pa);
|
|
268
268
|
continue;
|
|
269
269
|
}
|
|
270
|
-
if (isGlob(pa) && !isGlob(pb) &&
|
|
270
|
+
if (isGlob(pa) && !isGlob(pb) && filesGlob(pa)(pb))
|
|
271
271
|
hits.add(`${pb} ⊂ ${pa}`);
|
|
272
|
-
else if (isGlob(pb) && !isGlob(pa) &&
|
|
272
|
+
else if (isGlob(pb) && !isGlob(pa) && filesGlob(pb)(pa))
|
|
273
273
|
hits.add(`${pa} ⊂ ${pb}`);
|
|
274
274
|
}
|
|
275
275
|
}
|
|
@@ -515,7 +515,7 @@ export function symbolOwnershipErrors(tasks, repoRoot) {
|
|
|
515
515
|
}
|
|
516
516
|
for (const { t, symbols } of perTask) {
|
|
517
517
|
// OBS-22: scopeGate accepts picomatch globs; ownership must agree.
|
|
518
|
-
const scoped =
|
|
518
|
+
const scoped = filesGlob(t.files.map((f) => f.replace(/^\.\//, "")));
|
|
519
519
|
for (const sym of symbols) {
|
|
520
520
|
const defs = sites.get(sym) ?? [];
|
|
521
521
|
if (defs.length !== 1)
|
package/dist/compile/common.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import
|
|
2
|
+
import { filesGlob } from "../graph/files-glob.js";
|
|
3
3
|
export class CompileError extends Error {
|
|
4
4
|
constructor(msg) {
|
|
5
5
|
super(msg);
|
|
@@ -29,7 +29,7 @@ export function assertWriteScope(source, taskId, files, writes) {
|
|
|
29
29
|
// #1 over-fire landmine: empty files[] is unrestricted — mirrors scope.ts:12
|
|
30
30
|
if (!files.length)
|
|
31
31
|
return;
|
|
32
|
-
const inScope =
|
|
32
|
+
const inScope = filesGlob(files); // the ONE files[] matcher — src/graph/files-glob.ts (Q120s)
|
|
33
33
|
for (const { path, directive, bare } of writes) {
|
|
34
34
|
const normalized = path.replace(/^\.\//, "");
|
|
35
35
|
if (inScope(normalized))
|
package/dist/compile/native.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type RunGraph } from "../graph/schema.js";
|
|
1
|
+
import { type RunGraph, type Task } from "../graph/schema.js";
|
|
2
2
|
/**
|
|
3
3
|
* Classify one `context:` entry against the tree a worker's worktree is built from.
|
|
4
4
|
* ok — reachable
|
|
@@ -17,5 +17,19 @@ 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"];
|
|
21
|
+
export type AuthoringLintCode = (typeof AUTHORING_LINT_CODES)[number];
|
|
22
|
+
export interface AuthoringLintFinding {
|
|
23
|
+
code: AuthoringLintCode;
|
|
24
|
+
fixtureId: string;
|
|
25
|
+
taskId: string;
|
|
26
|
+
criterion?: number;
|
|
27
|
+
detail: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Frame-v2 authoring checks over the compiled native task shape. Criterion-scope is blocking at the
|
|
31
|
+
* compile boundary; the remaining checks are review findings, emitted by compileNative below.
|
|
32
|
+
*/
|
|
33
|
+
export declare function authoringLintFindings(tasks: readonly Task[], file: string): AuthoringLintFinding[];
|
|
20
34
|
export declare function compileNative(file: string): RunGraph;
|
|
21
35
|
export declare function specTemplate(): string;
|