@chengchenccc/oh-my-agent 0.2.0-rc.2 → 0.2.0-rc.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 (209) hide show
  1. package/README.md +11 -6
  2. package/dist/cli/args.d.ts +1 -1
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +9 -2
  5. package/dist/cli/gateway-commands.d.ts.map +1 -1
  6. package/dist/cli/gateway-commands.js +35 -5
  7. package/dist/cli/initial-input.d.ts +3 -2
  8. package/dist/cli/initial-input.d.ts.map +1 -1
  9. package/dist/core/coordination/registry.d.ts +11 -0
  10. package/dist/core/coordination/registry.d.ts.map +1 -1
  11. package/dist/core/coordination/registry.js +23 -0
  12. package/dist/core/gateway/artifact.d.ts +0 -1
  13. package/dist/core/gateway/artifact.d.ts.map +1 -1
  14. package/dist/core/gateway/artifact.js +1 -1
  15. package/dist/core/goals/index.d.ts +8 -0
  16. package/dist/core/goals/index.d.ts.map +1 -0
  17. package/dist/core/goals/index.js +7 -0
  18. package/dist/core/goals/runtime.d.ts +112 -0
  19. package/dist/core/goals/runtime.d.ts.map +1 -0
  20. package/dist/core/goals/runtime.js +241 -0
  21. package/dist/core/goals/state.d.ts +82 -0
  22. package/dist/core/goals/state.d.ts.map +1 -0
  23. package/dist/core/goals/state.js +225 -0
  24. package/dist/core/goals/tool.d.ts +10 -0
  25. package/dist/core/goals/tool.d.ts.map +1 -0
  26. package/dist/core/goals/tool.js +87 -0
  27. package/dist/core/index.d.ts.map +1 -1
  28. package/dist/core/index.js +4 -3
  29. package/dist/core/loops/condition.d.ts +57 -0
  30. package/dist/core/loops/condition.d.ts.map +1 -0
  31. package/dist/core/loops/condition.js +153 -0
  32. package/dist/core/loops/index.d.ts +9 -0
  33. package/dist/core/loops/index.d.ts.map +1 -0
  34. package/dist/core/loops/index.js +8 -0
  35. package/dist/core/loops/limits.d.ts +43 -0
  36. package/dist/core/loops/limits.d.ts.map +1 -0
  37. package/dist/core/loops/limits.js +219 -0
  38. package/dist/core/loops/ralph.d.ts +37 -0
  39. package/dist/core/loops/ralph.d.ts.map +1 -0
  40. package/dist/core/loops/ralph.js +72 -0
  41. package/dist/core/loops/runtime.d.ts +95 -0
  42. package/dist/core/loops/runtime.d.ts.map +1 -0
  43. package/dist/core/loops/runtime.js +222 -0
  44. package/dist/core/memory/managed-skills.d.ts +0 -11
  45. package/dist/core/memory/managed-skills.d.ts.map +1 -1
  46. package/dist/core/memory/managed-skills.js +2 -2
  47. package/dist/core/memory/vector-store.d.ts +0 -1
  48. package/dist/core/memory/vector-store.d.ts.map +1 -1
  49. package/dist/core/memory/vector-store.js +0 -3
  50. package/dist/core/plans/index.d.ts +7 -0
  51. package/dist/core/plans/index.d.ts.map +1 -0
  52. package/dist/core/plans/index.js +6 -0
  53. package/dist/core/plans/prompts.d.ts +20 -0
  54. package/dist/core/plans/prompts.d.ts.map +1 -0
  55. package/dist/core/plans/prompts.js +72 -0
  56. package/dist/core/plans/runtime.d.ts +89 -0
  57. package/dist/core/plans/runtime.d.ts.map +1 -0
  58. package/dist/core/plans/runtime.js +170 -0
  59. package/dist/core/plans/state.d.ts +55 -0
  60. package/dist/core/plans/state.d.ts.map +1 -0
  61. package/dist/core/plans/state.js +88 -0
  62. package/dist/core/runtime/agent-loop-messages.d.ts +6 -0
  63. package/dist/core/runtime/agent-loop-messages.d.ts.map +1 -1
  64. package/dist/core/runtime/agent-loop-messages.js +40 -15
  65. package/dist/core/runtime/agent-loop-run.d.ts.map +1 -1
  66. package/dist/core/runtime/agent-loop-run.js +18 -27
  67. package/dist/core/runtime/agent-loop-runner.d.ts +1 -23
  68. package/dist/core/runtime/agent-loop-runner.d.ts.map +1 -1
  69. package/dist/core/runtime/agent-loop-runner.js +84 -30
  70. package/dist/core/runtime/agent-loop-types.d.ts +4 -0
  71. package/dist/core/runtime/agent-loop-types.d.ts.map +1 -1
  72. package/dist/core/runtime/compaction.d.ts +9 -0
  73. package/dist/core/runtime/compaction.d.ts.map +1 -1
  74. package/dist/core/runtime/compaction.js +28 -0
  75. package/dist/core/runtime/context-estimate.d.ts +37 -4
  76. package/dist/core/runtime/context-estimate.d.ts.map +1 -1
  77. package/dist/core/runtime/context-estimate.js +70 -0
  78. package/dist/core/runtime/create-runtime.d.ts +27 -5
  79. package/dist/core/runtime/create-runtime.d.ts.map +1 -1
  80. package/dist/core/runtime/create-runtime.js +22 -2
  81. package/dist/core/runtime/permission-classifier.d.ts.map +1 -1
  82. package/dist/core/runtime/permission-classifier.js +83 -12
  83. package/dist/core/runtime/plugin.d.ts +7 -0
  84. package/dist/core/runtime/plugin.d.ts.map +1 -1
  85. package/dist/core/runtime/plugin.js +13 -0
  86. package/dist/core/runtime/process-tree.d.ts +0 -9
  87. package/dist/core/runtime/process-tree.d.ts.map +1 -1
  88. package/dist/core/runtime/process-tree.js +1 -1
  89. package/dist/core/runtime/run-runtime.d.ts.map +1 -1
  90. package/dist/core/runtime/run-runtime.js +88 -49
  91. package/dist/core/runtime/runtime-catalog.d.ts +0 -4
  92. package/dist/core/runtime/runtime-catalog.d.ts.map +1 -1
  93. package/dist/core/runtime/runtime-catalog.js +1 -1
  94. package/dist/core/runtime/stream-rules.d.ts +9 -2
  95. package/dist/core/runtime/stream-rules.d.ts.map +1 -1
  96. package/dist/core/runtime/stream-rules.js +19 -5
  97. package/dist/core/runtime/title.d.ts +13 -1
  98. package/dist/core/runtime/title.d.ts.map +1 -1
  99. package/dist/core/runtime/title.js +51 -17
  100. package/dist/core/runtime/tool-pruning.d.ts.map +1 -1
  101. package/dist/core/runtime/tool-pruning.js +8 -16
  102. package/dist/core/session/session-file.d.ts +91 -5
  103. package/dist/core/session/session-file.d.ts.map +1 -1
  104. package/dist/core/session/session-file.js +206 -24
  105. package/dist/core/session/session-loop.d.ts +3 -1
  106. package/dist/core/session/session-loop.d.ts.map +1 -1
  107. package/dist/core/session/session-loop.js +8 -4
  108. package/dist/core/settings/project-settings.d.ts +63 -9
  109. package/dist/core/settings/project-settings.d.ts.map +1 -1
  110. package/dist/core/settings/project-settings.js +43 -2
  111. package/dist/core/settings/workspace-context.js +4 -4
  112. package/dist/core/store/in-memory-session-store.d.ts.map +1 -1
  113. package/dist/core/store/in-memory-session-store.js +3 -2
  114. package/dist/core/store/session-store.contract.test-helper.d.ts +7 -5
  115. package/dist/core/store/session-store.contract.test-helper.d.ts.map +1 -1
  116. package/dist/core/store/session-store.contract.test-helper.js +7 -5
  117. package/dist/core/store/session-store.d.ts +4 -2
  118. package/dist/core/store/session-store.d.ts.map +1 -1
  119. package/dist/core/store/session-store.js +4 -2
  120. package/dist/core/tools/ask-question.d.ts.map +1 -1
  121. package/dist/core/tools/ask-question.js +51 -5
  122. package/dist/core/tools/bash-pty.d.ts +13 -4
  123. package/dist/core/tools/bash-pty.d.ts.map +1 -1
  124. package/dist/core/tools/bash-pty.js +15 -8
  125. package/dist/core/tools/bash-sandbox.d.ts +1 -1
  126. package/dist/core/tools/bash.d.ts +1 -1
  127. package/dist/core/tools/bash.d.ts.map +1 -1
  128. package/dist/core/tools/bash.js +18 -8
  129. package/dist/core/tools/browser.d.ts +54 -0
  130. package/dist/core/tools/browser.d.ts.map +1 -1
  131. package/dist/core/tools/browser.js +138 -1
  132. package/dist/core/tools/file-fingerprint.d.ts +24 -0
  133. package/dist/core/tools/file-fingerprint.d.ts.map +1 -0
  134. package/dist/core/tools/file-fingerprint.js +61 -0
  135. package/dist/core/tools/file-tools.d.ts +26 -0
  136. package/dist/core/tools/file-tools.d.ts.map +1 -1
  137. package/dist/core/tools/file-tools.js +113 -17
  138. package/dist/core/tools/index.d.ts +2 -0
  139. package/dist/core/tools/index.d.ts.map +1 -1
  140. package/dist/core/tools/index.js +1 -0
  141. package/dist/core/tools/mcp-mount.d.ts +1 -1
  142. package/dist/core/tools/mcp-mount.d.ts.map +1 -1
  143. package/dist/core/tools/mcp-mount.js +72 -26
  144. package/dist/core/tools/url-guard.d.ts +15 -0
  145. package/dist/core/tools/url-guard.d.ts.map +1 -1
  146. package/dist/core/tools/url-guard.js +54 -14
  147. package/dist/core/tools/web-ports-std.d.ts.map +1 -1
  148. package/dist/core/tools/web-ports-std.js +5 -2
  149. package/dist/modes/print-mode.d.ts +3 -2
  150. package/dist/modes/print-mode.d.ts.map +1 -1
  151. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  152. package/dist/modes/rpc/rpc-mode.js +2 -2
  153. package/dist/modes/shared.d.ts +12 -0
  154. package/dist/modes/shared.d.ts.map +1 -1
  155. package/dist/modes/shared.js +23 -6
  156. package/dist/modes/tui/agent-status.d.ts +20 -0
  157. package/dist/modes/tui/agent-status.d.ts.map +1 -0
  158. package/dist/modes/tui/agent-status.js +35 -0
  159. package/dist/modes/tui/ask-panel.d.ts +166 -0
  160. package/dist/modes/tui/ask-panel.d.ts.map +1 -0
  161. package/dist/modes/tui/ask-panel.js +888 -0
  162. package/dist/modes/tui/pty-console.d.ts +3 -0
  163. package/dist/modes/tui/pty-console.d.ts.map +1 -1
  164. package/dist/modes/tui/pty-console.js +19 -0
  165. package/dist/modes/tui/settings-overlay.d.ts +19 -0
  166. package/dist/modes/tui/settings-overlay.d.ts.map +1 -1
  167. package/dist/modes/tui/settings-overlay.js +46 -10
  168. package/dist/modes/tui/tui-commands-session.d.ts.map +1 -1
  169. package/dist/modes/tui/tui-commands-session.js +20 -6
  170. package/dist/modes/tui/tui-commands.d.ts +44 -3
  171. package/dist/modes/tui/tui-commands.d.ts.map +1 -1
  172. package/dist/modes/tui/tui-commands.js +380 -6
  173. package/dist/modes/tui/tui-components.d.ts +10 -0
  174. package/dist/modes/tui/tui-components.d.ts.map +1 -1
  175. package/dist/modes/tui/tui-components.js +26 -0
  176. package/dist/modes/tui/tui-format.d.ts +53 -4
  177. package/dist/modes/tui/tui-format.d.ts.map +1 -1
  178. package/dist/modes/tui/tui-format.js +137 -42
  179. package/dist/modes/tui/tui-frame-provider.d.ts +11 -3
  180. package/dist/modes/tui/tui-frame-provider.d.ts.map +1 -1
  181. package/dist/modes/tui/tui-frame-provider.js +14 -5
  182. package/dist/modes/tui/tui-interactive.d.ts.map +1 -1
  183. package/dist/modes/tui/tui-interactive.js +14 -0
  184. package/dist/modes/tui/tui-io.d.ts.map +1 -1
  185. package/dist/modes/tui/tui-io.js +150 -55
  186. package/dist/modes/tui/tui-mode.d.ts.map +1 -1
  187. package/dist/modes/tui/tui-mode.js +428 -23
  188. package/dist/modes/tui/tui-overlays.d.ts +6 -10
  189. package/dist/modes/tui/tui-overlays.d.ts.map +1 -1
  190. package/dist/modes/tui/tui-overlays.js +0 -43
  191. package/dist/modes/tui/tui-render.d.ts +10 -0
  192. package/dist/modes/tui/tui-render.d.ts.map +1 -1
  193. package/dist/modes/tui/tui-render.js +73 -38
  194. package/dist/modes/tui/tui-seam.d.ts +28 -6
  195. package/dist/modes/tui/tui-seam.d.ts.map +1 -1
  196. package/dist/modes/tui/tui-slash.d.ts.map +1 -1
  197. package/dist/modes/tui/tui-slash.js +3 -1
  198. package/dist/modes/tui/tui-tool-render.d.ts +14 -1
  199. package/dist/modes/tui/tui-tool-render.d.ts.map +1 -1
  200. package/dist/modes/tui/tui-tool-render.js +158 -50
  201. package/dist/modes/tui/tui-transcript-reconciler.d.ts +4 -0
  202. package/dist/modes/tui/tui-transcript-reconciler.d.ts.map +1 -1
  203. package/dist/modes/tui/tui-transcript-reconciler.js +12 -2
  204. package/dist/modes/tui/view-state.d.ts.map +1 -1
  205. package/dist/modes/tui/view-state.js +4 -1
  206. package/dist/protocol/transport.d.ts +10 -0
  207. package/dist/protocol/transport.d.ts.map +1 -1
  208. package/dist/protocol/transport.js +1 -0
  209. package/package.json +11 -11
