mcp-copilot-cli 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/README.md +207 -0
- package/bin/mcp-copilot-cli.mjs +3 -0
- package/dist/src/app.d.ts +71 -0
- package/dist/src/app.js +303 -0
- package/dist/src/app.js.map +1 -0
- package/dist/src/cli/doctor.d.ts +2 -0
- package/dist/src/cli/doctor.js +99 -0
- package/dist/src/cli/doctor.js.map +1 -0
- package/dist/src/config/defaults.d.ts +3 -0
- package/dist/src/config/defaults.js +4 -0
- package/dist/src/config/defaults.js.map +1 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +98 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/mcp/system-status.d.ts +19 -0
- package/dist/src/mcp/system-status.js +20 -0
- package/dist/src/mcp/system-status.js.map +1 -0
- package/dist/src/mcp/task-markdown.d.ts +3 -0
- package/dist/src/mcp/task-markdown.js +70 -0
- package/dist/src/mcp/task-markdown.js.map +1 -0
- package/dist/src/mcp/tool-banners.d.ts +4 -0
- package/dist/src/mcp/tool-banners.js +26 -0
- package/dist/src/mcp/tool-banners.js.map +1 -0
- package/dist/src/mcp/tool-definitions.d.ts +109 -0
- package/dist/src/mcp/tool-definitions.js +125 -0
- package/dist/src/mcp/tool-definitions.js.map +1 -0
- package/dist/src/services/copilot-runtime.d.ts +28 -0
- package/dist/src/services/copilot-runtime.js +194 -0
- package/dist/src/services/copilot-runtime.js.map +1 -0
- package/dist/src/services/output-log.d.ts +7 -0
- package/dist/src/services/output-log.js +59 -0
- package/dist/src/services/output-log.js.map +1 -0
- package/dist/src/services/profile-manager.d.ts +34 -0
- package/dist/src/services/profile-manager.js +113 -0
- package/dist/src/services/profile-manager.js.map +1 -0
- package/dist/src/services/question-registry.d.ts +29 -0
- package/dist/src/services/question-registry.js +115 -0
- package/dist/src/services/question-registry.js.map +1 -0
- package/dist/src/services/spawn-validation.d.ts +9 -0
- package/dist/src/services/spawn-validation.js +53 -0
- package/dist/src/services/spawn-validation.js.map +1 -0
- package/dist/src/services/task-manager.d.ts +34 -0
- package/dist/src/services/task-manager.js +107 -0
- package/dist/src/services/task-manager.js.map +1 -0
- package/dist/src/services/task-persistence.d.ts +20 -0
- package/dist/src/services/task-persistence.js +67 -0
- package/dist/src/services/task-persistence.js.map +1 -0
- package/dist/src/services/task-store.d.ts +12 -0
- package/dist/src/services/task-store.js +167 -0
- package/dist/src/services/task-store.js.map +1 -0
- package/dist/src/services/workspace-isolation.d.ts +13 -0
- package/dist/src/services/workspace-isolation.js +74 -0
- package/dist/src/services/workspace-isolation.js.map +1 -0
- package/dist/src/templates/agent-prompt.d.ts +6 -0
- package/dist/src/templates/agent-prompt.js +58 -0
- package/dist/src/templates/agent-prompt.js.map +1 -0
- package/dist/src/types/task.d.ts +79 -0
- package/dist/src/types/task.js +22 -0
- package/dist/src/types/task.js.map +1 -0
- package/package.json +63 -0
- package/src/app.ts +341 -0
- package/src/cli/doctor.ts +112 -0
- package/src/config/defaults.ts +3 -0
- package/src/index.ts +128 -0
- package/src/mcp/system-status.ts +41 -0
- package/src/mcp/task-markdown.ts +81 -0
- package/src/mcp/tool-banners.ts +32 -0
- package/src/mcp/tool-definitions.ts +151 -0
- package/src/services/copilot-runtime.ts +247 -0
- package/src/services/output-log.ts +68 -0
- package/src/services/profile-manager.ts +165 -0
- package/src/services/question-registry.ts +169 -0
- package/src/services/spawn-validation.ts +75 -0
- package/src/services/task-manager.ts +144 -0
- package/src/services/task-persistence.ts +100 -0
- package/src/services/task-store.ts +207 -0
- package/src/services/workspace-isolation.ts +100 -0
- package/src/templates/agent-prompt.ts +71 -0
- package/src/types/task.ts +95 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
import type { PersistedProfileState, PersistedWorkspaceState, TaskRecord } from '../types/task.js';
|
|
7
|
+
import { TaskStatus } from '../types/task.js';
|
|
8
|
+
|
|
9
|
+
export interface SaveWorkspaceStateInput {
|
|
10
|
+
cwd: string;
|
|
11
|
+
storageRoot?: string | undefined;
|
|
12
|
+
tasks: TaskRecord[];
|
|
13
|
+
profiles: PersistedProfileState[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface LoadWorkspaceStateInput {
|
|
17
|
+
cwd: string;
|
|
18
|
+
storageRoot?: string | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface LoadedWorkspaceState {
|
|
22
|
+
tasks: TaskRecord[];
|
|
23
|
+
profiles: PersistedProfileState[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const STORAGE_DIR_NAME = '.super-agents';
|
|
27
|
+
|
|
28
|
+
export function defaultStorageRoot(): string {
|
|
29
|
+
return join(homedir(), STORAGE_DIR_NAME);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getWorkspaceStatePath(cwd: string, storageRoot = defaultStorageRoot()): string {
|
|
33
|
+
const digest = createHash('md5').update(cwd).digest('hex');
|
|
34
|
+
return join(storageRoot, `${digest}.json`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function recoverTask(task: TaskRecord): TaskRecord {
|
|
38
|
+
if (task.status === TaskStatus.RATE_LIMITED) {
|
|
39
|
+
return task;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (
|
|
43
|
+
task.status === TaskStatus.RUNNING ||
|
|
44
|
+
task.status === TaskStatus.PENDING ||
|
|
45
|
+
task.status === TaskStatus.WAITING ||
|
|
46
|
+
task.status === TaskStatus.WAITING_ANSWER
|
|
47
|
+
) {
|
|
48
|
+
return {
|
|
49
|
+
...task,
|
|
50
|
+
status: TaskStatus.FAILED,
|
|
51
|
+
error: 'Server restarted while task was active',
|
|
52
|
+
endTime: new Date().toISOString(),
|
|
53
|
+
updatedAt: new Date().toISOString(),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return task;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function saveWorkspaceState(input: SaveWorkspaceStateInput): Promise<void> {
|
|
61
|
+
const storageRoot = input.storageRoot ?? defaultStorageRoot();
|
|
62
|
+
const path = getWorkspaceStatePath(input.cwd, storageRoot);
|
|
63
|
+
const tempPath = `${path}.${Date.now().toString(36)}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
64
|
+
|
|
65
|
+
await mkdir(storageRoot, { recursive: true });
|
|
66
|
+
|
|
67
|
+
const payload: PersistedWorkspaceState = {
|
|
68
|
+
version: 1,
|
|
69
|
+
tasks: input.tasks,
|
|
70
|
+
profiles: input.profiles,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
await writeFile(tempPath, JSON.stringify(payload, null, 2), 'utf8');
|
|
74
|
+
await rename(tempPath, path);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function loadWorkspaceState(input: LoadWorkspaceStateInput): Promise<LoadedWorkspaceState> {
|
|
78
|
+
const storageRoot = input.storageRoot ?? defaultStorageRoot();
|
|
79
|
+
const path = getWorkspaceStatePath(input.cwd, storageRoot);
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const raw = await readFile(path, 'utf8');
|
|
83
|
+
const parsed = JSON.parse(raw) as PersistedWorkspaceState;
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
tasks: parsed.tasks.map(recoverTask),
|
|
87
|
+
profiles: parsed.profiles ?? [],
|
|
88
|
+
};
|
|
89
|
+
} catch {
|
|
90
|
+
return {
|
|
91
|
+
tasks: [],
|
|
92
|
+
profiles: [],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function deleteWorkspaceState(cwd: string, storageRoot?: string): Promise<void> {
|
|
98
|
+
const path = getWorkspaceStatePath(cwd, storageRoot ?? defaultStorageRoot());
|
|
99
|
+
await unlink(path).catch(() => {});
|
|
100
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { uniqueNamesGenerator, adjectives, animals, NumberDictionary } from 'unique-names-generator';
|
|
2
|
+
|
|
3
|
+
import type { CreateTaskInput, TaskRecord } from '../types/task.js';
|
|
4
|
+
import { TaskStatus, isTerminalStatus } from '../types/task.js';
|
|
5
|
+
|
|
6
|
+
const numberDictionary = NumberDictionary.generate({ min: 1, max: 9999 });
|
|
7
|
+
|
|
8
|
+
const VALID_TRANSITIONS: Record<TaskStatus, Set<TaskStatus>> = {
|
|
9
|
+
[TaskStatus.PENDING]: new Set([TaskStatus.RUNNING, TaskStatus.WAITING, TaskStatus.WAITING_ANSWER, TaskStatus.COMPLETED, TaskStatus.CANCELLED, TaskStatus.FAILED, TaskStatus.TIMED_OUT]),
|
|
10
|
+
[TaskStatus.WAITING]: new Set([TaskStatus.PENDING, TaskStatus.WAITING_ANSWER, TaskStatus.COMPLETED, TaskStatus.CANCELLED, TaskStatus.FAILED, TaskStatus.TIMED_OUT]),
|
|
11
|
+
[TaskStatus.RUNNING]: new Set([TaskStatus.COMPLETED, TaskStatus.FAILED, TaskStatus.CANCELLED, TaskStatus.RATE_LIMITED, TaskStatus.WAITING_ANSWER, TaskStatus.TIMED_OUT]),
|
|
12
|
+
[TaskStatus.WAITING_ANSWER]: new Set([TaskStatus.RUNNING, TaskStatus.FAILED, TaskStatus.CANCELLED, TaskStatus.TIMED_OUT]),
|
|
13
|
+
[TaskStatus.COMPLETED]: new Set(),
|
|
14
|
+
[TaskStatus.FAILED]: new Set(),
|
|
15
|
+
[TaskStatus.CANCELLED]: new Set(),
|
|
16
|
+
[TaskStatus.RATE_LIMITED]: new Set([TaskStatus.RUNNING, TaskStatus.FAILED, TaskStatus.CANCELLED, TaskStatus.TIMED_OUT]),
|
|
17
|
+
[TaskStatus.TIMED_OUT]: new Set(),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function generateTaskId(existingIds: Set<string>): string {
|
|
21
|
+
let next = uniqueNamesGenerator({
|
|
22
|
+
dictionaries: [adjectives, animals, numberDictionary],
|
|
23
|
+
separator: '-',
|
|
24
|
+
style: 'lowerCase',
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
while (existingIds.has(next)) {
|
|
28
|
+
next = uniqueNamesGenerator({
|
|
29
|
+
dictionaries: [adjectives, animals, numberDictionary],
|
|
30
|
+
separator: '-',
|
|
31
|
+
style: 'lowerCase',
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return next;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function dependenciesSatisfied(task: TaskRecord, tasks: Map<string, TaskRecord>): boolean {
|
|
39
|
+
return (task.dependsOn ?? []).every((dependencyId) => tasks.get(dependencyId)?.status === TaskStatus.COMPLETED);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function circularDependencyPath(
|
|
43
|
+
tasks: Map<string, TaskRecord>,
|
|
44
|
+
startId: string,
|
|
45
|
+
newDependsOn?: string[],
|
|
46
|
+
): string[] | null {
|
|
47
|
+
const visiting = new Set<string>();
|
|
48
|
+
const visited = new Set<string>();
|
|
49
|
+
const trail: string[] = [];
|
|
50
|
+
|
|
51
|
+
const visit = (taskId: string): string[] | null => {
|
|
52
|
+
if (visiting.has(taskId)) {
|
|
53
|
+
const index = trail.indexOf(taskId);
|
|
54
|
+
return [...trail.slice(index), taskId];
|
|
55
|
+
}
|
|
56
|
+
if (visited.has(taskId)) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
visiting.add(taskId);
|
|
61
|
+
trail.push(taskId);
|
|
62
|
+
|
|
63
|
+
const dependsOn = taskId === startId && newDependsOn !== undefined
|
|
64
|
+
? newDependsOn
|
|
65
|
+
: tasks.get(taskId)?.dependsOn ?? [];
|
|
66
|
+
|
|
67
|
+
for (const dependencyId of dependsOn) {
|
|
68
|
+
const cycle = visit(dependencyId);
|
|
69
|
+
if (cycle) {
|
|
70
|
+
return cycle;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
trail.pop();
|
|
75
|
+
visiting.delete(taskId);
|
|
76
|
+
visited.add(taskId);
|
|
77
|
+
return null;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return visit(startId);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export class TaskStore {
|
|
84
|
+
private readonly tasks = new Map<string, TaskRecord>();
|
|
85
|
+
|
|
86
|
+
createTask(input: CreateTaskInput): TaskRecord {
|
|
87
|
+
const id = generateTaskId(new Set(this.tasks.keys()));
|
|
88
|
+
const dependencyError = this.validateDependencies(input.dependsOn ?? [], id);
|
|
89
|
+
if (dependencyError) {
|
|
90
|
+
throw new Error(dependencyError);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const now = new Date().toISOString();
|
|
94
|
+
const task: TaskRecord = {
|
|
95
|
+
id,
|
|
96
|
+
status: (input.dependsOn?.length ?? 0) > 0 ? TaskStatus.WAITING : TaskStatus.PENDING,
|
|
97
|
+
prompt: input.prompt,
|
|
98
|
+
cwd: input.cwd,
|
|
99
|
+
model: input.model,
|
|
100
|
+
agentType: input.agentType,
|
|
101
|
+
output: [],
|
|
102
|
+
startTime: now,
|
|
103
|
+
createdAt: now,
|
|
104
|
+
updatedAt: now,
|
|
105
|
+
labels: input.labels ?? [],
|
|
106
|
+
contextFiles: input.contextFiles ?? [],
|
|
107
|
+
dependsOn: input.dependsOn?.length ? [...input.dependsOn] : undefined,
|
|
108
|
+
timeoutMs: input.timeoutMs,
|
|
109
|
+
sessionId: input.sessionId,
|
|
110
|
+
parentTaskId: input.parentTaskId,
|
|
111
|
+
profileId: input.profileId,
|
|
112
|
+
profileConfigDir: input.profileConfigDir,
|
|
113
|
+
isolationMode: input.isolationMode,
|
|
114
|
+
baseCwd: input.baseCwd,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
if (task.status === TaskStatus.WAITING && dependenciesSatisfied(task, this.tasks)) {
|
|
118
|
+
task.status = TaskStatus.PENDING;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
this.tasks.set(task.id, task);
|
|
122
|
+
return task;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
getTask(taskId: string): TaskRecord | undefined {
|
|
126
|
+
return this.tasks.get(taskId);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
getAllTasks(): TaskRecord[] {
|
|
130
|
+
return [...this.tasks.values()];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
importTask(task: TaskRecord): void {
|
|
134
|
+
this.tasks.set(task.id, task);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
validateDependencies(dependsOn: string[], newTaskId?: string): string | null {
|
|
138
|
+
if (dependsOn.length === 0) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (new Set(dependsOn).size !== dependsOn.length) {
|
|
143
|
+
return 'Duplicate dependency IDs are not allowed';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (newTaskId && dependsOn.includes(newTaskId)) {
|
|
147
|
+
return `Task '${newTaskId}' cannot depend on itself`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
for (const dependencyId of dependsOn) {
|
|
151
|
+
if (!this.tasks.has(dependencyId)) {
|
|
152
|
+
return `Dependency task '${dependencyId}' not found`;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (newTaskId) {
|
|
157
|
+
const cycle = circularDependencyPath(this.tasks, newTaskId, dependsOn);
|
|
158
|
+
if (cycle) {
|
|
159
|
+
return `Circular dependency detected: ${cycle.join(' -> ')}`;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
updateTask(taskId: string, updates: Partial<TaskRecord>): TaskRecord | undefined {
|
|
167
|
+
const task = this.tasks.get(taskId);
|
|
168
|
+
if (!task) {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (updates.status && updates.status !== task.status) {
|
|
173
|
+
if (isTerminalStatus(task.status)) {
|
|
174
|
+
return task;
|
|
175
|
+
}
|
|
176
|
+
const allowed = VALID_TRANSITIONS[task.status];
|
|
177
|
+
if (!allowed.has(updates.status)) {
|
|
178
|
+
return task;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
Object.assign(task, updates, { updatedAt: new Date().toISOString() });
|
|
183
|
+
return task;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
releaseReadyTasks(): TaskRecord[] {
|
|
187
|
+
const released: TaskRecord[] = [];
|
|
188
|
+
|
|
189
|
+
for (const task of this.tasks.values()) {
|
|
190
|
+
if (task.status !== TaskStatus.WAITING) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (dependenciesSatisfied(task, this.tasks)) {
|
|
195
|
+
task.status = TaskStatus.PENDING;
|
|
196
|
+
task.updatedAt = new Date().toISOString();
|
|
197
|
+
released.push(task);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return released;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
clear(): void {
|
|
205
|
+
this.tasks.clear();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { cp, mkdir, readdir, rm } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
|
|
8
|
+
export type IsolationMode = 'shared' | 'isolated';
|
|
9
|
+
|
|
10
|
+
export interface PreparedWorkspace {
|
|
11
|
+
cwd: string;
|
|
12
|
+
cleanupRequired: boolean;
|
|
13
|
+
mode: 'shared' | 'copy' | 'worktree';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface PrepareTaskWorkspaceInput {
|
|
17
|
+
baseCwd: string;
|
|
18
|
+
taskId: string;
|
|
19
|
+
isolationMode: IsolationMode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const SKIP_NAMES = new Set(['.git', 'node_modules', '.super-agents', '.worktrees', 'worktrees', 'dist']);
|
|
23
|
+
|
|
24
|
+
export async function prepareTaskWorkspace(input: PrepareTaskWorkspaceInput): Promise<PreparedWorkspace> {
|
|
25
|
+
if (input.isolationMode === 'shared') {
|
|
26
|
+
return {
|
|
27
|
+
cwd: input.baseCwd,
|
|
28
|
+
cleanupRequired: false,
|
|
29
|
+
mode: 'shared',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const worktree = await tryCreateGitWorktree(input);
|
|
34
|
+
if (worktree) {
|
|
35
|
+
return worktree;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const copyRoot = join(input.baseCwd, '.super-agents', 'workspaces', input.taskId);
|
|
39
|
+
await mkdir(dirname(copyRoot), { recursive: true });
|
|
40
|
+
await rm(copyRoot, { recursive: true, force: true });
|
|
41
|
+
await mkdir(copyRoot, { recursive: true });
|
|
42
|
+
|
|
43
|
+
const topLevelEntries = await readdir(input.baseCwd);
|
|
44
|
+
for (const entry of topLevelEntries) {
|
|
45
|
+
if (SKIP_NAMES.has(entry)) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
await cp(join(input.baseCwd, entry), join(copyRoot, entry), {
|
|
50
|
+
recursive: true,
|
|
51
|
+
filter(source) {
|
|
52
|
+
return !SKIP_NAMES.has(source.split('/').pop() ?? '');
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
cwd: copyRoot,
|
|
59
|
+
cleanupRequired: true,
|
|
60
|
+
mode: 'copy',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function cleanupPreparedWorkspace(workspace: PreparedWorkspace): Promise<void> {
|
|
65
|
+
if (!workspace.cleanupRequired || workspace.mode === 'shared') {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
await rm(workspace.cwd, { recursive: true, force: true });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function tryCreateGitWorktree(input: PrepareTaskWorkspaceInput): Promise<PreparedWorkspace | null> {
|
|
73
|
+
try {
|
|
74
|
+
const { stdout: rootStdout } = await execFileAsync('git', ['rev-parse', '--show-toplevel'], {
|
|
75
|
+
cwd: input.baseCwd,
|
|
76
|
+
});
|
|
77
|
+
const gitRoot = rootStdout.trim();
|
|
78
|
+
if (!gitRoot) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
await execFileAsync('git', ['rev-parse', '--verify', 'HEAD'], {
|
|
83
|
+
cwd: gitRoot,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const worktreePath = join(gitRoot, '.worktrees', input.taskId);
|
|
87
|
+
await mkdir(dirname(worktreePath), { recursive: true });
|
|
88
|
+
await execFileAsync('git', ['worktree', 'add', '--detach', worktreePath, 'HEAD'], {
|
|
89
|
+
cwd: gitRoot,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
cwd: worktreePath,
|
|
94
|
+
cleanupRequired: true,
|
|
95
|
+
mode: 'worktree',
|
|
96
|
+
};
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
import type { AgentType, ContextFile } from '../types/task.js';
|
|
4
|
+
|
|
5
|
+
const ROLE_TEMPLATES: Record<AgentType, string> = {
|
|
6
|
+
coder: [
|
|
7
|
+
'You are the implementation agent.',
|
|
8
|
+
'Work carefully, change as little as needed, and verify the requested behavior.',
|
|
9
|
+
'Prefer deterministic edits over speculative refactors.',
|
|
10
|
+
].join(' '),
|
|
11
|
+
planner: [
|
|
12
|
+
'You are the planning agent.',
|
|
13
|
+
'Clarify scope, constraints, risks, and the concrete implementation path.',
|
|
14
|
+
'Produce actionable implementation guidance instead of vague advice.',
|
|
15
|
+
].join(' '),
|
|
16
|
+
researcher: [
|
|
17
|
+
'You are the research agent.',
|
|
18
|
+
'Investigate the code and runtime behavior before proposing conclusions.',
|
|
19
|
+
'Surface the facts that a downstream implementer needs.',
|
|
20
|
+
].join(' '),
|
|
21
|
+
tester: [
|
|
22
|
+
'You are the testing agent.',
|
|
23
|
+
'Exercise the real behavior, collect evidence, and describe failures concretely.',
|
|
24
|
+
'Prefer end-to-end verification when possible.',
|
|
25
|
+
].join(' '),
|
|
26
|
+
general: [
|
|
27
|
+
'You are a general-purpose autonomous agent.',
|
|
28
|
+
'Stay focused on the requested deliverable and keep your output concrete.',
|
|
29
|
+
].join(' '),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
async function renderContextFiles(contextFiles: ContextFile[]): Promise<string> {
|
|
33
|
+
if (contextFiles.length === 0) {
|
|
34
|
+
return '';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const sections = await Promise.all(
|
|
38
|
+
contextFiles.map(async (contextFile) => {
|
|
39
|
+
const content = await readFile(contextFile.path, 'utf8');
|
|
40
|
+
return [
|
|
41
|
+
`## Context File: ${contextFile.path}`,
|
|
42
|
+
contextFile.description ? `Description: ${contextFile.description}` : null,
|
|
43
|
+
'',
|
|
44
|
+
'```text',
|
|
45
|
+
content,
|
|
46
|
+
'```',
|
|
47
|
+
].filter(Boolean).join('\n');
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return sections.join('\n\n');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function buildAgentPrompt(input: {
|
|
55
|
+
agentType: AgentType;
|
|
56
|
+
prompt: string;
|
|
57
|
+
contextFiles: ContextFile[];
|
|
58
|
+
}): Promise<string> {
|
|
59
|
+
const context = await renderContextFiles(input.contextFiles);
|
|
60
|
+
|
|
61
|
+
return [
|
|
62
|
+
ROLE_TEMPLATES[input.agentType],
|
|
63
|
+
'',
|
|
64
|
+
'Treat the task as stateful background work. If you need user input, ask a concise question.',
|
|
65
|
+
'',
|
|
66
|
+
'## Assignment',
|
|
67
|
+
input.prompt,
|
|
68
|
+
context ? '' : null,
|
|
69
|
+
context || null,
|
|
70
|
+
].filter(Boolean).join('\n');
|
|
71
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export type AgentType = 'coder' | 'planner' | 'researcher' | 'tester' | 'general';
|
|
2
|
+
|
|
3
|
+
export interface ContextFile {
|
|
4
|
+
path: string;
|
|
5
|
+
description?: string | undefined;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface PendingQuestion {
|
|
9
|
+
question: string;
|
|
10
|
+
choices?: string[] | undefined;
|
|
11
|
+
allowFreeform: boolean;
|
|
12
|
+
askedAt: string;
|
|
13
|
+
sessionId: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export enum TaskStatus {
|
|
17
|
+
PENDING = 'pending',
|
|
18
|
+
WAITING = 'waiting',
|
|
19
|
+
RUNNING = 'running',
|
|
20
|
+
WAITING_ANSWER = 'waiting_answer',
|
|
21
|
+
COMPLETED = 'completed',
|
|
22
|
+
FAILED = 'failed',
|
|
23
|
+
CANCELLED = 'cancelled',
|
|
24
|
+
RATE_LIMITED = 'rate_limited',
|
|
25
|
+
TIMED_OUT = 'timed_out',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const TERMINAL_STATUSES = new Set<TaskStatus>([
|
|
29
|
+
TaskStatus.COMPLETED,
|
|
30
|
+
TaskStatus.FAILED,
|
|
31
|
+
TaskStatus.CANCELLED,
|
|
32
|
+
TaskStatus.TIMED_OUT,
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
export function isTerminalStatus(status: TaskStatus): boolean {
|
|
36
|
+
return TERMINAL_STATUSES.has(status);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface TaskRecord {
|
|
40
|
+
id: string;
|
|
41
|
+
status: TaskStatus;
|
|
42
|
+
prompt: string;
|
|
43
|
+
cwd: string;
|
|
44
|
+
model: string;
|
|
45
|
+
agentType: AgentType;
|
|
46
|
+
output: string[];
|
|
47
|
+
startTime: string;
|
|
48
|
+
createdAt: string;
|
|
49
|
+
updatedAt: string;
|
|
50
|
+
labels: string[];
|
|
51
|
+
contextFiles: ContextFile[];
|
|
52
|
+
dependsOn?: string[] | undefined;
|
|
53
|
+
error?: string | undefined;
|
|
54
|
+
endTime?: string | undefined;
|
|
55
|
+
outputFilePath?: string | undefined;
|
|
56
|
+
timeoutMs?: number | undefined;
|
|
57
|
+
sessionId?: string | undefined;
|
|
58
|
+
parentTaskId?: string | undefined;
|
|
59
|
+
pendingQuestion?: PendingQuestion | undefined;
|
|
60
|
+
profileId?: string | undefined;
|
|
61
|
+
profileConfigDir?: string | undefined;
|
|
62
|
+
isolationMode?: 'shared' | 'isolated' | undefined;
|
|
63
|
+
baseCwd?: string | undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface CreateTaskInput {
|
|
67
|
+
prompt: string;
|
|
68
|
+
cwd: string;
|
|
69
|
+
model: string;
|
|
70
|
+
agentType: AgentType;
|
|
71
|
+
labels?: string[] | undefined;
|
|
72
|
+
contextFiles?: ContextFile[] | undefined;
|
|
73
|
+
dependsOn?: string[] | undefined;
|
|
74
|
+
timeoutMs?: number | undefined;
|
|
75
|
+
sessionId?: string | undefined;
|
|
76
|
+
parentTaskId?: string | undefined;
|
|
77
|
+
profileId?: string | undefined;
|
|
78
|
+
profileConfigDir?: string | undefined;
|
|
79
|
+
isolationMode?: 'shared' | 'isolated' | undefined;
|
|
80
|
+
baseCwd?: string | undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface PersistedWorkspaceState {
|
|
84
|
+
version: 1;
|
|
85
|
+
tasks: TaskRecord[];
|
|
86
|
+
profiles: PersistedProfileState[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface PersistedProfileState {
|
|
90
|
+
id: string;
|
|
91
|
+
configDir: string;
|
|
92
|
+
cooldownUntil?: number | undefined;
|
|
93
|
+
failureCount: number;
|
|
94
|
+
lastFailureReason?: string | undefined;
|
|
95
|
+
}
|