@danypops/papyrus 0.11.3 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +16 -2
  2. package/extension/src/active-task-continuation.ts +6 -0
  3. package/extension/src/domain-tools.ts +108 -52
  4. package/extension/src/index.ts +90 -37
  5. package/extension/src/notes.ts +14 -1
  6. package/extension/src/task-focus-events.ts +57 -0
  7. package/extension/src/tasks.ts +51 -15
  8. package/extension/src/tool-rendering/artifact-card.ts +117 -0
  9. package/extension/src/tool-rendering/artifact-list.ts +179 -0
  10. package/extension/src/tool-rendering/index.ts +107 -0
  11. package/extension/src/tool-rendering/render-model.ts +406 -0
  12. package/package.json +4 -2
  13. package/src/adapters/in-memory-conversation-journal-store.ts +48 -0
  14. package/src/adapters/sqlite-artifact-scope-store.ts +36 -0
  15. package/src/adapters/sqlite-artifact-store.ts +20 -11
  16. package/src/adapters/sqlite-discourse-store.ts +325 -0
  17. package/src/adapters/sqlite-graph-projection-store.ts +41 -0
  18. package/src/adapters/sqlite-task-focus-store.ts +34 -15
  19. package/src/authority-registry.ts +115 -0
  20. package/src/cli.ts +904 -124
  21. package/src/constants.ts +38 -5
  22. package/src/conversation-journal-service.ts +87 -0
  23. package/src/db.ts +336 -8
  24. package/src/domain/artifact-event.ts +99 -0
  25. package/src/domain/conversation-journal.ts +168 -0
  26. package/src/domain/discourse-store.ts +142 -0
  27. package/src/domain/graph-projection.ts +74 -0
  28. package/src/domain/task-event.ts +4 -0
  29. package/src/domain-services.ts +133 -38
  30. package/src/graph-projection-service.ts +103 -0
  31. package/src/id-migration.ts +200 -0
  32. package/src/module-registry.ts +53 -0
  33. package/src/modules/docs.ts +77 -0
  34. package/src/modules/graph-projection.ts +82 -0
  35. package/src/modules/notes.ts +76 -0
  36. package/src/modules/rules.ts +81 -0
  37. package/src/modules/skills.ts +113 -0
  38. package/src/modules/tasks.ts +164 -0
  39. package/src/ops.ts +142 -15
  40. package/src/ports/artifact-scope-store.ts +20 -0
  41. package/src/ports/artifact-store.ts +10 -5
  42. package/src/ports/conversation-journal-store.ts +17 -0
  43. package/src/ports/graph-projection-store.ts +15 -0
  44. package/src/ports/task-focus-store.ts +62 -20
  45. package/src/service.ts +218 -223
  46. package/src/task-service.ts +70 -38
package/src/cli.ts CHANGED
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env bun
2
2
  import { execFileSync } from "node:child_process";
3
- import { mkdirSync, writeFileSync } from "node:fs";
3
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
4
4
  import { homedir } from "node:os";
5
5
  import { dirname, join } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { connectPapyrusClient, type PapyrusClient } from "./client.ts";
8
- import { DAEMON_UNIT_NAME, TASK_EXECUTION_MAX_NODES } from "./constants.ts";
8
+ import { DAEMON_UNIT_NAME, TASK_EXECUTION_MAX_NODES, dbPath } from "./constants.ts";
9
9
  import { serveMain } from "./daemon.ts";
10
+ import { openDb } from "./db.ts";
11
+ import { applyIdMigration, mirrorDatabase, planIdMigration, verifyIdMigration, type IdMigrationPlan } from "./id-migration.ts";
10
12
  import type { GateResult } from "./domain/gate.ts";
11
13
  import type { TaskExecutionPlan } from "./task-execution.ts";
12
14
  import type { TaskBlockage, TaskCompletion } from "./task-service.ts";
@@ -41,6 +43,15 @@ function systemctl(...args: string[]): void {
41
43
  execFileSync("systemctl", ["--user", ...args], { stdio: "inherit" });
42
44
  }
43
45
 
