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
10 changes: 9 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ jobs:
bun test src/commands/run.test.ts --coverage --coverage-reporter=text --coverage-reporter=lcov 2>&1 | tee -a coverage-output.txt
cp coverage/lcov.info coverage-parts/run.lcov

echo "=== Running remote server tests ===" | tee -a coverage-output.txt
bun test src/remote/server.test.ts --coverage --coverage-reporter=text --coverage-reporter=lcov 2>&1 | tee -a coverage-output.txt
cp coverage/lcov.info coverage-parts/remote-server.lcov

echo "=== Running src/config/ ===" | tee -a coverage-output.txt
bun test src/config/ --coverage --coverage-reporter=text --coverage-reporter=lcov 2>&1 | tee -a coverage-output.txt
cp coverage/lcov.info coverage-parts/config.lcov
Expand All @@ -153,6 +157,10 @@ jobs:
bun test src/plugins/agents/ src/plugins/trackers/builtin/beads.test.ts --coverage --coverage-reporter=text --coverage-reporter=lcov 2>&1 | tee -a coverage-output.txt || true
cp coverage/lcov.info coverage-parts/plugins.lcov || true

echo "=== Running multi-scope tracker tests ===" | tee -a coverage-output.txt
bun test src/plugins/trackers/multi-scope.test.ts --coverage --coverage-reporter=text --coverage-reporter=lcov 2>&1 | tee -a coverage-output.txt
cp coverage/lcov.info coverage-parts/multi-scope.lcov

# Linear tracker tests: client.test.ts and index.test.ts both use mock.module()
# (client mocks @linear/sdk, index mocks ./client.js) so they must run in separate processes.
# body.test.ts is pure functions with no mocks and can batch with convert.test.ts.
Expand Down Expand Up @@ -241,7 +249,7 @@ jobs:
uses: codecov/codecov-action@v4
with:
# Upload all batch coverage files - Codecov will merge them correctly
files: ./coverage-parts/tests.lcov,./coverage-parts/tests-beads-rust-bv.lcov,./coverage-parts/tests-info.lcov,./coverage-parts/tests-run-remote-only.lcov,./coverage-parts/doctor.lcov,./coverage-parts/info.lcov,./coverage-parts/skills.lcov,./coverage-parts/run.lcov,./coverage-parts/config.lcov,./coverage-parts/engine.lcov,./coverage-parts/beads-bv.lcov,./coverage-parts/beads-rust.lcov,./coverage-parts/beads.lcov,./coverage-parts/plugins.lcov,./coverage-parts/jira.lcov,./coverage-parts/linear-body.lcov,./coverage-parts/linear-client.lcov,./coverage-parts/linear-index.lcov,./coverage-parts/session.lcov,./coverage-parts/sandbox.lcov,./coverage-parts/wizard.lcov,./coverage-parts/setup.lcov,./coverage-parts/skill-installer-spawn.lcov,./coverage-parts/migration-install.lcov,./coverage-parts/templates.lcov,./coverage-parts/tui.lcov,./coverage-parts/prd.lcov,./coverage-parts/chat.lcov,./coverage-parts/parallel.lcov
files: ./coverage-parts/tests.lcov,./coverage-parts/tests-beads-rust-bv.lcov,./coverage-parts/tests-info.lcov,./coverage-parts/tests-run-remote-only.lcov,./coverage-parts/doctor.lcov,./coverage-parts/info.lcov,./coverage-parts/skills.lcov,./coverage-parts/run.lcov,./coverage-parts/remote-server.lcov,./coverage-parts/config.lcov,./coverage-parts/engine.lcov,./coverage-parts/beads-bv.lcov,./coverage-parts/beads-rust.lcov,./coverage-parts/beads.lcov,./coverage-parts/plugins.lcov,./coverage-parts/multi-scope.lcov,./coverage-parts/jira.lcov,./coverage-parts/linear-body.lcov,./coverage-parts/linear-client.lcov,./coverage-parts/linear-index.lcov,./coverage-parts/session.lcov,./coverage-parts/sandbox.lcov,./coverage-parts/wizard.lcov,./coverage-parts/setup.lcov,./coverage-parts/skill-installer-spawn.lcov,./coverage-parts/migration-install.lcov,./coverage-parts/templates.lcov,./coverage-parts/tui.lcov,./coverage-parts/prd.lcov,./coverage-parts/chat.lcov,./coverage-parts/parallel.lcov
fail_ci_if_error: false
verbose: true
env:
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ ralph-tui run --prd ./prd.json
# Run with a Beads epic
ralph-tui run --epic my-epic-id

