wiggum-cli 0.17.2 → 0.18.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 +58 -14
- package/dist/agent/orchestrator.d.ts +21 -3
- package/dist/agent/orchestrator.js +394 -187
- package/dist/agent/resolve-config.js +1 -1
- package/dist/agent/scheduler.d.ts +29 -0
- package/dist/agent/scheduler.js +1149 -0
- package/dist/agent/tools/backlog.d.ts +6 -0
- package/dist/agent/tools/backlog.js +23 -4
- package/dist/agent/tools/execution.js +1 -1
- package/dist/agent/tools/introspection.js +26 -4
- package/dist/agent/types.d.ts +113 -0
- package/dist/ai/conversation/url-fetcher.js +46 -13
- package/dist/ai/enhancer.js +1 -2
- package/dist/ai/providers.js +4 -4
- package/dist/commands/agent.d.ts +1 -0
- package/dist/commands/agent.js +53 -1
- package/dist/commands/config.js +100 -6
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.js +47 -2
- package/dist/commands/sync.js +2 -2
- package/dist/generator/config.js +13 -2
- package/dist/index.js +11 -3
- package/dist/repl/command-parser.d.ts +1 -1
- package/dist/repl/command-parser.js +1 -1
- package/dist/templates/config/ralph.config.cjs.tmpl +9 -2
- package/dist/templates/prompts/PROMPT_e2e.md.tmpl +16 -89
- package/dist/templates/prompts/PROMPT_e2e_fix.md.tmpl +55 -0
- package/dist/templates/prompts/PROMPT_feature.md.tmpl +12 -98
- package/dist/templates/prompts/PROMPT_review_auto.md.tmpl +52 -49
- package/dist/templates/prompts/PROMPT_review_manual.md.tmpl +30 -2
- package/dist/templates/prompts/PROMPT_review_merge.md.tmpl +59 -69
- package/dist/templates/prompts/PROMPT_verify.md.tmpl +7 -0
- package/dist/templates/root/README.md.tmpl +2 -3
- package/dist/templates/scripts/feature-loop.sh.tmpl +835 -93
- package/dist/templates/scripts/loop.sh.tmpl +5 -1
- package/dist/templates/scripts/ralph-monitor.sh.tmpl +0 -2
- package/dist/tui/app.d.ts +5 -1
- package/dist/tui/app.js +22 -3
- package/dist/tui/components/HeaderContent.d.ts +4 -1
- package/dist/tui/components/HeaderContent.js +4 -2
- package/dist/tui/hooks/useAgentOrchestrator.d.ts +2 -1
- package/dist/tui/hooks/useAgentOrchestrator.js +86 -33
- package/dist/tui/hooks/useInit.d.ts +5 -1
- package/dist/tui/hooks/useInit.js +20 -2
- package/dist/tui/screens/AgentScreen.js +3 -1
- package/dist/tui/screens/InitScreen.js +12 -1
- package/dist/tui/screens/MainShell.js +70 -6
- package/dist/tui/screens/RunScreen.d.ts +6 -2
- package/dist/tui/screens/RunScreen.js +48 -6
- package/dist/tui/utils/loop-status.d.ts +15 -0
- package/dist/tui/utils/loop-status.js +89 -27
- package/dist/tui/utils/polishGoal.js +14 -1
- package/dist/utils/config.d.ts +7 -0
- package/dist/utils/config.js +14 -0
- package/dist/utils/env.js +7 -1
- package/dist/utils/github.d.ts +13 -0
- package/dist/utils/github.js +63 -4
- package/dist/utils/logger.js +1 -1
- package/package.json +9 -7
- package/src/templates/config/ralph.config.cjs.tmpl +9 -2
- package/src/templates/prompts/PROMPT_e2e.md.tmpl +16 -89
- package/src/templates/prompts/PROMPT_e2e_fix.md.tmpl +55 -0
- package/src/templates/prompts/PROMPT_feature.md.tmpl +12 -98
- package/src/templates/prompts/PROMPT_review_auto.md.tmpl +52 -49
- package/src/templates/prompts/PROMPT_review_manual.md.tmpl +30 -2
- package/src/templates/prompts/PROMPT_review_merge.md.tmpl +59 -69
- package/src/templates/prompts/PROMPT_verify.md.tmpl +7 -0
- package/src/templates/root/README.md.tmpl +2 -3
- package/src/templates/scripts/feature-loop.sh.tmpl +835 -93
- package/src/templates/scripts/loop.sh.tmpl +5 -1
- package/src/templates/scripts/ralph-monitor.sh.tmpl +0 -2
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export interface BacklogToolsOptions {
|
|
2
2
|
defaultLabels?: string[];
|
|
3
3
|
issueNumbers?: number[];
|
|
4
|
+
scopeListIssuesToIssueNumbers?: boolean;
|
|
5
|
+
scopeReadIssueToIssueNumbers?: boolean;
|
|
6
|
+
allowGlobalBugDuplicateChecks?: boolean;
|
|
4
7
|
}
|
|
5
8
|
export declare function createBacklogTools(owner: string, repo: string, options?: BacklogToolsOptions): {
|
|
6
9
|
listIssues: import("ai").Tool<{
|
|
@@ -20,9 +23,12 @@ export declare function createBacklogTools(owner: string, repo: string, options?
|
|
|
20
23
|
error: string;
|
|
21
24
|
} | {
|
|
22
25
|
dependsOn: number[];
|
|
26
|
+
number: number;
|
|
23
27
|
title: string;
|
|
24
28
|
body: string;
|
|
25
29
|
labels: string[];
|
|
30
|
+
state: "open" | "closed";
|
|
31
|
+
createdAt: string;
|
|
26
32
|
error?: undefined;
|
|
27
33
|
}>;
|
|
28
34
|
};
|
|
@@ -8,6 +8,10 @@ function extractDependencyHints(body) {
|
|
|
8
8
|
return [...new Set(numbers)].sort((a, b) => a - b);
|
|
9
9
|
}
|
|
10
10
|
export function createBacklogTools(owner, repo, options = {}) {
|
|
11
|
+
const issueNumberSet = options.issueNumbers?.length
|
|
12
|
+
? new Set(options.issueNumbers)
|
|
13
|
+
: undefined;
|
|
14
|
+
const listVisibleIssueNumbers = new Set();
|
|
11
15
|
const listIssues = tool({
|
|
12
16
|
description: 'List open GitHub issues from the backlog, optionally filtered by labels or milestone.',
|
|
13
17
|
inputSchema: zodSchema(z.object({
|
|
@@ -30,10 +34,19 @@ export function createBacklogTools(owner, repo, options = {}) {
|
|
|
30
34
|
return { issues: [], error: result.error };
|
|
31
35
|
// Sort by issue number ascending — lower numbers are typically more foundational
|
|
32
36
|
const sorted = [...result.issues].sort((a, b) => a.number - b.number);
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
const isBugDuplicateCheck = options.allowGlobalBugDuplicateChecks === true
|
|
38
|
+
&& milestone == null
|
|
39
|
+
&& uniqueLabels.length === 1
|
|
40
|
+
&& uniqueLabels[0] === 'bug';
|
|
41
|
+
if (issueNumberSet && options.scopeListIssuesToIssueNumbers !== false && !isBugDuplicateCheck) {
|
|
42
|
+
const filtered = sorted.filter(i => issueNumberSet.has(Number(i.number)));
|
|
43
|
+
for (const issue of filtered)
|
|
44
|
+
listVisibleIssueNumbers.add(Number(issue.number));
|
|
45
|
+
return { issues: filtered };
|
|
46
|
+
}
|
|
47
|
+
for (const issue of sorted)
|
|
48
|
+
listVisibleIssueNumbers.add(Number(issue.number));
|
|
49
|
+
return { issues: sorted };
|
|
37
50
|
},
|
|
38
51
|
});
|
|
39
52
|
const readIssue = tool({
|
|
@@ -42,6 +55,12 @@ export function createBacklogTools(owner, repo, options = {}) {
|
|
|
42
55
|
issueNumber: z.number().int().min(1).describe('The issue number to read'),
|
|
43
56
|
})),
|
|
44
57
|
execute: async ({ issueNumber }) => {
|
|
58
|
+
if (issueNumberSet
|
|
59
|
+
&& options.scopeReadIssueToIssueNumbers !== false
|
|
60
|
+
&& !issueNumberSet.has(issueNumber)
|
|
61
|
+
&& !listVisibleIssueNumbers.has(issueNumber)) {
|
|
62
|
+
return { error: `Issue #${issueNumber} is outside the selected worker scope` };
|
|
63
|
+
}
|
|
45
64
|
const detail = await fetchGitHubIssue(owner, repo, issueNumber);
|
|
46
65
|
if (!detail)
|
|
47
66
|
return { error: `Issue #${issueNumber} not found` };
|
|
@@ -106,7 +106,7 @@ export function createExecutionTools(projectRoot, options) {
|
|
|
106
106
|
},
|
|
107
107
|
});
|
|
108
108
|
const runLoop = tool({
|
|
109
|
-
description: 'Run the development loop for a feature. Spawns a background process and returns when complete. The loop uses
|
|
109
|
+
description: 'Run the development loop for a feature. Spawns a background process and returns when complete. The loop uses the coding CLI configured in ralph.config.cjs; do NOT forward the agent model here.',
|
|
110
110
|
inputSchema: zodSchema(z.object({
|
|
111
111
|
featureName: FEATURE_NAME_SCHEMA,
|
|
112
112
|
worktree: z.boolean().default(true).describe('Use git worktree isolation'),
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { tool, zodSchema } from 'ai';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import {
|
|
4
|
-
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { readFile, stat, open } from 'node:fs/promises';
|
|
5
4
|
import { join } from 'node:path';
|
|
6
5
|
import { FEATURE_NAME_SCHEMA } from './schemas.js';
|
|
6
|
+
const MAX_LOG_BYTES = 1_048_576; // 1 MB
|
|
7
7
|
export function createIntrospectionTools(projectRoot) {
|
|
8
8
|
const readLoopLog = tool({
|
|
9
9
|
description: 'Read the stdout/stderr log of a development loop (running or completed).',
|
|
@@ -13,10 +13,32 @@ export function createIntrospectionTools(projectRoot) {
|
|
|
13
13
|
})),
|
|
14
14
|
execute: async ({ featureName, tailLines }) => {
|
|
15
15
|
const logPath = join('/tmp', `ralph-loop-${featureName}.log`);
|
|
16
|
-
|
|
16
|
+
let fileSize;
|
|
17
|
+
try {
|
|
18
|
+
const fileStat = await stat(logPath);
|
|
19
|
+
fileSize = fileStat.size;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
17
22
|
return { error: `No log found at ${logPath} — verify featureName matches exactly what runLoop used` };
|
|
18
23
|
}
|
|
19
|
-
|
|
24
|
+
let content;
|
|
25
|
+
if (fileSize <= MAX_LOG_BYTES) {
|
|
26
|
+
content = await readFile(logPath, 'utf-8');
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
// For large files, read only the last MAX_LOG_BYTES to bound memory usage.
|
|
30
|
+
// totalLines will reflect lines in the chunk, not the full file.
|
|
31
|
+
const offset = fileSize - MAX_LOG_BYTES;
|
|
32
|
+
const fd = await open(logPath, 'r');
|
|
33
|
+
try {
|
|
34
|
+
const buffer = Buffer.allocUnsafe(MAX_LOG_BYTES);
|
|
35
|
+
const { bytesRead } = await fd.read(buffer, 0, MAX_LOG_BYTES, offset);
|
|
36
|
+
content = buffer.subarray(0, bytesRead).toString('utf-8');
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
await fd.close();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
20
42
|
const allLines = content.split('\n');
|
|
21
43
|
const lines = allLines.slice(-tailLines);
|
|
22
44
|
return { lines, totalLines: allLines.length };
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
import type { LanguageModel } from 'ai';
|
|
2
2
|
export type ReviewMode = 'manual' | 'auto' | 'merge';
|
|
3
|
+
export type DependencyKind = 'explicit' | 'inferred';
|
|
4
|
+
export type DependencyConfidence = 'high' | 'medium' | 'low';
|
|
5
|
+
export type TaskActionability = 'ready' | 'housekeeping' | 'waiting_pr' | 'blocked_dependency' | 'blocked_cycle' | 'blocked_out_of_scope';
|
|
6
|
+
export type AttemptState = 'never_tried' | 'partial' | 'failure' | 'success' | 'skipped';
|
|
7
|
+
export type PriorityTier = 'P0' | 'P1' | 'P2' | 'unlabeled';
|
|
8
|
+
export type ScopeOrigin = 'requested' | 'dependency';
|
|
9
|
+
export interface DependencyEvidence {
|
|
10
|
+
summary: string;
|
|
11
|
+
codebaseSignals?: string[];
|
|
12
|
+
backlogSignals?: string[];
|
|
13
|
+
}
|
|
14
|
+
export interface DependencyEdge {
|
|
15
|
+
sourceIssue: number;
|
|
16
|
+
targetIssue: number;
|
|
17
|
+
kind: DependencyKind;
|
|
18
|
+
confidence: DependencyConfidence;
|
|
19
|
+
evidence: DependencyEvidence;
|
|
20
|
+
blocking: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface SelectionReason {
|
|
23
|
+
kind: 'priority' | 'explicit_dependency' | 'inferred_dependency' | 'scope_expansion' | 'retry' | 'existing_work' | 'housekeeping' | 'blocked' | 'tie_break';
|
|
24
|
+
message: string;
|
|
25
|
+
confidence?: DependencyConfidence;
|
|
26
|
+
issueNumber?: number;
|
|
27
|
+
}
|
|
28
|
+
export interface TaskScoreBreakdown {
|
|
29
|
+
actionability: number;
|
|
30
|
+
retryResume: number;
|
|
31
|
+
priority: number;
|
|
32
|
+
dependencyHint: number;
|
|
33
|
+
existingWork: number;
|
|
34
|
+
issueNumber: number;
|
|
35
|
+
total: number;
|
|
36
|
+
}
|
|
3
37
|
export interface AgentConfig {
|
|
4
38
|
model: LanguageModel;
|
|
5
39
|
modelId?: string;
|
|
@@ -14,6 +48,7 @@ export interface AgentConfig {
|
|
|
14
48
|
reviewMode?: ReviewMode;
|
|
15
49
|
dryRun?: boolean;
|
|
16
50
|
onStepUpdate?: (event: AgentStepEvent) => void;
|
|
51
|
+
onOrchestratorEvent?: (event: AgentOrchestratorEvent) => void;
|
|
17
52
|
onProgress?: (toolName: string, line: string) => void;
|
|
18
53
|
}
|
|
19
54
|
export interface AgentStepEvent {
|
|
@@ -33,14 +68,92 @@ export interface AgentLogEntry {
|
|
|
33
68
|
level: 'info' | 'warn' | 'error' | 'success';
|
|
34
69
|
}
|
|
35
70
|
export type AgentPhase = 'idle' | 'planning' | 'generating_spec' | 'running_loop' | 'reporting' | 'reflecting';
|
|
71
|
+
export interface FeatureStateSummary {
|
|
72
|
+
recommendation?: string;
|
|
73
|
+
hasExistingBranch?: boolean;
|
|
74
|
+
commitsAhead?: number;
|
|
75
|
+
hasPlan?: boolean;
|
|
76
|
+
hasOpenPr?: boolean;
|
|
77
|
+
}
|
|
36
78
|
export interface AgentIssueState {
|
|
37
79
|
issueNumber: number;
|
|
38
80
|
title: string;
|
|
39
81
|
labels: string[];
|
|
40
82
|
phase: AgentPhase;
|
|
83
|
+
scopeOrigin?: ScopeOrigin;
|
|
84
|
+
requestedBy?: number[];
|
|
85
|
+
actionability?: TaskActionability;
|
|
86
|
+
priorityTier?: PriorityTier;
|
|
87
|
+
dependsOn?: number[];
|
|
88
|
+
inferredDependsOn?: Array<{
|
|
89
|
+
issueNumber: number;
|
|
90
|
+
confidence: DependencyConfidence;
|
|
91
|
+
}>;
|
|
92
|
+
blockedBy?: Array<{
|
|
93
|
+
issueNumber: number;
|
|
94
|
+
reason: string;
|
|
95
|
+
confidence?: DependencyConfidence;
|
|
96
|
+
}>;
|
|
97
|
+
recommendation?: string;
|
|
98
|
+
selectionReasons?: SelectionReason[];
|
|
99
|
+
score?: TaskScoreBreakdown;
|
|
100
|
+
attemptState?: AttemptState;
|
|
101
|
+
featureState?: FeatureStateSummary;
|
|
41
102
|
loopPhase?: string;
|
|
42
103
|
loopFeatureName?: string;
|
|
43
104
|
loopIterations?: number;
|
|
44
105
|
prUrl?: string;
|
|
45
106
|
error?: string;
|
|
46
107
|
}
|
|
108
|
+
export interface BacklogCandidate extends AgentIssueState {
|
|
109
|
+
body: string;
|
|
110
|
+
createdAt: string;
|
|
111
|
+
explicitDependencyEdges: DependencyEdge[];
|
|
112
|
+
inferredDependencyEdges: DependencyEdge[];
|
|
113
|
+
}
|
|
114
|
+
export interface ScopeExpansion {
|
|
115
|
+
issueNumber: number;
|
|
116
|
+
requestedBy: number[];
|
|
117
|
+
}
|
|
118
|
+
export type AgentOrchestratorEvent = {
|
|
119
|
+
type: 'scope_expanded';
|
|
120
|
+
expansions: ScopeExpansion[];
|
|
121
|
+
} | {
|
|
122
|
+
type: 'backlog_progress';
|
|
123
|
+
phase: 'listing' | 'scope_expansion' | 'hydration' | 'enrichment' | 'dependency_inference' | 'ranking';
|
|
124
|
+
message: string;
|
|
125
|
+
completed?: number;
|
|
126
|
+
total?: number;
|
|
127
|
+
} | {
|
|
128
|
+
type: 'backlog_timing';
|
|
129
|
+
phase: 'listing' | 'scope_expansion' | 'hydration' | 'enrichment' | 'dependency_inference' | 'ranking';
|
|
130
|
+
durationMs: number;
|
|
131
|
+
count?: number;
|
|
132
|
+
} | {
|
|
133
|
+
type: 'backlog_scanned';
|
|
134
|
+
total: number;
|
|
135
|
+
issues: AgentIssueState[];
|
|
136
|
+
} | {
|
|
137
|
+
type: 'candidate_enriched';
|
|
138
|
+
issue: AgentIssueState;
|
|
139
|
+
} | {
|
|
140
|
+
type: 'dependencies_inferred';
|
|
141
|
+
issueNumber: number;
|
|
142
|
+
edges: DependencyEdge[];
|
|
143
|
+
} | {
|
|
144
|
+
type: 'queue_ranked';
|
|
145
|
+
queue: AgentIssueState[];
|
|
146
|
+
} | {
|
|
147
|
+
type: 'task_selected';
|
|
148
|
+
issue: AgentIssueState;
|
|
149
|
+
} | {
|
|
150
|
+
type: 'task_blocked';
|
|
151
|
+
issue: AgentIssueState;
|
|
152
|
+
} | {
|
|
153
|
+
type: 'task_started';
|
|
154
|
+
issue: AgentIssueState;
|
|
155
|
+
} | {
|
|
156
|
+
type: 'task_completed';
|
|
157
|
+
issue: AgentIssueState;
|
|
158
|
+
outcome: 'success' | 'partial' | 'failure' | 'skipped' | 'unknown';
|
|
159
|
+
};
|
|
@@ -24,25 +24,58 @@ export function isUrl(input) {
|
|
|
24
24
|
* Simple extraction that removes scripts, styles, and HTML tags
|
|
25
25
|
*/
|
|
26
26
|
function extractTextFromHtml(html) {
|
|
27
|
-
// Remove script and
|
|
28
|
-
let text = html
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
.replace(/<noscript[^>]*>[\s\S]*?<\/noscript>/gi, '');
|
|
27
|
+
// Remove script, style, and noscript blocks before generic tag stripping.
|
|
28
|
+
let text = stripElementBlocks(html, 'script');
|
|
29
|
+
text = stripElementBlocks(text, 'style');
|
|
30
|
+
text = stripElementBlocks(text, 'noscript');
|
|
32
31
|
// Remove HTML tags but keep content
|
|
33
32
|
text = text.replace(/<[^>]+>/g, ' ');
|
|
34
|
-
// Decode
|
|
35
|
-
text = text
|
|
36
|
-
.replace(/ /g, ' ')
|
|
37
|
-
.replace(/&/g, '&')
|
|
38
|
-
.replace(/</g, '<')
|
|
39
|
-
.replace(/>/g, '>')
|
|
40
|
-
.replace(/"/g, '"')
|
|
41
|
-
.replace(/'/g, "'");
|
|
33
|
+
// Decode only safe presentation entities; keep angle brackets encoded.
|
|
34
|
+
text = decodeSafeHtmlEntities(text);
|
|
42
35
|
// Clean up whitespace
|
|
43
36
|
text = text.replace(/\s+/g, ' ').trim();
|
|
44
37
|
return text;
|
|
45
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Remove full HTML element blocks (open tag + content + closing tag) using
|
|
41
|
+
* deterministic string scanning instead of regex.
|
|
42
|
+
*/
|
|
43
|
+
function stripElementBlocks(input, tagName) {
|
|
44
|
+
let output = input;
|
|
45
|
+
const openToken = `<${tagName}`;
|
|
46
|
+
const closeToken = `</${tagName}`;
|
|
47
|
+
while (true) {
|
|
48
|
+
const lower = output.toLowerCase();
|
|
49
|
+
const openStart = lower.indexOf(openToken);
|
|
50
|
+
if (openStart === -1) {
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
const openEnd = lower.indexOf('>', openStart + openToken.length);
|
|
54
|
+
if (openEnd === -1) {
|
|
55
|
+
output = output.slice(0, openStart);
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
const closeStart = lower.indexOf(closeToken, openEnd + 1);
|
|
59
|
+
if (closeStart === -1) {
|
|
60
|
+
output = output.slice(0, openStart);
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
const closeEnd = lower.indexOf('>', closeStart + closeToken.length);
|
|
64
|
+
if (closeEnd === -1) {
|
|
65
|
+
output = output.slice(0, openStart);
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
output = output.slice(0, openStart) + output.slice(closeEnd + 1);
|
|
69
|
+
}
|
|
70
|
+
return output;
|
|
71
|
+
}
|
|
72
|
+
/** Decode non-structural entities only (quotes/spaces), preserving `<`/`>`/`&`. */
|
|
73
|
+
function decodeSafeHtmlEntities(input) {
|
|
74
|
+
return input
|
|
75
|
+
.replace(/ /gi, ' ')
|
|
76
|
+
.replace(/"/gi, '"')
|
|
77
|
+
.replace(/'/g, "'");
|
|
78
|
+
}
|
|
46
79
|
/**
|
|
47
80
|
* Fetch content from a URL
|
|
48
81
|
*/
|
package/dist/ai/enhancer.js
CHANGED
|
@@ -102,11 +102,10 @@ export class AIEnhancer {
|
|
|
102
102
|
async enhance(scanResult) {
|
|
103
103
|
// Check if API key is available
|
|
104
104
|
if (!this.isAvailable()) {
|
|
105
|
-
const envVar = this.getRequiredEnvVar();
|
|
106
105
|
return {
|
|
107
106
|
...scanResult,
|
|
108
107
|
aiEnhanced: false,
|
|
109
|
-
aiError:
|
|
108
|
+
aiError: 'API key not found. Configure credentials to enable AI enhancement.',
|
|
110
109
|
};
|
|
111
110
|
}
|
|
112
111
|
try {
|
package/dist/ai/providers.js
CHANGED
|
@@ -100,11 +100,11 @@ export function hasApiKey(provider) {
|
|
|
100
100
|
*/
|
|
101
101
|
function getApiKey(provider) {
|
|
102
102
|
const envVar = API_KEY_ENV_VARS[provider];
|
|
103
|
-
const
|
|
104
|
-
if (!
|
|
105
|
-
throw new Error(`API key not found
|
|
103
|
+
const credential = process.env[envVar];
|
|
104
|
+
if (!credential) {
|
|
105
|
+
throw new Error(`API key not found for provider: ${provider}.`);
|
|
106
106
|
}
|
|
107
|
-
return
|
|
107
|
+
return credential;
|
|
108
108
|
}
|
|
109
109
|
/**
|
|
110
110
|
* Get a configured AI model for the specified provider
|
package/dist/commands/agent.d.ts
CHANGED
package/dist/commands/agent.js
CHANGED
|
@@ -9,8 +9,25 @@ import { logger } from '../utils/logger.js';
|
|
|
9
9
|
import { createAgentOrchestrator, } from '../agent/orchestrator.js';
|
|
10
10
|
import { resolveAgentEnv } from '../agent/resolve-config.js';
|
|
11
11
|
import { initTracing, flushTracing, traced, currentSpan } from '../utils/tracing.js';
|
|
12
|
+
import { detectGitHubRemote, runGitHubDiagnostics } from '../utils/github.js';
|
|
12
13
|
export async function agentCommand(options = {}) {
|
|
13
14
|
const projectRoot = process.cwd();
|
|
15
|
+
if (options.diagnoseGh) {
|
|
16
|
+
const repo = await detectGitHubRemote(projectRoot);
|
|
17
|
+
if (!repo) {
|
|
18
|
+
console.error('Error: No GitHub remote detected. Run this inside a git repo with an origin remote.');
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
const diagnostics = await runGitHubDiagnostics(repo.owner, repo.repo, options.issues);
|
|
22
|
+
for (const check of diagnostics.checks) {
|
|
23
|
+
const status = check.ok ? 'OK' : 'FAIL';
|
|
24
|
+
console.log(`[diagnose-gh] ${status} ${check.name}: ${check.message}`);
|
|
25
|
+
}
|
|
26
|
+
if (!diagnostics.success) {
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
14
31
|
// Initialize Braintrust tracing (no-op if BRAINTRUST_API_KEY not set)
|
|
15
32
|
initTracing();
|
|
16
33
|
// Resolve provider, model, and GitHub remote
|
|
@@ -19,7 +36,7 @@ export async function agentCommand(options = {}) {
|
|
|
19
36
|
env = await resolveAgentEnv(projectRoot, { model: options.model });
|
|
20
37
|
}
|
|
21
38
|
catch (err) {
|
|
22
|
-
|
|
39
|
+
logger.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
23
40
|
process.exit(1);
|
|
24
41
|
}
|
|
25
42
|
const { provider, model, modelId, owner, repo } = env;
|
|
@@ -51,6 +68,38 @@ export async function agentCommand(options = {}) {
|
|
|
51
68
|
log(`[tool:done] ${tr.toolName} → ${summary}`);
|
|
52
69
|
}
|
|
53
70
|
},
|
|
71
|
+
onOrchestratorEvent: (event) => {
|
|
72
|
+
const log = options.stream
|
|
73
|
+
? (msg) => process.stdout.write(`${msg}\n`)
|
|
74
|
+
: (msg) => logger.info(msg);
|
|
75
|
+
switch (event.type) {
|
|
76
|
+
case 'scope_expanded':
|
|
77
|
+
log(`[orchestrator] expanded scope with ${event.expansions.map(expansion => `#${expansion.issueNumber}`).join(', ')}`);
|
|
78
|
+
break;
|
|
79
|
+
case 'backlog_progress':
|
|
80
|
+
log(`[orchestrator] ${event.message}`);
|
|
81
|
+
break;
|
|
82
|
+
case 'backlog_timing':
|
|
83
|
+
log(`[orchestrator] ${event.phase} took ${event.durationMs}ms${event.count != null ? ` (${event.count})` : ''}`);
|
|
84
|
+
break;
|
|
85
|
+
case 'queue_ranked':
|
|
86
|
+
log(`[orchestrator] ranked ${event.queue.length} issue(s)`);
|
|
87
|
+
break;
|
|
88
|
+
case 'task_selected': {
|
|
89
|
+
const reason = event.issue.selectionReasons?.[0]?.message;
|
|
90
|
+
log(`[orchestrator] selected #${event.issue.issueNumber}${reason ? ` — ${reason}` : ''}`);
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
case 'task_blocked':
|
|
94
|
+
log(`[orchestrator] blocked #${event.issue.issueNumber} — ${event.issue.blockedBy?.[0]?.reason ?? event.issue.actionability ?? 'blocked'}`);
|
|
95
|
+
break;
|
|
96
|
+
case 'task_completed':
|
|
97
|
+
log(`[orchestrator] completed #${event.issue.issueNumber} (${event.outcome})`);
|
|
98
|
+
break;
|
|
99
|
+
default:
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
},
|
|
54
103
|
onProgress: (toolName, line) => {
|
|
55
104
|
const log = options.stream
|
|
56
105
|
? (msg) => process.stdout.write(`${msg}\n`)
|
|
@@ -107,6 +156,9 @@ export async function agentCommand(options = {}) {
|
|
|
107
156
|
catch (err) {
|
|
108
157
|
const message = err instanceof Error ? err.message : String(err);
|
|
109
158
|
console.error(`Error: Agent failed — ${message}`);
|
|
159
|
+
if (message.includes('GitHub') || message.includes('gh ')) {
|
|
160
|
+
console.error(`Hint: run 'wiggum agent --diagnose-gh${options.issues?.length ? ` --issues ${options.issues.join(',')}` : ''}' to inspect GitHub connectivity.`);
|
|
161
|
+
}
|
|
110
162
|
process.exit(1);
|
|
111
163
|
}
|
|
112
164
|
finally {
|
package/dist/commands/config.js
CHANGED
|
@@ -9,6 +9,7 @@ import { logger } from '../utils/logger.js';
|
|
|
9
9
|
import { simpson } from '../utils/colors.js';
|
|
10
10
|
import { getAvailableProvider, AVAILABLE_MODELS } from '../ai/providers.js';
|
|
11
11
|
import { writeKeysToEnvFile } from '../utils/env.js';
|
|
12
|
+
import { loadConfigWithDefaults } from '../utils/config.js';
|
|
12
13
|
/**
|
|
13
14
|
* Supported services for API key configuration
|
|
14
15
|
*/
|
|
@@ -26,6 +27,11 @@ const CONFIGURABLE_SERVICES = {
|
|
|
26
27
|
description: 'AI tracing and analytics',
|
|
27
28
|
},
|
|
28
29
|
};
|
|
30
|
+
const LOOP_CLI_SETTINGS = ['cli', 'review-cli'];
|
|
31
|
+
const LOOP_CLI_VALUES = ['claude', 'codex'];
|
|
32
|
+
const DEFAULT_CLAUDE_IMPL_MODEL = 'sonnet';
|
|
33
|
+
const DEFAULT_CLAUDE_REVIEW_MODEL = 'opus';
|
|
34
|
+
const DEFAULT_CODEX_MODEL = 'gpt-5.3-codex';
|
|
29
35
|
/**
|
|
30
36
|
* Check if a service API key is configured
|
|
31
37
|
*/
|
|
@@ -45,6 +51,69 @@ function saveKeyToEnvLocal(projectRoot, envVar, value) {
|
|
|
45
51
|
const envLocalPath = path.join(ralphDir, '.env.local');
|
|
46
52
|
writeKeysToEnvFile(envLocalPath, { [envVar]: value });
|
|
47
53
|
}
|
|
54
|
+
function toConfigFileContent(config) {
|
|
55
|
+
const content = `module.exports = ${JSON.stringify(config, null, 2)};
|
|
56
|
+
`;
|
|
57
|
+
return content
|
|
58
|
+
.replace(/"(\w+)":/g, '$1:')
|
|
59
|
+
.replace(/: "([^"]+)"/g, ": '$1'");
|
|
60
|
+
}
|
|
61
|
+
function normalizeLoopCliSetting(raw) {
|
|
62
|
+
if (raw === 'cli')
|
|
63
|
+
return 'cli';
|
|
64
|
+
if (raw === 'review-cli' || raw === 'reviewCli')
|
|
65
|
+
return 'review-cli';
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
function isLoopCliValue(value) {
|
|
69
|
+
return LOOP_CLI_VALUES.includes(value);
|
|
70
|
+
}
|
|
71
|
+
function reconcileLoopModelsForCliSelection(loop, codingCli, reviewCli) {
|
|
72
|
+
let defaultModel = loop.defaultModel;
|
|
73
|
+
let planningModel = loop.planningModel;
|
|
74
|
+
const usesClaude = codingCli === 'claude' || reviewCli === 'claude';
|
|
75
|
+
const codexOnly = codingCli === 'codex' && reviewCli === 'codex';
|
|
76
|
+
if (usesClaude) {
|
|
77
|
+
if (defaultModel === DEFAULT_CODEX_MODEL) {
|
|
78
|
+
defaultModel = DEFAULT_CLAUDE_IMPL_MODEL;
|
|
79
|
+
}
|
|
80
|
+
if (planningModel === DEFAULT_CODEX_MODEL) {
|
|
81
|
+
planningModel = DEFAULT_CLAUDE_REVIEW_MODEL;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else if (codexOnly) {
|
|
85
|
+
if (defaultModel === DEFAULT_CLAUDE_IMPL_MODEL) {
|
|
86
|
+
defaultModel = DEFAULT_CODEX_MODEL;
|
|
87
|
+
}
|
|
88
|
+
if (planningModel === DEFAULT_CLAUDE_REVIEW_MODEL) {
|
|
89
|
+
planningModel = DEFAULT_CODEX_MODEL;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { defaultModel, planningModel };
|
|
93
|
+
}
|
|
94
|
+
async function saveLoopCliToConfig(projectRoot, setting, value) {
|
|
95
|
+
// Check that .ralph/ exists (project is initialized)
|
|
96
|
+
const ralphDir = path.join(projectRoot, '.ralph');
|
|
97
|
+
if (!fs.existsSync(ralphDir) || !fs.statSync(ralphDir).isDirectory()) {
|
|
98
|
+
throw new Error('This project is not initialized. Run \'wiggum init\' before using loop CLI settings.');
|
|
99
|
+
}
|
|
100
|
+
const configPath = path.join(projectRoot, 'ralph.config.cjs');
|
|
101
|
+
const config = await loadConfigWithDefaults(projectRoot);
|
|
102
|
+
const nextCodingCli = setting === 'cli' ? value : config.loop.codingCli;
|
|
103
|
+
const nextReviewCli = setting === 'review-cli' ? value : config.loop.reviewCli;
|
|
104
|
+
const { defaultModel, planningModel } = reconcileLoopModelsForCliSelection(config.loop, nextCodingCli, nextReviewCli);
|
|
105
|
+
const nextConfig = {
|
|
106
|
+
...config,
|
|
107
|
+
loop: {
|
|
108
|
+
...config.loop,
|
|
109
|
+
defaultModel,
|
|
110
|
+
planningModel,
|
|
111
|
+
codingCli: nextCodingCli,
|
|
112
|
+
reviewCli: nextReviewCli,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
fs.writeFileSync(configPath, toConfigFileContent(nextConfig), 'utf-8');
|
|
116
|
+
}
|
|
48
117
|
/**
|
|
49
118
|
* Display current configuration status
|
|
50
119
|
*/
|
|
@@ -75,6 +144,8 @@ function displayConfigStatus(state) {
|
|
|
75
144
|
console.log(` ${simpson.yellow('/config set tavily')} ${pc.dim('<api-key>')}`);
|
|
76
145
|
console.log(` ${simpson.yellow('/config set context7')} ${pc.dim('<api-key>')}`);
|
|
77
146
|
console.log(` ${simpson.yellow('/config set braintrust')} ${pc.dim('<api-key>')}`);
|
|
147
|
+
console.log(` ${simpson.yellow('/config set cli')} ${pc.dim('<claude|codex>')}`);
|
|
148
|
+
console.log(` ${simpson.yellow('/config set review-cli')} ${pc.dim('<claude|codex>')}`);
|
|
78
149
|
console.log('');
|
|
79
150
|
}
|
|
80
151
|
/**
|
|
@@ -93,17 +164,37 @@ export async function handleConfigCommand(args, state) {
|
|
|
93
164
|
}
|
|
94
165
|
// /config set <service> <key>
|
|
95
166
|
if (args.length < 3) {
|
|
96
|
-
logger.error('Usage: /config set <service> <
|
|
167
|
+
logger.error('Usage: /config set <service> <value>');
|
|
97
168
|
console.log('');
|
|
98
169
|
console.log('Available services:');
|
|
99
170
|
for (const [service, config] of Object.entries(CONFIGURABLE_SERVICES)) {
|
|
100
171
|
console.log(` ${service.padEnd(12)} ${pc.dim(config.description)}`);
|
|
101
172
|
}
|
|
173
|
+
for (const setting of LOOP_CLI_SETTINGS) {
|
|
174
|
+
console.log(` ${setting.padEnd(12)} ${pc.dim('Loop CLI setting')}`);
|
|
175
|
+
}
|
|
102
176
|
console.log('');
|
|
103
177
|
return state;
|
|
104
178
|
}
|
|
105
|
-
const
|
|
106
|
-
const
|
|
179
|
+
const rawService = args[1]?.toLowerCase() ?? '';
|
|
180
|
+
const value = args[2];
|
|
181
|
+
const loopCliSetting = normalizeLoopCliSetting(rawService);
|
|
182
|
+
if (loopCliSetting) {
|
|
183
|
+
if (!isLoopCliValue(value)) {
|
|
184
|
+
logger.error(`Invalid ${loopCliSetting} value: '${value}'. Allowed values: ${LOOP_CLI_VALUES.join(', ')}`);
|
|
185
|
+
return state;
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
await saveLoopCliToConfig(state.projectRoot, loopCliSetting, value);
|
|
189
|
+
logger.success(`${loopCliSetting} saved to ralph.config.cjs (${value})`);
|
|
190
|
+
console.log('');
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
logger.error(`Failed to save ${loopCliSetting}: ${error instanceof Error ? error.message : String(error)}`);
|
|
194
|
+
}
|
|
195
|
+
return state;
|
|
196
|
+
}
|
|
197
|
+
const service = rawService;
|
|
107
198
|
if (!(service in CONFIGURABLE_SERVICES)) {
|
|
108
199
|
logger.error(`Unknown service: ${service}`);
|
|
109
200
|
console.log('');
|
|
@@ -111,20 +202,23 @@ export async function handleConfigCommand(args, state) {
|
|
|
111
202
|
for (const [svc, config] of Object.entries(CONFIGURABLE_SERVICES)) {
|
|
112
203
|
console.log(` ${svc.padEnd(12)} ${pc.dim(config.description)}`);
|
|
113
204
|
}
|
|
205
|
+
for (const setting of LOOP_CLI_SETTINGS) {
|
|
206
|
+
console.log(` ${setting.padEnd(12)} ${pc.dim('Loop CLI setting')}`);
|
|
207
|
+
}
|
|
114
208
|
console.log('');
|
|
115
209
|
return state;
|
|
116
210
|
}
|
|
117
211
|
const { envVar } = CONFIGURABLE_SERVICES[service];
|
|
118
212
|
// Validate API key format (basic check)
|
|
119
|
-
if (!
|
|
213
|
+
if (!value || value.length < 10) {
|
|
120
214
|
logger.error('Invalid API key. Key appears too short.');
|
|
121
215
|
return state;
|
|
122
216
|
}
|
|
123
217
|
try {
|
|
124
218
|
// Save to .env.local
|
|
125
|
-
saveKeyToEnvLocal(state.projectRoot, envVar,
|
|
219
|
+
saveKeyToEnvLocal(state.projectRoot, envVar, value);
|
|
126
220
|
// Also set in current process environment
|
|
127
|
-
process.env[envVar] =
|
|
221
|
+
process.env[envVar] = value;
|
|
128
222
|
logger.success(`${envVar} saved to .ralph/.env.local`);
|
|
129
223
|
console.log(pc.dim('Restart Wiggum to apply changes to tool availability.'));
|
|
130
224
|
console.log('');
|
package/dist/commands/run.d.ts
CHANGED