@vincemakes/kiso-subagent-ext 0.45.3 → 0.46.1
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 +22 -2
- package/dist/kiso-subagent.mjs +318 -103
- package/index.d.ts +24 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -13,8 +13,28 @@ user-layer loader accepts exactly this shape.
|
|
|
13
13
|
|
|
14
14
|
## Configuration
|
|
15
15
|
|
|
16
|
-
None. Every delegation is asked of the human (no auto-allow);
|
|
17
|
-
guarded so children can never nest.
|
|
16
|
+
None for a user. Every delegation is asked of the human (no auto-allow);
|
|
17
|
+
depth is guarded so children can never nest.
|
|
18
|
+
|
|
19
|
+
## Background children (ADR-0058)
|
|
20
|
+
|
|
21
|
+
A host that passes its per-session task manager gets
|
|
22
|
+
`delegate({ …, background: true })`:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
createSubagentExtension({
|
|
26
|
+
tasks: (sessionId) => managerFor(sessionId), // the runtime's TaskManager
|
|
27
|
+
backgroundMax: 20, // live background children per session (default)
|
|
28
|
+
backgroundMaxTurns: 32, // a child's model requests before its wrap-up (default)
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Explorer and reviewer only. Each child is an agent task under the task
|
|
33
|
+
runner — it outlives the parent — and the call returns at once; the
|
|
34
|
+
runtime's task delivery tells the model when the turn's children have all
|
|
35
|
+
ended, with each child's answer (`result.md` beside its task). Without
|
|
36
|
+
`tasks`, the schema and behaviour are the foreground's, byte for byte. The
|
|
37
|
+
CLI wires it.
|
|
18
38
|
|
|
19
39
|
## Versioning
|
|
20
40
|
|
package/dist/kiso-subagent.mjs
CHANGED
|
@@ -16,23 +16,41 @@
|
|
|
16
16
|
* finding #8: this extension holds NO persistent resources — children are
|
|
17
17
|
* spawned per call and exit on their own, the role-policy temp dirs are
|
|
18
18
|
* cleaned in runChild's finally — so NO dispose is needed, explicitly.
|
|
19
|
+
*
|
|
20
|
+
* ADR-0058 3d — background children: a host that passes its task manager
|
|
21
|
+
* (`tasks`) gets `delegate({ …, background: true })`. Each child is then an
|
|
22
|
+
* agent TASK — the same child kiso, launched by argv under the task runner,
|
|
23
|
+
* so it outlives this process — and the call returns at once; the host's
|
|
24
|
+
* delivery tells the model when the children have ended. Explorer and
|
|
25
|
+
* reviewer only (D1), the whole batch within the session's cap or nothing
|
|
26
|
+
* (D4), no timeoutMs (D5), and a turn budget instead of a deadline (D6).
|
|
27
|
+
* Without `tasks` the schema and behaviour are exactly the foreground's.
|
|
19
28
|
*/
|
|
20
29
|
|
|
21
30
|
import { spawn, execFileSync } from "node:child_process";
|
|
22
31
|
import { createHash, randomBytes } from "node:crypto";
|
|
23
|
-
import { closeSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
32
|
+
import { closeSync, existsSync, fsyncSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
24
33
|
import { homedir, tmpdir } from "node:os";
|
|
25
|
-
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
34
|
+
import { dirname, isAbsolute, join, relative, resolve, sep, win32 } from "node:path";
|
|
26
35
|
import { fileURLToPath } from "node:url";
|
|
27
36
|
|
|
28
37
|
/** Default per-child timeout (ms) — a subagent must never hang the parent. */
|
|
29
38
|
const TIMEOUT_MS = 10 * 60 * 1000;
|
|
30
39
|
/** Max simultaneous children — the concurrency cap (mapLimited). */
|
|
31
40
|
const CONCURRENCY = 4;
|
|
41
|
+
/** ADR-0058 3d (D4): live background children per session, by default. */
|
|
42
|
+
const BACKGROUND_MAX = 20;
|
|
43
|
+
/** ADR-0058 3d (D6): a background child's model requests, by default — about
|
|
44
|
+
* twice the longest real explorer measured (17); 3f tunes it. */
|
|
45
|
+
const BACKGROUND_MAX_TURNS = 32;
|
|
46
|
+
/** ADR-0058 3d (D1): the roles whose unattended contract is defined. */
|
|
47
|
+
const BACKGROUND_ROLES = ["explorer", "reviewer"];
|
|
32
48
|
|
|
33
49
|
const SIX_TOOLS = ["read_file", "list_dir", "search_text", "write_file", "edit_file", "shell"];
|
|
34
50
|
const READ_ONLY = ["read_file", "list_dir", "search_text"];
|
|
35
|
-
|
|
51
|
+
// verifier: runs checks and reports evidence; its changes are never
|
|
52
|
+
// collected (ADR-0032 Amendment 1 — it was called tester until 0.46.0)
|
|
53
|
+
const ROLES = ["explorer", "implementer", "reviewer", "verifier"];
|
|
36
54
|
|
|
37
55
|
// DT1a-F2 (owner dogfood 2026-09-08): the model's first delegate call was
|
|
38
56
|
// refused twice — `scope` on an explorer, then an `acceptance` naming no
|
|
@@ -41,7 +59,7 @@ const ROLES = ["explorer", "implementer", "reviewer", "tester"];
|
|
|
41
59
|
// check and profile names are written into the schema when the extension
|
|
42
60
|
// loads (the tool table is snapshotted per request — F7b — so this is the
|
|
43
61
|
// table the model reads).
|
|
44
|
-
const delegateParameters = (cfg) => ({
|
|
62
|
+
const delegateParameters = (cfg, background) => ({
|
|
45
63
|
type: "object",
|
|
46
64
|
properties: {
|
|
47
65
|
tasks: {
|
|
@@ -58,46 +76,67 @@ const delegateParameters = (cfg) => ({
|
|
|
58
76
|
// acceptance names a configured check ({ check }) or a parent-held
|
|
59
77
|
// evaluator ({ evaluator: absolute path outside the project }) —
|
|
60
78
|
// never a command; model names a configured profile; after names
|
|
61
|
-
// a completed implementer's childId (
|
|
62
|
-
// in that worktree.
|
|
79
|
+
// a completed implementer's childId (verifier only): the verifier
|
|
80
|
+
// runs in a copy of that worktree.
|
|
63
81
|
scope: {
|
|
64
82
|
type: "array",
|
|
65
83
|
items: { type: "string", minLength: 1 },
|
|
66
84
|
maxItems: 32,
|
|
67
|
-
description: "implementer and
|
|
85
|
+
description: "implementer and verifier tasks ONLY (an explorer or reviewer task with scope is refused): path globs the child may write; a scoped child has no shell and a write outside the scope is refused",
|
|
68
86
|
},
|
|
69
87
|
acceptance: {
|
|
70
88
|
type: "object",
|
|
71
89
|
properties: { check: { type: "string", minLength: 1 }, evaluator: { type: "string", minLength: 1 } },
|
|
72
90
|
additionalProperties: false,
|
|
73
|
-
description: `optional; implementer and
|
|
91
|
+
description: `optional; implementer and verifier only. Exactly one of: { check } naming a check configured by the user (configured now: ${Object.keys(cfg.checks).length ? Object.keys(cfg.checks).join(", ") : "none configured — omit acceptance"}), or { evaluator } — an absolute path to an evaluator script OUTSIDE the project. Never a command: the parent runs the acceptance after the child completes`,
|
|
74
92
|
},
|
|
75
93
|
model: { type: "string", minLength: 1, description: `a model profile configured by the user (configured now: ${cfg.profiles.length ? cfg.profiles.map((x) => (typeof x === "string" ? x : x.name ?? x.id ?? JSON.stringify(x))).join(", ") : "none — omit model"})` },
|
|
76
|
-
after: { type: "string", minLength: 1, description: "
|
|
94
|
+
after: { type: "string", minLength: 1, description: "verifier only: the earlier task (by its index, 1-based) whose worktree this verifier runs in a copy of" },
|
|
77
95
|
timeoutMs: { type: "integer", minimum: 1000, description: "the child's wall-clock budget in milliseconds" },
|
|
78
96
|
},
|
|
79
97
|
required: ["role", "task"],
|
|
80
98
|
additionalProperties: false,
|
|
81
99
|
},
|
|
82
100
|
},
|
|
101
|
+
// ADR-0058 3d: present only when the host wired tasks — without them
|
|
102
|
+
// the schema is byte-for-byte the foreground one
|
|
103
|
+
...(background
|
|
104
|
+
? {
|
|
105
|
+
background: {
|
|
106
|
+
type: "boolean",
|
|
107
|
+
description:
|
|
108
|
+
"explorer and reviewer only: start the tasks in the background and return at once; you are told when all of them have ended. They read the workspace as it is while they run; task_stop stops one",
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
: {}),
|
|
83
112
|
},
|
|
84
113
|
required: ["tasks"],
|
|
85
114
|
additionalProperties: false,
|
|
86
115
|
});
|
|
87
116
|
|
|
88
|
-
|
|
117
|
+
/**
|
|
118
|
+
* @param {{ tasks?: (sessionId: string | undefined) => any, backgroundMax?: number, backgroundMaxTurns?: number }} [host]
|
|
119
|
+
* ADR-0058 3d: the host's per-session task manager (the runtime's
|
|
120
|
+
* TaskManager satisfies it structurally — no dependency), the cap on live
|
|
121
|
+
* background children per session, and a background child's turn budget.
|
|
122
|
+
*/
|
|
123
|
+
export default async function createSubagentExtension(host = {}) {
|
|
89
124
|
const depth = Number.parseInt(process.env.KISO_SUBAGENT_DEPTH ?? "0", 10) || 0;
|
|
90
125
|
if (depth >= 1) return { name: "subagent", tools: [] }; // depth guard — no nesting
|
|
126
|
+
const tasksOf = typeof host.tasks === "function" ? host.tasks : undefined;
|
|
127
|
+
const background = tasksOf === undefined ? undefined : { tasksOf, max: host.backgroundMax ?? BACKGROUND_MAX, maxTurns: host.backgroundMaxTurns ?? BACKGROUND_MAX_TURNS, reserved: new Map() };
|
|
91
128
|
return {
|
|
92
129
|
name: "subagent",
|
|
93
130
|
tools: [
|
|
94
131
|
{
|
|
95
132
|
name: "delegate",
|
|
96
|
-
description: "run subagent tasks (explorer/implementer/reviewer/
|
|
97
|
-
parameters: delegateParameters(delegationConfig()),
|
|
133
|
+
description: "run subagent tasks (explorer/implementer/reviewer/verifier) in child kiso processes",
|
|
134
|
+
parameters: delegateParameters(delegationConfig(), background !== undefined),
|
|
98
135
|
execute: async (input, ctx) => {
|
|
99
136
|
const tasks = ((input ?? {}).tasks ?? []).slice(0, 8);
|
|
100
137
|
if (tasks.length === 0) return { content: "delegate: no tasks", isError: true, errorKind: "precondition" };
|
|
138
|
+
const inBackground = (input ?? {}).background === true;
|
|
139
|
+
if (inBackground && background === undefined) return { content: "delegate: background is not available here — run the delegation in the foreground", isError: true, errorKind: "precondition" };
|
|
101
140
|
// 0.40.0: the parent's own folder (one per project), handed over
|
|
102
141
|
// by the CLI; a pinned KISO_SESSIONS_DIR, or the legacy folder,
|
|
103
142
|
// when no CLI said (a direct load)
|
|
@@ -130,6 +169,7 @@ export default async function createSubagentExtension() {
|
|
|
130
169
|
// applied" banner (loop.ts keys that banner on errorKind !== "precondition")
|
|
131
170
|
if (why !== null) return { content: `delegate: refused — task ${i + 1}: ${why}`, isError: true, errorKind: "precondition" };
|
|
132
171
|
}
|
|
172
|
+
if (inBackground) return startBackground({ tasks, ctx, background, sessionsDir, parentId, bin, delegationId, manifestDir });
|
|
133
173
|
const sections = await runLimited(tasks, CONCURRENCY, (task, i) =>
|
|
134
174
|
runChild({
|
|
135
175
|
childId: `sub-${parentId}-${delegationId}-${i + 1}-${task.role}`,
|
|
@@ -163,6 +203,104 @@ export default async function createSubagentExtension() {
|
|
|
163
203
|
};
|
|
164
204
|
}
|
|
165
205
|
|
|
206
|
+
const refuse = (why) => ({ content: `delegate: refused — ${why}`, isError: true, errorKind: "precondition" });
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* ADR-0058 3d — start the children as agent tasks and return at once.
|
|
210
|
+
*
|
|
211
|
+
* Everything up to the reservation is synchronous: the check that the
|
|
212
|
+
* whole batch fits (live agent tasks + slots reserved by calls still
|
|
213
|
+
* starting + this batch ≤ max) and the reservation are one step, so two
|
|
214
|
+
* calls can never both fit into the last slots (D4). Each child's inputs —
|
|
215
|
+
* its task file, role policy and manifest — are written and fsynced before
|
|
216
|
+
* its task is planned (the runner and the child need nothing a `finally`
|
|
217
|
+
* here could remove). The child is the same invocation a foreground child
|
|
218
|
+
* runs, launched by argv (no shell), with its turn budget and its result
|
|
219
|
+
* file beside its task (D6).
|
|
220
|
+
*/
|
|
221
|
+
async function startBackground({ tasks, ctx, background, sessionsDir, parentId, bin, delegationId, manifestDir }) {
|
|
222
|
+
for (const task of tasks) {
|
|
223
|
+
if (!BACKGROUND_ROLES.includes(task.role)) return refuse(`background is not supported for the ${task.role} role in this release; run this delegation in the foreground`);
|
|
224
|
+
if (task.timeoutMs !== undefined) return refuse("timeoutMs applies to a foreground delegation; a background child has no wall-clock kill — task_stop stops one");
|
|
225
|
+
}
|
|
226
|
+
const manager = background.tasksOf(ctx.sessionId);
|
|
227
|
+
if (manager === undefined) return refuse("background is not available here — run the delegation in the foreground");
|
|
228
|
+
const key = ctx.sessionId ?? "";
|
|
229
|
+
const live = manager.list().filter((t) => t.agent !== undefined && t.state.kind !== "ended" && t.state.kind !== "not_run").length + (background.reserved.get(key) ?? 0);
|
|
230
|
+
if (live + tasks.length > background.max) {
|
|
231
|
+
return refuse(`${live} background children are running in this session and the cap is ${background.max}; this call asks for ${tasks.length}. Do not retry — wait for some to end, or stop one with task_stop`);
|
|
232
|
+
}
|
|
233
|
+
background.reserved.set(key, (background.reserved.get(key) ?? 0) + tasks.length);
|
|
234
|
+
let unstarted = tasks.length;
|
|
235
|
+
const release = (n) => background.reserved.set(key, Math.max(0, (background.reserved.get(key) ?? 0) - n));
|
|
236
|
+
const lines = [];
|
|
237
|
+
let failures = 0;
|
|
238
|
+
try {
|
|
239
|
+
for (let i = 0; i < tasks.length; i += 1) {
|
|
240
|
+
const task = tasks[i];
|
|
241
|
+
const childId = `sub-${parentId}-${delegationId}-${i + 1}-${task.role}`;
|
|
242
|
+
try {
|
|
243
|
+
const inputs = writeChildInputs(manifestDir, childId, task, { parentId, delegationId, index: i + 1, role: task.role, startedAt: Date.now() });
|
|
244
|
+
const started = await manager.start({
|
|
245
|
+
command: `${task.role}: ${task.task.split("\n")[0].slice(0, 80)}`,
|
|
246
|
+
cwd: process.cwd(),
|
|
247
|
+
env: childEnv(inputs.policyDir, sessionsDir),
|
|
248
|
+
...(ctx.executionId !== undefined ? { executionId: ctx.executionId } : {}),
|
|
249
|
+
agent: { role: task.role, session: childId },
|
|
250
|
+
exec: (dir) => ({ file: process.execPath, args: [...childArgs(bin, childId, inputs.taskPath, task.model), "--max-turns", String(background.maxTurns), "--result-file", join(dir, "result.md")] }),
|
|
251
|
+
});
|
|
252
|
+
lines.push(`${started.id} ${task.role} (session ${childId})`);
|
|
253
|
+
} catch (err) {
|
|
254
|
+
failures += 1;
|
|
255
|
+
lines.push(`task ${i + 1} (${task.role}) did not start: ${msg(err)}`);
|
|
256
|
+
}
|
|
257
|
+
unstarted -= 1;
|
|
258
|
+
release(1);
|
|
259
|
+
}
|
|
260
|
+
} finally {
|
|
261
|
+
release(unstarted);
|
|
262
|
+
}
|
|
263
|
+
const n = tasks.length - failures;
|
|
264
|
+
const content =
|
|
265
|
+
`started ${n} background ${n === 1 ? "child" : "children"}: ${lines.join(", ")}. ` +
|
|
266
|
+
"They read the workspace as it is while they run. You will be told when all of them have ended; task_stop stops one.";
|
|
267
|
+
return { content, isError: n === 0 };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** The durable inputs of a background child: the task file (the fixed
|
|
271
|
+
* UNRESOLVED trailer included), its role policy, its manifest — fsynced
|
|
272
|
+
* with their directories before the task is planned. */
|
|
273
|
+
function writeChildInputs(manifestDir, childId, task, manifest) {
|
|
274
|
+
const taskPath = join(manifestDir, `${childId}.task`);
|
|
275
|
+
const policyDir = join(manifestDir, `${childId}.policy`);
|
|
276
|
+
mkdirSync(policyDir, { recursive: true });
|
|
277
|
+
writeDurable(join(policyDir, "policy.mjs"), rolePolicyContent(task.role, undefined));
|
|
278
|
+
writeDurable(taskPath, `${task.task}\n\n${UNRESOLVED_INSTRUCTION}\n`);
|
|
279
|
+
writeDurable(join(manifestDir, `${childId}.json`), `${JSON.stringify({ ...manifest, childId, task: task.task, background: true, ...(task.model !== undefined ? { model: task.model } : {}) })}\n`);
|
|
280
|
+
fsyncPath(policyDir);
|
|
281
|
+
fsyncPath(manifestDir);
|
|
282
|
+
return { taskPath, policyDir };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function writeDurable(path, content) {
|
|
286
|
+
const fd = openSync(path, "w");
|
|
287
|
+
try {
|
|
288
|
+
writeSync(fd, content);
|
|
289
|
+
fsyncSync(fd);
|
|
290
|
+
} finally {
|
|
291
|
+
closeSync(fd);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function fsyncPath(dir) {
|
|
296
|
+
const fd = openSync(dir, "r");
|
|
297
|
+
try {
|
|
298
|
+
fsyncSync(fd);
|
|
299
|
+
} finally {
|
|
300
|
+
closeSync(fd);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
166
304
|
/**
|
|
167
305
|
* The parent session id for the child naming: the explicit
|
|
168
306
|
* KISO_SESSION_ID wins; otherwise the NEWEST *.jsonl in the sessions dir
|
|
@@ -213,20 +351,34 @@ async function runChild({ childId, role, task, scope, acceptance, model, after,
|
|
|
213
351
|
}
|
|
214
352
|
// Isolation (DT-1a R2.2): implementer → its own detached worktree from
|
|
215
353
|
// the parent's HEAD (the parent's UNCOMMITTED changes are not visible —
|
|
216
|
-
// the section says so);
|
|
217
|
-
// `after` names one
|
|
218
|
-
//
|
|
219
|
-
//
|
|
354
|
+
// the section says so); verifier → a COPY of the implementer's kept
|
|
355
|
+
// worktree when `after` names one (a fresh worktree at the same base
|
|
356
|
+
// with the implementer's changes applied — the kept worktree the
|
|
357
|
+
// parent was handed is never touched), else a fresh worktree from HEAD;
|
|
358
|
+
// explorer and reviewer → the parent's tree under a read-only policy.
|
|
359
|
+
// Non-git parents fail the task HONESTLY.
|
|
220
360
|
let worktree = null;
|
|
221
361
|
let baseRev = null;
|
|
222
362
|
let childCwd = parentCwd;
|
|
223
363
|
let ownsWorktree = false;
|
|
224
|
-
if (role === "
|
|
364
|
+
if (role === "verifier" && after !== undefined) {
|
|
225
365
|
const prior = readResultFile(manifestDir, after);
|
|
226
|
-
worktree = prior.worktree;
|
|
227
366
|
baseRev = prior.baseRev;
|
|
228
|
-
|
|
229
|
-
|
|
367
|
+
worktree = mkdtempSync(join(tmpdir(), "kiso-subagent-wt-"));
|
|
368
|
+
ownsWorktree = true;
|
|
369
|
+
try {
|
|
370
|
+
execFileSync("git", ["-C", parentCwd, "worktree", "add", "--detach", worktree, baseRev ?? "HEAD"], { stdio: "ignore" });
|
|
371
|
+
// the implementer's state against its base — committed or not,
|
|
372
|
+
// new files included (intent-to-add), binary-safe
|
|
373
|
+
execFileSync("git", ["-C", prior.worktree, "add", "-N", "."], { stdio: "ignore" });
|
|
374
|
+
const patch = execFileSync("git", ["-C", prior.worktree, "diff", "--binary", baseRev ?? "HEAD"], { maxBuffer: 256 * 1024 * 1024 });
|
|
375
|
+
if (patch.length > 0) execFileSync("git", ["-C", worktree, "apply", "--binary", "--whitespace=nowarn"], { input: patch, stdio: ["pipe", "ignore", "pipe"] });
|
|
376
|
+
childCwd = worktree;
|
|
377
|
+
} catch (err) {
|
|
378
|
+
removeWorktree(parentCwd, worktree);
|
|
379
|
+
return failSection(childId, role, task, `the verifier could not copy the implementer's worktree: ${msg(err)}`);
|
|
380
|
+
}
|
|
381
|
+
} else if (role === "implementer" || role === "verifier") {
|
|
230
382
|
worktree = mkdtempSync(join(tmpdir(), "kiso-subagent-wt-"));
|
|
231
383
|
ownsWorktree = true;
|
|
232
384
|
try {
|
|
@@ -252,28 +404,34 @@ async function runChild({ childId, role, task, scope, acceptance, model, after,
|
|
|
252
404
|
// the fixed UNRESOLVED instruction the result parser reads back.
|
|
253
405
|
const taskPath = join(manifestDir ?? policyDir, `${childId}.task`);
|
|
254
406
|
writeFileSync(taskPath, `${task}\n\n${UNRESOLVED_INSTRUCTION}\n`, "utf8");
|
|
255
|
-
const { code, stdout, killed } = await runProcess(childId, bin, childCwd, policyDir, taskPath, timeout, signal, model, sessionsDir);
|
|
407
|
+
const { code, stdout, killed, unconfirmed } = await runProcess(childId, bin, childCwd, policyDir, taskPath, timeout, signal, model, sessionsDir);
|
|
256
408
|
const extraction = await extractChildResult(sessionsDir, childId, `exit ${code}\n${stdout}`);
|
|
257
409
|
const status = killed === "timeout" ? "timeout" : killed === "abort" ? "killed" : code !== 0 && extraction.outcome === "missing" ? "spawn-failed" : extraction.outcome;
|
|
258
410
|
let failed = code !== 0 || killed !== null || extraction.failed;
|
|
259
411
|
const lines = [];
|
|
412
|
+
// Windows P6: a kill that did not take is never reported as a kill
|
|
413
|
+
const killNote = unconfirmed.length > 0 ? ` — could not confirm the child exited (pid ${unconfirmed.join(", ")}): treat its side effects as UNCERTAIN` : " (the child process group was killed)";
|
|
260
414
|
if (killed === "timeout") {
|
|
261
|
-
lines.push(` FAILED: timed out after ${timeout}ms
|
|
415
|
+
lines.push(` FAILED: timed out after ${timeout}ms${killNote}`);
|
|
262
416
|
} else if (killed === "abort") {
|
|
263
|
-
lines.push(
|
|
417
|
+
lines.push(` FAILED: aborted by the parent run${killNote}`);
|
|
264
418
|
} else if (code !== 0) {
|
|
265
419
|
lines.push(` FAILED: the child exited with code ${code}\n${stdout}`);
|
|
266
420
|
} else if (extraction.failed) {
|
|
267
421
|
lines.push(` FAILED: ${extraction.reason}${extraction.diag !== "" ? `\n${extraction.diag}` : ""}`);
|
|
268
422
|
}
|
|
269
423
|
const unresolved = parseUnresolved(extraction.text);
|
|
270
|
-
// the collection (implementers only — a
|
|
271
|
-
//
|
|
424
|
+
// the collection (implementers only — a verifier's worktree is a
|
|
425
|
+
// throwaway copy; its changes are not its result)
|
|
272
426
|
let changedFiles = null;
|
|
273
427
|
let patchPath = null;
|
|
274
428
|
let patchBytes = null;
|
|
275
429
|
let collection = null;
|
|
276
|
-
|
|
430
|
+
// Windows P6: a child that may still be running keeps its worktree,
|
|
431
|
+
// and nothing is collected from it — a patch of a tree that may
|
|
432
|
+
// still change is not the child's result
|
|
433
|
+
if (unconfirmed.length > 0 && worktree !== null) keepWorktree = true;
|
|
434
|
+
if (role === "implementer" && unconfirmed.length === 0) {
|
|
277
435
|
// CX-1 F2 (audit F2): tri-state collection. `collected` is earned
|
|
278
436
|
// (the patch file closed AND git exited 0); a failure PRESERVES the
|
|
279
437
|
// worktree and says so; "consumed" is undefined, so a worktree
|
|
@@ -316,6 +474,7 @@ async function runChild({ childId, role, task, scope, acceptance, model, after,
|
|
|
316
474
|
patchPath,
|
|
317
475
|
patchBytes,
|
|
318
476
|
verification,
|
|
477
|
+
...(unconfirmed.length > 0 ? { unconfirmed } : {}),
|
|
319
478
|
unresolved,
|
|
320
479
|
answer: extraction.text,
|
|
321
480
|
usage: extraction.usage,
|
|
@@ -330,7 +489,8 @@ async function runChild({ childId, role, task, scope, acceptance, model, after,
|
|
|
330
489
|
if (scope !== undefined) text += `\n scoped: no shell · writes only under ${scope.join(", ")}`;
|
|
331
490
|
if (baseRev !== null) text += `\n child saw HEAD ${baseRev.slice(0, 7)}; the parent's uncommitted changes were not visible`;
|
|
332
491
|
if (verification !== null && verification.skipped === undefined) {
|
|
333
|
-
|
|
492
|
+
const exit = verification.unconfirmed !== undefined ? `killed, not confirmed (pid ${verification.unconfirmed.join(", ")} may still be running — treat its side effects as UNCERTAIN)` : verification.exitCode === null ? "killed" : verification.exitCode;
|
|
493
|
+
text += `\n verification: ${verification.kind} ${verification.passed ? "PASSED" : "FAILED"} · exit ${exit} · ${verification.durationMs}ms · ${verification.kind === "check" ? verification.command : verification.evaluator}`;
|
|
334
494
|
if (!verification.passed && verification.tail !== "") text += `\n${verification.tail}`;
|
|
335
495
|
}
|
|
336
496
|
for (const l of lines) text += `\n${l}`;
|
|
@@ -345,6 +505,8 @@ async function runChild({ childId, role, task, scope, acceptance, model, after,
|
|
|
345
505
|
} else if (collection.kind === "failed") {
|
|
346
506
|
text += `\n FAILED: collecting the worktree's changes: ${collection.reason}${collection.partialPath !== undefined ? ` (partial patch at ${collection.partialPath})` : ""}\n worktree kept at: ${worktree}`;
|
|
347
507
|
}
|
|
508
|
+
} else if (unconfirmed.length > 0 && worktree !== null) {
|
|
509
|
+
text += `\n worktree kept at: ${worktree} — the child may still be writing to it; nothing was collected`;
|
|
348
510
|
}
|
|
349
511
|
return { failed, ...(failed ? { failKind: failKindOf(status, verification) } : {}), text, toolCalls: extraction.toolCalls };
|
|
350
512
|
} finally {
|
|
@@ -416,11 +578,12 @@ function artifactDir(home, sessionsDir) {
|
|
|
416
578
|
/** DT-1a: every task's inputs are judged BEFORE any child runs; a string is the refusal. */
|
|
417
579
|
export function validateTask(task, cfg, parentCwd, manifestDir) {
|
|
418
580
|
if (task === null || typeof task !== "object") return "a task must be an object";
|
|
581
|
+
if (task.role === "tester") return 'unknown role "tester" — it is called "verifier" since 0.46.0';
|
|
419
582
|
if (!ROLES.includes(task.role)) return `unknown role ${JSON.stringify(task.role)}`;
|
|
420
583
|
if (typeof task.task !== "string" || task.task.trim() === "") return "task must be a non-empty string";
|
|
421
584
|
if (task.scope !== undefined) {
|
|
422
585
|
if (!Array.isArray(task.scope) || task.scope.length === 0 || task.scope.some((g) => typeof g !== "string" || g === "" || isAbsolute(g) || g.split("/").includes(".."))) return "scope must be a non-empty list of relative globs (no absolute paths, no ..)";
|
|
423
|
-
if (task.role !== "implementer" && task.role !== "
|
|
586
|
+
if (task.role !== "implementer" && task.role !== "verifier") return "scope applies to implementer and verifier tasks only";
|
|
424
587
|
}
|
|
425
588
|
if (task.acceptance !== undefined) {
|
|
426
589
|
const a = task.acceptance;
|
|
@@ -449,7 +612,7 @@ export function validateTask(task, cfg, parentCwd, manifestDir) {
|
|
|
449
612
|
}
|
|
450
613
|
if (task.model !== undefined && !cfg.profiles.includes(task.model)) return `refused: unknown model profile ${JSON.stringify(task.model)} (configured: ${cfg.profiles.join(", ") || "none"})`;
|
|
451
614
|
if (task.after !== undefined) {
|
|
452
|
-
if (task.role !== "
|
|
615
|
+
if (task.role !== "verifier") return "refused: `after` is for verifier tasks";
|
|
453
616
|
let prior;
|
|
454
617
|
try {
|
|
455
618
|
prior = readResultFile(manifestDir, task.after);
|
|
@@ -555,9 +718,95 @@ export function parseChangedFiles(numstat, nameStatus) {
|
|
|
555
718
|
const ACCEPTANCE_OUTPUT_CAP = 64 * 1024;
|
|
556
719
|
const ACCEPTANCE_TAIL = 2 * 1024;
|
|
557
720
|
|
|
721
|
+
/** A child in its own process group, so a timeout or an abort kills it
|
|
722
|
+
* whole; win32 has no groups — the child is hidden, and its tree dies by
|
|
723
|
+
* taskkill /T (which walks it by parent). */
|
|
724
|
+
const ownGroup = () => (process.platform === "win32" ? { windowsHide: true } : { detached: true });
|
|
725
|
+
|
|
726
|
+
function killGroup(child) {
|
|
727
|
+
try {
|
|
728
|
+
if (process.platform === "win32") execFileSync("taskkill", ["/T", "/F", "/PID", String(child.pid)], { stdio: "ignore", windowsHide: true });
|
|
729
|
+
else process.kill(-child.pid, "SIGKILL");
|
|
730
|
+
} catch {
|
|
731
|
+
// already gone, or the kill was refused: the bounded wait decides
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/** How long the wait after a kill lasts before the child's exit is
|
|
736
|
+
* declared unconfirmed — tools-node's killTree uses the same fallback. */
|
|
737
|
+
const KILL_CONFIRM_MS = 2_000;
|
|
738
|
+
|
|
739
|
+
/** The child's exit, supervised: the timeout and the parent's abort kill
|
|
740
|
+
* its group, and the wait after a kill is bounded (Windows P6). A kill
|
|
741
|
+
* that does not take — taskkill refused, a group SIGKILL refused with
|
|
742
|
+
* EPERM — leaves the child running; after KILL_CONFIRM_MS its pid comes
|
|
743
|
+
* back in `unconfirmed` (tools-node's contract: the caller must not
|
|
744
|
+
* report it gone) and the child is let go, so it never holds the parent. */
|
|
745
|
+
function superviseExit(child, timeout, signal) {
|
|
746
|
+
return new Promise((resolve) => {
|
|
747
|
+
let killed = null;
|
|
748
|
+
let done = false;
|
|
749
|
+
let confirm;
|
|
750
|
+
const settle = (code, unconfirmed) => {
|
|
751
|
+
if (done) return;
|
|
752
|
+
done = true;
|
|
753
|
+
clearTimeout(timer);
|
|
754
|
+
clearTimeout(confirm);
|
|
755
|
+
signal?.removeEventListener("abort", onAbort);
|
|
756
|
+
resolve({ code, killed, unconfirmed });
|
|
757
|
+
};
|
|
758
|
+
const kill = (why) => {
|
|
759
|
+
if (killed !== null) return;
|
|
760
|
+
killed = why;
|
|
761
|
+
killGroup(child);
|
|
762
|
+
if (done) return;
|
|
763
|
+
confirm = setTimeout(() => {
|
|
764
|
+
child.stdout?.destroy();
|
|
765
|
+
child.stderr?.destroy();
|
|
766
|
+
child.unref();
|
|
767
|
+
settle(null, [child.pid]);
|
|
768
|
+
}, KILL_CONFIRM_MS);
|
|
769
|
+
};
|
|
770
|
+
const timer = setTimeout(() => kill("timeout"), timeout);
|
|
771
|
+
const onAbort = () => kill("abort");
|
|
772
|
+
child.on("error", () => settle(null, []));
|
|
773
|
+
child.on("exit", (code) => settle(code, []));
|
|
774
|
+
if (signal?.aborted) onAbort();
|
|
775
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/** The bash a check runs through on win32 — this build's own copy of the
|
|
780
|
+
* process module's rule (tools-node `process.ts`; zero dependencies here),
|
|
781
|
+
* pinned to it by subagent-win32.test.ts. */
|
|
782
|
+
function resolveBash(env) {
|
|
783
|
+
const fail = (message) => {
|
|
784
|
+
throw Object.assign(new Error(message), { code: "KISO_NO_BASH" });
|
|
785
|
+
};
|
|
786
|
+
const override = env.KISO_BASH;
|
|
787
|
+
if (override !== undefined && override !== "") {
|
|
788
|
+
if (!/^bash(\.exe)?$/i.test(win32.basename(override))) {
|
|
789
|
+
fail(`KISO_BASH must name a bash executable (bash.exe), not ${win32.basename(override)}: kiso's shell commands are POSIX shell`);
|
|
790
|
+
}
|
|
791
|
+
if (!existsSync(override)) fail(`KISO_BASH is set to ${override}, which does not exist`);
|
|
792
|
+
return override;
|
|
793
|
+
}
|
|
794
|
+
for (const root of [env.ProgramFiles ?? "C:\\Program Files", env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)"]) {
|
|
795
|
+
const candidate = win32.join(root, "Git", "bin", "bash.exe");
|
|
796
|
+
if (existsSync(candidate)) return candidate;
|
|
797
|
+
}
|
|
798
|
+
const system32 = win32.join(env.SystemRoot ?? "C:\\Windows", "System32").toLowerCase();
|
|
799
|
+
for (const dir of (env.PATH ?? "").split(win32.delimiter)) {
|
|
800
|
+
if (dir === "" || win32.join(dir, ".").toLowerCase() === system32) continue;
|
|
801
|
+
const candidate = win32.join(dir, "bash.exe");
|
|
802
|
+
if (existsSync(candidate)) return candidate;
|
|
803
|
+
}
|
|
804
|
+
return fail("no bash found — kiso runs shell commands through Git Bash on Windows: install Git for Windows, or set KISO_BASH to the path of bash.exe");
|
|
805
|
+
}
|
|
806
|
+
|
|
558
807
|
/** DT-1a R2.3: the parent runs the acceptance in the worktree — a configured check
|
|
559
|
-
* through /bin/sh (user-authored), or the evaluator binary with the
|
|
560
|
-
* its argument. Own process group (the abort and the timeout kill it whole), the
|
|
808
|
+
* through /bin/sh (bash on win32; user-authored), or the evaluator binary with the
|
|
809
|
+
* worktree as its argument. Own process group (the abort and the timeout kill it whole), the
|
|
561
810
|
* output capped, the tail kept. The exit code proves the command RAN on the tree
|
|
562
811
|
* as the child left it (patchSha256 names that state); only an evaluator proves
|
|
563
812
|
* correctness — the child can edit a check's tests. */
|
|
@@ -573,37 +822,23 @@ export async function runAcceptance(acceptance, cfg, worktree, baseRev, timeout,
|
|
|
573
822
|
} catch {
|
|
574
823
|
// a non-git worktree: the state hash stays the empty one
|
|
575
824
|
}
|
|
576
|
-
|
|
825
|
+
let child;
|
|
826
|
+
try {
|
|
827
|
+
const options = { cwd: worktree, ...ownGroup(), stdio: ["ignore", "pipe", "pipe"] };
|
|
828
|
+
child = kind === "check" ? spawn(process.platform === "win32" ? resolveBash(process.env) : "/bin/sh", ["-c", command], options) : spawn(command, [worktree], options);
|
|
829
|
+
} catch (err) {
|
|
830
|
+
// win32 with no usable bash: the check fails with a message the model can act on
|
|
831
|
+
if (err?.code !== "KISO_NO_BASH") throw err;
|
|
832
|
+
const name = kind === "check" ? { name: acceptance.check, command } : { evaluator: command };
|
|
833
|
+
return { kind, ...name, exitCode: null, passed: false, tail: err.message, durationMs: Date.now() - started, patchSha256, baseRev };
|
|
834
|
+
}
|
|
577
835
|
let output = "";
|
|
578
836
|
const capture = (d) => {
|
|
579
837
|
if (output.length < ACCEPTANCE_OUTPUT_CAP) output += String(d).slice(0, ACCEPTANCE_OUTPUT_CAP - output.length);
|
|
580
838
|
};
|
|
581
839
|
child.stdout.on("data", capture);
|
|
582
840
|
child.stderr.on("data", capture);
|
|
583
|
-
|
|
584
|
-
const killGroup = () => {
|
|
585
|
-
try {
|
|
586
|
-
process.kill(-child.pid, "SIGKILL");
|
|
587
|
-
} catch {
|
|
588
|
-
// already gone
|
|
589
|
-
}
|
|
590
|
-
};
|
|
591
|
-
const timer = setTimeout(() => {
|
|
592
|
-
killed = "timeout";
|
|
593
|
-
killGroup();
|
|
594
|
-
}, timeout);
|
|
595
|
-
const onAbort = () => {
|
|
596
|
-
killed = "abort";
|
|
597
|
-
killGroup();
|
|
598
|
-
};
|
|
599
|
-
if (signal?.aborted) onAbort();
|
|
600
|
-
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
601
|
-
const exitCode = await new Promise((resolveExit) => {
|
|
602
|
-
child.on("error", () => resolveExit(null));
|
|
603
|
-
child.on("exit", (code) => resolveExit(code));
|
|
604
|
-
});
|
|
605
|
-
clearTimeout(timer);
|
|
606
|
-
signal?.removeEventListener("abort", onAbort);
|
|
841
|
+
const { code: exitCode, killed, unconfirmed } = await superviseExit(child, timeout, signal);
|
|
607
842
|
const code = killed !== null ? null : exitCode;
|
|
608
843
|
return {
|
|
609
844
|
kind,
|
|
@@ -611,6 +846,7 @@ export async function runAcceptance(acceptance, cfg, worktree, baseRev, timeout,
|
|
|
611
846
|
exitCode: code,
|
|
612
847
|
passed: code === 0,
|
|
613
848
|
...(killed !== null ? { killed } : {}),
|
|
849
|
+
...(unconfirmed.length > 0 ? { unconfirmed } : {}),
|
|
614
850
|
tail: output.slice(-ACCEPTANCE_TAIL),
|
|
615
851
|
durationMs: Date.now() - started,
|
|
616
852
|
patchSha256,
|
|
@@ -636,26 +872,32 @@ export function childArgs(bin, childId, taskPath, model) {
|
|
|
636
872
|
return [bin, ...(model !== undefined ? ["--model", model] : []), "chat", childId, "--task-file", taskPath];
|
|
637
873
|
}
|
|
638
874
|
|
|
639
|
-
|
|
875
|
+
/** A child's environment — the parent's, plus the depth guard, its role
|
|
876
|
+
* policy and the sessions folder (the foreground and background share it). */
|
|
877
|
+
function childEnv(policyDir, sessionsDir) {
|
|
640
878
|
const depth = Number.parseInt(process.env.KISO_SUBAGENT_DEPTH ?? "0", 10) || 0;
|
|
879
|
+
return {
|
|
880
|
+
...process.env,
|
|
881
|
+
KISO_SUBAGENT_DEPTH: String(depth + 1),
|
|
882
|
+
KISO_EXTENSIONS_DIR: policyDir,
|
|
883
|
+
// 0.40.0: the child writes its log beside its parent's — the
|
|
884
|
+
// parent reads it back from there — whatever cwd it runs in
|
|
885
|
+
KISO_SESSIONS_DIR: sessionsDir,
|
|
886
|
+
// Modes: a headless child has no human — the mode tiers'
|
|
887
|
+
// ask would stall it. Bypass is the neutral tier here; the
|
|
888
|
+
// role policy dir (allow/deny only — a child must never
|
|
889
|
+
// see an ask) stays the child's ONLY gate, exactly as
|
|
890
|
+
// before the mode tiers existed (deny>ask>allow honors its
|
|
891
|
+
// denials; the mode's all-allow never overrides them).
|
|
892
|
+
KISO_MODE: "bypass",
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function runProcess(childId, bin, cwd, policyDir, taskPath, timeout, signal, model, sessionsDir) {
|
|
641
897
|
const child = spawn(process.execPath, childArgs(bin, childId, taskPath, model), {
|
|
642
898
|
cwd,
|
|
643
|
-
env:
|
|
644
|
-
|
|
645
|
-
KISO_SUBAGENT_DEPTH: String(depth + 1),
|
|
646
|
-
KISO_EXTENSIONS_DIR: policyDir,
|
|
647
|
-
// 0.40.0: the child writes its log beside its parent's — the
|
|
648
|
-
// parent reads it back from there — whatever cwd it runs in
|
|
649
|
-
KISO_SESSIONS_DIR: sessionsDir,
|
|
650
|
-
// Modes: a headless child has no human — the mode tiers'
|
|
651
|
-
// ask would stall it. Bypass is the neutral tier here; the
|
|
652
|
-
// role policy dir (allow/deny only — a child must never
|
|
653
|
-
// see an ask) stays the child's ONLY gate, exactly as
|
|
654
|
-
// before the mode tiers existed (deny>ask>allow honors its
|
|
655
|
-
// denials; the mode's all-allow never overrides them).
|
|
656
|
-
KISO_MODE: "bypass",
|
|
657
|
-
},
|
|
658
|
-
detached: true,
|
|
899
|
+
env: childEnv(policyDir, sessionsDir),
|
|
900
|
+
...ownGroup(),
|
|
659
901
|
stdio: ["pipe", "pipe", "inherit"],
|
|
660
902
|
});
|
|
661
903
|
child.stdin.end(); // CX-1 F5: nothing rides stdin — the task is the file
|
|
@@ -663,41 +905,14 @@ function runProcess(childId, bin, cwd, policyDir, taskPath, timeout, signal, mod
|
|
|
663
905
|
child.stdout.on("data", (d) => {
|
|
664
906
|
stdout += String(d);
|
|
665
907
|
});
|
|
666
|
-
|
|
667
|
-
child.on("exit", (code, sig) => resolve({ code: code ?? -1, signal: sig }));
|
|
668
|
-
});
|
|
669
|
-
let killed = null;
|
|
670
|
-
const killGroup = () => {
|
|
671
|
-
try {
|
|
672
|
-
process.kill(-child.pid, "SIGKILL");
|
|
673
|
-
} catch {
|
|
674
|
-
// already gone
|
|
675
|
-
}
|
|
676
|
-
};
|
|
677
|
-
const timer = setTimeout(() => {
|
|
678
|
-
killed = "timeout";
|
|
679
|
-
killGroup();
|
|
680
|
-
}, timeout);
|
|
681
|
-
const onAbort = () => {
|
|
682
|
-
killed = "abort";
|
|
683
|
-
killGroup();
|
|
684
|
-
};
|
|
685
|
-
if (signal?.aborted) onAbort();
|
|
686
|
-
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
687
|
-
// The timeout and abort listener live until the child EXITS — clearing
|
|
688
|
-
// them in a finally around the setup would disarm them before the exit.
|
|
689
|
-
return exited.then(({ code }) => {
|
|
690
|
-
clearTimeout(timer);
|
|
691
|
-
signal?.removeEventListener("abort", onAbort);
|
|
692
|
-
return { code, stdout, killed };
|
|
693
|
-
});
|
|
908
|
+
return superviseExit(child, timeout, signal).then(({ code, killed, unconfirmed }) => ({ code: code ?? -1, stdout, killed, unconfirmed }));
|
|
694
909
|
}
|
|
695
910
|
|
|
696
911
|
/** The role policy: read-only for explorer/reviewer, the full six for
|
|
697
|
-
* implementer/
|
|
912
|
+
* implementer/verifier. Only allow/deny — NEVER ask (a headless child cannot
|
|
698
913
|
* answer an approval prompt; ask would deadlock). */
|
|
699
914
|
export function rolePolicyContent(role, scope) {
|
|
700
|
-
const allowed = role === "implementer" || role === "
|
|
915
|
+
const allowed = role === "implementer" || role === "verifier" ? SIX_TOOLS : READ_ONLY;
|
|
701
916
|
if (scope === undefined) {
|
|
702
917
|
return `export default { name: "subagent-${role}", approvals: [{
|
|
703
918
|
decide(call) {
|
package/index.d.ts
CHANGED
|
@@ -7,5 +7,28 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { KisoExtension } from "@vincemakes/kiso-core";
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
/** ADR-0058 3d: what a background delegation needs of the host's per-session
|
|
11
|
+
* task manager — the runtime's TaskManager satisfies it structurally. */
|
|
12
|
+
export interface SubagentTasks {
|
|
13
|
+
list(): readonly { readonly agent?: unknown; readonly state: { readonly kind: string } }[];
|
|
14
|
+
start(options: {
|
|
15
|
+
readonly command: string;
|
|
16
|
+
readonly cwd: string;
|
|
17
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
18
|
+
readonly executionId?: string;
|
|
19
|
+
readonly agent: { readonly role: string; readonly session: string };
|
|
20
|
+
readonly exec: (dir: string) => { readonly file: string; readonly args: readonly string[] };
|
|
21
|
+
}): Promise<{ readonly id: string }>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SubagentHost {
|
|
25
|
+
/** The session's task manager; with it, `delegate` offers `background`. */
|
|
26
|
+
readonly tasks?: (sessionId: string | undefined) => SubagentTasks | undefined;
|
|
27
|
+
/** Live background children per session (default 20). */
|
|
28
|
+
readonly backgroundMax?: number;
|
|
29
|
+
/** A background child's model requests before its wrap-up (default 32). */
|
|
30
|
+
readonly backgroundMaxTurns?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
declare const createSubagentExtension: (host?: SubagentHost) => KisoExtension | Promise<KisoExtension>;
|
|
11
34
|
export default createSubagentExtension;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-subagent-ext",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.46.1",
|
|
4
4
|
"description": "kiso official subagent extension \u2014 child kiso processes with role policies, kernel untouched",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"test": "vitest run"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@vincemakes/kiso-core": "0.
|
|
26
|
+
"@vincemakes/kiso-core": "0.46.1",
|
|
27
27
|
"@types/node": "^26.1.2",
|
|
28
28
|
"typescript": "^5.7.2",
|
|
29
29
|
"vitest": "^3.0.0"
|