wiggum-cli 0.16.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.
Files changed (97) hide show
  1. package/bin/ralph.js +0 -0
  2. package/dist/agent/memory/ingest.d.ts +14 -0
  3. package/dist/agent/memory/ingest.js +77 -0
  4. package/dist/agent/memory/store.d.ts +15 -0
  5. package/dist/agent/memory/store.js +98 -0
  6. package/dist/agent/memory/types.d.ts +16 -0
  7. package/dist/agent/memory/types.js +14 -0
  8. package/dist/agent/orchestrator.d.ts +7 -0
  9. package/dist/agent/orchestrator.js +266 -0
  10. package/dist/agent/resolve-config.d.ts +26 -0
  11. package/dist/agent/resolve-config.js +43 -0
  12. package/dist/agent/tools/backlog.d.ts +27 -0
  13. package/dist/agent/tools/backlog.js +51 -0
  14. package/dist/agent/tools/dry-run.d.ts +106 -0
  15. package/dist/agent/tools/dry-run.js +119 -0
  16. package/dist/agent/tools/execution.d.ts +51 -0
  17. package/dist/agent/tools/execution.js +256 -0
  18. package/dist/agent/tools/feature-state.d.ts +43 -0
  19. package/dist/agent/tools/feature-state.js +184 -0
  20. package/dist/agent/tools/introspection.d.ts +23 -0
  21. package/dist/agent/tools/introspection.js +40 -0
  22. package/dist/agent/tools/memory.d.ts +44 -0
  23. package/dist/agent/tools/memory.js +99 -0
  24. package/dist/agent/tools/preflight.d.ts +7 -0
  25. package/dist/agent/tools/preflight.js +137 -0
  26. package/dist/agent/tools/reporting.d.ts +58 -0
  27. package/dist/agent/tools/reporting.js +119 -0
  28. package/dist/agent/tools/schemas.d.ts +2 -0
  29. package/dist/agent/tools/schemas.js +3 -0
  30. package/dist/agent/types.d.ts +45 -0
  31. package/dist/agent/types.js +1 -0
  32. package/dist/ai/conversation/conversation-manager.js +8 -0
  33. package/dist/ai/conversation/url-fetcher.js +27 -0
  34. package/dist/ai/providers.js +5 -5
  35. package/dist/commands/agent.d.ts +17 -0
  36. package/dist/commands/agent.js +114 -0
  37. package/dist/commands/monitor.js +50 -183
  38. package/dist/commands/new-auto.d.ts +15 -0
  39. package/dist/commands/new-auto.js +237 -0
  40. package/dist/commands/run.js +20 -10
  41. package/dist/commands/sync.d.ts +15 -0
  42. package/dist/commands/sync.js +68 -0
  43. package/dist/generator/config.d.ts +1 -41
  44. package/dist/generator/config.js +7 -0
  45. package/dist/generator/index.d.ts +2 -2
  46. package/dist/generator/templates.d.ts +2 -0
  47. package/dist/generator/templates.js +9 -1
  48. package/dist/index.d.ts +1 -1
  49. package/dist/index.js +115 -4
  50. package/dist/repl/command-parser.d.ts +5 -0
  51. package/dist/repl/command-parser.js +5 -0
  52. package/dist/templates/prompts/PROMPT.md.tmpl +13 -10
  53. package/dist/templates/prompts/PROMPT_e2e.md.tmpl +13 -7
  54. package/dist/templates/prompts/PROMPT_feature.md.tmpl +16 -3
  55. package/dist/templates/prompts/PROMPT_review_auto.md.tmpl +32 -12
  56. package/dist/templates/prompts/PROMPT_review_manual.md.tmpl +4 -1
  57. package/dist/templates/prompts/PROMPT_review_merge.md.tmpl +39 -14
  58. package/dist/templates/prompts/PROMPT_verify.md.tmpl +5 -2
  59. package/dist/templates/scripts/feature-loop.sh.tmpl +441 -69
  60. package/dist/tui/app.d.ts +19 -2
  61. package/dist/tui/app.js +22 -4
  62. package/dist/tui/components/IssuePicker.d.ts +27 -0
  63. package/dist/tui/components/IssuePicker.js +64 -0
  64. package/dist/tui/components/RunCompletionSummary.js +6 -3
  65. package/dist/tui/hooks/useAgentOrchestrator.d.ts +29 -0
  66. package/dist/tui/hooks/useAgentOrchestrator.js +453 -0
  67. package/dist/tui/orchestration/interview-orchestrator.d.ts +5 -1
  68. package/dist/tui/orchestration/interview-orchestrator.js +27 -6
  69. package/dist/tui/screens/AgentScreen.d.ts +21 -0
  70. package/dist/tui/screens/AgentScreen.js +159 -0
  71. package/dist/tui/screens/InitScreen.js +4 -0
  72. package/dist/tui/screens/InterviewScreen.d.ts +3 -1
  73. package/dist/tui/screens/InterviewScreen.js +146 -10
  74. package/dist/tui/screens/MainShell.d.ts +1 -1
  75. package/dist/tui/screens/MainShell.js +36 -1
  76. package/dist/tui/screens/RunScreen.js +38 -6
  77. package/dist/tui/utils/build-run-summary.d.ts +1 -1
  78. package/dist/tui/utils/build-run-summary.js +40 -84
  79. package/dist/tui/utils/clear-screen.d.ts +14 -0
  80. package/dist/tui/utils/clear-screen.js +16 -0
  81. package/dist/tui/utils/loop-status.d.ts +41 -1
  82. package/dist/tui/utils/loop-status.js +243 -35
  83. package/dist/tui/utils/pr-summary.d.ts +3 -2
  84. package/dist/tui/utils/pr-summary.js +41 -6
  85. package/dist/utils/config.d.ts +8 -0
  86. package/dist/utils/config.js +8 -0
  87. package/dist/utils/github.d.ts +32 -0
  88. package/dist/utils/github.js +106 -0
  89. package/package.json +4 -1
  90. package/src/templates/prompts/PROMPT.md.tmpl +13 -10
  91. package/src/templates/prompts/PROMPT_e2e.md.tmpl +13 -7
  92. package/src/templates/prompts/PROMPT_feature.md.tmpl +16 -3
  93. package/src/templates/prompts/PROMPT_review_auto.md.tmpl +32 -12
  94. package/src/templates/prompts/PROMPT_review_manual.md.tmpl +4 -1
  95. package/src/templates/prompts/PROMPT_review_merge.md.tmpl +39 -14
  96. package/src/templates/prompts/PROMPT_verify.md.tmpl +5 -2
  97. package/src/templates/scripts/feature-loop.sh.tmpl +441 -69
