Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ flowchart LR
Job --> Orchestrator[Orchestrator]
Orchestrator --> Worker[Pi worker process]
Worker --> Runner[Pi SDK runner]
Orchestrator --> Checks[Check runner]
Orchestrator --> Checks[Shell check runner]
Orchestrator --> Git[Git transaction]
Orchestrator --> State[Run state]
Docker[Docker executors] --> CLI
Expand All @@ -23,9 +23,10 @@ flowchart LR
| Module | Responsibility |
| --- | --- |
| `cli.ts` | Selects commands and execution environments, reports exit status and output locations. |
| `job.ts` | Loads direct task text, plain text files, or restricted YAML and validates job fields. |
| `job.ts` | Loads and validates task inputs, adds prompt and context files, and derives the progress identity. |
| `run-args.ts` | Parses CLI overrides, model selection, Pi configuration, and environment forwarding. |
| `orchestrator.ts` | Owns the state machine, limits, checks, progress, commits, and terminal result. |
| `orchestrator.ts` | Owns the state machine, limits, check decisions, progress, commits, and terminal result. |
| `check-runner.ts` | Runs one shell check with output limits, credential redaction, timeout, and cancellation. |
| `pi-process-runner.ts`, `pi-worker.ts` | Isolate each CLI Pi iteration in a process that can be stopped at the run deadline. |
| `pi-runner.ts` | Creates one Pi SDK session per iteration and normalizes model events, output, errors, and cost. |
| `runner.ts` | Defines the small runner interface and the diagnostic dry-run implementation. |
Expand All @@ -35,8 +36,9 @@ flowchart LR
| `docker-clone-executor.ts` | Clones a branch into the sandbox and exports an inspectable binary patch plus run records. |
| `docker-env.ts` | Transfers only selected model, Pi, check, context, override, and provider environment settings. |
| `remote-executor.ts` | Dispatches GitHub Actions, correlates the run, waits for completion, and downloads artifacts. |
| `init.ts`, `*-workflow.ts` | Generate the manual and issue/PR workflows without overwriting existing files; ignore local RalphWorks state. |
| `workflow-execution.ts` | Shares the identical RalphWorks checkout/build steps and provider credential environment across scenario workflow templates. |
| `init.ts` | Writes the manual and scenario workflow templates without replacing existing files; ignores local RalphWorks state. |
| `remote-workflow.ts`, `*-workflow.ts` | Define the manual remote workflow and the issue/PR scenario templates. |
| `workflow-execution.ts` | Shares RalphWorks checkout/build steps, provider credential environment, and checked patch artifact validation across scenario workflow templates. |
| `status.ts`, `trace.ts` | Read compact summaries from structured result and event files. |

## Core state machine
Expand Down Expand Up @@ -79,6 +81,14 @@ Every run defaults to a 30-minute wall-clock limit unless the job or CLI supplie

The Docker and remote adapters have a separate 120-minute default outer deadline for setup, agent execution, and result delivery. `--total-minutes` overrides it. A timed-out Docker adapter attempts to force-remove its named container. A timed-out remote wait requests cancellation of its Actions run and returns the run URL for follow-up.

## Prompt and skill boundaries

RalphWorks does not replace Pi's system prompt. Pi supplies its own system instructions and discovers skills from its configured agent and trusted project directories. RalphWorks sends each fresh Pi session a task prompt containing the job, iteration number, task text, bounded saved progress, and rules for checks, Git ownership, handoff, completion, and human input. `buildRalphPrompt` in `src/pi-runner.ts` is the source of truth for that text.

Scenario workflows create task files before invoking the same loop. Their instructions and referenced repository, Issue, PR, diff, or feedback files become the task portion of the RalphWorks prompt; they are not separate system prompts. The `src/*-workflow.ts` templates are the source of truth for scenario wording. When an architecture workflow uses an external skill, it checks out a selected `mattpocock/skills` commit, installs the skill for Pi, and records the resolved commit in its artifact and Issue. Skill content can change between runs when the configured ref is a branch.

The agent requests completion with the configured `<promise>…</promise>` value, or asks for a person with a final `<needs-input>…</needs-input>` handoff. RalphWorks interprets those signals and independently applies its checks and limits; prompt instructions alone do not establish that a task passed verification.

