@crouton-kit/crouter 0.2.5 → 0.3.1

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.
Files changed (79) hide show
  1. package/dist/builtin-skills/skills/crouter-development/marketplaces/SKILL.md +9 -9
  2. package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +19 -19
  3. package/dist/cli.js +42 -37
  4. package/dist/commands/__tests__/human.test.d.ts +1 -0
  5. package/dist/commands/__tests__/human.test.js +214 -0
  6. package/dist/commands/__tests__/skill.test.d.ts +1 -0
  7. package/dist/commands/__tests__/skill.test.js +287 -0
  8. package/dist/commands/debug.d.ts +3 -0
  9. package/dist/commands/debug.js +179 -0
  10. package/dist/commands/flow.d.ts +2 -0
  11. package/dist/commands/flow.js +24 -0
  12. package/dist/commands/human.d.ts +2 -0
  13. package/dist/commands/human.js +480 -0
  14. package/dist/commands/job.d.ts +2 -0
  15. package/dist/commands/job.js +669 -0
  16. package/dist/commands/pkg.d.ts +2 -0
  17. package/dist/commands/pkg.js +1021 -0
  18. package/dist/commands/plan.d.ts +4 -2
  19. package/dist/commands/plan.js +306 -22
  20. package/dist/commands/skill.d.ts +2 -2
  21. package/dist/commands/skill.js +615 -459
  22. package/dist/commands/spec.d.ts +3 -2
  23. package/dist/commands/spec.js +283 -10
  24. package/dist/commands/sys.d.ts +2 -0
  25. package/dist/commands/sys.js +712 -0
  26. package/dist/core/__tests__/argv-parser.test.d.ts +1 -0
  27. package/dist/core/__tests__/argv-parser.test.js +199 -0
  28. package/dist/core/__tests__/flow-leaves.test.d.ts +1 -0
  29. package/dist/core/__tests__/flow-leaves.test.js +248 -0
  30. package/dist/core/__tests__/job.test.d.ts +1 -0
  31. package/dist/core/__tests__/job.test.js +346 -0
  32. package/dist/core/__tests__/pkg.test.d.ts +1 -0
  33. package/dist/core/__tests__/pkg.test.js +218 -0
  34. package/dist/core/__tests__/sys.test.d.ts +1 -0
  35. package/dist/core/__tests__/sys.test.js +208 -0
  36. package/dist/core/artifact.d.ts +29 -18
  37. package/dist/core/artifact.js +78 -221
  38. package/dist/core/auto-update.js +11 -3
  39. package/dist/core/command.d.ts +36 -0
  40. package/dist/core/command.js +287 -0
  41. package/dist/core/errors.d.ts +3 -0
  42. package/dist/core/errors.js +5 -0
  43. package/dist/core/fs-utils.d.ts +1 -0
  44. package/dist/core/fs-utils.js +4 -0
  45. package/dist/core/help.d.ts +98 -0
  46. package/dist/core/help.js +163 -0
  47. package/dist/core/io.d.ts +29 -0
  48. package/dist/core/io.js +83 -0
  49. package/dist/core/jobs.d.ts +87 -0
  50. package/dist/core/jobs.js +353 -0
  51. package/dist/core/pagination.d.ts +33 -0
  52. package/dist/core/pagination.js +89 -0
  53. package/dist/core/self-update.d.ts +21 -0
  54. package/dist/{commands/update.js → core/self-update.js} +28 -63
  55. package/dist/core/spawn.d.ts +47 -65
  56. package/dist/core/spawn.js +78 -228
  57. package/dist/prompts/agent.d.ts +10 -5
  58. package/dist/prompts/agent.js +51 -74
  59. package/dist/prompts/debug.d.ts +8 -0
  60. package/dist/prompts/debug.js +37 -0
  61. package/dist/prompts/review.js +4 -11
  62. package/dist/prompts/skill.d.ts +0 -1
  63. package/dist/prompts/skill.js +95 -149
  64. package/package.json +4 -2
  65. package/dist/commands/agent.d.ts +0 -2
  66. package/dist/commands/agent.js +0 -265
  67. package/dist/commands/config.d.ts +0 -2
  68. package/dist/commands/config.js +0 -146
  69. package/dist/commands/doctor.d.ts +0 -2
  70. package/dist/commands/doctor.js +0 -268
  71. package/dist/commands/marketplace.d.ts +0 -2
  72. package/dist/commands/marketplace.js +0 -365
  73. package/dist/commands/plugin.d.ts +0 -2
  74. package/dist/commands/plugin.js +0 -367
  75. package/dist/commands/update.d.ts +0 -4
  76. package/dist/prompts/plan.d.ts +0 -1
  77. package/dist/prompts/plan.js +0 -175
  78. package/dist/prompts/spec.d.ts +0 -1
  79. package/dist/prompts/spec.js +0 -153
