@danypops/papyrus 0.46.0 → 0.46.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/papyrus",
3
- "version": "0.46.0",
3
+ "version": "0.46.2",
4
4
  "description": "Daemon-backed graph artifacts, evidence-bearing tasks, rules, skills, and native TUI workflows for Pi",
5
5
  "type": "module",
6
6
  "keywords": ["pi-package"],
@@ -44,6 +44,10 @@ import {
44
44
  } from "./shared.ts";
45
45
 
46
46
  const OWNER = "playbooks";
47
+ const jsonObjectProp = {
48
+ type: ["object", "string"],
49
+ description: "A JSON object; a JSON-encoded object string is also accepted for tool-calling compatibility.",
50
+ } as const;
47
51
 
48
52
  export interface PlaybooksVehicleDeps {
49
53
  artifacts: ArtifactStore;
@@ -126,7 +130,7 @@ export function registerPlaybooksVehicleOperations(registry: VehicleRegistry, de
126
130
  "preview",
127
131
  "Renders a Playbook's whole composition tree as text, with no side effects.",
128
132
  "read",
129
- { id: stringProp, name: stringProp, arguments: { type: "object" } },
133
+ { id: stringProp, name: stringProp, arguments: jsonObjectProp },
130
134
  [],
131
135
  (input) => {
132
136
  normalizeJsonEncodedField(input, "arguments");
@@ -138,7 +142,7 @@ export function registerPlaybooksVehicleOperations(registry: VehicleRegistry, de
138
142
  "invoke",
139
143
  "Compiles the Playbook's steps and composition tree into real Tasks wired with dependsOn, and focuses the first one -- one step surfaces at a time as it becomes focused, exactly like any other Task. `arguments` supplies known values as {name: value}; if a declared REQUIRED argument is still missing, nothing is created and missingArguments is returned instead -- ask the human for these (discuss tool, live:true) and invoke again, never guess. Drive the returned entryTaskId forward with the tasks tool (start/submit/complete).",
140
144
  "local-write",
141
- { id: stringProp, name: stringProp, run_id: stringProp, arguments: { type: "object" }, project_root: stringProp },
145
+ { id: stringProp, name: stringProp, run_id: stringProp, arguments: jsonObjectProp, project_root: stringProp },
142
146
  [],
143
147
  (input) => {
144
148
  normalizeJsonEncodedField(input, "arguments");
@@ -7,11 +7,13 @@ import {
7
7
  bindVehicleOperation,
8
8
  defineVehicleOperation,
9
9
  defineVehicleSchema,
10
+ type JsonSchema,
10
11
  type VehicleContentBlock,
11
12
  VehicleError,
12
13
  type VehicleLimits,
13
14
  type VehicleOperationContext,
14
15
  type VehicleSchemaCodec,
16
+ type VehicleSchemaIssue,
15
17
  } from "@danypops/vehicle-core";
16
18
  import type { VehicleRegistry } from "@danypops/vehicle-server";
17
19
  import type { Artifact } from "../artifact/artifact.ts";
@@ -21,33 +23,118 @@ import { InvalidSessionSecretError } from "../session-identity/session-identity-
21
23
  import { TaskCreateIdempotencyConflictError } from "../stores/task-create-request-store.ts";
22
24
  import { TaskDependencyCycleError, TaskExecutionBoundExceededError, type TaskExecutionPlan } from "../task/task-execution.ts";
23
25
 
24
- /**
25
- * VehicleRegistry only ever calls a schema's own safeParse -- jsonSchema is
26
- * descriptive metadata surfaced to a client/Pi projection, never itself
27
- * enforced at runtime -- so a declared `enum` has to be checked here for
28
- * real, or it's a documentation gesture, not an honest contract.
29
- */
26
+ interface OperationSchemaNode {
27
+ readonly type?: string | readonly string[];
28
+ readonly enum?: readonly unknown[];
29
+ readonly properties?: Readonly<Record<string, OperationSchemaNode>>;
30
+ readonly required?: readonly string[];
31
+ readonly additionalProperties?: boolean | OperationSchemaNode;
32
+ readonly items?: OperationSchemaNode;
33
+ readonly minLength?: number;
34
+ readonly maxLength?: number;
35
+ readonly minimum?: number;
36
+ readonly maximum?: number;
37
+ readonly minItems?: number;
38
+ readonly maxItems?: number;
39
+ readonly description?: string;
40
+ readonly [key: string]: unknown;
41
+ }
42
+
43
+ function schemaIssue(path: readonly (string | number)[], message: string): VehicleSchemaIssue[] {
44
+ return [{ path, message }];
45
+ }
46
+
47
+ function matchesSchemaType(value: unknown, type: string): boolean {
48
+ if (type === "object") return typeof value === "object" && value !== null && !Array.isArray(value);
49
+ if (type === "array") return Array.isArray(value);
50
+ if (type === "string") return typeof value === "string";
51
+ if (type === "number") return typeof value === "number" && Number.isFinite(value);
52
+ if (type === "integer") return typeof value === "number" && Number.isInteger(value);
53
+ if (type === "boolean") return typeof value === "boolean";
54
+ return true;
55
+ }
56
+
57
+ function validateSchemaValue(value: unknown, schema: OperationSchemaNode, path: readonly (string | number)[]): VehicleSchemaIssue[] {
58
+ const label = path.length === 0 ? "input" : String(path.at(-1));
59
+ const declaredTypes = typeof schema.type === "string" ? [schema.type] : (schema.type ?? []);
60
+ const type = declaredTypes.find((candidate) => matchesSchemaType(value, candidate));
61
+ if (declaredTypes.length > 0 && type === undefined) {
62
+ const accepted = declaredTypes.map((candidate) =>
63
+ candidate === "integer" ? "an integer" : `${candidate === "object" ? "an" : "a"} ${candidate}`,
64
+ );
65
+ return schemaIssue(path, `${label} must be ${accepted.join(" or ")}`);
66
+ }
67
+ if (type === "object") {
68
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return schemaIssue(path, `${label} must be an object`);
69
+ const record = value as Record<string, unknown>;
70
+ for (const key of schema.required ?? []) {
71
+ if (!(key in record)) {
72
+ const acceptedShape = schema.description ? `; ${schema.description}` : "";
73
+ return schemaIssue([...path, key], `${key} is required${acceptedShape}`);
74
+ }
75
+ }
76
+ for (const [key, child] of Object.entries(schema.properties ?? {})) {
77
+ if (!(key in record)) continue;
78
+ const issues = validateSchemaValue(record[key], child, [...path, key]);
79
+ if (issues.length > 0) return issues;
80
+ }
81
+ for (const key of Object.keys(record)) {
82
+ if (key in (schema.properties ?? {})) continue;
83
+ if (schema.additionalProperties === false) return schemaIssue([...path, key], `${key} is not allowed`);
84
+ if (typeof schema.additionalProperties === "object") {
85
+ const issues = validateSchemaValue(record[key], schema.additionalProperties, [...path, key]);
86
+ if (issues.length > 0) return issues;
87
+ }
88
+ }
89
+ } else if (type === "array") {
90
+ const entries = value as unknown[];
91
+ if (schema.minItems !== undefined && entries.length < schema.minItems) {
92
+ return schemaIssue(path, `${label} must contain at least ${schema.minItems} item(s)`);
93
+ }
94
+ if (schema.maxItems !== undefined && entries.length > schema.maxItems) {
95
+ return schemaIssue(path, `${label} cannot contain more than ${schema.maxItems} item(s)`);
96
+ }
97
+ if (schema.items) {
98
+ for (const [index, entry] of entries.entries()) {
99
+ const issues = validateSchemaValue(entry, schema.items, [...path, index]);
100
+ if (issues.length > 0) return issues;
101
+ }
102
+ }
103
+ } else if (type === "string") {
104
+ const text = value as string;
105
+ if (schema.minLength !== undefined && text.length < schema.minLength) {
106
+ return schemaIssue(path, `${label} must contain at least ${schema.minLength} character(s)`);
107
+ }
108
+ if (schema.maxLength !== undefined && text.length > schema.maxLength) {
109
+ return schemaIssue(path, `${label} cannot exceed ${schema.maxLength} character(s)`);
110
+ }
111
+ } else if (type === "number" || type === "integer") {
112
+ const number = value as number;
113
+ if (schema.minimum !== undefined && number < schema.minimum) {
114
+ return schemaIssue(path, `${label} must be at least ${schema.minimum}`);
115
+ }
116
+ if (schema.maximum !== undefined && number > schema.maximum) {
117
+ return schemaIssue(path, `${label} cannot exceed ${schema.maximum}`);
118
+ }
119
+ }
120
+ if (schema.enum && !schema.enum.includes(value)) {
121
+ return schemaIssue(path, `${label} must be one of ${schema.enum.join(", ")}`);
122
+ }
123
+ return [];
124
+ }
125
+
126
+ /** VehicleRegistry executes this codec before resolving or dispatching an operation. Keep the
127
+ * recursive runtime checks aligned with the same JSON Schema clients and tools_man receive. */
30
128
  export function looseObjectSchema(
31
- properties: Record<string, { type: string; enum?: readonly string[] }>,
129
+ properties: Readonly<Record<string, OperationSchemaNode>>,
32
130
  required: readonly string[] = [],
33
131
  ): VehicleSchemaCodec<Record<string, unknown>> {
132
+ const schema = { type: "object", properties, required: [...required], additionalProperties: false } as const;
34
133
  return defineVehicleSchema<Record<string, unknown>>({
35
- jsonSchema: { type: "object", properties, required: [...required], additionalProperties: false },
134
+ jsonSchema: schema as unknown as JsonSchema,
36
135
  safeParse(value) {
37
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
38
- return { success: false, issues: [{ path: [], message: "input must be an object" }] };
39
- }
40
- const input = value as Record<string, unknown>;
41
- for (const key of required) {
42
- if (!(key in input)) return { success: false, issues: [{ path: [key], message: `${key} is required` }] };
43
- }
44
- for (const [key, schema] of Object.entries(properties)) {
45
- if (!schema.enum || !(key in input)) continue;
46
- if (!schema.enum.includes(input[key] as string)) {
47
- return { success: false, issues: [{ path: [key], message: `${key} must be one of ${schema.enum.join(", ")}` }] };
48
- }
49
- }
50
- return { success: true, value: input };
136
+ const issues = validateSchemaValue(value, schema, []);
137
+ return issues.length > 0 ? { success: false, issues } : { success: true, value: value as Record<string, unknown> };
51
138
  },
52
139
  });
53
140
  }
@@ -249,7 +336,7 @@ export function buildWorkflowRunContent(
249
336
 
250
337
  export type OperationSchemaProperties = Record<
251
338
  string,
252
- { type: string; enum?: readonly string[]; description?: string; [key: string]: unknown }
339
+ { type: string | readonly string[]; enum?: readonly string[]; description?: string; [key: string]: unknown }
253
340
  >;
254
341
 
255
342
  export type DefineOperation = (
@@ -81,6 +81,7 @@ const gateProp = {
81
81
  description: "Validation gates run by tasks.run_gates and tasks.complete.",
82
82
  items: {
83
83
  type: "object",
84
+ description: "Accepted gate shape: {type, target, expect?, timeoutMs?}.",
84
85
  properties: {
85
86
  type: { type: "string", enum: GATE_TYPES, description: "Gate evaluator." },
86
87
  target: { type: "string", minLength: 1, description: "Path, command, text target, or test command." },
@@ -109,6 +110,7 @@ const checklistProp = {
109
110
  minItems: 1,
110
111
  items: {
111
112
  type: "object",
113
+ description: "Accepted proof shape: {type, target, expect?}.",
112
114
  properties: {
113
115
  type: { type: "string", enum: PROOF_TYPES },
114
116
  target: { type: "string", minLength: 1 },
@@ -286,7 +288,7 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
286
288
 
287
289
  define(
288
290
  "create",
289
- "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.",
291
+ 'Creates a Task -- work: desired outcomes, gates, checklists, and dependencies. Gates are {type, target, expect?, timeoutMs?}; for example [{type: "command", target: "bun run typecheck", timeoutMs: 60000}]. Checklist criteria are {proof: [{type, target, expect?}]}. project_root is required (no ambient cwd server-side). Prefer parent_name/depends_on_names over parent_id/depends_on -- resolved server-side.',
290
292
  "local-write",
291
293
  {
292
294
  title: stringProp,
@@ -752,11 +754,11 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
752
754
 
753
755
  define(
754
756
  "claim",
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.",
757
+ "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.",
756
758
  "local-write",
757
759
  {
758
- id: stringProp,
759
760
  name: stringProp,
761
+ id: stringProp,
760
762
  owner: stringProp,
761
763
  ttl_ms: numberProp,
762
764
  note: stringProp,
@@ -772,11 +774,11 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
772
774
  );
773
775
  define(
774
776
  "heartbeat_lease",
775
- "Extends this Task's lease -- needs the exact owner/token claim() returned and returns the reusable taskName plus taskTitle.",
777
+ "Extends this Task's lease -- needs the exact owner/token claim() returned. Prefer name over id. Returns the reusable taskName plus taskTitle.",
776
778
  "local-write",
777
779
  {
778
- id: stringProp,
779
780
  name: stringProp,
781
+ id: stringProp,
780
782
  owner: stringProp,
781
783
  token: stringProp,
782
784
  ttl_ms: numberProp,
@@ -791,9 +793,9 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
791
793
  );
792
794
  define(
793
795
  "release_lease",
794
- "Releases this Task's lease -- needs the exact owner/token claim() returned.",
796
+ "Releases this Task's lease -- needs the exact owner/token claim() returned. Prefer name over id.",
795
797
  "local-write",
796
- { id: stringProp, name: stringProp, owner: stringProp, token: stringProp, project_root: stringProp, session_id: stringProp },
798
+ { name: stringProp, id: stringProp, owner: stringProp, token: stringProp, project_root: stringProp, session_id: stringProp },
797
799
  ["owner", "token"],
798
800
  (input) => ({
799
801
  ...input,
@@ -802,9 +804,9 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
802
804
  );
803
805
  define(
804
806
  "lease",
805
- "Shows this Task's current lease, if any, identified by reusable taskName plus taskTitle rather than its backend UUID.",
807
+ "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.",
806
808
  "read",
807
- { id: stringProp, name: stringProp, project_root: stringProp },
809
+ { name: stringProp, id: stringProp, project_root: stringProp },
808
810
  [],
809
811
  (input) => ({
810
812
  ...input,