@danypops/papyrus 0.41.0 → 0.42.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +8 -11
  2. package/package.json +2 -2
  3. package/src/adapters/sqlite-artifact-scope-store.ts +18 -9
  4. package/src/adapters/sqlite-artifact-store.ts +7 -5
  5. package/src/adapters/sqlite-discussion-round-store.ts +26 -17
  6. package/src/adapters/sqlite-gate-runner.ts +1 -1
  7. package/src/adapters/sqlite-graph-projection-store.ts +14 -10
  8. package/src/adapters/sqlite-log-store.ts +36 -17
  9. package/src/adapters/sqlite-note-event-store.ts +20 -16
  10. package/src/adapters/sqlite-session-identity-store.ts +13 -7
  11. package/src/adapters/sqlite-task-event-store.ts +29 -21
  12. package/src/adapters/sqlite-task-focus-store.ts +36 -10
  13. package/src/adapters/sqlite-task-lease-store.ts +12 -6
  14. package/src/adapters/sqlite-task-scope-store.ts +25 -14
  15. package/src/artifact-relationship-view.ts +4 -4
  16. package/src/artifact-subtree.ts +4 -2
  17. package/src/authority-registry.ts +2 -1
  18. package/src/cli.ts +794 -354
  19. package/src/client.ts +6 -3
  20. package/src/constants.ts +34 -56
  21. package/src/daemon-state.ts +4 -12
  22. package/src/daemon.ts +31 -9
  23. package/src/db.ts +153 -105
  24. package/src/discussion-service.ts +109 -44
  25. package/src/domain/artifact-event.ts +18 -5
  26. package/src/domain/artifact.ts +3 -1
  27. package/src/domain/blueprint-definition.ts +268 -0
  28. package/src/domain/checklist.ts +20 -17
  29. package/src/domain/discussion.ts +37 -18
  30. package/src/domain/gate.ts +7 -7
  31. package/src/domain/log-entry.ts +1 -1
  32. package/src/domain/note-event.ts +20 -7
  33. package/src/domain/task-event.ts +17 -7
  34. package/src/domain-services.ts +362 -288
  35. package/src/graph-projection-service.ts +34 -8
  36. package/src/id-migration.ts +17 -4
  37. package/src/index.ts +16 -11
  38. package/src/log-service.ts +6 -5
  39. package/src/log.ts +19 -0
  40. package/src/modules/discuss.ts +63 -28
  41. package/src/modules/docs.ts +74 -17
  42. package/src/modules/graph-projection.ts +20 -9
  43. package/src/modules/logs.ts +34 -22
  44. package/src/modules/notes.ts +66 -28
  45. package/src/modules/playbooks.ts +93 -32
  46. package/src/modules/rules.ts +57 -15
  47. package/src/modules/session-identity.ts +6 -2
  48. package/src/modules/tasks.ts +142 -67
  49. package/src/note-service.ts +11 -7
  50. package/src/ops.ts +134 -69
  51. package/src/playbook-definition.ts +124 -39
  52. package/src/playbook-execution.ts +18 -25
  53. package/src/ports/artifact-scope-store.ts +1 -1
  54. package/src/ports/note-event-store.ts +6 -4
  55. package/src/ports/task-event-store.ts +9 -6
  56. package/src/ports/task-focus-store.ts +17 -4
  57. package/src/ports/task-lease-store.ts +9 -4
  58. package/src/ports/task-scope-store.ts +3 -1
  59. package/src/service.ts +148 -110
  60. package/src/session-identity-service.ts +10 -2
  61. package/src/task-context.ts +28 -16
  62. package/src/task-execution.ts +4 -12
  63. package/src/task-graph-view.ts +12 -12
  64. package/src/task-relationship-view.ts +1 -3
  65. package/src/task-service.ts +168 -73
  66. package/src/vehicle/artifact-trash-vehicle.ts +26 -14
  67. package/src/vehicle/artifact-vehicle-shared.ts +32 -13
  68. package/src/vehicle/docs-vehicle.ts +50 -18
  69. package/src/vehicle/notes-vehicle.ts +26 -8
  70. package/src/vehicle/papyrus-vehicle.ts +16 -8
  71. package/src/vehicle/playbooks-vehicle.ts +88 -19
  72. package/src/vehicle/rules-vehicle.ts +58 -21
  73. package/src/vehicle/tasks-vehicle.ts +366 -54
  74. package/src/version.ts +1 -1
  75. package/src/workflow-execution.ts +198 -109
  76. package/src/domain/skill-definition.ts +0 -270
  77. package/src/modules/skills.ts +0 -158
  78. package/src/vehicle/skills-vehicle.ts +0 -194
package/src/client.ts CHANGED
@@ -2,7 +2,7 @@ import { spawn as spawnProcess } from "node:child_process";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import { connectWithPolicy, spawnDetachedDaemon } from "@danypops/vehicle-client/daemon-client";
4
4
  import { DAEMON_CLIENT_TIMEOUT_MS, DAEMON_DIR_ENV, DAEMON_PROBE_TIMEOUT_MS } from "./constants.ts";
