tickmarkr 1.87.0 → 1.90.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.
Files changed (93) hide show
  1. package/dist/adapters/catalog.d.ts +18 -1
  2. package/dist/adapters/catalog.js +44 -1
  3. package/dist/adapters/fake.d.ts +2 -1
  4. package/dist/adapters/fake.js +7 -0
  5. package/dist/adapters/grok.js +11 -0
  6. package/dist/adapters/kimi.d.ts +2 -1
  7. package/dist/adapters/kimi.js +36 -0
  8. package/dist/adapters/opencode.js +17 -0
  9. package/dist/adapters/pi.js +11 -0
  10. package/dist/adapters/prompt.js +8 -1
  11. package/dist/adapters/registry.js +76 -57
  12. package/dist/adapters/types.d.ts +34 -3
  13. package/dist/adapters/types.js +99 -1
  14. package/dist/cli/commands/approve.d.ts +2 -0
  15. package/dist/cli/commands/approve.js +104 -84
  16. package/dist/cli/commands/compile.d.ts +1 -1
  17. package/dist/cli/commands/compile.js +29 -12
  18. package/dist/cli/commands/doctor.d.ts +16 -0
  19. package/dist/cli/commands/doctor.js +52 -0
  20. package/dist/cli/commands/init.js +2 -1
  21. package/dist/cli/commands/plan.d.ts +1 -1
  22. package/dist/cli/commands/plan.js +10 -1
  23. package/dist/cli/commands/report.js +49 -0
  24. package/dist/cli/commands/status.js +298 -96
  25. package/dist/cli/commands/verify.d.ts +9 -0
  26. package/dist/cli/commands/verify.js +177 -0
  27. package/dist/cli/harness.d.ts +13 -0
  28. package/dist/cli/harness.js +50 -0
  29. package/dist/cli/index.d.ts +1 -1
  30. package/dist/cli/index.js +3 -1
  31. package/dist/compile/collateral.js +11 -11
  32. package/dist/compile/common.js +2 -2
  33. package/dist/compile/index.d.ts +14 -3
  34. package/dist/compile/index.js +36 -10
  35. package/dist/compile/native.d.ts +15 -1
  36. package/dist/compile/native.js +310 -28
  37. package/dist/config/config.js +2 -2
  38. package/dist/drivers/subprocess.d.ts +6 -1
  39. package/dist/drivers/subprocess.js +9 -4
  40. package/dist/gates/acceptance.d.ts +21 -1
  41. package/dist/gates/acceptance.js +67 -22
  42. package/dist/gates/artifact-manifest.d.ts +119 -0
  43. package/dist/gates/artifact-manifest.js +357 -0
  44. package/dist/gates/baseline.d.ts +45 -5
  45. package/dist/gates/baseline.js +119 -15
  46. package/dist/gates/llm.js +37 -26
  47. package/dist/gates/review.d.ts +16 -11
  48. package/dist/gates/review.js +44 -150
  49. package/dist/gates/run-gates.js +124 -7
  50. package/dist/gates/scope.js +3 -3
  51. package/dist/graph/files-glob.d.ts +18 -0
  52. package/dist/graph/files-glob.js +22 -0
  53. package/dist/graph/schema.d.ts +3 -1
  54. package/dist/graph/schema.js +4 -1
  55. package/dist/run/daemon.d.ts +44 -0
  56. package/dist/run/daemon.js +2334 -1973
  57. package/dist/run/git.d.ts +53 -0
  58. package/dist/run/git.js +119 -5
  59. package/dist/run/interactive-seed.d.ts +6 -2
  60. package/dist/run/interactive-seed.js +72 -5
  61. package/dist/run/journal.d.ts +9 -1
  62. package/dist/run/journal.js +99 -9
  63. package/dist/run/lock.d.ts +11 -0
  64. package/dist/run/lock.js +97 -6
  65. package/dist/run/merge.d.ts +4 -1
  66. package/dist/run/merge.js +26 -7
  67. package/dist/run/outcome.d.ts +50 -0
  68. package/dist/run/outcome.js +152 -0
  69. package/dist/run/protocol.d.ts +460 -0
  70. package/dist/run/protocol.js +433 -0
  71. package/dist/run/supervision.d.ts +29 -0
  72. package/dist/run/supervision.js +189 -0
  73. package/fixtures/authoring-lints/01-awk-range-self-pass.spec.md +12 -0
  74. package/fixtures/authoring-lints/02-judge-text-key-miss.spec.md +7 -0
  75. package/fixtures/authoring-lints/03-c1-t41-rendered-observable.spec.md +8 -0
  76. package/fixtures/authoring-lints/04-c1-t24-named-file.spec.md +8 -0
  77. package/fixtures/authoring-lints/05-c2-t24-t28-dep-inversion.spec.md +7 -0
  78. package/fixtures/authoring-lints/06-c2-denumbered-coupling.spec.md +7 -0
  79. package/fixtures/authoring-lints/07-c3a-t41-line-count-proxy.spec.md +7 -0
  80. package/fixtures/authoring-lints/08-c3b-t41-governance-referent.spec.md +7 -0
  81. package/fixtures/authoring-lints/09-c4-universals-without-pointer.spec.md +7 -0
  82. package/fixtures/authoring-lints/10-c5-t34-conjunct-flood.spec.md +7 -0
  83. package/fixtures/authoring-lints/11-c6-t34-q3-q9-q20-bundle.spec.md +7 -0
  84. package/fixtures/authoring-lints/12-c7-t24-prose-seam.spec.md +8 -0
  85. package/fixtures/wrapped-acceptance.native.md +29 -0
  86. package/package.json +1 -1
  87. package/schema/rungraph.schema.json +21 -2
  88. package/skills/tickmarkr-overseer/SKILL.md +262 -5
  89. package/skills/tickmarkr-overseer/scripts/watch-artifacts.sh +79 -8
  90. package/skills/tickmarkr-overseer/scripts/watch-contamination.sh +80 -0
  91. package/skills/tickmarkr-overseer/scripts/watch-context.sh +86 -0
  92. package/skills/tickmarkr-overseer/scripts/watch-parks.sh +96 -0
  93. package/skills/tickmarkr-overseer/scripts/watch-pending-input.sh +201 -0
