@danypops/papyrus 0.11.4 → 0.12.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.
Files changed (46) hide show
  1. package/README.md +16 -2
  2. package/extension/src/active-task-continuation.ts +6 -0
  3. package/extension/src/domain-tools.ts +108 -52
  4. package/extension/src/index.ts +90 -37
  5. package/extension/src/notes.ts +14 -1
  6. package/extension/src/task-focus-events.ts +57 -0
  7. package/extension/src/tasks.ts +51 -15
  8. package/extension/src/tool-rendering/artifact-card.ts +117 -0
  9. package/extension/src/tool-rendering/artifact-list.ts +179 -0
  10. package/extension/src/tool-rendering/index.ts +107 -0
  11. package/extension/src/tool-rendering/render-model.ts +406 -0
  12. package/package.json +4 -2
  13. package/src/adapters/in-memory-conversation-journal-store.ts +48 -0
  14. package/src/adapters/sqlite-artifact-scope-store.ts +36 -0
  15. package/src/adapters/sqlite-artifact-store.ts +20 -11
  16. package/src/adapters/sqlite-discourse-store.ts +325 -0
  17. package/src/adapters/sqlite-graph-projection-store.ts +41 -0
  18. package/src/adapters/sqlite-task-focus-store.ts +34 -15
  19. package/src/authority-registry.ts +115 -0
  20. package/src/cli.ts +904 -124
  21. package/src/constants.ts +38 -5
  22. package/src/conversation-journal-service.ts +87 -0
  23. package/src/db.ts +285 -33
  24. package/src/domain/artifact-event.ts +99 -0
  25. package/src/domain/conversation-journal.ts +168 -0
  26. package/src/domain/discourse-store.ts +142 -0
  27. package/src/domain/graph-projection.ts +74 -0
  28. package/src/domain/task-event.ts +4 -0
  29. package/src/domain-services.ts +133 -38
  30. package/src/graph-projection-service.ts +103 -0
  31. package/src/id-migration.ts +200 -0
  32. package/src/module-registry.ts +53 -0
  33. package/src/modules/docs.ts +77 -0
  34. package/src/modules/graph-projection.ts +82 -0
  35. package/src/modules/notes.ts +76 -0
  36. package/src/modules/rules.ts +81 -0
  37. package/src/modules/skills.ts +113 -0
  38. package/src/modules/tasks.ts +164 -0
  39. package/src/ops.ts +142 -15
  40. package/src/ports/artifact-scope-store.ts +20 -0
  41. package/src/ports/artifact-store.ts +10 -5
  42. package/src/ports/conversation-journal-store.ts +17 -0
  43. package/src/ports/graph-projection-store.ts +15 -0
  44. package/src/ports/task-focus-store.ts +62 -20
  45. package/src/service.ts +218 -223
  46. package/src/task-service.ts +70 -38
