ashlrcode 1.0.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/LICENSE +21 -0
- package/README.md +295 -0
- package/package.json +46 -0
- package/src/__tests__/branded-types.test.ts +47 -0
- package/src/__tests__/context.test.ts +163 -0
- package/src/__tests__/cost-tracker.test.ts +274 -0
- package/src/__tests__/cron.test.ts +197 -0
- package/src/__tests__/dream.test.ts +204 -0
- package/src/__tests__/error-handler.test.ts +192 -0
- package/src/__tests__/features.test.ts +69 -0
- package/src/__tests__/file-history.test.ts +177 -0
- package/src/__tests__/hooks.test.ts +145 -0
- package/src/__tests__/keybindings.test.ts +159 -0
- package/src/__tests__/model-patches.test.ts +82 -0
- package/src/__tests__/permissions-rules.test.ts +121 -0
- package/src/__tests__/permissions.test.ts +108 -0
- package/src/__tests__/project-config.test.ts +63 -0
- package/src/__tests__/retry.test.ts +321 -0
- package/src/__tests__/router.test.ts +158 -0
- package/src/__tests__/session-compact.test.ts +191 -0
- package/src/__tests__/session.test.ts +145 -0
- package/src/__tests__/skill-registry.test.ts +130 -0
- package/src/__tests__/speculation.test.ts +196 -0
- package/src/__tests__/tasks-v2.test.ts +267 -0
- package/src/__tests__/telemetry.test.ts +149 -0
- package/src/__tests__/tool-executor.test.ts +141 -0
- package/src/__tests__/tool-registry.test.ts +166 -0
- package/src/__tests__/undercover.test.ts +93 -0
- package/src/__tests__/workflow.test.ts +195 -0
- package/src/agent/async-context.ts +64 -0
- package/src/agent/context.ts +245 -0
- package/src/agent/cron.ts +189 -0
- package/src/agent/dream.ts +165 -0
- package/src/agent/error-handler.ts +108 -0
- package/src/agent/ipc.ts +256 -0
- package/src/agent/kairos.ts +207 -0
- package/src/agent/loop.ts +314 -0
- package/src/agent/model-patches.ts +68 -0
- package/src/agent/speculation.ts +219 -0
- package/src/agent/sub-agent.ts +125 -0
- package/src/agent/system-prompt.ts +231 -0
- package/src/agent/team.ts +220 -0
- package/src/agent/tool-executor.ts +162 -0
- package/src/agent/workflow.ts +189 -0
- package/src/agent/worktree-manager.ts +86 -0
- package/src/autopilot/queue.ts +186 -0
- package/src/autopilot/scanner.ts +245 -0
- package/src/autopilot/types.ts +58 -0
- package/src/bridge/bridge-client.ts +57 -0
- package/src/bridge/bridge-server.ts +81 -0
- package/src/cli.ts +1120 -0
- package/src/config/features.ts +51 -0
- package/src/config/git.ts +137 -0
- package/src/config/hooks.ts +201 -0
- package/src/config/permissions.ts +251 -0
- package/src/config/project-config.ts +63 -0
- package/src/config/remote-settings.ts +163 -0
- package/src/config/settings-sync.ts +170 -0
- package/src/config/settings.ts +113 -0
- package/src/config/undercover.ts +76 -0
- package/src/config/upgrade-notice.ts +65 -0
- package/src/mcp/client.ts +197 -0
- package/src/mcp/manager.ts +125 -0
- package/src/mcp/oauth.ts +252 -0
- package/src/mcp/types.ts +61 -0
- package/src/persistence/memory.ts +129 -0
- package/src/persistence/session.ts +289 -0
- package/src/planning/plan-mode.ts +128 -0
- package/src/planning/plan-tools.ts +138 -0
- package/src/providers/anthropic.ts +177 -0
- package/src/providers/cost-tracker.ts +184 -0
- package/src/providers/retry.ts +264 -0
- package/src/providers/router.ts +159 -0
- package/src/providers/types.ts +79 -0
- package/src/providers/xai.ts +217 -0
- package/src/repl.tsx +1384 -0
- package/src/setup.ts +119 -0
- package/src/skills/loader.ts +78 -0
- package/src/skills/registry.ts +78 -0
- package/src/skills/types.ts +11 -0
- package/src/state/file-history.ts +264 -0
- package/src/telemetry/event-log.ts +116 -0
- package/src/tools/agent.ts +133 -0
- package/src/tools/ask-user.ts +229 -0
- package/src/tools/bash.ts +146 -0
- package/src/tools/config.ts +147 -0
- package/src/tools/diff.ts +137 -0
- package/src/tools/file-edit.ts +123 -0
- package/src/tools/file-read.ts +82 -0
- package/src/tools/file-write.ts +82 -0
- package/src/tools/glob.ts +76 -0
- package/src/tools/grep.ts +187 -0
- package/src/tools/ls.ts +77 -0
- package/src/tools/lsp.ts +375 -0
- package/src/tools/mcp-resources.ts +83 -0
- package/src/tools/mcp-tool.ts +47 -0
- package/src/tools/memory.ts +148 -0
- package/src/tools/notebook-edit.ts +133 -0
- package/src/tools/peers.ts +113 -0
- package/src/tools/powershell.ts +83 -0
- package/src/tools/registry.ts +114 -0
- package/src/tools/send-message.ts +75 -0
- package/src/tools/sleep.ts +50 -0
- package/src/tools/snip.ts +143 -0
- package/src/tools/tasks.ts +349 -0
- package/src/tools/team.ts +309 -0
- package/src/tools/todo-write.ts +93 -0
- package/src/tools/tool-search.ts +83 -0
- package/src/tools/types.ts +52 -0
- package/src/tools/web-browser.ts +263 -0
- package/src/tools/web-fetch.ts +118 -0
- package/src/tools/web-search.ts +107 -0
- package/src/tools/workflow.ts +188 -0
- package/src/tools/worktree.ts +143 -0
- package/src/types/branded.ts +22 -0
- package/src/ui/App.tsx +184 -0
- package/src/ui/BuddyPanel.tsx +52 -0
- package/src/ui/PermissionPrompt.tsx +29 -0
- package/src/ui/banner.ts +217 -0
- package/src/ui/buddy-ai.ts +108 -0
- package/src/ui/buddy.ts +466 -0
- package/src/ui/context-bar.ts +60 -0
- package/src/ui/effort.ts +65 -0
- package/src/ui/keybindings.ts +143 -0
- package/src/ui/markdown.ts +271 -0
- package/src/ui/message-renderer.ts +73 -0
- package/src/ui/mode.ts +80 -0
- package/src/ui/notifications.ts +57 -0
- package/src/ui/speech-bubble.ts +95 -0
- package/src/ui/spinner.ts +116 -0
- package/src/ui/theme.ts +98 -0
- package/src/version.ts +5 -0
- package/src/voice/voice-mode.ts +169 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming Tool Executor — parallel execution for concurrency-safe tools.
|
|
3
|
+
*
|
|
4
|
+
* Partitions tool calls by isConcurrencySafe():
|
|
5
|
+
* - Safe tools run in parallel via Promise.all()
|
|
6
|
+
* - Unsafe tools run sequentially
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ToolRegistry } from "../tools/registry.ts";
|
|
10
|
+
import type { ToolContext } from "../tools/types.ts";
|
|
11
|
+
import type { ToolCall } from "../providers/types.ts";
|
|
12
|
+
import type { SpeculationCache } from "./speculation.ts";
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Module-level speculation cache (set from repl startup)
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
let _speculationCache: SpeculationCache | null = null;
|
|
19
|
+
|
|
20
|
+
export function setSpeculationCache(cache: SpeculationCache): void {
|
|
21
|
+
_speculationCache = cache;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getSpeculationCache(): SpeculationCache | null {
|
|
25
|
+
return _speculationCache;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ToolExecutionResult {
|
|
29
|
+
toolCallId: string;
|
|
30
|
+
name: string;
|
|
31
|
+
input: Record<string, unknown>;
|
|
32
|
+
result: string;
|
|
33
|
+
isError: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Execute tool calls with optimal parallelism.
|
|
38
|
+
*/
|
|
39
|
+
export async function executeToolCalls(
|
|
40
|
+
toolCalls: ToolCall[],
|
|
41
|
+
registry: ToolRegistry,
|
|
42
|
+
context: ToolContext,
|
|
43
|
+
callbacks?: {
|
|
44
|
+
onToolStart?: (name: string, input: Record<string, unknown>) => void;
|
|
45
|
+
onToolEnd?: (name: string, result: string, isError: boolean) => void;
|
|
46
|
+
}
|
|
47
|
+
): Promise<ToolExecutionResult[]> {
|
|
48
|
+
if (toolCalls.length === 0) return [];
|
|
49
|
+
if (toolCalls.length === 1) {
|
|
50
|
+
return [await executeSingle(toolCalls[0]!, registry, context, callbacks)];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Partition by concurrency safety
|
|
54
|
+
const safe: ToolCall[] = [];
|
|
55
|
+
const unsafe: ToolCall[] = [];
|
|
56
|
+
|
|
57
|
+
for (const tc of toolCalls) {
|
|
58
|
+
const tool = registry.get(tc.name);
|
|
59
|
+
if (tool?.isConcurrencySafe()) {
|
|
60
|
+
safe.push(tc);
|
|
61
|
+
} else {
|
|
62
|
+
unsafe.push(tc);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const results: ToolExecutionResult[] = [];
|
|
67
|
+
|
|
68
|
+
// Run safe tools in parallel
|
|
69
|
+
if (safe.length > 0) {
|
|
70
|
+
const parallelResults = await Promise.all(
|
|
71
|
+
safe.map((tc) => executeSingle(tc, registry, context, callbacks))
|
|
72
|
+
);
|
|
73
|
+
results.push(...parallelResults);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Run unsafe tools sequentially
|
|
77
|
+
for (const tc of unsafe) {
|
|
78
|
+
const result = await executeSingle(tc, registry, context, callbacks);
|
|
79
|
+
results.push(result);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Restore original tool call ordering
|
|
83
|
+
const orderMap = new Map(toolCalls.map((tc, i) => [tc.id, i]));
|
|
84
|
+
results.sort((a, b) => (orderMap.get(a.toolCallId) ?? 0) - (orderMap.get(b.toolCallId) ?? 0));
|
|
85
|
+
|
|
86
|
+
return results;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Recent tool calls tracked for speculation heuristics. */
|
|
90
|
+
const recentToolCalls: Array<{ name: string; input: Record<string, unknown>; result?: string }> = [];
|
|
91
|
+
const MAX_RECENT = 10;
|
|
92
|
+
|
|
93
|
+
async function executeSingle(
|
|
94
|
+
tc: ToolCall,
|
|
95
|
+
registry: ToolRegistry,
|
|
96
|
+
context: ToolContext,
|
|
97
|
+
callbacks?: {
|
|
98
|
+
onToolStart?: (name: string, input: Record<string, unknown>) => void;
|
|
99
|
+
onToolEnd?: (name: string, result: string, isError: boolean) => void;
|
|
100
|
+
}
|
|
101
|
+
): Promise<ToolExecutionResult> {
|
|
102
|
+
const tool = registry.get(tc.name);
|
|
103
|
+
|
|
104
|
+
// Check speculation cache for read-only tools (skip the full execute path)
|
|
105
|
+
if (tool?.isReadOnly() && _speculationCache) {
|
|
106
|
+
const cached = _speculationCache.get(tc.name, tc.input);
|
|
107
|
+
if (cached !== null) {
|
|
108
|
+
callbacks?.onToolStart?.(tc.name, tc.input);
|
|
109
|
+
callbacks?.onToolEnd?.(tc.name, cached, false);
|
|
110
|
+
|
|
111
|
+
// Track for speculation and trigger pre-fetch for next likely call
|
|
112
|
+
trackAndSpeculate(tc.name, tc.input, cached);
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
toolCallId: tc.id,
|
|
116
|
+
name: tc.name,
|
|
117
|
+
input: tc.input,
|
|
118
|
+
result: cached,
|
|
119
|
+
isError: false,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
callbacks?.onToolStart?.(tc.name, tc.input);
|
|
125
|
+
|
|
126
|
+
const { result, isError } = await registry.execute(tc.name, tc.input, context);
|
|
127
|
+
|
|
128
|
+
callbacks?.onToolEnd?.(tc.name, result, isError);
|
|
129
|
+
|
|
130
|
+
// Cache successful read-only results for future speculation
|
|
131
|
+
if (tool?.isReadOnly() && _speculationCache && !isError) {
|
|
132
|
+
_speculationCache.set(tc.name, tc.input, result);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Invalidate cache when write tools execute
|
|
136
|
+
if (!tool?.isReadOnly() && _speculationCache) {
|
|
137
|
+
const filePath = tc.input.file_path;
|
|
138
|
+
if (typeof filePath === "string") {
|
|
139
|
+
_speculationCache.invalidateForFile(filePath);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Track and speculatively pre-fetch next likely calls
|
|
144
|
+
trackAndSpeculate(tc.name, tc.input, isError ? undefined : result);
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
toolCallId: tc.id,
|
|
148
|
+
name: tc.name,
|
|
149
|
+
input: tc.input,
|
|
150
|
+
result,
|
|
151
|
+
isError,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Track a tool call and kick off speculative pre-fetching in background. */
|
|
156
|
+
function trackAndSpeculate(name: string, input: Record<string, unknown>, result?: string): void {
|
|
157
|
+
recentToolCalls.push({ name, input, result });
|
|
158
|
+
if (recentToolCalls.length > MAX_RECENT) recentToolCalls.shift();
|
|
159
|
+
|
|
160
|
+
// Fire-and-forget — speculation failures are harmless
|
|
161
|
+
_speculationCache?.speculateFromHistory(recentToolCalls).catch(() => {});
|
|
162
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow system — define and run multi-step automation scripts.
|
|
3
|
+
* Workflows are stored as JSON files in .ashlrcode/workflows/
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync } from "fs";
|
|
7
|
+
import { readFile, writeFile, readdir, mkdir, unlink } from "fs/promises";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import { getConfigDir } from "../config/settings.ts";
|
|
10
|
+
|
|
11
|
+
export interface WorkflowStep {
|
|
12
|
+
name: string;
|
|
13
|
+
type: "prompt" | "command" | "tool";
|
|
14
|
+
value: string; // prompt text, shell command, or tool name
|
|
15
|
+
input?: Record<string, unknown>; // for tool type
|
|
16
|
+
continueOnError?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface Workflow {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
description: string;
|
|
23
|
+
steps: WorkflowStep[];
|
|
24
|
+
createdAt: string;
|
|
25
|
+
lastRunAt?: string;
|
|
26
|
+
runCount: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface WorkflowResult {
|
|
30
|
+
workflow: string;
|
|
31
|
+
stepsCompleted: number;
|
|
32
|
+
stepsTotal: number;
|
|
33
|
+
results: Array<{ step: string; success: boolean; output: string }>;
|
|
34
|
+
durationMs: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getWorkflowsDir(): string {
|
|
38
|
+
return join(getConfigDir(), "workflows");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function createWorkflow(
|
|
42
|
+
name: string,
|
|
43
|
+
description: string,
|
|
44
|
+
steps: WorkflowStep[],
|
|
45
|
+
): Promise<Workflow> {
|
|
46
|
+
await mkdir(getWorkflowsDir(), { recursive: true });
|
|
47
|
+
const workflow: Workflow = {
|
|
48
|
+
id: `wf-${Date.now()}`,
|
|
49
|
+
name,
|
|
50
|
+
description,
|
|
51
|
+
steps,
|
|
52
|
+
createdAt: new Date().toISOString(),
|
|
53
|
+
runCount: 0,
|
|
54
|
+
};
|
|
55
|
+
await saveWorkflow(workflow);
|
|
56
|
+
return workflow;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function listWorkflows(): Promise<Workflow[]> {
|
|
60
|
+
const dir = getWorkflowsDir();
|
|
61
|
+
if (!existsSync(dir)) return [];
|
|
62
|
+
const files = await readdir(dir);
|
|
63
|
+
const workflows: Workflow[] = [];
|
|
64
|
+
for (const file of files.filter((f) => f.endsWith(".json"))) {
|
|
65
|
+
try {
|
|
66
|
+
const raw = await readFile(join(dir, file), "utf-8");
|
|
67
|
+
workflows.push(JSON.parse(raw) as Workflow);
|
|
68
|
+
} catch {
|
|
69
|
+
// skip malformed workflow files
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return workflows.sort((a, b) => a.name.localeCompare(b.name));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function loadWorkflow(id: string): Promise<Workflow | null> {
|
|
76
|
+
const path = join(getWorkflowsDir(), `${id}.json`);
|
|
77
|
+
if (!existsSync(path)) return null;
|
|
78
|
+
try {
|
|
79
|
+
return JSON.parse(await readFile(path, "utf-8"));
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function deleteWorkflow(id: string): Promise<boolean> {
|
|
86
|
+
const path = join(getWorkflowsDir(), `${id}.json`);
|
|
87
|
+
if (!existsSync(path)) return false;
|
|
88
|
+
await unlink(path);
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function markWorkflowRun(id: string): Promise<void> {
|
|
93
|
+
const wf = await loadWorkflow(id);
|
|
94
|
+
if (!wf) return;
|
|
95
|
+
wf.lastRunAt = new Date().toISOString();
|
|
96
|
+
wf.runCount++;
|
|
97
|
+
await saveWorkflow(wf);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function saveWorkflow(wf: Workflow): Promise<void> {
|
|
101
|
+
await mkdir(getWorkflowsDir(), { recursive: true });
|
|
102
|
+
await writeFile(
|
|
103
|
+
join(getWorkflowsDir(), `${wf.id}.json`),
|
|
104
|
+
JSON.stringify(wf, null, 2),
|
|
105
|
+
"utf-8",
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Execute a workflow step by step.
|
|
111
|
+
* The executor callbacks allow the caller to wire up actual prompt/command/tool
|
|
112
|
+
* execution and observe progress.
|
|
113
|
+
*/
|
|
114
|
+
export async function executeWorkflow(
|
|
115
|
+
workflow: Workflow,
|
|
116
|
+
executor: {
|
|
117
|
+
runPrompt: (prompt: string) => Promise<string>;
|
|
118
|
+
runCommand: (cmd: string) => Promise<{ output: string; exitCode: number }>;
|
|
119
|
+
runTool: (
|
|
120
|
+
name: string,
|
|
121
|
+
input: Record<string, unknown>,
|
|
122
|
+
) => Promise<{ result: string; isError: boolean }>;
|
|
123
|
+
onStepStart: (step: WorkflowStep, index: number) => void;
|
|
124
|
+
onStepEnd: (
|
|
125
|
+
step: WorkflowStep,
|
|
126
|
+
index: number,
|
|
127
|
+
success: boolean,
|
|
128
|
+
output: string,
|
|
129
|
+
) => void;
|
|
130
|
+
},
|
|
131
|
+
): Promise<WorkflowResult> {
|
|
132
|
+
const startTime = Date.now();
|
|
133
|
+
const results: WorkflowResult["results"] = [];
|
|
134
|
+
let completed = 0;
|
|
135
|
+
|
|
136
|
+
for (let i = 0; i < workflow.steps.length; i++) {
|
|
137
|
+
const step = workflow.steps[i]!;
|
|
138
|
+
executor.onStepStart(step, i);
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
let output: string;
|
|
142
|
+
let success: boolean;
|
|
143
|
+
|
|
144
|
+
switch (step.type) {
|
|
145
|
+
case "prompt":
|
|
146
|
+
output = await executor.runPrompt(step.value);
|
|
147
|
+
success = true;
|
|
148
|
+
break;
|
|
149
|
+
case "command": {
|
|
150
|
+
const cmdResult = await executor.runCommand(step.value);
|
|
151
|
+
output = cmdResult.output;
|
|
152
|
+
success = cmdResult.exitCode === 0;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
case "tool": {
|
|
156
|
+
const toolResult = await executor.runTool(
|
|
157
|
+
step.value,
|
|
158
|
+
step.input ?? {},
|
|
159
|
+
);
|
|
160
|
+
output = toolResult.result;
|
|
161
|
+
success = !toolResult.isError;
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
default:
|
|
165
|
+
output = `Unknown step type: ${(step as WorkflowStep).type}`;
|
|
166
|
+
success = false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
results.push({ step: step.name, success, output: output.slice(0, 500) });
|
|
170
|
+
executor.onStepEnd(step, i, success, output);
|
|
171
|
+
completed++;
|
|
172
|
+
|
|
173
|
+
if (!success && !step.continueOnError) break;
|
|
174
|
+
} catch (err) {
|
|
175
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
176
|
+
results.push({ step: step.name, success: false, output: msg });
|
|
177
|
+
executor.onStepEnd(step, i, false, msg);
|
|
178
|
+
if (!step.continueOnError) break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
workflow: workflow.name,
|
|
184
|
+
stepsCompleted: completed,
|
|
185
|
+
stepsTotal: workflow.steps.length,
|
|
186
|
+
results,
|
|
187
|
+
durationMs: Date.now() - startTime,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worktree Manager — manages isolated git worktrees for sub-agents.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
import { mkdir } from "fs/promises";
|
|
8
|
+
|
|
9
|
+
export interface WorktreeInfo {
|
|
10
|
+
path: string;
|
|
11
|
+
branch: string;
|
|
12
|
+
parentBranch: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const WORKTREE_DIR = join(homedir(), ".ashlrcode", "worktrees");
|
|
16
|
+
|
|
17
|
+
export async function createWorktree(name: string): Promise<WorktreeInfo> {
|
|
18
|
+
await mkdir(WORKTREE_DIR, { recursive: true });
|
|
19
|
+
|
|
20
|
+
const safeName = name.replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 50);
|
|
21
|
+
const timestamp = Date.now();
|
|
22
|
+
const branch = `agent/${safeName}-${timestamp}`;
|
|
23
|
+
const wtPath = join(WORKTREE_DIR, `${safeName}-${timestamp}`);
|
|
24
|
+
|
|
25
|
+
// Get current branch — read streams before awaiting exit to avoid deadlock
|
|
26
|
+
const headProc = Bun.spawn(["git", "rev-parse", "--abbrev-ref", "HEAD"], {
|
|
27
|
+
stdout: "pipe",
|
|
28
|
+
stderr: "pipe",
|
|
29
|
+
});
|
|
30
|
+
const headStdoutPromise = new Response(headProc.stdout).text();
|
|
31
|
+
const headStderrPromise = new Response(headProc.stderr).text();
|
|
32
|
+
const headExit = await headProc.exited;
|
|
33
|
+
const parentBranch = (await headStdoutPromise).trim();
|
|
34
|
+
if (headExit !== 0 || !parentBranch) {
|
|
35
|
+
const headStderr = await headStderrPromise;
|
|
36
|
+
throw new Error(`Not inside a git repository: ${headStderr}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Create worktree with new branch — same pattern: read streams before exit
|
|
40
|
+
const proc = Bun.spawn(["git", "worktree", "add", "-b", branch, wtPath], {
|
|
41
|
+
stdout: "pipe",
|
|
42
|
+
stderr: "pipe",
|
|
43
|
+
});
|
|
44
|
+
const procStdoutPromise = new Response(proc.stdout).text();
|
|
45
|
+
const procStderrPromise = new Response(proc.stderr).text();
|
|
46
|
+
const exitCode = await proc.exited;
|
|
47
|
+
if (exitCode !== 0) {
|
|
48
|
+
const stderr = await procStderrPromise;
|
|
49
|
+
throw new Error(`Failed to create worktree: ${stderr}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { path: wtPath, branch, parentBranch };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function removeWorktree(path: string): Promise<void> {
|
|
56
|
+
const proc = Bun.spawn(
|
|
57
|
+
["git", "worktree", "remove", path, "--force"],
|
|
58
|
+
{ stdout: "pipe", stderr: "pipe" },
|
|
59
|
+
);
|
|
60
|
+
await proc.exited;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function listWorktrees(): Promise<WorktreeInfo[]> {
|
|
64
|
+
const proc = Bun.spawn(["git", "worktree", "list", "--porcelain"], {
|
|
65
|
+
stdout: "pipe",
|
|
66
|
+
stderr: "pipe",
|
|
67
|
+
});
|
|
68
|
+
const output = (await new Response(proc.stdout).text()).trim();
|
|
69
|
+
await proc.exited;
|
|
70
|
+
|
|
71
|
+
const worktrees: WorktreeInfo[] = [];
|
|
72
|
+
const blocks = output.split("\n\n");
|
|
73
|
+
for (const block of blocks) {
|
|
74
|
+
const lines = block.split("\n");
|
|
75
|
+
const pathLine = lines.find((l) => l.startsWith("worktree "));
|
|
76
|
+
const branchLine = lines.find((l) => l.startsWith("branch "));
|
|
77
|
+
if (pathLine && branchLine) {
|
|
78
|
+
worktrees.push({
|
|
79
|
+
path: pathLine.replace("worktree ", ""),
|
|
80
|
+
branch: branchLine.replace("branch refs/heads/", ""),
|
|
81
|
+
parentBranch: "main",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return worktrees;
|
|
86
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Work queue — stores and manages discovered work items.
|
|
3
|
+
* Persisted to ~/.ashlrcode/autopilot/<project-hash>.json
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync } from "fs";
|
|
7
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import { createHash } from "crypto";
|
|
10
|
+
import { getConfigDir } from "../config/settings.ts";
|
|
11
|
+
import type { WorkItem, WorkItemStatus } from "./types.ts";
|
|
12
|
+
|
|
13
|
+
function getQueueDir(): string {
|
|
14
|
+
return join(getConfigDir(), "autopilot");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getQueuePath(cwd: string): string {
|
|
18
|
+
const hash = createHash("sha256").update(cwd).digest("hex").slice(0, 12);
|
|
19
|
+
return join(getQueueDir(), `${hash}.json`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class WorkQueue {
|
|
23
|
+
private items: WorkItem[] = [];
|
|
24
|
+
private path: string;
|
|
25
|
+
|
|
26
|
+
constructor(cwd: string) {
|
|
27
|
+
this.path = getQueuePath(cwd);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async load(): Promise<void> {
|
|
31
|
+
if (!existsSync(this.path)) return;
|
|
32
|
+
try {
|
|
33
|
+
const raw = await readFile(this.path, "utf-8");
|
|
34
|
+
this.items = JSON.parse(raw) as WorkItem[];
|
|
35
|
+
} catch {
|
|
36
|
+
this.items = [];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async save(): Promise<void> {
|
|
41
|
+
await mkdir(getQueueDir(), { recursive: true });
|
|
42
|
+
await writeFile(this.path, JSON.stringify(this.items, null, 2), "utf-8");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Add new items, deduplicating by file + line + type.
|
|
47
|
+
*/
|
|
48
|
+
addItems(newItems: WorkItem[]): number {
|
|
49
|
+
let added = 0;
|
|
50
|
+
for (const item of newItems) {
|
|
51
|
+
const exists = this.items.some(
|
|
52
|
+
(i) => i.file === item.file && i.line === item.line && i.type === item.type && i.status !== "completed" && i.status !== "rejected"
|
|
53
|
+
);
|
|
54
|
+
if (!exists) {
|
|
55
|
+
this.items.push(item);
|
|
56
|
+
added++;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return added;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get items by status, sorted by priority.
|
|
64
|
+
*/
|
|
65
|
+
getByStatus(status: WorkItemStatus): WorkItem[] {
|
|
66
|
+
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
67
|
+
return this.items
|
|
68
|
+
.filter((i) => i.status === status)
|
|
69
|
+
.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get all pending items (discovered + approved).
|
|
74
|
+
*/
|
|
75
|
+
getPending(): WorkItem[] {
|
|
76
|
+
return [...this.getByStatus("discovered"), ...this.getByStatus("approved")];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Approve an item for execution.
|
|
81
|
+
*/
|
|
82
|
+
approve(id: string): boolean {
|
|
83
|
+
const item = this.items.find((i) => i.id === id);
|
|
84
|
+
if (item && item.status === "discovered") {
|
|
85
|
+
item.status = "approved";
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Approve all discovered items.
|
|
93
|
+
*/
|
|
94
|
+
approveAll(): number {
|
|
95
|
+
let count = 0;
|
|
96
|
+
for (const item of this.items) {
|
|
97
|
+
if (item.status === "discovered") {
|
|
98
|
+
item.status = "approved";
|
|
99
|
+
count++;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return count;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Reject an item.
|
|
107
|
+
*/
|
|
108
|
+
reject(id: string): boolean {
|
|
109
|
+
const item = this.items.find((i) => i.id === id);
|
|
110
|
+
if (item && item.status === "discovered") {
|
|
111
|
+
item.status = "rejected";
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Mark item as in progress.
|
|
119
|
+
*/
|
|
120
|
+
startItem(id: string): WorkItem | null {
|
|
121
|
+
const item = this.items.find((i) => i.id === id);
|
|
122
|
+
if (item && item.status === "approved") {
|
|
123
|
+
item.status = "in_progress";
|
|
124
|
+
return item;
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Mark item as completed.
|
|
131
|
+
*/
|
|
132
|
+
completeItem(id: string): void {
|
|
133
|
+
const item = this.items.find((i) => i.id === id);
|
|
134
|
+
if (item) {
|
|
135
|
+
item.status = "completed";
|
|
136
|
+
item.completedAt = new Date().toISOString();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Mark item as failed.
|
|
142
|
+
*/
|
|
143
|
+
failItem(id: string, error: string): void {
|
|
144
|
+
const item = this.items.find((i) => i.id === id);
|
|
145
|
+
if (item) {
|
|
146
|
+
item.status = "failed";
|
|
147
|
+
item.error = error;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get summary stats.
|
|
153
|
+
*/
|
|
154
|
+
getStats(): Record<string, number> {
|
|
155
|
+
const stats: Record<string, number> = {};
|
|
156
|
+
for (const item of this.items) {
|
|
157
|
+
stats[item.status] = (stats[item.status] ?? 0) + 1;
|
|
158
|
+
}
|
|
159
|
+
return stats;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Get the next approved item to work on.
|
|
164
|
+
*/
|
|
165
|
+
getNextApproved(): WorkItem | null {
|
|
166
|
+
return this.getByStatus("approved")[0] ?? null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Total items.
|
|
171
|
+
*/
|
|
172
|
+
get length(): number {
|
|
173
|
+
return this.items.length;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Clean old completed/rejected items (keep last 100).
|
|
178
|
+
*/
|
|
179
|
+
cleanup(): void {
|
|
180
|
+
const active = this.items.filter((i) => !["completed", "rejected", "failed"].includes(i.status));
|
|
181
|
+
const archived = this.items
|
|
182
|
+
.filter((i) => ["completed", "rejected", "failed"].includes(i.status))
|
|
183
|
+
.slice(-100);
|
|
184
|
+
this.items = [...active, ...archived];
|
|
185
|
+
}
|
|
186
|
+
}
|