@crouton-kit/crouter 0.3.8 → 0.3.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/crtrd +2 -0
- package/dist/builtin-personas/design/base.md +9 -0
- package/dist/builtin-personas/design/orchestrator.md +10 -0
- package/dist/builtin-personas/developer/base.md +9 -0
- package/dist/builtin-personas/developer/orchestrator.md +12 -0
- package/dist/builtin-personas/explore/base.md +9 -0
- package/dist/builtin-personas/explore/orchestrator.md +9 -0
- package/dist/builtin-personas/general/base.md +5 -0
- package/dist/builtin-personas/general/orchestrator.md +7 -0
- package/dist/builtin-personas/orchestration-kernel.md +71 -0
- package/dist/builtin-personas/plan/base.md +7 -0
- package/dist/builtin-personas/plan/orchestrator.md +12 -0
- package/dist/builtin-personas/review/base.md +7 -0
- package/dist/builtin-personas/review/orchestrator.md +9 -0
- package/dist/builtin-personas/runtime-base.md +39 -0
- package/dist/builtin-personas/spec/base.md +7 -0
- package/dist/builtin-personas/spec/orchestrator.md +10 -0
- package/dist/builtin-skills/skills/design/SKILL.md +51 -0
- package/dist/builtin-skills/skills/development/SKILL.md +109 -0
- package/dist/builtin-skills/skills/planning/SKILL.md +59 -0
- package/dist/builtin-skills/skills/spec/SKILL.md +83 -0
- package/dist/cli.js +25 -27
- package/dist/commands/{job.d.ts → attention.d.ts} +1 -1
- package/dist/commands/attention.js +152 -0
- package/dist/commands/canvas.d.ts +2 -0
- package/dist/commands/canvas.js +35 -0
- package/dist/commands/{agent.d.ts → daemon.d.ts} +1 -1
- package/dist/commands/daemon.js +111 -0
- package/dist/commands/dashboard.d.ts +2 -0
- package/dist/commands/dashboard.js +65 -0
- package/dist/commands/human/prompts.d.ts +5 -0
- package/dist/commands/human/prompts.js +269 -0
- package/dist/commands/human/queue.d.ts +3 -0
- package/dist/commands/human/queue.js +133 -0
- package/dist/commands/human/shared.d.ts +43 -0
- package/dist/commands/human/shared.js +107 -0
- package/dist/commands/human.js +15 -427
- package/dist/commands/node.d.ts +2 -0
- package/dist/commands/node.js +354 -0
- package/dist/commands/pkg/market-inspect.d.ts +1 -0
- package/dist/commands/pkg/market-inspect.js +157 -0
- package/dist/commands/pkg/market-manage.d.ts +1 -0
- package/dist/commands/pkg/market-manage.js +316 -0
- package/dist/commands/pkg/market.d.ts +1 -0
- package/dist/commands/pkg/market.js +16 -0
- package/dist/commands/pkg/plugin-inspect.d.ts +1 -0
- package/dist/commands/pkg/plugin-inspect.js +142 -0
- package/dist/commands/pkg/plugin-manage.d.ts +1 -0
- package/dist/commands/pkg/plugin-manage.js +294 -0
- package/dist/commands/pkg/plugin.d.ts +1 -0
- package/dist/commands/pkg/plugin.js +16 -0
- package/dist/commands/pkg/shared.d.ts +5 -0
- package/dist/commands/pkg/shared.js +61 -0
- package/dist/commands/pkg.js +8 -1004
- package/dist/commands/push.d.ts +3 -0
- package/dist/commands/push.js +159 -0
- package/dist/commands/revive.d.ts +2 -0
- package/dist/commands/revive.js +64 -0
- package/dist/commands/skill/author.d.ts +3 -0
- package/dist/commands/skill/author.js +147 -0
- package/dist/commands/skill/find.d.ts +4 -0
- package/dist/commands/skill/find.js +254 -0
- package/dist/commands/skill/read.d.ts +1 -0
- package/dist/commands/skill/read.js +89 -0
- package/dist/commands/skill/shared.d.ts +19 -0
- package/dist/commands/skill/shared.js +207 -0
- package/dist/commands/skill/state.d.ts +3 -0
- package/dist/commands/skill/state.js +69 -0
- package/dist/commands/skill.js +12 -681
- package/dist/commands/sys/config.d.ts +1 -0
- package/dist/commands/sys/config.js +186 -0
- package/dist/commands/sys/doctor.d.ts +1 -0
- package/dist/commands/sys/doctor.js +369 -0
- package/dist/commands/sys/shared.d.ts +3 -0
- package/dist/commands/sys/shared.js +24 -0
- package/dist/commands/sys/update.d.ts +2 -0
- package/dist/commands/sys/update.js +114 -0
- package/dist/commands/sys.js +9 -694
- package/dist/core/__tests__/argv-parser.test.js +19 -1
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +100 -0
- package/dist/core/__tests__/canvas.test.js +154 -0
- package/dist/core/__tests__/reset.test.js +105 -0
- package/dist/core/__tests__/resolver.test.js +69 -1
- package/dist/core/__tests__/unknown-path.test.d.ts +1 -0
- package/dist/core/__tests__/unknown-path.test.js +52 -0
- package/dist/core/bootstrap.d.ts +2 -0
- package/dist/core/bootstrap.js +66 -0
- package/dist/core/canvas/attention.d.ts +24 -0
- package/dist/core/canvas/attention.js +94 -0
- package/dist/core/canvas/canvas.d.ts +40 -0
- package/dist/core/canvas/canvas.js +210 -0
- package/dist/core/canvas/db.d.ts +7 -0
- package/dist/core/canvas/db.js +61 -0
- package/dist/core/canvas/index.d.ts +4 -0
- package/dist/core/canvas/index.js +6 -0
- package/dist/core/canvas/paths.d.ts +16 -0
- package/dist/core/canvas/paths.js +62 -0
- package/dist/core/canvas/render.d.ts +30 -0
- package/dist/core/canvas/render.js +186 -0
- package/dist/core/canvas/types.d.ts +87 -0
- package/dist/core/canvas/types.js +8 -0
- package/dist/core/command.d.ts +63 -2
- package/dist/core/command.js +97 -24
- package/dist/core/feed/feed.d.ts +43 -0
- package/dist/core/feed/feed.js +116 -0
- package/dist/core/feed/inbox.d.ts +50 -0
- package/dist/core/feed/inbox.js +124 -0
- package/dist/core/frontmatter.d.ts +10 -0
- package/dist/core/frontmatter.js +24 -9
- package/dist/core/help.d.ts +39 -8
- package/dist/core/help.js +69 -35
- package/dist/core/io.d.ts +15 -1
- package/dist/core/io.js +56 -6
- package/dist/core/personas/index.d.ts +12 -0
- package/dist/core/personas/index.js +10 -0
- package/dist/core/personas/loader.d.ts +44 -0
- package/dist/core/personas/loader.js +157 -0
- package/dist/core/personas/resolve.d.ts +36 -0
- package/dist/core/personas/resolve.js +110 -0
- package/dist/core/render.d.ts +11 -0
- package/dist/core/render.js +126 -0
- package/dist/core/resolver.d.ts +10 -0
- package/dist/core/resolver.js +160 -2
- package/dist/core/runtime/front-door.d.ts +10 -0
- package/dist/core/runtime/front-door.js +97 -0
- package/dist/core/runtime/kickoff.d.ts +23 -0
- package/dist/core/runtime/kickoff.js +134 -0
- package/dist/core/runtime/launch.d.ts +34 -0
- package/dist/core/runtime/launch.js +85 -0
- package/dist/core/runtime/nodes.d.ts +38 -0
- package/dist/core/runtime/nodes.js +95 -0
- package/dist/core/runtime/presence.d.ts +38 -0
- package/dist/core/runtime/presence.js +152 -0
- package/dist/core/runtime/promote.d.ts +30 -0
- package/dist/core/runtime/promote.js +105 -0
- package/dist/core/runtime/reset.d.ts +13 -0
- package/dist/core/runtime/reset.js +97 -0
- package/dist/core/runtime/revive.d.ts +26 -0
- package/dist/core/runtime/revive.js +89 -0
- package/dist/core/runtime/roadmap.d.ts +12 -0
- package/dist/core/runtime/roadmap.js +52 -0
- package/dist/core/runtime/spawn.d.ts +33 -0
- package/dist/core/runtime/spawn.js +118 -0
- package/dist/core/runtime/stop-guard.d.ts +18 -0
- package/dist/core/runtime/stop-guard.js +33 -0
- package/dist/core/runtime/tmux.d.ts +88 -0
- package/dist/core/runtime/tmux.js +198 -0
- package/dist/core/spawn.d.ts +17 -80
- package/dist/core/spawn.js +15 -219
- package/dist/daemon/crtrd-cli.d.ts +1 -0
- package/dist/daemon/crtrd-cli.js +4 -0
- package/dist/daemon/crtrd.d.ts +20 -0
- package/dist/daemon/crtrd.js +200 -0
- package/dist/daemon/manage.d.ts +17 -0
- package/dist/daemon/manage.js +57 -0
- package/dist/pi-extensions/canvas-inbox-watcher.d.ts +16 -0
- package/dist/pi-extensions/canvas-inbox-watcher.js +229 -0
- package/dist/pi-extensions/canvas-nav.d.ts +32 -0
- package/dist/pi-extensions/canvas-nav.js +536 -0
- package/dist/pi-extensions/canvas-stophook.d.ts +17 -0
- package/dist/pi-extensions/canvas-stophook.js +373 -0
- package/dist/types.d.ts +21 -0
- package/dist/types.js +3 -0
- package/package.json +6 -5
- package/dist/commands/agent.js +0 -384
- package/dist/commands/debug.d.ts +0 -3
- package/dist/commands/debug.js +0 -179
- package/dist/commands/job.js +0 -344
- package/dist/commands/plan.d.ts +0 -4
- package/dist/commands/plan.js +0 -309
- package/dist/commands/spec.d.ts +0 -3
- package/dist/commands/spec.js +0 -286
- package/dist/core/__tests__/flow-leaves.test.js +0 -248
- package/dist/core/__tests__/job.test.js +0 -310
- package/dist/core/__tests__/jobs.test.js +0 -66
- package/dist/core/jobs.d.ts +0 -101
- package/dist/core/jobs.js +0 -462
- package/dist/prompts/agent.d.ts +0 -18
- package/dist/prompts/agent.js +0 -153
- package/dist/prompts/debug.d.ts +0 -8
- package/dist/prompts/debug.js +0 -44
- /package/dist/core/__tests__/{flow-leaves.test.d.ts → canvas-inbox-watcher.test.d.ts} +0 -0
- /package/dist/core/__tests__/{job.test.d.ts → canvas.test.d.ts} +0 -0
- /package/dist/core/__tests__/{jobs.test.d.ts → reset.test.d.ts} +0 -0
package/dist/commands/human.js
CHANGED
|
@@ -1,443 +1,31 @@
|
|
|
1
1
|
// `crtr human` subtree — in-process humanloop bridge.
|
|
2
2
|
//
|
|
3
|
-
// Kickoff leaves (ask/approve/review) create a kind:'human'
|
|
4
|
-
// deck.json/run.json into the per-cwd interaction dir, spawn
|
|
5
|
-
// `crtr human _run` pane, and return immediately. The
|
|
6
|
-
//
|
|
7
|
-
// notify/show create no
|
|
8
|
-
// TTY and
|
|
3
|
+
// Kickoff leaves (ask/approve/review) create a kind:'human' node under the
|
|
4
|
+
// asking node, write deck.json/run.json into the per-cwd interaction dir, spawn
|
|
5
|
+
// a detached `crtr human _run` pane, and return immediately. The human's answer
|
|
6
|
+
// is pushed as the node's final report, which fans out to the asking node's
|
|
7
|
+
// inbox — no polling surface. notify/show create no node. _run runs the blocking
|
|
8
|
+
// humanloop call at the pane TTY and pushes the result itself.
|
|
9
9
|
//
|
|
10
10
|
// TTY safety: every leaf is argv-only — none declares a stdin parameter, so
|
|
11
11
|
// the spawned pane's TTY stays free for humanloop's raw-mode input. Control
|
|
12
12
|
// params travel via CRTR_HUMAN_DIR (set inline in the spawned command) +
|
|
13
13
|
// run.json, never stdin.
|
|
14
|
-
import { defineBranch
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import { spawnAndDetach, shellQuote, isInTmux, countPanesInCurrentWindow } from '../core/spawn.js';
|
|
18
|
-
import { interactionsRoot, interactionDir } from '../core/artifact.js';
|
|
19
|
-
import { paginate } from '../core/pagination.js';
|
|
20
|
-
import { readConfig } from '../core/config.js';
|
|
21
|
-
import { mkdirSync, existsSync } from 'node:fs';
|
|
22
|
-
import { join, resolve } from 'node:path';
|
|
23
|
-
import { randomBytes } from 'node:crypto';
|
|
24
|
-
import { ask, launchReview, display, inbox, scanInbox, validateDeck, approveDeck, notifyDeck, parseDeck, deckPath, atomicWriteJson, readJson, } from '@crouton-kit/humanloop';
|
|
25
|
-
const DECK_SCHEMA_HINT = 'Deck must match the humanloop deck schema: {title?, ' +
|
|
26
|
-
'source?:{sessionName?,askedBy?,blockedSince?}, ' +
|
|
27
|
-
'interactions:[{id,title,subtitle?,(body?|bodyPath?),options:[{id,label,' +
|
|
28
|
-
'description?,shortcut?}],multiSelect?,allowFreetext?,freetextLabel?,' +
|
|
29
|
-
"kind?:'notify'|'validation'|'decision'|'context'|'error'}]}.";
|
|
30
|
-
function resolveMaxPanes() {
|
|
31
|
-
return readConfig('user').max_panes_per_window;
|
|
32
|
-
}
|
|
33
|
-
function pickPlacement() {
|
|
34
|
-
return countPanesInCurrentWindow() >= resolveMaxPanes() ? 'new-window' : 'split-h';
|
|
35
|
-
}
|
|
36
|
-
function runCmd(dir) {
|
|
37
|
-
return `CRTR_HUMAN_DIR=${shellQuote(dir)} crtr human _run`;
|
|
38
|
-
}
|
|
39
|
-
function followUpResult(jobId) {
|
|
40
|
-
return `crtr job read result ${jobId}`;
|
|
41
|
-
}
|
|
42
|
-
function followUpDrain(jobId) {
|
|
43
|
-
return ('Not in tmux: a human must drain it — run `crtr human inbox` (or re-run ' +
|
|
44
|
-
`inside tmux). Then: crtr job read result ${jobId}`);
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Spawn the detached `_run` pane for a job-backed kickoff, record the pane for
|
|
48
|
-
* cancellation, log the start, and return the appropriate follow_up. Degrades
|
|
49
|
-
* to the inbox-drain follow_up (job still created) when not in tmux / spawn
|
|
50
|
-
* fails — kickoffs are intentionally non-fatal off-tmux.
|
|
51
|
-
*/
|
|
52
|
-
function spawnHumanJob(jobId, idir, cwd) {
|
|
53
|
-
const spawn = spawnAndDetach({
|
|
54
|
-
command: runCmd(idir),
|
|
55
|
-
cwd,
|
|
56
|
-
jobId,
|
|
57
|
-
placement: pickPlacement(),
|
|
58
|
-
killAfterSeconds: 0,
|
|
59
|
-
failGuard: true,
|
|
60
|
-
});
|
|
61
|
-
if (spawn.status !== 'spawned') {
|
|
62
|
-
return followUpDrain(jobId);
|
|
63
|
-
}
|
|
64
|
-
if (spawn.paneId !== undefined)
|
|
65
|
-
recordJobPane(jobId, spawn.paneId);
|
|
66
|
-
const paneLabel = spawn.paneId !== undefined ? spawn.paneId : 'unknown';
|
|
67
|
-
appendEvent(jobId, {
|
|
68
|
-
level: 'info',
|
|
69
|
-
event: 'worker_started',
|
|
70
|
-
message: `human pane ${paneLabel} spawned`,
|
|
71
|
-
});
|
|
72
|
-
return followUpResult(jobId);
|
|
73
|
-
}
|
|
74
|
-
// ---------------------------------------------------------------------------
|
|
75
|
-
// ask
|
|
76
|
-
// ---------------------------------------------------------------------------
|
|
77
|
-
const humanAsk = defineLeaf({
|
|
78
|
-
name: 'ask',
|
|
79
|
-
help: {
|
|
80
|
-
name: 'human ask',
|
|
81
|
-
summary: 'put a humanloop decision deck in front of a person; returns a job handle immediately. Humans respond on human time (often >10 min) — never block on the result.',
|
|
82
|
-
params: [
|
|
83
|
-
{ kind: 'context-file', name: 'deck', required: true, constraint: 'Contains a humanloop deck. Validated before any job is created.', shape: DECK_SCHEMA_HINT },
|
|
84
|
-
{ kind: 'flag', name: 'wait', type: 'bool', required: false, constraint: 'Accepted for symmetry with the job contract; the kickoff never blocks.' },
|
|
85
|
-
],
|
|
86
|
-
output: [
|
|
87
|
-
{ name: 'job_id', type: 'string', required: true, constraint: 'Poll with `crtr job read result|status|logs`; cancel with `crtr job cancel`.' },
|
|
88
|
-
{ name: 'dir', type: 'string', required: true, constraint: 'Interaction directory holding deck.json/run.json/response.json.' },
|
|
89
|
-
{ name: 'follow_up', type: 'string', required: true, constraint: 'A non-blocking status peek. The human may take minutes to hours — never block waiting on this.' },
|
|
90
|
-
],
|
|
91
|
-
outputKind: 'object',
|
|
92
|
-
effects: [
|
|
93
|
-
'Creates a kind:"human" job and writes deck.json/run.json to the interaction dir.',
|
|
94
|
-
'Spawns the decision TUI in a detached tmux pane (when in tmux).',
|
|
95
|
-
],
|
|
96
|
-
},
|
|
97
|
-
run: async (input) => {
|
|
98
|
-
let deck;
|
|
99
|
-
try {
|
|
100
|
-
deck = validateDeck(input['deck']);
|
|
101
|
-
}
|
|
102
|
-
catch (e) {
|
|
103
|
-
throw new InputError({
|
|
104
|
-
error: 'deck_invalid',
|
|
105
|
-
message: String(e),
|
|
106
|
-
field: 'deck',
|
|
107
|
-
next: DECK_SCHEMA_HINT,
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
const cwd = process.cwd();
|
|
111
|
-
const { jobId } = createJob('human', { cwd });
|
|
112
|
-
const idir = interactionDir(jobId, cwd);
|
|
113
|
-
mkdirSync(idir, { recursive: true });
|
|
114
|
-
atomicWriteJson(deckPath(idir), deck);
|
|
115
|
-
const rc = { mode: 'ask', job_id: jobId };
|
|
116
|
-
atomicWriteJson(join(idir, 'run.json'), rc);
|
|
117
|
-
const follow_up = spawnHumanJob(jobId, idir, cwd);
|
|
118
|
-
return { job_id: jobId, dir: idir, follow_up };
|
|
119
|
-
},
|
|
120
|
-
});
|
|
121
|
-
// ---------------------------------------------------------------------------
|
|
122
|
-
// approve
|
|
123
|
-
// ---------------------------------------------------------------------------
|
|
124
|
-
const humanApprove = defineLeaf({
|
|
125
|
-
name: 'approve',
|
|
126
|
-
help: {
|
|
127
|
-
name: 'human approve',
|
|
128
|
-
summary: 'a Yes/No approval gate; returns a job handle immediately. Humans respond on human time (often >10 min) — never block on the result.',
|
|
129
|
-
params: [
|
|
130
|
-
{ kind: 'positional', name: 'title', type: 'string', required: true, constraint: 'The question shown to the human.' },
|
|
131
|
-
{ kind: 'flag', name: 'subtitle', type: 'string', required: false, constraint: 'Optional one-line context.' },
|
|
132
|
-
{ kind: 'flag', name: 'body', type: 'string', required: false, constraint: 'Optional markdown body.' },
|
|
133
|
-
],
|
|
134
|
-
output: [
|
|
135
|
-
{ name: 'job_id', type: 'string', required: true, constraint: 'Poll with `crtr job read result`; result is {approved, …envelope}.' },
|
|
136
|
-
{ name: 'dir', type: 'string', required: true, constraint: 'Interaction directory.' },
|
|
137
|
-
{ name: 'follow_up', type: 'string', required: true, constraint: 'A non-blocking status peek. The human may take minutes to hours — never block waiting on this.' },
|
|
138
|
-
],
|
|
139
|
-
outputKind: 'object',
|
|
140
|
-
effects: [
|
|
141
|
-
'Creates a kind:"human" job and writes a Yes/No validation deck.',
|
|
142
|
-
'Spawns the approval TUI in a detached tmux pane (when in tmux).',
|
|
143
|
-
],
|
|
144
|
-
},
|
|
145
|
-
run: async (input) => {
|
|
146
|
-
const title = input['title'];
|
|
147
|
-
const subtitle = input['subtitle'];
|
|
148
|
-
const body = input['body'];
|
|
149
|
-
const deck = approveDeck(title, {
|
|
150
|
-
...(subtitle !== undefined ? { subtitle } : {}),
|
|
151
|
-
...(body !== undefined ? { body } : {}),
|
|
152
|
-
});
|
|
153
|
-
const cwd = process.cwd();
|
|
154
|
-
const { jobId } = createJob('human', { cwd });
|
|
155
|
-
const idir = interactionDir(jobId, cwd);
|
|
156
|
-
mkdirSync(idir, { recursive: true });
|
|
157
|
-
atomicWriteJson(deckPath(idir), deck);
|
|
158
|
-
const rc = { mode: 'approve', job_id: jobId, approve_iid: 'approve' };
|
|
159
|
-
atomicWriteJson(join(idir, 'run.json'), rc);
|
|
160
|
-
const follow_up = spawnHumanJob(jobId, idir, cwd);
|
|
161
|
-
return { job_id: jobId, dir: idir, follow_up };
|
|
162
|
-
},
|
|
163
|
-
});
|
|
164
|
-
// ---------------------------------------------------------------------------
|
|
165
|
-
// review
|
|
166
|
-
// ---------------------------------------------------------------------------
|
|
167
|
-
const humanReview = defineLeaf({
|
|
168
|
-
name: 'review',
|
|
169
|
-
help: {
|
|
170
|
-
name: 'human review',
|
|
171
|
-
summary: 'open a .md in a read-only review editor for anchored comments; returns a job handle immediately. Humans respond on human time (often >10 min) — never block on the result.',
|
|
172
|
-
params: [
|
|
173
|
-
{ kind: 'positional', name: 'file', type: 'path', required: true, constraint: 'Absolute path to an existing .md file.' },
|
|
174
|
-
{ kind: 'flag', name: 'output', type: 'path', required: false, constraint: 'Where the FeedbackResult JSON is written. Default: <dir>/feedback.json.' },
|
|
175
|
-
],
|
|
176
|
-
output: [
|
|
177
|
-
{ name: 'job_id', type: 'string', required: true, constraint: 'Poll with `crtr job read result`; result is the humanloop FeedbackResult.' },
|
|
178
|
-
{ name: 'output', type: 'string', required: true, constraint: 'Path the FeedbackResult JSON is autosaved to.' },
|
|
179
|
-
{ name: 'follow_up', type: 'string', required: true, constraint: 'A non-blocking status peek. The human may take minutes to hours — never block waiting on this.' },
|
|
180
|
-
],
|
|
181
|
-
outputKind: 'object',
|
|
182
|
-
effects: [
|
|
183
|
-
'Creates a kind:"human" job and writes run.json to the interaction dir.',
|
|
184
|
-
'Spawns a read-only nvim/vim review session in a detached tmux pane (when in tmux).',
|
|
185
|
-
],
|
|
186
|
-
},
|
|
187
|
-
run: async (input) => {
|
|
188
|
-
const fileArg = input['file'];
|
|
189
|
-
const abs = resolve(fileArg);
|
|
190
|
-
if (!existsSync(abs)) {
|
|
191
|
-
throw new InputError({
|
|
192
|
-
error: 'file_not_found',
|
|
193
|
-
message: `file not found: ${abs}`,
|
|
194
|
-
field: 'file',
|
|
195
|
-
next: 'Provide an absolute path to an existing .md file.',
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
if (!abs.endsWith('.md')) {
|
|
199
|
-
throw new InputError({
|
|
200
|
-
error: 'invalid_field',
|
|
201
|
-
message: `review requires a .md file: ${abs}`,
|
|
202
|
-
field: 'file',
|
|
203
|
-
next: 'Point `file` at a Markdown (.md) artifact.',
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
const cwd = process.cwd();
|
|
207
|
-
const { jobId } = createJob('human', { cwd });
|
|
208
|
-
const idir = interactionDir(jobId, cwd);
|
|
209
|
-
mkdirSync(idir, { recursive: true });
|
|
210
|
-
const outputArg = input['output'];
|
|
211
|
-
const output = outputArg !== undefined ? outputArg : join(idir, 'feedback.json');
|
|
212
|
-
const rc = { mode: 'review', job_id: jobId, file: abs, output };
|
|
213
|
-
atomicWriteJson(join(idir, 'run.json'), rc);
|
|
214
|
-
const follow_up = spawnHumanJob(jobId, idir, cwd);
|
|
215
|
-
return { job_id: jobId, output, follow_up };
|
|
216
|
-
},
|
|
217
|
-
});
|
|
218
|
-
// ---------------------------------------------------------------------------
|
|
219
|
-
// notify (no job)
|
|
220
|
-
// ---------------------------------------------------------------------------
|
|
221
|
-
const humanNotify = defineLeaf({
|
|
222
|
-
name: 'notify',
|
|
223
|
-
help: {
|
|
224
|
-
name: 'human notify',
|
|
225
|
-
summary: 'show a fire-and-forget acknowledgement; creates no job',
|
|
226
|
-
params: [
|
|
227
|
-
{ kind: 'positional', name: 'title', type: 'string', required: true, constraint: 'The notification headline.' },
|
|
228
|
-
{ kind: 'flag', name: 'body', type: 'string', required: false, constraint: 'Optional markdown body.' },
|
|
229
|
-
],
|
|
230
|
-
output: [
|
|
231
|
-
{ name: 'shown', type: 'boolean', required: true, constraint: 'True if the TUI pane was spawned; false when not in tmux (deck surfaces in `human inbox`).' },
|
|
232
|
-
{ name: 'dir', type: 'string', required: true, constraint: 'Interaction directory holding deck.json.' },
|
|
233
|
-
],
|
|
234
|
-
outputKind: 'object',
|
|
235
|
-
effects: [
|
|
236
|
-
'Writes a notify deck to the per-project interactions root.',
|
|
237
|
-
'Spawns the acknowledgement TUI in a detached tmux pane when in tmux. Creates no crtr job.',
|
|
238
|
-
],
|
|
239
|
-
},
|
|
240
|
-
run: async (input) => {
|
|
241
|
-
const title = input['title'];
|
|
242
|
-
const body = input['body'];
|
|
243
|
-
const deck = notifyDeck(title, body !== undefined ? { body } : {});
|
|
244
|
-
const cwd = process.cwd();
|
|
245
|
-
const id = `nfy-${randomBytes(4).toString('hex')}`;
|
|
246
|
-
const idir = interactionDir(id, cwd);
|
|
247
|
-
mkdirSync(idir, { recursive: true });
|
|
248
|
-
atomicWriteJson(deckPath(idir), deck);
|
|
249
|
-
const rc = { mode: 'notify' };
|
|
250
|
-
atomicWriteJson(join(idir, 'run.json'), rc);
|
|
251
|
-
let shown = false;
|
|
252
|
-
if (isInTmux()) {
|
|
253
|
-
const spawn = spawnAndDetach({
|
|
254
|
-
command: runCmd(idir),
|
|
255
|
-
cwd,
|
|
256
|
-
placement: pickPlacement(),
|
|
257
|
-
killAfterSeconds: 0,
|
|
258
|
-
failGuard: false,
|
|
259
|
-
});
|
|
260
|
-
shown = spawn.status === 'spawned';
|
|
261
|
-
}
|
|
262
|
-
return { shown, dir: idir };
|
|
263
|
-
},
|
|
264
|
-
});
|
|
265
|
-
// ---------------------------------------------------------------------------
|
|
266
|
-
// show (no job, non-blocking passthrough)
|
|
267
|
-
// ---------------------------------------------------------------------------
|
|
268
|
-
const humanShow = defineLeaf({
|
|
269
|
-
name: 'show',
|
|
270
|
-
help: {
|
|
271
|
-
name: 'human show',
|
|
272
|
-
summary: 'put a file live on screen in a tmux pane via humanloop display',
|
|
273
|
-
params: [
|
|
274
|
-
{ kind: 'positional', name: 'path', type: 'path', required: true, constraint: 'Path to the file to render.' },
|
|
275
|
-
{ kind: 'flag', name: 'watch', type: 'bool', required: false, constraint: 'When present, live-update the pane on edits. Default off.' },
|
|
276
|
-
{ kind: 'flag', name: 'window', type: 'enum', choices: ['auto', 'split', 'new'], required: false, default: 'auto', constraint: 'Placement. Default auto.' },
|
|
277
|
-
],
|
|
278
|
-
output: [
|
|
279
|
-
{ name: 'pane_id', type: 'string | null', required: true, constraint: 'Tmux pane id, or null when not displayed.' },
|
|
280
|
-
{ name: 'reason', type: 'string | null', required: true, constraint: 'Why no pane was created, or null on success.' },
|
|
281
|
-
],
|
|
282
|
-
outputKind: 'object',
|
|
283
|
-
effects: ['Spawns a live-watch tmux pane when possible. No job. Always exits 0.'],
|
|
284
|
-
},
|
|
285
|
-
run: async (input) => {
|
|
286
|
-
const path = input['path'];
|
|
287
|
-
const watch = input['watch'] === true;
|
|
288
|
-
const windowArg = input['window'];
|
|
289
|
-
const window = windowArg !== undefined ? windowArg : 'auto';
|
|
290
|
-
// `human show` must never fail the caller: any display error degrades to
|
|
291
|
-
// {pane_id:null, reason} with exit 0 (matches humanloop display semantics).
|
|
292
|
-
let paneId;
|
|
293
|
-
try {
|
|
294
|
-
const r = display(path, { watch, window, maxPanes: resolveMaxPanes() });
|
|
295
|
-
paneId = r.paneId;
|
|
296
|
-
}
|
|
297
|
-
catch {
|
|
298
|
-
paneId = undefined;
|
|
299
|
-
}
|
|
300
|
-
if (paneId !== undefined) {
|
|
301
|
-
return { pane_id: paneId, reason: null };
|
|
302
|
-
}
|
|
303
|
-
const reason = isInTmux()
|
|
304
|
-
? 'renderer unavailable (termrender/uv missing)'
|
|
305
|
-
: 'not in tmux';
|
|
306
|
-
return { pane_id: null, reason };
|
|
307
|
-
},
|
|
308
|
-
});
|
|
309
|
-
// ---------------------------------------------------------------------------
|
|
310
|
-
// inbox (human-invoked, blocking)
|
|
311
|
-
// ---------------------------------------------------------------------------
|
|
312
|
-
const humanInbox = defineLeaf({
|
|
313
|
-
name: 'inbox',
|
|
314
|
-
help: {
|
|
315
|
-
name: 'human inbox',
|
|
316
|
-
summary: 'interactively drain pending interactions at your own terminal',
|
|
317
|
-
params: [],
|
|
318
|
-
inputNote: 'No input. Run this at a human terminal — it blocks until the backlog is drained or you quit.',
|
|
319
|
-
output: [{ name: 'drained', type: 'boolean', required: true, constraint: 'True once the loop returns.' }],
|
|
320
|
-
outputKind: 'object',
|
|
321
|
-
effects: ['Resolves pending interactions in the per-project interactions root via the TUI.'],
|
|
322
|
-
},
|
|
323
|
-
run: async () => {
|
|
324
|
-
await inbox([interactionsRoot(process.cwd())]);
|
|
325
|
-
return { drained: true };
|
|
326
|
-
},
|
|
327
|
-
});
|
|
328
|
-
// ---------------------------------------------------------------------------
|
|
329
|
-
// list (read-only, paginated)
|
|
330
|
-
// ---------------------------------------------------------------------------
|
|
331
|
-
const humanList = defineLeaf({
|
|
332
|
-
name: 'list',
|
|
333
|
-
help: {
|
|
334
|
-
name: 'human list',
|
|
335
|
-
summary: 'paginated list of pending, unclaimed interactions, oldest first',
|
|
336
|
-
params: [
|
|
337
|
-
{ kind: 'flag', name: 'limit', type: 'int', required: false, default: 20, constraint: 'Default 20, max 100.' },
|
|
338
|
-
{ kind: 'flag', name: 'cursor', type: 'string', required: false, constraint: "Opaque token from a previous response's next_cursor. Omit on first call." },
|
|
339
|
-
],
|
|
340
|
-
output: [
|
|
341
|
-
{ name: 'items', type: 'object[]', required: true, constraint: 'Each: {id, dir, title, kind, blocked_since}. Oldest first.' },
|
|
342
|
-
{ name: 'next_cursor', type: 'string | null', required: true, constraint: 'Pass on the next call to continue. null means no more items.' },
|
|
343
|
-
{ name: 'total', type: 'integer | null', required: true, constraint: 'Total pending interactions.' },
|
|
344
|
-
],
|
|
345
|
-
outputKind: 'object',
|
|
346
|
-
effects: ['None. Read-only.'],
|
|
347
|
-
},
|
|
348
|
-
run: async (input) => {
|
|
349
|
-
const limitRaw = input['limit'];
|
|
350
|
-
const limit = Math.min(Math.max(1, limitRaw), 100);
|
|
351
|
-
const cursor = input['cursor'];
|
|
352
|
-
const raw = scanInbox([interactionsRoot(process.cwd())]);
|
|
353
|
-
const items = raw
|
|
354
|
-
.map((i) => ({
|
|
355
|
-
id: i.id,
|
|
356
|
-
dir: i.dir,
|
|
357
|
-
title: i.title !== undefined ? i.title : null,
|
|
358
|
-
kind: i.kind !== undefined ? i.kind : null,
|
|
359
|
-
blocked_since: i.blockedSince,
|
|
360
|
-
}))
|
|
361
|
-
.sort((a, b) => {
|
|
362
|
-
const ka = `${a.blocked_since}|${a.id}`;
|
|
363
|
-
const kb = `${b.blocked_since}|${b.id}`;
|
|
364
|
-
return ka < kb ? -1 : ka > kb ? 1 : 0;
|
|
365
|
-
});
|
|
366
|
-
const page = paginate(items, { limit, cursor }, {
|
|
367
|
-
defaultLimit: 20,
|
|
368
|
-
maxLimit: 100,
|
|
369
|
-
keyOf: (i) => `${i.blocked_since}|${i.id}`,
|
|
370
|
-
total: 'count',
|
|
371
|
-
});
|
|
372
|
-
return { items: page.items, next_cursor: page.next_cursor, total: page.total };
|
|
373
|
-
},
|
|
374
|
-
});
|
|
375
|
-
// ---------------------------------------------------------------------------
|
|
376
|
-
// _run (hidden worker; not listed in branch help)
|
|
377
|
-
// ---------------------------------------------------------------------------
|
|
378
|
-
const humanRun = defineLeaf({
|
|
379
|
-
name: '_run',
|
|
380
|
-
help: {
|
|
381
|
-
name: 'human _run',
|
|
382
|
-
summary: 'internal: the detached worker that runs the blocking humanloop call at the pane TTY',
|
|
383
|
-
params: [],
|
|
384
|
-
inputNote: 'Internal; invoked by the spawned pane via CRTR_HUMAN_DIR + run.json. Not for manual use.',
|
|
385
|
-
output: [{ name: 'none', type: 'void', required: false, constraint: 'No stdout; writes the job result file directly.' }],
|
|
386
|
-
outputKind: 'object',
|
|
387
|
-
effects: ['Runs the blocking humanloop call; for job-backed modes writes result.json via the job model.'],
|
|
388
|
-
},
|
|
389
|
-
run: async () => {
|
|
390
|
-
const dir = process.env['CRTR_HUMAN_DIR'];
|
|
391
|
-
if (dir === undefined || dir === '') {
|
|
392
|
-
process.exit(1);
|
|
393
|
-
}
|
|
394
|
-
const rc = readJson(join(dir, 'run.json'));
|
|
395
|
-
if (rc === null) {
|
|
396
|
-
process.exit(1);
|
|
397
|
-
}
|
|
398
|
-
try {
|
|
399
|
-
if (rc.mode === 'ask' || rc.mode === 'approve' || rc.mode === 'notify') {
|
|
400
|
-
const deck = parseDeck(deckPath(dir));
|
|
401
|
-
const env = await ask(deck, { dir });
|
|
402
|
-
if (rc.mode === 'ask') {
|
|
403
|
-
writeResult(rc.job_id, env, 'done');
|
|
404
|
-
}
|
|
405
|
-
else if (rc.mode === 'approve') {
|
|
406
|
-
const sel = env.responses.find((r) => r.id === rc.approve_iid)?.selectedOptionId;
|
|
407
|
-
writeResult(rc.job_id, {
|
|
408
|
-
approved: sel === 'yes',
|
|
409
|
-
summary: env.summary,
|
|
410
|
-
responses: env.responses,
|
|
411
|
-
responsePath: env.responsePath,
|
|
412
|
-
completedAt: env.completedAt,
|
|
413
|
-
}, 'done');
|
|
414
|
-
}
|
|
415
|
-
// notify: no job — nothing to write
|
|
416
|
-
}
|
|
417
|
-
else if (rc.mode === 'review') {
|
|
418
|
-
const res = await launchReview(rc.file, {
|
|
419
|
-
output: rc.output,
|
|
420
|
-
});
|
|
421
|
-
writeResult(rc.job_id, res, 'done');
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
catch (e) {
|
|
425
|
-
if (rc.job_id !== undefined) {
|
|
426
|
-
writeResult(rc.job_id, { error: 'human_run_failed', message: String(e) }, 'failed');
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
},
|
|
430
|
-
});
|
|
431
|
-
// ---------------------------------------------------------------------------
|
|
432
|
-
// branch
|
|
433
|
-
// ---------------------------------------------------------------------------
|
|
14
|
+
import { defineBranch } from '../core/command.js';
|
|
15
|
+
import { humanAsk, humanApprove, humanReview, humanNotify, humanShow } from './human/prompts.js';
|
|
16
|
+
import { humanInbox, humanList, humanRun } from './human/queue.js';
|
|
434
17
|
export function registerHuman() {
|
|
435
18
|
return defineBranch({
|
|
436
19
|
name: 'human',
|
|
20
|
+
rootEntry: {
|
|
21
|
+
concept: 'human-in-the-loop decisions, document review, and live display: ask puts a structured choice to a person, approve gates a handoff on a Yes/No sign-off, review collects anchored comments on a plan or spec, notify informs without blocking, show puts a file live on screen',
|
|
22
|
+
desc: 'ask, approve, review, notify, show, inbox, list',
|
|
23
|
+
useWhen: 'you have a question for the user or want their feedback — always reach for human instead of guessing or assuming when a person can decide',
|
|
24
|
+
},
|
|
437
25
|
help: {
|
|
438
26
|
name: 'human',
|
|
439
27
|
summary: 'human-in-the-loop decisions, document review, and live display',
|
|
440
|
-
model: "
|
|
28
|
+
model: "Reach for human whenever you have a question for the user or want their feedback — never guess or assume when a person can decide. ask puts a structured choice in front of them; approve gates a handoff on a Yes/No sign-off; review collects anchored comments on a plan or spec; notify informs without blocking; show puts a file live on screen. Every body and displayed file is directive-flavored markdown rendered by termrender (panels, columns, trees, callouts, mermaid) — see `termrender doc -h` for the directive set before authoring one. ask/approve/review are the DEFAULT channel for questions, sign-offs, and feedback — reach for them even for quick or open-ended asks (use `allowFreetext`), and don't substitute prose in your reply. ask and approve are kickoffs: they create a kind:'human' node under you and return instantly, never blocking — the answer is pushed to your inbox when the human responds, so just keep working and you'll be woken with it. review is different: it BLOCKS until the human submits, so background the call if you want to keep working (your harness notifies you when it finishes). 'Humans respond on human time' describes response latency only — it is never a reason to avoid asking. notify/show create no node.",
|
|
441
29
|
children: [
|
|
442
30
|
{ name: 'ask', desc: 'put a decision deck to a person', useWhen: 'a structured choice needs a human' },
|
|
443
31
|
{ name: 'approve', desc: 'a Yes/No approval gate', useWhen: 'gating a handoff on human sign-off' },
|