@@ -0,0 +1,117 @@
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import { type Component, truncateToWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
3
+ import type { ArtifactToolDetails } from "./render-model.ts";
4
+
5
+ const KIND_GLYPHS: Readonly<Record<string, string>> = {
6
+ task: "◇",
7
+ doc: "▤",
8
+ rule: "◆",
9
+ skill: "✦",
10
+ };
11
+
12
+ const STATUS_GLYPHS: Readonly<Record<string, string>> = {
13
+ done: "✓",
14
+ active: "●",
15
+ "in-progress": "●",
16
+ review: "◐",
17
+ rejected: "✗",
18
+ canceled: "×",
19
+ todo: "○",
20
+ draft: "○",
21
+ archived: "·",
22
+ deprecated: "·",
23
+ };
24
+
25
+ type SemanticColor = "success" | "error" | "warning" | "accent" | "muted";
26
+
27
+ function statusColor(status: string): SemanticColor {
28
+ if (status === "done" || status === "active") return "success";
29
+ if (status === "rejected" || status === "canceled") return "error";
30
+ if (status === "review") return "warning";
31
+ if (status === "in-progress") return "accent";
32
+ return "muted";
33
+ }
34
+
35
+ export function kindGlyph(kind: string): string {
36
+ return KIND_GLYPHS[kind] ?? "•";
37
+ }
38
+
39
+ export function statusGlyph(status: string): string {
40
+ return STATUS_GLYPHS[status] ?? "•";
41
+ }
42
+
43
+ export function countSummary(returned: number, total: number): string {
44
+ return returned === total ? String(total) : `${returned} of ${total}`;
45
+ }
46
+
47
+ export function emptyState(noun: string): string {
48
+ return `No ${noun}.`;
49
+ }
50
+
51
+ export function treeConnector(last: boolean): string {
52
+ return last ? "└─" : "├─";
53
+ }
54
+
55
+ export function expandHint(): string {
56
+ return "expand for details";
57
+ }
58
+
59
+ /** Reusable width-safe artifact card for native tool result rows. */
60
+ export class ArtifactCard implements Component {
61
+ private details: ArtifactToolDetails;
62
+ private theme: Theme;
63
+ private expanded: boolean;
64
+ private cachedWidth: number | undefined;
65
+ private cachedLines: string[] | undefined;
66
+
67
+ constructor(details: ArtifactToolDetails, theme: Theme, expanded: boolean) {
68
+ this.details = details;
69
+ this.theme = theme;
70
+ this.expanded = expanded;
71
+ }
72
+
73
+ update(details: ArtifactToolDetails, theme: Theme, expanded: boolean): void {
74
+ this.details = details;
75
+ this.theme = theme;
76
+ this.expanded = expanded;
77
+ this.invalidate();
78
+ }
79
+
80
+ render(width: number): string[] {
81
+ const safeWidth = Math.max(1, width);
82
+ if (this.cachedLines && this.cachedWidth === safeWidth) return this.cachedLines;
83
+
84
+ const artifact = this.details.artifact;
85
+ const status = `${statusGlyph(artifact.status)} ${artifact.status}`;
86
+ const header = [
87
+ this.theme.fg("toolTitle", this.theme.bold(`${kindGlyph(artifact.kind)} ${artifact.kind.toUpperCase()}`)),
88
+ this.theme.fg("accent", artifact.id),
89
+ this.theme.fg(statusColor(artifact.status), status),
90
+ ].join(" ");
91
+ const lines = [truncateToWidth(header, safeWidth)];
92
+ lines.push(truncateToWidth(this.theme.fg("text", artifact.title), safeWidth));
93
+
94
+ if (this.expanded) {
95
+ const metadata = [artifact.subtype, ...artifact.labels].filter(Boolean).join(" · ");
96
+ if (metadata) lines.push(truncateToWidth(this.theme.fg("muted", metadata), safeWidth));
97
+ if (artifact.body) lines.push(...wrapTextWithAnsi(artifact.body, safeWidth));
98
+ if (this.details.completeness.truncated) {
99
+ lines.push(truncateToWidth(
100
+ this.theme.fg("warning", `[truncated ${this.details.completeness.omitted} characters]`),
101
+ safeWidth,
102
+ ));
103
+ }
104
+ } else if (artifact.body || artifact.labels.length > 0) {
105
+ lines.push(truncateToWidth(this.theme.fg("dim", expandHint()), safeWidth));
106
+ }
107
+
108
+ this.cachedWidth = safeWidth;
109
+ this.cachedLines = lines;
110
+ return lines;
111
+ }
112
+
113
+ invalidate(): void {
114
+ this.cachedWidth = undefined;
115
+ this.cachedLines = undefined;
116
+ }
117
+ }
@@ -0,0 +1,179 @@
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import { type Component, truncateToWidth } from "@earendil-works/pi-tui";
3
+ import { TOOL_COLLAPSED_ROW_LIMIT } from "../../../src/constants.ts";
4
+ import { countSummary, expandHint, kindGlyph, statusGlyph, treeConnector } from "./artifact-card.ts";
5
+ import type {
6
+ ArtifactListToolDetails,
7
+ GraphToolDetails,
8
+ ToolArtifactSummary,
9
+ } from "./render-model.ts";
10
+
11
+ function pluralKind(rows: readonly ToolArtifactSummary[]): string {
12
+ const kind = rows[0]?.kind ?? "artifact";
13
+ if (kind === "task") return "tasks";
14
+ if (kind === "doc") return "documents";
15
+ if (kind === "skill") return "skills";
16
+ if (kind === "rule") return "rules";
17
+ return "artifacts";
18
+ }
19
+
20
+ function statusSummary(rows: readonly ToolArtifactSummary[]): string {
21
+ const counts = new Map<string, number>();
22
+ for (const row of rows) counts.set(row.status, (counts.get(row.status) ?? 0) + 1);
23
+ return [...counts.entries()].map(([status, count]) => `${status} ${count}`).join(" · ");
24
+ }
25
+
26
+ function rowLine(row: ToolArtifactSummary, expanded: boolean, theme: Theme): string {
27
+ const identity = expanded ? `${row.id} ` : "";
28
+ return [
29
+ theme.fg("muted", `${statusGlyph(row.status)} ${row.status}`),
30
+ theme.fg("accent", identity),
31
+ theme.fg("text", row.title),
32
+ ].join(" ");
33
+ }
34
+
35
+ function rowMetadata(row: ToolArtifactSummary): string {
36
+ return [row.subtype, ...row.labels].filter(Boolean).join(" · ");
37
+ }
38
+
39
+ /** Bounded collapsed/expanded artifact collection presentation. */
40
+ export class ArtifactListCard implements Component {
41
+ private details: ArtifactListToolDetails;
42
+ private theme: Theme;
43
+ private expanded: boolean;
44
+ private cachedWidth: number | undefined;
45
+ private cachedLines: string[] | undefined;
46
+
47
+ constructor(details: ArtifactListToolDetails, theme: Theme, expanded: boolean) {
48
+ this.details = details;
49
+ this.theme = theme;
50
+ this.expanded = expanded;
51
+ }
52
+
53
+ update(details: ArtifactListToolDetails, theme: Theme, expanded: boolean): void {
54
+ this.details = details;
55
+ this.theme = theme;
56
+ this.expanded = expanded;
57
+ this.invalidate();
58
+ }
59
+
60
+ render(width: number): string[] {
61
+ const safeWidth = Math.max(1, width);
62
+ if (this.cachedLines && this.cachedWidth === safeWidth) return this.cachedLines;
63
+ const rows = this.details.rows;
64
+ const noun = pluralKind(rows);
65
+ const lines = [truncateToWidth(
66
+ this.theme.fg("toolTitle", this.theme.bold(`${countSummary(rows.length, this.details.total)} ${noun}`)),
67
+ safeWidth,
68
+ )];
69
+ if (rows.length === 0) {
70
+ lines.push(truncateToWidth(this.theme.fg("dim", `No ${noun}.`), safeWidth));
71
+ } else {
72
+ lines.push(truncateToWidth(this.theme.fg("muted", statusSummary(rows)), safeWidth));
73
+ const display = this.expanded ? rows : rows.slice(0, TOOL_COLLAPSED_ROW_LIMIT);
74
+ for (const row of display) {
75
+ lines.push(truncateToWidth(rowLine(row, this.expanded, this.theme), safeWidth));
76
+ if (this.expanded) {
77
+ const metadata = rowMetadata(row);
78
+ if (metadata) lines.push(truncateToWidth(this.theme.fg("dim", ` ${metadata}`), safeWidth));
79
+ }
80
+ }
81
+ const omitted = Math.max(0, this.details.total - display.length);
82
+ if (omitted > 0) lines.push(truncateToWidth(this.theme.fg("dim", `${omitted} more · ${expandHint()}`), safeWidth));
83
+ }
84
+ this.cachedWidth = safeWidth;
85
+ this.cachedLines = lines;
86
+ return lines;
87
+ }
88
+
89
+ invalidate(): void {
90
+ this.cachedWidth = undefined;
91
+ this.cachedLines = undefined;
92
+ }
93
+ }
94
+
95
+ interface HierarchyRow {
96
+ node: ToolArtifactSummary;
97
+ prefix: string;
98
+ connector: string;
99
+ }
100
+
101
+ function hierarchyRows(details: GraphToolDetails): HierarchyRow[] {
102
+ const byId = new Map(details.nodes.map((node) => [node.id, node]));
103
+ const childIds = new Map<string, string[]>();
104
+ const contained = new Set<string>();
105
+ for (const edge of details.edges) {
106
+ if (edge.relation !== "contains" || !byId.has(edge.from) || !byId.has(edge.to)) continue;
107
+ const children = childIds.get(edge.from) ?? [];
108
+ children.push(edge.to);
109
+ childIds.set(edge.from, children);
110
+ contained.add(edge.to);
111
+ }
112
+ const roots = details.nodes.filter((node) => !contained.has(node.id));
113
+ const rows: HierarchyRow[] = [];
114
+ const visited = new Set<string>();
115
+ const visit = (node: ToolArtifactSummary, prefix: string, connector: string): void => {
116
+ if (visited.has(node.id)) return;
117
+ visited.add(node.id);
118
+ rows.push({ node, prefix, connector });
119
+ const children = (childIds.get(node.id) ?? []).map((id) => byId.get(id)).filter((child): child is ToolArtifactSummary => child !== undefined);
120
+ children.forEach((child, index) => {
121
+ const last = index === children.length - 1;
122
+ visit(child, `${prefix}${connector ? (connector === "└─" ? " " : "│ ") : ""}`, treeConnector(last));
123
+ });
124
+ };
125
+ for (const root of roots) visit(root, "", "");
126
+ for (const node of details.nodes) visit(node, "", "");
127
+ return rows;
128
+ }
129
+
130
+ /** Bounded task containment preview; dependency graphs use the dedicated graph renderer. */
131
+ export class TaskHierarchyPreview implements Component {
132
+ private details: GraphToolDetails;
133
+ private theme: Theme;
134
+ private expanded: boolean;
135
+ private cachedWidth: number | undefined;
136
+ private cachedLines: string[] | undefined;
137
+
138
+ constructor(details: GraphToolDetails, theme: Theme, expanded: boolean) {
139
+ this.details = details;
140
+ this.theme = theme;
141
+ this.expanded = expanded;
142
+ }
143
+
144
+ update(details: GraphToolDetails, theme: Theme, expanded: boolean): void {
145
+ this.details = details;
146
+ this.theme = theme;
147
+ this.expanded = expanded;
148
+ this.invalidate();
149
+ }
150
+
151
+ render(width: number): string[] {
152
+ const safeWidth = Math.max(1, width);
153
+ if (this.cachedLines && this.cachedWidth === safeWidth) return this.cachedLines;
154
+ const rows = hierarchyRows(this.details);
155
+ const lines = [truncateToWidth(
156
+ this.theme.fg("toolTitle", this.theme.bold(`${this.details.nodes.length} tasks · ${this.details.edges.length} edges`)),
157
+ safeWidth,
158
+ )];
159
+ for (const row of rows) {
160
+ const identity = this.expanded ? `${row.node.id} ` : "";
161
+ lines.push(truncateToWidth(
162
+ `${row.prefix}${row.connector}${row.connector ? " " : ""}${this.theme.fg("accent", kindGlyph(row.node.kind))} ${this.theme.fg("muted", statusGlyph(row.node.status))} ${this.theme.fg("accent", identity)}${this.theme.fg("text", row.node.title)}`,
163
+ safeWidth,
164
+ ));
165
+ if (this.expanded) {
166
+ const metadata = rowMetadata(row.node);
167
+ if (metadata) lines.push(truncateToWidth(this.theme.fg("dim", `${row.prefix} ${metadata}`), safeWidth));
168
+ }
169
+ }
170
+ this.cachedWidth = safeWidth;
171
+ this.cachedLines = lines;
172
+ return lines;
173
+ }
174
+
175
+ invalidate(): void {
176
+ this.cachedWidth = undefined;
177
+ this.cachedLines = undefined;
178
+ }
179
+ }
@@ -0,0 +1,107 @@
1
+ import type {
2
+ AgentToolResult,
3
+ Theme,
4
+ ToolRenderResultOptions,
5
+ } from "@earendil-works/pi-coding-agent";
6
+ import { type Component, Text } from "@earendil-works/pi-tui";
7
+ import { ArtifactCard } from "./artifact-card.ts";
8
+ import { ArtifactListCard, TaskHierarchyPreview } from "./artifact-list.ts";
9
+ import { parsePapyrusToolDetails, type PapyrusToolDetails } from "./render-model.ts";
10
+
11
+ const CALL_VALUE_MAX_CHARACTERS = 80;
12
+
13
+ export interface PapyrusToolRenderContext {
14
+ lastComponent: Component | undefined;
15
+ isError: boolean;
16
+ }
17
+
18
+ function primaryArgument(args: Record<string, unknown>): string | undefined {
19
+ for (const key of ["id", "title", "text", "query", "kind", "template_id"]) {
20
+ const value = args[key];
21
+ if (typeof value === "string" && value.trim()) return value.slice(0, CALL_VALUE_MAX_CHARACTERS);
22
+ }
23
+ return undefined;
24
+ }
25
+
26
+ /** Compact native call header that never echoes bodies or structured payloads. */
27
+ export function renderPapyrusToolCall(label: string, args: Record<string, unknown>, theme: Theme): Component {
28
+ const action = typeof args.action === "string" ? args.action : "call";
29
+ const primary = primaryArgument(args);
30
+ const text = [
31
+ theme.fg("toolTitle", theme.bold(label)),
32
+ theme.fg("muted", action),
33
+ ...(primary ? [theme.fg("accent", primary)] : []),
34
+ ].join(" ");
35
+ return new Text(text, 0, 0);
36
+ }
37
+
38
+ function textContent(result: AgentToolResult<unknown>): string {
39
+ return result.content
40
+ .filter((entry): entry is { type: "text"; text: string } => entry.type === "text")
41
+ .map((entry) => entry.text)
42
+ .join("\n");
43
+ }
44
+
45
+ function simpleDetailsText(details: Exclude<PapyrusToolDetails, { kind: "artifact" | "artifact-list" | "graph" }>): string {
46
+ switch (details.kind) {
47
+ case "transition":
48
+ return `✓ ${details.artifact.id} ${details.fromStatus} → ${details.toStatus}\n${details.artifact.title}`;
49
+ case "gate-run": {
50
+ const passed = details.gates.filter((gate) => gate.passed).length;
51
+ return [
52
+ `${passed}/${details.gates.length} gates passed for ${details.artifactId}`,
53
+ ...details.gates.map((gate) => `${gate.passed ? "✓" : "✗"} ${gate.type}: ${gate.target}${gate.output ? ` — ${gate.output}` : ""}`),
54
+ ].join("\n");
55
+ }
56
+ case "invocation":
57
+ return [
58
+ `✓ Run ${details.runId}`,
59
+ `${details.created.tasks.length} tasks · ${details.created.docs.length} docs · ${details.created.rules.length} rules`,
60
+ ...(details.created.roots.length ? [`Roots: ${details.created.roots.join(", ")}`] : []),
61
+ ].join("\n");
62
+ case "preview":
63
+ return `${details.title}\n${details.content}${details.completeness.truncated ? `\n[truncated ${details.completeness.omitted} characters]` : ""}`;
64
+ case "error":
65
+ return `${details.code}: ${details.message}`;
66
+ }
67
+ }
68
+
69
+ /** Render structured details for humans while preserving compact model content as fallback. */
70
+ export function renderPapyrusToolResult(
71
+ result: AgentToolResult<unknown>,
72
+ options: ToolRenderResultOptions,
73
+ theme: Theme,
74
+ context: PapyrusToolRenderContext,
75
+ ): Component {
76
+ if (options.isPartial) return new Text(theme.fg("warning", "Working…"), 0, 0);
77
+ const details = parsePapyrusToolDetails(result.details);
78
+ if (!details) return new Text(theme.fg("toolOutput", textContent(result)), 0, 0);
79
+
80
+ if (details.kind === "artifact") {
81
+ const previous = context.lastComponent instanceof ArtifactCard ? context.lastComponent : undefined;
82
+ if (previous) {
83
+ previous.update(details, theme, options.expanded);
84
+ return previous;
85
+ }
86
+ return new ArtifactCard(details, theme, options.expanded);
87
+ }
88
+ if (details.kind === "artifact-list") {
89
+ const previous = context.lastComponent instanceof ArtifactListCard ? context.lastComponent : undefined;
90
+ if (previous) {
91
+ previous.update(details, theme, options.expanded);
92
+ return previous;
93
+ }
94
+ return new ArtifactListCard(details, theme, options.expanded);
95
+ }
96
+ if (details.kind === "graph") {
97
+ const previous = context.lastComponent instanceof TaskHierarchyPreview ? context.lastComponent : undefined;
98
+ if (previous) {
99
+ previous.update(details, theme, options.expanded);
100
+ return previous;
101
+ }
102
+ return new TaskHierarchyPreview(details, theme, options.expanded);
103
+ }
104
+
105
+ const color = details.kind === "error" || context.isError ? "error" : "toolOutput";
106
+ return new Text(theme.fg(color, simpleDetailsText(details)), 0, 0);
107
+ }