@alisio/plugin-subagents 0.1.0-alpha.3
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 +20 -0
- package/dist/config.d.ts +28 -0
- package/dist/config.js +30 -0
- package/dist/definitions.d.ts +60 -0
- package/dist/definitions.js +269 -0
- package/dist/git.d.ts +25 -0
- package/dist/git.js +93 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +254 -0
- package/dist/manager.d.ts +89 -0
- package/dist/manager.js +519 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gustavo Gutiérrez
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# @alisio/plugin-subagents
|
|
2
|
+
|
|
3
|
+
Subagents for [Alisio](https://github.com/GustavoGutierrez/alisio), shipped as a built-in plugin of the
|
|
4
|
+
`alisio` CLI (disable with `--disable-plugin subagents`). The parent agent delegates work with the
|
|
5
|
+
`task` tool to specialized agents that run in separate child sessions: fresh context, narrowed
|
|
6
|
+
permissions, their own model and limits. It depends only on `@alisio/sdk` (generic
|
|
7
|
+
`api.sessions`, `api.ui.panel`, `api.ui.select`) plus `zod` and `yaml`.
|
|
8
|
+
|
|
9
|
+
- Agent definitions: Markdown + YAML frontmatter (`name`, `description`, `tools`, `model`,
|
|
10
|
+
`permission`, `maxTurns`, …) from `--agents`, `.alisio/agents`, `.agents/agents`, `.claude/agents`,
|
|
11
|
+
`.opencode/agent(s)`, user directories and plugins; built-ins `general`, `explore`, `plan`.
|
|
12
|
+
- Tools: `task`, `task_status`, `task_wait`, `send_message`; parallel and background tasks, bounded
|
|
13
|
+
waits, `<task …>` results, `<task-notification>` on background completion.
|
|
14
|
+
- Limits: depth, concurrency per parent and total, bounded queue, turns, timeout and token budget.
|
|
15
|
+
- Parallel writes in git: worktree per subagent (merge/discard with `/agents`), serial or shared.
|
|
16
|
+
- A live agent tree in the TUI with keyboard navigation and read-only child views.
|
|
17
|
+
|
|
18
|
+
Docs: [Subagents](https://gustavogutierrez.github.io/alisio/subagents).
|
|
19
|
+
|
|
20
|
+
Maintainer: Gustavo Gutiérrez · License: MIT
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** Options under `builtinPlugins.subagents`. */
|
|
3
|
+
export declare const subagentsConfigSchema: z.ZodObject<{
|
|
4
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
5
|
+
maxDepth: z.ZodDefault<z.ZodNumber>;
|
|
6
|
+
maxConcurrentPerParent: z.ZodDefault<z.ZodNumber>;
|
|
7
|
+
maxConcurrentTotal: z.ZodDefault<z.ZodNumber>;
|
|
8
|
+
maxQueued: z.ZodDefault<z.ZodNumber>;
|
|
9
|
+
maxTurns: z.ZodDefault<z.ZodNumber>;
|
|
10
|
+
timeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
11
|
+
maxTokensPerChild: z.ZodOptional<z.ZodNumber>;
|
|
12
|
+
parallelWrites: z.ZodDefault<z.ZodEnum<{
|
|
13
|
+
ask: "ask";
|
|
14
|
+
serial: "serial";
|
|
15
|
+
shared: "shared";
|
|
16
|
+
worktree: "worktree";
|
|
17
|
+
}>>;
|
|
18
|
+
waitMaxMs: z.ZodDefault<z.ZodNumber>;
|
|
19
|
+
resultMaxBytes: z.ZodDefault<z.ZodNumber>;
|
|
20
|
+
agents: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
21
|
+
description: z.ZodString;
|
|
22
|
+
prompt: z.ZodDefault<z.ZodString>;
|
|
23
|
+
tools: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
24
|
+
model: z.ZodOptional<z.ZodString>;
|
|
25
|
+
}, z.core.$strip>>>;
|
|
26
|
+
worktreeDir: z.ZodOptional<z.ZodString>;
|
|
27
|
+
}, z.core.$strict>;
|
|
28
|
+
export type SubagentsConfig = z.infer<typeof subagentsConfigSchema>;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const cliAgent = z.object({
|
|
3
|
+
description: z.string().min(1),
|
|
4
|
+
prompt: z.string().default(""),
|
|
5
|
+
tools: z.array(z.string()).optional(),
|
|
6
|
+
model: z.string().optional(),
|
|
7
|
+
});
|
|
8
|
+
/** Options under `builtinPlugins.subagents`. */
|
|
9
|
+
export const subagentsConfigSchema = z
|
|
10
|
+
.object({
|
|
11
|
+
enabled: z.boolean().default(true),
|
|
12
|
+
maxDepth: z.number().int().min(1).max(8).default(3),
|
|
13
|
+
maxConcurrentPerParent: z.number().int().min(1).max(32).default(4),
|
|
14
|
+
maxConcurrentTotal: z.number().int().min(1).max(64).default(8),
|
|
15
|
+
/** Waiting tasks beyond the concurrency limits; more fail fast. */
|
|
16
|
+
maxQueued: z.number().int().min(0).max(256).default(16),
|
|
17
|
+
maxTurns: z.number().int().min(1).max(500).default(50),
|
|
18
|
+
timeoutMs: z.number().int().min(1_000).default(600_000),
|
|
19
|
+
/** Per-child cumulative token budget; default follows the core proportional budget. */
|
|
20
|
+
maxTokensPerChild: z.number().int().positive().optional(),
|
|
21
|
+
/** Parallel write-capable children in a git repository. */
|
|
22
|
+
parallelWrites: z.enum(["ask", "worktree", "serial", "shared"]).default("ask"),
|
|
23
|
+
waitMaxMs: z.number().int().min(100).default(600_000),
|
|
24
|
+
resultMaxBytes: z.number().int().min(1_000).max(1_000_000).default(50_000),
|
|
25
|
+
/** Agents from `--agents <json>` (highest precedence). */
|
|
26
|
+
agents: z.record(z.string(), cliAgent).default({}),
|
|
27
|
+
/** Where worktrees are created (default `<state>/worktrees`). */
|
|
28
|
+
worktreeDir: z.string().optional(),
|
|
29
|
+
})
|
|
30
|
+
.strict();
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export type Level = "allow" | "ask" | "deny";
|
|
2
|
+
export interface AgentDefinition {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
/** System prompt (the Markdown body). */
|
|
6
|
+
prompt: string;
|
|
7
|
+
/** Allowlist; `*` means every tool the parent has (including `task`). */
|
|
8
|
+
tools?: string[];
|
|
9
|
+
disallowedTools?: string[];
|
|
10
|
+
/** Model selector (`provider/model` or a unique model id); undefined inherits the parent. */
|
|
11
|
+
model?: string;
|
|
12
|
+
mode: "subagent" | "primary" | "all";
|
|
13
|
+
maxTurns?: number;
|
|
14
|
+
color?: string;
|
|
15
|
+
permission?: {
|
|
16
|
+
write?: Level;
|
|
17
|
+
process?: Level;
|
|
18
|
+
};
|
|
19
|
+
readOnly?: boolean;
|
|
20
|
+
hidden: boolean;
|
|
21
|
+
background: boolean;
|
|
22
|
+
skills?: string[];
|
|
23
|
+
/** builtin | cli | project | convention | compat | user | plugin:<id> */
|
|
24
|
+
source: string;
|
|
25
|
+
path?: string;
|
|
26
|
+
}
|
|
27
|
+
export declare function parseAgentDefinition(text: string, meta: {
|
|
28
|
+
source: string;
|
|
29
|
+
path?: string;
|
|
30
|
+
}): {
|
|
31
|
+
definition?: AgentDefinition;
|
|
32
|
+
warnings: string[];
|
|
33
|
+
};
|
|
34
|
+
export declare const BUILTIN_AGENTS: AgentDefinition[];
|
|
35
|
+
export interface CliAgent {
|
|
36
|
+
description: string;
|
|
37
|
+
prompt: string;
|
|
38
|
+
tools?: string[];
|
|
39
|
+
model?: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Discovery, first match wins by name: CLI `--agents` > project `.alisio/agents` >
|
|
43
|
+
* `.agents/agents` (speculative convention) > compat `.claude/agents`, `.opencode/agent(s)`
|
|
44
|
+
* > user `<config>/agents`, `~/.claude/agents`, `~/.config/opencode/agent(s)` > plugin
|
|
45
|
+
* directories (namespaced `plugin:name`) > built-ins. Project sources require trust.
|
|
46
|
+
*/
|
|
47
|
+
export declare function discoverAgents(options: {
|
|
48
|
+
workspace: string;
|
|
49
|
+
home: string;
|
|
50
|
+
configHome: string;
|
|
51
|
+
trusted: boolean;
|
|
52
|
+
cli?: Record<string, CliAgent>;
|
|
53
|
+
plugins: Array<{
|
|
54
|
+
plugin: string;
|
|
55
|
+
dir: string;
|
|
56
|
+
}>;
|
|
57
|
+
}): Promise<{
|
|
58
|
+
agents: Map<string, AgentDefinition>;
|
|
59
|
+
warnings: string[];
|
|
60
|
+
}>;
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent definitions: Markdown files with YAML frontmatter (Alisio format), plus compatible
|
|
3
|
+
* readers for Claude Code (`.claude/agents`) and opencode (`.opencode/agent(s)`) files.
|
|
4
|
+
*/
|
|
5
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
6
|
+
import { basename, join } from "node:path";
|
|
7
|
+
import { parse } from "yaml";
|
|
8
|
+
const NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
9
|
+
const KNOWN = new Set([
|
|
10
|
+
"name",
|
|
11
|
+
"description",
|
|
12
|
+
"tools",
|
|
13
|
+
"disallowedTools",
|
|
14
|
+
"model",
|
|
15
|
+
"mode",
|
|
16
|
+
"maxTurns",
|
|
17
|
+
"steps",
|
|
18
|
+
"maxSteps",
|
|
19
|
+
"color",
|
|
20
|
+
"permission",
|
|
21
|
+
"hidden",
|
|
22
|
+
"background",
|
|
23
|
+
"skills",
|
|
24
|
+
"readOnly",
|
|
25
|
+
// Compat keys accepted silently.
|
|
26
|
+
"temperature",
|
|
27
|
+
"top_p",
|
|
28
|
+
"disable",
|
|
29
|
+
]);
|
|
30
|
+
/** Claude Code tool names → Alisio tools. */
|
|
31
|
+
const TOOL_ALIASES = {
|
|
32
|
+
read: ["read_file"],
|
|
33
|
+
grep: ["search_text"],
|
|
34
|
+
glob: ["list_files"],
|
|
35
|
+
ls: ["list_files"],
|
|
36
|
+
edit: ["edit_file"],
|
|
37
|
+
multiedit: ["edit_file"],
|
|
38
|
+
write: ["write_file"],
|
|
39
|
+
bash: ["shell", "run_process"],
|
|
40
|
+
task: ["task"],
|
|
41
|
+
};
|
|
42
|
+
const CLAUDE_MODEL_ALIASES = new Set(["sonnet", "opus", "haiku"]);
|
|
43
|
+
const list = (value) => {
|
|
44
|
+
if (Array.isArray(value))
|
|
45
|
+
return value
|
|
46
|
+
.map(String)
|
|
47
|
+
.map((s) => s.trim())
|
|
48
|
+
.filter(Boolean);
|
|
49
|
+
if (typeof value === "string")
|
|
50
|
+
return value
|
|
51
|
+
.split(",")
|
|
52
|
+
.map((s) => s.trim())
|
|
53
|
+
.filter(Boolean);
|
|
54
|
+
return undefined;
|
|
55
|
+
};
|
|
56
|
+
const mapTools = (names) => [
|
|
57
|
+
...new Set(names.flatMap((n) => TOOL_ALIASES[n.toLowerCase()] ?? [n])),
|
|
58
|
+
];
|
|
59
|
+
const level = (value) => value === "allow" || value === "ask" || value === "deny"
|
|
60
|
+
? value
|
|
61
|
+
: value && typeof value === "object"
|
|
62
|
+
? "ask" // pattern maps (opencode) narrow to per-call approval
|
|
63
|
+
: undefined;
|
|
64
|
+
export function parseAgentDefinition(text, meta) {
|
|
65
|
+
const warnings = [];
|
|
66
|
+
const where = meta.path ?? meta.source;
|
|
67
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)([\s\S]*)$/.exec(text);
|
|
68
|
+
if (!match)
|
|
69
|
+
return { warnings: [`${where}: missing YAML frontmatter`] };
|
|
70
|
+
let front;
|
|
71
|
+
try {
|
|
72
|
+
front = (parse(match[1] ?? "", { maxAliasCount: 20 }) ?? {});
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
return { warnings: [`${where}: invalid YAML (${String(error)})`] };
|
|
76
|
+
}
|
|
77
|
+
const fileName = meta.path ? basename(meta.path, ".md") : undefined;
|
|
78
|
+
const name = String(front.name ?? fileName ?? "");
|
|
79
|
+
if (!NAME.test(name) || name.length > 64)
|
|
80
|
+
return {
|
|
81
|
+
warnings: [`${where}: invalid agent name "${name}" (lowercase letters, digits, hyphens)`],
|
|
82
|
+
};
|
|
83
|
+
const description = typeof front.description === "string" ? front.description.trim() : "";
|
|
84
|
+
if (!description)
|
|
85
|
+
return { warnings: [`${where}: description is required`] };
|
|
86
|
+
for (const key of Object.keys(front))
|
|
87
|
+
if (!KNOWN.has(key))
|
|
88
|
+
warnings.push(`${where}: unknown key "${key}" ignored`);
|
|
89
|
+
const definition = {
|
|
90
|
+
name,
|
|
91
|
+
description,
|
|
92
|
+
prompt: (match[2] ?? "").trim(),
|
|
93
|
+
mode: front.mode === "primary" || front.mode === "all" ? front.mode : "subagent",
|
|
94
|
+
hidden: front.hidden === true,
|
|
95
|
+
background: front.background === true,
|
|
96
|
+
source: meta.source,
|
|
97
|
+
...(meta.path ? { path: meta.path } : {}),
|
|
98
|
+
};
|
|
99
|
+
const tools = front.tools;
|
|
100
|
+
if (tools && typeof tools === "object" && !Array.isArray(tools)) {
|
|
101
|
+
// opencode: { tool: false } disables a tool.
|
|
102
|
+
const off = Object.entries(tools)
|
|
103
|
+
.filter(([, v]) => v === false)
|
|
104
|
+
.map(([k]) => k);
|
|
105
|
+
if (off.length)
|
|
106
|
+
definition.disallowedTools = mapTools(off);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
const allow = list(tools);
|
|
110
|
+
if (allow)
|
|
111
|
+
definition.tools = mapTools(allow);
|
|
112
|
+
}
|
|
113
|
+
const deny = list(front.disallowedTools);
|
|
114
|
+
if (deny)
|
|
115
|
+
definition.disallowedTools = [
|
|
116
|
+
...new Set([...(definition.disallowedTools ?? []), ...mapTools(deny)]),
|
|
117
|
+
];
|
|
118
|
+
if (typeof front.model === "string" && front.model !== "inherit") {
|
|
119
|
+
if (CLAUDE_MODEL_ALIASES.has(front.model))
|
|
120
|
+
warnings.push(`${where}: model alias "${front.model}" is Claude-specific; inheriting the parent's model`);
|
|
121
|
+
else
|
|
122
|
+
definition.model = front.model;
|
|
123
|
+
}
|
|
124
|
+
const turns = front.maxTurns ?? front.steps ?? front.maxSteps;
|
|
125
|
+
if (typeof turns === "number" && Number.isInteger(turns) && turns > 0)
|
|
126
|
+
definition.maxTurns = turns;
|
|
127
|
+
if (typeof front.color === "string")
|
|
128
|
+
definition.color = front.color;
|
|
129
|
+
const permission = front.permission;
|
|
130
|
+
if (permission && typeof permission === "object") {
|
|
131
|
+
const write = level(permission.edit ?? permission.write);
|
|
132
|
+
const process = level(permission.bash ?? permission.process);
|
|
133
|
+
definition.permission = { ...(write ? { write } : {}), ...(process ? { process } : {}) };
|
|
134
|
+
}
|
|
135
|
+
if (front.readOnly === true)
|
|
136
|
+
definition.readOnly = true;
|
|
137
|
+
const skills = list(front.skills);
|
|
138
|
+
if (skills)
|
|
139
|
+
definition.skills = skills;
|
|
140
|
+
return { definition, warnings };
|
|
141
|
+
}
|
|
142
|
+
export const BUILTIN_AGENTS = [
|
|
143
|
+
{
|
|
144
|
+
name: "general",
|
|
145
|
+
description: "General-purpose agent for multi-step tasks: research, code changes and verification. Has every tool its parent has, including delegation.",
|
|
146
|
+
prompt: "You are a general-purpose subagent. Work autonomously on the delegated task, verify your results, and finish with a concise report of what you did, what you found and anything left undone.",
|
|
147
|
+
tools: ["*"],
|
|
148
|
+
mode: "subagent",
|
|
149
|
+
hidden: false,
|
|
150
|
+
background: false,
|
|
151
|
+
source: "builtin",
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: "explore",
|
|
155
|
+
description: "Fast read-only agent for exploring a codebase: find files, search code and summarize how things work. Cannot modify files or run processes.",
|
|
156
|
+
prompt: "You are a read-only exploration subagent. Use list_files, search_text, read_file and git tools efficiently to answer the delegated question. Cite file paths. Do not propose edits unless asked. Finish with a concise, factual summary.",
|
|
157
|
+
tools: [
|
|
158
|
+
"read_file",
|
|
159
|
+
"list_files",
|
|
160
|
+
"search_text",
|
|
161
|
+
"git_status",
|
|
162
|
+
"git_diff",
|
|
163
|
+
"context_explain",
|
|
164
|
+
"skill_load",
|
|
165
|
+
"skill_search",
|
|
166
|
+
"skill_resource",
|
|
167
|
+
],
|
|
168
|
+
readOnly: true,
|
|
169
|
+
mode: "subagent",
|
|
170
|
+
hidden: false,
|
|
171
|
+
background: false,
|
|
172
|
+
color: "cyan",
|
|
173
|
+
source: "builtin",
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: "plan",
|
|
177
|
+
description: "Read-only planning agent: analyzes the code and returns a step-by-step implementation plan with the files to change. Never edits.",
|
|
178
|
+
prompt: "You are a read-only planning subagent. Investigate the relevant code, then return a concrete, ordered implementation plan listing files, functions and risks. Do not modify anything.",
|
|
179
|
+
tools: ["read_file", "list_files", "search_text", "git_status", "git_diff", "context_explain"],
|
|
180
|
+
readOnly: true,
|
|
181
|
+
mode: "subagent",
|
|
182
|
+
hidden: false,
|
|
183
|
+
background: false,
|
|
184
|
+
color: "yellow",
|
|
185
|
+
source: "builtin",
|
|
186
|
+
},
|
|
187
|
+
];
|
|
188
|
+
async function readDir(dir) {
|
|
189
|
+
let names;
|
|
190
|
+
try {
|
|
191
|
+
names = (await readdir(dir)).filter((n) => n.endsWith(".md")).sort();
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
const out = [];
|
|
197
|
+
for (const name of names.slice(0, 200)) {
|
|
198
|
+
const path = join(dir, name);
|
|
199
|
+
try {
|
|
200
|
+
const text = await readFile(path, "utf8");
|
|
201
|
+
if (text.length <= 64_000)
|
|
202
|
+
out.push({ path, text });
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
/* unreadable entries are skipped */
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Discovery, first match wins by name: CLI `--agents` > project `.alisio/agents` >
|
|
212
|
+
* `.agents/agents` (speculative convention) > compat `.claude/agents`, `.opencode/agent(s)`
|
|
213
|
+
* > user `<config>/agents`, `~/.claude/agents`, `~/.config/opencode/agent(s)` > plugin
|
|
214
|
+
* directories (namespaced `plugin:name`) > built-ins. Project sources require trust.
|
|
215
|
+
*/
|
|
216
|
+
export async function discoverAgents(options) {
|
|
217
|
+
const agents = new Map();
|
|
218
|
+
const warnings = [];
|
|
219
|
+
const add = (definition) => {
|
|
220
|
+
const existing = agents.get(definition.name);
|
|
221
|
+
if (existing)
|
|
222
|
+
warnings.push(`Agent ${definition.name} from ${definition.path ?? definition.source} is shadowed by ${existing.path ?? existing.source}`);
|
|
223
|
+
else
|
|
224
|
+
agents.set(definition.name, definition);
|
|
225
|
+
};
|
|
226
|
+
for (const [name, spec] of Object.entries(options.cli ?? {})) {
|
|
227
|
+
const { definition, warnings: w } = parseAgentDefinition(`---\n${JSON.stringify({ name, description: spec.description, ...(spec.tools ? { tools: spec.tools } : {}), ...(spec.model ? { model: spec.model } : {}) })}\n---\n${spec.prompt ?? ""}`, { source: "cli" });
|
|
228
|
+
warnings.push(...w);
|
|
229
|
+
if (definition)
|
|
230
|
+
add(definition);
|
|
231
|
+
}
|
|
232
|
+
const sources = [
|
|
233
|
+
{ dir: join(options.workspace, ".alisio", "agents"), source: "project", project: true },
|
|
234
|
+
{ dir: join(options.workspace, ".agents", "agents"), source: "convention", project: true },
|
|
235
|
+
{ dir: join(options.workspace, ".claude", "agents"), source: "compat", project: true },
|
|
236
|
+
{ dir: join(options.workspace, ".opencode", "agent"), source: "compat", project: true },
|
|
237
|
+
{ dir: join(options.workspace, ".opencode", "agents"), source: "compat", project: true },
|
|
238
|
+
{ dir: join(options.configHome, "agents"), source: "user", project: false },
|
|
239
|
+
{ dir: join(options.home, ".claude", "agents"), source: "user", project: false },
|
|
240
|
+
{ dir: join(options.home, ".config", "opencode", "agent"), source: "user", project: false },
|
|
241
|
+
{ dir: join(options.home, ".config", "opencode", "agents"), source: "user", project: false },
|
|
242
|
+
];
|
|
243
|
+
for (const { dir, source, project } of sources) {
|
|
244
|
+
if (project && !options.trusted)
|
|
245
|
+
continue;
|
|
246
|
+
for (const file of await readDir(dir)) {
|
|
247
|
+
const { definition, warnings: w } = parseAgentDefinition(file.text, {
|
|
248
|
+
source,
|
|
249
|
+
path: file.path,
|
|
250
|
+
});
|
|
251
|
+
warnings.push(...w);
|
|
252
|
+
if (definition)
|
|
253
|
+
add(definition);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
for (const { plugin, dir } of options.plugins)
|
|
257
|
+
for (const file of await readDir(dir)) {
|
|
258
|
+
const { definition, warnings: w } = parseAgentDefinition(file.text, {
|
|
259
|
+
source: `plugin:${plugin}`,
|
|
260
|
+
path: file.path,
|
|
261
|
+
});
|
|
262
|
+
warnings.push(...w);
|
|
263
|
+
if (definition)
|
|
264
|
+
add({ ...definition, name: `${plugin}:${definition.name}` });
|
|
265
|
+
}
|
|
266
|
+
for (const builtin of BUILTIN_AGENTS)
|
|
267
|
+
add(builtin);
|
|
268
|
+
return { agents, warnings };
|
|
269
|
+
}
|
package/dist/git.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface GitResult {
|
|
2
|
+
code: number;
|
|
3
|
+
stdout: string;
|
|
4
|
+
stderr: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function git(cwd: string, args: string[], env?: NodeJS.ProcessEnv): Promise<GitResult>;
|
|
7
|
+
export declare function repoRoot(dir: string): Promise<string | undefined>;
|
|
8
|
+
export declare function isDirty(root: string): Promise<boolean>;
|
|
9
|
+
export declare function head(root: string): Promise<string>;
|
|
10
|
+
export declare function addWorktree(root: string, path: string, branch: string): Promise<string>;
|
|
11
|
+
/** Commits everything in a worktree; returns false when there was nothing to commit. */
|
|
12
|
+
export declare function commitAll(cwd: string, message: string): Promise<boolean>;
|
|
13
|
+
export declare function diffSummary(root: string, base: string, branch: string): Promise<{
|
|
14
|
+
files: string[];
|
|
15
|
+
stat: string;
|
|
16
|
+
}>;
|
|
17
|
+
/** `git merge --no-ff`; on conflict the merge is aborted and conflicting files are reported. */
|
|
18
|
+
export declare function merge(root: string, branch: string, message: string): Promise<{
|
|
19
|
+
ok: true;
|
|
20
|
+
} | {
|
|
21
|
+
ok: false;
|
|
22
|
+
conflicts: string[];
|
|
23
|
+
message: string;
|
|
24
|
+
}>;
|
|
25
|
+
export declare function removeWorktree(root: string, path: string, branch: string, deleteBranch: boolean): Promise<void>;
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/** Minimal git plumbing for worktree isolation (argument arrays, never a shell). */
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
export function git(cwd, args, env = {}) {
|
|
4
|
+
return new Promise((resolve) => {
|
|
5
|
+
execFile("git", args, {
|
|
6
|
+
cwd,
|
|
7
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0", ...env },
|
|
8
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
9
|
+
}, (error, stdout, stderr) => {
|
|
10
|
+
const code = error ? (typeof error.code === "number" ? error.code : 1) : 0;
|
|
11
|
+
resolve({ code, stdout: String(stdout), stderr: String(stderr) });
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
export async function repoRoot(dir) {
|
|
16
|
+
const r = await git(dir, ["rev-parse", "--show-toplevel"]);
|
|
17
|
+
return r.code === 0 ? r.stdout.trim() : undefined;
|
|
18
|
+
}
|
|
19
|
+
export async function isDirty(root) {
|
|
20
|
+
const r = await git(root, ["status", "--porcelain"]);
|
|
21
|
+
return r.code === 0 && r.stdout.trim().length > 0;
|
|
22
|
+
}
|
|
23
|
+
export async function head(root) {
|
|
24
|
+
const r = await git(root, ["rev-parse", "HEAD"]);
|
|
25
|
+
if (r.code)
|
|
26
|
+
throw new Error(`git rev-parse HEAD failed: ${r.stderr.trim()}`);
|
|
27
|
+
return r.stdout.trim();
|
|
28
|
+
}
|
|
29
|
+
export async function addWorktree(root, path, branch) {
|
|
30
|
+
const base = await head(root);
|
|
31
|
+
const r = await git(root, ["worktree", "add", "-b", branch, path, base]);
|
|
32
|
+
if (r.code)
|
|
33
|
+
throw new Error(`git worktree add failed: ${r.stderr.trim()}`);
|
|
34
|
+
return base;
|
|
35
|
+
}
|
|
36
|
+
/** Identity used only when the repository has none configured. */
|
|
37
|
+
async function identity(cwd) {
|
|
38
|
+
const email = await git(cwd, ["config", "user.email"]);
|
|
39
|
+
return email.code === 0 && email.stdout.trim()
|
|
40
|
+
? {}
|
|
41
|
+
: {
|
|
42
|
+
GIT_AUTHOR_NAME: "Alisio",
|
|
43
|
+
GIT_AUTHOR_EMAIL: "alisio@localhost",
|
|
44
|
+
GIT_COMMITTER_NAME: "Alisio",
|
|
45
|
+
GIT_COMMITTER_EMAIL: "alisio@localhost",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** Commits everything in a worktree; returns false when there was nothing to commit. */
|
|
49
|
+
export async function commitAll(cwd, message) {
|
|
50
|
+
await git(cwd, ["add", "-A"]);
|
|
51
|
+
const staged = await git(cwd, ["diff", "--cached", "--quiet"]);
|
|
52
|
+
if (staged.code === 0)
|
|
53
|
+
return false;
|
|
54
|
+
const r = await git(cwd, ["commit", "-q", "-m", message], await identity(cwd));
|
|
55
|
+
if (r.code)
|
|
56
|
+
throw new Error(`git commit failed: ${r.stderr.trim()}`);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
export async function diffSummary(root, base, branch) {
|
|
60
|
+
const names = await git(root, ["diff", "--name-only", `${base}..${branch}`]);
|
|
61
|
+
const stat = await git(root, ["diff", "--stat", `${base}..${branch}`]);
|
|
62
|
+
return {
|
|
63
|
+
files: names.stdout.split("\n").filter(Boolean),
|
|
64
|
+
stat: stat.stdout.trim(),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** `git merge --no-ff`; on conflict the merge is aborted and conflicting files are reported. */
|
|
68
|
+
export async function merge(root, branch, message) {
|
|
69
|
+
if (await isDirty(root))
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
conflicts: [],
|
|
73
|
+
message: "Working tree has uncommitted changes; commit or stash them first.",
|
|
74
|
+
};
|
|
75
|
+
const r = await git(root, ["merge", "--no-ff", "-m", message, branch], await identity(root));
|
|
76
|
+
if (r.code === 0)
|
|
77
|
+
return { ok: true };
|
|
78
|
+
const conflicts = (await git(root, ["diff", "--name-only", "--diff-filter=U"])).stdout
|
|
79
|
+
.split("\n")
|
|
80
|
+
.filter(Boolean);
|
|
81
|
+
await git(root, ["merge", "--abort"]);
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
conflicts,
|
|
85
|
+
message: (r.stdout + r.stderr).trim().split("\n").slice(-3).join(" "),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export async function removeWorktree(root, path, branch, deleteBranch) {
|
|
89
|
+
await git(root, ["worktree", "remove", "--force", path]);
|
|
90
|
+
if (deleteBranch)
|
|
91
|
+
await git(root, ["branch", "-D", branch]);
|
|
92
|
+
await git(root, ["worktree", "prune"]);
|
|
93
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Plugin } from "@alisio/sdk";
|
|
2
|
+
export type { AgentDefinition } from "./definitions.ts";
|
|
3
|
+
export { BUILTIN_AGENTS, discoverAgents, parseAgentDefinition } from "./definitions.ts";
|
|
4
|
+
export { SubagentManager } from "./manager.ts";
|
|
5
|
+
export interface SubagentsPluginContext {
|
|
6
|
+
workspace: string;
|
|
7
|
+
stateHome: string;
|
|
8
|
+
configHome: string;
|
|
9
|
+
configDir: string;
|
|
10
|
+
home: string;
|
|
11
|
+
trusted: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function createSubagentsPlugin(rawOptions: unknown, context: SubagentsPluginContext): Plugin;
|
|
14
|
+
export declare const subagentsWorktreeDir: (stateHome: string) => string;
|