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,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sub-agent — spawn child agents with isolated message context.
|
|
3
|
+
*
|
|
4
|
+
* Pattern from Claude Code's AgentTool:
|
|
5
|
+
* - Fresh messages[] per child agent
|
|
6
|
+
* - Shared tool registry and provider router
|
|
7
|
+
* - Results returned to parent
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ProviderRouter } from "../providers/router.ts";
|
|
11
|
+
import type { ToolRegistry } from "../tools/registry.ts";
|
|
12
|
+
import type { ToolContext } from "../tools/types.ts";
|
|
13
|
+
import type { Message } from "../providers/types.ts";
|
|
14
|
+
import { runAgentLoop } from "./loop.ts";
|
|
15
|
+
import { createWorktree, removeWorktree } from "./worktree-manager.ts";
|
|
16
|
+
import { runWithAgentContext, createChildContext, getAgentContext } from "./async-context.ts";
|
|
17
|
+
|
|
18
|
+
export interface SubAgentConfig {
|
|
19
|
+
name: string;
|
|
20
|
+
prompt: string;
|
|
21
|
+
systemPrompt: string;
|
|
22
|
+
router: ProviderRouter;
|
|
23
|
+
toolRegistry: ToolRegistry;
|
|
24
|
+
toolContext: ToolContext;
|
|
25
|
+
/** Only allow read-only tools */
|
|
26
|
+
readOnly?: boolean;
|
|
27
|
+
/** Execution mode: in-process (default) or worktree-isolated */
|
|
28
|
+
mode?: "in_process" | "worktree";
|
|
29
|
+
/** Max iterations for this sub-agent */
|
|
30
|
+
maxIterations?: number;
|
|
31
|
+
/** Callback for streaming text */
|
|
32
|
+
onText?: (text: string) => void;
|
|
33
|
+
/** Callback for tool events */
|
|
34
|
+
onToolStart?: (name: string, input: Record<string, unknown>) => void;
|
|
35
|
+
onToolEnd?: (name: string, result: string, isError: boolean) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface SubAgentResult {
|
|
39
|
+
name: string;
|
|
40
|
+
text: string;
|
|
41
|
+
toolCalls: Array<{ name: string; input: Record<string, unknown>; result: string }>;
|
|
42
|
+
messages: Message[];
|
|
43
|
+
worktree?: { path: string; branch: string };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Run a sub-agent in an isolated git worktree.
|
|
48
|
+
* The worktree is created before the agent runs and preserved after
|
|
49
|
+
* so the parent can inspect or merge the branch. On error, the
|
|
50
|
+
* worktree is cleaned up automatically.
|
|
51
|
+
*/
|
|
52
|
+
async function runWorktreeSubAgent(config: SubAgentConfig): Promise<SubAgentResult> {
|
|
53
|
+
const wt = await createWorktree(config.name);
|
|
54
|
+
const worktreeContext: ToolContext = { ...config.toolContext, cwd: wt.path };
|
|
55
|
+
|
|
56
|
+
const parentCtx = getAgentContext();
|
|
57
|
+
const childCtx = createChildContext(parentCtx, config.name, wt.path, config.readOnly ?? true);
|
|
58
|
+
|
|
59
|
+
return runWithAgentContext(childCtx, async () => {
|
|
60
|
+
try {
|
|
61
|
+
const result = await runAgentLoop(config.prompt, [], {
|
|
62
|
+
systemPrompt: config.systemPrompt,
|
|
63
|
+
router: config.router,
|
|
64
|
+
toolRegistry: config.toolRegistry,
|
|
65
|
+
toolContext: worktreeContext,
|
|
66
|
+
readOnly: config.readOnly,
|
|
67
|
+
maxIterations: config.maxIterations ?? 15,
|
|
68
|
+
onText: config.onText,
|
|
69
|
+
onToolStart: config.onToolStart,
|
|
70
|
+
onToolEnd: config.onToolEnd,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
name: config.name,
|
|
75
|
+
text: result.finalText,
|
|
76
|
+
toolCalls: result.toolCalls,
|
|
77
|
+
messages: result.messages,
|
|
78
|
+
worktree: { path: wt.path, branch: wt.branch },
|
|
79
|
+
};
|
|
80
|
+
} catch (err) {
|
|
81
|
+
await removeWorktree(wt.path).catch(() => {});
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Run a sub-agent with its own fresh message context.
|
|
89
|
+
*/
|
|
90
|
+
export async function runSubAgent(config: SubAgentConfig): Promise<SubAgentResult> {
|
|
91
|
+
if (config.mode === "worktree") return runWorktreeSubAgent(config);
|
|
92
|
+
|
|
93
|
+
const parentCtx = getAgentContext();
|
|
94
|
+
const childCtx = createChildContext(parentCtx, config.name, config.toolContext.cwd, config.readOnly ?? true);
|
|
95
|
+
|
|
96
|
+
return runWithAgentContext(childCtx, async () => {
|
|
97
|
+
const result = await runAgentLoop(config.prompt, [], {
|
|
98
|
+
systemPrompt: config.systemPrompt,
|
|
99
|
+
router: config.router,
|
|
100
|
+
toolRegistry: config.toolRegistry,
|
|
101
|
+
toolContext: config.toolContext,
|
|
102
|
+
readOnly: config.readOnly,
|
|
103
|
+
maxIterations: config.maxIterations ?? 15,
|
|
104
|
+
onText: config.onText,
|
|
105
|
+
onToolStart: config.onToolStart,
|
|
106
|
+
onToolEnd: config.onToolEnd,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
name: config.name,
|
|
111
|
+
text: result.finalText,
|
|
112
|
+
toolCalls: result.toolCalls,
|
|
113
|
+
messages: result.messages,
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Run multiple sub-agents in parallel.
|
|
120
|
+
*/
|
|
121
|
+
export async function runSubAgentsParallel(
|
|
122
|
+
configs: SubAgentConfig[]
|
|
123
|
+
): Promise<SubAgentResult[]> {
|
|
124
|
+
return Promise.all(configs.map(runSubAgent));
|
|
125
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dynamic System Prompt Assembly — builds prompts from parts.
|
|
3
|
+
*
|
|
4
|
+
* Pattern from Claude Code: prompt is assembled at query time from
|
|
5
|
+
* core instructions + tool schemas + permissions + project config + knowledge files.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync } from "fs";
|
|
9
|
+
import { readFile, readdir } from "fs/promises";
|
|
10
|
+
import { join } from "path";
|
|
11
|
+
import { getConfigDir } from "../config/settings.ts";
|
|
12
|
+
import { getModelPatches } from "./model-patches.ts";
|
|
13
|
+
import { getUndercoverPrompt } from "../config/undercover.ts";
|
|
14
|
+
import type { ToolRegistry } from "../tools/registry.ts";
|
|
15
|
+
|
|
16
|
+
export interface PromptPart {
|
|
17
|
+
name: string;
|
|
18
|
+
content: string;
|
|
19
|
+
priority: number; // Lower = earlier in prompt
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface AssembledPrompt {
|
|
23
|
+
text: string;
|
|
24
|
+
parts: string[]; // Names of included parts
|
|
25
|
+
estimatedTokens: number; // Rough token count
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Estimate tokens from text (rough: chars / 4).
|
|
30
|
+
*/
|
|
31
|
+
function estimateTokens(text: string): number {
|
|
32
|
+
return Math.ceil(text.length / 4);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class SystemPromptBuilder {
|
|
36
|
+
private parts: PromptPart[] = [];
|
|
37
|
+
|
|
38
|
+
/** Add a named section to the prompt */
|
|
39
|
+
addPart(name: string, content: string, priority: number = 50): this {
|
|
40
|
+
this.parts.push({ name, content, priority });
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Add core instructions */
|
|
45
|
+
addCoreInstructions(instructions: string): this {
|
|
46
|
+
return this.addPart("core", instructions, 0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Add tool descriptions from registry */
|
|
50
|
+
addToolDescriptions(registry: ToolRegistry, readOnly: boolean = false): this {
|
|
51
|
+
const tools = readOnly
|
|
52
|
+
? registry.getAll().filter((t) => t.isReadOnly())
|
|
53
|
+
: registry.getAll();
|
|
54
|
+
const descriptions = tools
|
|
55
|
+
.map((t) => `### ${t.name}\n${t.prompt()}`)
|
|
56
|
+
.join("\n\n");
|
|
57
|
+
return this.addPart("tools", `## Available Tools\n\n${descriptions}`, 10);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Add permission context */
|
|
61
|
+
addPermissionContext(mode: string, rules?: string): this {
|
|
62
|
+
let content = `## Permissions\nCurrent mode: ${mode}`;
|
|
63
|
+
if (rules) content += `\nRules:\n${rules}`;
|
|
64
|
+
return this.addPart("permissions", content, 20);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Load and add knowledge files (CLAUDE.md equivalent) from project dir */
|
|
68
|
+
async addKnowledgeFiles(projectDir: string): Promise<this> {
|
|
69
|
+
const knowledgeDir = join(projectDir, ".ashlrcode");
|
|
70
|
+
if (!existsSync(knowledgeDir)) return this;
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const files = await readdir(knowledgeDir);
|
|
74
|
+
for (const file of files) {
|
|
75
|
+
if (!file.endsWith(".md")) continue;
|
|
76
|
+
const content = await readFile(join(knowledgeDir, file), "utf-8");
|
|
77
|
+
this.addPart(
|
|
78
|
+
`knowledge:${file}`,
|
|
79
|
+
`## Project Knowledge: ${file}\n\n${content}`,
|
|
80
|
+
30
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
// Knowledge dir unreadable — skip silently
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Load and add memory files */
|
|
91
|
+
async addMemoryFiles(): Promise<this> {
|
|
92
|
+
const memoryDir = join(getConfigDir(), "memory");
|
|
93
|
+
if (!existsSync(memoryDir)) return this;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const files = await readdir(memoryDir);
|
|
97
|
+
const memories: string[] = [];
|
|
98
|
+
for (const file of files) {
|
|
99
|
+
if (!file.endsWith(".md") || file === "index.md") continue;
|
|
100
|
+
const content = await readFile(join(memoryDir, file), "utf-8");
|
|
101
|
+
memories.push(content.trim());
|
|
102
|
+
}
|
|
103
|
+
if (memories.length > 0) {
|
|
104
|
+
this.addPart(
|
|
105
|
+
"memory",
|
|
106
|
+
`## Memory\n\n${memories.join("\n\n---\n\n")}`,
|
|
107
|
+
40
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
// Memory dir unreadable — skip silently
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return this;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Add plan mode instructions */
|
|
118
|
+
addPlanMode(planFile?: string): this {
|
|
119
|
+
let content =
|
|
120
|
+
"## Plan Mode Active\nYou are in read-only plan mode. Only use read-only tools.";
|
|
121
|
+
if (planFile) content += `\nPlan file: ${planFile}`;
|
|
122
|
+
return this.addPart("plan-mode", content, 5);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Add custom section */
|
|
126
|
+
addSection(name: string, content: string, priority?: number): this {
|
|
127
|
+
return this.addPart(name, content, priority ?? 50);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Add git context (branch, recent commits, working tree status) */
|
|
131
|
+
async addGitContext(cwd: string): Promise<this> {
|
|
132
|
+
const { isGitRepo, getCurrentBranch, getRecentCommits, getGitStatus } = await import("../config/git.ts");
|
|
133
|
+
if (!await isGitRepo(cwd)) return this;
|
|
134
|
+
|
|
135
|
+
const branch = await getCurrentBranch(cwd);
|
|
136
|
+
const commits = await getRecentCommits(cwd, 3);
|
|
137
|
+
const status = await getGitStatus(cwd);
|
|
138
|
+
|
|
139
|
+
let context = `## Git Context\nBranch: ${branch ?? "unknown"}`;
|
|
140
|
+
if (commits.length > 0) context += `\nRecent commits:\n${commits.map(c => ` ${c}`).join("\n")}`;
|
|
141
|
+
if (status.modified + status.untracked > 0) {
|
|
142
|
+
context += `\nWorking tree: ${status.modified} modified, ${status.untracked} untracked`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return this.addPart("git", context, 35);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Add model-specific behavior patches */
|
|
149
|
+
addModelPatches(modelName: string): this {
|
|
150
|
+
const { combinedSuffix } = getModelPatches(modelName);
|
|
151
|
+
if (combinedSuffix) this.addPart("model-patches", combinedSuffix, 90);
|
|
152
|
+
return this;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Add undercover mode prompt if active */
|
|
156
|
+
addUndercoverPrompt(): this {
|
|
157
|
+
const prompt = getUndercoverPrompt();
|
|
158
|
+
if (prompt) this.addPart("undercover", prompt, 95);
|
|
159
|
+
return this;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Build the final prompt, respecting a token budget */
|
|
163
|
+
build(maxTokens?: number): AssembledPrompt {
|
|
164
|
+
// Sort by priority (lower = earlier / more important)
|
|
165
|
+
const sorted = [...this.parts].sort((a, b) => a.priority - b.priority);
|
|
166
|
+
|
|
167
|
+
const included: PromptPart[] = [];
|
|
168
|
+
let totalTokens = 0;
|
|
169
|
+
|
|
170
|
+
for (const part of sorted) {
|
|
171
|
+
const partTokens = estimateTokens(part.content);
|
|
172
|
+
if (maxTokens && totalTokens + partTokens > maxTokens) {
|
|
173
|
+
continue; // Skip parts that would exceed budget
|
|
174
|
+
}
|
|
175
|
+
included.push(part);
|
|
176
|
+
totalTokens += partTokens;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const text = included.map((p) => p.content).join("\n\n");
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
text,
|
|
183
|
+
parts: included.map((p) => p.name),
|
|
184
|
+
estimatedTokens: totalTokens,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Reset for fresh assembly */
|
|
189
|
+
reset(): this {
|
|
190
|
+
this.parts = [];
|
|
191
|
+
return this;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Convenience: build a standard system prompt with all common parts.
|
|
197
|
+
*/
|
|
198
|
+
export async function buildSystemPrompt(options: {
|
|
199
|
+
coreInstructions: string;
|
|
200
|
+
toolRegistry: ToolRegistry;
|
|
201
|
+
readOnly?: boolean;
|
|
202
|
+
mode?: string;
|
|
203
|
+
projectDir?: string;
|
|
204
|
+
planFile?: string;
|
|
205
|
+
modelName?: string;
|
|
206
|
+
maxTokens?: number;
|
|
207
|
+
}): Promise<AssembledPrompt> {
|
|
208
|
+
const builder = new SystemPromptBuilder();
|
|
209
|
+
|
|
210
|
+
builder.addCoreInstructions(options.coreInstructions);
|
|
211
|
+
builder.addToolDescriptions(options.toolRegistry, options.readOnly);
|
|
212
|
+
builder.addPermissionContext(options.mode ?? "normal");
|
|
213
|
+
|
|
214
|
+
if (options.readOnly && options.planFile) {
|
|
215
|
+
builder.addPlanMode(options.planFile);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (options.projectDir) {
|
|
219
|
+
await builder.addKnowledgeFiles(options.projectDir);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
await builder.addMemoryFiles();
|
|
223
|
+
|
|
224
|
+
if (options.modelName) {
|
|
225
|
+
builder.addModelPatches(options.modelName);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
builder.addUndercoverPrompt();
|
|
229
|
+
|
|
230
|
+
return builder.build(options.maxTokens);
|
|
231
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Team Persistence — manage persistent agent teammates.
|
|
3
|
+
*
|
|
4
|
+
* Pattern from Claude Code's coordinator mode: a lead agent can create
|
|
5
|
+
* teammates that persist across sessions, claim tasks from a shared
|
|
6
|
+
* board, and report results back.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync } from "fs";
|
|
10
|
+
import { readFile, writeFile, readdir, mkdir, unlink } from "fs/promises";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
import { getConfigDir } from "../config/settings.ts";
|
|
13
|
+
|
|
14
|
+
export interface Teammate {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
role: string; // e.g., "code-reviewer", "test-writer", "explorer"
|
|
18
|
+
systemPrompt: string; // Specialized instructions for this teammate
|
|
19
|
+
createdAt: string;
|
|
20
|
+
lastActiveAt?: string;
|
|
21
|
+
stats: {
|
|
22
|
+
tasksCompleted: number;
|
|
23
|
+
totalIterations: number;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface Team {
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
teammates: Teammate[];
|
|
31
|
+
createdAt: string;
|
|
32
|
+
updatedAt: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getTeamsDir(): string {
|
|
36
|
+
return join(getConfigDir(), "teams");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getTeamPath(teamId: string): string {
|
|
40
|
+
return join(getTeamsDir(), `${teamId}.json`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create a new team.
|
|
45
|
+
*/
|
|
46
|
+
export async function createTeam(name: string): Promise<Team> {
|
|
47
|
+
await mkdir(getTeamsDir(), { recursive: true });
|
|
48
|
+
|
|
49
|
+
const team: Team = {
|
|
50
|
+
id: `team-${Date.now()}`,
|
|
51
|
+
name,
|
|
52
|
+
teammates: [],
|
|
53
|
+
createdAt: new Date().toISOString(),
|
|
54
|
+
updatedAt: new Date().toISOString(),
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
await saveTeam(team);
|
|
58
|
+
return team;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Add a teammate to a team.
|
|
63
|
+
*/
|
|
64
|
+
export async function addTeammate(
|
|
65
|
+
teamId: string,
|
|
66
|
+
name: string,
|
|
67
|
+
role: string,
|
|
68
|
+
systemPrompt?: string,
|
|
69
|
+
): Promise<Teammate> {
|
|
70
|
+
const team = await loadTeam(teamId);
|
|
71
|
+
if (!team) throw new Error(`Team ${teamId} not found`);
|
|
72
|
+
|
|
73
|
+
const rolePrompts: Record<string, string> = {
|
|
74
|
+
"code-reviewer":
|
|
75
|
+
"You are a code reviewer. Focus on bugs, logic errors, security vulnerabilities, and code quality. Be thorough but only flag high-confidence issues.",
|
|
76
|
+
"test-writer":
|
|
77
|
+
"You are a test writer. Write comprehensive tests for the code you're given. Cover edge cases, error scenarios, and happy paths.",
|
|
78
|
+
explorer:
|
|
79
|
+
"You are a codebase explorer. Map patterns, find dependencies, and report findings with specific file paths and line numbers.",
|
|
80
|
+
implementer:
|
|
81
|
+
"You are an implementer. Write clean, well-structured code that follows the existing patterns and conventions in the codebase.",
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const teammate: Teammate = {
|
|
85
|
+
id: `mate-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
86
|
+
name,
|
|
87
|
+
role,
|
|
88
|
+
systemPrompt: systemPrompt ?? rolePrompts[role] ?? `You are a ${role}.`,
|
|
89
|
+
createdAt: new Date().toISOString(),
|
|
90
|
+
stats: { tasksCompleted: 0, totalIterations: 0 },
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
team.teammates.push(teammate);
|
|
94
|
+
team.updatedAt = new Date().toISOString();
|
|
95
|
+
await saveTeam(team);
|
|
96
|
+
|
|
97
|
+
return teammate;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Remove a teammate from a team.
|
|
102
|
+
*/
|
|
103
|
+
export async function removeTeammate(
|
|
104
|
+
teamId: string,
|
|
105
|
+
teammateId: string,
|
|
106
|
+
): Promise<boolean> {
|
|
107
|
+
const team = await loadTeam(teamId);
|
|
108
|
+
if (!team) return false;
|
|
109
|
+
|
|
110
|
+
const idx = team.teammates.findIndex((t) => t.id === teammateId);
|
|
111
|
+
if (idx === -1) return false;
|
|
112
|
+
|
|
113
|
+
team.teammates.splice(idx, 1);
|
|
114
|
+
team.updatedAt = new Date().toISOString();
|
|
115
|
+
await saveTeam(team);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Record teammate activity.
|
|
121
|
+
*/
|
|
122
|
+
export async function recordTeammateActivity(
|
|
123
|
+
teamId: string,
|
|
124
|
+
teammateId: string,
|
|
125
|
+
tasksCompleted: number = 1,
|
|
126
|
+
iterations: number = 0,
|
|
127
|
+
): Promise<void> {
|
|
128
|
+
const team = await loadTeam(teamId);
|
|
129
|
+
if (!team) return;
|
|
130
|
+
|
|
131
|
+
const mate = team.teammates.find((t) => t.id === teammateId);
|
|
132
|
+
if (!mate) return;
|
|
133
|
+
|
|
134
|
+
mate.stats.tasksCompleted += tasksCompleted;
|
|
135
|
+
mate.stats.totalIterations += iterations;
|
|
136
|
+
mate.lastActiveAt = new Date().toISOString();
|
|
137
|
+
team.updatedAt = new Date().toISOString();
|
|
138
|
+
await saveTeam(team);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Load a team by ID.
|
|
143
|
+
*/
|
|
144
|
+
export async function loadTeam(teamId: string): Promise<Team | null> {
|
|
145
|
+
const path = getTeamPath(teamId);
|
|
146
|
+
if (!existsSync(path)) return null;
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const raw = await readFile(path, "utf-8");
|
|
150
|
+
return JSON.parse(raw) as Team;
|
|
151
|
+
} catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* List all teams.
|
|
158
|
+
*/
|
|
159
|
+
export async function listTeams(): Promise<Team[]> {
|
|
160
|
+
const dir = getTeamsDir();
|
|
161
|
+
if (!existsSync(dir)) return [];
|
|
162
|
+
|
|
163
|
+
const files = await readdir(dir);
|
|
164
|
+
const teams: Team[] = [];
|
|
165
|
+
|
|
166
|
+
for (const file of files.filter((f) => f.endsWith(".json"))) {
|
|
167
|
+
try {
|
|
168
|
+
const raw = await readFile(join(dir, file), "utf-8");
|
|
169
|
+
teams.push(JSON.parse(raw) as Team);
|
|
170
|
+
} catch {
|
|
171
|
+
// Skip corrupt team files
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return teams.sort(
|
|
176
|
+
(a, b) =>
|
|
177
|
+
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(),
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Delete a team.
|
|
183
|
+
*/
|
|
184
|
+
export async function deleteTeam(teamId: string): Promise<boolean> {
|
|
185
|
+
const path = getTeamPath(teamId);
|
|
186
|
+
if (!existsSync(path)) return false;
|
|
187
|
+
|
|
188
|
+
await unlink(path);
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Save team to disk.
|
|
194
|
+
*/
|
|
195
|
+
async function saveTeam(team: Team): Promise<void> {
|
|
196
|
+
await mkdir(getTeamsDir(), { recursive: true });
|
|
197
|
+
await writeFile(
|
|
198
|
+
getTeamPath(team.id),
|
|
199
|
+
JSON.stringify(team, null, 2),
|
|
200
|
+
"utf-8",
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Get the best teammate for a task type.
|
|
206
|
+
*/
|
|
207
|
+
export function pickTeammateForTask(
|
|
208
|
+
team: Team,
|
|
209
|
+
taskType: string,
|
|
210
|
+
): Teammate | null {
|
|
211
|
+
// Match by role
|
|
212
|
+
const roleMatch = team.teammates.find((t) => t.role === taskType);
|
|
213
|
+
if (roleMatch) return roleMatch;
|
|
214
|
+
|
|
215
|
+
// Fallback: least busy teammate
|
|
216
|
+
const sorted = [...team.teammates].sort(
|
|
217
|
+
(a, b) => a.stats.tasksCompleted - b.stats.tasksCompleted,
|
|
218
|
+
);
|
|
219
|
+
return sorted[0] ?? null;
|
|
220
|
+
}
|