@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,98 @@
1
+ import { createHash } from "node:crypto";
2
+ import { join } from "node:path";
3
+ import { err, ok, type Result } from "neverthrow";
4
+ import { executeLoop, type LoopExecutionAction } from "../loop/execution.ts";
5
+ import type { RuntimeError } from "../model/index.ts";
6
+ import { createFsTool, type FsTool } from "../tools/index.ts";
7
+
8
+ export interface PlanTask {
9
+ readonly id: string;
10
+ readonly description: string;
11
+ }
12
+
13
+ export interface PlanLoopResult {
14
+ readonly completed: readonly string[];
15
+ }
16
+
17
+ export function planHash(input: string): string {
18
+ return createHash("sha256").update(input).digest("hex").slice(0, 12);
19
+ }
20
+
21
+ export function defaultPlanPath(root: string, input: string): string {
22
+ return join(root, ".orca", `plan-${planHash(input)}.md`);
23
+ }
24
+
25
+ export async function writePlan(
26
+ root: string,
27
+ input: string,
28
+ content: string,
29
+ fsTool: FsTool = createFsTool()
30
+ ): Promise<Result<string, RuntimeError>> {
31
+ const path = defaultPlanPath(root, input);
32
+ const written = await fsTool.writeText(path, content);
33
+ return written.map(() => path);
34
+ }
35
+
36
+ export async function recoverPlan(
37
+ path: string,
38
+ fsTool: FsTool = createFsTool()
39
+ ): Promise<Result<string, RuntimeError>> {
40
+ return await fsTool.readText(path);
41
+ }
42
+
43
+ const IMPLEMENT_TASK_ACTION: LoopExecutionAction = { identity: "implement-task", inputs: null };
44
+
45
+ /** Sequential-task `.until()` strategy over loop execution (design D7):
46
+ * drive the pending-task count to zero, implementing one task per cycle and
47
+ * stopping at the first typed failure. */
48
+ export async function sequentialTaskStrategy(
49
+ tasks: readonly PlanTask[],
50
+ implement: (task: PlanTask) => Promise<Result<void, RuntimeError>>
51
+ ): Promise<Result<PlanLoopResult, RuntimeError>> {
52
+ const completed: string[] = [];
53
+ let index = 0;
54
+
55
+ const loop = await executeLoop<{ readonly pending: number }>({
56
+ evaluate: () => Promise.resolve(ok({ pending: tasks.length - index })),
57
+ converged: (state) => state.pending === 0,
58
+ nextAction: (state) => (state.pending === 0 ? undefined : IMPLEMENT_TASK_ACTION),
59
+ execute: async () => {
60
+ const task = tasks[index];
61
+ if (task === undefined) {
62
+ return ok({});
63
+ }
64
+ const result = await implement(task);
65
+ if (result.isErr()) {
66
+ return err(result.error);
67
+ }
68
+ completed.push(task.id);
69
+ index += 1;
70
+ return ok({});
71
+ },
72
+ fingerprint: false,
73
+ maxIterations: Math.max(tasks.length, 1),
74
+ });
75
+
76
+ if (loop.isErr()) {
77
+ return err(loop.error);
78
+ }
79
+ return ok({ completed });
80
+ }
81
+
82
+ /** Deprecated compatibility wrapper kept for one release (design D7): delegates to
83
+ * `sequentialTaskStrategy` and emits a runtime DeprecationWarning. Not tagged
84
+ * `@deprecated` so existing callers still using it for this release stay lint-clean. */
85
+ export function implementTaskLoop(
86
+ tasks: readonly PlanTask[],
87
+ implement: (task: PlanTask) => Promise<Result<void, RuntimeError>>
88
+ ): Promise<Result<PlanLoopResult, RuntimeError>> {
89
+ emitDeprecation("implementTaskLoop", "sequentialTaskStrategy");
90
+ return sequentialTaskStrategy(tasks, implement);
91
+ }
92
+
93
+ function emitDeprecation(name: string, replacement: string): void {
94
+ process.emitWarning(
95
+ `${name}() is deprecated and will be removed in a future release; migrate to ${replacement}().`,
96
+ { type: "DeprecationWarning", code: "ORCA_DEP_LOOP_COLLAPSE" }
97
+ );
98
+ }
@@ -0,0 +1,3 @@
1
+ export type Verdict<A> =
2
+ | { readonly kind: "proceed"; readonly value: A }
3
+ | { readonly kind: "rejection"; readonly category: "question" | "critique" | "rebuff"; readonly body: string }
@@ -0,0 +1,3 @@
1
+ export * from "./loop.ts";
2
+ export * from "./reviewers.ts";
3
+ export * from "./tool.ts";
@@ -0,0 +1,365 @@
1
+ import { err, ok, type Result } from "neverthrow";
2
+ import {
3
+ makeFingerprintStuckDetector,
4
+ type FingerprintAction,
5
+ type FingerprintDetectorOptions,
6
+ type TokenUsageSummary,
7
+ } from "../loop/termination.ts";
8
+ import { executeLoop, type LoopExecutionStop } from "../loop/execution.ts";
9
+
10
+ import type { RuntimeError } from "../model/index.ts";
11
+ import type { ReviewerId, ReviewerPrompt } from "./reviewers.ts";
12
+ import { loadReviewerPrompts, selectReviewers } from "./reviewers.ts";
13
+
14
+ export type {
15
+ FingerprintAction,
16
+ FingerprintDetectorOptions,
17
+ TokenUsageSummary,
18
+ } from "../loop/termination.ts";
19
+
20
+ /** Why the loop stopped. `converged` is the only success; the rest are
21
+ * non-convergence reasons that map onto a `regressed` verdict. */
22
+ export type FixLoopStop = "converged" | "unfixable" | "stuck" | "timeout" | "ceiling" | "budget-exhausted";
23
+
24
+ export type FixLoopFixOutcome = Result<unknown, RuntimeError>;
25
+
26
+ export interface FixLoopSummary<I extends { readonly fixable: boolean } = ReviewIssue> {
27
+ readonly iterations: number;
28
+ readonly ignoredIssues: readonly I[];
29
+ readonly converged: boolean;
30
+ readonly stop: FixLoopStop;
31
+ readonly events: readonly string[];
32
+ readonly tokenUsage?: TokenUsageSummary;
33
+ }
34
+
35
+ export type IssueFingerprint<I extends { readonly fixable: boolean }> =
36
+ | ((issues: readonly I[]) => FingerprintAction | undefined)
37
+ | (Omit<FingerprintDetectorOptions<readonly I[]>, "project"> & {
38
+ readonly project: (issues: readonly I[]) => FingerprintAction | undefined;
39
+ });
40
+
41
+ export interface FixLoopOptions<I extends { readonly fixable: boolean }> {
42
+ /** High sanity ceiling on fix iterations — a seatbelt, not the policy. */
43
+ readonly maxIterations?: number;
44
+ /** Wall-clock backstop in ms across the whole loop. */
45
+ readonly wallClockMs?: number;
46
+ /** Optional token budget across fix cycles that report backend usage. */
47
+ readonly tokenBudget?: number;
48
+ /** Stateful no-progress detector, owned by the caller. Called with the
49
+ * current round's issues before each fix; returning `true` stops the loop as
50
+ * `stuck`. Preserved for callers that already own their signature state. */
51
+ readonly stalled?: (issues: readonly I[]) => boolean;
52
+ /** Configured issue-list projection into the shared fingerprint primitive. */
53
+ readonly fingerprint?: IssueFingerprint<I>;
54
+ /** Injectable clock for deterministic tests. */
55
+ readonly now?: () => number;
56
+ }
57
+
58
+ export type FixLoopAction = FingerprintAction;
59
+
60
+ export interface GenericFixLoopSummary<State> {
61
+ readonly iterations: number;
62
+ readonly converged: boolean;
63
+ readonly stop: FixLoopStop;
64
+ readonly lastEvaluation: State;
65
+ readonly events: readonly string[];
66
+ readonly tokenUsage?: TokenUsageSummary;
67
+ }
68
+
69
+ export interface GenericFixLoopOptions<State, Action extends FixLoopAction = FixLoopAction> {
70
+ readonly evaluate: () => Promise<Result<State, RuntimeError>>;
71
+ readonly converged: (state: State) => boolean;
72
+ readonly nextAction: (state: State) => Action | undefined;
73
+ readonly fix: (action: Action, state: State) => Promise<FixLoopFixOutcome>;
74
+ /** High sanity ceiling on fix iterations — a seatbelt, not the policy. */
75
+ readonly maxIterations?: number;
76
+ /** Wall-clock backstop in ms across the whole loop. */
77
+ readonly wallClockMs?: number;
78
+ /** Optional token budget across fix cycles that report backend usage. */
79
+ readonly tokenBudget?: number;
80
+ /** Additional caller-owned stop predicate. The built-in fingerprint guard is
81
+ * still the default stuck detector for generic actions. */
82
+ readonly stalled?: (state: State, action: Action) => boolean;
83
+ /** Generic actions are fingerprinted by `{identity, inputs}` by default. */
84
+ readonly fingerprint?: false | Omit<FingerprintDetectorOptions<{ readonly state: State; readonly action: Action }>, "project">;
85
+ /** Injectable clock for deterministic tests. */
86
+ readonly now?: () => number;
87
+ }
88
+
89
+ interface IssueFixAction<I extends { readonly fixable: boolean }> extends FixLoopAction {
90
+ readonly fixable: readonly I[];
91
+ }
92
+
93
+ /** Generic convergence primitive: iterate `evaluate → action → fix` until the
94
+ * state converges or one guard fires. The action fingerprint guard hashes
95
+ * `{identity, inputs}` over a sliding window, so repeated commands and A→B→A
96
+ * oscillation use the same primitive. */
97
+ export function fixLoop<State, Action extends FixLoopAction = FixLoopAction>(
98
+ options: GenericFixLoopOptions<State, Action>
99
+ ): Promise<Result<GenericFixLoopSummary<State>, RuntimeError>>;
100
+
101
+ /** Issue-list overload kept for current review callers. The third argument
102
+ * accepts a bare iteration count for backward compatibility or a full
103
+ * {@link FixLoopOptions}. */
104
+ export function fixLoop<I extends { readonly fixable: boolean }>(
105
+ evaluate: () => Promise<Result<readonly I[], RuntimeError>>,
106
+ fix: (issues: readonly I[]) => Promise<FixLoopFixOutcome>,
107
+ options?: number | FixLoopOptions<I>,
108
+ ): Promise<Result<FixLoopSummary<I>, RuntimeError>>;
109
+
110
+ export async function fixLoop<State, Action extends FixLoopAction, I extends { readonly fixable: boolean }>(
111
+ first: GenericFixLoopOptions<State, Action> | (() => Promise<Result<readonly I[], RuntimeError>>),
112
+ second?: (issues: readonly I[]) => Promise<FixLoopFixOutcome>,
113
+ options: number | FixLoopOptions<I> = {},
114
+ ): Promise<Result<GenericFixLoopSummary<State> | FixLoopSummary<I>, RuntimeError>> {
115
+ if (typeof first !== "function") {
116
+ return runGenericFixLoop(first);
117
+ }
118
+
119
+ if (second === undefined) {
120
+ throw new TypeError("fixLoop issue-list overload requires a fix function");
121
+ }
122
+
123
+ return runIssueFixLoop(first, second, options);
124
+ }
125
+
126
+ async function runIssueFixLoop<I extends { readonly fixable: boolean }>(
127
+ evaluate: () => Promise<Result<readonly I[], RuntimeError>>,
128
+ fix: (issues: readonly I[]) => Promise<FixLoopFixOutcome>,
129
+ options: number | FixLoopOptions<I>,
130
+ ): Promise<Result<FixLoopSummary<I>, RuntimeError>> {
131
+ const opts = typeof options === "number" ? { maxIterations: options } : options;
132
+ const fingerprintStalled = issueFingerprintDetector(opts.fingerprint);
133
+ const loop = await runGenericFixLoop<readonly I[], IssueFixAction<I>>({
134
+ evaluate,
135
+ converged: (issues) => issues.length === 0,
136
+ nextAction: (issues) => {
137
+ const fixable = issues.filter((issue) => issue.fixable);
138
+ if (fixable.length === 0) {
139
+ return undefined;
140
+ }
141
+ return { identity: "fix-issues", inputs: fixable, fixable };
142
+ },
143
+ fix: (action) => fix(action.fixable),
144
+ ...(opts.maxIterations === undefined ? {} : { maxIterations: opts.maxIterations }),
145
+ ...(opts.wallClockMs === undefined ? {} : { wallClockMs: opts.wallClockMs }),
146
+ ...(opts.tokenBudget === undefined ? {} : { tokenBudget: opts.tokenBudget }),
147
+ ...(opts.now === undefined ? {} : { now: opts.now }),
148
+ fingerprint: false,
149
+ stalled: (issues) => Boolean(opts.stalled?.(issues) || fingerprintStalled?.(issues)),
150
+ });
151
+
152
+ if (loop.isErr()) {
153
+ return err(loop.error);
154
+ }
155
+
156
+ const summary = loop.value;
157
+ return ok({
158
+ iterations: summary.iterations,
159
+ ignoredIssues: summary.converged ? [] : [...summary.lastEvaluation],
160
+ converged: summary.converged,
161
+ stop: summary.stop,
162
+ events: summary.events,
163
+ ...(summary.tokenUsage === undefined ? {} : { tokenUsage: summary.tokenUsage }),
164
+ });
165
+ }
166
+
167
+ async function runGenericFixLoop<State, Action extends FixLoopAction>(
168
+ options: GenericFixLoopOptions<State, Action>
169
+ ): Promise<Result<GenericFixLoopSummary<State>, RuntimeError>> {
170
+ const loop = await executeLoop<State, Action>({
171
+ evaluate: options.evaluate,
172
+ converged: options.converged,
173
+ nextAction: options.nextAction,
174
+ execute: (action, state) => options.fix(action, state),
175
+ maxIterations: options.maxIterations ?? 10,
176
+ ...(options.wallClockMs === undefined ? {} : { wallClockMs: options.wallClockMs }),
177
+ ...(options.tokenBudget === undefined ? {} : { tokenBudget: options.tokenBudget }),
178
+ ...(options.now === undefined ? {} : { now: options.now }),
179
+ ...(options.stalled === undefined ? {} : { stalled: options.stalled }),
180
+ fingerprint: options.fingerprint ?? {},
181
+ });
182
+
183
+ if (loop.isErr()) {
184
+ return err(loop.error);
185
+ }
186
+ const summary = loop.value;
187
+ return ok({
188
+ iterations: summary.iterations,
189
+ converged: summary.converged,
190
+ stop: toFixLoopStop(summary.stop),
191
+ lastEvaluation: summary.lastEvaluation,
192
+ events: summary.events,
193
+ ...(summary.tokenUsage === undefined ? {} : { tokenUsage: summary.tokenUsage }),
194
+ });
195
+ }
196
+
197
+ function issueFingerprintDetector<I extends { readonly fixable: boolean }>(
198
+ fingerprint: IssueFingerprint<I> | undefined,
199
+ ): ((issues: readonly I[]) => boolean) | undefined {
200
+ if (fingerprint === undefined) {
201
+ return undefined;
202
+ }
203
+
204
+ if (typeof fingerprint === "function") {
205
+ return makeFingerprintStuckDetector({ project: fingerprint });
206
+ }
207
+
208
+ return makeFingerprintStuckDetector({
209
+ project: fingerprint.project,
210
+ ...(fingerprint.windowSize === undefined ? {} : { windowSize: fingerprint.windowSize }),
211
+ ...(fingerprint.repeatThreshold === undefined ? {} : { repeatThreshold: fingerprint.repeatThreshold }),
212
+ });
213
+ }
214
+
215
+ function toFixLoopStop(stop: LoopExecutionStop): FixLoopStop {
216
+ if (stop === "cancelled") {
217
+ return "timeout";
218
+ }
219
+ return stop;
220
+ }
221
+
222
+ export interface ReviewIssue {
223
+ readonly reviewer: ReviewerId;
224
+ readonly message: string;
225
+ readonly fixable: boolean;
226
+ }
227
+
228
+ export interface ReviewTurn {
229
+ readonly reviewer: ReviewerId;
230
+ readonly issues: readonly ReviewIssue[];
231
+ }
232
+
233
+ export interface ReviewLoopSummary {
234
+ readonly selected: readonly ReviewerId[];
235
+ readonly issues: readonly ReviewIssue[];
236
+ readonly fixed: boolean;
237
+ readonly events: readonly string[];
238
+ }
239
+
240
+ export async function runReviewersParallel<I extends ReviewIssue>(
241
+ reviewers: readonly ReviewerPrompt[],
242
+ review: (reviewer: ReviewerPrompt) => Promise<Result<readonly I[], RuntimeError>>,
243
+ ): Promise<Result<readonly I[], RuntimeError>> {
244
+ const results = await Promise.all(reviewers.map((r) => review(r)));
245
+ const issues: I[] = [];
246
+ for (const result of results) {
247
+ if (result.isErr()) return err(result.error);
248
+ issues.push(...result.value);
249
+ }
250
+ const seen = new Set<string>();
251
+ return ok(
252
+ issues.filter((issue) => {
253
+ const key = `${issue.reviewer}::${issue.message}`;
254
+ if (seen.has(key)) return false;
255
+ seen.add(key);
256
+ return true;
257
+ }),
258
+ );
259
+ }
260
+
261
+ export interface ReviewAndFixOptions {
262
+ readonly requested?: readonly ReviewerId[];
263
+ readonly loadPrompts?: () => Promise<ReviewerPrompt[]>;
264
+ readonly review: (reviewer: ReviewerPrompt) => Promise<Result<readonly ReviewIssue[], RuntimeError>>;
265
+ readonly fix: (issues: readonly ReviewIssue[]) => Promise<Result<void, RuntimeError>>;
266
+ readonly parallel?: boolean;
267
+ }
268
+
269
+ /** Review-and-fix `.until()` strategy over loop execution (design D7):
270
+ * one review pass collects issues, then loop execution drives the fixable-issue count
271
+ * to zero. A two-phase state runs reviewers exactly once and applies the fix at
272
+ * most once, reproducing the single-pass review/fix behavior. */
273
+ export async function reviewAndFixStrategy(
274
+ options: ReviewAndFixOptions
275
+ ): Promise<Result<ReviewLoopSummary, RuntimeError>> {
276
+ const prompts = await (options.loadPrompts ?? loadReviewerPrompts)();
277
+ const selected = selectReviewers(prompts, options.requested);
278
+ const events: string[] = [];
279
+
280
+ let issues: readonly ReviewIssue[] = [];
281
+ let fixable: readonly ReviewIssue[] = [];
282
+ let phase: "review" | "fixed" = "review";
283
+ let fixed = false;
284
+
285
+ const loop = await executeLoop<{ readonly fixableRemaining: number }>({
286
+ evaluate: async () => {
287
+ if (phase === "review") {
288
+ const reviewed = await runReviewPhase(selected, options, events);
289
+ if (reviewed.isErr()) return err(reviewed.error);
290
+ issues = reviewed.value;
291
+ fixable = issues.filter((issue) => issue.fixable);
292
+ }
293
+ return ok({ fixableRemaining: phase === "review" ? fixable.length : 0 });
294
+ },
295
+ converged: (state) => state.fixableRemaining === 0,
296
+ nextAction: (state) => (state.fixableRemaining === 0 ? undefined : REVIEW_FIX_ACTION),
297
+ execute: async () => {
298
+ events.push("fix:started");
299
+ const result = await options.fix(fixable);
300
+ if (result.isErr()) return err(result.error);
301
+ events.push("fix:completed");
302
+ fixed = true;
303
+ phase = "fixed";
304
+ return ok({});
305
+ },
306
+ fingerprint: false,
307
+ maxIterations: 1,
308
+ });
309
+
310
+ if (loop.isErr()) {
311
+ return err(loop.error);
312
+ }
313
+
314
+ return ok({
315
+ selected: selected.map((reviewer) => reviewer.id),
316
+ issues,
317
+ fixed,
318
+ events,
319
+ });
320
+ }
321
+
322
+ const REVIEW_FIX_ACTION: FixLoopAction = { identity: "review-fix", inputs: null };
323
+
324
+ /** One review pass — parallel dispatch or sequential per-reviewer — recording the
325
+ * same domain events the loop has always emitted. */
326
+ async function runReviewPhase(
327
+ selected: readonly ReviewerPrompt[],
328
+ options: ReviewAndFixOptions,
329
+ events: string[]
330
+ ): Promise<Result<readonly ReviewIssue[], RuntimeError>> {
331
+ if (options.parallel) {
332
+ events.push("review:parallel:started");
333
+ const result = await runReviewersParallel(selected, options.review);
334
+ if (result.isErr()) return err(result.error);
335
+ events.push("review:parallel:completed");
336
+ return ok(result.value);
337
+ }
338
+
339
+ const collected: ReviewIssue[] = [];
340
+ for (const reviewer of selected) {
341
+ events.push(`review:${reviewer.id}:started`);
342
+ const result = await options.review(reviewer);
343
+ if (result.isErr()) return err(result.error);
344
+ collected.push(...result.value);
345
+ events.push(`review:${reviewer.id}:completed`);
346
+ }
347
+ return ok(collected);
348
+ }
349
+
350
+ /** Deprecated compatibility wrapper kept for one release (design D7): delegates to
351
+ * `reviewAndFixStrategy` and emits a runtime DeprecationWarning. Not tagged
352
+ * `@deprecated` so existing callers still using it for this release stay lint-clean. */
353
+ export function runReviewAndFixLoop(
354
+ options: ReviewAndFixOptions
355
+ ): Promise<Result<ReviewLoopSummary, RuntimeError>> {
356
+ emitDeprecation("runReviewAndFixLoop", "reviewAndFixStrategy");
357
+ return reviewAndFixStrategy(options);
358
+ }
359
+
360
+ function emitDeprecation(name: string, replacement: string): void {
361
+ process.emitWarning(
362
+ `${name}() is deprecated and will be removed in a future release; migrate to ${replacement}().`,
363
+ { type: "DeprecationWarning", code: "ORCA_DEP_LOOP_COLLAPSE" }
364
+ );
365
+ }
@@ -0,0 +1,92 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ /** Reviewer prompt markdown ships alongside this module, so resolve the default
6
+ * directory relative to the package — not `process.cwd()`, which is the caller's
7
+ * project when the review tool runs inside another repo. */
8
+ const defaultReviewersDir = fileURLToPath(new URL("./prompts/reviewers", import.meta.url));
9
+
10
+ export const ReviewerIds = [
11
+ "code-functionality",
12
+ "test",
13
+ "readability",
14
+ "code-structure",
15
+ "simplicity",
16
+ "performance",
17
+ "security",
18
+ "scala-fp"
19
+ ] as const;
20
+
21
+ export type ReviewerId = (typeof ReviewerIds)[number];
22
+
23
+ export interface ReviewerPrompt {
24
+ readonly id: ReviewerId;
25
+ readonly name?: string;
26
+ readonly description?: string;
27
+ readonly files?: string;
28
+ readonly prompt: string;
29
+ }
30
+
31
+ export async function loadReviewerPrompts(
32
+ root = defaultReviewersDir
33
+ ): Promise<ReviewerPrompt[]> {
34
+ return await Promise.all(
35
+ ReviewerIds.map(async (id) => {
36
+ const content = await readFile(join(root, `${id}.md`), "utf8");
37
+ return parseReviewerPrompt(id, content);
38
+ })
39
+ );
40
+ }
41
+
42
+ export function selectReviewers(
43
+ prompts: readonly ReviewerPrompt[],
44
+ requested: readonly ReviewerId[] = ["code-functionality", "readability", "test"]
45
+ ): ReviewerPrompt[] {
46
+ const byId = new Map(prompts.map((prompt) => [prompt.id, prompt]));
47
+ return requested.map((id) => {
48
+ const prompt = byId.get(id);
49
+ if (!prompt) {
50
+ throw new Error(`Reviewer prompt not loaded: ${id}`);
51
+ }
52
+ return prompt;
53
+ });
54
+ }
55
+
56
+ export function parseReviewerPrompt(id: ReviewerId, content: string): ReviewerPrompt {
57
+ if (!content.startsWith("---\n")) {
58
+ return { id, prompt: content };
59
+ }
60
+
61
+ const end = content.indexOf("\n---\n", 4);
62
+ if (end === -1) {
63
+ return { id, prompt: content };
64
+ }
65
+
66
+ const frontmatter = content.slice(4, end);
67
+ const prompt = content.slice(end + 5).replace(/^\n/, "");
68
+ const metadata: {
69
+ name?: string;
70
+ description?: string;
71
+ files?: string;
72
+ } = {};
73
+ for (const line of frontmatter.split("\n")) {
74
+ const match = line.match(/^([^:]+):\s*(.*)$/);
75
+ if (!match) {
76
+ continue;
77
+ }
78
+ const key = match[1];
79
+ const value = match[2] ?? "";
80
+ if (key === "name" || key === "description" || key === "files") {
81
+ metadata[key] = value;
82
+ }
83
+ }
84
+
85
+ return {
86
+ id,
87
+ ...(metadata.name === undefined ? {} : { name: metadata.name }),
88
+ ...(metadata.description === undefined ? {} : { description: metadata.description }),
89
+ ...(metadata.files === undefined ? {} : { files: metadata.files }),
90
+ prompt
91
+ };
92
+ }
@@ -0,0 +1,41 @@
1
+ import type { Result } from "neverthrow";
2
+ import type { RuntimeError } from "../model/index.ts";
3
+ import {
4
+ reviewAndFixStrategy,
5
+ type ReviewAndFixOptions,
6
+ type ReviewLoopSummary
7
+ } from "./loop.ts";
8
+ import type { ReviewerId, ReviewerPrompt } from "./reviewers.ts";
9
+
10
+ export const DefaultReviewers = ["code-functionality", "readability", "test"] as const;
11
+
12
+ export interface ReviewTool {
13
+ readonly reviewers: readonly ReviewerId[];
14
+ run(options: ReviewToolRunOptions): Promise<Result<ReviewLoopSummary, RuntimeError>>;
15
+ }
16
+
17
+ export interface ReviewToolOptions {
18
+ readonly reviewers?: readonly ReviewerId[];
19
+ }
20
+
21
+ export interface ReviewToolRunOptions {
22
+ readonly requested?: readonly ReviewerId[];
23
+ readonly loadPrompts?: () => Promise<ReviewerPrompt[]>;
24
+ readonly review: ReviewAndFixOptions["review"];
25
+ readonly fix: ReviewAndFixOptions["fix"];
26
+ }
27
+
28
+ export function createReviewTool(options: ReviewToolOptions = {}): ReviewTool {
29
+ const reviewers = options.reviewers ?? DefaultReviewers;
30
+ return {
31
+ reviewers,
32
+ async run(runOptions) {
33
+ return await reviewAndFixStrategy({
34
+ requested: runOptions.requested ?? reviewers,
35
+ ...(runOptions.loadPrompts === undefined ? {} : { loadPrompts: runOptions.loadPrompts }),
36
+ review: runOptions.review,
37
+ fix: runOptions.fix
38
+ });
39
+ }
40
+ };
41
+ }