@crouton-kit/crouter 0.3.36 → 0.3.38
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/dist/build-root.js +3 -6
- package/dist/{builtin-personas/runtime-base.md → builtin-memory/00-runtime-base.md} +8 -1
- package/dist/{builtin-personas/spine/has-manager.md → builtin-memory/01-spine/00-has-manager.md} +8 -0
- package/dist/builtin-memory/01-spine/01-no-manager.md +10 -0
- package/dist/{builtin-personas/lifecycle/terminal.md → builtin-memory/02-lifecycle/00-terminal.md} +8 -0
- package/dist/{builtin-personas/lifecycle/resident.md → builtin-memory/02-lifecycle/01-resident.md} +8 -0
- package/dist/{builtin-personas/waiting.md → builtin-memory/03-waiting.md} +7 -0
- package/dist/{builtin-personas/orchestration-kernel.md → builtin-memory/04-orchestration-kernel.md} +8 -0
- package/dist/{builtin-personas/advisor/PERSONA.md → builtin-memory/05-kinds/advisor/00-base.md} +5 -2
- package/dist/{builtin-personas/design/PERSONA.md → builtin-memory/05-kinds/design/00-base.md} +5 -2
- package/dist/{builtin-personas/design/orchestrator.md → builtin-memory/05-kinds/design/01-orchestrator.md} +5 -4
- package/dist/{builtin-personas/developer/PERSONA.md → builtin-memory/05-kinds/developer/00-base.md} +5 -2
- package/dist/{builtin-personas/developer/orchestrator.md → builtin-memory/05-kinds/developer/01-orchestrator.md} +5 -4
- package/dist/{builtin-personas/explore/PERSONA.md → builtin-memory/05-kinds/explore/00-base.md} +5 -2
- package/dist/{builtin-personas/explore/orchestrator.md → builtin-memory/05-kinds/explore/01-orchestrator.md} +5 -3
- package/dist/{builtin-personas/general/PERSONA.md → builtin-memory/05-kinds/general/00-base.md} +5 -2
- package/dist/{builtin-personas/general/orchestrator.md → builtin-memory/05-kinds/general/01-orchestrator.md} +5 -3
- package/dist/{builtin-personas/plan/PERSONA.md → builtin-memory/05-kinds/plan/00-base.md} +5 -2
- package/dist/{builtin-personas/plan/orchestrator.md → builtin-memory/05-kinds/plan/01-orchestrator.md} +5 -4
- package/dist/{builtin-personas/plan/reviewers/architecture-fit/PERSONA.md → builtin-memory/05-kinds/plan/reviewers/architecture-fit.md} +5 -2
- package/dist/{builtin-personas/plan/reviewers/code-smells/PERSONA.md → builtin-memory/05-kinds/plan/reviewers/code-smells.md} +5 -2
- package/dist/{builtin-personas/plan/reviewers/pattern-consistency/PERSONA.md → builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md} +5 -2
- package/dist/{builtin-personas/plan/reviewers/requirements-coverage/PERSONA.md → builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md} +5 -2
- package/dist/{builtin-personas/plan/reviewers/security/PERSONA.md → builtin-memory/05-kinds/plan/reviewers/security.md} +5 -2
- package/dist/{builtin-personas/product/PERSONA.md → builtin-memory/05-kinds/product/00-base.md} +5 -2
- package/dist/{builtin-personas/product/orchestrator.md → builtin-memory/05-kinds/product/01-orchestrator.md} +5 -4
- package/dist/{builtin-personas/product/teardown/PERSONA.md → builtin-memory/05-kinds/product/teardown.md} +5 -3
- package/dist/{builtin-personas/review/PERSONA.md → builtin-memory/05-kinds/review/00-base.md} +5 -2
- package/dist/{builtin-personas/review/orchestrator.md → builtin-memory/05-kinds/review/01-orchestrator.md} +5 -3
- package/dist/{builtin-personas/spec/PERSONA.md → builtin-memory/05-kinds/spec/00-base.md} +5 -2
- package/dist/{builtin-personas/spec/orchestrator.md → builtin-memory/05-kinds/spec/01-orchestrator.md} +5 -4
- package/dist/{builtin-personas/spec/requirements/PERSONA.md → builtin-memory/05-kinds/spec/requirements.md} +5 -2
- package/dist/builtin-memory/internal/INDEX.md +1 -1
- package/dist/builtin-memory/internal/examples/imessage-assistant.md +2 -2
- package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
- package/dist/builtin-pi-packages/pi-mode-switch/extensions/index.ts +4 -4
- package/dist/builtin-views/canvas/core.mjs +3 -2
- package/dist/builtin-views/prompt-review/core.mjs +6 -134
- package/dist/builtin-views/settings/core.mjs +26 -94
- package/dist/builtin-views/settings/text.mjs +0 -5
- package/dist/builtin-views/settings/tui.mjs +1 -7
- package/dist/builtin-views/settings/web.jsx +3 -49
- package/dist/clients/attach/__tests__/crtr-output-render.test.js +21 -205
- package/dist/clients/attach/attach-cmd.js +429 -432
- package/dist/clients/web/web-cmd.js +7 -7
- package/dist/commands/__tests__/human.test.js +1 -15
- package/dist/commands/attention.js +9 -6
- package/dist/commands/canvas-history/search.js +2 -2
- package/dist/commands/canvas-history/show.js +3 -3
- package/dist/commands/canvas-history.js +1 -1
- package/dist/commands/canvas-issue.js +1 -0
- package/dist/commands/canvas-snapshot.js +1 -0
- package/dist/commands/canvas.js +10 -11
- package/dist/commands/chord.js +1 -0
- package/dist/commands/human/prompts.js +0 -1
- package/dist/commands/human/queue.js +1 -0
- package/dist/commands/memory/read.js +3 -3
- package/dist/commands/memory/shared.d.ts +13 -8
- package/dist/commands/memory/shared.js +22 -8
- package/dist/commands/memory/write.js +5 -3
- package/dist/commands/node-snapshot.js +1 -0
- package/dist/commands/node.js +52 -22
- package/dist/commands/profile/add-project.d.ts +1 -0
- package/dist/commands/profile/add-project.js +42 -0
- package/dist/commands/profile/delete.d.ts +1 -0
- package/dist/commands/profile/delete.js +39 -0
- package/dist/commands/profile/list.d.ts +1 -0
- package/dist/commands/profile/list.js +35 -0
- package/dist/commands/profile/new.d.ts +1 -0
- package/dist/commands/profile/new.js +50 -0
- package/dist/commands/profile/remove-project.d.ts +1 -0
- package/dist/commands/profile/remove-project.js +42 -0
- package/dist/commands/profile/rename.d.ts +1 -0
- package/dist/commands/profile/rename.js +42 -0
- package/dist/commands/profile/show.d.ts +1 -0
- package/dist/commands/profile/show.js +50 -0
- package/dist/commands/profile.d.ts +2 -0
- package/dist/commands/profile.js +33 -0
- package/dist/commands/push.d.ts +0 -1
- package/dist/commands/push.js +2 -228
- package/dist/commands/revive.js +2 -2
- package/dist/commands/{canvas-tmux-spread.js → surface-tmux-spread.js} +9 -6
- package/dist/commands/surface.js +3 -2
- package/dist/commands/sys/config.js +6 -21
- package/dist/commands/sys/daemon.d.ts +2 -0
- package/dist/commands/{daemon.js → sys/daemon.js} +12 -12
- package/dist/commands/sys/doctor.js +1 -0
- package/dist/commands/sys/prompt-review.js +90 -17
- package/dist/commands/sys.js +4 -3
- package/dist/core/__tests__/daemon-boot.test.js +1 -1
- package/dist/core/__tests__/daemon-wedge.test.d.ts +1 -0
- package/dist/core/__tests__/daemon-wedge.test.js +87 -0
- package/dist/core/__tests__/hearth-bootstrap.test.js +2 -2
- package/dist/core/__tests__/history-inbox.test.d.ts +1 -0
- package/dist/core/__tests__/history-inbox.test.js +105 -0
- package/dist/core/__tests__/kickoff.test.js +3 -3
- package/dist/core/__tests__/revive.test.js +39 -1
- package/dist/core/canvas/canvas.js +10 -9
- package/dist/core/canvas/db.js +11 -0
- package/dist/core/canvas/history.d.ts +1 -1
- package/dist/core/canvas/history.js +91 -3
- package/dist/core/canvas/paths.d.ts +6 -0
- package/dist/core/canvas/paths.js +9 -0
- package/dist/core/canvas/status-glyph.js +1 -0
- package/dist/core/canvas/types.d.ts +10 -0
- package/dist/core/config.d.ts +44 -1
- package/dist/core/config.js +155 -12
- package/dist/core/fault-classifier.d.ts +2 -2
- package/dist/core/feed/inbox.d.ts +7 -12
- package/dist/core/feed/inbox.js +45 -29
- package/dist/core/hearth/providers/blaxel-bootstrap.js +1 -1
- package/dist/core/memory-resolver.d.ts +26 -15
- package/dist/core/memory-resolver.js +158 -40
- package/dist/core/preview-registry.js +14 -112
- package/dist/core/profiles/manifest.d.ts +33 -0
- package/dist/core/profiles/manifest.js +332 -0
- package/dist/core/profiles/select.d.ts +11 -0
- package/dist/core/profiles/select.js +115 -0
- package/dist/core/render.js +1 -1
- package/dist/core/runtime/bearings.d.ts +9 -0
- package/dist/core/runtime/bearings.js +31 -0
- package/dist/core/runtime/busy.d.ts +15 -0
- package/dist/core/runtime/busy.js +32 -1
- package/dist/core/runtime/close.d.ts +10 -0
- package/dist/core/runtime/close.js +18 -13
- package/dist/core/runtime/fault-recovery.js +4 -0
- package/dist/core/runtime/front-door.js +9 -2
- package/dist/core/runtime/kickoff.js +5 -7
- package/dist/core/runtime/launch.d.ts +25 -7
- package/dist/core/runtime/launch.js +42 -55
- package/dist/core/runtime/nodes.d.ts +7 -0
- package/dist/core/runtime/nodes.js +6 -0
- package/dist/core/runtime/persona.js +18 -32
- package/dist/core/runtime/placement.js +1 -1
- package/dist/core/runtime/revive.js +9 -1
- package/dist/core/runtime/spawn.d.ts +13 -0
- package/dist/core/runtime/spawn.js +25 -0
- package/dist/core/runtime/tmux.d.ts +2 -2
- package/dist/core/runtime/tmux.js +2 -2
- package/dist/core/scope.d.ts +14 -7
- package/dist/core/scope.js +76 -20
- package/dist/core/substrate/index.d.ts +3 -3
- package/dist/core/substrate/index.js +5 -3
- package/dist/core/substrate/on-read.js +3 -2
- package/dist/core/substrate/render.d.ts +16 -11
- package/dist/core/substrate/render.js +249 -62
- package/dist/core/substrate/schema.d.ts +11 -3
- package/dist/core/substrate/schema.js +25 -0
- package/dist/core/substrate/subject.d.ts +15 -2
- package/dist/core/substrate/subject.js +24 -2
- package/dist/daemon/crtrd-cli.js +1 -1
- package/dist/daemon/crtrd.d.ts +24 -0
- package/dist/daemon/crtrd.js +213 -5
- package/dist/daemon/manage.js +2 -2
- package/dist/pi-extensions/canvas-doc-substrate.js +2 -3
- package/dist/pi-extensions/canvas-nav.js +2 -2
- package/dist/pi-extensions/canvas-stophook.d.ts +1 -1
- package/dist/pi-extensions/canvas-stophook.js +59 -2
- package/dist/types.d.ts +57 -1
- package/dist/types.js +83 -1
- package/dist/web-client/assets/{index-IAJVtuVe.js → index-CbO8L0mN.js} +4 -4
- package/dist/web-client/assets/index-DwO46Cs5.css +2 -0
- package/dist/web-client/index.html +2 -2
- package/package.json +2 -2
- package/dist/builtin-memory/crouter-development/personas/base-prompt.md +0 -56
- package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +0 -57
- package/dist/builtin-memory/crouter-development/personas.md +0 -112
- package/dist/builtin-personas/spine/no-manager.md +0 -2
- package/dist/commands/daemon.d.ts +0 -2
- package/dist/core/personas/index.d.ts +0 -13
- package/dist/core/personas/index.js +0 -11
- package/dist/core/personas/loader.d.ts +0 -159
- package/dist/core/personas/loader.js +0 -327
- package/dist/core/personas/resolve.d.ts +0 -59
- package/dist/core/personas/resolve.js +0 -376
- package/dist/web-client/assets/index-DUThOUzU.css +0 -2
- /package/dist/commands/{canvas-tmux-spread.d.ts → surface-tmux-spread.d.ts} +0 -0
package/dist/daemon/crtrd.js
CHANGED
|
@@ -49,11 +49,14 @@
|
|
|
49
49
|
import { writeFileSync, readFileSync, rmSync, existsSync, mkdirSync, statSync, } from 'node:fs';
|
|
50
50
|
import { dirname, join } from 'node:path';
|
|
51
51
|
import { homedir } from 'node:os';
|
|
52
|
+
import { spawnSync } from 'node:child_process';
|
|
52
53
|
import { crtrHome, nodeDir, jobDir } from '../core/canvas/paths.js';
|
|
53
|
-
import { listNodes, getRow, getNode, dueClockTriggers, consumeTrigger, advanceTrigger, listFocuses, closeFocusRow, pruneToLimit, } from '../core/canvas/index.js';
|
|
54
|
+
import { listNodes, getRow, getNode, dueClockTriggers, consumeTrigger, advanceTrigger, listFocuses, closeFocusRow, pruneToLimit, subscribersOf, } from '../core/canvas/index.js';
|
|
55
|
+
import { fullName } from '../core/canvas/labels.js';
|
|
54
56
|
import { transition } from '../core/runtime/lifecycle.js';
|
|
55
|
-
import { isBusy } from '../core/runtime/busy.js';
|
|
56
|
-
import { FAULT_QUIET_MS } from '../core/runtime/fault.js';
|
|
57
|
+
import { isBusy, busySince } from '../core/runtime/busy.js';
|
|
58
|
+
import { FAULT_QUIET_MS, recordFault } from '../core/runtime/fault.js';
|
|
59
|
+
import { fanDoctrineWake } from '../core/runtime/close.js';
|
|
57
60
|
import { logger } from '../core/log.js';
|
|
58
61
|
export { FAULT_QUIET_MS };
|
|
59
62
|
import { isPidAlive } from '../core/canvas/pid.js';
|
|
@@ -209,6 +212,207 @@ function handleYieldStall(row, pid, now) {
|
|
|
209
212
|
}
|
|
210
213
|
}
|
|
211
214
|
// ---------------------------------------------------------------------------
|
|
215
|
+
// Wedge detection (issue #110): a broker whose pid is alive but whose engine is
|
|
216
|
+
// frozen MID-TURN (a runaway subprocess, a hung stream) is invisible to every
|
|
217
|
+
// other guarantee — the pid-alive branch above returns early, yield-stall only
|
|
218
|
+
// fires on intent='refresh', and provider-fault recovery needs a completed
|
|
219
|
+
// ERROR turn. A node in this state never pushes, so its parent waits forever
|
|
220
|
+
// with no wake and no visible fault marker (`node inspect list --hanging`
|
|
221
|
+
// returns nothing — there's no fault recorded, just silence).
|
|
222
|
+
//
|
|
223
|
+
// Detection, not recovery: this NOTIFIES (fans a doctrine wake + records a
|
|
224
|
+
// `daemon→node`/'wedged' fault so the node shows ⌘ “wedged · needs you” on the
|
|
225
|
+
// canvas graph views, same surface `node inspect list --hanging` already reads)
|
|
226
|
+
// — it never kills or restarts the engine. Killing the runaway SUBPROCESS (not
|
|
227
|
+
// the node) is the sanctioned recovery; see `crtr memory read
|
|
228
|
+
// wedged-child-on-runaway-bash`.
|
|
229
|
+
//
|
|
230
|
+
// Two corroborating signals, both generous, so a routine long-running bash/
|
|
231
|
+
// build/test tool call is never mistaken for a wedge:
|
|
232
|
+
// 1. quietForMs — ms since the busy marker's mtime last moved. canvas-
|
|
233
|
+
// stophook re-touches that marker (`touchBusy`) on every engine-level sign
|
|
234
|
+
// of life within a turn: a tool starting/streaming/ending, a streamed
|
|
235
|
+
// assistant token, or the turn ending. A tool that is actively streaming
|
|
236
|
+
// output, or a turn producing tokens, keeps this at ~0 no matter how long
|
|
237
|
+
// the OVERALL turn runs. Only a turn with NO observed engine event at all
|
|
238
|
+
// for WEDGE_QUIET_MS is even a candidate.
|
|
239
|
+
// 2. cpuPercent — the broker's WHOLE PROCESS TREE (the broker pid + every
|
|
240
|
+
// descendant), summed, sampled ONLY once a node is already a time-based
|
|
241
|
+
// candidate (never every tick — the `ps` shell-out is cheap but pointless
|
|
242
|
+
// on a healthy node). Sampling the tree, not just the broker pid, matters
|
|
243
|
+
// because the broker often sits idle awaiting a CHILD subprocess (bash,
|
|
244
|
+
// a build, a test run) that is the one actually burning CPU — sampling
|
|
245
|
+
// the broker alone would read that as 0% and false-fire on a perfectly
|
|
246
|
+
// healthy long-running tool call. A pi process (or any descendant)
|
|
247
|
+
// genuinely computing shows non-trivial tree CPU and is exonerated even
|
|
248
|
+
// past the quiet grace; a pi merely awaiting a subprocess or a stalled
|
|
249
|
+
// I/O call — wedged OR legitimately blocked on something equally silent —
|
|
250
|
+
// reads ~0% across the whole tree, matching the filed issue's own
|
|
251
|
+
// evidence (pid alive, 0% CPU, session file not advancing). A failed/
|
|
252
|
+
// unreadable CPU sample is treated as UNKNOWN, never as “low” — the
|
|
253
|
+
// verdict stays 'pending' rather than risk a false fire on an unreadable
|
|
254
|
+
// signal.
|
|
255
|
+
//
|
|
256
|
+
// ACCEPTED residual: a legitimate subprocess blocked on silent I/O (e.g. a
|
|
257
|
+
// 20-minute download emitting no output, a long `sleep`) reads ~0% tree
|
|
258
|
+
// CPU and is indistinguishable from a real wedge at the OS level — no CPU
|
|
259
|
+
// signal can tell a hung child from an I/O-blocked one. This is accepted,
|
|
260
|
+
// not deferred, because detection only NOTIFIES and never kills: the cost
|
|
261
|
+
// of that rare false positive is one doctrine wake to a parent, and 20+
|
|
262
|
+
// minutes of total silence with near-zero tree CPU is itself worth
|
|
263
|
+
// surfacing either way.
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
// Generous: routine long tool calls settle well under this; only a turn with
|
|
266
|
+
// zero observed engine progress for 20+ minutes is even a wedge CANDIDATE.
|
|
267
|
+
export const WEDGE_QUIET_MS = 20 * 60_000;
|
|
268
|
+
// CPU% ceiling for the corroborating sample, taken once quietForMs already
|
|
269
|
+
// clears the grace above. Small but nonzero — tolerates measurement noise
|
|
270
|
+
// without accepting a process that's visibly still doing work.
|
|
271
|
+
export const WEDGE_CPU_MAX_PERCENT = 2;
|
|
272
|
+
/** Pure wedge decision — mirrors livenessVerdict/yieldStallVerdict: a total
|
|
273
|
+
* function of the observed signals, no clock or process access of its own.
|
|
274
|
+
* `busy` false — the turn already ended (however it routed) — is never a
|
|
275
|
+
* wedge, regardless of how stale the marker looks. `quietForMs` null means
|
|
276
|
+
* the heartbeat marker couldn't be read (treated as “not enough evidence”, not
|
|
277
|
+
* “wedged”). `cpuPercent` null (sample failed/unavailable) or above the
|
|
278
|
+
* ceiling — EITHER keeps the verdict at 'pending' (keep watching next tick)
|
|
279
|
+
* rather than firing on an unconfirmed or contradicted signal. */
|
|
280
|
+
export function wedgeVerdict(input) {
|
|
281
|
+
if (!input.busy)
|
|
282
|
+
return 'leave';
|
|
283
|
+
if (input.quietForMs === null || input.quietForMs < WEDGE_QUIET_MS)
|
|
284
|
+
return 'leave';
|
|
285
|
+
if (input.cpuPercent === null || input.cpuPercent > WEDGE_CPU_MAX_PERCENT)
|
|
286
|
+
return 'pending';
|
|
287
|
+
return 'wedged';
|
|
288
|
+
}
|
|
289
|
+
/** Notified-episode dedup (one notice per wedge, per §Detection-not-repeat-fire):
|
|
290
|
+
* keyed on node id → the busy-marker mtime we already notified for. The marker
|
|
291
|
+
* only advances on genuine engine progress (a heartbeat) or a fresh turn
|
|
292
|
+
* starting, so a stale wedge that later recovers (or a brand-new turn) always
|
|
293
|
+
* carries a DIFFERENT mtime and is free to notify again. In-memory only, like
|
|
294
|
+
* every other daemon dedup latch here — a restart re-arms it (worst case: one
|
|
295
|
+
* repeat notice), which is the same tradeoff `notifiedTriggerFailures` and
|
|
296
|
+
* `unhealthySince` already accept. */
|
|
297
|
+
const wedgeNotifiedAt = new Map();
|
|
298
|
+
/** Pure: sum CPU% across a process's entire tree (itself + every descendant).
|
|
299
|
+
* Parses `ps -ax -o pid=,ppid=,pcpu=` output, builds a ppid→children map, and
|
|
300
|
+
* DFS's from `rootPid` collecting root + all descendants; malformed/blank
|
|
301
|
+
* lines are skipped. Returns `null` if `rootPid` never appears in the output
|
|
302
|
+
* (can't measure at all — treated as UNKNOWN by wedgeVerdict, never as
|
|
303
|
+
* “low”) or the output has no parsable rows. Exported for direct unit testing
|
|
304
|
+
* — no process access of its own, mirroring wedgeVerdict's purity. */
|
|
305
|
+
export function sumTreeCpu(psOutput, rootPid) {
|
|
306
|
+
const byPid = new Map(); // pid → pcpu
|
|
307
|
+
const childrenOf = new Map(); // ppid → child pids
|
|
308
|
+
for (const line of psOutput.split('\n')) {
|
|
309
|
+
const trimmed = line.trim();
|
|
310
|
+
if (trimmed === '')
|
|
311
|
+
continue;
|
|
312
|
+
const parts = trimmed.split(/\s+/);
|
|
313
|
+
if (parts.length < 3)
|
|
314
|
+
continue;
|
|
315
|
+
const pid = Number(parts[0]);
|
|
316
|
+
const ppid = Number(parts[1]);
|
|
317
|
+
const pcpu = Number(parts[2]);
|
|
318
|
+
if (!Number.isFinite(pid) || !Number.isFinite(ppid) || !Number.isFinite(pcpu))
|
|
319
|
+
continue;
|
|
320
|
+
byPid.set(pid, pcpu);
|
|
321
|
+
const siblings = childrenOf.get(ppid);
|
|
322
|
+
if (siblings === undefined)
|
|
323
|
+
childrenOf.set(ppid, [pid]);
|
|
324
|
+
else
|
|
325
|
+
siblings.push(pid);
|
|
326
|
+
}
|
|
327
|
+
if (!byPid.has(rootPid))
|
|
328
|
+
return null;
|
|
329
|
+
let total = 0;
|
|
330
|
+
const stack = [rootPid];
|
|
331
|
+
const seen = new Set();
|
|
332
|
+
while (stack.length > 0) {
|
|
333
|
+
const pid = stack.pop();
|
|
334
|
+
if (seen.has(pid))
|
|
335
|
+
continue; // guard against a malformed/cyclic ps snapshot
|
|
336
|
+
seen.add(pid);
|
|
337
|
+
total += byPid.get(pid) ?? 0;
|
|
338
|
+
const kids = childrenOf.get(pid);
|
|
339
|
+
if (kids !== undefined)
|
|
340
|
+
stack.push(...kids);
|
|
341
|
+
}
|
|
342
|
+
return total;
|
|
343
|
+
}
|
|
344
|
+
/** Sample the broker's WHOLE PROCESS TREE — the broker pid plus every
|
|
345
|
+
* descendant, summed — via one `ps` shell-out, the OS-level signal that
|
|
346
|
+
* corroborates the heartbeat silence (see the section banner above). Null on
|
|
347
|
+
* ANY failure (the shell-out itself failing, `ps` missing, or `sumTreeCpu`
|
|
348
|
+
* finding the root pid absent/output unparseable) — wedgeVerdict treats null
|
|
349
|
+
* exactly like “too high”, so a broken probe can only ever suppress a false
|
|
350
|
+
* fire, never cause one. */
|
|
351
|
+
function readTreeCpuPercent(pid) {
|
|
352
|
+
try {
|
|
353
|
+
const r = spawnSync('ps', ['-ax', '-o', 'pid=,ppid=,pcpu='], { encoding: 'utf8', timeout: 2000 });
|
|
354
|
+
if (r.status !== 0 || typeof r.stdout !== 'string')
|
|
355
|
+
return null;
|
|
356
|
+
return sumTreeCpu(r.stdout, pid);
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/** Enact wedge detection for a LIVE, busy node (called from the pid-alive
|
|
363
|
+
* branch of handleNodeLiveness, alongside handleYieldStall — the two never
|
|
364
|
+
* overlap in practice: yield-stall requires the turn OVER, wedge requires it
|
|
365
|
+
* mid-flight). Reads the heartbeat + samples CPU only when already past the
|
|
366
|
+
* time grace, fans one doctrine wake + records one `daemon→node` fault per
|
|
367
|
+
* episode, and never touches the engine itself. */
|
|
368
|
+
function handleWedgeDetection(id, pid, now) {
|
|
369
|
+
if (!isBusy(id)) {
|
|
370
|
+
wedgeNotifiedAt.delete(id); // turn ended (however it routed) — next turn starts a fresh episode
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const since = busySince(id);
|
|
374
|
+
const quietForMs = since === null ? null : now - since;
|
|
375
|
+
const cpuPercent = quietForMs !== null && quietForMs >= WEDGE_QUIET_MS ? readTreeCpuPercent(pid) : null;
|
|
376
|
+
const verdict = wedgeVerdict({ busy: true, quietForMs, cpuPercent });
|
|
377
|
+
if (verdict !== 'wedged')
|
|
378
|
+
return;
|
|
379
|
+
if (since !== null && wedgeNotifiedAt.get(id) === since)
|
|
380
|
+
return; // already notified THIS episode
|
|
381
|
+
if (since !== null)
|
|
382
|
+
wedgeNotifiedAt.set(id, since);
|
|
383
|
+
const meta = getNode(id);
|
|
384
|
+
const name = meta !== null ? fullName(meta) : id;
|
|
385
|
+
const minutes = Math.round((quietForMs ?? WEDGE_QUIET_MS) / 60_000);
|
|
386
|
+
const label = `Child wedged — ${name} (${id}) is alive and mid-turn but has shown NO engine progress for ` +
|
|
387
|
+
`${minutes}+ minutes and its process is near-idle. It will NOT recover on its own — read ` +
|
|
388
|
+
`\`crtr memory read wedged-child-on-runaway-bash\` and, if a runaway subprocess is to blame, kill ` +
|
|
389
|
+
`the SUBPROCESS (not this node) so its turn can resume with no lost context.`;
|
|
390
|
+
process.stderr.write(`[crtrd] wedged ${id} (busy ${minutes}+ min with no engine progress, cpu~0%)\n`);
|
|
391
|
+
try {
|
|
392
|
+
recordFault(id, {
|
|
393
|
+
link: 'daemon→node',
|
|
394
|
+
op: 'engine wedge',
|
|
395
|
+
kind: 'wedged',
|
|
396
|
+
retry: { disposition: 'manual' },
|
|
397
|
+
message: label,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
/* best-effort — the doctrine wake below is the load-bearing notice */
|
|
402
|
+
}
|
|
403
|
+
try {
|
|
404
|
+
fanDoctrineWake(id, subscribersOf(id), label, {
|
|
405
|
+
reason: 'child-wedged',
|
|
406
|
+
child: id,
|
|
407
|
+
quiet_ms: quietForMs,
|
|
408
|
+
cpu_percent: cpuPercent,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
/* best-effort, mirrors every other fan-out in this file */
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// ---------------------------------------------------------------------------
|
|
212
416
|
// Auth-reload fan (one /login → every live broker follows)
|
|
213
417
|
// ---------------------------------------------------------------------------
|
|
214
418
|
/** Path of the shared pi credentials file every broker reads. VENDORED from pi
|
|
@@ -306,7 +510,10 @@ export function livenessVerdict(piPidAlive, deadFor) {
|
|
|
306
510
|
* the authority on refresh, and the dead-pid refresh branch below then
|
|
307
511
|
* revives it fresh. Fault recovery now runs in a separate pass: live,
|
|
308
512
|
* daemon-owned provider faults are retried by inbox nudge, while dead-pid
|
|
309
|
-
* crashes still fall through the ordinary resume path below.
|
|
513
|
+
* crashes still fall through the ordinary resume path below. Wedge
|
|
514
|
+
* detection (issue #110) runs here too: handleWedgeDetection no-ops unless
|
|
515
|
+
* the node is BUSY (mid-turn) and its heartbeat has gone quiet past a
|
|
516
|
+
* generous grace, corroborated by near-zero CPU.
|
|
310
517
|
* • pid null + intent==='idle-release' → dormant by choice; its pid may have
|
|
311
518
|
* just been cleared by boot reconciliation, but a null pid here is still
|
|
312
519
|
* that chosen dormancy, not a stranded relaunch — leave it, the second pass
|
|
@@ -337,6 +544,7 @@ async function handleNodeLiveness(row, now, revivedThisTick) {
|
|
|
337
544
|
if (pid != null && isPidAlive(pid)) {
|
|
338
545
|
unhealthySince.delete(id);
|
|
339
546
|
handleYieldStall(row, pid, now);
|
|
547
|
+
handleWedgeDetection(id, pid, now);
|
|
340
548
|
return;
|
|
341
549
|
}
|
|
342
550
|
if (pid == null) {
|
|
@@ -642,7 +850,7 @@ export function readPidfile() {
|
|
|
642
850
|
}
|
|
643
851
|
// isPidAlive now lives in canvas/pid.ts (the one shared signal-0 probe at the
|
|
644
852
|
// lowest layer, so canvas/, runtime/, AND daemon/ all import it down). Re-
|
|
645
|
-
// exported here to preserve the public surface consumed by commands/daemon.ts
|
|
853
|
+
// exported here to preserve the public surface consumed by commands/sys/daemon.ts
|
|
646
854
|
// and the grace-clock test.
|
|
647
855
|
export { isPidAlive };
|
|
648
856
|
/** True when a crtrd process is already running (pidfile exists + pid alive). */
|
package/dist/daemon/manage.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Daemon management helpers — importable without the full command tree.
|
|
2
2
|
//
|
|
3
|
-
// spawnDaemon() is the low-level spawn call shared by `crtr
|
|
3
|
+
// spawnDaemon() is the low-level spawn call shared by `crtr sys daemon start` and
|
|
4
4
|
// ensureDaemon(). ensureDaemon() is the silent "start if not running" front-
|
|
5
5
|
// door helper called by the canvas runtime before spawning child nodes.
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
@@ -68,7 +68,7 @@ export async function spawnDaemon() {
|
|
|
68
68
|
// Launch from the crouter-branded host binary so the daemon shows "crouter"
|
|
69
69
|
// in macOS Full Disk Access, not "node" (see branded-host). NOTE: when crtrd
|
|
70
70
|
// is launchd-owned, the plist's ProgramArguments must point at the branded
|
|
71
|
-
// binary too — this path only covers a manual `crtr
|
|
71
|
+
// binary too — this path only covers a manual `crtr sys daemon start`.
|
|
72
72
|
const entry = resolveCrtrdEntry();
|
|
73
73
|
const child = spawn(hostExecPath(), [entry], {
|
|
74
74
|
detached: true,
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
// a single self-contained canvas pi extension.
|
|
3
3
|
//
|
|
4
4
|
// Loaded into a canvas node's pi process via the node's launch.extensions list
|
|
5
|
-
// (registered in CANVAS_EXTENSIONS
|
|
6
|
-
//
|
|
7
|
-
// is absent (a plain pi session or legacy job agent loads it as a no-op).
|
|
5
|
+
// (registered in CANVAS_EXTENSIONS, src/core/runtime/launch.ts). INERT when
|
|
6
|
+
// CRTR_NODE_ID is absent (a plain pi session or legacy job agent loads it as a no-op).
|
|
8
7
|
//
|
|
9
8
|
// It owns the substrate's two new pi hooks:
|
|
10
9
|
//
|
|
@@ -479,8 +479,8 @@ export function registerCanvasNav(pi) {
|
|
|
479
479
|
}
|
|
480
480
|
if (isPlain(data, 'e')) {
|
|
481
481
|
// Expand → tmux: spread the cursor node's local subtree into a tiled tmux
|
|
482
|
-
// window (
|
|
483
|
-
shellCrtr(['
|
|
482
|
+
// window (`crtr surface tmux-spread`).
|
|
483
|
+
shellCrtr(['surface', 'tmux-spread', cursorId ?? nodeId], render);
|
|
484
484
|
render();
|
|
485
485
|
return { consume: true };
|
|
486
486
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
type PiEvents = 'agent_start' | 'turn_end' | 'agent_end' | 'session_shutdown' | 'session_start' | 'tool_execution_start' | 'tool_execution_end';
|
|
1
|
+
type PiEvents = 'agent_start' | 'turn_end' | 'agent_end' | 'session_shutdown' | 'session_start' | 'tool_execution_start' | 'tool_execution_end' | 'tool_execution_update' | 'message_update';
|
|
2
2
|
interface PiLike {
|
|
3
3
|
on: (event: PiEvents, handler: (event: any, ctx: any) => void | Promise<void>) => void;
|
|
4
4
|
sendUserMessage: (content: string, options?: {
|
|
@@ -30,7 +30,7 @@ import { writeFileSync, mkdirSync, existsSync, readFileSync } from 'node:fs';
|
|
|
30
30
|
import { join } from 'node:path';
|
|
31
31
|
import { getNode, jobDir, updateNode, recordPid, setPresence } from '../core/canvas/index.js';
|
|
32
32
|
import { transition } from '../core/runtime/lifecycle.js';
|
|
33
|
-
import { markBusy, clearBusy } from '../core/runtime/busy.js';
|
|
33
|
+
import { markBusy, clearBusy, touchBusy } from '../core/runtime/busy.js';
|
|
34
34
|
import { recordFault, clearFault } from '../core/runtime/fault.js';
|
|
35
35
|
import { classify } from '../core/fault-classifier.js';
|
|
36
36
|
import { evaluateStop } from '../core/runtime/stop-guard.js';
|
|
@@ -241,6 +241,25 @@ export function registerCanvasStophook(pi) {
|
|
|
241
241
|
// checkpoint roadmap + yield; a resident base (root chat) to promote-or-wrap-
|
|
242
242
|
// up (it has no roadmap); a terminal worker to promote / push final.
|
|
243
243
|
const firedBands = new Set();
|
|
244
|
+
// Wedge-heartbeat throttle: message_update/tool_execution_update can fire many
|
|
245
|
+
// times per second while streaming, and touchBusy is a disk write — rate-limit
|
|
246
|
+
// it to at most once per HEARTBEAT_THROTTLE_MS. The daemon's wedge grace is
|
|
247
|
+
// measured in minutes, so this throttle costs it nothing. Also clears any
|
|
248
|
+
// stale `daemon→node` wedge fault at the same throttled cadence: a sign of
|
|
249
|
+
// life should both refresh the heartbeat marker AND clear a stale wedge
|
|
250
|
+
// notice as one coherent act, same as every coarser progress site below
|
|
251
|
+
// (tool_execution_start/end, turn_end, agent_end) already does. clearFault is
|
|
252
|
+
// a cheap no-op when no fault exists, so this stays safe to call every time.
|
|
253
|
+
const HEARTBEAT_THROTTLE_MS = 3_000;
|
|
254
|
+
let lastHeartbeatAt = 0;
|
|
255
|
+
const heartbeat = () => {
|
|
256
|
+
const now = Date.now();
|
|
257
|
+
if (now - lastHeartbeatAt < HEARTBEAT_THROTTLE_MS)
|
|
258
|
+
return;
|
|
259
|
+
lastHeartbeatAt = now;
|
|
260
|
+
touchBusy(nodeId);
|
|
261
|
+
clearFault(nodeId, { link: 'daemon→node' });
|
|
262
|
+
};
|
|
244
263
|
// ---------------------------------------------------------------------------
|
|
245
264
|
// session_start — capture pi's session id, and detect `/new`.
|
|
246
265
|
//
|
|
@@ -336,6 +355,36 @@ export function registerCanvasStophook(pi) {
|
|
|
336
355
|
try {
|
|
337
356
|
lastActivity = summarizeActivity(String(event?.toolName ?? ''), event?.args);
|
|
338
357
|
flushTelemetry(jobDirPath, totalIn, totalOut, model, lastContext, lastActivity);
|
|
358
|
+
touchBusy(nodeId); // wedge heartbeat: a fresh tool starting is a sign of life
|
|
359
|
+
clearFault(nodeId, { link: 'daemon→node' }); // a tool actually starting clears any prior wedge notice
|
|
360
|
+
}
|
|
361
|
+
catch { /* best-effort */ }
|
|
362
|
+
});
|
|
363
|
+
// ---------------------------------------------------------------------------
|
|
364
|
+
// tool_execution_update — wedge heartbeat + fault clear only. Fired with
|
|
365
|
+
// partial/streaming tool output (when a tool reports progress); throttled via
|
|
366
|
+
// `heartbeat()` since it can arrive many times per second. Most tools (e.g.
|
|
367
|
+
// bash) don't stream progress today, so this is currently a light-touch no-op
|
|
368
|
+
// for them — it exists so any tool that DOES stream partial output (now or in
|
|
369
|
+
// the future) counts as live progress instead of tripping the wedge detector,
|
|
370
|
+
// AND clears any wedge notice that already fired before this progress resumed.
|
|
371
|
+
// ---------------------------------------------------------------------------
|
|
372
|
+
pi.on('tool_execution_update', () => {
|
|
373
|
+
try {
|
|
374
|
+
heartbeat();
|
|
375
|
+
}
|
|
376
|
+
catch { /* best-effort */ }
|
|
377
|
+
});
|
|
378
|
+
// ---------------------------------------------------------------------------
|
|
379
|
+
// message_update — wedge heartbeat + fault clear only. Fired token-by-token
|
|
380
|
+
// while the assistant is actively streaming a reply — genuine progress,
|
|
381
|
+
// throttled the same way, and equally clears a stale wedge notice if streaming
|
|
382
|
+
// resumes before a coarser boundary. A node stuck streaming forever without
|
|
383
|
+
// ever firing this is exactly the daemon's busy+quiet wedge signature.
|
|
384
|
+
// ---------------------------------------------------------------------------
|
|
385
|
+
pi.on('message_update', () => {
|
|
386
|
+
try {
|
|
387
|
+
heartbeat();
|
|
339
388
|
}
|
|
340
389
|
catch { /* best-effort */ }
|
|
341
390
|
});
|
|
@@ -353,6 +402,8 @@ export function registerCanvasStophook(pi) {
|
|
|
353
402
|
// refreshes on its first tool — the desired outcome.)
|
|
354
403
|
pi.on('tool_execution_end', (_event, ctx) => {
|
|
355
404
|
try {
|
|
405
|
+
touchBusy(nodeId); // wedge heartbeat: the tool returning is a sign of life
|
|
406
|
+
clearFault(nodeId, { link: 'daemon→node' }); // a tool that was "stuck" just returned — the wedge notice was resolved
|
|
356
407
|
if (getNode(nodeId)?.intent === 'refresh') {
|
|
357
408
|
clearBusy(nodeId); // agent_end won't run to clear it; harmless if it lingers
|
|
358
409
|
try {
|
|
@@ -422,6 +473,8 @@ export function registerCanvasStophook(pi) {
|
|
|
422
473
|
lastContext = contextTokens;
|
|
423
474
|
// Fire-and-forget: flushTelemetry uses synchronous fs writes and never throws.
|
|
424
475
|
flushTelemetry(jobDirPath, totalIn, totalOut, model, contextTokens, lastActivity);
|
|
476
|
+
touchBusy(nodeId); // wedge heartbeat: a turn ending is a sign of life
|
|
477
|
+
clearFault(nodeId, { link: 'daemon→node' }); // reaching turn_end proves this turn wasn't wedged
|
|
425
478
|
// Context-size steering: fire the current band once, with lifecycle-specific
|
|
426
479
|
// guidance (lifecycle is read live — a terminal worker may have promoted to
|
|
427
480
|
// resident since launch).
|
|
@@ -473,8 +526,12 @@ export function registerCanvasStophook(pi) {
|
|
|
473
526
|
// removed, so the handler no longer needs to be async — the node reaches its
|
|
474
527
|
// subscribers ONLY through its own explicit `crtr push` calls.
|
|
475
528
|
// The turn has ended regardless of how it routes below — clear the mid-turn
|
|
476
|
-
// marker FIRST so a focus-away from this now-parked node despawns it.
|
|
529
|
+
// marker FIRST so a focus-away from this now-parked node despawns it. Also
|
|
530
|
+
// clear any wedge notice: reaching agent_end at all proves the turn ended
|
|
531
|
+
// and the engine was never permanently stuck (covers an abort/error path
|
|
532
|
+
// that skips the tool_execution_end/turn_end clear sites above).
|
|
477
533
|
clearBusy(nodeId);
|
|
534
|
+
clearFault(nodeId, { link: 'daemon→node' });
|
|
478
535
|
try {
|
|
479
536
|
const messages = Array.isArray(event?.messages) ? event.messages : [];
|
|
480
537
|
// Accumulate tokens from the final batch (edge case: a turn that fired
|
package/dist/types.d.ts
CHANGED
|
@@ -100,6 +100,29 @@ export interface ModelLaddersConfig {
|
|
|
100
100
|
anthropic: Record<ModelStrength, string>;
|
|
101
101
|
openai: Record<ModelStrength, string>;
|
|
102
102
|
}
|
|
103
|
+
/** Launch metadata for one kind (a top-level kind like `developer`, or a full
|
|
104
|
+
* sub-kind string like `plan/reviewers/security`) — the settings-file
|
|
105
|
+
* replacement for per-kind persona frontmatter. `model`/`tools`/`extensions`
|
|
106
|
+
* are launch knobs consumed by `buildLaunchSpec` (Phase 3); `whenToUse` is the
|
|
107
|
+
* one-line gloss shown in `node new -h` / `node promote -h`. `availableTo` is
|
|
108
|
+
* VISIBILITY-ONLY (never launch validation): the list of top-level kind names
|
|
109
|
+
* whose spawn menus surface this sub-kind, `'*'` meaning every kind; omitted
|
|
110
|
+
* defaults to the sub-kind's own top-level ancestor (e.g.
|
|
111
|
+
* `plan/reviewers/security` defaults to `['plan']`) — this reproduces the
|
|
112
|
+
* original sub-persona `availableTo` semantics. A direct launch by full kind
|
|
113
|
+
* string is always valid regardless of `availableTo`. */
|
|
114
|
+
export interface KindConfig {
|
|
115
|
+
model?: string;
|
|
116
|
+
/** Model override for ORCHESTRATOR mode only, e.g. a kind whose orchestrator
|
|
117
|
+
* body warrants a stronger model than its base worker. Selection: base mode
|
|
118
|
+
* always uses `model`; orchestrator mode uses `orchestratorModel ?? model`.
|
|
119
|
+
* Omit when the kind runs the same model in both modes. */
|
|
120
|
+
orchestratorModel?: string;
|
|
121
|
+
tools?: string[];
|
|
122
|
+
extensions?: string[];
|
|
123
|
+
whenToUse: string;
|
|
124
|
+
availableTo?: string[];
|
|
125
|
+
}
|
|
103
126
|
export interface ScopeConfig {
|
|
104
127
|
schema_version: number;
|
|
105
128
|
marketplaces: Record<string, ConfigMarketplaceEntry>;
|
|
@@ -108,7 +131,12 @@ export interface ScopeConfig {
|
|
|
108
131
|
max_panes_per_window: number;
|
|
109
132
|
canvasNav: CanvasNavConfig;
|
|
110
133
|
modelLadders: ModelLaddersConfig;
|
|
111
|
-
|
|
134
|
+
/** The kind registry (spec §1.5): kind existence + launch knobs, keyed by
|
|
135
|
+
* full kind string (top-level e.g. `developer`, or sub-kind e.g.
|
|
136
|
+
* `plan/reviewers/security`). Builtins ship a default registry via
|
|
137
|
+
* `defaultScopeConfig()`; user/project `config.json` adds or shadows
|
|
138
|
+
* entries at the same scope precedence as the rest of `ScopeConfig`. */
|
|
139
|
+
kinds: Record<string, KindConfig>;
|
|
112
140
|
}
|
|
113
141
|
export interface ScopeState {
|
|
114
142
|
marketplaces: Record<string, {
|
|
@@ -158,6 +186,23 @@ export interface InstalledMarketplace {
|
|
|
158
186
|
url: string;
|
|
159
187
|
ref: string;
|
|
160
188
|
}
|
|
189
|
+
/** The on-disk manifest at `~/.crouter/profiles/<slug>-<id>/profile.json`
|
|
190
|
+
* (spec §2.1). A profile is an agent identity defined at the user root: it
|
|
191
|
+
* names N project directories and owns its own `memory/` store. `projects`
|
|
192
|
+
* are absolute, real-path-resolved directories in manifest order — the
|
|
193
|
+
* pointer set `findProjectScopeRoots` walks (Phase 5), after the node's own
|
|
194
|
+
* cwd. `name` is the mutable display name (and the gateable `profile` subject
|
|
195
|
+
* field, Phase 6); `<slug>-<id>` in the directory name is the stable id and
|
|
196
|
+
* never changes on rename. See `src/core/profiles/manifest.ts` for every
|
|
197
|
+
* read/write of this file — it is the sole owner of path resolution and
|
|
198
|
+
* mutation. */
|
|
199
|
+
export interface ProfileManifest {
|
|
200
|
+
schema_version: number;
|
|
201
|
+
name: string;
|
|
202
|
+
projects: string[];
|
|
203
|
+
created_at: string;
|
|
204
|
+
last_used_at: string | null;
|
|
205
|
+
}
|
|
161
206
|
export declare const PLUGIN_MANIFEST_DIR = ".crouter-plugin";
|
|
162
207
|
export declare const PLUGIN_MANIFEST_FILE = "plugin.json";
|
|
163
208
|
export declare const MARKETPLACE_MANIFEST_DIR = ".crouter-marketplace";
|
|
@@ -166,8 +211,19 @@ export declare const CRTR_DIR_NAME = ".crouter";
|
|
|
166
211
|
export declare const CONFIG_FILE = "config.json";
|
|
167
212
|
export declare const STATE_FILE = "state.json";
|
|
168
213
|
export declare const AGENTS_DIR = "agents";
|
|
214
|
+
export declare const PROFILE_DIR = "profiles";
|
|
169
215
|
export declare const DEFAULT_MAX_PANES_PER_WINDOW = 3;
|
|
170
216
|
export declare function defaultScopeConfig(): ScopeConfig;
|
|
217
|
+
/** The builtin kind registry (spec §1.5): the built-in defaults for every
|
|
218
|
+
* top-level kind and its sub-persona kinds. `whenToUse`/`model` are the
|
|
219
|
+
* one-per-kind defaults (not one per mode) that seed a node's spawn menu
|
|
220
|
+
* and default model choice. Roadmap-shaping guidance is orchestrator-only —
|
|
221
|
+
* an orchestrator-gated memory doc keyed to the kind, not baked into the
|
|
222
|
+
* registry entry. Sub-persona `availableTo` is
|
|
223
|
+
* omitted where it only reproduces the default (its own top-level
|
|
224
|
+
* ancestor) — `product/teardown` is included explicitly below for parity
|
|
225
|
+
* even though its value equals the default. */
|
|
226
|
+
export declare function defaultKindsConfig(): Record<string, KindConfig>;
|
|
171
227
|
/** Built-in canvas-nav binds so an absent config still gives the intended UX.
|
|
172
228
|
* prefixBinds restate the current menu defaults; graphBinds stay empty.
|
|
173
229
|
*/
|
package/dist/types.js
CHANGED
|
@@ -17,6 +17,9 @@ export const STATE_FILE = 'state.json';
|
|
|
17
17
|
// Subagent definitions live as flat `<name>.md` files under `<root>/agents/`,
|
|
18
18
|
// for both scope roots and plugins.
|
|
19
19
|
export const AGENTS_DIR = 'agents';
|
|
20
|
+
// Profile stores live as `<slug>-<id>/` dirs under `<user-scope-root>/profiles/`
|
|
21
|
+
// (never a project/builtin scope — profiles are a user-root-only concept).
|
|
22
|
+
export const PROFILE_DIR = 'profiles';
|
|
20
23
|
export const DEFAULT_MAX_PANES_PER_WINDOW = 3;
|
|
21
24
|
export function defaultScopeConfig() {
|
|
22
25
|
return {
|
|
@@ -27,7 +30,86 @@ export function defaultScopeConfig() {
|
|
|
27
30
|
max_panes_per_window: DEFAULT_MAX_PANES_PER_WINDOW,
|
|
28
31
|
canvasNav: defaultCanvasNavConfig(),
|
|
29
32
|
modelLadders: defaultModelLaddersConfig(),
|
|
30
|
-
|
|
33
|
+
kinds: defaultKindsConfig(),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** The builtin kind registry (spec §1.5): the built-in defaults for every
|
|
37
|
+
* top-level kind and its sub-persona kinds. `whenToUse`/`model` are the
|
|
38
|
+
* one-per-kind defaults (not one per mode) that seed a node's spawn menu
|
|
39
|
+
* and default model choice. Roadmap-shaping guidance is orchestrator-only —
|
|
40
|
+
* an orchestrator-gated memory doc keyed to the kind, not baked into the
|
|
41
|
+
* registry entry. Sub-persona `availableTo` is
|
|
42
|
+
* omitted where it only reproduces the default (its own top-level
|
|
43
|
+
* ancestor) — `product/teardown` is included explicitly below for parity
|
|
44
|
+
* even though its value equals the default. */
|
|
45
|
+
export function defaultKindsConfig() {
|
|
46
|
+
return {
|
|
47
|
+
general: {
|
|
48
|
+
whenToUse: 'Anything else — the catch-all worker for a task that does not fit a specialist kind.',
|
|
49
|
+
model: 'anthropic/strong',
|
|
50
|
+
},
|
|
51
|
+
explore: {
|
|
52
|
+
whenToUse: 'Map unfamiliar code or architecture — read-only orientation and code-path research with concrete file:line evidence. Anything diagnostic — investigating a failure, why something is broken or misbehaving, or live/runtime behavior — is advisor, not explore.',
|
|
53
|
+
model: 'anthropic/light',
|
|
54
|
+
},
|
|
55
|
+
developer: {
|
|
56
|
+
whenToUse: 'Implement a change — make the feature or fix genuinely work against its acceptance criteria, not merely compile.',
|
|
57
|
+
model: 'anthropic/medium',
|
|
58
|
+
orchestratorModel: 'anthropic/strong',
|
|
59
|
+
},
|
|
60
|
+
design: {
|
|
61
|
+
whenToUse: 'Architect a solution — produce one design document an implementer can build from without re-deciding anything left open.',
|
|
62
|
+
model: 'anthropic/strong',
|
|
63
|
+
},
|
|
64
|
+
plan: {
|
|
65
|
+
whenToUse: 'Break work into steps — turn a spec or design into a concrete, phased, parallelizable plan with every decision resolved.',
|
|
66
|
+
model: 'openai/strong',
|
|
67
|
+
},
|
|
68
|
+
spec: {
|
|
69
|
+
whenToUse: 'Collaborate with the user to discover what to build, then write the spec — behavior, non-goals, interfaces, edge cases, testable acceptance criteria.',
|
|
70
|
+
model: 'anthropic/strong',
|
|
71
|
+
},
|
|
72
|
+
product: {
|
|
73
|
+
whenToUse: 'Discover the real user need behind a request and define the product experience that meets it — use cases, how it should feel and look, grounded in how comparable products solve it. Hands off to spec.',
|
|
74
|
+
model: 'anthropic/strong',
|
|
75
|
+
},
|
|
76
|
+
review: {
|
|
77
|
+
whenToUse: 'Validate or critique code, a plan, or a spec — deliver a complete, severity-rated verdict without adjudicating.',
|
|
78
|
+
model: 'openai/strong',
|
|
79
|
+
},
|
|
80
|
+
advisor: {
|
|
81
|
+
whenToUse: 'Debug failures, investigate why something is broken or misbehaving, diagnose live/runtime issues, or give engineering advice and second opinions — reason from evidence and recommend the next move. Use advisor (not explore) whenever the task is to find out what is going wrong.',
|
|
82
|
+
model: 'anthropic/ultra',
|
|
83
|
+
},
|
|
84
|
+
'plan/reviewers/requirements-coverage': {
|
|
85
|
+
whenToUse: 'every requirement and design constraint maps to a concrete plan task, classified Covered/Partial/Missing; flags only blocking gaps',
|
|
86
|
+
model: 'openai/strong',
|
|
87
|
+
},
|
|
88
|
+
'plan/reviewers/pattern-consistency': {
|
|
89
|
+
whenToUse: "the plan honors the codebase's real conventions; reads actual source and cites the pattern each finding deviates from; owns contract-level conflicts between parts",
|
|
90
|
+
model: 'openai/strong',
|
|
91
|
+
},
|
|
92
|
+
'plan/reviewers/code-smells': {
|
|
93
|
+
whenToUse: 'nullability mismatches, type conflicts across parts, hidden N+1s, over-fetching, missing error boundaries, leaky abstractions; owns file-level conflicts between parts',
|
|
94
|
+
model: 'openai/strong',
|
|
95
|
+
},
|
|
96
|
+
'plan/reviewers/security': {
|
|
97
|
+
whenToUse: 'input validation, injection surfaces, auth/authz gaps, data exposure, races; flags only risks with a concrete exploit path',
|
|
98
|
+
model: 'openai/strong',
|
|
99
|
+
},
|
|
100
|
+
'plan/reviewers/architecture-fit': {
|
|
101
|
+
whenToUse: "proposed files/modules/abstractions fit the system's existing decomposition; flags new units that duplicate existing ones or cross layer boundaries",
|
|
102
|
+
model: 'openai/strong',
|
|
103
|
+
},
|
|
104
|
+
'spec/requirements': {
|
|
105
|
+
whenToUse: 'Derive testable EARS requirements from a finished, approved design — in isolation, from the rendered design text alone.',
|
|
106
|
+
model: 'anthropic/strong',
|
|
107
|
+
},
|
|
108
|
+
'product/teardown': {
|
|
109
|
+
whenToUse: 'Tear down ONE comparable product against a specific job-to-be-done — how it solves it, where it succeeds and fails its users, and what to borrow, avoid, or differentiate from.',
|
|
110
|
+
model: 'anthropic/strong',
|
|
111
|
+
availableTo: ['product'],
|
|
112
|
+
},
|
|
31
113
|
};
|
|
32
114
|
}
|
|
33
115
|
/** Built-in canvas-nav binds so an absent config still gives the intended UX.
|