@danypops/papyrus 0.45.2 → 0.46.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.
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Shared, kind-agnostic CRUD composition helpers used by docs/docs-service.ts,
3
+ * rules/rules-service.ts, and playbook/playbook-service.ts. Split out of the former
4
+ * domain-services.ts (which combined all three domains in one 849-line file) so each
5
+ * domain's own service file only imports the cross-domain pieces it actually needs.
6
+ */
7
+
8
+ import type { Artifact } from "./artifact/artifact.ts";
9
+ import type { ArtifactEventContext } from "./artifact/artifact-event.ts";
10
+ import type { ArtifactScopeStore } from "./artifact/artifact-scope-store.ts";
11
+ import type { ArtifactStore } from "./artifact/artifact-store.ts";
12
+ import {
13
+ ARTIFACT_BODY_MAX_LENGTH,
14
+ ARTIFACT_LABEL_MAX_COUNT,
15
+ ARTIFACT_LABEL_MAX_LENGTH,
16
+ ARTIFACT_SCOPE_MAX_ARTIFACTS,
17
+ ARTIFACT_TITLE_MAX_LENGTH,
18
+ } from "./constants.ts";
19
+ import { normalizeProjectRoot } from "./domain/task-scope.ts";
20
+
21
+ export interface UpdateContentInput {
22
+ title?: string;
23
+ body?: string;
24
+ labels?: string[];
25
+ }
26
+
27
+ export function requireContentUpdateFields(input: UpdateContentInput): void {
28
+ if (input.title === undefined && input.body === undefined && input.labels === undefined) {
29
+ throw new Error("update requires title, body, or labels");
30
+ }
31
+ }
32
+
33
+ export function assertTitleBounds(title: string | undefined): void {
34
+ if (title !== undefined && (title.trim().length === 0 || title.length > ARTIFACT_TITLE_MAX_LENGTH)) {
35
+ throw new Error(`title must be between 1 and ${ARTIFACT_TITLE_MAX_LENGTH} characters`);
36
+ }
37
+ }
38
+
39
+ export function assertBodyBounds(body: string | undefined): void {
40
+ if (body !== undefined && body.length > ARTIFACT_BODY_MAX_LENGTH)
41
+ throw new Error(`body cannot exceed ${ARTIFACT_BODY_MAX_LENGTH} characters`);
42
+ }
43
+
44
+ export function assertLabelsBounds(labels: string[] | undefined): void {
45
+ if (labels === undefined) return;
46
+ if (labels.length > ARTIFACT_LABEL_MAX_COUNT) throw new Error(`labels cannot exceed ${ARTIFACT_LABEL_MAX_COUNT} entries`);
47
+ if (labels.some((label) => label.length === 0 || label.length > ARTIFACT_LABEL_MAX_LENGTH)) {
48
+ throw new Error(`each label must be between 1 and ${ARTIFACT_LABEL_MAX_LENGTH} characters`);
49
+ }
50
+ }
51
+
52
+ export interface ListFilter {
53
+ status?: string;
54
+ text?: string;
55
+ limit?: number;
56
+ /** When supplied, results are limited to artifacts scoped to this project (or the unscoped bucket, for an empty string is not accepted -- use assignArtifactProject's own validation). */
57
+ projectRoot?: string;
58
+ }
59
+
60
+ /**
61
+ * Shared by listDocuments/listRules/listPlaybooks: when filter.projectRoot is given, resolve
62
+ * via ArtifactScopeStore first and post-filter by kind/status/text (mirrors Tasks.list's
63
+ * established scoped-listing shape); otherwise fall back to the existing unscoped query
64
+ * path unchanged, so every caller that predates project scoping keeps working exactly as
65
+ * before.
66
+ */
67
+ export function listScoped(
68
+ artifacts: ArtifactStore,
69
+ scopes: ArtifactScopeStore,
70
+ kind: string,
71
+ filter: ListFilter,
72
+ excludeSubtype?: string,
73
+ ): Artifact[] {
74
+ if (filter.projectRoot === undefined)
75
+ return artifacts.query({ kind, excludeSubtype, status: filter.status, text: filter.text, limit: filter.limit });
76
+ const limit = filter.limit ?? ARTIFACT_SCOPE_MAX_ARTIFACTS;
77
+ if (!Number.isInteger(limit) || limit < 1 || limit > ARTIFACT_SCOPE_MAX_ARTIFACTS) {
78
+ throw new Error(`list limit must be between 1 and ${ARTIFACT_SCOPE_MAX_ARTIFACTS}`);
79
+ }
80
+ const projectRoot = normalizeProjectRoot(filter.projectRoot);
81
+ const ids = scopes.ids(projectRoot, ARTIFACT_SCOPE_MAX_ARTIFACTS);
82
+ const text = filter.text?.toLowerCase();
83
+ return ids
84
+ .map((id) => artifacts.get(id))
85
+ .filter((artifact): artifact is Artifact => artifact?.kind === kind && artifact.subtype !== excludeSubtype)
86
+ .filter((artifact) => filter.status === undefined || artifact.status === filter.status)
87
+ .filter((artifact) => text === undefined || artifact.title.toLowerCase().includes(text) || artifact.body.toLowerCase().includes(text))
88
+ .sort((left, right) => right.updated_at.localeCompare(left.updated_at) || left.id.localeCompare(right.id))
89
+ .slice(0, limit);
90
+ }
91
+
92
+ export function requireKind(artifacts: ArtifactStore, id: string, kind: string): Artifact {
93
+ const artifact = artifacts.get(id);
94
+ if (!artifact) throw new Error(`${kind} artifact "${id}" not found`);
95
+ if (artifact.kind !== kind) throw new Error(`artifact "${id}" is not a ${kind}`);
96
+ return artifact;
97
+ }
98
+
99
+ /**
100
+ * Shared shape for a kind's own declarative status-transition table -- Document and Task
101
+ * already independently converged on this exact shape; Rule/Playbook used an inline ternary
102
+ * instead only because they happened to have just two states. See validateTransitionFrom's
103
+ * own comment for why this is split from runTransition rather than always combined.
104
+ */
105
+ export type TransitionTable<Action extends string, Status extends string> = Record<Action, { from: Status[]; to: Status }>;
106
+
107
+ /**
108
+ * The from/to-table lookup+validation half of a transition, split out from runTransition
109
+ * (below) so a caller with its own side effects gated on "is this action even valid from the
110
+ * current status" (e.g. Tasks.transition's dependency-blocking check and focus-store
111
+ * bookkeeping, which must run -- or not -- before the status write itself) can call this
112
+ * directly instead of duplicating the same lookup+throw three times. Every other caller
113
+ * (Document/Rule/Playbook, none of which have that ordering constraint) uses runTransition
114
+ * instead, which does this same check plus the write in one call.
115
+ */
116
+ export function validateTransitionFrom<Action extends string, Status extends string>(
117
+ kind: string,
118
+ action: Action,
119
+ status: string,
120
+ table: TransitionTable<Action, Status>,
121
+ ): { from: Status[]; to: Status } {
122
+ const transition = table[action];
123
+ if (!transition.from.includes(status as Status)) throw new Error(`cannot ${action} ${kind} from ${status}`);
124
+ return transition;
125
+ }
126
+
127
+ /**
128
+ * Shared by transitionDocument/transitionRule/transitionPlaybook: validates the requested
129
+ * action is legal from the artifact's current status per its own kind's transition table, then
130
+ * writes the resulting status. Any authority/ownership guard (e.g. requireMutableDocument,
131
+ * requireLocallyOwnedContent) is the caller's own responsibility, resolved on `artifact`
132
+ * *before* calling this -- kept out of this primitive so it stays usable by a kind with no
133
+ * such guard (Rule, Playbook) without a no-op parameter every call site has to pass.
134
+ */
135
+ export function runTransition<Action extends string, Status extends string>(
136
+ artifacts: ArtifactStore,
137
+ artifact: Artifact,
138
+ kind: string,
139
+ action: Action,
140
+ table: TransitionTable<Action, Status>,
141
+ context?: ArtifactEventContext,
142
+ ): Artifact {
143
+ const transition = validateTransitionFrom(kind, action, artifact.status, table);
144
+ return artifacts.setStatus(artifact.id, transition.to, context)!;
145
+ }
146
+
147
+ /** Shared by assignRuleProject/assignPlaybookProject. assignDocumentProject has its own body -- it must reject Notes via requireDocument, not the generic requireKind this helper uses. */
148
+ export function assignArtifactProject(
149
+ artifacts: ArtifactStore,
150
+ scopes: ArtifactScopeStore,
151
+ id: string,
152
+ kind: string,
153
+ projectRoot: string | undefined,
154
+ ): Artifact {
155
+ requireKind(artifacts, id, kind);
156
+ scopes.assign(
157
+ id,
158
+ projectRoot === undefined ? undefined : normalizeProjectRoot(projectRoot),
159
+ projectRoot === undefined ? "unscoped" : "explicit",
160
+ );
161
+ return artifacts.get(id)!;
162
+ }
@@ -7,7 +7,7 @@ import type { VehicleRegistry } from "@danypops/vehicle-server";
7
7
  import type { ArtifactScopeStore } from "../artifact/artifact-scope-store.ts";
