@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/commands/push.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
// `crtr push`
|
|
1
|
+
// `crtr push` — the one verb up the graph.
|
|
2
2
|
//
|
|
3
3
|
// crtr push update [body] — routine progress to subscribers
|
|
4
4
|
// crtr push urgent [body] — force-wake subscribers
|
|
5
5
|
// crtr push final [body] — finish: write result, mark node done, close window
|
|
6
|
-
// crtr feed read — drain the caller's (or a named) inbox into a digest
|
|
7
|
-
// crtr feed peek — live state of the nodes below you, cursor untouched
|
|
8
6
|
//
|
|
9
7
|
// "push" is THE verb a node uses to talk to its managers; the tier is the
|
|
10
8
|
// subcommand. The caller's node is resolved from CRTR_NODE_ID (injected by the
|
|
@@ -13,8 +11,7 @@
|
|
|
13
11
|
import { defineBranch, defineLeaf } from '../core/command.js';
|
|
14
12
|
import { InputError, readStdinRaw } from '../core/io.js';
|
|
15
13
|
import { push } from '../core/feed/feed.js';
|
|
16
|
-
import { getNode, subscribersOf
|
|
17
|
-
import { readInboxSince, readInboxEntryById, readCursor, writeCursor, coalesce, } from '../core/feed/inbox.js';
|
|
14
|
+
import { getNode, subscribersOf } from '../core/canvas/index.js';
|
|
18
15
|
function requireCallerNode() {
|
|
19
16
|
const id = process.env['CRTR_NODE_ID'];
|
|
20
17
|
if (id === undefined || id.trim() === '') {
|
|
@@ -142,213 +139,6 @@ function makeTierLeaf(tier) {
|
|
|
142
139
|
});
|
|
143
140
|
}
|
|
144
141
|
// ---------------------------------------------------------------------------
|
|
145
|
-
// feed read — drain the inbox
|
|
146
|
-
// ---------------------------------------------------------------------------
|
|
147
|
-
const feedReadLeaf = defineLeaf({
|
|
148
|
-
name: 'read',
|
|
149
|
-
description: 'drain unread pointers into a digest',
|
|
150
|
-
whenToUse: 'you want to PROACTIVELY poll what the nodes you subscribe to — your children and anyone you follow — have reported before the watcher wakes you, draining the unread pointers in your inbox into one coalesced digest. NOTE: when a subscriber push wakes you, that wake message already IS this digest (the watcher drains your inbox to wake you), so don\'t re-run feed read to "open" it — dereference the refs in the digest you already have. Reach for it to poll before the next wake, to inspect another node\'s inbox, or to re-read the whole history after the cursor has advanced.',
|
|
151
|
-
help: {
|
|
152
|
-
name: 'feed read',
|
|
153
|
-
summary: 'drain unread inbox pointers for the caller (or a named node) into a compact digest',
|
|
154
|
-
params: [
|
|
155
|
-
{ kind: 'flag', name: 'node', type: 'string', required: false, constraint: 'Node whose inbox to read. Defaults to CRTR_NODE_ID. Use to inspect a worker\'s inbox as an orchestrator.' },
|
|
156
|
-
{ kind: 'flag', name: 'all', type: 'bool', required: false, default: false, constraint: 'Ignore the cursor and return everything from the start — use to re-read history the wake already drained.' },
|
|
157
|
-
],
|
|
158
|
-
output: [
|
|
159
|
-
{ name: 'node_id', type: 'string', required: true, constraint: 'Node whose inbox was read.' },
|
|
160
|
-
{ name: 'digest', type: 'string', required: true, constraint: 'Coalesced digest — paste directly into a prompt.' },
|
|
161
|
-
{ name: 'entries', type: 'object[]', required: true, constraint: 'Raw InboxEntry objects.' },
|
|
162
|
-
{ name: 'cursor', type: 'string', required: true, constraint: 'New cursor ISO written after draining.' },
|
|
163
|
-
{ name: 'inbox_total', type: 'number', required: true, constraint: 'Total entries in the inbox (read + unread). Distinguishes a never-used inbox from one already drained at wake.' },
|
|
164
|
-
],
|
|
165
|
-
outputKind: 'object',
|
|
166
|
-
effects: ['Advances nodes/<nodeId>/inbox.jsonl.cursor.', 'Read-only on inbox.jsonl itself.'],
|
|
167
|
-
},
|
|
168
|
-
run: async (input) => {
|
|
169
|
-
const nodeId = typeof input['node'] === 'string' && input['node'].trim() !== ''
|
|
170
|
-
? input['node'].trim()
|
|
171
|
-
: requireCallerNode();
|
|
172
|
-
const cursor = input['all'] === true ? undefined : readCursor(nodeId);
|
|
173
|
-
const entries = readInboxSince(nodeId, cursor);
|
|
174
|
-
const total = readInboxSince(nodeId, undefined).length;
|
|
175
|
-
const newCursor = entries.length > 0 ? entries[entries.length - 1].ts : cursor ?? new Date().toISOString();
|
|
176
|
-
writeCursor(nodeId, newCursor);
|
|
177
|
-
return {
|
|
178
|
-
node_id: nodeId,
|
|
179
|
-
digest: coalesce(entries),
|
|
180
|
-
entries: entries,
|
|
181
|
-
cursor: newCursor,
|
|
182
|
-
inbox_total: total,
|
|
183
|
-
};
|
|
184
|
-
},
|
|
185
|
-
render: (r) => {
|
|
186
|
-
const n = Array.isArray(r['entries']) ? r['entries'].length : 0;
|
|
187
|
-
const rawDigest = typeof r['digest'] === 'string' ? r['digest'] : '';
|
|
188
|
-
if (n > 0 && rawDigest.trim() !== '') {
|
|
189
|
-
return `${n} unread pointer${n === 1 ? '' : 's'} drained from ${r['node_id']}'s inbox.\n\n${rawDigest.trim()}`;
|
|
190
|
-
}
|
|
191
|
-
// Empty drain has two distinct causes — say which, honestly. An inbox that
|
|
192
|
-
// already holds entries was drained when the watcher woke you (the wake
|
|
193
|
-
// message WAS that digest); a never-used inbox simply has nothing yet.
|
|
194
|
-
const total = typeof r['inbox_total'] === 'number' ? r['inbox_total'] : 0;
|
|
195
|
-
const explanation = total > 0
|
|
196
|
-
? 'Your inbox is not empty, though. The watcher drains your inbox to wake you, so the entries you already saw in your wake message are the same ones you would read here; that is why this is empty. Re-read the whole history (full message bodies included) with `crtr feed read --all`, or dereference the refs from the wake digest you already have.'
|
|
197
|
-
: 'Your inbox is empty — nothing has arrived from your subscriptions yet. Expected while workers run: a worker that has not pushed yet leaves no pointer, and it will wake you the moment it does. The wake is automatic — just continue your own work or end your turn. (Reach for `crtr feed peek` only if you suspect a worker died, not to confirm a live one.)';
|
|
198
|
-
return `No unread pointers for ${r['node_id']} (unread=0).\n\n${explanation}`;
|
|
199
|
-
},
|
|
200
|
-
});
|
|
201
|
-
// ---------------------------------------------------------------------------
|
|
202
|
-
// feed message — read one inbox message's full body back from the jsonl
|
|
203
|
-
// ---------------------------------------------------------------------------
|
|
204
|
-
const feedMessageLeaf = defineLeaf({
|
|
205
|
-
name: 'message',
|
|
206
|
-
description: 'print one inbox entry\'s full body by id',
|
|
207
|
-
whenToUse: 'a digest line ended in `… (clipped — full message: crtr feed message <id>)` — run it to read the rest of that direct message. The full body lives in your inbox.jsonl; this prints it verbatim by the entry\'s short id. Read-only: it does NOT advance your inbox cursor.',
|
|
208
|
-
help: {
|
|
209
|
-
name: 'feed message',
|
|
210
|
-
summary: 'print the full body of one inbox entry, addressed by the short id shown in a clipped digest line',
|
|
211
|
-
params: [
|
|
212
|
-
{ kind: 'positional', name: 'id', required: true, constraint: 'The short entry id from the digest\'s `… (full message: …)` pointer.' },
|
|
213
|
-
{ kind: 'flag', name: 'node', type: 'string', required: false, constraint: 'Whose inbox to read. Defaults to CRTR_NODE_ID; pass to read another node\'s inbox as an orchestrator.' },
|
|
214
|
-
],
|
|
215
|
-
output: [
|
|
216
|
-
{ name: 'node_id', type: 'string', required: true, constraint: 'Node whose inbox was read.' },
|
|
217
|
-
{ name: 'id', type: 'string', required: true, constraint: 'The requested entry id.' },
|
|
218
|
-
{ name: 'found', type: 'boolean', required: true, constraint: 'True when an entry with that id exists.' },
|
|
219
|
-
{ name: 'from', type: 'string', required: false, constraint: 'Sender of the message, when found.' },
|
|
220
|
-
{ name: 'body', type: 'string', required: false, constraint: 'Full message body, when found.' },
|
|
221
|
-
],
|
|
222
|
-
outputKind: 'object',
|
|
223
|
-
effects: ['Read-only: scans inbox.jsonl. Does NOT advance the cursor.'],
|
|
224
|
-
},
|
|
225
|
-
run: async (input) => {
|
|
226
|
-
const nodeId = typeof input['node'] === 'string' && input['node'].trim() !== ''
|
|
227
|
-
? input['node'].trim()
|
|
228
|
-
: requireCallerNode();
|
|
229
|
-
const id = input['id'].trim();
|
|
230
|
-
const entry = readInboxEntryById(nodeId, id);
|
|
231
|
-
if (entry === undefined) {
|
|
232
|
-
return { node_id: nodeId, id, found: false };
|
|
233
|
-
}
|
|
234
|
-
const body = typeof entry.data?.['body'] === 'string' ? entry.data['body'] : entry.label;
|
|
235
|
-
return { node_id: nodeId, id, found: true, from: entry.from ?? 'system', body };
|
|
236
|
-
},
|
|
237
|
-
render: (r) => {
|
|
238
|
-
if (r['found'] !== true) {
|
|
239
|
-
return `No inbox entry with id ${r['id']} in ${r['node_id']}'s inbox. Re-read the digest with \`crtr feed read --all\`, or list raw entries to find the right id.`;
|
|
240
|
-
}
|
|
241
|
-
return `Full message ${r['id']} (from ${r['from']}):\n\n${r['body']}`;
|
|
242
|
-
},
|
|
243
|
-
});
|
|
244
|
-
// ---------------------------------------------------------------------------
|
|
245
|
-
// feed peek — live state of the nodes below you, without draining anything
|
|
246
|
-
// ---------------------------------------------------------------------------
|
|
247
|
-
const STATUS_GLYPH = {
|
|
248
|
-
active: '●', idle: '○', done: '✓', dead: '✗', canceled: '⊘',
|
|
249
|
-
};
|
|
250
|
-
/** Coarse "Nm ago" age from an ISO timestamp — enough to read staleness at a glance. */
|
|
251
|
-
function fmtAge(iso) {
|
|
252
|
-
const ms = Date.now() - new Date(iso).getTime();
|
|
253
|
-
if (!Number.isFinite(ms) || ms < 0)
|
|
254
|
-
return 'just now';
|
|
255
|
-
const s = Math.floor(ms / 1000);
|
|
256
|
-
if (s < 60)
|
|
257
|
-
return `${s}s ago`;
|
|
258
|
-
const m = Math.floor(s / 60);
|
|
259
|
-
if (m < 60)
|
|
260
|
-
return `${m}m ago`;
|
|
261
|
-
const h = Math.floor(m / 60);
|
|
262
|
-
if (h < 24)
|
|
263
|
-
return `${h}h ago`;
|
|
264
|
-
return `${Math.floor(h / 24)}d ago`;
|
|
265
|
-
}
|
|
266
|
-
const feedPeekLeaf = defineLeaf({
|
|
267
|
-
name: 'peek',
|
|
268
|
-
description: 'live state of the nodes below you, without draining anything',
|
|
269
|
-
whenToUse: 'you are about to end a turn and want to confirm your workers are running before you chill — peek shows every node you subscribe to (the workers below you) with its live status (working/idle/done/dead), how long it has run, its cycle count, and whether it has pushed yet, plus a one-line verdict on whether it is safe to yield. Non-destructive: it never advances your inbox cursor, so a later `feed read` still delivers undrained reports. Reach for it exactly when the feed reads empty but you have outstanding children — that empty feed is EXPECTED (a worker that has not pushed yet contributes no inbox pointer); peek confirms those workers are alive and running async so you can stop and wait instead of polling.',
|
|
270
|
-
help: {
|
|
271
|
-
name: 'feed peek',
|
|
272
|
-
summary: 'show the live state of every node you subscribe to (the workers below you) with a yield-or-not verdict; never drains the inbox',
|
|
273
|
-
params: [
|
|
274
|
-
{ kind: 'flag', name: 'node', type: 'string', required: false, constraint: 'Node whose subscriptions to peek. Defaults to CRTR_NODE_ID. Use to inspect a worker\'s downstream as an orchestrator.' },
|
|
275
|
-
],
|
|
276
|
-
output: [
|
|
277
|
-
{ name: 'node_id', type: 'string', required: true, constraint: 'Node that was peeked.' },
|
|
278
|
-
{ name: 'unread', type: 'number', required: true, constraint: 'Inbox pointers not yet drained by `feed read`.' },
|
|
279
|
-
{ name: 'children', type: 'object[]', required: true, constraint: 'One row per subscription: {node_id, kind, name, status, active, spawned, cycles, last_push}.' },
|
|
280
|
-
],
|
|
281
|
-
outputKind: 'object',
|
|
282
|
-
effects: ['Read-only: reads canvas.db edges + node metas + inbox.jsonl.', 'Does NOT advance the inbox cursor — peek leaves the feed intact for a later `feed read`.'],
|
|
283
|
-
},
|
|
284
|
-
run: async (input) => {
|
|
285
|
-
const nodeId = typeof input['node'] === 'string' && input['node'].trim() !== ''
|
|
286
|
-
? input['node'].trim()
|
|
287
|
-
: requireCallerNode();
|
|
288
|
-
// Read the WHOLE inbox to find each child's last push, but with no cursor
|
|
289
|
-
// write — peek is non-destructive by contract. `unread` is computed from the
|
|
290
|
-
// persisted cursor so peek can tell you a `feed read` would deliver something.
|
|
291
|
-
const all = readInboxSince(nodeId, undefined);
|
|
292
|
-
const unread = readInboxSince(nodeId, readCursor(nodeId)).length;
|
|
293
|
-
const children = subscriptionsOf(nodeId).map((s) => {
|
|
294
|
-
const n = getNode(s.node_id);
|
|
295
|
-
const fromMe = all.filter((e) => e.from === s.node_id);
|
|
296
|
-
const last = fromMe.length > 0 ? fromMe[fromMe.length - 1] : undefined;
|
|
297
|
-
return {
|
|
298
|
-
node_id: s.node_id,
|
|
299
|
-
kind: n?.kind ?? '?',
|
|
300
|
-
name: n !== null ? fullName(n) : s.node_id,
|
|
301
|
-
status: n?.status ?? 'dead',
|
|
302
|
-
active: s.active,
|
|
303
|
-
spawned: n?.created ?? s.created,
|
|
304
|
-
cycles: n?.cycles ?? 0,
|
|
305
|
-
last_push: last !== undefined
|
|
306
|
-
? { kind: last.kind, ts: last.ts, ref: last.ref ?? null, label: last.label }
|
|
307
|
-
: null,
|
|
308
|
-
};
|
|
309
|
-
});
|
|
310
|
-
return { node_id: nodeId, unread, children };
|
|
311
|
-
},
|
|
312
|
-
render: (r) => {
|
|
313
|
-
const id = r['node_id'];
|
|
314
|
-
const unread = r['unread'] ?? 0;
|
|
315
|
-
const kids = r['children'] ?? [];
|
|
316
|
-
if (kids.length === 0) {
|
|
317
|
-
const tail = unread > 0
|
|
318
|
-
? `${unread} unread report${unread === 1 ? '' : 's'} sit in your inbox — run \`crtr feed read\` to absorb ${unread === 1 ? 'it' : 'them'}.`
|
|
319
|
-
: 'If you spawned workers, they have finished and detached, or you never subscribed. Nothing will wake you.';
|
|
320
|
-
return `No nodes below you (0 subscriptions, ${unread} unread). ${tail}`;
|
|
321
|
-
}
|
|
322
|
-
const working = kids.filter((k) => k.status === 'active' || k.status === 'idle');
|
|
323
|
-
const liveWaking = working.filter((k) => k.active); // active sub to a live node = it will wake me
|
|
324
|
-
const done = kids.filter((k) => k.status === 'done' || k.status === 'canceled');
|
|
325
|
-
const dead = kids.filter((k) => k.status === 'dead');
|
|
326
|
-
let line;
|
|
327
|
-
if (dead.length > 0) {
|
|
328
|
-
line = `\u26a0 ${dead.length} below you ${dead.length === 1 ? 'is' : 'are'} dead and will NOT wake you. Inspect with \`crtr node inspect show <id>\`, then re-delegate or proceed without ${dead.length === 1 ? 'it' : 'them'}.`;
|
|
329
|
-
}
|
|
330
|
-
else if (liveWaking.length > 0) {
|
|
331
|
-
line = `Safe to yield \u2014 ${liveWaking.length} worker${liveWaking.length === 1 ? '' : 's'} running async will wake you on the next push. Nothing to do now; end your turn and chill.`;
|
|
332
|
-
}
|
|
333
|
-
else if (unread > 0) {
|
|
334
|
-
line = `Nothing still running, but ${unread} unread report${unread === 1 ? '' : 's'} \u2014 run \`crtr feed read\` to absorb ${unread === 1 ? 'it' : 'them'}, then continue or finish.`;
|
|
335
|
-
}
|
|
336
|
-
else {
|
|
337
|
-
line = 'Everything below you has finished and been drained \u2014 nothing is running. Continue your own work, or `crtr push final` to finish.';
|
|
338
|
-
}
|
|
339
|
-
const rows = kids.map((k) => {
|
|
340
|
-
const sub = k.active ? '' : ' (passive)';
|
|
341
|
-
const push = k.last_push !== null
|
|
342
|
-
? `pushed ${fmtAge(k.last_push.ts)} [${k.last_push.kind}]${k.last_push.ref !== null ? ` ref:${k.last_push.ref}` : ''}`
|
|
343
|
-
: 'no push yet';
|
|
344
|
-
const glyph = STATUS_GLYPH[k.status] ?? '?';
|
|
345
|
-
return `- ${glyph} ${k.name}${sub} (${k.node_id}) \u00b7 ${k.kind} \u00b7 ${k.status} \u00b7 spawned ${fmtAge(k.spawned)} \u00b7 cyc ${k.cycles} \u00b7 ${push}`;
|
|
346
|
-
}).join('\n');
|
|
347
|
-
const summary = `${kids.length} node${kids.length === 1 ? '' : 's'} below you — ${working.length} working, ${done.length} done, ${dead.length} dead, ${unread} unread:`;
|
|
348
|
-
return `${line}\n\n${summary}\n${rows}`;
|
|
349
|
-
},
|
|
350
|
-
});
|
|
351
|
-
// ---------------------------------------------------------------------------
|
|
352
142
|
// Registration
|
|
353
143
|
// ---------------------------------------------------------------------------
|
|
354
144
|
export function registerPush() {
|
|
@@ -367,19 +157,3 @@ export function registerPush() {
|
|
|
367
157
|
children: [makeTierLeaf('update'), makeTierLeaf('urgent'), makeTierLeaf('final')],
|
|
368
158
|
});
|
|
369
159
|
}
|
|
370
|
-
export function registerFeed() {
|
|
371
|
-
return defineBranch({
|
|
372
|
-
name: 'feed',
|
|
373
|
-
rootEntry: {
|
|
374
|
-
concept: 'the read side of the spine — pointers your subscriptions have pushed',
|
|
375
|
-
desc: 'drain your inbox feed into a digest',
|
|
376
|
-
useWhen: 'catching up on what the nodes you subscribe to have reported',
|
|
377
|
-
},
|
|
378
|
-
help: {
|
|
379
|
-
name: 'feed',
|
|
380
|
-
summary: 'read the per-node inbox feed',
|
|
381
|
-
model: 'Each node has an inbox.jsonl that accumulates ~30-token pointers from publishers it subscribes to. The watcher drains this inbox to wake you, so the wake message you receive ALREADY IS the coalesced digest \u2014 dereference the reports that matter by reading their ref paths (a push carries a ref; a direct message inlines a bounded body preview, and when that clips it ends in `crtr feed message <id>` to read the full text back from the jsonl). `feed read` is for PROACTIVELY polling before a wake (it advances the cursor); after a wake it reads empty because the cursor already moved \u2014 use `--all` to re-read history. An empty feed is normal while workers run \u2014 a worker that has not pushed leaves no pointer \u2014 so use `feed peek` to see the live state of the nodes below you (and whether to yield) without draining anything.',
|
|
382
|
-
},
|
|
383
|
-
children: [feedReadLeaf, feedMessageLeaf, feedPeekLeaf],
|
|
384
|
-
});
|
|
385
|
-
}
|
package/dist/commands/revive.js
CHANGED
|
@@ -164,8 +164,8 @@ export const reviveLeaf = defineLeaf({
|
|
|
164
164
|
next: 'Use --now only on a node the canvas shows as hanging (⚠). For a routine relaunch, omit --now.',
|
|
165
165
|
});
|
|
166
166
|
}
|
|
167
|
-
//
|
|
168
|
-
//
|
|
167
|
+
// The daemon never kills a BUSY engine (its verdict paths all return 'leave'
|
|
168
|
+
// on a live busy marker): a node mid-turn is making progress, not wedged —
|
|
169
169
|
// refuse rather than SIGTERM it mid-flight.
|
|
170
170
|
if (isBusy(nodeId)) {
|
|
171
171
|
throw new InputError({
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
// `crtr
|
|
1
|
+
// `crtr surface tmux-spread <node>` — explode the local subtree into a tiled grid.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
3
|
+
// A genuine viewer-placement decision (tile every live viewer in a subtree into
|
|
4
|
+
// one window), so it lives under `surface` alongside view/attach/workspace/web.
|
|
5
|
+
// Also bound to the `/graph` view's `e` key (src/pi-extensions/canvas-nav.ts),
|
|
6
|
+
// which shells this leaf directly for the cursor node. It "expands" the graph
|
|
7
|
+
// you are watching: the caller's
|
|
5
8
|
// own viewer pane is broken out into a BRAND-NEW window, then a surface-attach
|
|
6
9
|
// viewer pane is tiled beside it for every broker-alive node in the local
|
|
7
10
|
// subtree (ancestry root → self → descendants), so the user lands on a single
|
|
@@ -48,16 +51,16 @@ function brokerAlive(meta) {
|
|
|
48
51
|
export const tmuxSpreadLeaf = defineLeaf({
|
|
49
52
|
name: 'tmux-spread',
|
|
50
53
|
description: 'break the caller\'s pane into a new window and tile a viewer for every broker-alive node in the local subtree',
|
|
51
|
-
whenToUse: 'you want the whole live subtree you are watching on one tiled tmux window at once — bound to the
|
|
54
|
+
whenToUse: 'you want the whole live subtree you are watching on one tiled tmux window at once, instead of hunting down each node\'s viewer by hand — also bound to the `/graph` view\'s `e` key',
|
|
52
55
|
help: {
|
|
53
|
-
name: '
|
|
56
|
+
name: 'surface tmux-spread',
|
|
54
57
|
summary: 'Explode the local subtree into a tiled grid: break the caller\'s viewer pane into a brand-new window, then tile a surface-attach viewer pane beside it for every broker-alive node (status active/idle + live pi_pid) in the ancestry-root subtree, most-recently-active first, capped at 8 panes',
|
|
55
58
|
params: [
|
|
56
59
|
{
|
|
57
60
|
kind: 'positional',
|
|
58
61
|
name: 'node',
|
|
59
62
|
required: true,
|
|
60
|
-
constraint: 'The spread anchor (self) — its ancestry root + descendants define the subtree to tile.
|
|
63
|
+
constraint: 'The spread anchor (self) — its ancestry root + descendants define the subtree to tile. Passed as {self} by the `/graph` view\'s `e` key.',
|
|
61
64
|
},
|
|
62
65
|
{
|
|
63
66
|
kind: 'flag',
|
package/dist/commands/surface.js
CHANGED
|
@@ -8,6 +8,7 @@ import { registerView } from './view.js';
|
|
|
8
8
|
import { registerWorkspace } from './workspace.js';
|
|
9
9
|
import { registerWeb } from '../clients/web/web-cmd.js';
|
|
10
10
|
import { registerAttach } from '../clients/attach/attach-cmd.js';
|
|
11
|
+
import { tmuxSpreadLeaf } from './surface-tmux-spread.js';
|
|
11
12
|
function namespaceForSurface(node) {
|
|
12
13
|
node.help.name = `surface ${node.help.name}`;
|
|
13
14
|
if (node.kind === 'branch') {
|
|
@@ -36,8 +37,8 @@ export function registerSurface() {
|
|
|
36
37
|
help: {
|
|
37
38
|
name: 'surface',
|
|
38
39
|
summary: 'open and manage screen-facing surfaces',
|
|
39
|
-
model: '
|
|
40
|
+
model: 'Five entrypoints share the screen-facing space. `view` hosts a standalone raw-ANSI surface in tmux; `attach` connects this pane to a node broker; `workspace` lays out the left rail plus chat pane in one tmux window; `web` serves the browser shell and broker relay; `tmux-spread` tiles a viewer for every live node in a local subtree into one new window. Pick the family by host and layout, then choose the leaf that matches the action.',
|
|
40
41
|
},
|
|
41
|
-
children: [view, attach, workspace, web],
|
|
42
|
+
children: [view, attach, workspace, web, tmuxSpreadLeaf],
|
|
42
43
|
});
|
|
43
44
|
}
|
|
@@ -13,7 +13,6 @@ const TOP_LEVEL_KEYS = new Set([
|
|
|
13
13
|
'max_panes_per_window',
|
|
14
14
|
'canvasNav',
|
|
15
15
|
'modelLadders',
|
|
16
|
-
'personaStrengths',
|
|
17
16
|
]);
|
|
18
17
|
const MODEL_PROVIDERS = new Set(['anthropic', 'openai']);
|
|
19
18
|
const MODEL_STRENGTHS = new Set(['ultra', 'strong', 'medium', 'light']);
|
|
@@ -105,20 +104,6 @@ function setNestedValue(cfg, key, rawValue, value) {
|
|
|
105
104
|
}
|
|
106
105
|
throw usage(`unsupported key path for set: ${key}`);
|
|
107
106
|
}
|
|
108
|
-
if (topKey === 'personaStrengths') {
|
|
109
|
-
if (parts.length !== 2) {
|
|
110
|
-
throw usage(`unsupported key path for set: ${key}`);
|
|
111
|
-
}
|
|
112
|
-
const persona = parts[1];
|
|
113
|
-
if (persona.trim() === '' || persona !== persona.trim()) {
|
|
114
|
-
throw usage(`personaStrengths.<persona-kind> must be a non-empty persona kind without leading or trailing whitespace`);
|
|
115
|
-
}
|
|
116
|
-
if (!isModelStrength(rawValue)) {
|
|
117
|
-
throw usage(`personaStrengths.<persona-kind> must be ultra, strong, medium, or light`);
|
|
118
|
-
}
|
|
119
|
-
cfg.personaStrengths[persona] = rawValue;
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
107
|
if (parts.length === 1) {
|
|
123
108
|
cfg[topKey] = value;
|
|
124
109
|
return;
|
|
@@ -136,12 +121,12 @@ function setNestedValue(cfg, key, rawValue, value) {
|
|
|
136
121
|
const configGet = defineLeaf({
|
|
137
122
|
name: 'get',
|
|
138
123
|
description: 'read a config value by key',
|
|
139
|
-
whenToUse: 'you want to read a single config value by its dotted key — the current auto_update policy, max_panes_per_window, the canvasNav prefix key, model ladders
|
|
124
|
+
whenToUse: 'you want to read a single config value by its dotted key — the current auto_update policy, max_panes_per_window, the canvasNav prefix key, or model ladders — optionally from a specific scope. Use sys config set instead to change a value, sys config path to locate the file for hand-editing.',
|
|
140
125
|
help: {
|
|
141
126
|
name: 'sys config get',
|
|
142
127
|
summary: 'read a config value by dotted key',
|
|
143
128
|
params: [
|
|
144
|
-
{ kind: 'positional', name: 'key', type: 'string', required: true, constraint: 'Dotted key path. Top-level keys: auto_update, marketplaces, plugins, max_panes_per_window, canvasNav, modelLadders
|
|
129
|
+
{ kind: 'positional', name: 'key', type: 'string', required: true, constraint: 'Dotted key path. Top-level keys: auto_update, marketplaces, plugins, max_panes_per_window, canvasNav, modelLadders (read whole; edit canvasNav.prefixBinds/graphBinds in config.json directly).' },
|
|
145
130
|
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Scope to read from. Default: user.' },
|
|
146
131
|
],
|
|
147
132
|
output: [
|
|
@@ -166,12 +151,12 @@ const configGet = defineLeaf({
|
|
|
166
151
|
const configSet = defineLeaf({
|
|
167
152
|
name: 'set',
|
|
168
153
|
description: 'write a config value by key',
|
|
169
|
-
whenToUse: 'you want to change a crtr setting — flip auto_update.crtr or auto_update.content to notify, apply, or off, raise max_panes_per_window, rebind canvasNav.prefixKey, set model ladders
|
|
154
|
+
whenToUse: 'you want to change a crtr setting — flip auto_update.crtr or auto_update.content to notify, apply, or off, raise max_panes_per_window, rebind canvasNav.prefixKey, or set model ladders — written to the user or project scope. Use sys config get instead to read a value; the canvasNav record tables are not settable here, so edit config.json directly (sys config path) for those.',
|
|
170
155
|
help: {
|
|
171
156
|
name: 'sys config set',
|
|
172
157
|
summary: 'write a config value by dotted key',
|
|
173
158
|
params: [
|
|
174
|
-
{ kind: 'positional', name: 'key', type: 'string', required: true, constraint: 'Dotted key path. Supported: auto_update.crtr, auto_update.content, auto_update.interval_hours, max_panes_per_window, canvasNav.prefixKey, modelLadders.defaultProvider, modelLadders.<anthropic|openai>.<ultra|strong|medium|light
|
|
159
|
+
{ kind: 'positional', name: 'key', type: 'string', required: true, constraint: 'Dotted key path. Supported: auto_update.crtr, auto_update.content, auto_update.interval_hours, max_panes_per_window, canvasNav.prefixKey, modelLadders.defaultProvider, modelLadders.<anthropic|openai>.<ultra|strong|medium|light>. The canvasNav.prefixBinds/graphBinds record tables are not settable here — edit config.json directly (`crtr sys config path`).' },
|
|
175
160
|
{ kind: 'flag', name: 'value', type: 'string', required: true, constraint: 'value VALUE — string, required. Stored as-is if quoted; coerced to number or boolean when unambiguous.' },
|
|
176
161
|
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Scope to write to. Default: user.' },
|
|
177
162
|
],
|
|
@@ -240,10 +225,10 @@ const configPath = defineLeaf({
|
|
|
240
225
|
export const configBranch = defineBranch({
|
|
241
226
|
name: 'config',
|
|
242
227
|
description: 'read and write configuration',
|
|
243
|
-
whenToUse: 'inspecting or changing crtr settings — read a value with sys config get, change one with sys config set (auto_update policy, max_panes_per_window, canvasNav.prefixKey, model ladders
|
|
228
|
+
whenToUse: 'inspecting or changing crtr settings — read a value with sys config get, change one with sys config set (auto_update policy, max_panes_per_window, canvasNav.prefixKey, model ladders), or locate config.json with sys config path to hand-edit the record tables set cannot reach.',
|
|
244
229
|
help: {
|
|
245
230
|
name: 'sys config',
|
|
246
|
-
summary: 'read and write crtr configuration, including model ladders
|
|
231
|
+
summary: 'read and write crtr configuration, including model ladders',
|
|
247
232
|
},
|
|
248
233
|
children: [configGet, configSet, configPath],
|
|
249
234
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `crtr
|
|
1
|
+
// `crtr sys daemon` — thin supervisor daemon management.
|
|
2
2
|
//
|
|
3
3
|
// The daemon (crtrd) polls active+idle nodes and handles engine-container exit
|
|
4
4
|
// (a tmux pane or a headless broker process):
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
// • refresh-yield (intent=refresh) → fresh respawn
|
|
7
7
|
//
|
|
8
8
|
// This subtree starts, checks, and stops the daemon process.
|
|
9
|
-
import { defineLeaf, defineBranch } from '
|
|
10
|
-
import { InputError } from '
|
|
11
|
-
import { spawnDaemon } from '
|
|
12
|
-
import { readPidfile, isPidAlive } from '
|
|
9
|
+
import { defineLeaf, defineBranch } from '../../core/command.js';
|
|
10
|
+
import { InputError } from '../../core/io.js';
|
|
11
|
+
import { spawnDaemon } from '../../daemon/manage.js';
|
|
12
|
+
import { readPidfile, isPidAlive } from '../../daemon/crtrd.js';
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
14
|
// daemon start
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
@@ -18,7 +18,7 @@ const daemonStart = defineLeaf({
|
|
|
18
18
|
description: 'start the daemon in the background',
|
|
19
19
|
whenToUse: 'bringing the crtrd supervisor up for the first time in a session so node window-exits get handled (no-op if it is already running)',
|
|
20
20
|
help: {
|
|
21
|
-
name: '
|
|
21
|
+
name: 'sys daemon start',
|
|
22
22
|
summary: 'start the crtrd supervisor daemon in the background (no-op if already running)',
|
|
23
23
|
params: [],
|
|
24
24
|
output: [
|
|
@@ -43,9 +43,9 @@ const daemonStart = defineLeaf({
|
|
|
43
43
|
const daemonStatus = defineLeaf({
|
|
44
44
|
name: 'status',
|
|
45
45
|
description: 'check whether the daemon is running',
|
|
46
|
-
whenToUse: '
|
|
46
|
+
whenToUse: 'operator/dev diagnostic — probing whether the crtrd supervisor process is alive while troubleshooting revive/supervision behavior',
|
|
47
47
|
help: {
|
|
48
|
-
name: '
|
|
48
|
+
name: 'sys daemon status',
|
|
49
49
|
summary: 'check whether the crtrd supervisor daemon is currently running',
|
|
50
50
|
params: [],
|
|
51
51
|
output: [
|
|
@@ -71,7 +71,7 @@ const daemonStop = defineLeaf({
|
|
|
71
71
|
description: 'stop the running daemon',
|
|
72
72
|
whenToUse: 'shutting the crtrd supervisor down, ending auto-revival of nodes on window exit',
|
|
73
73
|
help: {
|
|
74
|
-
name: '
|
|
74
|
+
name: 'sys daemon stop',
|
|
75
75
|
summary: 'send SIGTERM to the crtrd supervisor daemon',
|
|
76
76
|
params: [],
|
|
77
77
|
output: [
|
|
@@ -100,16 +100,16 @@ const daemonStop = defineLeaf({
|
|
|
100
100
|
},
|
|
101
101
|
});
|
|
102
102
|
// ---------------------------------------------------------------------------
|
|
103
|
-
// Export — mounted under `crtr
|
|
103
|
+
// Export — mounted under `crtr sys`
|
|
104
104
|
// ---------------------------------------------------------------------------
|
|
105
105
|
export const daemonBranch = defineBranch({
|
|
106
106
|
name: 'daemon',
|
|
107
107
|
description: 'manage the crtrd supervisor process',
|
|
108
108
|
whenToUse: 'managing the crtrd supervisor that auto-revives nodes on window exit — start it, check its status, or stop it',
|
|
109
109
|
help: {
|
|
110
|
-
name: '
|
|
110
|
+
name: 'sys daemon',
|
|
111
111
|
summary: 'manage the crtrd canvas supervisor daemon',
|
|
112
|
-
model: 'crtrd is a thin background daemon that polls active+idle nodes and acts on window exit: crashed windows become dead; refresh-yield windows get a fresh respawn. It holds no orchestration logic — just process supervision.',
|
|
112
|
+
model: 'crtrd is a thin background daemon that polls active+idle nodes and acts on window exit: crashed windows become dead; refresh-yield windows get a fresh respawn. It holds no orchestration logic — just process supervision. start/stop/status are operator/dev self-management, not something a normal agent turn needs to check.',
|
|
113
113
|
},
|
|
114
114
|
children: [daemonStart, daemonStatus, daemonStop],
|
|
115
115
|
});
|
|
@@ -71,6 +71,7 @@ function faultRemediation(fault) {
|
|
|
71
71
|
auth: 're-authenticate',
|
|
72
72
|
protocol: 'fix the protocol mismatch',
|
|
73
73
|
other: 'fix the underlying issue',
|
|
74
|
+
wedged: 'read `crtr memory read wedged-child-on-runaway-bash` and, if a runaway subprocess is to blame, kill the subprocess (not the node)',
|
|
74
75
|
};
|
|
75
76
|
const progress = fault.retry.disposition === 'auto'
|
|
76
77
|
? fault.retry.by === 'client'
|
|
@@ -3,8 +3,10 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { defineLeaf } from '../../core/command.js';
|
|
4
4
|
import { InputError, readStdinRaw } from '../../core/io.js';
|
|
5
5
|
import { workspaceRoot } from '../../core/artifact.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { getNode } from '../../core/canvas/index.js';
|
|
7
|
+
import { readMergedLaunchConfig, subKindsAvailableTo } from '../../core/config.js';
|
|
8
|
+
import { LAUNCH_SYSTEM_PROMPT } from '../../core/runtime/launch.js';
|
|
9
|
+
import { renderPreferencesForSubject, renderKnowledgeBlock, scopeForCwd, spineDepth, profileNameFor, } from '../../core/substrate/index.js';
|
|
8
10
|
function resolveLifecycle(mode, lifecycle) {
|
|
9
11
|
if (lifecycle !== undefined)
|
|
10
12
|
return lifecycle;
|
|
@@ -41,24 +43,47 @@ function parseBool(value, field, defaultValue) {
|
|
|
41
43
|
function optionalString(value) {
|
|
42
44
|
return typeof value === 'string' && value.trim() !== '' ? value : null;
|
|
43
45
|
}
|
|
44
|
-
function
|
|
45
|
-
return
|
|
46
|
+
function approxTokens(text) {
|
|
47
|
+
return Math.ceil(text.length / 4);
|
|
46
48
|
}
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
+
function topLevelKinds() {
|
|
50
|
+
const registry = readMergedLaunchConfig().kinds;
|
|
51
|
+
return Object.entries(registry)
|
|
52
|
+
.filter(([kind]) => !kind.includes('/'))
|
|
53
|
+
.map(([kind, cfg]) => ({ kind, whenToUse: cfg.whenToUse }))
|
|
54
|
+
.sort((a, b) => a.kind.localeCompare(b.kind));
|
|
55
|
+
}
|
|
56
|
+
function subKindsFor(kind) {
|
|
57
|
+
return subKindsAvailableTo(kind);
|
|
49
58
|
}
|
|
50
59
|
function reviewList() {
|
|
51
|
-
const kinds =
|
|
60
|
+
const kinds = topLevelKinds();
|
|
52
61
|
const subPersonas = {};
|
|
53
62
|
for (const { kind } of kinds)
|
|
54
|
-
subPersonas[kind] =
|
|
63
|
+
subPersonas[kind] = subKindsFor(kind);
|
|
64
|
+
return { kinds, modes: ['base', 'orchestrator'], lifecycles: ['terminal', 'resident'], subPersonas };
|
|
65
|
+
}
|
|
66
|
+
function buildReviewSubject(kind, mode, lifecycle, hasManager, nodeId) {
|
|
67
|
+
const node = nodeId !== null ? getNode(nodeId) : null;
|
|
68
|
+
const cwd = node?.cwd ?? process.cwd();
|
|
55
69
|
return {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
70
|
+
kind,
|
|
71
|
+
mode,
|
|
72
|
+
lifecycle,
|
|
73
|
+
hasManager,
|
|
74
|
+
cwd,
|
|
75
|
+
scope: scopeForCwd(cwd),
|
|
76
|
+
orchestration: { depth: node !== null ? spineDepth(node.node_id) : 0 },
|
|
77
|
+
profile: profileNameFor(node?.profile_id),
|
|
60
78
|
};
|
|
61
79
|
}
|
|
80
|
+
function spliceIntoScaffold(scaffold, block) {
|
|
81
|
+
const anchor = '\n\nGuidelines:';
|
|
82
|
+
const idx = scaffold.indexOf(anchor);
|
|
83
|
+
if (idx === -1)
|
|
84
|
+
return `${scaffold}\n\n${block}`;
|
|
85
|
+
return `${scaffold.slice(0, idx)}\n\n${block}${scaffold.slice(idx)}`;
|
|
86
|
+
}
|
|
62
87
|
function promptReviewPath() {
|
|
63
88
|
const dir = workspaceRoot();
|
|
64
89
|
const reviewsDir = join(dir, 'prompt-reviews');
|
|
@@ -118,14 +143,62 @@ function resolveDeck(input) {
|
|
|
118
143
|
const mode = input['mode'] ?? 'base';
|
|
119
144
|
const lifecycle = resolveLifecycle(mode, input['lifecycle']);
|
|
120
145
|
const hasManager = parseBool(input['hasManager'], 'hasManager', true);
|
|
121
|
-
const
|
|
146
|
+
const nodeId = optionalString(input['node']);
|
|
122
147
|
if (kind === undefined || kind.trim() === '') {
|
|
123
148
|
throw new InputError({ error: 'missing_parameter', message: 'kind is required', field: 'kind', next: 'Pass --kind <kind> or use --list.' });
|
|
124
149
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
150
|
+
const node = nodeId !== null ? getNode(nodeId) : null;
|
|
151
|
+
const subject = buildReviewSubject(kind, mode, lifecycle, hasManager, node?.node_id ?? null);
|
|
152
|
+
const { block, contentDocs, tree } = renderPreferencesForSubject(subject);
|
|
153
|
+
const layers = [
|
|
154
|
+
{
|
|
155
|
+
id: 'launch-scaffold', label: 'Launch scaffold', group: 'generated',
|
|
156
|
+
source: 'generated: launch scaffold', sourcePath: null, scope: 'generated',
|
|
157
|
+
included: true, condition: null, text: LAUNCH_SYSTEM_PROMPT,
|
|
158
|
+
tokens: approxTokens(LAUNCH_SYSTEM_PROMPT), chars: LAUNCH_SYSTEM_PROMPT.length,
|
|
159
|
+
},
|
|
160
|
+
...contentDocs.map((d) => ({
|
|
161
|
+
id: d.name, label: d.name, group: 'substrate',
|
|
162
|
+
source: `memory: ${d.name}`, sourcePath: d.path, scope: d.scope,
|
|
163
|
+
included: true, condition: null, text: d.body,
|
|
164
|
+
tokens: approxTokens(d.body), chars: d.body.length,
|
|
165
|
+
})),
|
|
166
|
+
];
|
|
167
|
+
if (tree !== '') {
|
|
168
|
+
layers.push({
|
|
169
|
+
id: 'preference-catalog', label: 'Preference catalog (routing entries)', group: 'substrate',
|
|
170
|
+
source: 'generated: preference catalog', sourcePath: null, scope: 'generated',
|
|
171
|
+
included: true, condition: null, text: tree, tokens: approxTokens(tree), chars: tree.length,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
if (node !== null) {
|
|
175
|
+
const knowledge = renderKnowledgeBlock(node.node_id);
|
|
176
|
+
if (knowledge !== '') {
|
|
177
|
+
layers.push({
|
|
178
|
+
id: 'memory-knowledge', label: 'Memory (knowledge) — context message, not system prompt',
|
|
179
|
+
group: 'substrate', source: 'runtime-spliced', sourcePath: null, scope: 'runtime',
|
|
180
|
+
included: false, condition: 'session_start context message, not the system prompt',
|
|
181
|
+
text: knowledge, tokens: approxTokens(knowledge), chars: knowledge.length,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const assembled = spliceIntoScaffold(LAUNCH_SYSTEM_PROMPT, block);
|
|
186
|
+
const sources = [];
|
|
187
|
+
const seen = new Set();
|
|
188
|
+
for (const d of contentDocs) {
|
|
189
|
+
const key = `${d.scope}:${d.path}`;
|
|
190
|
+
if (seen.has(key))
|
|
191
|
+
continue;
|
|
192
|
+
seen.add(key);
|
|
193
|
+
sources.push({ id: d.name, label: d.name, path: d.path, scope: d.scope });
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
config: { kind, mode, lifecycle, hasManager, node: node?.node_id ?? null },
|
|
197
|
+
layers,
|
|
198
|
+
assembled,
|
|
199
|
+
total: { tokens: approxTokens(assembled), chars: assembled.length },
|
|
200
|
+
sources,
|
|
201
|
+
};
|
|
129
202
|
}
|
|
130
203
|
export const promptReviewLeaf = defineLeaf({
|
|
131
204
|
name: 'prompt-review',
|