@danypops/papyrus 0.40.0 → 0.42.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 CHANGED
@@ -42,7 +42,7 @@ The daemon uses WAL, foreign keys, a bounded busy timeout, versioned migrations,
42
42
 
43
43
  `artifacts` is the shared graph-identity supertype, not a second copy of every application's database. `edges` references that single identity table at both endpoints, preserving foreign-key integrity for cross-domain links. Domain extension tables exist only where application invariants require indexed relational state: Task chronology/focus/scope and Discourse posts/events/session cursors/projection checkpoints. This is a class-table/table-per-type variant with explicit child-to-parent foreign keys; Papyrus does not use SQLite table inheritance or orphan-prone `(target_type, target_id)` links.
44
44
 
45
- The owning application remains the mutation authority. Discourse commits its extension rows and `context-thread`/`context-message` Doc projections atomically through `discourse.store`; generic artifact, document, Skill-template, lifecycle, and graph-link operations reject those owned subtypes and the `reply_to`/`discusses` relations. SQLite triggers additionally verify that each extension row references the expected Doc subtype. Domain tables are canonical for domain invariants; graph bodies and metadata are read-oriented projections committed in the same transaction.
45
+ The owning application remains the mutation authority. Discourse commits its extension rows and `context-thread`/`context-message` Doc projections atomically through `discourse.store`; generic artifact, document, lifecycle, and graph-link operations reject those owned subtypes and the `reply_to`/`discusses` relations. SQLite triggers additionally verify that each extension row references the expected Doc subtype. Domain tables are canonical for domain invariants; graph bodies and metadata are read-oriented projections committed in the same transaction.
46
46
 
47
47
  The authenticated CLI exposes the same operation for diagnostics and adapter parity:
48
48
 
@@ -59,7 +59,7 @@ Papyrus enforces four artifact kinds:
59
59
  - `doc` — knowledge: specifications, decisions, and research
60
60
  - `task` — work: desired outcomes, gates, checklists, and dependencies
61
61
  - `rule` — governance injected into the Pi system prompt
62
- - `skill` — a parameterized workflow bundle whose validated arguments render a connected collection of deterministic Tasks plus contextual Rules and Docs
62
+ - `playbook` — a trigger and an ordered list of steps whose validated arguments render a connected collection of deterministic Tasks plus contextual Rules and Docs
63
63
 
64
64
  Each kind has an enforced status vocabulary. Every edge endpoint must exist, and every edge relation must be registered in `relation_names`. Relations are universal: any artifact kind can link to any other kind.
65
65
 
@@ -67,34 +67,31 @@ Each kind has an enforced status vocabulary. Every edge endpoint must exist, and
67
67
 
68
68
  Use `contains` and `part_of` for explicit parent/child structure; use `depends_on` for execution ordering. Dependency edges form an executable DAG: self-dependencies and cycles are rejected, fan-in waits for every prerequisite, and fan-out can expose several ready successors while active focus remains singular. Graph reads are cycle-safe and bounded by `depth` and `max_nodes` (defaults: depth 4, 100 nodes; hard ceilings: depth 20, 1,000 nodes). Executable task plans are additionally bounded to 1,000 tasks and 10,000 relationships.
69
69
 
70
- ### Skills and compatibility templates
70
+ ### Playbooks
71
71
 
72
- A Papyrus Skill is distinct from a conventional prompt-only skill: its input API and blueprints define a connected Task/Rule/Doc workflow. `skills.run` validates and normalizes all arguments, safely renders placeholders in memory, validates the complete graph, then persists artifacts and edges in one transaction. Task dependencies, containment, gates, checklists, and context survive rendering. Run Rules are injected only while active focus belongs to that run. Docs retain invocation context and provenance; missing evidence references remain unknown and no gate runs during instantiation.
72
+ A Playbook's steps are a plain prose string (a Task), or a structured object: `{kind:'doc',...}` creates a Doc, `{kind:'rule',...}` creates a Rule, `{kind:'call',...}` nests another Playbook's own run as a pipeline step. `playbooks.invoke` validates and normalizes all arguments, safely renders placeholders in memory, validates the complete graph, then persists artifacts and edges in one transaction. Task dependencies, containment, gates, checklists, and context survive rendering. Run Rules are injected only while active focus belongs to that run. Docs retain invocation context and provenance; missing evidence references remain unknown and no gate runs during instantiation.
73
73
 
74
- A run result has a stable schema: Skill ID, run ID, normalized arguments, created IDs grouped by kind, ready root task IDs, and the bounded execution plan. Explicit run IDs produce deterministic artifact IDs (`<run-id>-<blueprint-ref>`); collisions roll back the entire run.
74
+ A run result has a stable schema: Playbook ID, run ID, normalized arguments, created IDs grouped by kind, ready root task IDs, and the bounded execution plan. Explicit run IDs produce deterministic artifact IDs (`<run-id>-<blueprint-ref>`); collisions roll back the entire run.
75
75
 
76
76
  ```bash
77
- papyrus skills run <skill-id> \
77
+ papyrus playbooks invoke <playbook-id> \
78
78
  --arguments-json '{"project":"Papyrus"}' \
79
- --run-id papyrus-001 \
80
79
  --json
81
80
  ```
