@danypops/papyrus 0.47.0 → 0.47.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/README.md +81 -54
- package/package.json +1 -2
- package/src/handlers/shared.ts +8 -0
- package/src/handlers/tasks.ts +33 -17
package/README.md
CHANGED
|
@@ -1,29 +1,45 @@
|
|
|
1
1
|
# @danypops/papyrus
|
|
2
2
|
|
|
3
|
-
The daemon, CLI, and domain services behind Papyrus's graph artifact store
|
|
3
|
+
The daemon, CLI, and domain services behind Papyrus's graph artifact store: evidence-bearing Tasks, Docs, Rules, Playbooks, and Notes over one authenticated local database. Runs standalone as a plain daemon and CLI. `@danypops/pi-papyrus` projects the same daemon into native Pi tools.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Contents
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
- [Install](#install)
|
|
8
|
+
- [Quick start](#quick-start)
|
|
9
|
+
- [Schema protocol](#schema-protocol)
|
|
10
|
+
- [Hierarchy and traversal](#hierarchy-and-traversal)
|
|
11
|
+
- [Playbooks](#playbooks)
|
|
12
|
+
- [Naming vs. ids](#naming-vs-ids)
|
|
13
|
+
- [Mutability](#mutability)
|
|
14
|
+
- [Idempotent lifecycle mutations](#idempotent-lifecycle-mutations)
|
|
15
|
+
- [Removing an artifact](#removing-an-artifact)
|
|
16
|
+
- [Context Mesh persistence model](#context-mesh-persistence-model)
|
|
17
|
+
- [Storage and service](#storage-and-service)
|
|
18
|
+
- [Related packages](#related-packages)
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
bun add @danypops/papyrus
|
|
10
24
|
```
|
|
11
25
|
|
|
26
|
+
The `papyrus` binary ships with the package.
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
12
30
|
```bash
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
31
|
+
papyrus service install # install, enable, and start the user service
|
|
32
|
+
papyrus service status
|
|
33
|
+
papyrus service restart
|
|
16
34
|
|
|
17
|
-
#
|
|
35
|
+
# Task operations (add --json for machine output)
|
|
18
36
|
papyrus tasks plan
|
|
19
37
|
papyrus tasks depend <task-id> <prerequisite-id>
|
|
20
38
|
papyrus tasks update <task-id> --title "Revised task"
|
|
21
39
|
papyrus tasks focus <task-id>
|
|
22
|
-
papyrus tasks pause
|
|
23
|
-
papyrus tasks unpause
|
|
24
40
|
papyrus tasks complete <task-id>
|
|
25
41
|
|
|
26
|
-
# Resolve a
|
|
42
|
+
# Resolve a registered project name before scoped operations
|
|
27
43
|
papyrus tasks projects --query lector --json
|
|
28
44
|
papyrus tasks resolve-project Lector --json
|
|
29
45
|
|
|
@@ -32,79 +48,90 @@ papyrus notes capture "Review release provenance later"
|
|
|
32
48
|
papyrus notes list --json
|
|
33
49
|
```
|
|
34
50
|
|
|
35
|
-
|
|
51
|
+
## Schema protocol
|
|
36
52
|
|
|
37
|
-
|
|
38
|
-
bun run guard:install
|
|
39
|
-
```
|
|
53
|
+
Papyrus enforces four artifact kinds, each with its own status vocabulary:
|
|
40
54
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
Task project names are explicit registrations, never ambient-directory guesses. `tasks.projects` searches bounded registered identities; `tasks.resolve_project` requires one case-insensitive exact id, name, alias, or canonical root and fails on unknown or ambiguous references. Pass its returned `projectRoot` as `project_root` to subsequent task operations. `tasks.register_project` can rename or move an existing identity while preserving its stable id and prior name as an alias.
|
|
46
|
-
|
|
47
|
-
`tasks.create` accepts an optional `idempotency_key`. Replays with the same caller, canonical project root, key, and payload return the original response without another mutation; conflicting payload reuse is rejected. Keys are retained for seven days, isolated across callers and projects, and then expire. Retry only when reusing the exact key and payload; an unkeyed create remains unsafe to replay after an ambiguous transport failure.
|
|
55
|
+
- `doc` — knowledge: specifications, decisions, and research
|
|
56
|
+
- `task` — work: desired outcomes, gates, checklists, and dependencies
|
|
57
|
+
- `rule` — governance injected into the Pi system prompt
|
|
58
|
+
- `playbook` — a trigger and an ordered list of steps whose validated arguments render a connected collection of deterministic Tasks plus contextual Rules and Docs
|
|
48
59
|
|
|
49
|
-
|
|
60
|
+
Every edge endpoint resolves to a real artifact, and every edge relation is registered in `relation_names`. Relations are universal: any kind can link to any other kind.
|
|
50
61
|
|
|
51
|
-
|
|
62
|
+
## Hierarchy and traversal
|
|
52
63
|
|
|
53
|
-
|
|
64
|
+
`contains`/`part_of` express parent/child structure; `depends_on` expresses execution ordering. Dependency edges form an executable DAG: a self-dependency or cycle is rejected, fan-in waits for every prerequisite, and fan-out can expose several ready successors while active focus stays singular. Graph reads are cycle-safe and bounded by `depth`/`max_nodes` (default 4/100, ceiling 20/1,000). Executable task plans are bounded to 1,000 tasks and 10,000 relationships.
|
|
54
65
|
|
|
55
|
-
|
|
66
|
+
## Playbooks
|
|
56
67
|
|
|
57
|
-
|
|
68
|
+
A Playbook step is 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 arguments, renders placeholders in memory, validates the complete graph, then persists artifacts and edges in one transaction. Dependencies, containment, gates, checklists, and context all survive rendering. A run's Rules are active only while focus belongs to that run; Docs keep their invocation context and provenance.
|
|
58
69
|
|
|
59
|
-
|
|
70
|
+
A run result carries a stable schema: Playbook id, run id, normalized arguments, created ids grouped by kind, ready root task ids, and the bounded execution plan. An explicit run id produces deterministic artifact ids (`<run-id>-<blueprint-ref>`); a collision rolls the whole run back.
|
|
60
71
|
|
|
61
72
|
```bash
|
|
62
|
-
papyrus
|
|
63
|
-
--
|
|
73
|
+
papyrus playbooks invoke <playbook-id> \
|
|
74
|
+
--arguments-json '{"project":"Papyrus"}' \
|
|
64
75
|
--json
|
|
65
76
|
```
|
|
66
77
|
|
|
67
|
-
##
|
|
78
|
+
## Naming vs. ids
|
|
68
79
|
|
|
69
|
-
|
|
80
|
+
Every agent-facing domain (tasks, docs, rules, playbooks, notes, discuss) addresses artifacts by `name` (the exact title) anywhere `id` would otherwise be required — `dependency_name`/`parent_name`/`child_name`/`root_task_name`/`depends_on_names` (tasks), `target_name` (docs link, searched across every kind), `task_name` (rules gate, discuss block/unblock), and `blocks_task_names` (discuss open). Resolution is an exact, case-insensitive, trimmed title match scoped like a plain list call; an ambiguous name's error lists the real ids, the one place disambiguation needs them. A returned result leads with name and status; id surfaces only when two artifacts in the same result share a title. `id` itself keeps working, in every tool.
|
|
70
81
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
82
|
+
## Mutability
|
|
83
|
+
|
|
84
|
+
Tasks, Docs, Rules, and Playbooks all support `update` (title/body/labels, at least one field required) alongside creation. Every update shares creation's own bounds (Rules keep their own tighter condition+action+body ceiling) and lands on the artifact's append-only mutation history, queryable via `graph.history`. An artifact carrying a `source:<system>` label (e.g. `source:web-spider`) is a read-only projection owned by that system; `update` is refused with a clear error, and a correction belongs in a new linked Doc until that system ships its own write-back path. Notes route every content change through their own facade.
|
|
85
|
+
|
|
86
|
+
## Idempotent lifecycle mutations
|
|
75
87
|
|
|
76
|
-
|
|
88
|
+
`tasks.create` accepts an optional `idempotency_key`, scoped to the caller and canonical project root. Replaying the same key with the same payload returns the original response; a changed payload is rejected. Keys expire after seven days.
|
|
77
89
|
|
|
78
|
-
|
|
90
|
+
Task lifecycle mutations (`start`, `submit`, `reject`, `retry`, `cancel`, `reopen`, `complete`, `pause`, `unpause`) are destination-state idempotent: repeating an already-reached transition is a `changed: false` no-op with no duplicate history. Pass `idempotency_key` on any mutation whose response might get lost; after an unclear outcome, call `tasks.show` and `tasks.mutation_status` with that same key before deciding the next action. Completed receipts are retained for seven days; concurrent duplicate completion calls share one gate run. An incompatible transition returns typed `invalid-transition` details with current/intended status, allowed actions, and recovery guidance.
|
|
79
91
|
|
|
80
|
-
|
|
92
|
+
`tasks.claim`, `tasks.heartbeat_lease`, and `tasks.lease` return the reusable artifact alias as `taskName` plus `taskTitle`; use `taskName` for later Task operations and keep the lease token for heartbeat/release.
|
|
81
93
|
|
|
82
|
-
|
|
94
|
+
Task project names are registered identities, resolved explicitly rather than guessed from a working directory. `tasks.projects` searches bounded registered identities; `tasks.resolve_project` matches one case-insensitive exact id, name, alias, or canonical root, and reports unknown or ambiguous references directly. Pass the returned `projectRoot` into subsequent task operations. `tasks.register_project` renames or moves an existing identity while keeping its stable id and folding the prior name into its aliases.
|
|
83
95
|
|
|
84
|
-
|
|
96
|
+
## Removing an artifact
|
|
85
97
|
|
|
86
|
-
|
|
98
|
+
An artifact gets a permanent, immutable `created` row in the mutation event log the moment it exists, so removal is a time-gated trash entry. `remove` (the shared `artifact.remove`/`artifact.remove_subtree` operations every domain routes through, or `papyrus artifact remove <id> [--reason <text>]`) moves an artifact to the trash: excluded from every list/query immediately, still reachable directly by id, and recoverable via `restore` for 30 days. `remove` on a Task currently holding live Focus in any scope is refused.
|
|
99
|
+
|
|
100
|
+
Past the 30-day deadline, the daemon's periodic sweep performs a real, cascading delete — the one deliberate exception to Papyrus's append-only history, enforced by a database trigger checked at delete time.
|
|
101
|
+
|
|
102
|
+
## Context Mesh persistence model
|
|
103
|
+
|
|
104
|
+
`artifacts` is the shared graph-identity supertype; `edges` references that single identity table at both endpoints, keeping foreign-key integrity across domains. Domain extension tables exist only where application invariants need indexed relational state — Task chronology/focus/scope, Discourse posts/events/session cursors/projection checkpoints — a class-table/table-per-type layout with explicit child-to-parent foreign keys.
|
|
105
|
+
|
|
106
|
+
The owning application stays the mutation authority for its own extension rows: Discourse commits its rows and `context-thread`/`context-message` Doc projections atomically through `discourse.store`, while generic artifact/document/lifecycle/graph-link operations reject those owned subtypes and the `reply_to`/`discusses` relations. SQLite triggers verify each extension row references its expected Doc subtype. Domain tables stay canonical for domain invariants; graph bodies and metadata are read-oriented projections committed in the same transaction.
|
|
87
107
|
|
|
88
108
|
```bash
|
|
89
|
-
papyrus
|
|
90
|
-
--
|
|
109
|
+
papyrus discourse store read_thread --store-id team-forum \
|
|
110
|
+
--input-json '{"forumId":"engineering","topicId":"reviews","threadId":"mesh","limit":25}' \
|
|
91
111
|
--json
|
|
92
112
|
```
|
|
93
113
|
|
|
94
|
-
|
|
114
|
+
## Storage and service
|
|
95
115
|
|
|
96
|
-
|
|
116
|
+
```text
|
|
117
|
+
$XDG_DATA_HOME/papyrus/papyrus.db # durable graph
|
|
118
|
+
$XDG_RUNTIME_DIR/papyrus/{port,token} # private daemon discovery
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The daemon runs SQLite with WAL, foreign keys, a bounded busy timeout, versioned migrations, periodic passive checkpoints, and periodic `PRAGMA optimize`. Keep the database on a local filesystem — WAL depends on real local file locking.
|
|
97
122
|
|
|
98
|
-
|
|
123
|
+
Application services depend on the `ArtifactStore` and `GateRunner` ports; SQLite and subprocess execution are adapters the daemon composes, so task behavior is unit-tested against fakes without a database. Task visualization projects the same `TaskGraph` into semantic display graphs through a `GraphRenderer` port — the Pi adapter in `@danypops/pi-papyrus` renders terminal Unicode via `beautiful-mermaid` behind that port, so the task domain carries no Mermaid syntax.
|
|
99
124
|
|
|
100
|
-
|
|
125
|
+
For repository work, install the versioned ownership guard from the workspace root:
|
|
101
126
|
|
|
102
|
-
|
|
127
|
+
```bash
|
|
128
|
+
bun run guard:install
|
|
129
|
+
```
|
|
103
130
|
|
|
104
|
-
|
|
131
|
+
It checks a push's destination against `DanyPops/papyrus`, including an explicit fallback URL, so a push bound for the wrong remote fails locally before it reaches GitHub.
|
|
105
132
|
|
|
106
|
-
|
|
133
|
+
`src/index.ts` is this package's public surface for `@danypops/pi-papyrus` and any other consumer: explicit, named exports curated for outside use.
|
|
107
134
|
|
|
108
|
-
|
|
135
|
+
## Related packages
|
|
109
136
|
|
|
110
|
-
|
|
137
|
+
- **[`@danypops/pi-papyrus`](https://www.npmjs.com/package/@danypops/pi-papyrus)** — the Pi extension: native tools, TUI panels, and context injection over this daemon's authenticated loopback connection.
|
package/package.json
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/papyrus",
|
|
3
|
-
"version": "0.47.
|
|
3
|
+
"version": "0.47.2",
|
|
4
4
|
"description": "Daemon-backed graph artifacts, evidence-bearing tasks, rules, skills, and native TUI workflows for Pi",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"keywords": ["pi-package"],
|
|
7
6
|
"main": "./src/index.ts",
|
|
8
7
|
"types": "./src/index.ts",
|
|
9
8
|
"bin": {
|
package/src/handlers/shared.ts
CHANGED
|
@@ -29,6 +29,8 @@ interface OperationSchemaNode {
|
|
|
29
29
|
readonly properties?: Readonly<Record<string, OperationSchemaNode>>;
|
|
30
30
|
readonly required?: readonly string[];
|
|
31
31
|
readonly additionalProperties?: boolean | OperationSchemaNode;
|
|
32
|
+
/** A key not in `properties` is validated against the first pattern here whose RegExp matches it, instead of falling through to `additionalProperties` -- e.g. a free-form string-keyed map (tasks.create's checklist) uses `{"^.*$": entrySchema}` so a client-side JSON-Schema validator that reports `additionalProperties`-as-schema violations only as a generic top-level "must not have additional properties" (TypeBox's own real, confirmed behavior -- see vehicle-shell.ts's formatSchemaChildren for the matching tools_man rendering) instead descends into the real nested violation, matching an array's `items` precision. */
|
|
33
|
+
readonly patternProperties?: Readonly<Record<string, OperationSchemaNode>>;
|
|
32
34
|
readonly items?: OperationSchemaNode;
|
|
33
35
|
readonly minLength?: number;
|
|
34
36
|
readonly maxLength?: number;
|
|
@@ -80,6 +82,12 @@ function validateSchemaValue(value: unknown, schema: OperationSchemaNode, path:
|
|
|
80
82
|
}
|
|
81
83
|
for (const key of Object.keys(record)) {
|
|
82
84
|
if (key in (schema.properties ?? {})) continue;
|
|
85
|
+
const patternMatch = Object.entries(schema.patternProperties ?? {}).find(([pattern]) => new RegExp(pattern).test(key));
|
|
86
|
+
if (patternMatch) {
|
|
87
|
+
const issues = validateSchemaValue(record[key], patternMatch[1], [...path, key]);
|
|
88
|
+
if (issues.length > 0) return issues;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
83
91
|
if (schema.additionalProperties === false) return schemaIssue([...path, key], `${key} is not allowed`);
|
|
84
92
|
if (typeof schema.additionalProperties === "object") {
|
|
85
93
|
const issues = validateSchemaValue(record[key], schema.additionalProperties, [...path, key]);
|
package/src/handlers/tasks.ts
CHANGED
|
@@ -114,31 +114,47 @@ const gateProp = {
|
|
|
114
114
|
],
|
|
115
115
|
} as const;
|
|
116
116
|
|
|
117
|
+
/**
|
|
118
|
+
* `patternProperties: {"^.*$": entrySchema}` rather than `additionalProperties: entrySchema`,
|
|
119
|
+
* despite both meaning "every key maps to entrySchema" for a free-form string-keyed map:
|
|
120
|
+
* confirmed live (2026-08-09) that TypeBox's own Value.Errors -- the schema validator Pi's tool-
|
|
121
|
+
* calling harness runs client-side, before a call ever reaches this daemon -- reports an
|
|
122
|
+
* additionalProperties-as-schema violation only as a generic top-level "must not have additional
|
|
123
|
+
* properties", with zero descent into which nested field actually broke, while the structurally
|
|
124
|
+
* identical items-as-schema case (gates, proof arrays below) descends and reports the exact
|
|
125
|
+
* broken field. patternProperties does not have that limitation and gives the same precision as
|
|
126
|
+
* items. See handlers/shared.ts's OperationSchemaNode.patternProperties for the matching
|
|
127
|
+
* server-side runtime check, and vehicle-shell.ts's formatSchemaChildren for the matching
|
|
128
|
+
* tools_man rendering.
|
|
129
|
+
*/
|
|
117
130
|
const checklistProp = {
|
|
118
131
|
type: "object",
|
|
119
132
|
description: "Map from completion criterion text to one or more typed proof references. An empty map clears the checklist.",
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
+
patternProperties: {
|
|
134
|
+
"^.*$": {
|
|
135
|
+
type: "object",
|
|
136
|
+
properties: {
|
|
137
|
+
proof: {
|
|
138
|
+
type: "array",
|
|
139
|
+
minItems: 1,
|
|
140
|
+
items: {
|
|
141
|
+
type: "object",
|
|
142
|
+
description: "Accepted proof shape: {type, target, expect?}.",
|
|
143
|
+
properties: {
|
|
144
|
+
type: { type: "string", enum: PROOF_TYPES },
|
|
145
|
+
target: { type: "string", minLength: 1 },
|
|
146
|
+
expect: { type: "string" },
|
|
147
|
+
},
|
|
148
|
+
required: ["type", "target"],
|
|
149
|
+
additionalProperties: false,
|
|
133
150
|
},
|
|
134
|
-
required: ["type", "target"],
|
|
135
|
-
additionalProperties: false,
|
|
136
151
|
},
|
|
137
152
|
},
|
|
153
|
+
required: ["proof"],
|
|
154
|
+
additionalProperties: false,
|
|
138
155
|
},
|
|
139
|
-
required: ["proof"],
|
|
140
|
-
additionalProperties: false,
|
|
141
156
|
},
|
|
157
|
+
additionalProperties: false,
|
|
142
158
|
examples: [
|
|
143
159
|
{
|
|
144
160
|
"tests pass": { proof: [{ type: "test", target: "bun test", expect: "0 failures" }] },
|