@@ -0,0 +1,184 @@
1
+ import { tool, zodSchema } from 'ai';
2
+ import { z } from 'zod';
3
+ import { execFile } from 'node:child_process';
4
+ import { existsSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { promisify } from 'node:util';
7
+ import { FEATURE_NAME_SCHEMA } from './schemas.js';
8
+ import { loadConfigWithDefaults } from '../../utils/config.js';
9
+ import { findImplementationPlan, parseImplementationPlan } from '../../tui/utils/loop-status.js';
10
+ const execFileAsync = promisify(execFile);
11
+ function computeRecommendation(state) {
12
+ // PR state takes priority
13
+ if (state.pr.exists) {
14
+ if (state.pr.state === 'MERGED')
15
+ return 'pr_merged';
16
+ if (state.pr.state === 'CLOSED')
17
+ return 'pr_closed';
18
+ if (state.pr.state === 'OPEN')
19
+ return 'pr_exists_open';
20
+ }
21
+ // Linked PR (found via issue search, different branch name)
22
+ if (state.linkedPr.exists) {
23
+ if (state.linkedPr.state === 'MERGED')
24
+ return 'linked_pr_merged';
25
+ if (state.linkedPr.state === 'OPEN')
26
+ return 'linked_pr_open';
27
+ }
28
+ // Plan with all tasks done but no PR → resume to PR phase
29
+ if (state.plan.exists && state.plan.totalTasks > 0 && state.plan.completedTasks === state.plan.totalTasks) {
30
+ return 'resume_pr_phase';
31
+ }
32
+ // Plan with pending tasks → resume implementation
33
+ if (state.plan.exists && state.plan.totalTasks > 0 && state.plan.completedTasks < state.plan.totalTasks) {
34
+ return 'resume_implementation';
35
+ }
36
+ // Branch has commits but no plan found locally — the plan likely
37
+ // exists on the feature branch while we're checking from main.
38
+ // Recommend resume so the loop switches to the branch and picks up the work.
39
+ // This MUST come before the generate_plan check to prevent branch reset
40
+ // via `git checkout -B` which would destroy existing work.
41
+ if (state.branch.exists && state.branch.commitsAhead > 0) {
42
+ return 'resume_implementation';
43
+ }
44
+ // Spec exists but no plan → generate plan (fresh loop, no branch work to lose)
45
+ if (state.spec.exists) {
46
+ return 'generate_plan';
47
+ }
48
+ return 'start_fresh';
49
+ }
50
+ export async function assessFeatureStateImpl(projectRoot, featureName, issueNumber) {
51
+ const opts = { cwd: projectRoot };
52
+ const branchName = `feat/${featureName}`;
53
+ // 1. Check branch
54
+ let branchExists = false;
55
+ let commitsAhead = 0;
56
+ try {
57
+ await execFileAsync('git', ['rev-parse', '--verify', branchName], opts);
58
+ branchExists = true;
59
+ // Count commits ahead of default branch
60
+ let defaultBranch = 'main';
61
+ try {
62
+ const { stdout } = await execFileAsync('git', ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD'], opts);
63
+ defaultBranch = stdout.trim().replace(/^origin\//, '');
64
+ }
65
+ catch {
66
+ // fall back to main
67
+ }
68
+ try {
69
+ const { stdout } = await execFileAsync('git', ['rev-list', '--count', `${defaultBranch}..${branchName}`], opts);
70
+ commitsAhead = parseInt(stdout.trim(), 10) || 0;
71
+ }
72
+ catch {
73
+ // count failure is non-fatal
74
+ }
75
+ }
76
+ catch {
77
+ // branch doesn't exist
78
+ }
79
+ // 2. Load config for correct paths
80
+ let specsDir = '.ralph/specs';
81
+ try {
82
+ const config = await loadConfigWithDefaults(projectRoot);
83
+ specsDir = config.paths.specs;
84
+ }
85
+ catch {
86
+ // Config load failure is non-fatal — use default paths
87
+ }
88
+ // 3. Check spec
89
+ const specPath = join(projectRoot, specsDir, `${featureName}.md`);
90
+ const specExists = existsSync(specPath);
91
+ // 4. Check implementation plan using shared utility (handles worktrees too)
92
+ const planPath = findImplementationPlan(projectRoot, specsDir, featureName);
93
+ const planExists = planPath !== null;
94
+ let totalTasks = 0;
95
+ let completedTasks = 0;
96
+ if (planExists) {
97
+ try {
98
+ const taskCounts = await parseImplementationPlan(projectRoot, featureName, specsDir);
99
+ completedTasks = taskCounts.tasksDone + taskCounts.e2eDone;
100
+ totalTasks = completedTasks + taskCounts.tasksPending + taskCounts.e2ePending;
101
+ }
102
+ catch {
103
+ // parse failure is non-fatal
104
+ }
105
+ }
106
+ // 5. Check PR via gh CLI
107
+ let prExists = false;
108
+ let prState;
109
+ let prNumber;
110
+ let prUrl;
111
+ try {
112
+ const { stdout } = await execFileAsync('gh', [
113
+ 'pr', 'list',
114
+ '--head', branchName,
115
+ '--state', 'all',
116
+ '--json', 'number,state,url',
117
+ '--limit', '1',
118
+ ], { ...opts, timeout: 15000 });
119
+ const prs = JSON.parse(stdout.trim() || '[]');
120
+ if (prs.length > 0) {
121
+ prExists = true;
122
+ prState = prs[0].state;
123
+ prNumber = prs[0].number;
124
+ prUrl = prs[0].url;
125
+ }
126
+ }
127
+ catch {
128
+ // gh failure is non-fatal — tool works without GitHub CLI
129
+ }
130
+ // 6. Linked PR search (only when issueNumber provided and no branch-name PR found)
131
+ let linkedPrExists = false;
132
+ let linkedPrState;
133
+ let linkedPrNumber;
134
+ let linkedPrUrl;
135
+ let linkedPrHeadRefName;
136
+ if (issueNumber != null && !prExists) {
137
+ try {
138
+ const { stdout } = await execFileAsync('gh', [
139
+ 'pr', 'list',
140
+ '--search', `in:body "closes #${issueNumber}" OR in:body "fixes #${issueNumber}" OR in:body "resolves #${issueNumber}"`,
141
+ '--state', 'all',
142
+ '--json', 'number,state,url,headRefName',
143
+ '--limit', '1',
144
+ ], { ...opts, timeout: 15000 });
145
+ const prs = JSON.parse(stdout.trim() || '[]');
146
+ if (prs.length > 0) {
147
+ linkedPrExists = true;
148
+ linkedPrState = prs[0].state;
149
+ linkedPrNumber = prs[0].number;
150
+ linkedPrUrl = prs[0].url;
151
+ linkedPrHeadRefName = prs[0].headRefName;
152
+ }
153
+ }
154
+ catch {
155
+ // gh failure is non-fatal
156
+ }
157
+ }
158
+ // 7. Check loop status files
159
+ const prefix = join('/tmp', `ralph-loop-${featureName}`);
160
+ const hasStatusFiles = existsSync(`${prefix}.final`) || existsSync(`${prefix}.phases`) || existsSync(`${prefix}.log`);
161
+ const partial = {
162
+ featureName,
163
+ branch: { exists: branchExists, commitsAhead },
164
+ spec: { exists: specExists, path: specExists ? specPath : undefined },
165
+ plan: { exists: planExists, path: planPath ?? undefined, totalTasks, completedTasks, completionPercent: totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0 },
166
+ pr: { exists: prExists, state: prState, number: prNumber, url: prUrl },
167
+ linkedPr: { exists: linkedPrExists, state: linkedPrState, number: linkedPrNumber, url: linkedPrUrl, headRefName: linkedPrHeadRefName },
168
+ loopStatus: { hasStatusFiles },
169
+ };
170
+ return { ...partial, recommendation: computeRecommendation(partial) };
171
+ }
172
+ export function createFeatureStateTools(projectRoot) {
173
+ const assessFeatureState = tool({
174
+ description: 'Assess the current state of a feature: branch, spec, plan, PR, loop status. Returns a recommendation for what action to take next. MUST be called before generateSpec or runLoop.',
175
+ inputSchema: zodSchema(z.object({
176
+ featureName: FEATURE_NAME_SCHEMA,
177
+ issueNumber: z.number().int().optional().describe('GitHub issue number — enables linked PR detection when the feature was shipped under a different branch name'),
178
+ })),
179
+ execute: async ({ featureName, issueNumber }) => {
180
+ return assessFeatureStateImpl(projectRoot, featureName, issueNumber);
181
+ },
182
+ });
183
+ return { assessFeatureState };
184
+ }
@@ -0,0 +1,23 @@
1
+ export declare function createIntrospectionTools(projectRoot: string): {
2
+ readLoopLog: import("ai").Tool<{
3
+ featureName: string;
4
+ tailLines: number;
5
+ }, {
6
+ error: string;
7
+ lines?: undefined;
8
+ totalLines?: undefined;
9
+ } | {
10
+ lines: string[];
11
+ totalLines: number;
12
+ error?: undefined;
13
+ }>;
14
+ syncContext: import("ai").Tool<Record<string, never>, {
15
+ success: boolean;
16
+ contextPath: string;
17
+ error?: undefined;
18
+ } | {
19
+ success: boolean;
20
+ error: string;
21
+ contextPath?: undefined;
22
+ }>;
23
+ };
@@ -0,0 +1,40 @@
1
+ import { tool, zodSchema } from 'ai';
2
+ import { z } from 'zod';
3
+ import { existsSync } from 'node:fs';
4
+ import { readFile } from 'node:fs/promises';
5
+ import { join } from 'node:path';
6
+ import { FEATURE_NAME_SCHEMA } from './schemas.js';
7
+ export function createIntrospectionTools(projectRoot) {
8
+ const readLoopLog = tool({
9
+ description: 'Read the stdout/stderr log of a development loop (running or completed).',
10
+ inputSchema: zodSchema(z.object({
11
+ featureName: FEATURE_NAME_SCHEMA,
12
+ tailLines: z.number().int().min(1).max(500).default(100).describe('Number of lines from the end'),
13
+ })),
14
+ execute: async ({ featureName, tailLines }) => {
15
+ const logPath = join('/tmp', `ralph-loop-${featureName}.log`);
16
+ if (!existsSync(logPath)) {
17
+ return { error: `No log found at ${logPath} — verify featureName matches exactly what runLoop used` };
18
+ }
19
+ const content = await readFile(logPath, 'utf-8');
20
+ const allLines = content.split('\n');
21
+ const lines = allLines.slice(-tailLines);
22
+ return { lines, totalLines: allLines.length };
23
+ },
24
+ });
25
+ const syncContext = tool({
26
+ description: 'Refresh project context by scanning the codebase and running AI analysis. Call before planning if context is stale.',
27
+ inputSchema: zodSchema(z.object({})),
28
+ execute: async () => {
29
+ const { syncProjectContext } = await import('../../commands/sync.js');
30
+ try {
31
+ const contextPath = await syncProjectContext(projectRoot);
32
+ return { success: true, contextPath };
33
+ }
34
+ catch (err) {
35
+ return { success: false, error: err instanceof Error ? err.message : String(err) };
36
+ }
37
+ },
38
+ });
39
+ return { readLoopLog, syncContext };
40
+ }
@@ -0,0 +1,44 @@
1
+ import type { MemoryStore } from '../memory/store.js';
2
+ export declare const REFLECT_TOOL_NAME = "reflectOnWork";
3
+ export declare function createMemoryTools(store: MemoryStore, projectRoot?: string): {
4
+ readMemory: import("ai").Tool<{
5
+ limit: number;
6
+ type?: "work_log" | "project_knowledge" | "decision" | "strategic_context" | undefined;
7
+ search?: string | undefined;
8
+ }, {
9
+ entries: import("../memory/types.js").MemoryEntry[];
10
+ }>;
11
+ writeMemory: import("ai").Tool<{
12
+ type: "work_log" | "project_knowledge" | "decision";
13
+ content: string;
14
+ tags?: string[] | undefined;
15
+ relatedIssue?: number | undefined;
16
+ }, {
17
+ id: string;
18
+ timestamp: string;
19
+ }>;
20
+ reflectOnWork: import("ai").Tool<{
21
+ issueNumber: number;
22
+ outcome: "success" | "skipped" | "partial" | "failure";
23
+ whatWorked: string;
24
+ whatFailed: string;
25
+ patternDiscovered?: string | undefined;
26
+ specQualityNote?: string | undefined;
27
+ }, {
28
+ memoriesWritten: number;
29
+ }>;
30
+ listStrategicDocs: import("ai").Tool<Record<string, never>, {
31
+ files: string[];
32
+ }>;
33
+ readStrategicDoc: import("ai").Tool<{
34
+ filename: string;
35
+ }, {
36
+ error: string;
37
+ filename?: undefined;
38
+ content?: undefined;
39
+ } | {
40
+ filename: string;
41
+ content: string;
42
+ error?: undefined;
43
+ }>;
44
+ };
@@ -0,0 +1,99 @@
1
+ import { tool, zodSchema } from 'ai';
2
+ import { z } from 'zod';
3
+ import { createMemoryEntry } from '../memory/types.js';
4
+ import { listStrategicDocs, readStrategicDoc, } from '../memory/ingest.js';
5
+ export const REFLECT_TOOL_NAME = 'reflectOnWork';
6
+ export function createMemoryTools(store, projectRoot) {
7
+ const readMemory = tool({
8
+ description: 'Read recent memory entries. Use before planning to recall past outcomes and decisions.',
9
+ inputSchema: zodSchema(z.object({
10
+ type: z.enum(['work_log', 'project_knowledge', 'decision', 'strategic_context']).optional()
11
+ .describe('Filter by memory type'),
12
+ limit: z.number().int().min(1).max(50).default(10).describe('Max entries to return'),
13
+ search: z.string().optional().describe('Search term to filter by content'),
14
+ })),
15
+ execute: async ({ type, limit, search }) => {
16
+ const entries = await store.read({ type, limit, search });
17
+ return { entries };
18
+ },
19
+ });
20
+ const writeMemory = tool({
21
+ description: 'Write a memory entry. Use after completing work or learning something important.',
22
+ inputSchema: zodSchema(z.object({
23
+ type: z.enum(['work_log', 'project_knowledge', 'decision']).describe('Type of memory'),
24
+ content: z.string().describe('The memory content — be specific and narrative'),
25
+ tags: z.array(z.string()).optional().describe('Tags for filtering (e.g., auth, api)'),
26
+ relatedIssue: z.number().int().optional().describe('Related GitHub issue number'),
27
+ })),
28
+ execute: async ({ type, content, tags, relatedIssue }) => {
29
+ const entry = createMemoryEntry({ type, content, tags, relatedIssue });
30
+ await store.append(entry);
31
+ return { id: entry.id, timestamp: entry.timestamp };
32
+ },
33
+ });
34
+ const reflectOnWork = tool({
35
+ description: 'Reflect on completed work to extract learnings and patterns. Call after each issue.',
36
+ inputSchema: zodSchema(z.object({
37
+ issueNumber: z.number().int().describe('The issue that was worked on'),
38
+ outcome: z.enum(['success', 'partial', 'failure', 'skipped']).describe('How did it go? Use "skipped" when the issue was already complete or requires no work.'),
39
+ whatWorked: z.string().describe('What went well'),
40
+ whatFailed: z.string().describe('What went wrong or was difficult'),
41
+ patternDiscovered: z.string().optional().describe('Any reusable pattern discovered'),
42
+ specQualityNote: z.string().optional().describe('How could the spec have been better?'),
43
+ })),
44
+ execute: async ({ issueNumber, outcome, whatWorked, whatFailed, patternDiscovered, specQualityNote }) => {
45
+ const entries = [];
46
+ entries.push(createMemoryEntry({
47
+ type: 'work_log',
48
+ content: `Issue #${issueNumber} (${outcome}). Worked: ${whatWorked}. Failed: ${whatFailed}`,
49
+ relatedIssue: issueNumber,
50
+ tags: [outcome],
51
+ }));
52
+ if (patternDiscovered) {
53
+ entries.push(createMemoryEntry({
54
+ type: 'project_knowledge',
55
+ content: patternDiscovered,
56
+ relatedIssue: issueNumber,
57
+ tags: ['pattern'],
58
+ }));
59
+ }
60
+ if (specQualityNote) {
61
+ entries.push(createMemoryEntry({
62
+ type: 'project_knowledge',
63
+ content: `Spec quality (#${issueNumber}): ${specQualityNote}`,
64
+ relatedIssue: issueNumber,
65
+ tags: ['spec-quality'],
66
+ }));
67
+ }
68
+ for (const entry of entries) {
69
+ await store.append(entry);
70
+ }
71
+ return { memoriesWritten: entries.length };
72
+ },
73
+ });
74
+ const listStrategicDocsT = tool({
75
+ description: 'List available strategic documents in .ralph/strategic/. Returns filenames. Use readStrategicDoc to read full content.',
76
+ inputSchema: zodSchema(z.object({})),
77
+ execute: async () => {
78
+ if (!projectRoot)
79
+ return { files: [] };
80
+ const files = await listStrategicDocs(projectRoot);
81
+ return { files };
82
+ },
83
+ });
84
+ const readStrategicDocT = tool({
85
+ description: 'Read the full content of a strategic document. Use to get detailed architecture, design, or implementation plans relevant to the current task.',
86
+ inputSchema: zodSchema(z.object({
87
+ filename: z.string().describe('Filename to read (e.g. "design.md")'),
88
+ })),
89
+ execute: async ({ filename }) => {
90
+ if (!projectRoot)
91
+ return { error: 'No project root configured' };
92
+ const content = await readStrategicDoc(projectRoot, filename);
93
+ if (content === null)
94
+ return { error: `File not found: ${filename}` };
95
+ return { filename, content };
96
+ },
97
+ });
98
+ return { readMemory, writeMemory, reflectOnWork, listStrategicDocs: listStrategicDocsT, readStrategicDoc: readStrategicDocT };
99
+ }
@@ -0,0 +1,7 @@
1
+ export interface PreflightResult {
2
+ ok: boolean;
3
+ defaultBranch?: string;
4
+ stashed?: boolean;
5
+ error?: string;
6
+ }
7
+ export declare function runPreflightChecks(projectRoot: string, featureName: string, emitProgress?: (toolName: string, line: string) => void): Promise<PreflightResult>;
@@ -0,0 +1,137 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { existsSync, realpathSync } from 'node:fs';
3
+ import { promisify } from 'node:util';
4
+ const execFileAsync = promisify(execFile);
5
+ export async function runPreflightChecks(projectRoot, featureName, emitProgress) {
6
+ const opts = { cwd: projectRoot };
7
+ // 1. Detect default branch
8
+ let defaultBranch;
9
+ try {
10
+ const { stdout } = await execFileAsync('git', ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD'], opts);
11
+ defaultBranch = stdout.trim().replace(/^origin\//, '');
12
+ }
13
+ catch {
14
+ // symbolic-ref not set, try fallbacks
15
+ }
16
+ if (!defaultBranch) {
17
+ try {
18
+ await execFileAsync('git', ['rev-parse', '--verify', 'main'], opts);
19
+ defaultBranch = 'main';
20
+ }
21
+ catch {
22
+ try {
23
+ await execFileAsync('git', ['rev-parse', '--verify', 'master'], opts);
24
+ defaultBranch = 'master';
25
+ }
26
+ catch {
27
+ // neither exists
28
+ }
29
+ }
30
+ }
31
+ if (!defaultBranch) {
32
+ return { ok: false, error: 'Cannot determine default branch: no origin/HEAD, main, or master found' };
33
+ }
34
+ emitProgress?.('preflight', `Default branch: ${defaultBranch}`);
35
+ // 2. Clean stale worktrees
36
+ try {
37
+ await execFileAsync('git', ['worktree', 'prune'], opts);
38
+ }
39
+ catch {
40
+ // prune failure is non-fatal
41
+ }
42
+ const branchName = `feat/${featureName}`;
43
+ try {
44
+ const { stdout } = await execFileAsync('git', ['worktree', 'list', '--porcelain'], opts);
45
+ const worktrees = parseWorktreeList(stdout);
46
+ // Resolve projectRoot to compare with worktree paths
47
+ let resolvedRoot;
48
+ try {
49
+ resolvedRoot = realpathSync(projectRoot);
50
+ }
51
+ catch {
52
+ resolvedRoot = projectRoot;
53
+ }
54
+ for (const wt of worktrees) {
55
+ if (wt.branch === branchName || wt.branch === `refs/heads/${branchName}`) {
56
+ // Branch is checked out in the main worktree (projectRoot itself) —
57
+ // the loop script handles this case (it detects CURRENT_BRANCH == BRANCH).
58
+ let resolvedWtPath;
59
+ try {
60
+ resolvedWtPath = realpathSync(wt.path);
61
+ }
62
+ catch {
63
+ resolvedWtPath = wt.path;
64
+ }
65
+ if (resolvedWtPath === resolvedRoot) {
66
+ emitProgress?.('preflight', `Branch ${branchName} already checked out in project root`);
67
+ continue;
68
+ }
69
+ // Branch is checked out in a different worktree
70
+ const isEphemeral = wt.path.includes('/.claude/worktrees/');
71
+ if (!existsSync(wt.path) || !existsSync(`${wt.path}/.git`) || isEphemeral) {
72
+ // Stale or ephemeral worktree — auto-fix
73
+ emitProgress?.('preflight', `Removing ${isEphemeral ? 'ephemeral' : 'stale'} worktree at ${wt.path}`);
74
+ try {
75
+ await execFileAsync('git', ['worktree', 'remove', '--force', wt.path], opts);
76
+ }
77
+ catch {
78
+ // If remove fails, try prune again
79
+ await execFileAsync('git', ['worktree', 'prune'], opts).catch(() => { });
80
+ }
81
+ }
82
+ else {
83
+ return { ok: false, error: `Branch locked by active worktree at ${wt.path}` };
84
+ }
85
+ }
86
+ }
87
+ }
88
+ catch {
89
+ // worktree list failure is non-fatal (may not be in a git repo with worktree support)
90
+ }
91
+ // 3. Stash dirty working tree so branch switching succeeds
92
+ let stashed = false;
93
+ let dirty = false;
94
+ try {
95
+ const { stdout } = await execFileAsync('git', ['status', '--porcelain'], opts);
96
+ dirty = stdout.trim().length > 0;
97
+ }
98
+ catch {
99
+ // status check failure — proceed optimistically
100
+ }
101
+ if (dirty) {
102
+ emitProgress?.('preflight', 'Stashing uncommitted changes');
103
+ try {
104
+ await execFileAsync('git', ['stash', 'push', '-m', `wiggum-preflight: auto-stash before ${branchName}`], opts);
105
+ stashed = true;
106
+ }
107
+ catch {
108
+ return { ok: false, error: 'Working tree has uncommitted changes and git stash failed' };
109
+ }
110
+ }
111
+ return { ok: true, defaultBranch, stashed };
112
+ }
113
+ function parseWorktreeList(output) {
114
+ const entries = [];
115
+ let currentPath = '';
116
+ let currentBranch = '';
117
+ for (const line of output.split('\n')) {
118
+ if (line.startsWith('worktree ')) {
119
+ currentPath = line.slice('worktree '.length);
120
+ }
121
+ else if (line.startsWith('branch ')) {
122
+ currentBranch = line.slice('branch '.length);
123
+ }
124
+ else if (line === '') {
125
+ if (currentPath && currentBranch) {
126
+ entries.push({ path: currentPath, branch: currentBranch });
127
+ }
128
+ currentPath = '';
129
+ currentBranch = '';
130
+ }
131
+ }
132
+ // Handle last entry without trailing newline
133
+ if (currentPath && currentBranch) {
134
+ entries.push({ path: currentPath, branch: currentBranch });
135
+ }
136
+ return entries;
137
+ }
@@ -0,0 +1,58 @@
1
+ export declare function createReportingTools(owner: string, repo: string): {
2
+ commentOnIssue: import("ai").Tool<{
3
+ issueNumber: number;
4
+ body: string;
5
+ }, {
6
+ success: boolean;
7
+ error?: undefined;
8
+ } | {
9
+ success: boolean;
10
+ error: string;
11
+ }>;
12
+ createIssue: import("ai").Tool<{
13
+ title: string;
14
+ body: string;
15
+ labels: string[];
16
+ }, {
17
+ success: boolean;
18
+ issueNumber: number | undefined;
19
+ url: string;
20
+ error?: undefined;
21
+ } | {
22
+ success: boolean;
23
+ error: string;
24
+ issueNumber?: undefined;
25
+ url?: undefined;
26
+ }>;
27
+ closeIssue: import("ai").Tool<{
28
+ issueNumber: number;
29
+ comment?: string | undefined;
30
+ }, {
31
+ success: boolean;
32
+ error?: undefined;
33
+ } | {
34
+ success: boolean;
35
+ error: string;
36
+ }>;
37
+ checkAllBoxes: import("ai").Tool<{
38
+ issueNumber: number;
39
+ }, {
40
+ success: boolean;
41
+ changed: boolean;
42
+ message: string;
43
+ totalChecked?: undefined;
44
+ error?: undefined;
45
+ } | {
46
+ success: boolean;
47
+ changed: boolean;
48
+ totalChecked: number;
49
+ message?: undefined;
50
+ error?: undefined;
51
+ } | {
52
+ success: boolean;
53
+ error: string;
54
+ changed?: undefined;
55
+ message?: undefined;
56
+ totalChecked?: undefined;
57
+ }>;
58
+ };