@danypops/papyrus 0.44.9 → 0.44.11
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/package.json +3 -2
- package/src/artifact/artifact.ts +23 -0
- package/src/cli/artifact-command.ts +210 -0
- package/src/cli/discuss-command.ts +256 -0
- package/src/cli/docs-command.ts +192 -0
- package/src/cli/gates-command.ts +46 -0
- package/src/cli/graph-command.ts +171 -0
- package/src/cli/graph-projection-command.ts +79 -0
- package/src/cli/log-command.ts +101 -0
- package/src/cli/migration-command.ts +40 -0
- package/src/cli/note-command.ts +186 -0
- package/src/cli/playbooks-command.ts +313 -0
- package/src/cli/rules-command.ts +220 -0
- package/src/cli/session-identity-command.ts +58 -0
- package/src/cli/shared.ts +5 -0
- package/src/cli/stricli-run.ts +30 -0
- package/src/cli/task-command.ts +892 -0
- package/src/cli.ts +30 -1912
- package/src/handlers/discuss.ts +22 -20
- package/src/handlers/docs.ts +4 -30
- package/src/handlers/notes.ts +2 -29
- package/src/handlers/playbooks.ts +27 -92
- package/src/handlers/rules.ts +4 -30
- package/src/handlers/shared.ts +92 -1
- package/src/handlers/tasks.ts +31 -118
- package/src/modules/discuss.ts +5 -26
- package/src/modules/docs.ts +6 -23
- package/src/modules/graph-projection.ts +1 -8
- package/src/modules/logs.ts +1 -22
- package/src/modules/notes.ts +1 -22
- package/src/modules/operation-input.ts +42 -0
- package/src/modules/playbooks.ts +6 -23
- package/src/modules/rules.ts +6 -23
- package/src/modules/session-identity.ts +1 -15
- package/src/modules/tasks.ts +5 -33
- package/src/service.ts +1 -28
package/src/cli.ts
CHANGED
|
@@ -3,15 +3,25 @@ import { execFileSync } from "node:child_process";
|
|
|
3
3
|
import { copyFileSync, existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { createNodeServiceInstallDeps, generateSystemdUnit, installUserService, type ServiceSpec } from "@danypops/vehicle-server/service";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { runArtifactCli } from "./cli/artifact-command.ts";
|
|
7
|
+
import { runDiscussCli } from "./cli/discuss-command.ts";
|
|
8
|
+
import { runDocsCli } from "./cli/docs-command.ts";
|
|
9
|
+
import { runGatesCli } from "./cli/gates-command.ts";
|
|
10
|
+
import { runGraphCli } from "./cli/graph-command.ts";
|
|
11
|
+
import { runGraphProjectionCli } from "./cli/graph-projection-command.ts";
|
|
12
|
+
import { runLogCli } from "./cli/log-command.ts";
|
|
13
|
+
import { runMigrationCli } from "./cli/migration-command.ts";
|
|
14
|
+
import { runNoteCli } from "./cli/note-command.ts";
|
|
15
|
+
import { runPlaybooksCli } from "./cli/playbooks-command.ts";
|
|
16
|
+
import { runRulesCli } from "./cli/rules-command.ts";
|
|
17
|
+
import { runSessionIdentityCli } from "./cli/session-identity-command.ts";
|
|
18
|
+
import { runTaskCli } from "./cli/task-command.ts";
|
|
19
|
+
import { connectPapyrusClient } from "./client.ts";
|
|
20
|
+
import { DAEMON_UNIT_NAME, dbPath } from "./constants.ts";
|
|
8
21
|
import { serveMain } from "./daemon/daemon.ts";
|
|
9
22
|
import { daemonStateDir, vehicleHandlePath } from "./daemon/daemon-state.ts";
|
|
10
23
|
import { openDb } from "./db.ts";
|
|
11
|
-
import type { GateResult } from "./domain/gate.ts";
|
|
12
24
|
import { applyIdMigration, type IdMigrationPlan, mirrorDatabase, planIdMigration, verifyIdMigration } from "./id-migration.ts";
|
|
13
|
-
import type { TaskExecutionPlan } from "./task/task-execution.ts";
|
|
14
|
-
import type { TaskBlockage, TaskCompletion } from "./task/task-service.ts";
|
|
15
25
|
import { VERSION } from "./version.ts";
|
|
16
26
|
|
|
17
27
|
export interface SystemdUnitOptions {
|
|
@@ -194,80 +204,7 @@ function usage(): never {
|
|
|
194
204
|
process.exit(2);
|
|
195
205
|
}
|
|
196
206
|
|
|
197
|
-
|
|
198
|
-
type MigrationResult = { from: number; to: number; applied: string[] };
|
|
199
|
-
type CliArtifact = { id: string; alias: string; title: string; status: string; body?: string };
|
|
200
|
-
type CliTaskLease = {
|
|
201
|
-
taskId: string;
|
|
202
|
-
owner: string;
|
|
203
|
-
token: string;
|
|
204
|
-
claimedAt: string;
|
|
205
|
-
leaseExpiresAt: string;
|
|
206
|
-
heartbeatAt?: string;
|
|
207
|
-
note?: string;
|
|
208
|
-
};
|
|
209
|
-
type CliCompletion = Omit<TaskCompletion, "artifact" | "blocked"> & {
|
|
210
|
-
artifact: CliArtifact;
|
|
211
|
-
blocked: Array<Omit<TaskBlockage, "artifact"> & { artifact: CliArtifact }>;
|
|
212
|
-
gates: GateResult[];
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
function parseJsonObjectFlag(value: string | undefined, flag: string): Record<string, unknown> {
|
|
216
|
-
if (value === undefined) throw new Error(`${flag} requires a value`);
|
|
217
|
-
const parsed = JSON.parse(value) as unknown;
|
|
218
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(`${flag} must be a JSON object`);
|
|
219
|
-
return parsed as Record<string, unknown>;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function parseJsonStringArrayFlag(value: string | undefined, flag: string): string[] {
|
|
223
|
-
if (value === undefined) throw new Error(`${flag} requires a value`);
|
|
224
|
-
const parsed = JSON.parse(value) as unknown;
|
|
225
|
-
if (!Array.isArray(parsed) || parsed.some((entry) => typeof entry !== "string")) throw new Error(`${flag} must be a JSON string array`);
|
|
226
|
-
return parsed as string[];
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* No shape assertion here -- playbooks --arguments-json is genuinely polymorphic (an array on
|
|
231
|
-
* create, a {name: value} map on invoke, sharing one flag-parsing pass in runPlaybooksCli), and
|
|
232
|
-
* --steps-json accepts a mix of plain prose strings and structured step objects (doc/rule/call/
|
|
233
|
-
* task) that a single string-array assertion would wrongly reject. The service validates the
|
|
234
|
-
* real shape for whichever operation actually receives it.
|
|
235
|
-
*/
|
|
236
|
-
function parseJsonAnyFlag(value: string | undefined, flag: string): unknown {
|
|
237
|
-
if (value === undefined) throw new Error(`${flag} requires a value`);
|
|
238
|
-
return JSON.parse(value) as unknown;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function artifactLabel(artifact: CliArtifact): string {
|
|
242
|
-
return `${artifact.alias} ${artifact.title}`;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function planText(plan: TaskExecutionPlan): string {
|
|
246
|
-
const byId = new Map(plan.nodes.map((node) => [node.id, node]));
|
|
247
|
-
const lines = ["Execution order:"];
|
|
248
|
-
plan.layers.forEach((layer, index) => {
|
|
249
|
-
lines.push(` Layer ${index + 1}:`);
|
|
250
|
-
for (const id of layer) {
|
|
251
|
-
const node = byId.get(id);
|
|
252
|
-
lines.push(node ? ` [${node.state}] ${node.id} ${node.title}` : ` [unknown] ${id}`);
|
|
253
|
-
}
|
|
254
|
-
});
|
|
255
|
-
if (plan.layers.length === 0) lines.push(" (no tasks)");
|
|
256
|
-
if (plan.cycleIds.length > 0) lines.push(` Invalid cycle: ${plan.cycleIds.join(", ")}`);
|
|
257
|
-
return lines.join("\n");
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
export async function runMigrationCli(args: string[], client: TaskCliClient): Promise<string> {
|
|
261
|
-
const json = args.includes("--json");
|
|
262
|
-
const positional = args.filter((arg) => arg !== "--json");
|
|
263
|
-
if (positional.length !== 1 || positional[0] !== "schema") {
|
|
264
|
-
throw new Error("migrate requires exactly `schema`");
|
|
265
|
-
}
|
|
266
|
-
const result = await client.call<Record<string, never>, MigrationResult>("system.migrate", {});
|
|
267
|
-
if (json) return JSON.stringify(result);
|
|
268
|
-
if (result.applied.length === 0) return `Schema already current at version ${result.to}.`;
|
|
269
|
-
return `Migrated schema ${result.from} → ${result.to}: ${result.applied.join(", ")}`;
|
|
270
|
-
}
|
|
207
|
+
export { runMigrationCli };
|
|
271
208
|
|
|
272
209
|
function readIdMap(sidecarPath: string): IdMigrationPlan {
|
|
273
210
|
const raw = JSON.parse(readFileSync(sidecarPath, "utf8")) as { idMap: Record<string, string> };
|
|
@@ -390,1839 +327,20 @@ export function runIdMigrationCli(args: string[]): string {
|
|
|
390
327
|
throw new Error("migrate-ids requires one of: mirror, validate, promote");
|
|
391
328
|
}
|
|
392
329
|
|
|
393
|
-
export
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
"--direction": "direction",
|
|
408
|
-
};
|
|
409
|
-
if (argument in flags) {
|
|
410
|
-
const value = args[++index];
|
|
411
|
-
if (!value) throw new Error(`${argument} requires a value`);
|
|
412
|
-
input[flags[argument]!] = value;
|
|
413
|
-
continue;
|
|
414
|
-
}
|
|
415
|
-
if (argument === "--limit" || argument === "--cursor") {
|
|
416
|
-
const value = args[++index];
|
|
417
|
-
if (!value || Number.isNaN(Number(value))) throw new Error(`${argument} requires a numeric value`);
|
|
418
|
-
input[argument.slice(2)] = Number(value);
|
|
419
|
-
continue;
|
|
420
|
-
}
|
|
421
|
-
if (argument === "--depth") {
|
|
422
|
-
const value = args[++index];
|
|
423
|
-
if (!value || Number.isNaN(Number(value))) throw new Error("--depth requires a numeric value");
|
|
424
|
-
depth = Number(value);
|
|
425
|
-
continue;
|
|
426
|
-
}
|
|
427
|
-
if (argument === "--max-nodes") {
|
|
428
|
-
const value = args[++index];
|
|
429
|
-
if (!value || Number.isNaN(Number(value))) throw new Error("--max-nodes requires a numeric value");
|
|
430
|
-
maxNodes = Number(value);
|
|
431
|
-
continue;
|
|
432
|
-
}
|
|
433
|
-
if (argument.startsWith("--")) throw new Error(`unknown graph option ${argument}`);
|
|
434
|
-
positional.push(argument);
|
|
435
|
-
}
|
|
436
|
-
const [action, first, second, third] = positional;
|
|
437
|
-
if (action === "link") {
|
|
438
|
-
if (positional.length !== 4) throw new Error("graph link requires <from> <relation> <to>");
|
|
439
|
-
const result = await client.call<Record<string, unknown>, { ok: boolean }>("graph.link", { from: first, relation: second, to: third });
|
|
440
|
-
return json ? JSON.stringify(result) : `Linked ${first} --${second}--> ${third}`;
|
|
441
|
-
}
|
|
442
|
-
if (action === "unlink") {
|
|
443
|
-
if (positional.length !== 4) throw new Error("graph unlink requires <from> <relation> <to>");
|
|
444
|
-
const result = await client.call<Record<string, unknown>, { removed: boolean }>("graph.unlink", {
|
|
445
|
-
from: first,
|
|
446
|
-
relation: second,
|
|
447
|
-
to: third,
|
|
448
|
-
});
|
|
449
|
-
if (json) return JSON.stringify(result);
|
|
450
|
-
return result.removed ? `Unlinked ${first} --${second}--> ${third}` : `No such relationship: ${first} --${second}--> ${third}`;
|
|
451
|
-
}
|
|
452
|
-
if (action === "tree") {
|
|
453
|
-
if (positional.length !== 2) throw new Error("graph tree requires exactly one artifact id");
|
|
454
|
-
const artifact = await client.call<
|
|
455
|
-
Record<string, unknown>,
|
|
456
|
-
CliArtifact & { edges?: Array<{ from: string; relation: string; to: string }> }
|
|
457
|
-
>("graph.tree", { id: first, depth, max_nodes: maxNodes });
|
|
458
|
-
if (json) return JSON.stringify(artifact);
|
|
459
|
-
const edges = artifact.edges ?? [];
|
|
460
|
-
return edges.length === 0
|
|
461
|
-
? `${artifactLabel(artifact)} — no edges`
|
|
462
|
-
: `${artifactLabel(artifact)}\n${edges.map((edge) => ` ${edge.from} --${edge.relation}--> ${edge.to}`).join("\n")}`;
|
|
463
|
-
}
|
|
464
|
-
if (action === "status") {
|
|
465
|
-
if (positional.length !== 3) throw new Error("graph status requires <id> <status>");
|
|
466
|
-
const artifact = await client.call<Record<string, unknown>, CliArtifact>("graph.status", { id: first, status: second });
|
|
467
|
-
return json ? JSON.stringify(artifact) : `Updated ${artifact.id} → [${artifact.status}]`;
|
|
468
|
-
}
|
|
469
|
-
if (action !== "history") throw new Error("graph action must be link, unlink, tree, status, or history");
|
|
470
|
-
const page = await client.call<Record<string, unknown>, import("./artifact/artifact-event.ts").ArtifactEventPage>("graph.history", input);
|
|
471
|
-
if (json) return JSON.stringify(page);
|
|
472
|
-
if (page.events.length === 0) return "No recorded events.";
|
|
473
|
-
return page.events
|
|
474
|
-
.map((event) => {
|
|
475
|
-
const transition = event.fromStatus || event.toStatus ? ` ${event.fromStatus ?? "\u2205"} \u2192 ${event.toStatus ?? "\u2205"}` : "";
|
|
476
|
-
const relation = event.relation ? ` ${event.relation} \u2192 ${event.relatedId}` : "";
|
|
477
|
-
return `${event.occurredAt} ${event.artifactId} ${event.type}${transition}${relation} \u00b7 ${event.actor}/${event.source}${event.sessionId ? ` \u00b7 ${event.sessionId}` : ""}`;
|
|
478
|
-
})
|
|
479
|
-
.join("\n");
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
export async function runDocsCli(args: string[], client: TaskCliClient): Promise<string> {
|
|
483
|
-
const json = args.includes("--json");
|
|
484
|
-
const positional: string[] = [];
|
|
485
|
-
let title: string | undefined;
|
|
486
|
-
let body: string | undefined;
|
|
487
|
-
let subtype: string | undefined;
|
|
488
|
-
let labels: string[] | undefined;
|
|
489
|
-
let extra: Record<string, unknown> | undefined;
|
|
490
|
-
let templateId: string | undefined;
|
|
491
|
-
let status: string | undefined;
|
|
492
|
-
let text: string | undefined;
|
|
493
|
-
let limit: number | undefined;
|
|
494
|
-
let projectRoot: string | undefined;
|
|
495
|
-
for (let index = 0; index < args.length; index++) {
|
|
496
|
-
const argument = args[index]!;
|
|
497
|
-
if (argument === "--json") continue;
|
|
498
|
-
if (argument === "--title") {
|
|
499
|
-
title = args[++index];
|
|
500
|
-
if (title === undefined) throw new Error("--title requires a value");
|
|
501
|
-
continue;
|
|
502
|
-
}
|
|
503
|
-
if (argument === "--body") {
|
|
504
|
-
body = args[++index];
|
|
505
|
-
if (body === undefined) throw new Error("--body requires a value");
|
|
506
|
-
continue;
|
|
507
|
-
}
|
|
508
|
-
if (argument === "--subtype") {
|
|
509
|
-
subtype = args[++index];
|
|
510
|
-
if (!subtype) throw new Error("--subtype requires a value");
|
|
511
|
-
continue;
|
|
512
|
-
}
|
|
513
|
-
if (argument === "--labels-json") {
|
|
514
|
-
labels = parseJsonStringArrayFlag(args[++index], "--labels-json");
|
|
515
|
-
continue;
|
|
516
|
-
}
|
|
517
|
-
if (argument === "--extra-json") {
|
|
518
|
-
extra = parseJsonObjectFlag(args[++index], "--extra-json");
|
|
519
|
-
continue;
|
|
520
|
-
}
|
|
521
|
-
if (argument === "--template-id") {
|
|
522
|
-
templateId = args[++index];
|
|
523
|
-
if (!templateId) throw new Error("--template-id requires a value");
|
|
524
|
-
continue;
|
|
525
|
-
}
|
|
526
|
-
if (argument === "--status") {
|
|
527
|
-
status = args[++index];
|
|
528
|
-
if (!status) throw new Error("--status requires a value");
|
|
529
|
-
continue;
|
|
530
|
-
}
|
|
531
|
-
if (argument === "--text") {
|
|
532
|
-
text = args[++index];
|
|
533
|
-
if (text === undefined) throw new Error("--text requires a value");
|
|
534
|
-
continue;
|
|
535
|
-
}
|
|
536
|
-
if (argument === "--project-root") {
|
|
537
|
-
projectRoot = args[++index];
|
|
538
|
-
if (!projectRoot) throw new Error("--project-root requires a value");
|
|
539
|
-
continue;
|
|
540
|
-
}
|
|
541
|
-
if (argument === "--limit") {
|
|
542
|
-
const value = args[++index];
|
|
543
|
-
if (!value || Number.isNaN(Number(value))) throw new Error("--limit requires a numeric value");
|
|
544
|
-
limit = Number(value);
|
|
545
|
-
continue;
|
|
546
|
-
}
|
|
547
|
-
if (argument.startsWith("--")) throw new Error(`unknown docs option ${argument}`);
|
|
548
|
-
positional.push(argument);
|
|
549
|
-
}
|
|
550
|
-
const [action, id, second, third] = positional;
|
|
551
|
-
let result: unknown;
|
|
552
|
-
let human: string;
|
|
553
|
-
switch (action) {
|
|
554
|
-
case "create": {
|
|
555
|
-
if (id) throw new Error("docs create accepts no positional arguments");
|
|
556
|
-
if (!title) throw new Error("docs create requires --title");
|
|
557
|
-
const artifact = await client.call<Record<string, unknown>, CliArtifact>("docs.create", {
|
|
558
|
-
title,
|
|
559
|
-
body,
|
|
560
|
-
subtype,
|
|
561
|
-
labels,
|
|
562
|
-
extra,
|
|
563
|
-
template_id: templateId,
|
|
564
|
-
project_root: projectRoot,
|
|
565
|
-
});
|
|
566
|
-
result = artifact;
|
|
567
|
-
human = `Created document: ${artifactLabel(artifact)}`;
|
|
568
|
-
break;
|
|
569
|
-
}
|
|
570
|
-
case "list": {
|
|
571
|
-
if (id) throw new Error("docs list accepts no positional arguments");
|
|
572
|
-
const rows = await client.call<Record<string, unknown>, CliArtifact[]>("docs.list", {
|
|
573
|
-
status,
|
|
574
|
-
text,
|
|
575
|
-
limit,
|
|
576
|
-
project_root: projectRoot,
|
|
577
|
-
});
|
|
578
|
-
result = rows;
|
|
579
|
-
human = rows.length === 0 ? "No documents found." : rows.map((row) => artifactLabel(row)).join("\n");
|
|
580
|
-
break;
|
|
581
|
-
}
|
|
582
|
-
case "assign-project": {
|
|
583
|
-
if (!id || third !== undefined) throw new Error("docs assign-project requires <id> [project-root]");
|
|
584
|
-
const artifact = await client.call<Record<string, unknown>, CliArtifact>("docs.assign_project", { id, project_root: second });
|
|
585
|
-
result = artifact;
|
|
586
|
-
human = second ? `Assigned ${id} to ${second}` : `Unscoped ${id}`;
|
|
587
|
-
break;
|
|
588
|
-
}
|
|
589
|
-
case "show": {
|
|
590
|
-
if (!id || second) throw new Error("docs show requires exactly one document id");
|
|
591
|
-
const artifact = await client.call<Record<string, unknown>, CliArtifact>("docs.show", { id });
|
|
592
|
-
result = artifact;
|
|
593
|
-
human = `${artifactLabel(artifact)}\n\n${artifact.body ?? ""}`;
|
|
594
|
-
break;
|
|
595
|
-
}
|
|
596
|
-
case "activate":
|
|
597
|
-
case "archive":
|
|
598
|
-
case "reopen": {
|
|
599
|
-
if (!id || second) throw new Error(`docs ${action} requires exactly one document id`);
|
|
600
|
-
const artifact = await client.call<Record<string, unknown>, CliArtifact>(`docs.${action}`, { id });
|
|
601
|
-
result = artifact;
|
|
602
|
-
human = `${artifactLabel(artifact)}`;
|
|
603
|
-
break;
|
|
604
|
-
}
|
|
605
|
-
case "link": {
|
|
606
|
-
if (!id || !second || !third || positional.length !== 4) throw new Error("docs link requires <id> <relation> <target-id>");
|
|
607
|
-
const artifact = await client.call<Record<string, unknown>, CliArtifact>("docs.link", { id, relation: second, target_id: third });
|
|
608
|
-
result = artifact;
|
|
609
|
-
human = `Linked ${id} --${second}--> ${third}`;
|
|
610
|
-
break;
|
|
611
|
-
}
|
|
612
|
-
case "update": {
|
|
613
|
-
if (!id || second) throw new Error("docs update requires exactly one document id");
|
|
614
|
-
if (title === undefined && body === undefined && labels === undefined)
|
|
615
|
-
throw new Error("docs update requires --title, --body, or --labels-json");
|
|
616
|
-
const artifact = await client.call<Record<string, unknown>, CliArtifact>("docs.update", { id, title, body, labels });
|
|
617
|
-
result = artifact;
|
|
618
|
-
human = `${artifactLabel(artifact)}`;
|
|
619
|
-
break;
|
|
620
|
-
}
|
|
621
|
-
default:
|
|
622
|
-
throw new Error("docs action must be create, list, show, activate, archive, reopen, link, assign-project, or update");
|
|
623
|
-
}
|
|
624
|
-
return json ? JSON.stringify(result) : human;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
export async function runRulesCli(args: string[], client: TaskCliClient, projectRoot: string = process.cwd()): Promise<string> {
|
|
628
|
-
const json = args.includes("--json");
|
|
629
|
-
const positional: string[] = [];
|
|
630
|
-
let title: string | undefined;
|
|
631
|
-
let body: string | undefined;
|
|
632
|
-
let condition: string | undefined;
|
|
633
|
-
let ruleAction: string | undefined;
|
|
634
|
-
let severity: string | undefined;
|
|
635
|
-
let labels: string[] | undefined;
|
|
636
|
-
let extra: Record<string, unknown> | undefined;
|
|
637
|
-
let status: string | undefined;
|
|
638
|
-
let text: string | undefined;
|
|
639
|
-
let limit: number | undefined;
|
|
640
|
-
let ruleProjectRoot: string | undefined;
|
|
641
|
-
for (let index = 0; index < args.length; index++) {
|
|
642
|
-
const argument = args[index]!;
|
|
643
|
-
if (argument === "--json") continue;
|
|
644
|
-
if (argument === "--title") {
|
|
645
|
-
title = args[++index];
|
|
646
|
-
if (title === undefined) throw new Error("--title requires a value");
|
|
647
|
-
continue;
|
|
648
|
-
}
|
|
649
|
-
if (argument === "--body") {
|
|
650
|
-
body = args[++index];
|
|
651
|
-
if (body === undefined) throw new Error("--body requires a value");
|
|
652
|
-
continue;
|
|
653
|
-
}
|
|
654
|
-
if (argument === "--condition") {
|
|
655
|
-
condition = args[++index];
|
|
656
|
-
if (condition === undefined) throw new Error("--condition requires a value");
|
|
657
|
-
continue;
|
|
658
|
-
}
|
|
659
|
-
if (argument === "--rule-action") {
|
|
660
|
-
ruleAction = args[++index];
|
|
661
|
-
if (ruleAction === undefined) throw new Error("--rule-action requires a value");
|
|
662
|
-
continue;
|
|
663
|
-
}
|
|
664
|
-
if (argument === "--severity") {
|
|
665
|
-
severity = args[++index];
|
|
666
|
-
if (!severity) throw new Error("--severity requires a value");
|
|
667
|
-
continue;
|
|
668
|
-
}
|
|
669
|
-
if (argument === "--labels-json") {
|
|
670
|
-
labels = parseJsonStringArrayFlag(args[++index], "--labels-json");
|
|
671
|
-
continue;
|
|
672
|
-
}
|
|
673
|
-
if (argument === "--extra-json") {
|
|
674
|
-
extra = parseJsonObjectFlag(args[++index], "--extra-json");
|
|
675
|
-
continue;
|
|
676
|
-
}
|
|
677
|
-
if (argument === "--status") {
|
|
678
|
-
status = args[++index];
|
|
679
|
-
if (!status) throw new Error("--status requires a value");
|
|
680
|
-
continue;
|
|
681
|
-
}
|
|
682
|
-
if (argument === "--text") {
|
|
683
|
-
text = args[++index];
|
|
684
|
-
if (text === undefined) throw new Error("--text requires a value");
|
|
685
|
-
continue;
|
|
686
|
-
}
|
|
687
|
-
if (argument === "--project-root") {
|
|
688
|
-
ruleProjectRoot = args[++index];
|
|
689
|
-
if (!ruleProjectRoot) throw new Error("--project-root requires a value");
|
|
690
|
-
continue;
|
|
691
|
-
}
|
|
692
|
-
if (argument === "--limit") {
|
|
693
|
-
const value = args[++index];
|
|
694
|
-
if (!value || Number.isNaN(Number(value))) throw new Error("--limit requires a numeric value");
|
|
695
|
-
limit = Number(value);
|
|
696
|
-
continue;
|
|
697
|
-
}
|
|
698
|
-
if (argument.startsWith("--")) throw new Error(`unknown rules option ${argument}`);
|
|
699
|
-
positional.push(argument);
|
|
700
|
-
}
|
|
701
|
-
const [action, id, second, third] = positional;
|
|
702
|
-
let result: unknown;
|
|
703
|
-
let human: string;
|
|
704
|
-
switch (action) {
|
|
705
|
-
case "create": {
|
|
706
|
-
if (id) throw new Error("rules create accepts no positional arguments");
|
|
707
|
-
if (!title) throw new Error("rules create requires --title");
|
|
708
|
-
const artifact = await client.call<Record<string, unknown>, CliArtifact>("rules.create", {
|
|
709
|
-
title,
|
|
710
|
-
body,
|
|
711
|
-
condition,
|
|
712
|
-
rule_action: ruleAction,
|
|
713
|
-
severity,
|
|
714
|
-
labels,
|
|
715
|
-
extra,
|
|
716
|
-
project_root: ruleProjectRoot,
|
|
717
|
-
});
|
|
718
|
-
result = artifact;
|
|
719
|
-
human = `Created rule: ${artifactLabel(artifact)}`;
|
|
720
|
-
break;
|
|
721
|
-
}
|
|
722
|
-
case "list": {
|
|
723
|
-
if (id) throw new Error("rules list accepts no positional arguments");
|
|
724
|
-
const rows = await client.call<Record<string, unknown>, CliArtifact[]>("rules.list", {
|
|
725
|
-
status,
|
|
726
|
-
text,
|
|
727
|
-
limit,
|
|
728
|
-
project_root: ruleProjectRoot,
|
|
729
|
-
});
|
|
730
|
-
result = rows;
|
|
731
|
-
human = rows.length === 0 ? "No rules found." : rows.map((row) => artifactLabel(row)).join("\n");
|
|
732
|
-
break;
|
|
733
|
-
}
|
|
734
|
-
case "assign-project": {
|
|
735
|
-
if (!id || third !== undefined) throw new Error("rules assign-project requires <id> [project-root]");
|
|
736
|
-
const artifact = await client.call<Record<string, unknown>, CliArtifact>("rules.assign_project", { id, project_root: second });
|
|
737
|
-
result = artifact;
|
|
738
|
-
human = second ? `Assigned ${id} to ${second}` : `Unscoped ${id}`;
|
|
739
|
-
break;
|
|
740
|
-
}
|
|
741
|
-
case "show": {
|
|
742
|
-
if (!id || second) throw new Error("rules show requires exactly one rule id");
|
|
743
|
-
const artifact = await client.call<Record<string, unknown>, CliArtifact>("rules.show", { id });
|
|
744
|
-
result = artifact;
|
|
745
|
-
human = `${artifactLabel(artifact)}\n\n${artifact.body ?? ""}`;
|
|
746
|
-
break;
|
|
747
|
-
}
|
|
748
|
-
case "preview": {
|
|
749
|
-
if (!id || second) throw new Error("rules preview requires exactly one rule id");
|
|
750
|
-
const preview = await client.call<Record<string, unknown>, string>("rules.preview", { id });
|
|
751
|
-
result = preview;
|
|
752
|
-
human = preview;
|
|
753
|
-
break;
|
|
754
|
-
}
|
|
755
|
-
case "enable":
|
|
756
|
-
case "disable": {
|
|
757
|
-
if (!id || second) throw new Error(`rules ${action} requires exactly one rule id`);
|
|
758
|
-
const artifact = await client.call<Record<string, unknown>, CliArtifact>(`rules.${action}`, { id });
|
|
759
|
-
result = artifact;
|
|
760
|
-
human = `${artifactLabel(artifact)}`;
|
|
761
|
-
break;
|
|
762
|
-
}
|
|
763
|
-
case "gate": {
|
|
764
|
-
if (!id || !second || positional.length !== 3) throw new Error("rules gate requires <rule-id> <task-id>");
|
|
765
|
-
const artifact = await client.call<Record<string, unknown>, CliArtifact>("rules.gate", { id, task_id: second });
|
|
766
|
-
result = artifact;
|
|
767
|
-
human = `Gated ${second} with rule ${artifactLabel(artifact)}`;
|
|
768
|
-
break;
|
|
769
|
-
}
|
|
770
|
-
case "injectable": {
|
|
771
|
-
if (id) throw new Error("rules injectable accepts no positional arguments");
|
|
772
|
-
const rows = await client.call<Record<string, unknown>, CliArtifact[]>("rules.injectable", { project_root: projectRoot });
|
|
773
|
-
result = rows;
|
|
774
|
-
human = rows.length === 0 ? "No injectable rules." : rows.map((row) => row.title).join("\n");
|
|
775
|
-
break;
|
|
776
|
-
}
|
|
777
|
-
case "update": {
|
|
778
|
-
if (!id || second) throw new Error("rules update requires exactly one rule id");
|
|
779
|
-
if (title === undefined && body === undefined && labels === undefined)
|
|
780
|
-
throw new Error("rules update requires --title, --body, or --labels-json");
|
|
781
|
-
const artifact = await client.call<Record<string, unknown>, CliArtifact>("rules.update", { id, title, body, labels });
|
|
782
|
-
result = artifact;
|
|
783
|
-
human = `${artifactLabel(artifact)}`;
|
|
784
|
-
break;
|
|
785
|
-
}
|
|
786
|
-
default:
|
|
787
|
-
throw new Error("rules action must be create, list, show, preview, enable, disable, gate, injectable, assign-project, or update");
|
|
788
|
-
}
|
|
789
|
-
return json ? JSON.stringify(result) : human;
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
export async function runPlaybooksCli(args: string[], client: TaskCliClient): Promise<string> {
|
|
793
|
-
const json = args.includes("--json");
|
|
794
|
-
const positional: string[] = [];
|
|
795
|
-
let title: string | undefined;
|
|
796
|
-
let body: string | undefined;
|
|
797
|
-
let trigger: string | undefined;
|
|
798
|
-
let steps: unknown;
|
|
799
|
-
let tools: string[] | undefined;
|
|
800
|
-
let labels: string[] | undefined;
|
|
801
|
-
let extra: Record<string, unknown> | undefined;
|
|
802
|
-
let playbookArguments: unknown;
|
|
803
|
-
let runId: string | undefined;
|
|
804
|
-
let status: string | undefined;
|
|
805
|
-
let text: string | undefined;
|
|
806
|
-
let limit: number | undefined;
|
|
807
|
-
let playbookProjectRoot: string | undefined;
|
|
808
|
-
for (let index = 0; index < args.length; index++) {
|
|
809
|
-
const argument = args[index]!;
|
|
810
|
-
if (argument === "--json") continue;
|
|
811
|
-
if (argument === "--title") {
|
|
812
|
-
title = args[++index];
|
|
813
|
-
if (title === undefined) throw new Error("--title requires a value");
|
|
814
|
-
continue;
|
|
815
|
-
}
|
|
816
|
-
if (argument === "--body") {
|
|
817
|
-
body = args[++index];
|
|
818
|
-
if (body === undefined) throw new Error("--body requires a value");
|
|
819
|
-
continue;
|
|
820
|
-
}
|
|
821
|
-
if (argument === "--trigger") {
|
|
822
|
-
trigger = args[++index];
|
|
823
|
-
if (trigger === undefined) throw new Error("--trigger requires a value");
|
|
824
|
-
continue;
|
|
825
|
-
}
|
|
826
|
-
if (argument === "--steps-json") {
|
|
827
|
-
steps = parseJsonAnyFlag(args[++index], "--steps-json");
|
|
828
|
-
continue;
|
|
829
|
-
}
|
|
830
|
-
if (argument === "--tools-json") {
|
|
831
|
-
tools = parseJsonStringArrayFlag(args[++index], "--tools-json");
|
|
832
|
-
continue;
|
|
833
|
-
}
|
|
834
|
-
if (argument === "--labels-json") {
|
|
835
|
-
labels = parseJsonStringArrayFlag(args[++index], "--labels-json");
|
|
836
|
-
continue;
|
|
837
|
-
}
|
|
838
|
-
if (argument === "--extra-json") {
|
|
839
|
-
extra = parseJsonObjectFlag(args[++index], "--extra-json");
|
|
840
|
-
continue;
|
|
841
|
-
}
|
|
842
|
-
if (argument === "--arguments-json") {
|
|
843
|
-
playbookArguments = parseJsonAnyFlag(args[++index], "--arguments-json");
|
|
844
|
-
continue;
|
|
845
|
-
}
|
|
846
|
-
if (argument === "--run-id") {
|
|
847
|
-
runId = args[++index];
|
|
848
|
-
if (!runId) throw new Error("--run-id requires a value");
|
|
849
|
-
continue;
|
|
850
|
-
}
|
|
851
|
-
if (argument === "--status") {
|
|
852
|
-
status = args[++index];
|
|
853
|
-
if (!status) throw new Error("--status requires a value");
|
|
854
|
-
continue;
|
|
855
|
-
}
|
|
856
|
-
if (argument === "--text") {
|
|
857
|
-
text = args[++index];
|
|
858
|
-
if (text === undefined) throw new Error("--text requires a value");
|
|
859
|
-
continue;
|
|
860
|
-
}
|
|
861
|
-
if (argument === "--project-root") {
|
|
862
|
-
playbookProjectRoot = args[++index];
|
|
863
|
-
if (!playbookProjectRoot) throw new Error("--project-root requires a value");
|
|
864
|
-
continue;
|
|
865
|
-
}
|
|
866
|
-
if (argument === "--limit") {
|
|
867
|
-
const value = args[++index];
|
|
868
|
-
if (!value || Number.isNaN(Number(value))) throw new Error("--limit requires a numeric value");
|
|
869
|
-
limit = Number(value);
|
|
870
|
-
continue;
|
|
871
|
-
}
|
|
872
|
-
if (argument.startsWith("--")) throw new Error(`unknown playbooks option ${argument}`);
|
|
873
|
-
positional.push(argument);
|
|
874
|
-
}
|
|
875
|
-
const [action, id, second] = positional;
|
|
876
|
-
let result: unknown;
|
|
877
|
-
let human: string;
|
|
878
|
-
switch (action) {
|
|
879
|
-
case "create": {
|
|
880
|
-
if (id) throw new Error("playbooks create accepts no positional arguments");
|
|
881
|
-
if (!title) throw new Error("playbooks create requires --title");
|
|
882
|
-
const artifact = await client.call<Record<string, unknown>, CliArtifact>("playbooks.create", {
|
|
883
|
-
title,
|
|
884
|
-
body,
|
|
885
|
-
trigger,
|
|
886
|
-
steps,
|
|
887
|
-
tools,
|
|
888
|
-
labels,
|
|
889
|
-
extra,
|
|
890
|
-
arguments: playbookArguments,
|
|
891
|
-
project_root: playbookProjectRoot,
|
|
892
|
-
});
|
|
893
|
-
result = artifact;
|
|
894
|
-
human = `Created playbook: ${artifactLabel(artifact)}`;
|
|
895
|
-
break;
|
|
896
|
-
}
|
|
897
|
-
case "list": {
|
|
898
|
-
if (id) throw new Error("playbooks list accepts no positional arguments");
|
|
899
|
-
const rows = await client.call<Record<string, unknown>, CliArtifact[]>("playbooks.list", {
|
|
900
|
-
status,
|
|
901
|
-
text,
|
|
902
|
-
limit,
|
|
903
|
-
project_root: playbookProjectRoot,
|
|
904
|
-
});
|
|
905
|
-
result = rows;
|
|
906
|
-
human = rows.length === 0 ? "No playbooks found." : rows.map((row) => artifactLabel(row)).join("\n");
|
|
907
|
-
break;
|
|
908
|
-
}
|
|
909
|
-
case "show": {
|
|
910
|
-
if (!id || second) throw new Error("playbooks show requires exactly one playbook id");
|
|
911
|
-
const artifact = await client.call<Record<string, unknown>, CliArtifact>("playbooks.show", { id });
|
|
912
|
-
result = artifact;
|
|
913
|
-
human = `${artifactLabel(artifact)}\n\n${artifact.body ?? ""}`;
|
|
914
|
-
break;
|
|
915
|
-
}
|
|
916
|
-
case "preview": {
|
|
917
|
-
if (!id || second) throw new Error("playbooks preview requires exactly one playbook id");
|
|
918
|
-
const rendered = await client.call<Record<string, unknown>, string>("playbooks.preview", { id, arguments: playbookArguments });
|
|
919
|
-
result = rendered;
|
|
920
|
-
human = rendered;
|
|
921
|
-
break;
|
|
922
|
-
}
|
|
923
|
-
case "invoke": {
|
|
924
|
-
if (!id || second) throw new Error("playbooks invoke requires exactly one playbook id");
|
|
925
|
-
const invocation = await client.call<Record<string, unknown>, { entryTaskId: string; missingArguments?: string[] }>(
|
|
926
|
-
"playbooks.invoke",
|
|
927
|
-
{ id, arguments: playbookArguments, run_id: runId, project_root: playbookProjectRoot },
|
|
928
|
-
);
|
|
929
|
-
result = invocation;
|
|
930
|
-
human = invocation.missingArguments
|
|
931
|
-
? `Missing required argument(s): ${invocation.missingArguments.join(", ")}.`
|
|
932
|
-
: `Invoked: entry task ${invocation.entryTaskId} focused. Drive it forward with \`tasks start/submit/complete\` like any other task.`;
|
|
933
|
-
break;
|
|
934
|
-
}
|
|
935
|
-
case "enable":
|
|
936
|
-
case "disable": {
|
|
937
|
-
if (!id || second) throw new Error(`playbooks ${action} requires exactly one playbook id`);
|
|
938
|
-
const artifact = await client.call<Record<string, unknown>, CliArtifact>(`playbooks.${action}`, { id });
|
|
939
|
-
result = artifact;
|
|
940
|
-
human = `${artifactLabel(artifact)}`;
|
|
941
|
-
break;
|
|
942
|
-
}
|
|
943
|
-
case "assign-project": {
|
|
944
|
-
if (!id || (second === undefined && positional.length > 2)) throw new Error("playbooks assign-project requires <id> [project-root]");
|
|
945
|
-
const artifact = await client.call<Record<string, unknown>, CliArtifact>("playbooks.assign_project", { id, project_root: second });
|
|
946
|
-
result = artifact;
|
|
947
|
-
human = second ? `Assigned ${id} to ${second}` : `Unscoped ${id}`;
|
|
948
|
-
break;
|
|
949
|
-
}
|
|
950
|
-
case "update": {
|
|
951
|
-
if (!id || second) throw new Error("playbooks update requires exactly one playbook id");
|
|
952
|
-
if (title === undefined && body === undefined && labels === undefined)
|
|
953
|
-
throw new Error("playbooks update requires --title, --body, or --labels-json");
|
|
954
|
-
const artifact = await client.call<Record<string, unknown>, CliArtifact>("playbooks.update", { id, title, body, labels });
|
|
955
|
-
result = artifact;
|
|
956
|
-
human = `${artifactLabel(artifact)}`;
|
|
957
|
-
break;
|
|
958
|
-
}
|
|
959
|
-
case "contain": {
|
|
960
|
-
if (!id || !second || positional.length !== 3) throw new Error("playbooks contain requires a parent id and child id");
|
|
961
|
-
const artifact = await client.call<Record<string, unknown>, CliArtifact>("playbooks.contain", { parent_id: id, child_id: second });
|
|
962
|
-
result = artifact;
|
|
963
|
-
human = `Nested: ${second} → ${artifactLabel(artifact)}`;
|
|
964
|
-
break;
|
|
965
|
-
}
|
|
966
|
-
case "uncontain": {
|
|
967
|
-
if (!id || !second || positional.length !== 3) throw new Error("playbooks uncontain requires a parent id and child id");
|
|
968
|
-
const artifact = await client.call<Record<string, unknown>, CliArtifact>("playbooks.uncontain", { parent_id: id, child_id: second });
|
|
969
|
-
result = artifact;
|
|
970
|
-
human = `Removed ${second} from ${artifactLabel(artifact)}`;
|
|
971
|
-
break;
|
|
972
|
-
}
|
|
973
|
-
case "depend": {
|
|
974
|
-
if (!id || !second || positional.length !== 3) throw new Error("playbooks depend requires a playbook id and prerequisite id");
|
|
975
|
-
const artifact = await client.call<Record<string, unknown>, CliArtifact>("playbooks.depend", { id, dependency_id: second });
|
|
976
|
-
result = artifact;
|
|
977
|
-
human = `Dependency added: ${artifactLabel(artifact)} waits for ${second}`;
|
|
978
|
-
break;
|
|
979
|
-
}
|
|
980
|
-
case "undepend": {
|
|
981
|
-
if (!id || !second || positional.length !== 3) throw new Error("playbooks undepend requires a playbook id and prerequisite id");
|
|
982
|
-
const artifact = await client.call<Record<string, unknown>, CliArtifact>("playbooks.undepend", { id, dependency_id: second });
|
|
983
|
-
result = artifact;
|
|
984
|
-
human = `Dependency removed: ${artifactLabel(artifact)} no longer waits for ${second}`;
|
|
985
|
-
break;
|
|
986
|
-
}
|
|
987
|
-
default:
|
|
988
|
-
throw new Error(
|
|
989
|
-
"playbooks action must be create, list, show, invoke, preview, enable, disable, assign-project, update, contain, uncontain, depend, or undepend",
|
|
990
|
-
);
|
|
991
|
-
}
|
|
992
|
-
return json ? JSON.stringify(result) : human;
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
export async function runArtifactCli(args: string[], client: TaskCliClient, projectRoot: string = process.cwd()): Promise<string> {
|
|
996
|
-
const json = args.includes("--json");
|
|
997
|
-
const positional: string[] = [];
|
|
998
|
-
let kind: string | undefined;
|
|
999
|
-
let title: string | undefined;
|
|
1000
|
-
let body: string | undefined;
|
|
1001
|
-
let status: string | undefined;
|
|
1002
|
-
let subtype: string | undefined;
|
|
1003
|
-
let labels: string[] | undefined;
|
|
1004
|
-
let extra: Record<string, unknown> | undefined;
|
|
1005
|
-
let templateId: string | undefined;
|
|
1006
|
-
let reason: string | undefined;
|
|
1007
|
-
let text: string | undefined;
|
|
1008
|
-
let limit: number | undefined;
|
|
1009
|
-
let depth: number | undefined;
|
|
1010
|
-
let maxNodes: number | undefined;
|
|
1011
|
-
for (let index = 0; index < args.length; index++) {
|
|
1012
|
-
const argument = args[index]!;
|
|
1013
|
-
if (argument === "--json") continue;
|
|
1014
|
-
if (argument === "--kind") {
|
|
1015
|
-
kind = args[++index];
|
|
1016
|
-
if (!kind) throw new Error("--kind requires a value");
|
|
1017
|
-
continue;
|
|
1018
|
-
}
|
|
1019
|
-
if (argument === "--title") {
|
|
1020
|
-
title = args[++index];
|
|
1021
|
-
if (title === undefined) throw new Error("--title requires a value");
|
|
1022
|
-
continue;
|
|
1023
|
-
}
|
|
1024
|
-
if (argument === "--body") {
|
|
1025
|
-
body = args[++index];
|
|
1026
|
-
if (body === undefined) throw new Error("--body requires a value");
|
|
1027
|
-
continue;
|
|
1028
|
-
}
|
|
1029
|
-
if (argument === "--status") {
|
|
1030
|
-
status = args[++index];
|
|
1031
|
-
if (!status) throw new Error("--status requires a value");
|
|
1032
|
-
continue;
|
|
1033
|
-
}
|
|
1034
|
-
if (argument === "--subtype") {
|
|
1035
|
-
subtype = args[++index];
|
|
1036
|
-
if (!subtype) throw new Error("--subtype requires a value");
|
|
1037
|
-
continue;
|
|
1038
|
-
}
|
|
1039
|
-
if (argument === "--labels-json") {
|
|
1040
|
-
labels = parseJsonStringArrayFlag(args[++index], "--labels-json");
|
|
1041
|
-
continue;
|
|
1042
|
-
}
|
|
1043
|
-
if (argument === "--extra-json") {
|
|
1044
|
-
extra = parseJsonObjectFlag(args[++index], "--extra-json");
|
|
1045
|
-
continue;
|
|
1046
|
-
}
|
|
1047
|
-
if (argument === "--template-id") {
|
|
1048
|
-
templateId = args[++index];
|
|
1049
|
-
if (!templateId) throw new Error("--template-id requires a value");
|
|
1050
|
-
continue;
|
|
1051
|
-
}
|
|
1052
|
-
if (argument === "--reason") {
|
|
1053
|
-
reason = args[++index];
|
|
1054
|
-
if (reason === undefined) throw new Error("--reason requires a value");
|
|
1055
|
-
continue;
|
|
1056
|
-
}
|
|
1057
|
-
if (argument === "--text") {
|
|
1058
|
-
text = args[++index];
|
|
1059
|
-
if (text === undefined) throw new Error("--text requires a value");
|
|
1060
|
-
continue;
|
|
1061
|
-
}
|
|
1062
|
-
if (argument === "--limit") {
|
|
1063
|
-
const value = args[++index];
|
|
1064
|
-
if (!value || Number.isNaN(Number(value))) throw new Error("--limit requires a numeric value");
|
|
1065
|
-
limit = Number(value);
|
|
1066
|
-
continue;
|
|
1067
|
-
}
|
|
1068
|
-
if (argument === "--depth") {
|
|
1069
|
-
const value = args[++index];
|
|
1070
|
-
if (!value || Number.isNaN(Number(value))) throw new Error("--depth requires a numeric value");
|
|
1071
|
-
depth = Number(value);
|
|
1072
|
-
continue;
|
|
1073
|
-
}
|
|
1074
|
-
if (argument === "--max-nodes") {
|
|
1075
|
-
const value = args[++index];
|
|
1076
|
-
if (!value || Number.isNaN(Number(value))) throw new Error("--max-nodes requires a numeric value");
|
|
1077
|
-
maxNodes = Number(value);
|
|
1078
|
-
continue;
|
|
1079
|
-
}
|
|
1080
|
-
if (argument.startsWith("--")) throw new Error(`unknown artifact option ${argument}`);
|
|
1081
|
-
positional.push(argument);
|
|
1082
|
-
}
|
|
1083
|
-
const [action, id] = positional;
|
|
1084
|
-
let result: unknown;
|
|
1085
|
-
let human: string;
|
|
1086
|
-
switch (action) {
|
|
1087
|
-
case "create": {
|
|
1088
|
-
if (id) throw new Error("artifact create accepts no positional arguments");
|
|
1089
|
-
if (!kind && !templateId) throw new Error("artifact create requires --kind (or --template-id)");
|
|
1090
|
-
const artifact = await client.call<Record<string, unknown>, CliArtifact>("artifact.create", {
|
|
1091
|
-
kind,
|
|
1092
|
-
title,
|
|
1093
|
-
body,
|
|
1094
|
-
status,
|
|
1095
|
-
subtype,
|
|
1096
|
-
labels,
|
|
1097
|
-
extra,
|
|
1098
|
-
template_id: templateId,
|
|
1099
|
-
...(kind === "task" ? { project_root: projectRoot } : {}),
|
|
1100
|
-
});
|
|
1101
|
-
result = artifact;
|
|
1102
|
-
human = `Created: ${artifactLabel(artifact)}`;
|
|
1103
|
-
break;
|
|
1104
|
-
}
|
|
1105
|
-
case "query": {
|
|
1106
|
-
if (id) throw new Error("artifact query accepts no positional arguments");
|
|
1107
|
-
const rows = await client.call<Record<string, unknown>, CliArtifact[]>("artifact.query", { kind, status, text, limit });
|
|
1108
|
-
result = rows;
|
|
1109
|
-
human = rows.length === 0 ? "No artifacts found." : rows.map((row) => artifactLabel(row)).join("\n");
|
|
1110
|
-
break;
|
|
1111
|
-
}
|
|
1112
|
-
case "show": {
|
|
1113
|
-
if (!id) throw new Error("artifact show requires exactly one artifact id");
|
|
1114
|
-
const artifact = await client.call<Record<string, unknown>, CliArtifact>("artifact.show", { id, depth, max_nodes: maxNodes });
|
|
1115
|
-
result = artifact;
|
|
1116
|
-
human = `${artifactLabel(artifact)}\n\n${artifact.body ?? ""}`;
|
|
1117
|
-
break;
|
|
1118
|
-
}
|
|
1119
|
-
case "remove": {
|
|
1120
|
-
if (!id) throw new Error("artifact remove requires exactly one artifact id");
|
|
1121
|
-
const record = await client.call<
|
|
1122
|
-
Record<string, unknown>,
|
|
1123
|
-
{ artifactId: string; trashedAt: string; purgeAfter: string; reason?: string }
|
|
1124
|
-
>("artifact.remove", { id, reason });
|
|
1125
|
-
result = record;
|
|
1126
|
-
human = `Trashed ${record.artifactId}: eligible for purge at ${record.purgeAfter}`;
|
|
1127
|
-
break;
|
|
1128
|
-
}
|
|
1129
|
-
case "remove-subtree": {
|
|
1130
|
-
if (!id) throw new Error("artifact remove-subtree requires exactly one artifact id");
|
|
1131
|
-
const outcome = await client.call<Record<string, unknown>, { removed: string[]; skipped: string[] }>("artifact.remove_subtree", {
|
|
1132
|
-
id,
|
|
1133
|
-
reason,
|
|
1134
|
-
});
|
|
1135
|
-
result = outcome;
|
|
1136
|
-
human = `Trashed ${outcome.removed.length} artifact(s)${outcome.skipped.length > 0 ? `, skipped ${outcome.skipped.length} already-trashed` : ""}.`;
|
|
1137
|
-
break;
|
|
1138
|
-
}
|
|
1139
|
-
case "restore": {
|
|
1140
|
-
if (!id) throw new Error("artifact restore requires exactly one artifact id");
|
|
1141
|
-
const outcome = await client.call<Record<string, unknown>, { restored: boolean }>("artifact.restore", { id });
|
|
1142
|
-
result = outcome;
|
|
1143
|
-
human = outcome.restored ? `Restored ${id}` : `${id} was not trashed`;
|
|
1144
|
-
break;
|
|
1145
|
-
}
|
|
1146
|
-
case "trash-status": {
|
|
1147
|
-
if (!id) throw new Error("artifact trash-status requires exactly one artifact id");
|
|
1148
|
-
const record = await client.call<
|
|
1149
|
-
Record<string, unknown>,
|
|
1150
|
-
{ artifactId: string; trashedAt: string; purgeAfter: string; reason?: string } | null
|
|
1151
|
-
>("artifact.trash_status", { id });
|
|
1152
|
-
result = record;
|
|
1153
|
-
human = record
|
|
1154
|
-
? `${record.artifactId}: trashed at ${record.trashedAt}, purge eligible at ${record.purgeAfter}`
|
|
1155
|
-
: `${id} is not trashed`;
|
|
1156
|
-
break;
|
|
1157
|
-
}
|
|
1158
|
-
case "trash-list": {
|
|
1159
|
-
if (id) throw new Error("artifact trash-list accepts no positional arguments");
|
|
1160
|
-
const rows = await client.call<
|
|
1161
|
-
Record<string, unknown>,
|
|
1162
|
-
Array<{ artifactId: string; trashedAt: string; purgeAfter: string; reason?: string }>
|
|
1163
|
-
>("artifact.trash_list", {});
|
|
1164
|
-
result = rows;
|
|
1165
|
-
human =
|
|
1166
|
-
rows.length === 0 ? "Trash is empty." : rows.map((row) => `${row.artifactId}: purge eligible at ${row.purgeAfter}`).join("\n");
|
|
1167
|
-
break;
|
|
1168
|
-
}
|
|
1169
|
-
default:
|
|
1170
|
-
throw new Error("artifact action must be create, query, show, remove, remove-subtree, restore, trash-status, or trash-list");
|
|
1171
|
-
}
|
|
1172
|
-
return json ? JSON.stringify(result) : human;
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
export async function runGatesCli(args: string[], client: TaskCliClient): Promise<string> {
|
|
1176
|
-
const json = args.includes("--json");
|
|
1177
|
-
const positional = args.filter((arg) => arg !== "--json");
|
|
1178
|
-
if (positional.length !== 2 || positional[0] !== "run") throw new Error("gates requires `run <id>`");
|
|
1179
|
-
const results = await client.call<Record<string, unknown>, GateResult[]>("gates.run", { id: positional[1] });
|
|
1180
|
-
if (json) return JSON.stringify(results);
|
|
1181
|
-
return results.length === 0
|
|
1182
|
-
? "No gates configured."
|
|
1183
|
-
: results.map((gate) => `${gate.passed ? "✓" : "✗"} ${gate.gate.type}: ${gate.gate.target} — ${gate.output}`).join("\n");
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
export async function runGraphProjectionCli(args: string[], client: TaskCliClient): Promise<string> {
|
|
1187
|
-
const json = args.includes("--json");
|
|
1188
|
-
const positional: string[] = [];
|
|
1189
|
-
let batch: Record<string, unknown> | undefined;
|
|
1190
|
-
let producerId: string | undefined;
|
|
1191
|
-
for (let index = 0; index < args.length; index++) {
|
|
1192
|
-
const argument = args[index]!;
|
|
1193
|
-
if (argument === "--json") continue;
|
|
1194
|
-
if (argument === "--batch-json") {
|
|
1195
|
-
batch = parseJsonObjectFlag(args[++index], "--batch-json");
|
|
1196
|
-
continue;
|
|
1197
|
-
}
|
|
1198
|
-
if (argument === "--producer-id") {
|
|
1199
|
-
producerId = args[++index];
|
|
1200
|
-
if (!producerId) throw new Error("--producer-id requires a value");
|
|
1201
|
-
continue;
|
|
1202
|
-
}
|
|
1203
|
-
positional.push(argument);
|
|
1204
|
-
}
|
|
1205
|
-
const [action] = positional;
|
|
1206
|
-
if (action === "apply") {
|
|
1207
|
-
if (!batch) throw new Error("graph-projection apply requires --batch-json");
|
|
1208
|
-
const result = await client.call<Record<string, unknown>, unknown>("graph_projection.apply", batch);
|
|
1209
|
-
return json ? JSON.stringify(result) : JSON.stringify(result, null, 2);
|
|
1210
|
-
}
|
|
1211
|
-
if (action === "checkpoint") {
|
|
1212
|
-
if (!producerId) throw new Error("graph-projection checkpoint requires --producer-id");
|
|
1213
|
-
const result = await client.call<Record<string, unknown>, unknown>("graph_projection.checkpoint", { producer_id: producerId });
|
|
1214
|
-
if (json) return JSON.stringify(result);
|
|
1215
|
-
return result === null ? `No projection checkpoint for producer "${producerId}".` : JSON.stringify(result, null, 2);
|
|
1216
|
-
}
|
|
1217
|
-
throw new Error("graph-projection action must be apply or checkpoint");
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
export async function runLogCli(args: string[], client: TaskCliClient, projectRoot: string = process.cwd()): Promise<string> {
|
|
1221
|
-
const json = args.includes("--json");
|
|
1222
|
-
const positional: string[] = [];
|
|
1223
|
-
let source: string | undefined;
|
|
1224
|
-
let sourceLabel: string | undefined;
|
|
1225
|
-
let level: string | undefined;
|
|
1226
|
-
let message: string | undefined;
|
|
1227
|
-
let operationId: string | undefined;
|
|
1228
|
-
let sessionId: string | undefined;
|
|
1229
|
-
let occurredAt: string | undefined;
|
|
1230
|
-
let since: string | undefined;
|
|
1231
|
-
let limit: number | undefined;
|
|
1232
|
-
let fields: Record<string, unknown> | undefined;
|
|
1233
|
-
let global = false;
|
|
1234
|
-
for (let index = 0; index < args.length; index++) {
|
|
1235
|
-
const argument = args[index]!;
|
|
1236
|
-
if (argument === "--json") continue;
|
|
1237
|
-
if (argument === "--global") {
|
|
1238
|
-
global = true;
|
|
1239
|
-
continue;
|
|
1240
|
-
}
|
|
1241
|
-
if (argument === "--fields-json") {
|
|
1242
|
-
fields = parseJsonObjectFlag(args[++index], "--fields-json");
|
|
1243
|
-
continue;
|
|
1244
|
-
}
|
|
1245
|
-
if (
|
|
1246
|
-
[
|
|
1247
|
-
"--source",
|
|
1248
|
-
"--source-label",
|
|
1249
|
-
"--level",
|
|
1250
|
-
"--message",
|
|
1251
|
-
"--operation-id",
|
|
1252
|
-
"--session-id",
|
|
1253
|
-
"--occurred-at",
|
|
1254
|
-
"--since",
|
|
1255
|
-
"--limit",
|
|
1256
|
-
].includes(argument)
|
|
1257
|
-
) {
|
|
1258
|
-
const value = args[++index];
|
|
1259
|
-
if (value === undefined) throw new Error(`${argument} requires a value`);
|
|
1260
|
-
if (argument === "--source") source = value;
|
|
1261
|
-
else if (argument === "--source-label") sourceLabel = value;
|
|
1262
|
-
else if (argument === "--level") level = value;
|
|
1263
|
-
else if (argument === "--message") message = value;
|
|
1264
|
-
else if (argument === "--operation-id") operationId = value;
|
|
1265
|
-
else if (argument === "--session-id") sessionId = value;
|
|
1266
|
-
else if (argument === "--occurred-at") occurredAt = value;
|
|
1267
|
-
else if (argument === "--since") since = value;
|
|
1268
|
-
else {
|
|
1269
|
-
limit = Number(value);
|
|
1270
|
-
if (!Number.isInteger(limit)) throw new Error("--limit requires an integer");
|
|
1271
|
-
}
|
|
1272
|
-
continue;
|
|
1273
|
-
}
|
|
1274
|
-
if (argument.startsWith("--")) throw new Error(`unknown log option ${argument}`);
|
|
1275
|
-
positional.push(argument);
|
|
1276
|
-
}
|
|
1277
|
-
const [action] = positional;
|
|
1278
|
-
if (action === "append") {
|
|
1279
|
-
if (!source) throw new Error("log append requires --source");
|
|
1280
|
-
if (!level) throw new Error("log append requires --level");
|
|
1281
|
-
if (!message) throw new Error("log append requires --message");
|
|
1282
|
-
if (!operationId) throw new Error("log append requires --operation-id");
|
|
1283
|
-
const result = await client.call("logs.append", {
|
|
1284
|
-
source_id: source,
|
|
1285
|
-
...(sourceLabel ? { source_label: sourceLabel } : {}),
|
|
1286
|
-
...(global ? {} : { project_root: projectRoot }),
|
|
1287
|
-
level,
|
|
1288
|
-
message,
|
|
1289
|
-
operation_id: operationId,
|
|
1290
|
-
...(fields ? { fields } : {}),
|
|
1291
|
-
...(sessionId ? { session_id: sessionId } : {}),
|
|
1292
|
-
...(occurredAt ? { occurred_at: occurredAt } : {}),
|
|
1293
|
-
});
|
|
1294
|
-
return json ? JSON.stringify(result) : JSON.stringify(result, null, 2);
|
|
1295
|
-
}
|
|
1296
|
-
if (action === "query") {
|
|
1297
|
-
if (!source) throw new Error("log query requires --source");
|
|
1298
|
-
const result = await client.call<Record<string, unknown>, { entries: unknown[]; truncated: boolean }>("logs.query", {
|
|
1299
|
-
source_id: source,
|
|
1300
|
-
...(since ? { since } : {}),
|
|
1301
|
-
...(level ? { level } : {}),
|
|
1302
|
-
...(limit === undefined ? {} : { limit }),
|
|
1303
|
-
});
|
|
1304
|
-
if (json) return JSON.stringify(result);
|
|
1305
|
-
const lines = result.entries.map((entry) => JSON.stringify(entry));
|
|
1306
|
-
return [...lines, result.truncated ? `(truncated -- more entries exist beyond this page)` : `(${lines.length} entries)`].join("\n");
|
|
1307
|
-
}
|
|
1308
|
-
throw new Error("log action must be append or query");
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
export async function runSessionIdentityCli(args: string[], client: TaskCliClient): Promise<string> {
|
|
1312
|
-
const json = args.includes("--json");
|
|
1313
|
-
const positional: string[] = [];
|
|
1314
|
-
let sessionId: string | undefined;
|
|
1315
|
-
let secret: string | undefined;
|
|
1316
|
-
for (let index = 0; index < args.length; index++) {
|
|
1317
|
-
const argument = args[index]!;
|
|
1318
|
-
if (argument === "--json") continue;
|
|
1319
|
-
if (argument === "--session-id") {
|
|
1320
|
-
sessionId = args[++index];
|
|
1321
|
-
continue;
|
|
1322
|
-
}
|
|
1323
|
-
if (argument === "--session-secret") {
|
|
1324
|
-
secret = args[++index];
|
|
1325
|
-
continue;
|
|
1326
|
-
}
|
|
1327
|
-
if (argument.startsWith("--")) throw new Error(`unknown session option ${argument}`);
|
|
1328
|
-
positional.push(argument);
|
|
1329
|
-
}
|
|
1330
|
-
const [action] = positional;
|
|
1331
|
-
if (action === "register") {
|
|
1332
|
-
if (!sessionId) throw new Error("session register requires --session-id");
|
|
1333
|
-
const result = await client.call<Record<string, unknown>, { sessionId: string; secret: string }>("session.register", {
|
|
1334
|
-
session_id: sessionId,
|
|
1335
|
-
});
|
|
1336
|
-
return json ? JSON.stringify(result) : JSON.stringify(result, null, 2);
|
|
1337
|
-
}
|
|
1338
|
-
if (action === "release") {
|
|
1339
|
-
if (!sessionId) throw new Error("session release requires --session-id");
|
|
1340
|
-
const result = await client.call<Record<string, unknown>, { released: boolean }>("session.release", {
|
|
1341
|
-
session_id: sessionId,
|
|
1342
|
-
...(secret ? { session_secret: secret } : {}),
|
|
1343
|
-
});
|
|
1344
|
-
return json ? JSON.stringify(result) : JSON.stringify(result, null, 2);
|
|
1345
|
-
}
|
|
1346
|
-
throw new Error("session action must be register or release");
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
export async function runDiscussCli(args: string[], client: TaskCliClient): Promise<string> {
|
|
1350
|
-
const json = args.includes("--json");
|
|
1351
|
-
const positional: string[] = [];
|
|
1352
|
-
let title: string | undefined;
|
|
1353
|
-
let actor: string | undefined;
|
|
1354
|
-
let content: string | undefined;
|
|
1355
|
-
let body: string | undefined;
|
|
1356
|
-
let labels: string[] | undefined;
|
|
1357
|
-
let blocksTaskIds: string[] | undefined;
|
|
1358
|
-
let taskId: string | undefined;
|
|
1359
|
-
let reason: string | undefined;
|
|
1360
|
-
let settlement: string | undefined;
|
|
1361
|
-
let state: string | undefined;
|
|
1362
|
-
let afterRound: number | undefined;
|
|
1363
|
-
let limit: number | undefined;
|
|
1364
|
-
let options: string[] | undefined;
|
|
1365
|
-
let optionsMode: string | undefined;
|
|
1366
|
-
let optionDescriptions: string[] | undefined;
|
|
1367
|
-
let selected: string[] | undefined;
|
|
1368
|
-
for (let index = 0; index < args.length; index++) {
|
|
1369
|
-
const argument = args[index]!;
|
|
1370
|
-
if (argument === "--json") continue;
|
|
1371
|
-
if (argument === "--title") {
|
|
1372
|
-
title = args[++index];
|
|
1373
|
-
if (!title) throw new Error("--title requires a value");
|
|
1374
|
-
continue;
|
|
1375
|
-
}
|
|
1376
|
-
if (argument === "--actor") {
|
|
1377
|
-
actor = args[++index];
|
|
1378
|
-
if (!actor) throw new Error("--actor requires a value");
|
|
1379
|
-
continue;
|
|
1380
|
-
}
|
|
1381
|
-
if (argument === "--content") {
|
|
1382
|
-
content = args[++index];
|
|
1383
|
-
if (content === undefined) throw new Error("--content requires a value");
|
|
1384
|
-
continue;
|
|
1385
|
-
}
|
|
1386
|
-
if (argument === "--body") {
|
|
1387
|
-
body = args[++index];
|
|
1388
|
-
if (body === undefined) throw new Error("--body requires a value");
|
|
1389
|
-
continue;
|
|
1390
|
-
}
|
|
1391
|
-
if (argument === "--labels-json") {
|
|
1392
|
-
labels = parseJsonStringArrayFlag(args[++index], "--labels-json");
|
|
1393
|
-
continue;
|
|
1394
|
-
}
|
|
1395
|
-
if (argument === "--blocks-json") {
|
|
1396
|
-
blocksTaskIds = parseJsonStringArrayFlag(args[++index], "--blocks-json");
|
|
1397
|
-
continue;
|
|
1398
|
-
}
|
|
1399
|
-
if (argument === "--task-id") {
|
|
1400
|
-
taskId = args[++index];
|
|
1401
|
-
if (!taskId) throw new Error("--task-id requires a value");
|
|
1402
|
-
continue;
|
|
1403
|
-
}
|
|
1404
|
-
if (argument === "--options-json") {
|
|
1405
|
-
options = parseJsonStringArrayFlag(args[++index], "--options-json");
|
|
1406
|
-
continue;
|
|
1407
|
-
}
|
|
1408
|
-
if (argument === "--options-mode") {
|
|
1409
|
-
optionsMode = args[++index];
|
|
1410
|
-
if (!optionsMode) throw new Error("--options-mode requires a value");
|
|
1411
|
-
continue;
|
|
1412
|
-
}
|
|
1413
|
-
if (argument === "--option-descriptions-json") {
|
|
1414
|
-
optionDescriptions = parseJsonStringArrayFlag(args[++index], "--option-descriptions-json");
|
|
1415
|
-
continue;
|
|
1416
|
-
}
|
|
1417
|
-
if (argument === "--selected-json") {
|
|
1418
|
-
selected = parseJsonStringArrayFlag(args[++index], "--selected-json");
|
|
1419
|
-
continue;
|
|
1420
|
-
}
|
|
1421
|
-
if (argument === "--reason") {
|
|
1422
|
-
reason = args[++index];
|
|
1423
|
-
if (reason === undefined) throw new Error("--reason requires a value");
|
|
1424
|
-
continue;
|
|
1425
|
-
}
|
|
1426
|
-
if (argument === "--settlement") {
|
|
1427
|
-
settlement = args[++index];
|
|
1428
|
-
if (!settlement) throw new Error("--settlement requires a value");
|
|
1429
|
-
continue;
|
|
1430
|
-
}
|
|
1431
|
-
if (argument === "--state") {
|
|
1432
|
-
state = args[++index];
|
|
1433
|
-
if (!state) throw new Error("--state requires a value");
|
|
1434
|
-
continue;
|
|
1435
|
-
}
|
|
1436
|
-
if (argument === "--after-round") {
|
|
1437
|
-
const value = args[++index];
|
|
1438
|
-
if (!value || Number.isNaN(Number(value))) throw new Error("--after-round requires a numeric value");
|
|
1439
|
-
afterRound = Number(value);
|
|
1440
|
-
continue;
|
|
1441
|
-
}
|
|
1442
|
-
if (argument === "--limit") {
|
|
1443
|
-
const value = args[++index];
|
|
1444
|
-
if (!value || Number.isNaN(Number(value))) throw new Error("--limit requires a numeric value");
|
|
1445
|
-
limit = Number(value);
|
|
1446
|
-
continue;
|
|
1447
|
-
}
|
|
1448
|
-
if (argument.startsWith("--")) throw new Error(`unknown discuss option ${argument}`);
|
|
1449
|
-
positional.push(argument);
|
|
1450
|
-
}
|
|
1451
|
-
const [action, id] = positional;
|
|
1452
|
-
switch (action) {
|
|
1453
|
-
case "open": {
|
|
1454
|
-
if (id) throw new Error("discuss open accepts no positional arguments");
|
|
1455
|
-
const result = await client.call<Record<string, unknown>, unknown>("discuss.open", {
|
|
1456
|
-
title,
|
|
1457
|
-
actor,
|
|
1458
|
-
content,
|
|
1459
|
-
body,
|
|
1460
|
-
labels,
|
|
1461
|
-
blocks_task_ids: blocksTaskIds,
|
|
1462
|
-
options,
|
|
1463
|
-
options_mode: optionsMode,
|
|
1464
|
-
option_descriptions: optionDescriptions,
|
|
1465
|
-
});
|
|
1466
|
-
return json ? JSON.stringify(result) : JSON.stringify(result, null, 2);
|
|
1467
|
-
}
|
|
1468
|
-
case "reply": {
|
|
1469
|
-
if (!id) throw new Error("discuss reply requires exactly one discussion id");
|
|
1470
|
-
const result = await client.call<Record<string, unknown>, unknown>("discuss.reply", {
|
|
1471
|
-
id,
|
|
1472
|
-
actor,
|
|
1473
|
-
content,
|
|
1474
|
-
selected,
|
|
1475
|
-
options,
|
|
1476
|
-
options_mode: optionsMode,
|
|
1477
|
-
option_descriptions: optionDescriptions,
|
|
1478
|
-
});
|
|
1479
|
-
return json ? JSON.stringify(result) : JSON.stringify(result, null, 2);
|
|
1480
|
-
}
|
|
1481
|
-
case "defer": {
|
|
1482
|
-
if (!id) throw new Error("discuss defer requires exactly one discussion id");
|
|
1483
|
-
const result = await client.call<Record<string, unknown>, unknown>("discuss.defer", { id, reason });
|
|
1484
|
-
return json ? JSON.stringify(result) : JSON.stringify(result, null, 2);
|
|
1485
|
-
}
|
|
1486
|
-
case "resume": {
|
|
1487
|
-
if (!id) throw new Error("discuss resume requires exactly one discussion id");
|
|
1488
|
-
const result = await client.call<Record<string, unknown>, unknown>("discuss.resume", { id });
|
|
1489
|
-
return json ? JSON.stringify(result) : JSON.stringify(result, null, 2);
|
|
1490
|
-
}
|
|
1491
|
-
case "settle": {
|
|
1492
|
-
if (!id) throw new Error("discuss settle requires exactly one discussion id");
|
|
1493
|
-
const result = await client.call<Record<string, unknown>, unknown>("discuss.settle", { id, settlement });
|
|
1494
|
-
return json ? JSON.stringify(result) : JSON.stringify(result, null, 2);
|
|
1495
|
-
}
|
|
1496
|
-
case "block": {
|
|
1497
|
-
if (!id) throw new Error("discuss block requires exactly one discussion id");
|
|
1498
|
-
const result = await client.call<Record<string, unknown>, unknown>("discuss.block", { id, task_id: taskId });
|
|
1499
|
-
return json ? JSON.stringify(result) : JSON.stringify(result, null, 2);
|
|
1500
|
-
}
|
|
1501
|
-
case "unblock": {
|
|
1502
|
-
if (!id) throw new Error("discuss unblock requires exactly one discussion id");
|
|
1503
|
-
const result = await client.call<Record<string, unknown>, unknown>("discuss.unblock", { id, task_id: taskId });
|
|
1504
|
-
return json ? JSON.stringify(result) : JSON.stringify(result, null, 2);
|
|
1505
|
-
}
|
|
1506
|
-
case "show": {
|
|
1507
|
-
if (!id) throw new Error("discuss show requires exactly one discussion id");
|
|
1508
|
-
const result = await client.call<Record<string, unknown>, unknown>("discuss.show", { id });
|
|
1509
|
-
return json ? JSON.stringify(result) : JSON.stringify(result, null, 2);
|
|
1510
|
-
}
|
|
1511
|
-
case "rounds": {
|
|
1512
|
-
if (!id) throw new Error("discuss rounds requires exactly one discussion id");
|
|
1513
|
-
const result = await client.call<Record<string, unknown>, unknown>("discuss.rounds", { id, after_round: afterRound, limit });
|
|
1514
|
-
return json ? JSON.stringify(result) : JSON.stringify(result, null, 2);
|
|
1515
|
-
}
|
|
1516
|
-
case "list": {
|
|
1517
|
-
if (id) throw new Error("discuss list accepts no positional arguments");
|
|
1518
|
-
const result = await client.call<Record<string, unknown>, unknown>("discuss.list", { state, limit });
|
|
1519
|
-
return json ? JSON.stringify(result) : JSON.stringify(result, null, 2);
|
|
1520
|
-
}
|
|
1521
|
-
default:
|
|
1522
|
-
throw new Error("discuss action must be open, reply, defer, resume, settle, block, unblock, show, rounds, or list");
|
|
1523
|
-
}
|
|
1524
|
-
}
|
|
1525
|
-
|
|
1526
|
-
export async function runNoteCli(args: string[], client: TaskCliClient, projectRoot: string = process.cwd()): Promise<string> {
|
|
1527
|
-
const json = args.includes("--json");
|
|
1528
|
-
const positional: string[] = [];
|
|
1529
|
-
let title: string | undefined;
|
|
1530
|
-
let status: string | undefined;
|
|
1531
|
-
let text: string | undefined;
|
|
1532
|
-
let reason: string | undefined;
|
|
1533
|
-
let limit: number | undefined;
|
|
1534
|
-
for (let index = 0; index < args.length; index++) {
|
|
1535
|
-
const argument = args[index]!;
|
|
1536
|
-
if (argument === "--json") continue;
|
|
1537
|
-
if (["--title", "--status", "--text", "--reason", "--limit"].includes(argument)) {
|
|
1538
|
-
const value = args[++index];
|
|
1539
|
-
if (value === undefined) throw new Error(`${argument} requires a value`);
|
|
1540
|
-
if (argument === "--title") title = value;
|
|
1541
|
-
else if (argument === "--status") status = value;
|
|
1542
|
-
else if (argument === "--text") text = value;
|
|
1543
|
-
else if (argument === "--reason") reason = value;
|
|
1544
|
-
else {
|
|
1545
|
-
limit = Number(value);
|
|
1546
|
-
if (!Number.isInteger(limit)) throw new Error("--limit requires an integer");
|
|
1547
|
-
}
|
|
1548
|
-
continue;
|
|
1549
|
-
}
|
|
1550
|
-
if (argument.startsWith("--")) throw new Error(`unknown notes option ${argument}`);
|
|
1551
|
-
positional.push(argument);
|
|
1552
|
-
}
|
|
1553
|
-
const [action, id, target] = positional;
|
|
1554
|
-
let result: CliArtifact | CliArtifact[] | import("./domain/note-event.ts").NoteHistoryPage;
|
|
1555
|
-
let human: string;
|
|
1556
|
-
if (action === "capture") {
|
|
1557
|
-
if (!id || target) throw new Error("notes capture requires exactly one request argument");
|
|
1558
|
-
result = (await client.call("notes.capture", {
|
|
1559
|
-
body: id,
|
|
1560
|
-
...(title ? { title } : {}),
|
|
1561
|
-
project_root: projectRoot,
|
|
1562
|
-
actor: "human",
|
|
1563
|
-
source: "cli",
|
|
1564
|
-
})) as CliArtifact;
|
|
1565
|
-
human = `Captured: ${artifactLabel(result)}`;
|
|
1566
|
-
} else if (action === "list") {
|
|
1567
|
-
if (id) throw new Error("notes list accepts no positional arguments");
|
|
1568
|
-
result = (await client.call("notes.list", {
|
|
1569
|
-
project_root: projectRoot,
|
|
1570
|
-
...(status ? { status } : {}),
|
|
1571
|
-
...(text ? { text } : {}),
|
|
1572
|
-
...(limit === undefined ? {} : { limit }),
|
|
1573
|
-
})) as CliArtifact[];
|
|
1574
|
-
human = result.length > 0 ? result.map((note) => `[${note.status}] ${artifactLabel(note)}`).join("\n") : "No open notes.";
|
|
1575
|
-
} else if (action === "show") {
|
|
1576
|
-
if (!id || target) throw new Error("notes show requires exactly one note id");
|
|
1577
|
-
result = (await client.call("notes.show", { id, project_root: projectRoot })) as CliArtifact;
|
|
1578
|
-
human = `${artifactLabel(result)}\n\n${result.body ?? ""}`.trimEnd();
|
|
1579
|
-
} else if (action === "history") {
|
|
1580
|
-
if (!id || target) throw new Error("notes history requires exactly one note id");
|
|
1581
|
-
const page = await client.call<Record<string, unknown>, import("./domain/note-event.ts").NoteHistoryPage>("notes.history", {
|
|
1582
|
-
id,
|
|
1583
|
-
project_root: projectRoot,
|
|
1584
|
-
direction: "desc",
|
|
1585
|
-
...(limit === undefined ? {} : { limit }),
|
|
1586
|
-
});
|
|
1587
|
-
result = page;
|
|
1588
|
-
human =
|
|
1589
|
-
page.events.length === 0
|
|
1590
|
-
? `No recorded history for ${id}.`
|
|
1591
|
-
: [...page.events]
|
|
1592
|
-
.reverse()
|
|
1593
|
-
.map(
|
|
1594
|
-
(event) =>
|
|
1595
|
-
`${event.occurredAt} ${event.type} · ${event.actor}/${event.source}${event.relatedId ? ` · ${event.relatedId}` : ""}${event.disposition ? ` · ${event.disposition}` : ""}${event.reason ? ` · ${event.reason}` : ""}`,
|
|
1596
|
-
)
|
|
1597
|
-
.join("\n");
|
|
1598
|
-
} else if (action === "consume") {
|
|
1599
|
-
if (!id || target) throw new Error("notes consume requires exactly one note id");
|
|
1600
|
-
result = (await client.call("notes.consume", {
|
|
1601
|
-
id,
|
|
1602
|
-
project_root: projectRoot,
|
|
1603
|
-
actor: "agent",
|
|
1604
|
-
source: "cli",
|
|
1605
|
-
...(reason ? { reason } : {}),
|
|
1606
|
-
})) as CliArtifact;
|
|
1607
|
-
human = `Consumed: ${artifactLabel(result)}`;
|
|
1608
|
-
} else if (action === "promote") {
|
|
1609
|
-
if (!id || !target || positional.length !== 3) throw new Error("notes promote requires a note id and target artifact id");
|
|
1610
|
-
result = (await client.call("notes.promote", {
|
|
1611
|
-
id,
|
|
1612
|
-
target_id: target,
|
|
1613
|
-
project_root: projectRoot,
|
|
1614
|
-
actor: "agent",
|
|
1615
|
-
source: "cli",
|
|
1616
|
-
...(reason ? { reason } : {}),
|
|
1617
|
-
})) as CliArtifact;
|
|
1618
|
-
human = `Promoted: ${artifactLabel(result)} → ${target}`;
|
|
1619
|
-
} else if (action === "archive") {
|
|
1620
|
-
if (!id || !target || positional.length !== 3) throw new Error("notes archive requires a note id and disposition");
|
|
1621
|
-
result = (await client.call("notes.archive", {
|
|
1622
|
-
id,
|
|
1623
|
-
disposition: target,
|
|
1624
|
-
project_root: projectRoot,
|
|
1625
|
-
actor: "human",
|
|
1626
|
-
source: "cli",
|
|
1627
|
-
...(reason ? { reason } : {}),
|
|
1628
|
-
})) as CliArtifact;
|
|
1629
|
-
human = `Archived: ${artifactLabel(result)} · ${target}`;
|
|
1630
|
-
} else {
|
|
1631
|
-
throw new Error("notes action must be capture, list, show, history, consume, promote, or archive");
|
|
1632
|
-
}
|
|
1633
|
-
return json ? JSON.stringify(result) : human;
|
|
1634
|
-
}
|
|
1635
|
-
|
|
1636
|
-
export async function runTaskCli(args: string[], client: TaskCliClient, projectRoot: string = process.cwd()): Promise<string> {
|
|
1637
|
-
const json = args.includes("--json");
|
|
1638
|
-
const positional: string[] = [];
|
|
1639
|
-
const updateInput: { title?: string; body?: string; labels?: string[]; status?: "todo" } = {};
|
|
1640
|
-
let reason: string | undefined;
|
|
1641
|
-
let sessionId: string | undefined;
|
|
1642
|
-
let sessionSecret: string | undefined;
|
|
1643
|
-
let title: string | undefined;
|
|
1644
|
-
let body: string | undefined;
|
|
1645
|
-
let status: string | undefined;
|
|
1646
|
-
let labels: string[] | undefined;
|
|
1647
|
-
let extra: Record<string, unknown> | undefined;
|
|
1648
|
-
let gates: unknown[] | undefined;
|
|
1649
|
-
let checklist: Record<string, unknown> | undefined;
|
|
1650
|
-
let templateId: string | undefined;
|
|
1651
|
-
let parentId: string | undefined;
|
|
1652
|
-
let dependsOn: string[] | undefined;
|
|
1653
|
-
let text: string | undefined;
|
|
1654
|
-
let limit: number | undefined;
|
|
1655
|
-
let listScope: "project" | "graph" | "all" | undefined;
|
|
1656
|
-
let rootTaskId: string | undefined;
|
|
1657
|
-
let owner: string | undefined;
|
|
1658
|
-
let token: string | undefined;
|
|
1659
|
-
let ttlMs: number | undefined;
|
|
1660
|
-
let note: string | undefined;
|
|
1661
|
-
let cursor: number | undefined;
|
|
1662
|
-
let eventTypes: string[] | undefined;
|
|
1663
|
-
for (let index = 0; index < args.length; index++) {
|
|
1664
|
-
const argument = args[index]!;
|
|
1665
|
-
if (argument === "--json") continue;
|
|
1666
|
-
if (argument === "--session-id") {
|
|
1667
|
-
sessionId = args[++index];
|
|
1668
|
-
if (!sessionId) throw new Error("--session-id requires a value");
|
|
1669
|
-
continue;
|
|
1670
|
-
}
|
|
1671
|
-
if (argument === "--session-secret") {
|
|
1672
|
-
sessionSecret = args[++index];
|
|
1673
|
-
if (!sessionSecret) throw new Error("--session-secret requires a value");
|
|
1674
|
-
continue;
|
|
1675
|
-
}
|
|
1676
|
-
if (
|
|
1677
|
-
argument === "--title" ||
|
|
1678
|
-
argument === "--body" ||
|
|
1679
|
-
argument === "--labels-json" ||
|
|
1680
|
-
argument === "--status" ||
|
|
1681
|
-
argument === "--reason"
|
|
1682
|
-
) {
|
|
1683
|
-
const value = args[++index];
|
|
1684
|
-
if (value === undefined) throw new Error(`${argument} requires a value`);
|
|
1685
|
-
if (argument === "--title") {
|
|
1686
|
-
updateInput.title = value;
|
|
1687
|
-
title = value;
|
|
1688
|
-
} else if (argument === "--body") {
|
|
1689
|
-
updateInput.body = value;
|
|
1690
|
-
body = value;
|
|
1691
|
-
} else if (argument === "--reason") reason = value;
|
|
1692
|
-
else if (argument === "--status") {
|
|
1693
|
-
status = value;
|
|
1694
|
-
if (value === "todo") updateInput.status = value;
|
|
1695
|
-
} else {
|
|
1696
|
-
labels = parseJsonStringArrayFlag(value, "--labels-json");
|
|
1697
|
-
updateInput.labels = labels;
|
|
1698
|
-
}
|
|
1699
|
-
continue;
|
|
1700
|
-
}
|
|
1701
|
-
if (argument === "--extra-json") {
|
|
1702
|
-
extra = parseJsonObjectFlag(args[++index]!, "--extra-json");
|
|
1703
|
-
continue;
|
|
1704
|
-
}
|
|
1705
|
-
if (argument === "--gates-json") {
|
|
1706
|
-
const value = args[++index];
|
|
1707
|
-
if (!value) throw new Error("--gates-json requires a value");
|
|
1708
|
-
const parsed = JSON.parse(value) as unknown;
|
|
1709
|
-
if (!Array.isArray(parsed)) throw new Error("--gates-json must be a JSON array");
|
|
1710
|
-
gates = parsed;
|
|
1711
|
-
continue;
|
|
1712
|
-
}
|
|
1713
|
-
if (argument === "--checklist-json") {
|
|
1714
|
-
checklist = parseJsonObjectFlag(args[++index]!, "--checklist-json");
|
|
1715
|
-
continue;
|
|
1716
|
-
}
|
|
1717
|
-
if (argument === "--template-id") {
|
|
1718
|
-
templateId = args[++index];
|
|
1719
|
-
if (!templateId) throw new Error("--template-id requires a value");
|
|
1720
|
-
continue;
|
|
1721
|
-
}
|
|
1722
|
-
if (argument === "--parent-id") {
|
|
1723
|
-
parentId = args[++index];
|
|
1724
|
-
if (!parentId) throw new Error("--parent-id requires a value");
|
|
1725
|
-
continue;
|
|
1726
|
-
}
|
|
1727
|
-
if (argument === "--depends-on-json") {
|
|
1728
|
-
dependsOn = parseJsonStringArrayFlag(args[++index]!, "--depends-on-json");
|
|
1729
|
-
continue;
|
|
1730
|
-
}
|
|
1731
|
-
if (argument === "--text") {
|
|
1732
|
-
text = args[++index];
|
|
1733
|
-
if (text === undefined) throw new Error("--text requires a value");
|
|
1734
|
-
continue;
|
|
1735
|
-
}
|
|
1736
|
-
if (argument === "--limit") {
|
|
1737
|
-
const value = args[++index];
|
|
1738
|
-
if (!value || Number.isNaN(Number(value))) throw new Error("--limit requires a numeric value");
|
|
1739
|
-
limit = Number(value);
|
|
1740
|
-
continue;
|
|
1741
|
-
}
|
|
1742
|
-
if (argument === "--scope") {
|
|
1743
|
-
const value = args[++index];
|
|
1744
|
-
if (value !== "project" && value !== "graph" && value !== "all") throw new Error("--scope must be project, graph, or all");
|
|
1745
|
-
listScope = value;
|
|
1746
|
-
continue;
|
|
1747
|
-
}
|
|
1748
|
-
if (argument === "--root-task-id") {
|
|
1749
|
-
rootTaskId = args[++index];
|
|
1750
|
-
if (!rootTaskId) throw new Error("--root-task-id requires a value");
|
|
1751
|
-
continue;
|
|
1752
|
-
}
|
|
1753
|
-
if (argument === "--owner") {
|
|
1754
|
-
owner = args[++index];
|
|
1755
|
-
if (!owner) throw new Error("--owner requires a value");
|
|
1756
|
-
continue;
|
|
1757
|
-
}
|
|
1758
|
-
if (argument === "--token") {
|
|
1759
|
-
token = args[++index];
|
|
1760
|
-
if (!token) throw new Error("--token requires a value");
|
|
1761
|
-
continue;
|
|
1762
|
-
}
|
|
1763
|
-
if (argument === "--note") {
|
|
1764
|
-
note = args[++index];
|
|
1765
|
-
if (note === undefined) throw new Error("--note requires a value");
|
|
1766
|
-
continue;
|
|
1767
|
-
}
|
|
1768
|
-
if (argument === "--ttl-ms") {
|
|
1769
|
-
const value = args[++index];
|
|
1770
|
-
if (!value || Number.isNaN(Number(value))) throw new Error("--ttl-ms requires a numeric value");
|
|
1771
|
-
ttlMs = Number(value);
|
|
1772
|
-
continue;
|
|
1773
|
-
}
|
|
1774
|
-
if (argument === "--cursor") {
|
|
1775
|
-
const value = args[++index];
|
|
1776
|
-
if (!value || Number.isNaN(Number(value))) throw new Error("--cursor requires a numeric value");
|
|
1777
|
-
cursor = Number(value);
|
|
1778
|
-
continue;
|
|
1779
|
-
}
|
|
1780
|
-
if (argument === "--event-types-json") {
|
|
1781
|
-
eventTypes = parseJsonStringArrayFlag(args[++index]!, "--event-types-json");
|
|
1782
|
-
continue;
|
|
1783
|
-
}
|
|
1784
|
-
if (argument.startsWith("--")) throw new Error(`unknown tasks option ${argument}`);
|
|
1785
|
-
positional.push(argument);
|
|
1786
|
-
}
|
|
1787
|
-
const [action, id, dependencyId] = positional;
|
|
1788
|
-
const reasonSupportedActions = new Set(["update", "depend", "undepend", "contain", "uncontain"]);
|
|
1789
|
-
if (reason !== undefined && !reasonSupportedActions.has(action ?? ""))
|
|
1790
|
-
throw new Error("--reason is only supported by tasks update, depend, undepend, contain, and uncontain");
|
|
1791
|
-
const sessionScope = sessionId ? { session_id: sessionId } : {};
|
|
1792
|
-
// Only meaningful alongside a registered session_id (see session.register); required by the
|
|
1793
|
-
// daemon only for the specific Focus-mutating operations below (focus/pause/unpause/clear_focus)
|
|
1794
|
-
// once that session_id has been armored (see session-identity-service.ts assertAuthorized).
|
|
1795
|
-
const sessionSecretField = sessionSecret ? { session_secret: sessionSecret } : {};
|
|
1796
|
-
let result: unknown;
|
|
1797
|
-
let human: string;
|
|
1798
|
-
switch (action) {
|
|
1799
|
-
case "active": {
|
|
1800
|
-
if (id) throw new Error("tasks active accepts no positional arguments");
|
|
1801
|
-
const active = await client.call<Record<string, unknown>, CliArtifact | null>("tasks.active", {
|
|
1802
|
-
project_root: projectRoot,
|
|
1803
|
-
...sessionScope,
|
|
1804
|
-
});
|
|
1805
|
-
result = active;
|
|
1806
|
-
human = active ? `Active: ${artifactLabel(active)}` : "No active task.";
|
|
1807
|
-
break;
|
|
1808
|
-
}
|
|
1809
|
-
case "focused": {
|
|
1810
|
-
if (id) throw new Error("tasks focused accepts no positional arguments");
|
|
1811
|
-
const focus = await client.call<
|
|
1812
|
-
Record<string, unknown>,
|
|
1813
|
-
{ artifact: CliArtifact; status: "active" | "paused"; updatedAt: string } | null
|
|
1814
|
-
>("tasks.focused", { project_root: projectRoot, ...sessionScope });
|
|
1815
|
-
result = focus;
|
|
1816
|
-
human = focus ? `Focused (${focus.status}): ${artifactLabel(focus.artifact)}` : "No focused task.";
|
|
1817
|
-
break;
|
|
1818
|
-
}
|
|
1819
|
-
case "pause":
|
|
1820
|
-
case "unpause": {
|
|
1821
|
-
if (id) throw new Error(`tasks ${action} accepts no positional arguments`);
|
|
1822
|
-
const operation = action === "pause" ? "tasks.pause" : "tasks.unpause";
|
|
1823
|
-
const focus = await client.call<Record<string, unknown>, { artifact: CliArtifact; status: string }>(operation, {
|
|
1824
|
-
actor: "user",
|
|
1825
|
-
source: "cli",
|
|
1826
|
-
...sessionScope,
|
|
1827
|
-
...sessionSecretField,
|
|
1828
|
-
});
|
|
1829
|
-
result = focus;
|
|
1830
|
-
human = `Focused (${focus.status}): ${artifactLabel(focus.artifact)}`;
|
|
1831
|
-
break;
|
|
1832
|
-
}
|
|
1833
|
-
case "clear-focus": {
|
|
1834
|
-
if (id) throw new Error("tasks clear-focus accepts no positional arguments");
|
|
1835
|
-
const cleared = await client.call<Record<string, unknown>, { cleared: boolean }>("tasks.clear_focus", {
|
|
1836
|
-
actor: "user",
|
|
1837
|
-
source: "cli",
|
|
1838
|
-
...sessionScope,
|
|
1839
|
-
...sessionSecretField,
|
|
1840
|
-
});
|
|
1841
|
-
result = cleared;
|
|
1842
|
-
human = cleared.cleared ? "Task focus cleared." : "No focused task.";
|
|
1843
|
-
break;
|
|
1844
|
-
}
|
|
1845
|
-
case "reap-stale-focus": {
|
|
1846
|
-
if (id) throw new Error("tasks reap-stale-focus accepts no positional arguments");
|
|
1847
|
-
const reaped = await client.call<Record<string, unknown>, { removed: number }>("tasks.reap_stale_focus", {});
|
|
1848
|
-
result = reaped;
|
|
1849
|
-
human = `Reaped ${reaped.removed} stale Focus scope(s).`;
|
|
1850
|
-
break;
|
|
1851
|
-
}
|
|
1852
|
-
case "claim": {
|
|
1853
|
-
if (!id || dependencyId) throw new Error("tasks claim requires exactly one task id");
|
|
1854
|
-
if (!owner) throw new Error("tasks claim requires --owner");
|
|
1855
|
-
const lease = await client.call<Record<string, unknown>, CliTaskLease>("tasks.claim", { id, owner, ttl_ms: ttlMs, note });
|
|
1856
|
-
result = lease;
|
|
1857
|
-
human = `Claimed by "${lease.owner}" until ${lease.leaseExpiresAt} (token ${lease.token}).`;
|
|
1858
|
-
break;
|
|
1859
|
-
}
|
|
1860
|
-
case "heartbeat-lease": {
|
|
1861
|
-
if (!id || dependencyId) throw new Error("tasks heartbeat-lease requires exactly one task id");
|
|
1862
|
-
if (!owner || !token) throw new Error("tasks heartbeat-lease requires --owner and --token");
|
|
1863
|
-
const lease = await client.call<Record<string, unknown>, CliTaskLease>("tasks.heartbeat_lease", { id, owner, token, ttl_ms: ttlMs });
|
|
1864
|
-
result = lease;
|
|
1865
|
-
human = `Renewed until ${lease.leaseExpiresAt}.`;
|
|
1866
|
-
break;
|
|
1867
|
-
}
|
|
1868
|
-
case "release-lease": {
|
|
1869
|
-
if (!id || dependencyId) throw new Error("tasks release-lease requires exactly one task id");
|
|
1870
|
-
if (!owner || !token) throw new Error("tasks release-lease requires --owner and --token");
|
|
1871
|
-
const released = await client.call<Record<string, unknown>, { released: boolean }>("tasks.release_lease", { id, owner, token });
|
|
1872
|
-
result = released;
|
|
1873
|
-
human = released.released ? "Lease released." : "No live lease to release.";
|
|
1874
|
-
break;
|
|
1875
|
-
}
|
|
1876
|
-
case "lease": {
|
|
1877
|
-
if (!id || dependencyId) throw new Error("tasks lease requires exactly one task id");
|
|
1878
|
-
const lease = await client.call<Record<string, unknown>, CliTaskLease | null>("tasks.lease", { id });
|
|
1879
|
-
result = lease;
|
|
1880
|
-
human = lease ? `Leased by "${lease.owner}" until ${lease.leaseExpiresAt}.` : "No live lease.";
|
|
1881
|
-
break;
|
|
1882
|
-
}
|
|
1883
|
-
case "reap-stale-leases": {
|
|
1884
|
-
if (id) throw new Error("tasks reap-stale-leases accepts no positional arguments");
|
|
1885
|
-
const reaped = await client.call<Record<string, unknown>, { removed: number }>("tasks.reap_stale_leases", {});
|
|
1886
|
-
result = reaped;
|
|
1887
|
-
human = `Reaped ${reaped.removed} expired lease(s).`;
|
|
1888
|
-
break;
|
|
1889
|
-
}
|
|
1890
|
-
case "event-feed": {
|
|
1891
|
-
if (id) throw new Error("tasks event-feed accepts no positional arguments");
|
|
1892
|
-
const page = await client.call<Record<string, unknown>, import("./domain/task-event.ts").TaskEventFeedPage>("tasks.event_feed", {
|
|
1893
|
-
cursor,
|
|
1894
|
-
limit,
|
|
1895
|
-
event_types: eventTypes,
|
|
1896
|
-
});
|
|
1897
|
-
result = page;
|
|
1898
|
-
human =
|
|
1899
|
-
page.events.length === 0
|
|
1900
|
-
? "No events."
|
|
1901
|
-
: page.events.map((event) => `${event.id} ${event.occurredAt} ${event.taskId} ${event.type}`).join("\n");
|
|
1902
|
-
break;
|
|
1903
|
-
}
|
|
1904
|
-
case "create": {
|
|
1905
|
-
if (id) throw new Error("tasks create accepts no positional arguments");
|
|
1906
|
-
if (!title) throw new Error("tasks create requires --title");
|
|
1907
|
-
const artifact = await client.call<Record<string, unknown>, CliArtifact>("tasks.create", {
|
|
1908
|
-
title,
|
|
1909
|
-
body,
|
|
1910
|
-
status,
|
|
1911
|
-
labels,
|
|
1912
|
-
extra,
|
|
1913
|
-
gates,
|
|
1914
|
-
checklist,
|
|
1915
|
-
template_id: templateId,
|
|
1916
|
-
parent_id: parentId,
|
|
1917
|
-
depends_on: dependsOn,
|
|
1918
|
-
project_root: projectRoot,
|
|
1919
|
-
actor: "user",
|
|
1920
|
-
source: "cli",
|
|
1921
|
-
...sessionScope,
|
|
1922
|
-
});
|
|
1923
|
-
result = artifact;
|
|
1924
|
-
human = `Created task: ${artifactLabel(artifact)}`;
|
|
1925
|
-
break;
|
|
1926
|
-
}
|
|
1927
|
-
case "list": {
|
|
1928
|
-
if (id) throw new Error("tasks list accepts no positional arguments");
|
|
1929
|
-
const rows = await client.call<Record<string, unknown>, CliArtifact[]>("tasks.list", {
|
|
1930
|
-
status,
|
|
1931
|
-
text,
|
|
1932
|
-
limit,
|
|
1933
|
-
labels,
|
|
1934
|
-
project_root: projectRoot,
|
|
1935
|
-
scope: listScope,
|
|
1936
|
-
root_task_id: rootTaskId,
|
|
1937
|
-
...sessionScope,
|
|
1938
|
-
});
|
|
1939
|
-
result = rows;
|
|
1940
|
-
human = rows.length === 0 ? "No tasks found." : rows.map((row) => artifactLabel(row)).join("\n");
|
|
1941
|
-
break;
|
|
1942
|
-
}
|
|
1943
|
-
case "show": {
|
|
1944
|
-
if (!id || dependencyId) throw new Error("tasks show requires exactly one task id");
|
|
1945
|
-
const artifact = await client.call<Record<string, unknown>, CliArtifact>("tasks.show", { id });
|
|
1946
|
-
result = artifact;
|
|
1947
|
-
human = `${artifactLabel(artifact)}\n\n${artifact.body ?? ""}`;
|
|
1948
|
-
break;
|
|
1949
|
-
}
|
|
1950
|
-
case "run-gates": {
|
|
1951
|
-
if (!id || dependencyId) throw new Error("tasks run-gates requires exactly one task id");
|
|
1952
|
-
const results = await client.call<Record<string, unknown>, GateResult[]>("tasks.run_gates", { id, actor: "user", source: "cli" });
|
|
1953
|
-
result = results;
|
|
1954
|
-
human =
|
|
1955
|
-
results.length === 0
|
|
1956
|
-
? "No gates configured."
|
|
1957
|
-
: results.map((gate) => `${gate.passed ? "✓" : "✗"} ${gate.gate.type}: ${gate.gate.target} — ${gate.output}`).join("\n");
|
|
1958
|
-
break;
|
|
1959
|
-
}
|
|
1960
|
-
case "set-checklist": {
|
|
1961
|
-
if (!id || dependencyId) throw new Error("tasks set-checklist requires exactly one task id");
|
|
1962
|
-
if (!checklist) throw new Error("tasks set-checklist requires --checklist-json");
|
|
1963
|
-
const artifact = await client.call<Record<string, unknown>, CliArtifact>("tasks.set_checklist", { id, checklist });
|
|
1964
|
-
result = artifact;
|
|
1965
|
-
human = `Updated checklist: ${artifactLabel(artifact)}`;
|
|
1966
|
-
break;
|
|
1967
|
-
}
|
|
1968
|
-
case "set-gates": {
|
|
1969
|
-
if (!id || dependencyId) throw new Error("tasks set-gates requires exactly one task id");
|
|
1970
|
-
if (!gates) throw new Error("tasks set-gates requires --gates-json");
|
|
1971
|
-
const artifact = await client.call<Record<string, unknown>, CliArtifact>("tasks.set_gates", { id, gates });
|
|
1972
|
-
result = artifact;
|
|
1973
|
-
human = `Updated gates: ${artifactLabel(artifact)}`;
|
|
1974
|
-
break;
|
|
1975
|
-
}
|
|
1976
|
-
case "context": {
|
|
1977
|
-
if (id) throw new Error("tasks context accepts no positional arguments");
|
|
1978
|
-
const summary = await client.call<Record<string, unknown>, string | null>("tasks.context", {
|
|
1979
|
-
project_root: projectRoot,
|
|
1980
|
-
scope: listScope,
|
|
1981
|
-
root_task_id: rootTaskId,
|
|
1982
|
-
...sessionScope,
|
|
1983
|
-
});
|
|
1984
|
-
result = summary;
|
|
1985
|
-
human = summary ?? "No open tasks.";
|
|
1986
|
-
break;
|
|
1987
|
-
}
|
|
1988
|
-
case "contain": {
|
|
1989
|
-
if (!id || !dependencyId || positional.length !== 3) throw new Error("tasks contain requires a parent id and child id");
|
|
1990
|
-
const artifact = await client.call<Record<string, unknown>, CliArtifact>("tasks.contain", {
|
|
1991
|
-
parent_id: id,
|
|
1992
|
-
child_id: dependencyId,
|
|
1993
|
-
actor: "user",
|
|
1994
|
-
source: "cli",
|
|
1995
|
-
...(reason ? { reason } : {}),
|
|
1996
|
-
...sessionScope,
|
|
1997
|
-
});
|
|
1998
|
-
result = artifact;
|
|
1999
|
-
human = `Contained: ${dependencyId} → ${artifactLabel(artifact)}`;
|
|
2000
|
-
break;
|
|
2001
|
-
}
|
|
2002
|
-
case "uncontain": {
|
|
2003
|
-
if (!id || !dependencyId || positional.length !== 3) throw new Error("tasks uncontain requires a parent id and child id");
|
|
2004
|
-
const artifact = await client.call<Record<string, unknown>, CliArtifact>("tasks.uncontain", {
|
|
2005
|
-
parent_id: id,
|
|
2006
|
-
child_id: dependencyId,
|
|
2007
|
-
actor: "user",
|
|
2008
|
-
source: "cli",
|
|
2009
|
-
...(reason ? { reason } : {}),
|
|
2010
|
-
...sessionScope,
|
|
2011
|
-
});
|
|
2012
|
-
result = artifact;
|
|
2013
|
-
human = `Removed ${dependencyId} from ${artifactLabel(artifact)}`;
|
|
2014
|
-
break;
|
|
2015
|
-
}
|
|
2016
|
-
case "update": {
|
|
2017
|
-
if (!id || dependencyId) throw new Error("tasks update requires exactly one task id");
|
|
2018
|
-
if (status !== undefined && status !== "todo")
|
|
2019
|
-
throw new Error("tasks update --status only supports todo for accidental creation recovery");
|
|
2020
|
-
if (Object.keys(updateInput).length === 0) throw new Error("tasks update requires --title, --body, --labels-json, or --status todo");
|
|
2021
|
-
if (updateInput.status !== undefined && !reason?.trim()) throw new Error("tasks update --status requires --reason");
|
|
2022
|
-
if (reason !== undefined && updateInput.status === undefined) throw new Error("tasks update --reason requires --status todo");
|
|
2023
|
-
const artifact = await client.call<Record<string, unknown>, CliArtifact>("tasks.update", {
|
|
2024
|
-
id,
|
|
2025
|
-
...updateInput,
|
|
2026
|
-
...(reason ? { reason } : {}),
|
|
2027
|
-
actor: "user",
|
|
2028
|
-
source: "cli",
|
|
2029
|
-
});
|
|
2030
|
-
result = artifact;
|
|
2031
|
-
human = `Updated: ${artifactLabel(artifact)}`;
|
|
2032
|
-
break;
|
|
2033
|
-
}
|
|
2034
|
-
case "history": {
|
|
2035
|
-
if (!id || dependencyId) throw new Error("tasks history requires exactly one task id");
|
|
2036
|
-
const page = await client.call<{ id: string; direction: "desc" }, import("./domain/task-event.ts").TaskHistoryPage>("tasks.history", {
|
|
2037
|
-
id,
|
|
2038
|
-
direction: "desc",
|
|
2039
|
-
});
|
|
2040
|
-
result = page;
|
|
2041
|
-
human =
|
|
2042
|
-
page.events.length === 0
|
|
2043
|
-
? `No recorded history for ${id}.`
|
|
2044
|
-
: [...page.events]
|
|
2045
|
-
.reverse()
|
|
2046
|
-
.map(
|
|
2047
|
-
(event) =>
|
|
2048
|
-
`${event.occurredAt} ${event.type} ${event.fromStatus ?? "∅"} → ${event.toStatus ?? "∅"} · ${event.actor}/${event.source}${event.reason ? ` · ${event.reason}` : ""}`,
|
|
2049
|
-
)
|
|
2050
|
-
.join("\n");
|
|
2051
|
-
break;
|
|
2052
|
-
}
|
|
2053
|
-
case "scope": {
|
|
2054
|
-
if (!id) {
|
|
2055
|
-
const selection = await client.call<Record<string, string>, import("./domain/task-scope.ts").TaskViewSelection>("tasks.scope", {
|
|
2056
|
-
project_root: projectRoot,
|
|
2057
|
-
});
|
|
2058
|
-
result = selection;
|
|
2059
|
-
human = `Task scope: ${selection.label}`;
|
|
2060
|
-
break;
|
|
2061
|
-
}
|
|
2062
|
-
if (id !== "project" && id !== "all" && id !== "graph") throw new Error("tasks scope mode must be project, all, or graph");
|
|
2063
|
-
if (id === "graph" && !dependencyId) throw new Error("tasks scope graph requires a root task id");
|
|
2064
|
-
if (id !== "graph" && dependencyId) throw new Error(`tasks scope ${id} accepts no root task id`);
|
|
2065
|
-
const selection = await client.call<Record<string, unknown>, import("./domain/task-scope.ts").TaskViewSelection>("tasks.set_scope", {
|
|
2066
|
-
project_root: projectRoot,
|
|
2067
|
-
scope: id,
|
|
2068
|
-
...(dependencyId ? { root_task_id: dependencyId } : {}),
|
|
2069
|
-
});
|
|
2070
|
-
result = selection;
|
|
2071
|
-
human = `Task scope: ${selection.label}`;
|
|
2072
|
-
break;
|
|
2073
|
-
}
|
|
2074
|
-
case "assign-project": {
|
|
2075
|
-
if (!id || positional.length > 3) throw new Error("tasks assign-project requires a task id and optional project root");
|
|
2076
|
-
const artifact = await client.call<Record<string, unknown>, CliArtifact>("tasks.assign_project", {
|
|
2077
|
-
id,
|
|
2078
|
-
project_root: dependencyId ?? projectRoot,
|
|
2079
|
-
actor: "user",
|
|
2080
|
-
source: "cli",
|
|
2081
|
-
});
|
|
2082
|
-
result = artifact;
|
|
2083
|
-
human = `Project assigned: ${artifactLabel(artifact)}`;
|
|
2084
|
-
break;
|
|
2085
|
-
}
|
|
2086
|
-
case "focus": {
|
|
2087
|
-
if (!id || dependencyId) throw new Error("tasks focus requires exactly one task id");
|
|
2088
|
-
const active = await client.call<Record<string, unknown>, CliArtifact>("tasks.focus", {
|
|
2089
|
-
id,
|
|
2090
|
-
actor: "user",
|
|
2091
|
-
source: "cli",
|
|
2092
|
-
...sessionScope,
|
|
2093
|
-
...sessionSecretField,
|
|
2094
|
-
});
|
|
2095
|
-
result = active;
|
|
2096
|
-
human = `Active: ${artifactLabel(active)}`;
|
|
2097
|
-
break;
|
|
2098
|
-
}
|
|
2099
|
-
case "graph": {
|
|
2100
|
-
if (id) throw new Error("tasks graph accepts no positional arguments");
|
|
2101
|
-
const graph = await client.call<
|
|
2102
|
-
Record<string, unknown>,
|
|
2103
|
-
{
|
|
2104
|
-
nodes: Array<{ dependencyIds: string[]; childIds: string[] }>;
|
|
2105
|
-
rootIds: string[];
|
|
2106
|
-
}
|
|
2107
|
-
>("tasks.graph", {
|
|
2108
|
-
limit: TASK_EXECUTION_MAX_NODES + 1,
|
|
2109
|
-
labels,
|
|
2110
|
-
project_root: projectRoot,
|
|
2111
|
-
scope: listScope,
|
|
2112
|
-
root_task_id: rootTaskId,
|
|
2113
|
-
...sessionScope,
|
|
2114
|
-
});
|
|
2115
|
-
result = graph;
|
|
2116
|
-
const dependencies = graph.nodes.reduce((count, node) => count + node.dependencyIds.length, 0);
|
|
2117
|
-
const children = graph.nodes.reduce((count, node) => count + node.childIds.length, 0);
|
|
2118
|
-
human = `Task graph: ${graph.nodes.length} nodes, ${graph.rootIds.length} roots, ${dependencies} dependencies, ${children} containment edges`;
|
|
2119
|
-
break;
|
|
2120
|
-
}
|
|
2121
|
-
case "plan": {
|
|
2122
|
-
if (id) throw new Error("tasks plan accepts no positional arguments");
|
|
2123
|
-
const plan = await client.call<Record<string, unknown>, TaskExecutionPlan>("tasks.plan", {
|
|
2124
|
-
project_root: projectRoot,
|
|
2125
|
-
...sessionScope,
|
|
2126
|
-
});
|
|
2127
|
-
result = plan;
|
|
2128
|
-
human = planText(plan);
|
|
2129
|
-
break;
|
|
2130
|
-
}
|
|
2131
|
-
case "complete": {
|
|
2132
|
-
if (!id || dependencyId) throw new Error("tasks complete requires exactly one task id");
|
|
2133
|
-
const completion = await client.call<Record<string, unknown>, CliCompletion>("tasks.complete", {
|
|
2134
|
-
id,
|
|
2135
|
-
actor: "user",
|
|
2136
|
-
source: "cli",
|
|
2137
|
-
...sessionScope,
|
|
2138
|
-
});
|
|
2139
|
-
result = completion;
|
|
2140
|
-
const lines = [`${completion.completed ? "Completed" : "Rejected"}: ${artifactLabel(completion.artifact)}`];
|
|
2141
|
-
if (completion.focused) lines.push(`Active: ${artifactLabel(completion.focused)}`);
|
|
2142
|
-
if (completion.blocked.length > 0) {
|
|
2143
|
-
lines.push(
|
|
2144
|
-
`Blocked: ${completion.blocked.map((entry) => `${artifactLabel(entry.artifact)} waits for ${entry.dependencyIds.join(", ")}`).join("; ")}`,
|
|
2145
|
-
);
|
|
2146
|
-
}
|
|
2147
|
-
for (const gate of completion.gates) lines.push(`${gate.passed ? "✓" : "✗"} ${gate.gate.type}: ${gate.gate.target} — ${gate.output}`);
|
|
2148
|
-
human = lines.join("\n");
|
|
2149
|
-
break;
|
|
2150
|
-
}
|
|
2151
|
-
case "start": {
|
|
2152
|
-
if (!id || dependencyId) throw new Error("tasks start requires exactly one task id");
|
|
2153
|
-
const artifact = await client.call<Record<string, unknown>, CliArtifact>("tasks.start", {
|
|
2154
|
-
id,
|
|
2155
|
-
actor: "user",
|
|
2156
|
-
source: "cli",
|
|
2157
|
-
...sessionScope,
|
|
2158
|
-
});
|
|
2159
|
-
result = artifact;
|
|
2160
|
-
human = `Started: ${artifactLabel(artifact)}`;
|
|
2161
|
-
break;
|
|
2162
|
-
}
|
|
2163
|
-
case "submit":
|
|
2164
|
-
case "reject":
|
|
2165
|
-
case "retry":
|
|
2166
|
-
case "cancel": {
|
|
2167
|
-
if (!id || dependencyId) throw new Error(`tasks ${action} requires exactly one task id`);
|
|
2168
|
-
const operation = `tasks.${action}` as "tasks.submit" | "tasks.reject" | "tasks.retry" | "tasks.cancel";
|
|
2169
|
-
const artifact = await client.call<Record<string, unknown>, CliArtifact>(operation, {
|
|
2170
|
-
id,
|
|
2171
|
-
actor: "user",
|
|
2172
|
-
source: "cli",
|
|
2173
|
-
...sessionScope,
|
|
2174
|
-
});
|
|
2175
|
-
result = artifact;
|
|
2176
|
-
human = `${action[0]!.toUpperCase()}${action.slice(1)}: ${artifactLabel(artifact)}`;
|
|
2177
|
-
break;
|
|
2178
|
-
}
|
|
2179
|
-
case "cancel-subtree": {
|
|
2180
|
-
if (!id || dependencyId) throw new Error("tasks cancel-subtree requires exactly one task id");
|
|
2181
|
-
const outcome = await client.call<Record<string, unknown>, { canceled: string[]; skipped: string[] }>("tasks.cancel_subtree", {
|
|
2182
|
-
id,
|
|
2183
|
-
actor: "user",
|
|
2184
|
-
source: "cli",
|
|
2185
|
-
...sessionScope,
|
|
2186
|
-
});
|
|
2187
|
-
result = outcome;
|
|
2188
|
-
human = `Canceled ${outcome.canceled.length} task(s)${outcome.skipped.length > 0 ? `, skipped ${outcome.skipped.length} already-terminal` : ""}.`;
|
|
2189
|
-
break;
|
|
2190
|
-
}
|
|
2191
|
-
case "depend": {
|
|
2192
|
-
if (!id || !dependencyId || positional.length !== 3) throw new Error("tasks depend requires a task id and prerequisite id");
|
|
2193
|
-
const artifact = await client.call<Record<string, unknown>, CliArtifact>("tasks.depend", {
|
|
2194
|
-
id,
|
|
2195
|
-
dependency_id: dependencyId,
|
|
2196
|
-
actor: "user",
|
|
2197
|
-
source: "cli",
|
|
2198
|
-
...(reason ? { reason } : {}),
|
|
2199
|
-
...sessionScope,
|
|
2200
|
-
});
|
|
2201
|
-
result = artifact;
|
|
2202
|
-
human = `Dependency added: ${artifactLabel(artifact)} waits for ${dependencyId}`;
|
|
2203
|
-
break;
|
|
2204
|
-
}
|
|
2205
|
-
case "undepend": {
|
|
2206
|
-
if (!id || !dependencyId || positional.length !== 3) throw new Error("tasks undepend requires a task id and prerequisite id");
|
|
2207
|
-
const artifact = await client.call<Record<string, unknown>, CliArtifact>("tasks.undepend", {
|
|
2208
|
-
id,
|
|
2209
|
-
dependency_id: dependencyId,
|
|
2210
|
-
actor: "user",
|
|
2211
|
-
source: "cli",
|
|
2212
|
-
...(reason ? { reason } : {}),
|
|
2213
|
-
...sessionScope,
|
|
2214
|
-
});
|
|
2215
|
-
result = artifact;
|
|
2216
|
-
human = `Dependency removed: ${artifactLabel(artifact)} no longer waits for ${dependencyId}`;
|
|
2217
|
-
break;
|
|
2218
|
-
}
|
|
2219
|
-
default:
|
|
2220
|
-
throw new Error(
|
|
2221
|
-
"tasks action must be create, list, show, active, focused, focus, pause, unpause, clear-focus, update, graph, plan, context, history, scope, assign-project, complete, start, submit, reject, retry, cancel, cancel-subtree, depend, undepend, contain, uncontain, run-gates, set-checklist, or set-gates",
|
|
2222
|
-
);
|
|
2223
|
-
}
|
|
2224
|
-
return json ? JSON.stringify(result) : human;
|
|
2225
|
-
}
|
|
330
|
+
export {
|
|
331
|
+
runArtifactCli,
|
|
332
|
+
runDiscussCli,
|
|
333
|
+
runDocsCli,
|
|
334
|
+
runGatesCli,
|
|
335
|
+
runGraphCli,
|
|
336
|
+
runGraphProjectionCli,
|
|
337
|
+
runLogCli,
|
|
338
|
+
runNoteCli,
|
|
339
|
+
runPlaybooksCli,
|
|
340
|
+
runRulesCli,
|
|
341
|
+
runSessionIdentityCli,
|
|
342
|
+
runTaskCli,
|
|
343
|
+
};
|
|
2226
344
|
|
|
2227
345
|
export async function main(args: string[] = process.argv.slice(2)): Promise<void> {
|
|
2228
346
|
const [command, action] = args;
|