## Git ownership

RalphWorks owns commits when `commit: verified` is enabled:
Expand Down
4 changes: 4 additions & 0 deletions docs/ISSUE_WORKFLOWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

This guide installs RalphWorks' label-driven GitHub Actions workflows in another repository. The workflows turn an issue into a checked draft PR, split and implement a PRD, promote issues when dependencies close, review a PR, apply feedback, update a PR branch, and propose architecture work. A maintainer still reviews and merges implementation PRs.

Each model-backed workflow builds a task file from its trigger and relevant repository context, then runs it through the same RalphWorks loop. Its execution job records the result and artifacts; a separate delivery job checks them before writing to GitHub. Scenario task text is defined in the generated workflow templates, while the loop's shared prompt rules are described in [the architecture guide](ARCHITECTURE.md#prompt-and-skill-boundaries).

Implementation delivery requires a completed run, passing checks in its final iteration, a nonempty patch, and the patch's base commit record. The delivery job also verifies that the target branch has not advanced before applying the patch.

## 1. Prepare the target repository

The target must have GitHub Issues, Pull Requests, and Actions enabled. The workflows run on GitHub-hosted Ubuntu runners with Node.js 24 and pnpm 10.13.1. Project dependencies are installed with pnpm, npm, or Yarn when the corresponding lockfile exists. Make sure the repository's own test and build commands run on Ubuntu.
Expand Down
2 changes: 2 additions & 0 deletions docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,8 @@ When a remote run returns `needs_input`, read its `reason` and run URL. Update t

`ralphworks init` also creates eight label-driven workflows for issue implementation, PRD sub-issues, dependency queues, PR review and feedback, branch updates, and architecture proposals. They are separate from the `remote` command: successful implementation runs can push a branch and open or update a PR. Maintainers review and merge those PRs.

Before an implementation workflow delivers a patch, it requires a completed run, passing checks from the final iteration, a nonempty patch, and its base commit record. Missing or failed artifacts stop delivery and leave the run available for inspection.

