@twelvehart/orcats 0.0.0-seed.0 → 0.2.3

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 (275) hide show
  1. package/LICENSE +48 -0
  2. package/NOTICE +5 -0
  3. package/README.md +399 -0
  4. package/bin/orcats +4 -0
  5. package/dist/backends/acp-client.d.ts +74 -0
  6. package/dist/backends/acp-client.d.ts.map +1 -0
  7. package/dist/backends/acp-run.d.ts +31 -0
  8. package/dist/backends/acp-run.d.ts.map +1 -0
  9. package/dist/backends/claude-run.d.ts +35 -0
  10. package/dist/backends/claude-run.d.ts.map +1 -0
  11. package/dist/backends/claude-stream-json.d.ts +15 -0
  12. package/dist/backends/claude-stream-json.d.ts.map +1 -0
  13. package/dist/backends/codex-jsonl.d.ts +34 -0
  14. package/dist/backends/codex-jsonl.d.ts.map +1 -0
  15. package/dist/backends/codex-run.d.ts +40 -0
  16. package/dist/backends/codex-run.d.ts.map +1 -0
  17. package/dist/backends/codex.d.ts +5 -0
  18. package/dist/backends/codex.d.ts.map +1 -0
  19. package/dist/backends/conversation-config.d.ts +12 -0
  20. package/dist/backends/conversation-config.d.ts.map +1 -0
  21. package/dist/backends/index.d.ts +14 -0
  22. package/dist/backends/index.d.ts.map +1 -0
  23. package/dist/backends/opencode-run.d.ts +48 -0
  24. package/dist/backends/opencode-run.d.ts.map +1 -0
  25. package/dist/backends/opencode-sse.d.ts +35 -0
  26. package/dist/backends/opencode-sse.d.ts.map +1 -0
  27. package/dist/backends/pi-rpc.d.ts +21 -0
  28. package/dist/backends/pi-rpc.d.ts.map +1 -0
  29. package/dist/backends/pi-run.d.ts +21 -0
  30. package/dist/backends/pi-run.d.ts.map +1 -0
  31. package/dist/backends/select.d.ts +17 -0
  32. package/dist/backends/select.d.ts.map +1 -0
  33. package/dist/backends/subprocess-run.d.ts +68 -0
  34. package/dist/backends/subprocess-run.d.ts.map +1 -0
  35. package/dist/backends/types.d.ts +17 -0
  36. package/dist/backends/types.d.ts.map +1 -0
  37. package/dist/backends/unsupported.d.ts +9 -0
  38. package/dist/backends/unsupported.d.ts.map +1 -0
  39. package/dist/baseline/index.d.ts +56 -0
  40. package/dist/baseline/index.d.ts.map +1 -0
  41. package/dist/cli/args.d.ts +22 -0
  42. package/dist/cli/args.d.ts.map +1 -0
  43. package/dist/cli/embedded.d.ts +7 -0
  44. package/dist/cli/embedded.d.ts.map +1 -0
  45. package/dist/cli/main.d.ts +6 -0
  46. package/dist/cli/main.d.ts.map +1 -0
  47. package/dist/cli/version.d.ts +2 -0
  48. package/dist/cli/version.d.ts.map +1 -0
  49. package/dist/conversation/ask-user.d.ts +17 -0
  50. package/dist/conversation/ask-user.d.ts.map +1 -0
  51. package/dist/conversation/conversation.d.ts +46 -0
  52. package/dist/conversation/conversation.d.ts.map +1 -0
  53. package/dist/conversation/harness.d.ts +14 -0
  54. package/dist/conversation/harness.d.ts.map +1 -0
  55. package/dist/conversation/index.d.ts +5 -0
  56. package/dist/conversation/index.d.ts.map +1 -0
  57. package/dist/conversation/queue.d.ts +15 -0
  58. package/dist/conversation/queue.d.ts.map +1 -0
  59. package/dist/flow/accessors.d.ts +13 -0
  60. package/dist/flow/accessors.d.ts.map +1 -0
  61. package/dist/flow/approvals.d.ts +6 -0
  62. package/dist/flow/approvals.d.ts.map +1 -0
  63. package/dist/flow/args.d.ts +4 -0
  64. package/dist/flow/args.d.ts.map +1 -0
  65. package/dist/flow/context.d.ts +36 -0
  66. package/dist/flow/context.d.ts.map +1 -0
  67. package/dist/flow/index.d.ts +5 -0
  68. package/dist/flow/index.d.ts.map +1 -0
  69. package/dist/index.d.ts +16 -0
  70. package/dist/index.d.ts.map +1 -0
  71. package/dist/loop/builder/index.d.ts +17 -0
  72. package/dist/loop/builder/index.d.ts.map +1 -0
  73. package/dist/loop/builder/types.d.ts +97 -0
  74. package/dist/loop/builder/types.d.ts.map +1 -0
  75. package/dist/loop/context/compaction.d.ts +38 -0
  76. package/dist/loop/context/compaction.d.ts.map +1 -0
  77. package/dist/loop/context/index.d.ts +4 -0
  78. package/dist/loop/context/index.d.ts.map +1 -0
  79. package/dist/loop/context/offload.d.ts +34 -0
  80. package/dist/loop/context/offload.d.ts.map +1 -0
  81. package/dist/loop/context/types.d.ts +13 -0
  82. package/dist/loop/context/types.d.ts.map +1 -0
  83. package/dist/loop/engine/bridge.d.ts +21 -0
  84. package/dist/loop/engine/bridge.d.ts.map +1 -0
  85. package/dist/loop/engine/index.d.ts +73 -0
  86. package/dist/loop/engine/index.d.ts.map +1 -0
  87. package/dist/loop/execution.d.ts +68 -0
  88. package/dist/loop/execution.d.ts.map +1 -0
  89. package/dist/loop/fanout.d.ts +148 -0
  90. package/dist/loop/fanout.d.ts.map +1 -0
  91. package/dist/loop/firing.d.ts +54 -0
  92. package/dist/loop/firing.d.ts.map +1 -0
  93. package/dist/loop/graph/analyze.d.ts +8 -0
  94. package/dist/loop/graph/analyze.d.ts.map +1 -0
  95. package/dist/loop/graph/index.d.ts +3 -0
  96. package/dist/loop/graph/index.d.ts.map +1 -0
  97. package/dist/loop/graph/types.d.ts +35 -0
  98. package/dist/loop/graph/types.d.ts.map +1 -0
  99. package/dist/loop/index.d.ts +7 -0
  100. package/dist/loop/index.d.ts.map +1 -0
  101. package/dist/loop/io/index.d.ts +4 -0
  102. package/dist/loop/io/index.d.ts.map +1 -0
  103. package/dist/loop/io/linear.d.ts +142 -0
  104. package/dist/loop/io/linear.d.ts.map +1 -0
  105. package/dist/loop/io/sink.d.ts +47 -0
  106. package/dist/loop/io/sink.d.ts.map +1 -0
  107. package/dist/loop/io/source.d.ts +64 -0
  108. package/dist/loop/io/source.d.ts.map +1 -0
  109. package/dist/loop/serve.d.ts +97 -0
  110. package/dist/loop/serve.d.ts.map +1 -0
  111. package/dist/loop/state/index.d.ts +5 -0
  112. package/dist/loop/state/index.d.ts.map +1 -0
  113. package/dist/loop/state/manifest.d.ts +36 -0
  114. package/dist/loop/state/manifest.d.ts.map +1 -0
  115. package/dist/loop/state/port.d.ts +33 -0
  116. package/dist/loop/state/port.d.ts.map +1 -0
  117. package/dist/loop/state/snapshot.d.ts +15 -0
  118. package/dist/loop/state/snapshot.d.ts.map +1 -0
  119. package/dist/loop/state/sqlite.d.ts +31 -0
  120. package/dist/loop/state/sqlite.d.ts.map +1 -0
  121. package/dist/loop/termination-contract.d.ts +28 -0
  122. package/dist/loop/termination-contract.d.ts.map +1 -0
  123. package/dist/loop/termination.d.ts +33 -0
  124. package/dist/loop/termination.d.ts.map +1 -0
  125. package/dist/model/backend-config.d.ts +30 -0
  126. package/dist/model/backend-config.d.ts.map +1 -0
  127. package/dist/model/brand.d.ts +9 -0
  128. package/dist/model/brand.d.ts.map +1 -0
  129. package/dist/model/errors.d.ts +11 -0
  130. package/dist/model/errors.d.ts.map +1 -0
  131. package/dist/model/index.d.ts +8 -0
  132. package/dist/model/index.d.ts.map +1 -0
  133. package/dist/model/json-schema.d.ts +4 -0
  134. package/dist/model/json-schema.d.ts.map +1 -0
  135. package/dist/model/result.d.ts +3 -0
  136. package/dist/model/result.d.ts.map +1 -0
  137. package/dist/model/schemas.d.ts +435 -0
  138. package/dist/model/schemas.d.ts.map +1 -0
  139. package/dist/model/structured.d.ts +9 -0
  140. package/dist/model/structured.d.ts.map +1 -0
  141. package/dist/monitor/index.d.ts +139 -0
  142. package/dist/monitor/index.d.ts.map +1 -0
  143. package/dist/plan/index.d.ts +3 -0
  144. package/dist/plan/index.d.ts.map +1 -0
  145. package/dist/plan/persistent.d.ts +23 -0
  146. package/dist/plan/persistent.d.ts.map +1 -0
  147. package/dist/plan/verdict.d.ts +9 -0
  148. package/dist/plan/verdict.d.ts.map +1 -0
  149. package/dist/review/index.d.ts +4 -0
  150. package/dist/review/index.d.ts.map +1 -0
  151. package/dist/review/loop.d.ts +117 -0
  152. package/dist/review/loop.d.ts.map +1 -0
  153. package/dist/review/reviewers.d.ts +13 -0
  154. package/dist/review/reviewers.d.ts.map +1 -0
  155. package/dist/review/tool.d.ts +20 -0
  156. package/dist/review/tool.d.ts.map +1 -0
  157. package/dist/run-output/index.d.ts +112 -0
  158. package/dist/run-output/index.d.ts.map +1 -0
  159. package/dist/runner/index.d.ts +2 -0
  160. package/dist/runner/index.d.ts.map +1 -0
  161. package/dist/runner/typecheck.d.ts +23 -0
  162. package/dist/runner/typecheck.d.ts.map +1 -0
  163. package/dist/test-utils/fakes.d.ts +76 -0
  164. package/dist/test-utils/fakes.d.ts.map +1 -0
  165. package/dist/test-utils/fixtures.d.ts +4 -0
  166. package/dist/test-utils/fixtures.d.ts.map +1 -0
  167. package/dist/test-utils/index.d.ts +4 -0
  168. package/dist/test-utils/index.d.ts.map +1 -0
  169. package/dist/test-utils/tier2.d.ts +8 -0
  170. package/dist/test-utils/tier2.d.ts.map +1 -0
  171. package/dist/tools/fs.d.ts +11 -0
  172. package/dist/tools/fs.d.ts.map +1 -0
  173. package/dist/tools/git.d.ts +10 -0
  174. package/dist/tools/git.d.ts.map +1 -0
  175. package/dist/tools/github.d.ts +13 -0
  176. package/dist/tools/github.d.ts.map +1 -0
  177. package/dist/tools/index.d.ts +7 -0
  178. package/dist/tools/index.d.ts.map +1 -0
  179. package/dist/tools/linear.d.ts +117 -0
  180. package/dist/tools/linear.d.ts.map +1 -0
  181. package/dist/tools/process.d.ts +40 -0
  182. package/dist/tools/process.d.ts.map +1 -0
  183. package/dist/tools/terminal.d.ts +23 -0
  184. package/dist/tools/terminal.d.ts.map +1 -0
  185. package/package.json +89 -9
  186. package/src/backends/acp-client.ts +324 -0
  187. package/src/backends/acp-run.ts +636 -0
  188. package/src/backends/claude-run.ts +226 -0
  189. package/src/backends/claude-stream-json.ts +273 -0
  190. package/src/backends/codex-jsonl.ts +339 -0
  191. package/src/backends/codex-run.ts +177 -0
  192. package/src/backends/codex.ts +84 -0
  193. package/src/backends/conversation-config.ts +26 -0
  194. package/src/backends/index.ts +13 -0
  195. package/src/backends/opencode-run.ts +432 -0
  196. package/src/backends/opencode-sse.ts +360 -0
  197. package/src/backends/pi-rpc.ts +242 -0
  198. package/src/backends/pi-run.ts +154 -0
  199. package/src/backends/select.ts +77 -0
  200. package/src/backends/subprocess-run.ts +305 -0
  201. package/src/backends/types.ts +29 -0
  202. package/src/backends/unsupported.ts +48 -0
  203. package/src/baseline/index.ts +437 -0
  204. package/src/cli/args.ts +109 -0
  205. package/src/cli/embedded.ts +206 -0
  206. package/src/cli/main.ts +332 -0
  207. package/src/cli/version.ts +1 -0
  208. package/src/conversation/ask-user.ts +105 -0
  209. package/src/conversation/conversation.ts +109 -0
  210. package/src/conversation/harness.ts +39 -0
  211. package/src/conversation/index.ts +4 -0
  212. package/src/conversation/queue.ts +86 -0
  213. package/src/flow/accessors.ts +43 -0
  214. package/src/flow/approvals.ts +10 -0
  215. package/src/flow/args.ts +26 -0
  216. package/src/flow/context.ts +106 -0
  217. package/src/flow/index.ts +4 -0
  218. package/src/index.ts +17 -0
  219. package/src/loop/builder/index.ts +346 -0
  220. package/src/loop/builder/types.ts +131 -0
  221. package/src/loop/context/compaction.ts +175 -0
  222. package/src/loop/context/index.ts +3 -0
  223. package/src/loop/context/offload.ts +79 -0
  224. package/src/loop/context/types.ts +17 -0
  225. package/src/loop/engine/bridge.ts +61 -0
  226. package/src/loop/engine/index.ts +166 -0
  227. package/src/loop/execution.ts +453 -0
  228. package/src/loop/fanout.ts +444 -0
  229. package/src/loop/firing.ts +188 -0
  230. package/src/loop/graph/analyze.ts +128 -0
  231. package/src/loop/graph/index.ts +2 -0
  232. package/src/loop/graph/types.ts +45 -0
  233. package/src/loop/index.ts +11 -0
  234. package/src/loop/io/index.ts +3 -0
  235. package/src/loop/io/linear.ts +714 -0
  236. package/src/loop/io/sink.ts +170 -0
  237. package/src/loop/io/source.ts +255 -0
  238. package/src/loop/serve.ts +307 -0
  239. package/src/loop/state/index.ts +4 -0
  240. package/src/loop/state/manifest.ts +56 -0
  241. package/src/loop/state/port.ts +43 -0
  242. package/src/loop/state/snapshot.ts +129 -0
  243. package/src/loop/state/sqlite.ts +265 -0
  244. package/src/loop/termination-contract.ts +89 -0
  245. package/src/loop/termination.ts +156 -0
  246. package/src/model/backend-config.ts +34 -0
  247. package/src/model/brand.ts +11 -0
  248. package/src/model/errors.ts +28 -0
  249. package/src/model/index.ts +7 -0
  250. package/src/model/json-schema.ts +14 -0
  251. package/src/model/result.ts +28 -0
  252. package/src/model/schemas.ts +218 -0
  253. package/src/model/structured.ts +26 -0
  254. package/src/monitor/index.ts +367 -0
  255. package/src/plan/index.ts +2 -0
  256. package/src/plan/persistent.ts +98 -0
  257. package/src/plan/verdict.ts +3 -0
  258. package/src/review/index.ts +3 -0
  259. package/src/review/loop.ts +365 -0
  260. package/src/review/reviewers.ts +92 -0
  261. package/src/review/tool.ts +41 -0
  262. package/src/run-output/index.ts +319 -0
  263. package/src/runner/index.ts +1 -0
  264. package/src/runner/typecheck.ts +72 -0
  265. package/src/test-utils/fakes.ts +260 -0
  266. package/src/test-utils/fixtures.ts +33 -0
  267. package/src/test-utils/index.ts +3 -0
  268. package/src/test-utils/tier2.ts +12 -0
  269. package/src/tools/fs.ts +44 -0
  270. package/src/tools/git.ts +34 -0
  271. package/src/tools/github.ts +25 -0
  272. package/src/tools/index.ts +6 -0
  273. package/src/tools/linear.ts +428 -0
  274. package/src/tools/process.ts +156 -0
  275. package/src/tools/terminal.ts +116 -0
