agent-standup 0.20.0

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.
@@ -0,0 +1,3351 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ EXIT,
4
+ HOOK_VERBS,
5
+ createHttpFlush,
6
+ exitCodeFor,
7
+ fileSpool,
8
+ isHookVerb,
9
+ malformed,
10
+ ok,
11
+ rejected,
12
+ runHookCommand,
13
+ spoolPath
14
+ } from "../chunk-N7G677FC.js";
15
+ import {
16
+ NotFoundError,
17
+ SERVICE_ERROR_CODES,
18
+ faultContext,
19
+ getDefinition,
20
+ isRehearsalRollback,
21
+ isSettingKey,
22
+ listOperations,
23
+ log,
24
+ newRequestId,
25
+ toServiceError
26
+ } from "../chunk-4TIZQTUZ.js";
27
+ import "../chunk-VBXNDGOD.js";
28
+
29
+ // src/lib/cli/args.ts
30
+ function parseArgs(argv) {
31
+ const words = [];
32
+ const flags = {};
33
+ let literal = false;
34
+ for (let index = 0; index < argv.length; index += 1) {
35
+ const token = argv[index];
36
+ if (token === void 0) continue;
37
+ if (literal) {
38
+ words.push(token);
39
+ continue;
40
+ }
41
+ if (token === "--") {
42
+ literal = true;
43
+ continue;
44
+ }
45
+ if (token.startsWith("--")) {
46
+ const body = token.slice(2);
47
+ if (body.length === 0) continue;
48
+ const equals = body.indexOf("=");
49
+ if (equals !== -1) {
50
+ const name = body.slice(0, equals);
51
+ if (name.length === 0) {
52
+ return { ok: false, envelope: malformed(`Not a flag: ${token}.`) };
53
+ }
54
+ flags[name] = body.slice(equals + 1);
55
+ continue;
56
+ }
57
+ const next = argv[index + 1];
58
+ if (next !== void 0 && !next.startsWith("--")) {
59
+ flags[body] = next;
60
+ index += 1;
61
+ } else {
62
+ flags[body] = true;
63
+ }
64
+ continue;
65
+ }
66
+ if (token.startsWith("-") && token.length > 1) {
67
+ return {
68
+ ok: false,
69
+ envelope: malformed(
70
+ `Not a flag this build understands: ${token}. Flags are spelled --name.`
71
+ )
72
+ };
73
+ }
74
+ words.push(token);
75
+ }
76
+ return { ok: true, parsed: { words, flags } };
77
+ }
78
+ function stringFlag(flags, name) {
79
+ const value = flags[name];
80
+ if (value === void 0) return { ok: true };
81
+ if (value === true) {
82
+ return { ok: false, envelope: malformed(`--${name} needs a value.`, [name]) };
83
+ }
84
+ return { ok: true, value };
85
+ }
86
+ function booleanFlag(flags, name) {
87
+ const value = flags[name];
88
+ if (value === void 0) return { ok: true, value: false };
89
+ if (value !== true) {
90
+ return { ok: false, envelope: malformed(`--${name} does not take a value.`, [name]) };
91
+ }
92
+ return { ok: true, value: true };
93
+ }
94
+ function numericFlag(flags, name) {
95
+ const raw = stringFlag(flags, name);
96
+ if (!raw.ok) return raw;
97
+ if (raw.value === void 0) return { ok: true };
98
+ const text = raw.value.trim();
99
+ const parsed = text === "" ? Number.NaN : Number(text);
100
+ if (!Number.isInteger(parsed)) {
101
+ return { ok: false, envelope: malformed(`--${name} must be a whole number.`, [name]) };
102
+ }
103
+ return { ok: true, value: parsed };
104
+ }
105
+
106
+ // src/lib/cli/commands-admin.ts
107
+ function idArg(rest, label) {
108
+ const id = rest[0];
109
+ if (id === void 0) {
110
+ return { ok: false, envelope: malformed(`\`standup ${label}\` needs an id.`, ["id"]) };
111
+ }
112
+ return { ok: true, id };
113
+ }
114
+ function sourceGlobsFlag(flags) {
115
+ const clear = booleanFlag(flags, "clear-source-globs");
116
+ if (!clear.ok) return clear;
117
+ const raw = stringFlag(flags, "source-globs");
118
+ if (!raw.ok) return raw;
119
+ if (clear.value && raw.value !== void 0) {
120
+ return {
121
+ ok: false,
122
+ envelope: malformed("--source-globs and --clear-source-globs are mutually exclusive.", [
123
+ "sourceGlobs"
124
+ ])
125
+ };
126
+ }
127
+ if (clear.value) return { ok: true, value: null };
128
+ if (raw.value === void 0) return { ok: true };
129
+ return {
130
+ ok: true,
131
+ value: raw.value.split(",").map((glob) => glob.trim()).filter((glob) => glob.length > 0)
132
+ };
133
+ }
134
+ function budgetWindowsFlag(flags) {
135
+ const clear = booleanFlag(flags, "clear-budget-windows");
136
+ if (!clear.ok) return clear;
137
+ const raw = stringFlag(flags, "budget-windows");
138
+ if (!raw.ok) return raw;
139
+ if (clear.value && raw.value !== void 0) {
140
+ return {
141
+ ok: false,
142
+ envelope: malformed("--budget-windows and --clear-budget-windows are mutually exclusive.", [
143
+ "budgetWindows"
144
+ ])
145
+ };
146
+ }
147
+ if (clear.value) return { ok: true, value: null };
148
+ if (raw.value === void 0) return { ok: true };
149
+ try {
150
+ return { ok: true, value: JSON.parse(raw.value) };
151
+ } catch {
152
+ return {
153
+ ok: false,
154
+ envelope: malformed("--budget-windows must be valid JSON.", ["budgetWindows"])
155
+ };
156
+ }
157
+ }
158
+ var ADMIN_COMMANDS = Object.freeze([
159
+ // ── repo ──────────────────────────────────────────────────────────────
160
+ {
161
+ noun: "repo",
162
+ verb: "list",
163
+ operation: "list_repos",
164
+ summary: "List repositories.",
165
+ buildInput: (_rest, flags) => {
166
+ const includeArchived = booleanFlag(flags, "include-archived");
167
+ if (!includeArchived.ok) return includeArchived;
168
+ return { ok: true, input: { includeArchived: includeArchived.value } };
169
+ }
170
+ },
171
+ {
172
+ noun: "repo",
173
+ verb: "get",
174
+ operation: "get_repo",
175
+ summary: "Show one repository.",
176
+ buildInput: (rest) => {
177
+ const idResult = idArg(rest, "repo get");
178
+ if (!("id" in idResult)) return idResult;
179
+ return { ok: true, input: { id: idResult.id } };
180
+ }
181
+ },
182
+ {
183
+ noun: "repo",
184
+ verb: "create",
185
+ operation: "create_repo",
186
+ summary: "Create a repository. Refused if the id already exists.",
187
+ buildInput: (rest, flags) => {
188
+ const idResult = idArg(rest, "repo create");
189
+ if (!("id" in idResult)) return idResult;
190
+ const displayName = stringFlag(flags, "display-name");
191
+ if (!displayName.ok) return displayName;
192
+ const defaultBranch = stringFlag(flags, "default-branch");
193
+ if (!defaultBranch.ok) return defaultBranch;
194
+ const host = stringFlag(flags, "host");
195
+ if (!host.ok) return host;
196
+ const needsVisualReview = booleanFlag(flags, "needs-visual-review");
197
+ if (!needsVisualReview.ok) return needsVisualReview;
198
+ return {
199
+ ok: true,
200
+ input: {
201
+ id: idResult.id,
202
+ ...displayName.value === void 0 ? {} : { displayName: displayName.value },
203
+ ...defaultBranch.value === void 0 ? {} : { defaultBranch: defaultBranch.value },
204
+ ...host.value === void 0 ? {} : { host: host.value },
205
+ needsVisualReview: needsVisualReview.value
206
+ }
207
+ };
208
+ }
209
+ },
210
+ {
211
+ noun: "repo",
212
+ verb: "update",
213
+ operation: "update_repo",
214
+ summary: "Edit a repository, and archive or un-archive it.",
215
+ buildInput: (rest, flags) => {
216
+ const idResult = idArg(rest, "repo update");
217
+ if (!("id" in idResult)) return idResult;
218
+ const displayName = stringFlag(flags, "display-name");
219
+ if (!displayName.ok) return displayName;
220
+ const defaultBranch = stringFlag(flags, "default-branch");
221
+ if (!defaultBranch.ok) return defaultBranch;
222
+ const host = stringFlag(flags, "host");
223
+ if (!host.ok) return host;
224
+ const needsVisualReview = booleanFlag(flags, "needs-visual-review");
225
+ if (!needsVisualReview.ok) return needsVisualReview;
226
+ const archive = booleanFlag(flags, "archive");
227
+ if (!archive.ok) return archive;
228
+ const unarchive = booleanFlag(flags, "unarchive");
229
+ if (!unarchive.ok) return unarchive;
230
+ if (archive.value && unarchive.value) {
231
+ return {
232
+ ok: false,
233
+ envelope: malformed("--archive and --unarchive are mutually exclusive.", ["archived"])
234
+ };
235
+ }
236
+ return {
237
+ ok: true,
238
+ input: {
239
+ id: idResult.id,
240
+ ...displayName.value === void 0 ? {} : { displayName: displayName.value },
241
+ ...defaultBranch.value === void 0 ? {} : { defaultBranch: defaultBranch.value },
242
+ ...host.value === void 0 ? {} : { host: host.value },
243
+ ...needsVisualReview.value ? { needsVisualReview: true } : {},
244
+ ...archive.value ? { archived: true } : {},
245
+ ...unarchive.value ? { archived: false } : {}
246
+ }
247
+ };
248
+ }
249
+ },
250
+ // ── area ──────────────────────────────────────────────────────────────
251
+ {
252
+ noun: "area",
253
+ verb: "list",
254
+ operation: "list_areas",
255
+ summary: "List areas.",
256
+ buildInput: (_rest, flags) => {
257
+ const includeArchived = booleanFlag(flags, "include-archived");
258
+ if (!includeArchived.ok) return includeArchived;
259
+ return { ok: true, input: { includeArchived: includeArchived.value } };
260
+ }
261
+ },
262
+ {
263
+ noun: "area",
264
+ verb: "get",
265
+ operation: "get_area",
266
+ summary: "Show one area.",
267
+ buildInput: (rest) => {
268
+ const idResult = idArg(rest, "area get");
269
+ if (!("id" in idResult)) return idResult;
270
+ return { ok: true, input: { id: idResult.id } };
271
+ }
272
+ },
273
+ {
274
+ noun: "area",
275
+ verb: "create",
276
+ operation: "create_area",
277
+ summary: "Find or create an area by its normalised name.",
278
+ buildInput: (rest) => {
279
+ const name = rest[0];
280
+ if (name === void 0) {
281
+ return { ok: false, envelope: malformed("`standup area create` needs a name.", ["name"]) };
282
+ }
283
+ return { ok: true, input: { name } };
284
+ }
285
+ },
286
+ {
287
+ noun: "area",
288
+ verb: "update",
289
+ operation: "update_area",
290
+ summary: "Rename an area's display name, and archive or un-archive it.",
291
+ buildInput: (rest, flags) => {
292
+ const idResult = idArg(rest, "area update");
293
+ if (!("id" in idResult)) return idResult;
294
+ const displayName = stringFlag(flags, "display-name");
295
+ if (!displayName.ok) return displayName;
296
+ const archive = booleanFlag(flags, "archive");
297
+ if (!archive.ok) return archive;
298
+ const unarchive = booleanFlag(flags, "unarchive");
299
+ if (!unarchive.ok) return unarchive;
300
+ if (archive.value && unarchive.value) {
301
+ return {
302
+ ok: false,
303
+ envelope: malformed("--archive and --unarchive are mutually exclusive.", ["archived"])
304
+ };
305
+ }
306
+ return {
307
+ ok: true,
308
+ input: {
309
+ id: idResult.id,
310
+ ...displayName.value === void 0 ? {} : { displayName: displayName.value },
311
+ ...archive.value ? { archived: true } : {},
312
+ ...unarchive.value ? { archived: false } : {}
313
+ }
314
+ };
315
+ }
316
+ },
317
+ {
318
+ noun: "area",
319
+ verb: "merge",
320
+ operation: "merge_areas",
321
+ summary: "Fold one area's membership into another, and archive the losing area.",
322
+ // No local "needs two ids" or "from === to" check: `merge_areas`'
323
+ // own schema (`min(1)` on both fields) and its `SAME_AREA_GUARD` are
324
+ // what refuse those, exactly as they do for the `http` and `mcp`
325
+ // adapters — the http route (`../../app/api/areas/merge/route.ts`)
326
+ // passes its body straight through with no route-side validation
327
+ // either. Refusing here first would mean this adapter answers a
328
+ // missing/duplicate `from`/`to` with `malformed_command` while the
329
+ // other two answer `invalid_input`/`area_merge.same_area` for the
330
+ // identical caller mistake — the divergence the conformance suite's
331
+ // assertion 4 bound exists to catch. `from`/`to` are simply passed
332
+ // through, undefined or not, and the operation says what is wrong.
333
+ buildInput: (rest) => ({ ok: true, input: { from: rest[0], to: rest[1] } })
334
+ },
335
+ // ── machine ───────────────────────────────────────────────────────────
336
+ {
337
+ noun: "machine",
338
+ verb: "list",
339
+ operation: "list_machines",
340
+ summary: "List machines.",
341
+ buildInput: () => ({ ok: true, input: {} })
342
+ },
343
+ {
344
+ noun: "machine",
345
+ verb: "get",
346
+ operation: "get_machine",
347
+ summary: "Show one machine.",
348
+ buildInput: (rest) => {
349
+ const id = rest[0];
350
+ if (id === void 0) {
351
+ return { ok: false, envelope: malformed("`standup machine get` needs a name.", ["name"]) };
352
+ }
353
+ return { ok: true, input: { name: id } };
354
+ }
355
+ },
356
+ {
357
+ noun: "machine",
358
+ verb: "update",
359
+ operation: "update_machine",
360
+ summary: "Set or clear a machine's source-globs override, creating it if it is new.",
361
+ buildInput: (rest, flags) => {
362
+ const name = rest[0];
363
+ if (name === void 0) {
364
+ return {
365
+ ok: false,
366
+ envelope: malformed("`standup machine update` needs a name.", ["name"])
367
+ };
368
+ }
369
+ const sourceGlobs = sourceGlobsFlag(flags);
370
+ if (!sourceGlobs.ok) return sourceGlobs;
371
+ return {
372
+ ok: true,
373
+ input: {
374
+ name,
375
+ ..."value" in sourceGlobs ? { sourceGlobs: sourceGlobs.value } : {}
376
+ }
377
+ };
378
+ }
379
+ },
380
+ // ── account ───────────────────────────────────────────────────────────
381
+ {
382
+ noun: "account",
383
+ verb: "list",
384
+ operation: "list_accounts",
385
+ summary: "List accounts.",
386
+ buildInput: () => ({ ok: true, input: {} })
387
+ },
388
+ {
389
+ noun: "account",
390
+ verb: "get",
391
+ operation: "get_account",
392
+ summary: "Show one account.",
393
+ buildInput: (rest) => {
394
+ const idResult = idArg(rest, "account get");
395
+ if (!("id" in idResult)) return idResult;
396
+ return { ok: true, input: { id: idResult.id } };
397
+ }
398
+ },
399
+ {
400
+ noun: "account",
401
+ verb: "update",
402
+ operation: "update_account",
403
+ summary: "Edit an account, or create one if the id is new (needs vendor, display-name, plan-type).",
404
+ buildInput: (rest, flags) => {
405
+ const idResult = idArg(rest, "account update");
406
+ if (!("id" in idResult)) return idResult;
407
+ const vendor = stringFlag(flags, "vendor");
408
+ if (!vendor.ok) return vendor;
409
+ const displayName = stringFlag(flags, "display-name");
410
+ if (!displayName.ok) return displayName;
411
+ const planType = stringFlag(flags, "plan-type");
412
+ if (!planType.ok) return planType;
413
+ const budgetWindows = budgetWindowsFlag(flags);
414
+ if (!budgetWindows.ok) return budgetWindows;
415
+ return {
416
+ ok: true,
417
+ input: {
418
+ id: idResult.id,
419
+ ...vendor.value === void 0 ? {} : { vendor: vendor.value },
420
+ ...displayName.value === void 0 ? {} : { displayName: displayName.value },
421
+ ...planType.value === void 0 ? {} : { planType: planType.value },
422
+ ..."value" in budgetWindows ? { budgetWindows: budgetWindows.value } : {}
423
+ }
424
+ };
425
+ }
426
+ }
427
+ ]);
428
+
429
+ // src/lib/cli/commands-ownership.ts
430
+ var GLOBAL_FLAGS = /* @__PURE__ */ new Set(["json", "direct", "as", "session", "url", "help"]);
431
+ function passThroughFlags(flags, consumed = []) {
432
+ const input = {};
433
+ for (const [name, value] of Object.entries(flags)) {
434
+ if (GLOBAL_FLAGS.has(name)) continue;
435
+ if (consumed.includes(name)) continue;
436
+ if (value === true) {
437
+ return { ok: false, envelope: malformed(`--${name} needs a value.`, [name]) };
438
+ }
439
+ input[name] = value;
440
+ }
441
+ return { ok: true, input };
442
+ }
443
+ function itemIdPositional(rest, usage) {
444
+ const itemId = rest[0];
445
+ if (itemId === void 0) {
446
+ return { ok: false, envelope: malformed(`\`standup ${usage}\` needs an item id.`, ["itemId"]) };
447
+ }
448
+ return { ok: true, itemId };
449
+ }
450
+ function withSessionId(input, flags) {
451
+ const session = stringFlag(flags, "session");
452
+ if (!session.ok) return session;
453
+ return {
454
+ ok: true,
455
+ input: session.value === void 0 ? input : { ...input, sessionId: session.value }
456
+ };
457
+ }
458
+ function buildClaimInput(rest, flags) {
459
+ const idResult = itemIdPositional(rest, "session claim <item-id>");
460
+ if (!idResult.ok) return idResult;
461
+ const passthrough = passThroughFlags(flags);
462
+ if (!passthrough.ok) return passthrough;
463
+ const withSession = withSessionId(passthrough.input, flags);
464
+ if (!withSession.ok) return withSession;
465
+ const input = { ...withSession.input, itemId: idResult.itemId };
466
+ const pid = numericFlag(flags, "pid");
467
+ if (!pid.ok) return pid;
468
+ if (pid.value !== void 0) input.pid = pid.value;
469
+ return { ok: true, input };
470
+ }
471
+ function buildItemSessionInput(usage) {
472
+ return (rest, flags) => {
473
+ const idResult = itemIdPositional(rest, usage);
474
+ if (!idResult.ok) return idResult;
475
+ const passthrough = passThroughFlags(flags);
476
+ if (!passthrough.ok) return passthrough;
477
+ const withSession = withSessionId(passthrough.input, flags);
478
+ if (!withSession.ok) return withSession;
479
+ return { ok: true, input: { ...withSession.input, itemId: idResult.itemId } };
480
+ };
481
+ }
482
+ function buildCheckpointInput(rest, flags) {
483
+ const idResult = itemIdPositional(rest, "session checkpoint <item-id>");
484
+ if (!idResult.ok) return idResult;
485
+ const passthrough = passThroughFlags(flags);
486
+ if (!passthrough.ok) return passthrough;
487
+ const withSession = withSessionId(passthrough.input, flags);
488
+ if (!withSession.ok) return withSession;
489
+ return { ok: true, input: { ...withSession.input, itemId: idResult.itemId } };
490
+ }
491
+ function buildTakeoverInput(rest, flags) {
492
+ const idResult = itemIdPositional(rest, "session takeover <item-id>");
493
+ if (!idResult.ok) return idResult;
494
+ const force = booleanFlag(flags, "force");
495
+ if (!force.ok) return force;
496
+ const others = Object.fromEntries(
497
+ Object.entries(flags).filter(([name]) => name !== "force")
498
+ );
499
+ const passthrough = passThroughFlags(others);
500
+ if (!passthrough.ok) return passthrough;
501
+ const input = { ...passthrough.input, itemId: idResult.itemId };
502
+ if (flags.force !== void 0) input.force = force.value;
503
+ return { ok: true, input };
504
+ }
505
+ function buildSweepInput(_rest, flags) {
506
+ const dryRun = booleanFlag(flags, "dry-run");
507
+ if (!dryRun.ok) return dryRun;
508
+ const others = Object.fromEntries(
509
+ Object.entries(flags).filter(([name]) => name !== "dry-run")
510
+ );
511
+ const passthrough = passThroughFlags(others);
512
+ if (!passthrough.ok) return passthrough;
513
+ const input = { ...passthrough.input };
514
+ if (flags["dry-run"] !== void 0) input.dryRun = dryRun.value;
515
+ return { ok: true, input };
516
+ }
517
+ function buildMyWorkInput(_rest, flags) {
518
+ const passthrough = passThroughFlags(flags);
519
+ if (!passthrough.ok) return passthrough;
520
+ return withSessionId(passthrough.input, flags);
521
+ }
522
+ function buildProgressReportInput(_rest, flags) {
523
+ const includeCompleted = booleanFlag(flags, "include-completed");
524
+ if (!includeCompleted.ok) return includeCompleted;
525
+ const passthrough = passThroughFlags(flags, ["include-completed"]);
526
+ if (!passthrough.ok) return passthrough;
527
+ const withSession = withSessionId(passthrough.input, flags);
528
+ if (!withSession.ok) return withSession;
529
+ return {
530
+ ok: true,
531
+ input: { ...withSession.input, includeCompleted: includeCompleted.value }
532
+ };
533
+ }
534
+ function buildNoteInput(rest, flags) {
535
+ const idResult = itemIdPositional(rest, "item note <item-id>");
536
+ if (!idResult.ok) return idResult;
537
+ const passthrough = passThroughFlags(flags);
538
+ if (!passthrough.ok) return passthrough;
539
+ const withSession = withSessionId(passthrough.input, flags);
540
+ if (!withSession.ok) return withSession;
541
+ return { ok: true, input: { ...withSession.input, itemId: idResult.itemId } };
542
+ }
543
+ function buildOrientationInput(rest, flags) {
544
+ const idResult = itemIdPositional(rest, "item orientation <item-id>");
545
+ if (!idResult.ok) return idResult;
546
+ const limit = numericFlag(flags, "limit");
547
+ if (!limit.ok) return limit;
548
+ const passthrough = passThroughFlags(flags, ["limit"]);
549
+ if (!passthrough.ok) return passthrough;
550
+ return {
551
+ ok: true,
552
+ input: {
553
+ ...passthrough.input,
554
+ itemId: idResult.itemId,
555
+ ...limit.value === void 0 ? {} : { limit: limit.value }
556
+ }
557
+ };
558
+ }
559
+ function buildCrewNameInput(_rest, flags) {
560
+ const passthrough = passThroughFlags(flags);
561
+ if (!passthrough.ok) return passthrough;
562
+ return withSessionId(passthrough.input, flags);
563
+ }
564
+ var OWNERSHIP_COMMANDS = Object.freeze([
565
+ {
566
+ noun: "session",
567
+ verb: "claim",
568
+ operation: "claim",
569
+ summary: "Takes ownership of an item in a role. Atomic \u2014 two agents can't both win.",
570
+ buildInput: buildClaimInput
571
+ },
572
+ {
573
+ noun: "session",
574
+ verb: "release",
575
+ operation: "release",
576
+ summary: "Gives up ownership of an item.",
577
+ buildInput: buildItemSessionInput("session release <item-id>")
578
+ },
579
+ {
580
+ noun: "session",
581
+ verb: "heartbeat",
582
+ operation: "heartbeat",
583
+ summary: "Still alive. Unnecessary if your hook flushes tool calls; needed if you run no hook.",
584
+ buildInput: buildItemSessionInput("session heartbeat <item-id>")
585
+ },
586
+ {
587
+ noun: "session",
588
+ verb: "takeover",
589
+ operation: "takeover",
590
+ summary: "Takes an item from another session. Free if that session is dead; needs --force and --reason if it may be alive.",
591
+ buildInput: buildTakeoverInput
592
+ },
593
+ {
594
+ noun: "session",
595
+ verb: "sweep",
596
+ operation: "sweep",
597
+ summary: "Runs the liveness sweep: ages quiet sessions, releases claims held by dead ones, escalates stuck items. --dry-run reports what it would do and writes nothing.",
598
+ buildInput: buildSweepInput
599
+ },
600
+ {
601
+ noun: "session",
602
+ verb: "checkpoint",
603
+ operation: "checkpoint",
604
+ summary: "Records what you tried, what you ruled out, what's next. --headline gives it a one-line BLUF that reads pick up without the prose.",
605
+ buildInput: buildCheckpointInput
606
+ },
607
+ {
608
+ noun: "session",
609
+ verb: "my-work",
610
+ operation: "my_work",
611
+ summary: "What this session holds right now, and in what role.",
612
+ buildInput: buildMyWorkInput
613
+ },
614
+ {
615
+ noun: "session",
616
+ verb: "progress",
617
+ operation: "progress_report",
618
+ summary: "A progress report on everything this session holds, in one fixed shape every time it is asked. Finished work is counted but not listed; --include-completed lists it.",
619
+ buildInput: buildProgressReportInput
620
+ },
621
+ {
622
+ noun: "item",
623
+ verb: "note",
624
+ operation: "note",
625
+ summary: "Leaves a timestamped remark on an item.",
626
+ buildInput: buildNoteInput
627
+ },
628
+ {
629
+ noun: "item",
630
+ verb: "orientation",
631
+ operation: "orientation",
632
+ summary: "Catch me up: latest checkpoint, current state, what changed since, open loops, and crew.",
633
+ buildInput: buildOrientationInput
634
+ },
635
+ {
636
+ noun: "crew",
637
+ verb: "name",
638
+ operation: "get_crew_name",
639
+ summary: "Requests a name for a new agent. Hands out one available name, atomically.",
640
+ buildInput: buildCrewNameInput
641
+ }
642
+ ]);
643
+ var OWNERSHIP_ALIASES = Object.freeze(
644
+ {
645
+ claim: ["session", "claim"],
646
+ sweep: ["session", "sweep"]
647
+ }
648
+ );
649
+
650
+ // src/lib/cli/config-command.ts
651
+ function parseSettingValue(raw) {
652
+ try {
653
+ return JSON.parse(raw);
654
+ } catch {
655
+ return raw;
656
+ }
657
+ }
658
+ function confirmationGate(key, flags, verb) {
659
+ if (!isSettingKey(key)) return void 0;
660
+ const definition = getDefinition(key);
661
+ if (!definition.sensitive && !definition.irreversible) return void 0;
662
+ const confirm = booleanFlag(flags, "confirm");
663
+ if (!confirm.ok) return confirm;
664
+ if (confirm.value) return void 0;
665
+ const why = definition.irreversible ? "irreversible \u2014 it can destroy data that cannot be recreated" : "sensitive \u2014 it relaxes something this build enforces";
666
+ return {
667
+ ok: false,
668
+ envelope: malformed(`${key} is ${why}. Re-run with --confirm to ${verb} it.`, ["confirm"])
669
+ };
670
+ }
671
+ function buildListInput() {
672
+ return { ok: true, input: {} };
673
+ }
674
+ function buildGetInput(rest, verb) {
675
+ const key = rest[0];
676
+ if (key === void 0) {
677
+ return {
678
+ ok: false,
679
+ envelope: malformed(`\`standup config ${verb}\` needs a setting key.`, ["key"])
680
+ };
681
+ }
682
+ return { ok: true, input: { key } };
683
+ }
684
+ function buildSetInput(rest, flags) {
685
+ const key = rest[0];
686
+ if (key === void 0) {
687
+ return {
688
+ ok: false,
689
+ envelope: malformed("`standup config set` needs a setting key and a value.", ["key"])
690
+ };
691
+ }
692
+ if (rest.length < 2) {
693
+ return {
694
+ ok: false,
695
+ envelope: malformed(`\`standup config set ${key}\` needs a value.`, ["value"])
696
+ };
697
+ }
698
+ const gated = confirmationGate(key, flags, "set");
699
+ if (gated) return gated;
700
+ const raw = rest[1];
701
+ return { ok: true, input: { key, value: parseSettingValue(raw) } };
702
+ }
703
+ function buildClearInput(rest, flags) {
704
+ const key = rest[0];
705
+ if (key === void 0) {
706
+ return {
707
+ ok: false,
708
+ envelope: malformed("`standup config clear` needs a setting key.", ["key"])
709
+ };
710
+ }
711
+ const gated = confirmationGate(key, flags, "clear");
712
+ if (gated) return gated;
713
+ return { ok: true, input: { key } };
714
+ }
715
+ var CONFIG_COMMANDS = Object.freeze([
716
+ {
717
+ noun: "config",
718
+ verb: "list",
719
+ operation: "get_settings",
720
+ summary: "List every declared setting: value, source, label, help and category.",
721
+ buildInput: buildListInput
722
+ },
723
+ {
724
+ noun: "config",
725
+ verb: "get",
726
+ operation: "get_setting",
727
+ summary: "Show one setting: its value, source, label, help and category.",
728
+ buildInput: (rest) => buildGetInput(rest, "get")
729
+ },
730
+ {
731
+ noun: "config",
732
+ verb: "describe",
733
+ operation: "get_setting",
734
+ summary: "Explain one setting \u2014 same detail as `get`, worded for reading before changing it.",
735
+ buildInput: (rest) => buildGetInput(rest, "describe")
736
+ },
737
+ {
738
+ noun: "config",
739
+ verb: "set",
740
+ operation: "put_setting",
741
+ summary: "Set one setting's override. `sensitive`/`irreversible` keys need --confirm.",
742
+ buildInput: (rest, flags) => buildSetInput(rest, flags)
743
+ },
744
+ {
745
+ noun: "config",
746
+ verb: "clear",
747
+ operation: "delete_setting",
748
+ summary: "Clear one setting's override, reverting it to the registry default. `sensitive`/`irreversible` keys need --confirm.",
749
+ buildInput: (rest, flags) => buildClearInput(rest, flags)
750
+ }
751
+ ]);
752
+
753
+ // src/lib/cli/commands-backfill.ts
754
+ import { readFileSync } from "node:fs";
755
+ var BACKFILL_FILE_FLAG = "file";
756
+ function readPayloadFile(flags) {
757
+ const file2 = flags[BACKFILL_FILE_FLAG];
758
+ if (file2 === void 0) {
759
+ return {
760
+ ok: false,
761
+ envelope: malformed("`standup backfill run` needs --file <payload.json>.", [
762
+ BACKFILL_FILE_FLAG
763
+ ])
764
+ };
765
+ }
766
+ if (file2 === true) {
767
+ return {
768
+ ok: false,
769
+ envelope: malformed(`--${BACKFILL_FILE_FLAG} needs a value.`, [BACKFILL_FILE_FLAG])
770
+ };
771
+ }
772
+ let text;
773
+ try {
774
+ text = readFileSync(file2, "utf-8");
775
+ } catch {
776
+ return {
777
+ ok: false,
778
+ envelope: malformed(`Could not read ${file2}.`, [BACKFILL_FILE_FLAG])
779
+ };
780
+ }
781
+ try {
782
+ return { ok: true, input: { payload: JSON.parse(text) } };
783
+ } catch {
784
+ return {
785
+ ok: false,
786
+ envelope: malformed(`${file2} is not valid JSON.`, [BACKFILL_FILE_FLAG])
787
+ };
788
+ }
789
+ }
790
+ var BACKFILL_COMMANDS = Object.freeze([
791
+ {
792
+ noun: "backfill",
793
+ verb: "run",
794
+ operation: "backfill",
795
+ summary: "Bulk-load an existing body of work from a payload file. Needs ENABLE_BACKFILL=true.",
796
+ buildInput: (_rest, flags) => readPayloadFile(flags)
797
+ }
798
+ ]);
799
+
800
+ // src/lib/cli/commands-artifacts.ts
801
+ var GLOBAL_FLAGS2 = /* @__PURE__ */ new Set(["json", "direct", "as", "session", "url", "help"]);
802
+ function passThroughFlags2(flags) {
803
+ const input = {};
804
+ for (const [name, value] of Object.entries(flags)) {
805
+ if (GLOBAL_FLAGS2.has(name)) continue;
806
+ if (value === true) {
807
+ return { ok: false, envelope: malformed(`--${name} needs a value.`, [name]) };
808
+ }
809
+ input[name] = value;
810
+ }
811
+ return { ok: true, input };
812
+ }
813
+ function itemIdPositional2(rest, usage) {
814
+ const itemId = rest[0];
815
+ if (itemId === void 0) {
816
+ return { ok: false, envelope: malformed(`\`standup ${usage}\` needs an item id.`, ["itemId"]) };
817
+ }
818
+ return { ok: true, itemId };
819
+ }
820
+ function withSessionId2(input, flags) {
821
+ const session = stringFlag(flags, "session");
822
+ if (!session.ok) return session;
823
+ if (session.value === void 0) return { ok: true, input };
824
+ return { ok: true, input: { ...input, sessionId: session.value } };
825
+ }
826
+ function buildRecordArtifactInput(rest, flags) {
827
+ const idResult = itemIdPositional2(rest, "item artifact <item-id>");
828
+ if (!idResult.ok) return idResult;
829
+ const passthrough = passThroughFlags2(flags);
830
+ if (!passthrough.ok) return passthrough;
831
+ const withSession = withSessionId2(passthrough.input, flags);
832
+ if (!withSession.ok) return withSession;
833
+ return { ok: true, input: { ...withSession.input, itemId: idResult.itemId } };
834
+ }
835
+ function buildRequestReviewInput(rest, flags) {
836
+ const idResult = itemIdPositional2(rest, "item request-review <item-id>");
837
+ if (!idResult.ok) return idResult;
838
+ const passthrough = passThroughFlags2(flags);
839
+ if (!passthrough.ok) return passthrough;
840
+ const withSession = withSessionId2(passthrough.input, flags);
841
+ if (!withSession.ok) return withSession;
842
+ return { ok: true, input: { ...withSession.input, itemId: idResult.itemId } };
843
+ }
844
+ var ARTIFACT_COMMANDS = Object.freeze([
845
+ {
846
+ noun: "item",
847
+ verb: "artifact",
848
+ operation: "record_artifact",
849
+ summary: "Records an artifact \u2014 a plan, a review, a commit, a screenshot \u2014 against an item.",
850
+ buildInput: buildRecordArtifactInput
851
+ },
852
+ {
853
+ noun: "item",
854
+ verb: "request-review",
855
+ operation: "request_review",
856
+ summary: "Requests a review of an item, recording that one was asked for.",
857
+ buildInput: buildRequestReviewInput
858
+ }
859
+ ]);
860
+
861
+ // src/lib/cli/commands-loops.ts
862
+ var GLOBAL_FLAGS3 = /* @__PURE__ */ new Set(["json", "direct", "as", "session", "url", "help"]);
863
+ function passThroughFlags3(flags, consumed = []) {
864
+ const input = {};
865
+ for (const [name, value] of Object.entries(flags)) {
866
+ if (GLOBAL_FLAGS3.has(name)) continue;
867
+ if (consumed.includes(name)) continue;
868
+ if (value === true) {
869
+ return { ok: false, envelope: malformed(`--${name} needs a value.`, [name]) };
870
+ }
871
+ input[name] = value;
872
+ }
873
+ return { ok: true, input };
874
+ }
875
+ function itemIdPositional3(rest, usage) {
876
+ const itemId = rest[0];
877
+ if (itemId === void 0) {
878
+ return { ok: false, envelope: malformed(`\`standup ${usage}\` needs an item id.`, ["itemId"]) };
879
+ }
880
+ return { ok: true, itemId };
881
+ }
882
+ function withSessionId3(input, flags) {
883
+ const session = stringFlag(flags, "session");
884
+ if (!session.ok) return session;
885
+ if (session.value === void 0) return { ok: true, input };
886
+ return { ok: true, input: { ...input, sessionId: session.value } };
887
+ }
888
+ function buildLoopAddInput(rest, flags) {
889
+ const idResult = itemIdPositional3(rest, "item loop <item-id> <text>");
890
+ if (!idResult.ok) return idResult;
891
+ const passthrough = passThroughFlags3(flags);
892
+ if (!passthrough.ok) return passthrough;
893
+ const withSession = withSessionId3(passthrough.input, flags);
894
+ if (!withSession.ok) return withSession;
895
+ const words = rest.slice(1);
896
+ const input = { ...withSession.input, itemId: idResult.itemId };
897
+ if (words.length > 0) {
898
+ input.text = words.join(" ");
899
+ }
900
+ return { ok: true, input };
901
+ }
902
+ function buildLoopCloseInput(rest, flags) {
903
+ const idResult = itemIdPositional3(rest, "item loop-close <item-id> <loop-id>");
904
+ if (!idResult.ok) return idResult;
905
+ const passthrough = passThroughFlags3(flags);
906
+ if (!passthrough.ok) return passthrough;
907
+ const withSession = withSessionId3(passthrough.input, flags);
908
+ if (!withSession.ok) return withSession;
909
+ const input = { ...withSession.input, itemId: idResult.itemId };
910
+ const loopId = rest[1];
911
+ if (loopId !== void 0) {
912
+ input.loopId = loopId;
913
+ }
914
+ return { ok: true, input };
915
+ }
916
+ function buildLoopListInput(rest, flags) {
917
+ const idResult = itemIdPositional3(rest, "item loops <item-id>");
918
+ if (!idResult.ok) return idResult;
919
+ const all = booleanFlag(flags, "all");
920
+ if (!all.ok) return all;
921
+ const deleted = booleanFlag(flags, "deleted");
922
+ if (!deleted.ok) return deleted;
923
+ const notes = booleanFlag(flags, "notes");
924
+ if (!notes.ok) return notes;
925
+ const passthrough = passThroughFlags3(flags, ["all", "deleted", "notes"]);
926
+ if (!passthrough.ok) return passthrough;
927
+ const withSession = withSessionId3(passthrough.input, flags);
928
+ if (!withSession.ok) return withSession;
929
+ return {
930
+ ok: true,
931
+ input: {
932
+ ...withSession.input,
933
+ itemId: idResult.itemId,
934
+ includeClosed: all.value,
935
+ includeDeleted: deleted.value,
936
+ includeNonWork: notes.value
937
+ }
938
+ };
939
+ }
940
+ function buildLoopGetInput(rest, flags) {
941
+ const idResult = itemIdPositional3(rest, "item loop-get <item-id> <loop-id>");
942
+ if (!idResult.ok) return idResult;
943
+ const passthrough = passThroughFlags3(flags);
944
+ if (!passthrough.ok) return passthrough;
945
+ const input = { ...passthrough.input, itemId: idResult.itemId };
946
+ const loopId = rest[1];
947
+ if (loopId !== void 0) {
948
+ input.loopId = loopId;
949
+ }
950
+ return { ok: true, input };
951
+ }
952
+ function buildLoopEditInput(rest, flags) {
953
+ const idResult = itemIdPositional3(rest, "item loop-edit <item-id> <loop-id> <text>");
954
+ if (!idResult.ok) return idResult;
955
+ const passthrough = passThroughFlags3(flags);
956
+ if (!passthrough.ok) return passthrough;
957
+ const withSession = withSessionId3(passthrough.input, flags);
958
+ if (!withSession.ok) return withSession;
959
+ const input = { ...withSession.input, itemId: idResult.itemId };
960
+ const loopId = rest[1];
961
+ if (loopId !== void 0) {
962
+ input.loopId = loopId;
963
+ }
964
+ const words = rest.slice(2);
965
+ if (words.length > 0) {
966
+ input.text = words.join(" ");
967
+ }
968
+ return { ok: true, input };
969
+ }
970
+ function buildLoopDeleteInput(rest, flags) {
971
+ const idResult = itemIdPositional3(rest, "item loop-delete <item-id> <loop-id>");
972
+ if (!idResult.ok) return idResult;
973
+ const passthrough = passThroughFlags3(flags);
974
+ if (!passthrough.ok) return passthrough;
975
+ const withSession = withSessionId3(passthrough.input, flags);
976
+ if (!withSession.ok) return withSession;
977
+ const input = { ...withSession.input, itemId: idResult.itemId };
978
+ const loopId = rest[1];
979
+ if (loopId !== void 0) {
980
+ input.loopId = loopId;
981
+ }
982
+ return { ok: true, input };
983
+ }
984
+ var LOOP_COMMANDS = Object.freeze([
985
+ {
986
+ noun: "item",
987
+ verb: "loop",
988
+ operation: "loop_add",
989
+ summary: "Records a loose end on an item \u2014 a piece of work that still needs doing but is not big enough to be its own item. Loops track WORK: a reference or a status note belongs in the repo or in a note, not here. --kind note keeps one out of the count of work outstanding; --kind blocked_on_person is for something real waiting on a human.",
990
+ buildInput: buildLoopAddInput
991
+ },
992
+ {
993
+ noun: "item",
994
+ verb: "loop-close",
995
+ operation: "loop_close",
996
+ summary: "Closes an open loop on an item. --reason optionally records how it was resolved, and is reported with the closed loop.",
997
+ buildInput: buildLoopCloseInput
998
+ },
999
+ {
1000
+ noun: "item",
1001
+ verb: "loops",
1002
+ operation: "loop_list",
1003
+ summary: "List an item's loops \u2014 id, kind, status, when it opened and the first 200 characters. Open loops that track work only; --all includes closed ones, --deleted includes retracted ones, --notes includes loops filed as notes.",
1004
+ buildInput: buildLoopListInput
1005
+ },
1006
+ {
1007
+ noun: "item",
1008
+ verb: "loop-get",
1009
+ operation: "loop_get",
1010
+ summary: "Show one loop on an item in full, by its loop id.",
1011
+ buildInput: buildLoopGetInput
1012
+ },
1013
+ {
1014
+ noun: "item",
1015
+ verb: "loop-edit",
1016
+ operation: "loop_edit",
1017
+ summary: "Rewrite an open loop's text. Keeps its original openedAt.",
1018
+ buildInput: buildLoopEditInput
1019
+ },
1020
+ {
1021
+ noun: "item",
1022
+ verb: "loop-delete",
1023
+ operation: "loop_delete",
1024
+ summary: "Retract a loop that should never have existed \u2014 a duplicate, or one recorded by accident. Needs --reason. Use loop-close for a real loose end that is resolved.",
1025
+ buildInput: buildLoopDeleteInput
1026
+ }
1027
+ ]);
1028
+
1029
+ // src/lib/cli/commands-sessions.ts
1030
+ var GLOBAL_FLAGS4 = /* @__PURE__ */ new Set(["json", "direct", "as", "session", "url", "help"]);
1031
+ function buildRegisterInput(_rest, flags) {
1032
+ const session = stringFlag(flags, "session");
1033
+ if (!session.ok) return session;
1034
+ if (session.value === void 0) {
1035
+ return {
1036
+ ok: false,
1037
+ envelope: malformed("`standup session register` needs --session.", ["sessionId"])
1038
+ };
1039
+ }
1040
+ const hookVersion = numericFlag(flags, "hook-version");
1041
+ if (!hookVersion.ok) return hookVersion;
1042
+ const input = { sessionId: session.value };
1043
+ for (const [name, value] of Object.entries(flags)) {
1044
+ if (GLOBAL_FLAGS4.has(name)) continue;
1045
+ if (name === "hook-version") continue;
1046
+ if (value === true) {
1047
+ return { ok: false, envelope: malformed(`--${name} needs a value.`, [name]) };
1048
+ }
1049
+ input[name === "hook-variant" ? "hookVariant" : name] = value;
1050
+ }
1051
+ if (hookVersion.value !== void 0) input.hookVersion = hookVersion.value;
1052
+ return { ok: true, input };
1053
+ }
1054
+ var SESSION_COMMANDS = Object.freeze([
1055
+ {
1056
+ noun: "session",
1057
+ verb: "register",
1058
+ operation: "register_session",
1059
+ summary: "Registers this session and reports which hook to install, and whether it may claim (`hook.require_registration_to_claim`, off by default, is what decides this \u2014 the protocol version alone does not).",
1060
+ buildInput: buildRegisterInput
1061
+ }
1062
+ ]);
1063
+
1064
+ // src/lib/cli/commands.ts
1065
+ function noInput() {
1066
+ return { ok: true, input: {} };
1067
+ }
1068
+ var GLOBAL_FLAGS5 = /* @__PURE__ */ new Set(["json", "direct", "as", "session", "url", "help"]);
1069
+ function flagsToInput(flags, consumed = []) {
1070
+ const input = {};
1071
+ for (const [name, value] of Object.entries(flags)) {
1072
+ if (GLOBAL_FLAGS5.has(name)) continue;
1073
+ if (consumed.includes(name)) continue;
1074
+ if (value === true) {
1075
+ return { ok: false, envelope: malformed(`--${name} needs a value.`, [name]) };
1076
+ }
1077
+ input[name] = value;
1078
+ }
1079
+ return { ok: true, input };
1080
+ }
1081
+ function jsonFlag(flags, name) {
1082
+ const raw = stringFlag(flags, name);
1083
+ if (!raw.ok) return raw;
1084
+ if (raw.value === void 0) return { ok: true };
1085
+ try {
1086
+ return { ok: true, value: JSON.parse(raw.value) };
1087
+ } catch {
1088
+ return { ok: false, envelope: malformed(`--${name} must be valid JSON.`, [name]) };
1089
+ }
1090
+ }
1091
+ var COMMANDS = Object.freeze([
1092
+ {
1093
+ noun: "item",
1094
+ verb: "get",
1095
+ operation: "get_item",
1096
+ summary: "Show one item \u2014 id, title, state, headline and the latest checkpoint's headline; --full for the whole record.",
1097
+ /**
1098
+ * `--full` is the command line's spelling of the `full` opt-in
1099
+ * (MILESTONES.md #107) — a bare switch, for the same reason `--all` is
1100
+ * on `item list`: `--full true` is not a thing anyone types. It goes
1101
+ * through `booleanFlag` rather than `flagsToInput`, which refuses a
1102
+ * valueless flag outright.
1103
+ */
1104
+ buildInput: (rest, flags) => {
1105
+ const id = rest[0];
1106
+ if (id === void 0) {
1107
+ return { ok: false, envelope: malformed("`standup item get` needs an item id.", ["id"]) };
1108
+ }
1109
+ const full = booleanFlag(flags, "full");
1110
+ if (!full.ok) return full;
1111
+ return { ok: true, input: { id, full: full.value } };
1112
+ }
1113
+ },
1114
+ {
1115
+ noun: "item",
1116
+ verb: "search",
1117
+ operation: "search",
1118
+ summary: "Find items by text in their title, headline or body, best match first. Searches every state including finished work \u2014 --open-only excludes it.",
1119
+ /**
1120
+ * The query is a positional argument rather than a flag, because it is
1121
+ * the whole point of the command and `standup item search hook script`
1122
+ * is how anyone would type it. The words are rejoined with single
1123
+ * spaces: a shell splits an unquoted phrase into several arguments, and
1124
+ * refusing everything past the first would make the quoting mandatory
1125
+ * for the most ordinary use of the verb.
1126
+ */
1127
+ buildInput: (rest, flags) => {
1128
+ const query = rest.join(" ").trim();
1129
+ if (query === "") {
1130
+ return {
1131
+ ok: false,
1132
+ envelope: malformed("`standup item search` needs something to search for.", ["query"])
1133
+ };
1134
+ }
1135
+ const openOnly = booleanFlag(flags, "open-only");
1136
+ if (!openOnly.ok) return openOnly;
1137
+ const limit = numericFlag(flags, "limit");
1138
+ if (!limit.ok) return limit;
1139
+ const built = flagsToInput(flags, ["open-only", "limit"]);
1140
+ if (!built.ok) return built;
1141
+ return {
1142
+ ok: true,
1143
+ input: {
1144
+ ...built.input,
1145
+ query,
1146
+ openOnly: openOnly.value,
1147
+ ...limit.value === void 0 ? {} : { limit: limit.value }
1148
+ }
1149
+ };
1150
+ }
1151
+ },
1152
+ {
1153
+ noun: "item",
1154
+ verb: "list",
1155
+ operation: "list_items",
1156
+ summary: "List items, filtered by state, priority, area, repo or parent. Returns id, title, state and headline; --full for whole records. Finished work is excluded by default; --all includes it.",
1157
+ /**
1158
+ * `--all` is the command line's spelling of `includeTerminal`
1159
+ * (MILESTONES.md #103) — a bare flag, because that is what a switch
1160
+ * looks like here and `--include-terminal true` is not a thing anyone
1161
+ * would type. It cannot go through `flagsToInput`, which refuses a
1162
+ * valueless flag outright ("--all needs a value"), so this verb builds
1163
+ * its own input: `booleanFlag` for the switch, `flagsToInput` for
1164
+ * everything else, with `--all` declared consumed so it does not arrive
1165
+ * at the operation twice under two names.
1166
+ */
1167
+ buildInput: (_rest, flags) => {
1168
+ const all = booleanFlag(flags, "all");
1169
+ if (!all.ok) return all;
1170
+ const full = booleanFlag(flags, "full");
1171
+ if (!full.ok) return full;
1172
+ const limit = numericFlag(flags, "limit");
1173
+ if (!limit.ok) return limit;
1174
+ const built = flagsToInput(flags, ["all", "full", "limit"]);
1175
+ if (!built.ok) return built;
1176
+ return {
1177
+ ok: true,
1178
+ input: {
1179
+ ...built.input,
1180
+ includeTerminal: all.value,
1181
+ full: full.value,
1182
+ ...limit.value === void 0 ? {} : { limit: limit.value }
1183
+ }
1184
+ };
1185
+ }
1186
+ },
1187
+ {
1188
+ noun: "item",
1189
+ verb: "stale",
1190
+ operation: "get_stale_candidates",
1191
+ summary: "List open rows that another row's recorded work already named \u2014 the check that catches a stale row before a crew is dispatched onto it. Reports evidence and closes nothing. --include-unlanded also shows rows named only by a plan or a review.",
1192
+ /**
1193
+ * `--include-unlanded` is a bare switch, so it cannot go through
1194
+ * `flagsToInput` (which refuses a valueless flag), and `--limit` is a
1195
+ * `z.number()` field where a flag is always a string. Both are built
1196
+ * here and declared consumed so neither reaches the operation twice
1197
+ * under two spellings — the same shape `item list` uses for `--all`.
1198
+ */
1199
+ buildInput: (_rest, flags) => {
1200
+ const includeUnlanded = booleanFlag(flags, "include-unlanded");
1201
+ if (!includeUnlanded.ok) return includeUnlanded;
1202
+ const limit = numericFlag(flags, "limit");
1203
+ if (!limit.ok) return limit;
1204
+ const built = flagsToInput(flags, ["include-unlanded", "limit"]);
1205
+ if (!built.ok) return built;
1206
+ return {
1207
+ ok: true,
1208
+ input: {
1209
+ ...built.input,
1210
+ includeUnlanded: includeUnlanded.value,
1211
+ ...limit.value === void 0 ? {} : { limit: limit.value }
1212
+ }
1213
+ };
1214
+ }
1215
+ },
1216
+ {
1217
+ noun: "item",
1218
+ verb: "create",
1219
+ operation: "create_item",
1220
+ summary: "Deprecated \u2014 use `project create`, `task create` or `subtask create`. Creates an item whose kind is inferred from --parentId.",
1221
+ buildInput: (_rest, flags) => flagsToInput(flags)
1222
+ },
1223
+ // The three explicit creates, one noun each. A separate noun rather than
1224
+ // `item create --kind project`: a `--kind` flag would be a value the
1225
+ // operation would then have to reconcile against the parent it was given,
1226
+ // which is the ambiguity these commands exist to remove. The noun *is* the
1227
+ // kind, so there is nothing to reconcile.
1228
+ //
1229
+ // The parent goes through `flagsToInput` like every other field rather
1230
+ // than being read as a positional. It is required, but requiredness is the
1231
+ // operation's schema's to enforce (this file's header: "Field validation
1232
+ // is not done here"), and a positional would produce a second rejection
1233
+ // this adapter could give that no other adapter would.
1234
+ {
1235
+ noun: "project",
1236
+ verb: "create",
1237
+ operation: "create_project",
1238
+ summary: "Create a project \u2014 a root container for tasks. A project has no state of its own and cannot be transitioned.",
1239
+ buildInput: (_rest, flags) => flagsToInput(flags)
1240
+ },
1241
+ {
1242
+ noun: "task",
1243
+ verb: "create",
1244
+ operation: "create_task",
1245
+ summary: 'Create a task under a project. --projectId is required; pass a project id, or "inbox" for the configured inbox project.',
1246
+ buildInput: (_rest, flags) => flagsToInput(flags)
1247
+ },
1248
+ {
1249
+ noun: "subtask",
1250
+ verb: "create",
1251
+ operation: "create_subtask",
1252
+ summary: "Create a subtask under a task. --taskId is required and must name a task, not a project.",
1253
+ buildInput: (_rest, flags) => flagsToInput(flags)
1254
+ },
1255
+ {
1256
+ noun: "item",
1257
+ verb: "update",
1258
+ operation: "update_item",
1259
+ summary: "Edit an item's non-state fields.",
1260
+ buildInput: (rest, flags) => {
1261
+ const id = rest[0];
1262
+ if (id === void 0) {
1263
+ return {
1264
+ ok: false,
1265
+ envelope: malformed("`standup item update` needs an item id.", ["id"])
1266
+ };
1267
+ }
1268
+ const built = flagsToInput(flags);
1269
+ if (!built.ok) return built;
1270
+ return { ok: true, input: { id, ...built.input } };
1271
+ }
1272
+ },
1273
+ {
1274
+ noun: "item",
1275
+ verb: "transition",
1276
+ operation: "transition_item",
1277
+ summary: "Move an item to a new state. --dry-run previews the outcome, including a rejection, without writing (routes to the service layer's rehearsal mode \u2014 MILESTONES.md #27).",
1278
+ buildInput: (rest, flags) => {
1279
+ const id = rest[0];
1280
+ if (id === void 0) {
1281
+ return {
1282
+ ok: false,
1283
+ envelope: malformed("`standup item transition` needs an item id.", ["id"])
1284
+ };
1285
+ }
1286
+ const to = stringFlag(flags, "to");
1287
+ if (!to.ok) return to;
1288
+ if (to.value === void 0) {
1289
+ return { ok: false, envelope: malformed("`standup item transition` needs --to.", ["to"]) };
1290
+ }
1291
+ const dryRun = booleanFlag(flags, "dry-run");
1292
+ if (!dryRun.ok) return dryRun;
1293
+ const fields = jsonFlag(flags, "fields");
1294
+ if (!fields.ok) return fields;
1295
+ const input = { id, to: to.value, dryRun: dryRun.value };
1296
+ if (fields.value !== void 0) input.fields = fields.value;
1297
+ return { ok: true, input };
1298
+ }
1299
+ },
1300
+ {
1301
+ noun: "item",
1302
+ verb: "complete",
1303
+ operation: "complete_item",
1304
+ summary: "Finish an item: move it into a completed state and record the closing summary.",
1305
+ buildInput: (rest, flags) => {
1306
+ const id = rest[0];
1307
+ if (id === void 0) {
1308
+ return {
1309
+ ok: false,
1310
+ envelope: malformed("`standup item complete` needs an item id.", ["id"])
1311
+ };
1312
+ }
1313
+ const to = stringFlag(flags, "to");
1314
+ if (!to.ok) return to;
1315
+ if (to.value === void 0) {
1316
+ return { ok: false, envelope: malformed("`standup item complete` needs --to.", ["to"]) };
1317
+ }
1318
+ const summary = jsonFlag(flags, "summary");
1319
+ if (!summary.ok) return summary;
1320
+ if (summary.value === void 0) {
1321
+ return {
1322
+ ok: false,
1323
+ envelope: malformed("`standup item complete` needs --summary (JSON).", ["summary"])
1324
+ };
1325
+ }
1326
+ const fields = jsonFlag(flags, "fields");
1327
+ if (!fields.ok) return fields;
1328
+ const input = { id, to: to.value, summary: summary.value };
1329
+ if (fields.value !== void 0) input.fields = fields.value;
1330
+ return { ok: true, input };
1331
+ }
1332
+ },
1333
+ {
1334
+ noun: "service",
1335
+ verb: "info",
1336
+ operation: "service_info",
1337
+ summary: "What this build exposes, and the limits a caller has to respect.",
1338
+ buildInput: noInput
1339
+ },
1340
+ // MILESTONES.md #92 — repo/area/machine/account nouns. Kept in their own
1341
+ // module (./commands-admin.ts) and appended here as a single spread, per
1342
+ // that module's own header, so concurrent CLI rows landing entries above
1343
+ // never conflict with this one.
1344
+ ...ADMIN_COMMANDS,
1345
+ ...OWNERSHIP_COMMANDS,
1346
+ ...CONFIG_COMMANDS,
1347
+ // row #83 — `standup config`
1348
+ ...BACKFILL_COMMANDS,
1349
+ // the one-time bulk load (docs/plans/BACKFILL.md)
1350
+ ...ARTIFACT_COMMANDS,
1351
+ // row #98 — artifact writes
1352
+ ...LOOP_COMMANDS,
1353
+ // row #100 - open-loop writes
1354
+ ...SESSION_COMMANDS
1355
+ // the registration handshake (MILESTONES.md #43, SCHEMA.md §21)
1356
+ ]);
1357
+ var ALIASES = Object.freeze({
1358
+ ls: ["item", "list"],
1359
+ show: ["item", "get"],
1360
+ new: ["item", "create"],
1361
+ ...OWNERSHIP_ALIASES
1362
+ });
1363
+ function nouns() {
1364
+ return [...new Set(COMMANDS.map((command) => command.noun))].sort();
1365
+ }
1366
+ function verbsFor(noun) {
1367
+ return COMMANDS.filter((command) => command.noun === noun).map((command) => command.verb).sort();
1368
+ }
1369
+ function lookupCommand(words) {
1370
+ const first = words[0];
1371
+ if (first === void 0) {
1372
+ return {
1373
+ ok: false,
1374
+ envelope: malformed(
1375
+ `Nothing to do. Usage: standup <noun> <verb>. Nouns: ${nouns().join(", ")}.`
1376
+ )
1377
+ };
1378
+ }
1379
+ const alias = ALIASES[first];
1380
+ const [noun, verb, rest, viaAlias] = alias ? [alias[0], alias[1], words.slice(1), first] : [first, words[1], words.slice(2), void 0];
1381
+ const known = COMMANDS.filter((command2) => command2.noun === noun);
1382
+ if (known.length === 0) {
1383
+ return {
1384
+ ok: false,
1385
+ envelope: malformed(`No such noun: ${noun}. Nouns: ${nouns().join(", ")}.`, ["noun"])
1386
+ };
1387
+ }
1388
+ if (verb === void 0) {
1389
+ return {
1390
+ ok: false,
1391
+ envelope: malformed(
1392
+ `\`standup ${noun}\` needs a verb. Verbs: ${verbsFor(noun).join(", ")}.`,
1393
+ ["verb"]
1394
+ )
1395
+ };
1396
+ }
1397
+ const command = known.find((candidate) => candidate.verb === verb);
1398
+ if (!command) {
1399
+ return {
1400
+ ok: false,
1401
+ envelope: malformed(
1402
+ `No such verb for ${noun}: ${verb}. Verbs: ${verbsFor(noun).join(", ")}.`,
1403
+ ["verb"]
1404
+ )
1405
+ };
1406
+ }
1407
+ return {
1408
+ ok: true,
1409
+ match: { command, rest, ...viaAlias === void 0 ? {} : { viaAlias } }
1410
+ };
1411
+ }
1412
+ function identityFlags(flags) {
1413
+ const as = stringFlag(flags, "as");
1414
+ if (!as.ok) return as;
1415
+ const session = stringFlag(flags, "session");
1416
+ if (!session.ok) return session;
1417
+ const url = stringFlag(flags, "url");
1418
+ if (!url.ok) return url;
1419
+ return {
1420
+ ok: true,
1421
+ ...as.value === void 0 ? {} : { as: as.value },
1422
+ ...session.value === void 0 ? {} : { session: session.value },
1423
+ ...url.value === void 0 ? {} : { url: url.value }
1424
+ };
1425
+ }
1426
+
1427
+ // src/lib/cli/binding.ts
1428
+ function bindingOk(data) {
1429
+ return { ok: true, data };
1430
+ }
1431
+ function bindingRejected(rejection, message) {
1432
+ return { ok: false, rejection, message };
1433
+ }
1434
+
1435
+ // src/lib/cli/config.ts
1436
+ function firstDefined(...values) {
1437
+ for (const value of values) {
1438
+ if (value !== void 0 && value.trim() !== "") return value.trim();
1439
+ }
1440
+ return void 0;
1441
+ }
1442
+ function resolveConfig({ flags = {}, env = {}, file: file2 = {} } = {}) {
1443
+ const standupUrl = firstDefined(flags.url, env.STANDUP_URL, file2.standupUrl);
1444
+ const databaseUrl = firstDefined(env.DATABASE_URL, file2.databaseUrl);
1445
+ const sessionId = firstDefined(flags.session, env.STANDUP_SESSION_ID, file2.sessionId);
1446
+ const actor = firstDefined(flags.as, env.STANDUP_ACTOR, file2.actor);
1447
+ const token = firstDefined(env.STANDUP_TOKEN, file2.token);
1448
+ const identity = {
1449
+ ...sessionId === void 0 ? {} : { sessionId },
1450
+ ...actor === void 0 ? {} : { actor }
1451
+ };
1452
+ if (flags.direct === true) {
1453
+ if (databaseUrl === void 0) {
1454
+ return unconfigured(
1455
+ "--direct needs a database to talk to, and neither DATABASE_URL nor the local configuration supplied one. Run `standup init` first.",
1456
+ ["DATABASE_URL"]
1457
+ );
1458
+ }
1459
+ return { ok: true, config: { binding: "direct", databaseUrl, ...identity } };
1460
+ }
1461
+ if (standupUrl !== void 0) {
1462
+ return {
1463
+ ok: true,
1464
+ config: {
1465
+ binding: "http",
1466
+ standupUrl,
1467
+ ...identity,
1468
+ ...token === void 0 ? {} : { token }
1469
+ }
1470
+ };
1471
+ }
1472
+ if (databaseUrl !== void 0) {
1473
+ return { ok: true, config: { binding: "direct", databaseUrl, ...identity } };
1474
+ }
1475
+ return unconfigured(
1476
+ "Neither STANDUP_URL nor DATABASE_URL resolved, so there is nothing to talk to. Run `standup init` first.",
1477
+ ["STANDUP_URL", "DATABASE_URL"]
1478
+ );
1479
+ }
1480
+ function unconfigured(message, fields) {
1481
+ return { ok: false, envelope: malformed(message, fields), exitCode: EXIT.UNCONFIGURED };
1482
+ }
1483
+ function sourceOf(flag, env, file2) {
1484
+ if (firstDefined(flag) !== void 0) return "flag";
1485
+ if (firstDefined(env) !== void 0) return "environment";
1486
+ if (firstDefined(file2) !== void 0) return "file";
1487
+ return "none";
1488
+ }
1489
+ function describeResolution({
1490
+ flags = {},
1491
+ env = {},
1492
+ file: file2 = {}
1493
+ } = {}) {
1494
+ const entries = [
1495
+ ["STANDUP_URL", flags.url, env.STANDUP_URL, file2.standupUrl],
1496
+ ["DATABASE_URL", void 0, env.DATABASE_URL, file2.databaseUrl],
1497
+ ["STANDUP_SESSION_ID", flags.session, env.STANDUP_SESSION_ID, file2.sessionId],
1498
+ ["STANDUP_ACTOR", flags.as, env.STANDUP_ACTOR, file2.actor]
1499
+ ];
1500
+ return entries.map(([name, flag, envValue, fileValue]) => {
1501
+ const source = sourceOf(flag, envValue, fileValue);
1502
+ return { name, present: source !== "none", source };
1503
+ });
1504
+ }
1505
+
1506
+ // src/lib/cli/bindings/direct.ts
1507
+ function createDirectBinding({ service, sessionId, actor }) {
1508
+ const caller = {
1509
+ transport: "cli-direct",
1510
+ ...sessionId === void 0 ? {} : { sessionId },
1511
+ ...actor === void 0 ? {} : { actor }
1512
+ };
1513
+ return {
1514
+ name: "direct",
1515
+ async invoke(operation, input) {
1516
+ const requestId = newRequestId();
1517
+ try {
1518
+ return bindingOk(
1519
+ await service.call(operation, input, { caller: { ...caller, requestId } })
1520
+ );
1521
+ } catch (error) {
1522
+ if (isRehearsalRollback(error)) {
1523
+ return bindingOk({ outcome: error.outcome });
1524
+ }
1525
+ const serviceError = toServiceError(error);
1526
+ if (serviceError.fault === "server") {
1527
+ log.error("Command failed unexpectedly.", {
1528
+ requestId,
1529
+ transport: caller.transport,
1530
+ operation,
1531
+ ...faultContext(serviceError),
1532
+ err: serviceError
1533
+ });
1534
+ } else {
1535
+ log.debug("Command refused.", {
1536
+ requestId,
1537
+ transport: caller.transport,
1538
+ operation,
1539
+ code: serviceError.code,
1540
+ ...faultContext(serviceError),
1541
+ ...serviceError.guard === void 0 ? {} : { guard: serviceError.guard }
1542
+ });
1543
+ }
1544
+ return bindingRejected(serviceError.toRejection(), serviceError.message);
1545
+ }
1546
+ }
1547
+ };
1548
+ }
1549
+
1550
+ // src/lib/cli/bindings/http-routes-admin.ts
1551
+ function property(body, key) {
1552
+ return typeof body === "object" && body !== null ? body[key] : void 0;
1553
+ }
1554
+ function queryString(input) {
1555
+ const params = new URLSearchParams();
1556
+ for (const [key, value] of Object.entries(input)) {
1557
+ if (value === void 0) continue;
1558
+ params.set(key, value === null ? "" : String(value));
1559
+ }
1560
+ const query = params.toString();
1561
+ return query.length === 0 ? "" : `?${query}`;
1562
+ }
1563
+ function without(input, key) {
1564
+ return Object.fromEntries(Object.entries(input).filter(([k]) => k !== key));
1565
+ }
1566
+ var ADMIN_HTTP_ROUTES = Object.freeze({
1567
+ list_repos: {
1568
+ method: "GET",
1569
+ request: (input) => ({ path: `/api/repos${queryString(input)}` }),
1570
+ unwrap: (body) => body
1571
+ },
1572
+ get_repo: {
1573
+ method: "GET",
1574
+ request: (input) => ({ path: `/api/repos/${encodeURIComponent(String(input.id ?? ""))}` }),
1575
+ unwrap: (body) => property(body, "repo")
1576
+ },
1577
+ create_repo: {
1578
+ method: "POST",
1579
+ request: (input) => ({ path: "/api/repos", body: input }),
1580
+ unwrap: (body) => property(body, "repo")
1581
+ },
1582
+ update_repo: {
1583
+ method: "PATCH",
1584
+ request: (input) => ({
1585
+ path: `/api/repos/${encodeURIComponent(String(input.id ?? ""))}`,
1586
+ body: without(input, "id")
1587
+ }),
1588
+ unwrap: (body) => property(body, "repo")
1589
+ },
1590
+ list_areas: {
1591
+ method: "GET",
1592
+ request: (input) => ({ path: `/api/areas${queryString(input)}` }),
1593
+ unwrap: (body) => body
1594
+ },
1595
+ get_area: {
1596
+ method: "GET",
1597
+ request: (input) => ({ path: `/api/areas/${encodeURIComponent(String(input.id ?? ""))}` }),
1598
+ unwrap: (body) => property(body, "area")
1599
+ },
1600
+ create_area: {
1601
+ method: "POST",
1602
+ request: (input) => ({ path: "/api/areas", body: input }),
1603
+ unwrap: (body) => property(body, "area")
1604
+ },
1605
+ update_area: {
1606
+ method: "PATCH",
1607
+ request: (input) => ({
1608
+ path: `/api/areas/${encodeURIComponent(String(input.id ?? ""))}`,
1609
+ body: without(input, "id")
1610
+ }),
1611
+ unwrap: (body) => property(body, "area")
1612
+ },
1613
+ merge_areas: {
1614
+ method: "POST",
1615
+ request: (input) => ({ path: "/api/areas/merge", body: input }),
1616
+ // `POST /api/areas/merge` returns `MergeAreasOutput` unwrapped — `to`,
1617
+ // `from`, `itemsMerged`, `duplicatesResolved` — same as `list_areas`,
1618
+ // not nested under a named key the way the single-row `get`/`create`
1619
+ // routes are.
1620
+ unwrap: (body) => body
1621
+ },
1622
+ list_machines: {
1623
+ method: "GET",
1624
+ request: () => ({ path: "/api/machines" }),
1625
+ unwrap: (body) => body
1626
+ },
1627
+ get_machine: {
1628
+ method: "GET",
1629
+ request: (input) => ({ path: `/api/machines/${encodeURIComponent(String(input.name ?? ""))}` }),
1630
+ unwrap: (body) => property(body, "machine")
1631
+ },
1632
+ update_machine: {
1633
+ method: "PATCH",
1634
+ request: (input) => ({
1635
+ path: `/api/machines/${encodeURIComponent(String(input.name ?? ""))}`,
1636
+ body: without(input, "name")
1637
+ }),
1638
+ unwrap: (body) => property(body, "machine")
1639
+ },
1640
+ list_accounts: {
1641
+ method: "GET",
1642
+ request: () => ({ path: "/api/accounts" }),
1643
+ unwrap: (body) => body
1644
+ },
1645
+ get_account: {
1646
+ method: "GET",
1647
+ request: (input) => ({ path: `/api/accounts/${encodeURIComponent(String(input.id ?? ""))}` }),
1648
+ unwrap: (body) => property(body, "account")
1649
+ },
1650
+ update_account: {
1651
+ method: "PATCH",
1652
+ request: (input) => ({
1653
+ path: `/api/accounts/${encodeURIComponent(String(input.id ?? ""))}`,
1654
+ body: without(input, "id")
1655
+ }),
1656
+ unwrap: (body) => property(body, "account")
1657
+ }
1658
+ });
1659
+
1660
+ // src/lib/cli/bindings/http-routes-ownership.ts
1661
+ function property2(body, key) {
1662
+ return typeof body === "object" && body !== null ? body[key] : void 0;
1663
+ }
1664
+ function queryString2(input) {
1665
+ const params = new URLSearchParams();
1666
+ for (const [key, value] of Object.entries(input)) {
1667
+ if (value === void 0) continue;
1668
+ params.set(key, value === null ? "" : String(value));
1669
+ }
1670
+ const query = params.toString();
1671
+ return query.length === 0 ? "" : `?${query}`;
1672
+ }
1673
+ var OWNERSHIP_HTTP_ROUTES = Object.freeze({
1674
+ claim: {
1675
+ method: "POST",
1676
+ request: (input) => ({ path: "/api/claims", body: input }),
1677
+ unwrap: (body) => property2(body, "assignment")
1678
+ },
1679
+ release: {
1680
+ method: "POST",
1681
+ request: (input) => ({ path: "/api/claims/release", body: input }),
1682
+ unwrap: (body) => property2(body, "assignment")
1683
+ },
1684
+ heartbeat: {
1685
+ method: "POST",
1686
+ request: (input) => ({ path: "/api/claims/heartbeat", body: input }),
1687
+ unwrap: (body) => property2(body, "assignment")
1688
+ },
1689
+ // Reclamation (MILESTONES.md #99). Both return their operation's result
1690
+ // object whole rather than under a key — `takeover`'s result is not just an
1691
+ // assignment (it carries how alive the holder was judged, whether the
1692
+ // warning had to be forced, and what has NOT been enforced), and `sweep`'s
1693
+ // is a report with four lists in it. Unwrapping either to one field would
1694
+ // discard the part the caller most needs to read.
1695
+ takeover: {
1696
+ method: "POST",
1697
+ request: (input) => ({ path: "/api/claims/takeover", body: input }),
1698
+ unwrap: (body) => body
1699
+ },
1700
+ sweep: {
1701
+ method: "POST",
1702
+ request: (input) => ({ path: "/api/sweep", body: input }),
1703
+ unwrap: (body) => body
1704
+ },
1705
+ checkpoint: {
1706
+ method: "POST",
1707
+ request: (input) => ({ path: "/api/checkpoints", body: input }),
1708
+ unwrap: (body) => property2(body, "event")
1709
+ },
1710
+ note: {
1711
+ method: "POST",
1712
+ request: (input) => {
1713
+ const { itemId, ...rest } = input;
1714
+ return { path: `/api/items/${encodeURIComponent(String(itemId ?? ""))}/notes`, body: rest };
1715
+ },
1716
+ unwrap: (body) => property2(body, "event")
1717
+ },
1718
+ orientation: {
1719
+ method: "GET",
1720
+ request: (input) => {
1721
+ const { itemId, ...rest } = input;
1722
+ return {
1723
+ path: `/api/items/${encodeURIComponent(String(itemId ?? ""))}/orientation${queryString2(rest)}`
1724
+ };
1725
+ },
1726
+ unwrap: (body) => body
1727
+ },
1728
+ my_work: {
1729
+ method: "GET",
1730
+ request: (input) => ({ path: `/api/my-work${queryString2(input)}` }),
1731
+ unwrap: (body) => body
1732
+ },
1733
+ progress_report: {
1734
+ method: "GET",
1735
+ request: (input) => ({ path: `/api/progress-report${queryString2(input)}` }),
1736
+ unwrap: (body) => body
1737
+ },
1738
+ get_crew_name: {
1739
+ method: "POST",
1740
+ request: (input) => ({ path: "/api/crew/name", body: input }),
1741
+ unwrap: (body) => property2(body, "name")
1742
+ }
1743
+ });
1744
+
1745
+ // src/lib/cli/bindings/http-routes-backfill.ts
1746
+ var BACKFILL_HTTP_ROUTES = Object.freeze({
1747
+ backfill: {
1748
+ method: "POST",
1749
+ // The whole input is the body. `POST /api/backfill` takes the same
1750
+ // `{ payload }` wrapper the operation's own schema does, so there is
1751
+ // nothing to move into a path or a query string — and a payload of this
1752
+ // size could not go in one anyway.
1753
+ request: (input) => ({ path: "/api/backfill", body: input }),
1754
+ // The route returns the operation's result unwrapped (no envelope key),
1755
+ // so there is nothing to unwrap here.
1756
+ unwrap: (body) => body
1757
+ }
1758
+ });
1759
+
1760
+ // src/lib/cli/bindings/http-routes-artifacts.ts
1761
+ function property3(body, key) {
1762
+ return typeof body === "object" && body !== null ? body[key] : void 0;
1763
+ }
1764
+ var ARTIFACT_HTTP_ROUTES = Object.freeze({
1765
+ record_artifact: {
1766
+ method: "POST",
1767
+ request: (input) => {
1768
+ const { itemId, ...rest } = input;
1769
+ return {
1770
+ path: `/api/items/${encodeURIComponent(String(itemId ?? ""))}/artifacts`,
1771
+ body: rest
1772
+ };
1773
+ },
1774
+ unwrap: (body) => property3(body, "artifact")
1775
+ },
1776
+ request_review: {
1777
+ method: "POST",
1778
+ request: (input) => {
1779
+ const { itemId, ...rest } = input;
1780
+ return {
1781
+ path: `/api/items/${encodeURIComponent(String(itemId ?? ""))}/review-requests`,
1782
+ body: rest
1783
+ };
1784
+ },
1785
+ unwrap: (body) => property3(body, "event")
1786
+ }
1787
+ });
1788
+
1789
+ // src/lib/cli/bindings/http-routes-loops.ts
1790
+ function property4(body, key) {
1791
+ return typeof body === "object" && body !== null ? body[key] : void 0;
1792
+ }
1793
+ function queryString3(input) {
1794
+ const params = new URLSearchParams();
1795
+ for (const [key, value] of Object.entries(input)) {
1796
+ if (value === void 0 || value === null) continue;
1797
+ params.set(key, String(value));
1798
+ }
1799
+ const query = params.toString();
1800
+ return query.length === 0 ? "" : `?${query}`;
1801
+ }
1802
+ function loopPath(itemId, loopId) {
1803
+ return `/api/items/${encodeURIComponent(String(itemId ?? ""))}/loops/${encodeURIComponent(String(loopId ?? ""))}`;
1804
+ }
1805
+ var LOOP_HTTP_ROUTES = Object.freeze({
1806
+ loop_add: {
1807
+ method: "POST",
1808
+ request: (input) => {
1809
+ const { itemId, ...rest } = input;
1810
+ return { path: `/api/items/${encodeURIComponent(String(itemId ?? ""))}/loops`, body: rest };
1811
+ },
1812
+ // The whole body, not one property: `loop_add` returns `{loopId, event}`
1813
+ // and the `loopId` is the half the caller cannot do without — it is
1814
+ // generated server-side, and a loop whose id the caller never learns can
1815
+ // never be closed. Unwrapping to `event` would throw it away.
1816
+ unwrap: (body) => body
1817
+ },
1818
+ loop_close: {
1819
+ method: "POST",
1820
+ request: (input) => {
1821
+ const { itemId, loopId, ...rest } = input;
1822
+ return { path: `${loopPath(itemId, loopId)}/close`, body: rest };
1823
+ },
1824
+ unwrap: (body) => property4(body, "event")
1825
+ },
1826
+ // The list read. Everything but the item id travels in the query string,
1827
+ // as it does for every other list-shaped read here, and the result object
1828
+ // comes back unwrapped so `http` and `direct` return the same shape.
1829
+ loop_list: {
1830
+ method: "GET",
1831
+ request: (input) => {
1832
+ const { itemId, ...rest } = input;
1833
+ return {
1834
+ path: `/api/items/${encodeURIComponent(String(itemId ?? ""))}/loops${queryString3(rest)}`
1835
+ };
1836
+ },
1837
+ unwrap: (body) => body
1838
+ },
1839
+ loop_get: {
1840
+ method: "GET",
1841
+ request: (input) => ({ path: loopPath(input.itemId, input.loopId) }),
1842
+ unwrap: (body) => body
1843
+ },
1844
+ // The whole body, not just the event: `previousText` cannot be recovered
1845
+ // from any later read, because the loop now reports its new wording.
1846
+ loop_edit: {
1847
+ method: "PATCH",
1848
+ request: (input) => {
1849
+ const { itemId, loopId, ...rest } = input;
1850
+ return { path: loopPath(itemId, loopId), body: rest };
1851
+ },
1852
+ unwrap: (body) => body
1853
+ },
1854
+ loop_delete: {
1855
+ method: "DELETE",
1856
+ request: (input) => {
1857
+ const { itemId, loopId, ...rest } = input;
1858
+ return { path: loopPath(itemId, loopId), body: rest };
1859
+ },
1860
+ unwrap: (body) => body
1861
+ }
1862
+ });
1863
+
1864
+ // src/lib/cli/bindings/http-routes-sessions.ts
1865
+ function property5(body, key) {
1866
+ return typeof body === "object" && body !== null ? body[key] : void 0;
1867
+ }
1868
+ var SESSION_HTTP_ROUTES = Object.freeze({
1869
+ register_session: {
1870
+ method: "POST",
1871
+ /**
1872
+ * The session id goes in the path and everything else in the body,
1873
+ * matching the endpoint's shape. It is *not* also sent in the body: the
1874
+ * route puts the path's id last when it composes the operation input, so
1875
+ * a body copy would be overwritten by the identical value — dead weight
1876
+ * that a reader would have to check was in fact identical.
1877
+ */
1878
+ request: (input) => {
1879
+ const { sessionId, ...rest } = input;
1880
+ return {
1881
+ path: `/api/sessions/${encodeURIComponent(String(sessionId ?? ""))}/register`,
1882
+ body: rest
1883
+ };
1884
+ },
1885
+ unwrap: (body) => property5(body, "registration")
1886
+ }
1887
+ });
1888
+
1889
+ // src/lib/session-transport-header.ts
1890
+ var CLI_TRANSPORT_HEADER = "X-Standup-Transport";
1891
+ var SESSION_HEADER = "X-Standup-Session";
1892
+ var ACTOR_HEADER = "X-Standup-Actor";
1893
+ var CLI_TRANSPORTS = Object.freeze(["cli-http"]);
1894
+
1895
+ // src/lib/request-id-header.ts
1896
+ var REQUEST_ID_HEADER = "X-Request-Id";
1897
+
1898
+ // src/lib/cli/bindings/http.ts
1899
+ function asRecord(input) {
1900
+ return typeof input === "object" && input !== null ? input : {};
1901
+ }
1902
+ function property6(body, key) {
1903
+ return typeof body === "object" && body !== null ? body[key] : void 0;
1904
+ }
1905
+ function queryString4(input) {
1906
+ const params = new URLSearchParams();
1907
+ for (const [key, value] of Object.entries(input)) {
1908
+ if (value === void 0) continue;
1909
+ params.set(key, value === null ? "" : String(value));
1910
+ }
1911
+ const query = params.toString();
1912
+ return query.length === 0 ? "" : `?${query}`;
1913
+ }
1914
+ var HTTP_ROUTES = Object.freeze({
1915
+ create_item: {
1916
+ method: "POST",
1917
+ request: (input) => ({ path: "/api/items", body: input }),
1918
+ unwrap: (body) => property6(body, "item")
1919
+ },
1920
+ // The three explicit creates. Each posts to its own collection, so which
1921
+ // kind is being made is visible in the request rather than inferred from
1922
+ // the body — the same property the operations exist to give a caller.
1923
+ create_project: {
1924
+ method: "POST",
1925
+ request: (input) => ({ path: "/api/projects", body: input }),
1926
+ unwrap: (body) => property6(body, "item")
1927
+ },
1928
+ create_task: {
1929
+ method: "POST",
1930
+ request: (input) => ({ path: "/api/tasks", body: input }),
1931
+ unwrap: (body) => property6(body, "item")
1932
+ },
1933
+ create_subtask: {
1934
+ method: "POST",
1935
+ request: (input) => ({ path: "/api/subtasks", body: input }),
1936
+ unwrap: (body) => property6(body, "item")
1937
+ },
1938
+ get_item: {
1939
+ method: "GET",
1940
+ // `id` goes in the path; every other input — `full` (MILESTONES.md
1941
+ // #107) — goes in the query string. Without this the two
1942
+ // bindings would disagree about what `--full` does: `direct` would
1943
+ // return the whole record and `http` the slim shape, from one command
1944
+ // line. Row #85's one-interface test compares exactly that.
1945
+ request: (input) => {
1946
+ const { id, ...rest } = input;
1947
+ return {
1948
+ path: `/api/items/${encodeURIComponent(String(id ?? ""))}${queryString4(rest)}`
1949
+ };
1950
+ },
1951
+ unwrap: (body) => property6(body, "item")
1952
+ },
1953
+ update_item: {
1954
+ method: "PATCH",
1955
+ request: (input) => {
1956
+ const { id, ...rest } = input;
1957
+ return { path: `/api/items/${encodeURIComponent(String(id ?? ""))}`, body: rest };
1958
+ },
1959
+ unwrap: (body) => property6(body, "item")
1960
+ },
1961
+ list_items: {
1962
+ method: "GET",
1963
+ request: (input) => ({ path: `/api/items${queryString4(input)}` }),
1964
+ unwrap: (body) => body
1965
+ },
1966
+ // Stale candidates. `GET /api/stale-candidates` returns the result object
1967
+ // unwrapped, like every other list-shaped read, so `unwrap` is the
1968
+ // identity — the same shape `direct` returns for the same call.
1969
+ get_stale_candidates: {
1970
+ method: "GET",
1971
+ request: (input) => ({ path: `/api/stale-candidates${queryString4(input)}` }),
1972
+ unwrap: (body) => body
1973
+ },
1974
+ // Row #105. `GET /search` returns the result object unwrapped, like every
1975
+ // other list-shaped read, so `unwrap` is the identity — the same shape
1976
+ // `direct` returns for the same call.
1977
+ search: {
1978
+ method: "GET",
1979
+ request: (input) => ({ path: `/api/search${queryString4(input)}` }),
1980
+ unwrap: (body) => body
1981
+ },
1982
+ // Row #83 — `standup config`. `src/app/api/settings/**` returns every
1983
+ // settings operation's result unwrapped already (SCHEMA.md §19), so
1984
+ // `unwrap` is the identity for all four — the same shape `direct` returns.
1985
+ get_settings: {
1986
+ method: "GET",
1987
+ request: () => ({ path: "/api/settings" }),
1988
+ unwrap: (body) => body
1989
+ },
1990
+ get_setting: {
1991
+ method: "GET",
1992
+ request: (input) => ({
1993
+ path: `/api/settings/${encodeURIComponent(String(input.key ?? ""))}`
1994
+ }),
1995
+ unwrap: (body) => body
1996
+ },
1997
+ put_setting: {
1998
+ method: "PUT",
1999
+ request: (input) => {
2000
+ const { key, ...rest } = input;
2001
+ return { path: `/api/settings/${encodeURIComponent(String(key ?? ""))}`, body: rest };
2002
+ },
2003
+ unwrap: (body) => body
2004
+ },
2005
+ delete_setting: {
2006
+ method: "DELETE",
2007
+ request: (input) => ({
2008
+ path: `/api/settings/${encodeURIComponent(String(input.key ?? ""))}`
2009
+ }),
2010
+ unwrap: (body) => body
2011
+ },
2012
+ transition_item: {
2013
+ method: "POST",
2014
+ request: (input) => {
2015
+ const { id, dryRun, ...rest } = input;
2016
+ const query = dryRun === true ? "?dry_run=true" : "";
2017
+ return {
2018
+ path: `/api/items/${encodeURIComponent(String(id ?? ""))}/transition${query}`,
2019
+ body: rest
2020
+ };
2021
+ },
2022
+ // The route answers `{ item, outcome }` for a real move and `{ outcome }`
2023
+ // alone for a rehearsal (`transition/route.ts`'s `RehearsalRollback`
2024
+ // unwrapping) — the same two shapes the `direct` binding's own
2025
+ // rehearsal handling produces. Returning the body unchanged, rather
2026
+ // than pulling one key out the way the single-item routes above do, is
2027
+ // what keeps those two shapes identical between bindings.
2028
+ unwrap: (body) => body
2029
+ },
2030
+ complete_item: {
2031
+ method: "POST",
2032
+ request: (input) => {
2033
+ const { id, ...rest } = input;
2034
+ return { path: `/api/items/${encodeURIComponent(String(id ?? ""))}/complete`, body: rest };
2035
+ },
2036
+ unwrap: (body) => property6(body, "item")
2037
+ },
2038
+ // MILESTONES.md #92 — repo/area/machine/account routes, kept in their own
2039
+ // module (./http-routes-admin.ts) and spread in as a single line, per
2040
+ // that module's own header, so concurrent CLI rows adding entries above
2041
+ // never conflict with this one.
2042
+ ...ADMIN_HTTP_ROUTES,
2043
+ ...OWNERSHIP_HTTP_ROUTES,
2044
+ ...BACKFILL_HTTP_ROUTES,
2045
+ ...ARTIFACT_HTTP_ROUTES,
2046
+ // row #98 — artifact writes
2047
+ ...LOOP_HTTP_ROUTES,
2048
+ // row #100 - open-loop writes
2049
+ ...SESSION_HTTP_ROUTES
2050
+ // row #43 — the registration handshake
2051
+ });
2052
+ function isServiceErrorCode(value) {
2053
+ return typeof value === "string" && SERVICE_ERROR_CODES.includes(value);
2054
+ }
2055
+ function rejectionFromBody(body, status) {
2056
+ const error = property6(body, "error");
2057
+ const code = property6(error, "code");
2058
+ const message = property6(error, "message");
2059
+ const fields = property6(error, "fields");
2060
+ const guard = property6(error, "guard");
2061
+ if (!isServiceErrorCode(code)) {
2062
+ return {
2063
+ rejection: { code: "internal", fields: [] },
2064
+ message: `The server answered ${status} with a body this build does not recognise.`
2065
+ };
2066
+ }
2067
+ return {
2068
+ rejection: {
2069
+ code,
2070
+ fields: Array.isArray(fields) ? fields.map(String) : [],
2071
+ ...typeof guard === "string" ? { guard } : {}
2072
+ },
2073
+ message: typeof message === "string" ? message : `The server refused with ${code}.`
2074
+ };
2075
+ }
2076
+ function createHttpBinding({
2077
+ baseUrl: baseUrl2,
2078
+ fetch: fetchImpl,
2079
+ sessionId,
2080
+ actor,
2081
+ token
2082
+ }) {
2083
+ const root = baseUrl2.replace(/\/+$/, "");
2084
+ const doFetch = fetchImpl ?? ((url, init) => globalThis.fetch(url, init));
2085
+ return {
2086
+ name: "http",
2087
+ async invoke(operation, input) {
2088
+ const route = HTTP_ROUTES[operation];
2089
+ if (!route) {
2090
+ return bindingRejected(
2091
+ { code: "not_implemented", fields: ["operation"] },
2092
+ `The server does not expose ${operation} over HTTP.`
2093
+ );
2094
+ }
2095
+ const { path, body } = route.request(asRecord(input));
2096
+ const headers = { Accept: "application/json" };
2097
+ if (body !== void 0) headers["Content-Type"] = "application/json";
2098
+ if (sessionId !== void 0) headers[SESSION_HEADER] = sessionId;
2099
+ if (actor !== void 0) headers[ACTOR_HEADER] = actor;
2100
+ if (token !== void 0) headers.Authorization = `Bearer ${token}`;
2101
+ headers[CLI_TRANSPORT_HEADER] = "cli-http";
2102
+ const requestId = newRequestId();
2103
+ headers[REQUEST_ID_HEADER] = requestId;
2104
+ let response;
2105
+ try {
2106
+ response = await doFetch(`${root}${path}`, {
2107
+ method: route.method,
2108
+ headers,
2109
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
2110
+ });
2111
+ } catch (cause) {
2112
+ log.error("Could not reach the server.", {
2113
+ requestId,
2114
+ transport: "cli",
2115
+ binding: "http",
2116
+ operation,
2117
+ method: route.method,
2118
+ // The path, not the base URL: the path is this build's own route
2119
+ // table and says which call failed, while the base URL is the
2120
+ // configured address §20 keeps out of a rendered message. In a
2121
+ // log it would be defensible; it is left out because the log line
2122
+ // does not need it to be useful and the rule is easier to keep
2123
+ // than to qualify.
2124
+ path,
2125
+ err: cause
2126
+ });
2127
+ return bindingRejected(
2128
+ { code: "internal", fields: [] },
2129
+ `Could not reach the server (${cause instanceof Error ? cause.name : "unknown error"}). Check the configured address.`
2130
+ );
2131
+ }
2132
+ let parsed;
2133
+ try {
2134
+ parsed = await response.json();
2135
+ } catch {
2136
+ parsed = void 0;
2137
+ }
2138
+ if (!response.ok) {
2139
+ const { rejection, message } = rejectionFromBody(parsed, response.status);
2140
+ if (faultContext(rejection.code).fault === "server") {
2141
+ log.error("The server failed or answered unrecognisably.", {
2142
+ requestId,
2143
+ transport: "cli",
2144
+ binding: "http",
2145
+ operation,
2146
+ status: response.status,
2147
+ ...faultContext(rejection.code)
2148
+ });
2149
+ } else {
2150
+ log.debug("The server refused the command.", {
2151
+ requestId,
2152
+ transport: "cli",
2153
+ binding: "http",
2154
+ operation,
2155
+ status: response.status,
2156
+ code: rejection.code,
2157
+ ...faultContext(rejection.code),
2158
+ ...rejection.guard === void 0 ? {} : { guard: rejection.guard }
2159
+ });
2160
+ }
2161
+ return bindingRejected(rejection, message);
2162
+ }
2163
+ return bindingOk(route.unwrap(parsed));
2164
+ }
2165
+ };
2166
+ }
2167
+
2168
+ // src/lib/cli/doctor.ts
2169
+ function doctorReport(inputs = {}) {
2170
+ const configuration = describeResolution(inputs);
2171
+ const resolution = resolveConfig(inputs);
2172
+ const byName = new Map(configuration.map((note) => [note.name, note]));
2173
+ const serverConfigured = byName.get("STANDUP_URL")?.present === true;
2174
+ const databaseConfigured = byName.get("DATABASE_URL")?.present === true;
2175
+ const capabilities = [
2176
+ {
2177
+ name: "server",
2178
+ status: serverConfigured ? "available" : "unavailable",
2179
+ detail: serverConfigured ? "A server address is configured; commands can call the API." : "No server address configured. The front end and the long-poll need one."
2180
+ },
2181
+ {
2182
+ name: "database",
2183
+ status: databaseConfigured ? "available" : "unavailable",
2184
+ detail: databaseConfigured ? "A database is configured; commands can run the service layer in this process." : "No database configured. --direct and `standup init` need one."
2185
+ },
2186
+ {
2187
+ // #84's own check, as this file's other comment named it: `standup
2188
+ // mcp` is `--direct`-only (`./mcp.ts`), so it is available under
2189
+ // exactly the same condition as the `database` capability above — a
2190
+ // separate entry rather than folding it into that one because a
2191
+ // person asking "what can this installation do" reads MCP over stdio
2192
+ // as its own capability, not as a detail of the database's.
2193
+ name: "mcp_stdio",
2194
+ status: databaseConfigured ? "available" : "unavailable",
2195
+ detail: databaseConfigured ? "A database is configured; `standup mcp` can serve MCP over stdio." : "No database configured. `standup mcp` needs one, the same as --direct."
2196
+ }
2197
+ ];
2198
+ if (resolution.ok) {
2199
+ return {
2200
+ configured: true,
2201
+ binding: resolution.config.binding,
2202
+ configuration,
2203
+ problems: [],
2204
+ capabilities
2205
+ };
2206
+ }
2207
+ return {
2208
+ configured: false,
2209
+ configuration,
2210
+ problems: [resolution.envelope.error.message],
2211
+ capabilities
2212
+ };
2213
+ }
2214
+
2215
+ // src/lib/cli/config-file.ts
2216
+ import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
2217
+ import { homedir } from "node:os";
2218
+ import { dirname, join } from "node:path";
2219
+ var FILE_KEYS = ["standupUrl", "databaseUrl", "sessionId", "actor"];
2220
+ function configFilePath(env = process.env) {
2221
+ const override = env.STANDUP_CONFIG_FILE;
2222
+ if (override !== void 0 && override.trim() !== "") return override;
2223
+ const xdg = env.XDG_CONFIG_HOME;
2224
+ const home = env.HOME ?? env.USERPROFILE ?? homedir();
2225
+ const base = xdg !== void 0 && xdg.trim() !== "" ? xdg : join(home, ".config");
2226
+ return join(base, "agent-standup", "config.json");
2227
+ }
2228
+ function sanitize(value) {
2229
+ if (typeof value !== "object" || value === null) return {};
2230
+ const record = value;
2231
+ const result = {};
2232
+ for (const key of FILE_KEYS) {
2233
+ const raw = record[key];
2234
+ if (typeof raw === "string" && raw.trim() !== "") result[key] = raw;
2235
+ }
2236
+ return result;
2237
+ }
2238
+ function readConfigFile(path = configFilePath()) {
2239
+ try {
2240
+ const raw = readFileSync2(path, "utf-8");
2241
+ return sanitize(JSON.parse(raw));
2242
+ } catch {
2243
+ return {};
2244
+ }
2245
+ }
2246
+ function writeConfigFile(patch, path = configFilePath()) {
2247
+ const merged = { ...readConfigFile(path), ...patch };
2248
+ mkdirSync(dirname(path), { recursive: true });
2249
+ writeFileSync(path, `${JSON.stringify(merged, null, 2)}
2250
+ `, { mode: 384 });
2251
+ return merged;
2252
+ }
2253
+
2254
+ // src/lib/cli/init/resolve.ts
2255
+ var DEFAULT_DATABASE_NAME = "standup";
2256
+ var DEFAULT_APP_ROLE = "standup_app";
2257
+ function resolveInitSource({
2258
+ flags = {},
2259
+ env = {},
2260
+ file: file2 = {}
2261
+ } = {}) {
2262
+ const databaseUrl = firstDefined(flags.databaseUrl, env.DATABASE_URL, file2.databaseUrl);
2263
+ if (databaseUrl !== void 0) {
2264
+ return { kind: "accept", databaseUrl };
2265
+ }
2266
+ const databaseName = firstDefined(flags.databaseName, env.STANDUP_DB_NAME) ?? DEFAULT_DATABASE_NAME;
2267
+ const appRole = firstDefined(flags.appRole, env.STANDUP_APP_ROLE) ?? DEFAULT_APP_ROLE;
2268
+ const appPassword = firstDefined(flags.appPassword, env.STANDUP_APP_PASSWORD);
2269
+ const provisionUrl = firstDefined(flags.provisionUrl, env.STANDUP_PROVISION_URL);
2270
+ if (provisionUrl !== void 0) {
2271
+ return {
2272
+ kind: "provision",
2273
+ provisionUrl,
2274
+ databaseName,
2275
+ appRole,
2276
+ ...appPassword === void 0 ? {} : { appPassword }
2277
+ };
2278
+ }
2279
+ return {
2280
+ kind: "auto",
2281
+ databaseName,
2282
+ appRole,
2283
+ ...appPassword === void 0 ? {} : { appPassword }
2284
+ };
2285
+ }
2286
+
2287
+ // src/lib/cli/init/index.ts
2288
+ async function defaultRunInitSequence(options) {
2289
+ const mod = await import("../run-init-VVQPJOTB.js");
2290
+ return mod.runInitSequence(options);
2291
+ }
2292
+ function readInitFlags(flags) {
2293
+ const names = [
2294
+ "database-url",
2295
+ "provision-url",
2296
+ "database-name",
2297
+ "app-role",
2298
+ "app-password"
2299
+ ];
2300
+ const values = {};
2301
+ for (const name of names) {
2302
+ const result = stringFlag(flags, name);
2303
+ if (!result.ok) return { ok: false, envelope: result.envelope };
2304
+ if (result.value !== void 0) values[name] = result.value;
2305
+ }
2306
+ return {
2307
+ ok: true,
2308
+ value: {
2309
+ ...values["database-url"] === void 0 ? {} : { databaseUrl: values["database-url"] },
2310
+ ...values["provision-url"] === void 0 ? {} : { provisionUrl: values["provision-url"] },
2311
+ ...values["database-name"] === void 0 ? {} : { databaseName: values["database-name"] },
2312
+ ...values["app-role"] === void 0 ? {} : { appRole: values["app-role"] },
2313
+ ...values["app-password"] === void 0 ? {} : { appPassword: values["app-password"] }
2314
+ }
2315
+ };
2316
+ }
2317
+ async function runInitCommand({
2318
+ flags,
2319
+ env = {},
2320
+ file: file2 = {},
2321
+ cwd,
2322
+ deps = {}
2323
+ }) {
2324
+ const parsedFlags = readInitFlags(flags);
2325
+ if (!parsedFlags.ok) {
2326
+ return { envelope: parsedFlags.envelope, exitCode: EXIT.MALFORMED };
2327
+ }
2328
+ const source = resolveInitSource({ flags: parsedFlags.value, env, file: file2 });
2329
+ const runInitSequence = deps.runInitSequence ?? defaultRunInitSequence;
2330
+ const write = deps.writeConfigFile ?? writeConfigFile;
2331
+ const path = deps.configPath ?? configFilePath(env);
2332
+ const result = await runInitSequence({ source, cwd, env });
2333
+ if (!result.ok) {
2334
+ return {
2335
+ envelope: malformed(`standup init could not finish: ${result.message}`, [result.stage]),
2336
+ exitCode: EXIT.UNCONFIGURED
2337
+ };
2338
+ }
2339
+ write({ databaseUrl: result.databaseUrl }, path);
2340
+ return {
2341
+ envelope: ok({
2342
+ source: result.source,
2343
+ database: result.database,
2344
+ ...result.appRole === void 0 ? {} : { appRole: result.appRole },
2345
+ steps: result.steps,
2346
+ configWritten: true,
2347
+ configPath: path
2348
+ }),
2349
+ exitCode: EXIT.OK
2350
+ };
2351
+ }
2352
+
2353
+ // src/lib/mcp/stdio.ts
2354
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2355
+
2356
+ // src/lib/mcp/server.ts
2357
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2358
+
2359
+ // src/lib/adapters/registry.ts
2360
+ function defineAdapter(descriptor) {
2361
+ return Object.freeze(descriptor);
2362
+ }
2363
+ var ADAPTER_REGISTRY = {
2364
+ http: defineAdapter({
2365
+ name: "http",
2366
+ summary: "The web API \u2014 the JSON surface routes under src/app/api mount.",
2367
+ transport: "network"
2368
+ }),
2369
+ mcp_http: defineAdapter({
2370
+ name: "mcp_http",
2371
+ summary: "The MCP server, reached over streamable HTTP.",
2372
+ transport: "network"
2373
+ }),
2374
+ mcp_stdio: defineAdapter({
2375
+ name: "mcp_stdio",
2376
+ summary: "The MCP server, reached over stdio.",
2377
+ transport: "embedded"
2378
+ }),
2379
+ cli: defineAdapter({
2380
+ name: "cli",
2381
+ summary: "The standup command line, on either of its two bindings.",
2382
+ transport: "embedded"
2383
+ })
2384
+ };
2385
+ var ADAPTER_NAMES = Object.freeze(
2386
+ Object.keys(ADAPTER_REGISTRY).sort()
2387
+ );
2388
+
2389
+ // src/lib/adapters/waivers.ts
2390
+ var ADAPTER_WAIVERS = Object.freeze([
2391
+ {
2392
+ adapter: "mcp_http",
2393
+ operation: "backfill",
2394
+ reason: "An MCP tool list is sent to the model on every session, so every tool costs context permanently. Backfill is a one-shot bulk load that is disabled during normal operation; paying a per-session cost for a surface open for minutes is the wrong trade. It carries no guard rejection, so \xA722's bound on waivers is satisfied. Reach it over HTTP or the command line."
2395
+ },
2396
+ {
2397
+ adapter: "mcp_stdio",
2398
+ operation: "backfill",
2399
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, and the per-session tool-list cost is identical on both."
2400
+ },
2401
+ {
2402
+ adapter: "mcp_http",
2403
+ operation: "get_crew_name",
2404
+ reason: "Naming is assigned server-side as a side effect of register_session and claim (ensureNameForSession, @/lib/agent-names) \u2014 an agent never needs to call this separately, so it has no business sitting in a tool list with a required sessionId field the other agent-facing tools' schemas do not explain. It carries no guard rejection (handOutName's only failure mode is an exhausted pool, mapped to a plain conflict), so \xA722's bound on waivers is satisfied. Reach it over HTTP or the command line for the rare caller that wants a name with no other side effect."
2405
+ },
2406
+ {
2407
+ adapter: "mcp_stdio",
2408
+ operation: "get_crew_name",
2409
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2410
+ },
2411
+ {
2412
+ adapter: "mcp_http",
2413
+ operation: "readiness",
2414
+ reason: "Readiness answers a question infrastructure asks \u2014 a deployment gate, a compose condition, a load balancer \u2014 and its consumers reach it as an unauthenticated HTTP probe, which is the one shape an MCP tool cannot be. An agent has no use for it: a session is already talking to a server that answered, so the question is settled by the time any tool could ask it. It carries no guard rejection \u2014 it runs one query and reports counts \u2014 so \xA722's bound on waivers is satisfied."
2415
+ },
2416
+ {
2417
+ adapter: "mcp_stdio",
2418
+ operation: "readiness",
2419
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2420
+ },
2421
+ {
2422
+ adapter: "mcp_http",
2423
+ operation: "hook_decision",
2424
+ reason: "Hook and scheduler infrastructure. Its callers are the hook client and the launcher, which reach the service over its own HTTP routes, not through MCP; an agent session has no use for it and cannot act on what it returns. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied. Reach it over HTTP or the command line."
2425
+ },
2426
+ {
2427
+ adapter: "mcp_stdio",
2428
+ operation: "hook_decision",
2429
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2430
+ },
2431
+ {
2432
+ adapter: "mcp_http",
2433
+ operation: "kill_guard",
2434
+ reason: "Hook and scheduler infrastructure. Its callers are the hook client and the launcher, which reach the service over its own HTTP routes, not through MCP; an agent session has no use for it and cannot act on what it returns. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied. Reach it over HTTP or the command line."
2435
+ },
2436
+ {
2437
+ adapter: "mcp_stdio",
2438
+ operation: "kill_guard",
2439
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2440
+ },
2441
+ {
2442
+ adapter: "mcp_http",
2443
+ operation: "record_tool_calls",
2444
+ reason: "Hook and scheduler infrastructure. Its callers are the hook client and the launcher, which reach the service over its own HTTP routes, not through MCP; an agent session has no use for it and cannot act on what it returns. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied. Reach it over HTTP or the command line."
2445
+ },
2446
+ {
2447
+ adapter: "mcp_stdio",
2448
+ operation: "record_tool_calls",
2449
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2450
+ },
2451
+ {
2452
+ adapter: "mcp_http",
2453
+ operation: "get_session_detail",
2454
+ reason: "Hook and scheduler infrastructure. Its callers are the hook client and the launcher, which reach the service over its own HTTP routes, not through MCP; an agent session has no use for it and cannot act on what it returns. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied. Reach it over HTTP or the command line."
2455
+ },
2456
+ {
2457
+ adapter: "mcp_stdio",
2458
+ operation: "get_session_detail",
2459
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2460
+ },
2461
+ {
2462
+ adapter: "mcp_http",
2463
+ operation: "get_fleet",
2464
+ reason: "Hook and scheduler infrastructure. Its callers are the hook client and the launcher, which reach the service over its own HTTP routes, not through MCP; an agent session has no use for it and cannot act on what it returns. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied. Reach it over HTTP or the command line."
2465
+ },
2466
+ {
2467
+ adapter: "mcp_stdio",
2468
+ operation: "get_fleet",
2469
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2470
+ },
2471
+ {
2472
+ adapter: "mcp_http",
2473
+ operation: "create_area",
2474
+ reason: "Reference-entity administration - a person curates the areas, repos, accounts, machines and people a board refers to, through the web interface or the command line, and an agent consumes the result by name rather than maintaining the table. Twenty of these spend context on every session to serve an administrative task no agent performs. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied."
2475
+ },
2476
+ {
2477
+ adapter: "mcp_stdio",
2478
+ operation: "create_area",
2479
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2480
+ },
2481
+ {
2482
+ adapter: "mcp_http",
2483
+ operation: "delete_area",
2484
+ reason: "Reference-entity administration - a person curates the areas, repos, accounts, machines and people a board refers to, through the web interface or the command line, and an agent consumes the result by name rather than maintaining the table. Twenty of these spend context on every session to serve an administrative task no agent performs. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied."
2485
+ },
2486
+ {
2487
+ adapter: "mcp_stdio",
2488
+ operation: "delete_area",
2489
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2490
+ },
2491
+ {
2492
+ adapter: "mcp_http",
2493
+ operation: "get_area",
2494
+ reason: "Reference-entity administration - a person curates the areas, repos, accounts, machines and people a board refers to, through the web interface or the command line, and an agent consumes the result by name rather than maintaining the table. Twenty of these spend context on every session to serve an administrative task no agent performs. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied."
2495
+ },
2496
+ {
2497
+ adapter: "mcp_stdio",
2498
+ operation: "get_area",
2499
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2500
+ },
2501
+ {
2502
+ adapter: "mcp_http",
2503
+ operation: "list_areas",
2504
+ reason: "Reference-entity administration - a person curates the areas, repos, accounts, machines and people a board refers to, through the web interface or the command line, and an agent consumes the result by name rather than maintaining the table. Twenty of these spend context on every session to serve an administrative task no agent performs. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied."
2505
+ },
2506
+ {
2507
+ adapter: "mcp_stdio",
2508
+ operation: "list_areas",
2509
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2510
+ },
2511
+ {
2512
+ adapter: "mcp_http",
2513
+ operation: "update_area",
2514
+ reason: "Reference-entity administration - a person curates the areas, repos, accounts, machines and people a board refers to, through the web interface or the command line, and an agent consumes the result by name rather than maintaining the table. Twenty of these spend context on every session to serve an administrative task no agent performs. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied."
2515
+ },
2516
+ {
2517
+ adapter: "mcp_stdio",
2518
+ operation: "update_area",
2519
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2520
+ },
2521
+ {
2522
+ adapter: "mcp_http",
2523
+ operation: "merge_areas",
2524
+ reason: "Reference-entity administration - a person curates the areas, repos, accounts, machines and people a board refers to, through the web interface or the command line, and an agent consumes the result by name rather than maintaining the table. Twenty of these spend context on every session to serve an administrative task no agent performs. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied."
2525
+ },
2526
+ {
2527
+ adapter: "mcp_stdio",
2528
+ operation: "merge_areas",
2529
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2530
+ },
2531
+ {
2532
+ adapter: "mcp_http",
2533
+ operation: "create_repo",
2534
+ reason: "Reference-entity administration - a person curates the areas, repos, accounts, machines and people a board refers to, through the web interface or the command line, and an agent consumes the result by name rather than maintaining the table. Twenty of these spend context on every session to serve an administrative task no agent performs. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied."
2535
+ },
2536
+ {
2537
+ adapter: "mcp_stdio",
2538
+ operation: "create_repo",
2539
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2540
+ },
2541
+ {
2542
+ adapter: "mcp_http",
2543
+ operation: "delete_repo",
2544
+ reason: "Reference-entity administration - a person curates the areas, repos, accounts, machines and people a board refers to, through the web interface or the command line, and an agent consumes the result by name rather than maintaining the table. Twenty of these spend context on every session to serve an administrative task no agent performs. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied."
2545
+ },
2546
+ {
2547
+ adapter: "mcp_stdio",
2548
+ operation: "delete_repo",
2549
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2550
+ },
2551
+ {
2552
+ adapter: "mcp_http",
2553
+ operation: "get_repo",
2554
+ reason: "Reference-entity administration - a person curates the areas, repos, accounts, machines and people a board refers to, through the web interface or the command line, and an agent consumes the result by name rather than maintaining the table. Twenty of these spend context on every session to serve an administrative task no agent performs. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied."
2555
+ },
2556
+ {
2557
+ adapter: "mcp_stdio",
2558
+ operation: "get_repo",
2559
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2560
+ },
2561
+ {
2562
+ adapter: "mcp_http",
2563
+ operation: "list_repos",
2564
+ reason: "Reference-entity administration - a person curates the areas, repos, accounts, machines and people a board refers to, through the web interface or the command line, and an agent consumes the result by name rather than maintaining the table. Twenty of these spend context on every session to serve an administrative task no agent performs. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied."
2565
+ },
2566
+ {
2567
+ adapter: "mcp_stdio",
2568
+ operation: "list_repos",
2569
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2570
+ },
2571
+ {
2572
+ adapter: "mcp_http",
2573
+ operation: "update_repo",
2574
+ reason: "Reference-entity administration - a person curates the areas, repos, accounts, machines and people a board refers to, through the web interface or the command line, and an agent consumes the result by name rather than maintaining the table. Twenty of these spend context on every session to serve an administrative task no agent performs. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied."
2575
+ },
2576
+ {
2577
+ adapter: "mcp_stdio",
2578
+ operation: "update_repo",
2579
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2580
+ },
2581
+ {
2582
+ adapter: "mcp_http",
2583
+ operation: "get_account",
2584
+ reason: "Reference-entity administration - a person curates the areas, repos, accounts, machines and people a board refers to, through the web interface or the command line, and an agent consumes the result by name rather than maintaining the table. Twenty of these spend context on every session to serve an administrative task no agent performs. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied."
2585
+ },
2586
+ {
2587
+ adapter: "mcp_stdio",
2588
+ operation: "get_account",
2589
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2590
+ },
2591
+ {
2592
+ adapter: "mcp_http",
2593
+ operation: "list_accounts",
2594
+ reason: "Reference-entity administration - a person curates the areas, repos, accounts, machines and people a board refers to, through the web interface or the command line, and an agent consumes the result by name rather than maintaining the table. Twenty of these spend context on every session to serve an administrative task no agent performs. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied."
2595
+ },
2596
+ {
2597
+ adapter: "mcp_stdio",
2598
+ operation: "list_accounts",
2599
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2600
+ },
2601
+ {
2602
+ adapter: "mcp_http",
2603
+ operation: "update_account",
2604
+ reason: "Reference-entity administration - a person curates the areas, repos, accounts, machines and people a board refers to, through the web interface or the command line, and an agent consumes the result by name rather than maintaining the table. Twenty of these spend context on every session to serve an administrative task no agent performs. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied."
2605
+ },
2606
+ {
2607
+ adapter: "mcp_stdio",
2608
+ operation: "update_account",
2609
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2610
+ },
2611
+ {
2612
+ adapter: "mcp_http",
2613
+ operation: "get_machine",
2614
+ reason: "Reference-entity administration - a person curates the areas, repos, accounts, machines and people a board refers to, through the web interface or the command line, and an agent consumes the result by name rather than maintaining the table. Twenty of these spend context on every session to serve an administrative task no agent performs. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied."
2615
+ },
2616
+ {
2617
+ adapter: "mcp_stdio",
2618
+ operation: "get_machine",
2619
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2620
+ },
2621
+ {
2622
+ adapter: "mcp_http",
2623
+ operation: "list_machines",
2624
+ reason: "Reference-entity administration - a person curates the areas, repos, accounts, machines and people a board refers to, through the web interface or the command line, and an agent consumes the result by name rather than maintaining the table. Twenty of these spend context on every session to serve an administrative task no agent performs. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied."
2625
+ },
2626
+ {
2627
+ adapter: "mcp_stdio",
2628
+ operation: "list_machines",
2629
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2630
+ },
2631
+ {
2632
+ adapter: "mcp_http",
2633
+ operation: "update_machine",
2634
+ reason: "Reference-entity administration - a person curates the areas, repos, accounts, machines and people a board refers to, through the web interface or the command line, and an agent consumes the result by name rather than maintaining the table. Twenty of these spend context on every session to serve an administrative task no agent performs. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied."
2635
+ },
2636
+ {
2637
+ adapter: "mcp_stdio",
2638
+ operation: "update_machine",
2639
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2640
+ },
2641
+ {
2642
+ adapter: "mcp_http",
2643
+ operation: "delete_person",
2644
+ reason: "Reference-entity administration - a person curates the areas, repos, accounts, machines and people a board refers to, through the web interface or the command line, and an agent consumes the result by name rather than maintaining the table. Twenty of these spend context on every session to serve an administrative task no agent performs. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied."
2645
+ },
2646
+ {
2647
+ adapter: "mcp_stdio",
2648
+ operation: "delete_person",
2649
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2650
+ },
2651
+ {
2652
+ adapter: "mcp_http",
2653
+ operation: "list_people",
2654
+ reason: "Reference-entity administration - a person curates the areas, repos, accounts, machines and people a board refers to, through the web interface or the command line, and an agent consumes the result by name rather than maintaining the table. Twenty of these spend context on every session to serve an administrative task no agent performs. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied."
2655
+ },
2656
+ {
2657
+ adapter: "mcp_stdio",
2658
+ operation: "list_people",
2659
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2660
+ },
2661
+ {
2662
+ adapter: "mcp_http",
2663
+ operation: "update_person",
2664
+ reason: "Reference-entity administration - a person curates the areas, repos, accounts, machines and people a board refers to, through the web interface or the command line, and an agent consumes the result by name rather than maintaining the table. Twenty of these spend context on every session to serve an administrative task no agent performs. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied."
2665
+ },
2666
+ {
2667
+ adapter: "mcp_stdio",
2668
+ operation: "update_person",
2669
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2670
+ },
2671
+ {
2672
+ adapter: "mcp_http",
2673
+ operation: "get_setting",
2674
+ reason: "Settings administration. A setting is a deployment-wide policy decision a person makes \u2014 what the guards require, what the caps are \u2014 and an agent that could rewrite one could turn off the checks it is subject to. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied. Reach it over HTTP or the command line."
2675
+ },
2676
+ {
2677
+ adapter: "mcp_stdio",
2678
+ operation: "get_setting",
2679
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2680
+ },
2681
+ {
2682
+ adapter: "mcp_http",
2683
+ operation: "get_settings",
2684
+ reason: "Settings administration. A setting is a deployment-wide policy decision a person makes \u2014 what the guards require, what the caps are \u2014 and an agent that could rewrite one could turn off the checks it is subject to. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied. Reach it over HTTP or the command line."
2685
+ },
2686
+ {
2687
+ adapter: "mcp_stdio",
2688
+ operation: "get_settings",
2689
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2690
+ },
2691
+ {
2692
+ adapter: "mcp_http",
2693
+ operation: "put_setting",
2694
+ reason: "Settings administration. A setting is a deployment-wide policy decision a person makes \u2014 what the guards require, what the caps are \u2014 and an agent that could rewrite one could turn off the checks it is subject to. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied. Reach it over HTTP or the command line."
2695
+ },
2696
+ {
2697
+ adapter: "mcp_stdio",
2698
+ operation: "put_setting",
2699
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2700
+ },
2701
+ {
2702
+ adapter: "mcp_http",
2703
+ operation: "patch_settings",
2704
+ reason: "Settings administration. A setting is a deployment-wide policy decision a person makes \u2014 what the guards require, what the caps are \u2014 and an agent that could rewrite one could turn off the checks it is subject to. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied. Reach it over HTTP or the command line."
2705
+ },
2706
+ {
2707
+ adapter: "mcp_stdio",
2708
+ operation: "patch_settings",
2709
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2710
+ },
2711
+ {
2712
+ adapter: "mcp_http",
2713
+ operation: "delete_setting",
2714
+ reason: "Settings administration. A setting is a deployment-wide policy decision a person makes \u2014 what the guards require, what the caps are \u2014 and an agent that could rewrite one could turn off the checks it is subject to. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied. Reach it over HTTP or the command line."
2715
+ },
2716
+ {
2717
+ adapter: "mcp_stdio",
2718
+ operation: "delete_setting",
2719
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2720
+ },
2721
+ {
2722
+ adapter: "mcp_http",
2723
+ operation: "remove_unrecognised_setting",
2724
+ reason: "Settings administration. A setting is a deployment-wide policy decision a person makes \u2014 what the guards require, what the caps are \u2014 and an agent that could rewrite one could turn off the checks it is subject to. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied. Reach it over HTTP or the command line."
2725
+ },
2726
+ {
2727
+ adapter: "mcp_stdio",
2728
+ operation: "remove_unrecognised_setting",
2729
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2730
+ },
2731
+ {
2732
+ adapter: "mcp_http",
2733
+ operation: "retype_to_task",
2734
+ reason: "Structural repair \u2014 rare, person-driven surgery on a board that has gone wrong, performed deliberately by someone who has looked at it rather than reached for mid-task by an agent. It runs no state transition through the guarded path, so no registered guard can reject it and \xA722's bound on waivers is satisfied. Reach it over HTTP or the command line."
2735
+ },
2736
+ {
2737
+ adapter: "mcp_stdio",
2738
+ operation: "retype_to_task",
2739
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2740
+ },
2741
+ {
2742
+ adapter: "mcp_http",
2743
+ operation: "restore_item",
2744
+ reason: "Structural repair \u2014 rare, person-driven surgery on a board that has gone wrong, performed deliberately by someone who has looked at it rather than reached for mid-task by an agent. It runs no state transition through the guarded path, so no registered guard can reject it and \xA722's bound on waivers is satisfied. Reach it over HTTP or the command line."
2745
+ },
2746
+ {
2747
+ adapter: "mcp_stdio",
2748
+ operation: "restore_item",
2749
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2750
+ },
2751
+ {
2752
+ adapter: "mcp_http",
2753
+ operation: "delete_item",
2754
+ reason: "Structural repair \u2014 rare, person-driven surgery on a board that has gone wrong, performed deliberately by someone who has looked at it rather than reached for mid-task by an agent. It runs no state transition through the guarded path, so no registered guard can reject it and \xA722's bound on waivers is satisfied. Reach it over HTTP or the command line."
2755
+ },
2756
+ {
2757
+ adapter: "mcp_stdio",
2758
+ operation: "delete_item",
2759
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2760
+ },
2761
+ {
2762
+ adapter: "mcp_http",
2763
+ operation: "record_intervention",
2764
+ reason: "Scoring and telemetry with no agent consumer \u2014 these feed dashboards and the intervention scoring loop, both read by people. An agent neither records nor reads them in the course of doing work. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied."
2765
+ },
2766
+ {
2767
+ adapter: "mcp_stdio",
2768
+ operation: "record_intervention",
2769
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2770
+ },
2771
+ {
2772
+ adapter: "mcp_http",
2773
+ operation: "get_costs",
2774
+ reason: "Scoring and telemetry with no agent consumer \u2014 these feed dashboards and the intervention scoring loop, both read by people. An agent neither records nor reads them in the course of doing work. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied."
2775
+ },
2776
+ {
2777
+ adapter: "mcp_stdio",
2778
+ operation: "get_costs",
2779
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2780
+ },
2781
+ {
2782
+ adapter: "mcp_http",
2783
+ operation: "get_activity",
2784
+ reason: "Scoring and telemetry with no agent consumer \u2014 these feed dashboards and the intervention scoring loop, both read by people. An agent neither records nor reads them in the course of doing work. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied."
2785
+ },
2786
+ {
2787
+ adapter: "mcp_stdio",
2788
+ operation: "get_activity",
2789
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2790
+ },
2791
+ {
2792
+ adapter: "mcp_http",
2793
+ operation: "get_needs_you",
2794
+ reason: "A user-interface read (\xA722 names 'the board is a user-interface read' as the archetypal waiver). This one backs a specific screen \u2014 the needs-you inbox, the Activity tab's paging, the unread marker \u2014 and its shape is chosen for that screen rather than for a session. An agent asks about its own work with get_item, my_work and progress_report. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied."
2795
+ },
2796
+ {
2797
+ adapter: "mcp_stdio",
2798
+ operation: "get_needs_you",
2799
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2800
+ },
2801
+ {
2802
+ adapter: "mcp_http",
2803
+ operation: "get_item_history",
2804
+ reason: "A user-interface read (\xA722 names 'the board is a user-interface read' as the archetypal waiver). This one backs a specific screen \u2014 the needs-you inbox, the Activity tab's paging, the unread marker \u2014 and its shape is chosen for that screen rather than for a session. An agent asks about its own work with get_item, my_work and progress_report. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied."
2805
+ },
2806
+ {
2807
+ adapter: "mcp_stdio",
2808
+ operation: "get_item_history",
2809
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2810
+ },
2811
+ {
2812
+ adapter: "mcp_http",
2813
+ operation: "mark_event_seen",
2814
+ reason: "A user-interface read (\xA722 names 'the board is a user-interface read' as the archetypal waiver). This one backs a specific screen \u2014 the needs-you inbox, the Activity tab's paging, the unread marker \u2014 and its shape is chosen for that screen rather than for a session. An agent asks about its own work with get_item, my_work and progress_report. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied."
2815
+ },
2816
+ {
2817
+ adapter: "mcp_stdio",
2818
+ operation: "mark_event_seen",
2819
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2820
+ },
2821
+ {
2822
+ adapter: "mcp_http",
2823
+ operation: "create_item",
2824
+ reason: "Deprecated in favour of the purpose-built creation tools, which state their required fields in their own schemas rather than behind a conditional refinement. Keeping a superseded tool in a list sent on every session spends context to offer agents the worse of two ways to do the same thing. It remains on HTTP and the command line for callers already written against it."
2825
+ },
2826
+ {
2827
+ adapter: "mcp_stdio",
2828
+ operation: "create_item",
2829
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, same reasoning."
2830
+ },
2831
+ {
2832
+ adapter: "mcp_http",
2833
+ operation: "loop_add",
2834
+ reason: "Folded into the single `loop` tool, which takes the verb as an `action` field. Six verbs describing one capability spend the per-session tool-list budget six times over to say `itemId` and `loopId` again; a caller reaching for any of them has already decided it is working on loops. The folded tool dispatches to this operation, so its refusals, its guard ids and its fields reach the caller unchanged, and it stays exposed on HTTP and the command line. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied."
2835
+ },
2836
+ {
2837
+ adapter: "mcp_stdio",
2838
+ operation: "loop_add",
2839
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, and the per-session tool-list cost is identical on both."
2840
+ },
2841
+ {
2842
+ adapter: "mcp_http",
2843
+ operation: "loop_get",
2844
+ reason: "Folded into the single `loop` tool, which takes the verb as an `action` field. Six verbs describing one capability spend the per-session tool-list budget six times over to say `itemId` and `loopId` again; a caller reaching for any of them has already decided it is working on loops. The folded tool dispatches to this operation, so its refusals, its guard ids and its fields reach the caller unchanged, and it stays exposed on HTTP and the command line. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied."
2845
+ },
2846
+ {
2847
+ adapter: "mcp_stdio",
2848
+ operation: "loop_get",
2849
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, and the per-session tool-list cost is identical on both."
2850
+ },
2851
+ {
2852
+ adapter: "mcp_http",
2853
+ operation: "loop_list",
2854
+ reason: "Folded into the single `loop` tool, which takes the verb as an `action` field. Six verbs describing one capability spend the per-session tool-list budget six times over to say `itemId` and `loopId` again; a caller reaching for any of them has already decided it is working on loops. The folded tool dispatches to this operation, so its refusals, its guard ids and its fields reach the caller unchanged, and it stays exposed on HTTP and the command line. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied."
2855
+ },
2856
+ {
2857
+ adapter: "mcp_stdio",
2858
+ operation: "loop_list",
2859
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, and the per-session tool-list cost is identical on both."
2860
+ },
2861
+ {
2862
+ adapter: "mcp_http",
2863
+ operation: "loop_edit",
2864
+ reason: "Folded into the single `loop` tool, which takes the verb as an `action` field. Six verbs describing one capability spend the per-session tool-list budget six times over to say `itemId` and `loopId` again; a caller reaching for any of them has already decided it is working on loops. The folded tool dispatches to this operation, so its refusals, its guard ids and its fields reach the caller unchanged, and it stays exposed on HTTP and the command line. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied."
2865
+ },
2866
+ {
2867
+ adapter: "mcp_stdio",
2868
+ operation: "loop_edit",
2869
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, and the per-session tool-list cost is identical on both."
2870
+ },
2871
+ {
2872
+ adapter: "mcp_http",
2873
+ operation: "loop_close",
2874
+ reason: "Folded into the single `loop` tool, which takes the verb as an `action` field. Six verbs describing one capability spend the per-session tool-list budget six times over to say `itemId` and `loopId` again; a caller reaching for any of them has already decided it is working on loops. The folded tool dispatches to this operation, so its refusals, its guard ids and its fields reach the caller unchanged, and it stays exposed on HTTP and the command line. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied."
2875
+ },
2876
+ {
2877
+ adapter: "mcp_stdio",
2878
+ operation: "loop_close",
2879
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, and the per-session tool-list cost is identical on both."
2880
+ },
2881
+ {
2882
+ adapter: "mcp_http",
2883
+ operation: "loop_delete",
2884
+ reason: "Folded into the single `loop` tool, which takes the verb as an `action` field. Six verbs describing one capability spend the per-session tool-list budget six times over to say `itemId` and `loopId` again; a caller reaching for any of them has already decided it is working on loops. The folded tool dispatches to this operation, so its refusals, its guard ids and its fields reach the caller unchanged, and it stays exposed on HTTP and the command line. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied."
2885
+ },
2886
+ {
2887
+ adapter: "mcp_stdio",
2888
+ operation: "loop_delete",
2889
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, and the per-session tool-list cost is identical on both."
2890
+ },
2891
+ {
2892
+ adapter: "mcp_http",
2893
+ operation: "create_project",
2894
+ reason: "Folded into the single `create_work` tool, which takes the kind as a required `type` field. The three share seventeen fields of one common shape and differ by a single parent pointer, so three near-identical schemas spend the per-session tool-list budget three times to describe one decision. This is not `create_item`'s inference returning: `type` is stated by the caller and never derived, and a type the supplied parent cannot produce is refused by name. The folded tool dispatches to this operation, so every depth check and every refusal here reaches the caller unchanged, and it stays exposed on HTTP and the command line. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied."
2895
+ },
2896
+ {
2897
+ adapter: "mcp_stdio",
2898
+ operation: "create_project",
2899
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, and the per-session tool-list cost is identical on both."
2900
+ },
2901
+ {
2902
+ adapter: "mcp_http",
2903
+ operation: "create_task",
2904
+ reason: "Folded into the single `create_work` tool, which takes the kind as a required `type` field. The three share seventeen fields of one common shape and differ by a single parent pointer, so three near-identical schemas spend the per-session tool-list budget three times to describe one decision. This is not `create_item`'s inference returning: `type` is stated by the caller and never derived, and a type the supplied parent cannot produce is refused by name. The folded tool dispatches to this operation, so every depth check and every refusal here reaches the caller unchanged, and it stays exposed on HTTP and the command line. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied."
2905
+ },
2906
+ {
2907
+ adapter: "mcp_stdio",
2908
+ operation: "create_task",
2909
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, and the per-session tool-list cost is identical on both."
2910
+ },
2911
+ {
2912
+ adapter: "mcp_http",
2913
+ operation: "create_subtask",
2914
+ reason: "Folded into the single `create_work` tool, which takes the kind as a required `type` field. The three share seventeen fields of one common shape and differ by a single parent pointer, so three near-identical schemas spend the per-session tool-list budget three times to describe one decision. This is not `create_item`'s inference returning: `type` is stated by the caller and never derived, and a type the supplied parent cannot produce is refused by name. The folded tool dispatches to this operation, so every depth check and every refusal here reaches the caller unchanged, and it stays exposed on HTTP and the command line. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied."
2915
+ },
2916
+ {
2917
+ adapter: "mcp_stdio",
2918
+ operation: "create_subtask",
2919
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, and the per-session tool-list cost is identical on both."
2920
+ },
2921
+ {
2922
+ adapter: "mcp_http",
2923
+ operation: "progress_report",
2924
+ reason: "A fixed rendering of rows `my_work` already returns for the same input, and a rendering is the one thing a caller can reproduce for itself from data it holds. Every tool costs the per-session tool-list budget whether or not it is called, and this one buys a report shape rather than a capability: no state is reachable through it that is not reachable through `my_work`. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied. Reach it over HTTP or the command line, which is where a scheduled report is generated from anyway."
2925
+ },
2926
+ {
2927
+ adapter: "mcp_stdio",
2928
+ operation: "progress_report",
2929
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, and the per-session tool-list cost is identical on both."
2930
+ },
2931
+ {
2932
+ adapter: "mcp_http",
2933
+ operation: "poll",
2934
+ reason: "Its caller is a long-poll loop, not a reasoning agent: it blocks until work appears or a timeout elapses, which is a shape a session cannot use \u2014 an agent holding a turn open waiting for a change is an agent doing nothing with a context window. The same class as the hook and scheduler operations already waived here, and for the same reason: the consumer is a process that reaches the service over its own HTTP route. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied. Reach it over HTTP."
2935
+ },
2936
+ {
2937
+ adapter: "mcp_stdio",
2938
+ operation: "poll",
2939
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, and the per-session tool-list cost is identical on both."
2940
+ },
2941
+ {
2942
+ adapter: "mcp_http",
2943
+ operation: "service_info",
2944
+ reason: "Most of what it returns is a catalogue of every operation, which every MCP client is already sent on connect \u2014 so on this surface, and only on this surface, the bulk of the answer is a duplicate of something the caller already holds. What it carried that nothing else did \u2014 the build, the limits and the settings revision \u2014 now lives on `describe_tool`, which is authenticated and is the one remaining MCP read whose subject is the contract rather than the data; call it with no `tool` to get them. That home landed before this waiver, deliberately, so the information was never unreachable in between. It runs no state transition, so no registered guard can reject it and \xA722's bound on waivers is satisfied. Reach the catalogue itself over the command line."
2945
+ },
2946
+ {
2947
+ adapter: "mcp_stdio",
2948
+ operation: "service_info",
2949
+ reason: "Same as mcp_http \u2014 one MCP surface, two transports, and the per-session tool-list cost is identical on both."
2950
+ }
2951
+ ]);
2952
+ function isWaived(adapter, operation) {
2953
+ return ADAPTER_WAIVERS.some(
2954
+ (waiver) => waiver.adapter === adapter && waiver.operation === operation
2955
+ );
2956
+ }
2957
+ function waiverFor(adapter, operation) {
2958
+ return ADAPTER_WAIVERS.find(
2959
+ (waiver) => waiver.adapter === adapter && waiver.operation === operation
2960
+ );
2961
+ }
2962
+ function exposedOperations(adapter, operations) {
2963
+ return operations.filter((operation) => !isWaived(adapter, operation.name));
2964
+ }
2965
+
2966
+ // src/lib/mcp/result.ts
2967
+ function bigintSafe(value) {
2968
+ if (typeof value === "bigint") return value.toString();
2969
+ if (value instanceof Date) return value;
2970
+ if (Array.isArray(value)) return value.map(bigintSafe);
2971
+ if (value !== null && typeof value === "object") {
2972
+ return Object.fromEntries(
2973
+ Object.entries(value).map(([key, entry]) => [key, bigintSafe(entry)])
2974
+ );
2975
+ }
2976
+ return value;
2977
+ }
2978
+ function toolSuccess(value) {
2979
+ const safe = bigintSafe(value);
2980
+ const text = safe === void 0 ? "null" : JSON.stringify(safe);
2981
+ const structuredContent = isPlainRecord(safe) ? safe : { result: safe ?? null };
2982
+ return {
2983
+ content: [{ type: "text", text }],
2984
+ structuredContent
2985
+ };
2986
+ }
2987
+ function toolRejection(error) {
2988
+ const serviceError = toServiceError(error);
2989
+ const rejection = {
2990
+ ...serviceError.toRejection(),
2991
+ message: serviceError.message
2992
+ };
2993
+ return {
2994
+ content: [{ type: "text", text: JSON.stringify(rejection) }],
2995
+ structuredContent: { ...rejection, fields: [...rejection.fields] },
2996
+ isError: true
2997
+ };
2998
+ }
2999
+ function isPlainRecord(value) {
3000
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3001
+ }
3002
+
3003
+ // src/lib/mcp/tools.ts
3004
+ function advertisedSchema(schema) {
3005
+ const permissive = schema.catch((ctx) => ctx.input);
3006
+ const shape = shapeOf(schema);
3007
+ if (shape !== void 0) {
3008
+ Object.defineProperty(permissive, "shape", {
3009
+ get: () => shape,
3010
+ enumerable: false,
3011
+ configurable: true
3012
+ });
3013
+ }
3014
+ return permissive;
3015
+ }
3016
+ function shapeOf(schema) {
3017
+ const direct = schema.shape;
3018
+ if (direct !== void 0) return direct;
3019
+ const inner = schema._def?.schema;
3020
+ return inner ? shapeOf(inner) : void 0;
3021
+ }
3022
+ function toolsFromOperations(operations) {
3023
+ return operations.map((operation) => ({
3024
+ name: operation.name,
3025
+ description: operation.summary,
3026
+ inputSchema: operation.input,
3027
+ readOnly: operation.kind === "read"
3028
+ }));
3029
+ }
3030
+
3031
+ // src/lib/mcp/server.ts
3032
+ var MCP_SERVER_INFO = { name: "agent-standup", version: "0.1.0" };
3033
+ function createMcpServer({
3034
+ call,
3035
+ transport,
3036
+ adapter,
3037
+ identity,
3038
+ operations = exposedOperations(adapter, listOperations()),
3039
+ serverInfo = MCP_SERVER_INFO
3040
+ }) {
3041
+ const server = new McpServer({ name: serverInfo.name, version: serverInfo.version });
3042
+ for (const tool of toolsFromOperations(operations)) {
3043
+ server.registerTool(
3044
+ tool.name,
3045
+ {
3046
+ description: tool.description,
3047
+ // The operation's own schema, wrapped so the SDK advertises it but
3048
+ // never rejects with it — see `advertisedSchema`'s comment. The
3049
+ // rejection has to come from the service layer, or MCP's refusal
3050
+ // of a bad input would not match the web API's for the same call.
3051
+ inputSchema: advertisedSchema(tool.inputSchema),
3052
+ annotations: { readOnlyHint: tool.readOnly }
3053
+ },
3054
+ async (args) => callTool(call, transport, tool.name, args, identity)
3055
+ );
3056
+ }
3057
+ return server;
3058
+ }
3059
+ async function callTool(call, transport, name, args, identity = {}, adapter, served) {
3060
+ const requestId = newRequestId();
3061
+ if (adapter !== void 0) {
3062
+ const withheld = served !== void 0 && served.has(name) ? void 0 : withheldToolRejection(adapter, name);
3063
+ if (withheld !== void 0) {
3064
+ log.debug("A withheld tool was called by name.", {
3065
+ requestId,
3066
+ transport,
3067
+ tool: name,
3068
+ code: "not_found"
3069
+ });
3070
+ return withheld;
3071
+ }
3072
+ }
3073
+ try {
3074
+ return toolSuccess(
3075
+ await call(name, args, {
3076
+ // Spread conditionally rather than assigned unconditionally: an
3077
+ // explicit `sessionId: undefined` is a different value from an
3078
+ // absent key to anything reading the object with `in` or
3079
+ // `Object.keys`, and the whole point of an unidentified call is
3080
+ // that it carries no claim about who made it.
3081
+ caller: {
3082
+ transport,
3083
+ requestId,
3084
+ ...identity.sessionId === void 0 ? {} : { sessionId: identity.sessionId },
3085
+ ...identity.actor === void 0 ? {} : { actor: identity.actor },
3086
+ ...identity.machine === void 0 ? {} : { machine: identity.machine }
3087
+ }
3088
+ })
3089
+ );
3090
+ } catch (error) {
3091
+ const serviceError = toServiceError(error);
3092
+ if (serviceError.fault === "server") {
3093
+ log.error("MCP tool call failed unexpectedly.", {
3094
+ requestId,
3095
+ transport,
3096
+ tool: name,
3097
+ ...faultContext(serviceError),
3098
+ err: serviceError
3099
+ });
3100
+ } else {
3101
+ log.debug("MCP tool call refused.", {
3102
+ requestId,
3103
+ transport,
3104
+ tool: name,
3105
+ code: serviceError.code,
3106
+ ...faultContext(serviceError),
3107
+ ...serviceError.guard === void 0 ? {} : { guard: serviceError.guard }
3108
+ });
3109
+ }
3110
+ return toolRejection(serviceError);
3111
+ }
3112
+ }
3113
+ function withheldToolRejection(adapter, name) {
3114
+ const waiver = waiverFor(adapter, name);
3115
+ if (waiver === void 0) return void 0;
3116
+ return toolRejection(
3117
+ new NotFoundError(
3118
+ `The tool ${name} is not served by this adapter. It was withheld deliberately, not dropped, and no arguments will make this call succeed \u2014 do not retry it. Why: ${waiver.reason}`,
3119
+ // No `details`: `Rejection` — the shape every adapter rebuilds from a
3120
+ // wire body — carries only what conformance compares, and widening it
3121
+ // for one adapter would make two adapters disagree about an identical
3122
+ // refusal. Everything a caller needs is in the message.
3123
+ {}
3124
+ )
3125
+ );
3126
+ }
3127
+
3128
+ // src/lib/mcp/stdio.ts
3129
+ var MCP_STDIO_TRANSPORT = "mcp-stdio";
3130
+ function serveMcpStdio(call, options = {}) {
3131
+ const input = options.input ?? process.stdin;
3132
+ const server = createMcpServer({ call, transport: MCP_STDIO_TRANSPORT, adapter: "mcp_stdio" });
3133
+ const transport = new StdioServerTransport(input, options.output ?? process.stdout);
3134
+ return new Promise((resolve, reject) => {
3135
+ transport.onclose = () => resolve();
3136
+ input.once("end", () => {
3137
+ server.close().catch(reject);
3138
+ });
3139
+ server.connect(transport).catch(reject);
3140
+ });
3141
+ }
3142
+
3143
+ // src/lib/cli/mcp.ts
3144
+ async function runMcpStdio(options = {}) {
3145
+ const resolution = resolveConfig({
3146
+ flags: { direct: true },
3147
+ env: options.env,
3148
+ file: options.file
3149
+ });
3150
+ if (!resolution.ok) {
3151
+ return { envelope: resolution.envelope, exitCode: resolution.exitCode };
3152
+ }
3153
+ const loadService = options.loadService ?? (async () => (await import("../live-R6WH7ER7.js")).service);
3154
+ const service = await loadService();
3155
+ await serveMcpStdio((name, input, callOptions) => service.call(name, input, callOptions), {
3156
+ input: options.input,
3157
+ output: options.output
3158
+ });
3159
+ return { envelope: ok({ transport: "mcp-stdio" }), exitCode: EXIT.OK };
3160
+ }
3161
+
3162
+ // src/lib/cli/run.ts
3163
+ async function runCommand(argv, binding) {
3164
+ const parsed = parseArgs(argv);
3165
+ if (!parsed.ok) return refuse(parsed.envelope);
3166
+ const found = lookupCommand(parsed.parsed.words);
3167
+ if (!found.ok) return refuse(found.envelope);
3168
+ const built = found.match.command.buildInput(found.match.rest, parsed.parsed.flags);
3169
+ if (!built.ok) return refuse(built.envelope);
3170
+ const result = await binding.invoke(found.match.command.operation, built.input);
3171
+ if (result.ok) {
3172
+ const envelope2 = ok(result.data);
3173
+ return { envelope: envelope2, exitCode: exitCodeFor(envelope2), binding: binding.name };
3174
+ }
3175
+ const envelope = rejected(result.rejection, result.message);
3176
+ return { envelope, exitCode: exitCodeFor(envelope), binding: binding.name };
3177
+ }
3178
+ function refuse(envelope) {
3179
+ return { envelope, exitCode: exitCodeFor(envelope) };
3180
+ }
3181
+ async function runCliHook(rest, options) {
3182
+ const verb = rest[0];
3183
+ if (!isHookVerb(verb)) {
3184
+ return refuse(
3185
+ malformed(
3186
+ `standup hook needs one of: ${HOOK_VERBS.join(", ")}${verb === void 0 ? "" : ` (got "${verb}")`}`,
3187
+ ["verb"]
3188
+ )
3189
+ );
3190
+ }
3191
+ const edges = options.hook;
3192
+ if (edges === void 0) {
3193
+ return refuse(malformed("standup hook is not available on this entry point", ["hook"]));
3194
+ }
3195
+ const outcome = await runHookCommand({
3196
+ verb,
3197
+ spool: edges.spool,
3198
+ now: edges.now,
3199
+ ...edges.stdin === void 0 ? {} : { stdin: edges.stdin },
3200
+ ...edges.hook === void 0 ? {} : { hook: edges.hook },
3201
+ ...edges.send === void 0 ? {} : { send: edges.send },
3202
+ ...edges.batchSize === void 0 ? {} : { batchSize: edges.batchSize },
3203
+ ...edges.maxRecords === void 0 ? {} : { maxRecords: edges.maxRecords }
3204
+ });
3205
+ if (outcome.kind === "hook-response") {
3206
+ return {
3207
+ envelope: ok({ transport: "hook" }),
3208
+ exitCode: outcome.response.exitCode,
3209
+ hookResponse: outcome.response
3210
+ };
3211
+ }
3212
+ return { envelope: outcome.envelope, exitCode: outcome.exitCode };
3213
+ }
3214
+ async function runCli(argv, options = {}) {
3215
+ const parsed = parseArgs(argv);
3216
+ if (!parsed.ok) return refuse(parsed.envelope);
3217
+ const { words, flags } = parsed.parsed;
3218
+ const help = booleanFlag(flags, "help");
3219
+ if (!help.ok) return refuse(help.envelope);
3220
+ if (help.value || words.length === 0) {
3221
+ return { envelope: ok(helpText()), exitCode: EXIT.OK };
3222
+ }
3223
+ const direct = booleanFlag(flags, "direct");
3224
+ if (!direct.ok) return refuse(direct.envelope);
3225
+ const identity = identityFlags(flags);
3226
+ if (!identity.ok) return refuse(identity.envelope);
3227
+ const resolution = resolveConfig({
3228
+ flags: { ...identity, direct: direct.value },
3229
+ env: options.env,
3230
+ file: options.file
3231
+ });
3232
+ if (words[0] === "doctor") {
3233
+ const report = doctorReport({
3234
+ flags: { ...identity, direct: direct.value },
3235
+ env: options.env,
3236
+ file: options.file
3237
+ });
3238
+ return { envelope: ok(report), exitCode: report.configured ? EXIT.OK : EXIT.UNCONFIGURED };
3239
+ }
3240
+ if (words[0] === "init") {
3241
+ return runInitCommand({ flags, env: options.env, file: options.file });
3242
+ }
3243
+ if (words[0] === "mcp") {
3244
+ return runMcpStdio({ env: options.env, file: options.file });
3245
+ }
3246
+ if (words[0] === "hook") {
3247
+ return await runCliHook(words.slice(1), options);
3248
+ }
3249
+ if (!resolution.ok) {
3250
+ return { envelope: resolution.envelope, exitCode: resolution.exitCode };
3251
+ }
3252
+ const binding = await buildBinding(resolution.config, options);
3253
+ return runCommand(argv, binding);
3254
+ }
3255
+ async function buildBinding(config, options) {
3256
+ const identity = {
3257
+ ...config.sessionId === void 0 ? {} : { sessionId: config.sessionId },
3258
+ ...config.actor === void 0 ? {} : { actor: config.actor }
3259
+ };
3260
+ if (config.binding === "http") {
3261
+ return createHttpBinding({
3262
+ // Non-null by construction: `resolveConfig` returns `http` only when
3263
+ // it resolved a URL, and the two are set in the same branch.
3264
+ baseUrl: config.standupUrl,
3265
+ ...options.fetch === void 0 ? {} : { fetch: options.fetch },
3266
+ ...identity,
3267
+ ...config.token === void 0 ? {} : { token: config.token }
3268
+ });
3269
+ }
3270
+ const loadService = options.loadService ?? (async () => (await import("../live-R6WH7ER7.js")).service);
3271
+ return createDirectBinding({ service: await loadService(), ...identity });
3272
+ }
3273
+ function helpText() {
3274
+ return {
3275
+ usage: "standup <noun> <verb> [--json] [--direct] [--as <person>] [--session <id>]",
3276
+ nouns: nouns(),
3277
+ commands: COMMANDS.map((command) => `${command.noun} ${command.verb} \u2014 ${command.summary}`)
3278
+ };
3279
+ }
3280
+
3281
+ // src/lib/cli/render.ts
3282
+ function render(outcome, streams, json) {
3283
+ if (outcome.hookResponse !== void 0) {
3284
+ if (outcome.hookResponse.stdout !== "") streams.out(outcome.hookResponse.stdout);
3285
+ if (outcome.hookResponse.stderr !== "") streams.err(outcome.hookResponse.stderr);
3286
+ return;
3287
+ }
3288
+ if (json) {
3289
+ streams.out(`${JSON.stringify(outcome.envelope)}
3290
+ `);
3291
+ return;
3292
+ }
3293
+ streams.err(`${humanText(outcome.envelope)}
3294
+ `);
3295
+ }
3296
+ function humanText(envelope) {
3297
+ if (envelope.ok) {
3298
+ return typeof envelope.data === "string" ? envelope.data : JSON.stringify(envelope.data, null, 2);
3299
+ }
3300
+ const { code, message, fields, guard } = envelope.error;
3301
+ const parts = [`${code}: ${message}`];
3302
+ if (guard !== void 0) parts.push(` rule: ${guard}`);
3303
+ if (fields.length > 0) parts.push(` fields: ${fields.join(", ")}`);
3304
+ return parts.join("\n");
3305
+ }
3306
+
3307
+ // src/lib/cli/main.ts
3308
+ async function main(argv, { streams, ...options }) {
3309
+ const parsed = parseArgs(argv);
3310
+ const jsonFlag2 = parsed.ok ? booleanFlag(parsed.parsed.flags, "json") : void 0;
3311
+ const json = jsonFlag2?.ok === true && jsonFlag2.value;
3312
+ let outcome;
3313
+ try {
3314
+ outcome = await runCli(argv, options);
3315
+ } catch (cause) {
3316
+ log.fatal("The command failed outside the binding boundary.", {
3317
+ transport: "cli",
3318
+ err: cause
3319
+ });
3320
+ const envelope = {
3321
+ ok: false,
3322
+ error: {
3323
+ code: "internal",
3324
+ message: `The command failed unexpectedly (${cause instanceof Error ? cause.name : "unknown error"}).`,
3325
+ fields: []
3326
+ }
3327
+ };
3328
+ render({ envelope, exitCode: EXIT.FAILURE }, streams, json);
3329
+ return exitCodeFor(envelope);
3330
+ }
3331
+ render(outcome, streams, json);
3332
+ return outcome.exitCode;
3333
+ }
3334
+
3335
+ // src/bin/standup.ts
3336
+ var file = readConfigFile();
3337
+ var baseUrl = process.env.STANDUP_URL?.trim();
3338
+ var exitCode = await main(process.argv.slice(2), {
3339
+ env: process.env,
3340
+ file,
3341
+ hook: {
3342
+ spool: fileSpool(spoolPath(process.env)),
3343
+ now: Date.now(),
3344
+ ...baseUrl === void 0 || baseUrl === "" ? {} : { send: createHttpFlush({ baseUrl, fetch: globalThis.fetch }) }
3345
+ },
3346
+ streams: {
3347
+ out: (text) => process.stdout.write(text),
3348
+ err: (text) => process.stderr.write(text)
3349
+ }
3350
+ });
3351
+ process.exitCode = exitCode;