82
81
 
83
- The existing `artifact-template` skill subtype remains a compatibility mechanism for one-artifact templates with metadata `{targetKind, defaults, required}`. Instantiate it via the `skills.instantiate` operation with `template_id`; defaults merge recursively, explicit arrays replace defaults, required paths such as `extra.owner` are validated, and target-kind mismatches are rejected.
84
-
85
82
  ### Removing an artifact
86
83
 
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` (any of the `tasks`/`docs`/`rules`/`skills` domain tools, 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.
84
+ 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
85
 
89
86
  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
87
 
91
88
  ### Naming vs. ids
92
89
 
93
- Every agent domain tool (tasks, docs, rules, skills, notes, discuss) addresses its artifacts by `name` (the exact title) wherever `id` would otherwise be required -- `dependency_name`/`parent_name`/`child_name`/`root_task_name`/`depends_on_names` (tasks), `target_name` (docs link, searches every kind since a link target can be any of them), `task_name` (rules gate, discuss block/unblock), `template_name` (skills instantiate), and `blocks_task_names` (discuss open) are the name-based equivalents of their `*_id` counterparts. Resolution is an exact, case-insensitive, trimmed title match scoped like a plain list call; an unmatched or ambiguous name fails with a clear error (ambiguous names list the real ids, since that's the one point disambiguation genuinely needs them). Results returned to the agent likewise lead with name and status, never id, unless two artifacts in the same result share a title -- id is a backend implementation detail, not a conversational handle. `id` itself still works exactly as before for every action, in every tool.
90
+ Every agent domain tool (tasks, docs, rules, playbooks, notes, discuss) addresses its artifacts by `name` (the exact title) wherever `id` would otherwise be required -- `dependency_name`/`parent_name`/`child_name`/`root_task_name`/`depends_on_names` (tasks), `target_name` (docs link, searches every kind since a link target can be any of them), `task_name` (rules gate, discuss block/unblock), and `blocks_task_names` (discuss open) are the name-based equivalents of their `*_id` counterparts. Resolution is an exact, case-insensitive, trimmed title match scoped like a plain list call; an unmatched or ambiguous name fails with a clear error (ambiguous names list the real ids, since that's the one point disambiguation genuinely needs them). Results returned to the agent likewise lead with name and status, never id, unless two artifacts in the same result share a title -- id is a backend implementation detail, not a conversational handle. `id` itself still works exactly as before for every action, in every tool.
94
91
 
95
92
  ### Mutability
96
93
 
97
- Tasks, Docs, Rules, and Skills all support first-class `update` (title/body/labels, at least one required) alongside creation -- a Doc's body is no longer immutable once created. Every update is bounded the same way creation is (Rules keep their own stricter combined condition+action+body ceiling; Docs/Skills share Tasks' own length bounds) and recorded on the artifact's append-only mutation history, queryable via `graph.history`. An artifact carrying a `source:<system>` label (e.g. `source:web-spider` on an ingested page) is a read-only projection from a system Papyrus doesn't own the source of; updating one is refused with a clear error rather than silently forking it -- capture a correction as a new linked Doc instead until a write-back capability to that system exists. Notes stay behind their own facade for any content change, same as every other Notes mutation.
94
+ Tasks, Docs, Rules, and Playbooks all support first-class `update` (title/body/labels, at least one required) alongside creation -- a Doc's body is no longer immutable once created. Every update is bounded the same way creation is (Rules keep their own stricter combined condition+action+body ceiling; Docs/Playbooks share Tasks' own length bounds) and recorded on the artifact's append-only mutation history, queryable via `graph.history`. An artifact carrying a `source:<system>` label (e.g. `source:web-spider` on an ingested page) is a read-only projection from a system Papyrus doesn't own the source of; updating one is refused with a clear error rather than silently forking it -- capture a correction as a new linked Doc instead until a write-back capability to that system exists. Notes stay behind their own facade for any content change, same as every other Notes mutation.
98
95
 
99
96
  Internally, application services depend on the `ArtifactStore` and `GateRunner` ports. SQLite and subprocess execution are adapters composed only by the daemon; task behavior is unit-tested against fakes without a database. Task visualization projects the same `TaskGraph` into semantic display graphs and sends them through a `GraphRenderer` port -- the Pi adapter (in `@danypops/pi-papyrus`) uses `beautiful-mermaid` for terminal Unicode output without leaking Mermaid syntax into the task domain.
100
97
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/papyrus",
3
- "version": "0.40.0",
3
+ "version": "0.42.0",
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"],
@@ -4,7 +4,7 @@ import { fallbackLabel } from "./task-relationship-view.ts";
4
4
 
5
5
  /**
6
6
  * The generic-artifact counterpart to projectTaskRelationships: unlike a Task, which already
7
- * has its containing TaskGraph's real titles in memory, a generic Doc/Rule/Skill/Playbook only
7
+ * has its containing TaskGraph's real titles in memory, a generic Doc/Rule/Playbook only
8
8
  * has raw edge id pairs (artifact.show's tree fetch never resolves neighbor titles -- see
9
9
  * ops.ts getArtifact). Reuses the same fallbackLabel heuristic rather than adding a network
10
10
  * round-trip per neighbor, which would turn a rendering concern into a new daemon-adjacent one.
package/src/cli.ts CHANGED
@@ -118,18 +118,8 @@ const USAGE = `Usage:
118
118
  papyrus rules injectable [--json]
