@vincemakes/kiso-subagent-ext 0.1.45
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 +23 -0
- package/dist/kiso-subagent.mjs +356 -0
- package/index.d.ts +11 -0
- package/package.json +31 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kiso contributors
|
|
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,23 @@
|
|
|
1
|
+
# kiso-subagent-ext
|
|
2
|
+
|
|
3
|
+
The kiso official subagent extension: child kiso processes with role
|
|
4
|
+
policies, the kernel untouched.
|
|
5
|
+
|
|
6
|
+
## How it is loaded
|
|
7
|
+
|
|
8
|
+
Since 0.1.45 this extension ships **built-in** with the kiso CLI — a fresh
|
|
9
|
+
install starts with it registered (the startup banner lists it), with zero
|
|
10
|
+
disk setup. The same artifact can also be installed as a user-level
|
|
11
|
+
extension: copy `dist/kiso-subagent.mjs` into `~/.kiso/extensions/` — the
|
|
12
|
+
user-layer loader accepts exactly this shape.
|
|
13
|
+
|
|
14
|
+
## Configuration
|
|
15
|
+
|
|
16
|
+
None. Every delegation is asked of the human (no auto-allow); depth is
|
|
17
|
+
guarded so children can never nest.
|
|
18
|
+
|
|
19
|
+
## Versioning
|
|
20
|
+
|
|
21
|
+
The version counter is this package's own. It is pinned exactly by the kiso
|
|
22
|
+
CLI it ships with; an extension release reaches CLI users through the next
|
|
23
|
+
CLI release.
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kiso (foundation) official subagent extension — ④: child kiso processes with
|
|
3
|
+
* role policies, kernel untouched.
|
|
4
|
+
*
|
|
5
|
+
* `delegate` spawns child kiso processes (the SAME binary) that work in
|
|
6
|
+
* isolated, role-policy-gated environments and report back from their OWN
|
|
7
|
+
* durable session JSONL (children land in the normal sessions directory —
|
|
8
|
+
* durable, auditable, resumable after a parent crash). Depth is guarded
|
|
9
|
+
* (KISO_SUBAGENT_DEPTH ≥ 1 → no delegate) so children can never nest.
|
|
10
|
+
*
|
|
11
|
+
* Approval: no auto-allow — delegate falls in the ask tier, so a human
|
|
12
|
+
* sees every delegation (ruling A: the ask reaches the human directly).
|
|
13
|
+
*
|
|
14
|
+
* Zero runtime dependencies: child_process/fs/os/path are builtins.
|
|
15
|
+
*
|
|
16
|
+
* finding #8: this extension holds NO persistent resources — children are
|
|
17
|
+
* spawned per call and exit on their own, the role-policy temp dirs are
|
|
18
|
+
* cleaned in runChild's finally — so NO dispose is needed, explicitly.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { spawn, execFileSync } from "node:child_process";
|
|
22
|
+
import { mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
23
|
+
import { homedir, tmpdir } from "node:os";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
|
|
26
|
+
/** Default per-child timeout (ms) — a subagent must never hang the parent. */
|
|
27
|
+
const TIMEOUT_MS = 10 * 60 * 1000;
|
|
28
|
+
/** Max simultaneous children — the concurrency cap (mapLimited). */
|
|
29
|
+
const CONCURRENCY = 4;
|
|
30
|
+
|
|
31
|
+
const SIX_TOOLS = ["read_file", "list_dir", "search_text", "write_file", "edit_file", "shell"];
|
|
32
|
+
const READ_ONLY = ["read_file", "list_dir", "search_text"];
|
|
33
|
+
const ROLES = ["explorer", "implementer", "reviewer", "tester"];
|
|
34
|
+
|
|
35
|
+
const DELEGATE_PARAMETERS = {
|
|
36
|
+
type: "object",
|
|
37
|
+
properties: {
|
|
38
|
+
tasks: {
|
|
39
|
+
type: "array",
|
|
40
|
+
minItems: 1,
|
|
41
|
+
maxItems: 8,
|
|
42
|
+
items: {
|
|
43
|
+
type: "object",
|
|
44
|
+
properties: {
|
|
45
|
+
role: { type: "string", enum: ROLES },
|
|
46
|
+
task: { type: "string", minLength: 1 },
|
|
47
|
+
},
|
|
48
|
+
required: ["role", "task"],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
required: ["tasks"],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export default async function createSubagentExtension() {
|
|
56
|
+
const depth = Number.parseInt(process.env.KISO_SUBAGENT_DEPTH ?? "0", 10) || 0;
|
|
57
|
+
if (depth >= 1) return { name: "subagent", tools: [] }; // depth guard — no nesting
|
|
58
|
+
return {
|
|
59
|
+
name: "subagent",
|
|
60
|
+
tools: [
|
|
61
|
+
{
|
|
62
|
+
name: "delegate",
|
|
63
|
+
description: "run subagent tasks (explorer/implementer/reviewer/tester) in child kiso processes",
|
|
64
|
+
parameters: DELEGATE_PARAMETERS,
|
|
65
|
+
execute: async (input, ctx) => {
|
|
66
|
+
const tasks = ((input ?? {}).tasks ?? []).slice(0, 8);
|
|
67
|
+
if (tasks.length === 0) return { content: "delegate: no tasks", isError: true };
|
|
68
|
+
const sessionsDir = join(process.env.KISO_HOME ?? join(homedir(), ".kiso"), "sessions");
|
|
69
|
+
// P3: the loop now threads the session id through
|
|
70
|
+
// ToolContext.sessionId — the discovery heuristic below is
|
|
71
|
+
// kept ONLY as a fallback for direct tool use / tests.
|
|
72
|
+
const parentId = ctx.sessionId ?? discoverParentId(sessionsDir);
|
|
73
|
+
const bin = process.env.KISO_SUBAGENT_BIN ?? process.argv[1];
|
|
74
|
+
const timeout = Number.parseInt(process.env.KISO_SUBAGENT_TIMEOUT_MS ?? "", 10) || TIMEOUT_MS;
|
|
75
|
+
const sections = await runLimited(tasks, CONCURRENCY, (task, i) =>
|
|
76
|
+
runChild({
|
|
77
|
+
childId: `sub-${parentId}-${i + 1}-${task.role}`,
|
|
78
|
+
role: task.role,
|
|
79
|
+
task: task.task,
|
|
80
|
+
sessionsDir,
|
|
81
|
+
bin,
|
|
82
|
+
timeout,
|
|
83
|
+
signal: ctx.signal,
|
|
84
|
+
parentCwd: process.cwd(),
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
// Partial success is not overall failure — only ALL failed
|
|
88
|
+
// makes the whole result an error.
|
|
89
|
+
// W12: the blob opens with a machine-readable summary line —
|
|
90
|
+
// the ONE-LINE shape the TUI's settled row renders (└ N
|
|
91
|
+
// tool calls · R roles · F failed · /last for the report).
|
|
92
|
+
// The per-section text below is unchanged — the model's
|
|
93
|
+
// view is preserved, the summary is additive.
|
|
94
|
+
const toolCalls = sections.reduce((n, s) => n + (s.toolCalls ?? 0), 0);
|
|
95
|
+
const roles = new Set(tasks.map((t) => t.role)).size;
|
|
96
|
+
const failed = sections.filter((s) => s.failed).length;
|
|
97
|
+
const summary = `summary: ${toolCalls} tool calls · ${roles} role${roles === 1 ? "" : "s"} · ${failed} failed`;
|
|
98
|
+
return { content: `${summary}\n${sections.map((s) => s.text).join("\n")}`, isError: sections.every((s) => s.failed) };
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The parent session id for the child naming: the explicit
|
|
107
|
+
* KISO_SESSION_ID wins; otherwise the NEWEST *.jsonl in the sessions dir
|
|
108
|
+
* IS the parent (its approval events were just persisted before this tool
|
|
109
|
+
* ran); a constant fallback covers direct tool use / tests.
|
|
110
|
+
*/
|
|
111
|
+
function discoverParentId(sessionsDir) {
|
|
112
|
+
if (process.env.KISO_SESSION_ID !== undefined) return process.env.KISO_SESSION_ID;
|
|
113
|
+
let newest = null;
|
|
114
|
+
let newestMtime = -1;
|
|
115
|
+
try {
|
|
116
|
+
for (const file of readdirSync(sessionsDir)) {
|
|
117
|
+
if (!file.endsWith(".jsonl")) continue;
|
|
118
|
+
const st = statSync(join(sessionsDir, file));
|
|
119
|
+
if (st.mtimeMs > newestMtime) {
|
|
120
|
+
newestMtime = st.mtimeMs;
|
|
121
|
+
newest = file.slice(0, -".jsonl".length);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} catch {
|
|
125
|
+
// no sessions dir yet
|
|
126
|
+
}
|
|
127
|
+
return newest ?? "parent";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** mapLimited — at most `limit` tasks in flight at once. */
|
|
131
|
+
function runLimited(items, limit, fn) {
|
|
132
|
+
const results = new Array(items.length);
|
|
133
|
+
let next = 0;
|
|
134
|
+
async function worker() {
|
|
135
|
+
while (true) {
|
|
136
|
+
const i = next;
|
|
137
|
+
next += 1;
|
|
138
|
+
if (i >= items.length) return;
|
|
139
|
+
results[i] = await fn(items[i], i);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker)).then(() => results);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function runChild({ childId, role, task, sessionsDir, bin, timeout, signal, parentCwd }) {
|
|
146
|
+
// implementer isolation: a detached git worktree; the child works inside
|
|
147
|
+
// it and its diff comes back. Non-git parents fail the task HONESTLY.
|
|
148
|
+
let worktree = null;
|
|
149
|
+
let childCwd = parentCwd;
|
|
150
|
+
if (role === "implementer") {
|
|
151
|
+
worktree = mkdtempSync(join(tmpdir(), "kiso-subagent-wt-"));
|
|
152
|
+
try {
|
|
153
|
+
execFileSync("git", ["-C", parentCwd, "worktree", "add", "--detach", worktree], { stdio: "ignore" });
|
|
154
|
+
childCwd = worktree;
|
|
155
|
+
} catch (err) {
|
|
156
|
+
rmSync(worktree, { recursive: true, force: true });
|
|
157
|
+
return failSection(childId, role, task, `implementer needs a git repository: ${msg(err)}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// The child-only role policy: one .mjs in its own temp extensions dir.
|
|
161
|
+
const policyDir = mkdtempSync(join(tmpdir(), "kiso-subagent-policy-"));
|
|
162
|
+
writeFileSync(join(policyDir, "policy.mjs"), rolePolicyContent(role), "utf8");
|
|
163
|
+
let keepWorktree = false;
|
|
164
|
+
try {
|
|
165
|
+
const { code, stdout, killed } = await runProcess(childId, bin, childCwd, policyDir, task, timeout, signal);
|
|
166
|
+
const extraction = await extractChildResult(sessionsDir, childId, `exit ${code}\n${stdout}`);
|
|
167
|
+
const failed = code !== 0 || killed !== null || extraction.failed;
|
|
168
|
+
let text = `[subagent] ${role}: ${task}\n outcome: ${extraction.outcome}\n tools: ${extraction.toolCalls}`;
|
|
169
|
+
if (killed === "timeout") {
|
|
170
|
+
text += `\n FAILED: timed out after ${timeout}ms (the child process group was killed)`;
|
|
171
|
+
} else if (killed === "abort") {
|
|
172
|
+
text += "\n FAILED: aborted by the parent run (the child process group was killed)";
|
|
173
|
+
} else if (code !== 0) {
|
|
174
|
+
text += `\n FAILED: the child exited with code ${code}\n${stdout}`;
|
|
175
|
+
} else if (extraction.failed) {
|
|
176
|
+
text += `\n FAILED: ${extraction.reason}${extraction.diag !== "" ? `\n${extraction.diag}` : ""}`;
|
|
177
|
+
}
|
|
178
|
+
if (extraction.text !== "") text += `\n${extraction.text}`;
|
|
179
|
+
if (role === "implementer") {
|
|
180
|
+
const diff = worktreeDiff(worktree);
|
|
181
|
+
if (diff !== null) {
|
|
182
|
+
keepWorktree = true;
|
|
183
|
+
text += `\n diff:\n${diff}\n worktree kept at: ${worktree}`;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return { failed, text, toolCalls: extraction.toolCalls };
|
|
187
|
+
} finally {
|
|
188
|
+
rmSync(policyDir, { recursive: true, force: true });
|
|
189
|
+
if (worktree !== null && !keepWorktree) rmSync(worktree, { recursive: true, force: true });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* The child process: same binary, detached (own process group — a timeout
|
|
195
|
+
* or abort SIGKILLs the WHOLE group), input piped as the task line + exit
|
|
196
|
+
* (the same shape the CLI e2e drivers use), stdout captured for
|
|
197
|
+
* diagnostics only — the RESULT comes from the child's session JSONL.
|
|
198
|
+
*
|
|
199
|
+
* ENV — deliberately the parent's full environment PLUS the depth guard
|
|
200
|
+
* and the role policy dir. Note the difference from the shell tool (#7):
|
|
201
|
+
* shell = arbitrary commands, stripped by default; delegate = a CONTROLLED
|
|
202
|
+
* spawn the human just approved in the ask tier, so the provider
|
|
203
|
+
* credentials the parent was trusted with ride along.
|
|
204
|
+
*/
|
|
205
|
+
function runProcess(childId, bin, cwd, policyDir, task, timeout, signal) {
|
|
206
|
+
const depth = Number.parseInt(process.env.KISO_SUBAGENT_DEPTH ?? "0", 10) || 0;
|
|
207
|
+
const child = spawn(process.execPath, [bin, "chat", childId], {
|
|
208
|
+
cwd,
|
|
209
|
+
env: {
|
|
210
|
+
...process.env,
|
|
211
|
+
KISO_SUBAGENT_DEPTH: String(depth + 1),
|
|
212
|
+
KISO_EXTENSIONS_DIR: policyDir,
|
|
213
|
+
// Modes: a headless child has no human — the mode tiers'
|
|
214
|
+
// ask would stall it. Bypass is the neutral tier here; the
|
|
215
|
+
// role policy dir (allow/deny only — a child must never
|
|
216
|
+
// see an ask) stays the child's ONLY gate, exactly as
|
|
217
|
+
// before the mode tiers existed (deny>ask>allow honors its
|
|
218
|
+
// denials; the mode's all-allow never overrides them).
|
|
219
|
+
KISO_MODE: "bypass",
|
|
220
|
+
},
|
|
221
|
+
detached: true,
|
|
222
|
+
stdio: ["pipe", "pipe", "inherit"],
|
|
223
|
+
});
|
|
224
|
+
child.stdin.write(`${task}\nexit\n`);
|
|
225
|
+
child.stdin.end();
|
|
226
|
+
let stdout = "";
|
|
227
|
+
child.stdout.on("data", (d) => {
|
|
228
|
+
stdout += String(d);
|
|
229
|
+
});
|
|
230
|
+
const exited = new Promise((resolve) => {
|
|
231
|
+
child.on("exit", (code, sig) => resolve({ code: code ?? -1, signal: sig }));
|
|
232
|
+
});
|
|
233
|
+
let killed = null;
|
|
234
|
+
const killGroup = () => {
|
|
235
|
+
try {
|
|
236
|
+
process.kill(-child.pid, "SIGKILL");
|
|
237
|
+
} catch {
|
|
238
|
+
// already gone
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
const timer = setTimeout(() => {
|
|
242
|
+
killed = "timeout";
|
|
243
|
+
killGroup();
|
|
244
|
+
}, timeout);
|
|
245
|
+
const onAbort = () => {
|
|
246
|
+
killed = "abort";
|
|
247
|
+
killGroup();
|
|
248
|
+
};
|
|
249
|
+
if (signal?.aborted) onAbort();
|
|
250
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
251
|
+
// The timeout and abort listener live until the child EXITS — clearing
|
|
252
|
+
// them in a finally around the setup would disarm them before the exit.
|
|
253
|
+
return exited.then(({ code }) => {
|
|
254
|
+
clearTimeout(timer);
|
|
255
|
+
signal?.removeEventListener("abort", onAbort);
|
|
256
|
+
return { code, stdout, killed };
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** The role policy: read-only for explorer/reviewer, the full six for
|
|
261
|
+
* implementer/tester. Only allow/deny — NEVER ask (a headless child cannot
|
|
262
|
+
* answer an approval prompt; ask would deadlock). */
|
|
263
|
+
export function rolePolicyContent(role) {
|
|
264
|
+
const allowed = role === "implementer" || role === "tester" ? SIX_TOOLS : READ_ONLY;
|
|
265
|
+
return `export default { name: "subagent-${role}", approvals: [{
|
|
266
|
+
decide(call) {
|
|
267
|
+
if (${JSON.stringify(allowed)}.includes(call.name)) return { action: "allow" };
|
|
268
|
+
return { action: "deny", reason: "not allowed for the ${role} role" };
|
|
269
|
+
}
|
|
270
|
+
}] };
|
|
271
|
+
`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* The RESULT source (hard clause): the child's own session JSONL — its
|
|
276
|
+
* terminal outcome, final assistant text (a projection-equivalent parse:
|
|
277
|
+
* the text_delta events since the last message boundary), and its tool
|
|
278
|
+
* call count. stdout is NEVER a result source — it rides along only as a
|
|
279
|
+
* diagnostic on a non-zero exit or a missing JSONL.
|
|
280
|
+
*/
|
|
281
|
+
export async function extractChildResult(sessionsDir, childId, diag) {
|
|
282
|
+
const file = join(sessionsDir, `${childId}.jsonl`);
|
|
283
|
+
// The child's exit event can land a beat before its final JSONL write
|
|
284
|
+
// (the terminal line) is visible — retry briefly before giving up.
|
|
285
|
+
let events = null;
|
|
286
|
+
let lastErr = null;
|
|
287
|
+
for (let attempt = 0; attempt < 10 && (events === null || !events.some((e) => e.type === "terminal")); attempt++) {
|
|
288
|
+
try {
|
|
289
|
+
// The store's JSONL records are {runId, ts, event} wrappers —
|
|
290
|
+
// unwrap; bare events (fixtures) pass through.
|
|
291
|
+
events = readFileSync(file, "utf8")
|
|
292
|
+
.trim()
|
|
293
|
+
.split("\n")
|
|
294
|
+
.filter((l) => l !== "")
|
|
295
|
+
.map((l) => JSON.parse(l))
|
|
296
|
+
.map((r) => r.event ?? r);
|
|
297
|
+
} catch (err) {
|
|
298
|
+
lastErr = err;
|
|
299
|
+
events = null;
|
|
300
|
+
}
|
|
301
|
+
if (events === null || !events.some((e) => e.type === "terminal")) await new Promise((r) => setTimeout(r, 200));
|
|
302
|
+
}
|
|
303
|
+
if (events === null) {
|
|
304
|
+
return { outcome: "missing", toolCalls: 0, text: "", failed: true, reason: `child session JSONL missing: ${msg(lastErr)}`, diag };
|
|
305
|
+
}
|
|
306
|
+
const terminal = events.find((e) => e.type === "terminal");
|
|
307
|
+
if (terminal === undefined) {
|
|
308
|
+
return { outcome: "no-terminal", toolCalls: countToolCalls(events), text: finalText(events), failed: true, reason: "child session has no terminal", diag };
|
|
309
|
+
}
|
|
310
|
+
const outcome = terminal.outcome?.kind ?? "unknown";
|
|
311
|
+
const toolCalls = countToolCalls(events);
|
|
312
|
+
const text = finalText(events);
|
|
313
|
+
return {
|
|
314
|
+
outcome,
|
|
315
|
+
toolCalls,
|
|
316
|
+
text,
|
|
317
|
+
failed: outcome !== "completed",
|
|
318
|
+
reason: outcome === "completed" ? "" : `child ended with ${outcome}`,
|
|
319
|
+
diag: outcome === "completed" ? "" : diag,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function countToolCalls(events) {
|
|
324
|
+
return events.filter((e) => e.type === "tool_call_end").length;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** Projection-equivalent: the assistant text since the last flush boundary. */
|
|
328
|
+
function finalText(events) {
|
|
329
|
+
let text = "";
|
|
330
|
+
for (const e of events) {
|
|
331
|
+
if (e.type === "text_delta") text += e.text;
|
|
332
|
+
else if (e.type === "tool_result" || e.type === "user_input") text = "";
|
|
333
|
+
}
|
|
334
|
+
return text;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** The implementer's changes: git diff (with its --stat header) over the
|
|
338
|
+
* worktree — intent-to-add first so NEW files are part of the diff, not
|
|
339
|
+
* silently invisible to `git diff`. null = no changes. */
|
|
340
|
+
function worktreeDiff(worktree) {
|
|
341
|
+
try {
|
|
342
|
+
execFileSync("git", ["-C", worktree, "add", "-N", "."], { stdio: "ignore" });
|
|
343
|
+
const stat = execFileSync("git", ["-C", worktree, "diff", "--stat"], { encoding: "utf8" }).trim();
|
|
344
|
+
const diff = execFileSync("git", ["-C", worktree, "diff"], { encoding: "utf8" });
|
|
345
|
+
if (stat === "" && diff.trim() === "") return null;
|
|
346
|
+
return `${stat}\n${diff}`;
|
|
347
|
+
} catch {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function failSection(childId, role, task, reason) {
|
|
353
|
+
return { failed: true, text: `[subagent] ${role}: ${task}\n FAILED: ${reason}`, toolCalls: 0 };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const msg = (err) => (err instanceof Error ? err.message : String(err));
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The published type surface of @vincemakes/kiso-subagent-ext: the
|
|
3
|
+
* default export is the FACTORY (the same contract the user-layer disk
|
|
4
|
+
* loader accepts — a KisoExtension or a factory returning one). The type
|
|
5
|
+
* import from kiso-core is compile-time only — the shipped bundle is
|
|
6
|
+
* self-contained, zero runtime dependencies.
|
|
7
|
+
*/
|
|
8
|
+
import type { KisoExtension } from "@vincemakes/kiso-core";
|
|
9
|
+
|
|
10
|
+
declare const createSubagentExtension: () => KisoExtension | Promise<KisoExtension>;
|
|
11
|
+
export default createSubagentExtension;
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vincemakes/kiso-subagent-ext",
|
|
3
|
+
"version": "0.1.45",
|
|
4
|
+
"description": "kiso official subagent extension \u2014 child kiso processes with role policies, kernel untouched",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "./dist/kiso-subagent.mjs",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./index.d.ts",
|
|
11
|
+
"import": "./dist/kiso-subagent.mjs"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"index.d.ts",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "node build.mjs",
|
|
22
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
23
|
+
"test": "vitest run"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@vincemakes/kiso-core": "0.1.35",
|
|
27
|
+
"@types/node": "^26.1.2",
|
|
28
|
+
"typescript": "^5.7.2",
|
|
29
|
+
"vitest": "^3.0.0"
|
|
30
|
+
}
|
|
31
|
+
}
|