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,105 @@
1
+ import { channelKey } from "../adapters/types.js";
2
+ import { TIER_RANK } from "../config/config.js";
3
+ import { getAdapter } from "../adapters/registry.js";
4
+ import { GATE_NAMES } from "../graph/schema.js";
5
+ import { acceptanceGate } from "./acceptance.js";
6
+ import { compareToBaseline } from "./baseline.js";
7
+ import { evidenceGate } from "./evidence.js";
8
+ import { marginalCostRank } from "../route/router.js";
9
+ import { reviewGate } from "./review.js";
10
+ import { scopeGate } from "./scope.js";
11
+ export async function runGates(task, ctx) {
12
+ const results = [];
13
+ let commits = [];
14
+ const shapeGates = ctx.cfg.gates.byShape?.[task.shape];
15
+ const enabled = (g) => task.gates.includes(g) && (g !== "acceptance" && g !== "review" || shapeGates?.[g] !== false);
16
+ const failed = () => results.some((r) => !r.pass);
17
+ const sequence = GATE_NAMES.filter((g) => enabled(g));
18
+ const total = sequence.length;
19
+ const indexOf = (gate) => sequence.indexOf(gate) + 1;
20
+ const record = async (result) => {
21
+ results.push(result);
22
+ await ctx.onGate?.({ phase: "end", gate: result.gate, result });
23
+ };
24
+ const emitStart = async (gate) => {
25
+ await ctx.onGate?.({ phase: "start", gate, index: indexOf(gate), total });
26
+ };
27
+ // 1. build/test/lint vs shared baseline — deterministic and cheap, first
28
+ const toolGates = ["build", "test", "lint"].filter(enabled);
29
+ if (toolGates.length) {
30
+ // ponytail: compareToBaseline batches build/test/lint — their starts are emitted at iteration,
31
+ // not at true execution start. They are collectively sub-second (measured), so the debounce
32
+ // suppresses them anyway; split compareToBaseline only if a tool gate ever gets slow.
33
+ const toolResults = await compareToBaseline(ctx.worktree, ctx.commands, ctx.baseline, toolGates);
34
+ for (const r of toolResults) {
35
+ await emitStart(r.gate);
36
+ await record(r);
37
+ }
38
+ if (failed())
39
+ return { results, commits };
40
+ }
41
+ // 2. evidence — did the worker actually commit anything?
42
+ if (enabled("evidence")) {
43
+ await emitStart("evidence");
44
+ const e = await evidenceGate(ctx.worktree, ctx.baseRef);
45
+ commits = e.commits;
46
+ await record({ gate: e.gate, pass: e.pass, details: e.details });
47
+ if (!e.pass)
48
+ return { results, commits };
49
+ }
50
+ // 3. scope
51
+ if (enabled("scope")) {
52
+ await emitStart("scope");
53
+ await record(await scopeGate(ctx.worktree, ctx.baseRef, task.files, ctx.result, ctx.cfg.scope?.allowDeviations ?? []));
54
+ if (failed())
55
+ return { results, commits };
56
+ }
57
+ // 4. acceptance judge (first LLM spend — everything cheaper already passed)
58
+ if (enabled("acceptance")) {
59
+ await emitStart("acceptance");
60
+ const judgeAdapter = getAdapter(ctx.cfg.judge.adapter, ctx.adapters);
61
+ const jvia = ctx.via
62
+ ? { driver: ctx.via.driver, keep: ctx.via.keep, onSlot: ctx.via.onSlot, name: ctx.via.nameFor("judge", judgeAdapter.id), label: ctx.via.labelFor("judge") }
63
+ : undefined;
64
+ // v1.19 (T2): testCmd threads the detected test runner to the gate so named-test oracles run
65
+ // deterministically (filtered via -t) before any LLM judge dispatch.
66
+ let a = await acceptanceGate(task, ctx.worktree, ctx.baseRef, { adapter: judgeAdapter, model: ctx.cfg.judge.model }, jvia, { testCmd: ctx.commands.test });
67
+ // GATE-09: an unparseable judge verdict retries the JUDGE exactly once on a failover channel — never
68
+ // the worker (run-20260711-185020 P43-03 L70-72 billed a judge flake as a worker attempt). The flaked
69
+ // first verdict NEVER enters results (no false gate-result journal event, no operator notify, no stale
70
+ // failed() short-circuit — research Pitfall 5). Detection is meta-only (D-03), never string-matching
71
+ // details. The v1.1 badReviewers precedent's TIMING can't transfer: its failover lands on the NEXT
72
+ // worker attempt — exactly what this fix forbids; only its meta-carries-channel pattern is mirrored.
73
+ // Straight-line single `if` — NO loop/counter/knob: exactly-once by construction (a knob is a
74
+ // fail-closed weakening vector); a second garbage verdict fails the gate closed exactly as today.
75
+ if (a.meta?.unparseable === true && typeof a.meta.judge === "string") {
76
+ const flakedKey = a.meta.judge;
77
+ const candidate = ctx.channels
78
+ .filter((c) => channelKey(c) !== flakedKey)
79
+ // pickReviewer's sort (review.ts:37): TIER_RANK desc, marginalCostRank asc — proven ordering; both
80
+ // symbols already imported by a sibling gate file. No vendor-diversity axis (the judge isn't review).
81
+ .sort((x, y) => TIER_RANK[y.tier] - TIER_RANK[x.tier] || marginalCostRank(x) - marginalCostRank(y))[0];
82
+ // ponytail: same-channel fallback when the fleet has no alternative (D-03). One judge call against a
83
+ // deterministic garbage source is wasted, bounded; fail-closed unchanged on a second garbage verdict.
84
+ const retry = candidate ?? { adapter: ctx.cfg.judge.adapter, model: ctx.cfg.judge.model };
85
+ const retryAdapter = getAdapter(retry.adapter, ctx.adapters);
86
+ const retryJvia = ctx.via
87
+ // unconditional -r1 suffix: under keepPanes:forever a same-channel retry cannot collide with the
88
+ // still-open first pane (herdr agent_name_taken regression, research Pitfall 4)
89
+ ? { driver: ctx.via.driver, keep: ctx.via.keep, onSlot: ctx.via.onSlot, name: ctx.via.nameFor("judge", retryAdapter.id) + "-r1", label: ctx.via.labelFor("judge") }
90
+ : undefined;
91
+ // the retry IS a second acceptanceGate call: one code path, one parser, zero new parse leniency.
92
+ a = await acceptanceGate(task, ctx.worktree, ctx.baseRef, { adapter: retryAdapter, model: retry.model }, retryJvia, { testCmd: ctx.commands.test });
93
+ a = { ...a, meta: { ...a.meta, judgeRetry: { flaked: flakedKey, retried: channelKey({ adapter: retry.adapter, model: retry.model }) } } };
94
+ }
95
+ await record(a);
96
+ if (failed())
97
+ return { results, commits };
98
+ }
99
+ // 5. cross-vendor review
100
+ if (enabled("review")) {
101
+ await emitStart("review");
102
+ await record(await reviewGate(task, ctx.worktree, ctx.baseRef, ctx.author, ctx.channels, ctx.adapters, ctx.cfg, ctx.via, ctx.excludeReviewers));
103
+ }
104
+ return { results, commits };
105
+ }
@@ -0,0 +1,8 @@
1
+ import type { WorkerResult } from "../adapters/types.js";
2
+ import type { GateResult } from "./types.js";
3
+ /** Pure offender split — corpus-replay oracle exercises this exact decision logic. */
4
+ export declare function dispositionOffenders(offenders: string[], allowDeviations: string[]): {
5
+ hard: string[];
6
+ allowed: string[];
7
+ };
8
+ export declare function scopeGate(worktree: string, baseRef: string, files: string[], result: WorkerResult, allowDeviations?: string[]): Promise<GateResult>;
@@ -0,0 +1,32 @@
1
+ import picomatch from "picomatch";
2
+ import { shOk } from "../run/git.js";
3
+ /** Pure offender split — corpus-replay oracle exercises this exact decision logic. */
4
+ export function dispositionOffenders(offenders, allowDeviations) {
5
+ const allowedMatch = allowDeviations.length ? picomatch(allowDeviations, { dot: true }) : () => false;
6
+ const allowed = [];
7
+ const hard = [];
8
+ for (const f of offenders) {
9
+ if (allowedMatch(f))
10
+ allowed.push(f);
11
+ else
12
+ hard.push(f);
13
+ }
14
+ return { hard, allowed };
15
+ }
16
+ export async function scopeGate(worktree, baseRef, files, result, allowDeviations = []) {
17
+ if (!files.length)
18
+ return { gate: "scope", pass: true, details: "no file scope declared — unrestricted" };
19
+ const changed = (await shOk(`git diff --name-only '${baseRef}..HEAD'`, worktree)).trim().split("\n").filter(Boolean);
20
+ const inScope = picomatch(files, { dot: true }); // byte-identical options to assertWriteScope in src/compile/common.ts
21
+ const offenders = changed.filter((f) => !inScope(f));
22
+ if (!offenders.length)
23
+ return { gate: "scope", pass: true, details: `all ${changed.length} changed files in scope` };
24
+ // HARD-08: a worker's declared deviation is an audit note, NEVER a pass — the gate decides against
25
+ // operator config only (invariant: gates never trust worker claims).
26
+ const { hard, allowed } = dispositionOffenders(offenders, allowDeviations);
27
+ const note = result.deviations.length ? `\nworker-declared deviations (audit note, not authority): ${result.deviations.join("; ")}` : "";
28
+ if (!hard.length) {
29
+ return { gate: "scope", pass: true, details: `out-of-scope but operator-allowlisted:\n${allowed.join("\n")}${note}` };
30
+ }
31
+ return { gate: "scope", pass: false, details: `out-of-scope edits not covered by scope.allowDeviations:\n${hard.join("\n")}${note}` };
32
+ }
@@ -0,0 +1,6 @@
1
+ export interface GateResult {
2
+ gate: string;
3
+ pass: boolean;
4
+ details: string;
5
+ meta?: Record<string, unknown>;
6
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,20 @@
1
+ import { type RunGraph, type Task, type TaskStatus } from "./schema.js";
2
+ export declare function stateDirName(repoRoot: string): string;
3
+ export declare function graphPath(repoRoot: string): string;
4
+ export declare function drovrDir(repoRoot: string): string;
5
+ export declare function loadGraph(repoRoot: string): RunGraph;
6
+ export declare function saveGraph(repoRoot: string, g: RunGraph): void;
7
+ export declare function getTask(g: RunGraph, id: string): Task;
8
+ export declare function setStatus(g: RunGraph, id: string, status: TaskStatus): RunGraph;
9
+ export declare function addEvidence(g: RunGraph, id: string, patch: {
10
+ commits?: string[];
11
+ artifacts?: string[];
12
+ gateResults?: unknown[];
13
+ }): RunGraph;
14
+ export declare function readyTasks(g: RunGraph): Task[];
15
+ export declare function isComplete(g: RunGraph): boolean;
16
+ export declare function isStalled(g: RunGraph): boolean;
17
+ export declare function closureReaches(g: RunGraph, taskId: string, pred: (t: Task) => boolean): boolean;
18
+ export declare function blockedTasks(g: RunGraph): Task[];
19
+ export declare function pendingTasks(g: RunGraph): Task[];
20
+ export declare function attributeBlocked(g: RunGraph): Map<string, number>;
@@ -0,0 +1,148 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { validateGraph } from "./schema.js";
4
+ const stateDirs = new Map();
5
+ export function stateDirName(repoRoot) {
6
+ const cached = stateDirs.get(repoRoot);
7
+ if (cached)
8
+ return cached;
9
+ const name = existsSync(join(repoRoot, ".drovr")) ? ".drovr" : existsSync(join(repoRoot, ".drover")) ? ".drover" : ".drovr";
10
+ stateDirs.set(repoRoot, name);
11
+ return name;
12
+ }
13
+ export function graphPath(repoRoot) {
14
+ return join(repoRoot, stateDirName(repoRoot), "graph.json");
15
+ }
16
+ export function drovrDir(repoRoot) {
17
+ const dir = join(repoRoot, stateDirName(repoRoot));
18
+ mkdirSync(dir, { recursive: true });
19
+ const gi = join(dir, ".gitignore");
20
+ if (!existsSync(gi))
21
+ writeFileSync(gi, "*\n");
22
+ return dir;
23
+ }
24
+ export function loadGraph(repoRoot) {
25
+ const p = graphPath(repoRoot);
26
+ if (!existsSync(p))
27
+ throw new Error(`no graph at ${p} — run \`drovr compile <src>\` first`);
28
+ return validateGraph(JSON.parse(readFileSync(p, "utf8")));
29
+ }
30
+ export function saveGraph(repoRoot, g) {
31
+ drovrDir(repoRoot);
32
+ const p = graphPath(repoRoot);
33
+ // Temp file MUST be a sibling of graph.json: rename(2) is atomic only within one filesystem
34
+ // (never os.tmpdir()). pid-suffix so a racing writer can't clobber our in-flight temp (HARD-04).
35
+ const tmp = `${p}.${process.pid}.tmp`;
36
+ try {
37
+ writeFileSync(tmp, JSON.stringify(g, null, 2) + "\n");
38
+ renameSync(tmp, p); // atomic same-volume (APFS included) — a reader never sees a torn document
39
+ }
40
+ catch (e) {
41
+ rmSync(tmp, { force: true }); // no .tmp litter behind a failed write
42
+ throw e;
43
+ }
44
+ }
45
+ export function getTask(g, id) {
46
+ const t = g.tasks.find((t) => t.id === id);
47
+ if (!t)
48
+ throw new Error(`unknown task ${id}`);
49
+ return t;
50
+ }
51
+ export function setStatus(g, id, status) {
52
+ getTask(g, id);
53
+ return { ...g, tasks: g.tasks.map((t) => (t.id === id ? { ...t, status } : t)) };
54
+ }
55
+ export function addEvidence(g, id, patch) {
56
+ getTask(g, id);
57
+ return {
58
+ ...g,
59
+ tasks: g.tasks.map((t) => t.id === id
60
+ ? {
61
+ ...t,
62
+ evidence: {
63
+ commits: [...t.evidence.commits, ...(patch.commits ?? [])],
64
+ artifacts: [...t.evidence.artifacts, ...(patch.artifacts ?? [])],
65
+ gateResults: [...t.evidence.gateResults, ...(patch.gateResults ?? [])],
66
+ },
67
+ }
68
+ : t),
69
+ };
70
+ }
71
+ export function readyTasks(g) {
72
+ const done = new Set(g.tasks.filter((t) => t.status === "done").map((t) => t.id));
73
+ return g.tasks.filter((t) => t.status === "pending" && t.deps.every((d) => done.has(d)));
74
+ }
75
+ export function isComplete(g) {
76
+ return g.tasks.every((t) => t.status === "done");
77
+ }
78
+ export function isStalled(g) {
79
+ const running = g.tasks.some((t) => t.status === "running" || t.status === "gated");
80
+ return !isComplete(g) && !running && readyTasks(g).length === 0;
81
+ }
82
+ // parked = a terminal-for-now state that strands every downstream task (D-02/D-06)
83
+ const isParked = (t) => t.status === "human" || t.status === "failed";
84
+ // closureReaches: true iff the transitive dep-closure of `taskId` contains a task matching `pred`.
85
+ // Walks FORWARD along Task.deps with an iterative stack + visited-set; cycles are impossible
86
+ // (validateGraph rejects them at load), so a visited-set is for efficiency, not safety. Pure —
87
+ // depends on RunGraph alone, never daemon/run state (D-06).
88
+ export function closureReaches(g, taskId, pred) {
89
+ const byId = new Map(g.tasks.map((t) => [t.id, t]));
90
+ const visited = new Set();
91
+ const stack = [...(byId.get(taskId)?.deps ?? [])];
92
+ while (stack.length) {
93
+ const id = stack.pop();
94
+ if (visited.has(id))
95
+ continue;
96
+ visited.add(id);
97
+ const t = byId.get(id);
98
+ if (!t)
99
+ continue; // unknown deps are rejected at validate; defensive only
100
+ if (pred(t))
101
+ return true;
102
+ for (const d of t.deps)
103
+ if (!visited.has(d))
104
+ stack.push(d);
105
+ }
106
+ return false;
107
+ }
108
+ // blockedTasks: pending tasks whose dep-closure reaches a parked (human|failed) task —
109
+ // "not-yet-run AND structurally unreachable" — the truthful bucket the operator must see (VIS-01).
110
+ export function blockedTasks(g) {
111
+ return g.tasks.filter((t) => t.status === "pending" && closureReaches(g, t.id, isParked));
112
+ }
113
+ // pendingTasks: pending tasks whose closure does NOT reach a parked task — still runnable
114
+ // in principle (deps pending/running). Distinct from blocked so the buckets never lie mid-quiesce.
115
+ export function pendingTasks(g) {
116
+ return g.tasks.filter((t) => t.status === "pending" && !closureReaches(g, t.id, isParked));
117
+ }
118
+ // attributeBlocked: for every blockedTasks(g) member, BFS forward over deps (level order, not
119
+ // closureReaches' DFS) to find its NEAREST parked (human|failed) ancestor and count it there.
120
+ // A task under two parked roots attributes to the nearer one only — never double-counted (D-04).
121
+ export function attributeBlocked(g) {
122
+ const byId = new Map(g.tasks.map((t) => [t.id, t]));
123
+ const counts = new Map();
124
+ for (const t of blockedTasks(g)) {
125
+ const visited = new Set();
126
+ const queue = [...t.deps];
127
+ let root;
128
+ while (queue.length) {
129
+ const id = queue.shift();
130
+ if (visited.has(id))
131
+ continue;
132
+ visited.add(id);
133
+ const dep = byId.get(id);
134
+ if (!dep)
135
+ continue;
136
+ if (isParked(dep)) {
137
+ root = dep.id;
138
+ break;
139
+ }
140
+ for (const d of dep.deps)
141
+ if (!visited.has(d))
142
+ queue.push(d);
143
+ }
144
+ if (root)
145
+ counts.set(root, (counts.get(root) ?? 0) + 1);
146
+ }
147
+ return counts;
148
+ }
@@ -0,0 +1,185 @@
1
+ import { z } from "zod";
2
+ export declare const SHAPES: readonly ["plan", "spec", "implement", "tests", "docs", "migration", "ui", "refactor", "chore"];
3
+ export declare const STATUSES: readonly ["pending", "running", "gated", "failed", "done", "human"];
4
+ export declare const GATE_NAMES: readonly ["build", "test", "lint", "evidence", "scope", "acceptance", "review"];
5
+ export declare const TIERS: readonly ["cheap", "mid", "frontier"];
6
+ export declare const ORACLES: readonly ["command", "test", "judge"];
7
+ export type Shape = (typeof SHAPES)[number];
8
+ export type TaskStatus = (typeof STATUSES)[number];
9
+ export type GateName = (typeof GATE_NAMES)[number];
10
+ export type Oracle = (typeof ORACLES)[number];
11
+ export declare const AcceptanceItemSchema: z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
12
+ oracle: z.ZodLiteral<"command">;
13
+ command: z.ZodString;
14
+ }, z.core.$strip>, z.ZodObject<{
15
+ oracle: z.ZodLiteral<"test">;
16
+ test: z.ZodString;
17
+ }, z.core.$strip>, z.ZodObject<{
18
+ oracle: z.ZodLiteral<"judge">;
19
+ text: z.ZodString;
20
+ }, z.core.$strip>]>;
21
+ export type AcceptanceItem = z.infer<typeof AcceptanceItemSchema>;
22
+ export declare function renderAcceptanceItem(item: AcceptanceItem): string;
23
+ export declare const TaskSchema: z.ZodObject<{
24
+ id: z.ZodString;
25
+ title: z.ZodString;
26
+ goal: z.ZodString;
27
+ shape: z.ZodEnum<{
28
+ plan: "plan";
29
+ spec: "spec";
30
+ implement: "implement";
31
+ tests: "tests";
32
+ docs: "docs";
33
+ migration: "migration";
34
+ ui: "ui";
35
+ refactor: "refactor";
36
+ chore: "chore";
37
+ }>;
38
+ complexity: z.ZodNumber;
39
+ deps: z.ZodDefault<z.ZodArray<z.ZodString>>;
40
+ files: z.ZodDefault<z.ZodArray<z.ZodString>>;
41
+ context: z.ZodDefault<z.ZodArray<z.ZodString>>;
42
+ acceptance: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
43
+ oracle: z.ZodLiteral<"command">;
44
+ command: z.ZodString;
45
+ }, z.core.$strip>, z.ZodObject<{
46
+ oracle: z.ZodLiteral<"test">;
47
+ test: z.ZodString;
48
+ }, z.core.$strip>, z.ZodObject<{
49
+ oracle: z.ZodLiteral<"judge">;
50
+ text: z.ZodString;
51
+ }, z.core.$strip>]>>;
52
+ scopeHints: z.ZodOptional<z.ZodArray<z.ZodObject<{
53
+ paths: z.ZodArray<z.ZodString>;
54
+ confidence: z.ZodOptional<z.ZodNumber>;
55
+ reason: z.ZodOptional<z.ZodString>;
56
+ }, z.core.$strip>>>;
57
+ gates: z.ZodDefault<z.ZodArray<z.ZodEnum<{
58
+ build: "build";
59
+ test: "test";
60
+ lint: "lint";
61
+ evidence: "evidence";
62
+ scope: "scope";
63
+ acceptance: "acceptance";
64
+ review: "review";
65
+ }>>>;
66
+ routingHints: z.ZodOptional<z.ZodObject<{
67
+ pin: z.ZodOptional<z.ZodObject<{
68
+ via: z.ZodString;
69
+ model: z.ZodString;
70
+ }, z.core.$strip>>;
71
+ floor: z.ZodOptional<z.ZodEnum<{
72
+ cheap: "cheap";
73
+ mid: "mid";
74
+ frontier: "frontier";
75
+ }>>;
76
+ source: z.ZodOptional<z.ZodString>;
77
+ escalate: z.ZodOptional<z.ZodBoolean>;
78
+ }, z.core.$strip>>;
79
+ humanGate: z.ZodDefault<z.ZodBoolean>;
80
+ status: z.ZodDefault<z.ZodEnum<{
81
+ pending: "pending";
82
+ running: "running";
83
+ gated: "gated";
84
+ failed: "failed";
85
+ done: "done";
86
+ human: "human";
87
+ }>>;
88
+ evidence: z.ZodDefault<z.ZodObject<{
89
+ commits: z.ZodDefault<z.ZodArray<z.ZodString>>;
90
+ artifacts: z.ZodDefault<z.ZodArray<z.ZodString>>;
91
+ gateResults: z.ZodDefault<z.ZodArray<z.ZodUnknown>>;
92
+ }, z.core.$strip>>;
93
+ }, z.core.$strip>;
94
+ export declare const RunGraphSchema: z.ZodObject<{
95
+ version: z.ZodLiteral<1>;
96
+ spec: z.ZodObject<{
97
+ source: z.ZodEnum<{
98
+ speckit: "speckit";
99
+ gsd: "gsd";
100
+ prd: "prd";
101
+ native: "native";
102
+ taskmaster: "taskmaster";
103
+ }>;
104
+ paths: z.ZodArray<z.ZodString>;
105
+ hash: z.ZodString;
106
+ }, z.core.$strip>;
107
+ tasks: z.ZodArray<z.ZodObject<{
108
+ id: z.ZodString;
109
+ title: z.ZodString;
110
+ goal: z.ZodString;
111
+ shape: z.ZodEnum<{
112
+ plan: "plan";
113
+ spec: "spec";
114
+ implement: "implement";
115
+ tests: "tests";
116
+ docs: "docs";
117
+ migration: "migration";
118
+ ui: "ui";
119
+ refactor: "refactor";
120
+ chore: "chore";
121
+ }>;
122
+ complexity: z.ZodNumber;
123
+ deps: z.ZodDefault<z.ZodArray<z.ZodString>>;
124
+ files: z.ZodDefault<z.ZodArray<z.ZodString>>;
125
+ context: z.ZodDefault<z.ZodArray<z.ZodString>>;
126
+ acceptance: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
127
+ oracle: z.ZodLiteral<"command">;
128
+ command: z.ZodString;
129
+ }, z.core.$strip>, z.ZodObject<{
130
+ oracle: z.ZodLiteral<"test">;
131
+ test: z.ZodString;
132
+ }, z.core.$strip>, z.ZodObject<{
133
+ oracle: z.ZodLiteral<"judge">;
134
+ text: z.ZodString;
135
+ }, z.core.$strip>]>>;
136
+ scopeHints: z.ZodOptional<z.ZodArray<z.ZodObject<{
137
+ paths: z.ZodArray<z.ZodString>;
138
+ confidence: z.ZodOptional<z.ZodNumber>;
139
+ reason: z.ZodOptional<z.ZodString>;
140
+ }, z.core.$strip>>>;
141
+ gates: z.ZodDefault<z.ZodArray<z.ZodEnum<{
142
+ build: "build";
143
+ test: "test";
144
+ lint: "lint";
145
+ evidence: "evidence";
146
+ scope: "scope";
147
+ acceptance: "acceptance";
148
+ review: "review";
149
+ }>>>;
150
+ routingHints: z.ZodOptional<z.ZodObject<{
151
+ pin: z.ZodOptional<z.ZodObject<{
152
+ via: z.ZodString;
153
+ model: z.ZodString;
154
+ }, z.core.$strip>>;
155
+ floor: z.ZodOptional<z.ZodEnum<{
156
+ cheap: "cheap";
157
+ mid: "mid";
158
+ frontier: "frontier";
159
+ }>>;
160
+ source: z.ZodOptional<z.ZodString>;
161
+ escalate: z.ZodOptional<z.ZodBoolean>;
162
+ }, z.core.$strip>>;
163
+ humanGate: z.ZodDefault<z.ZodBoolean>;
164
+ status: z.ZodDefault<z.ZodEnum<{
165
+ pending: "pending";
166
+ running: "running";
167
+ gated: "gated";
168
+ failed: "failed";
169
+ done: "done";
170
+ human: "human";
171
+ }>>;
172
+ evidence: z.ZodDefault<z.ZodObject<{
173
+ commits: z.ZodDefault<z.ZodArray<z.ZodString>>;
174
+ artifacts: z.ZodDefault<z.ZodArray<z.ZodString>>;
175
+ gateResults: z.ZodDefault<z.ZodArray<z.ZodUnknown>>;
176
+ }, z.core.$strip>>;
177
+ }, z.core.$strip>>;
178
+ }, z.core.$strip>;
179
+ export type Task = z.infer<typeof TaskSchema>;
180
+ export type RunGraph = z.infer<typeof RunGraphSchema>;
181
+ export declare class GraphValidationError extends Error {
182
+ issues: string[];
183
+ constructor(issues: string[]);
184
+ }
185
+ export declare function validateGraph(data: unknown): RunGraph;
@@ -0,0 +1,141 @@
1
+ import { z } from "zod";
2
+ export const SHAPES = ["plan", "spec", "implement", "tests", "docs", "migration", "ui", "refactor", "chore"];
3
+ export const STATUSES = ["pending", "running", "gated", "failed", "done", "human"];
4
+ export const GATE_NAMES = ["build", "test", "lint", "evidence", "scope", "acceptance", "review"];
5
+ export const TIERS = ["cheap", "mid", "frontier"];
6
+ // v1.19 acceptance oracles: command (exit code), test (named test), judge (LLM, free-text rubric).
7
+ // A plain string is the read-old/write-new compat form — semantically a judge oracle (spec §2).
8
+ export const ORACLES = ["command", "test", "judge"];
9
+ // Typed acceptance oracle: command carries the thing to run, test the test name, judge free text.
10
+ // Anything else (a typed object naming an unknown oracle) fails validation loudly here.
11
+ export const AcceptanceItemSchema = z.union([
12
+ z.string().min(1),
13
+ z.object({ oracle: z.literal("command"), command: z.string().min(1) }),
14
+ z.object({ oracle: z.literal("test"), test: z.string().min(1) }),
15
+ z.object({ oracle: z.literal("judge"), text: z.string().min(1) }),
16
+ ]);
17
+ // Shared text rendering of one acceptance item — every consumer (worker prompt, acceptance gate,
18
+ // review gate) renders typed items as text through THIS helper. Rendering only; oracle execution is T2.
19
+ export function renderAcceptanceItem(item) {
20
+ if (typeof item === "string")
21
+ return item;
22
+ if (item.oracle === "command")
23
+ return `$ ${item.command}`;
24
+ if (item.oracle === "test")
25
+ return `test: ${item.test}`;
26
+ return item.text; // judge — bare text, byte-identical to a plain-string judge criterion
27
+ }
28
+ export const TaskSchema = z.object({
29
+ // ids land in git branch names and herdr pane names — branch-safe characters only, bounded
30
+ // length (refs hit filesystem limits), and never "--" (the task-branch separator, locked
31
+ // decision 10) or a trailing dash. T1-style from prd/speckit; P07-01-style from gsd (v1.3).
32
+ id: z
33
+ .string()
34
+ .max(64)
35
+ .regex(/^[A-Za-z][A-Za-z0-9_-]*$/, "task ids look like T1 or P07-01 (letter start; letters/digits/-/_)")
36
+ .refine((id) => !id.includes("--") && !id.endsWith("-"), 'task ids may not contain "--" or end with "-"'),
37
+ title: z.string().min(1),
38
+ goal: z.string().min(1),
39
+ shape: z.enum(SHAPES),
40
+ complexity: z.number().int().min(1).max(10),
41
+ deps: z.array(z.string()).default([]),
42
+ files: z.array(z.string()).default([]),
43
+ context: z.array(z.string()).default([]),
44
+ acceptance: z.array(AcceptanceItemSchema).min(1, "acceptance[] is required and must be non-empty"),
45
+ // v1.19: purely advisory plan-time blast radius — NO gate reads this (the run-time scope gate stays
46
+ // authoritative fail-closed). Optional + best-effort: paths required, confidence/reason may be absent.
47
+ scopeHints: z
48
+ .array(z.object({
49
+ paths: z.array(z.string()),
50
+ confidence: z.number().min(0).max(1).optional(),
51
+ reason: z.string().optional(),
52
+ }))
53
+ .optional(),
54
+ gates: z.array(z.enum(GATE_NAMES)).default(["build", "test", "lint", "evidence", "scope", "acceptance", "review"]),
55
+ routingHints: z
56
+ .object({
57
+ pin: z.object({ via: z.string(), model: z.string() }).optional(),
58
+ floor: z.enum(TIERS).optional(),
59
+ source: z.string().optional(),
60
+ escalate: z.boolean().optional(),
61
+ })
62
+ .optional(),
63
+ humanGate: z.boolean().default(false),
64
+ status: z.enum(STATUSES).default("pending"),
65
+ evidence: z
66
+ .object({
67
+ commits: z.array(z.string()).default([]),
68
+ artifacts: z.array(z.string()).default([]),
69
+ gateResults: z.array(z.unknown()).default([]),
70
+ })
71
+ .default({ commits: [], artifacts: [], gateResults: [] }),
72
+ });
73
+ export const RunGraphSchema = z
74
+ .object({
75
+ version: z.literal(1),
76
+ spec: z.object({
77
+ source: z.enum(["speckit", "gsd", "prd", "native", "taskmaster"]),
78
+ paths: z.array(z.string()),
79
+ hash: z.string(),
80
+ }),
81
+ tasks: z.array(TaskSchema).min(1),
82
+ })
83
+ .superRefine((g, ctx) => {
84
+ const ids = new Set();
85
+ for (const t of g.tasks) {
86
+ if (ids.has(t.id))
87
+ ctx.addIssue({ code: "custom", message: `duplicate task id ${t.id}` });
88
+ ids.add(t.id);
89
+ }
90
+ for (const t of g.tasks)
91
+ for (const d of t.deps)
92
+ if (!ids.has(d))
93
+ ctx.addIssue({ code: "custom", message: `${t.id} depends on unknown task ${d}` });
94
+ const cycle = findCycle(g.tasks);
95
+ if (cycle)
96
+ ctx.addIssue({ code: "custom", message: `dependency cycle: ${cycle.join(" -> ")}` });
97
+ });
98
+ export class GraphValidationError extends Error {
99
+ issues;
100
+ constructor(issues) {
101
+ super(`invalid RunGraph:\n - ${issues.join("\n - ")}`);
102
+ this.issues = issues;
103
+ this.name = "GraphValidationError";
104
+ }
105
+ }
106
+ export function validateGraph(data) {
107
+ const r = RunGraphSchema.safeParse(data);
108
+ if (!r.success) {
109
+ throw new GraphValidationError(r.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`));
110
+ }
111
+ return r.data;
112
+ }
113
+ function findCycle(tasks) {
114
+ const deps = new Map(tasks.map((t) => [t.id, t.deps]));
115
+ const state = new Map();
116
+ const stack = [];
117
+ const visit = (id) => {
118
+ state.set(id, 1);
119
+ stack.push(id);
120
+ for (const d of deps.get(id) ?? []) {
121
+ if (state.get(d) === 1)
122
+ return [...stack.slice(stack.indexOf(d)), d];
123
+ if (!state.has(d) && deps.has(d)) {
124
+ const c = visit(d);
125
+ if (c)
126
+ return c;
127
+ }
128
+ }
129
+ state.set(id, 2);
130
+ stack.pop();
131
+ return null;
132
+ };
133
+ for (const t of tasks) {
134
+ if (!state.has(t.id)) {
135
+ const c = visit(t.id);
136
+ if (c)
137
+ return c;
138
+ }
139
+ }
140
+ return null;
141
+ }
@@ -0,0 +1 @@
1
+ export declare const VERSION = "0.1.0";
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export const VERSION = "0.1.0";
@@ -0,0 +1,4 @@
1
+ export declare function scopePrompt(intent: string, repair?: {
2
+ draft: string;
3
+ error: string;
4
+ }): string;