wiggum-cli 0.15.0 → 0.17.0
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 +7 -1
- package/bin/ralph.js +0 -0
- package/dist/agent/memory/ingest.d.ts +14 -0
- package/dist/agent/memory/ingest.js +77 -0
- package/dist/agent/memory/store.d.ts +15 -0
- package/dist/agent/memory/store.js +98 -0
- package/dist/agent/memory/types.d.ts +16 -0
- package/dist/agent/memory/types.js +14 -0
- package/dist/agent/orchestrator.d.ts +7 -0
- package/dist/agent/orchestrator.js +266 -0
- package/dist/agent/resolve-config.d.ts +26 -0
- package/dist/agent/resolve-config.js +43 -0
- package/dist/agent/tools/backlog.d.ts +27 -0
- package/dist/agent/tools/backlog.js +51 -0
- package/dist/agent/tools/dry-run.d.ts +106 -0
- package/dist/agent/tools/dry-run.js +119 -0
- package/dist/agent/tools/execution.d.ts +51 -0
- package/dist/agent/tools/execution.js +256 -0
- package/dist/agent/tools/feature-state.d.ts +43 -0
- package/dist/agent/tools/feature-state.js +184 -0
- package/dist/agent/tools/introspection.d.ts +23 -0
- package/dist/agent/tools/introspection.js +40 -0
- package/dist/agent/tools/memory.d.ts +44 -0
- package/dist/agent/tools/memory.js +99 -0
- package/dist/agent/tools/preflight.d.ts +7 -0
- package/dist/agent/tools/preflight.js +137 -0
- package/dist/agent/tools/reporting.d.ts +58 -0
- package/dist/agent/tools/reporting.js +119 -0
- package/dist/agent/tools/schemas.d.ts +2 -0
- package/dist/agent/tools/schemas.js +3 -0
- package/dist/agent/types.d.ts +45 -0
- package/dist/agent/types.js +1 -0
- package/dist/ai/conversation/conversation-manager.js +8 -0
- package/dist/ai/conversation/url-fetcher.js +27 -0
- package/dist/ai/providers.js +5 -5
- package/dist/commands/agent.d.ts +17 -0
- package/dist/commands/agent.js +114 -0
- package/dist/commands/monitor.js +50 -183
- package/dist/commands/new-auto.d.ts +15 -0
- package/dist/commands/new-auto.js +237 -0
- package/dist/commands/run.js +20 -10
- package/dist/commands/sync.d.ts +15 -0
- package/dist/commands/sync.js +68 -0
- package/dist/generator/config.d.ts +1 -41
- package/dist/generator/config.js +7 -0
- package/dist/generator/index.d.ts +2 -2
- package/dist/generator/templates.d.ts +3 -0
- package/dist/generator/templates.js +22 -1
- package/dist/index.d.ts +14 -1
- package/dist/index.js +333 -40
- package/dist/repl/command-parser.d.ts +5 -0
- package/dist/repl/command-parser.js +5 -0
- package/dist/templates/prompts/PROMPT.md.tmpl +13 -10
- package/dist/templates/prompts/PROMPT_e2e.md.tmpl +162 -5
- package/dist/templates/prompts/PROMPT_feature.md.tmpl +39 -3
- package/dist/templates/prompts/PROMPT_review_auto.md.tmpl +33 -8
- package/dist/templates/prompts/PROMPT_review_manual.md.tmpl +4 -1
- package/dist/templates/prompts/PROMPT_review_merge.md.tmpl +40 -10
- package/dist/templates/prompts/PROMPT_verify.md.tmpl +5 -2
- package/dist/templates/scripts/feature-loop.sh.tmpl +611 -95
- package/dist/tui/app.d.ts +34 -2
- package/dist/tui/app.js +31 -5
- package/dist/tui/components/ActivityFeed.d.ts +18 -0
- package/dist/tui/components/ActivityFeed.js +31 -0
- package/dist/tui/components/IssuePicker.d.ts +27 -0
- package/dist/tui/components/IssuePicker.js +64 -0
- package/dist/tui/components/RunCompletionSummary.d.ts +27 -1
- package/dist/tui/components/RunCompletionSummary.js +103 -10
- package/dist/tui/components/SummaryBox.d.ts +4 -0
- package/dist/tui/components/SummaryBox.js +4 -2
- package/dist/tui/hooks/useAgentOrchestrator.d.ts +29 -0
- package/dist/tui/hooks/useAgentOrchestrator.js +453 -0
- package/dist/tui/hooks/useBackgroundRuns.js +1 -1
- package/dist/tui/orchestration/interview-orchestrator.d.ts +5 -1
- package/dist/tui/orchestration/interview-orchestrator.js +27 -6
- package/dist/tui/screens/AgentScreen.d.ts +21 -0
- package/dist/tui/screens/AgentScreen.js +159 -0
- package/dist/tui/screens/InitScreen.js +4 -0
- package/dist/tui/screens/InterviewScreen.d.ts +3 -1
- package/dist/tui/screens/InterviewScreen.js +146 -10
- package/dist/tui/screens/MainShell.d.ts +1 -1
- package/dist/tui/screens/MainShell.js +36 -1
- package/dist/tui/screens/RunScreen.d.ts +15 -15
- package/dist/tui/screens/RunScreen.js +96 -11
- package/dist/tui/utils/build-run-summary.d.ts +1 -1
- package/dist/tui/utils/build-run-summary.js +44 -85
- package/dist/tui/utils/clear-screen.d.ts +14 -0
- package/dist/tui/utils/clear-screen.js +16 -0
- package/dist/tui/utils/git-summary.d.ts +13 -0
- package/dist/tui/utils/git-summary.js +30 -0
- package/dist/tui/utils/loop-status.d.ts +94 -0
- package/dist/tui/utils/loop-status.js +430 -10
- package/dist/tui/utils/pr-summary.d.ts +3 -2
- package/dist/tui/utils/pr-summary.js +41 -6
- package/dist/utils/ci.d.ts +8 -0
- package/dist/utils/ci.js +13 -0
- package/dist/utils/config.d.ts +8 -0
- package/dist/utils/config.js +8 -0
- package/dist/utils/github.d.ts +32 -0
- package/dist/utils/github.js +106 -0
- package/dist/utils/spec-names.js +5 -1
- package/package.json +10 -2
- package/src/templates/prompts/PROMPT.md.tmpl +13 -10
- package/src/templates/prompts/PROMPT_e2e.md.tmpl +162 -5
- package/src/templates/prompts/PROMPT_feature.md.tmpl +39 -3
- package/src/templates/prompts/PROMPT_review_auto.md.tmpl +33 -8
- package/src/templates/prompts/PROMPT_review_manual.md.tmpl +4 -1
- package/src/templates/prompts/PROMPT_review_merge.md.tmpl +40 -10
- package/src/templates/prompts/PROMPT_verify.md.tmpl +5 -2
- package/src/templates/scripts/feature-loop.sh.tmpl +611 -95
package/dist/commands/monitor.js
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* Monitor Command
|
|
3
3
|
* Display real-time status of a feature loop
|
|
4
4
|
*/
|
|
5
|
-
import { spawn
|
|
6
|
-
import { existsSync
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
7
|
import { join, dirname } from 'node:path';
|
|
8
8
|
import { logger } from '../utils/logger.js';
|
|
9
9
|
import { loadConfigWithDefaults } from '../utils/config.js';
|
|
10
|
+
import { readLoopStatus, parseImplementationPlan, getGitBranch, formatNumber, } from '../tui/utils/loop-status.js';
|
|
10
11
|
import pc from 'picocolors';
|
|
11
12
|
/**
|
|
12
13
|
* Find the ralph-monitor.sh script
|
|
@@ -29,162 +30,6 @@ function findMonitorScript(projectRoot) {
|
|
|
29
30
|
}
|
|
30
31
|
return null;
|
|
31
32
|
}
|
|
32
|
-
/**
|
|
33
|
-
* Check if a process matching pattern is running
|
|
34
|
-
* Uses pgrep with -f flag for full command line matching
|
|
35
|
-
*/
|
|
36
|
-
function isProcessRunning(pattern) {
|
|
37
|
-
try {
|
|
38
|
-
// Use execFileSync for safer execution
|
|
39
|
-
const result = execFileSync('pgrep', ['-f', pattern], { encoding: 'utf-8' });
|
|
40
|
-
return result.trim().length > 0;
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Detect current phase of the loop
|
|
48
|
-
*/
|
|
49
|
-
function detectPhase(feature) {
|
|
50
|
-
if (isProcessRunning('PROMPT_feature.md'))
|
|
51
|
-
return 'Planning';
|
|
52
|
-
if (isProcessRunning('PROMPT_e2e.md'))
|
|
53
|
-
return 'E2E Testing';
|
|
54
|
-
if (isProcessRunning('PROMPT_verify.md'))
|
|
55
|
-
return 'Verification';
|
|
56
|
-
if (isProcessRunning('PROMPT_review_manual.md'))
|
|
57
|
-
return 'PR Review';
|
|
58
|
-
if (isProcessRunning('PROMPT_review_auto.md'))
|
|
59
|
-
return 'PR Review';
|
|
60
|
-
if (isProcessRunning('PROMPT.md'))
|
|
61
|
-
return 'Implementation';
|
|
62
|
-
if (isProcessRunning(`feature-loop.sh.*${feature}`))
|
|
63
|
-
return 'Running';
|
|
64
|
-
return 'Idle';
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Read status from temp files
|
|
68
|
-
*/
|
|
69
|
-
function readStatus(feature) {
|
|
70
|
-
const statusFile = `/tmp/ralph-loop-${feature}.status`;
|
|
71
|
-
const tokensFile = `/tmp/ralph-loop-${feature}.tokens`;
|
|
72
|
-
let iteration = 0;
|
|
73
|
-
let maxIterations = 50;
|
|
74
|
-
// Read status file
|
|
75
|
-
if (existsSync(statusFile)) {
|
|
76
|
-
try {
|
|
77
|
-
const content = readFileSync(statusFile, 'utf-8').trim();
|
|
78
|
-
const parts = content.split('|');
|
|
79
|
-
iteration = parseInt(parts[0]) || 0;
|
|
80
|
-
maxIterations = parseInt(parts[1]) || 50;
|
|
81
|
-
}
|
|
82
|
-
catch {
|
|
83
|
-
// Ignore errors
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
// Read tokens file
|
|
87
|
-
let tokensInput = 0;
|
|
88
|
-
let tokensOutput = 0;
|
|
89
|
-
if (existsSync(tokensFile)) {
|
|
90
|
-
try {
|
|
91
|
-
const content = readFileSync(tokensFile, 'utf-8').trim();
|
|
92
|
-
const parts = content.split('|');
|
|
93
|
-
tokensInput = parseInt(parts[0]) || 0;
|
|
94
|
-
tokensOutput = parseInt(parts[1]) || 0;
|
|
95
|
-
}
|
|
96
|
-
catch {
|
|
97
|
-
// Ignore errors
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
return {
|
|
101
|
-
running: isProcessRunning(`feature-loop.sh.*${feature}`),
|
|
102
|
-
phase: detectPhase(feature),
|
|
103
|
-
iteration,
|
|
104
|
-
maxIterations,
|
|
105
|
-
tokensInput,
|
|
106
|
-
tokensOutput,
|
|
107
|
-
tasksDone: 0,
|
|
108
|
-
tasksPending: 0,
|
|
109
|
-
e2eDone: 0,
|
|
110
|
-
e2ePending: 0,
|
|
111
|
-
branch: '',
|
|
112
|
-
elapsed: '',
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* Parse implementation plan for task counts
|
|
117
|
-
*/
|
|
118
|
-
async function parseImplementationPlan(projectRoot, feature) {
|
|
119
|
-
const config = await loadConfigWithDefaults(projectRoot);
|
|
120
|
-
const planPath = join(projectRoot, config.paths.specs, `${feature}-implementation-plan.md`);
|
|
121
|
-
let tasksDone = 0;
|
|
122
|
-
let tasksPending = 0;
|
|
123
|
-
let e2eDone = 0;
|
|
124
|
-
let e2ePending = 0;
|
|
125
|
-
if (existsSync(planPath)) {
|
|
126
|
-
try {
|
|
127
|
-
const content = readFileSync(planPath, 'utf-8');
|
|
128
|
-
const lines = content.split('\n');
|
|
129
|
-
for (const line of lines) {
|
|
130
|
-
if (line.match(/^- \[x\]/)) {
|
|
131
|
-
if (line.includes('E2E:')) {
|
|
132
|
-
e2eDone++;
|
|
133
|
-
}
|
|
134
|
-
else {
|
|
135
|
-
tasksDone++;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
else if (line.match(/^- \[ \]/)) {
|
|
139
|
-
if (line.includes('E2E:')) {
|
|
140
|
-
e2ePending++;
|
|
141
|
-
}
|
|
142
|
-
else {
|
|
143
|
-
tasksPending++;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
catch {
|
|
149
|
-
// Ignore errors
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
return { tasksDone, tasksPending, e2eDone, e2ePending };
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Get current git branch
|
|
156
|
-
*/
|
|
157
|
-
function getGitBranch(projectRoot) {
|
|
158
|
-
try {
|
|
159
|
-
// Try app directory first
|
|
160
|
-
const appDir = join(projectRoot, '..', 'app');
|
|
161
|
-
if (existsSync(appDir)) {
|
|
162
|
-
return execFileSync('git', ['branch', '--show-current'], {
|
|
163
|
-
cwd: appDir,
|
|
164
|
-
encoding: 'utf-8',
|
|
165
|
-
}).trim();
|
|
166
|
-
}
|
|
167
|
-
return execFileSync('git', ['branch', '--show-current'], {
|
|
168
|
-
cwd: projectRoot,
|
|
169
|
-
encoding: 'utf-8',
|
|
170
|
-
}).trim();
|
|
171
|
-
}
|
|
172
|
-
catch {
|
|
173
|
-
return '-';
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* Format number with K/M suffix
|
|
178
|
-
*/
|
|
179
|
-
function formatNumber(num) {
|
|
180
|
-
if (num >= 1000000) {
|
|
181
|
-
return (num / 1000000).toFixed(1) + 'M';
|
|
182
|
-
}
|
|
183
|
-
if (num >= 1000) {
|
|
184
|
-
return (num / 1000).toFixed(1) + 'K';
|
|
185
|
-
}
|
|
186
|
-
return String(num);
|
|
187
|
-
}
|
|
188
33
|
/**
|
|
189
34
|
* Create a progress bar
|
|
190
35
|
*/
|
|
@@ -196,9 +41,9 @@ function progressBar(percent, width = 15) {
|
|
|
196
41
|
/**
|
|
197
42
|
* Display built-in monitor dashboard
|
|
198
43
|
*/
|
|
199
|
-
async function displayDashboard(feature, projectRoot, interval = 5) {
|
|
200
|
-
const status =
|
|
201
|
-
const tasks = await parseImplementationPlan(projectRoot, feature);
|
|
44
|
+
async function displayDashboard(feature, projectRoot, specsDir, interval = 5) {
|
|
45
|
+
const status = readLoopStatus(feature);
|
|
46
|
+
const tasks = await parseImplementationPlan(projectRoot, feature, specsDir);
|
|
202
47
|
const branch = getGitBranch(projectRoot);
|
|
203
48
|
// Calculate progress
|
|
204
49
|
const totalTasks = tasks.tasksDone + tasks.tasksPending;
|
|
@@ -233,14 +78,28 @@ async function displayDashboard(feature, projectRoot, interval = 5) {
|
|
|
233
78
|
console.log(` Phase: ${phaseColor(pc.bold(status.phase))}` +
|
|
234
79
|
` | Iter: ${pc.bold(String(status.iteration))}/${pc.dim(String(status.maxIterations))}` +
|
|
235
80
|
` | Branch: ${pc.cyan(branch)}`);
|
|
236
|
-
const totalTokens = status.tokensInput + status.tokensOutput;
|
|
81
|
+
const totalTokens = status.tokensInput + status.tokensOutput + status.cacheCreate + status.cacheRead;
|
|
82
|
+
let tokensSuffix = '';
|
|
83
|
+
if (status.tokensUpdatedAt) {
|
|
84
|
+
const agoMs = Date.now() - status.tokensUpdatedAt;
|
|
85
|
+
const agoSec = Math.floor(agoMs / 1000);
|
|
86
|
+
if (agoSec >= 60) {
|
|
87
|
+
tokensSuffix = pc.dim(` updated ${Math.floor(agoSec / 60)}m ago`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
237
90
|
console.log(` Tokens: ${pc.magenta(formatNumber(totalTokens))}` +
|
|
238
|
-
pc.dim(` (in:${formatNumber(status.tokensInput)} out:${formatNumber(status.tokensOutput)})`)
|
|
91
|
+
pc.dim(` (in:${formatNumber(status.tokensInput)} out:${formatNumber(status.tokensOutput)} cache:${formatNumber(status.cacheRead)})`) +
|
|
92
|
+
tokensSuffix);
|
|
239
93
|
console.log(pc.dim(' ' + '-'.repeat(74)));
|
|
240
94
|
// Progress
|
|
241
95
|
console.log('');
|
|
242
|
-
|
|
243
|
-
` ${pc.
|
|
96
|
+
if (!tasks.planExists && status.running) {
|
|
97
|
+
console.log(` ${pc.bold('Implementation:')} ${pc.dim('[waiting for plan...]')}`);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
console.log(` ${pc.bold('Implementation:')} ${progressBar(percentTasks)} ${pc.bold(percentTasks + '%')}` +
|
|
101
|
+
` ${pc.green('\u2713 ' + tasks.tasksDone)} / ${pc.yellow('\u25cb ' + tasks.tasksPending)}`);
|
|
102
|
+
}
|
|
244
103
|
if (totalE2e > 0) {
|
|
245
104
|
console.log(` ${pc.bold('E2E Tests: ')} ${progressBar(percentE2e)} ${pc.bold(percentE2e + '%')}` +
|
|
246
105
|
` ${pc.green('\u2713 ' + tasks.e2eDone)} / ${pc.yellow('\u25cb ' + tasks.e2ePending)}`);
|
|
@@ -305,12 +164,21 @@ export async function monitorCommand(feature, options = {}) {
|
|
|
305
164
|
logger.warn('Python TUI monitor not yet implemented');
|
|
306
165
|
logger.info('Using built-in monitor instead');
|
|
307
166
|
}
|
|
308
|
-
//
|
|
167
|
+
// Load config for correct specs path
|
|
168
|
+
let specsDir = '.ralph/specs';
|
|
169
|
+
try {
|
|
170
|
+
const config = await loadConfigWithDefaults(projectRoot);
|
|
171
|
+
specsDir = config.paths.specs;
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
logger.debug(`Failed to load config: ${err instanceof Error ? err.message : String(err)}`);
|
|
175
|
+
}
|
|
176
|
+
// Built-in monitor with sequential polling
|
|
309
177
|
const intervalSeconds = options.interval || 5;
|
|
310
178
|
const intervalMs = intervalSeconds * 1000;
|
|
311
179
|
// Initial display
|
|
312
180
|
try {
|
|
313
|
-
await displayDashboard(feature, projectRoot, intervalSeconds);
|
|
181
|
+
await displayDashboard(feature, projectRoot, specsDir, intervalSeconds);
|
|
314
182
|
}
|
|
315
183
|
catch (error) {
|
|
316
184
|
logger.error(`Failed to display dashboard: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -319,25 +187,24 @@ export async function monitorCommand(feature, options = {}) {
|
|
|
319
187
|
}
|
|
320
188
|
process.exit(1);
|
|
321
189
|
}
|
|
322
|
-
//
|
|
323
|
-
|
|
190
|
+
// Sequential refresh loop (prevents overlapping refreshes)
|
|
191
|
+
let running = true;
|
|
192
|
+
const cleanup = () => {
|
|
193
|
+
running = false;
|
|
194
|
+
console.log('');
|
|
195
|
+
logger.info('Monitor stopped');
|
|
196
|
+
};
|
|
197
|
+
process.on('SIGINT', cleanup);
|
|
198
|
+
process.on('SIGTERM', cleanup);
|
|
199
|
+
while (running) {
|
|
200
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
201
|
+
if (!running)
|
|
202
|
+
break;
|
|
324
203
|
try {
|
|
325
|
-
await displayDashboard(feature, projectRoot, intervalSeconds);
|
|
204
|
+
await displayDashboard(feature, projectRoot, specsDir, intervalSeconds);
|
|
326
205
|
}
|
|
327
206
|
catch (error) {
|
|
328
|
-
// Log error but continue monitoring
|
|
329
207
|
logger.debug(`Dashboard refresh error: ${error instanceof Error ? error.message : String(error)}`);
|
|
330
208
|
}
|
|
331
|
-
}
|
|
332
|
-
// Return a Promise that resolves on SIGINT
|
|
333
|
-
return new Promise((resolve) => {
|
|
334
|
-
const cleanup = () => {
|
|
335
|
-
clearInterval(refreshTimer);
|
|
336
|
-
console.log('');
|
|
337
|
-
logger.info('Monitor stopped');
|
|
338
|
-
resolve();
|
|
339
|
-
};
|
|
340
|
-
process.on('SIGINT', cleanup);
|
|
341
|
-
process.on('SIGTERM', cleanup);
|
|
342
|
-
});
|
|
209
|
+
}
|
|
343
210
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless Autonomous Spec Generation
|
|
3
|
+
* Drives InterviewOrchestrator without the Ink TUI for non-interactive use.
|
|
4
|
+
* Used by AI agents (e.g. OpenClaw orchestrator) to generate feature specs.
|
|
5
|
+
*/
|
|
6
|
+
import type { AIProvider } from '../ai/providers.js';
|
|
7
|
+
export interface NewAutoOptions {
|
|
8
|
+
goals?: string;
|
|
9
|
+
initialReferences?: string[];
|
|
10
|
+
model?: string;
|
|
11
|
+
provider?: AIProvider;
|
|
12
|
+
/** Timeout in ms for spec generation (default: 5 minutes) */
|
|
13
|
+
timeoutMs?: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function newAutoCommand(featureName: string, options?: NewAutoOptions): Promise<void>;
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless Autonomous Spec Generation
|
|
3
|
+
* Drives InterviewOrchestrator without the Ink TUI for non-interactive use.
|
|
4
|
+
* Used by AI agents (e.g. OpenClaw orchestrator) to generate feature specs.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { logger } from '../utils/logger.js';
|
|
9
|
+
import { getAvailableProvider, normalizeModelId, AVAILABLE_MODELS, } from '../ai/providers.js';
|
|
10
|
+
import { loadConfigWithDefaults, hasConfig } from '../utils/config.js';
|
|
11
|
+
import { loadContext, toScanResultFromPersisted } from '../context/index.js';
|
|
12
|
+
import { detectGitHubRemote, fetchGitHubIssue } from '../utils/github.js';
|
|
13
|
+
import { initTracing, flushTracing, traced, currentSpan } from '../utils/tracing.js';
|
|
14
|
+
import { InterviewOrchestrator, } from '../tui/orchestration/interview-orchestrator.js';
|
|
15
|
+
function createDeferred() {
|
|
16
|
+
let resolve;
|
|
17
|
+
let reject;
|
|
18
|
+
const promise = new Promise((res, rej) => {
|
|
19
|
+
resolve = res;
|
|
20
|
+
reject = rej;
|
|
21
|
+
});
|
|
22
|
+
return { promise, resolve, reject };
|
|
23
|
+
}
|
|
24
|
+
export async function newAutoCommand(featureName, options = {}) {
|
|
25
|
+
// Validate inputs
|
|
26
|
+
if (!featureName) {
|
|
27
|
+
console.error('Error: feature name is required for --auto mode');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
// Detect provider
|
|
31
|
+
const provider = options.provider ?? getAvailableProvider();
|
|
32
|
+
if (!provider) {
|
|
33
|
+
console.error('Error: No AI provider available. Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or OPENROUTER_API_KEY.');
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
// Load config
|
|
37
|
+
const projectRoot = process.cwd();
|
|
38
|
+
let specsDir = join(projectRoot, '.ralph/specs');
|
|
39
|
+
if (hasConfig(projectRoot)) {
|
|
40
|
+
const config = await loadConfigWithDefaults(projectRoot);
|
|
41
|
+
specsDir = join(projectRoot, config.paths.specs);
|
|
42
|
+
}
|
|
43
|
+
// Determine model — resolve aliases (e.g. 'sonnet' → full model ID)
|
|
44
|
+
const recommendedModel = AVAILABLE_MODELS[provider].find((m) => m.hint?.includes('recommended'));
|
|
45
|
+
const defaultModel = recommendedModel?.value ?? AVAILABLE_MODELS[provider][0].value;
|
|
46
|
+
const model = normalizeModelId(provider, options.model ?? defaultModel);
|
|
47
|
+
// Init tracing
|
|
48
|
+
try {
|
|
49
|
+
initTracing();
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
logger.debug(`Failed to init tracing: ${err instanceof Error ? err.message : String(err)}`);
|
|
53
|
+
}
|
|
54
|
+
// Load project context (same logic as InterviewScreen.tsx)
|
|
55
|
+
let resolvedScanResult;
|
|
56
|
+
let resolvedSessionContext;
|
|
57
|
+
try {
|
|
58
|
+
const persisted = await loadContext(projectRoot);
|
|
59
|
+
if (persisted) {
|
|
60
|
+
resolvedSessionContext = {
|
|
61
|
+
entryPoints: persisted.aiAnalysis.projectContext?.entryPoints,
|
|
62
|
+
keyDirectories: persisted.aiAnalysis.projectContext?.keyDirectories,
|
|
63
|
+
commands: persisted.aiAnalysis.commands,
|
|
64
|
+
namingConventions: persisted.aiAnalysis.projectContext?.namingConventions,
|
|
65
|
+
implementationGuidelines: persisted.aiAnalysis.implementationGuidelines,
|
|
66
|
+
keyPatterns: persisted.aiAnalysis.technologyPractices?.practices,
|
|
67
|
+
};
|
|
68
|
+
resolvedScanResult = toScanResultFromPersisted(persisted.scanResult, projectRoot);
|
|
69
|
+
logger.info('Loaded cached project context from .ralph/.context.json');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
logger.debug(`Unable to load cached project context: ${err instanceof Error ? err.message : String(err)}`);
|
|
74
|
+
}
|
|
75
|
+
// Drive the orchestrator headlessly
|
|
76
|
+
const spec = await traced(async () => {
|
|
77
|
+
currentSpan().log({
|
|
78
|
+
input: {
|
|
79
|
+
featureName,
|
|
80
|
+
provider,
|
|
81
|
+
model,
|
|
82
|
+
goals: options.goals,
|
|
83
|
+
hasReferences: (options.initialReferences?.length ?? 0) > 0,
|
|
84
|
+
},
|
|
85
|
+
metadata: {
|
|
86
|
+
command: 'new-auto',
|
|
87
|
+
provider,
|
|
88
|
+
model,
|
|
89
|
+
},
|
|
90
|
+
tags: ['new-auto'],
|
|
91
|
+
});
|
|
92
|
+
return driveOrchestrator({
|
|
93
|
+
featureName,
|
|
94
|
+
projectRoot,
|
|
95
|
+
provider,
|
|
96
|
+
model,
|
|
97
|
+
scanResult: resolvedScanResult,
|
|
98
|
+
sessionContext: resolvedSessionContext,
|
|
99
|
+
goals: options.goals,
|
|
100
|
+
initialReferences: options.initialReferences,
|
|
101
|
+
timeoutMs: options.timeoutMs,
|
|
102
|
+
});
|
|
103
|
+
}, { name: 'new-auto-run' });
|
|
104
|
+
// Save spec to disk
|
|
105
|
+
if (!existsSync(specsDir)) {
|
|
106
|
+
mkdirSync(specsDir, { recursive: true });
|
|
107
|
+
}
|
|
108
|
+
const specPath = join(specsDir, `${featureName}.md`);
|
|
109
|
+
writeFileSync(specPath, spec, 'utf-8');
|
|
110
|
+
// Print spec path to stdout (for piping/scripting)
|
|
111
|
+
console.log(specPath);
|
|
112
|
+
// Flush tracing before exit
|
|
113
|
+
try {
|
|
114
|
+
await flushTracing();
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Non-critical
|
|
118
|
+
}
|
|
119
|
+
process.exit(0);
|
|
120
|
+
}
|
|
121
|
+
async function driveOrchestrator(opts) {
|
|
122
|
+
let readyDeferred = createDeferred();
|
|
123
|
+
const completionDeferred = createDeferred();
|
|
124
|
+
let toolIdCounter = 0;
|
|
125
|
+
const orchestrator = new InterviewOrchestrator({
|
|
126
|
+
featureName: opts.featureName,
|
|
127
|
+
projectRoot: opts.projectRoot,
|
|
128
|
+
provider: opts.provider,
|
|
129
|
+
model: opts.model,
|
|
130
|
+
scanResult: opts.scanResult,
|
|
131
|
+
sessionContext: opts.sessionContext,
|
|
132
|
+
onMessage: (role, content) => {
|
|
133
|
+
logger.info(`[${role}] ${content}`);
|
|
134
|
+
},
|
|
135
|
+
onStreamChunk: () => {
|
|
136
|
+
// No-op in headless mode
|
|
137
|
+
},
|
|
138
|
+
onStreamComplete: () => {
|
|
139
|
+
// No-op in headless mode
|
|
140
|
+
},
|
|
141
|
+
onToolStart: (toolName, _input) => {
|
|
142
|
+
const id = `tool_${++toolIdCounter}`;
|
|
143
|
+
logger.debug(`Tool start: ${toolName} (${id})`);
|
|
144
|
+
return id;
|
|
145
|
+
},
|
|
146
|
+
onToolEnd: (toolId, _output, error) => {
|
|
147
|
+
if (error) {
|
|
148
|
+
logger.debug(`Tool error: ${toolId}: ${error}`);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
logger.debug(`Tool end: ${toolId}`);
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
onPhaseChange: (phase) => {
|
|
155
|
+
logger.info(`Phase: ${phase}`);
|
|
156
|
+
},
|
|
157
|
+
onComplete: (spec) => {
|
|
158
|
+
completionDeferred.resolve(spec);
|
|
159
|
+
},
|
|
160
|
+
onError: (error) => {
|
|
161
|
+
completionDeferred.reject(new Error(error));
|
|
162
|
+
// Also unblock readyDeferred so the flow doesn't hang
|
|
163
|
+
readyDeferred.resolve();
|
|
164
|
+
},
|
|
165
|
+
onWorkingChange: (_isWorking, status) => {
|
|
166
|
+
logger.debug(status);
|
|
167
|
+
},
|
|
168
|
+
onReady: () => {
|
|
169
|
+
readyDeferred.resolve();
|
|
170
|
+
},
|
|
171
|
+
onQuestion: () => {
|
|
172
|
+
// Auto-mode: skip Q&A when a question arrives
|
|
173
|
+
// skipToGeneration is called below after we detect interview phase
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
// Step 1: Start orchestrator (enters context phase)
|
|
177
|
+
await orchestrator.start();
|
|
178
|
+
await readyDeferred.promise;
|
|
179
|
+
readyDeferred = createDeferred();
|
|
180
|
+
// Step 2: Process initial references
|
|
181
|
+
if (opts.initialReferences && opts.initialReferences.length > 0) {
|
|
182
|
+
for (const ref of opts.initialReferences) {
|
|
183
|
+
if (ref.startsWith('issue:')) {
|
|
184
|
+
const value = ref.slice(6);
|
|
185
|
+
if (/^\d+$/.test(value)) {
|
|
186
|
+
// Bare issue number — resolve from repo remote
|
|
187
|
+
const repo = await detectGitHubRemote(opts.projectRoot);
|
|
188
|
+
if (repo) {
|
|
189
|
+
const detail = await fetchGitHubIssue(repo.owner, repo.repo, parseInt(value, 10));
|
|
190
|
+
if (detail) {
|
|
191
|
+
const content = `# ${detail.title}\n\n${detail.body ?? ''}`;
|
|
192
|
+
orchestrator.addReferenceContent(content, `GitHub issue #${value}`);
|
|
193
|
+
logger.info(`Added: GitHub issue #${value} ${detail.title}`);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
logger.warn(`Could not fetch issue #${value} — no GitHub remote detected or gh CLI unavailable`);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
// Full URL — use addReference which handles GitHub URLs
|
|
201
|
+
await orchestrator.addReference(value);
|
|
202
|
+
readyDeferred = createDeferred();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
await orchestrator.addReference(ref);
|
|
207
|
+
readyDeferred = createDeferred();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// Step 3: Advance to goals
|
|
212
|
+
await orchestrator.advanceToGoals();
|
|
213
|
+
await readyDeferred.promise;
|
|
214
|
+
readyDeferred = createDeferred();
|
|
215
|
+
// Step 4: Submit goals — this triggers codebase exploration + first question
|
|
216
|
+
// The onQuestion callback will fire, and we handle it after submitGoals returns
|
|
217
|
+
await orchestrator.submitGoals(opts.goals ?? '');
|
|
218
|
+
await readyDeferred.promise;
|
|
219
|
+
readyDeferred = createDeferred();
|
|
220
|
+
// Step 5: Skip to generation (auto-mode skips Q&A)
|
|
221
|
+
if (orchestrator.getPhase() === 'interview' ||
|
|
222
|
+
orchestrator.getPhase() === 'goals') {
|
|
223
|
+
await orchestrator.skipToGeneration();
|
|
224
|
+
}
|
|
225
|
+
// Wait for spec generation to complete (with timeout to prevent silent hangs)
|
|
226
|
+
const TIMEOUT_MS = opts.timeoutMs ?? 5 * 60 * 1000; // default: 5 minutes
|
|
227
|
+
let timeoutId;
|
|
228
|
+
const timeout = new Promise((_, reject) => {
|
|
229
|
+
timeoutId = setTimeout(() => reject(new Error('Spec generation timed out after 5 minutes')), TIMEOUT_MS);
|
|
230
|
+
});
|
|
231
|
+
try {
|
|
232
|
+
return await Promise.race([completionDeferred.promise, timeout]);
|
|
233
|
+
}
|
|
234
|
+
finally {
|
|
235
|
+
clearTimeout(timeoutId);
|
|
236
|
+
}
|
|
237
|
+
}
|
package/dist/commands/run.js
CHANGED
|
@@ -74,15 +74,24 @@ export async function runCommand(feature, options = {}) {
|
|
|
74
74
|
}
|
|
75
75
|
// Load config
|
|
76
76
|
const config = await loadConfigWithDefaults(projectRoot);
|
|
77
|
-
// Validate spec file exists
|
|
78
|
-
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
77
|
+
// Validate spec file exists (skip when resuming — spec lives on the feature branch)
|
|
78
|
+
let specFile = null;
|
|
79
|
+
if (options.resume) {
|
|
80
|
+
// Best-effort: find it if it's here, but don't fail
|
|
81
|
+
specFile = await validateSpecFile(projectRoot, feature);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
specFile = await validateSpecFile(projectRoot, feature);
|
|
85
|
+
if (!specFile) {
|
|
86
|
+
logger.error(`Spec file not found: ${feature}.md`);
|
|
87
|
+
logger.info(`Create the spec first: wiggum new ${feature}`);
|
|
88
|
+
logger.info(`Expected location: ${join(projectRoot, config.paths.specs, `${feature}.md`)}`);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (specFile) {
|
|
93
|
+
logger.info(`Found spec: ${specFile}`);
|
|
84
94
|
}
|
|
85
|
-
logger.info(`Found spec: ${specFile}`);
|
|
86
95
|
// Find the feature-loop.sh script
|
|
87
96
|
const scriptPath = findFeatureLoopScript(projectRoot);
|
|
88
97
|
if (!scriptPath) {
|
|
@@ -121,7 +130,7 @@ export async function runCommand(feature, options = {}) {
|
|
|
121
130
|
// Display configuration
|
|
122
131
|
console.log(pc.cyan('--- Run Configuration ---'));
|
|
123
132
|
console.log(` Feature: ${pc.bold(feature)}`);
|
|
124
|
-
console.log(` Spec: ${specFile}`);
|
|
133
|
+
console.log(` Spec: ${specFile ?? '(on feature branch)'}`);
|
|
125
134
|
console.log(` Max Iterations: ${maxIterations}`);
|
|
126
135
|
console.log(` Max E2E Attempts: ${maxE2eAttempts}`);
|
|
127
136
|
console.log(` Model: ${options.model || config.loop.defaultModel}`);
|
|
@@ -167,7 +176,8 @@ export async function runCommand(feature, options = {}) {
|
|
|
167
176
|
else {
|
|
168
177
|
logger.error(`Feature loop exited with code: ${code}`);
|
|
169
178
|
logger.info('Use --resume to continue from where you left off');
|
|
170
|
-
|
|
179
|
+
process.exitCode = code || 1;
|
|
180
|
+
resolve();
|
|
171
181
|
}
|
|
172
182
|
});
|
|
173
183
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless Sync Command
|
|
3
|
+
* Runs project scan + AI enhancement and persists context to .ralph/.context.json
|
|
4
|
+
* CLI equivalent of the TUI /sync command, for non-interactive use.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Pure sync logic — scans, enhances, persists context.
|
|
8
|
+
* Returns the context file path on success. Throws on failure.
|
|
9
|
+
* Safe to call from tools/agents (no process.exit).
|
|
10
|
+
*/
|
|
11
|
+
export declare function syncProjectContext(projectRoot: string): Promise<string>;
|
|
12
|
+
/**
|
|
13
|
+
* CLI entry point — wraps syncProjectContext with process.exit behavior.
|
|
14
|
+
*/
|
|
15
|
+
export declare function syncCommand(): Promise<void>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless Sync Command
|
|
3
|
+
* Runs project scan + AI enhancement and persists context to .ralph/.context.json
|
|
4
|
+
* CLI equivalent of the TUI /sync command, for non-interactive use.
|
|
5
|
+
*/
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { logger } from '../utils/logger.js';
|
|
8
|
+
import { Scanner } from '../scanner/index.js';
|
|
9
|
+
import { AIEnhancer } from '../ai/enhancer.js';
|
|
10
|
+
import { saveContext, toPersistedScanResult, toPersistedAIAnalysis, getGitMetadata, } from '../context/index.js';
|
|
11
|
+
import { getAvailableProvider, AVAILABLE_MODELS, normalizeModelId, } from '../ai/providers.js';
|
|
12
|
+
/**
|
|
13
|
+
* Pure sync logic — scans, enhances, persists context.
|
|
14
|
+
* Returns the context file path on success. Throws on failure.
|
|
15
|
+
* Safe to call from tools/agents (no process.exit).
|
|
16
|
+
*/
|
|
17
|
+
export async function syncProjectContext(projectRoot) {
|
|
18
|
+
// Detect provider
|
|
19
|
+
const provider = getAvailableProvider();
|
|
20
|
+
if (!provider) {
|
|
21
|
+
throw new Error('No AI provider available. Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or OPENROUTER_API_KEY.');
|
|
22
|
+
}
|
|
23
|
+
// Resolve model
|
|
24
|
+
const recommendedModel = AVAILABLE_MODELS[provider].find((m) => m.hint?.includes('recommended'));
|
|
25
|
+
const defaultModel = recommendedModel?.value ?? AVAILABLE_MODELS[provider][0].value;
|
|
26
|
+
const model = normalizeModelId(provider, defaultModel);
|
|
27
|
+
logger.info('Scanning project...');
|
|
28
|
+
// Step 1: Scan
|
|
29
|
+
const scanner = new Scanner();
|
|
30
|
+
const scanResult = await scanner.scan(projectRoot);
|
|
31
|
+
logger.info('Running AI analysis...');
|
|
32
|
+
// Step 2: AI enhancement
|
|
33
|
+
const enhancer = new AIEnhancer({
|
|
34
|
+
provider,
|
|
35
|
+
model,
|
|
36
|
+
agentic: true,
|
|
37
|
+
});
|
|
38
|
+
const enhanced = await enhancer.enhance(scanResult);
|
|
39
|
+
if (enhanced.aiError) {
|
|
40
|
+
throw new Error(`AI analysis failed: ${enhanced.aiError}`);
|
|
41
|
+
}
|
|
42
|
+
// Step 3: Persist
|
|
43
|
+
const git = await getGitMetadata(projectRoot);
|
|
44
|
+
await saveContext({
|
|
45
|
+
lastAnalyzedAt: new Date().toISOString(),
|
|
46
|
+
gitCommitHash: git.gitCommitHash,
|
|
47
|
+
gitBranch: git.gitBranch,
|
|
48
|
+
scanResult: toPersistedScanResult(enhanced),
|
|
49
|
+
aiAnalysis: toPersistedAIAnalysis(enhanced.aiAnalysis),
|
|
50
|
+
}, projectRoot);
|
|
51
|
+
return join(projectRoot, '.ralph', '.context.json');
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* CLI entry point — wraps syncProjectContext with process.exit behavior.
|
|
55
|
+
*/
|
|
56
|
+
export async function syncCommand() {
|
|
57
|
+
let contextPath;
|
|
58
|
+
try {
|
|
59
|
+
contextPath = await syncProjectContext(process.cwd());
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
return; // unreachable, but satisfies TS control flow
|
|
65
|
+
}
|
|
66
|
+
console.log(contextPath);
|
|
67
|
+
process.exit(0);
|
|
68
|
+
}
|