@lnilluv/pi-ralph-loop 0.1.3 → 0.1.4-dev.1

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 (46) hide show
  1. package/.github/workflows/ci.yml +5 -2
  2. package/.github/workflows/release.yml +7 -4
  3. package/README.md +151 -15
  4. package/package.json +13 -4
  5. package/src/index.ts +1419 -176
  6. package/src/ralph-draft-context.ts +618 -0
  7. package/src/ralph-draft-llm.ts +297 -0
  8. package/src/ralph-draft.ts +33 -0
  9. package/src/ralph.ts +1457 -0
  10. package/src/runner-rpc.ts +434 -0
  11. package/src/runner-state.ts +822 -0
  12. package/src/runner.ts +957 -0
  13. package/src/secret-paths.ts +66 -0
  14. package/src/shims.d.ts +23 -0
  15. package/tests/fixtures/parity/migrate/OPEN_QUESTIONS.md +3 -0
  16. package/tests/fixtures/parity/migrate/RALPH.md +27 -0
  17. package/tests/fixtures/parity/migrate/golden/MIGRATED.md +15 -0
  18. package/tests/fixtures/parity/migrate/legacy/source.md +6 -0
  19. package/tests/fixtures/parity/migrate/legacy/source.yaml +3 -0
  20. package/tests/fixtures/parity/migrate/scripts/show-legacy.sh +10 -0
  21. package/tests/fixtures/parity/migrate/scripts/verify.sh +15 -0
  22. package/tests/fixtures/parity/research/OPEN_QUESTIONS.md +3 -0
  23. package/tests/fixtures/parity/research/RALPH.md +45 -0
  24. package/tests/fixtures/parity/research/claim-evidence-checklist.md +15 -0
  25. package/tests/fixtures/parity/research/expected-outputs.md +22 -0
  26. package/tests/fixtures/parity/research/scripts/show-snapshots.sh +13 -0
  27. package/tests/fixtures/parity/research/scripts/verify.sh +55 -0
  28. package/tests/fixtures/parity/research/snapshots/app-factory-ai-cli.md +11 -0
  29. package/tests/fixtures/parity/research/snapshots/docs-factory-ai-cli-features-missions.md +11 -0
  30. package/tests/fixtures/parity/research/snapshots/factory-ai-news-missions.md +11 -0
  31. package/tests/fixtures/parity/research/source-manifest.md +20 -0
  32. package/tests/index.test.ts +3529 -0
  33. package/tests/parity/README.md +9 -0
  34. package/tests/parity/harness.py +526 -0
  35. package/tests/parity-harness.test.ts +42 -0
  36. package/tests/parity-research-fixture.test.ts +34 -0
  37. package/tests/ralph-draft-context.test.ts +672 -0
  38. package/tests/ralph-draft-llm.test.ts +434 -0
  39. package/tests/ralph-draft.test.ts +168 -0
  40. package/tests/ralph.test.ts +1840 -0
  41. package/tests/runner-event-contract.test.ts +235 -0
  42. package/tests/runner-rpc.test.ts +358 -0
  43. package/tests/runner-state.test.ts +553 -0
  44. package/tests/runner.test.ts +1347 -0
  45. package/tests/secret-paths.test.ts +55 -0
  46. package/tsconfig.json +3 -2
@@ -17,13 +17,16 @@ jobs:
17
17
  - name: Checkout
18
18
  uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
19
19
 
20
- - name: Setup Node.js 22
20
+ - name: Setup Node.js 22.22.1
21
21
  uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af
22
22
  with:
23
- node-version: 22
23
+ node-version: 22.22.1
24
24
 
25
25
  - name: Install dependencies
26
26
  run: npm ci --ignore-scripts
27
27
 
28
+ - name: Test
29
+ run: npm test
30
+
28
31
  - name: Typecheck
29
32
  run: npm run typecheck
@@ -24,15 +24,18 @@ jobs:
24
24
  with:
25
25
  fetch-depth: 0
26
26
 
27
- - name: Setup Node.js 22
27
+ - name: Setup Node.js 24.14.1
28
28
  uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af
29
29
  with:
30
- node-version: 24
30
+ node-version: 24.14.1
31
31
  registry-url: https://registry.npmjs.org
32
32
 
33
33
  - name: Install dependencies
34
34
  run: npm ci --ignore-scripts
35
35
 
36
+ - name: Test
37
+ run: npm test
38
+
36
39
  - name: Typecheck
37
40
  run: npm run typecheck
38
41
 
@@ -119,11 +122,11 @@ jobs:
119
122
 
120
123
  - name: Publish (main)
