tickmarkr 1.30.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 (123) hide show
  1. package/README.md +347 -0
  2. package/dist/adapters/claude-code.d.ts +4 -0
  3. package/dist/adapters/claude-code.js +191 -0
  4. package/dist/adapters/codex.d.ts +8 -0
  5. package/dist/adapters/codex.js +257 -0
  6. package/dist/adapters/cursor-agent.d.ts +4 -0
  7. package/dist/adapters/cursor-agent.js +62 -0
  8. package/dist/adapters/fake.d.ts +45 -0
  9. package/dist/adapters/fake.js +115 -0
  10. package/dist/adapters/grok.d.ts +4 -0
  11. package/dist/adapters/grok.js +122 -0
  12. package/dist/adapters/model-lints.d.ts +6 -0
  13. package/dist/adapters/model-lints.js +130 -0
  14. package/dist/adapters/opencode.d.ts +3 -0
  15. package/dist/adapters/opencode.js +108 -0
  16. package/dist/adapters/pi.d.ts +3 -0
  17. package/dist/adapters/pi.js +191 -0
  18. package/dist/adapters/prompt.d.ts +6 -0
  19. package/dist/adapters/prompt.js +84 -0
  20. package/dist/adapters/registry.d.ts +31 -0
  21. package/dist/adapters/registry.js +165 -0
  22. package/dist/adapters/types.d.ts +98 -0
  23. package/dist/adapters/types.js +56 -0
  24. package/dist/cli/commands/approve.d.ts +1 -0
  25. package/dist/cli/commands/approve.js +72 -0
  26. package/dist/cli/commands/compile.d.ts +1 -0
  27. package/dist/cli/commands/compile.js +23 -0
  28. package/dist/cli/commands/doctor.d.ts +2 -0
  29. package/dist/cli/commands/doctor.js +103 -0
  30. package/dist/cli/commands/init.d.ts +1 -0
  31. package/dist/cli/commands/init.js +32 -0
  32. package/dist/cli/commands/plan.d.ts +2 -0
  33. package/dist/cli/commands/plan.js +101 -0
  34. package/dist/cli/commands/profile.d.ts +1 -0
  35. package/dist/cli/commands/profile.js +56 -0
  36. package/dist/cli/commands/report.d.ts +4 -0
  37. package/dist/cli/commands/report.js +306 -0
  38. package/dist/cli/commands/resume.d.ts +1 -0
  39. package/dist/cli/commands/resume.js +11 -0
  40. package/dist/cli/commands/run.d.ts +1 -0
  41. package/dist/cli/commands/run.js +35 -0
  42. package/dist/cli/commands/scope.d.ts +3 -0
  43. package/dist/cli/commands/scope.js +22 -0
  44. package/dist/cli/commands/status.d.ts +9 -0
  45. package/dist/cli/commands/status.js +162 -0
  46. package/dist/cli/commands/unlock.d.ts +1 -0
  47. package/dist/cli/commands/unlock.js +11 -0
  48. package/dist/cli/index.d.ts +8 -0
  49. package/dist/cli/index.js +65 -0
  50. package/dist/compile/collateral.d.ts +6 -0
  51. package/dist/compile/collateral.js +128 -0
  52. package/dist/compile/common.d.ts +12 -0
  53. package/dist/compile/common.js +56 -0
  54. package/dist/compile/gsd.d.ts +3 -0
  55. package/dist/compile/gsd.js +222 -0
  56. package/dist/compile/index.d.ts +4 -0
  57. package/dist/compile/index.js +33 -0
  58. package/dist/compile/native.d.ts +4 -0
  59. package/dist/compile/native.js +196 -0
  60. package/dist/compile/prd.d.ts +2 -0
  61. package/dist/compile/prd.js +64 -0
  62. package/dist/compile/speckit.d.ts +2 -0
  63. package/dist/compile/speckit.js +66 -0
  64. package/dist/config/config.d.ts +184 -0
  65. package/dist/config/config.js +329 -0
  66. package/dist/drivers/herdr.d.ts +44 -0
  67. package/dist/drivers/herdr.js +394 -0
  68. package/dist/drivers/index.d.ts +3 -0
  69. package/dist/drivers/index.js +13 -0
  70. package/dist/drivers/subprocess.d.ts +25 -0
  71. package/dist/drivers/subprocess.js +106 -0
  72. package/dist/drivers/types.d.ts +61 -0
  73. package/dist/drivers/types.js +77 -0
  74. package/dist/gates/acceptance.d.ts +19 -0
  75. package/dist/gates/acceptance.js +92 -0
  76. package/dist/gates/baseline.d.ts +12 -0
  77. package/dist/gates/baseline.js +97 -0
  78. package/dist/gates/evidence.d.ts +4 -0
  79. package/dist/gates/evidence.js +12 -0
  80. package/dist/gates/llm.d.ts +25 -0
  81. package/dist/gates/llm.js +101 -0
  82. package/dist/gates/review.d.ts +12 -0
  83. package/dist/gates/review.js +80 -0
  84. package/dist/gates/run-gates.d.ts +34 -0
  85. package/dist/gates/run-gates.js +105 -0
  86. package/dist/gates/scope.d.ts +8 -0
  87. package/dist/gates/scope.js +32 -0
  88. package/dist/gates/types.d.ts +6 -0
  89. package/dist/gates/types.js +1 -0
  90. package/dist/graph/graph.d.ts +20 -0
  91. package/dist/graph/graph.js +148 -0
  92. package/dist/graph/schema.d.ts +185 -0
  93. package/dist/graph/schema.js +141 -0
  94. package/dist/index.d.ts +1 -0
  95. package/dist/index.js +1 -0
  96. package/dist/plan/prompt.d.ts +4 -0
  97. package/dist/plan/prompt.js +31 -0
  98. package/dist/plan/scope.d.ts +17 -0
  99. package/dist/plan/scope.js +141 -0
  100. package/dist/report/cost.d.ts +24 -0
  101. package/dist/report/cost.js +89 -0
  102. package/dist/route/preference.d.ts +27 -0
  103. package/dist/route/preference.js +49 -0
  104. package/dist/route/profile.d.ts +65 -0
  105. package/dist/route/profile.js +197 -0
  106. package/dist/route/router.d.ts +26 -0
  107. package/dist/route/router.js +200 -0
  108. package/dist/run/consult.d.ts +23 -0
  109. package/dist/run/consult.js +80 -0
  110. package/dist/run/daemon.d.ts +23 -0
  111. package/dist/run/daemon.js +728 -0
  112. package/dist/run/git.d.ts +12 -0
  113. package/dist/run/git.js +56 -0
  114. package/dist/run/journal.d.ts +91 -0
  115. package/dist/run/journal.js +287 -0
  116. package/dist/run/lock.d.ts +29 -0
  117. package/dist/run/lock.js +182 -0
  118. package/dist/run/merge.d.ts +12 -0
  119. package/dist/run/merge.js +44 -0
  120. package/dist/run/reconcile.d.ts +4 -0
  121. package/dist/run/reconcile.js +95 -0
  122. package/package.json +39 -0
  123. package/schema/rungraph.schema.json +306 -0