8
8
  import type { ArtifactStore } from "../artifact/artifact-store.ts";
9
9
  import type { AuthorityRegistry } from "../authority-registry.ts";
10
- import { listDocuments } from "../domain-services.ts";
10
+ import { listDocuments } from "../docs/docs-service.ts";
11
11
  import { docsOperations } from "../modules/docs.ts";
12
12
  import { booleanProp, createOperationDefiner, numberProp, resolveArtifactIdWidened, stringProp, validationError } from "./shared.ts";
13
13
 
@@ -21,9 +21,9 @@
21
21
  import type { VehicleRegistry } from "@danypops/vehicle-server";
22
22
  import type { ArtifactScopeStore } from "../artifact/artifact-scope-store.ts";
23
23
  import type { ArtifactStore } from "../artifact/artifact-store.ts";
24
- import { listPlaybooks } from "../domain-services.ts";
25
24
  import { playbooksOperations } from "../modules/playbooks.ts";
26
25
  import type { PlaybookInvocationResult, PlaybookMissingArguments } from "../playbook/playbook-execution.ts";
26
+ import { listPlaybooks } from "../playbook/playbook-service.ts";
27
27
  import type { SessionIdentity } from "../session-identity/session-identity-service.ts";
28
28
  import type { TaskEventStore } from "../stores/task-event-store.ts";
