@danypops/papyrus 0.41.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 +8 -11
- package/package.json +1 -1
- package/src/artifact-relationship-view.ts +1 -1
- package/src/cli.ts +12 -178
- package/src/constants.ts +19 -52
- package/src/db.ts +34 -7
- package/src/domain/artifact-event.ts +1 -1
- package/src/domain/blueprint-definition.ts +273 -0
- package/src/domain-services.ts +147 -204
- package/src/modules/logs.ts +1 -1
- package/src/modules/playbooks.ts +9 -7
- package/src/ops.ts +3 -1
- package/src/playbook-definition.ts +75 -29
- package/src/playbook-execution.ts +7 -24
- package/src/ports/artifact-scope-store.ts +1 -1
- package/src/service.ts +5 -21
- package/src/task-service.ts +1 -1
- package/src/vehicle/artifact-trash-vehicle.ts +1 -1
- package/src/vehicle/artifact-vehicle-shared.ts +4 -6
- package/src/vehicle/docs-vehicle.ts +2 -2
- package/src/vehicle/notes-vehicle.ts +2 -2
- package/src/vehicle/papyrus-vehicle.ts +2 -5
- package/src/vehicle/playbooks-vehicle.ts +1 -1
- package/src/workflow-execution.ts +139 -69
- package/src/domain/skill-definition.ts +0 -270
- package/src/modules/skills.ts +0 -158
- package/src/vehicle/skills-vehicle.ts +0 -194
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,
|
|
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
|
-
- `
|
|
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,21 +67,18 @@ 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
|
-
###
|
|
70
|
+
### Playbooks
|
|
71
71
|
|
|
72
|
-
A
|
|
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:
|
|
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
|
|
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
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.
|
|
@@ -90,11 +87,11 @@ Once the deadline passes, the daemon's periodic sweep performs a real, cascading
|
|
|
90
87
|
|
|
91
88
|
### Naming vs. ids
|
|
92
89
|
|
|
93
|
-
Every agent domain tool (tasks, docs, rules,
|
|
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
|
|
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
|
@@ -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/
|
|
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
|
|
122
|
-
papyrus
|
|
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 --
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
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:
|
|
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 =
|
|
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 =
|
|
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
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* cycle-safe -- a
|
|
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 (
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
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
|
-
|
|
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→
|
|
351
|
-
* depends_on: DAG ordering (task→task)
|
|
352
|
-
* documents: describes (doc→task, doc→rule, doc→
|
|
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
|
|
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→
|
|
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→
|
|
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
|
|
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,
|
|
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
|