@@ -0,0 +1,222 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { basename, dirname, isAbsolute, join, relative } from "node:path";
3
+ import { parse as parseYaml } from "yaml";
4
+ import { TIERS, validateGraph } from "../graph/schema.js";
5
+ import { CompileError, assertWriteScope, inferShape, sha256 } from "./common.js";
6
+ // GSD artifact front-end (spec v1.3): one GSD *plan* is one drovr *task* — a plan is
7
+ // worktree-sized; its inner <task> steps stay in the worker prompt via context[0] = the plan file.
8
+ // Artifact-level only: parses .planning/ markdown, never GSD repo/command internals.
9
+ const PLAN_SUFFIX = "-PLAN.md";
10
+ export function isGsdPhaseDir(dir) {
11
+ try {
12
+ return readdirSync(dir).some((f) => f.endsWith(PLAN_SUFFIX));
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }
18
+ const strings = (v) => (Array.isArray(v) ? v.map(String) : []);
19
+ const WRITE_DIRECTIVE = /\b(?:Create|Write|Add|Emit|Generate)\s+`([^`\s]+)`/gi;
20
+ const isPathish = (s) => /^[\w./*-]+$/.test(s) && /\.[a-z]{2,4}$/i.test(s);
21
+ const stripFences = (s) => s.replace(/^```[\s\S]*?^```/gm, "");
22
+ // ponytail: grammar matches GSD SUMMARY boilerplate only; if that boilerplate drifts the check
23
+ // goes vacuous — the v1.9-29 pin in tests/compile/scope-seam.test.ts fails loudly when it does.
24
+ function extractWriteDirectives(body, storedPath) {
25
+ const phaseDir = dirname(storedPath);
26
+ const writes = [];
27
+ for (const m of stripFences(body).matchAll(WRITE_DIRECTIVE)) {
28
+ const raw = m[1];
29
+ if (!isPathish(raw))
30
+ continue;
31
+ // HARD-09: a bare name (no "/") joins against the plan's CURRENT on-disk dir, which goes stale
32
+ // the moment the dir is archive-relocated. Carry the bare name through so assertWriteScope's
33
+ // fallback can resolve it in a relocation-invariant way (basename suffix against files_modified).
34
+ const bare = !raw.includes("/");
35
+ const path = bare ? join(phaseDir, raw) : raw;
36
+ writes.push({ path, directive: m[0], ...(bare ? { bare: raw } : {}) });
37
+ }
38
+ return writes;
39
+ }
40
+ const isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
41
+ // schema-legal, branch-safe id segment: dots etc. → dashes; no "--" runs (task-branch separator)
42
+ // or trailing dash — the id schema rejects both
43
+ const sanitize = (s) => s
44
+ .replace(/[^A-Za-z0-9_-]/g, "-")
45
+ .replace(/-{2,}/g, "-")
46
+ .replace(/-+$/, "");
47
+ // filename is the canonical id source — frontmatter `plan: 06` YAML-parses to the number 6,
48
+ // losing the zero-padding that depends_on entries ("07-01") reference
49
+ const planKey = (file) => sanitize(basename(file).replace(/-PLAN\.md$/, ""));
50
+ // "06" and 6 must resolve to the same plan: alias each key by its zero-stripped segments
51
+ const unpad = (key) => key
52
+ .split("-")
53
+ .map((seg) => (/^\d+$/.test(seg) ? String(Number(seg)) : seg))
54
+ .join("-");
55
+ // numeric-aware order: 2-PLAN.md before 10-PLAN.md even without zero-padding
56
+ function byPlanOrder(a, b) {
57
+ const as = a.split("-");
58
+ const bs = b.split("-");
59
+ for (let i = 0; i < Math.max(as.length, bs.length); i++) {
60
+ const x = as[i] ?? "";
61
+ const y = bs[i] ?? "";
62
+ if (/^\d+$/.test(x) && /^\d+$/.test(y) && Number(x) !== Number(y))
63
+ return Number(x) - Number(y);
64
+ if (x !== y)
65
+ return x < y ? -1 : 1;
66
+ }
67
+ return 0;
68
+ }
69
+ function compileOne(file, storedPath) {
70
+ const content = readFileSync(file, "utf8");
71
+ const fmMatch = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
72
+ let fm = {};
73
+ if (fmMatch) {
74
+ try {
75
+ fm = (parseYaml(fmMatch[1]) ?? {});
76
+ }
77
+ catch (e) {
78
+ // fail closed: a swallowed parse error would silently drop autonomous:false (a human gate)
79
+ throw new CompileError(`${file} has malformed YAML frontmatter: ${e instanceof Error ? e.message.split("\n")[0] : e}`);
80
+ }
81
+ }
82
+ const body = content.slice(fmMatch ? fmMatch[0].length : 0);
83
+ const key = planKey(file);
84
+ const objective = /<objective>\s*([\s\S]*?)<\/objective>/.exec(body)?.[1].trim() ?? "";
85
+ // sentence split that survives common abbreviations and version dots
86
+ const firstSentence = objective
87
+ .split(/(?<!\be\.g\.)(?<!\bi\.e\.)(?<!\bvs\.)(?<!\betc\.)(?<=\.)\s/)[0]
88
+ ?.replace(/\s+/g, " ")
89
+ .trim();
90
+ const title = firstSentence || key;
91
+ const dones = [...body.matchAll(/<done>\s*([\s\S]*?)\s*<\/done>/g)].map((m) => m[1].replace(/\s+/g, " ").trim());
92
+ const truths = strings(fm.must_haves?.truths);
93
+ const acceptance = [...dones, ...truths].filter(Boolean);
94
+ if (!acceptance.length) {
95
+ throw new CompileError(`${file} has no acceptance criteria — every drovr task needs them.\n` +
96
+ `GSD source: add <done> lines to the plan's tasks, or must_haves.truths in its frontmatter.`);
97
+ }
98
+ // @-refs from the <context> block only; keep repo-relative paths (no $VAR, ~, absolute, or ..-escape)
99
+ const contextBlock = /<context>([\s\S]*?)<\/context>/.exec(body)?.[1] ?? "";
100
+ const refs = [...contextBlock.matchAll(/^@(\S+)$/gm)]
101
+ .map((m) => m[1])
102
+ .filter((p) => !p.includes("$") && !p.startsWith("~") && !p.startsWith("/") && !p.split("/").includes(".."));
103
+ const taskCount = [...body.matchAll(/<task[\s>]/g)].length;
104
+ const humanGate = fm.autonomous === false || /<task type="checkpoint:/.test(body);
105
+ const done = existsSync(join(dirname(file), `${basename(file).replace(/-PLAN\.md$/, "")}-SUMMARY.md`));
106
+ const files = strings(fm.files_modified).map((f) => f.replace(/^\.\//, ""));
107
+ assertWriteScope(file, `P${key}`, files, extractWriteDirectives(body, storedPath));
108
+ // fail closed (D-03): a silently dropped floor/pin routes the task cheap instead of erroring
109
+ let routingHints;
110
+ if (fm.routing !== undefined) {
111
+ if (!isPlainObject(fm.routing)) {
112
+ throw new CompileError(`${file} has a routing frontmatter key that must be an object with floor and/or pin`);
113
+ }
114
+ const hints = { source: basename(file) };
115
+ if (fm.routing.floor !== undefined) {
116
+ if (!TIERS.includes(fm.routing.floor)) {
117
+ throw new CompileError(`${file} has routing.floor "${fm.routing.floor}" — valid tiers are ${TIERS.join(", ")}`);
118
+ }
119
+ hints.floor = fm.routing.floor;
120
+ }
121
+ if (fm.routing.pin !== undefined) {
122
+ const pin = fm.routing.pin;
123
+ const via = isPlainObject(pin) ? pin.via : undefined;
124
+ const model = isPlainObject(pin) ? pin.model : undefined;
125
+ if (typeof via !== "string" || typeof model !== "string") {
126
+ throw new CompileError(`${file} has a routing.pin missing a string "via" and/or "model"`);
127
+ }
128
+ hints.pin = { via, model };
129
+ }
130
+ if (hints.floor !== undefined || hints.pin !== undefined)
131
+ routingHints = hints;
132
+ }
133
+ return {
134
+ task: {
135
+ id: `P${key}`,
136
+ title,
137
+ goal: title,
138
+ // title only: files_modified nearly always lists test mirrors, which mis-shaped every plan as "tests"
139
+ shape: inferShape(title),
140
+ complexity: Math.max(1, Math.min(10, 2 * taskCount + truths.length)),
141
+ deps: strings(fm.depends_on).map((d) => sanitize(d)),
142
+ files,
143
+ context: [storedPath, ...refs],
144
+ acceptance,
145
+ ...(humanGate ? { humanGate: true } : {}),
146
+ ...(done ? { status: "done" } : {}),
147
+ ...(routingHints ? { routingHints } : {}),
148
+ },
149
+ content,
150
+ };
151
+ }
152
+ export function compileGsd(src, root) {
153
+ if (!existsSync(src))
154
+ throw new CompileError(`${src} does not exist — point drovr compile at a GSD phase directory or a *-PLAN.md`);
155
+ const isDir = statSync(src).isDirectory();
156
+ const files = isDir
157
+ ? readdirSync(src)
158
+ .filter((f) => f.endsWith(PLAN_SUFFIX))
159
+ .sort(byPlanOrder)
160
+ .map((f) => join(src, f))
161
+ : [src];
162
+ if (!files.length) {
163
+ throw new CompileError(`no *-PLAN.md files in ${src} — point drovr compile at a GSD phase directory`);
164
+ }
165
+ // context[0] must be readable from an isolated worktree: store repo-relative, never absolute
166
+ const rel = (f) => {
167
+ if (root && isAbsolute(f)) {
168
+ const r = relative(root, f);
169
+ if (!r.startsWith(".."))
170
+ return r;
171
+ }
172
+ return f;
173
+ };
174
+ const compiled = files.map((f) => compileOne(f, rel(f)));
175
+ const tasks = compiled.map((c) => c.task);
176
+ // deps reference plan keys; YAML may have stripped zero-padding ("06" → 6) — resolve via aliases.
177
+ // Phase compile: an unresolvable dep is a lost edge → fail closed. Single-plan compile: deps
178
+ // point at plans outside this graph (already executed by GSD) → drop them.
179
+ const byAlias = new Map();
180
+ for (const t of tasks) {
181
+ const key = t.id.slice(1);
182
+ byAlias.set(key, t.id);
183
+ byAlias.set(unpad(key), t.id);
184
+ }
185
+ if (isDir) {
186
+ const bareAliases = new Map();
187
+ const claimBareAlias = (alias, id) => {
188
+ const existing = bareAliases.get(alias);
189
+ if (existing === undefined && bareAliases.has(alias))
190
+ return;
191
+ if (existing !== undefined && existing !== id) {
192
+ bareAliases.set(alias, undefined);
193
+ return;
194
+ }
195
+ bareAliases.set(alias, id);
196
+ };
197
+ for (const t of tasks) {
198
+ const bare = t.id.slice(1).split("-").at(-1) ?? "";
199
+ claimBareAlias(bare, t.id);
200
+ claimBareAlias(unpad(bare), t.id);
201
+ }
202
+ for (const [alias, id] of bareAliases) {
203
+ if (id !== undefined && !byAlias.has(alias))
204
+ byAlias.set(alias, id);
205
+ }
206
+ }
207
+ for (const [i, t] of tasks.entries()) {
208
+ t.deps = t.deps.flatMap((d) => {
209
+ const id = byAlias.get(d) ?? byAlias.get(unpad(d));
210
+ if (id)
211
+ return [id];
212
+ if (!isDir)
213
+ return []; // external to this single-plan graph
214
+ throw new CompileError(`${files[i]} depends on "${d}" but no such plan exists in ${src}`);
215
+ });
216
+ }
217
+ return validateGraph({
218
+ version: 1,
219
+ spec: { source: "gsd", paths: files, hash: sha256(compiled.map((c) => c.content).join("\n")) },
220
+ tasks,
221
+ });
222
+ }
@@ -0,0 +1,4 @@
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;
4
+ export {};
@@ -0,0 +1,33 @@
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { CompileError } from "./common.js";
4
+ import { compileGsd, isGsdPhaseDir } from "./gsd.js";
5
+ import { compileNative, NATIVE_MARKER } from "./native.js";
6
+ import { compilePrd } from "./prd.js";
7
+ import { compileSpecKit } from "./speckit.js";
8
+ function detect(src) {
9
+ if (existsSync(src) && statSync(src).isDirectory()) {
10
+ if (existsSync(join(src, "tasks.md")))
11
+ return "speckit";
12
+ if (isGsdPhaseDir(src))
13
+ return "gsd"; // gsd.ts owns the *-PLAN.md rule; unreadable dirs fall to null
14
+ return null;
15
+ }
16
+ if (src.endsWith("-PLAN.md"))
17
+ return "gsd"; // before the generic .md → prd rule
18
+ if (src.endsWith(".md"))
19
+ return existsSync(src) && NATIVE_MARKER.test(readFileSync(src, "utf8")) ? "native" : "prd";
20
+ return null;
21
+ }
22
+ export function compileSource(src, type, root) {
23
+ const kind = type ?? detect(src);
24
+ if (kind === "speckit")
25
+ return compileSpecKit(src);
26
+ if (kind === "gsd")
27
+ return compileGsd(src, root);
28
+ if (kind === "native")
29
+ return compileNative(src);
30
+ if (kind === "prd")
31
+ return compilePrd(src);
32
+ 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`);
33
+ }
@@ -0,0 +1,4 @@
1
+ import { type RunGraph } from "../graph/schema.js";
2
+ export declare const NATIVE_MARKER: RegExp;
3
+ export declare function compileNative(file: string): RunGraph;
4
+ export declare function specTemplate(): string;
@@ -0,0 +1,196 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { GATE_NAMES, ORACLES, SHAPES, TIERS, validateGraph } from "../graph/schema.js";
3
+ import { CompileError, inferShape, sha256 } from "./common.js";
4
+ export const NATIVE_MARKER = /^<!--\s*drovr:spec(?:\s+v1)?\s*-->\s*$/m;
5
+ const HEAD_RE = /^## (T\d+):\s*(.+)$/;
6
+ const FIELD_RE = /^- (\w+):\s*(.*)$/;
7
+ const NESTED_RE = /^\s+- (.+)$/;
8
+ // v1.19: a typed acceptance oracle line — "command: ...", "test: ...", or "judge: ...". Anything
9
+ // without one of these prefixes is a plain-string judge criterion (compat path, emits a warning).
10
+ const ORACLE_RE = new RegExp(`^(${ORACLES.join("|")}):\\s*(.*)$`);
11
+ const FIELDS = new Set(["goal", "shape", "deps", "files", "context", "complexity", "humangate", "pin", "floor", "gates", "acceptance"]);
12
+ function invalid(task, field, detail) {
13
+ throw new CompileError(`Task ${task} field "${field}" ${detail}`);
14
+ }
15
+ export function compileNative(file) {
16
+ if (!existsSync(file))
17
+ throw new CompileError(`no such native spec file: ${file}`);
18
+ const content = readFileSync(file, "utf8");
19
+ const drafts = [];
20
+ let plainCount = 0; // v1.19: plain-string acceptance items compiled as judge oracles (compat) — warn once
21
+ for (const line of content.split("\n")) {
22
+ const heading = line.match(HEAD_RE);
23
+ if (heading) {
24
+ drafts.push({ id: heading[1], title: heading[2].trim(), fields: {}, acceptance: [], gates: [], hasGates: false, list: null });
25
+ continue;
26
+ }
27
+ const draft = drafts.at(-1);
28
+ if (!draft)
29
+ continue;
30
+ const field = line.match(FIELD_RE);
31
+ if (field) {
32
+ const name = field[1].toLowerCase();
33
+ if (!FIELDS.has(name))
34
+ invalid(draft.id, field[1], "is unknown");
35
+ const value = field[2].trim();
36
+ if (name === "acceptance" || name === "gates") {
37
+ if (value)
38
+ invalid(draft.id, field[1], "must be a nested list");
39
+ draft.list = name;
40
+ if (name === "gates")
41
+ draft.hasGates = true;
42
+ }
43
+ else {
44
+ if (name === "goal" && !value)
45
+ invalid(draft.id, field[1], "must not be empty");
46
+ draft.fields[name] = value;
47
+ draft.list = null;
48
+ }
49
+ continue;
50
+ }
51
+ const nested = line.match(NESTED_RE);
52
+ if (nested && draft.list) {
53
+ const value = nested[1].trim();
54
+ if (!value)
55
+ invalid(draft.id, draft.list, "must not contain empty entries");
56
+ // v1.19: only the acceptance list parses typed oracle prefixes; gates stay plain names.
57
+ if (draft.list === "acceptance") {
58
+ const typed = value.match(ORACLE_RE);
59
+ if (typed) {
60
+ const [, kind, body] = typed;
61
+ if (!body.trim())
62
+ invalid(draft.id, "acceptance", `${kind} oracle must carry a value`);
63
+ draft.acceptance.push(kind === "command" ? { oracle: "command", command: body.trim() }
64
+ : kind === "test" ? { oracle: "test", test: body.trim() }
65
+ : { oracle: "judge", text: body.trim() });
66
+ }
67
+ else {
68
+ plainCount++;
69
+ draft.acceptance.push(value);
70
+ }
71
+ }
72
+ else {
73
+ draft[draft.list].push(value);
74
+ }
75
+ continue;
76
+ }
77
+ if (line.startsWith("- "))
78
+ invalid(draft.id, "field", `bullet is malformed: ${JSON.stringify(line)}`);
79
+ if (line.trim() && !line.startsWith(" "))
80
+ draft.list = null;
81
+ }
82
+ if (!drafts.length) {
83
+ throw new CompileError(`${file} has no task sections. Expected "## T1: Title" headings with field bullets.`);
84
+ }
85
+ const missing = drafts.filter((draft) => !draft.acceptance.length).map((draft) => draft.id);
86
+ if (missing.length) {
87
+ throw new CompileError(`acceptance criteria are required on every task. Missing on: ${missing.join(", ")}.\n` +
88
+ `Add to each section in ${file}:\n- acceptance:\n - <observable outcome>`);
89
+ }
90
+ const csv = (value) => (value && value.toLowerCase() !== "none" ? value.split(",").map((item) => item.trim()).filter(Boolean) : []);
91
+ const tasks = drafts.map((draft) => {
92
+ const { fields } = draft;
93
+ const shape = fields.shape ?? inferShape(draft.title);
94
+ if (!SHAPES.includes(shape))
95
+ invalid(draft.id, "shape", `must be one of ${SHAPES.join(", ")}`);
96
+ const complexity = fields.complexity === undefined ? 5 : Number(fields.complexity);
97
+ if (!Number.isInteger(complexity) || complexity < 1 || complexity > 10)
98
+ invalid(draft.id, "complexity", "must be an integer from 1 to 10");
99
+ if (fields.humangate !== undefined && fields.humangate !== "true" && fields.humangate !== "false") {
100
+ invalid(draft.id, "humanGate", "must be literally true or false");
101
+ }
102
+ const pin = fields.pin?.split(/\s+/);
103
+ if (pin && (pin.length !== 2 || pin.some((part) => !part)))
104
+ invalid(draft.id, "pin", "must be exactly 'via model'");
105
+ if (fields.floor !== undefined && !TIERS.includes(fields.floor)) {
106
+ invalid(draft.id, "floor", `must be one of ${TIERS.join(", ")}`);
107
+ }
108
+ const badGate = draft.gates.find((gate) => !GATE_NAMES.includes(gate));
109
+ if (badGate)
110
+ invalid(draft.id, "gates", `contains invalid gate "${badGate}"`);
111
+ const routingHints = pin || fields.floor ? {
112
+ ...(pin ? { pin: { via: pin[0], model: pin[1] } } : {}),
113
+ ...(fields.floor ? { floor: fields.floor } : {}),
114
+ } : undefined;
115
+ return {
116
+ id: draft.id,
117
+ title: draft.title,
118
+ goal: fields.goal ?? draft.title,
119
+ shape,
120
+ complexity,
121
+ deps: csv(fields.deps),
122
+ files: csv(fields.files),
123
+ context: csv(fields.context),
124
+ acceptance: draft.acceptance,
125
+ ...(fields.humangate === "true" ? { humanGate: true } : {}),
126
+ ...(routingHints ? { routingHints } : {}),
127
+ ...(draft.hasGates ? { gates: draft.gates } : {}),
128
+ };
129
+ });
130
+ const result = validateGraph({
131
+ version: 1,
132
+ spec: { source: "native", paths: [file], hash: sha256(content) },
133
+ tasks,
134
+ });
135
+ // v1.19 read-old/write-new: a plain-string acceptance item compiles as a judge oracle. This is the
136
+ // one-time nudge toward typed oracles (command/test/judge); PRD/Spec Kit/GSD stay silent (untouched).
137
+ if (plainCount > 0) {
138
+ console.warn(`drovr: ${plainCount} acceptance item${plainCount === 1 ? "" : "s"} in ${file} ${plainCount === 1 ? "is a plain string" : "are plain strings"} — compiled as judge oracle${plainCount === 1 ? "" : "s"}. Prefix with command:/test:/judge: to make the oracle explicit.`);
139
+ }
140
+ return result;
141
+ }
142
+ // Commented native spec written to drovr.spec.md by `drovr init`. Documented via HTML comments (which the
143
+ // parser ignores) so the template itself round-trips through compileSource() unchanged. Every field is
144
+ // documented; the two example tasks illustrate plain vs deps+routing-hint, each with acceptance[].
145
+ export function specTemplate() {
146
+ return `<!-- drovr:spec -->
147
+ # drovr native spec
148
+
149
+ Your starting point for a drovr native spec. Edit this file, then run:
150
+ drovr compile drovr.spec.md && drovr plan && drovr run
151
+
152
+ Each task is a "## Tn: Title" heading with "- field: value" bullets.
153
+ acceptance is required on every task (a nested list of observable outcomes).
154
+
155
+ <!--
156
+ Fields available per task:
157
+ goal: outcome the task must achieve (defaults to the title if omitted)
158
+ shape: plan | spec | implement | tests | docs | migration | ui | refactor | chore
159
+ (auto-inferred from the title if omitted)
160
+ deps: comma-separated task ids this depends on, or "none"
161
+ files: comma-separated repo paths this task may touch
162
+ context: comma-separated paths the task should read for background
163
+ complexity: integer 1 to 10 (default 5)
164
+ humanGate: true | false — pauses for a human review before merging this task
165
+ pin: "via model" — pin an exact channel, e.g. "claude-code opus"
166
+ floor: cheap | mid | frontier — minimum capability tier for routing
167
+ gates: nested list, any of build | test | lint | evidence | scope | acceptance | review
168
+ acceptance: nested list (REQUIRED, non-empty). Each item is either a typed oracle or plain text:
169
+ - command: <shell> (oracle: command — exit code)
170
+ - test: <name> (oracle: test — named test)
171
+ - judge: <rubric> (oracle: judge — LLM-judged, free text)
172
+ - <plain text> (compat: compiles as judge oracle, warns)
173
+ -->
174
+
175
+ ## T1: Scaffold the feature
176
+ - goal: Lay the groundwork for the feature
177
+ - shape: implement
178
+ - files: src/feature.ts
179
+ - context: docs/design.md
180
+ - complexity: 4
181
+ - acceptance:
182
+ - feature module exists and exports its entry point
183
+ - npm test stays green
184
+
185
+ ## T2: Cover it with tests
186
+ - goal: Add tests for the feature
187
+ - shape: tests
188
+ - deps: T1
189
+ - files: tests/feature.test.ts
190
+ - complexity: 3
191
+ - floor: cheap
192
+ - acceptance:
193
+ - new tests pass
194
+ - coverage floor holds
195
+ `;
196
+ }
@@ -0,0 +1,2 @@
1
+ import { type RunGraph } from "../graph/schema.js";
2
+ export declare function compilePrd(file: string): RunGraph;
@@ -0,0 +1,64 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { validateGraph } from "../graph/schema.js";
3
+ import { CompileError, inferShape, sha256 } from "./common.js";
4
+ const HEAD_RE = /^## (T\d+):\s*(.+)$/;
5
+ const FIELD_RE = /^- (\w+):\s*(.*)$/;
6
+ const NESTED_RE = /^\s+- (.+)$/;
7
+ export function compilePrd(file) {
8
+ if (!existsSync(file))
9
+ throw new CompileError(`no such PRD file: ${file}`);
10
+ const content = readFileSync(file, "utf8");
11
+ const drafts = [];
12
+ for (const line of content.split("\n")) {
13
+ const h = line.match(HEAD_RE);
14
+ if (h) {
15
+ drafts.push({ id: h[1], title: h[2].trim(), fields: {}, acceptance: [], inAcceptance: false });
16
+ continue;
17
+ }
18
+ const d = drafts.at(-1);
19
+ if (!d)
20
+ continue;
21
+ const f = line.match(FIELD_RE);
22
+ if (f) {
23
+ d.inAcceptance = f[1].toLowerCase() === "acceptance";
24
+ if (!d.inAcceptance)
25
+ d.fields[f[1].toLowerCase()] = f[2].trim();
26
+ continue;
27
+ }
28
+ const n = line.match(NESTED_RE);
29
+ if (n && d.inAcceptance)
30
+ d.acceptance.push(n[1].trim());
31
+ else if (line.trim() && !line.startsWith(" "))
32
+ d.inAcceptance = false;
33
+ }
34
+ if (!drafts.length) {
35
+ throw new CompileError(`${file} has no task sections. Expected "## T1: Title" headings with field bullets (goal/shape/deps/files/acceptance).`);
36
+ }
37
+ const missing = drafts.filter((d) => !d.acceptance.length).map((d) => d.id);
38
+ if (missing.length) {
39
+ throw new CompileError(`acceptance criteria are required on every task. Missing on: ${missing.join(", ")}.\n` +
40
+ `Add to each section in ${file}:\n- acceptance:\n - <observable outcome>`);
41
+ }
42
+ const csv = (v) => (v && v.toLowerCase() !== "none" ? v.split(",").map((s) => s.trim()).filter(Boolean) : []);
43
+ const tasks = drafts.map((d) => {
44
+ const pin = d.fields.pin?.split(/\s+/);
45
+ return {
46
+ id: d.id,
47
+ title: d.title,
48
+ goal: d.fields.goal ?? d.title,
49
+ shape: d.fields.shape ?? inferShape(d.title),
50
+ complexity: d.fields.complexity ? Number(d.fields.complexity) : 5,
51
+ deps: csv(d.fields.deps),
52
+ files: csv(d.fields.files),
53
+ context: csv(d.fields.context),
54
+ acceptance: d.acceptance,
55
+ ...(d.fields.humangate === "true" ? { humanGate: true } : {}),
56
+ ...(pin && pin.length === 2 ? { routingHints: { pin: { via: pin[0], model: pin[1] } } } : {}),
57
+ };
58
+ });
59
+ return validateGraph({
60
+ version: 1,
61
+ spec: { source: "prd", paths: [file], hash: sha256(content) },
62
+ tasks,
63
+ });
64
+ }
@@ -0,0 +1,2 @@
1
+ import { type RunGraph } from "../graph/schema.js";
2
+ export declare function compileSpecKit(dir: string): RunGraph;
@@ -0,0 +1,66 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { validateGraph } from "../graph/schema.js";
4
+ import { CompileError, inferShape, sha256 } from "./common.js";
5
+ const TASK_RE = /^- \[( |x)\] (T\d+)\s*(\[P\])?\s*(.+)$/;
6
+ const SUB_RE = /^\s+- (\w+):\s*(.+)$/;
7
+ export function compileSpecKit(dir) {
8
+ const path = join(dir, "tasks.md");
9
+ if (!existsSync(path)) {
10
+ throw new CompileError(`no tasks.md in ${dir} — point drovr compile at a Spec Kit feature directory`);
11
+ }
12
+ const content = readFileSync(path, "utf8");
13
+ const drafts = [];
14
+ for (const line of content.split("\n")) {
15
+ const t = line.match(TASK_RE);
16
+ if (t) {
17
+ drafts.push({ id: t[2], title: t[4].trim(), parallel: !!t[3], done: t[1] === "x", fields: {} });
18
+ continue;
19
+ }
20
+ const s = line.match(SUB_RE);
21
+ if (s && drafts.length) {
22
+ const d = drafts[drafts.length - 1];
23
+ (d.fields[s[1].toLowerCase()] ??= []).push(s[2].trim());
24
+ }
25
+ }
26
+ if (!drafts.length)
27
+ throw new CompileError(`${path} contains no task lines ("- [ ] T001 ...")`);
28
+ const missing = drafts.filter((d) => !d.fields.acceptance?.length).map((d) => d.id);
29
+ if (missing.length) {
30
+ throw new CompileError(`acceptance criteria are required on every task (spec rule). Missing on: ${missing.join(", ")}.\n` +
31
+ `Annotate each task in ${path} with an indented sub-bullet:\n - acceptance: <observable outcome>`);
32
+ }
33
+ // [P] semantics → dependency edges
34
+ let barrier = null;
35
+ let sinceBarrier = [];
36
+ const csv = (v) => (v ? v.flatMap((x) => x.split(",")).map((s) => s.trim()).filter(Boolean) : []);
37
+ const tasks = drafts.map((d) => {
38
+ const deps = d.parallel
39
+ ? barrier ? [barrier] : []
40
+ : sinceBarrier.length ? [...sinceBarrier] : barrier ? [barrier] : [];
41
+ if (d.parallel) {
42
+ sinceBarrier.push(d.id);
43
+ }
44
+ else {
45
+ barrier = d.id;
46
+ sinceBarrier = [];
47
+ }
48
+ return {
49
+ id: d.id,
50
+ title: d.title,
51
+ goal: d.fields.goal?.[0] ?? d.title,
52
+ shape: d.fields.shape?.[0] ?? inferShape(d.title),
53
+ complexity: d.fields.complexity ? Number(d.fields.complexity[0]) : 5,
54
+ deps,
55
+ files: csv(d.fields.files),
56
+ context: csv(d.fields.context),
57
+ acceptance: d.fields.acceptance,
58
+ ...(d.done ? { status: "done" } : {}),
59
+ };
60
+ });
61
+ return validateGraph({
62
+ version: 1,
63
+ spec: { source: "speckit", paths: [path], hash: sha256(content) },
64
+ tasks,
65
+ });
66
+ }