46
+ function isDaemonActive(): boolean {
47
+ try {
48
+ execFileSync("systemctl", ["--user", "is-active", "--quiet", DAEMON_UNIT_NAME]);
49
+ return true;
50
+ } catch {
51
+ return false; // non-zero exit means inactive/failed/not-found -- treated the same, safely, as "not running"
52
+ }
53
+ }
54
+
44
55
  function installService(): void {
45
56
  const path = unitPath();
46
57
  mkdirSync(dirname(path), { recursive: true });
@@ -56,36 +67,81 @@ function installService(): void {
56
67
  const USAGE = `Usage:
57
68
  papyrus serve
58
69
  papyrus service <install|start|stop|restart|status>
59
- papyrus migrate task-focus [--json]
70
+ papyrus migrate schema [--json]
71
+ papyrus migrate-ids mirror [--db <path>] --out <mirror-path> [--json]
72
+ papyrus migrate-ids validate --mirror <mirror-path> [--idmap <path>] [--json]
73
+ papyrus migrate-ids promote --mirror <mirror-path> [--db <path>] [--idmap <path>] [--force] [--json]
74
+ papyrus discourse store <action> --store-id <id> [--input-json <json>] [--json]
75
+ papyrus graph link <from> <relation> <to> [--json]
76
+ papyrus graph unlink <from> <relation> <to> [--json]
77
+ papyrus graph tree <id> [--depth <n>] [--max-nodes <n>] [--json]
78
+ papyrus graph status <id> <status> [--json]
79
+ papyrus graph history [--id <artifact-id>] [--actor <actor>] [--session-id <id>] [--since <rfc3339>] [--limit <count>] [--cursor <id>] [--direction <asc|desc>] [--json]
80
+ papyrus gates run <id> [--json]
81
+ papyrus graph-projection apply --batch-json <json> [--json]
82
+ papyrus graph-projection checkpoint --producer-id <id> [--json]
83
+ papyrus artifact create --kind <kind> [--title <title>] [--status <status>] [--subtype <subtype>] [--body <body>] [--labels-json <json>] [--extra-json <json>] [--template-id <id>] [--json]
84
+ papyrus artifact query [--kind <kind>] [--status <status>] [--text <query>] [--limit <count>] [--json]
85
+ papyrus artifact show <id> [--depth <n>] [--max-nodes <n>] [--json]
86
+ papyrus docs create --title <title> [--body <body>] [--subtype <subtype>] [--labels-json <json>] [--extra-json <json>] [--template-id <id>] [--project-root <path>] [--json]
87
+ papyrus docs list [--status <status>] [--text <query>] [--limit <count>] [--project-root <path>] [--json]
88
+ papyrus docs show <id> [--json]
89
+ papyrus docs activate|archive|reopen <id> [--json]
90
+ papyrus docs link <id> <relation> <target-id> [--json]
91
+ papyrus docs assign-project <id> [project-root] [--json]
92
+ papyrus rules create --title <title> [--body <body>] [--condition <text>] [--rule-action <text>] [--severity block|warn|info] [--labels-json <json>] [--extra-json <json>] [--project-root <path>] [--json]
93
+ papyrus rules list [--status <status>] [--text <query>] [--limit <count>] [--project-root <path>] [--json]
94
+ papyrus rules show <id> [--json]
95
+ papyrus rules preview <id> [--json]
96
+ papyrus rules enable|disable <id> [--json]
97
+ papyrus rules gate <rule-id> <task-id> [--json]
98
+ papyrus rules injectable [--json]
99
+ papyrus rules assign-project <id> [project-root] [--json]
60
100
  papyrus skills run <id> [--arguments-json <json>] [--run-id <id>] [--json]
101
+ papyrus skills create --title <title> [--body <body>] [--trigger <text>] [--steps-json <json>] [--tools-json <json>] [--definition-json <json>] [--labels-json <json>] [--extra-json <json>] [--project-root <path>] [--json]
102
+ papyrus skills create-template --title <title> --target-kind <kind> [--defaults-json <json>] [--required-json <json>] [--body <body>] [--labels-json <json>] [--project-root <path>] [--json]
103
+ papyrus skills list [--status <status>] [--text <query>] [--limit <count>] [--project-root <path>] [--json]
104
+ papyrus skills show <id> [--json]
105
+ papyrus skills invoke <id> [--json]
106
+ papyrus skills enable|disable <id> [--json]
107
+ papyrus skills instantiate <template-id> [--title <title>] [--body <body>] [--status <status>] [--labels-json <json>] [--extra-json <json>] [--json]
108
+ papyrus skills assign-project <id> [project-root] [--json]
61
109
  papyrus notes capture <request> [--title <title>] [--json]
62
110
  papyrus notes list [--status <draft|active|archived>] [--text <query>] [--limit <count>] [--json]
63
111
  papyrus notes show <id> [--json]
64
112
  papyrus notes consume <id> [--reason <reason>] [--json]
65
113
  papyrus notes promote <id> <target-id> [--reason <reason>] [--json]
66
114
  papyrus notes archive <id> <completed|duplicate|declined|superseded> [--reason <reason>] [--json]
67
- papyrus tasks plan [--json]
68
- papyrus tasks graph [--json]
69
- papyrus tasks active [--json]
70
- papyrus tasks focused [--json]
71
- papyrus tasks pause [--json]
72
- papyrus tasks unpause [--json]
73
- papyrus tasks clear-focus [--json]
115
+ papyrus tasks plan [--session-id <id>] [--json]
116
+ papyrus tasks graph [--session-id <id>] [--json]
117
+ papyrus tasks active [--session-id <id>] [--json]
118
+ papyrus tasks focused [--session-id <id>] [--json]
119
+ papyrus tasks pause [--session-id <id>] [--json]
120
+ papyrus tasks unpause [--session-id <id>] [--json]
121
+ papyrus tasks clear-focus [--session-id <id>] [--json]
74
122
  papyrus tasks history <id> [--json]
75
123
  papyrus tasks scope [project|all|graph <root-id>] [--json]
76
124
  papyrus tasks assign-project <id> [project-root] [--json]
77
- papyrus tasks focus <id> [--json]
125
+ papyrus tasks focus <id> [--session-id <id>] [--json]
78
126
  papyrus tasks update <id> [--title <title>] [--body <body>] [--labels-json <json>] [--status todo --reason <reason>] [--json]
79
- papyrus tasks complete <id> [--json]
80
- papyrus tasks start <id> [--json]
81
- papyrus tasks submit <id> [--json]
82
- papyrus tasks reject <id> [--json]
83
- papyrus tasks retry <id> [--json]
84
- papyrus tasks cancel <id> [--json]
85
- papyrus tasks depend <id> <prerequisite-id> [--json]
86
- papyrus tasks create --title <title> [--body <body>] [--status <status>] [--labels-json <json>] [--extra-json <json>] [--gates-json <json>] [--checklist-json <json>] [--template-id <id>] [--parent-id <id>] [--depends-on-json <json>] [--json]
87
- papyrus tasks list [--status <status>] [--text <query>] [--limit <count>] [--json]
88
- papyrus tasks show <id> [--json]`;
127
+ papyrus tasks complete <id> [--session-id <id>] [--json]
128
+ papyrus tasks start <id> [--session-id <id>] [--json]
129
+ papyrus tasks submit <id> [--session-id <id>] [--json]
130
+ papyrus tasks reject <id> [--session-id <id>] [--json]
131
+ papyrus tasks retry <id> [--session-id <id>] [--json]
132
+ papyrus tasks cancel <id> [--session-id <id>] [--json]
133
+ papyrus tasks depend <id> <prerequisite-id> [--reason <reason>] [--session-id <id>] [--json]
134
+ papyrus tasks undepend <id> <prerequisite-id> [--reason <reason>] [--session-id <id>] [--json]
135
+ papyrus tasks contain <parent-id> <child-id> [--reason <reason>] [--session-id <id>] [--json]
136
+ papyrus tasks uncontain <parent-id> <child-id> [--reason <reason>] [--session-id <id>] [--json]
137
+ papyrus tasks create --title <title> [--body <body>] [--status <status>] [--labels-json <json>] [--extra-json <json>] [--gates-json <json>] [--checklist-json <json>] [--template-id <id>] [--parent-id <id>] [--depends-on-json <json>] [--session-id <id>] [--json]
138
+ papyrus tasks list [--status <status>] [--text <query>] [--limit <count>] [--scope <project|graph|all>] [--root-task-id <id>] [--session-id <id>] [--json]
139
+ papyrus tasks show <id> [--json]
140
+ papyrus tasks run-gates <id> [--json]
141
+ papyrus tasks set-checklist <id> --checklist-json <json> [--json]
142
+ papyrus tasks context [--scope <project|graph|all>] [--root-task-id <id>] [--session-id <id>] [--json]
143
+
144
+ A "--session-id" scopes Task Focus to one agent session; omit it to use the shared "global" Focus (today's behavior).`;
89
145
 
90
146
  function usage(): never {
91
147
  console.error(USAGE);
@@ -137,8 +193,8 @@ function planText(plan: TaskExecutionPlan): string {
137
193
  export async function runMigrationCli(args: string[], client: TaskCliClient): Promise<string> {
138
194
  const json = args.includes("--json");
139
195
  const positional = args.filter((arg) => arg !== "--json");
140
- if (positional.length !== 1 || positional[0] !== "task-focus") {
141
- throw new Error("migrate requires exactly `task-focus`");
196
+ if (positional.length !== 1 || positional[0] !== "schema") {
197
+ throw new Error("migrate requires exactly `schema`");
142
198
  }
143
199
  const result = await client.call<Record<string, never>, MigrationResult>("system.migrate", {});
144
200
  if (json) return JSON.stringify(result);
@@ -146,49 +202,675 @@ export async function runMigrationCli(args: string[], client: TaskCliClient): Pr
146
202
  return `Migrated schema ${result.from} → ${result.to}: ${result.applied.join(", ")}`;
147
203
  }
148
204
 
205
+ function readIdMap(sidecarPath: string): IdMigrationPlan {
206
+ const raw = JSON.parse(readFileSync(sidecarPath, "utf8")) as { idMap: Record<string, string> };
207
+ return { idMap: new Map(Object.entries(raw.idMap)) };
208
+ }
209
+
210
+ /**
211
+ * Offline, file-path-based -- deliberately not a daemon operation. Required sequence:
212
+ * mirror (produces a migrated copy + an inspectable id-map sidecar, touches nothing live) ->
213
+ * validate (re-checks the mirror in a fresh process, using only the sidecar) ->
214
+ * promote (re-validates, then swaps the mirror into place; refuses while the daemon is active).
215
+ * See src/id-migration.ts's module doc comment for why each step exists and what it does not cover.
216
+ */
217
+ export function runIdMigrationCli(args: string[]): string {
218
+ const [subcommand, ...rest] = args;
219
+ const json = rest.includes("--json");
220
+ const flag = (name: string): string | undefined => {
221
+ const index = rest.indexOf(`--${name}`);
222
+ return index === -1 ? undefined : rest[index + 1];
223
+ };
224
+
225
+ if (subcommand === "mirror") {
226
+ const source = flag("db") ?? dbPath();
227
+ const mirrorPath = flag("out");
228
+ if (!mirrorPath) throw new Error("migrate-ids mirror requires --out <path>");
229
+ const sourceDb = openDb(source);
230
+ try { mirrorDatabase(sourceDb, mirrorPath); } finally { sourceDb.close(); }
231
+
232
+ const mirror = openDb(mirrorPath);
233
+ let plan: IdMigrationPlan;
234
+ let report: ReturnType<typeof applyIdMigration>;
235
+ try {
236
+ plan = planIdMigration(mirror);
237
+ report = applyIdMigration(mirror, plan);
238
+ } finally {
239
+ mirror.close();
240
+ }
241
+ const sidecarPath = `${mirrorPath}.idmap.json`;
242
+ writeFileSync(sidecarPath, JSON.stringify({ idMap: Object.fromEntries(plan.idMap) }, null, 2));
243
+
244
+ const result = { source, mirrorPath, sidecarPath, ...report };
245
+ if (json) return JSON.stringify(result);
246
+ return [
247
+ `Mirrored ${source} -> ${mirrorPath}`,
248
+ `Remapped ${report.artifactsRemapped} artifact id(s), ${report.edgesRemapped} edge row(s), ${report.textOccurrencesRemapped} embedded text mention(s).`,
249
+ `Id map: ${sidecarPath}`,
250
+ `Next: papyrus migrate-ids validate --mirror ${mirrorPath}`,
251
+ ].join("\n");
252
+ }
253
+
254
+ if (subcommand === "validate") {
255
+ const mirrorPath = flag("mirror");
256
+ if (!mirrorPath) throw new Error("migrate-ids validate requires --mirror <path>");
257
+ const plan = readIdMap(flag("idmap") ?? `${mirrorPath}.idmap.json`);
258
+ const mirror = openDb(mirrorPath);
259
+ let result: ReturnType<typeof verifyIdMigration>;
260
+ try { result = verifyIdMigration(mirror, plan); } finally { mirror.close(); }
261
+ if (json) return JSON.stringify(result);
262
+ if (result.ok) return `Validation passed: ${plan.idMap.size} artifact id(s) correctly migrated.\nNext: papyrus migrate-ids promote --mirror ${mirrorPath}`;
263
+ return `Validation FAILED:\n${result.problems.map((problem) => ` - ${problem}`).join("\n")}\nDo not promote this mirror.`;
264
+ }
265
+
266
+ if (subcommand === "promote") {
267
+ const mirrorPath = flag("mirror");
268
+ if (!mirrorPath) throw new Error("migrate-ids promote requires --mirror <path>");
269
+ const target = flag("db") ?? dbPath();
270
+ const force = rest.includes("--force");
271
+ if (isDaemonActive() && !force) {
272
+ throw new Error(`refusing to promote while ${DAEMON_UNIT_NAME} is active -- stop it first (papyrus service stop), or pass --force if you have already verified it is safe`);
273
+ }
274
+ const plan = readIdMap(flag("idmap") ?? `${mirrorPath}.idmap.json`);
275
+ const mirror = openDb(mirrorPath);
276
+ let result: ReturnType<typeof verifyIdMigration>;
277
+ try { result = verifyIdMigration(mirror, plan); } finally { mirror.close(); }
278
+ if (!result.ok) {
279
+ throw new Error(`refusing to promote: mirror failed validation (${result.problems.length} problem(s)) -- run migrate-ids validate for details`);
280
+ }
281
+ let backupPath: string | undefined;
282
+ if (existsSync(target)) {
283
+ // Fold target's own WAL into its main file first -- otherwise a stale -wal/-shm left
284
+ // behind after the file swap below would make SQLite replay target's OWN pre-
285
+ // migration state back on top of the freshly copied (already-checkpointed) mirror on
286
+ // next open, silently discarding the migration. Copying only the main .db file while
287
+ // a WAL sidecar for the *old* file identity still sits at the same path is exactly
288
+ // the bug this closes.
289
+ const targetDb = openDb(target);
290
+ targetDb.exec("PRAGMA wal_checkpoint(TRUNCATE)");
291
+ targetDb.close();
292
+ backupPath = `${target}.pre-id-migration-${Date.now()}.bak`;
293
+ copyFileSync(target, backupPath);
294
+ for (const sidecar of [`${target}-wal`, `${target}-shm`]) if (existsSync(sidecar)) unlinkSync(sidecar);
295
+ }
296
+ copyFileSync(mirrorPath, target);
297
+ const result2 = { target, backupPath };
298
+ if (json) return JSON.stringify(result2);
299
+ return [
300
+ `Promoted ${mirrorPath} -> ${target}`,
301
+ ...(backupPath ? [`Previous database backed up to ${backupPath}`] : []),
302
+ "Restart papyrus.service for the daemon to pick this up.",
303
+ ].join("\n");
304
+ }
305
+
306
+ throw new Error("migrate-ids requires one of: mirror, validate, promote");
307
+ }
308
+
309
+ export async function runDiscourseCli(args: string[], client: TaskCliClient): Promise<string> {
310
+ const json = args.includes("--json");
311
+ const positional: string[] = [];
312
+ let storeId: string | undefined;
313
+ let operationInput: Record<string, unknown> = {};
314
+ for (let index = 0; index < args.length; index++) {
315
+ const argument = args[index]!;
316
+ if (argument === "--json") continue;
317
+ if (argument === "--store-id" || argument === "--input-json") {
318
+ const value = args[++index];
319
+ if (!value) throw new Error(`${argument} requires a value`);
320
+ if (argument === "--store-id") storeId = value;
321
+ else {
322
+ const parsed = JSON.parse(value) as unknown;
323
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("--input-json must be a JSON object");
324
+ operationInput = parsed as Record<string, unknown>;
325
+ }
326
+ continue;
327
+ }
328
+ if (argument.startsWith("--")) throw new Error(`unknown discourse option ${argument}`);
329
+ positional.push(argument);
330
+ }
331
+ if (positional.length !== 2 || positional[0] !== "store") throw new Error("discourse requires `store <action>`");
332
+ if (!storeId) throw new Error("discourse store requires --store-id");
333
+ const result = await client.call<Record<string, unknown>, unknown>("discourse.store", {
334
+ action: positional[1], store_id: storeId, ...operationInput,
335
+ });
336
+ return json ? JSON.stringify(result) : `Discourse store ${positional[1]} completed.`;
337
+ }
338
+
149
339
  export async function runSkillCli(args: string[], client: TaskCliClient, projectRoot: string = process.cwd()): Promise<string> {
150
340
  const json = args.includes("--json");
151
341
  const positional: string[] = [];
152
342
  let runId: string | undefined;
153
343
  let arguments_: Record<string, unknown> = {};
344
+ let title: string | undefined;
345
+ let body: string | undefined;
346
+ let trigger: string | undefined;
347
+ let steps: string[] | undefined;
348
+ let tools: string[] | undefined;
349
+ let definition: unknown;
350
+ let labels: string[] | undefined;
351
+ let extra: Record<string, unknown> | undefined;
352
+ let targetKind: string | undefined;
353
+ let defaults: Record<string, unknown> | undefined;
354
+ let required: string[] | undefined;
355
+ let status: string | undefined;
356
+ let text: string | undefined;
357
+ let limit: number | undefined;
358
+ let skillProjectRoot: string | undefined;
154
359
  for (let index = 0; index < args.length; index++) {
155
360
  const argument = args[index]!;
156
361
  if (argument === "--json") continue;
157
- if (argument === "--run-id") {
158
- runId = args[++index];
159
- if (!runId) throw new Error("--run-id requires a value");
362
+ if (argument === "--run-id") { runId = args[++index]; if (!runId) throw new Error("--run-id requires a value"); continue; }
363
+ if (argument === "--arguments-json") { arguments_ = parseJsonObjectFlag(args[++index], "--arguments-json"); continue; }
364
+ if (argument === "--title") { title = args[++index]; if (title === undefined) throw new Error("--title requires a value"); continue; }
365
+ if (argument === "--body") { body = args[++index]; if (body === undefined) throw new Error("--body requires a value"); continue; }
366
+ if (argument === "--trigger") { trigger = args[++index]; if (trigger === undefined) throw new Error("--trigger requires a value"); continue; }
367
+ if (argument === "--steps-json") { steps = parseJsonStringArrayFlag(args[++index], "--steps-json"); continue; }
368
+ if (argument === "--tools-json") { tools = parseJsonStringArrayFlag(args[++index], "--tools-json"); continue; }
369
+ if (argument === "--definition-json") {
370
+ const value = args[++index];
371
+ if (!value) throw new Error("--definition-json requires a value");
372
+ definition = JSON.parse(value);
160
373
  continue;
161
374
  }
162
- if (argument === "--arguments-json") {
163
- const source = args[++index];
164
- if (!source) throw new Error("--arguments-json requires a JSON object");
165
- const parsed = JSON.parse(source) as unknown;
166
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
167
- throw new Error("--arguments-json must be a JSON object");
168
- }
169
- arguments_ = parsed as Record<string, unknown>;
375
+ if (argument === "--labels-json") { labels = parseJsonStringArrayFlag(args[++index], "--labels-json"); continue; }
376
+ if (argument === "--extra-json") { extra = parseJsonObjectFlag(args[++index], "--extra-json"); continue; }
377
+ if (argument === "--target-kind") { targetKind = args[++index]; if (!targetKind) throw new Error("--target-kind requires a value"); continue; }
378
+ if (argument === "--defaults-json") { defaults = parseJsonObjectFlag(args[++index], "--defaults-json"); continue; }
379
+ if (argument === "--required-json") { required = parseJsonStringArrayFlag(args[++index], "--required-json"); continue; }
380
+ if (argument === "--status") { status = args[++index]; if (!status) throw new Error("--status requires a value"); continue; }
381
+ if (argument === "--text") { text = args[++index]; if (text === undefined) throw new Error("--text requires a value"); continue; }
382
+ if (argument === "--project-root") { skillProjectRoot = args[++index]; if (!skillProjectRoot) throw new Error("--project-root requires a value"); continue; }
383
+ if (argument === "--limit") {
384
+ const value = args[++index];
385
+ if (!value || Number.isNaN(Number(value))) throw new Error("--limit requires a numeric value");
386
+ limit = Number(value);
170
387
  continue;
171
388
  }
172
389
  if (argument.startsWith("--")) throw new Error(`unknown skills option ${argument}`);
173
390
  positional.push(argument);
174
391
  }
175
- if (positional.length !== 2 || positional[0] !== "run") throw new Error("skills requires `run <id>`");
176
- const input: Record<string, unknown> = { id: positional[1], arguments: arguments_, project_root: projectRoot };
177
- if (runId) input["run_id"] = runId;
178
- const result = await client.call<Record<string, unknown>, {
179
- runId: string;
180
- created: { tasks: string[]; rules: string[]; docs: string[] };
181
- rootTaskIds: string[];
182
- execution: TaskExecutionPlan;
183
- }>("skills.run", input);
184
- if (json) return JSON.stringify(result);
185
- return [
186
- `Created Skill run ${result.runId}: ${result.created.tasks.length} tasks, ${result.created.rules.length} rules, ${result.created.docs.length} docs`,
187
- `Ready roots: ${result.rootTaskIds.join(", ") || "none"}`,
188
- `Context docs: ${result.created.docs.join(", ") || "none"}`,
189
- `Scoped rules: ${result.created.rules.join(", ") || "none"}`,
190
- ...result.execution.nodes.map((node) => `[${node.state}] ${node.id} ${node.title}`),
191
- ].join("\n");
392
+ const [action, id, second] = positional;
393
+ if (action === "run") {
394
+ if (positional.length !== 2) throw new Error("skills requires `run <id>`");
395
+ const input: Record<string, unknown> = { id, arguments: arguments_, project_root: projectRoot };
396
+ if (runId) input["run_id"] = runId;
397
+ const result = await client.call<Record<string, unknown>, {
398
+ runId: string;
399
+ created: { tasks: string[]; rules: string[]; docs: string[] };
400
+ rootTaskIds: string[];
401
+ execution: TaskExecutionPlan;
402
+ }>("skills.run", input);
403
+ if (json) return JSON.stringify(result);
404
+ return [
405
+ `Created Skill run ${result.runId}: ${result.created.tasks.length} tasks, ${result.created.rules.length} rules, ${result.created.docs.length} docs`,
406
+ `Ready roots: ${result.rootTaskIds.join(", ") || "none"}`,
407
+ `Context docs: ${result.created.docs.join(", ") || "none"}`,
408
+ `Scoped rules: ${result.created.rules.join(", ") || "none"}`,
409
+ ...result.execution.nodes.map((node) => `[${node.state}] ${node.id} ${node.title}`),
410
+ ].join("\n");
411
+ }
412
+ let result: unknown;
413
+ let human: string;
414
+ switch (action) {
415
+ case "create": {
416
+ if (id) throw new Error("skills create accepts no positional arguments");
417
+ if (!title) throw new Error("skills create requires --title");
418
+ const artifact = await client.call<Record<string, unknown>, CliArtifact>("skills.create", { title, body, trigger, steps, tools, definition, labels, extra, project_root: skillProjectRoot });
419
+ result = artifact;
420
+ human = `Created skill: ${artifactLabel(artifact)}`;
421
+ break;
422
+ }
423
+ case "create-template": {
424
+ if (id) throw new Error("skills create-template accepts no positional arguments");
425
+ if (!title || !targetKind) throw new Error("skills create-template requires --title and --target-kind");
426
+ const artifact = await client.call<Record<string, unknown>, CliArtifact>("skills.create_template", {
427
+ title, target_kind: targetKind, defaults, required, body, labels, project_root: skillProjectRoot,
428
+ });
429
+ result = artifact;
430
+ human = `Created template: ${artifactLabel(artifact)}`;
431
+ break;
432
+ }
433
+ case "list": {
434
+ if (id) throw new Error("skills list accepts no positional arguments");
435
+ const rows = await client.call<Record<string, unknown>, CliArtifact[]>("skills.list", { status, text, limit, project_root: skillProjectRoot });
436
+ result = rows;
437
+ human = rows.length === 0 ? "No skills found." : rows.map((row) => artifactLabel(row)).join("\n");
438
+ break;
439
+ }
440
+ case "assign-project": {
441
+ if (!id) throw new Error("skills assign-project requires <id> [project-root]");
442
+ const artifact = await client.call<Record<string, unknown>, CliArtifact>("skills.assign_project", { id, project_root: second });
443
+ result = artifact;
444
+ human = second ? `Assigned ${id} to ${second}` : `Unscoped ${id}`;
445
+ break;
446
+ }
447
+ case "show": {
448
+ if (!id) throw new Error("skills show requires exactly one skill id");
449
+ const artifact = await client.call<Record<string, unknown>, CliArtifact>("skills.show", { id });
450
+ result = artifact;
451
+ human = `${artifactLabel(artifact)}\n\n${artifact.body ?? ""}`;
452
+ break;
453
+ }
454
+ case "invoke": {
455
+ if (!id) throw new Error("skills invoke requires exactly one skill id");
456
+ const invocation = await client.call<Record<string, unknown>, string>("skills.invoke", { id });
457
+ result = invocation;
458
+ human = invocation;
459
+ break;
460
+ }
461
+ case "enable":
462
+ case "disable": {
463
+ if (!id) throw new Error(`skills ${action} requires exactly one skill id`);
464
+ const operation = action === "enable" ? "skills.enable" : "skills.disable";
465
+ const artifact = await client.call<Record<string, unknown>, CliArtifact>(operation, { id });
466
+ result = artifact;
467
+ human = `${artifactLabel(artifact)}`;
468
+ break;
469
+ }
470
+ case "instantiate": {
471
+ if (!id) throw new Error("skills instantiate requires exactly one template id");
472
+ const artifact = await client.call<Record<string, unknown>, CliArtifact>("skills.instantiate", {
473
+ template_id: id, title, body, status, labels, extra, project_root: projectRoot,
474
+ });
475
+ result = artifact;
476
+ human = `Created: ${artifactLabel(artifact)}`;
477
+ break;
478
+ }
479
+ default:
480
+ throw new Error("skills action must be run, create, create-template, list, show, invoke, enable, disable, instantiate, or assign-project");
481
+ }
482
+ return json ? JSON.stringify(result) : human;
483
+ }
484
+
485
+ export async function runGraphCli(args: string[], client: TaskCliClient): Promise<string> {
486
+ const json = args.includes("--json");
487
+ const positional: string[] = [];
488
+ const input: Record<string, unknown> = {};
489
+ let depth: number | undefined;
490
+ let maxNodes: number | undefined;
491
+ for (let index = 0; index < args.length; index++) {
492
+ const argument = args[index]!;
493
+ if (argument === "--json") continue;
494
+ const flags: Record<string, string> = {
495
+ "--id": "id", "--actor": "actor", "--session-id": "session_id", "--since": "since",
496
+ "--direction": "direction",
497
+ };
498
+ if (argument in flags) {
499
+ const value = args[++index];
500
+ if (!value) throw new Error(`${argument} requires a value`);
501
+ input[flags[argument]!] = value;
502
+ continue;
503
+ }
504
+ if (argument === "--limit" || argument === "--cursor") {
505
+ const value = args[++index];
506
+ if (!value || Number.isNaN(Number(value))) throw new Error(`${argument} requires a numeric value`);
507
+ input[argument.slice(2)] = Number(value);
508
+ continue;
509
+ }
510
+ if (argument === "--depth") {
511
+ const value = args[++index];
512
+ if (!value || Number.isNaN(Number(value))) throw new Error("--depth requires a numeric value");
513
+ depth = Number(value);
514
+ continue;
515
+ }
516
+ if (argument === "--max-nodes") {
517
+ const value = args[++index];
518
+ if (!value || Number.isNaN(Number(value))) throw new Error("--max-nodes requires a numeric value");
519
+ maxNodes = Number(value);
520
+ continue;
521
+ }
522
+ if (argument.startsWith("--")) throw new Error(`unknown graph option ${argument}`);
523
+ positional.push(argument);
524
+ }
525
+ const [action, first, second, third] = positional;
526
+ if (action === "link") {
527
+ if (positional.length !== 4) throw new Error("graph link requires <from> <relation> <to>");
528
+ const result = await client.call<Record<string, unknown>, { ok: boolean }>("graph.link", { from: first, relation: second, to: third });
529
+ return json ? JSON.stringify(result) : `Linked ${first} --${second}--> ${third}`;
530
+ }
531
+ if (action === "unlink") {
532
+ if (positional.length !== 4) throw new Error("graph unlink requires <from> <relation> <to>");
533
+ const result = await client.call<Record<string, unknown>, { removed: boolean }>("graph.unlink", { from: first, relation: second, to: third });
534
+ if (json) return JSON.stringify(result);
535
+ return result.removed ? `Unlinked ${first} --${second}--> ${third}` : `No such relationship: ${first} --${second}--> ${third}`;
536
+ }
537
+ if (action === "tree") {
538
+ if (positional.length !== 2) throw new Error("graph tree requires exactly one artifact id");
539
+ const artifact = await client.call<Record<string, unknown>, CliArtifact & { edges?: Array<{ from: string; relation: string; to: string }> }>("graph.tree", { id: first, depth, max_nodes: maxNodes });
540
+ if (json) return JSON.stringify(artifact);
541
+ const edges = artifact.edges ?? [];
542
+ return edges.length === 0
543
+ ? `${artifactLabel(artifact)} — no edges`
544
+ : `${artifactLabel(artifact)}\n${edges.map((edge) => ` ${edge.from} --${edge.relation}--> ${edge.to}`).join("\n")}`;
545
+ }
546
+ if (action === "status") {
547
+ if (positional.length !== 3) throw new Error("graph status requires <id> <status>");
548
+ const artifact = await client.call<Record<string, unknown>, CliArtifact>("graph.status", { id: first, status: second });
549
+ return json ? JSON.stringify(artifact) : `Updated ${artifact.id} → [${artifact.status}]`;
550
+ }
551
+ if (action !== "history") throw new Error("graph action must be link, unlink, tree, status, or history");
552
+ const page = await client.call<Record<string, unknown>, import("./domain/artifact-event.ts").ArtifactEventPage>("graph.history", input);
553
+ if (json) return JSON.stringify(page);
554
+ if (page.events.length === 0) return "No recorded events.";
555
+ return page.events.map((event) => {
556
+ const transition = event.fromStatus || event.toStatus ? ` ${event.fromStatus ?? "\u2205"} \u2192 ${event.toStatus ?? "\u2205"}` : "";
557
+ const relation = event.relation ? ` ${event.relation} \u2192 ${event.relatedId}` : "";
558
+ return `${event.occurredAt} ${event.artifactId} ${event.type}${transition}${relation} \u00b7 ${event.actor}/${event.source}${event.sessionId ? ` \u00b7 ${event.sessionId}` : ""}`;
559
+ }).join("\n");
560
+ }
561
+
562
+ export async function runDocsCli(args: string[], client: TaskCliClient): Promise<string> {
563
+ const json = args.includes("--json");
564
+ const positional: string[] = [];
565
+ let title: string | undefined;
566
+ let body: string | undefined;
567
+ let subtype: string | undefined;
568
+ let labels: string[] | undefined;
569
+ let extra: Record<string, unknown> | undefined;
570
+ let templateId: string | undefined;
571
+ let status: string | undefined;
572
+ let text: string | undefined;
573
+ let limit: number | undefined;
574
+ let projectRoot: string | undefined;
575
+ for (let index = 0; index < args.length; index++) {
576
+ const argument = args[index]!;
577
+ if (argument === "--json") continue;
578
+ if (argument === "--title") { title = args[++index]; if (title === undefined) throw new Error("--title requires a value"); continue; }
579
+ if (argument === "--body") { body = args[++index]; if (body === undefined) throw new Error("--body requires a value"); continue; }
580
+ if (argument === "--subtype") { subtype = args[++index]; if (!subtype) throw new Error("--subtype requires a value"); continue; }
581
+ if (argument === "--labels-json") { labels = parseJsonStringArrayFlag(args[++index], "--labels-json"); continue; }
582
+ if (argument === "--extra-json") { extra = parseJsonObjectFlag(args[++index], "--extra-json"); continue; }
583
+ if (argument === "--template-id") { templateId = args[++index]; if (!templateId) throw new Error("--template-id requires a value"); continue; }
584
+ if (argument === "--status") { status = args[++index]; if (!status) throw new Error("--status requires a value"); continue; }
585
+ if (argument === "--text") { text = args[++index]; if (text === undefined) throw new Error("--text requires a value"); continue; }
586
+ if (argument === "--project-root") { projectRoot = args[++index]; if (!projectRoot) throw new Error("--project-root requires a value"); continue; }
587
+ if (argument === "--limit") {
588
+ const value = args[++index];
589
+ if (!value || Number.isNaN(Number(value))) throw new Error("--limit requires a numeric value");
590
+ limit = Number(value);
591
+ continue;
592
+ }
593
+ if (argument.startsWith("--")) throw new Error(`unknown docs option ${argument}`);
594
+ positional.push(argument);
595
+ }
596
+ const [action, id, second, third] = positional;
597
+ let result: unknown;
598
+ let human: string;
599
+ switch (action) {
600
+ case "create": {
601
+ if (id) throw new Error("docs create accepts no positional arguments");
602
+ if (!title) throw new Error("docs create requires --title");
603
+ const artifact = await client.call<Record<string, unknown>, CliArtifact>("docs.create", { title, body, subtype, labels, extra, template_id: templateId, project_root: projectRoot });
604
+ result = artifact;
605
+ human = `Created document: ${artifactLabel(artifact)}`;
606
+ break;
607
+ }
608
+ case "list": {
609
+ if (id) throw new Error("docs list accepts no positional arguments");
610
+ const rows = await client.call<Record<string, unknown>, CliArtifact[]>("docs.list", { status, text, limit, project_root: projectRoot });
611
+ result = rows;
612
+ human = rows.length === 0 ? "No documents found." : rows.map((row) => artifactLabel(row)).join("\n");
613
+ break;
614
+ }
615
+ case "assign-project": {
616
+ if (!id || third !== undefined) throw new Error("docs assign-project requires <id> [project-root]");
617
+ const artifact = await client.call<Record<string, unknown>, CliArtifact>("docs.assign_project", { id, project_root: second });
618
+ result = artifact;
619
+ human = second ? `Assigned ${id} to ${second}` : `Unscoped ${id}`;
620
+ break;
621
+ }
622
+ case "show": {
623
+ if (!id || second) throw new Error("docs show requires exactly one document id");
624
+ const artifact = await client.call<Record<string, unknown>, CliArtifact>("docs.show", { id });
625
+ result = artifact;
626
+ human = `${artifactLabel(artifact)}\n\n${artifact.body ?? ""}`;
627
+ break;
628
+ }
629
+ case "activate":
630
+ case "archive":
631
+ case "reopen": {
632
+ if (!id || second) throw new Error(`docs ${action} requires exactly one document id`);
633
+ const artifact = await client.call<Record<string, unknown>, CliArtifact>(`docs.${action}`, { id });
634
+ result = artifact;
635
+ human = `${artifactLabel(artifact)}`;
636
+ break;
637
+ }
638
+ case "link": {
639
+ if (!id || !second || !third || positional.length !== 4) throw new Error("docs link requires <id> <relation> <target-id>");
640
+ const artifact = await client.call<Record<string, unknown>, CliArtifact>("docs.link", { id, relation: second, target_id: third });
641
+ result = artifact;
642
+ human = `Linked ${id} --${second}--> ${third}`;
643
+ break;
644
+ }
645
+ default:
646
+ throw new Error("docs action must be create, list, show, activate, archive, reopen, link, or assign-project");
647
+ }
648
+ return json ? JSON.stringify(result) : human;
649
+ }
650
+
651
+ export async function runRulesCli(args: string[], client: TaskCliClient, projectRoot: string = process.cwd()): Promise<string> {
652
+ const json = args.includes("--json");
653
+ const positional: string[] = [];
654
+ let title: string | undefined;
655
+ let body: string | undefined;
656
+ let condition: string | undefined;
657
+ let ruleAction: string | undefined;
658
+ let severity: string | undefined;
659
+ let labels: string[] | undefined;
660
+ let extra: Record<string, unknown> | undefined;
661
+ let status: string | undefined;
662
+ let text: string | undefined;
663
+ let limit: number | undefined;
664
+ let ruleProjectRoot: string | undefined;
665
+ for (let index = 0; index < args.length; index++) {
666
+ const argument = args[index]!;
667
+ if (argument === "--json") continue;
668
+ if (argument === "--title") { title = args[++index]; if (title === undefined) throw new Error("--title requires a value"); continue; }
669
+ if (argument === "--body") { body = args[++index]; if (body === undefined) throw new Error("--body requires a value"); continue; }
670
+ if (argument === "--condition") { condition = args[++index]; if (condition === undefined) throw new Error("--condition requires a value"); continue; }
671
+ if (argument === "--rule-action") { ruleAction = args[++index]; if (ruleAction === undefined) throw new Error("--rule-action requires a value"); continue; }
672
+ if (argument === "--severity") { severity = args[++index]; if (!severity) throw new Error("--severity requires a value"); continue; }
673
+ if (argument === "--labels-json") { labels = parseJsonStringArrayFlag(args[++index], "--labels-json"); continue; }
674
+ if (argument === "--extra-json") { extra = parseJsonObjectFlag(args[++index], "--extra-json"); continue; }
675
+ if (argument === "--status") { status = args[++index]; if (!status) throw new Error("--status requires a value"); continue; }
676
+ if (argument === "--text") { text = args[++index]; if (text === undefined) throw new Error("--text requires a value"); continue; }
677
+ if (argument === "--project-root") { ruleProjectRoot = args[++index]; if (!ruleProjectRoot) throw new Error("--project-root requires a value"); continue; }
678
+ if (argument === "--limit") {
679
+ const value = args[++index];
680
+ if (!value || Number.isNaN(Number(value))) throw new Error("--limit requires a numeric value");
681
+ limit = Number(value);
682
+ continue;
683
+ }
684
+ if (argument.startsWith("--")) throw new Error(`unknown rules option ${argument}`);
685
+ positional.push(argument);
686
+ }
687
+ const [action, id, second, third] = positional;
688
+ let result: unknown;
689
+ let human: string;
690
+ switch (action) {
691
+ case "create": {
692
+ if (id) throw new Error("rules create accepts no positional arguments");
693
+ if (!title) throw new Error("rules create requires --title");
694
+ const artifact = await client.call<Record<string, unknown>, CliArtifact>("rules.create", { title, body, condition, rule_action: ruleAction, severity, labels, extra, project_root: ruleProjectRoot });
695
+ result = artifact;
696
+ human = `Created rule: ${artifactLabel(artifact)}`;
697
+ break;
698
+ }
699
+ case "list": {
700
+ if (id) throw new Error("rules list accepts no positional arguments");
701
+ const rows = await client.call<Record<string, unknown>, CliArtifact[]>("rules.list", { status, text, limit, project_root: ruleProjectRoot });
702
+ result = rows;
703
+ human = rows.length === 0 ? "No rules found." : rows.map((row) => artifactLabel(row)).join("\n");
704
+ break;
705
+ }
706
+ case "assign-project": {
707
+ if (!id || third !== undefined) throw new Error("rules assign-project requires <id> [project-root]");
708
+ const artifact = await client.call<Record<string, unknown>, CliArtifact>("rules.assign_project", { id, project_root: second });
709
+ result = artifact;
710
+ human = second ? `Assigned ${id} to ${second}` : `Unscoped ${id}`;
711
+ break;
712
+ }
713
+ case "show": {
714
+ if (!id || second) throw new Error("rules show requires exactly one rule id");
715
+ const artifact = await client.call<Record<string, unknown>, CliArtifact>("rules.show", { id });
716
+ result = artifact;
717
+ human = `${artifactLabel(artifact)}\n\n${artifact.body ?? ""}`;
718
+ break;
719
+ }
720
+ case "preview": {
721
+ if (!id || second) throw new Error("rules preview requires exactly one rule id");
722
+ const preview = await client.call<Record<string, unknown>, string>("rules.preview", { id });
723
+ result = preview;
724
+ human = preview;
725
+ break;
726
+ }
727
+ case "enable":
728
+ case "disable": {
729
+ if (!id || second) throw new Error(`rules ${action} requires exactly one rule id`);
730
+ const artifact = await client.call<Record<string, unknown>, CliArtifact>(`rules.${action}`, { id });
731
+ result = artifact;
732
+ human = `${artifactLabel(artifact)}`;
733
+ break;
734
+ }
735
+ case "gate": {
736
+ if (!id || !second || positional.length !== 3) throw new Error("rules gate requires <rule-id> <task-id>");
737
+ const artifact = await client.call<Record<string, unknown>, CliArtifact>("rules.gate", { id, task_id: second });
738
+ result = artifact;
739
+ human = `Gated ${second} with rule ${artifactLabel(artifact)}`;
740
+ break;
741
+ }
742
+ case "injectable": {
743
+ if (id) throw new Error("rules injectable accepts no positional arguments");
744
+ const rows = await client.call<Record<string, unknown>, CliArtifact[]>("rules.injectable", { project_root: projectRoot });
745
+ result = rows;
746
+ human = rows.length === 0 ? "No injectable rules." : rows.map((row) => row.title).join("\n");
747
+ break;
748
+ }
749
+ default:
750
+ throw new Error("rules action must be create, list, show, preview, enable, disable, gate, injectable, or assign-project");
751
+ }
752
+ return json ? JSON.stringify(result) : human;
753
+ }
754
+
755
+ export async function runArtifactCli(args: string[], client: TaskCliClient, projectRoot: string = process.cwd()): Promise<string> {
756
+ const json = args.includes("--json");
757
+ const positional: string[] = [];
758
+ let kind: string | undefined;
759
+ let title: string | undefined;
760
+ let body: string | undefined;
761
+ let status: string | undefined;
762
+ let subtype: string | undefined;
763
+ let labels: string[] | undefined;
764
+ let extra: Record<string, unknown> | undefined;
765
+ let templateId: string | undefined;
766
+ let text: string | undefined;
767
+ let limit: number | undefined;
768
+ let depth: number | undefined;
769
+ let maxNodes: number | undefined;
770
+ for (let index = 0; index < args.length; index++) {
771
+ const argument = args[index]!;
772
+ if (argument === "--json") continue;
773
+ if (argument === "--kind") { kind = args[++index]; if (!kind) throw new Error("--kind requires a value"); continue; }
774
+ if (argument === "--title") { title = args[++index]; if (title === undefined) throw new Error("--title requires a value"); continue; }
775
+ if (argument === "--body") { body = args[++index]; if (body === undefined) throw new Error("--body requires a value"); continue; }
776
+ if (argument === "--status") { status = args[++index]; if (!status) throw new Error("--status requires a value"); continue; }
777
+ if (argument === "--subtype") { subtype = args[++index]; if (!subtype) throw new Error("--subtype requires a value"); continue; }
778
+ if (argument === "--labels-json") { labels = parseJsonStringArrayFlag(args[++index], "--labels-json"); continue; }
779
+ if (argument === "--extra-json") { extra = parseJsonObjectFlag(args[++index], "--extra-json"); continue; }
780
+ if (argument === "--template-id") { templateId = args[++index]; if (!templateId) throw new Error("--template-id requires a value"); continue; }
781
+ if (argument === "--text") { text = args[++index]; if (text === undefined) throw new Error("--text requires a value"); continue; }
782
+ if (argument === "--limit") {
783
+ const value = args[++index];
784
+ if (!value || Number.isNaN(Number(value))) throw new Error("--limit requires a numeric value");
785
+ limit = Number(value);
786
+ continue;
787
+ }
788
+ if (argument === "--depth") {
789
+ const value = args[++index];
790
+ if (!value || Number.isNaN(Number(value))) throw new Error("--depth requires a numeric value");
791
+ depth = Number(value);
792
+ continue;
793
+ }
794
+ if (argument === "--max-nodes") {
795
+ const value = args[++index];
796
+ if (!value || Number.isNaN(Number(value))) throw new Error("--max-nodes requires a numeric value");
797
+ maxNodes = Number(value);
798
+ continue;
799
+ }
800
+ if (argument.startsWith("--")) throw new Error(`unknown artifact option ${argument}`);
801
+ positional.push(argument);
802
+ }
803
+ const [action, id] = positional;
804
+ let result: unknown;
805
+ let human: string;
806
+ switch (action) {
807
+ case "create": {
808
+ if (id) throw new Error("artifact create accepts no positional arguments");
809
+ if (!kind && !templateId) throw new Error("artifact create requires --kind (or --template-id)");
810
+ const artifact = await client.call<Record<string, unknown>, CliArtifact>("artifact.create", {
811
+ kind, title, body, status, subtype, labels, extra, template_id: templateId,
812
+ ...(kind === "task" ? { project_root: projectRoot } : {}),
813
+ });
814
+ result = artifact;
815
+ human = `Created: ${artifactLabel(artifact)}`;
816
+ break;
817
+ }
818
+ case "query": {
819
+ if (id) throw new Error("artifact query accepts no positional arguments");
820
+ const rows = await client.call<Record<string, unknown>, CliArtifact[]>("artifact.query", { kind, status, text, limit });
821
+ result = rows;
822
+ human = rows.length === 0 ? "No artifacts found." : rows.map((row) => artifactLabel(row)).join("\n");
823
+ break;
824
+ }
825
+ case "show": {
826
+ if (!id) throw new Error("artifact show requires exactly one artifact id");
827
+ const artifact = await client.call<Record<string, unknown>, CliArtifact>("artifact.show", { id, depth, max_nodes: maxNodes });
828
+ result = artifact;
829
+ human = `${artifactLabel(artifact)}\n\n${artifact.body ?? ""}`;
830
+ break;
831
+ }
832
+ default:
833
+ throw new Error("artifact action must be create, query, or show");
834
+ }
835
+ return json ? JSON.stringify(result) : human;
836
+ }
837
+
838
+ export async function runGatesCli(args: string[], client: TaskCliClient): Promise<string> {
839
+ const json = args.includes("--json");
840
+ const positional = args.filter((arg) => arg !== "--json");
841
+ if (positional.length !== 2 || positional[0] !== "run") throw new Error("gates requires `run <id>`");
842
+ const results = await client.call<Record<string, unknown>, GateResult[]>("gates.run", { id: positional[1] });
843
+ if (json) return JSON.stringify(results);
844
+ return results.length === 0
845
+ ? "No gates configured."
846
+ : results.map((gate) => `${gate.passed ? "✓" : "✗"} ${gate.gate.type}: ${gate.gate.target} — ${gate.output}`).join("\n");
847
+ }
848
+
849
+ export async function runGraphProjectionCli(args: string[], client: TaskCliClient): Promise<string> {
850
+ const json = args.includes("--json");
851
+ const positional: string[] = [];
852
+ let batch: Record<string, unknown> | undefined;
853
+ let producerId: string | undefined;
854
+ for (let index = 0; index < args.length; index++) {
855
+ const argument = args[index]!;
856
+ if (argument === "--json") continue;
857
+ if (argument === "--batch-json") { batch = parseJsonObjectFlag(args[++index], "--batch-json"); continue; }
858
+ if (argument === "--producer-id") { producerId = args[++index]; if (!producerId) throw new Error("--producer-id requires a value"); continue; }
859
+ positional.push(argument);
860
+ }
861
+ const [action] = positional;
862
+ if (action === "apply") {
863
+ if (!batch) throw new Error("graph-projection apply requires --batch-json");
864
+ const result = await client.call<Record<string, unknown>, unknown>("graph_projection.apply", batch);
865
+ return json ? JSON.stringify(result) : JSON.stringify(result, null, 2);
866
+ }
867
+ if (action === "checkpoint") {
868
+ if (!producerId) throw new Error("graph-projection checkpoint requires --producer-id");
869
+ const result = await client.call<Record<string, unknown>, unknown>("graph_projection.checkpoint", { producer_id: producerId });
870
+ if (json) return JSON.stringify(result);
871
+ return result === null ? `No projection checkpoint for producer "${producerId}".` : JSON.stringify(result, null, 2);
872
+ }
873
+ throw new Error("graph-projection action must be apply or checkpoint");
192
874
  }
193
875
 
194
876
  export async function runNoteCli(args: string[], client: TaskCliClient, projectRoot: string = process.cwd()): Promise<string> {
@@ -254,13 +936,13 @@ export async function runNoteCli(args: string[], client: TaskCliClient, projectR
254
936
  export async function runTaskCli(args: string[], client: TaskCliClient, projectRoot: string = process.cwd()): Promise<string> {
255
937
  const json = args.includes("--json");
256
938
  const positional: string[] = [];
939
+ const updateInput: { title?: string; body?: string; labels?: string[]; status?: "todo" } = {};
257
940
  let reason: string | undefined;
941
+ let sessionId: string | undefined;
258
942
  let title: string | undefined;
259
943
  let body: string | undefined;
260
- let labels: string[] | undefined;
261
- // Deliberately unrestricted here -- tasks update alone restricts this to "todo" (accidental-
262
- // creation recovery only), enforced in that case body, not in parsing shared by every action.
263
944
  let status: string | undefined;
945
+ let labels: string[] | undefined;
264
946
  let extra: Record<string, unknown> | undefined;
265
947
  let gates: unknown[] | undefined;
266
948
  let checklist: Record<string, unknown> | undefined;
@@ -269,53 +951,78 @@ export async function runTaskCli(args: string[], client: TaskCliClient, projectR
269
951
  let dependsOn: string[] | undefined;
270
952
  let text: string | undefined;
271
953
  let limit: number | undefined;
954
+ let listScope: "project" | "graph" | "all" | undefined;
955
+ let rootTaskId: string | undefined;
272
956
  for (let index = 0; index < args.length; index++) {
273
957
  const argument = args[index]!;
274
958
  if (argument === "--json") continue;
275
- if (argument === "--title" || argument === "--body" || argument === "--labels-json" || argument === "--status" || argument === "--reason"
276
- || argument === "--extra-json" || argument === "--gates-json" || argument === "--checklist-json" || argument === "--template-id"
277
- || argument === "--parent-id" || argument === "--depends-on-json" || argument === "--text" || argument === "--limit") {
959
+ if (argument === "--session-id") {
960
+ sessionId = args[++index];
961
+ if (!sessionId) throw new Error("--session-id requires a value");
962
+ continue;
963
+ }
964
+ if (argument === "--title" || argument === "--body" || argument === "--labels-json" || argument === "--status" || argument === "--reason") {
278
965
  const value = args[++index];
279
966
  if (value === undefined) throw new Error(`${argument} requires a value`);
280
- if (argument === "--title") title = value;
281
- else if (argument === "--body") body = value;
967
+ if (argument === "--title") { updateInput.title = value; title = value; }
968
+ else if (argument === "--body") { updateInput.body = value; body = value; }
282
969
  else if (argument === "--reason") reason = value;
283
- else if (argument === "--status") status = value;
284
- else if (argument === "--extra-json") extra = parseJsonObjectFlag(value, "--extra-json");
285
- else if (argument === "--checklist-json") checklist = parseJsonObjectFlag(value, "--checklist-json");
286
- else if (argument === "--template-id") templateId = value;
287
- else if (argument === "--parent-id") parentId = value;
288
- else if (argument === "--depends-on-json") dependsOn = parseJsonStringArrayFlag(value, "--depends-on-json");
289
- else if (argument === "--text") text = value;
290
- else if (argument === "--limit") {
291
- if (Number.isNaN(Number(value))) throw new Error("--limit requires a numeric value");
292
- limit = Number(value);
293
- } else if (argument === "--gates-json") {
294
- const parsed = JSON.parse(value) as unknown;
295
- if (!Array.isArray(parsed)) throw new Error("--gates-json must be a JSON array");
296
- gates = parsed;
970
+ else if (argument === "--status") {
971
+ status = value;
972
+ if (value === "todo") updateInput.status = value;
297
973
  } else {
298
974
  labels = parseJsonStringArrayFlag(value, "--labels-json");
975
+ updateInput.labels = labels;
299
976
  }
300
977
  continue;
301
978
  }
979
+ if (argument === "--extra-json") { extra = parseJsonObjectFlag(args[++index]!, "--extra-json"); continue; }
980
+ if (argument === "--gates-json") {
981
+ const value = args[++index];
982
+ if (!value) throw new Error("--gates-json requires a value");
983
+ const parsed = JSON.parse(value) as unknown;
984
+ if (!Array.isArray(parsed)) throw new Error("--gates-json must be a JSON array");
985
+ gates = parsed;
986
+ continue;
987
+ }
988
+ if (argument === "--checklist-json") { checklist = parseJsonObjectFlag(args[++index]!, "--checklist-json"); continue; }
989
+ if (argument === "--template-id") { templateId = args[++index]; if (!templateId) throw new Error("--template-id requires a value"); continue; }
990
+ if (argument === "--parent-id") { parentId = args[++index]; if (!parentId) throw new Error("--parent-id requires a value"); continue; }
991
+ if (argument === "--depends-on-json") { dependsOn = parseJsonStringArrayFlag(args[++index]!, "--depends-on-json"); continue; }
992
+ if (argument === "--text") { text = args[++index]; if (text === undefined) throw new Error("--text requires a value"); continue; }
993
+ if (argument === "--limit") {
994
+ const value = args[++index];
995
+ if (!value || Number.isNaN(Number(value))) throw new Error("--limit requires a numeric value");
996
+ limit = Number(value);
997
+ continue;
998
+ }
999
+ if (argument === "--scope") {
1000
+ const value = args[++index];
1001
+ if (value !== "project" && value !== "graph" && value !== "all") throw new Error("--scope must be project, graph, or all");
1002
+ listScope = value;
1003
+ continue;
1004
+ }
1005
+ if (argument === "--root-task-id") { rootTaskId = args[++index]; if (!rootTaskId) throw new Error("--root-task-id requires a value"); continue; }
1006
+ if (argument.startsWith("--")) throw new Error(`unknown tasks option ${argument}`);
302
1007
  positional.push(argument);
303
1008
  }
304
1009
  const [action, id, dependencyId] = positional;
305
- if (reason !== undefined && action !== "update") throw new Error("--reason is only supported by tasks update");
1010
+ const reasonSupportedActions = new Set(["update", "depend", "undepend", "contain", "uncontain"]);
1011
+ if (reason !== undefined && !reasonSupportedActions.has(action ?? "")) throw new Error("--reason is only supported by tasks update, depend, undepend, contain, and uncontain");
1012
+ const sessionScope = sessionId ? { session_id: sessionId } : {};
306
1013
  let result: unknown;
307
1014
  let human: string;
308
1015
  switch (action) {
309
1016
  case "active": {
310
1017
  if (id) throw new Error("tasks active accepts no positional arguments");
311
- const active = await client.call<Record<string, string>, CliArtifact | null>("tasks.active", { project_root: projectRoot });
1018
+ const active = await client.call<Record<string, unknown>, CliArtifact | null>("tasks.active", { project_root: projectRoot, ...sessionScope });
312
1019
  result = active;
313
1020
  human = active ? `Active: ${artifactLabel(active)}` : "No active task.";
314
1021
  break;
315
1022
  }
316
1023
  case "focused": {
317
1024
  if (id) throw new Error("tasks focused accepts no positional arguments");
318
- const focus = await client.call<Record<string, string>, { artifact: CliArtifact; status: "active" | "paused"; updatedAt: string } | null>("tasks.focused", { project_root: projectRoot });
1025
+ const focus = await client.call<Record<string, unknown>, { artifact: CliArtifact; status: "active" | "paused"; updatedAt: string } | null>("tasks.focused", { project_root: projectRoot, ...sessionScope });
319
1026
  result = focus;
320
1027
  human = focus ? `Focused (${focus.status}): ${artifactLabel(focus.artifact)}` : "No focused task.";
321
1028
  break;
@@ -324,53 +1031,25 @@ export async function runTaskCli(args: string[], client: TaskCliClient, projectR
324
1031
  case "unpause": {
325
1032
  if (id) throw new Error(`tasks ${action} accepts no positional arguments`);
326
1033
  const operation = action === "pause" ? "tasks.pause" : "tasks.unpause";
327
- const focus = await client.call<Record<string, string>, { artifact: CliArtifact; status: string }>(operation, { actor: "user", source: "cli" });
1034
+ const focus = await client.call<Record<string, unknown>, { artifact: CliArtifact; status: string }>(operation, { actor: "user", source: "cli", ...sessionScope });
328
1035
  result = focus;
329
1036
  human = `Focused (${focus.status}): ${artifactLabel(focus.artifact)}`;
330
1037
  break;
331
1038
  }
332
1039
  case "clear-focus": {
333
1040
  if (id) throw new Error("tasks clear-focus accepts no positional arguments");
334
- const cleared = await client.call<Record<string, string>, { cleared: boolean }>("tasks.clear_focus", { actor: "user", source: "cli" });
1041
+ const cleared = await client.call<Record<string, unknown>, { cleared: boolean }>("tasks.clear_focus", { actor: "user", source: "cli", ...sessionScope });
335
1042
  result = cleared;
336
1043
  human = cleared.cleared ? "Task focus cleared." : "No focused task.";
337
1044
  break;
338
1045
  }
339
- case "update": {
340
- if (!id || dependencyId) throw new Error("tasks update requires exactly one task id");
341
- const updateInput: { title?: string; body?: string; labels?: string[]; status?: "todo" } = {};
342
- if (title !== undefined) updateInput.title = title;
343
- if (body !== undefined) updateInput.body = body;
344
- if (labels !== undefined) updateInput.labels = labels;
345
- if (status !== undefined) {
346
- if (status !== "todo") throw new Error("--status only supports todo for accidental creation recovery");
347
- updateInput.status = status;
348
- }
349
- if (Object.keys(updateInput).length === 0) throw new Error("tasks update requires --title, --body, --labels-json, or --status todo");
350
- if (updateInput.status !== undefined && !reason?.trim()) throw new Error("tasks update --status requires --reason");
351
- if (reason !== undefined && updateInput.status === undefined) throw new Error("tasks update --reason requires --status todo");
352
- const artifact = await client.call<Record<string, unknown>, CliArtifact>("tasks.update", {
353
- id, ...updateInput, ...(reason ? { reason } : {}), actor: "user", source: "cli",
354
- });
355
- result = artifact;
356
- human = `Updated: ${artifactLabel(artifact)}`;
357
- break;
358
- }
359
1046
  case "create": {
360
1047
  if (id) throw new Error("tasks create accepts no positional arguments");
361
1048
  if (!title) throw new Error("tasks create requires --title");
362
1049
  const artifact = await client.call<Record<string, unknown>, CliArtifact>("tasks.create", {
363
- title,
364
- ...(body !== undefined ? { body } : {}),
365
- ...(status !== undefined ? { status } : {}),
366
- ...(labels !== undefined ? { labels } : {}),
367
- ...(extra !== undefined ? { extra } : {}),
368
- ...(gates !== undefined ? { gates } : {}),
369
- ...(checklist !== undefined ? { checklist } : {}),
370
- ...(templateId !== undefined ? { template_id: templateId } : {}),
371
- ...(parentId !== undefined ? { parent_id: parentId } : {}),
372
- ...(dependsOn !== undefined ? { depends_on: dependsOn } : {}),
373
- project_root: projectRoot, actor: "user", source: "cli",
1050
+ title, body, status, labels, extra, gates, checklist,
1051
+ template_id: templateId, parent_id: parentId, depends_on: dependsOn,
1052
+ project_root: projectRoot, actor: "user", source: "cli", ...sessionScope,
374
1053
  });
375
1054
  result = artifact;
376
1055
  human = `Created task: ${artifactLabel(artifact)}`;
@@ -379,10 +1058,7 @@ export async function runTaskCli(args: string[], client: TaskCliClient, projectR
379
1058
  case "list": {
380
1059
  if (id) throw new Error("tasks list accepts no positional arguments");
381
1060
  const rows = await client.call<Record<string, unknown>, CliArtifact[]>("tasks.list", {
382
- ...(status !== undefined ? { status } : {}),
383
- ...(text !== undefined ? { text } : {}),
384
- ...(limit !== undefined ? { limit } : {}),
385
- project_root: projectRoot,
1061
+ status, text, limit, project_root: projectRoot, scope: listScope, root_task_id: rootTaskId, ...sessionScope,
386
1062
  });
387
1063
  result = rows;
388
1064
  human = rows.length === 0 ? "No tasks found." : rows.map((row) => artifactLabel(row)).join("\n");
@@ -395,6 +1071,63 @@ export async function runTaskCli(args: string[], client: TaskCliClient, projectR
395
1071
  human = `${artifactLabel(artifact)}\n\n${artifact.body ?? ""}`;
396
1072
  break;
397
1073
  }
1074
+ case "run-gates": {
1075
+ if (!id || dependencyId) throw new Error("tasks run-gates requires exactly one task id");
1076
+ const results = await client.call<Record<string, unknown>, GateResult[]>("tasks.run_gates", { id, actor: "user", source: "cli" });
1077
+ result = results;
1078
+ human = results.length === 0
1079
+ ? "No gates configured."
1080
+ : results.map((gate) => `${gate.passed ? "✓" : "✗"} ${gate.gate.type}: ${gate.gate.target} — ${gate.output}`).join("\n");
1081
+ break;
1082
+ }
1083
+ case "set-checklist": {
1084
+ if (!id || dependencyId) throw new Error("tasks set-checklist requires exactly one task id");
1085
+ if (!checklist) throw new Error("tasks set-checklist requires --checklist-json");
1086
+ const artifact = await client.call<Record<string, unknown>, CliArtifact>("tasks.set_checklist", { id, checklist });
1087
+ result = artifact;
1088
+ human = `Updated checklist: ${artifactLabel(artifact)}`;
1089
+ break;
1090
+ }
1091
+ case "context": {
1092
+ if (id) throw new Error("tasks context accepts no positional arguments");
1093
+ const summary = await client.call<Record<string, unknown>, string | null>("tasks.context", {
1094
+ project_root: projectRoot, scope: listScope, root_task_id: rootTaskId, ...sessionScope,
1095
+ });
1096
+ result = summary;
1097
+ human = summary ?? "No open tasks.";
1098
+ break;
1099
+ }
1100
+ case "contain": {
1101
+ if (!id || !dependencyId || positional.length !== 3) throw new Error("tasks contain requires a parent id and child id");
1102
+ const artifact = await client.call<Record<string, unknown>, CliArtifact>("tasks.contain", {
1103
+ parent_id: id, child_id: dependencyId, actor: "user", source: "cli", ...(reason ? { reason } : {}), ...sessionScope,
1104
+ });
1105
+ result = artifact;
1106
+ human = `Contained: ${dependencyId} → ${artifactLabel(artifact)}`;
1107
+ break;
1108
+ }
1109
+ case "uncontain": {
1110
+ if (!id || !dependencyId || positional.length !== 3) throw new Error("tasks uncontain requires a parent id and child id");
1111
+ const artifact = await client.call<Record<string, unknown>, CliArtifact>("tasks.uncontain", {
1112
+ parent_id: id, child_id: dependencyId, actor: "user", source: "cli", ...(reason ? { reason } : {}), ...sessionScope,
1113
+ });
1114
+ result = artifact;
1115
+ human = `Removed ${dependencyId} from ${artifactLabel(artifact)}`;
1116
+ break;
1117
+ }
1118
+ case "update": {
1119
+ if (!id || dependencyId) throw new Error("tasks update requires exactly one task id");
1120
+ if (status !== undefined && status !== "todo") throw new Error("tasks update --status only supports todo for accidental creation recovery");
1121
+ if (Object.keys(updateInput).length === 0) throw new Error("tasks update requires --title, --body, --labels-json, or --status todo");
1122
+ if (updateInput.status !== undefined && !reason?.trim()) throw new Error("tasks update --status requires --reason");
1123
+ if (reason !== undefined && updateInput.status === undefined) throw new Error("tasks update --reason requires --status todo");
1124
+ const artifact = await client.call<Record<string, unknown>, CliArtifact>("tasks.update", {
1125
+ id, ...updateInput, ...(reason ? { reason } : {}), actor: "user", source: "cli",
1126
+ });
1127
+ result = artifact;
1128
+ human = `Updated: ${artifactLabel(artifact)}`;
1129
+ break;
1130
+ }
398
1131
  case "history": {
399
1132
  if (!id || dependencyId) throw new Error("tasks history requires exactly one task id");
400
1133
  const page = await client.call<{ id: string; direction: "desc" }, import("./domain/task-event.ts").TaskHistoryPage>("tasks.history", { id, direction: "desc" });
@@ -437,17 +1170,17 @@ export async function runTaskCli(args: string[], client: TaskCliClient, projectR
437
1170
  }
438
1171
  case "focus": {
439
1172
  if (!id || dependencyId) throw new Error("tasks focus requires exactly one task id");
440
- const active = await client.call<Record<string, string>, CliArtifact>("tasks.focus", { id, actor: "user", source: "cli" });
1173
+ const active = await client.call<Record<string, unknown>, CliArtifact>("tasks.focus", { id, actor: "user", source: "cli", ...sessionScope });
441
1174
  result = active;
442
1175
  human = `Active: ${artifactLabel(active)}`;
443
1176
  break;
444
1177
  }
445
1178
  case "graph": {
446
1179
  if (id) throw new Error("tasks graph accepts no positional arguments");
447
- const graph = await client.call<{ limit: number; project_root: string }, {
1180
+ const graph = await client.call<Record<string, unknown>, {
448
1181
  nodes: Array<{ dependencyIds: string[]; childIds: string[] }>;
449
1182
  rootIds: string[];
450
- }>("tasks.graph", { limit: TASK_EXECUTION_MAX_NODES + 1, project_root: projectRoot });
1183
+ }>("tasks.graph", { limit: TASK_EXECUTION_MAX_NODES + 1, project_root: projectRoot, ...sessionScope });
451
1184
  result = graph;
452
1185
  const dependencies = graph.nodes.reduce((count, node) => count + node.dependencyIds.length, 0);
453
1186
  const children = graph.nodes.reduce((count, node) => count + node.childIds.length, 0);
@@ -456,14 +1189,14 @@ export async function runTaskCli(args: string[], client: TaskCliClient, projectR
456
1189
  }
457
1190
  case "plan": {
458
1191
  if (id) throw new Error("tasks plan accepts no positional arguments");
459
- const plan = await client.call<Record<string, string>, TaskExecutionPlan>("tasks.plan", { project_root: projectRoot });
1192
+ const plan = await client.call<Record<string, unknown>, TaskExecutionPlan>("tasks.plan", { project_root: projectRoot, ...sessionScope });
460
1193
  result = plan;
461
1194
  human = planText(plan);
462
1195
  break;
463
1196
  }
464
1197
  case "complete": {
465
1198
  if (!id || dependencyId) throw new Error("tasks complete requires exactly one task id");
466
- const completion = await client.call<Record<string, string>, CliCompletion>("tasks.complete", { id, actor: "user", source: "cli" });
1199
+ const completion = await client.call<Record<string, unknown>, CliCompletion>("tasks.complete", { id, actor: "user", source: "cli", ...sessionScope });
467
1200
  result = completion;
468
1201
  const lines = [`${completion.completed ? "Completed" : "Rejected"}: ${artifactLabel(completion.artifact)}`];
469
1202
  if (completion.focused) lines.push(`Active: ${artifactLabel(completion.focused)}`);
@@ -476,7 +1209,7 @@ export async function runTaskCli(args: string[], client: TaskCliClient, projectR
476
1209
  }
477
1210
  case "start": {
478
1211
  if (!id || dependencyId) throw new Error("tasks start requires exactly one task id");
479
- const artifact = await client.call<Record<string, string>, CliArtifact>("tasks.start", { id, actor: "user", source: "cli" });
1212
+ const artifact = await client.call<Record<string, unknown>, CliArtifact>("tasks.start", { id, actor: "user", source: "cli", ...sessionScope });
480
1213
  result = artifact;
481
1214
  human = `Started: ${artifactLabel(artifact)}`;
482
1215
  break;
@@ -487,23 +1220,31 @@ export async function runTaskCli(args: string[], client: TaskCliClient, projectR
487
1220
  case "cancel": {
488
1221
  if (!id || dependencyId) throw new Error(`tasks ${action} requires exactly one task id`);
489
1222
  const operation = `tasks.${action}` as "tasks.submit" | "tasks.reject" | "tasks.retry" | "tasks.cancel";
490
- const artifact = await client.call<Record<string, string>, CliArtifact>(operation, { id, actor: "user", source: "cli" });
1223
+ const artifact = await client.call<Record<string, unknown>, CliArtifact>(operation, { id, actor: "user", source: "cli", ...sessionScope });
491
1224
  result = artifact;
492
1225
  human = `${action[0]!.toUpperCase()}${action.slice(1)}: ${artifactLabel(artifact)}`;
493
1226
  break;
494
1227
  }
495
1228
  case "depend": {
496
1229
  if (!id || !dependencyId || positional.length !== 3) throw new Error("tasks depend requires a task id and prerequisite id");
497
- const artifact = await client.call<{ id: string; dependency_id: string }, CliArtifact>("tasks.depend", {
498
- id,
499
- dependency_id: dependencyId,
1230
+ const artifact = await client.call<Record<string, unknown>, CliArtifact>("tasks.depend", {
1231
+ id, dependency_id: dependencyId, actor: "user", source: "cli", ...(reason ? { reason } : {}), ...sessionScope,
500
1232
  });
501
1233
  result = artifact;
502
1234
  human = `Dependency added: ${artifactLabel(artifact)} waits for ${dependencyId}`;
503
1235
  break;
504
1236
  }
1237
+ case "undepend": {
1238
+ if (!id || !dependencyId || positional.length !== 3) throw new Error("tasks undepend requires a task id and prerequisite id");
1239
+ const artifact = await client.call<Record<string, unknown>, CliArtifact>("tasks.undepend", {
1240
+ id, dependency_id: dependencyId, actor: "user", source: "cli", ...(reason ? { reason } : {}), ...sessionScope,
1241
+ });
1242
+ result = artifact;
1243
+ human = `Dependency removed: ${artifactLabel(artifact)} no longer waits for ${dependencyId}`;
1244
+ break;
1245
+ }
505
1246
  default:
506
- throw new Error("tasks action must be create, list, show, active, focused, focus, pause, unpause, clear-focus, update, graph, plan, history, scope, assign-project, complete, start, submit, reject, retry, cancel, or depend");
1247
+ throw new Error("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, depend, undepend, contain, uncontain, run-gates, or set-checklist");
507
1248
  }
508
1249
  return json ? JSON.stringify(result) : human;
509
1250
  }
@@ -516,6 +1257,11 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
516
1257
  console.log(await runTaskCli(args.slice(1), client));
517
1258
  return;
518
1259
  }
1260
+ if (command === "discourse") {
1261
+ const client = await connectPapyrusClient();
1262
+ console.log(await runDiscourseCli(args.slice(1), client));
1263
+ return;
1264
+ }
519
1265
  if (command === "skills") {
520
1266
  const client = await connectPapyrusClient();
521
1267
  console.log(await runSkillCli(args.slice(1), client));
@@ -531,6 +1277,40 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
531
1277
  console.log(await runMigrationCli(args.slice(1), client));
532
1278
  return;
533
1279
  }
1280
+ if (command === "migrate-ids") {
1281
+ console.log(runIdMigrationCli(args.slice(1)));
1282
+ return;
1283
+ }
1284
+ if (command === "graph") {
1285
+ const client = await connectPapyrusClient();
1286
+ console.log(await runGraphCli(args.slice(1), client));
1287
+ return;
1288
+ }
1289
+ if (command === "docs") {
1290
+ const client = await connectPapyrusClient();
1291
+ console.log(await runDocsCli(args.slice(1), client));
1292
+ return;
1293
+ }
1294
+ if (command === "rules") {
1295
+ const client = await connectPapyrusClient();
1296
+ console.log(await runRulesCli(args.slice(1), client));
1297
+ return;
1298
+ }
1299
+ if (command === "artifact") {
1300
+ const client = await connectPapyrusClient();
1301
+ console.log(await runArtifactCli(args.slice(1), client));
1302
+ return;
1303
+ }
1304
+ if (command === "gates") {
1305
+ const client = await connectPapyrusClient();
1306
+ console.log(await runGatesCli(args.slice(1), client));
1307
+ return;
1308
+ }
1309
+ if (command === "graph-projection") {
1310
+ const client = await connectPapyrusClient();
1311
+ console.log(await runGraphProjectionCli(args.slice(1), client));
1312
+ return;
1313
+ }
534
1314
  if (command !== "service") usage();
535
1315
  switch (action) {
536
1316
  case "install": installService(); break;