@crouton-kit/crouter 0.3.11 → 0.3.13

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