@crouton-kit/humanloop 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -11,88 +11,75 @@ program
11
11
  '\n' +
12
12
  'Use this when you (the agent) need the human to validate decisions, choose\n' +
13
13
  'between options, or provide freetext input before you continue. The tool\n' +
14
- 'blocks until the human finishes review and returns their answers as JSON.\n' +
14
+ 'blocks until the human finishes review and returns their responses as JSON.\n' +
15
15
  '\n' +
16
16
  'Workflow:\n' +
17
- ' 1. Write a decisions file matching `hl schema` (a JSON list of questions).\n' +
18
- ' 2. Run `hl create <file>`; it blocks and prints DecisionsOutput JSON to stdout.\n' +
19
- ' 3. Parse the JSON; each answer\'s `type` mirrors the question\'s `type`.\n' +
17
+ ' 1. Write a deck file matching `hl schema` (a JSON object with interactions[]).\n' +
18
+ ' 2. Run `hl create <file>`; it blocks and prints output JSON to stdout.\n' +
19
+ ' 3. Parse the JSON; each response has id, optional selectedOptionId, optional freetext.\n' +
20
20
  '\n' +
21
- 'Question types: validation (approve/reject a statement), choice (pick an\n' +
22
- 'option or enter custom), freetext (open-ended response).')
21
+ 'Interaction options: supply options[] for choices; set allowFreetext for comment/freetext.')
23
22
  .version('0.1.0')
24
23
  .addHelpText('after', '\nExamples:\n' +
25
24
  ' hl schema # print the input JSON schema\n' +
26
- ' hl create decisions.json # open TUI, block, print answers JSON\n' +
27
- ' hl create decisions.json --output answers.json # write result to file\n' +
28
- ' hl create decisions.json --no-tmux # run in current pane even inside tmux\n');
25
+ ' hl create deck.json # open TUI, block, print responses JSON\n' +
26
+ ' hl create deck.json --output out.json # write result to file\n' +
27
+ ' hl create deck.json --no-tmux # run in current pane even inside tmux\n');
29
28
  program
30
29
  .command('create')
31
30
  .description('Open the decisions TUI on <file> and block until the human finishes review.\n' +
32
- 'Prints DecisionsOutput JSON to stdout (or to --output / --write-to).')
33
- .argument('<file>', 'Path to decisions JSON file (see `hl schema` for format)')
34
- .option('--session-id <id>', 'Claude session ID; enables per-question visual context from conversation history. Defaults to the most recent session in cwd.')
31
+ 'Prints output JSON to stdout (or to --output / --write-to).')
32
+ .argument('<file>', 'Path to deck JSON file (see `hl schema` for format)')
33
+ .option('--session-id <id>', 'Claude session ID; enables per-interaction visual context from conversation history. Defaults to the most recent session in cwd.')
35
34
  .option('--no-visuals', 'Skip visual context generation (faster, no haiku calls)')
36
35
  .option('--output <path>', 'Write result JSON to <path> instead of stdout')
37
36
  .option('--no-tmux', 'Do not auto-dispatch the TUI to a new tmux pane even when $TMUX is set')
38
37
  .addOption(new Option('--write-to <path>', 'internal: tmux child mode').hideHelp())
