@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.
- package/README.md +8 -11
- package/package.json +2 -2
- package/src/adapters/sqlite-artifact-scope-store.ts +18 -9
- package/src/adapters/sqlite-artifact-store.ts +7 -5
- package/src/adapters/sqlite-discussion-round-store.ts +26 -17
- package/src/adapters/sqlite-gate-runner.ts +1 -1
- package/src/adapters/sqlite-graph-projection-store.ts +14 -10
- package/src/adapters/sqlite-log-store.ts +36 -17
- package/src/adapters/sqlite-note-event-store.ts +20 -16
- package/src/adapters/sqlite-session-identity-store.ts +13 -7
- package/src/adapters/sqlite-task-event-store.ts +29 -21
- package/src/adapters/sqlite-task-focus-store.ts +36 -10
- package/src/adapters/sqlite-task-lease-store.ts +12 -6
- package/src/adapters/sqlite-task-scope-store.ts +25 -14
- package/src/artifact-relationship-view.ts +4 -4
- package/src/artifact-subtree.ts +4 -2
- package/src/authority-registry.ts +2 -1
- package/src/cli.ts +794 -354
- package/src/client.ts +6 -3
- package/src/constants.ts +34 -56
- package/src/daemon-state.ts +4 -12
- package/src/daemon.ts +31 -9
- package/src/db.ts +153 -105
- package/src/discussion-service.ts +109 -44
- package/src/domain/artifact-event.ts +18 -5
- package/src/domain/artifact.ts +3 -1
- package/src/domain/blueprint-definition.ts +268 -0
- package/src/domain/checklist.ts +20 -17
- package/src/domain/discussion.ts +37 -18
- package/src/domain/gate.ts +7 -7
- package/src/domain/log-entry.ts +1 -1
- package/src/domain/note-event.ts +20 -7
- package/src/domain/task-event.ts +17 -7
- package/src/domain-services.ts +362 -288
- package/src/graph-projection-service.ts +34 -8
- package/src/id-migration.ts +17 -4
- package/src/index.ts +16 -11
- package/src/log-service.ts +6 -5
- package/src/log.ts +19 -0
- package/src/modules/discuss.ts +63 -28
- package/src/modules/docs.ts +74 -17
- package/src/modules/graph-projection.ts +20 -9
- package/src/modules/logs.ts +34 -22
- package/src/modules/notes.ts +66 -28
- package/src/modules/playbooks.ts +93 -32
- package/src/modules/rules.ts +57 -15
- package/src/modules/session-identity.ts +6 -2
- package/src/modules/tasks.ts +142 -67
- package/src/note-service.ts +11 -7
- package/src/ops.ts +134 -69
- package/src/playbook-definition.ts +124 -39
- package/src/playbook-execution.ts +18 -25
- package/src/ports/artifact-scope-store.ts +1 -1
- package/src/ports/note-event-store.ts +6 -4
- package/src/ports/task-event-store.ts +9 -6
- package/src/ports/task-focus-store.ts +17 -4
- package/src/ports/task-lease-store.ts +9 -4
- package/src/ports/task-scope-store.ts +3 -1
- package/src/service.ts +148 -110
- package/src/session-identity-service.ts +10 -2
- package/src/task-context.ts +28 -16
- package/src/task-execution.ts +4 -12
- package/src/task-graph-view.ts +12 -12
- package/src/task-relationship-view.ts +1 -3
- package/src/task-service.ts +168 -73
- package/src/vehicle/artifact-trash-vehicle.ts +26 -14
- package/src/vehicle/artifact-vehicle-shared.ts +32 -13
- package/src/vehicle/docs-vehicle.ts +50 -18
- package/src/vehicle/notes-vehicle.ts +26 -8
- package/src/vehicle/papyrus-vehicle.ts +16 -8
- package/src/vehicle/playbooks-vehicle.ts +88 -19
- package/src/vehicle/rules-vehicle.ts +58 -21
- package/src/vehicle/tasks-vehicle.ts +366 -54
- package/src/version.ts +1 -1
- package/src/workflow-execution.ts +198 -109
- package/src/domain/skill-definition.ts +0 -270
- package/src/modules/skills.ts +0 -158
- 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
|
|
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(
|
|
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 =
|
|
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
|
|
@@ -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
|
|
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 (
|
|
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,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→
|
|
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
|
-
"references",
|
|
361
|
-
"
|
|
362
|
-
"
|
|
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;
|
package/src/daemon-state.ts
CHANGED
|
@@ -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
|
|
26
|
-
if (env
|
|
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,
|
|
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",
|
|
17
|
-
"tasks.
|
|
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 {
|
|
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 {
|
|
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) {
|
|
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) {
|
|
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
|
|
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
|
};
|