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.
Files changed (104) hide show
  1. package/README.md +73 -16
  2. package/package.json +28 -9
  3. package/prompts/skills/commit.md +36 -0
  4. package/prompts/skills/coordinate.md +21 -0
  5. package/prompts/skills/daily-review.md +65 -0
  6. package/prompts/skills/debug.md +23 -0
  7. package/prompts/skills/deep-work.md +129 -0
  8. package/prompts/skills/explore.md +24 -0
  9. package/prompts/skills/init.md +39 -0
  10. package/prompts/skills/kairos.md +19 -0
  11. package/prompts/skills/plan.md +19 -0
  12. package/prompts/skills/polish.md +94 -0
  13. package/prompts/skills/pr.md +30 -0
  14. package/prompts/skills/refactor.md +26 -0
  15. package/prompts/skills/resume-branch.md +27 -0
  16. package/prompts/skills/review.md +27 -0
  17. package/prompts/skills/ship.md +32 -0
  18. package/prompts/skills/simplify.md +25 -0
  19. package/prompts/skills/test.md +19 -0
  20. package/prompts/skills/verify.md +17 -0
  21. package/prompts/skills/weekly-plan.md +63 -0
  22. package/prompts/system.md +451 -0
  23. package/src/agent/away-summary.ts +138 -0
  24. package/src/agent/context.ts +6 -0
  25. package/src/agent/coordinator.ts +494 -0
  26. package/src/agent/dream.ts +149 -11
  27. package/src/agent/error-handler.ts +51 -35
  28. package/src/agent/kairos.ts +52 -4
  29. package/src/agent/loop.ts +153 -13
  30. package/src/agent/mailbox.ts +151 -0
  31. package/src/agent/model-patches.ts +28 -3
  32. package/src/agent/product-agent.ts +463 -0
  33. package/src/agent/speculation.ts +21 -18
  34. package/src/agent/sub-agent.ts +11 -1
  35. package/src/agent/system-prompt.ts +19 -0
  36. package/src/agent/tool-executor.ts +83 -3
  37. package/src/agent/verification.ts +223 -0
  38. package/src/agent/worktree-manager.ts +50 -1
  39. package/src/cli.ts +228 -36
  40. package/src/config/features.ts +8 -8
  41. package/src/config/keychain.ts +105 -0
  42. package/src/config/permissions.ts +3 -2
  43. package/src/config/settings.ts +73 -5
  44. package/src/config/upgrade-notice.ts +15 -2
  45. package/src/mcp/client.ts +392 -2
  46. package/src/mcp/manager.ts +129 -13
  47. package/src/mcp/types.ts +4 -1
  48. package/src/migrate.ts +228 -0
  49. package/src/persistence/session.ts +209 -5
  50. package/src/providers/anthropic.ts +112 -98
  51. package/src/providers/cost-tracker.ts +71 -2
  52. package/src/providers/retry.ts +2 -4
  53. package/src/providers/types.ts +5 -1
  54. package/src/providers/xai.ts +1 -0
  55. package/src/repl.tsx +514 -127
  56. package/src/setup.ts +37 -1
  57. package/src/tools/coordinate.ts +88 -0
  58. package/src/tools/grep.ts +9 -11
  59. package/src/tools/lsp.ts +44 -32
  60. package/src/tools/registry.ts +75 -9
  61. package/src/tools/send-message.ts +89 -30
  62. package/src/tools/types.ts +2 -0
  63. package/src/tools/verify.ts +88 -0
  64. package/src/tools/web-browser.ts +8 -5
  65. package/src/tools/workflow.ts +34 -10
  66. package/src/ui/AnimatedSpinner.tsx +302 -0
  67. package/src/ui/App.tsx +16 -15
  68. package/src/ui/BuddyPanel.tsx +27 -34
  69. package/src/ui/SlashInput.tsx +99 -0
  70. package/src/ui/banner.ts +10 -0
  71. package/src/ui/buddy.ts +5 -4
  72. package/src/ui/effort.ts +5 -1
  73. package/src/ui/markdown.ts +269 -88
  74. package/src/ui/message-renderer.ts +183 -35
  75. package/src/ui/quips.json +41 -0
  76. package/src/ui/speech-bubble.ts +35 -19
  77. package/src/utils/ring-buffer.ts +101 -0
  78. package/src/voice/voice-mode.ts +13 -2
  79. package/src/__tests__/branded-types.test.ts +0 -47
  80. package/src/__tests__/context.test.ts +0 -163
  81. package/src/__tests__/cost-tracker.test.ts +0 -274
  82. package/src/__tests__/cron.test.ts +0 -197
  83. package/src/__tests__/dream.test.ts +0 -204
  84. package/src/__tests__/error-handler.test.ts +0 -192
  85. package/src/__tests__/features.test.ts +0 -69
  86. package/src/__tests__/file-history.test.ts +0 -177
  87. package/src/__tests__/hooks.test.ts +0 -145
  88. package/src/__tests__/keybindings.test.ts +0 -159
  89. package/src/__tests__/model-patches.test.ts +0 -82
  90. package/src/__tests__/permissions-rules.test.ts +0 -121
  91. package/src/__tests__/permissions.test.ts +0 -108
  92. package/src/__tests__/project-config.test.ts +0 -63
  93. package/src/__tests__/retry.test.ts +0 -321
  94. package/src/__tests__/router.test.ts +0 -158
  95. package/src/__tests__/session-compact.test.ts +0 -191
  96. package/src/__tests__/session.test.ts +0 -145
  97. package/src/__tests__/skill-registry.test.ts +0 -130
  98. package/src/__tests__/speculation.test.ts +0 -196
  99. package/src/__tests__/tasks-v2.test.ts +0 -267
  100. package/src/__tests__/telemetry.test.ts +0 -149
  101. package/src/__tests__/tool-executor.test.ts +0 -141
  102. package/src/__tests__/tool-registry.test.ts +0 -166
  103. package/src/__tests__/undercover.test.ts +0 -93
  104. 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() && _speculationCache) {
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.invalidateForFile(filePath);
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
+ }