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,165 @@
1
+ import { existsSync, mkdtempSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { drovrDir, stateDirName } from "../graph/graph.js";
5
+ import { disallowedBy } from "../route/preference.js";
6
+ import { sh } from "../run/git.js";
7
+ import { claudeCode } from "./claude-code.js";
8
+ import { codex } from "./codex.js";
9
+ import { cursorAgent } from "./cursor-agent.js";
10
+ import { FakeAdapter } from "./fake.js";
11
+ import { grok } from "./grok.js";
12
+ import { opencode } from "./opencode.js";
13
+ import { pi } from "./pi.js";
14
+ import { channelKey, modelAuthed, QUOTA_RE } from "./types.js";
15
+ export function allAdapters(opts = {}) {
16
+ // pi + grok appended LAST: same-tier ties resolve by discovery order (Phase 6 D2), so appending
17
+ // keeps the Phase 6 shape→channel matrix byte-identical; inserting anywhere else silently
18
+ // reassigns shapes on same-tier ties. grok is appended AFTER pi for the same reason.
19
+ const real = [claudeCode, codex, cursorAgent, opencode, pi, grok];
20
+ const fakePath = opts.fakeScriptPath ?? process.env.DROVR_FAKE_SCRIPT ?? process.env.DROVER_FAKE_SCRIPT;
21
+ return fakePath ? [new FakeAdapter(fakePath), ...real] : real;
22
+ }
23
+ export function getAdapter(id, adapters) {
24
+ const a = adapters.find((a) => a.id === id);
25
+ if (!a)
26
+ throw new Error(`unknown adapter ${id} (have: ${adapters.map((a) => a.id).join(", ")})`);
27
+ return a;
28
+ }
29
+ export async function probeAll(adapters) {
30
+ const out = {};
31
+ await Promise.all(adapters.map(async (a) => { out[a.id] = await a.probe(); }));
32
+ return out;
33
+ }
34
+ const MODEL_PROBE_PROMPT = "Reply with exactly OK and nothing else.";
35
+ const MODEL_PROBE_TIMEOUT_MS = 30000;
36
+ // Auth-words only match when tied to a failure word; bare "auth"/"OAuth"/"authored" never fail (v1.27 T2).
37
+ const AUTH_FAILURE_RE = /\b4\d\d\b|\bauth(?:entication|orization)?\s+(?:error|failed|failure|denied)|unauthori[sz]ed|forbidden|access denied|credit(?:s)?\s+(?:exhausted|error|denied)/i;
38
+ function probeFailure(code, stdout, stderr, timedOut, timeoutMs = MODEL_PROBE_TIMEOUT_MS) {
39
+ // SIGKILL-timeout is not exit-1: report the budget, never the masked kill code (v1.27 T1).
40
+ if (timedOut)
41
+ return `probe timed out after ${timeoutMs}ms`;
42
+ const output = `${stderr}\n${stdout}`.trim().replace(/\s+/g, " ");
43
+ return code !== 0 || QUOTA_RE.test(output) || AUTH_FAILURE_RE.test(output)
44
+ ? output.slice(0, 240) || `probe exited ${code}`
45
+ : undefined;
46
+ }
47
+ // v1.21: one bounded, headless call per configured model; detected-but-unclassified models never enter this loop.
48
+ export async function probeModels(cfg, repoRoot, adapters, health) {
49
+ await Promise.all(adapters.map(async (a) => {
50
+ const h = health[a.id];
51
+ if (!h?.installed)
52
+ return;
53
+ // Tests inject FakeAdapter; never let an incidental default adapter spend a real token in the suite.
54
+ if (process.env.VITEST && [claudeCode, codex, cursorAgent, opencode, pi, grok].includes(a))
55
+ return;
56
+ const verdicts = {};
57
+ for (const model of Object.keys(cfg.tiers[a.id]?.models ?? {})) {
58
+ const probedAt = new Date().toISOString();
59
+ try {
60
+ if (typeof a.headlessCommand !== "function") {
61
+ verdicts[model] = { authed: false, reason: "headless probe unavailable", probedAt };
62
+ continue;
63
+ }
64
+ const promptFile = join(mkdtempSync(join(tmpdir(), "drovr-auth-")), "probe.md");
65
+ writeFileSync(promptFile, MODEL_PROBE_PROMPT);
66
+ let r = await sh(a.headlessCommand(promptFile, model), repoRoot, MODEL_PROBE_TIMEOUT_MS);
67
+ if (r.timedOut) {
68
+ r = await sh(a.headlessCommand(promptFile, model), repoRoot, MODEL_PROBE_TIMEOUT_MS);
69
+ if (r.timedOut) {
70
+ verdicts[model] = { authed: false, reason: `probe timed out twice (${MODEL_PROBE_TIMEOUT_MS}ms)`, probedAt };
71
+ continue;
72
+ }
73
+ }
74
+ const reason = probeFailure(r.code, r.stdout, r.stderr, r.timedOut, MODEL_PROBE_TIMEOUT_MS);
75
+ verdicts[model] = reason ? { authed: false, reason, probedAt } : { authed: true, probedAt };
76
+ }
77
+ catch (e) {
78
+ verdicts[model] = { authed: false, reason: String(e), probedAt };
79
+ }
80
+ }
81
+ h.modelAuth = verdicts;
82
+ }));
83
+ }
84
+ const doctorPath = (repoRoot) => join(repoRoot, stateDirName(repoRoot), "doctor.json");
85
+ export function writeDoctor(repoRoot, health) {
86
+ drovrDir(repoRoot);
87
+ writeFileSync(doctorPath(repoRoot), JSON.stringify(health, null, 2) + "\n");
88
+ }
89
+ export function readDoctor(repoRoot) {
90
+ return existsSync(doctorPath(repoRoot)) ? JSON.parse(readFileSync(doctorPath(repoRoot), "utf8")) : null;
91
+ }
92
+ export function discoverChannels(cfg, adapters, health) {
93
+ const base = adapters
94
+ .filter((a) => health[a.id]?.installed && health[a.id]?.authed)
95
+ .flatMap((a) => {
96
+ const h = health[a.id];
97
+ const s = h?.servable;
98
+ // T2 (2026-07-13): a model doctor marked unauthed (modelAuth[model].authed===false) advertises NO
99
+ // channel — routing a 403 into dispatch was the v1.10 regression. modelAuthed is true when no verdict
100
+ // is recorded (absent modelAuth ⇒ unknown ⇒ routable), so an all-authed/pre-v1.5 fleet is byte-identical.
101
+ return a.channels(cfg).filter((c) => modelAuthed(h, c.model) && (!s || s.includes(c.model)));
102
+ });
103
+ if (!cfg.routing.allow && !cfg.routing.deny)
104
+ return base;
105
+ return base.filter((c) => disallowedBy(c, cfg.routing) === null);
106
+ }
107
+ // HYG-07(a): the channels discoverChannels silently dropped because their model isn't in the adapter's
108
+ // served list (the HYG-05 filter above). Computed from the SAME inputs as the drop (a.channels(cfg), not
109
+ // channelsFromConfig — FakeAdapter overrides channels()), so attribution can never drift from behavior.
110
+ // installed+authed+servable-defined mirrors the filter's three gates exactly.
111
+ export function servableExclusions(cfg, adapters, health) {
112
+ const out = [];
113
+ for (const a of adapters) {
114
+ const h = health[a.id];
115
+ if (!h?.installed || !h?.authed || !h.servable)
116
+ continue;
117
+ for (const c of a.channels(cfg)) {
118
+ if (!h.servable.includes(c.model))
119
+ out.push({ key: channelKey(c), adapter: a.id });
120
+ }
121
+ }
122
+ return out;
123
+ }
124
+ // HYG-07(a): exclusionLine() voice — the operator already parses this vocabulary in plan+doctor.
125
+ export function servabilityLine(excluded) {
126
+ const parts = excluded.map(({ key, adapter }) => `${key} (not in ${adapter}'s served model list)`);
127
+ return `servability: ${excluded.length} channel(s) unservable — ${parts.join(", ")}`;
128
+ }
129
+ // T2 (2026-07-13): channels discoverChannels dropped because doctor marked their model unauthed
130
+ // (modelAuth[model].authed===false). Computed from the SAME inputs as the drop (a.channels(cfg)) so
131
+ // attribution can never drift from behavior. installed+authed mirrors the discoverChannels adapter gate;
132
+ // a missing modelAuth record ⇒ unknown ⇒ NOT excluded (compat with pre-v1.5 doctor.json + probeAll fallback).
133
+ export function modelAuthExclusions(cfg, adapters, health) {
134
+ const out = [];
135
+ for (const a of adapters) {
136
+ const h = health[a.id];
137
+ if (!h?.installed || !h?.authed)
138
+ continue;
139
+ const verdicts = h.modelAuth;
140
+ if (!verdicts)
141
+ continue;
142
+ for (const c of a.channels(cfg)) {
143
+ const v = verdicts[c.model];
144
+ if (v?.authed === false)
145
+ out.push({ key: channelKey(c), adapter: a.id, reason: v.reason ?? "probe failed", probedAt: v.probedAt });
146
+ }
147
+ }
148
+ return out;
149
+ }
150
+ // T2: one lint per exclusion, naming the probe reason and date (acceptance criterion). Mirrors servabilityLine voice.
151
+ export function modelAuthLine(excluded) {
152
+ const parts = excluded.map(({ key, reason, probedAt }) => `${key} (${reason} — probed ${probedAt.split("T")[0]})`);
153
+ return `model auth: ${excluded.length} channel(s) unauthed — ${parts.join(", ")}`;
154
+ }
155
+ // HYG-07(b): file mtime is the zero-schema-change staleness signal — doctor.json carries no probe timestamp,
156
+ // so a schema field would break the existing-files compat invariant. null when the file is absent (probeAll
157
+ // fallback path is fresh by construction). statSync is free vs the readDoctor that already happened.
158
+ export function doctorAgeMs(repoRoot) {
159
+ const p = doctorPath(repoRoot);
160
+ if (!existsSync(p))
161
+ return null;
162
+ // Math.max: mtimeMs is sub-ms float, Date.now() is int ms — clamp so a clock-skew fraction never
163
+ // renders a nonsensical negative age ("doctor.json is -0h old").
164
+ return Math.max(0, Date.now() - statSync(p).mtimeMs);
165
+ }
@@ -0,0 +1,98 @@
1
+ import { z } from "zod";
2
+ import type { DrovrConfig, Tier } from "../config/config.js";
3
+ import type { Task } from "../graph/schema.js";
4
+ export declare const TokenUsageSchema: z.ZodObject<{
5
+ input: z.ZodNumber;
6
+ output: z.ZodNumber;
7
+ cacheRead: z.ZodOptional<z.ZodNumber>;
8
+ cacheWrite: z.ZodOptional<z.ZodNumber>;
9
+ reasoning: z.ZodOptional<z.ZodNumber>;
10
+ }, z.core.$strip>;
11
+ export type TokenUsage = z.infer<typeof TokenUsageSchema>;
12
+ export declare function addUsage(a: TokenUsage | undefined, b: TokenUsage): TokenUsage;
13
+ export interface Assignment {
14
+ adapter: string;
15
+ model: string;
16
+ channel: "sub" | "api";
17
+ tier: Tier;
18
+ }
19
+ export interface BillingChannel {
20
+ adapter: string;
21
+ vendor: string;
22
+ model: string;
23
+ channel: "sub" | "api";
24
+ tier: Tier;
25
+ }
26
+ export interface ModelAuth {
27
+ authed: boolean;
28
+ reason?: string;
29
+ probedAt: string;
30
+ }
31
+ export interface AuthHealth {
32
+ installed: boolean;
33
+ authed: boolean;
34
+ version?: string;
35
+ models: string[];
36
+ note?: string;
37
+ modelsDetectedAt?: string;
38
+ servable?: string[];
39
+ modelAuth?: Record<string, ModelAuth>;
40
+ }
41
+ export declare function modelAuthed(health: AuthHealth | undefined, model: string): boolean;
42
+ export interface Invocation {
43
+ command: string;
44
+ }
45
+ export interface WorkerResult {
46
+ ok: boolean;
47
+ summary: string;
48
+ deviations: string[];
49
+ raw: string;
50
+ }
51
+ export interface ContextUsage {
52
+ tokens: number;
53
+ limit?: number;
54
+ }
55
+ export interface SessionRef {
56
+ cwd: string;
57
+ id: string;
58
+ }
59
+ export type TrustVerdict = {
60
+ status: "trusted";
61
+ } | {
62
+ status: "seeded";
63
+ } | {
64
+ status: "action-required";
65
+ command: string;
66
+ };
67
+ export interface TrustDialog {
68
+ fingerprint: string;
69
+ key: string;
70
+ }
71
+ export declare function matchesTrustDialog(paneText: string, dialog: TrustDialog): boolean;
72
+ export interface WorkerAdapter {
73
+ id: string;
74
+ vendor: string;
75
+ probe(): Promise<AuthHealth>;
76
+ channels(cfg: DrovrConfig): BillingChannel[];
77
+ headlessCommand(promptFile: string, model: string): string;
78
+ interactiveCommand(promptFile: string, model: string): string | null;
79
+ resumeCommand?(sessionId: string, promptFile: string, model: string): string;
80
+ invoke(task: Task, cwd: string, a: Assignment, ctx: {
81
+ promptFile: string;
82
+ }): Invocation;
83
+ parse(output: string, nonce: string): WorkerResult;
84
+ listModels?(): Promise<string[]>;
85
+ listModelsFetchedAt?(): string | undefined;
86
+ collectUsage?(cwd: string, sinceMs: number): TokenUsage | undefined;
87
+ contextUsage?(session: SessionRef): ContextUsage | null;
88
+ trust?(repoRoot: string): TrustVerdict;
89
+ trustDialog?: TrustDialog;
90
+ }
91
+ export declare function channelsFromConfig(adapterId: string, cfg: DrovrConfig): BillingChannel[];
92
+ export declare function channelKey(c: {
93
+ adapter: string;
94
+ model: string;
95
+ }): string;
96
+ export declare function shq(s: string): string;
97
+ export declare const QUOTA_RE: RegExp;
98
+ export declare const MODEL_ID_RE: RegExp;
@@ -0,0 +1,56 @@
1
+ import { z } from "zod";
2
+ // SPEND-01/06: normalized token counts — the measurable fact. NO cost field, ever: CLIs report
3
+ // cost:0 on sub plans and notional list prices on others (LIVE-CHECK finding 3); money is Phase 18's
4
+ // derivation (operator price × tokens), never a CLI claim (SPEND-06).
5
+ export const TokenUsageSchema = z.object({
6
+ input: z.number().int().nonnegative(),
7
+ output: z.number().int().nonnegative(),
8
+ cacheRead: z.number().int().nonnegative().optional(),
9
+ cacheWrite: z.number().int().nonnegative().optional(),
10
+ reasoning: z.number().int().nonnegative().optional(),
11
+ });
12
+ // SPEND-02 accumulation across attempts. The inner ?? 0 is the ONE sanctioned coalesce for usage —
13
+ // safe ONLY behind the both-undefined guard (mirrors classify()'s consults ?? 0 precedent): it can
14
+ // merge two observed counts, it can never MATERIALIZE a count for an unobserved attempt. Absent+absent
15
+ // stays absent; the top-level fold in daemon.ts only calls this when attemptUsage is real.
16
+ export function addUsage(a, b) {
17
+ if (!a)
18
+ return b;
19
+ const add = (x, y) => (x === undefined && y === undefined ? undefined : (x ?? 0) + (y ?? 0));
20
+ return {
21
+ input: a.input + b.input, output: a.output + b.output,
22
+ cacheRead: add(a.cacheRead, b.cacheRead), cacheWrite: add(a.cacheWrite, b.cacheWrite),
23
+ reasoning: add(a.reasoning, b.reasoning),
24
+ };
25
+ }
26
+ export function modelAuthed(health, model) {
27
+ return health?.modelAuth?.[model]?.authed !== false;
28
+ }
29
+ export function matchesTrustDialog(paneText, dialog) {
30
+ return paneText.includes(dialog.fingerprint);
31
+ }
32
+ export function channelsFromConfig(adapterId, cfg) {
33
+ const e = cfg.tiers[adapterId];
34
+ if (!e)
35
+ return [];
36
+ return Object.entries(e.models).map(([model, tier]) => ({
37
+ adapter: adapterId, vendor: e.vendor, model, channel: e.channel, tier,
38
+ }));
39
+ }
40
+ export function channelKey(c) {
41
+ return `${c.adapter}:${c.model}`;
42
+ }
43
+ export function shq(s) {
44
+ return `'${s.replaceAll("'", `'\\''`)}'`;
45
+ }
46
+ // Quota exhaustion is detected from CLI errors, never predicted (spec §4).
47
+ // ZAI coding-plan exhaustion text: "Insufficient balance or no resource package. Please recharge."
48
+ // Anchor the distinctive full phrase, not the two-word "insufficient balance" fragment — that fires
49
+ // on ordinary billing/wallet task output the harness edits (research Pitfall 3, 2026-07-10).
50
+ export const QUOTA_RE = /rate.?limit|quota|usage limit|out of credits|insufficient credit|insufficient balance or no resource|\b429\b/i;
51
+ // v1.5 MODEL-01: charset gate for detected model ids (research Pitfall 4, verified 2026-07-10).
52
+ // Ids come from CLI stdout / another program's JSON (models_cache.json) and are echoed into
53
+ // operator-facing lint text and persisted to doctor.json — defense-in-depth for MODEL-05 (config
54
+ // suggestions that could reach a shell). Covers observed ids incl. zai-coding-plan/glm-5.2,
55
+ // gpt-5.6-sol, composer-2.5, gpt-5.3-codex; non-conforming (ANSI/control/shell-metachar) ids dropped.
56
+ export const MODEL_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._/:\[\]=,-]*$/;
@@ -0,0 +1 @@
1
+ export declare function approve(argv: string[], cwd?: string): Promise<string>;
@@ -0,0 +1,72 @@
1
+ import { userInfo } from "node:os";
2
+ import { ATTEMPT_CAP_RELEASE, Journal } from "../../run/journal.js";
3
+ // GATE-08 (v1.12): approve a parked human gate so the next `drovr resume <runId>` dispatches it.
4
+ //
5
+ // The approval is a JOURNAL EVENT (task-approved) carrying who and when — it touches ONLY the
6
+ // append-only journal. Writing it into drovr's compiled graph artifact would be silently erased by
7
+ // the next recompile (which re-emits humanGate:true from the plan frontmatter) — Phase 42 D-02.
8
+ //
9
+ // v1.24 OBS-18: when the park was attempt-cap (not a humanGate pre-dispatch park), the event also
10
+ // carries `release: "attempt-cap"`. replayResumeState zeros the attempt budget on that marker so
11
+ // resume dispatches instead of re-parking in the same tick; tried-list is preserved. humanGate
12
+ // parks (attempts 0, reason starts with "humanGate:") stamp no release — byte-identical to GATE-08.
13
+ //
14
+ // Fail-closed (D-05): unknown runId, unknown taskId, a not-parked task, and a double-approve are all
15
+ // LOUD refusals that name the reason and append NO event — never a silent no-op. A handler throw
16
+ // becomes `drovr approve: <message>` at exit 1 (src/cli/index.ts dispatch).
17
+ //
18
+ // Who/when is truthful, not dressed-up auth (D-03): default actor os.userInfo().username; --by overrides
19
+ // for delegated approval; optional --reason; the event's ts (stamped by Journal.append) is the when.
20
+ export async function approve(argv, cwd = process.cwd()) {
21
+ const { runId, taskId, by, reason } = parseArgs(argv);
22
+ // Journal.open throws `no journal for <runId> at <dir>` on an unknown run — that IS the refusal.
23
+ const journal = Journal.open(cwd, runId);
24
+ const status = journal.replayStatuses().get(taskId);
25
+ if (status === undefined) {
26
+ throw new Error(`task ${taskId} has no events in run ${runId} — unknown task or never dispatched`);
27
+ }
28
+ if (status !== "human") {
29
+ // a silent no-op would be worse than a loud refusal — name the actual status (D-05)
30
+ throw new Error(`task ${taskId} is ${status}, not a parked human gate — refusing (a silent no-op would be worse)`);
31
+ }
32
+ // OBS-18: only the most recent task-human for this task decides whether this approval grants a
33
+ // fresh attempt budget. Match the daemon's park reason literally (`attempt cap (N) reached`).
34
+ const lastHuman = journal.read().filter((e) => e.event === "task-human" && e.taskId === taskId).at(-1);
35
+ const capPark = typeof lastHuman?.data.reason === "string"
36
+ && /attempt cap \(\d+\) reached/.test(lastHuman.data.reason);
37
+ journal.append("task-approved", taskId, {
38
+ by,
39
+ ...(reason ? { reason } : {}),
40
+ via: "cli",
41
+ ...(capPark ? { release: ATTEMPT_CAP_RELEASE } : {}),
42
+ });
43
+ return `approved ${taskId} in ${runId} — by ${by}; run \`drovr resume ${runId}\` to dispatch it`;
44
+ }
45
+ // hand-parsed argv — no CLI framework (house style). Flags --by <name> and --reason <text>; positionals
46
+ // are runId then taskId. Throws usage on missing positionals (mirrors resume.ts/unlock.ts).
47
+ function parseArgs(argv) {
48
+ const positionals = [];
49
+ let by;
50
+ let reason;
51
+ for (let i = 0; i < argv.length; i++) {
52
+ const a = argv[i];
53
+ if (a === "--by") {
54
+ by = argv[++i];
55
+ if (!by)
56
+ throw new Error("usage: drovr approve <run-id> <task-id> [--by <name>] [--reason <text>]");
57
+ }
58
+ else if (a === "--reason") {
59
+ reason = argv[++i];
60
+ if (!reason)
61
+ throw new Error("usage: drovr approve <run-id> <task-id> [--by <name>] [--reason <text>]");
62
+ }
63
+ else {
64
+ positionals.push(a);
65
+ }
66
+ }
67
+ const [runId, taskId] = positionals;
68
+ if (!runId || !taskId) {
69
+ throw new Error("usage: drovr approve <run-id> <task-id> [--by <name>] [--reason <text>]");
70
+ }
71
+ return { runId, taskId, by: by ?? userInfo().username, reason };
72
+ }
@@ -0,0 +1 @@
1
+ export declare function compile(argv: string[], cwd?: string): Promise<string>;
@@ -0,0 +1,23 @@
1
+ import { isAbsolute, join } from "node:path";
2
+ import { parseArgs } from "node:util";
3
+ import { compileSource } from "../../compile/index.js";
4
+ import { saveGraph } from "../../graph/graph.js";
5
+ import { isRunLockLive } from "../../run/lock.js";
6
+ export async function compile(argv, cwd = process.cwd()) {
7
+ const { values, positionals } = parseArgs({
8
+ args: argv,
9
+ options: { type: { type: "string" } },
10
+ allowPositionals: true,
11
+ });
12
+ const src = positionals[0];
13
+ if (!src)
14
+ throw new Error("usage: drovr compile <spec-dir-or-md> [--type speckit|prd|gsd|native]");
15
+ // resolve against the target repo, not the process cwd (the CLI test passes a tmp repo)
16
+ const g = compileSource(isAbsolute(src) ? src : join(cwd, src), values.type, cwd);
17
+ // HARD-01: a compile clobbering a running daemon's graph.json is the same last-write-wins race
18
+ // as a second daemon. Read-only check — compile never acquires/holds (it's instantaneous).
19
+ if (isRunLockLive(cwd))
20
+ throw new Error(".drovr/graph.lock is held by another drovr run, or is a stale/garbage lock — refusing to overwrite graph.json. If no run is active, run `drovr unlock`.");
21
+ saveGraph(cwd, g);
22
+ return `compiled ${src} → .drovr/graph.json (${g.tasks.length} tasks, source ${g.spec.source}, hash ${g.spec.hash.slice(0, 12)})`;
23
+ }
@@ -0,0 +1,2 @@
1
+ import type { WorkerAdapter } from "../../adapters/types.js";
2
+ export declare function doctor(_argv: string[], cwd?: string, adapters?: WorkerAdapter[]): Promise<string>;
@@ -0,0 +1,103 @@
1
+ import { allAdapters, probeAll, probeModels, servableExclusions, servabilityLine, writeDoctor } from "../../adapters/registry.js";
2
+ import { modelLints, suggestOverlay } from "../../adapters/model-lints.js";
3
+ import { loadConfig } from "../../config/config.js";
4
+ import { HerdrDriver } from "../../drivers/herdr.js";
5
+ import { disallowedBy, excludedChannels, exclusionLine, preferRanks } from "../../route/preference.js";
6
+ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters()) {
7
+ const cfg = loadConfig(cwd);
8
+ const health = await probeAll(adapters);
9
+ // MODEL-02: detect models where the adapter exposes a list surface, BEFORE writing doctor.json (write once, below).
10
+ // Fail OPEN — the inverse of gates' fail-closed: detection is advisory, so a broken list surface NEVER fails doctor.
11
+ for (const a of adapters) {
12
+ if (!a.listModels || !health[a.id].installed)
13
+ continue;
14
+ try {
15
+ health[a.id].models = await a.listModels();
16
+ // MODEL-05: prefer the source's own fetch time (codex's cache fetched_at) so the staleness lint
17
+ // measures real knowledge age, not run time. opencode reads its own offline cache too but exposes
18
+ // no fetch timestamp, so it still stamps now — its staleness lint is best-effort until it surfaces one.
19
+ if (health[a.id].models.length)
20
+ health[a.id].modelsDetectedAt = a.listModelsFetchedAt?.() ?? new Date().toISOString();
21
+ }
22
+ catch { /* fail open: leave models as-is, doctor stays healthy */ }
23
+ }
24
+ await probeModels(cfg, cwd, adapters, health);
25
+ writeDoctor(cwd, health);
26
+ const rows = adapters.map((a) => {
27
+ const h = health[a.id];
28
+ const state = !h.installed ? "not installed" : `${h.version ?? "installed"}${h.note ? ` (${h.note})` : ""}`;
29
+ return ` ${h.installed ? "✓" : "✗"} ${a.id.padEnd(14)} ${state}`;
30
+ });
31
+ rows.push(` ${HerdrDriver.available() ? "✓" : "✗"} herdr ${HerdrDriver.available() ? "driver available (HERDR_ENV=1)" : "not detected — subprocess driver will be used"}`);
32
+ // v1.22 T5: workspace-trust pre-flight — per installed adapter: trusted | seeded | action-required | n/a.
33
+ // action-required names the exact one-time command (or dialog) the operator must run once.
34
+ rows.push("workspace trust:");
35
+ for (const a of adapters) {
36
+ if (!health[a.id]?.installed)
37
+ continue;
38
+ if (!a.trust) {
39
+ rows.push(` = ${a.id.padEnd(14)} trust: n/a`);
40
+ continue;
41
+ }
42
+ try {
43
+ const v = a.trust(cwd);
44
+ if (v.status === "trusted")
45
+ rows.push(` ✓ ${a.id.padEnd(14)} trust: trusted`);
46
+ else if (v.status === "seeded")
47
+ rows.push(` ✓ ${a.id.padEnd(14)} trust: seeded`);
48
+ else
49
+ rows.push(` ! ${a.id.padEnd(14)} trust: action-required — run ONCE: ${v.command}`);
50
+ }
51
+ catch (e) {
52
+ // fail closed on the trust line only — never abort the rest of doctor
53
+ rows.push(` ! ${a.id.padEnd(14)} trust: action-required — run ONCE: (trust check failed: ${e instanceof Error ? e.message : String(e)})`);
54
+ }
55
+ }
56
+ for (const [role, sel] of [["judge", cfg.judge], ["consult", cfg.consult]]) {
57
+ if (!health[sel.adapter]?.installed) {
58
+ rows.push(` ! ${role} runs on ${sel.adapter}:${sel.model} — NOT installed; that gate will fail closed until you install it or remap cfg.${role}`);
59
+ }
60
+ }
61
+ rows.push(...modelLints(cfg, health, adapters).map((l) => ` ! ${l}`));
62
+ const excluded = excludedChannels(cfg, adapters, health);
63
+ if (excluded.length)
64
+ rows.push(` ! ${exclusionLine(excluded)}`);
65
+ // HYG-07(a): doctor just probed fresh (probeAll above), so servability attribution is current by construction.
66
+ const servable = servableExclusions(cfg, adapters, health);
67
+ if (servable.length)
68
+ rows.push(` ! ${servabilityLine(servable)}`);
69
+ // MODEL-05/06: print-only drift fragment; advisory, whole-line-commented additions, drovr NEVER applies it.
70
+ const frag = suggestOverlay(cfg, health, adapters);
71
+ const drift = frag ? `\nmodel drift — paste-ready overlay (advisory; drovr never applies):\n${frag}` : "";
72
+ // T4: model-status table — one row per CLASSIFIED model (tiers config) with tier, auth verdict
73
+ // (reason + date when unauthed), operator-deny flag, and prefer rank across the routing map.
74
+ // Unclassified listed models (detected via listModels but never tiered) compress to one count line
75
+ // per adapter — they aren't routable, so they don't earn table rows.
76
+ const trunc = (s, n) => (s.length <= n ? s : `${s.slice(0, n - 1)}…`);
77
+ const dateOf = (iso) => iso.slice(0, 10);
78
+ const modelStatus = adapters.flatMap((a) => {
79
+ const h = health[a.id];
80
+ if (!h?.installed)
81
+ return [];
82
+ const classified = cfg.tiers[a.id]?.models ?? {};
83
+ const models = Object.keys(classified);
84
+ const unclassified = (h.models ?? []).filter((m) => !(m in classified));
85
+ if (!models.length && !unclassified.length)
86
+ return [];
87
+ const w = Math.max(8, ...models.map((m) => m.length));
88
+ const rows = [` ${a.id}`];
89
+ for (const m of models) {
90
+ const v = h.modelAuth?.[m];
91
+ const auth = !v ? "unknown" : v.authed ? "authed" : `unauthed: ${trunc(v.reason ?? "probe failed", 40)} (${dateOf(v.probedAt)})`;
92
+ const d = disallowedBy({ adapter: a.id, model: m }, cfg.routing);
93
+ const denied = d?.by === "deny" ? d.entry : "—";
94
+ const pref = preferRanks({ adapter: a.id, model: m }, cfg).map((p) => `${p.shape}#${p.rank}`).join(",") || "—";
95
+ rows.push(` ${m.padEnd(w)} ${classified[m].padEnd(8)} ${auth} denied=${denied} prefer=${pref}`);
96
+ }
97
+ if (unclassified.length)
98
+ rows.push(` (${unclassified.length} more listed, unclassified)`);
99
+ return rows;
100
+ });
101
+ const modelSummary = modelStatus.length ? `\nmodel status:\n${modelStatus.join("\n")}` : "";
102
+ return `drovr doctor — capability matrix:\n${rows.join("\n")}${modelSummary}${drift}\nwrote .drovr/doctor.json`;
103
+ }
@@ -0,0 +1 @@
1
+ export declare function init(argv: string[], cwd?: string): Promise<string>;
@@ -0,0 +1,32 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { parseArgs } from "node:util";
4
+ import { configTemplate, globalConfigDir } from "../../config/config.js";
5
+ import { specTemplate } from "../../compile/native.js";
6
+ import { drovrDir } from "../../graph/graph.js";
7
+ import { doctor } from "./doctor.js";
8
+ export async function init(argv, cwd = process.cwd()) {
9
+ const { values } = parseArgs({ args: argv, options: { "global-dir": { type: "string" } } });
10
+ const gdir = values["global-dir"] ?? globalConfigDir();
11
+ mkdirSync(gdir, { recursive: true });
12
+ const notes = [];
13
+ for (const p of [join(gdir, "config.yaml"), join(drovrDir(cwd), "config.yaml")]) {
14
+ if (!existsSync(p)) {
15
+ writeFileSync(p, configTemplate());
16
+ notes.push(`wrote ${p}`);
17
+ }
18
+ else {
19
+ notes.push(`kept existing ${p}`);
20
+ }
21
+ }
22
+ const specPath = join(cwd, "drovr.spec.md");
23
+ if (!existsSync(specPath)) {
24
+ writeFileSync(specPath, specTemplate());
25
+ notes.push(`wrote ${specPath}`);
26
+ }
27
+ else {
28
+ notes.push(`kept existing ${specPath}`);
29
+ }
30
+ const doc = await doctor([], cwd);
31
+ return `${notes.join("\n")}\n${doc}\nnext: edit drovr.spec.md, then drovr compile drovr.spec.md && drovr plan && drovr run`;
32
+ }
@@ -0,0 +1,2 @@
1
+ import type { WorkerAdapter } from "../../adapters/types.js";
2
+ export declare function plan(_argv: string[], cwd?: string, adapters?: WorkerAdapter[]): Promise<string>;