@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.
- package/README.md +16 -2
- package/extension/src/active-task-continuation.ts +6 -0
- package/extension/src/domain-tools.ts +108 -52
- package/extension/src/index.ts +90 -37
- package/extension/src/notes.ts +14 -1
- package/extension/src/task-focus-events.ts +57 -0
- package/extension/src/tasks.ts +51 -15
- package/extension/src/tool-rendering/artifact-card.ts +117 -0
- package/extension/src/tool-rendering/artifact-list.ts +179 -0
- package/extension/src/tool-rendering/index.ts +107 -0
- package/extension/src/tool-rendering/render-model.ts +406 -0
- package/package.json +4 -2
- package/src/adapters/in-memory-conversation-journal-store.ts +48 -0
- package/src/adapters/sqlite-artifact-scope-store.ts +36 -0
- package/src/adapters/sqlite-artifact-store.ts +20 -11
- package/src/adapters/sqlite-discourse-store.ts +325 -0
- package/src/adapters/sqlite-graph-projection-store.ts +41 -0
- package/src/adapters/sqlite-task-focus-store.ts +34 -15
- package/src/authority-registry.ts +115 -0
- package/src/cli.ts +904 -124
- package/src/constants.ts +38 -5
- package/src/conversation-journal-service.ts +87 -0
- package/src/db.ts +336 -8
- package/src/domain/artifact-event.ts +99 -0
- package/src/domain/conversation-journal.ts +168 -0
- package/src/domain/discourse-store.ts +142 -0
- package/src/domain/graph-projection.ts +74 -0
- package/src/domain/task-event.ts +4 -0
- package/src/domain-services.ts +133 -38
- package/src/graph-projection-service.ts +103 -0
- package/src/id-migration.ts +200 -0
- package/src/module-registry.ts +53 -0
- package/src/modules/docs.ts +77 -0
- package/src/modules/graph-projection.ts +82 -0
- package/src/modules/notes.ts +76 -0
- package/src/modules/rules.ts +81 -0
- package/src/modules/skills.ts +113 -0
- package/src/modules/tasks.ts +164 -0
- package/src/ops.ts +142 -15
- package/src/ports/artifact-scope-store.ts +20 -0
- package/src/ports/artifact-store.ts +10 -5
- package/src/ports/conversation-journal-store.ts +17 -0
- package/src/ports/graph-projection-store.ts +15 -0
- package/src/ports/task-focus-store.ts +62 -20
- package/src/service.ts +218 -223
- 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
|
|
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
|
|
87
|
-
papyrus tasks
|
|
88
|
-
papyrus tasks
|
|
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] !== "
|
|
141
|
-
throw new Error("migrate requires exactly `
|
|
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
|
-
|
|
159
|
-
|
|
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 === "--
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
runId
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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 === "--
|
|
276
|
-
|
|
277
|
-
|
|
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")
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
365
|
-
|
|
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
|
-
|
|
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,
|
|
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<
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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<
|
|
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
|
|
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;
|