@danypops/papyrus 0.40.0 → 0.41.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 +1 -1
- package/package.json +1 -1
- package/src/vehicle/papyrus-vehicle.ts +4 -2
- package/src/vehicle/tasks-vehicle.ts +407 -0
package/README.md
CHANGED
|
@@ -84,7 +84,7 @@ The existing `artifact-template` skill subtype remains a compatibility mechanism
|
|
|
84
84
|
|
|
85
85
|
### Removing an artifact
|
|
86
86
|
|
|
87
|
-
Artifacts are never hard-deleted on request: every artifact gets a permanent, immutable `created` row in the mutation event log the moment it exists, so removal is a real, time-gated trash rather than a status flip. `remove` (
|
|
87
|
+
Artifacts are never hard-deleted on request: every artifact gets a permanent, immutable `created` row in the mutation event log the moment it exists, so removal is a real, time-gated trash rather than a status flip. `remove` (the shared `artifact.remove`/`artifact.remove_subtree` operations every agent-facing domain routes through, or `papyrus artifact remove <id> [--reason <text>]`) moves an artifact to the trash: it is immediately excluded from every list/query, still directly reachable by id, and fully recoverable via `restore` until its purge deadline (30 days later) passes. `remove` refuses a Task that is the live Task Focus in any scope.
|
|
88
88
|
|
|
89
89
|
Once the deadline passes, the daemon's periodic sweep performs a real, cascading, irreversible deletion — the one deliberate, narrow exception to Papyrus's otherwise-absolute append-only history, enforced by the database itself (not merely application code) via a trigger condition checked at delete time.
|
|
90
90
|
|
package/package.json
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* artifact.*), so merging costs nothing and avoids a separate registry/mount/client
|
|
6
6
|
* per domain.
|
|
7
7
|
*
|
|
8
|
-
* discuss
|
|
9
|
-
*
|
|
8
|
+
* discuss still registers via pi-papyrus's own pi.registerTool() in domain-tools.ts,
|
|
9
|
+
* not here -- see the papyrus Vehicle migration task for why.
|
|
10
10
|
*/
|
|
11
11
|
import { VehicleRegistry } from "@danypops/vehicle-server";
|
|
12
12
|
import type { AuthorityRegistry } from "../authority-registry.ts";
|
|
@@ -24,6 +24,7 @@ import { registerNotesVehicleOperations } from "./notes-vehicle.ts";
|
|
|
24
24
|
import { registerPlaybooksVehicleOperations } from "./playbooks-vehicle.ts";
|
|
25
25
|
import { registerRulesVehicleOperations } from "./rules-vehicle.ts";
|
|
26
26
|
import { registerSkillsVehicleOperations } from "./skills-vehicle.ts";
|
|
27
|
+
import { registerTasksVehicleOperations } from "./tasks-vehicle.ts";
|
|
27
28
|
|
|
28
29
|
export interface PapyrusVehicleDeps {
|
|
29
30
|
artifacts: ArtifactStore & ArtifactTrashStore;
|
|
@@ -43,6 +44,7 @@ export function createPapyrusVehicleRegistry(deps: PapyrusVehicleDeps): VehicleR
|
|
|
43
44
|
registerDocsVehicleOperations(registry, deps.artifacts, deps.scopes, deps.authority);
|
|
44
45
|
registerSkillsVehicleOperations(registry, { artifacts: deps.artifacts, events: deps.events, scopes: deps.taskScopes, artifactScopes: deps.scopes, authority: deps.authority, tasks: deps.tasks });
|
|
45
46
|
registerPlaybooksVehicleOperations(registry, { artifacts: deps.artifacts, events: deps.events, scopes: deps.taskScopes, artifactScopes: deps.scopes, tasks: deps.tasks, sessionIdentity: deps.sessionIdentity });
|
|
47
|
+
registerTasksVehicleOperations(registry, { tasks: deps.tasks, artifacts: deps.artifacts, sessionIdentity: deps.sessionIdentity });
|
|
46
48
|
registerArtifactTrashOperations(registry, deps.artifacts);
|
|
47
49
|
return registry;
|
|
48
50
|
}
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tasks projected as a real VehicleRegistry: one VehicleOperation per real action.
|
|
3
|
+
* Wraps modules/tasks.ts's operation definitions -- the largest domain (37 actions),
|
|
4
|
+
* already fully extracted server-side.
|
|
5
|
+
*
|
|
6
|
+
* tasks.focus/pause/unpause/clear_focus keep two things the raw RPC tool used to
|
|
7
|
+
* handle client-side, since neither is expressible inside a stateless Vehicle
|
|
8
|
+
* operation's own input/output contract:
|
|
9
|
+
*
|
|
10
|
+
* - session_secret authorizes which session's Task Focus row gets mutated
|
|
11
|
+
* (modules/tasks.ts's own guardFocusMutation). It must never be a model-visible
|
|
12
|
+
* input field -- it travels through VehicleInvocationOptions.principal.claims,
|
|
13
|
+
* the same mechanism playbooks.invoke uses (see vehicle-notes-client.ts).
|
|
14
|
+
* - papyrus.task-focus.v1 is a same-process Pi extension event bus broadcast (a
|
|
15
|
+
* token-cost router or similar can correlate its own telemetry with the
|
|
16
|
+
* currently focused task) with no Vehicle-transport equivalent -- fired from
|
|
17
|
+
* pi-papyrus's own onInvoked hook (see vehicle-client-pi's registerVehicleTools),
|
|
18
|
+
* not from this module, since a remote HTTP Vehicle consumer has no such bus.
|
|
19
|
+
*
|
|
20
|
+
* remove/remove_subtree/restore are not duplicated here -- see ./artifact-trash-vehicle.ts.
|
|
21
|
+
*/
|
|
22
|
+
import { bindVehicleOperation, defineVehicleOperation, type VehicleOperationContext } from "@danypops/vehicle-core";
|
|
23
|
+
import type { VehicleRegistry } from "@danypops/vehicle-server";
|
|
24
|
+
import type { TaskExecutionPlan } from "../task-execution.ts";
|
|
25
|
+
import type { TaskCompletion, Tasks } from "../task-service.ts";
|
|
26
|
+
import type { TaskViewMode } from "../domain/task-scope.ts";
|
|
27
|
+
import { tasksOperations } from "../modules/tasks.ts";
|
|
28
|
+
import type { ArtifactStore } from "../ports/artifact-store.ts";
|
|
29
|
+
import type { SessionIdentity } from "../session-identity-service.ts";
|
|
30
|
+
import { labelsById, looseObjectSchema, numberProp, passthroughOutput, resolveArtifactIdWidened, stringProp } from "./artifact-vehicle-shared.ts";
|
|
31
|
+
|
|
32
|
+
const OWNER = "tasks";
|
|
33
|
+
const LIMITS = { defaultTimeoutMs: 5_000, maxTimeoutMs: 30_000, maxRequestBytes: 65_536, maxResponseBytes: 262_144 };
|
|
34
|
+
|
|
35
|
+
const objectProp = { type: "object" } as unknown as { type: string };
|
|
36
|
+
const arrayProp = { type: "array" } as unknown as { type: string };
|
|
37
|
+
const boolProp = { type: "boolean" } as unknown as { type: string };
|
|
38
|
+
|
|
39
|
+
export interface TasksVehicleDeps {
|
|
40
|
+
tasks: Tasks;
|
|
41
|
+
artifacts: ArtifactStore;
|
|
42
|
+
sessionIdentity: SessionIdentity;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolves an id from either an explicit id or a title lookup scoped to the exact
|
|
47
|
+
* same view (project_root/scope/root_task_id) a plain tasks.list call under those
|
|
48
|
+
* same filters would use -- name resolution must never search a wider or narrower
|
|
49
|
+
* scope than the caller's own view. tasks.list itself requires project_root (see
|
|
50
|
+
* modules/tasks.ts's taskFilter), so resolving by name does too: there is no
|
|
51
|
+
* ambient cwd server-side to default to, unlike the removed client-side tool.
|
|
52
|
+
*
|
|
53
|
+
* A two-task action (depend/contain) routinely names tasks that live in two
|
|
54
|
+
* different projects. When the caller didn't already pin an explicit `scope`, a
|
|
55
|
+
* miss under the narrow filter retries once against `scope: "all"` before giving
|
|
56
|
+
* up -- the same widen-once behavior the removed tool's own resolveArtifactIdByName
|
|
57
|
+
* carried, hard-won from real cross-project depend/contain friction.
|
|
58
|
+
*/
|
|
59
|
+
function resolveTaskId(
|
|
60
|
+
tasks: Tasks,
|
|
61
|
+
filter: { projectRoot?: string; scope?: TaskViewMode; rootTaskId?: string },
|
|
62
|
+
id: unknown,
|
|
63
|
+
name: unknown,
|
|
64
|
+
): string {
|
|
65
|
+
if (typeof id === "string" && id.length > 0) return id;
|
|
66
|
+
if (typeof name !== "string" || name.length === 0) throw new Error("id or name is required");
|
|
67
|
+
if (!filter.projectRoot) throw new Error("project_root is required when resolving a task by name");
|
|
68
|
+
return resolveArtifactIdWidened(
|
|
69
|
+
name,
|
|
70
|
+
() => tasks.list({ ...filter, text: name }),
|
|
71
|
+
filter.scope === undefined ? () => tasks.list({ ...filter, scope: "all", text: name }) : undefined,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Resolves root_task_name first and scoped to "project" only, matching the removed tool's own resolution order -- every other name lookup below must see the caller's FINAL scope/root selection, which root_task_id itself feeds into. */
|
|
76
|
+
function resolveRootTaskId(tasks: Tasks, projectRoot: string | undefined, rootTaskId: unknown, rootTaskName: unknown): string | undefined {
|
|
77
|
+
if (typeof rootTaskId === "string" && rootTaskId.length > 0) return rootTaskId;
|
|
78
|
+
if (typeof rootTaskName !== "string" || rootTaskName.length === 0) return undefined;
|
|
79
|
+
return resolveTaskId(tasks, { projectRoot, scope: "project" }, undefined, rootTaskName);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function resolveArrayField(tasks: Tasks, filter: { projectRoot?: string; scope?: TaskViewMode; rootTaskId?: string }, ids: unknown, names: unknown): string[] | undefined {
|
|
83
|
+
if (Array.isArray(ids)) return ids as string[];
|
|
84
|
+
if (!Array.isArray(names) || names.length === 0) return undefined;
|
|
85
|
+
return names.map((entry) => resolveTaskId(tasks, filter, undefined, String(entry)));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const readSchemaProps = { status: stringProp, text: stringProp, limit: numberProp, project_root: stringProp, scope: { type: "string", enum: ["project", "graph", "all"] }, root_task_id: stringProp, root_task_name: stringProp, session_id: stringProp, labels: arrayProp };
|
|
89
|
+
|
|
90
|
+
/** Same gate/checklist narrative lines the removed tool built client-side. */
|
|
91
|
+
function completionContentText(labels: Map<string, string>, result: TaskCompletion): string {
|
|
92
|
+
const gates = result.gates.map((gate) => `${gate.passed ? "✓" : "✗"} ${gate.gate.type}: ${gate.gate.target} — ${gate.output}`).join("\n");
|
|
93
|
+
const checklist = result.checklist.map((item) => `${item.accepted ? "✓" : "✗"} proof: ${item.item}${item.reason ? ` — ${item.reason}` : ""}`).join("\n");
|
|
94
|
+
const focused = result.focused ? `\nActive: ${result.focused.title} (${result.focused.id})` : "";
|
|
95
|
+
const blocked = result.blocked.length > 0
|
|
96
|
+
? `\nBlocked: ${result.blocked.map((entry) => `${entry.artifact.title} (${entry.artifact.id}) waits for ${entry.dependencyIds.map((id) => labels.get(id) ?? "unknown task").join(", ")}`).join("; ")}`
|
|
97
|
+
: "";
|
|
98
|
+
return `${result.completed ? "Completed" : "Rejected"}: ${result.artifact.title} (${result.artifact.id})${focused}${blocked}${checklist ? `\n${checklist}` : ""}${gates ? `\n${gates}` : ""}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function planContentText(plan: TaskExecutionPlan): string {
|
|
102
|
+
const byId = new Map(plan.nodes.map((node) => [node.id, node]));
|
|
103
|
+
const titleCounts = new Map<string, number>();
|
|
104
|
+
for (const node of plan.nodes) titleCounts.set(node.title, (titleCounts.get(node.title) ?? 0) + 1);
|
|
105
|
+
const nodeLabel = (id: string): string => {
|
|
106
|
+
const node = byId.get(id);
|
|
107
|
+
if (!node) return "unknown task";
|
|
108
|
+
return (titleCounts.get(node.title) ?? 0) > 1 ? `${node.title} (${node.id})` : node.title;
|
|
109
|
+
};
|
|
110
|
+
const lines = plan.layers.flatMap((layer, index) => [
|
|
111
|
+
`Layer ${index + 1}`,
|
|
112
|
+
...layer.map((id) => ` [${byId.get(id)?.state ?? "unknown"}] ${nodeLabel(id)}`),
|
|
113
|
+
]);
|
|
114
|
+
if (plan.cycleIds.length > 0) lines.push(`Invalid cycle: ${plan.cycleIds.map(nodeLabel).join(", ")}`);
|
|
115
|
+
return lines.join("\n") || "No tasks in execution plan.";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function registerTasksVehicleOperations(registry: VehicleRegistry, deps: TasksVehicleDeps): void {
|
|
119
|
+
const { tasks, artifacts, sessionIdentity } = deps;
|
|
120
|
+
const moduleOperations = new Map(tasksOperations(tasks, artifacts, sessionIdentity).map((op) => [op.name, op]));
|
|
121
|
+
const call = (name: string, input: Record<string, unknown>): unknown => moduleOperations.get(name)!.execute(input);
|
|
122
|
+
|
|
123
|
+
const define = (
|
|
124
|
+
action: string,
|
|
125
|
+
description: string,
|
|
126
|
+
effect: "read" | "local-write",
|
|
127
|
+
properties: Record<string, { type: string; enum?: readonly string[] }>,
|
|
128
|
+
required: readonly string[],
|
|
129
|
+
resolve: (input: Record<string, unknown>) => Record<string, unknown>,
|
|
130
|
+
execute?: (input: Record<string, unknown>, context: VehicleOperationContext<Record<string, unknown>>) => unknown,
|
|
131
|
+
): void => {
|
|
132
|
+
const operation = defineVehicleOperation({
|
|
133
|
+
name: `tasks.${action}`,
|
|
134
|
+
version: 1,
|
|
135
|
+
description,
|
|
136
|
+
input: looseObjectSchema(properties, required),
|
|
137
|
+
output: passthroughOutput,
|
|
138
|
+
permissions: ["tasks:read", "tasks:write"],
|
|
139
|
+
effect,
|
|
140
|
+
idempotency: { mode: effect === "read" ? "safe" : "unsafe" },
|
|
141
|
+
limits: LIMITS,
|
|
142
|
+
});
|
|
143
|
+
registry.register(OWNER, bindVehicleOperation(operation, () => async (context) => (execute ?? ((input: Record<string, unknown>) => call(`tasks.${action}`, input)))(resolve(context.input), context)));
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/** Shared by every action taking a single id/name: resolves root_task_name first, then name -> id against the final scope. */
|
|
147
|
+
const resolveIdAndScope = (input: Record<string, unknown>): Record<string, unknown> => {
|
|
148
|
+
const projectRoot = input.project_root as string | undefined;
|
|
149
|
+
const rootTaskId = resolveRootTaskId(tasks, projectRoot, input.root_task_id, input.root_task_name);
|
|
150
|
+
const scope = input.scope as TaskViewMode | undefined;
|
|
151
|
+
const filter = { projectRoot, scope, rootTaskId };
|
|
152
|
+
return {
|
|
153
|
+
...input,
|
|
154
|
+
...(rootTaskId ? { root_task_id: rootTaskId } : {}),
|
|
155
|
+
id: resolveTaskId(tasks, filter, input.id, input.name),
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
define(
|
|
160
|
+
"create",
|
|
161
|
+
"Creates a Task -- work: desired outcomes, gates, checklists, and dependencies. project_root is required (no ambient cwd server-side). Prefer parent_name/depends_on_names over parent_id/depends_on -- resolved server-side.",
|
|
162
|
+
"local-write",
|
|
163
|
+
{ title: stringProp, body: stringProp, status: stringProp, labels: arrayProp, extra: objectProp, gates: arrayProp, checklist: objectProp, template_id: stringProp, parent_id: stringProp, parent_name: stringProp, depends_on: arrayProp, depends_on_names: arrayProp, project_root: stringProp, session_id: stringProp },
|
|
164
|
+
["title", "project_root"],
|
|
165
|
+
(input) => {
|
|
166
|
+
const projectRoot = input.project_root as string;
|
|
167
|
+
const filter = { projectRoot };
|
|
168
|
+
const parentId = typeof input.parent_id === "string" && input.parent_id.length > 0 ? input.parent_id : (typeof input.parent_name === "string" && input.parent_name.length > 0 ? resolveTaskId(tasks, filter, undefined, input.parent_name) : undefined);
|
|
169
|
+
const dependsOn = resolveArrayField(tasks, filter, input.depends_on, input.depends_on_names);
|
|
170
|
+
return { ...input, ...(parentId ? { parent_id: parentId } : {}), ...(dependsOn ? { depends_on: dependsOn } : {}) };
|
|
171
|
+
},
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
define(
|
|
175
|
+
"update",
|
|
176
|
+
"Recovers an accidentally-terminal task via status=todo + reason, or changes title/body/labels, without rewriting real history. Never touches gates -- use set_gates.",
|
|
177
|
+
"local-write",
|
|
178
|
+
{ id: stringProp, name: stringProp, title: stringProp, body: stringProp, labels: arrayProp, status: stringProp, reason: stringProp, session_id: stringProp, project_root: stringProp },
|
|
179
|
+
[],
|
|
180
|
+
resolveIdAndScope,
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
define("list", "Lists Tasks matching an optional status/text/labels filter, scoped to project_root. project_root is required (no ambient cwd server-side).", "read", readSchemaProps, ["project_root"], (input) => {
|
|
184
|
+
const rootTaskId = resolveRootTaskId(tasks, input.project_root as string, input.root_task_id, input.root_task_name);
|
|
185
|
+
return { ...input, ...(rootTaskId ? { root_task_id: rootTaskId } : {}) };
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
define(
|
|
189
|
+
"graph",
|
|
190
|
+
"Returns the full task graph (nodes with parent/child/dependency ids) for the requested scope. project_root is required.",
|
|
191
|
+
"read",
|
|
192
|
+
readSchemaProps,
|
|
193
|
+
["project_root"],
|
|
194
|
+
(input) => {
|
|
195
|
+
const rootTaskId = resolveRootTaskId(tasks, input.project_root as string, input.root_task_id, input.root_task_name);
|
|
196
|
+
return { ...input, ...(rootTaskId ? { root_task_id: rootTaskId } : {}) };
|
|
197
|
+
},
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
define(
|
|
201
|
+
"plan",
|
|
202
|
+
"Projects the task graph into layered execution order (ready/blocked/invalid states, cycle detection). project_root is required.",
|
|
203
|
+
"read",
|
|
204
|
+
readSchemaProps,
|
|
205
|
+
["project_root"],
|
|
206
|
+
(input) => {
|
|
207
|
+
const rootTaskId = resolveRootTaskId(tasks, input.project_root as string, input.root_task_id, input.root_task_name);
|
|
208
|
+
return { ...input, ...(rootTaskId ? { root_task_id: rootTaskId } : {}) };
|
|
209
|
+
},
|
|
210
|
+
(input) => {
|
|
211
|
+
const plan = call("tasks.plan", input) as TaskExecutionPlan;
|
|
212
|
+
return { ...plan, content: [{ type: "text" as const, text: planContentText(plan) }] };
|
|
213
|
+
},
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
define("show", "Shows one Task by id or title.", "read", { id: stringProp, name: stringProp, project_root: stringProp, scope: { type: "string", enum: ["project", "graph", "all"] }, root_task_id: stringProp, root_task_name: stringProp }, [], resolveIdAndScope);
|
|
217
|
+
|
|
218
|
+
define(
|
|
219
|
+
"history",
|
|
220
|
+
"Task's append-only lifecycle event history, cursor-paginated.",
|
|
221
|
+
"read",
|
|
222
|
+
{ id: stringProp, name: stringProp, limit: numberProp, cursor: numberProp, direction: { type: "string", enum: ["asc", "desc"] }, project_root: stringProp, scope: { type: "string", enum: ["project", "graph", "all"] }, root_task_id: stringProp, root_task_name: stringProp },
|
|
223
|
+
[],
|
|
224
|
+
resolveIdAndScope,
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
define("scope", "Describes the current task-view scope selection for project_root.", "read", { project_root: stringProp }, ["project_root"], (input) => input);
|
|
228
|
+
|
|
229
|
+
define(
|
|
230
|
+
"set_scope",
|
|
231
|
+
"Sets the task-view scope (project/graph/all) for project_root, optionally pinned to root_task_id.",
|
|
232
|
+
"local-write",
|
|
233
|
+
{ project_root: stringProp, scope: { type: "string", enum: ["project", "graph", "all"] }, root_task_id: stringProp, root_task_name: stringProp },
|
|
234
|
+
["project_root", "scope"],
|
|
235
|
+
(input) => {
|
|
236
|
+
const rootTaskId = resolveRootTaskId(tasks, input.project_root as string, input.root_task_id, input.root_task_name);
|
|
237
|
+
return { ...input, ...(rootTaskId ? { root_task_id: rootTaskId } : {}) };
|
|
238
|
+
},
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
define(
|
|
242
|
+
"assign_project",
|
|
243
|
+
"Reassigns a Task's project_root.",
|
|
244
|
+
"local-write",
|
|
245
|
+
{ id: stringProp, name: stringProp, project_root: stringProp, session_id: stringProp },
|
|
246
|
+
["project_root"],
|
|
247
|
+
(input) => ({ ...input, id: resolveTaskId(tasks, { projectRoot: input.project_root as string | undefined }, input.id, input.name) }),
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
define("active", "The current active Task (the one being worked on) for this scope. project_root is required.", "read", readSchemaProps, ["project_root"], (input) => input);
|
|
251
|
+
define("focused", "The current focused Task and its focus status (focused/paused) for this session's scope. project_root is required.", "read", readSchemaProps, ["project_root"], (input) => input);
|
|
252
|
+
|
|
253
|
+
const focusOperation = (
|
|
254
|
+
action: "focus" | "pause" | "unpause" | "clear_focus",
|
|
255
|
+
description: string,
|
|
256
|
+
properties: Record<string, { type: string; enum?: readonly string[] }>,
|
|
257
|
+
required: readonly string[],
|
|
258
|
+
resolve: (input: Record<string, unknown>) => Record<string, unknown>,
|
|
259
|
+
): void => {
|
|
260
|
+
const operation = defineVehicleOperation({
|
|
261
|
+
name: `tasks.${action}`,
|
|
262
|
+
version: 1,
|
|
263
|
+
description,
|
|
264
|
+
input: looseObjectSchema(properties, required),
|
|
265
|
+
output: passthroughOutput,
|
|
266
|
+
permissions: ["tasks:read", "tasks:write"],
|
|
267
|
+
effect: "local-write",
|
|
268
|
+
idempotency: { mode: "unsafe" },
|
|
269
|
+
limits: LIMITS,
|
|
270
|
+
});
|
|
271
|
+
registry.register(OWNER, bindVehicleOperation(operation, () => async (context) => {
|
|
272
|
+
const claims = context.principal?.claims as { sessionId?: string; sessionSecret?: string } | undefined;
|
|
273
|
+
return call(`tasks.${action}`, { ...resolve(context.input), session_id: claims?.sessionId, session_secret: claims?.sessionSecret });
|
|
274
|
+
}));
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
focusOperation("focus", "Sets the active Task Focus (singular per scope) to this Task. Multiple sessions can focus the same task while only one holds its lease.", { id: stringProp, name: stringProp, project_root: stringProp }, [], (input) => ({ ...input, id: resolveTaskId(tasks, { projectRoot: input.project_root as string | undefined }, input.id, input.name) }));
|
|
278
|
+
focusOperation("pause", "Pauses the active Task Focus without clearing it.", { reason: stringProp }, [], (input) => input);
|
|
279
|
+
focusOperation("unpause", "Resumes a paused Task Focus.", {}, [], (input) => input);
|
|
280
|
+
focusOperation("clear_focus", "Clears the active Task Focus.", {}, [], (input) => input);
|
|
281
|
+
|
|
282
|
+
define("start", "Lifecycle transition: todo -> in-progress.", "local-write", { id: stringProp, name: stringProp, reason: stringProp, session_id: stringProp, project_root: stringProp }, [], resolveIdAndScope);
|
|
283
|
+
define("submit", "Lifecycle transition: in-progress -> review.", "local-write", { id: stringProp, name: stringProp, reason: stringProp, session_id: stringProp, project_root: stringProp }, [], resolveIdAndScope);
|
|
284
|
+
define("reject", "Lifecycle transition: review -> rejected.", "local-write", { id: stringProp, name: stringProp, reason: stringProp, session_id: stringProp, project_root: stringProp }, [], resolveIdAndScope);
|
|
285
|
+
define("retry", "Lifecycle transition: rejected -> in-progress.", "local-write", { id: stringProp, name: stringProp, reason: stringProp, session_id: stringProp, project_root: stringProp }, [], resolveIdAndScope);
|
|
286
|
+
define("cancel", "Lifecycle transition to canceled (terminal) from todo/in-progress/review/rejected.", "local-write", { id: stringProp, name: stringProp, reason: stringProp, session_id: stringProp, project_root: stringProp }, [], resolveIdAndScope);
|
|
287
|
+
|
|
288
|
+
define(
|
|
289
|
+
"complete",
|
|
290
|
+
"Runs gates + checklist-proof review, then focuses one deterministic ready successor without claiming effort. Rejects (not completes) on gate/checklist failure.",
|
|
291
|
+
"local-write",
|
|
292
|
+
{ id: stringProp, name: stringProp, reason: stringProp, session_id: stringProp, project_root: stringProp, scope: { type: "string", enum: ["project", "graph", "all"] }, root_task_id: stringProp, root_task_name: stringProp },
|
|
293
|
+
[],
|
|
294
|
+
resolveIdAndScope,
|
|
295
|
+
async (input) => {
|
|
296
|
+
const result = (await call("tasks.complete", input)) as TaskCompletion;
|
|
297
|
+
const dependencyIds = result.blocked.flatMap((entry) => entry.dependencyIds);
|
|
298
|
+
const labels = labelsById(artifacts, dependencyIds);
|
|
299
|
+
return { ...result, content: [{ type: "text" as const, text: completionContentText(labels, result) }] };
|
|
300
|
+
},
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
define(
|
|
304
|
+
"run_gates",
|
|
305
|
+
"Runs a Task's configured gates without transitioning its status -- for checking readiness before submit/complete.",
|
|
306
|
+
"read",
|
|
307
|
+
{ id: stringProp, name: stringProp, session_id: stringProp, project_root: stringProp, scope: { type: "string", enum: ["project", "graph", "all"] }, root_task_id: stringProp, root_task_name: stringProp },
|
|
308
|
+
[],
|
|
309
|
+
resolveIdAndScope,
|
|
310
|
+
async (input) => {
|
|
311
|
+
const gates = (await call("tasks.run_gates", input)) as Array<{ gate: { type: string; target: string }; passed: boolean; output: string }>;
|
|
312
|
+
const text = gates.map((gate) => `${gate.passed ? "✓" : "✗"} ${gate.gate.type}: ${gate.gate.target} — ${gate.output}`).join("\n") || "No gates configured.";
|
|
313
|
+
return { gates, content: [{ type: "text" as const, text }] };
|
|
314
|
+
},
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
define("set_checklist", "Replaces a Task's evidence-bearing checklist (proof requirements) in full.", "local-write", { id: stringProp, name: stringProp, checklist: objectProp, project_root: stringProp }, ["checklist"], resolveIdAndScope);
|
|
318
|
+
define("set_gates", "Replaces a Task's gate commands in full.", "local-write", { id: stringProp, name: stringProp, gates: arrayProp, project_root: stringProp }, ["gates"], resolveIdAndScope);
|
|
319
|
+
|
|
320
|
+
define(
|
|
321
|
+
"context",
|
|
322
|
+
"The full plan-reconciliation context (the system prompt itself only carries a one-line pointer) -- call explicitly after a compaction or before reconciling. project_root is required.",
|
|
323
|
+
"read",
|
|
324
|
+
readSchemaProps,
|
|
325
|
+
["project_root"],
|
|
326
|
+
(input) => ({ ...input, verbosity: "full" }),
|
|
327
|
+
(input) => {
|
|
328
|
+
const summary = call("tasks.context", input) as string | null;
|
|
329
|
+
const text = summary ?? "No open tasks.";
|
|
330
|
+
return { context: summary, content: [{ type: "text" as const, text }] };
|
|
331
|
+
},
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
define(
|
|
335
|
+
"cancel_subtree",
|
|
336
|
+
"Cancels a Task and its whole containment subtree in one call, skipping tasks already done/canceled.",
|
|
337
|
+
"local-write",
|
|
338
|
+
{ id: stringProp, name: stringProp, reason: stringProp, session_id: stringProp, project_root: stringProp, scope: { type: "string", enum: ["project", "graph", "all"] }, root_task_id: stringProp, root_task_name: stringProp },
|
|
339
|
+
[],
|
|
340
|
+
resolveIdAndScope,
|
|
341
|
+
(input) => {
|
|
342
|
+
const outcome = call("tasks.cancel_subtree", input) as { canceled: string[]; skipped: string[] };
|
|
343
|
+
const text = `Canceled ${outcome.canceled.length} task(s)${outcome.skipped.length > 0 ? `, skipped ${outcome.skipped.length} already-terminal` : ""}.`;
|
|
344
|
+
return { ...outcome, content: [{ type: "text" as const, text }] };
|
|
345
|
+
},
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
const scopeProp = { type: "string", enum: ["project", "graph", "all"] } as const;
|
|
349
|
+
|
|
350
|
+
define(
|
|
351
|
+
"depend",
|
|
352
|
+
"Adds a dependency edge (this task waits for dependency_id/dependency_name). Dependency edges form an executable DAG -- self-dependencies and cycles are rejected. A name resolved outside project_root's own scope is retried once against every project before failing, unless scope is pinned explicitly.",
|
|
353
|
+
"local-write",
|
|
354
|
+
{ id: stringProp, name: stringProp, dependency_id: stringProp, dependency_name: stringProp, project_root: stringProp, scope: scopeProp, session_id: stringProp },
|
|
355
|
+
[],
|
|
356
|
+
(input) => {
|
|
357
|
+
const filter = { projectRoot: input.project_root as string | undefined, scope: input.scope as TaskViewMode | undefined };
|
|
358
|
+
return { ...input, id: resolveTaskId(tasks, filter, input.id, input.name), dependency_id: resolveTaskId(tasks, filter, input.dependency_id, input.dependency_name) };
|
|
359
|
+
},
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
define(
|
|
363
|
+
"undepend",
|
|
364
|
+
"Removes a dependency edge. Idempotent -- a no-op if the edge is already absent.",
|
|
365
|
+
"local-write",
|
|
366
|
+
{ id: stringProp, name: stringProp, dependency_id: stringProp, dependency_name: stringProp, project_root: stringProp, scope: scopeProp, session_id: stringProp },
|
|
367
|
+
[],
|
|
368
|
+
(input) => {
|
|
369
|
+
const filter = { projectRoot: input.project_root as string | undefined, scope: input.scope as TaskViewMode | undefined };
|
|
370
|
+
return { ...input, id: resolveTaskId(tasks, filter, input.id, input.name), dependency_id: resolveTaskId(tasks, filter, input.dependency_id, input.dependency_name) };
|
|
371
|
+
},
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
define(
|
|
375
|
+
"contain",
|
|
376
|
+
"Nests a child Task inside a parent (parent_id/parent_name contains child_id/child_name) -- explicit hierarchy, distinct from depends_on execution ordering. A name resolved outside project_root's own scope is retried once against every project before failing, unless scope is pinned explicitly.",
|
|
377
|
+
"local-write",
|
|
378
|
+
{ parent_id: stringProp, parent_name: stringProp, child_id: stringProp, child_name: stringProp, project_root: stringProp, scope: scopeProp, session_id: stringProp },
|
|
379
|
+
[],
|
|
380
|
+
(input) => {
|
|
381
|
+
const filter = { projectRoot: input.project_root as string | undefined, scope: input.scope as TaskViewMode | undefined };
|
|
382
|
+
return { ...input, parent_id: resolveTaskId(tasks, filter, input.parent_id, input.parent_name), child_id: resolveTaskId(tasks, filter, input.child_id, input.child_name) };
|
|
383
|
+
},
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
define(
|
|
387
|
+
"uncontain",
|
|
388
|
+
"Removes a parent/child nesting. Idempotent -- a no-op if the edge is already absent.",
|
|
389
|
+
"local-write",
|
|
390
|
+
{ parent_id: stringProp, parent_name: stringProp, child_id: stringProp, child_name: stringProp, project_root: stringProp, scope: scopeProp, session_id: stringProp },
|
|
391
|
+
[],
|
|
392
|
+
(input) => {
|
|
393
|
+
const filter = { projectRoot: input.project_root as string | undefined, scope: input.scope as TaskViewMode | undefined };
|
|
394
|
+
return { ...input, parent_id: resolveTaskId(tasks, filter, input.parent_id, input.parent_name), child_id: resolveTaskId(tasks, filter, input.child_id, input.child_name) };
|
|
395
|
+
},
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
define("claim", "Claims this Task's lease under owner (defaults to session_id). Throws if a different owner already holds one.", "local-write", { id: stringProp, name: stringProp, owner: stringProp, ttl_ms: numberProp, note: stringProp, project_root: stringProp, session_id: stringProp }, [], (input) => ({ ...input, id: resolveTaskId(tasks, { projectRoot: input.project_root as string | undefined }, input.id, input.name), owner: input.owner ?? input.session_id }));
|
|
399
|
+
define("heartbeat_lease", "Extends this Task's lease -- needs the exact owner/token claim() returned.", "local-write", { id: stringProp, name: stringProp, owner: stringProp, token: stringProp, ttl_ms: numberProp, project_root: stringProp, session_id: stringProp }, ["owner", "token"], (input) => ({ ...input, id: resolveTaskId(tasks, { projectRoot: input.project_root as string | undefined }, input.id, input.name) }));
|
|
400
|
+
define("release_lease", "Releases this Task's lease -- needs the exact owner/token claim() returned.", "local-write", { id: stringProp, name: stringProp, owner: stringProp, token: stringProp, project_root: stringProp, session_id: stringProp }, ["owner", "token"], (input) => ({ ...input, id: resolveTaskId(tasks, { projectRoot: input.project_root as string | undefined }, input.id, input.name) }));
|
|
401
|
+
define("lease", "Shows this Task's current lease, if any.", "read", { id: stringProp, name: stringProp, project_root: stringProp }, [], (input) => ({ ...input, id: resolveTaskId(tasks, { projectRoot: input.project_root as string | undefined }, input.id, input.name) }));
|
|
402
|
+
|
|
403
|
+
define("event_feed", "Cursor-paginated feed of raw Task lifecycle events across every task, optionally filtered by event_types.", "read", { cursor: numberProp, limit: numberProp, event_types: arrayProp }, [], (input) => input);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** Kept out of the registry deliberately, matching the removed tool's own ACTIONS list -- system maintenance, not an agent-facing action. Exposed via reapStale* CLI/cron paths, not a Vehicle operation. */
|
|
407
|
+
export const TASKS_MAINTENANCE_OPERATIONS = ["tasks.reap_stale_focus", "tasks.reap_stale_leases"] as const;
|