5
- import { daemonStateDir, readDaemonHandle, type DaemonHandle } from "./daemon-state.ts";
5
+ import { type DaemonHandle, daemonStateDir, readDaemonHandle } from "./daemon-state.ts";
6
6
  import type { OperationName, SchemaState } from "./service.ts";
7
7
 
8
8
  export type FetchAdapter = (request: Request) => Promise<Response>;
@@ -26,7 +26,7 @@ export class PapyrusClient {
26
26
  signal: init.signal ?? AbortSignal.timeout(this.timeoutMs),
27
27
  });
28
28
  const response = await this.fetchAdapter(request);
29
- const body = await response.json() as { error?: string } & T;
29
+ const body = (await response.json()) as { error?: string } & T;
30
30
  if (!response.ok) throw new Error(body.error ?? `Papyrus daemon HTTP ${response.status}`);
31
31
  return body;
32
32
  }
@@ -87,7 +87,10 @@ export interface ConnectPapyrusClientOptions {
87
87
  * failure (stale, not "never started") and is NOT auto-recovered here -- it still
88
88
  * throws its own actionable "restart manually" error, unchanged from before.
89
89
  */
90
- export async function connectPapyrusClient(dir: string = daemonStateDir(), options: ConnectPapyrusClientOptions = {}): Promise<PapyrusClient> {
90
+ export async function connectPapyrusClient(
91
+ dir: string = daemonStateDir(),
92
+ options: ConnectPapyrusClientOptions = {},
93
+ ): Promise<PapyrusClient> {
91
94
  return connectWithPolicy({
92
95
  readHandle: () => readDaemonHandle(dir) ?? null,
93
96
  buildClient: probedPapyrusClient,
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
@@ -289,57 +289,24 @@ export const TASK_RECONCILIATION_INSTRUCTION = [
289
289
 
290
290
  /** $XDG_DATA_HOME/papyrus/papyrus.db */
291
291
  export function dbPath(): string {
292
- const xdg = process.env["XDG_DATA_HOME"] || `${process.env["HOME"]}/.local/share`;
292
+ const xdg = process.env.XDG_DATA_HOME || `${process.env.HOME}/.local/share`;
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,17 +314,28 @@ 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
- "references", "implements", "follows", "depends_on",
361
- "documents", "blocks", "supersedes", "relates_to",
362
- "gates", "triggers", "contains", "part_of", "reply_to", "discusses",
327
+ "references",
328
+ "implements",
329
+ "follows",
330
+ "depends_on",
331
+ "documents",
332
+ "blocks",
333
+ "supersedes",
334
+ "relates_to",
335
+ "gates",
336
+ "triggers",
337
+ "contains",
338
+ "part_of",
339
+ "reply_to",
340
+ "discusses",
363
341
  ] as const;
@@ -2,12 +2,7 @@ import { randomBytes } from "node:crypto";
2
2
  import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
- import {
6
- DAEMON_DIR_ENV,
7
- DAEMON_HOST,
8
- DAEMON_PORT_FILE,
9
- DAEMON_TOKEN_FILE,
10
- } from "./constants.ts";
5
+ import { DAEMON_DIR_ENV, DAEMON_HOST, DAEMON_PORT_FILE, DAEMON_TOKEN_FILE } from "./constants.ts";
11
6
 
12
7
  export interface DaemonHandle {
13
8
  baseUrl: string;
@@ -17,13 +12,10 @@ export interface DaemonHandle {
17
12
  pid: number;
18
13
  }
19
14
 
20
- export function daemonStateDir(
21
- env: Record<string, string | undefined> = process.env,
22
- home: string = homedir(),
23
- ): string {
15
+ export function daemonStateDir(env: Record<string, string | undefined> = process.env, home: string = homedir()): string {
24
16
  if (env[DAEMON_DIR_ENV]) return env[DAEMON_DIR_ENV];
25
- if (env["XDG_RUNTIME_DIR"]) return join(env["XDG_RUNTIME_DIR"], "papyrus");
26
- if (env["XDG_STATE_HOME"]) return join(env["XDG_STATE_HOME"], "papyrus");
17
+ if (env.XDG_RUNTIME_DIR) return join(env.XDG_RUNTIME_DIR, "papyrus");
18
+ if (env.XDG_STATE_HOME) return join(env.XDG_STATE_HOME, "papyrus");
27
19
  return join(home, ".local", "state", "papyrus");
28
20
  }
29
21
 
package/src/daemon.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { PushChannel } from "@danypops/vehicle-server/push-channel";
2
- import { DAEMON_HOST, DB_OPTIMIZE_INTERVAL_MS, WAL_CHECKPOINT_INTERVAL_MS, dbPath } from "./constants.ts";
2
+ import { DAEMON_HOST, DB_OPTIMIZE_INTERVAL_MS, dbPath, WAL_CHECKPOINT_INTERVAL_MS } from "./constants.ts";
3
3
  import { clearDaemonPort, daemonStateDir, loadOrCreateToken, writeDaemonPort } from "./daemon-state.ts";
4
+ import { logEvent, vehicleLogger } from "./log.ts";
4
5
  import { createApp, createPapyrusService } from "./service.ts";
5
- import { logEvent } from "./log.ts";
6
6
 
7
7
  /**
8
8
  * Operations that never change what a Task-graph reader (the pi-papyrus widget's
@@ -13,8 +13,16 @@ import { logEvent } from "./log.ts";
13
13
  * silently-uncovered new mutation.
14
14
  */
15
15
  const TASK_READ_ONLY_OPERATIONS = new Set([
16
- "tasks.active", "tasks.context", "tasks.event_feed", "tasks.focused",
17
- "tasks.graph", "tasks.history", "tasks.list", "tasks.plan", "tasks.scope", "tasks.show",
16
+ "tasks.active",
17
+ "tasks.context",
18
+ "tasks.event_feed",
19
+ "tasks.focused",
20
+ "tasks.graph",
21
+ "tasks.history",
22
+ "tasks.list",
23
+ "tasks.plan",
24
+ "tasks.scope",
25
+ "tasks.show",
18
26
  ]);
19
27
 
20
28
  /** Start the supervised, long-running Papyrus service. */
@@ -31,6 +39,7 @@ export function serveMain(): void {
31
39
  pushChannel.publish("tasks", { operation });
32
40
  }
33
41
  },
42
+ logger: vehicleLogger(),
34
43
  });
35
44
  const server = Bun.serve({
36
45
  hostname: DAEMON_HOST,
@@ -49,10 +58,18 @@ export function serveMain(): void {
49
58
  }
50
59
  writeDaemonPort(stateDir, server.port);
51
60
  const checkpointTimer = setInterval(() => {
52
- try { service.checkpoint(); } catch (error) { logEvent("error", "checkpoint_failed", { message: error instanceof Error ? error.message : String(error) }); }
61
+ try {
62
+ service.checkpoint();
63
+ } catch (error) {
64
+ logEvent("error", "checkpoint_failed", { message: error instanceof Error ? error.message : String(error) });
65
+ }
53
66
  }, WAL_CHECKPOINT_INTERVAL_MS);
54
67
  const optimizeTimer = setInterval(() => {
55
- try { service.optimize(); } catch (error) { logEvent("error", "optimize_failed", { message: error instanceof Error ? error.message : String(error) }); }
68
+ try {
69
+ service.optimize();
70
+ } catch (error) {
71
+ logEvent("error", "optimize_failed", { message: error instanceof Error ? error.message : String(error) });
72
+ }
56
73
  }, DB_OPTIMIZE_INTERVAL_MS);
57
74
  // Daily cadence (reusing DB_OPTIMIZE_INTERVAL_MS) is plenty against a 30-day staleness
58
75
  // threshold (TASK_FOCUS_STALE_AFTER_MS) -- see clean-up-stale-per-session-task-focus-rows-
@@ -61,7 +78,9 @@ export function serveMain(): void {
61
78
  try {
62
79
  const removed = service.reapStaleFocus();
63
80
  if (removed > 0) logEvent("info", "stale_focus_reaped", { removed });
64
- } catch (error) { logEvent("error", "reap_stale_focus_failed", { message: error instanceof Error ? error.message : String(error) }); }
81
+ } catch (error) {
82
+ logEvent("error", "reap_stale_focus_failed", { message: error instanceof Error ? error.message : String(error) });
83
+ }
65
84
  }, DB_OPTIMIZE_INTERVAL_MS);
66
85
  // Same daily cadence: ARTIFACT_TRASH_RETENTION_MS is 30 days, so a daily sweep finds newly
67
86
  // due artifacts promptly without needing its own tighter interval -- see domain/artifact-trash.ts.
@@ -69,7 +88,9 @@ export function serveMain(): void {
69
88
  try {
70
89
  const purged = service.purgeDueTrash();
71
90
  if (purged > 0) logEvent("info", "artifact_trash_purged", { purged });
72
- } catch (error) { logEvent("error", "purge_trash_failed", { message: error instanceof Error ? error.message : String(error) }); }
91
+ } catch (error) {
92
+ logEvent("error", "purge_trash_failed", { message: error instanceof Error ? error.message : String(error) });
93
+ }
73
94
  }, DB_OPTIMIZE_INTERVAL_MS);
74
95
  let stopping = false;
75
96
  const shutdown = () => {
@@ -83,7 +104,8 @@ export function serveMain(): void {
83
104
  service.close();
84
105
  // .finally() re-throws rather than handling a rejection -- catching it first turns a bare
85
106
  // unhandled-rejection warning into a real, queryable shutdown-failure log line.
86
- void server.stop(true)
107
+ void server
108
+ .stop(true)
87
109
  .catch((error) => logEvent("error", "server_stop_failed", { message: error instanceof Error ? error.message : String(error) }))
88
110
  .finally(() => process.exit(0));
89
111
  };