@crouton-kit/crouter 0.3.13 → 0.3.15
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.d.ts +8 -0
- package/dist/build-root.js +30 -0
- package/dist/builtin-personas/design/base.md +3 -7
- package/dist/builtin-personas/design/orchestrator.md +4 -3
- package/dist/builtin-personas/developer/base.md +3 -7
- package/dist/builtin-personas/developer/orchestrator.md +5 -4
- package/dist/builtin-personas/explore/base.md +3 -7
- package/dist/builtin-personas/explore/orchestrator.md +1 -5
- package/dist/builtin-personas/general/base.md +2 -4
- package/dist/builtin-personas/general/orchestrator.md +2 -4
- package/dist/builtin-personas/lifecycle/resident.md +2 -0
- package/dist/builtin-personas/lifecycle/terminal.md +6 -0
- package/dist/builtin-personas/orchestration-kernel.md +42 -3
- package/dist/builtin-personas/plan/base.md +3 -5
- package/dist/builtin-personas/plan/orchestrator.md +5 -4
- package/dist/builtin-personas/plan/reviewers/architecture-fit/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/code-smells/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/pattern-consistency/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/requirements-coverage/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/security/base.md +9 -0
- package/dist/builtin-personas/review/base.md +3 -5
- package/dist/builtin-personas/review/orchestrator.md +2 -6
- package/dist/builtin-personas/runtime-base.md +3 -19
- package/dist/builtin-personas/spec/base.md +3 -5
- package/dist/builtin-personas/spec/orchestrator.md +4 -3
- package/dist/builtin-personas/spine/has-manager.md +10 -0
- package/dist/builtin-personas/spine/no-manager.md +2 -0
- package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +96 -0
- package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +49 -0
- package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +49 -0
- package/dist/builtin-skills/skills/planning/SKILL.md +1 -1
- package/dist/builtin-skills/skills/spec/SKILL.md +2 -2
- package/dist/cli.js +6 -29
- package/dist/commands/__tests__/human.test.js +73 -2
- package/dist/commands/attention.js +76 -7
- package/dist/commands/canvas-prune.d.ts +2 -0
- package/dist/commands/canvas-prune.js +66 -0
- package/dist/commands/canvas.js +5 -8
- package/dist/commands/chord.d.ts +2 -0
- package/dist/commands/chord.js +143 -0
- package/dist/commands/daemon.js +8 -5
- package/dist/commands/dashboard.js +2 -0
- package/dist/commands/human/prompts.js +28 -27
- package/dist/commands/human/queue.d.ts +1 -0
- package/dist/commands/human/queue.js +105 -2
- package/dist/commands/human/shared.d.ts +28 -18
- package/dist/commands/human/shared.js +53 -60
- package/dist/commands/human.js +6 -14
- package/dist/commands/node.d.ts +11 -0
- package/dist/commands/node.js +381 -87
- package/dist/commands/pkg/market-inspect.js +6 -4
- package/dist/commands/pkg/market-manage.js +10 -6
- package/dist/commands/pkg/market.js +2 -4
- package/dist/commands/pkg/plugin-inspect.js +6 -4
- package/dist/commands/pkg/plugin-manage.js +12 -7
- package/dist/commands/pkg/plugin.js +2 -4
- package/dist/commands/pkg.js +0 -4
- package/dist/commands/push.js +178 -15
- package/dist/commands/revive.js +5 -3
- package/dist/commands/skill/author.js +6 -4
- package/dist/commands/skill/find.js +8 -5
- package/dist/commands/skill/read.js +2 -0
- package/dist/commands/skill/state.js +6 -4
- package/dist/commands/skill.js +0 -6
- package/dist/commands/sys/config.js +21 -7
- package/dist/commands/sys/doctor.js +2 -0
- package/dist/commands/sys/update.js +4 -0
- package/dist/commands/sys.js +0 -6
- package/dist/commands/tmux-spread.d.ts +2 -0
- package/dist/commands/tmux-spread.js +130 -0
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +25 -0
- package/dist/core/__tests__/child-followup.test.d.ts +1 -0
- package/dist/core/__tests__/child-followup.test.js +83 -0
- package/dist/core/__tests__/close.test.d.ts +1 -0
- package/dist/core/__tests__/close.test.js +148 -0
- package/dist/core/__tests__/context-intro.test.d.ts +1 -0
- package/dist/core/__tests__/context-intro.test.js +196 -0
- package/dist/core/__tests__/daemon-boot.test.d.ts +1 -0
- package/dist/core/__tests__/daemon-boot.test.js +93 -0
- package/dist/core/__tests__/daemon-liveness.test.d.ts +1 -0
- package/dist/core/__tests__/daemon-liveness.test.js +223 -0
- package/dist/core/__tests__/focuses.test.d.ts +1 -0
- package/dist/core/__tests__/focuses.test.js +259 -0
- package/dist/core/__tests__/fork.test.d.ts +1 -0
- package/dist/core/__tests__/fork.test.js +91 -0
- package/dist/core/__tests__/home-session.test.d.ts +1 -0
- package/dist/core/__tests__/home-session.test.js +153 -0
- package/dist/core/__tests__/human-cancel-guard.test.d.ts +1 -0
- package/dist/core/__tests__/human-cancel-guard.test.js +49 -0
- package/dist/core/__tests__/keystone.test.d.ts +1 -0
- package/dist/core/__tests__/keystone.test.js +185 -0
- package/dist/core/__tests__/kickoff.test.d.ts +1 -0
- package/dist/core/__tests__/kickoff.test.js +89 -0
- package/dist/core/__tests__/lifecycle.test.d.ts +1 -0
- package/dist/core/__tests__/lifecycle.test.js +178 -0
- package/dist/core/__tests__/listing-completeness.test.d.ts +1 -0
- package/dist/core/__tests__/listing-completeness.test.js +31 -0
- package/dist/core/__tests__/memory.test.d.ts +1 -0
- package/dist/core/__tests__/memory.test.js +152 -0
- package/dist/core/__tests__/migration.test.d.ts +1 -0
- package/dist/core/__tests__/migration.test.js +238 -0
- package/dist/core/__tests__/pane-column.test.d.ts +1 -0
- package/dist/core/__tests__/pane-column.test.js +153 -0
- package/dist/core/__tests__/passive-subscription.test.d.ts +1 -0
- package/dist/core/__tests__/passive-subscription.test.js +164 -0
- package/dist/core/__tests__/persona-compose.test.d.ts +1 -0
- package/dist/core/__tests__/persona-compose.test.js +53 -0
- package/dist/core/__tests__/persona-subkind.test.d.ts +1 -0
- package/dist/core/__tests__/persona-subkind.test.js +62 -0
- package/dist/core/__tests__/persona.test.d.ts +1 -0
- package/dist/core/__tests__/persona.test.js +107 -0
- package/dist/core/__tests__/placement-focus.test.d.ts +1 -0
- package/dist/core/__tests__/placement-focus.test.js +244 -0
- package/dist/core/__tests__/placement-reconcile.test.d.ts +1 -0
- package/dist/core/__tests__/placement-reconcile.test.js +212 -0
- package/dist/core/__tests__/placement-revive.test.d.ts +1 -0
- package/dist/core/__tests__/placement-revive.test.js +238 -0
- package/dist/core/__tests__/placement-teardown.test.d.ts +1 -0
- package/dist/core/__tests__/placement-teardown.test.js +183 -0
- package/dist/core/__tests__/prune.test.d.ts +1 -0
- package/dist/core/__tests__/prune.test.js +116 -0
- package/dist/core/__tests__/push-final-guard.test.d.ts +1 -0
- package/dist/core/__tests__/push-final-guard.test.js +71 -0
- package/dist/core/__tests__/relaunch.test.d.ts +1 -0
- package/dist/core/__tests__/relaunch.test.js +328 -0
- package/dist/core/__tests__/reset.test.js +26 -7
- package/dist/core/__tests__/revive.test.d.ts +1 -0
- package/dist/core/__tests__/revive.test.js +217 -0
- package/dist/core/__tests__/spawn-root.test.d.ts +1 -0
- package/dist/core/__tests__/spawn-root.test.js +73 -0
- package/dist/core/__tests__/steer-note.test.d.ts +1 -0
- package/dist/core/__tests__/steer-note.test.js +39 -0
- package/dist/core/__tests__/stop-guard.test.d.ts +1 -0
- package/dist/core/__tests__/stop-guard.test.js +82 -0
- package/dist/core/__tests__/subcommand-tier.test.d.ts +1 -0
- package/dist/core/__tests__/subcommand-tier.test.js +99 -0
- package/dist/core/__tests__/tmux-surface.test.d.ts +1 -0
- package/dist/core/__tests__/tmux-surface.test.js +106 -0
- package/dist/core/__tests__/unknown-path.test.js +8 -2
- package/dist/core/canvas/attention.d.ts +10 -0
- package/dist/core/canvas/attention.js +40 -0
- package/dist/core/canvas/canvas.d.ts +66 -7
- package/dist/core/canvas/canvas.js +209 -21
- package/dist/core/canvas/db.d.ts +8 -0
- package/dist/core/canvas/db.js +206 -4
- package/dist/core/canvas/focuses.d.ts +22 -0
- package/dist/core/canvas/focuses.js +80 -0
- package/dist/core/canvas/index.d.ts +3 -0
- package/dist/core/canvas/index.js +3 -0
- package/dist/core/canvas/labels.d.ts +27 -0
- package/dist/core/canvas/labels.js +36 -0
- package/dist/core/canvas/paths.d.ts +4 -0
- package/dist/core/canvas/paths.js +6 -0
- package/dist/core/canvas/render.js +25 -10
- package/dist/core/canvas/telemetry.d.ts +14 -0
- package/dist/core/canvas/telemetry.js +35 -0
- package/dist/core/canvas/types.d.ts +115 -12
- package/dist/core/command.d.ts +25 -1
- package/dist/core/command.js +48 -7
- package/dist/core/config.js +36 -2
- package/dist/core/feed/feed.js +14 -12
- package/dist/core/feed/inbox.d.ts +3 -1
- package/dist/core/feed/inbox.js +45 -5
- package/dist/core/feed/passive.d.ts +17 -0
- package/dist/core/feed/passive.js +92 -0
- package/dist/core/help.d.ts +59 -13
- package/dist/core/help.js +73 -28
- package/dist/core/personas/index.d.ts +1 -1
- package/dist/core/personas/index.js +1 -1
- package/dist/core/personas/loader.d.ts +40 -1
- package/dist/core/personas/loader.js +63 -1
- package/dist/core/personas/resolve.d.ts +13 -6
- package/dist/core/personas/resolve.js +46 -34
- package/dist/core/runtime/bearings.d.ts +20 -0
- package/dist/core/runtime/bearings.js +92 -0
- package/dist/core/runtime/close.d.ts +14 -0
- package/dist/core/runtime/close.js +151 -0
- package/dist/core/runtime/demote.d.ts +14 -0
- package/dist/core/runtime/demote.js +120 -0
- package/dist/core/runtime/front-door.js +1 -1
- package/dist/core/runtime/kickoff.d.ts +32 -6
- package/dist/core/runtime/kickoff.js +111 -37
- package/dist/core/runtime/launch.d.ts +29 -6
- package/dist/core/runtime/launch.js +85 -13
- package/dist/core/runtime/lifecycle.d.ts +13 -0
- package/dist/core/runtime/lifecycle.js +86 -0
- package/dist/core/runtime/memory.d.ts +43 -0
- package/dist/core/runtime/memory.js +165 -0
- package/dist/core/runtime/naming.d.ts +22 -0
- package/dist/core/runtime/naming.js +166 -0
- package/dist/core/runtime/nodes.d.ts +32 -1
- package/dist/core/runtime/nodes.js +60 -10
- package/dist/core/runtime/persona.d.ts +25 -0
- package/dist/core/runtime/persona.js +139 -0
- package/dist/core/runtime/placement.d.ts +287 -0
- package/dist/core/runtime/placement.js +663 -0
- package/dist/core/runtime/presence.d.ts +7 -32
- package/dist/core/runtime/presence.js +90 -110
- package/dist/core/runtime/promote.d.ts +18 -7
- package/dist/core/runtime/promote.js +70 -65
- package/dist/core/runtime/reset.d.ts +47 -4
- package/dist/core/runtime/reset.js +223 -52
- package/dist/core/runtime/revive.d.ts +26 -2
- package/dist/core/runtime/revive.js +166 -39
- package/dist/core/runtime/roadmap.d.ts +5 -4
- package/dist/core/runtime/roadmap.js +9 -16
- package/dist/core/runtime/spawn.d.ts +20 -5
- package/dist/core/runtime/spawn.js +169 -44
- package/dist/core/runtime/stop-guard.d.ts +1 -1
- package/dist/core/runtime/stop-guard.js +18 -8
- package/dist/core/runtime/tmux.d.ts +106 -21
- package/dist/core/runtime/tmux.js +249 -45
- package/dist/core/spawn.js +15 -0
- package/dist/daemon/crtrd.d.ts +12 -1
- package/dist/daemon/crtrd.js +152 -34
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +266 -0
- package/dist/pi-extensions/canvas-commands.d.ts +34 -0
- package/dist/pi-extensions/canvas-commands.js +103 -0
- package/dist/pi-extensions/canvas-context-intro.d.ts +70 -0
- package/dist/pi-extensions/canvas-context-intro.js +164 -0
- package/dist/pi-extensions/canvas-goal-capture.d.ts +21 -0
- package/dist/pi-extensions/canvas-goal-capture.js +67 -0
- package/dist/pi-extensions/canvas-inbox-watcher.js +11 -0
- package/dist/pi-extensions/canvas-nav.d.ts +12 -4
- package/dist/pi-extensions/canvas-nav.js +586 -262
- package/dist/pi-extensions/canvas-passive-context.d.ts +32 -0
- package/dist/pi-extensions/canvas-passive-context.js +114 -0
- package/dist/pi-extensions/canvas-stophook.d.ts +16 -0
- package/dist/pi-extensions/canvas-stophook.js +344 -228
- package/dist/types.d.ts +28 -0
- package/dist/types.js +16 -0
- package/package.json +1 -1
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// `crtr canvas chord` — the tmux prefix-menu dispatcher.
|
|
2
|
+
//
|
|
3
|
+
// The alt+c display-menu can't see the cursor node or read config at popup time
|
|
4
|
+
// — it only knows the active pane. So every config-driven prefix chord routes
|
|
5
|
+
// through this one leaf: tmux passes `--pane '#{pane_id}'` + `--key <k>`, and
|
|
6
|
+
// the dispatcher resolves the node in that pane, reads
|
|
7
|
+
// `canvasNav.prefixBinds[key]`, interpolates the template vars, and execs
|
|
8
|
+
// `crtr <argv>`. This keeps the menu static while the behaviour stays fully
|
|
9
|
+
// config-driven (no per-node menu rebuild).
|
|
10
|
+
//
|
|
11
|
+
// Two special cases bypass the bind table:
|
|
12
|
+
// • a digit key 1..9 → focus report N (the Nth live report of the pane node)
|
|
13
|
+
// • a bind whose `run` is the sentinel `__graph__` → send-keys `/graph` into
|
|
14
|
+
// the pane (toggles the in-pi GRAPH modal); the menu emits this directly so
|
|
15
|
+
// the dispatcher only handles it defensively.
|
|
16
|
+
//
|
|
17
|
+
// execFile (never `sh -c`) runs the interpolated argv, so a node name with
|
|
18
|
+
// shell metacharacters can never inject — each argv element is literal.
|
|
19
|
+
import { promisify } from 'node:util';
|
|
20
|
+
import { execFile } from 'node:child_process';
|
|
21
|
+
import { defineLeaf } from '../core/command.js';
|
|
22
|
+
import { InputError } from '../core/io.js';
|
|
23
|
+
import { readConfig } from '../core/config.js';
|
|
24
|
+
import { sendKeysEnter } from '../core/runtime/tmux.js';
|
|
25
|
+
import { nodeInPane } from './node.js';
|
|
26
|
+
import { getNode, subscribersOf, subscriptionsOf, view, fullName, } from '../core/canvas/index.js';
|
|
27
|
+
const pexec = promisify(execFile);
|
|
28
|
+
/** Template vars available to a `run` string. Single-valued vars interpolate
|
|
29
|
+
* in place (preserving spaces, e.g. a node name); `{subtree}` is multi-valued
|
|
30
|
+
* and a bare `{subtree}` token expands to several argv elements. */
|
|
31
|
+
function buildVars(selfId) {
|
|
32
|
+
const node = getNode(selfId);
|
|
33
|
+
const manager = subscribersOf(selfId)[0]?.node_id ?? '';
|
|
34
|
+
return {
|
|
35
|
+
id: selfId,
|
|
36
|
+
self: selfId,
|
|
37
|
+
lane: selfId,
|
|
38
|
+
name: node !== null ? fullName(node) : selfId,
|
|
39
|
+
manager,
|
|
40
|
+
subtree: view(selfId).join(' '),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** Split a `run` string argv-style and interpolate the template vars. A bare
|
|
44
|
+
* `{subtree}` token expands to several argv elements; every other placeholder
|
|
45
|
+
* is substituted in place (kept as one element so a multi-word name survives
|
|
46
|
+
* as a single argument under execFile). */
|
|
47
|
+
function interpolateArgv(run, vars) {
|
|
48
|
+
const out = [];
|
|
49
|
+
for (const tok of run.split(/\s+/).filter((t) => t !== '')) {
|
|
50
|
+
if (tok === '{subtree}') {
|
|
51
|
+
for (const part of (vars['subtree'] ?? '').split(/\s+/).filter((p) => p !== ''))
|
|
52
|
+
out.push(part);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
out.push(tok.replace(/\{(\w+)\}/g, (_, name) => vars[name] ?? ''));
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
export const chordLeaf = defineLeaf({
|
|
60
|
+
name: 'chord',
|
|
61
|
+
description: 'tmux prefix-menu dispatcher (bound by alt+c; not run by hand)',
|
|
62
|
+
whenToUse: 'never directly — the alt+c menu routes config-driven chords through it',
|
|
63
|
+
help: {
|
|
64
|
+
name: 'canvas chord',
|
|
65
|
+
summary: 'tmux prefix-menu dispatcher — resolve the node in --pane, look up canvasNav.prefixBinds[--key], interpolate, and exec `crtr <argv>`. Bound by the alt+c menu; not meant to be run by hand.',
|
|
66
|
+
params: [
|
|
67
|
+
{
|
|
68
|
+
kind: 'flag',
|
|
69
|
+
name: 'pane',
|
|
70
|
+
type: 'string',
|
|
71
|
+
required: false,
|
|
72
|
+
constraint: 'tmux pane id whose node the chord acts on. Defaults to $TMUX_PANE / your current pane.',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
kind: 'flag',
|
|
76
|
+
name: 'key',
|
|
77
|
+
type: 'string',
|
|
78
|
+
required: true,
|
|
79
|
+
constraint: 'The chord key pressed after alt+c (e.g. m, e, or a digit 1-9 for focus report N).',
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
output: [
|
|
83
|
+
{ name: 'ran', type: 'boolean', required: true, constraint: 'True when an action was dispatched.' },
|
|
84
|
+
{ name: 'key', type: 'string', required: true, constraint: 'Echo of the chord key.' },
|
|
85
|
+
{ name: 'node_id', type: 'string', required: false, constraint: 'The node the chord resolved against.' },
|
|
86
|
+
{ name: 'action', type: 'string', required: false, constraint: 'What ran: a crtr argv string, "graph-toggle", or "noop".' },
|
|
87
|
+
],
|
|
88
|
+
outputKind: 'object',
|
|
89
|
+
effects: ['Runs a `crtr` subcommand (focus/close/tmux-spread/…) or sends `/graph` into the pane, per the matched bind.'],
|
|
90
|
+
},
|
|
91
|
+
run: async (input) => {
|
|
92
|
+
const pane = input['pane'] ?? process.env['TMUX_PANE'];
|
|
93
|
+
const key = input['key'].trim();
|
|
94
|
+
const selfId = nodeInPane(pane);
|
|
95
|
+
if (selfId === undefined) {
|
|
96
|
+
throw new InputError({
|
|
97
|
+
error: 'no_node',
|
|
98
|
+
message: 'no node found in this pane',
|
|
99
|
+
next: 'Run from inside an agent\'s pane, or pass --pane <pane-id>.',
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
// Digit keys 1..9 → focus the Nth live report (generated, not a bind entry).
|
|
103
|
+
if (/^[1-9]$/.test(key)) {
|
|
104
|
+
const n = parseInt(key, 10);
|
|
105
|
+
const reports = subscriptionsOf(selfId)
|
|
106
|
+
.map((r) => r.node_id)
|
|
107
|
+
.filter((id) => {
|
|
108
|
+
const s = getNode(id)?.status;
|
|
109
|
+
return s === 'active' || s === 'idle';
|
|
110
|
+
});
|
|
111
|
+
const target = reports[n - 1];
|
|
112
|
+
if (target === undefined)
|
|
113
|
+
return { ran: false, key, node_id: selfId, action: 'noop' };
|
|
114
|
+
try {
|
|
115
|
+
await pexec('crtr', ['node', 'focus', target], { timeout: 15_000 });
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
/* best-effort */
|
|
119
|
+
}
|
|
120
|
+
return { ran: true, key, node_id: selfId, action: `node focus ${target}` };
|
|
121
|
+
}
|
|
122
|
+
const bind = readConfig('user').canvasNav.prefixBinds[key];
|
|
123
|
+
if (bind === undefined)
|
|
124
|
+
return { ran: false, key, node_id: selfId, action: 'noop' };
|
|
125
|
+
// The GRAPH-toggle sentinel: type /graph into the pane (the menu normally
|
|
126
|
+
// emits this directly; handle it here too so a manual chord still works).
|
|
127
|
+
if (bind.run === '__graph__') {
|
|
128
|
+
if (pane !== undefined && pane !== '')
|
|
129
|
+
sendKeysEnter(pane, '/graph');
|
|
130
|
+
return { ran: true, key, node_id: selfId, action: 'graph-toggle' };
|
|
131
|
+
}
|
|
132
|
+
const argv = interpolateArgv(bind.run, buildVars(selfId));
|
|
133
|
+
if (argv.length === 0)
|
|
134
|
+
return { ran: false, key, node_id: selfId, action: 'noop' };
|
|
135
|
+
try {
|
|
136
|
+
await pexec('crtr', argv, { timeout: 15_000 });
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
/* best-effort: the keystroke just acts; errors are surfaced by the inner cmd */
|
|
140
|
+
}
|
|
141
|
+
return { ran: true, key, node_id: selfId, action: `crtr ${argv.join(' ')}` };
|
|
142
|
+
},
|
|
143
|
+
});
|
package/dist/commands/daemon.js
CHANGED
|
@@ -14,6 +14,8 @@ import { readPidfile, isPidAlive } from '../daemon/crtrd.js';
|
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
const daemonStart = defineLeaf({
|
|
16
16
|
name: 'start',
|
|
17
|
+
description: 'start the daemon in the background',
|
|
18
|
+
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)',
|
|
17
19
|
help: {
|
|
18
20
|
name: 'canvas daemon start',
|
|
19
21
|
summary: 'start the crtrd supervisor daemon in the background (no-op if already running)',
|
|
@@ -39,6 +41,8 @@ const daemonStart = defineLeaf({
|
|
|
39
41
|
// ---------------------------------------------------------------------------
|
|
40
42
|
const daemonStatus = defineLeaf({
|
|
41
43
|
name: 'status',
|
|
44
|
+
description: 'check whether the daemon is running',
|
|
45
|
+
whenToUse: 'checking whether the crtrd supervisor is running — e.g. confirming supervision is up before relying on auto-revive',
|
|
42
46
|
help: {
|
|
43
47
|
name: 'canvas daemon status',
|
|
44
48
|
summary: 'check whether the crtrd supervisor daemon is currently running',
|
|
@@ -63,6 +67,8 @@ const daemonStatus = defineLeaf({
|
|
|
63
67
|
// ---------------------------------------------------------------------------
|
|
64
68
|
const daemonStop = defineLeaf({
|
|
65
69
|
name: 'stop',
|
|
70
|
+
description: 'stop the running daemon',
|
|
71
|
+
whenToUse: 'shutting the crtrd supervisor down, ending auto-revival of nodes on window exit',
|
|
66
72
|
help: {
|
|
67
73
|
name: 'canvas daemon stop',
|
|
68
74
|
summary: 'send SIGTERM to the crtrd supervisor daemon',
|
|
@@ -97,15 +103,12 @@ const daemonStop = defineLeaf({
|
|
|
97
103
|
// ---------------------------------------------------------------------------
|
|
98
104
|
export const daemonBranch = defineBranch({
|
|
99
105
|
name: 'daemon',
|
|
106
|
+
description: 'manage the crtrd supervisor process',
|
|
107
|
+
whenToUse: 'managing the crtrd supervisor that auto-revives nodes on window exit — start it, check its status, or stop it',
|
|
100
108
|
help: {
|
|
101
109
|
name: 'canvas daemon',
|
|
102
110
|
summary: 'manage the crtrd canvas supervisor daemon',
|
|
103
111
|
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.',
|
|
104
|
-
children: [
|
|
105
|
-
{ name: 'start', desc: 'start the daemon in the background', useWhen: 'bringing up the supervisor for the first time in a session' },
|
|
106
|
-
{ name: 'status', desc: 'check whether the daemon is running', useWhen: 'verifying the daemon is up before spawning canvas nodes' },
|
|
107
|
-
{ name: 'stop', desc: 'stop the running daemon', useWhen: 'shutting down supervision' },
|
|
108
|
-
],
|
|
109
112
|
},
|
|
110
113
|
children: [daemonStart, daemonStatus, daemonStop],
|
|
111
114
|
});
|
|
@@ -14,6 +14,8 @@ import { renderTree, renderForest, dashboardRows, dashboardRowsAll } from '../co
|
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
export const dashboardLeaf = defineLeaf({
|
|
16
16
|
name: 'dashboard',
|
|
17
|
+
description: 'render the canvas as a subscription tree',
|
|
18
|
+
whenToUse: 'you want the whole graph at a glance as a rendered tree — the subscription forest drawn in ASCII so you can read its SHAPE: who reports to whom, how deep each branch runs, plus each node\'s status and context size. Scope to one root or show the full forest. Use `node inspect list` instead for a flat roster without the tree, `node inspect show` to drill into one node\'s neighbors, and `canvas attention` to find which nodes are blocked on a human',
|
|
17
19
|
help: {
|
|
18
20
|
name: 'canvas dashboard',
|
|
19
21
|
summary: 'render the canvas as an ASCII subscription tree — scoped to a root or the full forest',
|
|
@@ -7,7 +7,7 @@ import { mkdirSync, existsSync } from 'node:fs';
|
|
|
7
7
|
import { join, resolve } from 'node:path';
|
|
8
8
|
import { randomBytes } from 'node:crypto';
|
|
9
9
|
import { validateDeck, approveDeck, notifyDeck, atomicWriteJson, deckPath, display, } from '@crouton-kit/humanloop';
|
|
10
|
-
import { DECK_SCHEMA_HINT,
|
|
10
|
+
import { DECK_SCHEMA_HINT, followUpReview, spawnHumanJob, pickPlacement, runCmd, resolveMaxPanes, } from './shared.js';
|
|
11
11
|
/** The asking node's id, or null when run from a bare shell (no parent to route to). */
|
|
12
12
|
function askingNode() {
|
|
13
13
|
return process.env['CRTR_NODE_ID'] ?? null;
|
|
@@ -17,6 +17,8 @@ function askingNode() {
|
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
18
18
|
export const humanAsk = defineLeaf({
|
|
19
19
|
name: 'ask',
|
|
20
|
+
description: 'put a structured choice or open question to a person',
|
|
21
|
+
whenToUse: 'you would otherwise lay a decision, a set of options, or a question out for the user as prose — reach for this instead, for anything from a quick yes/no to a judgment-heavy call: reviewing all the requirements before building, choosing among implementation patterns, walking a list of risks and deciding what to do about each, settling a naming or scope question, picking which of several findings to act on. Works for open-ended asks too (set `allowFreetext`, offer a few `options` as starting points). The kickoff never blocks, so the human answering on their own time is never a reason to skip the ask and guess instead',
|
|
20
22
|
help: {
|
|
21
23
|
name: 'human ask',
|
|
22
24
|
summary: 'put a humanloop decision deck in front of a person; returns a job handle immediately. This is the default, expected channel for posing ANY question or decision to the user — reach for it instead of writing the question as prose in your reply.',
|
|
@@ -65,6 +67,8 @@ export const humanAsk = defineLeaf({
|
|
|
65
67
|
// ---------------------------------------------------------------------------
|
|
66
68
|
export const humanApprove = defineLeaf({
|
|
67
69
|
name: 'approve',
|
|
70
|
+
description: 'a Yes/No sign-off gate',
|
|
71
|
+
whenToUse: 'a step needs an explicit human yes before it proceeds and a plain answer (not anchored comments) is enough: before a handoff, a merge or deploy, a destructive or irreversible operation, spending real budget, or acting on a risky plan. Reach for `ask` instead when you need them to choose among options or answer something open-ended; reach for `review` when the feedback belongs inline on a document',
|
|
68
72
|
help: {
|
|
69
73
|
name: 'human approve',
|
|
70
74
|
summary: 'a Yes/No approval gate; returns a job handle immediately. The standard way to gate a handoff on human sign-off. Kickoff never blocks — peek at the result later rather than busy-waiting; the human answering on their own time is not a reason to skip the gate.',
|
|
@@ -109,27 +113,27 @@ export const humanApprove = defineLeaf({
|
|
|
109
113
|
// ---------------------------------------------------------------------------
|
|
110
114
|
export const humanReview = defineLeaf({
|
|
111
115
|
name: 'review',
|
|
116
|
+
description: 'collect anchored comments on a .md (plan or spec)',
|
|
117
|
+
whenToUse: 'a human should comment line-by-line on a document rather than give one overall answer: reviewing a plan or spec before you build it, marking up a draft, flagging specific sections to change. The comments come back anchored to the lines they touch. Use `approve` instead for a single yes/no on the whole thing, or `ask` to pose a discrete choice',
|
|
112
118
|
help: {
|
|
113
119
|
name: 'human review',
|
|
114
|
-
summary:
|
|
115
|
-
guide:
|
|
120
|
+
summary: "put a .md live on the human's screen for anchored, line-by-line comments; returns a job handle instantly (a non-blocking kickoff, like ask/approve). The pane tracks the file and re-renders on every save, so edit in place rather than re-presenting.",
|
|
121
|
+
guide: "A kickoff, not a blocking call: it returns at once and the human reviews on their own time. When they submit, the FeedbackResult (anchored comments, plus any line edits they made) is pushed to your inbox — waking you — and autosaved to `output`; you never poll, verify it opened, or background it. The pane is a LIVE view of the file: keep editing the .md in place and it updates, so do not cancel and re-present to show a change. The .md is directive-flavored markdown rendered by termrender (panels, columns, trees, callouts, mermaid) — see `termrender doc -h` for the directive set before authoring one.",
|
|
116
122
|
params: [
|
|
117
123
|
{ kind: 'positional', name: 'file', type: 'path', required: true, constraint: 'Absolute path to an existing .md file.' },
|
|
118
124
|
{ kind: 'flag', name: 'output', type: 'path', required: false, constraint: 'Where the FeedbackResult JSON is written. Default: <dir>/feedback.json.' },
|
|
119
125
|
],
|
|
120
126
|
output: [
|
|
121
|
-
{ name: 'job_id', type: 'string', required: true, constraint: 'Node id of the kind:"human" node backing this review.' },
|
|
122
|
-
{ name: '
|
|
123
|
-
{ name: '
|
|
124
|
-
{ name: '
|
|
125
|
-
{ name: 'reason', type: 'string', required: false, constraint: 'Short explanation when status is failed or closed.' },
|
|
126
|
-
{ name: 'follow_up', type: 'string', required: false, constraint: 'Present only when off-tmux: a human must drain the review via `crtr human inbox`, then read the result.' },
|
|
127
|
+
{ name: 'job_id', type: 'string', required: true, constraint: 'Node id of the kind:"human" node backing this review. Its FeedbackResult is pushed to your inbox when the human submits.' },
|
|
128
|
+
{ name: 'dir', type: 'string', required: true, constraint: 'Interaction directory holding run.json and the autosaved feedback.json.' },
|
|
129
|
+
{ name: 'output', type: 'string', required: true, constraint: 'Path the FeedbackResult JSON is autosaved to when the human submits.' },
|
|
130
|
+
{ name: 'follow_up', type: 'string', required: true, constraint: 'A non-blocking road sign. The human may take minutes to hours; do not block, poll, or verify the pane — just end your turn and you are woken when they submit.' },
|
|
127
131
|
],
|
|
128
132
|
outputKind: 'object',
|
|
129
133
|
effects: [
|
|
130
134
|
'Creates a kind:"human" node under you and writes run.json to the interaction dir.',
|
|
131
|
-
'Spawns a read-only
|
|
132
|
-
'
|
|
135
|
+
'Spawns a live, read-only review session (in a detached tmux pane when in tmux) that tracks the file and re-renders on save.',
|
|
136
|
+
'Returns instantly; the anchored comments fan into your inbox when the human submits (off-tmux, drain via `crtr human inbox`).',
|
|
133
137
|
],
|
|
134
138
|
},
|
|
135
139
|
run: async (input) => {
|
|
@@ -159,22 +163,15 @@ export const humanReview = defineLeaf({
|
|
|
159
163
|
const output = outputArg !== undefined ? outputArg : join(idir, 'feedback.json');
|
|
160
164
|
const rc = { mode: 'review', job_id: jobId, file: abs, output };
|
|
161
165
|
atomicWriteJson(join(idir, 'run.json'), rc);
|
|
162
|
-
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const r = await waitForFinalReport(jobId, paneId);
|
|
172
|
-
const out = { job_id: jobId, output, status: r.status };
|
|
173
|
-
if (r.result !== undefined)
|
|
174
|
-
out['result'] = r.result;
|
|
175
|
-
if (r.reason !== undefined)
|
|
176
|
-
out['reason'] = r.reason;
|
|
177
|
-
return out;
|
|
166
|
+
// review is a non-blocking kickoff, exactly like ask/approve: the detached
|
|
167
|
+
// `_run` worker pushes the FeedbackResult as this node's final report when
|
|
168
|
+
// the human submits, which fans into our inbox and wakes us (and is also
|
|
169
|
+
// autosaved to `output`). There is nothing to block on — the doc is live on
|
|
170
|
+
// the human's screen and tracks the file, so the caller keeps editing in
|
|
171
|
+
// place or simply ends its turn.
|
|
172
|
+
const { spawned, follow_up: drainFollowUp } = spawnHumanJob(jobId, idir, cwd);
|
|
173
|
+
const follow_up = spawned ? followUpReview(jobId) : drainFollowUp;
|
|
174
|
+
return { job_id: jobId, dir: idir, output, follow_up };
|
|
178
175
|
},
|
|
179
176
|
});
|
|
180
177
|
// ---------------------------------------------------------------------------
|
|
@@ -182,6 +179,8 @@ export const humanReview = defineLeaf({
|
|
|
182
179
|
// ---------------------------------------------------------------------------
|
|
183
180
|
export const humanNotify = defineLeaf({
|
|
184
181
|
name: 'notify',
|
|
182
|
+
description: 'fire-and-forget acknowledgement, no reply expected',
|
|
183
|
+
whenToUse: 'informing a person without blocking or expecting an answer',
|
|
185
184
|
help: {
|
|
186
185
|
name: 'human notify',
|
|
187
186
|
summary: 'show a fire-and-forget acknowledgement; creates no job',
|
|
@@ -229,6 +228,8 @@ export const humanNotify = defineLeaf({
|
|
|
229
228
|
// ---------------------------------------------------------------------------
|
|
230
229
|
export const humanShow = defineLeaf({
|
|
231
230
|
name: 'show',
|
|
231
|
+
description: "put a file live on the human's screen",
|
|
232
|
+
whenToUse: 'displaying a doc on screen while a human comments',
|
|
232
233
|
help: {
|
|
233
234
|
name: 'human show',
|
|
234
235
|
summary: 'put a file live on screen in a tmux pane via humanloop display',
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export declare const humanInbox: import("../../core/command.js").LeafDef;
|
|
2
2
|
export declare const humanList: import("../../core/command.js").LeafDef;
|
|
3
|
+
export declare const humanCancel: import("../../core/command.js").LeafDef;
|
|
3
4
|
export declare const humanRun: import("../../core/command.js").LeafDef;
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import { defineLeaf } from '../../core/command.js';
|
|
2
|
+
import { InputError } from '../../core/io.js';
|
|
2
3
|
import { pushFinal } from '../../core/feed/feed.js';
|
|
3
|
-
import { interactionsRoot } from '../../core/artifact.js';
|
|
4
|
+
import { interactionsRoot, interactionDir } from '../../core/artifact.js';
|
|
4
5
|
import { paginate } from '../../core/pagination.js';
|
|
6
|
+
import { getNode, subscribersOf } from '../../core/canvas/index.js';
|
|
7
|
+
import { transition } from '../../core/runtime/lifecycle.js';
|
|
8
|
+
import { appendInbox } from '../../core/feed/inbox.js';
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
5
10
|
import { join } from 'node:path';
|
|
6
|
-
import { inbox, scanInbox, parseDeck, deckPath, ask, launchReview, readJson, } from '@crouton-kit/humanloop';
|
|
11
|
+
import { inbox, scanInbox, parseDeck, deckPath, responsePath, isResolved, atomicWriteJson, ask, launchReview, readJson, } from '@crouton-kit/humanloop';
|
|
12
|
+
import { killPane } from './shared.js';
|
|
7
13
|
// ---------------------------------------------------------------------------
|
|
8
14
|
// inbox (human-invoked, blocking)
|
|
9
15
|
// ---------------------------------------------------------------------------
|
|
10
16
|
export const humanInbox = defineLeaf({
|
|
11
17
|
name: 'inbox',
|
|
18
|
+
description: 'interactively drain pending interactions',
|
|
19
|
+
whenToUse: 'a human is clearing the queue at their terminal',
|
|
12
20
|
help: {
|
|
13
21
|
name: 'human inbox',
|
|
14
22
|
summary: 'interactively drain pending interactions at your own terminal',
|
|
@@ -28,6 +36,8 @@ export const humanInbox = defineLeaf({
|
|
|
28
36
|
// ---------------------------------------------------------------------------
|
|
29
37
|
export const humanList = defineLeaf({
|
|
30
38
|
name: 'list',
|
|
39
|
+
description: 'enumerate pending interactions',
|
|
40
|
+
whenToUse: 'discovering what is blocked on a human',
|
|
31
41
|
help: {
|
|
32
42
|
name: 'human list',
|
|
33
43
|
summary: 'paginated list of pending, unclaimed interactions, oldest first',
|
|
@@ -71,10 +81,103 @@ export const humanList = defineLeaf({
|
|
|
71
81
|
},
|
|
72
82
|
});
|
|
73
83
|
// ---------------------------------------------------------------------------
|
|
84
|
+
// cancel — retract a pending ask/approve/review
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
export const humanCancel = defineLeaf({
|
|
87
|
+
name: 'cancel',
|
|
88
|
+
description: 'retract a pending ask/approve/review',
|
|
89
|
+
whenToUse: 'a question went stale before the human answered',
|
|
90
|
+
help: {
|
|
91
|
+
name: 'human cancel',
|
|
92
|
+
summary: 'retract a pending ask/approve/review you posed — kills its TUI pane, drops it from the human queue, and retires the node. Reach for this the moment a question goes stale (you answered it yourself, the situation changed) so a human is not left resolving a prompt whose answer no longer matters',
|
|
93
|
+
guide: 'Pass the job_id returned by `human ask`/`approve`/`review`. Best-effort and idempotent: if the human already answered, or it was already canceled, it reports canceled:false with reason "already_resolved" and changes nothing. The agent that posed the deck is almost always the one canceling it, so the caller is never messaged — only OTHER subscribers (e.g. the asking node when a human dismisses the prompt) get a quiet deferred note that no answer is coming. Canceling a review kills its live on-screen pane and delivers no comments — the same quiet deferred note covers it.',
|
|
94
|
+
params: [
|
|
95
|
+
{ kind: 'positional', name: 'job_id', type: 'string', required: true, constraint: 'Node id of the interaction to cancel — the job_id returned by ask/approve/review.' },
|
|
96
|
+
{ kind: 'flag', name: 'reason', type: 'string', required: false, constraint: 'Optional short note delivered to subscribers explaining why it was retracted.' },
|
|
97
|
+
],
|
|
98
|
+
output: [
|
|
99
|
+
{ name: 'canceled', type: 'boolean', required: true, constraint: 'True when the interaction was retracted; false when there was nothing live to cancel (already answered/canceled).' },
|
|
100
|
+
{ name: 'job_id', type: 'string', required: true, constraint: 'The interaction node id.' },
|
|
101
|
+
{ name: 'reason', type: 'string', required: false, constraint: 'Why nothing was canceled (e.g. "already_resolved"), present when canceled is false.' },
|
|
102
|
+
],
|
|
103
|
+
outputKind: 'object',
|
|
104
|
+
effects: [
|
|
105
|
+
"Kills the detached TUI pane (if any) so the prompt leaves the human's screen.",
|
|
106
|
+
'Writes a canceled response.json so the interaction drops out of `human list`/`inbox`.',
|
|
107
|
+
'Marks the node done and, only for subscribers other than the caller, drops a deferred note that no answer is coming.',
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
run: async (input) => {
|
|
111
|
+
const jobId = input['job_id'];
|
|
112
|
+
const reason = input['reason'];
|
|
113
|
+
const node = getNode(jobId);
|
|
114
|
+
if (node === null) {
|
|
115
|
+
throw new InputError({
|
|
116
|
+
error: 'not_found',
|
|
117
|
+
message: `no interaction node: ${jobId}`,
|
|
118
|
+
field: 'job_id',
|
|
119
|
+
next: 'Pass the job_id from human ask/approve/review, or list pending with `crtr human list`.',
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
// Resolve the interaction dir from the node's RECORDED cwd: interaction dirs
|
|
123
|
+
// are keyed by the asking process's cwd, which may differ from the caller's.
|
|
124
|
+
const idir = interactionDir(jobId, node.cwd);
|
|
125
|
+
// Nothing live to cancel: the human already answered, or it was retired.
|
|
126
|
+
// 'canceled' is in the guard too so transition('finalize') below — legal only
|
|
127
|
+
// from active|idle — can never throw on an already-canceled (but unresolved)
|
|
128
|
+
// interaction node.
|
|
129
|
+
if (node.status === 'done' || node.status === 'dead' || node.status === 'canceled' || isResolved(idir)) {
|
|
130
|
+
return { canceled: false, job_id: jobId, reason: 'already_resolved' };
|
|
131
|
+
}
|
|
132
|
+
// (1) Kill the detached TUI pane so the prompt (or a review's live doc) leaves
|
|
133
|
+
// the human's screen. Pass `idir` so killPane only fires when the target
|
|
134
|
+
// pane is provably the worker we spawned for THIS job (its launch command
|
|
135
|
+
// carries CRTR_HUMAN_DIR=idir) — never the agent's own pane or a shell.
|
|
136
|
+
const rc = readJson(join(idir, 'run.json'));
|
|
137
|
+
if (rc?.pane_id !== undefined && rc.pane_id !== '')
|
|
138
|
+
killPane(rc.pane_id, idir);
|
|
139
|
+
// (2) Drop it from the human queue: a response.json marks the dir resolved,
|
|
140
|
+
// so scanInbox (human list/inbox) skips it.
|
|
141
|
+
if (existsSync(idir)) {
|
|
142
|
+
atomicWriteJson(responsePath(idir), {
|
|
143
|
+
canceled: true,
|
|
144
|
+
canceledAt: new Date().toISOString(),
|
|
145
|
+
...(reason !== undefined && reason !== '' ? { reason } : {}),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
// (3) Retire the node. We do NOT push a -final.md report: a cancel must not
|
|
149
|
+
// masquerade as a human-submitted result. Subscribers get the quiet
|
|
150
|
+
// deferred 'no answer is coming' note below instead.
|
|
151
|
+
transition(jobId, 'finalize');
|
|
152
|
+
// Almost always the asking agent cancels its OWN deck — it already knows, so
|
|
153
|
+
// never message the caller. Only a third-party cancel (a human, an
|
|
154
|
+
// orchestrator) leaves a genuinely-waiting asker uninformed; give them a
|
|
155
|
+
// quiet deferred note (informational, never nudges) so nobody waits forever.
|
|
156
|
+
const caller = process.env['CRTR_NODE_ID'] ?? 'human';
|
|
157
|
+
const note = reason !== undefined && reason !== '' ? ` — ${reason}` : '';
|
|
158
|
+
for (const sub of subscribersOf(jobId)) {
|
|
159
|
+
if (sub.node_id === caller)
|
|
160
|
+
continue; // don't ping whoever issued the cancel
|
|
161
|
+
appendInbox(sub.node_id, {
|
|
162
|
+
from: caller,
|
|
163
|
+
tier: 'deferred',
|
|
164
|
+
kind: 'message',
|
|
165
|
+
label: `human interaction ${jobId} canceled — no answer is coming${note}`,
|
|
166
|
+
data: { body: `The human interaction ${jobId} was canceled${note}. No response will arrive.` },
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return { canceled: true, job_id: jobId };
|
|
170
|
+
},
|
|
171
|
+
render: (r) => r['canceled'] === true
|
|
172
|
+
? `<canceled job_id="${r['job_id']}"/>`
|
|
173
|
+
: `<cancel-noop job_id="${r['job_id']}">${r['reason'] ?? 'nothing to cancel'}</cancel-noop>`,
|
|
174
|
+
});
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
74
176
|
// _run (hidden worker; not listed in branch help)
|
|
75
177
|
// ---------------------------------------------------------------------------
|
|
76
178
|
export const humanRun = defineLeaf({
|
|
77
179
|
name: '_run',
|
|
180
|
+
tier: 'hidden',
|
|
78
181
|
help: {
|
|
79
182
|
name: 'human _run',
|
|
80
183
|
summary: 'internal: the detached worker that runs the blocking humanloop call at the pane TTY',
|
|
@@ -5,39 +5,49 @@ export interface RunRecord {
|
|
|
5
5
|
approve_iid?: string;
|
|
6
6
|
file?: string;
|
|
7
7
|
output?: string;
|
|
8
|
+
/** tmux pane id of the detached TUI, recorded so `human cancel` can kill it. */
|
|
9
|
+
pane_id?: string;
|
|
8
10
|
}
|
|
9
11
|
export declare function resolveMaxPanes(): number;
|
|
10
12
|
export declare function pickPlacement(): 'split-h' | 'new-window';
|
|
11
13
|
export declare function runCmd(dir: string): string;
|
|
12
14
|
export declare function followUpResult(_jobId: string): string;
|
|
13
15
|
export declare function followUpDrain(_jobId: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Road sign for a spawned `human review`. It is a non-blocking kickoff, so the
|
|
18
|
+
* text steers the caller to stop rather than wait, verify, or re-present: the
|
|
19
|
+
* pane is already live and tracks the file, and the comments arrive via the
|
|
20
|
+
* inbox/wake when the human submits.
|
|
21
|
+
*/
|
|
22
|
+
export declare function followUpReview(_jobId: string): string;
|
|
14
23
|
/**
|
|
15
24
|
* Spawn the detached `_run` pane that drives the humanloop TUI for this node.
|
|
16
|
-
* Returns whether the pane spawned
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* spawn fails — kickoffs are intentionally non-fatal off-tmux.
|
|
25
|
+
* Returns whether the pane spawned and the follow_up road sign. Degrades to the
|
|
26
|
+
* inbox-drain follow_up when not in tmux / spawn fails — kickoffs are
|
|
27
|
+
* intentionally non-fatal off-tmux.
|
|
20
28
|
*
|
|
21
29
|
* Completion routing needs no bookkeeping here: the human node was created
|
|
22
30
|
* under the asking node as its parent (spawnNode auto-subscribes the parent),
|
|
23
|
-
* so the `pushFinal` the `_run` worker emits
|
|
24
|
-
* asking node's inbox.
|
|
31
|
+
* so the `pushFinal` the `_run` worker emits — for ask, approve, AND review —
|
|
32
|
+
* fans the answer straight into the asking node's inbox. The pane id is recorded
|
|
33
|
+
* on run.json (not returned) so `human cancel` can later kill the TUI.
|
|
25
34
|
*/
|
|
26
35
|
export declare function spawnHumanJob(jobId: string, idir: string, cwd: string): {
|
|
27
36
|
spawned: boolean;
|
|
28
37
|
follow_up: string;
|
|
29
|
-
paneId?: string;
|
|
30
38
|
};
|
|
31
|
-
export interface HumanResult {
|
|
32
|
-
status: string;
|
|
33
|
-
result?: unknown;
|
|
34
|
-
reason?: string;
|
|
35
|
-
}
|
|
36
39
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
40
|
+
* Best-effort kill of a humanloop worker pane. SAFETY-CRITICAL: a malformed or
|
|
41
|
+
* empty `-t` target makes tmux fall back to the CALLER's current pane, so a bad
|
|
42
|
+
* paneId could kill the agent's own pi pane (and, if it is the last pane, the
|
|
43
|
+
* whole session). This refuses to kill anything that is not provably the worker:
|
|
44
|
+
*
|
|
45
|
+
* 1. paneId must be a real tmux pane id (`%<n>`) — never an empty/odd string.
|
|
46
|
+
* 2. The pane's start command must contain `verify` (the interaction dir, which
|
|
47
|
+
* humanloop bakes into the worker's `CRTR_HUMAN_DIR=... crtr human _run`
|
|
48
|
+
* launch). A shell (`zsh -l`) or the agent's pi never matches, so we can
|
|
49
|
+
* only ever kill the exact worker we spawned for this job.
|
|
50
|
+
*
|
|
51
|
+
* Returns true only when a matching pane was found and killed. Never throws.
|
|
42
52
|
*/
|
|
43
|
-
export declare function
|
|
53
|
+
export declare function killPane(paneId: string, verify: string): boolean;
|