@danypops/papyrus 0.44.8 → 0.44.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -2
- package/src/{ports → artifact}/artifact-event-reader.ts +1 -1
- package/src/{artifact-relationship-view.ts → artifact/artifact-relationship-view.ts} +3 -3
- package/src/{ports → artifact}/artifact-store.ts +2 -2
- package/src/{artifact-subtree.ts → artifact/artifact-subtree.ts} +4 -4
- package/src/{ports → artifact}/artifact-trash-store.ts +2 -2
- package/src/{adapters → artifact}/sqlite-artifact-scope-store.ts +1 -1
- package/src/{adapters → artifact}/sqlite-artifact-store.ts +15 -15
- package/src/authority-registry.ts +3 -3
- package/src/cli/artifact-command.ts +210 -0
- package/src/cli/discuss-command.ts +256 -0
- package/src/cli/docs-command.ts +192 -0
- package/src/cli/gates-command.ts +46 -0
- package/src/cli/graph-command.ts +171 -0
- package/src/cli/graph-projection-command.ts +79 -0
- package/src/cli/log-command.ts +101 -0
- package/src/cli/migration-command.ts +40 -0
- package/src/cli/note-command.ts +186 -0
- package/src/cli/playbooks-command.ts +313 -0
- package/src/cli/rules-command.ts +220 -0
- package/src/cli/session-identity-command.ts +58 -0
- package/src/cli/shared.ts +5 -0
- package/src/cli/stricli-run.ts +30 -0
- package/src/cli/task-command.ts +892 -0
- package/src/cli.ts +32 -1914
- package/src/client.ts +2 -2
- package/src/{daemon-state.ts → daemon/daemon-state.ts} +1 -1
- package/src/{daemon.ts → daemon/daemon.ts} +3 -3
- package/src/db.ts +1 -1
- package/src/{discussion-service.ts → discussion/discussion-service.ts} +7 -6
- package/src/domain-services.ts +5 -5
- package/src/{graph-projection-service.ts → graph-projection/graph-projection-service.ts} +6 -6
- package/src/{vehicle/artifact-trash-vehicle.ts → handlers/artifact-trash.ts} +4 -4
- package/src/{vehicle/discuss-vehicle.ts → handlers/discuss.ts} +20 -25
- package/src/{vehicle/docs-vehicle.ts → handlers/docs.ts} +4 -37
- package/src/{vehicle/notes-vehicle.ts → handlers/notes.ts} +4 -38
- package/src/{vehicle/playbooks-vehicle.ts → handlers/playbooks.ts} +34 -100
- package/src/{vehicle/papyrus-vehicle.ts → handlers/registry.ts} +16 -16
- package/src/{vehicle/rules-vehicle.ts → handlers/rules.ts} +4 -37
- package/src/{vehicle/artifact-vehicle-shared.ts → handlers/shared.ts} +96 -6
- package/src/{vehicle/tasks-vehicle.ts → handlers/tasks.ts} +37 -124
- package/src/index.ts +13 -13
- package/src/{log-service.ts → log/log-service.ts} +2 -2
- package/src/modules/discuss.ts +7 -28
- package/src/modules/docs.ts +4 -24
- package/src/modules/graph-projection.ts +5 -11
- package/src/modules/logs.ts +2 -23
- package/src/modules/notes.ts +2 -23
- package/src/modules/operation-input.ts +35 -0
- package/src/modules/playbooks.ts +13 -33
- package/src/modules/rules.ts +4 -24
- package/src/modules/session-identity.ts +2 -16
- package/src/modules/tasks.ts +11 -38
- package/src/{note-service.ts → note/note-service.ts} +6 -6
- package/src/ops.ts +6 -6
- package/src/{playbook-definition.ts → playbook/playbook-definition.ts} +7 -6
- package/src/{playbook-execution.ts → playbook/playbook-execution.ts} +5 -4
- package/src/{workflow-execution.ts → playbook/workflow-execution.ts} +12 -12
- package/src/service.ts +30 -57
- package/src/{session-identity-service.ts → session-identity/session-identity-service.ts} +2 -2
- package/src/{ports → stores}/session-identity-store.ts +1 -1
- package/src/{adapters → stores}/sqlite-discussion-round-store.ts +1 -1
- package/src/{adapters → stores}/sqlite-gate-runner.ts +1 -1
- package/src/{adapters → stores}/sqlite-graph-projection-store.ts +1 -1
- package/src/{adapters → stores}/sqlite-log-store.ts +1 -1
- package/src/{adapters → stores}/sqlite-note-event-store.ts +1 -1
- package/src/{adapters → stores}/sqlite-session-identity-store.ts +1 -1
- package/src/{adapters → stores}/sqlite-task-event-store.ts +1 -1
- package/src/{adapters → stores}/sqlite-task-focus-store.ts +1 -1
- package/src/{adapters → stores}/sqlite-task-lease-store.ts +1 -1
- package/src/{adapters → stores}/sqlite-task-scope-store.ts +1 -1
- package/src/{task-context.ts → task/task-context.ts} +4 -4
- package/src/{task-execution.ts → task/task-execution.ts} +1 -1
- package/src/{task-graph-view.ts → task/task-graph-view.ts} +1 -1
- package/src/{task-relationship-view.ts → task/task-relationship-view.ts} +3 -3
- package/src/{task-service.ts → task/task-service.ts} +14 -14
- /package/src/{domain → artifact}/artifact-alias.ts +0 -0
- /package/src/{domain → artifact}/artifact-event.ts +0 -0
- /package/src/{ports → artifact}/artifact-scope-store.ts +0 -0
- /package/src/{domain → artifact}/artifact-trash.ts +0 -0
- /package/src/{domain → artifact}/artifact.ts +0 -0
- /package/src/{ports → artifact}/atomic-artifact-store.ts +0 -0
- /package/src/{log.ts → log/log.ts} +0 -0
- /package/src/{ports → stores}/discussion-round-store.ts +0 -0
- /package/src/{ports → stores}/gate-runner.ts +0 -0
- /package/src/{ports → stores}/graph-projection-store.ts +0 -0
- /package/src/{ports → stores}/graph-renderer.ts +0 -0
- /package/src/{ports → stores}/log-store.ts +0 -0
- /package/src/{ports → stores}/note-event-store.ts +0 -0
- /package/src/{ports → stores}/task-event-store.ts +0 -0
- /package/src/{ports → stores}/task-focus-store.ts +0 -0
- /package/src/{ports → stores}/task-lease-store.ts +0 -0
- /package/src/{ports → stores}/task-scope-store.ts +0 -0
|
@@ -0,0 +1,892 @@
|
|
|
1
|
+
import type { CommandContext } from "@stricli/core";
|
|
2
|
+
import { buildApplication, buildCommand, buildRouteMap, numberParser } from "@stricli/core";
|
|
3
|
+
import type { PapyrusClient } from "../client.ts";
|
|
4
|
+
import { TASK_EXECUTION_MAX_NODES } from "../constants.ts";
|
|
5
|
+
import type { GateResult } from "../domain/gate.ts";
|
|
6
|
+
import type { OperationName } from "../service.ts";
|
|
7
|
+
import type { TaskExecutionPlan } from "../task/task-execution.ts";
|
|
8
|
+
import type { TaskBlockage, TaskCompletion } from "../task/task-service.ts";
|
|
9
|
+
import { artifactLabel, type CliArtifact } from "./shared.ts";
|
|
10
|
+
import { runStricliToString } from "./stricli-run.ts";
|
|
11
|
+
|
|
12
|
+
type TaskClient = Pick<PapyrusClient, "call">;
|
|
13
|
+
|
|
14
|
+
interface TaskContext extends CommandContext {
|
|
15
|
+
readonly client: TaskClient;
|
|
16
|
+
readonly json: boolean;
|
|
17
|
+
readonly projectRoot: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type CliTaskLease = {
|
|
21
|
+
taskId: string;
|
|
22
|
+
owner: string;
|
|
23
|
+
token: string;
|
|
24
|
+
claimedAt: string;
|
|
25
|
+
leaseExpiresAt: string;
|
|
26
|
+
heartbeatAt?: string;
|
|
27
|
+
note?: string;
|
|
28
|
+
};
|
|
29
|
+
type CliCompletion = Omit<TaskCompletion, "artifact" | "blocked"> & {
|
|
30
|
+
artifact: CliArtifact;
|
|
31
|
+
blocked: Array<Omit<TaskBlockage, "artifact"> & { artifact: CliArtifact }>;
|
|
32
|
+
gates: GateResult[];
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function parseStringArray(value: string): string[] {
|
|
36
|
+
const parsed = JSON.parse(value) as unknown;
|
|
37
|
+
if (!Array.isArray(parsed) || parsed.some((entry) => typeof entry !== "string")) throw new Error("value must be a JSON string array");
|
|
38
|
+
return parsed as string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseObject(value: string): Record<string, unknown> {
|
|
42
|
+
const parsed = JSON.parse(value) as unknown;
|
|
43
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("value must be a JSON object");
|
|
44
|
+
return parsed as Record<string, unknown>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseArray(value: string): unknown[] {
|
|
48
|
+
const parsed = JSON.parse(value) as unknown;
|
|
49
|
+
if (!Array.isArray(parsed)) throw new Error("value must be a JSON array");
|
|
50
|
+
return parsed;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function render(this: TaskContext, result: unknown, human: string): void {
|
|
54
|
+
this.process.stdout.write(this.json ? JSON.stringify(result) : human);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function planText(plan: TaskExecutionPlan): string {
|
|
58
|
+
const byId = new Map(plan.nodes.map((node) => [node.id, node]));
|
|
59
|
+
const lines = ["Execution order:"];
|
|
60
|
+
plan.layers.forEach((layer, index) => {
|
|
61
|
+
lines.push(` Layer ${index + 1}:`);
|
|
62
|
+
for (const id of layer) {
|
|
63
|
+
const node = byId.get(id);
|
|
64
|
+
lines.push(node ? ` [${node.state}] ${node.id} ${node.title}` : ` [unknown] ${id}`);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
if (plan.layers.length === 0) lines.push(" (no tasks)");
|
|
68
|
+
if (plan.cycleIds.length > 0) lines.push(` Invalid cycle: ${plan.cycleIds.join(", ")}`);
|
|
69
|
+
return lines.join("\n");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// -- Focus --------------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
const activeCommand = buildCommand({
|
|
75
|
+
func: async function (this: TaskContext, flags: { sessionId?: string }) {
|
|
76
|
+
const active = await this.client.call<Record<string, unknown>, CliArtifact | null>("tasks.active", {
|
|
77
|
+
project_root: this.projectRoot,
|
|
78
|
+
session_id: flags.sessionId,
|
|
79
|
+
});
|
|
80
|
+
render.call(this, active, active ? `Active: ${artifactLabel(active)}` : "No active task.");
|
|
81
|
+
},
|
|
82
|
+
parameters: {
|
|
83
|
+
flags: { sessionId: { brief: "Scope Focus to one agent session", kind: "parsed", parse: String, placeholder: "id", optional: true } },
|
|
84
|
+
},
|
|
85
|
+
docs: { brief: "The current active Task" },
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const focusedCommand = buildCommand({
|
|
89
|
+
func: async function (this: TaskContext, flags: { sessionId?: string }) {
|
|
90
|
+
const focus = await this.client.call<Record<string, unknown>, { artifact: CliArtifact; status: "active" | "paused" } | null>(
|
|
91
|
+
"tasks.focused",
|
|
92
|
+
{ project_root: this.projectRoot, session_id: flags.sessionId },
|
|
93
|
+
);
|
|
94
|
+
render.call(this, focus, focus ? `Focused (${focus.status}): ${artifactLabel(focus.artifact)}` : "No focused task.");
|
|
95
|
+
},
|
|
96
|
+
parameters: {
|
|
97
|
+
flags: { sessionId: { brief: "Scope Focus to one agent session", kind: "parsed", parse: String, placeholder: "id", optional: true } },
|
|
98
|
+
},
|
|
99
|
+
docs: { brief: "The current focused Task and its focus status" },
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
function buildPauseUnpauseCommand(action: "pause" | "unpause") {
|
|
103
|
+
return buildCommand({
|
|
104
|
+
func: async function (this: TaskContext, flags: { sessionId?: string; sessionSecret?: string }) {
|
|
105
|
+
const focus = await this.client.call<Record<string, unknown>, { artifact: CliArtifact; status: string }>(
|
|
106
|
+
`tasks.${action}` as OperationName,
|
|
107
|
+
{ actor: "user", source: "cli", session_id: flags.sessionId, session_secret: flags.sessionSecret },
|
|
108
|
+
);
|
|
109
|
+
render.call(this, focus, `Focused (${focus.status}): ${artifactLabel(focus.artifact)}`);
|
|
110
|
+
},
|
|
111
|
+
parameters: {
|
|
112
|
+
flags: {
|
|
113
|
+
sessionId: { brief: "Scope Focus to one agent session", kind: "parsed", parse: String, placeholder: "id", optional: true },
|
|
114
|
+
sessionSecret: {
|
|
115
|
+
brief: "Required once the session is registered",
|
|
116
|
+
kind: "parsed",
|
|
117
|
+
parse: String,
|
|
118
|
+
placeholder: "secret",
|
|
119
|
+
optional: true,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
docs: { brief: `${action === "pause" ? "Pause" : "Resume"} the active Task Focus` },
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const clearFocusCommand = buildCommand({
|
|
128
|
+
func: async function (this: TaskContext, flags: { sessionId?: string; sessionSecret?: string }) {
|
|
129
|
+
const cleared = await this.client.call<Record<string, unknown>, { cleared: boolean }>("tasks.clear_focus", {
|
|
130
|
+
actor: "user",
|
|
131
|
+
source: "cli",
|
|
132
|
+
session_id: flags.sessionId,
|
|
133
|
+
session_secret: flags.sessionSecret,
|
|
134
|
+
});
|
|
135
|
+
render.call(this, cleared, cleared.cleared ? "Task focus cleared." : "No focused task.");
|
|
136
|
+
},
|
|
137
|
+
parameters: {
|
|
138
|
+
flags: {
|
|
139
|
+
sessionId: { brief: "Scope Focus to one agent session", kind: "parsed", parse: String, placeholder: "id", optional: true },
|
|
140
|
+
sessionSecret: {
|
|
141
|
+
brief: "Required once the session is registered",
|
|
142
|
+
kind: "parsed",
|
|
143
|
+
parse: String,
|
|
144
|
+
placeholder: "secret",
|
|
145
|
+
optional: true,
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
docs: { brief: "Clear the active Task Focus" },
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const reapStaleFocusCommand = buildCommand({
|
|
153
|
+
func: async function (this: TaskContext) {
|
|
154
|
+
const reaped = await this.client.call<Record<string, unknown>, { removed: number }>("tasks.reap_stale_focus", {});
|
|
155
|
+
render.call(this, reaped, `Reaped ${reaped.removed} stale Focus scope(s).`);
|
|
156
|
+
},
|
|
157
|
+
parameters: { flags: {} },
|
|
158
|
+
docs: { brief: "Reap stale Focus scopes" },
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const focusCommand = buildCommand({
|
|
162
|
+
func: async function (this: TaskContext, flags: { sessionId?: string; sessionSecret?: string }, id: string) {
|
|
163
|
+
const active = await this.client.call<Record<string, unknown>, CliArtifact>("tasks.focus", {
|
|
164
|
+
id,
|
|
165
|
+
actor: "user",
|
|
166
|
+
source: "cli",
|
|
167
|
+
session_id: flags.sessionId,
|
|
168
|
+
session_secret: flags.sessionSecret,
|
|
169
|
+
});
|
|
170
|
+
render.call(this, active, `Active: ${artifactLabel(active)}`);
|
|
171
|
+
},
|
|
172
|
+
parameters: {
|
|
173
|
+
flags: {
|
|
174
|
+
sessionId: { brief: "Scope Focus to one agent session", kind: "parsed", parse: String, placeholder: "id", optional: true },
|
|
175
|
+
sessionSecret: {
|
|
176
|
+
brief: "Required once the session is registered",
|
|
177
|
+
kind: "parsed",
|
|
178
|
+
parse: String,
|
|
179
|
+
placeholder: "secret",
|
|
180
|
+
optional: true,
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
positional: { kind: "tuple", parameters: [{ brief: "Task id", parse: String, placeholder: "id" }] },
|
|
184
|
+
},
|
|
185
|
+
docs: { brief: "Set the active Task Focus to this Task" },
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// -- Leases ---------------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
const claimCommand = buildCommand({
|
|
191
|
+
func: async function (this: TaskContext, flags: { owner: string; ttlMs?: number; note?: string }, id: string) {
|
|
192
|
+
const lease = await this.client.call<Record<string, unknown>, CliTaskLease>("tasks.claim", {
|
|
193
|
+
id,
|
|
194
|
+
owner: flags.owner,
|
|
195
|
+
ttl_ms: flags.ttlMs,
|
|
196
|
+
note: flags.note,
|
|
197
|
+
});
|
|
198
|
+
render.call(this, lease, `Claimed by "${lease.owner}" until ${lease.leaseExpiresAt} (token ${lease.token}).`);
|
|
199
|
+
},
|
|
200
|
+
parameters: {
|
|
201
|
+
flags: {
|
|
202
|
+
owner: { brief: "Lease owner", kind: "parsed", parse: String, placeholder: "owner" },
|
|
203
|
+
ttlMs: { brief: "Lease time-to-live in milliseconds", kind: "parsed", parse: numberParser, optional: true },
|
|
204
|
+
note: { brief: "Freeform note", kind: "parsed", parse: String, placeholder: "text", optional: true },
|
|
205
|
+
},
|
|
206
|
+
positional: { kind: "tuple", parameters: [{ brief: "Task id", parse: String, placeholder: "id" }] },
|
|
207
|
+
},
|
|
208
|
+
docs: { brief: "Claim this Task's lease" },
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const heartbeatLeaseCommand = buildCommand({
|
|
212
|
+
func: async function (this: TaskContext, flags: { owner: string; token: string; ttlMs?: number }, id: string) {
|
|
213
|
+
const lease = await this.client.call<Record<string, unknown>, CliTaskLease>("tasks.heartbeat_lease", {
|
|
214
|
+
id,
|
|
215
|
+
owner: flags.owner,
|
|
216
|
+
token: flags.token,
|
|
217
|
+
ttl_ms: flags.ttlMs,
|
|
218
|
+
});
|
|
219
|
+
render.call(this, lease, `Renewed until ${lease.leaseExpiresAt}.`);
|
|
220
|
+
},
|
|
221
|
+
parameters: {
|
|
222
|
+
flags: {
|
|
223
|
+
owner: { brief: "Lease owner", kind: "parsed", parse: String, placeholder: "owner" },
|
|
224
|
+
token: { brief: "Lease token from claim", kind: "parsed", parse: String, placeholder: "token" },
|
|
225
|
+
ttlMs: { brief: "New time-to-live in milliseconds", kind: "parsed", parse: numberParser, optional: true },
|
|
226
|
+
},
|
|
227
|
+
positional: { kind: "tuple", parameters: [{ brief: "Task id", parse: String, placeholder: "id" }] },
|
|
228
|
+
},
|
|
229
|
+
docs: { brief: "Extend this Task's lease" },
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const releaseLeaseCommand = buildCommand({
|
|
233
|
+
func: async function (this: TaskContext, flags: { owner: string; token: string }, id: string) {
|
|
234
|
+
const released = await this.client.call<Record<string, unknown>, { released: boolean }>("tasks.release_lease", {
|
|
235
|
+
id,
|
|
236
|
+
owner: flags.owner,
|
|
237
|
+
token: flags.token,
|
|
238
|
+
});
|
|
239
|
+
render.call(this, released, released.released ? "Lease released." : "No live lease to release.");
|
|
240
|
+
},
|
|
241
|
+
parameters: {
|
|
242
|
+
flags: {
|
|
243
|
+
owner: { brief: "Lease owner", kind: "parsed", parse: String, placeholder: "owner" },
|
|
244
|
+
token: { brief: "Lease token from claim", kind: "parsed", parse: String, placeholder: "token" },
|
|
245
|
+
},
|
|
246
|
+
positional: { kind: "tuple", parameters: [{ brief: "Task id", parse: String, placeholder: "id" }] },
|
|
247
|
+
},
|
|
248
|
+
docs: { brief: "Release this Task's lease" },
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const leaseCommand = buildCommand({
|
|
252
|
+
func: async function (this: TaskContext, _flags: Record<string, never>, id: string) {
|
|
253
|
+
const lease = await this.client.call<Record<string, unknown>, CliTaskLease | null>("tasks.lease", { id });
|
|
254
|
+
render.call(this, lease, lease ? `Leased by "${lease.owner}" until ${lease.leaseExpiresAt}.` : "No live lease.");
|
|
255
|
+
},
|
|
256
|
+
parameters: { flags: {}, positional: { kind: "tuple", parameters: [{ brief: "Task id", parse: String, placeholder: "id" }] } },
|
|
257
|
+
docs: { brief: "Show this Task's current lease" },
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const reapStaleLeasesCommand = buildCommand({
|
|
261
|
+
func: async function (this: TaskContext) {
|
|
262
|
+
const reaped = await this.client.call<Record<string, unknown>, { removed: number }>("tasks.reap_stale_leases", {});
|
|
263
|
+
render.call(this, reaped, `Reaped ${reaped.removed} expired lease(s).`);
|
|
264
|
+
},
|
|
265
|
+
parameters: { flags: {} },
|
|
266
|
+
docs: { brief: "Reap expired leases" },
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// -- Events -------------------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
const eventFeedCommand = buildCommand({
|
|
272
|
+
func: async function (this: TaskContext, flags: { cursor?: number; limit?: number; eventTypesJson?: string[] }) {
|
|
273
|
+
const page = await this.client.call<
|
|
274
|
+
Record<string, unknown>,
|
|
275
|
+
{ events: Array<{ id: string; occurredAt: string; taskId: string; type: string }> }
|
|
276
|
+
>("tasks.event_feed", { cursor: flags.cursor, limit: flags.limit, event_types: flags.eventTypesJson });
|
|
277
|
+
render.call(
|
|
278
|
+
this,
|
|
279
|
+
page,
|
|
280
|
+
page.events.length === 0
|
|
281
|
+
? "No events."
|
|
282
|
+
: page.events.map((event) => `${event.id} ${event.occurredAt} ${event.taskId} ${event.type}`).join("\n"),
|
|
283
|
+
);
|
|
284
|
+
},
|
|
285
|
+
parameters: {
|
|
286
|
+
flags: {
|
|
287
|
+
cursor: { brief: "Pagination cursor", kind: "parsed", parse: numberParser, optional: true },
|
|
288
|
+
limit: { brief: "Maximum events to return", kind: "parsed", parse: numberParser, optional: true },
|
|
289
|
+
eventTypesJson: {
|
|
290
|
+
brief: "JSON string array of event types to filter by",
|
|
291
|
+
kind: "parsed",
|
|
292
|
+
parse: parseStringArray,
|
|
293
|
+
placeholder: "json",
|
|
294
|
+
optional: true,
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
docs: { brief: "Feed of raw Task lifecycle events across every task" },
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// -- CRUD -----------------------------------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
const createCommand = buildCommand({
|
|
304
|
+
func: async function (
|
|
305
|
+
this: TaskContext,
|
|
306
|
+
flags: {
|
|
307
|
+
title: string;
|
|
308
|
+
body?: string;
|
|
309
|
+
status?: string;
|
|
310
|
+
labelsJson?: string[];
|
|
311
|
+
extraJson?: Record<string, unknown>;
|
|
312
|
+
gatesJson?: unknown[];
|
|
313
|
+
checklistJson?: Record<string, unknown>;
|
|
314
|
+
templateId?: string;
|
|
315
|
+
parentId?: string;
|
|
316
|
+
dependsOnJson?: string[];
|
|
317
|
+
sessionId?: string;
|
|
318
|
+
},
|
|
319
|
+
) {
|
|
320
|
+
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>("tasks.create", {
|
|
321
|
+
title: flags.title,
|
|
322
|
+
body: flags.body,
|
|
323
|
+
status: flags.status,
|
|
324
|
+
labels: flags.labelsJson,
|
|
325
|
+
extra: flags.extraJson,
|
|
326
|
+
gates: flags.gatesJson,
|
|
327
|
+
checklist: flags.checklistJson,
|
|
328
|
+
template_id: flags.templateId,
|
|
329
|
+
parent_id: flags.parentId,
|
|
330
|
+
depends_on: flags.dependsOnJson,
|
|
331
|
+
project_root: this.projectRoot,
|
|
332
|
+
actor: "user",
|
|
333
|
+
source: "cli",
|
|
334
|
+
session_id: flags.sessionId,
|
|
335
|
+
});
|
|
336
|
+
render.call(this, artifact, `Created task: ${artifactLabel(artifact)}`);
|
|
337
|
+
},
|
|
338
|
+
parameters: {
|
|
339
|
+
flags: {
|
|
340
|
+
title: { brief: "Task title", kind: "parsed", parse: String, placeholder: "text" },
|
|
341
|
+
body: { brief: "Task body", kind: "parsed", parse: String, placeholder: "text", optional: true },
|
|
342
|
+
status: { brief: "Initial status", kind: "parsed", parse: String, placeholder: "status", optional: true },
|
|
343
|
+
labelsJson: { brief: "JSON string array of labels", kind: "parsed", parse: parseStringArray, placeholder: "json", optional: true },
|
|
344
|
+
extraJson: { brief: "JSON object of extra fields", kind: "parsed", parse: parseObject, placeholder: "json", optional: true },
|
|
345
|
+
gatesJson: { brief: "JSON array of gate definitions", kind: "parsed", parse: parseArray, placeholder: "json", optional: true },
|
|
346
|
+
checklistJson: { brief: "JSON object checklist", kind: "parsed", parse: parseObject, placeholder: "json", optional: true },
|
|
347
|
+
templateId: { brief: "Template to instantiate from", kind: "parsed", parse: String, placeholder: "id", optional: true },
|
|
348
|
+
parentId: { brief: "Parent task id", kind: "parsed", parse: String, placeholder: "id", optional: true },
|
|
349
|
+
dependsOnJson: {
|
|
350
|
+
brief: "JSON string array of prerequisite task ids",
|
|
351
|
+
kind: "parsed",
|
|
352
|
+
parse: parseStringArray,
|
|
353
|
+
placeholder: "json",
|
|
354
|
+
optional: true,
|
|
355
|
+
},
|
|
356
|
+
sessionId: {
|
|
357
|
+
brief: "Attribute this creation to one agent session",
|
|
358
|
+
kind: "parsed",
|
|
359
|
+
parse: String,
|
|
360
|
+
placeholder: "id",
|
|
361
|
+
optional: true,
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
docs: { brief: "Create a Task" },
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const listCommand = buildCommand({
|
|
369
|
+
func: async function (
|
|
370
|
+
this: TaskContext,
|
|
371
|
+
flags: {
|
|
372
|
+
status?: string;
|
|
373
|
+
text?: string;
|
|
374
|
+
limit?: number;
|
|
375
|
+
labelsJson?: string[];
|
|
376
|
+
scope?: "project" | "graph" | "all";
|
|
377
|
+
rootTaskId?: string;
|
|
378
|
+
sessionId?: string;
|
|
379
|
+
},
|
|
380
|
+
) {
|
|
381
|
+
const rows = await this.client.call<Record<string, unknown>, CliArtifact[]>("tasks.list", {
|
|
382
|
+
status: flags.status,
|
|
383
|
+
text: flags.text,
|
|
384
|
+
limit: flags.limit,
|
|
385
|
+
labels: flags.labelsJson,
|
|
386
|
+
project_root: this.projectRoot,
|
|
387
|
+
scope: flags.scope,
|
|
388
|
+
root_task_id: flags.rootTaskId,
|
|
389
|
+
session_id: flags.sessionId,
|
|
390
|
+
});
|
|
391
|
+
render.call(this, rows, rows.length === 0 ? "No tasks found." : rows.map((row) => artifactLabel(row)).join("\n"));
|
|
392
|
+
},
|
|
393
|
+
parameters: {
|
|
394
|
+
flags: {
|
|
395
|
+
status: { brief: "Filter by status", kind: "parsed", parse: String, placeholder: "status", optional: true },
|
|
396
|
+
text: { brief: "Substring match against title/body", kind: "parsed", parse: String, placeholder: "text", optional: true },
|
|
397
|
+
limit: { brief: "Maximum tasks to return", kind: "parsed", parse: numberParser, optional: true },
|
|
398
|
+
labelsJson: {
|
|
399
|
+
brief: "JSON string array of labels to filter by",
|
|
400
|
+
kind: "parsed",
|
|
401
|
+
parse: parseStringArray,
|
|
402
|
+
placeholder: "json",
|
|
403
|
+
optional: true,
|
|
404
|
+
},
|
|
405
|
+
scope: { brief: "project|graph|all", kind: "enum", values: ["project", "graph", "all"], optional: true },
|
|
406
|
+
rootTaskId: { brief: "Root task id, required with graph scope", kind: "parsed", parse: String, placeholder: "id", optional: true },
|
|
407
|
+
sessionId: { brief: "Scope to one agent session", kind: "parsed", parse: String, placeholder: "id", optional: true },
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
docs: { brief: "List Tasks" },
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
const showCommand = buildCommand({
|
|
414
|
+
func: async function (this: TaskContext, _flags: Record<string, never>, id: string) {
|
|
415
|
+
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>("tasks.show", { id });
|
|
416
|
+
render.call(this, artifact, `${artifactLabel(artifact)}\n\n${artifact.body ?? ""}`);
|
|
417
|
+
},
|
|
418
|
+
parameters: { flags: {}, positional: { kind: "tuple", parameters: [{ brief: "Task id", parse: String, placeholder: "id" }] } },
|
|
419
|
+
docs: { brief: "Show one Task" },
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
const runGatesCommand = buildCommand({
|
|
423
|
+
func: async function (this: TaskContext, _flags: Record<string, never>, id: string) {
|
|
424
|
+
const results = await this.client.call<Record<string, unknown>, GateResult[]>("tasks.run_gates", { id, actor: "user", source: "cli" });
|
|
425
|
+
render.call(
|
|
426
|
+
this,
|
|
427
|
+
results,
|
|
428
|
+
results.length === 0
|
|
429
|
+
? "No gates configured."
|
|
430
|
+
: results.map((gate) => `${gate.passed ? "✓" : "✗"} ${gate.gate.type}: ${gate.gate.target} — ${gate.output}`).join("\n"),
|
|
431
|
+
);
|
|
432
|
+
},
|
|
433
|
+
parameters: { flags: {}, positional: { kind: "tuple", parameters: [{ brief: "Task id", parse: String, placeholder: "id" }] } },
|
|
434
|
+
docs: { brief: "Run a Task's configured gates without transitioning its status" },
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
const setChecklistCommand = buildCommand({
|
|
438
|
+
func: async function (this: TaskContext, flags: { checklistJson: Record<string, unknown> }, id: string) {
|
|
439
|
+
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>("tasks.set_checklist", {
|
|
440
|
+
id,
|
|
441
|
+
checklist: flags.checklistJson,
|
|
442
|
+
});
|
|
443
|
+
render.call(this, artifact, `Updated checklist: ${artifactLabel(artifact)}`);
|
|
444
|
+
},
|
|
445
|
+
parameters: {
|
|
446
|
+
flags: { checklistJson: { brief: "JSON object checklist", kind: "parsed", parse: parseObject, placeholder: "json" } },
|
|
447
|
+
positional: { kind: "tuple", parameters: [{ brief: "Task id", parse: String, placeholder: "id" }] },
|
|
448
|
+
},
|
|
449
|
+
docs: { brief: "Replace a Task's evidence-bearing checklist" },
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
const setGatesCommand = buildCommand({
|
|
453
|
+
func: async function (this: TaskContext, flags: { gatesJson: unknown[] }, id: string) {
|
|
454
|
+
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>("tasks.set_gates", { id, gates: flags.gatesJson });
|
|
455
|
+
render.call(this, artifact, `Updated gates: ${artifactLabel(artifact)}`);
|
|
456
|
+
},
|
|
457
|
+
parameters: {
|
|
458
|
+
flags: { gatesJson: { brief: "JSON array of gate definitions", kind: "parsed", parse: parseArray, placeholder: "json" } },
|
|
459
|
+
positional: { kind: "tuple", parameters: [{ brief: "Task id", parse: String, placeholder: "id" }] },
|
|
460
|
+
},
|
|
461
|
+
docs: { brief: "Replace a Task's gate commands" },
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
const contextCommand = buildCommand({
|
|
465
|
+
func: async function (this: TaskContext, flags: { scope?: "project" | "graph" | "all"; rootTaskId?: string; sessionId?: string }) {
|
|
466
|
+
const summary = await this.client.call<Record<string, unknown>, string | null>("tasks.context", {
|
|
467
|
+
project_root: this.projectRoot,
|
|
468
|
+
scope: flags.scope,
|
|
469
|
+
root_task_id: flags.rootTaskId,
|
|
470
|
+
session_id: flags.sessionId,
|
|
471
|
+
});
|
|
472
|
+
render.call(this, summary, summary ?? "No open tasks.");
|
|
473
|
+
},
|
|
474
|
+
parameters: {
|
|
475
|
+
flags: {
|
|
476
|
+
scope: { brief: "project|graph|all", kind: "enum", values: ["project", "graph", "all"], optional: true },
|
|
477
|
+
rootTaskId: { brief: "Root task id, required with graph scope", kind: "parsed", parse: String, placeholder: "id", optional: true },
|
|
478
|
+
sessionId: { brief: "Scope to one agent session", kind: "parsed", parse: String, placeholder: "id", optional: true },
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
docs: { brief: "The full plan-reconciliation context" },
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
function buildContainmentCommand(operation: OperationName, human: (artifact: CliArtifact, second: string) => string, brief: string) {
|
|
485
|
+
return buildCommand({
|
|
486
|
+
func: async function (this: TaskContext, flags: { reason?: string; sessionId?: string }, parentId: string, childId: string) {
|
|
487
|
+
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>(operation, {
|
|
488
|
+
parent_id: parentId,
|
|
489
|
+
child_id: childId,
|
|
490
|
+
actor: "user",
|
|
491
|
+
source: "cli",
|
|
492
|
+
reason: flags.reason,
|
|
493
|
+
session_id: flags.sessionId,
|
|
494
|
+
});
|
|
495
|
+
render.call(this, artifact, human(artifact, childId));
|
|
496
|
+
},
|
|
497
|
+
parameters: {
|
|
498
|
+
flags: {
|
|
499
|
+
reason: { brief: "Why this change was made", kind: "parsed", parse: String, placeholder: "text", optional: true },
|
|
500
|
+
sessionId: {
|
|
501
|
+
brief: "Attribute this change to one agent session",
|
|
502
|
+
kind: "parsed",
|
|
503
|
+
parse: String,
|
|
504
|
+
placeholder: "id",
|
|
505
|
+
optional: true,
|
|
506
|
+
},
|
|
507
|
+
},
|
|
508
|
+
positional: {
|
|
509
|
+
kind: "tuple",
|
|
510
|
+
parameters: [
|
|
511
|
+
{ brief: "Parent task id", parse: String, placeholder: "parent-id" },
|
|
512
|
+
{ brief: "Child task id", parse: String, placeholder: "child-id" },
|
|
513
|
+
],
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
docs: { brief },
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const updateCommand = buildCommand({
|
|
521
|
+
func: async function (
|
|
522
|
+
this: TaskContext,
|
|
523
|
+
flags: { title?: string; body?: string; labelsJson?: string[]; status?: string; reason?: string },
|
|
524
|
+
id: string,
|
|
525
|
+
) {
|
|
526
|
+
if (flags.status !== undefined && flags.status !== "todo")
|
|
527
|
+
throw new Error("tasks update --status only supports todo for accidental creation recovery");
|
|
528
|
+
const recovering = flags.status === "todo";
|
|
529
|
+
if (flags.title === undefined && flags.body === undefined && flags.labelsJson === undefined && !recovering)
|
|
530
|
+
throw new Error("tasks update requires --title, --body, --labels-json, or --status todo");
|
|
531
|
+
if (recovering && !flags.reason?.trim()) throw new Error("tasks update --status requires --reason");
|
|
532
|
+
if (flags.reason !== undefined && !recovering) throw new Error("tasks update --reason requires --status todo");
|
|
533
|
+
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>("tasks.update", {
|
|
534
|
+
id,
|
|
535
|
+
title: flags.title,
|
|
536
|
+
body: flags.body,
|
|
537
|
+
labels: flags.labelsJson,
|
|
538
|
+
...(recovering ? { status: flags.status } : {}),
|
|
539
|
+
reason: flags.reason,
|
|
540
|
+
actor: "user",
|
|
541
|
+
source: "cli",
|
|
542
|
+
});
|
|
543
|
+
render.call(this, artifact, `Updated: ${artifactLabel(artifact)}`);
|
|
544
|
+
},
|
|
545
|
+
parameters: {
|
|
546
|
+
flags: {
|
|
547
|
+
title: { brief: "New title", kind: "parsed", parse: String, placeholder: "text", optional: true },
|
|
548
|
+
body: { brief: "New body", kind: "parsed", parse: String, placeholder: "text", optional: true },
|
|
549
|
+
labelsJson: { brief: "JSON string array of labels", kind: "parsed", parse: parseStringArray, placeholder: "json", optional: true },
|
|
550
|
+
status: {
|
|
551
|
+
brief: "Only 'todo', to recover from an accidental terminal state",
|
|
552
|
+
kind: "parsed",
|
|
553
|
+
parse: String,
|
|
554
|
+
placeholder: "status",
|
|
555
|
+
optional: true,
|
|
556
|
+
},
|
|
557
|
+
reason: { brief: "Required alongside --status todo", kind: "parsed", parse: String, placeholder: "text", optional: true },
|
|
558
|
+
},
|
|
559
|
+
positional: { kind: "tuple", parameters: [{ brief: "Task id", parse: String, placeholder: "id" }] },
|
|
560
|
+
},
|
|
561
|
+
docs: { brief: "Recover an accidentally-terminal task, or change its title/body/labels" },
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
const historyCommand = buildCommand({
|
|
565
|
+
func: async function (this: TaskContext, _flags: Record<string, never>, id: string) {
|
|
566
|
+
const page = await this.client.call<
|
|
567
|
+
{ id: string; direction: "desc" },
|
|
568
|
+
{
|
|
569
|
+
events: Array<{
|
|
570
|
+
occurredAt: string;
|
|
571
|
+
type: string;
|
|
572
|
+
fromStatus?: string;
|
|
573
|
+
toStatus?: string;
|
|
574
|
+
actor: string;
|
|
575
|
+
source: string;
|
|
576
|
+
reason?: string;
|
|
577
|
+
}>;
|
|
578
|
+
}
|
|
579
|
+
>("tasks.history", { id, direction: "desc" });
|
|
580
|
+
render.call(
|
|
581
|
+
this,
|
|
582
|
+
page,
|
|
583
|
+
page.events.length === 0
|
|
584
|
+
? `No recorded history for ${id}.`
|
|
585
|
+
: [...page.events]
|
|
586
|
+
.reverse()
|
|
587
|
+
.map(
|
|
588
|
+
(event) =>
|
|
589
|
+
`${event.occurredAt} ${event.type} ${event.fromStatus ?? "∅"} → ${event.toStatus ?? "∅"} · ${event.actor}/${event.source}${event.reason ? ` · ${event.reason}` : ""}`,
|
|
590
|
+
)
|
|
591
|
+
.join("\n"),
|
|
592
|
+
);
|
|
593
|
+
},
|
|
594
|
+
parameters: { flags: {}, positional: { kind: "tuple", parameters: [{ brief: "Task id", parse: String, placeholder: "id" }] } },
|
|
595
|
+
docs: { brief: "Task's append-only lifecycle event history" },
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
const scopeCommand = buildCommand({
|
|
599
|
+
func: async function (this: TaskContext, _flags: Record<string, never>, mode?: string, rootTaskId?: string) {
|
|
600
|
+
if (mode === undefined) {
|
|
601
|
+
const selection = await this.client.call<Record<string, string>, { mode: string; label: string }>("tasks.scope", {
|
|
602
|
+
project_root: this.projectRoot,
|
|
603
|
+
});
|
|
604
|
+
render.call(this, selection, `Task scope: ${selection.label}`);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
if (mode !== "project" && mode !== "all" && mode !== "graph") throw new Error("tasks scope mode must be project, all, or graph");
|
|
608
|
+
if (mode === "graph" && !rootTaskId) throw new Error("tasks scope graph requires a root task id");
|
|
609
|
+
if (mode !== "graph" && rootTaskId) throw new Error(`tasks scope ${mode} accepts no root task id`);
|
|
610
|
+
const selection = await this.client.call<Record<string, unknown>, { mode: string; label: string }>("tasks.set_scope", {
|
|
611
|
+
project_root: this.projectRoot,
|
|
612
|
+
scope: mode,
|
|
613
|
+
root_task_id: rootTaskId,
|
|
614
|
+
});
|
|
615
|
+
render.call(this, selection, `Task scope: ${selection.label}`);
|
|
616
|
+
},
|
|
617
|
+
parameters: {
|
|
618
|
+
flags: {},
|
|
619
|
+
positional: {
|
|
620
|
+
kind: "tuple",
|
|
621
|
+
parameters: [
|
|
622
|
+
{ brief: "project|all|graph", parse: String, placeholder: "mode", optional: true },
|
|
623
|
+
{ brief: "Root task id, required with graph mode", parse: String, placeholder: "root-task-id", optional: true },
|
|
624
|
+
],
|
|
625
|
+
},
|
|
626
|
+
},
|
|
627
|
+
docs: { brief: "Describe or set the task-view scope" },
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
const assignProjectCommand = buildCommand({
|
|
631
|
+
func: async function (this: TaskContext, _flags: Record<string, never>, id: string, projectRoot?: string) {
|
|
632
|
+
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>("tasks.assign_project", {
|
|
633
|
+
id,
|
|
634
|
+
project_root: projectRoot ?? this.projectRoot,
|
|
635
|
+
actor: "user",
|
|
636
|
+
source: "cli",
|
|
637
|
+
});
|
|
638
|
+
render.call(this, artifact, `Project assigned: ${artifactLabel(artifact)}`);
|
|
639
|
+
},
|
|
640
|
+
parameters: {
|
|
641
|
+
flags: {},
|
|
642
|
+
positional: {
|
|
643
|
+
kind: "tuple",
|
|
644
|
+
parameters: [
|
|
645
|
+
{ brief: "Task id", parse: String, placeholder: "id" },
|
|
646
|
+
{ brief: "New project root, defaults to the caller's own", parse: String, placeholder: "project-root", optional: true },
|
|
647
|
+
],
|
|
648
|
+
},
|
|
649
|
+
},
|
|
650
|
+
docs: { brief: "Reassign a Task's project root" },
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
const graphCommand = buildCommand({
|
|
654
|
+
func: async function (
|
|
655
|
+
this: TaskContext,
|
|
656
|
+
flags: { labelsJson?: string[]; scope?: "project" | "graph" | "all"; rootTaskId?: string; sessionId?: string },
|
|
657
|
+
) {
|
|
658
|
+
const graph = await this.client.call<
|
|
659
|
+
Record<string, unknown>,
|
|
660
|
+
{ nodes: Array<{ dependencyIds: string[]; childIds: string[] }>; rootIds: string[] }
|
|
661
|
+
>("tasks.graph", {
|
|
662
|
+
limit: TASK_EXECUTION_MAX_NODES + 1,
|
|
663
|
+
labels: flags.labelsJson,
|
|
664
|
+
project_root: this.projectRoot,
|
|
665
|
+
scope: flags.scope,
|
|
666
|
+
root_task_id: flags.rootTaskId,
|
|
667
|
+
session_id: flags.sessionId,
|
|
668
|
+
});
|
|
669
|
+
const dependencies = graph.nodes.reduce((count, node) => count + node.dependencyIds.length, 0);
|
|
670
|
+
const children = graph.nodes.reduce((count, node) => count + node.childIds.length, 0);
|
|
671
|
+
render.call(
|
|
672
|
+
this,
|
|
673
|
+
graph,
|
|
674
|
+
`Task graph: ${graph.nodes.length} nodes, ${graph.rootIds.length} roots, ${dependencies} dependencies, ${children} containment edges`,
|
|
675
|
+
);
|
|
676
|
+
},
|
|
677
|
+
parameters: {
|
|
678
|
+
flags: {
|
|
679
|
+
labelsJson: {
|
|
680
|
+
brief: "JSON string array of labels to filter by",
|
|
681
|
+
kind: "parsed",
|
|
682
|
+
parse: parseStringArray,
|
|
683
|
+
placeholder: "json",
|
|
684
|
+
optional: true,
|
|
685
|
+
},
|
|
686
|
+
scope: { brief: "project|graph|all", kind: "enum", values: ["project", "graph", "all"], optional: true },
|
|
687
|
+
rootTaskId: { brief: "Root task id, required with graph scope", kind: "parsed", parse: String, placeholder: "id", optional: true },
|
|
688
|
+
sessionId: { brief: "Scope to one agent session", kind: "parsed", parse: String, placeholder: "id", optional: true },
|
|
689
|
+
},
|
|
690
|
+
},
|
|
691
|
+
docs: { brief: "The full task graph" },
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
const planCommand = buildCommand({
|
|
695
|
+
func: async function (this: TaskContext, flags: { sessionId?: string }) {
|
|
696
|
+
const plan = await this.client.call<Record<string, unknown>, TaskExecutionPlan>("tasks.plan", {
|
|
697
|
+
project_root: this.projectRoot,
|
|
698
|
+
session_id: flags.sessionId,
|
|
699
|
+
});
|
|
700
|
+
render.call(this, plan, planText(plan));
|
|
701
|
+
},
|
|
702
|
+
parameters: {
|
|
703
|
+
flags: { sessionId: { brief: "Scope to one agent session", kind: "parsed", parse: String, placeholder: "id", optional: true } },
|
|
704
|
+
},
|
|
705
|
+
docs: { brief: "Layered execution order for the task graph" },
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
const completeCommand = buildCommand({
|
|
709
|
+
func: async function (this: TaskContext, flags: { sessionId?: string }, id: string) {
|
|
710
|
+
const completion = await this.client.call<Record<string, unknown>, CliCompletion>("tasks.complete", {
|
|
711
|
+
id,
|
|
712
|
+
actor: "user",
|
|
713
|
+
source: "cli",
|
|
714
|
+
session_id: flags.sessionId,
|
|
715
|
+
});
|
|
716
|
+
const lines = [`${completion.completed ? "Completed" : "Rejected"}: ${artifactLabel(completion.artifact)}`];
|
|
717
|
+
if (completion.focused) lines.push(`Active: ${artifactLabel(completion.focused)}`);
|
|
718
|
+
if (completion.blocked.length > 0) {
|
|
719
|
+
lines.push(
|
|
720
|
+
`Blocked: ${completion.blocked.map((entry) => `${artifactLabel(entry.artifact)} waits for ${entry.dependencyIds.join(", ")}`).join("; ")}`,
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
for (const gate of completion.gates) lines.push(`${gate.passed ? "✓" : "✗"} ${gate.gate.type}: ${gate.gate.target} — ${gate.output}`);
|
|
724
|
+
render.call(this, completion, lines.join("\n"));
|
|
725
|
+
},
|
|
726
|
+
parameters: {
|
|
727
|
+
flags: { sessionId: { brief: "Scope to one agent session", kind: "parsed", parse: String, placeholder: "id", optional: true } },
|
|
728
|
+
positional: { kind: "tuple", parameters: [{ brief: "Task id", parse: String, placeholder: "id" }] },
|
|
729
|
+
},
|
|
730
|
+
docs: { brief: "Run gates and checklist review, then complete" },
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
const startCommand = buildCommand({
|
|
734
|
+
func: async function (this: TaskContext, flags: { sessionId?: string }, id: string) {
|
|
735
|
+
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>("tasks.start", {
|
|
736
|
+
id,
|
|
737
|
+
actor: "user",
|
|
738
|
+
source: "cli",
|
|
739
|
+
session_id: flags.sessionId,
|
|
740
|
+
});
|
|
741
|
+
render.call(this, artifact, `Started: ${artifactLabel(artifact)}`);
|
|
742
|
+
},
|
|
743
|
+
parameters: {
|
|
744
|
+
flags: { sessionId: { brief: "Scope to one agent session", kind: "parsed", parse: String, placeholder: "id", optional: true } },
|
|
745
|
+
positional: { kind: "tuple", parameters: [{ brief: "Task id", parse: String, placeholder: "id" }] },
|
|
746
|
+
},
|
|
747
|
+
docs: { brief: "Lifecycle transition: todo -> in-progress" },
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
function buildSimpleTransitionCommand(action: "submit" | "reject" | "retry" | "cancel", brief: string) {
|
|
751
|
+
return buildCommand({
|
|
752
|
+
func: async function (this: TaskContext, flags: { sessionId?: string }, id: string) {
|
|
753
|
+
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>(`tasks.${action}` as OperationName, {
|
|
754
|
+
id,
|
|
755
|
+
actor: "user",
|
|
756
|
+
source: "cli",
|
|
757
|
+
session_id: flags.sessionId,
|
|
758
|
+
});
|
|
759
|
+
render.call(this, artifact, `${action[0]!.toUpperCase()}${action.slice(1)}: ${artifactLabel(artifact)}`);
|
|
760
|
+
},
|
|
761
|
+
parameters: {
|
|
762
|
+
flags: { sessionId: { brief: "Scope to one agent session", kind: "parsed", parse: String, placeholder: "id", optional: true } },
|
|
763
|
+
positional: { kind: "tuple", parameters: [{ brief: "Task id", parse: String, placeholder: "id" }] },
|
|
764
|
+
},
|
|
765
|
+
docs: { brief },
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const cancelSubtreeCommand = buildCommand({
|
|
770
|
+
func: async function (this: TaskContext, flags: { sessionId?: string }, id: string) {
|
|
771
|
+
const outcome = await this.client.call<Record<string, unknown>, { canceled: string[]; skipped: string[] }>("tasks.cancel_subtree", {
|
|
772
|
+
id,
|
|
773
|
+
actor: "user",
|
|
774
|
+
source: "cli",
|
|
775
|
+
session_id: flags.sessionId,
|
|
776
|
+
});
|
|
777
|
+
render.call(
|
|
778
|
+
this,
|
|
779
|
+
outcome,
|
|
780
|
+
`Canceled ${outcome.canceled.length} task(s)${outcome.skipped.length > 0 ? `, skipped ${outcome.skipped.length} already-terminal` : ""}.`,
|
|
781
|
+
);
|
|
782
|
+
},
|
|
783
|
+
parameters: {
|
|
784
|
+
flags: { sessionId: { brief: "Scope to one agent session", kind: "parsed", parse: String, placeholder: "id", optional: true } },
|
|
785
|
+
positional: { kind: "tuple", parameters: [{ brief: "Task id", parse: String, placeholder: "id" }] },
|
|
786
|
+
},
|
|
787
|
+
docs: { brief: "Cancel a Task and its whole containment subtree" },
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
function buildDependencyCommand(operation: OperationName, human: (artifact: CliArtifact, second: string) => string, brief: string) {
|
|
791
|
+
return buildCommand({
|
|
792
|
+
func: async function (this: TaskContext, flags: { reason?: string; sessionId?: string }, id: string, dependencyId: string) {
|
|
793
|
+
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>(operation, {
|
|
794
|
+
id,
|
|
795
|
+
dependency_id: dependencyId,
|
|
796
|
+
actor: "user",
|
|
797
|
+
source: "cli",
|
|
798
|
+
reason: flags.reason,
|
|
799
|
+
session_id: flags.sessionId,
|
|
800
|
+
});
|
|
801
|
+
render.call(this, artifact, human(artifact, dependencyId));
|
|
802
|
+
},
|
|
803
|
+
parameters: {
|
|
804
|
+
flags: {
|
|
805
|
+
reason: { brief: "Why this change was made", kind: "parsed", parse: String, placeholder: "text", optional: true },
|
|
806
|
+
sessionId: {
|
|
807
|
+
brief: "Attribute this change to one agent session",
|
|
808
|
+
kind: "parsed",
|
|
809
|
+
parse: String,
|
|
810
|
+
placeholder: "id",
|
|
811
|
+
optional: true,
|
|
812
|
+
},
|
|
813
|
+
},
|
|
814
|
+
positional: {
|
|
815
|
+
kind: "tuple",
|
|
816
|
+
parameters: [
|
|
817
|
+
{ brief: "Task id", parse: String, placeholder: "id" },
|
|
818
|
+
{ brief: "Prerequisite task id", parse: String, placeholder: "prerequisite-id" },
|
|
819
|
+
],
|
|
820
|
+
},
|
|
821
|
+
},
|
|
822
|
+
docs: { brief },
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const app = buildApplication(
|
|
827
|
+
buildRouteMap({
|
|
828
|
+
routes: {
|
|
829
|
+
active: activeCommand,
|
|
830
|
+
focused: focusedCommand,
|
|
831
|
+
pause: buildPauseUnpauseCommand("pause"),
|
|
832
|
+
unpause: buildPauseUnpauseCommand("unpause"),
|
|
833
|
+
"clear-focus": clearFocusCommand,
|
|
834
|
+
"reap-stale-focus": reapStaleFocusCommand,
|
|
835
|
+
claim: claimCommand,
|
|
836
|
+
"heartbeat-lease": heartbeatLeaseCommand,
|
|
837
|
+
"release-lease": releaseLeaseCommand,
|
|
838
|
+
lease: leaseCommand,
|
|
839
|
+
"reap-stale-leases": reapStaleLeasesCommand,
|
|
840
|
+
"event-feed": eventFeedCommand,
|
|
841
|
+
create: createCommand,
|
|
842
|
+
list: listCommand,
|
|
843
|
+
show: showCommand,
|
|
844
|
+
"run-gates": runGatesCommand,
|
|
845
|
+
"set-checklist": setChecklistCommand,
|
|
846
|
+
"set-gates": setGatesCommand,
|
|
847
|
+
context: contextCommand,
|
|
848
|
+
contain: buildContainmentCommand(
|
|
849
|
+
"tasks.contain",
|
|
850
|
+
(artifact, second) => `Contained: ${second} → ${artifactLabel(artifact)}`,
|
|
851
|
+
"Nest a child Task inside a parent",
|
|
852
|
+
),
|
|
853
|
+
uncontain: buildContainmentCommand(
|
|
854
|
+
"tasks.uncontain",
|
|
855
|
+
(artifact, second) => `Removed ${second} from ${artifactLabel(artifact)}`,
|
|
856
|
+
"Remove a parent/child nesting",
|
|
857
|
+
),
|
|
858
|
+
update: updateCommand,
|
|
859
|
+
history: historyCommand,
|
|
860
|
+
scope: scopeCommand,
|
|
861
|
+
"assign-project": assignProjectCommand,
|
|
862
|
+
focus: focusCommand,
|
|
863
|
+
graph: graphCommand,
|
|
864
|
+
plan: planCommand,
|
|
865
|
+
complete: completeCommand,
|
|
866
|
+
start: startCommand,
|
|
867
|
+
submit: buildSimpleTransitionCommand("submit", "Lifecycle transition: in-progress -> review"),
|
|
868
|
+
reject: buildSimpleTransitionCommand("reject", "Lifecycle transition: review -> rejected"),
|
|
869
|
+
retry: buildSimpleTransitionCommand("retry", "Lifecycle transition: rejected -> in-progress"),
|
|
870
|
+
cancel: buildSimpleTransitionCommand("cancel", "Lifecycle transition to canceled"),
|
|
871
|
+
"cancel-subtree": cancelSubtreeCommand,
|
|
872
|
+
depend: buildDependencyCommand(
|
|
873
|
+
"tasks.depend",
|
|
874
|
+
(artifact, second) => `Dependency added: ${artifactLabel(artifact)} waits for ${second}`,
|
|
875
|
+
"Add a dependency edge",
|
|
876
|
+
),
|
|
877
|
+
undepend: buildDependencyCommand(
|
|
878
|
+
"tasks.undepend",
|
|
879
|
+
(artifact, second) => `Dependency removed: ${artifactLabel(artifact)} no longer waits for ${second}`,
|
|
880
|
+
"Remove a dependency edge",
|
|
881
|
+
),
|
|
882
|
+
},
|
|
883
|
+
docs: { brief: "Task operations" },
|
|
884
|
+
}),
|
|
885
|
+
{ name: "tasks", scanner: { caseStyle: "allow-kebab-for-camel" } },
|
|
886
|
+
);
|
|
887
|
+
|
|
888
|
+
export async function runTaskCli(args: string[], client: TaskClient, projectRoot: string = process.cwd()): Promise<string> {
|
|
889
|
+
const json = args.includes("--json");
|
|
890
|
+
const positional = args.filter((arg) => arg !== "--json");
|
|
891
|
+
return runStricliToString(app, positional, { client, json, projectRoot });
|
|
892
|
+
}
|