@@ -0,0 +1,437 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { ok } from "neverthrow";
4
+ import { command as flowCommand } from "../flow/accessors.ts";
5
+ import type { Usage } from "../model/index.ts";
6
+ import type { CommandLog, RegressedReason, WorkflowMonitor } from "../monitor/index.ts";
7
+ import { fixLoop, type FixLoopStop } from "../review/index.ts";
8
+ import type { CommandTool, VerificationCommand, VerificationCommandResult } from "../tools/index.ts";
9
+
10
+ export const BaselinePolicies = ["repair", "strict", "accept-dirty"] as const;
11
+ export type BaselinePolicy = (typeof BaselinePolicies)[number];
12
+
13
+ export interface BaselinePolicyResolution {
14
+ readonly policy: BaselinePolicy;
15
+ readonly args: readonly string[];
16
+ readonly source: "args" | "env" | "default";
17
+ }
18
+
19
+ export interface BaselinePolicyParseOptions {
20
+ readonly args?: readonly string[];
21
+ readonly env?: Record<string, string | undefined>;
22
+ readonly defaultPolicy?: BaselinePolicy;
23
+ }
24
+
25
+ export interface BaselineGateIssue {
26
+ readonly message: string;
27
+ readonly fixable: true;
28
+ }
29
+
30
+ export interface BaselineRepairResult {
31
+ readonly usage?: Usage | undefined;
32
+ }
33
+
34
+ type BaselineRepairOutput<T = void> = BaselineRepairResult | T;
35
+
36
+ export interface BaselineGateResult {
37
+ readonly policy: BaselinePolicy;
38
+ readonly status: "clean" | "repaired";
39
+ readonly validation: readonly CommandLog[];
40
+ readonly iterations: number;
41
+ readonly snapshotPath?: string;
42
+ readonly usage?: Usage;
43
+ }
44
+
45
+ export interface RunBaselineGateOptions {
46
+ readonly commands: readonly VerificationCommand[];
47
+ readonly policy?: BaselinePolicy;
48
+ readonly commandTool?: CommandTool;
49
+ readonly repair?: (issues: readonly BaselineGateIssue[]) => Promise<BaselineRepairOutput>;
50
+ readonly monitor?: Pick<WorkflowMonitor, "stage" | "recordOutcome" | "recordFailure">;
51
+ readonly snapshotDir?: string;
52
+ readonly maxIterations?: number;
53
+ readonly wallClockMs?: number;
54
+ readonly tokenBudget?: number;
55
+ readonly stalled?: (issues: readonly BaselineGateIssue[]) => boolean;
56
+ readonly now?: () => number;
57
+ }
58
+
59
+ export interface DirtyBaselineSnapshotOptions {
60
+ readonly commands: readonly VerificationCommand[];
61
+ readonly commandTool?: CommandTool;
62
+ readonly snapshotDir?: string;
63
+ readonly now?: () => number;
64
+ }
65
+
66
+ interface GateRun {
67
+ readonly passed: boolean;
68
+ readonly logs: readonly CommandLog[];
69
+ }
70
+
71
+ interface DirtyBaselineSnapshot {
72
+ readonly status: string;
73
+ readonly stagedDiff: string;
74
+ readonly unstagedDiff: string;
75
+ readonly untrackedFiles: string;
76
+ }
77
+
78
+ const DefaultBaselinePolicy: BaselinePolicy = "repair";
79
+ const DefaultSnapshotDir = ".orca/baselines";
80
+
81
+ export function resolveBaselinePolicy(options: BaselinePolicyParseOptions = {}): BaselinePolicyResolution {
82
+ const args = options.args ?? [];
83
+ const env = options.env ?? process.env;
84
+ const remaining: string[] = [];
85
+ let argPolicy: string | undefined;
86
+
87
+ for (let index = 0; index < args.length; index += 1) {
88
+ const arg = args[index];
89
+ if (arg === "--baseline") {
90
+ const value = args[index + 1];
91
+ if (value === undefined) {
92
+ throw new Error("Missing value for --baseline");
93
+ }
94
+ argPolicy = value;
95
+ index += 1;
96
+ continue;
97
+ }
98
+
99
+ if (arg?.startsWith("--baseline=")) {
100
+ argPolicy = arg.slice("--baseline=".length);
101
+ continue;
102
+ }
103
+
104
+ if (arg !== undefined) {
105
+ remaining.push(arg);
106
+ }
107
+ }
108
+
109
+ if (argPolicy !== undefined) {
110
+ return { policy: parseBaselinePolicyValue(argPolicy), args: remaining, source: "args" };
111
+ }
112
+
113
+ const envPolicy = env.ORCA_BASELINE_POLICY;
114
+ if (envPolicy !== undefined && envPolicy.trim() !== "") {
115
+ return { policy: parseBaselinePolicyValue(envPolicy), args: remaining, source: "env" };
116
+ }
117
+
118
+ return { policy: options.defaultPolicy ?? DefaultBaselinePolicy, args: remaining, source: "default" };
119
+ }
120
+
121
+ export function parseBaselinePolicy(value: string | undefined): BaselinePolicy {
122
+ return parseBaselinePolicyValue(value ?? DefaultBaselinePolicy);
123
+ }
124
+
125
+ export async function runBaselineGate(options: RunBaselineGateOptions): Promise<BaselineGateResult> {
126
+ const run = async () => runBaselineGateInner(options);
127
+ return options.monitor === undefined ? run() : options.monitor.stage("baseline gate", run);
128
+ }
129
+
130
+ export async function captureDirtyBaselineSnapshot(
131
+ options: DirtyBaselineSnapshotOptions,
132
+ ): Promise<string | undefined> {
133
+ const commandTool = options.commandTool ?? flowCommand();
134
+ const status = await readWorktreeStatus(commandTool);
135
+ if (status.trim() === "") {
136
+ return undefined;
137
+ }
138
+
139
+ const snapshot = await captureDirtyBaseline(commandTool, status);
140
+ const initialGate = await runGate(commandTool, options.commands);
141
+ return writeBaselineSnapshot(snapshot, initialGate.logs, options);
142
+ }
143
+
144
+ async function runBaselineGateInner(options: RunBaselineGateOptions): Promise<BaselineGateResult> {
145
+ const commandTool = options.commandTool ?? flowCommand();
146
+ const policy = options.policy ?? DefaultBaselinePolicy;
147
+ const startedAt = currentTime(options);
148
+ const status = await readWorktreeStatus(commandTool);
149
+
150
+ if ((policy === "repair" || policy === "strict") && status.trim() !== "") {
151
+ const error = new Error(
152
+ `baseline policy "${policy}" requires a clean worktree; use --baseline=accept-dirty to opt into snapshot-backed dirty baseline repair.\n${status}`,
153
+ );
154
+ options.monitor?.recordFailure({
155
+ file: "baseline",
156
+ error: error.message,
157
+ durationMs: currentTime(options) - startedAt,
158
+ category: "baseline",
159
+ });
160
+ throw error;
161
+ }
162
+
163
+ const dirtySnapshot =
164
+ policy === "accept-dirty" && status.trim() !== ""
165
+ ? await captureDirtyBaseline(commandTool, status)
166
+ : undefined;
167
+ let latest = await runGate(commandTool, options.commands);
168
+ const snapshotPath =
169
+ dirtySnapshot === undefined ? undefined : await writeBaselineSnapshot(dirtySnapshot, latest.logs, options);
170
+
171
+ if (latest.passed) {
172
+ recordBaselineOutcome(options, {
173
+ startedAt,
174
+ verdict: "clean",
175
+ validation: latest.logs,
176
+ iterations: 0,
177
+ snapshotPath,
178
+ });
179
+ return buildResult(policy, "clean", latest.logs, 0, snapshotPath);
180
+ }
181
+
182
+ if (policy === "strict") {
183
+ const reason = "strict baseline policy";
184
+ recordBaselineOutcome(options, {
185
+ startedAt,
186
+ verdict: "regressed",
187
+ validation: latest.logs,
188
+ iterations: 0,
189
+ reason,
190
+ snapshotPath,
191
+ });
192
+ throw new Error(`baseline gate failed under strict policy:\n${renderValidationFailure(latest.logs)}`);
193
+ }
194
+
195
+ if (options.repair === undefined) {
196
+ const reason = "missing baseline repair callback";
197
+ recordBaselineOutcome(options, {
198
+ startedAt,
199
+ verdict: "regressed",
200
+ validation: latest.logs,
201
+ iterations: 0,
202
+ reason,
203
+ snapshotPath,
204
+ });
205
+ throw new Error(`baseline gate failed and no repair callback was provided:\n${renderValidationFailure(latest.logs)}`);
206
+ }
207
+
208
+ let usage: Usage | undefined;
209
+ const loop = await fixLoop<BaselineGateIssue>(
210
+ () => Promise.resolve(ok(latest.passed ? [] : [{ message: renderValidationFailure(latest.logs), fixable: true as const }])),
211
+ async (issues) => {
212
+ const repaired = await options.repair?.(issues);
213
+ usage = addUsage(usage, repaired?.usage);
214
+ latest = await runGate(commandTool, options.commands);
215
+ return ok(repaired?.usage === undefined ? {} : { usage: repaired.usage });
216
+ },
217
+ {
218
+ maxIterations: options.maxIterations ?? 3,
219
+ wallClockMs: options.wallClockMs ?? 10 * 60_000,
220
+ ...(options.tokenBudget === undefined ? {} : { tokenBudget: options.tokenBudget }),
221
+ ...(options.stalled === undefined ? {} : { stalled: options.stalled }),
222
+ },
223
+ );
224
+
225
+ if (loop.isErr() || !loop.value.converged) {
226
+ const reason = loop.isErr() ? JSON.stringify(loop.error) : loop.value.stop;
227
+ recordBaselineOutcome(options, {
228
+ startedAt,
229
+ verdict: "regressed",
230
+ validation: latest.logs,
231
+ iterations: loop.isOk() ? loop.value.iterations : 0,
232
+ reason,
233
+ regressedReason: loop.isOk() ? regressedReasonFor(loop.value.stop) : "stuck",
234
+ usage,
235
+ snapshotPath,
236
+ });
237
+ throw new Error(`baseline repair did not converge (${reason}):\n${renderValidationFailure(latest.logs)}`);
238
+ }
239
+
240
+ recordBaselineOutcome(options, {
241
+ startedAt,
242
+ verdict: "repaired",
243
+ validation: latest.logs,
244
+ iterations: loop.value.iterations,
245
+ usage,
246
+ snapshotPath,
247
+ });
248
+ return buildResult(policy, "repaired", latest.logs, loop.value.iterations, snapshotPath, usage);
249
+ }
250
+
251
+ function parseBaselinePolicyValue(value: string): BaselinePolicy {
252
+ const normalized = value.trim();
253
+ if (BaselinePolicies.includes(normalized as BaselinePolicy)) {
254
+ return normalized as BaselinePolicy;
255
+ }
256
+ throw new Error(`Invalid baseline policy "${value}"; expected repair, strict, or accept-dirty`);
257
+ }
258
+
259
+ async function readWorktreeStatus(commandTool: CommandTool): Promise<string> {
260
+ const result = await commandTool.run({ command: "git", args: ["status", "--porcelain=v1"] });
261
+ if (result.type !== "success") {
262
+ throw new Error(`git status failed:\n${result.stderr || result.stdout}`);
263
+ }
264
+ return result.stdout;
265
+ }
266
+
267
+ async function runGate(
268
+ commandTool: CommandTool,
269
+ commands: readonly VerificationCommand[],
270
+ ): Promise<GateRun> {
271
+ const logs: CommandLog[] = [];
272
+ for (const command of commands) {
273
+ const result = await commandTool.run(command);
274
+ logs.push(toCommandLog(result));
275
+ if (result.type !== "success") {
276
+ return { passed: false, logs };
277
+ }
278
+ }
279
+ return { passed: true, logs };
280
+ }
281
+
282
+ function toCommandLog(result: VerificationCommandResult): CommandLog {
283
+ return {
284
+ command: result.command,
285
+ status: result.type === "success" ? "passed" : "failed",
286
+ stdout: result.stdout,
287
+ stderr: result.stderr,
288
+ exitCode: result.exitCode,
289
+ durationMs: result.durationMs,
290
+ };
291
+ }
292
+
293
+ async function captureDirtyBaseline(
294
+ commandTool: CommandTool,
295
+ status: string,
296
+ ): Promise<DirtyBaselineSnapshot> {
297
+ const [stagedDiff, unstagedDiff, untrackedFiles] = await Promise.all([
298
+ capture(commandTool, { command: "git", args: ["diff", "--staged"] }),
299
+ capture(commandTool, { command: "git", args: ["diff"] }),
300
+ capture(commandTool, { command: "git", args: ["ls-files", "--others", "--exclude-standard"] }),
301
+ ]);
302
+ return { status, stagedDiff, unstagedDiff, untrackedFiles };
303
+ }
304
+
305
+ async function writeBaselineSnapshot(
306
+ snapshot: DirtyBaselineSnapshot,
307
+ validation: readonly CommandLog[],
308
+ options: Pick<DirtyBaselineSnapshotOptions, "snapshotDir" | "now">,
309
+ ): Promise<string> {
310
+ const snapshotDir = options.snapshotDir ?? DefaultSnapshotDir;
311
+ const snapshotPath = join(snapshotDir, `baseline-${new Date(currentTime(options)).toISOString().replace(/[:.]/g, "-")}.md`);
312
+ const body = [
313
+ "# Orca Baseline Snapshot",
314
+ "",
315
+ `Policy: accept-dirty`,
316
+ `Captured at: ${new Date(currentTime(options)).toISOString()}`,
317
+ "",
318
+ "## Git Status",
319
+ "",
320
+ fence(snapshot.status),
321
+ "",
322
+ "## Staged Diff",
323
+ "",
324
+ fence(snapshot.stagedDiff),
325
+ "",
326
+ "## Unstaged Diff",
327
+ "",
328
+ fence(snapshot.unstagedDiff),
329
+ "",
330
+ "## Untracked Files",
331
+ "",
332
+ fence(snapshot.untrackedFiles),
333
+ "",
334
+ "## Initial Gate Output",
335
+ "",
336
+ ...validation.flatMap((log) => [
337
+ `### ${log.command}`,
338
+ "",
339
+ `Status: ${log.status}`,
340
+ `Exit code: ${String(log.exitCode)}`,
341
+ "",
342
+ "stdout:",
343
+ fence(log.stdout),
344
+ "",
345
+ "stderr:",
346
+ fence(log.stderr),
347
+ "",
348
+ ]),
349
+ ].join("\n");
350
+
351
+ await mkdir(dirname(snapshotPath), { recursive: true });
352
+ await writeFile(snapshotPath, body);
353
+ return snapshotPath;
354
+ }
355
+
356
+ async function capture(commandTool: CommandTool, command: VerificationCommand): Promise<string> {
357
+ const result = await commandTool.run(command);
358
+ return result.type === "success" ? result.stdout : result.stderr || result.stdout;
359
+ }
360
+
361
+ function recordBaselineOutcome(
362
+ options: RunBaselineGateOptions,
363
+ log: {
364
+ readonly startedAt: number;
365
+ readonly verdict: "clean" | "repaired" | "regressed";
366
+ readonly validation: readonly CommandLog[];
367
+ readonly iterations: number;
368
+ readonly reason?: string | undefined;
369
+ readonly regressedReason?: RegressedReason | undefined;
370
+ readonly usage?: Usage | undefined;
371
+ readonly snapshotPath?: string | undefined;
372
+ },
373
+ ): void {
374
+ options.monitor?.recordOutcome({
375
+ file: "baseline",
376
+ verdict: log.verdict,
377
+ durationMs: currentTime(options) - log.startedAt,
378
+ smellsRemoved: [],
379
+ validation: log.validation,
380
+ iterations: log.iterations,
381
+ ...(log.reason === undefined ? {} : { reason: log.reason }),
382
+ ...(log.regressedReason === undefined ? {} : { regressedReason: log.regressedReason }),
383
+ ...(log.usage === undefined ? {} : { usage: log.usage, tokens: log.usage.input + log.usage.output }),
384
+ ...(log.snapshotPath === undefined ? {} : { snapshotPath: log.snapshotPath }),
385
+ });
386
+ }
387
+
388
+ function buildResult(
389
+ policy: BaselinePolicy,
390
+ status: "clean" | "repaired",
391
+ validation: readonly CommandLog[],
392
+ iterations: number,
393
+ snapshotPath?: string,
394
+ usage?: Usage,
395
+ ): BaselineGateResult {
396
+ return {
397
+ policy,
398
+ status,
399
+ validation,
400
+ iterations,
401
+ ...(snapshotPath === undefined ? {} : { snapshotPath }),
402
+ ...(usage === undefined ? {} : { usage }),
403
+ };
404
+ }
405
+
406
+ function renderValidationFailure(logs: readonly CommandLog[]): string {
407
+ const failed = logs.find((log) => log.status === "failed") ?? logs.at(-1);
408
+ if (failed === undefined) {
409
+ return "No validation commands ran.";
410
+ }
411
+ return `${failed.command}\n${failed.stderr || failed.stdout}`;
412
+ }
413
+
414
+ function regressedReasonFor(stop: FixLoopStop): RegressedReason {
415
+ if (stop === "timeout") return "timeout";
416
+ if (stop === "ceiling" || stop === "budget-exhausted") return "ceiling";
417
+ return "stuck";
418
+ }
419
+
420
+ function addUsage(left: Usage | undefined, right: Usage | undefined): Usage | undefined {
421
+ if (left === undefined) return right;
422
+ if (right === undefined) return left;
423
+ const reasoning = (left.reasoning ?? 0) + (right.reasoning ?? 0);
424
+ return {
425
+ input: left.input + right.input,
426
+ output: left.output + right.output,
427
+ ...(reasoning === 0 ? {} : { reasoning }),
428
+ };
429
+ }
430
+
431
+ function fence(value: string): string {
432
+ return `\`\`\`\n${value.trimEnd()}\n\`\`\``;
433
+ }
434
+
435
+ function currentTime(options: Pick<RunBaselineGateOptions, "now">): number {
436
+ return options.now?.() ?? Date.now();
437
+ }
@@ -0,0 +1,109 @@
1
+ import { BackendTagSchema, type BackendTag } from "../model/index.ts";
2
+ import { extractFlowArgs } from "../flow/args.ts";
3
+
4
+ /** Loop verbs (spec distribution). Absent => the legacy `orcats <flow.ts>` script path. */
5
+ export type LoopCommand = "run" | "serve" | "loops";
6
+
7
+ export interface CliArgs {
8
+ /** A loop verb when the first positional is `run`/`serve`/`loops`; otherwise undefined (legacy). */
9
+ readonly command?: LoopCommand;
10
+ /** The loop module path or registered name for `run`/`serve`. */
11
+ readonly loop?: string;
12
+ /** The legacy flow-script path (no loop verb). */
13
+ readonly script?: string;
14
+ readonly backend?: BackendTag;
15
+ readonly skipTypecheck: boolean;
16
+ readonly help: boolean;
17
+ readonly version: boolean;
18
+ /** Deferred durable-mode flags (spec distribution / design D5) — rejected by `main`. */
19
+ readonly durable?: true;
20
+ readonly postgresUrl?: string;
21
+ readonly stateAdapter?: string;
22
+ readonly flowArgs: readonly string[];
23
+ }
24
+
25
+ const LOOP_COMMANDS = new Set<string>(["run", "serve", "loops"]);
26
+
27
+ export function parseCliArgs(argv: readonly string[]): CliArgs {
28
+ let backend: BackendTag | undefined;
29
+ let skipTypecheck = false;
30
+ let help = false;
31
+ let version = false;
32
+ let durable = false;
33
+ let postgresUrl: string | undefined;
34
+ let stateAdapter: string | undefined;
35
+ const positionals: string[] = [];
36
+
37
+ for (let index = 0; index < argv.length; index += 1) {
38
+ const arg = argv[index];
39
+ if (arg === "--") {
40
+ break; // everything after `--` is the flow/loop task input, captured by extractFlowArgs
41
+ }
42
+ if (arg === "--no-typecheck") {
43
+ skipTypecheck = true;
44
+ continue;
45
+ }
46
+ if (arg === "--help" || arg === "-h") {
47
+ help = true;
48
+ continue;
49
+ }
50
+ if (arg === "--version" || arg === "-v") {
51
+ version = true;
52
+ continue;
53
+ }
54
+ if (arg === "--durable") {
55
+ durable = true;
56
+ continue;
57
+ }
58
+ if (arg === "--backend") {
59
+ backend = BackendTagSchema.parse(argv[index + 1]);
60
+ index += 1;
61
+ continue;
62
+ }
63
+ if (arg?.startsWith("--backend=")) {
64
+ backend = BackendTagSchema.parse(arg.slice("--backend=".length));
65
+ continue;
66
+ }
67
+ if (arg === "--postgres-url") {
68
+ postgresUrl = argv[index + 1] ?? "";
69
+ index += 1;
70
+ continue;
71
+ }
72
+ if (arg?.startsWith("--postgres-url=")) {
73
+ postgresUrl = arg.slice("--postgres-url=".length);
74
+ continue;
75
+ }
76
+ if (arg === "--state") {
77
+ stateAdapter = argv[index + 1];
78
+ index += 1;
79
+ continue;
80
+ }
81
+ if (arg?.startsWith("--state=")) {
82
+ stateAdapter = arg.slice("--state=".length);
83
+ continue;
84
+ }
85
+ if (arg !== undefined && !arg.startsWith("-")) {
86
+ positionals.push(arg);
87
+ }
88
+ }
89
+
90
+ const first = positionals.at(0);
91
+ const isCommand = first !== undefined && LOOP_COMMANDS.has(first);
92
+ const command = isCommand ? (first as LoopCommand) : undefined;
93
+ const loop = isCommand && command !== "loops" ? positionals.at(1) : undefined;
94
+ const script = isCommand ? undefined : first;
95
+
96
+ return {
97
+ ...(command === undefined ? {} : { command }),
98
+ ...(loop === undefined ? {} : { loop }),
99
+ ...(script === undefined ? {} : { script }),
100
+ ...(backend === undefined ? {} : { backend }),
101
+ skipTypecheck,
102
+ help,
103
+ version,
104
+ ...(durable ? { durable: true as const } : {}),
105
+ ...(postgresUrl === undefined ? {} : { postgresUrl }),
106
+ ...(stateAdapter === undefined ? {} : { stateAdapter }),
107
+ flowArgs: extractFlowArgs(argv)
108
+ };
109
+ }