# Run one parallel session across multiple Beads epics
ralph-tui run --parallel --epic ui-epic --epic backend-epic
ralph-tui run --parallel --epics ui-epic,backend-epic

# Override agent or model
ralph-tui run --agent claude --model sonnet
ralph-tui run --agent opencode --model anthropic/claude-3-5-sonnet
Expand Down Expand Up @@ -145,6 +149,8 @@ ralph-tui create-prd --output ./docs
| `s` | Start execution |
| `p` | Pause/Resume |
| `d` | Toggle dashboard |
| `g` | Cycle scope filter in multi-epic sessions |
| `G` | Reset scope filter to All |
| `T` | Toggle subagent tree panel (Shift+T) |
| `t` | Cycle subagent detail level |
| `o` | Cycle right panel views |
Expand All @@ -159,6 +165,8 @@ ralph-tui create-prd --output ./docs
| `e` | Edit current remote (when viewing remote tab) |
| `x` | Delete current remote (when viewing remote tab) |

**Multi-epic sessions:** Repeated `--epic` and comma-separated `--epics` create one Ralph session across all selected hierarchy-tracker epics. Ralph uses one scheduler, one repo lock, one session branch, one merge queue, and task-scoped worktrees. The TUI scope filter (`g`/`G`) is only a local view filter over that session.

**Dashboard (`d` key):** Toggle a status panel showing:
- Current execution status and active task
- Agent name and model (e.g., `claude-code`, `anthropic/claude-sonnet`)
Expand Down
1 change: 1 addition & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ ignore:
- "dist/**/*"
# TUI components are React/blessed rendering code - hard to unit test meaningfully
- "src/tui/**/*"
- "src/commands/*.tsx"
- "src/commands/**/*.tsx"

# Comment settings for PRs
Expand Down
87 changes: 79 additions & 8 deletions src/commands/resume.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ import { registerBuiltinAgents } from '../plugins/agents/builtin/index.js';
import { registerBuiltinTrackers } from '../plugins/trackers/builtin/index.js';
import { getAgentRegistry } from '../plugins/agents/registry.js';
import { getTrackerRegistry } from '../plugins/trackers/registry.js';
import type { TrackerTask } from '../plugins/trackers/types.js';
import {
MultiScopeTrackerPlugin,
createExecutionScopeFromTask,
} from '../plugins/trackers/multi-scope.js';
import type { ExecutionScope, TrackerPlugin, TrackerTask } from '../plugins/trackers/types.js';
import { RunApp } from '../tui/components/RunApp.js';

/**
Expand All @@ -66,6 +70,40 @@ export function shouldWarnAboutTrackerMismatch(
return engineTotalTasks === 0 && sessionTotalTasks > 0;
}

async function resolveExecutionScopes(
tracker: TrackerPlugin,
epicIds: string[],
persistedScopes?: ExecutionScope[]
): Promise<ExecutionScope[]> {
if (epicIds.length === 0) {
return [];
}

const persistedScopeMap = new Map((persistedScopes ?? []).map((scope) => [scope.id, scope]));
let epics: TrackerTask[] = [];
try {
epics = await tracker.getEpics();
} catch {
epics = [];
}

return epicIds.map((epicId) => {
const persistedScope = persistedScopeMap.get(epicId);
if (persistedScope) {
return persistedScope;
}

const epic = epics.find((candidate) => candidate.id === epicId);
return epic
? createExecutionScopeFromTask(epic)
: {
id: epicId,
title: epicId,
type: 'epic',
};
});
}

/**
* Parsed resume command arguments
*/
Expand Down Expand Up @@ -159,8 +197,10 @@ export function formatSessionEntry(entry: SessionRegistryEntry, index?: number):
entry.status === 'running' ? '▶' :
entry.status === 'interrupted' ? '⚠' : '•';
const sandboxTag = entry.sandbox ? ' [sandbox]' : '';
const trackerInfo = entry.epicId ? `epic:${entry.epicId}` :
entry.prdPath ? `prd:${entry.prdPath}` : entry.trackerPlugin;
const trackerInfo = entry.epicIds && entry.epicIds.length > 0
? `epic:${entry.epicIds.join(',')}`
: entry.epicId ? `epic:${entry.epicId}`
: entry.prdPath ? `prd:${entry.prdPath}` : entry.trackerPlugin;