119
119
  papyrus rules assign-project <id> [project-root] [--json]
120
120
  papyrus rules update <id> [--title <title>] [--body <body>] [--labels-json <json>] [--json]
121
- papyrus skills run <id> [--arguments-json <json>] [--run-id <id>] [--json]
122
- papyrus skills create --title <title> [--body <body>] [--trigger <text>] [--steps-json <json>] [--tools-json <json>] [--definition-json <json>] [--labels-json <json>] [--extra-json <json>] [--project-root <path>] [--json]
123
- papyrus skills create-template --title <title> --target-kind <kind> [--defaults-json <json>] [--required-json <json>] [--body <body>] [--labels-json <json>] [--project-root <path>] [--json]
124
- papyrus skills list [--status <status>] [--text <query>] [--limit <count>] [--project-root <path>] [--json]
125
- papyrus skills show <id> [--json]
126
- papyrus skills invoke <id> [--json]
127
- papyrus skills enable|disable <id> [--json]
128
- papyrus skills instantiate <template-id> [--title <title>] [--body <body>] [--status <status>] [--labels-json <json>] [--extra-json <json>] [--json]
129
- papyrus skills assign-project <id> [project-root] [--json]
130
- papyrus skills update <id> [--title <title>] [--body <body>] [--labels-json <json>] [--json]
131
- papyrus playbooks create --title <title> [--body <body>] [--trigger <text>] [--steps-json <json>] [--tools-json <json>] [--labels-json <json>] [--extra-json <json>] [--arguments-json <json array>] [--project-root <path>] [--json]
132
- papyrus playbooks invoke <id> [--arguments-json <json object>] [--project-root <path>] [--json] # materializes real tasks (contains/depends_on-wired) and focuses the entry task
121
+ papyrus playbooks create --title <title> [--body <body>] [--trigger <text>] [--steps-json <json array of strings and/or {kind:'doc'|'rule'|'call'|'task',...} objects>] [--tools-json <json>] [--labels-json <json>] [--extra-json <json>] [--arguments-json <json array, each optionally {type,enum,default}>] [--project-root <path>] [--json]
122
+ papyrus playbooks invoke <id> [--arguments-json <json object>] [--run-id <id>] [--project-root <path>] [--json] # materializes real tasks (contains/depends_on-wired) and focuses the entry task
133
123
  papyrus playbooks preview <id> [--arguments-json <json object>] [--json] # renders text only, creates nothing
134
124
  papyrus playbooks list [--status <status>] [--text <query>] [--limit <count>] [--project-root <path>] [--json]
135
125
  papyrus playbooks show <id> [--json]
@@ -231,10 +221,11 @@ function parseJsonStringArrayFlag(value: string | undefined, flag: string): stri
231
221
  }
232
222
 
233
223
  /**
234
- * No shape assertion here -- unlike every other JSON flag, playbooks --arguments-json is genuinely
235
- * polymorphic (an array on create, a {name: value} map on invoke), and the two actions share one
236
- * flag-parsing pass in runPlaybooksCli. The service validates the shape for whichever operation
237
- * actually receives it.
224
+ * No shape assertion here -- playbooks --arguments-json is genuinely polymorphic (an array on
225
+ * create, a {name: value} map on invoke, sharing one flag-parsing pass in runPlaybooksCli), and
226
+ * --steps-json accepts a mix of plain prose strings and structured step objects (doc/rule/call/
227
+ * task) that a single string-array assertion would wrongly reject. The service validates the
228
+ * real shape for whichever operation actually receives it.
238
229
  */