@@ -5,9 +5,10 @@
5
5
  // though every symbol it exports comes from a subdirectory (the loop lives in
6
6
  // ./runtime/, the stores in ./store/). Import from here so callers keep
7
7
  // one stable seam; read ./runtime/README.md for the loop's file map.
8
- // Honest scope: in-app consumers mostly import submodules directly; this
9
- // barrel serves the app's public `src/index.ts` re-exports and tests that
10
- // want the loop's whole contract in one import.
8
+ // Honest scope: this barrel is mostly used BY core's own submodules (they
9
+ // import `../index.js` to avoid deep relative paths) and by tests that want the
10
+ // loop's whole contract in one import. `apps/oh-my-agent/src/index.ts` — the
11
+ // app's public surface — imports the submodules directly, not this file.
11
12
  export { createOmaSession } from "./runtime/agent-loop.js";
12
13
  // Compaction + retry
13
14
  export { compactSession } from "./runtime/compaction.js";
@@ -0,0 +1,57 @@
1
+ import type { BashSandbox } from "../tools/bash-sandbox.js";
2
+ /** Deterministic continue-condition for loop mode: a shell command whose EXIT
3
+ * STATUS decides whether the next iteration runs.
4
+ *
5
+ * Exit status is authoritative and stdout is ignored. `echo false` exits 0,
6
+ * so "boolean-ish output" and the exit code actively disagree on the same
7
+ * command, and every predicate a user already reaches for (`test`, `grep -q`,
8
+ * `git diff --quiet`, `&&`) speaks exit codes.
9
+ *
10
+ * Exit 1 is the only "condition is false" status. Anything higher (127
11
+ * command not found, 126 not executable, 2 syntax error) means the CONDITION
12
+ * itself is broken and is surfaced as an error instead of being read as
13
+ * "false" — otherwise a typo'd condition halts the loop looking exactly like
14
+ * finished work, which is the failure mode this feature exists to avoid. */
15
+ export interface LoopConditionConfig {
16
+ /** Shell command; its exit status decides. */
17
+ command: string;
18
+ /** true = `--until` (stop when the command succeeds); false = `--while`
19
+ * (stop when it fails). */
20
+ until: boolean;
21
+ }
22
+ export type LoopConditionVerdict =
23
+ /** Another iteration should run. */
24
+ {
25
+ kind: "continue";
26
+ }
27
+ /** The condition resolved cleanly and says stop. */
28
+ | {
29
+ kind: "halt";
30
+ message: string;
31
+ }
32
+ /** The condition command is broken or timed out: stop and say why. */
33
+ | {
34
+ kind: "error";
35
+ message: string;
36
+ }
37
+ /** Aborted mid-evaluation (Esc); the caller owns that UX. */
38
+ | {
39
+ kind: "aborted";
40
+ };
41
+ export interface LoopConditionOptions {
42
+ cwd: string;
43
+ /** Deadline for one evaluation; 0 disables it. */
44
+ timeoutMs: number;
45
+ signal?: AbortSignal;
46
+ /** Sandbox the condition runs under (absent = unconfined). Conditions are
47
+ * the same trust class as a bash tool call, so a run that opted into the OS
48
+ * sandbox keeps it here too. */
49
+ sandbox?: BashSandbox;
50
+ }
51
+ export declare const DEFAULT_CONDITION_TIMEOUT_MS = 120000;
52
+ /** Human-readable form of the condition, for enable/status messages. */
53
+ export declare function describeLoopCondition(condition: LoopConditionConfig): string;
54
+ /** Run one evaluation and map the exit status onto a loop verdict. Never
55
+ * throws: a condition that cannot even start is an error verdict. */
56
+ export declare function evaluateLoopCondition(condition: LoopConditionConfig, options: LoopConditionOptions): Promise<LoopConditionVerdict>;
57
+ //# sourceMappingURL=condition.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"condition.d.ts","sourceRoot":"","sources":["../../../src/core/loops/condition.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAE5D;;;;;;;;;;;;6EAY6E;AAE7E,MAAM,WAAW,mBAAmB;IAClC,8CAA8C;IAC9C,OAAO,EAAE,MAAM,CAAC;IAChB;gCAC4B;IAC5B,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,MAAM,oBAAoB;AAC9B,oCAAoC;AAClC;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE;AACtB,oDAAoD;GAClD;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE;AACnC,sEAAsE;GACpE;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE;AACpC,6DAA6D;GAC3D;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAExB,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,kDAAkD;IAClD,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;qCAEiC;IACjC,OAAO,CAAC,EAAE,WAAW,CAAC;CACvB;AASD,eAAO,MAAM,4BAA4B,SAAU,CAAC;AAEpD,wEAAwE;AACxE,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,mBAAmB,GAAG,MAAM,CAE5E;AAwBD;sEACsE;AACtE,wBAAsB,qBAAqB,CACzC,SAAS,EAAE,mBAAmB,EAC9B,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,oBAAoB,CAAC,CAwE/B"}
@@ -0,0 +1,153 @@
1
+ export const DEFAULT_CONDITION_TIMEOUT_MS = 120_000;
2
+ /** Human-readable form of the condition, for enable/status messages. */
3
+ export function describeLoopCondition(condition) {
4
+ return `${condition.until ? "until" : "while"} ${quoteCommand(condition.command)} succeeds`;
5
+ }
6
+ function quoteCommand(command) {
7
+ const flat = command.replace(/\s+/g, " ").trim();
8
+ const bounded = flat.length > 60 ? `${flat.slice(0, 60)}…` : flat;
9
+ return `\`${bounded}\``;
10
+ }
11
+ /** First meaningful line of a failed condition's output, bounded. */
12
+ function previewOutput(output) {
13
+ const line = output
14
+ .split("\n")
15
+ .map((l) => l.trim())
16
+ .find((l) => l.length > 0);
17
+ if (!line)
18
+ return "";
19
+ return line.length > 60 ? `${line.slice(0, 60)}…` : line;
20
+ }
21
+ function formatTimeout(timeoutMs) {
22
+ if (timeoutMs < 60_000)
23
+ return `${Math.round(timeoutMs / 1_000)}s`;
24
+ const minutes = Math.round(timeoutMs / 60_000);
25
+ return minutes === 1 ? "1 minute" : `${minutes} minutes`;
26
+ }
27
+ /** Run one evaluation and map the exit status onto a loop verdict. Never
28
+ * throws: a condition that cannot even start is an error verdict. */
29
+ export async function evaluateLoopCondition(condition, options) {
30
+ let proc;
31
+ let timer;
32
+ let timedOut = false;
33
+ let aborted = false;
34
+ const onAbort = () => {
35
+ aborted = true;
36
+ proc?.kill();
37
+ };
38
+ try {
39
+ proc = startCondition(condition, options);
40
+ // Start draining BEFORE awaiting the exit: a condition whose output fills
41
+ // the pipe buffer would otherwise block forever on the write side.
42
+ const drained = collect(proc);
43
+ options.signal?.addEventListener("abort", onAbort, { once: true });
44
+ const exitCode = await Promise.race([
45
+ proc.exited,
46
+ options.timeoutMs > 0
47
+ ? new Promise((resolve) => {
48
+ timer = setTimeout(() => {
49
+ timedOut = true;
50
+ proc?.kill();
51
+ resolve(124); // conventional timeout status
52
+ }, options.timeoutMs);
53
+ })
54
+ : new Promise(() => { }),
55
+ ]);
56
+ // Timeout before abort: both can stop the process, and the distinction
57
+ // matters — a deadline is a broken condition, Esc is not.
58
+ if (timedOut) {
59
+ return {
60
+ kind: "error",
61
+ message: `loop condition ${quoteCommand(condition.command)} timed out after ${formatTimeout(options.timeoutMs)}`,
62
+ };
63
+ }
64
+ if (aborted)
65
+ return { kind: "aborted" };
66
+ const output = await drained;
67
+ if (exitCode === 0) {
68
+ if (!condition.until)
69
+ return { kind: "continue" };
70
+ return {
71
+ kind: "halt",
72
+ message: `loop condition ${quoteCommand(condition.command)} is now satisfied`,
73
+ };
74
+ }
75
+ if (exitCode === 1) {
76
+ if (condition.until)
77
+ return { kind: "continue" };
78
+ return {
79
+ kind: "halt",
80
+ message: `loop condition ${quoteCommand(condition.command)} no longer holds`,
81
+ };
82
+ }
83
+ // Exit >1 (or a signal death, which reports no status) is the condition
84
+ // breaking, not answering.
85
+ const preview = previewOutput(output);
86
+ const detail = preview ? `: ${preview}` : "";
87
+ const status = exitCode === null ? "no exit status" : `exit ${exitCode}`;
88
+ return {
89
+ kind: "error",
90
+ message: `loop condition ${quoteCommand(condition.command)} failed (${status})${detail}`,
91
+ };
92
+ }
93
+ catch (err) {
94
+ return {
95
+ kind: "error",
96
+ message: `loop condition ${quoteCommand(condition.command)} could not run: ${err instanceof Error ? err.message : String(err)}`,
97
+ };
98
+ }
99
+ finally {
100
+ clearTimeout(timer);
101
+ options.signal?.removeEventListener("abort", onAbort);
102
+ }
103
+ }
104
+ /** The condition runs in its OWN process, never the interactive shell: a `cd`
105
+ * inside a condition must not move the session's working directory, and
106
+ * stdin is closed so a predicate can never block on the terminal. When the
107
+ * run has an OS sandbox, the condition inherits it. */
108
+ function startCondition(condition, options) {
109
+ if (options.sandbox) {
110
+ const spawned = options.sandbox.spawn(condition.command, { cwd: options.cwd });
111
+ return {
112
+ stdout: spawned.stdout,
113
+ stderr: spawned.stderr,
114
+ exited: spawned.exited,
115
+ kill: () => spawned.kill(),
116
+ };
117
+ }
118
+ const proc = Bun.spawn(["bash", "-c", condition.command], {
119
+ stdout: "pipe",
120
+ stderr: "pipe",
121
+ stdin: "ignore",
122
+ cwd: options.cwd,
123
+ });
124
+ return {
125
+ stdout: proc.stdout,
126
+ stderr: proc.stderr,
127
+ exited: proc.exited,
128
+ kill: () => proc.kill(),
129
+ };
130
+ }
131
+ /** Merge both pipes into one bounded string. Runs concurrently with the exit
132
+ * await, so a chatty condition cannot deadlock on a full pipe. */
133
+ async function collect(proc) {
134
+ const read = async (stream) => {
135
+ const reader = stream.getReader();
136
+ const decoder = new TextDecoder();
137
+ let out = "";
138
+ for (;;) {
139
+ const { done, value } = await reader.read();
140
+ if (done)
141
+ break;
142
+ out += decoder.decode(value, { stream: true });
143
+ if (out.length > 8_000)
144
+ out = out.slice(-8_000); // bounded preview buffer
145
+ }
146
+ return out;
147
+ };
148
+ const [stdout, stderr] = await Promise.all([
149
+ read(proc.stdout).catch(() => ""),
150
+ read(proc.stderr).catch(() => ""),
151
+ ]);
152
+ return `${stdout}${stderr}`;
153
+ }
@@ -0,0 +1,9 @@
1
+ /** Loop mode: re-submit the last prompt after every settled turn, optionally
2
+ * bounded by an iteration count or a wall-clock duration. Limits + their
3
+ * parsing live in limits.ts; the session-scoped state machine in
4
+ * runtime.ts. */
5
+ export { DEFAULT_CONDITION_TIMEOUT_MS, describeLoopCondition, evaluateLoopCondition, type LoopConditionConfig, type LoopConditionOptions, type LoopConditionVerdict, } from "./condition.js";
6
+ export { consumeLoopLimitIteration, createLoopLimitRuntime, describeLoopLimit, describeLoopLimitRuntime, isLoopDurationExpired, LOOP_USAGE, type LoopLimitConfig, type LoopLimitRuntime, type ParsedLoopArgs, parseLoopArgs, } from "./limits.js";
7
+ export { RALPH_PROTOCOL, RALPH_QUEUE, ralphCondition, seedRalphQueue } from "./ralph.js";
8
+ export { type LoopAction, type LoopIterationDecision, LoopRuntime, type LoopStart, type LoopStatus, } from "./runtime.js";
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/loops/index.ts"],"names":[],"mappings":"AAAA;;;kBAGkB;AAElB,OAAO,EACL,4BAA4B,EAC5B,qBAAqB,EACrB,qBAAqB,EACrB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,GAC1B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,yBAAyB,EACzB,sBAAsB,EACtB,iBAAiB,EACjB,wBAAwB,EACxB,qBAAqB,EACrB,UAAU,EACV,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,aAAa,GACd,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACzF,OAAO,EACL,KAAK,UAAU,EACf,KAAK,qBAAqB,EAC1B,WAAW,EACX,KAAK,SAAS,EACd,KAAK,UAAU,GAChB,MAAM,cAAc,CAAC"}
@@ -0,0 +1,8 @@
1
+ /** Loop mode: re-submit the last prompt after every settled turn, optionally
2
+ * bounded by an iteration count or a wall-clock duration. Limits + their
3
+ * parsing live in limits.ts; the session-scoped state machine in
4
+ * runtime.ts. */
5
+ export { DEFAULT_CONDITION_TIMEOUT_MS, describeLoopCondition, evaluateLoopCondition, } from "./condition.js";
6
+ export { consumeLoopLimitIteration, createLoopLimitRuntime, describeLoopLimit, describeLoopLimitRuntime, isLoopDurationExpired, LOOP_USAGE, parseLoopArgs, } from "./limits.js";
7
+ export { RALPH_PROTOCOL, RALPH_QUEUE, ralphCondition, seedRalphQueue } from "./ralph.js";
8
+ export { LoopRuntime, } from "./runtime.js";
@@ -0,0 +1,43 @@
1
+ import type { LoopConditionConfig } from "./condition.js";
2
+ /** Loop-mode argument grammar and limits.
3
+ *
4
+ * `/loop [count|duration] [--while|--until <command>] [prompt]`: a leading
5
+ * token that LOOKS like a limit (starts with a digit or sign) or a FLAG
6
+ * (starts with `--`) is parsed as one, and a malformed one is a hard error;
7
+ * anything else is prose, i.e. the loop prompt — so `/loop keep going` starts
8
+ * an unbounded loop instead of erroring. */
9
+ export type LoopLimitConfig = {
10
+ kind: "iterations";
11
+ iterations: number;
12
+ } | {
13
+ kind: "duration";
14
+ durationMs: number;
15
+ };
16
+ export type LoopLimitRuntime = {
17
+ kind: "iterations";
18
+ initial: number;
19
+ remaining: number;
20
+ } | {
21
+ kind: "duration";
22
+ durationMs: number;
23
+ deadlineMs: number;
24
+ };
25
+ export declare const LOOP_USAGE = "usage: /loop [count|duration] [prompt] \u2014 examples: /loop, /loop 10, /loop 10m, /loop 1h30m keep going";
26
+ export interface ParsedLoopArgs {
27
+ /** Iteration/duration budget from the leading limit token, when present. */
28
+ limit?: LoopLimitConfig;
29
+ /** Continue-condition from `--while` / `--until`, re-evaluated before each
30
+ * iteration; absent = unconditional. */
31
+ condition?: LoopConditionConfig;
32
+ /** Inline loop prompt: text after the limit and flags, or the whole
33
+ * argument when neither was given. */
34
+ prompt?: string;
35
+ }
36
+ export declare function parseLoopArgs(args: string): ParsedLoopArgs | string;
37
+ export declare function createLoopLimitRuntime(config: LoopLimitConfig | undefined, nowMs?: number): LoopLimitRuntime | undefined;
38
+ /** Consume one iteration's budget. False = the limit is exhausted. */
39
+ export declare function consumeLoopLimitIteration(limit: LoopLimitRuntime | undefined, nowMs?: number): boolean;
40
+ export declare function isLoopDurationExpired(limit: LoopLimitRuntime | undefined, nowMs?: number): boolean;
41
+ export declare function describeLoopLimit(config: LoopLimitConfig): string;
42
+ export declare function describeLoopLimitRuntime(limit: LoopLimitRuntime): string;
43
+ //# sourceMappingURL=limits.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"limits.d.ts","sourceRoot":"","sources":["../../../src/core/loops/limits.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D;;;;;;6CAM6C;AAE7C,MAAM,MAAM,eAAe,GACvB;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7C,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAC1D;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC;AAoBjE,eAAO,MAAM,UAAU,+GACkF,CAAC;AAE1G,MAAM,WAAW,cAAc;IAC7B,4EAA4E;IAC5E,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB;6CACyC;IACzC,SAAS,CAAC,EAAE,mBAAmB,CAAC;IAChC;2CACuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,MAAM,CAcnE;AA8ID,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,eAAe,GAAG,SAAS,EACnC,KAAK,SAAa,GACjB,gBAAgB,GAAG,SAAS,CAM9B;AAED,sEAAsE;AACtE,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,gBAAgB,GAAG,SAAS,EACnC,KAAK,SAAa,GACjB,OAAO,CAMT;AAED,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,gBAAgB,GAAG,SAAS,EACnC,KAAK,SAAa,GACjB,OAAO,CAET;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,CAKjE;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,CAKxE"}
@@ -0,0 +1,219 @@
1
+ const TIME_UNITS_MS = new Map([
2
+ ["s", 1_000],
3
+ ["sec", 1_000],
4
+ ["secs", 1_000],
5
+ ["second", 1_000],
6
+ ["seconds", 1_000],
7
+ ["m", 60_000],
8
+ ["min", 60_000],
9
+ ["mins", 60_000],
10
+ ["minute", 60_000],
11
+ ["minutes", 60_000],
12
+ ["h", 3_600_000],
13
+ ["hr", 3_600_000],
14
+ ["hrs", 3_600_000],
15
+ ["hour", 3_600_000],
16
+ ["hours", 3_600_000],
17
+ ]);
18
+ export const LOOP_USAGE = "usage: /loop [count|duration] [prompt] — examples: /loop, /loop 10, /loop 10m, /loop 1h30m keep going";
19
+ export function parseLoopArgs(args) {
20
+ const trimmed = args.trim();
21
+ if (!trimmed)
22
+ return {};
23
+ const limitResult = takeLoopLimit(trimmed);
24
+ if (typeof limitResult === "string")
25
+ return limitResult;
26
+ const conditionResult = takeLoopCondition(limitResult.rest);
27
+ if (typeof conditionResult === "string")
28
+ return conditionResult;
29
+ const parsed = {};
30
+ if (limitResult.limit)
31
+ parsed.limit = limitResult.limit;
32
+ if (conditionResult.condition)
33
+ parsed.condition = conditionResult.condition;
34
+ if (conditionResult.rest)
35
+ parsed.prompt = conditionResult.rest;
36
+ return parsed;
37
+ }
38
+ /** Split an optional leading limit token off the argument string. */
39
+ function takeLoopLimit(input) {
40
+ const firstSpace = input.search(/\s/);
41
+ const firstToken = firstSpace === -1 ? input : input.slice(0, firstSpace);
42
+ const rest = firstSpace === -1 ? "" : input.slice(firstSpace + 1).trim();
43
+ const token = firstToken.toLowerCase();
44
+ // Not a limit attempt (prose, or a leading condition flag).
45
+ if (!/^[+-]?\d/.test(token))
46
+ return { rest: input };
47
+ // Bare integer: iteration count, unless the next token is a time unit
48
+ // ("10 minutes").
49
+ if (/^\d+$/.test(token)) {
50
+ if (rest) {
51
+ const unitToken = /^\S+/.exec(rest)?.[0] ?? "";
52
+ const unitMs = TIME_UNITS_MS.get(unitToken.toLowerCase());
53
+ if (unitMs !== undefined) {
54
+ const limit = makeDuration(token, unitMs);
55
+ if (typeof limit === "string")
56
+ return limit;
57
+ return { limit, rest: rest.slice(unitToken.length).trim() };
58
+ }
59
+ }
60
+ const limit = makeIterations(token);
61
+ if (typeof limit === "string")
62
+ return limit;
63
+ return { limit, rest };
64
+ }
65
+ // Compact / compound duration: "10m", "90s", "1h30m".
66
+ const duration = parseCompoundDuration(token);
67
+ if (duration !== undefined) {
68
+ if (typeof duration === "string")
69
+ return duration;
70
+ return { limit: duration, rest };
71
+ }
72
+ // Limit-shaped but unparseable ("-1", "1.5h", "10x10").
73
+ return LOOP_USAGE;
74
+ }
75
+ /** Split an optional leading `--while` / `--until` flag off the argument
76
+ * string. A flag-shaped token that does not parse is a hard error, so a typo
77
+ * surfaces instead of silently becoming prompt text. */
78
+ function takeLoopCondition(input) {
79
+ let rest = input.trim();
80
+ let condition;
81
+ while (rest.startsWith("--")) {
82
+ const name = /^(--[a-z][a-z-]*)(?=[\s=]|$)/.exec(rest)?.[1];
83
+ const until = name === undefined ? undefined : CONDITION_FLAGS[name];
84
+ if (name === undefined || until === undefined) {
85
+ return `unknown /loop flag ${name ?? rest.split(/\s+/, 1)[0]}. ${LOOP_USAGE}`;
86
+ }
87
+ if (condition)
88
+ return "use only one of --while or --until";
89
+ const afterName = rest.slice(name.length);
90
+ const valueText = afterName.startsWith("=") ? afterName.slice(1) : afterName;
91
+ const value = readShellWord(valueText);
92
+ if (value === "unterminated")
93
+ return `${name} has an unterminated quote`;
94
+ if (value === undefined || !value.value.trim() || valueText.trim().startsWith("-")) {
95
+ return `${name} needs a shell command. Quote it when it contains spaces: /loop ${name} 'bun test'`;
96
+ }
97
+ condition = { command: value.value.trim(), until };
98
+ rest = value.rest;
99
+ }
100
+ return { condition, rest };
101
+ }
102
+ /** `--until` stops when the command SUCCEEDS; `--while` stops when it fails. */
103
+ const CONDITION_FLAGS = { "--until": true, "--while": false };
104
+ /** Read one shell word, honoring single/double quotes (the condition is a
105
+ * command line, so it usually needs quoting). Returns "unterminated" for an
106
+ * unbalanced quote rather than guessing. */
107
+ function readShellWord(input) {
108
+ const trimmed = input.replace(/^\s+/, "");
109
+ if (trimmed === "")
110
+ return undefined;
111
+ let out = "";
112
+ let quote;
113
+ let i = 0;
114
+ for (; i < trimmed.length; i++) {
115
+ const ch = trimmed[i];
116
+ if (quote) {
117
+ if (ch === quote) {
118
+ quote = undefined;
119
+ continue;
120
+ }
121
+ out += ch;
122
+ continue;
123
+ }
124
+ if (ch === '"' || ch === "'") {
125
+ quote = ch;
126
+ continue;
127
+ }
128
+ if (/\s/.test(ch))
129
+ break;
130
+ out += ch;
131
+ }
132
+ if (quote)
133
+ return "unterminated";
134
+ return { value: out, rest: trimmed.slice(i).trim() };
135
+ }
136
+ function makeIterations(amountText) {
137
+ const amount = Number(amountText);
138
+ if (!Number.isSafeInteger(amount) || amount <= 0) {
139
+ return "loop count must be a positive integer";
140
+ }
141
+ return { kind: "iterations", iterations: amount };
142
+ }
143
+ function makeDuration(amountText, unitMs) {
144
+ const amount = Number(amountText);
145
+ if (!Number.isSafeInteger(amount) || amount <= 0)
146
+ return "loop duration must be positive";
147
+ return { kind: "duration", durationMs: amount * unitMs };
148
+ }
149
+ /** `10m`, `90s`, `1h30m`. Undefined when the token is not duration-shaped; an
150
+ * error string when it is shaped like one but uses an unknown unit. */
151
+ function parseCompoundDuration(token) {
152
+ if (!/^(?:\d+[a-z]+)+$/.test(token))
153
+ return undefined;
154
+ const segments = token.match(/\d+[a-z]+/g);
155
+ if (!segments)
156
+ return undefined;
157
+ let totalMs = 0;
158
+ for (const segment of segments) {
159
+ const match = /^(\d+)([a-z]+)$/.exec(segment);
160
+ if (!match)
161
+ return LOOP_USAGE;
162
+ const unitMs = TIME_UNITS_MS.get(match[2]);
163
+ if (unitMs === undefined) {
164
+ return "loop duration unit must be seconds, minutes, or hours";
165
+ }
166
+ const amount = Number(match[1]);
167
+ if (!Number.isSafeInteger(amount) || amount <= 0)
168
+ return "loop duration must be positive";
169
+ totalMs += amount * unitMs;
170
+ }
171
+ if (totalMs <= 0)
172
+ return "loop duration must be positive";
173
+ return { kind: "duration", durationMs: totalMs };
174
+ }
175
+ export function createLoopLimitRuntime(config, nowMs = Date.now()) {
176
+ if (!config)
177
+ return undefined;
178
+ if (config.kind === "iterations") {
179
+ return { kind: "iterations", initial: config.iterations, remaining: config.iterations };
180
+ }
181
+ return { kind: "duration", durationMs: config.durationMs, deadlineMs: nowMs + config.durationMs };
182
+ }
183
+ /** Consume one iteration's budget. False = the limit is exhausted. */
184
+ export function consumeLoopLimitIteration(limit, nowMs = Date.now()) {
185
+ if (!limit)
186
+ return true;
187
+ if (limit.kind === "duration")
188
+ return nowMs < limit.deadlineMs;
189
+ if (limit.remaining <= 0)
190
+ return false;
191
+ limit.remaining -= 1;
192
+ return true;
193
+ }
194
+ export function isLoopDurationExpired(limit, nowMs = Date.now()) {
195
+ return limit?.kind === "duration" && nowMs >= limit.deadlineMs;
196
+ }
197
+ export function describeLoopLimit(config) {
198
+ if (config.kind === "iterations") {
199
+ return `${config.iterations} iteration${config.iterations === 1 ? "" : "s"}`;
200
+ }
201
+ return formatDuration(config.durationMs);
202
+ }
203
+ export function describeLoopLimitRuntime(limit) {
204
+ if (limit.kind === "iterations") {
205
+ return `${limit.remaining}/${limit.initial} iterations left`;
206
+ }
207
+ return `${formatDuration(Math.max(0, limit.deadlineMs - Date.now()))} left`;
208
+ }
209
+ function formatDuration(ms) {
210
+ if (ms < 60_000)
211
+ return `${Math.round(ms / 1_000)}s`;
212
+ const minutes = Math.floor(ms / 60_000);
213
+ const seconds = Math.round((ms % 60_000) / 1_000);
214
+ if (minutes < 60)
215
+ return seconds > 0 ? `${minutes}m${seconds}s` : `${minutes}m`;
216
+ const hours = Math.floor(minutes / 60);
217
+ const rest = minutes % 60;
218
+ return rest > 0 ? `${hours}h${rest}m` : `${hours}h`;
219
+ }
@@ -0,0 +1,37 @@
1
+ import type { LoopConditionConfig } from "./condition.js";
2
+ /** The build loop ("ralph"): one work item per iteration, each iteration in a
3
+ * fresh session, with the queue and the repo as the only memory between them.
4
+ *
5
+ * The mechanism is loop mode's (`reset` already restarts the session); what
6
+ * this file adds is the discipline that makes the restart survivable — a
7
+ * queue on disk, a per-iteration protocol, and the stop condition derived
8
+ * from that queue. */
9
+ /** The work queue, relative to the workspace root. Project-scoped, NOT the
10
+ * session-keyed plan drafts: every iteration runs in a fresh session, so a
11
+ * session-keyed path would hand each iteration a different (empty) file and
12
+ * the loop would lose its place — which is the entire point of the queue. */
13
+ export declare const RALPH_QUEUE = ".oma/plan.md";
14
+ /** Continue while an unchecked item remains. The condition runs with the
15
+ * workspace root as its cwd; a missing queue exits 2, which the evaluator
16
+ * reports as a broken condition rather than as "finished" — deleting the
17
+ * queue mid-loop must not read as success. */
18
+ export declare const RALPH_QUEUE_CHECK = "grep -q \"^- \\[ \\]\" .oma/plan.md";
19
+ export declare function ralphQueuePath(workspaceRoot: string): string;
20
+ /** The default stop condition: the queue decides, not the model's opinion of
21
+ * its own progress. Overridable — `/loop --until 'bun test'` swaps the
22
+ * authority to the verifier. */
23
+ export declare function ralphCondition(): LoopConditionConfig;
24
+ /** One iteration's instructions. The queue is the plan; the model's job is to
25
+ * shrink it by exactly one item and leave the repo in a state it can be
26
+ * resumed from, because the next iteration remembers nothing else. */
27
+ export declare const RALPH_PROTOCOL = "You are one iteration of an autonomous build loop. You start with a fresh\ncontext every iteration: the repository and .oma/plan.md are the only memory.\nAnother iteration follows you, and it will not see this conversation.\n\n1. Read .oma/plan.md. It is a work queue: `- [ ]` is todo, `- [x]` is done.\n2. Pick the SINGLE most important unchecked item.\n3. Do only that item. Implement it properly; do not start a second one.\n4. Verify with the project's own checks (tests, typecheck, lint). Fix failures\n before you finish. Never mark an item done while its verification fails.\n5. Mark it `- [x]` in .oma/plan.md. If the work revealed new work, append it\n as a new `- [ ]` item instead of doing it now \u2014 that is how the queue stays\n honest.\n6. Commit everything you changed: one commit, message says what the item was.\n The queue goes in that commit when the project tracks it; if it is ignored,\n leave it alone rather than forcing it in.\n7. Stop. Do not summarise, do not ask whether to continue.\n\nIf every item is already checked, change nothing and say the queue is empty.";
28
+ /** Create the queue on first use. An existing queue is never touched — it is
29
+ * the loop's memory, and rewriting it would discard the loop's progress.
30
+ * `item` seeds a single concrete item instead of the "write the queue" stub,
31
+ * which turns iteration one into the first build step rather than a planning
32
+ * step. */
33
+ export declare function seedRalphQueue(workspaceRoot: string, item?: string): {
34
+ path: string;
35
+ created: boolean;
36
+ };
37
+ //# sourceMappingURL=ralph.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ralph.d.ts","sourceRoot":"","sources":["../../../src/core/loops/ralph.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D;;;;;;uBAMuB;AAEvB;;;8EAG8E;AAC9E,eAAO,MAAM,WAAW,iBAAiB,CAAC;AAE1C;;;+CAG+C;AAC/C,eAAO,MAAM,iBAAiB,wCAAwC,CAAC;AAEvE,wBAAgB,cAAc,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED;;iCAEiC;AACjC,wBAAgB,cAAc,IAAI,mBAAmB,CAEpD;AAED;;uEAEuE;AACvE,eAAO,MAAM,cAAc,olCAiBkD,CAAC;AAW9E;;;;YAIY;AACZ,wBAAgB,cAAc,CAC5B,aAAa,EAAE,MAAM,EACrB,IAAI,CAAC,EAAE,MAAM,GACZ;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAQpC"}
@@ -0,0 +1,72 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ /** The build loop ("ralph"): one work item per iteration, each iteration in a
4
+ * fresh session, with the queue and the repo as the only memory between them.
5
+ *
6
+ * The mechanism is loop mode's (`reset` already restarts the session); what
7
+ * this file adds is the discipline that makes the restart survivable — a
8
+ * queue on disk, a per-iteration protocol, and the stop condition derived
9
+ * from that queue. */
10
+ /** The work queue, relative to the workspace root. Project-scoped, NOT the
11
+ * session-keyed plan drafts: every iteration runs in a fresh session, so a
12
+ * session-keyed path would hand each iteration a different (empty) file and
13
+ * the loop would lose its place — which is the entire point of the queue. */
14
+ export const RALPH_QUEUE = ".oma/plan.md";
15
+ /** Continue while an unchecked item remains. The condition runs with the
16
+ * workspace root as its cwd; a missing queue exits 2, which the evaluator
17
+ * reports as a broken condition rather than as "finished" — deleting the
18
+ * queue mid-loop must not read as success. */
19
+ export const RALPH_QUEUE_CHECK = `grep -q "^- \\[ \\]" ${RALPH_QUEUE}`;
20
+ export function ralphQueuePath(workspaceRoot) {
21
+ return join(workspaceRoot, RALPH_QUEUE);
22
+ }
23
+ /** The default stop condition: the queue decides, not the model's opinion of
24
+ * its own progress. Overridable — `/loop --until 'bun test'` swaps the
25
+ * authority to the verifier. */
26
+ export function ralphCondition() {
27
+ return { command: RALPH_QUEUE_CHECK, until: false };
28
+ }
29
+ /** One iteration's instructions. The queue is the plan; the model's job is to
30
+ * shrink it by exactly one item and leave the repo in a state it can be
31
+ * resumed from, because the next iteration remembers nothing else. */
32
+ export const RALPH_PROTOCOL = `You are one iteration of an autonomous build loop. You start with a fresh
33
+ context every iteration: the repository and ${RALPH_QUEUE} are the only memory.
34
+ Another iteration follows you, and it will not see this conversation.
35
+
36
+ 1. Read ${RALPH_QUEUE}. It is a work queue: \`- [ ]\` is todo, \`- [x]\` is done.
37
+ 2. Pick the SINGLE most important unchecked item.
38
+ 3. Do only that item. Implement it properly; do not start a second one.
39
+ 4. Verify with the project's own checks (tests, typecheck, lint). Fix failures
40
+ before you finish. Never mark an item done while its verification fails.
41
+ 5. Mark it \`- [x]\` in ${RALPH_QUEUE}. If the work revealed new work, append it
42
+ as a new \`- [ ]\` item instead of doing it now — that is how the queue stays
43
+ honest.
44
+ 6. Commit everything you changed: one commit, message says what the item was.
45
+ The queue goes in that commit when the project tracks it; if it is ignored,
46
+ leave it alone rather than forcing it in.
47
+ 7. Stop. Do not summarise, do not ask whether to continue.
48
+
49
+ If every item is already checked, change nothing and say the queue is empty.`;
50
+ const RALPH_STUB = `# Work queue
51
+
52
+ \`- [ ]\` is todo, \`- [x]\` is done — one item per line, most important first.
53
+ An iteration does exactly one item and then stops.
54
+
55
+ - [ ] Study the repository and the request, then replace this file with a
56
+ prioritised queue of concrete, independently verifiable work items.
57
+ `;
58
+ /** Create the queue on first use. An existing queue is never touched — it is
59
+ * the loop's memory, and rewriting it would discard the loop's progress.
60
+ * `item` seeds a single concrete item instead of the "write the queue" stub,
61
+ * which turns iteration one into the first build step rather than a planning
62
+ * step. */
63
+ export function seedRalphQueue(workspaceRoot, item) {
64
+ const path = ralphQueuePath(workspaceRoot);
65
+ if (existsSync(path))
66
+ return { path, created: false };
67
+ const seeded = item?.trim();
68
+ const body = seeded ? `# Work queue\n\n- [ ] ${seeded}\n` : RALPH_STUB;
69
+ mkdirSync(join(workspaceRoot, ".oma"), { recursive: true });
70
+ writeFileSync(path, body, "utf-8");
71
+ return { path, created: true };
72
+ }