@danypops/papyrus 0.45.3 → 0.46.1

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,18 +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 {
32
34
  booleanProp,
33
35
  classifySessionAuthorization,
36
+ classifyTaskCreateIdempotency,
34
37
  classifyTaskDependencyCycles,
35
38
  classifyTaskExecutionBounds,
36
39
  createOperationDefiner,
@@ -69,9 +72,63 @@ const GATE_OPERATION_LIMITS: VehicleLimits = {
69
72
  maxResponseBytes: 262_144,
70
73
  };
71
74
 
72
- const objectProp = { type: "object" } as unknown as { type: string };
73
- const arrayProp = { type: "array" } as unknown as { type: string };
74
- 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;
75
132
 
76
133
  export interface TasksVehicleDeps {
77
134
  tasks: Tasks;
@@ -194,10 +251,26 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
194
251
  */
195
252
  const call = (name: string, input: Record<string, unknown>): unknown =>
196
253
  classifySessionAuthorization(() =>
197
- classifyTaskExecutionBounds(() => classifyTaskDependencyCycles(() => moduleOperations.get(name)!.execute(input))),
254
+ classifyTaskCreateIdempotency(() =>
255
+ classifyTaskExecutionBounds(() => classifyTaskDependencyCycles(() => moduleOperations.get(name)!.execute(input))),
256
+ ),
198
257
  );
199
258
  const define = createOperationDefiner(registry, OWNER, "tasks", ["tasks:read", "tasks:write"], call);
200
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
+
201
274
  /** Shared by every action taking a single id/name: resolves root_task_name first, then name -> id against the final scope. */
202
275
  const resolveIdAndScope = (input: Record<string, unknown>): Record<string, unknown> => {
203
276
  const projectRoot = input.project_root as string | undefined;
@@ -221,14 +294,21 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
221
294
  status: stringProp,
222
295
  labels: arrayProp,
223
296
  extra: objectProp,
224
- gates: arrayProp,
225
- checklist: objectProp,
297
+ gates: gateProp,
298
+ checklist: checklistProp,
226
299
  template_id: stringProp,
227
300
  parent_id: stringProp,
228
301
  parent_name: stringProp,
229
302
  depends_on: arrayProp,
230
303
  depends_on_names: arrayProp,
231
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
+ },
232
312
  session_id: stringProp,
233
313
  },
234
314
  ["title", "project_root"],
@@ -244,6 +324,12 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
244
324
  const dependsOn = resolveArrayField(artifacts, tasks, filter, input.depends_on, input.depends_on_names);
245
325
  return { ...input, ...(parentId ? { parent_id: parentId } : {}), ...(dependsOn ? { depends_on: dependsOn } : {}) };
246
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
+ }),
247
333
  );
248
334
 
249
335
  define(
@@ -340,6 +426,35 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
340
426
  resolveIdAndScope,
341
427
  );
342
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
+
343
458
  define(
344
459
  "scope",
345
460
  "Describes the current task-view scope selection for project_root.",
@@ -531,7 +646,7 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
531
646
  "set_checklist",
532
647
  "Replaces a Task's evidence-bearing checklist (proof requirements) in full.",
533
648
  "local-write",
534
- { id: stringProp, name: stringProp, checklist: objectProp, project_root: stringProp },
649
+ { id: stringProp, name: stringProp, checklist: checklistProp, project_root: stringProp },
535
650
  ["checklist"],
536
651
  resolveIdAndScope,
537
652
  );
@@ -539,7 +654,7 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
539
654
  "set_gates",
540
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.",
541
656
  "local-write",
542
- { id: stringProp, name: stringProp, gates: arrayProp, project_root: stringProp },
657
+ { id: stringProp, name: stringProp, gates: gateProp, project_root: stringProp },
543
658
  ["gates"],
544
659
  resolveIdAndScope,
545
660
  );
@@ -637,11 +752,11 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
637
752
 
638
753
  define(
639
754
  "claim",
640
- "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). Prefer name over id. Returns taskName (the reusable artifact alias) and taskTitle instead of exposing its backend UUID. Throws if a different owner already holds one.",
641
756
  "local-write",
642
757
  {
643
- id: stringProp,
644
758
  name: stringProp,
759
+ id: stringProp,
645
760
  owner: stringProp,
646
761
  ttl_ms: numberProp,
647
762
  note: stringProp,
@@ -657,11 +772,11 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
657
772
  );
658
773
  define(
659
774
  "heartbeat_lease",
660
- "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. Prefer name over id. Returns the reusable taskName plus taskTitle.",
661
776
  "local-write",
662
777
  {
663
- id: stringProp,
664
778
  name: stringProp,
779
+ id: stringProp,
665
780
  owner: stringProp,
666
781
  token: stringProp,
667
782
  ttl_ms: numberProp,
@@ -676,9 +791,9 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
676
791
  );
677
792
  define(
678
793
  "release_lease",
679
- "Releases this Task's lease -- needs the exact owner/token claim() returned.",
794
+ "Releases this Task's lease -- needs the exact owner/token claim() returned. Prefer name over id.",
680
795
  "local-write",
681
- { id: stringProp, name: stringProp, owner: stringProp, token: stringProp, project_root: stringProp, session_id: stringProp },
796
+ { name: stringProp, id: stringProp, owner: stringProp, token: stringProp, project_root: stringProp, session_id: stringProp },
682
797
  ["owner", "token"],
683
798
  (input) => ({
684
799
  ...input,
@@ -687,9 +802,9 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
687
802
  );
688
803
  define(
689
804
  "lease",
690
- "Shows this Task's current lease, if any.",
805
+ "Shows this Task's current lease, if any. Prefer name over id. The result is identified by reusable taskName plus taskTitle rather than its backend UUID.",
691
806
  "read",
692
- { id: stringProp, name: stringProp, project_root: stringProp },
807
+ { name: stringProp, id: stringProp, project_root: stringProp },
693
808
  [],
694
809
  (input) => ({
695
810
  ...input,
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),