return `${prefix}${statusIcon} ${shortId} ${entry.status.padEnd(11)} ${entry.agentPlugin.padEnd(10)} ${trackerInfo}${sandboxTag}\n ${entry.cwd}`;
}
Expand Down Expand Up @@ -313,7 +353,8 @@ async function runWithTui(
initialState: PersistedSessionState,
initialTasks: TrackerTask[],
trackerType?: string,
currentModel?: string
currentModel?: string,
executionScopes: ExecutionScope[] = []
): Promise<PersistedSessionState> {
let currentState = initialState;

Expand Down Expand Up @@ -410,6 +451,7 @@ async function runWithTui(
}}
onStart={handleStart}
trackerType={trackerType}
executionScopes={executionScopes}
initialSubagentPanelVisible={initialState.subagentPanelVisible ?? false}
onSubagentPanelVisibilityChange={handleSubagentPanelVisibilityChange}
currentModel={currentModel}
Expand Down Expand Up @@ -605,6 +647,7 @@ export async function executeResumeCommand(args: string[]): Promise<void> {
agent: persistedState.agentPlugin,
tracker: persistedState.trackerState.plugin,
epicId: persistedState.trackerState.epicId,
epicIds: persistedState.trackerState.epicIds,
prdPath: persistedState.trackerState.prdPath,
iterations: persistedState.maxIterations,
cwd,
Expand Down Expand Up @@ -652,9 +695,25 @@ export async function executeResumeCommand(args: string[]): Promise<void> {

// Create and initialize engine
const engine = new ExecutionEngine(config);
let executionScopes: ExecutionScope[] = [];
let trackerForResume: TrackerPlugin | undefined;

try {
await engine.initialize();
if (config.epicIds && config.epicIds.length > 1) {
const trackerRegistry = getTrackerRegistry();
trackerForResume = await trackerRegistry.getInstance(config.tracker);
executionScopes = await resolveExecutionScopes(
trackerForResume,
config.epicIds,
resumedState.trackerState.executionScopes
);
await engine.initialize(undefined, {
tracker: new MultiScopeTrackerPlugin(trackerForResume, executionScopes),
});
} else {
executionScopes = resumedState.trackerState.executionScopes ?? [];
await engine.initialize();
}
} catch (error) {
console.error(
'Failed to initialize engine:',
Expand All @@ -670,7 +729,11 @@ export async function executeResumeCommand(args: string[]): Promise<void> {
if (shouldWarnAboutTrackerMismatch(engineState.totalTasks, sessionTotalTasks)) {
console.warn('\nWarning: Session has task history but tracker returned no tasks.');
console.warn('This may happen if:');
if (resumedState.trackerState.epicId) {
if (resumedState.trackerState.epicIds?.length) {
console.warn(
` - The configured epic IDs "${resumedState.trackerState.epicIds.join(', ')}" no longer exist`
);
} else if (resumedState.trackerState.epicId) {
console.warn(` - The epic ID "${resumedState.trackerState.epicId}" no longer exists`);
}
if (resumedState.trackerState.prdPath) {
Expand All @@ -694,13 +757,21 @@ export async function executeResumeCommand(args: string[]): Promise<void> {
let tasks: TrackerTask[] = [];
try {
const trackerRegistry = getTrackerRegistry();
const tracker = await trackerRegistry.getInstance(config.tracker);
const tracker = engine.getTracker() ?? trackerForResume ?? await trackerRegistry.getInstance(config.tracker);
tasks = await tracker.getTasks({ status: ['open', 'in_progress', 'completed'] });
} catch (error) {
console.error('Warning: Failed to load tasks:', error);
}

finalState = await runWithTui(engine, cwd, resumedState, tasks, config.tracker.plugin, config.model);
finalState = await runWithTui(
engine,
cwd,
resumedState,
tasks,
config.tracker.plugin,
config.model,
executionScopes
);
} else {
finalState = await runHeadless(engine, cwd, resumedState);
}
Expand Down
Loading
Loading