239
230
  function parseJsonAnyFlag(value: string | undefined, flag: string): unknown {
240
231
  if (value === undefined) throw new Error(`${flag} requires a value`);
@@ -376,160 +367,6 @@ export function runIdMigrationCli(args: string[]): string {
376
367
  throw new Error("migrate-ids requires one of: mirror, validate, promote");
377
368
  }
378
369
 
379
- export async function runSkillCli(args: string[], client: TaskCliClient, projectRoot: string = process.cwd()): Promise<string> {
380
- const json = args.includes("--json");
381
- const positional: string[] = [];
382
- let runId: string | undefined;
383
- let arguments_: Record<string, unknown> = {};
384
- let title: string | undefined;
385
- let body: string | undefined;
386
- let trigger: string | undefined;
387
- let steps: string[] | undefined;
388
- let tools: string[] | undefined;
389
- let definition: unknown;
390
- let labels: string[] | undefined;
391
- let extra: Record<string, unknown> | undefined;
392
- let targetKind: string | undefined;
393
- let defaults: Record<string, unknown> | undefined;
394
- let required: string[] | undefined;
395
- let status: string | undefined;
396
- let text: string | undefined;
397
- let limit: number | undefined;
398
- let skillProjectRoot: string | undefined;
399
- for (let index = 0; index < args.length; index++) {
400
- const argument = args[index]!;
401
- if (argument === "--json") continue;
402
- if (argument === "--run-id") { runId = args[++index]; if (!runId) throw new Error("--run-id requires a value"); continue; }
403
- if (argument === "--arguments-json") { arguments_ = parseJsonObjectFlag(args[++index], "--arguments-json"); continue; }
404
- if (argument === "--title") { title = args[++index]; if (title === undefined) throw new Error("--title requires a value"); continue; }
405
- if (argument === "--body") { body = args[++index]; if (body === undefined) throw new Error("--body requires a value"); continue; }
406
- if (argument === "--trigger") { trigger = args[++index]; if (trigger === undefined) throw new Error("--trigger requires a value"); continue; }
407
- if (argument === "--steps-json") { steps = parseJsonStringArrayFlag(args[++index], "--steps-json"); continue; }
408
- if (argument === "--tools-json") { tools = parseJsonStringArrayFlag(args[++index], "--tools-json"); continue; }
409
- if (argument === "--definition-json") {
410
- const value = args[++index];
411
- if (!value) throw new Error("--definition-json requires a value");
412
- definition = JSON.parse(value);
413
- continue;
414
- }
415
- if (argument === "--labels-json") { labels = parseJsonStringArrayFlag(args[++index], "--labels-json"); continue; }
416
- if (argument === "--extra-json") { extra = parseJsonObjectFlag(args[++index], "--extra-json"); continue; }
417
- if (argument === "--target-kind") { targetKind = args[++index]; if (!targetKind) throw new Error("--target-kind requires a value"); continue; }
418
- if (argument === "--defaults-json") { defaults = parseJsonObjectFlag(args[++index], "--defaults-json"); continue; }
419
- if (argument === "--required-json") { required = parseJsonStringArrayFlag(args[++index], "--required-json"); continue; }
420
- if (argument === "--status") { status = args[++index]; if (!status) throw new Error("--status requires a value"); continue; }
421
- if (argument === "--text") { text = args[++index]; if (text === undefined) throw new Error("--text requires a value"); continue; }
422
- if (argument === "--project-root") { skillProjectRoot = args[++index]; if (!skillProjectRoot) throw new Error("--project-root requires a value"); continue; }
423
- if (argument === "--limit") {
424
- const value = args[++index];
425
- if (!value || Number.isNaN(Number(value))) throw new Error("--limit requires a numeric value");
426
- limit = Number(value);
427
- continue;
428
- }
429
- if (argument.startsWith("--")) throw new Error(`unknown skills option ${argument}`);
430
- positional.push(argument);
431
- }
432
- const [action, id, second] = positional;
433
- if (action === "run") {
434
- if (positional.length !== 2) throw new Error("skills requires `run <id>`");
435
- const input: Record<string, unknown> = { id, arguments: arguments_, project_root: projectRoot };
436
- if (runId) input["run_id"] = runId;
437
- const result = await client.call<Record<string, unknown>, {
438
- runId: string;
439
- created: { tasks: string[]; rules: string[]; docs: string[] };
440
- rootTaskIds: string[];
441
- execution: TaskExecutionPlan;
442
- }>("skills.run", input);
443
- if (json) return JSON.stringify(result);
444
- return [
445
- `Created Skill run ${result.runId}: ${result.created.tasks.length} tasks, ${result.created.rules.length} rules, ${result.created.docs.length} docs`,
446
- `Ready roots: ${result.rootTaskIds.join(", ") || "none"}`,
447
- `Context docs: ${result.created.docs.join(", ") || "none"}`,
448
- `Scoped rules: ${result.created.rules.join(", ") || "none"}`,
449
- ...result.execution.nodes.map((node) => `[${node.state}] ${node.id} ${node.title}`),
450
- ].join("\n");
451
- }
452
- let result: unknown;
453
- let human: string;
454
- switch (action) {
455
- case "create": {
456
- if (id) throw new Error("skills create accepts no positional arguments");
457
- if (!title) throw new Error("skills create requires --title");
458
- const artifact = await client.call<Record<string, unknown>, CliArtifact>("skills.create", { title, body, trigger, steps, tools, definition, labels, extra, project_root: skillProjectRoot });
459
- result = artifact;
460
- human = `Created skill: ${artifactLabel(artifact)}`;
461
- break;
462
- }
463
- case "create-template": {
464
- if (id) throw new Error("skills create-template accepts no positional arguments");
465
- if (!title || !targetKind) throw new Error("skills create-template requires --title and --target-kind");
466
- const artifact = await client.call<Record<string, unknown>, CliArtifact>("skills.create_template", {
467
- title, target_kind: targetKind, defaults, required, body, labels, project_root: skillProjectRoot,
468
- });
469
- result = artifact;
470
- human = `Created template: ${artifactLabel(artifact)}`;
471
- break;
472
- }
473
- case "list": {
474
- if (id) throw new Error("skills list accepts no positional arguments");
475
- const rows = await client.call<Record<string, unknown>, CliArtifact[]>("skills.list", { status, text, limit, project_root: skillProjectRoot });
476
- result = rows;
477
- human = rows.length === 0 ? "No skills found." : rows.map((row) => artifactLabel(row)).join("\n");
478
- break;
479
- }
480
- case "assign-project": {
481
- if (!id) throw new Error("skills assign-project requires <id> [project-root]");
482
- const artifact = await client.call<Record<string, unknown>, CliArtifact>("skills.assign_project", { id, project_root: second });
483
- result = artifact;
484
- human = second ? `Assigned ${id} to ${second}` : `Unscoped ${id}`;
485
- break;
486
- }
487
- case "show": {
488
- if (!id) throw new Error("skills show requires exactly one skill id");
489
- const artifact = await client.call<Record<string, unknown>, CliArtifact>("skills.show", { id });
490
- result = artifact;
491
- human = `${artifactLabel(artifact)}\n\n${artifact.body ?? ""}`;
492
- break;
493
- }
494
- case "invoke": {
495
- if (!id) throw new Error("skills invoke requires exactly one skill id");
496
- const invocation = await client.call<Record<string, unknown>, string>("skills.invoke", { id });
497
- result = invocation;
498
- human = invocation;
499
- break;
500
- }
501
- case "enable":
502
- case "disable": {
503
- if (!id) throw new Error(`skills ${action} requires exactly one skill id`);
504
- const operation = action === "enable" ? "skills.enable" : "skills.disable";
505
- const artifact = await client.call<Record<string, unknown>, CliArtifact>(operation, { id });
506
- result = artifact;
507
- human = `${artifactLabel(artifact)}`;
508
- break;
509
- }
510
- case "instantiate": {
511
- if (!id) throw new Error("skills instantiate requires exactly one template id");
512
- const artifact = await client.call<Record<string, unknown>, CliArtifact>("skills.instantiate", {
513
- template_id: id, title, body, status, labels, extra, project_root: projectRoot,
514
- });
515
- result = artifact;
516
- human = `Created: ${artifactLabel(artifact)}`;
517
- break;
518
- }
519
- case "update": {
520
- if (!id || second) throw new Error("skills update requires exactly one skill id");
521
- if (title === undefined && body === undefined && labels === undefined) throw new Error("skills update requires --title, --body, or --labels-json");
522
- const artifact = await client.call<Record<string, unknown>, CliArtifact>("skills.update", { id, title, body, labels });
523
- result = artifact;
524
- human = `${artifactLabel(artifact)}`;
525
- break;
526
- }
527
- default:
528
- throw new Error("skills action must be run, create, create-template, list, show, invoke, enable, disable, instantiate, assign-project, or update");
529
- }
530
- return json ? JSON.stringify(result) : human;
531
- }
532
-
533
370
  export async function runGraphCli(args: string[], client: TaskCliClient): Promise<string> {
534
371
  const json = args.includes("--json");
535
372
  const positional: string[] = [];
@@ -822,11 +659,12 @@ export async function runPlaybooksCli(args: string[], client: TaskCliClient): Pr
822
659
  let title: string | undefined;
823
660
  let body: string | undefined;
824
661
  let trigger: string | undefined;
825
- let steps: string[] | undefined;
662
+ let steps: unknown;
826
663
  let tools: string[] | undefined;
827
664
  let labels: string[] | undefined;
828
665
  let extra: Record<string, unknown> | undefined;
829
666
  let playbookArguments: unknown;
667
+ let runId: string | undefined;
830
668
  let status: string | undefined;
831
669
  let text: string | undefined;
832
670
  let limit: number | undefined;
@@ -837,11 +675,12 @@ export async function runPlaybooksCli(args: string[], client: TaskCliClient): Pr
837
675
  if (argument === "--title") { title = args[++index]; if (title === undefined) throw new Error("--title requires a value"); continue; }
838
676
  if (argument === "--body") { body = args[++index]; if (body === undefined) throw new Error("--body requires a value"); continue; }
839
677
  if (argument === "--trigger") { trigger = args[++index]; if (trigger === undefined) throw new Error("--trigger requires a value"); continue; }
840
- if (argument === "--steps-json") { steps = parseJsonStringArrayFlag(args[++index], "--steps-json"); continue; }
678
+ if (argument === "--steps-json") { steps = parseJsonAnyFlag(args[++index], "--steps-json"); continue; }
841
679
  if (argument === "--tools-json") { tools = parseJsonStringArrayFlag(args[++index], "--tools-json"); continue; }
842
680
  if (argument === "--labels-json") { labels = parseJsonStringArrayFlag(args[++index], "--labels-json"); continue; }
843
681
  if (argument === "--extra-json") { extra = parseJsonObjectFlag(args[++index], "--extra-json"); continue; }
844
682
  if (argument === "--arguments-json") { playbookArguments = parseJsonAnyFlag(args[++index], "--arguments-json"); continue; }
683
+ if (argument === "--run-id") { runId = args[++index]; if (!runId) throw new Error("--run-id requires a value"); continue; }
845
684
  if (argument === "--status") { status = args[++index]; if (!status) throw new Error("--status requires a value"); continue; }
846
685
  if (argument === "--text") { text = args[++index]; if (text === undefined) throw new Error("--text requires a value"); continue; }
847
686
  if (argument === "--project-root") { playbookProjectRoot = args[++index]; if (!playbookProjectRoot) throw new Error("--project-root requires a value"); continue; }
@@ -889,7 +728,7 @@ export async function runPlaybooksCli(args: string[], client: TaskCliClient): Pr
889
728
  }
890
729
  case "invoke": {
891
730
  if (!id || second) throw new Error("playbooks invoke requires exactly one playbook id");
892
- const invocation = await client.call<Record<string, unknown>, { entryTaskId: string; missingArguments?: string[] }>("playbooks.invoke", { id, arguments: playbookArguments, project_root: playbookProjectRoot });
731
+ const invocation = await client.call<Record<string, unknown>, { entryTaskId: string; missingArguments?: string[] }>("playbooks.invoke", { id, arguments: playbookArguments, run_id: runId, project_root: playbookProjectRoot });
893
732
  result = invocation;
894
733
  human = invocation.missingArguments
895
734
  ? `Missing required argument(s): ${invocation.missingArguments.join(", ")}.`
@@ -1803,11 +1642,6 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
1803
1642
  console.log(await runTaskCli(args.slice(1), client));
1804
1643
  return;
1805
1644
  }
1806
- if (command === "skills") {
1807
- const client = await connectPapyrusClient();
1808
- console.log(await runSkillCli(args.slice(1), client));
1809
- return;
1810
- }
1811
1645
  if (command === "playbooks") {
1812
1646
  const client = await connectPapyrusClient();
1813
1647
  console.log(await runPlaybooksCli(args.slice(1), client));
package/src/constants.ts CHANGED
@@ -7,7 +7,7 @@ export const DAEMON_PROBE_TIMEOUT_MS = 800;
7
7
  export const DAEMON_UNIT_NAME = "papyrus.service";
8
8
  export const DAEMON_DIR_ENV = "PAPYRUS_DAEMON_DIR";
9
9
  export const SQLITE_BUSY_TIMEOUT_MS = 5_000;
10
- export const SQLITE_SCHEMA_VERSION = 21;
10
+ export const SQLITE_SCHEMA_VERSION = 23;
11
11
  export const SERVICE_MAX_BODY_BYTES = 1_048_576;
12
12
 
13
13
  export const WAL_CHECKPOINT_INTERVAL_MS = 60_000;
@@ -97,22 +97,22 @@ export const SKILL_MAX_LINKS = 500;
97
97
  export const SKILL_MAX_RENDERED_BYTES = 1_048_576;
98
98
 
99
99
  /**
100
- * Skills are special: invoking one queries Papyrus for whatever it's actually graph-linked
101
- * to (existing Tasks/Rules/Docs via ordinary edges, not just its own static body/extra
102
- * fields), and a Skill can link to and invoke other Skills. Both traversals are bounded and
103
- * cycle-safe -- a skill-calls-skill edge cycle must not infinite-loop invocation, matching
104
- * the same cycle-safety discipline established by task dependency graphs and the
100
+ * Invoking a Playbook queries Papyrus for whatever it's actually graph-linked to (existing
101
+ * Tasks/Rules/Docs via ordinary edges, not just its own static body/extra fields), and a
102
+ * Playbook can link to and invoke other Playbooks. Both traversals are bounded and
103
+ * cycle-safe -- a playbook-calls-playbook edge cycle must not infinite-loop invocation,
104
+ * matching the same cycle-safety discipline established by task dependency graphs and the
105
105
  * (since-removed; see Doc "ConversationJournal design record") ConversationJournal domain's
106
106
  * own reply chains.
107
107
  */
108
- export const SKILL_INVOCATION_MAX_LINKED_ARTIFACTS = 20;
109
- export const SKILL_INVOCATION_MAX_CALL_DEPTH = 4;
110
108
  export const PLAYBOOK_INVOCATION_MAX_LINKED_ARTIFACTS = 20;
111
- /** Mirrors SKILL_INVOCATION_MAX_CALL_DEPTH: a playbook-calls-playbook edge chain is bounded the same way a skill-calls-skill chain is. */
112
109
  export const PLAYBOOK_INVOCATION_MAX_CALL_DEPTH = 4;
113
110
  export const PLAYBOOK_ARGUMENT_MAX_COUNT = 20;
114
111
  export const PLAYBOOK_ARGUMENT_NAME_MAX_LENGTH = 64;
115
112
  export const PLAYBOOK_ARGUMENT_DESCRIPTION_MAX_LENGTH = 500;
113
+ /** A Playbook argument's enum/default validation reuses SKILL_MAX_ENUM_VALUES directly (same value shape, no reason for a second bound). */
114
+ /** One playbook's own steps array, before composition with any contained/depended-on playbook -- mirrors SKILL_MAX_BLUEPRINTS' role for a workflow Skill's flat blueprint list. */
115
+ export const PLAYBOOK_MAX_STEPS = 100;
116
116
  /**
117
117
  * playbooks.invoke materializes a real Task per step (plus one container Task per playbook
118
118
  * node in the contains/depends_on composition tree) instead of rendering text -- this bounds
@@ -293,53 +293,20 @@ export function dbPath(): string {
293
293
  return `${xdg}/papyrus/papyrus.db`;
294
294
  }
295
295
 
296
- /**
297
- * Four purpose-built kinds — the enforced vocabulary.
298
- *
299
- * doc = Knowledge — descriptive ("here is what the architecture looks like")
300
- * task = Work — prescriptive action items with gates and checklists
301
- * rule = Governance — context injection ("when doing X, follow Y").
302
- * Maps to AGENTS.md semantics: active rules with inject:true are
303
- * appended to the system prompt on before_agent_start.
304
- * skill = Parameterized workflow bundle — validated inputs render connected Task, Rule, and Doc collections.
305
- */
306
- export const SEED_KINDS = [
307
- { name: "doc", description: "Knowledge — descriptive reference (specs, decisions, research, designs)" },
308
- { name: "task", description: "Work — action items with gates, checklists, and dependencies" },
309
- { name: "rule", description: "Governance — context injection (when doing X, follow Y). Maps to AGENTS.md" },
310
- { name: "skill", description: "Parameterized workflow bundle — inputs and templates load deterministic tasks plus contextual rules and docs" },
311
- ] as const;
312
-
313
- export const SEED_STATUSES = [
314
- { name: "draft", kind: "doc" },
315
- { name: "active", kind: "doc" },
316
- { name: "archived", kind: "doc" },
317
- { name: "todo", kind: "task" },
318
- { name: "in-progress", kind: "task" },
319
- { name: "review", kind: "task" },
320
- { name: "rejected", kind: "task" },
321
- { name: "done", kind: "task" },
322
- { name: "canceled", kind: "task" },
323
- { name: "active", kind: "rule" },
324
- { name: "deprecated", kind: "rule" },
325
- { name: "active", kind: "skill" },
326
- { name: "deprecated", kind: "skill" },
327
- ] as const;
328
-
329
296
  /**
330
297
  * The initial status a newly created artifact of a kind gets when no caller-supplied
331
298
  * status is given. This must be an explicit, named mapping — never derived from row order
332
- * in the `statuses` table (SEED_STATUSES' listed order, or a migration's insertion order,
333
- * is not a semantic guarantee; a migrated database can freely have a different physical
334
- * row order for the same logical status set). Deriving "the default" from "whichever row
335
- * happens to be first by rowid" was the root cause of a real production defect where
336
- * migrated databases created new Tasks as done instead of todo.
299
+ * in the `statuses` table (a migration's insertion order is not a semantic guarantee; a
300
+ * migrated database can freely have a different physical row order for the same logical
301
+ * status set). Deriving "the default" from "whichever row happens to be first by rowid"
302
+ * was the root cause of a real production defect where migrated databases created new
303
+ * Tasks as done instead of todo.
337
304
  */
338
305
  export const DEFAULT_STATUS_BY_KIND: Readonly<Record<string, string>> = {
339
306
  doc: "draft",
340
307
  task: "todo",
341
308
  rule: "active",
342
- skill: "active",
309
+ playbook: "active",
343
310
  };
344
311
 
345
312
  /**
@@ -347,14 +314,14 @@ export const DEFAULT_STATUS_BY_KIND: Readonly<Record<string, string>> = {
347
314
  *
348
315
  * references: source material (doc→doc, doc→task, doc→rule)
349
316
  * implements: this work satisfies that (task→doc, task→rule)
350
- * follows: this work obeys that (task→rule, task→skill)
351
- * depends_on: DAG ordering (task→task)
352
- * documents: describes (doc→task, doc→rule, doc→skill)
317
+ * follows: this work obeys that (task→rule, task→playbook)
318
+ * depends_on: DAG ordering (task→task, playbook→playbook)
319
+ * documents: describes (doc→task, doc→rule, doc→playbook)
353
320
  * blocks: blocking relationship (task→task)
354
321
  * supersedes: replaces (doc→doc, rule→rule)
355
322
  * relates_to: catch-all (any→any)
356
323
  * gates: this rule gates that task (rule→task)
357
- * triggers: this skill applies to that work (skill→task)
324
+ * triggers: this playbook run applies to that work (playbook→task)
358
325
  */
359
326
  export const SEED_RELATIONS = [
360
327
  "references", "implements", "follows", "depends_on",
package/src/db.ts CHANGED
@@ -291,7 +291,6 @@ const SEED_SQL = `
291
291
  INSERT OR IGNORE INTO kinds VALUES ('doc','Knowledge — what we know (specs, decisions, research, designs)');
292
292
  INSERT OR IGNORE INTO kinds VALUES ('task','Work — what we are doing (objectives, steps, checklists)');
293
293
  INSERT OR IGNORE INTO kinds VALUES ('rule','Governance — when doing X, follow Y');
294
- INSERT OR IGNORE INTO kinds VALUES ('skill','Parameterized workflow bundle — inputs and templates load tasks, rules, and docs');
295
294
  INSERT OR IGNORE INTO kinds VALUES ('playbook','Reusable procedure — a trigger and an ordered list of steps an agent reads and follows, not a mechanically instantiated blueprint');
296
295
  INSERT OR IGNORE INTO statuses VALUES ('draft','doc');
297
296
  INSERT OR IGNORE INTO statuses VALUES ('active','doc');
@@ -304,20 +303,18 @@ INSERT OR IGNORE INTO statuses VALUES ('done','task');
304
303
  INSERT OR IGNORE INTO statuses VALUES ('canceled','task');
305
304
  INSERT OR IGNORE INTO statuses VALUES ('active','rule');
306
305
  INSERT OR IGNORE INTO statuses VALUES ('deprecated','rule');
307
- INSERT OR IGNORE INTO statuses VALUES ('active','skill');
308
- INSERT OR IGNORE INTO statuses VALUES ('deprecated','skill');
309
306
  INSERT OR IGNORE INTO statuses VALUES ('active','playbook');
310
307
  INSERT OR IGNORE INTO statuses VALUES ('deprecated','playbook');
311
308
  INSERT OR IGNORE INTO relation_names VALUES ('references','Source material (doc→doc, doc→task, doc→rule)');
312
309
  INSERT OR IGNORE INTO relation_names VALUES ('implements','This work satisfies that (task→doc, task→rule)');
313
- INSERT OR IGNORE INTO relation_names VALUES ('follows','This work obeys that (task→rule, task→skill)');
314
- INSERT OR IGNORE INTO relation_names VALUES ('depends_on','DAG ordering (task→task)');
315
- INSERT OR IGNORE INTO relation_names VALUES ('documents','Describes (doc→task, doc→rule, doc→skill)');
310
+ INSERT OR IGNORE INTO relation_names VALUES ('follows','This work obeys that (task→rule, task→playbook)');
311
+ INSERT OR IGNORE INTO relation_names VALUES ('depends_on','DAG ordering (task→task, playbook→playbook)');
312
+ INSERT OR IGNORE INTO relation_names VALUES ('documents','Describes (doc→task, doc→rule, doc→playbook)');
316
313
  INSERT OR IGNORE INTO relation_names VALUES ('blocks','Blocking relationship (task→task, or an active Discussion doc→task)');
317
314
  INSERT OR IGNORE INTO relation_names VALUES ('supersedes','Replaces (doc→doc, rule→rule)');
318
315
  INSERT OR IGNORE INTO relation_names VALUES ('relates_to','Catch-all (any→any)');
319
316
  INSERT OR IGNORE INTO relation_names VALUES ('gates','This rule gates that task (rule→task)');
320
- INSERT OR IGNORE INTO relation_names VALUES ('triggers','This skill applies to that work (skill→task)');
317
+ INSERT OR IGNORE INTO relation_names VALUES ('triggers','This playbook run applies to that work (playbook→task)');
321
318
  INSERT OR IGNORE INTO relation_names VALUES ('contains','Parent contains a nested artifact (any→any)');
322
319
  INSERT OR IGNORE INTO relation_names VALUES ('part_of','Artifact belongs to a parent artifact (any→any)');
323
320
  CREATE INDEX IF NOT EXISTS edges_to_id_idx ON edges(to_id);
@@ -656,6 +653,36 @@ const FUTURE_MIGRATIONS: ReadonlyArray<PapyrusMigration> = [
656
653
  }
657
654
  },
658
655
  },
656
+ {
657
+ version: 22,
658
+ name: "skill-to-playbook-data-migration",
659
+ // Part of the Skill→Playbook consolidation. Migration 18 already moved subtype-less Skill
660
+ // rows to kind=playbook; this moves everything still left under kind=skill (workflow and
661
+ // artifact-template rows alike, plus any bare stragglers), preserving subtype and status so
662
+ // a later task can still tell which authoring shape each migrated row came from. The
663
+ // 'skill'/'artifact-template' kinds/statuses type rows themselves are deliberately NOT
664
+ // dropped here: src/modules/skills.ts's createSkill (and the CLI/Vehicle/TUI surfaces still
665
+ // wired to it) still writes kind='skill' rows in its own live, not-yet-retired test suite --
666
+ // dropping the kinds row now would break that FK-enforced write path before its own
667
+ // retirement task removes it. Dropping the type rows is that later task's job.
668
+ up: (db) => {
669
+ db.exec(`UPDATE artifacts SET kind = 'playbook' WHERE kind = 'skill'`);
670
+ },
671
+ },
672
+ {
673
+ version: 23,
674
+ name: "retire-skill-kind",
675
+ // Final step of the Skill→Playbook consolidation. Every module/CLI/Vehicle/TUI surface
676
+ // that wrote kind='skill' is now retired (confirmed live: zero real callers anywhere), and
677
+ // migration 22 already moved every existing row off kind=skill. The type rows themselves
678
+ // are now genuinely dead -- safe to drop.
679
+ up: (db) => {
680
+ db.exec(`
681
+ DELETE FROM statuses WHERE kind = 'skill';
682
+ DELETE FROM kinds WHERE name = 'skill';
683
+ `);
684
+ },
685
+ },
659
686
  ];
660
687
 
661
688
  /**
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Generic, kind-agnostic mutation event log — the "who did what, when" answer
3
- * shared by every artifact kind (doc, task, rule, skill), not reinvented per domain.
3
+ * shared by every artifact kind (doc, task, rule, playbook), not reinvented per domain.
4
4
  *
5
5
  * Modeled after scribe's parchment.Event/EventFilter/GetEvents shape, but avoids its
6
6
  * known gap: there, the Actor column is defined and filterable yet never populated by