@@ -0,0 +1,480 @@
1
+ // `crtr human` subtree — in-process humanloop bridge.
2
+ //
3
+ // Kickoff leaves (ask/approve/review) create a kind:'human' job, write
4
+ // deck.json/run.json into the per-cwd interaction dir, spawn a detached
5
+ // `crtr human _run` pane, and return immediately. The agent polls the existing
6
+ // `crtr job read result|status|logs` / `crtr job cancel` — no new poll surface.
7
+ // notify/show create no job. _run runs the blocking humanloop call at the pane
8
+ // TTY and writes the job result itself.
9
+ //
10
+ // TTY safety: every leaf is argv-only — none declares a stdin parameter, so
11
+ // the spawned pane's TTY stays free for humanloop's raw-mode input. Control
12
+ // params travel via CRTR_HUMAN_DIR (set inline in the spawned command) +
13
+ // run.json, never stdin.
14
+ import { defineBranch, defineLeaf } from '../core/command.js';
15
+ import { InputError } from '../core/io.js';
16
+ import { createJob, writeResult, recordJobPane, appendEvent } from '../core/jobs.js';
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, 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 interaction = {
150
+ id: 'approve',
151
+ title,
152
+ kind: 'validation',
153
+ options: [
154
+ { id: 'yes', label: 'Yes' },
155
+ { id: 'no', label: 'No' },
156
+ ],
157
+ };
158
+ if (subtitle !== undefined)
159
+ interaction['subtitle'] = subtitle;
160
+ if (body !== undefined)
161
+ interaction['body'] = body;
162
+ const deck = validateDeck({ interactions: [interaction] });
163
+ const cwd = process.cwd();
164
+ const { jobId } = createJob('human', { cwd });
165
+ const idir = interactionDir(jobId, cwd);
166
+ mkdirSync(idir, { recursive: true });
167
+ atomicWriteJson(deckPath(idir), deck);
168
+ const rc = { mode: 'approve', job_id: jobId, approve_iid: 'approve' };
169
+ atomicWriteJson(join(idir, 'run.json'), rc);
170
+ const follow_up = spawnHumanJob(jobId, idir, cwd);
171
+ return { job_id: jobId, dir: idir, follow_up };
172
+ },
173
+ });
174
+ // ---------------------------------------------------------------------------
175
+ // review
176
+ // ---------------------------------------------------------------------------
177
+ const humanReview = defineLeaf({
178
+ name: 'review',
179
+ help: {
180
+ name: 'human review',
181
+ 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.',
182
+ params: [
183
+ { kind: 'positional', name: 'file', type: 'path', required: true, constraint: 'Absolute path to an existing .md file.' },
184
+ { kind: 'flag', name: 'output', type: 'path', required: false, constraint: 'Where the FeedbackResult JSON is written. Default: <dir>/feedback.json.' },
185
+ ],
186
+ output: [
187
+ { name: 'job_id', type: 'string', required: true, constraint: 'Poll with `crtr job read result`; result is the humanloop FeedbackResult.' },
188
+ { name: 'output', type: 'string', required: true, constraint: 'Path the FeedbackResult JSON is autosaved to.' },
189
+ { 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.' },
190
+ ],
191
+ outputKind: 'object',
192
+ effects: [
193
+ 'Creates a kind:"human" job and writes run.json to the interaction dir.',
194
+ 'Spawns a read-only nvim/vim review session in a detached tmux pane (when in tmux).',
195
+ ],
196
+ },
197
+ run: async (input) => {
198
+ const fileArg = input['file'];
199
+ const abs = resolve(fileArg);
200
+ if (!existsSync(abs)) {
201
+ throw new InputError({
202
+ error: 'file_not_found',
203
+ message: `file not found: ${abs}`,
204
+ field: 'file',
205
+ next: 'Provide an absolute path to an existing .md file.',
206
+ });
207
+ }
208
+ if (!abs.endsWith('.md')) {
209
+ throw new InputError({
210
+ error: 'invalid_field',
211
+ message: `review requires a .md file: ${abs}`,
212
+ field: 'file',
213
+ next: 'Point `file` at a Markdown (.md) artifact.',
214
+ });
215
+ }
216
+ const cwd = process.cwd();
217
+ const { jobId } = createJob('human', { cwd });
218
+ const idir = interactionDir(jobId, cwd);
219
+ mkdirSync(idir, { recursive: true });
220
+ const outputArg = input['output'];
221
+ const output = outputArg !== undefined ? outputArg : join(idir, 'feedback.json');
222
+ const rc = { mode: 'review', job_id: jobId, file: abs, output };
223
+ atomicWriteJson(join(idir, 'run.json'), rc);
224
+ const follow_up = spawnHumanJob(jobId, idir, cwd);
225
+ return { job_id: jobId, output, follow_up };
226
+ },
227
+ });
228
+ // ---------------------------------------------------------------------------
229
+ // notify (no job)
230
+ // ---------------------------------------------------------------------------
231
+ const humanNotify = defineLeaf({
232
+ name: 'notify',
233
+ help: {
234
+ name: 'human notify',
235
+ summary: 'show a fire-and-forget acknowledgement; creates no job',
236
+ params: [
237
+ { kind: 'positional', name: 'title', type: 'string', required: true, constraint: 'The notification headline.' },
238
+ { kind: 'flag', name: 'body', type: 'string', required: false, constraint: 'Optional markdown body.' },
239
+ ],
240
+ output: [
241
+ { name: 'shown', type: 'boolean', required: true, constraint: 'True if the TUI pane was spawned; false when not in tmux (deck surfaces in `human inbox`).' },
242
+ { name: 'dir', type: 'string', required: true, constraint: 'Interaction directory holding deck.json.' },
243
+ ],
244
+ outputKind: 'object',
245
+ effects: [
246
+ 'Writes a notify deck to the per-project interactions root.',
247
+ 'Spawns the acknowledgement TUI in a detached tmux pane when in tmux. Creates no crtr job.',
248
+ ],
249
+ },
250
+ run: async (input) => {
251
+ const title = input['title'];
252
+ const body = input['body'];
253
+ const interaction = {
254
+ id: 'notify',
255
+ title,
256
+ kind: 'notify',
257
+ options: [{ id: 'ack', label: 'OK' }],
258
+ };
259
+ if (body !== undefined)
260
+ interaction['body'] = body;
261
+ const deck = validateDeck({ interactions: [interaction] });
262
+ const cwd = process.cwd();
263
+ const id = `nfy-${randomBytes(4).toString('hex')}`;
264
+ const idir = interactionDir(id, cwd);
265
+ mkdirSync(idir, { recursive: true });
266
+ atomicWriteJson(deckPath(idir), deck);
267
+ const rc = { mode: 'notify' };
268
+ atomicWriteJson(join(idir, 'run.json'), rc);
269
+ let shown = false;
270
+ if (isInTmux()) {
271
+ const spawn = spawnAndDetach({
272
+ command: runCmd(idir),
273
+ cwd,
274
+ placement: pickPlacement(),
275
+ killAfterSeconds: 0,
276
+ failGuard: false,
277
+ });
278
+ shown = spawn.status === 'spawned';
279
+ }
280
+ return { shown, dir: idir };
281
+ },
282
+ });
283
+ // ---------------------------------------------------------------------------
284
+ // show (no job, non-blocking passthrough)
285
+ // ---------------------------------------------------------------------------
286
+ const humanShow = defineLeaf({
287
+ name: 'show',
288
+ help: {
289
+ name: 'human show',
290
+ summary: 'put a file live on screen in a tmux pane via humanloop display',
291
+ params: [
292
+ { kind: 'positional', name: 'path', type: 'path', required: true, constraint: 'Path to the file to render.' },
293
+ { kind: 'flag', name: 'watch', type: 'bool', required: false, constraint: 'When present, live-update the pane on edits. Default off.' },
294
+ { kind: 'flag', name: 'window', type: 'enum', choices: ['auto', 'split', 'new'], required: false, default: 'auto', constraint: 'Placement. Default auto.' },
295
+ ],
296
+ output: [
297
+ { name: 'pane_id', type: 'string | null', required: true, constraint: 'Tmux pane id, or null when not displayed.' },
298
+ { name: 'reason', type: 'string | null', required: true, constraint: 'Why no pane was created, or null on success.' },
299
+ ],
300
+ outputKind: 'object',
301
+ effects: ['Spawns a live-watch tmux pane when possible. No job. Always exits 0.'],
302
+ },
303
+ run: async (input) => {
304
+ const path = input['path'];
305
+ const watch = input['watch'] === true;
306
+ const windowArg = input['window'];
307
+ const window = windowArg !== undefined ? windowArg : 'auto';
308
+ // `human show` must never fail the caller: any display error degrades to
309
+ // {pane_id:null, reason} with exit 0 (matches humanloop display semantics).
310
+ let paneId;
311
+ try {
312
+ const r = display(path, { watch, window, maxPanes: resolveMaxPanes() });
313
+ paneId = r.paneId;
314
+ }
315
+ catch {
316
+ paneId = undefined;
317
+ }
318
+ if (paneId !== undefined) {
319
+ return { pane_id: paneId, reason: null };
320
+ }
321
+ const reason = isInTmux()
322
+ ? 'renderer unavailable (termrender/uv missing)'
323
+ : 'not in tmux';
324
+ return { pane_id: null, reason };
325
+ },
326
+ });
327
+ // ---------------------------------------------------------------------------
328
+ // inbox (human-invoked, blocking)
329
+ // ---------------------------------------------------------------------------
330
+ const humanInbox = defineLeaf({
331
+ name: 'inbox',
332
+ help: {
333
+ name: 'human inbox',
334
+ summary: 'interactively drain pending interactions at your own terminal',
335
+ params: [],
336
+ inputNote: 'No input. Run this at a human terminal — it blocks until the backlog is drained or you quit.',
337
+ output: [{ name: 'drained', type: 'boolean', required: true, constraint: 'True once the loop returns.' }],
338
+ outputKind: 'object',
339
+ effects: ['Resolves pending interactions in the per-project interactions root via the TUI.'],
340
+ },
341
+ run: async () => {
342
+ await inbox([interactionsRoot(process.cwd())]);
343
+ return { drained: true };
344
+ },
345
+ });
346
+ // ---------------------------------------------------------------------------
347
+ // list (read-only, paginated)
348
+ // ---------------------------------------------------------------------------
349
+ const humanList = defineLeaf({
350
+ name: 'list',
351
+ help: {
352
+ name: 'human list',
353
+ summary: 'paginated list of pending, unclaimed interactions, oldest first',
354
+ params: [
355
+ { kind: 'flag', name: 'limit', type: 'int', required: false, default: 20, constraint: 'Default 20, max 100.' },
356
+ { kind: 'flag', name: 'cursor', type: 'string', required: false, constraint: "Opaque token from a previous response's next_cursor. Omit on first call." },
357
+ ],
358
+ output: [
359
+ { name: 'items', type: 'object[]', required: true, constraint: 'Each: {id, dir, title, kind, blocked_since}. Oldest first.' },
360
+ { name: 'next_cursor', type: 'string | null', required: true, constraint: 'Pass on the next call to continue. null means no more items.' },
361
+ { name: 'total', type: 'integer | null', required: true, constraint: 'Total pending interactions.' },
362
+ ],
363
+ outputKind: 'object',
364
+ effects: ['None. Read-only.'],
365
+ },
366
+ run: async (input) => {
367
+ const limitRaw = input['limit'];
368
+ const limit = Math.min(Math.max(1, limitRaw), 100);
369
+ const cursor = input['cursor'];
370
+ const raw = scanInbox([interactionsRoot(process.cwd())]);
371
+ const items = raw
372
+ .map((i) => ({
373
+ id: i.id,
374
+ dir: i.dir,
375
+ title: i.title !== undefined ? i.title : null,
376
+ kind: i.kind !== undefined ? i.kind : null,
377
+ blocked_since: i.blockedSince,
378
+ }))
379
+ .sort((a, b) => {
380
+ const ka = `${a.blocked_since}|${a.id}`;
381
+ const kb = `${b.blocked_since}|${b.id}`;
382
+ return ka < kb ? -1 : ka > kb ? 1 : 0;
383
+ });
384
+ const page = paginate(items, { limit, cursor }, {
385
+ defaultLimit: 20,
386
+ maxLimit: 100,
387
+ keyOf: (i) => `${i.blocked_since}|${i.id}`,
388
+ total: 'count',
389
+ });
390
+ return { items: page.items, next_cursor: page.next_cursor, total: page.total };
391
+ },
392
+ });
393
+ // ---------------------------------------------------------------------------
394
+ // _run (hidden worker; not listed in branch help)
395
+ // ---------------------------------------------------------------------------
396
+ const humanRun = defineLeaf({
397
+ name: '_run',
398
+ help: {
399
+ name: 'human _run',
400
+ summary: 'internal: the detached worker that runs the blocking humanloop call at the pane TTY',
401
+ params: [],
402
+ inputNote: 'Internal; invoked by the spawned pane via CRTR_HUMAN_DIR + run.json. Not for manual use.',
403
+ output: [{ name: 'none', type: 'void', required: false, constraint: 'No stdout; writes the job result file directly.' }],
404
+ outputKind: 'object',
405
+ effects: ['Runs the blocking humanloop call; for job-backed modes writes result.json via the job model.'],
406
+ },
407
+ run: async () => {
408
+ const dir = process.env['CRTR_HUMAN_DIR'];
409
+ if (dir === undefined || dir === '') {
410
+ process.exit(1);
411
+ }
412
+ const rc = readJson(join(dir, 'run.json'));
413
+ if (rc === null) {
414
+ process.exit(1);
415
+ }
416
+ try {
417
+ if (rc.mode === 'ask' || rc.mode === 'approve' || rc.mode === 'notify') {
418
+ const deck = parseDeck(deckPath(dir));
419
+ const env = await ask(deck, { dir });
420
+ if (rc.mode === 'ask') {
421
+ writeResult(rc.job_id, env, 'done');
422
+ }
423
+ else if (rc.mode === 'approve') {
424
+ const sel = env.responses.find((r) => r.id === rc.approve_iid)?.selectedOptionId;
425
+ writeResult(rc.job_id, {
426
+ approved: sel === 'yes',
427
+ summary: env.summary,
428
+ responses: env.responses,
429
+ responsePath: env.responsePath,
430
+ completedAt: env.completedAt,
431
+ }, 'done');
432
+ }
433
+ // notify: no job — nothing to write
434
+ }
435
+ else if (rc.mode === 'review') {
436
+ const res = await launchReview(rc.file, {
437
+ output: rc.output,
438
+ });
439
+ writeResult(rc.job_id, res, 'done');
440
+ }
441
+ }
442
+ catch (e) {
443
+ if (rc.job_id !== undefined) {
444
+ writeResult(rc.job_id, { error: 'human_run_failed', message: String(e) }, 'failed');
445
+ }
446
+ }
447
+ },
448
+ });
449
+ // ---------------------------------------------------------------------------
450
+ // branch
451
+ // ---------------------------------------------------------------------------
452
+ export function registerHuman() {
453
+ return defineBranch({
454
+ name: 'human',
455
+ help: {
456
+ name: 'human',
457
+ summary: 'human-in-the-loop decisions, document review, and live display',
458
+ model: "Kickoff leaves create kind:'human' jobs. Humans respond on human time (often >10 min) — never block waiting on the result; peek with `crtr job read result|status` (no `wait`). Cancel with `crtr job cancel`. notify/show create no job.",
459
+ children: [
460
+ { name: 'ask', desc: 'put a decision deck to a person', useWhen: 'a structured choice needs a human' },
461
+ { name: 'approve', desc: 'a Yes/No approval gate', useWhen: 'gating a handoff on human sign-off' },
462
+ { name: 'review', desc: 'anchored-comment review of a .md', useWhen: 'a human should comment on a plan or spec' },
463
+ { name: 'notify', desc: 'fire-and-forget acknowledgement', useWhen: 'informing a person without blocking' },
464
+ { name: 'show', desc: 'put a file live on screen', useWhen: 'displaying a doc while a human comments' },
465
+ { name: 'inbox', desc: 'interactively drain pending interactions', useWhen: 'a human clears the queue at their terminal' },
466
+ { name: 'list', desc: 'enumerate pending interactions', useWhen: 'discovering what is blocked on a human' },
467
+ ],
468
+ },
469
+ children: [
470
+ humanAsk,
471
+ humanApprove,
472
+ humanReview,
473
+ humanNotify,
474
+ humanShow,
475
+ humanInbox,
476
+ humanList,
477
+ humanRun,
478
+ ],
479
+ });
480
+ }
@@ -0,0 +1,2 @@
1
+ import type { BranchDef } from '../core/command.js';
2
+ export declare function registerJob(): BranchDef;