29
29
  import type { TaskScopeStore } from "../stores/task-scope-store.ts";
@@ -92,8 +92,10 @@ export function registerPlaybooksVehicleOperations(registry: VehicleRegistry, de
92
92
  steps: { type: "array" },
93
93
  tools: { type: "array" },
94
94
  arguments: { type: "array" },
95
+ subtype: stringProp,
95
96
  labels: { type: "array" },
96
97
  extra: { type: "object" },
98
+ template_id: stringProp,
97
99
  project_root: stringProp,
98
100
  actor: stringProp,
99
101
  source: stringProp,
@@ -7,8 +7,8 @@
7
7
  import type { VehicleRegistry } from "@danypops/vehicle-server";
8
8
  import type { ArtifactScopeStore } from "../artifact/artifact-scope-store.ts";
9
9
  import type { ArtifactStore } from "../artifact/artifact-store.ts";
10
- import { listRules } from "../domain-services.ts";
11
10
  import { rulesOperations } from "../modules/rules.ts";
11
+ import { listRules } from "../rules/rules-service.ts";
12
12
  import { booleanProp, createOperationDefiner, numberProp, resolveArtifactIdWidened, stringProp, validationError } from "./shared.ts";
13
13
 
14
14
  const OWNER = "rules";
@@ -49,7 +49,7 @@ export function registerRulesVehicleOperations(registry: VehicleRegistry, artifa
49
49
 
50
50
  define(
51
51
  "create",
52
- "Creates a Rule -- a standing constraint injected into the agent system prompt while active. project_root is optional (omitted = unscoped). The response includes combinedLength (condition+action+body character count) and a non-blocking warning once it exceeds the ~600-character soft target (hard-rejected past 4000).",
52
+ "Creates a Rule -- a standing constraint injected into the agent system prompt while active. Plain Rules start active; a template_id creates an inert draft that must pass the template's completionRequired fields through rules.enable. project_root is optional (omitted = unscoped). The response includes combinedLength (condition+action+body character count) and a non-blocking warning once it exceeds the ~600-character soft target (hard-rejected past 4000).",
53
53
  "local-write",
54
54
  {
55
55
  title: stringProp,
@@ -57,8 +57,10 @@ export function registerRulesVehicleOperations(registry: VehicleRegistry, artifa
57
57
  condition: stringProp,
58
58
  rule_action: stringProp,
59
59
  severity: { type: "string", enum: ["block", "warn", "info"] },
60
+ subtype: stringProp,
60
61
  labels: { type: "array" } as unknown as { type: string },
61
62
  extra: { type: "object" } as unknown as { type: string },
63
+ template_id: stringProp,
62
64
  project_root: stringProp,
63
65
  actor: stringProp,
64
66
  source: stringProp,
@@ -100,7 +102,7 @@ export function registerRulesVehicleOperations(registry: VehicleRegistry, artifa
100
102
 
101
103
  define(
102
104
  "enable",
103
- "Enables a Rule so it starts injecting into the agent system prompt.",
105
+ "Enables a Rule so it starts injecting into the agent system prompt. A template-derived draft is enabled only after every field path in its source template's completionRequired array is present.",
104
106
  "local-write",
105
107
  { id: stringProp, name: stringProp, project_root: stringProp, actor: stringProp, source: stringProp, session_id: stringProp },
106
108
  [],
@@ -18,6 +18,7 @@ import type { Artifact } from "../artifact/artifact.ts";
18
18
  import type { ArtifactStore } from "../artifact/artifact-store.ts";
19
19
  import { PlaybookCompositionError } from "../playbook/playbook-definition.ts";
20
20
  import { InvalidSessionSecretError } from "../session-identity/session-identity-service.ts";
21
+ import { TaskCreateIdempotencyConflictError } from "../stores/task-create-request-store.ts";
21
22
  import { TaskDependencyCycleError, TaskExecutionBoundExceededError, type TaskExecutionPlan } from "../task/task-execution.ts";
22
23
 
23
24
  /**
@@ -112,6 +113,17 @@ export function classifyTaskExecutionBounds<T>(run: () => T): T {
112
113
  }
113
114
  }
114
115
 
116
+ export function classifyTaskCreateIdempotency<T>(run: () => T): T {
117
+ try {
118
+ return run();
119
+ } catch (error) {
120
+ if (error instanceof TaskCreateIdempotencyConflictError) {
121
+ throw new VehicleError("idempotency-key-conflict", error.message, { category: "conflict" });
122
+ }
123
+ throw error;
124
+ }
125
+ }
126
+
115
127
  /** A self-dependency or dependency-cycle rejection (tasks.depend/undepend/create) is an ordinary, expected validation failure, not an unexpected crash. */
116
128
  export function classifyTaskDependencyCycles<T>(run: () => T): T {
117
129
  try {
@@ -235,7 +247,10 @@ export function buildWorkflowRunContent(
235
247
  return { type: "text", text };
236
248
  }
237
249
 
238
- export type OperationSchemaProperties = Record<string, { type: string; enum?: readonly string[] }>;
250
+ export type OperationSchemaProperties = Record<
251
+ string,
252
+ { type: string; enum?: readonly string[]; description?: string; [key: string]: unknown }
253
+ >;
239
254
 
240
255
  export type DefineOperation = (
241
256
  action: string,
@@ -19,17 +19,21 @@
19
19
  *
20
20
  * remove/remove_subtree/restore are not duplicated here -- see ./artifact-trash-vehicle.ts.
21
21
  */
22
- import type { VehicleLimits } from "@danypops/vehicle-core";
22
+ import { VehicleError, type VehicleLimits } from "@danypops/vehicle-core";
23
23
  import type { VehicleRegistry } from "@danypops/vehicle-server";
24
24
  import type { ArtifactStore } from "../artifact/artifact-store.ts";
25
- import { GATE_TIMEOUT_MAX_MS } from "../constants.ts";
25
+ import { GATE_TIMEOUT_MAX_MS, TASK_CREATE_IDEMPOTENCY_KEY_MAX_LENGTH } from "../constants.ts";
26
+ import { PROOF_TYPES } from "../domain/checklist.ts";
27
+ import { GATE_TYPES } from "../domain/gate.ts";
26
28
  import type { TaskViewMode } from "../domain/task-scope.ts";
27
29
  import { tasksOperations } from "../modules/tasks.ts";
28
30
  import type { SessionIdentity } from "../session-identity/session-identity-service.ts";
29
31
  import type { TaskExecutionPlan } from "../task/task-execution.ts";
30
- import type { TaskCompletion, Tasks } from "../task/task-service.ts";
32
+ import { type TaskCompletion, TaskProjectAmbiguousError, TaskProjectNotFoundError, type Tasks } from "../task/task-service.ts";
31
33
  import {
34
+ booleanProp,
32
35
  classifySessionAuthorization,
36
+ classifyTaskCreateIdempotency,
33
37
  classifyTaskDependencyCycles,
34
38
  classifyTaskExecutionBounds,
35
39
  createOperationDefiner,
@@ -68,9 +72,63 @@ const GATE_OPERATION_LIMITS: VehicleLimits = {
68
72
  maxResponseBytes: 262_144,
69
73
  };
70
74
 
71
- const objectProp = { type: "object" } as unknown as { type: string };
72
- const arrayProp = { type: "array" } as unknown as { type: string };
73
- const _boolProp = { type: "boolean" } as unknown as { type: string };
75
+ const objectProp = { type: "object" } as const;
76
+ const arrayProp = { type: "array" } as const;
77
+ const _boolProp = { type: "boolean" } as const;
78
+
79
+ const gateProp = {
80
+ type: "array",
81
+ description: "Validation gates run by tasks.run_gates and tasks.complete.",
82
+ items: {
83
+ type: "object",
84
+ properties: {
85
+ type: { type: "string", enum: GATE_TYPES, description: "Gate evaluator." },
86
+ target: { type: "string", minLength: 1, description: "Path, command, text target, or test command." },
87
+ expect: { type: "string", description: "Optional expected text/result." },
88
+ timeoutMs: { type: "integer", minimum: 1_000, maximum: GATE_TIMEOUT_MAX_MS, description: "Command/test timeout override." },
89
+ },
90
+ required: ["type", "target"],
91
+ additionalProperties: false,
92
+ },
93
+ examples: [
94
+ [{ type: "file-exists", target: "dist/index.js" }],
95
+ [{ type: "command", target: "bun run typecheck", timeoutMs: 60_000 }],
96
+ [{ type: "contains", target: "README.md", expect: "Retry semantics" }],
97
+ [{ type: "test", target: "bun test" }],
98
+ ],
99
+ } as const;
100
+
101
+ const checklistProp = {
102
+ type: "object",
103
+ description: "Map from completion criterion text to one or more typed proof references. An empty map clears the checklist.",
104
+ additionalProperties: {
105
+ type: "object",
106
+ properties: {
107
+ proof: {
108
+ type: "array",
109
+ minItems: 1,
110
+ items: {
111
+ type: "object",
112
+ properties: {
113
+ type: { type: "string", enum: PROOF_TYPES },
114
+ target: { type: "string", minLength: 1 },
115
+ expect: { type: "string" },
116
+ },
117
+ required: ["type", "target"],
118
+ additionalProperties: false,
119
+ },
120
+ },
121
+ },
122
+ required: ["proof"],
123
+ additionalProperties: false,
124
+ },
125
+ examples: [
126
+ {
127
+ "tests pass": { proof: [{ type: "test", target: "bun test", expect: "0 failures" }] },
128
+ "documentation updated": { proof: [{ type: "file", target: "README.md" }] },
129
+ },
130
+ ],
131
+ } as const;
74
132
 
75
133
  export interface TasksVehicleDeps {
76
134
  tasks: Tasks;
@@ -147,6 +205,9 @@ const readSchemaProps = {
147
205
  labels: arrayProp,
148
206
  };
149
207
 
208
+ /** list-only: opts into full Artifact bodies instead of the lean summarizeArtifact() default (modules/tasks.ts). */
209
+ const listSchemaProps = { ...readSchemaProps, full: booleanProp };
210
+
150
211
  /** Same gate/checklist narrative lines the removed tool built client-side. */
151
212
  function completionContentText(labels: Map<string, string>, result: TaskCompletion): string {
152
213
  const gates = result.gates.map((gate) => `${gate.passed ? "✓" : "✗"} ${gate.gate.type}: ${gate.gate.target} — ${gate.output}`).join("\n");
@@ -190,10 +251,26 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
190
251
  */
191
252
  const call = (name: string, input: Record<string, unknown>): unknown =>
192
253
  classifySessionAuthorization(() =>
193
- classifyTaskExecutionBounds(() => classifyTaskDependencyCycles(() => moduleOperations.get(name)!.execute(input))),
254
+ classifyTaskCreateIdempotency(() =>
255
+ classifyTaskExecutionBounds(() => classifyTaskDependencyCycles(() => moduleOperations.get(name)!.execute(input))),
256
+ ),
194
257
  );
195
258
  const define = createOperationDefiner(registry, OWNER, "tasks", ["tasks:read", "tasks:write"], call);
196
259
 
260
+ const resolveProject = (reference: string) => {
261
+ try {
262
+ return tasks.resolveProject(reference);
263
+ } catch (error) {
264
+ if (error instanceof TaskProjectNotFoundError) {
265
+ throw new VehicleError("task-project-not-found", error.message, { category: "not_found" });
266
+ }
267
+ if (error instanceof TaskProjectAmbiguousError) {
268
+ throw new VehicleError("task-project-ambiguous", error.message, { category: "conflict" });
269
+ }
270
+ throw error;
271
+ }
272
+ };
273
+
197
274
  /** Shared by every action taking a single id/name: resolves root_task_name first, then name -> id against the final scope. */
198
275
  const resolveIdAndScope = (input: Record<string, unknown>): Record<string, unknown> => {
199
276
  const projectRoot = input.project_root as string | undefined;
@@ -217,14 +294,21 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
217
294
  status: stringProp,
218
295
  labels: arrayProp,
219
296
  extra: objectProp,
220
- gates: arrayProp,
221
- checklist: objectProp,
297
+ gates: gateProp,
298
+ checklist: checklistProp,
222
299
  template_id: stringProp,
223
300
  parent_id: stringProp,
224
301
  parent_name: stringProp,
225
302
  depends_on: arrayProp,
226
303
  depends_on_names: arrayProp,
227
304
  project_root: stringProp,
305
+ idempotency_key: {
306
+ type: "string",
307
+ minLength: 1,
308
+ maxLength: TASK_CREATE_IDEMPOTENCY_KEY_MAX_LENGTH,
309
+ description:
310
+ "Optional retry key, scoped by caller and canonical project root. Reusing it with the same payload returns the original response; a different payload is rejected.",
311
+ },
228
312
  session_id: stringProp,
229
313
  },
230
314
  ["title", "project_root"],
@@ -240,6 +324,12 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
240
324
  const dependsOn = resolveArrayField(artifacts, tasks, filter, input.depends_on, input.depends_on_names);
241
325
  return { ...input, ...(parentId ? { parent_id: parentId } : {}), ...(dependsOn ? { depends_on: dependsOn } : {}) };
242
326
  },
327
+ (input, context) =>
328
+ call("tasks.create", {
329
+ ...input,
330
+ idempotency_key: input.idempotency_key ?? context.idempotencyKey,
331
+ idempotency_caller: context.principal?.id ?? "anonymous",
332
+ }),
243
333
  );
244
334
 
245
335
  define(
@@ -263,9 +353,9 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
263
353
 
264
354
  define(
265
355
  "list",
266
- "Lists Tasks matching an optional status/text/labels filter, scoped to project_root. project_root is required (no ambient cwd server-side).",
356
+ "Lists Tasks matching an optional status/text/labels filter, scoped to project_root. project_root is required (no ambient cwd server-side). Returns a lean summary (no body/extra) unless full: true is passed.",
267
357
  "read",
268
- readSchemaProps,
358
+ listSchemaProps,
269
359
  ["project_root"],
270
360
  (input) => {
271
361
  const rootTaskId = resolveRootTaskId(artifacts, tasks, input.project_root as string, input.root_task_id, input.root_task_name);
@@ -336,6 +426,35 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
336
426
  resolveIdAndScope,
337
427
  );
338
428
 
429
+ define(
430
+ "projects",
431
+ "Lists registered Task project scopes with stable ids, names, aliases, and canonical roots. Use resolve_project before a task operation when the user supplied a human project name.",
432
+ "read",
433
+ { query: stringProp, limit: numberProp },
434
+ [],
435
+ (input) => input,
436
+ );
437
+
438
+ define(
439
+ "resolve_project",
440
+ "Resolves one case-insensitive exact Task project id, name, alias, or canonical root. Fails closed on unknown or ambiguous references and returns the canonical project_root for subsequent task operations.",
441
+ "read",
442
+ { name: stringProp },
443
+ ["name"],
444
+ (input) => input,
445
+ (input) => resolveProject(input.name as string),
446
+ );
447
+
448
+ define(
449
+ "register_project",
450
+ "Registers a Task project name and aliases. Pass project to update/rename/move an existing registration while preserving its stable id and old name as an alias.",
451
+ "local-write",
452
+ { project_root: stringProp, name: stringProp, aliases: arrayProp, project: stringProp },
453
+ ["project_root"],
454
+ (input) => input,
455
+ (input) => call("tasks.register_project", input),
456
+ );
457
+
339
458
  define(
340
459
  "scope",
341
460
  "Describes the current task-view scope selection for project_root.",
@@ -527,7 +646,7 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
527
646
  "set_checklist",
528
647
  "Replaces a Task's evidence-bearing checklist (proof requirements) in full.",
529
648
  "local-write",
530
- { id: stringProp, name: stringProp, checklist: objectProp, project_root: stringProp },
649
+ { id: stringProp, name: stringProp, checklist: checklistProp, project_root: stringProp },
531
650
  ["checklist"],
532
651
  resolveIdAndScope,
533
652
  );
@@ -535,7 +654,7 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
535
654
  "set_gates",
536
655
  "Replaces a Task's gate commands in full. Each gate is {type, target, expect?, timeoutMs?} -- timeoutMs overrides the default per-type command timeout (30s)/test timeout (60s) for a legitimately slower gate, up to a bounded ceiling.",
537
656
  "local-write",
538
- { id: stringProp, name: stringProp, gates: arrayProp, project_root: stringProp },
657
+ { id: stringProp, name: stringProp, gates: gateProp, project_root: stringProp },
539
658
  ["gates"],
540
659
  resolveIdAndScope,
541
660
  );
@@ -633,7 +752,7 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
633
752
 
634
753
  define(
635
754
  "claim",
636
- "Claims this Task's lease under owner (defaults to session_id). Throws if a different owner already holds one.",
755
+ "Claims this Task's lease under owner (defaults to session_id). Returns taskName (the reusable artifact alias) and taskTitle instead of exposing its backend UUID. Throws if a different owner already holds one.",
637
756
  "local-write",
638
757
  {
639
758
  id: stringProp,
@@ -653,7 +772,7 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
653
772
  );
654
773
  define(
655
774
  "heartbeat_lease",
656
- "Extends this Task's lease -- needs the exact owner/token claim() returned.",
775
+ "Extends this Task's lease -- needs the exact owner/token claim() returned and returns the reusable taskName plus taskTitle.",
657
776
  "local-write",
658
777
  {
659
778
  id: stringProp,
@@ -683,7 +802,7 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
683
802
  );
684
803
  define(
685
804
  "lease",
686
- "Shows this Task's current lease, if any.",
805
+ "Shows this Task's current lease, if any, identified by reusable taskName plus taskTitle rather than its backend UUID.",
687
806
  "read",
688
807
  { id: stringProp, name: stringProp, project_root: stringProp },
689
808
  [],
package/src/index.ts CHANGED
@@ -24,7 +24,7 @@ export type { DisplayGraph, DisplayGraphEdge, DisplayGraphNode, RenderedGraph }
24
24
  export type { GateResult } from "./domain/gate.ts";
25
25
  export type { NoteHistoryPage } from "./domain/note-event.ts";
26
26
  export type { TaskEvent, TaskHistoryPage } from "./domain/task-event.ts";
27
- export type { TaskLease } from "./domain/task-lease.ts";
27
+ export type { TaskLease, TaskLeaseView } from "./domain/task-lease.ts";
28
28
  export type { TaskViewSelection } from "./domain/task-scope.ts";
29
29
  export { NOTE_DISPOSITIONS } from "./note/note-service.ts";
30
30
  export type { PlaybookInvocationResult, PlaybookMissingArguments } from "./playbook/playbook-execution.ts";
@@ -3,7 +3,7 @@
3
3
  * (step 5, continued, of the incremental refactor in
4
4
  * reducing-papyrus-consumer-change-amplification-with-modules--pvdo).
5
5
  *
6
- * Imports only src/domain-services.ts's Doc functions, which are already generic
6
+ * Imports only src/docs/docs-service.ts's Doc functions, which are already generic
7
7
  * ArtifactStore-based with no other module's concrete class dependency.
8
8
  */
9
9
 
@@ -20,7 +20,7 @@ import {
20
20
  showDocument,
21
21
  transitionDocument,
22
22
  updateDocument,
23
- } from "../domain-services.ts";
23
+ } from "../docs/docs-service.ts";
24
24
  import type { OperationDefinition } from "../module-registry.ts";
25
25
  import { type OperationInput, optionalBoolean, optionalNumber, optionalString, string } from "./operation-input.ts";
26
26
 
@@ -7,8 +7,8 @@
7
7
  * table via NOTE_SUBTYPE) and has exactly six operations — the smallest real module to
8
8
  * prove the OperationRegistry shape against before extracting Tasks or Docs.
9
9
  *
10
- * This module does not import another module's infrastructure (src/task-service.ts,
11
- * src/domain-services.ts, etc.) — only its own src/note-service.ts and the shared
10
+ * This module does not import another module's infrastructure (src/task/task-service.ts,
11
+ * src/docs/docs-service.ts, etc.) — only its own src/note/note-service.ts and the shared
12
12
  * OperationInput parsing helpers, matching the "module code does not import another
13
13
  * module's infrastructure" constraint.
14
14
  */
@@ -6,13 +6,15 @@
6
6
  * declare Doc/Rule blueprints and typed arguments and nested pipeline calls. playbooks.invoke
7
7
  * recycles the shared blueprint materialization engine (playbook/playbook-execution.ts compiles a
8
8
  * Playbook's steps and composition tree into a BlueprintDefinition, then hands off to
9
- * playbook/workflow-execution.ts's shared core). See domain-services.ts's Playbook section and
9
+ * playbook/workflow-execution.ts's shared core). See playbook/playbook-service.ts and
10
10
  * playbook/playbook-definition.ts for the full rationale.
11
11
  */
12
12
 
13
13
  import { summarizeArtifact } from "../artifact/artifact.ts";
14
14
  import type { ArtifactScopeStore } from "../artifact/artifact-scope-store.ts";
15
15
  import type { ArtifactStore } from "../artifact/artifact-store.ts";
16
+ import type { OperationDefinition } from "../module-registry.ts";
17
+ import { invokePlaybook } from "../playbook/playbook-execution.ts";
16
18
  import {
17
19
  assignPlaybookProject,
18
20
  containPlaybook,
@@ -25,9 +27,7 @@ import {
25
27
  uncontainPlaybook,
26
28
  undependPlaybook,
27
29
  updatePlaybook,
28
- } from "../domain-services.ts";
29
- import type { OperationDefinition } from "../module-registry.ts";
30
- import { invokePlaybook } from "../playbook/playbook-execution.ts";
30
+ } from "../playbook/playbook-service.ts";
31
31
  import type { SessionIdentity } from "../session-identity/session-identity-service.ts";
32
32
  import type { TaskEventStore } from "../stores/task-event-store.ts";
33
33
  import type { TaskScopeStore } from "../stores/task-scope-store.ts";
@@ -108,8 +108,10 @@ export function playbooksOperations({
108
108
  steps: input.steps,
109
109
  tools: input.tools as string[] | undefined,
110
110
  arguments: input.arguments,
111
+ subtype: optionalString(input, "subtype"),
111
112
  labels: input.labels as string[] | undefined,
112
113
  extra: input.extra as Record<string, unknown> | undefined,
114
+ templateId: optionalString(input, "template_id") ?? optionalString(input, "templateId"),
113
115
  projectRoot: optionalString(input, "project_root"),
114
116
  },
115
117
  eventContext(input),
@@ -14,6 +14,7 @@
14
14
  import { type Artifact, summarizeArtifact } from "../artifact/artifact.ts";
15
15
  import type { ArtifactScopeStore } from "../artifact/artifact-scope-store.ts";
16
16
  import type { ArtifactStore } from "../artifact/artifact-store.ts";
17
+ import type { OperationDefinition } from "../module-registry.ts";
17
18
  import {
18
19
  assignRuleProject,
19
20
  createRule,
@@ -25,8 +26,7 @@ import {
25
26
  showRule,
26
27
  transitionRule,
27
28
  updateRule,
28
- } from "../domain-services.ts";
29
- import type { OperationDefinition } from "../module-registry.ts";
29
+ } from "../rules/rules-service.ts";
30
30
  import { type OperationInput, optionalBoolean, optionalNumber, optionalString, string } from "./operation-input.ts";
31
31
 
32
32
  const MODULE_ID = "rules";
@@ -92,8 +92,10 @@ export function rulesOperations(artifacts: ArtifactStore, scopes: ArtifactScopeS
92
92
  condition: optionalString(input, "condition"),
93
93
  action: optionalString(input, "rule_action") ?? optionalString(input, "governance_action"),
94
94
  severity: optionalString(input, "severity") as "block" | "warn" | "info" | undefined,
95
+ subtype: optionalString(input, "subtype"),
95
96
  labels: input.labels as string[] | undefined,
96
97
  extra: input.extra as Record<string, unknown> | undefined,
98
+ templateId: optionalString(input, "template_id") ?? optionalString(input, "templateId"),
97
99
  projectRoot: optionalString(input, "project_root"),
98
100
  },
99
101
  eventContext(input),