121
124
  if: steps.bump.outputs.should_release == 'true' && github.ref_name == 'main'
122
- run: npx npm@11 publish --access public --provenance
125
+ run: npx -y npm@11.12.1 publish --access public --provenance
123
126
 
124
127
  - name: Publish (dev prerelease)
125
128
  if: steps.bump.outputs.should_release == 'true' && github.ref_name == 'dev'
126
- run: npx npm@11 publish --access public --provenance --tag dev
129
+ run: npx -y npm@11.12.1 publish --access public --provenance --tag dev
127
130
 
128
131
  - name: Commit version bump and tag
129
132
  if: steps.bump.outputs.should_release == 'true'
package/README.md CHANGED
@@ -10,12 +10,14 @@ pi install npm:@lnilluv/pi-ralph-loop
10
10
 
11
11
  ## Quick start
12
12
 
13
+ ### Run an existing task folder
14
+
13
15
  ```md
14
16
  # my-task/RALPH.md
15
17
  ---
16
18
  commands:
17
19
  - name: tests
18
- run: npm test -- --runInBand
20
+ run: npm test
19
21
  timeout: 60
20
22
  ---
21
23
  Fix failing tests using this output:
@@ -23,24 +25,115 @@ Fix failing tests using this output:
23
25
  {{ commands.tests }}
24
26
  ```
25
27
 
26
- Run `/ralph my-task` in pi.
28
+ Run:
29
+
30
+ ```text
31
+ /ralph my-task
32
+ ```
33
+
34
+ ### Draft a loop from natural language
35
+
36
+ ```text
37
+ /ralph reverse engineer this app
38
+ ```
39
+
40
+ pi drafts `./reverse-engineer-this-app/RALPH.md`, shows a short Mission Brief, lets you edit the file, and only starts after you confirm.
41
+
42
+ ### Draft without starting
43
+
44
+ ```text
45
+ /ralph-draft fix flaky auth tests
46
+ ```
47
+
48
+ That saves the draft but does not launch the loop.
49
+
50
+ ### Smart drafting
51
+
52
+ Smart drafting sends the selected repo excerpts from the current repo context to the currently selected active pi model when you start `/ralph` interactively, including models chosen with `/model` or by cycling within `/scoped-models`. It excludes common secret-bearing paths from that context, and non-analysis drafts use the shared `policy:secret-bearing-paths` token so runtime write protection stays aligned with the same policy. It does not switch models automatically. When the active model is used to strengthen an existing draft, it now accepts validated body-and-commands drafts instead of body-only drafts. If no active authenticated model is available, drafting falls back to the deterministic path.
27
53
 
28
54
  ## How it works
29
55
 
