ashlrcode 1.0.0 → 2.1.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 +73 -16
- package/package.json +28 -9
- package/prompts/skills/commit.md +36 -0
- package/prompts/skills/coordinate.md +21 -0
- package/prompts/skills/daily-review.md +65 -0
- package/prompts/skills/debug.md +23 -0
- package/prompts/skills/deep-work.md +129 -0
- package/prompts/skills/explore.md +24 -0
- package/prompts/skills/init.md +39 -0
- package/prompts/skills/kairos.md +19 -0
- package/prompts/skills/plan.md +19 -0
- package/prompts/skills/polish.md +94 -0
- package/prompts/skills/pr.md +30 -0
- package/prompts/skills/refactor.md +26 -0
- package/prompts/skills/resume-branch.md +27 -0
- package/prompts/skills/review.md +27 -0
- package/prompts/skills/ship.md +32 -0
- package/prompts/skills/simplify.md +25 -0
- package/prompts/skills/test.md +19 -0
- package/prompts/skills/verify.md +17 -0
- package/prompts/skills/weekly-plan.md +63 -0
- package/prompts/system.md +451 -0
- package/src/agent/away-summary.ts +138 -0
- package/src/agent/context.ts +6 -0
- package/src/agent/coordinator.ts +494 -0
- package/src/agent/dream.ts +149 -11
- package/src/agent/error-handler.ts +51 -35
- package/src/agent/kairos.ts +52 -4
- package/src/agent/loop.ts +153 -13
- package/src/agent/mailbox.ts +151 -0
- package/src/agent/model-patches.ts +28 -3
- package/src/agent/product-agent.ts +463 -0
- package/src/agent/speculation.ts +21 -18
- package/src/agent/sub-agent.ts +11 -1
- package/src/agent/system-prompt.ts +19 -0
- package/src/agent/tool-executor.ts +83 -3
- package/src/agent/verification.ts +223 -0
- package/src/agent/worktree-manager.ts +50 -1
- package/src/cli.ts +228 -36
- package/src/config/features.ts +8 -8
- package/src/config/keychain.ts +105 -0
- package/src/config/permissions.ts +3 -2
- package/src/config/settings.ts +73 -5
- package/src/config/upgrade-notice.ts +15 -2
- package/src/mcp/client.ts +392 -2
- package/src/mcp/manager.ts +129 -13
- package/src/mcp/types.ts +4 -1
- package/src/migrate.ts +228 -0
- package/src/persistence/session.ts +209 -5
- package/src/providers/anthropic.ts +112 -98
- package/src/providers/cost-tracker.ts +71 -2
- package/src/providers/retry.ts +2 -4
- package/src/providers/types.ts +5 -1
- package/src/providers/xai.ts +1 -0
- package/src/repl.tsx +514 -127
- package/src/setup.ts +37 -1
- package/src/tools/coordinate.ts +88 -0
- package/src/tools/grep.ts +9 -11
- package/src/tools/lsp.ts +44 -32
- package/src/tools/registry.ts +75 -9
- package/src/tools/send-message.ts +89 -30
- package/src/tools/types.ts +2 -0
- package/src/tools/verify.ts +88 -0
- package/src/tools/web-browser.ts +8 -5
- package/src/tools/workflow.ts +34 -10
- package/src/ui/AnimatedSpinner.tsx +302 -0
- package/src/ui/App.tsx +16 -15
- package/src/ui/BuddyPanel.tsx +27 -34
- package/src/ui/SlashInput.tsx +99 -0
- package/src/ui/banner.ts +10 -0
- package/src/ui/buddy.ts +5 -4
- package/src/ui/effort.ts +5 -1
- package/src/ui/markdown.ts +269 -88
- package/src/ui/message-renderer.ts +183 -35
- package/src/ui/quips.json +41 -0
- package/src/ui/speech-bubble.ts +35 -19
- package/src/utils/ring-buffer.ts +101 -0
- package/src/voice/voice-mode.ts +13 -2
- package/src/__tests__/branded-types.test.ts +0 -47
- package/src/__tests__/context.test.ts +0 -163
- package/src/__tests__/cost-tracker.test.ts +0 -274
- package/src/__tests__/cron.test.ts +0 -197
- package/src/__tests__/dream.test.ts +0 -204
- package/src/__tests__/error-handler.test.ts +0 -192
- package/src/__tests__/features.test.ts +0 -69
- package/src/__tests__/file-history.test.ts +0 -177
- package/src/__tests__/hooks.test.ts +0 -145
- package/src/__tests__/keybindings.test.ts +0 -159
- package/src/__tests__/model-patches.test.ts +0 -82
- package/src/__tests__/permissions-rules.test.ts +0 -121
- package/src/__tests__/permissions.test.ts +0 -108
- package/src/__tests__/project-config.test.ts +0 -63
- package/src/__tests__/retry.test.ts +0 -321
- package/src/__tests__/router.test.ts +0 -158
- package/src/__tests__/session-compact.test.ts +0 -191
- package/src/__tests__/session.test.ts +0 -145
- package/src/__tests__/skill-registry.test.ts +0 -130
- package/src/__tests__/speculation.test.ts +0 -196
- package/src/__tests__/tasks-v2.test.ts +0 -267
- package/src/__tests__/telemetry.test.ts +0 -149
- package/src/__tests__/tool-executor.test.ts +0 -141
- package/src/__tests__/tool-registry.test.ts +0 -166
- package/src/__tests__/undercover.test.ts +0 -93
- package/src/__tests__/workflow.test.ts +0 -195
|
@@ -10,6 +10,7 @@ import type { ToolRegistry } from "../tools/registry.ts";
|
|
|
10
10
|
import type { ToolContext } from "../tools/types.ts";
|
|
11
11
|
import type { ToolCall } from "../providers/types.ts";
|
|
12
12
|
import type { SpeculationCache } from "./speculation.ts";
|
|
13
|
+
import { trackFileModification } from "./verification.ts";
|
|
13
14
|
|
|
14
15
|
// ---------------------------------------------------------------------------
|
|
15
16
|
// Module-level speculation cache (set from repl startup)
|
|
@@ -25,6 +26,81 @@ export function getSpeculationCache(): SpeculationCache | null {
|
|
|
25
26
|
return _speculationCache;
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Tool execution metrics — cumulative timing and success tracking
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
interface ToolMetric {
|
|
34
|
+
name: string;
|
|
35
|
+
calls: number;
|
|
36
|
+
errors: number;
|
|
37
|
+
totalDurationMs: number;
|
|
38
|
+
minDurationMs: number;
|
|
39
|
+
maxDurationMs: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const _toolMetrics = new Map<string, ToolMetric>();
|
|
43
|
+
|
|
44
|
+
/** Record a tool execution for metrics tracking. */
|
|
45
|
+
function recordToolMetric(name: string, durationMs: number, isError: boolean): void {
|
|
46
|
+
const existing = _toolMetrics.get(name) ?? {
|
|
47
|
+
name,
|
|
48
|
+
calls: 0,
|
|
49
|
+
errors: 0,
|
|
50
|
+
totalDurationMs: 0,
|
|
51
|
+
minDurationMs: Infinity,
|
|
52
|
+
maxDurationMs: 0,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
existing.calls++;
|
|
56
|
+
if (isError) existing.errors++;
|
|
57
|
+
existing.totalDurationMs += durationMs;
|
|
58
|
+
existing.minDurationMs = Math.min(existing.minDurationMs, durationMs);
|
|
59
|
+
existing.maxDurationMs = Math.max(existing.maxDurationMs, durationMs);
|
|
60
|
+
|
|
61
|
+
_toolMetrics.set(name, existing);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Get all tool execution metrics (sorted by total calls descending). */
|
|
65
|
+
export function getToolMetrics(): ToolMetric[] {
|
|
66
|
+
return Array.from(_toolMetrics.values())
|
|
67
|
+
.sort((a, b) => b.calls - a.calls);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Format tool metrics for display. */
|
|
71
|
+
export function formatToolMetrics(): string {
|
|
72
|
+
const metrics = getToolMetrics();
|
|
73
|
+
if (metrics.length === 0) return "No tool calls recorded.";
|
|
74
|
+
|
|
75
|
+
const lines: string[] = ["Tool Execution Metrics:"];
|
|
76
|
+
const totalCalls = metrics.reduce((s, m) => s + m.calls, 0);
|
|
77
|
+
const totalDuration = metrics.reduce((s, m) => s + m.totalDurationMs, 0);
|
|
78
|
+
|
|
79
|
+
lines.push(` Total: ${totalCalls} calls, ${formatMs(totalDuration)} total`);
|
|
80
|
+
lines.push("");
|
|
81
|
+
|
|
82
|
+
for (const m of metrics.slice(0, 15)) {
|
|
83
|
+
const avgMs = m.calls > 0 ? m.totalDurationMs / m.calls : 0;
|
|
84
|
+
const errorRate = m.calls > 0 ? Math.round((m.errors / m.calls) * 100) : 0;
|
|
85
|
+
lines.push(
|
|
86
|
+
` ${m.name.padEnd(16)} ${String(m.calls).padStart(4)} calls · avg ${formatMs(avgMs)} · ${errorRate}% err`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return lines.join("\n");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function formatMs(ms: number): string {
|
|
94
|
+
if (ms >= 60_000) return `${(ms / 60_000).toFixed(1)}m`;
|
|
95
|
+
if (ms >= 1_000) return `${(ms / 1_000).toFixed(1)}s`;
|
|
96
|
+
return `${Math.round(ms)}ms`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Reset metrics (for testing). */
|
|
100
|
+
export function resetToolMetrics(): void {
|
|
101
|
+
_toolMetrics.clear();
|
|
102
|
+
}
|
|
103
|
+
|
|
28
104
|
export interface ToolExecutionResult {
|
|
29
105
|
toolCallId: string;
|
|
30
106
|
name: string;
|
|
@@ -99,6 +175,7 @@ async function executeSingle(
|
|
|
99
175
|
onToolEnd?: (name: string, result: string, isError: boolean) => void;
|
|
100
176
|
}
|
|
101
177
|
): Promise<ToolExecutionResult> {
|
|
178
|
+
const startTime = performance.now();
|
|
102
179
|
const tool = registry.get(tc.name);
|
|
103
180
|
|
|
104
181
|
// Check speculation cache for read-only tools (skip the full execute path)
|
|
@@ -107,6 +184,7 @@ async function executeSingle(
|
|
|
107
184
|
if (cached !== null) {
|
|
108
185
|
callbacks?.onToolStart?.(tc.name, tc.input);
|
|
109
186
|
callbacks?.onToolEnd?.(tc.name, cached, false);
|
|
187
|
+
recordToolMetric(tc.name, performance.now() - startTime, false);
|
|
110
188
|
|
|
111
189
|
// Track for speculation and trigger pre-fetch for next likely call
|
|
112
190
|
trackAndSpeculate(tc.name, tc.input, cached);
|
|
@@ -126,17 +204,19 @@ async function executeSingle(
|
|
|
126
204
|
const { result, isError } = await registry.execute(tc.name, tc.input, context);
|
|
127
205
|
|
|
128
206
|
callbacks?.onToolEnd?.(tc.name, result, isError);
|
|
207
|
+
recordToolMetric(tc.name, performance.now() - startTime, isError);
|
|
129
208
|
|
|
130
209
|
// Cache successful read-only results for future speculation
|
|
131
210
|
if (tool?.isReadOnly() && _speculationCache && !isError) {
|
|
132
211
|
_speculationCache.set(tc.name, tc.input, result);
|
|
133
212
|
}
|
|
134
213
|
|
|
135
|
-
// Invalidate cache when write tools execute
|
|
136
|
-
if (!tool?.isReadOnly()
|
|
214
|
+
// Invalidate cache and track modifications when write tools execute
|
|
215
|
+
if (!tool?.isReadOnly()) {
|
|
137
216
|
const filePath = tc.input.file_path;
|
|
138
217
|
if (typeof filePath === "string") {
|
|
139
|
-
_speculationCache
|
|
218
|
+
_speculationCache?.invalidateForFile(filePath);
|
|
219
|
+
trackFileModification(filePath);
|
|
140
220
|
}
|
|
141
221
|
}
|
|
142
222
|
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verification Agent — auto-validates multi-file changes.
|
|
3
|
+
*
|
|
4
|
+
* After non-trivial edits (2+ files modified), spawns a read-only sub-agent
|
|
5
|
+
* that reviews the git diff and modified files for:
|
|
6
|
+
* - Syntax errors and obvious bugs
|
|
7
|
+
* - Logic consistency with stated intent
|
|
8
|
+
* - Missing imports, undefined references
|
|
9
|
+
* - Unintended side effects
|
|
10
|
+
*
|
|
11
|
+
* Claude Code's internal verification agent "doubles completion rates."
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { runSubAgent, type SubAgentResult } from "./sub-agent.ts";
|
|
15
|
+
import type { ProviderRouter } from "../providers/router.ts";
|
|
16
|
+
import type { ToolRegistry } from "../tools/registry.ts";
|
|
17
|
+
import type { ToolContext } from "../tools/types.ts";
|
|
18
|
+
|
|
19
|
+
export interface VerificationConfig {
|
|
20
|
+
router: ProviderRouter;
|
|
21
|
+
toolRegistry: ToolRegistry;
|
|
22
|
+
toolContext: ToolContext;
|
|
23
|
+
systemPrompt: string;
|
|
24
|
+
/** Minimum number of modified files to auto-trigger verification. Default: 2 */
|
|
25
|
+
fileThreshold?: number;
|
|
26
|
+
/** Max iterations for the verification sub-agent. Default: 10 */
|
|
27
|
+
maxIterations?: number;
|
|
28
|
+
/** Callback for verification progress */
|
|
29
|
+
onOutput?: (text: string) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface VerificationResult {
|
|
33
|
+
passed: boolean;
|
|
34
|
+
issues: VerificationIssue[];
|
|
35
|
+
summary: string;
|
|
36
|
+
filesChecked: string[];
|
|
37
|
+
agentResult: SubAgentResult;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface VerificationIssue {
|
|
41
|
+
severity: "error" | "warning" | "info";
|
|
42
|
+
file: string;
|
|
43
|
+
line?: number;
|
|
44
|
+
description: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Track files modified during a turn for automatic verification triggering.
|
|
49
|
+
*
|
|
50
|
+
* Uses a per-session global set. In sub-agent contexts, modifications are
|
|
51
|
+
* tracked in the agent's AsyncLocalStorage context instead (see async-context.ts),
|
|
52
|
+
* but this global is kept for the main REPL session.
|
|
53
|
+
*/
|
|
54
|
+
const _modifiedFiles = new Set<string>();
|
|
55
|
+
|
|
56
|
+
export function trackFileModification(filePath: string): void {
|
|
57
|
+
_modifiedFiles.add(filePath);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getModifiedFiles(): string[] {
|
|
61
|
+
// TODO: When agent context gains file tracking, prefer ctx-scoped files
|
|
62
|
+
return Array.from(_modifiedFiles);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function clearModifiedFiles(): void {
|
|
66
|
+
_modifiedFiles.clear();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function shouldAutoVerify(threshold: number = 2): boolean {
|
|
70
|
+
return _modifiedFiles.size >= threshold;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Build the verification prompt with context about what changed.
|
|
75
|
+
*/
|
|
76
|
+
function buildVerificationPrompt(modifiedFiles: string[], intent?: string): string {
|
|
77
|
+
const fileList = modifiedFiles.map(f => ` - ${f}`).join("\n");
|
|
78
|
+
|
|
79
|
+
return `You are a VERIFICATION AGENT. Your job is to review recent code changes for correctness.
|
|
80
|
+
|
|
81
|
+
## Modified Files
|
|
82
|
+
${fileList}
|
|
83
|
+
|
|
84
|
+
${intent ? `## Stated Intent\n${intent}\n` : ""}
|
|
85
|
+
## Your Task
|
|
86
|
+
|
|
87
|
+
1. Use the Diff tool to see what changed (run \`git diff\` via Bash if Diff is unavailable)
|
|
88
|
+
2. Read each modified file to understand the full context
|
|
89
|
+
3. Check for:
|
|
90
|
+
- **Syntax errors**: Missing brackets, unclosed strings, invalid TypeScript
|
|
91
|
+
- **Logic bugs**: Off-by-one errors, null/undefined access, wrong conditions
|
|
92
|
+
- **Missing imports**: References to symbols not imported
|
|
93
|
+
- **Type mismatches**: Arguments that don't match function signatures
|
|
94
|
+
- **Unintended side effects**: Changes that break existing behavior
|
|
95
|
+
- **Incomplete changes**: TODOs left behind, partial implementations
|
|
96
|
+
|
|
97
|
+
4. Report your findings in this EXACT format:
|
|
98
|
+
|
|
99
|
+
VERIFICATION_RESULT:
|
|
100
|
+
STATUS: PASS | FAIL
|
|
101
|
+
ISSUES:
|
|
102
|
+
- [ERROR|WARNING|INFO] file.ts:123 — Description of issue
|
|
103
|
+
- [WARNING] other-file.ts — Description
|
|
104
|
+
SUMMARY: One-line summary of verification outcome
|
|
105
|
+
|
|
106
|
+
If everything looks correct, report STATUS: PASS with no issues.
|
|
107
|
+
Be thorough but avoid false positives — only flag real problems.`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Parse the verification agent's output into structured results.
|
|
112
|
+
*/
|
|
113
|
+
function parseVerificationOutput(text: string, files: string[]): Omit<VerificationResult, "agentResult"> {
|
|
114
|
+
const issues: VerificationIssue[] = [];
|
|
115
|
+
let passed = true;
|
|
116
|
+
let summary = "Verification completed";
|
|
117
|
+
|
|
118
|
+
// Extract STATUS
|
|
119
|
+
const statusMatch = text.match(/STATUS:\s*(PASS|FAIL)/i);
|
|
120
|
+
if (statusMatch) {
|
|
121
|
+
passed = statusMatch[1]!.toUpperCase() === "PASS";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Extract SUMMARY
|
|
125
|
+
const summaryMatch = text.match(/SUMMARY:\s*(.+)/i);
|
|
126
|
+
if (summaryMatch) {
|
|
127
|
+
summary = summaryMatch[1]!.trim();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Extract individual issues
|
|
131
|
+
const issuePattern = /\[(\w+)\]\s*([^\s:]+?)(?::(\d+))?\s*[—-]\s*(.+)/g;
|
|
132
|
+
let match;
|
|
133
|
+
while ((match = issuePattern.exec(text)) !== null) {
|
|
134
|
+
const severityRaw = match[1]!.toLowerCase();
|
|
135
|
+
const severity: VerificationIssue["severity"] =
|
|
136
|
+
severityRaw === "error" ? "error" :
|
|
137
|
+
severityRaw === "warning" ? "warning" : "info";
|
|
138
|
+
|
|
139
|
+
issues.push({
|
|
140
|
+
severity,
|
|
141
|
+
file: match[2]!,
|
|
142
|
+
line: match[3] ? parseInt(match[3], 10) : undefined,
|
|
143
|
+
description: match[4]!.trim(),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (severity === "error") passed = false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return { passed, issues, summary, filesChecked: files };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Run a verification sub-agent to check recent changes.
|
|
154
|
+
*/
|
|
155
|
+
export async function runVerification(
|
|
156
|
+
config: VerificationConfig,
|
|
157
|
+
options?: { intent?: string; files?: string[] }
|
|
158
|
+
): Promise<VerificationResult> {
|
|
159
|
+
const files = options?.files ?? getModifiedFiles();
|
|
160
|
+
|
|
161
|
+
if (files.length === 0) {
|
|
162
|
+
return {
|
|
163
|
+
passed: true,
|
|
164
|
+
issues: [],
|
|
165
|
+
summary: "No files to verify",
|
|
166
|
+
filesChecked: [],
|
|
167
|
+
agentResult: { name: "verification", text: "", toolCalls: [], messages: [] },
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const prompt = buildVerificationPrompt(files, options?.intent);
|
|
172
|
+
|
|
173
|
+
config.onOutput?.(" Running verification agent...\n");
|
|
174
|
+
|
|
175
|
+
const agentResult = await runSubAgent({
|
|
176
|
+
name: "verification-agent",
|
|
177
|
+
prompt,
|
|
178
|
+
systemPrompt: config.systemPrompt + "\n\nYou are a verification agent. Be thorough but concise. Only use read-only tools.",
|
|
179
|
+
router: config.router,
|
|
180
|
+
toolRegistry: config.toolRegistry,
|
|
181
|
+
toolContext: config.toolContext,
|
|
182
|
+
readOnly: true,
|
|
183
|
+
maxIterations: config.maxIterations ?? 10,
|
|
184
|
+
onText: config.onOutput,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const parsed = parseVerificationOutput(agentResult.text, files);
|
|
188
|
+
|
|
189
|
+
const errorCount = parsed.issues.filter(i => i.severity === "error").length;
|
|
190
|
+
const warnCount = parsed.issues.filter(i => i.severity === "warning").length;
|
|
191
|
+
|
|
192
|
+
config.onOutput?.(
|
|
193
|
+
parsed.passed
|
|
194
|
+
? ` ✓ Verification passed (${files.length} files checked)\n`
|
|
195
|
+
: ` ✗ Verification failed: ${errorCount} errors, ${warnCount} warnings\n`
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
return { ...parsed, agentResult };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Format verification results for display.
|
|
203
|
+
*/
|
|
204
|
+
export function formatVerificationReport(result: VerificationResult): string {
|
|
205
|
+
const lines: string[] = [];
|
|
206
|
+
|
|
207
|
+
lines.push(result.passed ? "## ✓ Verification Passed" : "## ✗ Verification Failed");
|
|
208
|
+
lines.push("");
|
|
209
|
+
lines.push(`**Files checked:** ${result.filesChecked.length}`);
|
|
210
|
+
lines.push(`**Summary:** ${result.summary}`);
|
|
211
|
+
|
|
212
|
+
if (result.issues.length > 0) {
|
|
213
|
+
lines.push("");
|
|
214
|
+
lines.push("### Issues");
|
|
215
|
+
for (const issue of result.issues) {
|
|
216
|
+
const icon = issue.severity === "error" ? "🔴" : issue.severity === "warning" ? "🟡" : "🔵";
|
|
217
|
+
const loc = issue.line ? `${issue.file}:${issue.line}` : issue.file;
|
|
218
|
+
lines.push(`${icon} **${loc}** — ${issue.description}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return lines.join("\n");
|
|
223
|
+
}
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import { join } from "path";
|
|
6
6
|
import { homedir } from "os";
|
|
7
|
-
import { mkdir } from "fs/promises";
|
|
7
|
+
import { mkdir, readdir, stat } from "fs/promises";
|
|
8
|
+
import { existsSync } from "fs";
|
|
8
9
|
|
|
9
10
|
export interface WorktreeInfo {
|
|
10
11
|
path: string;
|
|
@@ -84,3 +85,51 @@ export async function listWorktrees(): Promise<WorktreeInfo[]> {
|
|
|
84
85
|
}
|
|
85
86
|
return worktrees;
|
|
86
87
|
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Clean up orphaned worktrees older than maxAgeMs (default: 24 hours).
|
|
91
|
+
* Safe to call on startup or periodically.
|
|
92
|
+
*/
|
|
93
|
+
export async function cleanupOrphanedWorktrees(maxAgeMs: number = 24 * 60 * 60 * 1000): Promise<number> {
|
|
94
|
+
if (!existsSync(WORKTREE_DIR)) return 0;
|
|
95
|
+
|
|
96
|
+
let cleaned = 0;
|
|
97
|
+
const now = Date.now();
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const entries = await readdir(WORKTREE_DIR);
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
const fullPath = join(WORKTREE_DIR, entry);
|
|
103
|
+
try {
|
|
104
|
+
const stats = await stat(fullPath);
|
|
105
|
+
if (stats.isDirectory() && (now - stats.mtimeMs) > maxAgeMs) {
|
|
106
|
+
await removeWorktree(fullPath);
|
|
107
|
+
cleaned++;
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
// Skip entries we can't stat
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
// WORKTREE_DIR unreadable — skip
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return cleaned;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Cleanup hook for process exit — remove all worktrees from this session.
|
|
122
|
+
* Register with process.on("exit") or signal handlers.
|
|
123
|
+
*/
|
|
124
|
+
export async function cleanupAllWorktrees(): Promise<void> {
|
|
125
|
+
try {
|
|
126
|
+
const worktrees = await listWorktrees();
|
|
127
|
+
for (const wt of worktrees) {
|
|
128
|
+
if (wt.path.startsWith(WORKTREE_DIR)) {
|
|
129
|
+
await removeWorktree(wt.path);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
// Best effort
|
|
134
|
+
}
|
|
135
|
+
}
|