@kurrent/kcap 0.9.5 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/kcap.js +214 -36
- package/kcap/.claude-plugin/plugin.json +1 -1
- package/kcap/skills/agent-flows/SKILL.md +75 -6
- package/package.json +7 -7
package/bin/kcap.js
CHANGED
|
@@ -48,6 +48,157 @@ function probeArgs(updArgs) {
|
|
|
48
48
|
return ["update", "--check", "--no-update-check", ...channelFlags];
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
// ── Windows binary-lock handling ────────────────────────────────────────────
|
|
52
|
+
//
|
|
53
|
+
// Windows locks an executing image against overwrite/delete for the process
|
|
54
|
+
// lifetime, and kcap has long-lived native processes: MCP servers (four per
|
|
55
|
+
// open Claude Code session) and the daemon. A plain `npm install -g` therefore
|
|
56
|
+
// dies with EBUSY whenever any agent session is open. But Windows DOES allow
|
|
57
|
+
// renaming/moving a running image within a volume — so `kcap update` moves the
|
|
58
|
+
// exes into a trash directory first (rename-aside), lets npm lay down fresh
|
|
59
|
+
// files at the real path, and sweeps the trash on later launcher runs once the
|
|
60
|
+
// old processes have exited. Running processes keep executing the moved image
|
|
61
|
+
// untouched and pick up the new binary on their next start.
|
|
62
|
+
|
|
63
|
+
const TRASH_DIR_NAME = ".kcap-trash";
|
|
64
|
+
|
|
65
|
+
// Trash lives next to node_modules (e.g. %APPDATA%\npm\.kcap-trash): renaming
|
|
66
|
+
// a running image requires the destination to be on the same volume, and a
|
|
67
|
+
// sibling of the install tree guarantees that.
|
|
68
|
+
function trashDirFor(globalRoot) {
|
|
69
|
+
return path.join(globalRoot, "..", TRASH_DIR_NAME);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Trash dir as seen from this launcher file, without shelling out to
|
|
73
|
+
// `npm root -g` (too slow for the hook hot path). Returns null when the
|
|
74
|
+
// launcher isn't laid out as a node_modules install.
|
|
75
|
+
function trashDirFromLauncher(launcherDir) {
|
|
76
|
+
// <root>\node_modules\@kurrent\kcap\bin → <root>\.kcap-trash
|
|
77
|
+
const nodeModules = path.resolve(launcherDir, "..", "..", "..");
|
|
78
|
+
if (path.basename(nodeModules) !== "node_modules") return null;
|
|
79
|
+
return path.join(path.dirname(nodeModules), TRASH_DIR_NAME);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Best-effort reclaim of exes parked by earlier updates. Deleting a file whose
|
|
83
|
+
// process is still running fails; it just stays for a later sweep.
|
|
84
|
+
function sweepTrash(trashDir) {
|
|
85
|
+
if (!trashDir) return;
|
|
86
|
+
let entries;
|
|
87
|
+
try { entries = fs.readdirSync(trashDir); } catch { return; }
|
|
88
|
+
for (const f of entries) {
|
|
89
|
+
try { fs.unlinkSync(path.join(trashDir, f)); } catch {}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Move the native exes out of the install tree so npm can replace them.
|
|
94
|
+
// Returns the performed moves so a failed install can restore them. Throws if
|
|
95
|
+
// even a rename is refused (something holds the file exclusively) — after
|
|
96
|
+
// first undoing any move it already made, so a partial failure never leaves
|
|
97
|
+
// the install half-emptied.
|
|
98
|
+
function renameAsideBinaries(binDir, trashDir) {
|
|
99
|
+
const moved = [];
|
|
100
|
+
for (const name of ["kcap.exe", "kcap-daemon.exe"]) {
|
|
101
|
+
const src = path.join(binDir, name);
|
|
102
|
+
if (!fs.existsSync(src)) continue;
|
|
103
|
+
try {
|
|
104
|
+
fs.mkdirSync(trashDir, { recursive: true });
|
|
105
|
+
const dest = path.join(trashDir, `${name}.${process.pid}.${Date.now()}`);
|
|
106
|
+
fs.renameSync(src, dest);
|
|
107
|
+
moved.push({ from: src, to: dest });
|
|
108
|
+
} catch (e) {
|
|
109
|
+
restoreMoved(moved);
|
|
110
|
+
throw e;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return moved;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Undo rename-aside after a failed install, so the user isn't left without a
|
|
117
|
+
// binary. Skips any path npm already managed to replace.
|
|
118
|
+
function restoreMoved(moved) {
|
|
119
|
+
const failures = [];
|
|
120
|
+
for (const m of moved) {
|
|
121
|
+
try {
|
|
122
|
+
if (!fs.existsSync(m.from)) fs.renameSync(m.to, m.from);
|
|
123
|
+
} catch (e) {
|
|
124
|
+
failures.push({ ...m, error: e });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (failures.length) {
|
|
129
|
+
console.error("Could not restore one or more kcap binaries after a failed update:");
|
|
130
|
+
for (const f of failures) {
|
|
131
|
+
console.error(` ${f.to} -> ${f.from}: ${f.error.message}`);
|
|
132
|
+
}
|
|
133
|
+
console.error("You may need to move the files back manually from the .kcap-trash directory.");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return failures;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Pure helper (unit-tested): shape a Win32_Process JSON payload into the kcap
|
|
140
|
+
// processes running from the given install root. ConvertTo-Json unwraps
|
|
141
|
+
// single-element arrays, so `parsed` may be a bare object.
|
|
142
|
+
function filterKcapProcesses(parsed, installRoot) {
|
|
143
|
+
const list = Array.isArray(parsed) ? parsed : parsed ? [parsed] : [];
|
|
144
|
+
const root = installRoot.toLowerCase();
|
|
145
|
+
return list
|
|
146
|
+
.filter((p) => p && typeof p.ExecutablePath === "string"
|
|
147
|
+
&& p.ExecutablePath.toLowerCase().startsWith(root))
|
|
148
|
+
.map((p) => ({
|
|
149
|
+
pid: p.ProcessId,
|
|
150
|
+
name: path.basename(p.ExecutablePath),
|
|
151
|
+
role: describeRole(p.CommandLine || p.ExecutablePath || ""),
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Pure helper (unit-tested): human label for what a kcap process is, from its
|
|
156
|
+
// command line. MCP servers are the common locker — one per MCP entry in the
|
|
157
|
+
// kcap Claude Code plugin, so four per open session.
|
|
158
|
+
function describeRole(commandLine) {
|
|
159
|
+
if (/kcap-daemon/i.test(commandLine)) return "daemon";
|
|
160
|
+
if (/\bmcp\b/i.test(commandLine)) return "MCP server — open Claude Code/agent session";
|
|
161
|
+
if (/\bdaemon\b/i.test(commandLine)) return "daemon";
|
|
162
|
+
return "kcap process";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Enumerate running kcap processes under the install tree (Windows only).
|
|
166
|
+
// Returns null when enumeration itself fails, so callers can degrade to a
|
|
167
|
+
// generic message.
|
|
168
|
+
function listKcapProcesses(installRoot) {
|
|
169
|
+
try {
|
|
170
|
+
const script =
|
|
171
|
+
"Get-CimInstance Win32_Process -Filter \"Name='kcap.exe' OR Name='kcap-daemon.exe'\" | " +
|
|
172
|
+
"Select-Object ProcessId,ExecutablePath,CommandLine | ConvertTo-Json -Compress";
|
|
173
|
+
const out = execFileSync("powershell.exe",
|
|
174
|
+
["-NoProfile", "-NonInteractive", "-Command", script],
|
|
175
|
+
{ encoding: "utf8", windowsHide: true });
|
|
176
|
+
return filterKcapProcesses(out.trim() ? JSON.parse(out) : [], installRoot);
|
|
177
|
+
} catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Actionable diagnosis for a locked binary: name the processes holding it and
|
|
183
|
+
// say what to do, instead of leaving the user with npm's raw EBUSY. With
|
|
184
|
+
// `onlyIfFound` (the npm-failure path, where the cause may be unrelated —
|
|
185
|
+
// network, registry) it stays silent unless lockers are actually found.
|
|
186
|
+
function printLockedBinaryHelp(globalRoot, onlyIfFound = false) {
|
|
187
|
+
const procs = listKcapProcesses(path.join(globalRoot, "@kurrent"));
|
|
188
|
+
if (procs && procs.length) {
|
|
189
|
+
console.error("The kcap binary is locked by running kcap processes:");
|
|
190
|
+
for (const p of procs) {
|
|
191
|
+
console.error(` PID ${String(p.pid).padEnd(6)} ${p.name} (${p.role})`);
|
|
192
|
+
}
|
|
193
|
+
console.error("MCP servers belong to open Claude Code / agent sessions. Close those");
|
|
194
|
+
console.error("sessions (and stop the daemon, if running), then re-run `kcap update`.");
|
|
195
|
+
} else if (!onlyIfFound) {
|
|
196
|
+
console.error("The kcap binary appears to be locked by a running process (open");
|
|
197
|
+
console.error("Claude Code sessions run kcap MCP servers; the daemon also counts).");
|
|
198
|
+
console.error("Close agent sessions and stop the daemon, then re-run `kcap update`.");
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
51
202
|
// Everything below actually DOES something (resolves the binary, execs it,
|
|
52
203
|
// runs the update flow), so it's guarded to only run when this file is
|
|
53
204
|
// executed directly — not when `require()`d (e.g. by the test).
|
|
@@ -87,11 +238,18 @@ if (require.main === module) {
|
|
|
87
238
|
try { fs.chmodSync(binaryPath, 0o755); } catch {}
|
|
88
239
|
}
|
|
89
240
|
|
|
241
|
+
// Reclaim exes parked by a previous rename-aside update, now that their
|
|
242
|
+
// processes may have exited. Cheap when there's nothing to do (one stat).
|
|
243
|
+
if (process.platform === "win32") {
|
|
244
|
+
sweepTrash(trashDirFromLauncher(__dirname));
|
|
245
|
+
}
|
|
246
|
+
|
|
90
247
|
// `kcap update` for npm-global installs is driven HERE, in the Node launcher,
|
|
91
248
|
// not by the native binary. The OS locks an executable image for the whole
|
|
92
|
-
// process lifetime but a script only during load — so
|
|
93
|
-
//
|
|
94
|
-
//
|
|
249
|
+
// process lifetime but a script only during load — so with the short-lived
|
|
250
|
+
// probe exited, npm can overwrite the binary in place on macOS/Linux; on
|
|
251
|
+
// Windows the long-lived MCP/daemon processes are handled by rename-aside
|
|
252
|
+
// (see the binary-lock section above).
|
|
95
253
|
// `--check` (JSON probe) and `--help` fall through to the native binary.
|
|
96
254
|
{
|
|
97
255
|
const updArgs = process.argv.slice(3);
|
|
@@ -132,10 +290,12 @@ function runUpdate(binaryPath, updArgs) {
|
|
|
132
290
|
globalRoot = execFileSync("npm", ["root", "-g"], { encoding: "utf8", ...npmOpts }).trim();
|
|
133
291
|
} catch {}
|
|
134
292
|
|
|
293
|
+
let realGlobalRoot = null;
|
|
135
294
|
let isGlobalNpm = false;
|
|
136
295
|
try {
|
|
137
296
|
if (globalRoot) {
|
|
138
|
-
|
|
297
|
+
realGlobalRoot = fs.realpathSync(globalRoot);
|
|
298
|
+
const root = realGlobalRoot + path.sep;
|
|
139
299
|
isGlobalNpm = fs.realpathSync(__filename).startsWith(root);
|
|
140
300
|
}
|
|
141
301
|
} catch {}
|
|
@@ -174,28 +334,21 @@ function runUpdate(binaryPath, updArgs) {
|
|
|
174
334
|
// Couldn't determine — fall through and let npm decide (it's idempotent).
|
|
175
335
|
}
|
|
176
336
|
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
console.error(" kcap daemon service stop (if installed as a service)");
|
|
188
|
-
console.error(" kcap daemon stop (otherwise)");
|
|
189
|
-
process.exit(1);
|
|
190
|
-
}
|
|
191
|
-
} catch {
|
|
192
|
-
// status probe failed (no daemon / old binary) — fall through to the normal install.
|
|
193
|
-
}
|
|
337
|
+
// Detect a running daemon up front (short-lived child; it exits before npm
|
|
338
|
+
// runs). Used only for the post-update notice — the update itself no longer
|
|
339
|
+
// needs the daemon stopped on any platform. `--no-update-check` keeps this
|
|
340
|
+
// child's "update available" nudge from landing mid-update.
|
|
341
|
+
let daemonRunning = false;
|
|
342
|
+
try {
|
|
343
|
+
const status = execFileSync(binaryPath, ["daemon", "status", "--no-update-check"], { encoding: "utf8" });
|
|
344
|
+
daemonRunning = /running \(PID/i.test(status);
|
|
345
|
+
} catch {
|
|
346
|
+
// status probe failed (no daemon / old binary) — treat as not running.
|
|
194
347
|
}
|
|
195
348
|
|
|
196
349
|
// Fail clearly instead of half-installing under a root-owned prefix.
|
|
197
350
|
try {
|
|
198
|
-
fs.accessSync(
|
|
351
|
+
fs.accessSync(realGlobalRoot, fs.constants.W_OK);
|
|
199
352
|
} catch {
|
|
200
353
|
console.error(`Cannot write to the global npm directory (${globalRoot}).`);
|
|
201
354
|
console.error("Re-run with the permissions you installed kcap with, or reinstall");
|
|
@@ -203,13 +356,31 @@ function runUpdate(binaryPath, updArgs) {
|
|
|
203
356
|
process.exit(1);
|
|
204
357
|
}
|
|
205
358
|
|
|
359
|
+
// Windows: move the (possibly executing) exes out of npm's way. See the
|
|
360
|
+
// binary-lock section above for why rename works when overwrite doesn't.
|
|
361
|
+
let moved = [];
|
|
362
|
+
if (process.platform === "win32") {
|
|
363
|
+
try {
|
|
364
|
+
moved = renameAsideBinaries(path.dirname(binaryPath), trashDirFor(realGlobalRoot));
|
|
365
|
+
} catch (e) {
|
|
366
|
+
// Even a rename was refused — something holds the file exclusively
|
|
367
|
+
// (AV scan, exclusive open). renameAsideBinaries already rolled back its
|
|
368
|
+
// partial moves; diagnose instead of letting npm EBUSY.
|
|
369
|
+
console.error(`Could not move the current kcap binary aside: ${e.message}`);
|
|
370
|
+
printLockedBinaryHelp(realGlobalRoot);
|
|
371
|
+
process.exit(1);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
206
375
|
const res = spawnSync("npm", ["install", "-g", resolveInstallSpec(info)], {
|
|
207
376
|
stdio: "inherit",
|
|
208
377
|
windowsHide: true,
|
|
209
378
|
...npmOpts,
|
|
210
379
|
});
|
|
211
380
|
if (res.status !== 0) {
|
|
381
|
+
restoreMoved(moved);
|
|
212
382
|
console.error("npm install failed; kcap was not updated.");
|
|
383
|
+
if (process.platform === "win32") printLockedBinaryHelp(realGlobalRoot, /* onlyIfFound */ true);
|
|
213
384
|
process.exit(res.status == null ? 1 : res.status);
|
|
214
385
|
}
|
|
215
386
|
|
|
@@ -219,23 +390,30 @@ function runUpdate(binaryPath, updArgs) {
|
|
|
219
390
|
require("./refresh").runRefreshes(fs.realpathSync(__filename));
|
|
220
391
|
console.log("kcap updated.");
|
|
221
392
|
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
// best-effort notice only
|
|
393
|
+
// A running daemon keeps executing the old image until restarted. On
|
|
394
|
+
// macOS/Linux it self-detects the new binary and restarts when idle; on
|
|
395
|
+
// Windows self-detection is off (the running image was moved, not replaced),
|
|
396
|
+
// so the user applies it explicitly.
|
|
397
|
+
if (daemonRunning) {
|
|
398
|
+
if (process.platform === "win32") {
|
|
399
|
+
console.log("A kcap daemon is running and still uses the old version. Apply the");
|
|
400
|
+
console.log("update with `kcap daemon restart` (add --force if agents are running).");
|
|
401
|
+
} else {
|
|
402
|
+
console.log("A kcap daemon is running; it will restart automatically when idle to");
|
|
403
|
+
console.log("pick up the new version. Check with `kcap daemon status`, or apply now");
|
|
404
|
+
console.log("with `kcap daemon restart --force`.");
|
|
235
405
|
}
|
|
236
406
|
}
|
|
237
407
|
|
|
238
408
|
process.exit(0);
|
|
239
409
|
}
|
|
240
410
|
|
|
241
|
-
module.exports = {
|
|
411
|
+
module.exports = {
|
|
412
|
+
resolveInstallSpec,
|
|
413
|
+
probeArgs,
|
|
414
|
+
trashDirFor,
|
|
415
|
+
trashDirFromLauncher,
|
|
416
|
+
filterKcapProcesses,
|
|
417
|
+
describeRole,
|
|
418
|
+
restoreMoved,
|
|
419
|
+
};
|
|
@@ -50,25 +50,31 @@ If `start_flow` / `send_to_participant` are not among the tools available in thi
|
|
|
50
50
|
4. **Only call `close_flow` after the clean signal.** The run stays open until you explicitly close it — don't rely on it closing itself. Then report completion to the user.
|
|
51
51
|
5. **If participant output is unclear or requires user input**, pause and ask the user before proceeding.
|
|
52
52
|
6. **Never start a nested flow.** If you are the hosted participant (see above), do not call these tools yourself.
|
|
53
|
-
7. **
|
|
53
|
+
7. **Address each role independently.** A flow definition declares one or more participant roles in its `participants` map (single-participant definitions use `reviewer`). A multi-participant `start_flow` returns no round — nothing has launched yet. Call `send_to_participant(flow_run_id, participant=<role>, message=…)` naming the role you want to address; its first message launches that role's agent lazily. Only one round is in flight per role at a time — sending to a role that's still working on a round gets a `409` naming the busy round — but every OTHER role stays addressable in the meantime. Sending an unknown role is rejected by the server, which names the valid roles in its error.
|
|
54
54
|
8. **For a code review flow (`definition_id: "code-review"`), do NOT ask the participant to run tests.** CI covers test execution; participant feedback is on correctness, design, and adherence to conventions.
|
|
55
55
|
9. **State where your changes live.** The participant's worktree is mirrored from the working tree you LAUNCHED from (your cwd's git root) — nothing else. If any part of the changeset lives elsewhere (another git worktree, another repository, a different machine) or is not in that tree, say so explicitly in `context`/`message` and inline the relevant diffs or file contents — or pass `mode: "context-only"` so the participant treats your context as the sole source of truth. The participant is instructed to flag referenced changes it cannot find in its worktree; incomplete context wastes a full round.
|
|
56
56
|
|
|
57
|
+
## Pending messages
|
|
58
|
+
|
|
59
|
+
Participants can push you out-of-band notes between rounds — observations that don't warrant a full round result (e.g. "found something odd, still looking"). These ride along as `pending_messages` on `start_flow` (a single-participant start returns round 1's result, which can already carry them), `get_flow_status`, `send_to_participant`/`submit_review_round`, and `close_flow` responses, rendered as a list of `from <role> [<id>]: <text>` entries. React to each message **once, by its `<id>`**, the moment you see it. Delivery is acknowledged after rendering, so a message normally never reappears — but if that acknowledgment fails, the SAME message (same id) is redelivered on a later call: treat a repeated id as already handled, never react to it twice. `close_flow`'s response can carry final pending messages too — often the last thing a participant tells you — so read them before you report completion to the user.
|
|
60
|
+
|
|
57
61
|
## Guardrail errors
|
|
58
62
|
|
|
59
63
|
The server enforces per-run budgets; watch for these in tool error responses:
|
|
60
64
|
|
|
61
65
|
- **`400` containing `max_rounds (N) reached for this run — close the flow.`** — the run is still **open**, it's just hit its round cap. Stop submitting further rounds, summarize what you have, and call `close_flow`.
|
|
62
|
-
- **`400` containing `budget_exceeded: …`** — the run has **already failed** and
|
|
63
|
-
- **A round that exceeds the definition's `round_timeout`** lands as a terminal **`unclear`** round, with the timeout explained in its result text — if you check round status programmatically, look for `unclear` and read the text for the timeout reason. The run itself stays open — you may submit another round or close the flow.
|
|
66
|
+
- **`400` containing `budget_exceeded: …`** — the run has **already failed** and all participant agents have stopped. Report this to the user; do NOT retry and do NOT call `close_flow` — closing a failed run overwrites the failure status in the read model (the projector flips `failed` → `closed`), hiding what went wrong.
|
|
67
|
+
- **A round that exceeds the definition's `round_timeout`** lands as a terminal **`unclear`** round, with the timeout explained in its result text — if you check round status programmatically, look for `unclear` and read the text for the timeout reason. The run itself stays open — you may submit another round to that role or close the flow.
|
|
64
68
|
- **Idle runs are auto-reaped** after the definition's `idle_ttl` (server default 24h). Don't rely on this — always call `close_flow` yourself once you're done, whether the outcome was clean or you're abandoning the task.
|
|
65
69
|
- **`400` starting `no_daemon_available:`** — no connected daemon has the repo checked out. Tell the user to run `kcap agent` on a machine with the repo cloned (or pass an explicit `daemon_name` + `repo_path`).
|
|
66
70
|
- **`400` starting `daemon_outdated:`** — the daemon's kcap is too old to host flow participants. Tell the user to update (`npm i -g @kurrent/kcap`) and restart `kcap agent`.
|
|
67
|
-
- **`400`
|
|
68
|
-
- **A round result of `unclear` whose text is exactly `participant_died` or `participant_stopped`** —
|
|
71
|
+
- **`400` containing `participant_unreachable`** — that role's agent is in an ambiguous liveness state (its daemon disconnected or is restarting), so the server won't guess whether it's still alive rather than risk a duplicate launch. Retry the send shortly, or ask the user to stop the participant (dashboard/API) and then re-send to force a fresh relaunch.
|
|
72
|
+
- **A round result of `unclear` whose text is exactly `participant_died` or `participant_stopped`** — that role's agent crashed or was stopped mid-round. The run stays **open**: address the same role again with `send_to_participant` and it relaunches automatically — the fresh agent has **no memory of prior rounds**, so restate any context it needs in your message; its earlier spend still counts against the run budget. No need to close and restart the flow. Other roles are unaffected and remain addressable in the meantime.
|
|
69
73
|
|
|
70
74
|
## Workflow
|
|
71
75
|
|
|
76
|
+
Single-participant definitions start eagerly — round 1 runs as part of `start_flow`:
|
|
77
|
+
|
|
72
78
|
```
|
|
73
79
|
start_flow(definition_id, target_kind, target_ref, target_title, context)
|
|
74
80
|
→ participant returns a result: kind findings (with the result text) | kind clean
|
|
@@ -86,12 +92,36 @@ if findings:
|
|
|
86
92
|
report completion to user
|
|
87
93
|
```
|
|
88
94
|
|
|
95
|
+
Multi-participant definitions start round-less — you address each role yourself, and the run is clean only in aggregate:
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
start_flow(definition_id, target_kind, target_ref, target_title, context)
|
|
99
|
+
→ no round yet — roles have not launched
|
|
100
|
+
|
|
101
|
+
send_to_participant(flow_run_id, participant="reviewer", message=…)
|
|
102
|
+
→ launches the reviewer's agent; returns kind findings | clean
|
|
103
|
+
|
|
104
|
+
send_to_participant(flow_run_id, participant="tester", message=…)
|
|
105
|
+
→ launches the tester's agent independently — no need to wait on the reviewer's round;
|
|
106
|
+
returns kind findings | clean
|
|
107
|
+
|
|
108
|
+
# a role with an open round in flight 409s if you send to it again — address the OTHER
|
|
109
|
+
# role(s) in the meantime, then come back once its round completes
|
|
110
|
+
|
|
111
|
+
loop until every addressed role's latest round is clean and none is in flight:
|
|
112
|
+
address whichever role(s) still have findings
|
|
113
|
+
send_to_participant(flow_run_id, participant=<that role>, message=…)
|
|
114
|
+
|
|
115
|
+
close_flow(flow_run_id) # only once reviewer AND tester are both clean
|
|
116
|
+
report completion to user
|
|
117
|
+
```
|
|
118
|
+
|
|
89
119
|
## Tool reference
|
|
90
120
|
|
|
91
121
|
| Tool | Required args | Optional args | When to call |
|
|
92
122
|
|---|---|---|---|
|
|
93
123
|
| `start_flow` | `definition_id` (e.g. `spec-review`, `code-review`, or a custom catalog id), `target_kind` (what is being worked on: `spec`, `code`, `pr`, `branch`, `file`, etc.), `target_ref` (a path, branch name, or PR URL/number that identifies the target), `target_title` (short human-readable title), `context` (background context: what to focus on, constraints, definition of done) | `instructions`, `mode` (`context-only` — optional; by default, on the same machine, the participant's worktree is mirrored from your working tree including uncommitted changes, so it reads the actual source. Pass `context-only` to opt out and treat the submitted context as authoritative) | Once, at the start of a flow task. |
|
|
94
|
-
| `send_to_participant` | `flow_run_id`, `participant` (
|
|
124
|
+
| `send_to_participant` | `flow_run_id`, `participant` (role name declared in the flow definition's `participants` map; single-participant definitions use `reviewer` — an unknown role is rejected, naming the valid ones), `message` | `instructions`, `async` (defaults to `true`) | After addressing a non-clean result for that role, or to launch a role for the first time. Pass the same `flow_run_id`, the role's name, and the updated message. |
|
|
95
125
|
| `get_flow_status` | `flow_run_id` | — | Poll or check the current status of a flow run (running, waiting, completed, failed). |
|
|
96
126
|
| `close_flow` | `flow_run_id` | — | Only after the definition's clean signal — or when abandoning the task early; the run otherwise stays open until closed. |
|
|
97
127
|
|
|
@@ -121,3 +151,42 @@ send_to_participant(
|
|
|
121
151
|
close_flow(flow_run_id="flow_abc123")
|
|
122
152
|
# Report to user: flow complete, all findings resolved
|
|
123
153
|
```
|
|
154
|
+
|
|
155
|
+
## Example (two roles: reviewer + tester)
|
|
156
|
+
|
|
157
|
+
`review-and-test`'s `participants` map declares `reviewer` and `tester` — each is addressed independently, and neither's `send_to_participant` waits on the other's round:
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
# Step 1 — start; multi-participant, so no round comes back yet
|
|
161
|
+
start_flow(
|
|
162
|
+
definition_id="review-and-test",
|
|
163
|
+
target_kind="branch",
|
|
164
|
+
target_ref="feature/add-null-check",
|
|
165
|
+
target_title="Add null check on user input",
|
|
166
|
+
context="Review the diff on this branch and write/run tests for the new code path."
|
|
167
|
+
)
|
|
168
|
+
# → returns flow_run_id, e.g. "flow_xyz789"; no round in the response
|
|
169
|
+
|
|
170
|
+
# Step 2 — address both roles; each launches lazily on its first message.
|
|
171
|
+
# These are independent — send to tester without waiting for the reviewer's round.
|
|
172
|
+
send_to_participant(flow_run_id="flow_xyz789", participant="reviewer", message="…")
|
|
173
|
+
# → launches the reviewer; returns kind findings: missing null check on line 42
|
|
174
|
+
send_to_participant(flow_run_id="flow_xyz789", participant="tester", message="…")
|
|
175
|
+
# → launches the tester; returns kind clean: added a null-input test case, passing
|
|
176
|
+
|
|
177
|
+
# Step 3 — only the reviewer had findings; fix them and send a follow-up to JUST that role
|
|
178
|
+
send_to_participant(
|
|
179
|
+
flow_run_id="flow_xyz789",
|
|
180
|
+
participant="reviewer",
|
|
181
|
+
message="Fixed null check on line 42. Updated diff attached."
|
|
182
|
+
)
|
|
183
|
+
# → reviewer returns kind clean
|
|
184
|
+
|
|
185
|
+
# Step 4 — close only once EVERY addressed role's latest round is clean and none is
|
|
186
|
+
# in flight: reviewer clean + tester clean (from step 2, still current) = aggregate clean.
|
|
187
|
+
# One role going clean does not end the run by itself — track each role's latest result
|
|
188
|
+
# from your own send_to_participant responses; the run's status only reads "clean" once
|
|
189
|
+
# every addressed role's latest round is clean and none is in flight (the aggregate rule).
|
|
190
|
+
close_flow(flow_run_id="flow_xyz789")
|
|
191
|
+
# Report to user: flow complete, review and tests both clean
|
|
192
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kurrent/kcap",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "CLI companion for Kurrent Capacitor — records and visualizes Claude Code sessions",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"repository": {
|
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
"postinstall": "node bin/postinstall.js"
|
|
15
15
|
},
|
|
16
16
|
"optionalDependencies": {
|
|
17
|
-
"@kurrent/kcap-darwin-arm64": "0.
|
|
18
|
-
"@kurrent/kcap-linux-x64": "0.
|
|
19
|
-
"@kurrent/kcap-linux-arm64": "0.
|
|
20
|
-
"@kurrent/kcap-linux-musl-x64": "0.
|
|
21
|
-
"@kurrent/kcap-linux-musl-arm64": "0.
|
|
22
|
-
"@kurrent/kcap-win-x64": "0.
|
|
17
|
+
"@kurrent/kcap-darwin-arm64": "0.10.0",
|
|
18
|
+
"@kurrent/kcap-linux-x64": "0.10.0",
|
|
19
|
+
"@kurrent/kcap-linux-arm64": "0.10.0",
|
|
20
|
+
"@kurrent/kcap-linux-musl-x64": "0.10.0",
|
|
21
|
+
"@kurrent/kcap-linux-musl-arm64": "0.10.0",
|
|
22
|
+
"@kurrent/kcap-win-x64": "0.10.0"
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
25
|
"bin/",
|