@haaaiawd/loom 2.1.2 → 2.1.3

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/cli/bin/loom.js CHANGED
@@ -1,143 +1,145 @@
1
- #!/usr/bin/env node
2
-
3
- import { readFileSync } from 'node:fs';
4
- import { dirname, resolve } from 'node:path';
5
- import { fileURLToPath } from 'node:url';
6
- import {
7
- addDeliverable,
8
- blockTask,
9
- checkProject,
10
- checkDeliverableCoverage,
11
- compileContext,
12
- completeTask,
13
- confirmCapability,
14
- configureRuntime,
15
- createCapability,
16
- createDesign,
17
- recordDecision,
18
- getCapability,
19
- getCapabilityStatus,
20
- getDesign,
21
- getKeeperPrompt,
22
- getTask,
23
- importTasks,
24
- initProject,
25
- listCapabilities,
26
- listDeliverables,
27
- listDesigns,
28
- loadProject,
29
- markReady,
30
- recordKeeper,
31
- recordUnderstanding,
32
- reopenTask,
33
- researchCapability,
34
- scaffoldEval,
35
- synthesizeCapability,
36
- skipKeeper,
37
- startTask,
38
- taskSummary,
39
- updateTask,
40
- } from '../src/store.js';
41
- import { promptCatalog } from '../src/protocol.js';
42
-
43
- const argv = process.argv.slice(2);
44
- const [command, subcommand, ...rest] = argv;
45
-
46
- function output(value) {
47
- console.log(typeof value === 'string' ? value : JSON.stringify(value, null, 2));
48
- }
49
-
50
- function fail(message) {
51
- console.error(`Error: ${message}`);
52
- process.exitCode = 1;
53
- }
54
-
55
- function option(name) {
56
- const index = argv.indexOf(name);
57
- return index === -1 ? undefined : argv[index + 1];
58
- }
59
-
60
- function jsonFile() {
61
- const path = option('--json-file');
62
- if (!path) throw new Error('This command requires --json-file <path>');
63
- try {
64
- return JSON.parse(readFileSync(resolve(path), 'utf8'));
65
- } catch (error) {
66
- throw new Error(`Cannot read JSON file ${path}: ${error.message}`);
67
- }
68
- }
69
-
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync } from 'node:fs';
4
+ import { dirname, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import {
7
+ addDeliverable,
8
+ blockTask,
9
+ checkProject,
10
+ checkDeliverableCoverage,
11
+ compileContext,
12
+ completeTask,
13
+ confirmCapability,
14
+ configureRuntime,
15
+ createCapability,
16
+ createDesign,
17
+ recordDecision,
18
+ getCapability,
19
+ getCapabilityStatus,
20
+ getDesign,
21
+ getKeeperPrompt,
22
+ getTask,
23
+ importTasks,
24
+ initProject,
25
+ listCapabilities,
26
+ listDeliverables,
27
+ listDesigns,
28
+ loadProject,
29
+ markReady,
30
+ recordKeeper,
31
+ recordUnderstanding,
32
+ reopenTask,
33
+ researchCapability,
34
+ scaffoldEval,
35
+ synthesizeCapability,
36
+ skipKeeper,
37
+ startTask,
38
+ taskSummary,
39
+ updateTask,
40
+ } from '../src/store.js';
41
+ import { promptCatalog } from '../src/protocol.js';
42
+
43
+ const argv = process.argv.slice(2);
44
+ const [command, subcommand, ...rest] = argv;
45
+
46
+ function output(value) {
47
+ console.log(typeof value === 'string' ? value : JSON.stringify(value, null, 2));
48
+ }
49
+
50
+ function fail(message) {
51
+ console.error(`Error: ${message}`);
52
+ process.exitCode = 1;
53
+ }
54
+
55
+ function option(name) {
56
+ const index = argv.indexOf(name);
57
+ return index === -1 ? undefined : argv[index + 1];
58
+ }
59
+
60
+ function jsonFile() {
61
+ const path = option('--json-file');
62
+ if (!path) throw new Error('This command requires --json-file <path>');
63
+ try {
64
+ return JSON.parse(readFileSync(resolve(path), 'utf8'));
65
+ } catch (error) {
66
+ throw new Error(`Cannot read JSON file ${path}: ${error.message}`);
67
+ }
68
+ }
69
+
70
70
  function help() {
71
- return `loom — continuity infrastructure for Agents
72
-
73
- The human talks naturally to the Agent. The Agent uses these commands silently.
74
-
75
- Typical flow (first pass):
76
- 1. loom init — create .loom/ skeleton
77
- 2. loom record — confirm facts, assumptions, open questions
78
- 3. loom design add — write design documents for consequential systems
79
- 4. loom capability add → research → synthesize → confirm
80
- — build professional decision trees for fields that matter
81
- 5. loom deliverable add — decompose the delivery surface
82
- 6. loom task plan — write Tasks with acceptance criteria covering deliverables
83
- 7. loom project ready — freeze a digest for Keeper review
84
- 8. loom keeper prompt → record — fresh Agent verifies build-readiness (one-time)
85
- 9. loom task start → done — execute one Task at a time, fill acceptance evidence
86
- 10. loom check — verify health, coverage, and drift
87
-
88
- When a new idea changes an existing decision:
89
- loom decision --json-file <decision.json> — record what changed and affected tasks
90
- then update the design document directly; loom check warns which done tasks need reopening.
91
-
92
- Start and resume
93
- loom init
94
- loom context [--task TASK-001] [--keeper] [--human-channel available|unavailable]
95
- loom check
96
- loom prompts
97
-
98
- Preserve understanding
99
- loom record --json-file <update.json>
100
- loom decision --json-file <decision.json>
101
- loom project ready
102
- loom design add <slug> --title <text> --kind <product|experience|system|contract|verification|operations|research>
103
- loom design list|get <slug>
104
- loom capability add <slug> --title <professional-field>
105
- loom capability list|get <slug>
106
- loom capability research <slug> --field <text> (creates research/_guide.md — add .md files there)
107
- loom capability synthesize <slug> (builds decision tree from research, validates sources)
71
+ return `loom — continuity infrastructure for Agents
72
+
73
+ The human talks naturally to the Agent. The Agent uses these commands silently.
74
+
75
+ Typical flow (first pass):
76
+ 1. loom init — create .loom/ skeleton
77
+ 2. loom record — confirm facts, assumptions, open questions
78
+ 3. loom design add — write design documents for consequential systems
79
+ 4. loom capability add → research → synthesize → confirm
80
+ — build professional decision trees for fields that matter
81
+ 5. loom deliverable add — decompose the delivery surface
82
+ 6. loom task plan — write Tasks with acceptance criteria covering deliverables
83
+ 7. loom project ready — freeze a digest for Keeper review
84
+ 8. loom keeper prompt → record — fresh Agent verifies build-readiness (one-time)
85
+ 9. loom task start → done — execute one Task at a time, fill acceptance evidence
86
+ 10. loom check — verify health, coverage, and drift
87
+
88
+ When a new idea changes an existing decision:
89
+ loom decision --json-file <decision.json> — record what changed and affected tasks
90
+ then update the design document directly; loom check warns which done tasks need reopening.
91
+
92
+ Start and resume
93
+ loom init
94
+ loom context [--task TASK-001] [--keeper] [--human-channel available|unavailable]
95
+ loom check
96
+ loom prompts
97
+
98
+ Preserve understanding
99
+ loom record --json-file <update.json>
100
+ loom decision --json-file <decision.json>
101
+ loom project ready
102
+ loom design add <slug> --title <text> --kind <product|experience|system|contract|verification|operations|research>
103
+ loom design list|get <slug>
104
+ loom capability add <slug> --title <professional-field>
105
+ loom capability list|get <slug>
106
+ loom capability research <slug> --field <text> (creates research/_guide.md — add .md files there)
107
+ loom capability synthesize <slug> (builds decision tree from research, validates sources)
108
108
  loom capability confirm <slug> --scenario <text> --source human|agent
109
109
  — human confirms, or Agent records a provisional selection
110
- loom capability status <slug>
111
-
112
- Map the delivery surface
113
- loom deliverable add <slug> --title <text> --kind <module|feature|behavior|interface|artifact|operational|verification|other>
114
- loom deliverable list
115
- loom deliverable coverage
116
-
117
- Maintain the work map
118
- loom task plan --json-file <tasks.json>
119
- loom task status|next|get <id>
120
- loom task start <id>
121
- loom task update <id> --json-file <patch.json>
122
- loom task block <id> --json-file <block.json>
123
- loom task reopen <id> [--reason <reason>]
124
- loom task done <id> --json-file <evidence.json>
125
-
126
- Each Task should produce one verifiable unit of real work. Use acceptance[] with criterion,
127
- verify_by, and evidence fields. Task completion fills in each acceptance condition's evidence
128
- with the actual result (for acceptance tasks) or quotes each done_when criterion (for legacy tasks).
129
-
130
- One-time independent handoff
131
- loom keeper prompt
132
- loom keeper record --json-file <result.json>
133
- loom keeper skip --reason <reason>
134
-
135
- Evaluate LOOM itself
136
- loom eval scaffold --json-file <scenario.json>
137
-
138
- Use \`--state-dir <outside-workspace-dir>\` on every command to keep LOOM state in an isolated sidecar
139
- (for example, a benchmark runner's per-run state directory). Sidecar initialization never edits AGENTS.md.
140
-
110
+ loom capability status <slug>
111
+
112
+ Map the delivery surface
113
+ loom deliverable add <slug> --title <text> --kind <module|feature|behavior|interface|artifact|operational|verification|other>
114
+ loom deliverable list
115
+ loom deliverable coverage
116
+
117
+ Maintain the work map
118
+ loom task plan --json-file <tasks.json>
119
+ loom task status|next|get <id>
120
+ loom task start <id>
121
+ loom task update <id> --json-file <patch.json>
122
+ loom task block <id> --json-file <block.json>
123
+ loom task reopen <id> [--reason <reason>]
124
+ loom task done <id> --json-file <evidence.json>
125
+
126
+ Each Task should produce one verifiable unit of real work. Use acceptance[] with criterion,
127
+ verify_by, and evidence fields. Task completion fills in each acceptance condition's evidence
128
+ with the actual result (for acceptance tasks) or quotes each done_when criterion (for legacy tasks).
129
+
130
+ One-time independent handoff
131
+ loom review [--help] — Keeper handoff and stage-review guide
132
+ loom keeper --help — read-only guide in any project state
133
+ loom keeper prompt
134
+ loom keeper record --json-file <result.json>
135
+ loom keeper skip --reason <reason>
136
+
137
+ Evaluate LOOM itself
138
+ loom eval scaffold --json-file <scenario.json>
139
+
140
+ Use \`--state-dir <outside-workspace-dir>\` on every command to keep LOOM state in an isolated sidecar
141
+ (for example, a benchmark runner's per-run state directory). Sidecar initialization never edits AGENTS.md.
142
+
141
143
  Use \`loom <command> --help\` or \`loom help <topic>\` for canonical JSON payloads. Topics:
142
144
  record, decision, task-plan, task-update, task-block, task-done, keeper-record, eval-scaffold
143
145
 
@@ -153,6 +155,8 @@ const STRUCTURED_HELP = {
153
155
  confirmed: ['A fact confirmed by the human or workspace.'],
154
156
  assumptions: [{ text: 'A bounded, reversible Agent assumption.', source: 'agent' }],
155
157
  unresolved: [{ question: 'A consequential question still open?', impact: 'high' }],
158
+ resolved: [{ id: 'Q-001', status: 'resolved' }],
159
+ retire_assumptions: ['A-001'],
156
160
  decisions: [{ title: 'Decision title', decision: 'Current decision.', rationale: 'Why it follows.', supersedes: [], affects: ['.loom/PROJECT.md'] }],
157
161
  },
158
162
  },
@@ -187,11 +191,41 @@ const STRUCTURED_HELP = {
187
191
  };
188
192
 
189
193
  function structuredHelp(topic) {
194
+ if (['keeper', 'review', 'keeper-prompt'].includes(topic)) return reviewHelp();
195
+ if (topic === 'keeper-record') return `${STRUCTURED_HELP[topic].usage}\n\nCanonical JSON payload:\n${JSON.stringify(STRUCTURED_HELP[topic].example, null, 2)}\n\nFor a revised pass, add closure_results: [{"gap":"<exact prior gap text>","evidence":"<observed closure proof>"}] for every prior gap. Minor gaps require fresh verification too.`;
190
196
  const entry = STRUCTURED_HELP[topic];
191
197
  if (!entry) throw new Error(`Unknown help topic: ${topic}. Available topics: ${Object.keys(STRUCTURED_HELP).join(', ')}`);
192
198
  return `${entry.usage}\n\nCanonical JSON payload:\n${JSON.stringify(entry.example, null, 2)}`;
193
199
  }
194
200
 
201
+ function reviewHelp() {
202
+ return `LOOM review guide
203
+
204
+ Keeper is the first build-readiness handoff. Stage reviews use ordinary Tasks.
205
+ This guide is read-only and available before initialization or project readiness.
206
+
207
+ Keeper handoff:
208
+ 1. The shaping Agent runs loom project ready after preparing project truth.
209
+ 2. The host opens a fresh Agent without inherited conversation. Give it the workspace
210
+ path, the CLI location (absolute path in source checkouts), and this instruction:
211
+ Run loom keeper prompt, then loom context --keeper, and inspect the referenced files.
212
+ 3. The fresh Agent records its verdict with loom keeper record --json-file <result.json>.
213
+ See loom keeper record --help for the payload and independent review provenance.
214
+ 4. The parent waits for the result, then runs loom context. On needs_revision or blocked,
215
+ repair the named sources, prepare again, and obtain a fresh review, including minor gaps.
216
+ 5. On passed, resume with loom task next and loom task start <id>.
217
+ If isolation is unavailable, explicitly record loom keeper skip --reason <limitation>.
218
+ The CLI prints instructions and records state; it does not launch Agents itself.
219
+
220
+ Stage review:
221
+ Use loom task plan --help to create a review Task naming the implementation, design,
222
+ and verification artifacts in reads. Record each finding as a repair Task with concrete
223
+ acceptance criteria and evidence. Block the review Task with loom task block until repairs
224
+ are ready; reopen it and re-run verification before loom task done. A completed repair
225
+ alone is not a completed review. No findings: close the review Task with inspection evidence.
226
+ Do not rerun first-time Keeper just to review every implementation Task.`;
227
+ }
228
+
195
229
  function activeStructuredHelpTopic() {
196
230
  if (command === 'record' || command === 'decision') return command;
197
231
  if (command === 'task' && ['plan', 'update', 'block', 'done'].includes(subcommand)) return `task-${subcommand}`;
@@ -199,148 +233,153 @@ function activeStructuredHelpTopic() {
199
233
  if (command === 'eval' && subcommand === 'scaffold') return 'eval-scaffold';
200
234
  return '';
201
235
  }
202
-
203
- try {
204
- configureRuntime({ stateDir: option('--state-dir') });
205
- const humanChannel = option('--human-channel');
206
- if (humanChannel && !['available', 'unavailable'].includes(humanChannel)) throw new Error('--human-channel must be available or unavailable');
236
+
237
+ try {
238
+ configureRuntime({ stateDir: option('--state-dir') });
239
+ const humanChannel = option('--human-channel');
240
+ if (humanChannel && !['available', 'unavailable'].includes(humanChannel)) throw new Error('--human-channel must be available or unavailable');
207
241
  switch (command) {
208
- case '--version':
209
- case '-v': {
210
- const here = dirname(fileURLToPath(import.meta.url));
211
- const pkg = JSON.parse(readFileSync(resolve(here, '..', '..', 'package.json'), 'utf8'));
212
- output(`loom ${pkg.version}`);
213
- break;
214
- }
242
+ case '--version':
243
+ case '-v': {
244
+ const here = dirname(fileURLToPath(import.meta.url));
245
+ const pkg = JSON.parse(readFileSync(resolve(here, '..', '..', 'package.json'), 'utf8'));
246
+ output(`loom ${pkg.version}`);
247
+ break;
248
+ }
215
249
  case '--help':
216
- case '-h':
217
- case undefined:
250
+ case '-h':
251
+ case undefined:
218
252
  output(help());
219
253
  break;
220
254
  case 'help':
221
255
  output(subcommand ? structuredHelp(subcommand) : help());
222
256
  break;
223
- case 'init':
224
- output(initProject());
225
- break;
226
- case 'context':
227
- case 'resume':
228
- output(compileContext({ taskId: option('--task'), keeper: argv.includes('--keeper'), humanChannel: humanChannel || 'available' }));
229
- break;
230
- case 'prompts':
231
- output(promptCatalog());
232
- break;
257
+ case 'init':
258
+ output(initProject());
259
+ break;
260
+ case 'context':
261
+ case 'resume':
262
+ output(compileContext({ taskId: option('--task'), keeper: argv.includes('--keeper'), humanChannel: humanChannel || 'available' }));
263
+ break;
264
+ case 'prompts':
265
+ output(promptCatalog());
266
+ break;
233
267
  case 'record':
234
268
  output(subcommand === '--help' ? structuredHelp('record') : recordUnderstanding(jsonFile()));
235
269
  break;
236
270
  case 'decision':
237
271
  output(subcommand === '--help' ? structuredHelp('decision') : recordDecision(jsonFile()));
238
- break;
239
- case 'check': {
240
- const result = checkProject();
241
- output(result);
242
- if (!result.healthy) process.exitCode = 1;
243
- break;
244
- }
245
- case 'project':
246
- if (subcommand !== 'ready') throw new Error('Usage: loom project ready');
247
- output(markReady());
248
- break;
249
- case 'design': {
250
- if (subcommand === 'add') {
251
- const slug = rest[0];
252
- const title = option('--title');
253
- const kind = option('--kind');
254
- if (!slug) throw new Error('Usage: loom design add <slug> --title <text> --kind <kind>');
255
- output(createDesign(slug, { title, kind }));
256
- } else if (subcommand === 'list') output(listDesigns());
257
- else if (subcommand === 'get') {
258
- if (!rest[0]) throw new Error('Usage: loom design get <slug>');
259
- output(getDesign(rest[0]));
260
- } else throw new Error('Usage: loom design add|list|get');
261
- break;
262
- }
263
- case 'capability': {
264
- if (subcommand === 'add') {
265
- const slug = rest[0];
266
- const title = option('--title');
267
- if (!slug) throw new Error('Usage: loom capability add <slug> --title <text>');
268
- output(createCapability(slug, { title }));
269
- } else if (subcommand === 'list') output(listCapabilities());
270
- else if (subcommand === 'get') {
271
- if (!rest[0]) throw new Error('Usage: loom capability get <slug>');
272
- output(getCapability(rest[0]));
273
- } else if (subcommand === 'research') {
274
- if (!rest[0]) throw new Error('Usage: loom capability research <slug> --field <text>');
275
- output(researchCapability(rest[0], { field: option('--field') }));
276
- } else if (subcommand === 'synthesize') {
277
- if (!rest[0]) throw new Error('Usage: loom capability synthesize <slug>');
278
- output(synthesizeCapability(rest[0]));
279
- } else if (subcommand === 'confirm') {
280
- if (!rest[0]) throw new Error('Usage: loom capability confirm <slug> --scenario <text>');
272
+ break;
273
+ case 'check': {
274
+ const result = checkProject();
275
+ output(result);
276
+ if (!result.healthy) process.exitCode = 1;
277
+ break;
278
+ }
279
+ case 'project':
280
+ if (subcommand !== 'ready') throw new Error('Usage: loom project ready');
281
+ output(markReady());
282
+ break;
283
+ case 'design': {
284
+ if (subcommand === 'add') {
285
+ const slug = rest[0];
286
+ const title = option('--title');
287
+ const kind = option('--kind');
288
+ if (!slug) throw new Error('Usage: loom design add <slug> --title <text> --kind <kind>');
289
+ output(createDesign(slug, { title, kind }));
290
+ } else if (subcommand === 'list') output(listDesigns());
291
+ else if (subcommand === 'get') {
292
+ if (!rest[0]) throw new Error('Usage: loom design get <slug>');
293
+ output(getDesign(rest[0]));
294
+ } else throw new Error('Usage: loom design add|list|get');
295
+ break;
296
+ }
297
+ case 'capability': {
298
+ if (subcommand === 'add') {
299
+ const slug = rest[0];
300
+ const title = option('--title');
301
+ if (!slug) throw new Error('Usage: loom capability add <slug> --title <text>');
302
+ output(createCapability(slug, { title }));
303
+ } else if (subcommand === 'list') output(listCapabilities());
304
+ else if (subcommand === 'get') {
305
+ if (!rest[0]) throw new Error('Usage: loom capability get <slug>');
306
+ output(getCapability(rest[0]));
307
+ } else if (subcommand === 'research') {
308
+ if (!rest[0]) throw new Error('Usage: loom capability research <slug> --field <text>');
309
+ output(researchCapability(rest[0], { field: option('--field') }));
310
+ } else if (subcommand === 'synthesize') {
311
+ if (!rest[0]) throw new Error('Usage: loom capability synthesize <slug>');
312
+ output(synthesizeCapability(rest[0]));
313
+ } else if (subcommand === 'confirm') {
314
+ if (!rest[0]) throw new Error('Usage: loom capability confirm <slug> --scenario <text>');
281
315
  output(confirmCapability(rest[0], { scenario: option('--scenario'), source: option('--source') }));
282
- } else if (subcommand === 'status') {
283
- if (!rest[0]) throw new Error('Usage: loom capability status <slug>');
284
- output(getCapabilityStatus(rest[0]));
285
- } else throw new Error('Usage: loom capability add|list|get|research|synthesize|confirm|status');
286
- break;
287
- }
288
- case 'deliverable': {
289
- if (subcommand === 'add') {
290
- const slug = rest[0];
291
- const title = option('--title');
292
- const kind = option('--kind');
293
- if (!slug) throw new Error('Usage: loom deliverable add <slug> --title <text> --kind <module|feature|behavior|interface|artifact|operational|verification|other>');
294
- output(addDeliverable(slug, { title, kind, notes: option('--notes') }));
295
- } else if (subcommand === 'list') output(listDeliverables());
296
- else if (subcommand === 'coverage') output(checkDeliverableCoverage());
297
- else throw new Error('Usage: loom deliverable add|list|coverage');
298
- break;
299
- }
316
+ } else if (subcommand === 'status') {
317
+ if (!rest[0]) throw new Error('Usage: loom capability status <slug>');
318
+ output(getCapabilityStatus(rest[0]));
319
+ } else throw new Error('Usage: loom capability add|list|get|research|synthesize|confirm|status');
320
+ break;
321
+ }
322
+ case 'deliverable': {
323
+ if (subcommand === 'add') {
324
+ const slug = rest[0];
325
+ const title = option('--title');
326
+ const kind = option('--kind');
327
+ if (!slug) throw new Error('Usage: loom deliverable add <slug> --title <text> --kind <module|feature|behavior|interface|artifact|operational|verification|other>');
328
+ output(addDeliverable(slug, { title, kind, notes: option('--notes') }));
329
+ } else if (subcommand === 'list') output(listDeliverables());
330
+ else if (subcommand === 'coverage') output(checkDeliverableCoverage());
331
+ else throw new Error('Usage: loom deliverable add|list|coverage');
332
+ break;
333
+ }
300
334
  case 'task': {
301
335
  if (argv.includes('--help') && ['plan', 'update', 'block', 'done'].includes(subcommand)) output(structuredHelp(`task-${subcommand}`));
302
336
  else if (subcommand === 'plan') output(importTasks(jsonFile()));
303
- else if (subcommand === 'status') output(taskSummary(loadProject().taskStore.tasks));
304
- else if (subcommand === 'next') output(getTask());
305
- else if (subcommand === 'get') {
306
- if (!rest[0]) throw new Error('Usage: loom task get <id>');
307
- output(getTask(rest[0]));
308
- } else if (subcommand === 'start') {
309
- if (!rest[0]) throw new Error('Usage: loom task start <id>');
310
- output(startTask(rest[0]));
311
- } else if (subcommand === 'update') {
312
- if (!rest[0]) throw new Error('Usage: loom task update <id> --json-file <patch.json>');
313
- output(updateTask(rest[0], jsonFile()));
314
- } else if (subcommand === 'block') {
315
- if (!rest[0]) throw new Error('Usage: loom task block <id> --json-file <block.json>');
316
- output(blockTask(rest[0], jsonFile()));
317
- } else if (subcommand === 'reopen') {
318
- if (!rest[0]) throw new Error('Usage: loom task reopen <id>');
319
- output(reopenTask(rest[0], { reason: option('--reason') }));
320
- } else if (subcommand === 'done') {
321
- if (!rest[0]) throw new Error('Usage: loom task done <id> --json-file <evidence.json>');
322
- output(completeTask(rest[0], jsonFile()));
323
- } else throw new Error('Usage: loom task plan|status|next|get|start|update|block|reopen|done');
324
- break;
325
- }
337
+ else if (subcommand === 'status') output(taskSummary(loadProject().taskStore.tasks));
338
+ else if (subcommand === 'next') output(getTask());
339
+ else if (subcommand === 'get') {
340
+ if (!rest[0]) throw new Error('Usage: loom task get <id>');
341
+ output(getTask(rest[0]));
342
+ } else if (subcommand === 'start') {
343
+ if (!rest[0]) throw new Error('Usage: loom task start <id>');
344
+ output(startTask(rest[0]));
345
+ } else if (subcommand === 'update') {
346
+ if (!rest[0]) throw new Error('Usage: loom task update <id> --json-file <patch.json>');
347
+ output(updateTask(rest[0], jsonFile()));
348
+ } else if (subcommand === 'block') {
349
+ if (!rest[0]) throw new Error('Usage: loom task block <id> --json-file <block.json>');
350
+ output(blockTask(rest[0], jsonFile()));
351
+ } else if (subcommand === 'reopen') {
352
+ if (!rest[0]) throw new Error('Usage: loom task reopen <id>');
353
+ output(reopenTask(rest[0], { reason: option('--reason') }));
354
+ } else if (subcommand === 'done') {
355
+ if (!rest[0]) throw new Error('Usage: loom task done <id> --json-file <evidence.json>');
356
+ output(completeTask(rest[0], jsonFile()));
357
+ } else throw new Error('Usage: loom task plan|status|next|get|start|update|block|reopen|done');
358
+ break;
359
+ }
360
+ case 'review':
361
+ if (subcommand && subcommand !== '--help') throw new Error('Usage: loom review [--help]');
362
+ output(reviewHelp());
363
+ break;
326
364
  case 'keeper': {
327
365
  if (argv.includes('--help') && subcommand === 'record') output(structuredHelp('keeper-record'));
366
+ else if (!subcommand || argv.includes('--help')) output(reviewHelp());
328
367
  else if (subcommand === 'prompt') output(getKeeperPrompt());
329
- else if (subcommand === 'record') output(recordKeeper(jsonFile()));
330
- else if (subcommand === 'skip') output(skipKeeper(option('--reason')));
331
- else throw new Error('Usage: loom keeper prompt|record|skip');
332
- break;
333
- }
368
+ else if (subcommand === 'record') output(recordKeeper(jsonFile()));
369
+ else if (subcommand === 'skip') output(skipKeeper(option('--reason')));
370
+ else throw new Error('Usage: loom keeper prompt|record|skip');
371
+ break;
372
+ }
334
373
  case 'eval':
335
374
  if (argv.includes('--help') && subcommand === 'scaffold') output(structuredHelp('eval-scaffold'));
336
375
  else {
337
376
  if (subcommand !== 'scaffold') throw new Error('Usage: loom eval scaffold --json-file <scenario.json>');
338
377
  output(scaffoldEval(jsonFile()));
339
378
  }
340
- break;
341
- default:
342
- throw new Error(`Unknown command: ${command}\n\n${help()}`);
343
- }
379
+ break;
380
+ default:
381
+ throw new Error(`Unknown command: ${command}\n\n${help()}`);
382
+ }
344
383
  } catch (error) {
345
384
  const topic = activeStructuredHelpTopic();
346
385
  const pointer = topic && !argv.includes('--help') ? `\nRun ${STRUCTURED_HELP[topic].usage.replace(/ --json-file .+$/, ' --help').replace(/ <id>/, '')} for a canonical payload.` : '';