30
- On each iteration, pi-ralph reads `RALPH.md`, runs the configured commands, injects their output into the prompt through `{{ commands.<name> }}` placeholders, starts a fresh session, sends the prompt, and waits for completion. Failed test output appears in the next iteration, which creates a self-healing loop.
56
+ Each iteration re-reads `RALPH.md`, runs the configured commands, injects their output into `{{ commands.<name> }}` placeholders, and sends the task to a fresh `pi --mode rpc` subprocess instead of keeping a long-lived in-process session. If `RALPH_PROGRESS.md` exists at the task root, Ralph injects it into every prompt as a short writable memory and ignores its churn when deciding whether the loop made durable progress. Failed command output appears in the next iteration, which keeps the loop self-healing.
57
+
58
+ ### Subprocess runner
59
+
60
+ `runner.ts` orchestrates the loop for each iteration:
61
+ - re-read `RALPH.md` so live edits apply on the next turn
62
+ - run any configured pre-iteration commands
63
+ - snapshot the task directory before the agent runs
64
+ - spawn a fresh RPC subprocess
65
+ - compare before/after snapshots and evaluate completion
66
+ - stop on max iterations, timeout, or no-progress exhaustion
67
+
68
+ `runner-rpc.ts` manages the subprocess:
69
+ - starts `pi --mode rpc --no-session`
70
+ - sends `set_model`, `set_thinking_level`, and `prompt` over stdin as JSONL
71
+ - reads JSONL events from stdout
72
+ - keeps stdin open until `agent_end`
73
+ - handles timeouts and process lifecycle
74
+
75
+ ### Model selection
76
+
77
+ `modelPattern` supports `provider/modelId` and `provider/modelId:thinkingLevel`. When a thinking level is present, the runner sends `set_model` first, then `set_thinking_level`, and only then sends the prompt. The RPC manager waits for acknowledgments before continuing.
78
+
79
+ ### Progress detection
80
+
81
+ The runner hashes task-directory files before and after each iteration and diffs the snapshots. New or modified files count as progress. `.git`, `node_modules`, `.ralph-runner`, and similar ignored paths are excluded. If a snapshot is truncated because it exceeds 200 files or 2 MB, progress is reported as `"unknown"` instead of `false`. After the subprocess exits, the runner waits 100 ms and polls once more for late file writes.
82
+
83
+ ### Durable state
84
+
85
+ `runner-state.ts` stores durable state in `.ralph-runner/` inside the task directory:
86
+ - `status.json` — current status, loop token, and timestamps
87
+ - `iterations.jsonl` — appended iteration records
88
+ - `stop.flag` — graceful stop signal
89
+
90
+ `status.json` records runner states such as `initializing`, `running`, `complete`, `max-iterations`, `no-progress-exhaustion`, `stopped`, `timeout`, `error`, and `cancelled`. `/ralph-stop` now writes `stop.flag`, and the runner checks it before each iteration.
91
+
92
+ ## Smart `/ralph` behavior
93
+
94
+ `/ralph` is path-first:
95
+
96
+ - task folder with `RALPH.md` -> runs it
97
+ - direct `RALPH.md` path -> runs it
98
+ - no args in a folder without `RALPH.md` -> asks what the loop should work on, drafts `./RALPH.md`, then asks before starting
99
+ - natural-language task -> drafts `./<slug>/RALPH.md`, then asks before starting
100
+ - unresolved path-like input like `foo/bar` or `notes.md` -> offers recovery choices and normalizes missing markdown targets to `./<folder>/RALPH.md`
101
+ - arbitrary markdown files like `README.md` -> rejected instead of auto-run
102
+
103
+ ### Explicit flags
104
+
105
+ Use these when you want to skip heuristics:
106
+
107
+ ```text
108
+ /ralph --path my-task --arg owner="Ada Lovelace"
109
+ /ralph --task "reverse engineer the billing flow"
110
+ /ralph-draft --path my-task
111
+ /ralph-draft --task "fix flaky auth tests"
112
+ ```
113
+
114
+ `--arg` is for reusable templates that already declare runtime parameters. It is applied only when `/ralph` runs an existing `RALPH.md`; `/ralph-draft` leaves arg placeholders untouched for now. It accepts quoted multiword values like `--arg owner="Ada Lovelace"`.
115
+
116
+ ### Interactive review
117
+
118
+ Draft flows require an interactive UI because the extension uses a Mission Brief and editor dialog before saving or starting. In non-interactive contexts, pass an existing task folder or `RALPH.md` path instead.
31
119
 
32
120
  ## RALPH.md format
33
121
 
34
122
  ```md
35
123
  ---
124
+ args:
125
+ - owner
36
126
  commands:
37
127
  - name: tests
38
- run: npm test -- --runInBand
128
+ run: npm test
39
129
  timeout: 90
40
130
  - name: lint
41
131
  run: npm run lint
42
132
  timeout: 60
43
133
  max_iterations: 25
134
+ inter_iteration_delay: 0
135
+ timeout: 300
136
+ completion_promise: "DONE"
44
137
  guardrails:
45
138
  block_commands:
46
139
  - "rm\\s+-rf\\s+/"
@@ -51,35 +144,58 @@ guardrails:
51
144
  ---
52
145
  You are fixing flaky tests in the auth module.
53
146
 
147
+ <!-- This comment is stripped before sending to the agent -->
148
+
54
149
  Latest test output:
55
150
  {{ commands.tests }}
56
151
 
57
152
  Latest lint output:
58
153
  {{ commands.lint }}
59
154
 
155
+ Iteration {{ ralph.iteration }} of {{ ralph.name }}.
60
156
  Apply the smallest safe fix and explain why it works.
61
157
  ```
62
158
 
159
+ Strengthened body-and-commands drafts keep the deterministic baseline exact: command `name -> run` pairs must match the baseline, commands may only be reordered, dropped, or have timeouts stay the same or decrease, `max_iterations` and top-level `timeout` may stay the same or decrease, every `{{ commands.<name> }}` used in the strengthened draft must point to an accepted command, `completion_promise` must stay unchanged, including staying absent when absent, and guardrails stay fixed in this phase. If the strengthened frontmatter is invalid or unsupported, pi rejects the whole strengthened draft and falls back automatically instead of splicing fields.
160
+
63
161
  | Field | Type | Default | Description |
64
162
  |-------|------|---------|-------------|
65
163
  | `commands` | array | `[]` | Commands to run each iteration |
