@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.
- package/README.md +11 -6
- package/dist/cli/args.d.ts +1 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +9 -2
- package/dist/cli/gateway-commands.d.ts.map +1 -1
- package/dist/cli/gateway-commands.js +35 -5
- package/dist/cli/initial-input.d.ts +3 -2
- package/dist/cli/initial-input.d.ts.map +1 -1
- package/dist/core/coordination/registry.d.ts +11 -0
- package/dist/core/coordination/registry.d.ts.map +1 -1
- package/dist/core/coordination/registry.js +23 -0
- package/dist/core/gateway/artifact.d.ts +0 -1
- package/dist/core/gateway/artifact.d.ts.map +1 -1
- package/dist/core/gateway/artifact.js +1 -1
- package/dist/core/goals/index.d.ts +8 -0
- package/dist/core/goals/index.d.ts.map +1 -0
- package/dist/core/goals/index.js +7 -0
- package/dist/core/goals/runtime.d.ts +112 -0
- package/dist/core/goals/runtime.d.ts.map +1 -0
- package/dist/core/goals/runtime.js +241 -0
- package/dist/core/goals/state.d.ts +82 -0
- package/dist/core/goals/state.d.ts.map +1 -0
- package/dist/core/goals/state.js +225 -0
- package/dist/core/goals/tool.d.ts +10 -0
- package/dist/core/goals/tool.d.ts.map +1 -0
- package/dist/core/goals/tool.js +87 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +4 -3
- package/dist/core/loops/condition.d.ts +57 -0
- package/dist/core/loops/condition.d.ts.map +1 -0
- package/dist/core/loops/condition.js +153 -0
- package/dist/core/loops/index.d.ts +9 -0
- package/dist/core/loops/index.d.ts.map +1 -0
- package/dist/core/loops/index.js +8 -0
- package/dist/core/loops/limits.d.ts +43 -0
- package/dist/core/loops/limits.d.ts.map +1 -0
- package/dist/core/loops/limits.js +219 -0
- package/dist/core/loops/ralph.d.ts +37 -0
- package/dist/core/loops/ralph.d.ts.map +1 -0
- package/dist/core/loops/ralph.js +72 -0
- package/dist/core/loops/runtime.d.ts +95 -0
- package/dist/core/loops/runtime.d.ts.map +1 -0
- package/dist/core/loops/runtime.js +222 -0
- package/dist/core/memory/managed-skills.d.ts +0 -11
- package/dist/core/memory/managed-skills.d.ts.map +1 -1
- package/dist/core/memory/managed-skills.js +2 -2
- package/dist/core/memory/vector-store.d.ts +0 -1
- package/dist/core/memory/vector-store.d.ts.map +1 -1
- package/dist/core/memory/vector-store.js +0 -3
- package/dist/core/plans/index.d.ts +7 -0
- package/dist/core/plans/index.d.ts.map +1 -0
- package/dist/core/plans/index.js +6 -0
- package/dist/core/plans/prompts.d.ts +20 -0
- package/dist/core/plans/prompts.d.ts.map +1 -0
- package/dist/core/plans/prompts.js +72 -0
- package/dist/core/plans/runtime.d.ts +89 -0
- package/dist/core/plans/runtime.d.ts.map +1 -0
- package/dist/core/plans/runtime.js +170 -0
- package/dist/core/plans/state.d.ts +55 -0
- package/dist/core/plans/state.d.ts.map +1 -0
- package/dist/core/plans/state.js +88 -0
- package/dist/core/runtime/agent-loop-messages.d.ts +6 -0
- package/dist/core/runtime/agent-loop-messages.d.ts.map +1 -1
- package/dist/core/runtime/agent-loop-messages.js +40 -15
- package/dist/core/runtime/agent-loop-run.d.ts.map +1 -1
- package/dist/core/runtime/agent-loop-run.js +18 -27
- package/dist/core/runtime/agent-loop-runner.d.ts +1 -23
- package/dist/core/runtime/agent-loop-runner.d.ts.map +1 -1
- package/dist/core/runtime/agent-loop-runner.js +84 -30
- package/dist/core/runtime/agent-loop-types.d.ts +4 -0
- package/dist/core/runtime/agent-loop-types.d.ts.map +1 -1
- package/dist/core/runtime/compaction.d.ts +9 -0
- package/dist/core/runtime/compaction.d.ts.map +1 -1
- package/dist/core/runtime/compaction.js +28 -0
- package/dist/core/runtime/context-estimate.d.ts +37 -4
- package/dist/core/runtime/context-estimate.d.ts.map +1 -1
- package/dist/core/runtime/context-estimate.js +70 -0
- package/dist/core/runtime/create-runtime.d.ts +27 -5
- package/dist/core/runtime/create-runtime.d.ts.map +1 -1
- package/dist/core/runtime/create-runtime.js +22 -2
- package/dist/core/runtime/permission-classifier.d.ts.map +1 -1
- package/dist/core/runtime/permission-classifier.js +83 -12
- package/dist/core/runtime/plugin.d.ts +7 -0
- package/dist/core/runtime/plugin.d.ts.map +1 -1
- package/dist/core/runtime/plugin.js +13 -0
- package/dist/core/runtime/process-tree.d.ts +0 -9
- package/dist/core/runtime/process-tree.d.ts.map +1 -1
- package/dist/core/runtime/process-tree.js +1 -1
- package/dist/core/runtime/run-runtime.d.ts.map +1 -1
- package/dist/core/runtime/run-runtime.js +88 -49
- package/dist/core/runtime/runtime-catalog.d.ts +0 -4
- package/dist/core/runtime/runtime-catalog.d.ts.map +1 -1
- package/dist/core/runtime/runtime-catalog.js +1 -1
- package/dist/core/runtime/stream-rules.d.ts +9 -2
- package/dist/core/runtime/stream-rules.d.ts.map +1 -1
- package/dist/core/runtime/stream-rules.js +19 -5
- package/dist/core/runtime/title.d.ts +13 -1
- package/dist/core/runtime/title.d.ts.map +1 -1
- package/dist/core/runtime/title.js +51 -17
- package/dist/core/runtime/tool-pruning.d.ts.map +1 -1
- package/dist/core/runtime/tool-pruning.js +8 -16
- package/dist/core/session/session-file.d.ts +91 -5
- package/dist/core/session/session-file.d.ts.map +1 -1
- package/dist/core/session/session-file.js +206 -24
- package/dist/core/session/session-loop.d.ts +3 -1
- package/dist/core/session/session-loop.d.ts.map +1 -1
- package/dist/core/session/session-loop.js +8 -4
- package/dist/core/settings/project-settings.d.ts +63 -9
- package/dist/core/settings/project-settings.d.ts.map +1 -1
- package/dist/core/settings/project-settings.js +43 -2
- package/dist/core/settings/workspace-context.js +4 -4
- package/dist/core/store/in-memory-session-store.d.ts.map +1 -1
- package/dist/core/store/in-memory-session-store.js +3 -2
- package/dist/core/store/session-store.contract.test-helper.d.ts +7 -5
- package/dist/core/store/session-store.contract.test-helper.d.ts.map +1 -1
- package/dist/core/store/session-store.contract.test-helper.js +7 -5
- package/dist/core/store/session-store.d.ts +4 -2
- package/dist/core/store/session-store.d.ts.map +1 -1
- package/dist/core/store/session-store.js +4 -2
- package/dist/core/tools/ask-question.d.ts.map +1 -1
- package/dist/core/tools/ask-question.js +51 -5
- package/dist/core/tools/bash-pty.d.ts +13 -4
- package/dist/core/tools/bash-pty.d.ts.map +1 -1
- package/dist/core/tools/bash-pty.js +15 -8
- package/dist/core/tools/bash-sandbox.d.ts +1 -1
- package/dist/core/tools/bash.d.ts +1 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +18 -8
- package/dist/core/tools/browser.d.ts +54 -0
- package/dist/core/tools/browser.d.ts.map +1 -1
- package/dist/core/tools/browser.js +138 -1
- package/dist/core/tools/file-fingerprint.d.ts +24 -0
- package/dist/core/tools/file-fingerprint.d.ts.map +1 -0
- package/dist/core/tools/file-fingerprint.js +61 -0
- package/dist/core/tools/file-tools.d.ts +26 -0
- package/dist/core/tools/file-tools.d.ts.map +1 -1
- package/dist/core/tools/file-tools.js +113 -17
- package/dist/core/tools/index.d.ts +2 -0
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +1 -0
- package/dist/core/tools/mcp-mount.d.ts +1 -1
- package/dist/core/tools/mcp-mount.d.ts.map +1 -1
- package/dist/core/tools/mcp-mount.js +72 -26
- package/dist/core/tools/url-guard.d.ts +15 -0
- package/dist/core/tools/url-guard.d.ts.map +1 -1
- package/dist/core/tools/url-guard.js +54 -14
- package/dist/core/tools/web-ports-std.d.ts.map +1 -1
- package/dist/core/tools/web-ports-std.js +5 -2
- package/dist/modes/print-mode.d.ts +3 -2
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +2 -2
- package/dist/modes/shared.d.ts +12 -0
- package/dist/modes/shared.d.ts.map +1 -1
- package/dist/modes/shared.js +23 -6
- package/dist/modes/tui/agent-status.d.ts +20 -0
- package/dist/modes/tui/agent-status.d.ts.map +1 -0
- package/dist/modes/tui/agent-status.js +35 -0
- package/dist/modes/tui/ask-panel.d.ts +166 -0
- package/dist/modes/tui/ask-panel.d.ts.map +1 -0
- package/dist/modes/tui/ask-panel.js +888 -0
- package/dist/modes/tui/pty-console.d.ts +3 -0
- package/dist/modes/tui/pty-console.d.ts.map +1 -1
- package/dist/modes/tui/pty-console.js +19 -0
- package/dist/modes/tui/settings-overlay.d.ts +19 -0
- package/dist/modes/tui/settings-overlay.d.ts.map +1 -1
- package/dist/modes/tui/settings-overlay.js +46 -10
- package/dist/modes/tui/tui-commands-session.d.ts.map +1 -1
- package/dist/modes/tui/tui-commands-session.js +20 -6
- package/dist/modes/tui/tui-commands.d.ts +44 -3
- package/dist/modes/tui/tui-commands.d.ts.map +1 -1
- package/dist/modes/tui/tui-commands.js +380 -6
- package/dist/modes/tui/tui-components.d.ts +10 -0
- package/dist/modes/tui/tui-components.d.ts.map +1 -1
- package/dist/modes/tui/tui-components.js +26 -0
- package/dist/modes/tui/tui-format.d.ts +53 -4
- package/dist/modes/tui/tui-format.d.ts.map +1 -1
- package/dist/modes/tui/tui-format.js +137 -42
- package/dist/modes/tui/tui-frame-provider.d.ts +11 -3
- package/dist/modes/tui/tui-frame-provider.d.ts.map +1 -1
- package/dist/modes/tui/tui-frame-provider.js +14 -5
- package/dist/modes/tui/tui-interactive.d.ts.map +1 -1
- package/dist/modes/tui/tui-interactive.js +14 -0
- package/dist/modes/tui/tui-io.d.ts.map +1 -1
- package/dist/modes/tui/tui-io.js +150 -55
- package/dist/modes/tui/tui-mode.d.ts.map +1 -1
- package/dist/modes/tui/tui-mode.js +428 -23
- package/dist/modes/tui/tui-overlays.d.ts +6 -10
- package/dist/modes/tui/tui-overlays.d.ts.map +1 -1
- package/dist/modes/tui/tui-overlays.js +0 -43
- package/dist/modes/tui/tui-render.d.ts +10 -0
- package/dist/modes/tui/tui-render.d.ts.map +1 -1
- package/dist/modes/tui/tui-render.js +73 -38
- package/dist/modes/tui/tui-seam.d.ts +28 -6
- package/dist/modes/tui/tui-seam.d.ts.map +1 -1
- package/dist/modes/tui/tui-slash.d.ts.map +1 -1
- package/dist/modes/tui/tui-slash.js +3 -1
- package/dist/modes/tui/tui-tool-render.d.ts +14 -1
- package/dist/modes/tui/tui-tool-render.d.ts.map +1 -1
- package/dist/modes/tui/tui-tool-render.js +158 -50
- package/dist/modes/tui/tui-transcript-reconciler.d.ts +4 -0
- package/dist/modes/tui/tui-transcript-reconciler.d.ts.map +1 -1
- package/dist/modes/tui/tui-transcript-reconciler.js +12 -2
- package/dist/modes/tui/view-state.d.ts.map +1 -1
- package/dist/modes/tui/view-state.js +4 -1
- package/dist/protocol/transport.d.ts +10 -0
- package/dist/protocol/transport.d.ts.map +1 -1
- package/dist/protocol/transport.js +1 -0
- package/package.json +11 -11
package/dist/core/index.js
CHANGED
|
@@ -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:
|
|
9
|
-
//
|
|
10
|
-
//
|
|
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
|
+
}
|