39
38
  .addHelpText('after', '\n' +
40
39
  'INPUT FORMAT\n' +
41
- ' JSON file with a `questions` array. Each question has an `id`, a `type`\n' +
42
- ' ("validation" | "choice" | "freetext"), and `rationale`. Run `hl schema`\n' +
40
+ ' JSON file with an `interactions` array. Each interaction has an `id`,\n' +
41
+ ' `title`, `options[]`, and optional `allowFreetext`. Run `hl schema`\n' +
43
42
  ' for the full schema. Example:\n' +
44
43
  ' {\n' +
45
- ' "questions": [\n' +
46
- ' {"id": "q1", "type": "validation",\n' +
47
- ' "statement": "We should use Postgres over SQLite",\n' +
48
- ' "rationale": "Need concurrent writes from multiple services"},\n' +
49
- ' {"id": "q2", "type": "choice",\n' +
50
- ' "question": "Which migration tool?",\n' +
51
- ' "rationale": "Need repeatable schema changes",\n' +
52
- ' "options": ["Prisma", "Drizzle", "raw SQL"]},\n' +
53
- ' {"id": "q3", "type": "freetext",\n' +
54
- ' "question": "What should the retry policy be?",\n' +
55
- ' "rationale": "Affects reliability budget"}\n' +
44
+ ' "interactions": [\n' +
45
+ ' {"id":"i1","title":"Use Postgres","options":[\n' +
46
+ ' {"id":"approve","label":"Approve"},\n' +
47
+ ' {"id":"reject","label":"Reject"}\n' +
48
+ ' ],"allowFreetext":true},\n' +
49
+ ' {"id":"i2","title":"Migration tool","options":[\n' +
50
+ ' {"id":"prisma","label":"Prisma"},\n' +
51
+ ' {"id":"drizzle","label":"Drizzle"}\n' +
52
+ ' ]},\n' +
53
+ ' {"id":"i3","title":"Rate limit policy","options":[],"allowFreetext":true}\n' +
56
54
  ' ]\n' +
57
55
  ' }\n' +
58
56
  '\n' +
59
57
  'OUTPUT FORMAT (stdout on success, JSON)\n' +
60
58
  ' {\n' +
61
- ' "answers": [ ... ], # same order as input questions\n' +
59
+ ' "responses": [ ... ],\n' +
62
60
  ' "completedAt": "2026-04-20T15:23:00.000Z"\n' +
63
61
  ' }\n' +
64
62
  '\n' +
65
- ' Answer shape by `type`:\n' +
66
- ' validation: { id, type: "validation", approved: boolean, comment?: string }\n' +
67
- ' choice: { id, type: "choice", selected: string, isCustom: boolean, comment?: string }\n' +
68
- ' freetext: { id, type: "freetext", response: string }\n' +
63
+ ' Response shape:\n' +
64
+ ' { id: string, selectedOptionId?: string, freetext?: string }\n' +
69
65
  '\n' +
70
- ' The human can skip questions. `answers` may have FEWER entries than input\n' +
71
- ' questions — look up by `id`, do not assume index alignment.\n' +
66
+ ' The human can skip interactions. `responses` may have FEWER entries than\n' +
67
+ ' input interactions — look up by `id`, do not assume index alignment.\n' +
72
68
  '\n' +
73
69
  'BEHAVIOR\n' +
74
70
  ' tmux When $TMUX is set, the TUI auto-splits into a new pane to the\n' +
75
71
  ' right (-d keeps focus on the caller). Disable with --no-tmux.\n' +
76
- ' progress Answers are persisted atomically to <file>.progress.json after\n' +
72
+ ' progress Responses are persisted atomically to <file>.progress.json after\n' +
77
73
  ' every change. If the process is killed, the next run resumes\n' +
78
74
  ' from where the human left off. The file is removed on full\n' +
79
- ' completion; partial-answer files are preserved.\n' +
75
+ ' completion; partial-response files are preserved.\n' +
80
76
  ' visuals With --session-id (or auto-detected) haiku generates a short\n' +
81
- ' ANSI context block per question from recent conversation turns.\n' +
77
+ ' ANSI context block per interaction from recent conversation turns.\n' +
82
78
  '\n' +
83
79
  'EXIT CODES\n' +
84
80
  ' 0 success — result JSON emitted\n' +
85
81
  ' 1 error — message on stderr (file missing, invalid JSON, empty\n' +
86
- ' questions, no TTY, etc.)\n' +
87
- '\n' +
88
- 'EXAMPLES\n' +
89
- ' # Typical agent flow:\n' +
90
- ' cat > /tmp/d.json <<EOF\n' +
91
- ' {"questions":[{"id":"x","type":"validation",\n' +
92
- ' "statement":"...","rationale":"..."}]}\n' +
93
- ' EOF\n' +
94
- ' hl create /tmp/d.json > /tmp/answers.json\n' +
95
- ' jq \'.answers[] | select(.id=="x")\' /tmp/answers.json\n')
82
+ ' interactions, no TTY, etc.)\n')
96
83
  .action(async (file, opts) => {
97
84
  const sessionId = opts.visuals
98
85
  ? (opts.sessionId || findRecentSessionId(process.cwd()) || findRecentSessionId() || undefined)
@@ -128,86 +115,89 @@ program
128
115
  const msg = err instanceof Error ? err.message : String(err);
129
116
  process.stderr.write(`ERROR: ${msg}\n`);
130
117
  if (msg.includes('not found')) {
131
- process.stderr.write('\nFix: pass a path to an existing decisions JSON file.\nSee format: hl schema\n');
118
+ process.stderr.write('\nFix: pass a path to an existing deck JSON file.\nSee format: hl schema\n');
132
119
  }
133
- else if (msg.includes('No questions')) {
134
- process.stderr.write('\nFix: the file must contain a non-empty `questions` array.\nSee format: hl schema\n');
120
+ else if (msg.includes('No interactions')) {
121
+ process.stderr.write('\nFix: the file must contain a non-empty `interactions` array.\nSee format: hl schema\n');
135
122
  }
136
123
  else if (msg.includes('TTY')) {
137
124
  process.stderr.write('\nFix: hl needs an interactive terminal. If the caller captures stdin,\nrun inside tmux so hl can auto-dispatch the TUI to a new pane, or pipe\nstdin from /dev/tty.\n');
138
125
  }
139
126
  else if (msg.includes('JSON')) {
140
- process.stderr.write('\nFix: the decisions file must be valid JSON matching `hl schema`.\n');
127
+ process.stderr.write('\nFix: the deck file must be valid JSON matching `hl schema`.\n');
141
128
  }
142
- else if (msg.startsWith('questions[') || msg.includes('Duplicate question id') || msg.includes('must be')) {
143
- process.stderr.write('\nFix: the decisions file must match `hl schema`. Run `hl schema` to see the required shape.\n');
129
+ else if (msg.startsWith('interactions[') || msg.includes('Duplicate interaction id') || msg.includes('must be')) {
130
+ process.stderr.write('\nFix: the deck file must match `hl schema`. Run `hl schema` to see the required shape.\n');
144
131
  }
145
132
  process.exit(1);
146
133
  }
147
134
  });
148
135
  program
149
136
  .command('schema')
150
- .description('Print the decisions-input JSON schema to stdout')
137
+ .description('Print the v2 Interaction[] deck schema to stdout')
151
138
  .addHelpText('after', '\n' +
152
139
  'Use this to learn the exact input format for `hl create`.\n' +
153
- 'The schema documents the three question types and their required fields.\n' +
154
140
  '\n' +
155
141
  'Example:\n' +
156
- ' hl schema > decisions.schema.json # save for reference\n' +
157
- ' hl schema | jq # pretty-print\n')
142
+ ' hl schema > deck.schema.json # save for reference\n' +
143
+ ' hl schema | jq # pretty-print\n')
158
144
  .action(() => {
159
145
  const schema = {
160
146
  $schema: 'https://json-schema.org/draft/2020-12/schema',
161
- description: 'Input schema for hl create',
147
+ description: 'Input schema for hl create (v2)',
162
148
  type: 'object',
149
+ required: ['interactions'],
163
150
  properties: {
164
151
  title: {
165
152
  type: 'string',
166
- description: 'Optional title shown in the TUI header',
153
+ description: 'Optional deck title shown in the TUI header',
167
154
  },
168
- questions: {
155
+ interactions: {
169
156
  type: 'array',
157
+ minItems: 1,
170
158
  items: {
171
- oneOf: [
172
- {
173
- type: 'object',
174
- description: 'Validation a statement for the user to approve or reject (with optional comment)',
175
- required: ['id', 'type', 'statement', 'rationale'],
176
- properties: {
177
- id: { type: 'string' },
178
- type: { const: 'validation' },
179
- statement: { type: 'string', description: 'A statement to validate, not a question' },
180
- rationale: { type: 'string', description: 'Why this decision was made' },
159
+ type: 'object',
160
+ required: ['id', 'title', 'options'],
161
+ properties: {
162
+ id: { type: 'string', description: 'Unique identifier' },
163
+ title: { type: 'string', description: 'Short display label (≤4 words). Required.' },
164
+ subtitle: { type: 'string', description: 'One-line "why this matters"' },
165
+ body: { type: 'string', description: 'Markdown body shown in item-review' },
166
+ bodyPath: { type: 'string', description: 'Path to body file; sisyphus inlines before mount' },
167
+ options: {
168
+ type: 'array',
169
+ description: 'Selectable choices. Empty for freetext-only interactions.',
170
+ items: {
171
+ type: 'object',
172
+ required: ['id', 'label'],
173
+ properties: {
174
+ id: { type: 'string' },
175
+ label: { type: 'string' },
176
+ description: { type: 'string' },
177
+ shortcut: {
178
+ type: 'string',
179
+ description: 'Single char shortcut. Auto-assigned if absent. Avoid: c r n p q j k space',
180
+ },
181
+ },
181
182
  },
182
183
  },
183
- {
184
- type: 'object',
185
- description: 'Choice pick from options or provide a custom answer',
186
- required: ['id', 'type', 'question', 'rationale', 'options'],
187
- properties: {
188
- id: { type: 'string' },
189
- type: { const: 'choice' },
190
- question: { type: 'string' },
191
- rationale: { type: 'string' },
192
- options: { type: 'array', items: { type: 'string' }, minItems: 2 },
193
- },
184
+ allowFreetext: {
185
+ type: 'boolean',
186
+ description: 'If true, user can add a freetext comment (or respond freely if options is empty)',
194
187
  },
195
- {
196
- type: 'object',
197
- description: 'Freetext open-ended response',
198
- required: ['id', 'type', 'question', 'rationale'],
199
- properties: {
200
- id: { type: 'string' },
201
- type: { const: 'freetext' },
202
- question: { type: 'string' },
203
- rationale: { type: 'string' },
204
- },
188
+ freetextLabel: {
189
+ type: 'string',
190
+ description: 'Prompt shown above the freetext input. Default: "Comment" or "Response"',
191
+ },
192
+ kind: {
193
+ type: 'string',
194
+ enum: ['notify', 'validation', 'decision', 'context', 'error'],
195
+ description: 'Display hint opaque to humanloop, used by sisyphus for inbox icons',
205
196
  },
206
- ],
197
+ },
207
198
  },
208
199
  },
209
200
  },
210
- required: ['questions'],
211
201
  };
212
202
  process.stdout.write(JSON.stringify(schema, null, 2) + '\n');
213
203
  });
@@ -0,0 +1,5 @@
1
+ export { mountPanel, validateInput, launchTui } from './tui/app.js';
2
+ export { defaultGenerateVisual } from './visuals/generate.js';
3
+ export type { Interaction, InteractionOption, InteractionResponse, InteractionKind, Deck, DeckSource, MountedPanel, MountedPanelOpts, GenerateVisual, VisualBlock, } from './types.js';
4
+ export type { Key } from './tui/terminal.js';
5
+ export type { ConversationMessage } from './conversation/reader.js';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { mountPanel, validateInput, launchTui } from './tui/app.js';
2
+ export { defaultGenerateVisual } from './visuals/generate.js';
package/dist/tui/app.d.ts CHANGED
@@ -1,3 +1,7 @@
1
- import type { DecisionsInput, DecisionsOutput } from '../types.js';
2
- export declare function validateInput(parsed: unknown): DecisionsInput;
3
- export declare function launchTui(decisionsPath: string, sessionId?: string): Promise<DecisionsOutput>;
1
+ import type { Deck, InteractionResponse, MountedPanel, MountedPanelOpts } from '../types.js';
2
+ export declare function validateInput(parsed: unknown): Deck;
3
+ export declare function mountPanel(opts: MountedPanelOpts): MountedPanel;
4
+ export declare function launchTui(decisionsPath: string, sessionId?: string): Promise<{
5
+ responses: InteractionResponse[];
6
+ completedAt: string;
7
+ }>;