66
- | `commands[].name` | string | required | Key for `{{ commands.<name> }}` |
164
+ | `commands[].name` | string | required | Must match `^\w[\w-]*$`; key for `{{ commands.<name> }}` |
67
165
  | `commands[].run` | string | required | Shell command |
68
- | `commands[].timeout` | number | `60` | Seconds before kill |
69
- | `max_iterations` | number | `50` | Stop after N iterations |
166
+ | `commands[].timeout` | number | `60` | Seconds before kill; greater than 0 and at most 300 seconds, and must be `<= timeout` |
167
+ | `args` | string[] | `[]` | Declared runtime parameters for reusable templates |
168
+ | `args[]` | string | required | Must match `^\w[\w-]*$`; key for `{{ args.<name> }}` |
169
+ | `max_iterations` | integer | `50` | Stop after N iterations; must be 1-50 |
170
+ | `inter_iteration_delay` | integer | `0` | Wait N seconds between completed iterations; must be a non-negative integer |
171
+ | `timeout` | number | `300` | Per-iteration timeout in seconds; must be greater than 0 and at most 300; stops the loop if the agent is stuck |
172
+ | `completion_promise` | string | — | Agent signals completion by sending `<promise>DONE</promise>`; loop breaks on match |
70
173
  | `guardrails.block_commands` | string[] | `[]` | Regex patterns to block in bash |
71
- | `guardrails.protected_files` | string[] | `[]` | Glob patterns to block writes |
174
+ | `guardrails.protected_files` | string[] | `[]` | Glob patterns, or the shared `policy:secret-bearing-paths` token, enforced on `write`/`edit` tool calls |
175
+
176
+ ### Placeholders
177
+
178
+ | Placeholder | Description |
179
+ |-------------|-------------|
180
+ | `{{ commands.<name> }}` | Output from the command named `<name>` |
181
+ | `{{ args.<name> }}` | Runtime value supplied with `--arg name=value` during `/ralph` |
182
+ | `{{ ralph.iteration }}` | Current 1-based iteration number |
183
+ | `{{ ralph.name }}` | Directory name containing the `RALPH.md` |
184
+ | `{{ ralph.max_iterations }}` | Top-level iteration limit from the current frontmatter |
185
+
186
+ HTML comments (`<!-- ... -->`) are stripped from the prompt body after placeholder resolution, so you can annotate your `RALPH.md` freely. `args` are resolved at runtime during `/ralph` runs only; the template file is never rewritten with supplied values. Generated drafts also escape literal `<!--` and `-->` in the visible task line, and the leading metadata comment is URL-encoded so task text can safely contain comment-like sequences.
72
187
 
73
188
  ## Commands
74
189
 
75
- - `/ralph <path>`: Start the loop from a `RALPH.md` file or directory.
76
- - `/ralph-stop`: Request a graceful stop after the current iteration.
190
+ - `/ralph [path-or-task]` - Start Ralph from a task folder or `RALPH.md`, or draft a new loop from natural language.
191
+ - `/ralph-draft [path-or-task]` - Draft or edit a Ralph task without starting it.
192
+ - `/ralph-stop` - Request a graceful stop after the current iteration by writing `.ralph-runner/stop.flag`.
77
193
 
78
194
  ## Pi-only features
79
195
 
80
196
  ### Guardrails
81
197
 
82
- `guardrails.block_commands` and `guardrails.protected_files` come from RALPH frontmatter. The extension enforces them in the `tool_call` hook. Matching bash commands are blocked, and writes/edits to protected file globs are denied.
198
+ `guardrails.block_commands` and `guardrails.protected_files` come from RALPH frontmatter. The extension enforces them in the `tool_call` hook — but only for sessions created by the loop, so they don't leak into unrelated conversations. Matching bash commands are blocked, and `write`/`edit` tool calls targeting protected file globs or the shared secret-path policy token are denied.
83
199
 
84
200
  ### Cross-iteration memory
85
201
 
@@ -89,19 +205,39 @@ After each iteration, the extension stores a short summary with iteration number
89
205
 
90
206
  In the `tool_result` hook, bash outputs are scanned for failure patterns. After three or more failures in the same iteration, the extension appends a stop-and-think warning to push root-cause analysis before another retry.
91
207
 
