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.
- package/LICENSE +21 -0
- package/README.md +322 -0
- package/dist/bin/standup-hook.js +296 -0
- package/dist/bin/standup.js +3351 -0
- package/dist/chunk-4TIZQTUZ.js +19195 -0
- package/dist/chunk-N7G677FC.js +1042 -0
- package/dist/chunk-VBXNDGOD.js +203 -0
- package/dist/hook-scripts/http.js +1265 -0
- package/dist/live-R6WH7ER7.js +326 -0
- package/dist/plugin/.claude-plugin/plugin.json +9 -0
- package/dist/plugin/.mcp.json +8 -0
- package/dist/plugin/hooks/hooks.json +35 -0
- package/dist/plugin/skills/setup-agent-standup/SKILL.md +53 -0
- package/dist/run-init-VVQPJOTB.js +407 -0
- package/package.json +88 -0
|
@@ -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;
|