@@ -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
+ }
@@ -0,0 +1,13 @@
1
+ export type HarnessProvenance = "installed" | "checkout" | "unknown" | "unresolved";
2
+ export type Harness = {
3
+ path: string;
4
+ provenance: HarnessProvenance;
5
+ };
6
+ /**
7
+ * Resolve the harness that is executing from a path the CALLER supplies — never from process state, so a
8
+ * caller can pin the rendering without pinning its own filesystem. The resolution is the point: a global
9
+ * bin is a symlink into node_modules, and the invoked name hides exactly the segment provenance is read
10
+ * from (`process.argv[1]` never shows it).
11
+ */
12
+ export declare function resolveHarness(from: string | undefined): Harness;
13
+ export declare const harnessLine: (h: Harness) => string;
@@ -0,0 +1,50 @@
1
+ import { existsSync, realpathSync } from "node:fs";
2
+ import { dirname, join, sep } from "node:path";
3
+ const LABEL = {
4
+ installed: "installed package",
5
+ checkout: "working checkout",
6
+ unknown: "unknown origin",
7
+ unresolved: "unresolved location",
8
+ };
9
+ // Read off the RESOLVED location — never a build-time constant and never a value the binary states about
10
+ // itself, because a self-report is exactly as trustworthy as the version string it exists to replace.
11
+ // node_modules first: a package installed inside a checkout is still an installed package.
12
+ const provenanceOf = (path) => {
13
+ if (path.split(sep).includes("node_modules"))
14
+ return "installed";
15
+ // a git worktree's `.git` is a FILE, not a directory — existsSync covers both
16
+ for (let d = dirname(path);; d = dirname(d)) {
17
+ if (existsSync(join(d, ".git")))
18
+ return "checkout";
19
+ if (dirname(d) === d)
20
+ return "unknown";
21
+ }
22
+ };
23
+ /**
24
+ * Resolve the harness that is executing from a path the CALLER supplies — never from process state, so a
25
+ * caller can pin the rendering without pinning its own filesystem. The resolution is the point: a global
26
+ * bin is a symlink into node_modules, and the invoked name hides exactly the segment provenance is read
27
+ * from (`process.argv[1]` never shows it).
28
+ */
29
+ export function resolveHarness(from) {
30
+ let path;
31
+ try {
32
+ // `realpathSync("")` does NOT throw — it returns the cwd, so an absent `process.argv[1]` would name a
33
+ // DIRECTORY as the executing harness and read provenance off the caller's location instead of the
34
+ // binary's (inside a checkout: a confident, false "working checkout"). Absence is admitted as ITSELF:
35
+ // taking `undefined` means no caller has to invent an empty string to say "no location", and the one
36
+ // function every caller routes through rejects both forms before realpathSync ever sees them.
37
+ if (!from)
38
+ throw new Error("no location");
39
+ path = realpathSync(from);
40
+ }
41
+ catch {
42
+ // Fail closed: with no resolved location there is nothing to derive FROM, and classifying the
43
+ // unresolved TEXT would invent the claim — `/nowhere/node_modules/tickmarkr/…` would read
44
+ // "installed package" about a file that does not exist. The raw text survives as diagnostic only,
45
+ // never as evidence; a throw here would take down `plan`/`compile` over a provenance banner.
46
+ return { path: from ?? "", provenance: "unresolved" };
47
+ }
48
+ return { path, provenance: provenanceOf(path) };
49
+ }
50
+ export const harnessLine = (h) => `harness: ${h.path} (${LABEL[h.provenance]})`;
@@ -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 picomatch from "picomatch";
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 = picomatch(t.files.map((f) => f.replace(/^\.\//, "")), { dot: true });
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;
@@ -121,14 +121,14 @@ export function collateralLints(tasks, repoRoot) {
121
121
  continue;
122
122
  if (mentions(text, needles))
123
123
  hits.push(tf);
124
- if (hits.length >= MAX_HITS_PER_TASK)
125
- break;
126
124
  }
127
125
  if (!hits.length)
128
126
  continue;
129
127
  // deterministic: walk already sorted; stable list
130
- const listed = hits.join(", ");
131
- const tail = hits.length >= MAX_HITS_PER_TASK ? " (capped)" : "";
128
+ const listed = hits.slice(0, MAX_HITS_PER_TASK).join(", ");
129
+ const tail = hits.length > MAX_HITS_PER_TASK
130
+ ? ` (${hits.length} total; capped at ${MAX_HITS_PER_TASK} shown)`
131
+ : "";
132
132
  lines.push(`${t.id}: likely collateral tests not in files[]: ${listed}${tail}`);
133
133
  }
134
134
  return lines;
@@ -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 = picomatch(t.files.map((f) => f.replace(/^\.\//, "")), { dot: true });
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 = picomatch(t.files.map((f) => f.replace(/^\.\//, "")), { dot: true });
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) && picomatch(pa)(pb))
270
+ if (isGlob(pa) && !isGlob(pb) && filesGlob(pa)(pb))
271
271
  hits.add(`${pb} ⊂ ${pa}`);
272
- else if (isGlob(pb) && !isGlob(pa) && picomatch(pb)(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 = picomatch(t.files.map((f) => f.replace(/^\.\//, "")), { dot: true });
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)
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import picomatch from "picomatch";
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 = picomatch(files, { dot: true }); // byte-identical options to src/gates/scope.ts:15
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))
@@ -1,4 +1,15 @@
1
- import type { RunGraph } from "../graph/schema.js";
2
- type SourceType = "speckit" | "gsd" | "prd" | "native";
3
- export declare function compileSource(src: string, type?: SourceType, root?: string): RunGraph;
1
+ import { type RunGraph, type SpecSource } from "../graph/schema.js";
2
+ type SourceType = SpecSource;
3
+ export type PlanIR = {
4
+ version: RunGraph["version"];
5
+ source: RunGraph["spec"]["source"];
6
+ paths: RunGraph["spec"]["paths"];
7
+ hash: RunGraph["spec"]["hash"];
8
+ base?: RunGraph["spec"]["base"];
9
+ mode?: RunGraph["mode"];
10
+ tasks: RunGraph["tasks"];
11
+ };
12
+ export type PlanFinalizationHook = (plan: PlanIR) => PlanIR;
13
+ export declare function finalizePlan(plan: PlanIR, src: string): RunGraph;
14
+ export declare function compileSource(src: string, type?: SourceType, root?: string, beforeFinalize?: PlanFinalizationHook): RunGraph;
4
15
  export {};
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync, statSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { validateGraph } from "../graph/schema.js";
3
4
  import { taskUnitContractErrors } from "./collateral.js";
4
5
  import { CompileError } from "./common.js";
5
6
  import { compileGsd, isGsdPhaseDir } from "./gsd.js";
@@ -39,15 +40,40 @@ function enforceTaskUnitContract(g, src) {
39
40
  }
40
41
  return g;
41
42
  }
42
- export function compileSource(src, type, root) {
43
+ export function finalizePlan(plan, src) {
44
+ const graph = validateGraph({
45
+ version: plan.version,
46
+ ...(plan.mode !== undefined ? { mode: plan.mode } : {}),
47
+ spec: {
48
+ source: plan.source,
49
+ paths: plan.paths,
50
+ hash: plan.hash,
51
+ ...(plan.base !== undefined ? { base: plan.base } : {}),
52
+ },
53
+ tasks: plan.tasks,
54
+ });
55
+ return enforceTaskUnitContract(graph, src);
56
+ }
57
+ function compilePlan(src, type, root) {
43
58
  const kind = type ?? detect(src);
44
- if (kind === "speckit")
45
- return enforceTaskUnitContract(compileSpecKit(src), src);
46
- if (kind === "gsd")
47
- return enforceTaskUnitContract(compileGsd(src, root), src);
48
- if (kind === "native")
49
- return enforceTaskUnitContract(compileNative(src), src);
50
- if (kind === "prd")
51
- return enforceTaskUnitContract(compilePrd(src), src);
52
- throw new CompileError(`cannot detect spec type for ${src} — pass a Spec Kit feature dir (with tasks.md), a GSD phase dir (with *-PLAN.md), or a marked native/generic PRD .md file, or use --type speckit|prd|gsd|native`);
59
+ const graph = kind === "speckit" ? compileSpecKit(src)
60
+ : kind === "gsd" ? compileGsd(src, root)
61
+ : kind === "native" ? compileNative(src)
62
+ : kind === "prd" ? compilePrd(src)
63
+ : null;
64
+ if (!graph) {
65
+ throw new CompileError(`cannot detect spec type for ${src} — pass a Spec Kit feature dir (with tasks.md), a GSD phase dir (with *-PLAN.md), or a marked native/generic PRD .md file, or use --type speckit|prd|gsd|native`);
66
+ }
67
+ return {
68
+ version: graph.version,
69
+ source: graph.spec.source,
70
+ paths: graph.spec.paths,
71
+ hash: graph.spec.hash,
72
+ ...(graph.spec.base !== undefined ? { base: graph.spec.base } : {}),
73
+ ...(graph.mode !== undefined ? { mode: graph.mode } : {}),
74
+ tasks: graph.tasks,
75
+ };
76
+ }
77
+ export function compileSource(src, type, root, beforeFinalize = (plan) => plan) {
78
+ return finalizePlan(beforeFinalize(compilePlan(src, type, root)), src);
53
79
  }
@@ -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;