208
+ ### Completion promise
209
+
210
+ When `completion_promise` is set (for example, `"DONE"`), the loop scans the agent's messages for `<promise>DONE</promise>` after each iteration. If found, the loop only stops early once the completion gate passes: any configured required outputs must exist, and `OPEN_QUESTIONS.md` must have no remaining P0/P1 items.
211
+
212
+ ### Iteration timeout
213
+
214
+ Each iteration has a configurable timeout (default 300 seconds). If the agent is stuck and doesn't become idle within the timeout, the loop stops with a warning.
215
+
216
+ ### Input validation
217
+
218
+ The extension validates `RALPH.md` frontmatter before starting and on each re-parse: `max_iterations` must be an integer from 1 to 50, `timeout` must be greater than 0 and at most 300 seconds, command names must match `^\w[\w-]*$`, command timeouts must be greater than 0 and at most 300 seconds and no greater than top-level `timeout`, `block_commands` regexes must compile, and commands must have non-empty names and run strings. The current runtime also rejects unsafe `completion_promise` values (non-string, blank, multiline, or angle-bracketed) and universal `guardrails.protected_files` globs such as `**/*`.
219
+
92
220
  ## Comparison table
93
221
 
94
222
  | Feature | **@lnilluv/pi-ralph-loop** | pi-ralph | pi-ralph-wiggum | ralphi | ralphify |
95
- |---------|------------------------|----------------------|-----------------|--------|----------|
223
+ |---------|----------------------------|----------|-----------------|--------|----------|
96
224
  | Command output injection | ✓ | ✗ | ✗ | ✗ | ✓ |
97
225
  | Fresh-context sessions | ✓ | ✓ | ✗ | ✓ | ✓ |
226
+ | Subprocess isolation | ✓ | ✗ | ✗ | ✗ | ✗ |
227
+ | Durable state | ✓ | ✗ | ✗ | ✗ | ✗ |
228
+ | Model selection | ✓ | ✗ | ✗ | ✗ | ✗ |
229
+ | Progress detection | ✓ | ✗ | ✗ | ✗ | ✗ |
230
+ | Live RALPH.md editing | ✓ | ✗ | ✗ | ✗ | ✗ | |
98
231
  | Mid-turn guardrails | ✓ | ✗ | ✗ | ✗ | ✗ |
99
232
  | Cross-iteration memory | ✓ | ✗ | ✗ | ✗ | ✗ |
100
233
  | Mid-turn steering | ✓ | ✗ | ✗ | ✗ | ✗ |
101
- | Live prompt editing | ✓ | ✗ | ✗ | ✗ | |
102
- | Setup required | RALPH.md | config | RALPH.md | PRD pipeline | RALPH.md |
234
+ | Guided task drafting | ✓ | ✗ | ✗ | ✗ | separate scaffold |
235
+ | Completion promise | | | | | |
236
+ | Iteration timeout | ✓ | ✗ | ✗ | ✗ | ✗ |
237
+ | Session-scoped hooks | ✓ | ✗ | ✗ | ✗ | ✗ |
238
+ | Input validation | ✓ | ✗ | ✗ | ✗ | ✗ |
239
+ | Setup required | task folder or draft flow | config | RALPH.md | PRD pipeline | scaffold + RALPH.md |
103
240
 
104
241
  ## License
105
242
 
106
243
  MIT
107
- # CI provenance test
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lnilluv/pi-ralph-loop",
3
- "version": "0.1.3",
3
+ "version": "0.1.4-dev.1",
4
4
  "description": "Pi-native ralph loop — autonomous coding iterations with mid-turn supervision",
5
5
  "type": "module",
6
6
  "pi": {
@@ -8,7 +8,11 @@
8
8
  "./src/index.ts"
9
9
  ]
10
10
  },
11
+ "engines": {
12
+ "node": "22.22.1 || 24.14.1"
13
+ },
11
14
  "scripts": {
15
+ "test": "node --test --experimental-strip-types $(find tests -type f -name '*.ts' | sort)",
12
16
  "typecheck": "tsc --noEmit"
13
17
  },
14
18
  "repository": {
@@ -16,8 +20,9 @@
16
20
  "url": "git+https://github.com/lnilluv/pi-ralph-loop.git"
17
21
  },
18
22
  "dependencies": {
19
- "yaml": "2.8.3",
20
- "minimatch": "10.2.3"
23
+ "@mariozechner/pi-ai": "0.66.1",
24
+ "minimatch": "10.2.3",
25
+ "yaml": "2.8.3"
21
26
  },
22
27
  "peerDependencies": {
23
28
  "@mariozechner/pi-coding-agent": "*"
@@ -28,5 +33,9 @@
28
33
  "autonomous",
29
34
  "loop"
30
35
  ],
31
- "license": "MIT"
36
+ "license": "MIT",
37
+ "devDependencies": {
38
+ "@types/node": "25.6.0",
39
+ "typescript": "6.0.2"
40
+ }
32
41
  }