The queue workflow paginates through all open queued issues. If PRD splitting creates some sub-issues before delivery fails, re-run the failed job from the same Actions run to finish that proposal without duplicating matching sub-issues. See the [issue and PR workflow guide](ISSUE_WORKFLOWS.md#limits-failures-and-verification) for recovery steps.

The [issue and PR workflow guide](ISSUE_WORKFLOWS.md) covers installation, credentials, labels, each path, and retries. Existing workflow files are not overwritten by `init`.
Expand Down
70 changes: 70 additions & 0 deletions src/check-runner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { spawn } from "node:child_process";

import { redactSensitiveValues } from "./redact.ts";

export type CheckResult = {
command: string;
exitCode: number;
stdout: string;
stderr: string;
timedOut?: boolean;
cancelled?: boolean;
};

export function runCheck(command: string, cwd: string, timeoutMs: number, signal?: AbortSignal): Promise<CheckResult> {
return new Promise((resolveCheck) => {
const child = spawn(command, { cwd, shell: true, detached: process.platform !== "win32", stdio: ["ignore", "pipe", "pipe"] });
let stdout = "";
let stderr = "";
let timedOut = false;
let cancelled = false;
let error: Error | undefined;
let settled = false;
let killTimer: NodeJS.Timeout | undefined;
const killGroup = (signal: NodeJS.Signals) => {
try {
if (child.pid && process.platform !== "win32") process.kill(-child.pid, signal);
else child.kill(signal);
} catch (cause) {
if (!(cause instanceof Error && "code" in cause && cause.code === "ESRCH")) error = cause as Error;
}
};
const timer = setTimeout(() => {
timedOut = true;
killGroup("SIGTERM");
killTimer = setTimeout(() => killGroup("SIGKILL"), 1000);
}, timeoutMs);
const abort = () => {
cancelled = true;
clearTimeout(timer);
killGroup("SIGTERM");
killTimer ??= setTimeout(() => killGroup("SIGKILL"), 1000);
};
signal?.addEventListener("abort", abort, { once: true });
if (signal?.aborted) abort();
const capture = (chunk: Buffer, stream: "stdout" | "stderr") => {
if (stream === "stdout") stdout = (stdout + chunk.toString()).slice(-8192);
else stderr = (stderr + chunk.toString()).slice(-8192);
};
child.stdout.on("data", (chunk: Buffer) => capture(chunk, "stdout"));
child.stderr.on("data", (chunk: Buffer) => capture(chunk, "stderr"));
child.on("error", (cause) => {
error = cause;
});
child.on("close", (code) => {
if (settled) return;
settled = true;
clearTimeout(timer);
if (killTimer) clearTimeout(killTimer);
signal?.removeEventListener("abort", abort);
resolveCheck({
command,
exitCode: code ?? 1,
stdout: redactSensitiveValues(stdout),
stderr: redactSensitiveValues(error ? `${stderr}\n${error.message}`.trim() : stderr),
...(timedOut ? { timedOut: true } : {}),
...(cancelled ? { cancelled: true } : {}),
});
});
});
}
124 changes: 2 additions & 122 deletions src/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { PR_REVIEW_WORKFLOW } from "./pr-review-workflow.ts";
import { PRD_IMPLEMENT_WORKFLOW } from "./prd-implement-workflow.ts";
import { PRD_SPLIT_WORKFLOW } from "./prd-split-workflow.ts";
import { QUEUE_WORKFLOW } from "./queue-workflow.ts";
import { REMOTE_WORKFLOW } from "./remote-workflow.ts";
import { UPDATE_BRANCH_WORKFLOW } from "./update-branch-workflow.ts";

export type InitResult = {
Expand All @@ -29,128 +30,7 @@ const TEMPLATES: Template[] = [
{ path: ".github/workflows/ralphworks-pr-feedback.yml", content: PR_FEEDBACK_WORKFLOW },
{ path: ".github/workflows/ralphworks-update-branch.yml", content: UPDATE_BRANCH_WORKFLOW },
{ path: ".github/workflows/ralphworks-architecture.yml", content: ARCHITECTURE_WORKFLOW },
{
path: ".github/workflows/ralphworks.yml",
content: [
"name: RalphWorks",
"",
"on:",
" workflow_dispatch:",
" inputs:",
" task:",
" description: RalphWorks task file",
" required: true",
" default: ralphworks.yaml",
" request_id:",
" description: Dispatch correlation ID",
" required: false",
" resume_run_id:",
" description: Previous RalphWorks run ID to continue",
" required: false",
"",
"run-name: RalphWorks ${{ inputs.request_id }}",
"",
"jobs:",
" ralph:",
" runs-on: ubuntu-latest",
" permissions:",
" contents: read",
" actions: read",
" env:",
" RALPH_TASK: ${{ inputs.task }}",
" RALPH_RESUME_RUN: ${{ inputs.resume_run_id }}",
" RALPHWORKS_MODEL: ${{ vars.RALPHWORKS_MODEL }}",
" RALPHWORKS_REF: ${{ vars.RALPHWORKS_REF || 'v0.2.0' }}",
" RALPHWORKS_SOURCE_REPO: ${{ vars.RALPHWORKS_SOURCE_REPO || 'OctopusGarage/ralphworks' }}",
" ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}",
" OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}",
" NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }}",
" ZAI_CODING_CN_API_KEY: ${{ secrets.ZAI_CODING_CN_API_KEY }}",
" steps:",
" - uses: actions/checkout@v4",
" with:",
" fetch-depth: 0",
" - uses: pnpm/action-setup@v4",
" with:",
" version: 10.13.1",
" - uses: actions/setup-node@v4",
" with:",
" node-version: 24",
" - name: Checkout RalphWorks",
" env:",
" GH_TOKEN: ${{ secrets.RALPHWORKS_REPO_TOKEN }}",
" run: |",
' [[ "$RALPHWORKS_SOURCE_REPO" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] || { echo "Invalid RALPHWORKS_SOURCE_REPO" >&2; exit 1; }',
' if [ -n "$GH_TOKEN" ]; then gh repo clone "$RALPHWORKS_SOURCE_REPO" "$RUNNER_TEMP/ralphworks"',
' else git clone "https://github.com/${RALPHWORKS_SOURCE_REPO}.git" "$RUNNER_TEMP/ralphworks"; fi',
' if [ -n "$RALPHWORKS_REF" ]; then git -C "$RUNNER_TEMP/ralphworks" checkout --detach "$RALPHWORKS_REF"; fi',
' echo "RalphWorks commit: $(git -C "$RUNNER_TEMP/ralphworks" rev-parse HEAD)"',
" - name: Build RalphWorks",
' run: pnpm --dir "$RUNNER_TEMP/ralphworks" install --frozen-lockfile && pnpm --dir "$RUNNER_TEMP/ralphworks" build',
" - run: |",
" if [ -f pnpm-lock.yaml ]; then pnpm install --frozen-lockfile",
" elif [ -f package-lock.json ]; then npm ci",
" elif [ -f yarn.lock ]; then yarn install --frozen-lockfile",
" fi",
' - run: echo "RALPH_BASE=$(git rev-parse HEAD)" >> "$GITHUB_ENV"',
" - name: Restore previous remote run",
" if: env.RALPH_RESUME_RUN != ''",
" env:",
" GH_TOKEN: ${{ github.token }}",
" run: |",
' [[ "$RALPH_RESUME_RUN" =~ ^[1-9][0-9]*$ ]] || { echo "Invalid resume run ID" >&2; exit 1; }',
' RUN_META=$(gh run view "$RALPH_RESUME_RUN" --repo "$GITHUB_REPOSITORY" --json headSha,headBranch,event,workflowName)',
' [ "$(jq -r .event <<< "$RUN_META")" = workflow_dispatch ] || { echo "Resume source is not a manual workflow run" >&2; exit 1; }',
' [ "$(jq -r .workflowName <<< "$RUN_META")" = RalphWorks ] || { echo "Resume source is not a RalphWorks run" >&2; exit 1; }',
' [ "$(jq -r .headBranch <<< "$RUN_META")" = "$GITHUB_REF_NAME" ] || { echo "Resume source branch differs" >&2; exit 1; }',
' [ "$(jq -r .headSha <<< "$RUN_META")" = "$(git rev-parse HEAD)" ] || { echo "Branch changed since the previous run; review and apply its patch manually" >&2; exit 1; }',
' gh run download "$RALPH_RESUME_RUN" --repo "$GITHUB_REPOSITORY" --name ralphworks-result --dir "$RUNNER_TEMP/ralphworks-resume"',
' [ "$(cat "$RUNNER_TEMP/ralphworks-resume/export/task.txt")" = "$RALPH_TASK" ] || { echo "Resume source task differs" >&2; exit 1; }',
' [ -d "$RUNNER_TEMP/ralphworks-resume/progress" ] || { echo "Resume source has no progress artifact" >&2; exit 1; }',
' PATCH="$RUNNER_TEMP/ralphworks-resume/export/change.patch"',
' if [ -s "$PATCH" ]; then git apply --check "$PATCH" && git apply "$PATCH"; fi',
' mkdir -p .ralph/progress && cp -R "$RUNNER_TEMP/ralphworks-resume/progress/." .ralph/progress/',
" - run: |",
" git config user.name 'ralphworks[bot]'",
" git config user.email '41898282+github-actions[bot]@users.noreply.github.com'",
" - name: Prepare optional Pi model config",
" run: |",
" if [ -f .ralphworks/models.json ]; then",
" mkdir -p ~/.pi/agent",
" cp .ralphworks/models.json ~/.pi/agent/models.json",
" fi",
" - name: Run RalphWorks",
" env:",
" RALPHWORKS_AUTH_SECRET_NAME: ${{ vars.RALPHWORKS_AUTH_SECRET }}",
" RALPHWORKS_AUTH_SECRET_VALUE: ${{ secrets[vars.RALPHWORKS_AUTH_SECRET] }}",
" run: |",
' if [ -n "$RALPHWORKS_AUTH_SECRET_NAME" ]; then',
' [[ "$RALPHWORKS_AUTH_SECRET_NAME" =~ ^[A-Z][A-Z0-9_]*$ ]] || { echo "Invalid RALPHWORKS_AUTH_SECRET name" >&2; exit 1; }',
' [ -n "$RALPHWORKS_AUTH_SECRET_VALUE" ] || { echo "Configured provider secret is empty" >&2; exit 1; }',
' export "$RALPHWORKS_AUTH_SECRET_NAME=$RALPHWORKS_AUTH_SECRET_VALUE"',
" fi",
' node "$RUNNER_TEMP/ralphworks/dist/cli.js" run "$RALPH_TASK" --executor host',
" - name: Export inspectable patch",
" if: always()",
" run: |",
" mkdir -p .ralph/export",
' printf "%s\\n" "$RALPH_TASK" > .ralph/export/task.txt',
' git -C "$RUNNER_TEMP/ralphworks" rev-parse HEAD > .ralph/export/ralphworks-commit.txt',
" git add -N --all",
' git diff --binary "${RALPH_BASE:-HEAD}" -- . ":!.ralph" > .ralph/export/change.patch',
" - uses: actions/upload-artifact@v4",
" if: always()",
" with:",
" name: ralphworks-result",
" path: |",
" .ralph/export/change.patch",
" .ralph/export/ralphworks-commit.txt",
" .ralph/export/task.txt",
" .ralph/runs/",
" .ralph/progress/",
"",
].join("\n"),
},
{ path: ".github/workflows/ralphworks.yml", content: REMOTE_WORKFLOW },
];

export async function initProject(cwd = process.cwd()): Promise<InitResult> {
Expand Down
16 changes: 3 additions & 13 deletions src/issue-workflow.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { failureComment, PROVIDER_CREDENTIAL_ENV } from "./workflow-execution.ts";
import { checkedPatchArtifact, failureComment, PROVIDER_CREDENTIAL_ENV, RALPHWORKS_CHECKOUT_AND_BUILD } from "./workflow-execution.ts";

export const ISSUE_WORKFLOW = [
"name: RalphWorks Issue",
Expand Down Expand Up @@ -60,14 +60,7 @@ export const ISSUE_WORKFLOW = [
" run: |",
" set -euo pipefail",
' [[ "$RALPHWORKS_SOURCE_REPO" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] || exit 1',
' if [ -n "$SOURCE_TOKEN" ]; then',
' GH_TOKEN="$SOURCE_TOKEN" gh repo clone "$RALPHWORKS_SOURCE_REPO" "$RUNNER_TEMP/ralphworks"',
" else",
' git clone "https://github.com/${RALPHWORKS_SOURCE_REPO}.git" "$RUNNER_TEMP/ralphworks"',
" fi",
' git -C "$RUNNER_TEMP/ralphworks" checkout --detach "$RALPHWORKS_REF"',
' pnpm --dir "$RUNNER_TEMP/ralphworks" install --frozen-lockfile',
' pnpm --dir "$RUNNER_TEMP/ralphworks" build',
...RALPHWORKS_CHECKOUT_AND_BUILD,
" - name: Prepare issue task and dependencies",
" env:",
" ISSUE_TITLE: ${{ github.event.issue.title }}",
Expand Down Expand Up @@ -141,10 +134,7 @@ export const ISSUE_WORKFLOW = [
" - name: Validate completed result",
" run: |",
" set -euo pipefail",
' result=$(find "$RUNNER_TEMP/ralph-result/runs" -name result.json -type f -print -quit)',
' [ -n "$result" ] || { echo "Missing RalphWorks result" >&2; exit 1; }',
' jq -e \'.status == "completed" and (.iterations as $last | [.checks[] | select(.iteration == $last)] | length > 0) and (.iterations as $last | [.checks[] | select(.iteration == $last and .exitCode != 0)] | length == 0)\' "$result" >/dev/null',
' [ -s "$RUNNER_TEMP/ralph-result/export/change.patch" ] || { echo "Completed task produced no patch" >&2; exit 1; }',
...checkedPatchArtifact("ralph-result"),
" - uses: actions/checkout@v4",
" with:",
" persist-credentials: false",
Expand Down
Loading
Loading