@danypops/papyrus 0.2.1 → 0.4.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 +49 -14
- package/extension/src/active-task-continuation.ts +12 -18
- package/extension/src/artifact-format.ts +5 -3
- package/extension/src/beautiful-mermaid-renderer.ts +23 -0
- package/extension/src/domain-tools.ts +41 -7
- package/extension/src/index.ts +21 -36
- package/extension/src/skills.ts +61 -2
- package/extension/src/task-detail-format.ts +5 -3
- package/extension/src/task-graph.ts +22 -3
- package/extension/src/task-presentation.ts +26 -0
- package/extension/src/task-widget.ts +27 -20
- package/extension/src/tasks.ts +68 -31
- package/package.json +1 -1
- package/src/adapters/sqlite-artifact-store.ts +7 -2
- package/src/adapters/sqlite-task-focus-store.ts +31 -0
- package/src/cli.ts +119 -6
- package/src/client.ts +2 -2
- package/src/constants.ts +22 -10
- package/src/db.ts +98 -22
- package/src/domain/skill-definition.ts +15 -11
- package/src/domain-services.ts +31 -0
- package/src/ports/atomic-artifact-store.ts +13 -0
- package/src/ports/task-focus-store.ts +21 -0
- package/src/service.ts +53 -12
- package/src/skill-execution.ts +204 -0
- package/src/task-context.ts +10 -9
- package/src/task-execution.ts +16 -3
- package/src/task-graph-view.ts +6 -3
- package/src/task-service.ts +110 -27
|
@@ -1,49 +1,56 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TASK_WIDGET_OPEN_LIMIT } from "../../src/constants.ts";
|
|
2
2
|
import type { Artifact } from "../../src/domain/artifact.ts";
|
|
3
3
|
import type { TaskGraph } from "../../src/task-service.ts";
|
|
4
4
|
|
|
5
5
|
export interface TaskWidgetRow {
|
|
6
6
|
task: Artifact;
|
|
7
7
|
depth: number;
|
|
8
|
-
|
|
8
|
+
hasOpenChildren: boolean;
|
|
9
|
+
active: boolean;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export interface TaskWidgetProjection {
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
rows: TaskWidgetRow[];
|
|
14
|
+
openTotal: number;
|
|
14
15
|
total: number;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
function isOpen(task: Artifact): boolean {
|
|
19
|
+
return task.status !== "done" && task.status !== "canceled";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Keep bounded actionable work in containment order while always retaining active focus. */
|
|
18
23
|
export function buildTaskWidgetProjection(
|
|
19
24
|
graph: TaskGraph,
|
|
20
|
-
|
|
25
|
+
openLimit = TASK_WIDGET_OPEN_LIMIT,
|
|
21
26
|
): TaskWidgetProjection {
|
|
22
|
-
const
|
|
23
|
-
const byId = new Map(visibleNodes.map((node) => [node.task.id, node]));
|
|
27
|
+
const byId = new Map(graph.nodes.map((node) => [node.task.id, node]));
|
|
24
28
|
const visited = new Set<string>();
|
|
25
29
|
const ordered: TaskWidgetRow[] = [];
|
|
26
30
|
|
|
27
|
-
const visit = (id: string,
|
|
31
|
+
const visit = (id: string, openDepth: number): void => {
|
|
28
32
|
if (visited.has(id)) return;
|
|
29
33
|
const node = byId.get(id);
|
|
30
34
|
if (!node) return;
|
|
31
35
|
visited.add(id);
|
|
32
|
-
const
|
|
33
|
-
if (
|
|
34
|
-
const childDepth =
|
|
36
|
+
const open = isOpen(node.task);
|
|
37
|
+
if (open) ordered.push({ task: node.task, depth: openDepth, hasOpenChildren: false, active: node.active === true });
|
|
38
|
+
const childDepth = open ? openDepth + 1 : openDepth;
|
|
35
39
|
for (const childId of node.childIds) visit(childId, childDepth);
|
|
36
40
|
};
|
|
37
41
|
|
|
38
42
|
for (const rootId of graph.rootIds) visit(rootId, 0);
|
|
39
|
-
for (const node of
|
|
43
|
+
for (const node of graph.nodes) visit(node.task.id, 0);
|
|
40
44
|
for (let index = 0; index < ordered.length - 1; index++) {
|
|
41
|
-
ordered[index]!.
|
|
45
|
+
ordered[index]!.hasOpenChildren = ordered[index + 1]!.depth > ordered[index]!.depth;
|
|
42
46
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
|
|
48
|
+
const limit = Math.max(0, openLimit);
|
|
49
|
+
let rows = ordered.slice(0, limit);
|
|
50
|
+
const active = ordered.find((row) => row.active);
|
|
51
|
+
if (active && !rows.some((row) => row.task.id === active.task.id) && limit > 0) {
|
|
52
|
+
rows = [...rows.slice(0, Math.max(0, limit - 1)), active]
|
|
53
|
+
.sort((left, right) => ordered.indexOf(left) - ordered.indexOf(right));
|
|
54
|
+
}
|
|
55
|
+
return { rows, openTotal: ordered.length, total: graph.nodes.length };
|
|
49
56
|
}
|
package/extension/src/tasks.ts
CHANGED
|
@@ -15,23 +15,16 @@ export { showTaskDetails } from "./task-detail-view.ts";
|
|
|
15
15
|
import type { Artifact } from "../../src/domain/artifact.ts";
|
|
16
16
|
import type { GateResult } from "../../src/domain/gate.ts";
|
|
17
17
|
import { projectTaskExecution } from "../../src/task-execution.ts";
|
|
18
|
-
import type { TaskCompletion, TaskGraph } from "../../src/task-service.ts";
|
|
19
|
-
|
|
20
|
-
const GLYPHS: Record<string, string> = {
|
|
21
|
-
pending: "○",
|
|
22
|
-
ready: "◇",
|
|
23
|
-
blocked: "○",
|
|
24
|
-
active: "●",
|
|
25
|
-
done: "■",
|
|
26
|
-
failed: "▲",
|
|
27
|
-
invalid: "!",
|
|
28
|
-
};
|
|
18
|
+
import type { TaskCompletion, TaskGraph, TaskStatus } from "../../src/task-service.ts";
|
|
19
|
+
import { TASK_STATUS_PRESENTATION, taskTreeConnector } from "./task-presentation.ts";
|
|
29
20
|
|
|
30
21
|
const STATUS_ACTIONS: Record<string, string[]> = {
|
|
31
|
-
|
|
32
|
-
|
|
22
|
+
todo: ["Start", "Cancel"],
|
|
23
|
+
"in-progress": ["Submit for review", "Cancel"],
|
|
24
|
+
review: ["Complete review", "Reject", "Cancel"],
|
|
25
|
+
rejected: ["Retry", "Cancel"],
|
|
33
26
|
done: [],
|
|
34
|
-
|
|
27
|
+
canceled: [],
|
|
35
28
|
};
|
|
36
29
|
|
|
37
30
|
type TaskRow = Artifact;
|
|
@@ -41,6 +34,7 @@ export interface TaskHierarchyRow {
|
|
|
41
34
|
depth: number;
|
|
42
35
|
childCount: number;
|
|
43
36
|
dependencies: string[];
|
|
37
|
+
active: boolean;
|
|
44
38
|
}
|
|
45
39
|
|
|
46
40
|
export function buildTaskHierarchy(graph: TaskGraph): TaskHierarchyRow[] {
|
|
@@ -53,7 +47,7 @@ export function buildTaskHierarchy(graph: TaskGraph): TaskHierarchyRow[] {
|
|
|
53
47
|
if (!node) return;
|
|
54
48
|
visited.add(id);
|
|
55
49
|
const children = node.childIds.filter((childId) => byId.has(childId));
|
|
56
|
-
result.push({ task: node.task, depth, childCount: children.length, dependencies: [...node.dependencyIds] });
|
|
50
|
+
result.push({ task: node.task, depth, childCount: children.length, dependencies: [...node.dependencyIds], active: node.active === true });
|
|
57
51
|
for (const childId of children) visit(childId, depth + 1);
|
|
58
52
|
};
|
|
59
53
|
for (const rootId of graph.rootIds) visit(rootId, 0);
|
|
@@ -90,7 +84,13 @@ export async function showTasks(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
90
84
|
if (action.type === "graph") { await showTaskGraph(ctx, graph); continue; }
|
|
91
85
|
if (action.type !== "action" || !action.row) continue;
|
|
92
86
|
|
|
93
|
-
const
|
|
87
|
+
const active = graph.nodes.find((node) => node.task.id === action.row!.id)?.active === true;
|
|
88
|
+
const choices = [
|
|
89
|
+
"Show details",
|
|
90
|
+
...(!active && action.row.status !== "done" && action.row.status !== "canceled" ? ["Make active"] : []),
|
|
91
|
+
...(action.row.status === "review" ? ["Run gates"] : []),
|
|
92
|
+
...(STATUS_ACTIONS[action.row.status] ?? []),
|
|
93
|
+
];
|
|
94
94
|
const choice = await ctx.ui.select(action.row.title, choices);
|
|
95
95
|
if (!choice) continue;
|
|
96
96
|
|
|
@@ -98,6 +98,13 @@ export async function showTasks(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
98
98
|
const art = await callService<Record<string, unknown>, Artifact | null>("tasks.show", { id: action.row.id });
|
|
99
99
|
if (!art) { ctx.ui.notify("Not found", "error"); continue; }
|
|
100
100
|
await showTaskDetails(ctx, art, graph);
|
|
101
|
+
} else if (choice === "Make active") {
|
|
102
|
+
try {
|
|
103
|
+
await callService<Record<string, unknown>, Artifact>("tasks.focus", { id: action.row.id });
|
|
104
|
+
ctx.ui.notify(`Active: ${action.row.title}`, "info");
|
|
105
|
+
} catch (error) {
|
|
106
|
+
ctx.ui.notify(`Focus failed: ${error instanceof Error ? error.message : error}`, "error");
|
|
107
|
+
}
|
|
101
108
|
} else if (choice === "Run gates") {
|
|
102
109
|
try {
|
|
103
110
|
const results = await callService<Record<string, unknown>, GateResult[]>("tasks.run_gates", { id: action.row.id });
|
|
@@ -107,19 +114,30 @@ export async function showTasks(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
107
114
|
}
|
|
108
115
|
} else {
|
|
109
116
|
try {
|
|
110
|
-
const operation = choice === "Start"
|
|
117
|
+
const operation = choice === "Start"
|
|
118
|
+
? "tasks.start"
|
|
119
|
+
: choice === "Submit for review"
|
|
120
|
+
? "tasks.submit"
|
|
121
|
+
: choice === "Reject"
|
|
122
|
+
? "tasks.reject"
|
|
123
|
+
: choice === "Retry"
|
|
124
|
+
? "tasks.retry"
|
|
125
|
+
: choice === "Cancel"
|
|
126
|
+
? "tasks.cancel"
|
|
127
|
+
: "tasks.complete";
|
|
111
128
|
if (operation === "tasks.complete") {
|
|
112
129
|
const result = await callService<Record<string, unknown>, TaskCompletion>(operation, { id: action.row.id });
|
|
113
130
|
action.row.status = result.artifact.status;
|
|
114
131
|
const gates = result.gates.map((gate) => `${gate.passed ? "✓" : "✗"} ${gate.gate.type}: ${gate.gate.target}`).join("\n");
|
|
115
|
-
const
|
|
132
|
+
const checklist = result.checklist.map((item) => `${item.accepted ? "✓" : "✗"} proof: ${item.item}`).join("\n");
|
|
133
|
+
const focused = result.focused ? `\nActive: ${result.focused.title}` : "";
|
|
116
134
|
const blocked = result.blocked.length > 0
|
|
117
135
|
? `\nWaiting: ${result.blocked.map((entry) => `${entry.artifact.title} needs ${entry.dependencyIds.join(", ")}`).join("; ")}`
|
|
118
136
|
: "";
|
|
119
137
|
ctx.ui.notify(
|
|
120
138
|
result.completed
|
|
121
|
-
? `Completed ${result.artifact.id}${
|
|
122
|
-
: `
|
|
139
|
+
? `Completed ${result.artifact.id}${focused}${blocked}${checklist ? `\n${checklist}` : ""}${gates ? `\n${gates}` : ""}`
|
|
140
|
+
: `Review rejected${checklist ? `\n${checklist}` : ""}${gates ? `\n${gates}` : ""}`,
|
|
123
141
|
result.completed ? "info" : "warning",
|
|
124
142
|
);
|
|
125
143
|
} else {
|
|
@@ -162,11 +180,15 @@ function renderPanel(ctx: ExtensionCommandContext, graph: TaskGraph): Promise<Pa
|
|
|
162
180
|
|
|
163
181
|
function statusLine(): string {
|
|
164
182
|
const counts: Record<string, number> = {};
|
|
165
|
-
for (const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
183
|
+
for (const entry of hierarchy) counts[entry.task.status] = (counts[entry.task.status] ?? 0) + 1;
|
|
184
|
+
const parts = hierarchy.some((entry) => entry.active) ? ["▶ 1 active"] : [];
|
|
185
|
+
for (const status of ["todo", "in-progress", "review", "rejected", "done", "canceled"] as TaskStatus[]) {
|
|
186
|
+
if ((counts[status] ?? 0) > 0) {
|
|
187
|
+
const presentation = TASK_STATUS_PRESENTATION[status];
|
|
188
|
+
parts.push(`${presentation.glyph} ${counts[status]} ${presentation.label}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return parts.join(", ");
|
|
170
192
|
}
|
|
171
193
|
|
|
172
194
|
const header = {
|
|
@@ -210,14 +232,29 @@ function renderPanel(ctx: ExtensionCommandContext, graph: TaskGraph): Promise<Pa
|
|
|
210
232
|
const row = entry.task;
|
|
211
233
|
const selected = i === selectedIndex;
|
|
212
234
|
const cursor = selected ? theme.fg("accent", "❯") : " ";
|
|
235
|
+
const focus = entry.active ? theme.fg("accent", "▶") : " ";
|
|
213
236
|
const execution = executionById.get(row.id);
|
|
214
237
|
const state = execution?.state ?? row.status;
|
|
215
|
-
const
|
|
216
|
-
const
|
|
217
|
-
|
|
238
|
+
const presentation = TASK_STATUS_PRESENTATION[row.status as TaskStatus];
|
|
239
|
+
const glyphStyled = state === "invalid"
|
|
240
|
+
? theme.fg("error", "!")
|
|
241
|
+
: presentation
|
|
242
|
+
? theme.fg(presentation.color, presentation.glyph)
|
|
243
|
+
: theme.fg("muted", "?");
|
|
218
244
|
const title = selected ? theme.bold(row.title) : row.title;
|
|
219
|
-
|
|
220
|
-
|
|
245
|
+
let laterSibling = false;
|
|
246
|
+
for (let candidate = i + 1; candidate < filtered.length; candidate++) {
|
|
247
|
+
if (filtered[candidate]!.depth < entry.depth) break;
|
|
248
|
+
if (filtered[candidate]!.depth === entry.depth) { laterSibling = true; break; }
|
|
249
|
+
}
|
|
250
|
+
const connector = taskTreeConnector({
|
|
251
|
+
depth: entry.depth,
|
|
252
|
+
hasChildren: entry.childCount > 0,
|
|
253
|
+
hasLaterSibling: laterSibling,
|
|
254
|
+
});
|
|
255
|
+
const node = entry.depth === 0 && entry.childCount > 0
|
|
256
|
+
? theme.fg("accent", connector)
|
|
257
|
+
: theme.fg("dim", connector);
|
|
221
258
|
const gates = (row.extra?.["gates"] as any[])?.length;
|
|
222
259
|
const relationParts: string[] = [];
|
|
223
260
|
if (execution) relationParts.push(execution.layer === null ? state : `layer ${execution.layer + 1} · ${state}`);
|
|
@@ -228,7 +265,7 @@ function renderPanel(ctx: ExtensionCommandContext, graph: TaskGraph): Promise<Pa
|
|
|
228
265
|
}
|
|
229
266
|
if (gates) relationParts.push(`${gates} gate${gates === 1 ? "" : "s"}`);
|
|
230
267
|
const relationText = relationParts.length > 0 ? theme.fg("dim", ` · ${relationParts.join(" · ")}`) : "";
|
|
231
|
-
lines.push(truncateToWidth(`${cursor}
|
|
268
|
+
lines.push(truncateToWidth(`${cursor}${focus} ${node} ${glyphStyled} ${title}${relationText}`, width, ""));
|
|
232
269
|
}
|
|
233
270
|
const hasScroll = start > 0 || end < filtered.length;
|
|
234
271
|
lines.push(theme.fg("muted", ` ${hasScroll ? `${selectedIndex + 1}/${filtered.length} · ` : ""}↑/↓ navigate · Enter actions`));
|
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Db } from "../db.ts";
|
|
2
|
-
import
|
|
2
|
+
import { inTransaction } from "../db.ts";
|
|
3
|
+
import type { AtomicArtifactStore } from "../ports/atomic-artifact-store.ts";
|
|
3
4
|
import type {
|
|
4
5
|
Artifact,
|
|
5
6
|
ArtifactEdge,
|
|
@@ -11,9 +12,13 @@ import type {
|
|
|
11
12
|
} from "../domain/artifact.ts";
|
|
12
13
|
import { createArtifact, getArtifact, linkArtifacts, queryArtifacts, updateExtra, updateStatus } from "../ops.ts";
|
|
13
14
|
|
|
14
|
-
export class SQLiteArtifactStore implements
|
|
15
|
+
export class SQLiteArtifactStore implements AtomicArtifactStore {
|
|
15
16
|
constructor(private readonly db: Db) {}
|
|
16
17
|
|
|
18
|
+
atomic<T>(operation: () => T): T {
|
|
19
|
+
return inTransaction(this.db, operation);
|
|
20
|
+
}
|
|
21
|
+
|
|
17
22
|
create(input: CreateArtifactInput): Artifact {
|
|
18
23
|
return createArtifact(this.db, input);
|
|
19
24
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Db } from "../db.ts";
|
|
2
|
+
import { inTransaction } from "../db.ts";
|
|
3
|
+
import type { TaskFocusStore } from "../ports/task-focus-store.ts";
|
|
4
|
+
|
|
5
|
+
export class SQLiteTaskFocusStore implements TaskFocusStore {
|
|
6
|
+
constructor(private readonly db: Db) {}
|
|
7
|
+
|
|
8
|
+
get(): string | undefined {
|
|
9
|
+
const row = this.db.prepare("SELECT task_id FROM task_focus WHERE scope = 'global'").get() as
|
|
10
|
+
| { task_id: string }
|
|
11
|
+
| null;
|
|
12
|
+
return row?.task_id;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
set(taskId: string): void {
|
|
16
|
+
inTransaction(this.db, () => {
|
|
17
|
+
this.db.prepare(`
|
|
18
|
+
INSERT INTO task_focus (scope, task_id, updated_at)
|
|
19
|
+
VALUES ('global', ?, ?)
|
|
20
|
+
ON CONFLICT(scope) DO UPDATE SET task_id = excluded.task_id, updated_at = excluded.updated_at
|
|
21
|
+
`).run(taskId, new Date().toISOString());
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
clear(taskId?: string): void {
|
|
26
|
+
inTransaction(this.db, () => {
|
|
27
|
+
if (taskId === undefined) this.db.prepare("DELETE FROM task_focus WHERE scope = 'global'").run();
|
|
28
|
+
else this.db.prepare("DELETE FROM task_focus WHERE scope = 'global' AND task_id = ?").run(taskId);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { homedir } from "node:os";
|
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { connectPapyrusClient, type PapyrusClient } from "./client.ts";
|
|
8
|
-
import { DAEMON_UNIT_NAME } from "./constants.ts";
|
|
8
|
+
import { DAEMON_UNIT_NAME, TASK_EXECUTION_MAX_NODES } from "./constants.ts";
|
|
9
9
|
import { serveMain } from "./daemon.ts";
|
|
10
10
|
import type { GateResult } from "./domain/gate.ts";
|
|
11
11
|
import type { TaskExecutionPlan } from "./task-execution.ts";
|
|
@@ -56,9 +56,18 @@ function installService(): void {
|
|
|
56
56
|
const USAGE = `Usage:
|
|
57
57
|
papyrus serve
|
|
58
58
|
papyrus service <install|start|stop|restart|status>
|
|
59
|
+
papyrus migrate task-lifecycle [--json]
|
|
60
|
+
papyrus skills run <id> [--arguments-json <json>] [--run-id <id>] [--json]
|
|
59
61
|
papyrus tasks plan [--json]
|
|
62
|
+
papyrus tasks graph [--json]
|
|
63
|
+
papyrus tasks active [--json]
|
|
64
|
+
papyrus tasks focus <id> [--json]
|
|
60
65
|
papyrus tasks complete <id> [--json]
|
|
61
66
|
papyrus tasks start <id> [--json]
|
|
67
|
+
papyrus tasks submit <id> [--json]
|
|
68
|
+
papyrus tasks reject <id> [--json]
|
|
69
|
+
papyrus tasks retry <id> [--json]
|
|
70
|
+
papyrus tasks cancel <id> [--json]
|
|
62
71
|
papyrus tasks depend <id> <prerequisite-id> [--json]`;
|
|
63
72
|
|
|
64
73
|
function usage(): never {
|
|
@@ -67,10 +76,10 @@ function usage(): never {
|
|
|
67
76
|
}
|
|
68
77
|
|
|
69
78
|
type TaskCliClient = Pick<PapyrusClient, "call">;
|
|
79
|
+
type MigrationResult = { from: number; to: number; applied: string[] };
|
|
70
80
|
type CliArtifact = { id: string; title: string; status: string };
|
|
71
|
-
type CliCompletion = Omit<TaskCompletion, "artifact" | "
|
|
81
|
+
type CliCompletion = Omit<TaskCompletion, "artifact" | "blocked"> & {
|
|
72
82
|
artifact: CliArtifact;
|
|
73
|
-
started: CliArtifact[];
|
|
74
83
|
blocked: Array<Omit<TaskBlockage, "artifact"> & { artifact: CliArtifact }>;
|
|
75
84
|
gates: GateResult[];
|
|
76
85
|
};
|
|
@@ -94,6 +103,63 @@ function planText(plan: TaskExecutionPlan): string {
|
|
|
94
103
|
return lines.join("\n");
|
|
95
104
|
}
|
|
96
105
|
|
|
106
|
+
export async function runMigrationCli(args: string[], client: TaskCliClient): Promise<string> {
|
|
107
|
+
const json = args.includes("--json");
|
|
108
|
+
const positional = args.filter((arg) => arg !== "--json");
|
|
109
|
+
if (positional.length !== 1 || positional[0] !== "task-lifecycle") {
|
|
110
|
+
throw new Error("migrate requires exactly `task-lifecycle`");
|
|
111
|
+
}
|
|
112
|
+
const result = await client.call<Record<string, never>, MigrationResult>("system.migrate", {});
|
|
113
|
+
if (json) return JSON.stringify(result);
|
|
114
|
+
if (result.applied.length === 0) return `Schema already current at version ${result.to}.`;
|
|
115
|
+
return `Migrated schema ${result.from} → ${result.to}: ${result.applied.join(", ")}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function runSkillCli(args: string[], client: TaskCliClient): Promise<string> {
|
|
119
|
+
const json = args.includes("--json");
|
|
120
|
+
const positional: string[] = [];
|
|
121
|
+
let runId: string | undefined;
|
|
122
|
+
let arguments_: Record<string, unknown> = {};
|
|
123
|
+
for (let index = 0; index < args.length; index++) {
|
|
124
|
+
const argument = args[index]!;
|
|
125
|
+
if (argument === "--json") continue;
|
|
126
|
+
if (argument === "--run-id") {
|
|
127
|
+
runId = args[++index];
|
|
128
|
+
if (!runId) throw new Error("--run-id requires a value");
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (argument === "--arguments-json") {
|
|
132
|
+
const source = args[++index];
|
|
133
|
+
if (!source) throw new Error("--arguments-json requires a JSON object");
|
|
134
|
+
const parsed = JSON.parse(source) as unknown;
|
|
135
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
136
|
+
throw new Error("--arguments-json must be a JSON object");
|
|
137
|
+
}
|
|
138
|
+
arguments_ = parsed as Record<string, unknown>;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (argument.startsWith("--")) throw new Error(`unknown skills option ${argument}`);
|
|
142
|
+
positional.push(argument);
|
|
143
|
+
}
|
|
144
|
+
if (positional.length !== 2 || positional[0] !== "run") throw new Error("skills requires `run <id>`");
|
|
145
|
+
const input: Record<string, unknown> = { id: positional[1], arguments: arguments_ };
|
|
146
|
+
if (runId) input["run_id"] = runId;
|
|
147
|
+
const result = await client.call<Record<string, unknown>, {
|
|
148
|
+
runId: string;
|
|
149
|
+
created: { tasks: string[]; rules: string[]; docs: string[] };
|
|
150
|
+
rootTaskIds: string[];
|
|
151
|
+
execution: TaskExecutionPlan;
|
|
152
|
+
}>("skills.run", input);
|
|
153
|
+
if (json) return JSON.stringify(result);
|
|
154
|
+
return [
|
|
155
|
+
`Created Skill run ${result.runId}: ${result.created.tasks.length} tasks, ${result.created.rules.length} rules, ${result.created.docs.length} docs`,
|
|
156
|
+
`Ready roots: ${result.rootTaskIds.join(", ") || "none"}`,
|
|
157
|
+
`Context docs: ${result.created.docs.join(", ") || "none"}`,
|
|
158
|
+
`Scoped rules: ${result.created.rules.join(", ") || "none"}`,
|
|
159
|
+
...result.execution.nodes.map((node) => `[${node.state}] ${node.id} ${node.title}`),
|
|
160
|
+
].join("\n");
|
|
161
|
+
}
|
|
162
|
+
|
|
97
163
|
export async function runTaskCli(args: string[], client: TaskCliClient): Promise<string> {
|
|
98
164
|
const json = args.includes("--json");
|
|
99
165
|
const positional = args.filter((arg) => arg !== "--json");
|
|
@@ -101,6 +167,32 @@ export async function runTaskCli(args: string[], client: TaskCliClient): Promise
|
|
|
101
167
|
let result: unknown;
|
|
102
168
|
let human: string;
|
|
103
169
|
switch (action) {
|
|
170
|
+
case "active": {
|
|
171
|
+
if (id) throw new Error("tasks active accepts no positional arguments");
|
|
172
|
+
const active = await client.call<Record<string, never>, CliArtifact | null>("tasks.active", {});
|
|
173
|
+
result = active;
|
|
174
|
+
human = active ? `Active: ${artifactLabel(active)}` : "No active task.";
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
case "focus": {
|
|
178
|
+
if (!id || dependencyId) throw new Error("tasks focus requires exactly one task id");
|
|
179
|
+
const active = await client.call<{ id: string }, CliArtifact>("tasks.focus", { id });
|
|
180
|
+
result = active;
|
|
181
|
+
human = `Active: ${artifactLabel(active)}`;
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
case "graph": {
|
|
185
|
+
if (id) throw new Error("tasks graph accepts no positional arguments");
|
|
186
|
+
const graph = await client.call<{ limit: number }, {
|
|
187
|
+
nodes: Array<{ dependencyIds: string[]; childIds: string[] }>;
|
|
188
|
+
rootIds: string[];
|
|
189
|
+
}>("tasks.graph", { limit: TASK_EXECUTION_MAX_NODES + 1 });
|
|
190
|
+
result = graph;
|
|
191
|
+
const dependencies = graph.nodes.reduce((count, node) => count + node.dependencyIds.length, 0);
|
|
192
|
+
const children = graph.nodes.reduce((count, node) => count + node.childIds.length, 0);
|
|
193
|
+
human = `Task graph: ${graph.nodes.length} nodes, ${graph.rootIds.length} roots, ${dependencies} dependencies, ${children} containment edges`;
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
104
196
|
case "plan": {
|
|
105
197
|
if (id) throw new Error("tasks plan accepts no positional arguments");
|
|
106
198
|
const plan = await client.call<Record<string, never>, TaskExecutionPlan>("tasks.plan", {});
|
|
@@ -112,8 +204,8 @@ export async function runTaskCli(args: string[], client: TaskCliClient): Promise
|
|
|
112
204
|
if (!id || dependencyId) throw new Error("tasks complete requires exactly one task id");
|
|
113
205
|
const completion = await client.call<{ id: string }, CliCompletion>("tasks.complete", { id });
|
|
114
206
|
result = completion;
|
|
115
|
-
const lines = [`${completion.completed ? "Completed" : "
|
|
116
|
-
if (completion.
|
|
207
|
+
const lines = [`${completion.completed ? "Completed" : "Rejected"}: ${artifactLabel(completion.artifact)}`];
|
|
208
|
+
if (completion.focused) lines.push(`Active: ${artifactLabel(completion.focused)}`);
|
|
117
209
|
if (completion.blocked.length > 0) {
|
|
118
210
|
lines.push(`Blocked: ${completion.blocked.map((entry) => `${artifactLabel(entry.artifact)} waits for ${entry.dependencyIds.join(", ")}`).join("; ")}`);
|
|
119
211
|
}
|
|
@@ -128,6 +220,17 @@ export async function runTaskCli(args: string[], client: TaskCliClient): Promise
|
|
|
128
220
|
human = `Started: ${artifactLabel(artifact)}`;
|
|
129
221
|
break;
|
|
130
222
|
}
|
|
223
|
+
case "submit":
|
|
224
|
+
case "reject":
|
|
225
|
+
case "retry":
|
|
226
|
+
case "cancel": {
|
|
227
|
+
if (!id || dependencyId) throw new Error(`tasks ${action} requires exactly one task id`);
|
|
228
|
+
const operation = `tasks.${action}` as "tasks.submit" | "tasks.reject" | "tasks.retry" | "tasks.cancel";
|
|
229
|
+
const artifact = await client.call<{ id: string }, CliArtifact>(operation, { id });
|
|
230
|
+
result = artifact;
|
|
231
|
+
human = `${action[0]!.toUpperCase()}${action.slice(1)}: ${artifactLabel(artifact)}`;
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
131
234
|
case "depend": {
|
|
132
235
|
if (!id || !dependencyId || positional.length !== 3) throw new Error("tasks depend requires a task id and prerequisite id");
|
|
133
236
|
const artifact = await client.call<{ id: string; dependency_id: string }, CliArtifact>("tasks.depend", {
|
|
@@ -139,7 +242,7 @@ export async function runTaskCli(args: string[], client: TaskCliClient): Promise
|
|
|
139
242
|
break;
|
|
140
243
|
}
|
|
141
244
|
default:
|
|
142
|
-
throw new Error("tasks action must be plan, complete, start, or depend");
|
|
245
|
+
throw new Error("tasks action must be active, focus, graph, plan, complete, start, submit, reject, retry, cancel, or depend");
|
|
143
246
|
}
|
|
144
247
|
return json ? JSON.stringify(result) : human;
|
|
145
248
|
}
|
|
@@ -152,6 +255,16 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
|
|
|
152
255
|
console.log(await runTaskCli(args.slice(1), client));
|
|
153
256
|
return;
|
|
154
257
|
}
|
|
258
|
+
if (command === "skills") {
|
|
259
|
+
const client = await connectPapyrusClient();
|
|
260
|
+
console.log(await runSkillCli(args.slice(1), client));
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (command === "migrate") {
|
|
264
|
+
const client = await connectPapyrusClient();
|
|
265
|
+
console.log(await runMigrationCli(args.slice(1), client));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
155
268
|
if (command !== "service") usage();
|
|
156
269
|
switch (action) {
|
|
157
270
|
case "install": installService(); break;
|
package/src/client.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DAEMON_CLIENT_TIMEOUT_MS, DAEMON_PROBE_TIMEOUT_MS } from "./constants.ts";
|
|
2
2
|
import { daemonStateDir, readDaemonHandle } from "./daemon-state.ts";
|
|
3
|
-
import type { OperationName } from "./service.ts";
|
|
3
|
+
import type { OperationName, SchemaState } from "./service.ts";
|
|
4
4
|
|
|
5
5
|
export type FetchAdapter = (request: Request) => Promise<Response>;
|
|
6
6
|
|
|
@@ -28,7 +28,7 @@ export class PapyrusClient {
|
|
|
28
28
|
return body;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
health(): Promise<{ ok: true; version: string }> {
|
|
31
|
+
health(): Promise<{ ok: true; version: string; schema: SchemaState }> {
|
|
32
32
|
return this.request("/health");
|
|
33
33
|
}
|
|
34
34
|
|
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 = 2;
|
|
11
11
|
export const SERVICE_MAX_BODY_BYTES = 1_048_576;
|
|
12
12
|
export const WAL_CHECKPOINT_INTERVAL_MS = 60_000;
|
|
13
13
|
export const DB_OPTIMIZE_INTERVAL_MS = 24 * 60 * 60_000;
|
|
@@ -17,9 +17,9 @@ export const GATE_OUTPUT_LIMIT = 200;
|
|
|
17
17
|
export const GATE_MAX_BUFFER_BYTES = 1_048_576;
|
|
18
18
|
|
|
19
19
|
/** Compact task-context limits keep recurring prompt injection bounded. */
|
|
20
|
-
export const
|
|
21
|
-
export const
|
|
22
|
-
export const
|
|
20
|
+
export const TASK_CONTEXT_CURRENT_LIMIT = 3;
|
|
21
|
+
export const TASK_CONTEXT_REJECTED_LIMIT = 3;
|
|
22
|
+
export const TASK_WIDGET_OPEN_LIMIT = 3;
|
|
23
23
|
export const TASK_DETAIL_MIN_VISIBLE_LINES = 8;
|
|
24
24
|
export const TASK_DETAIL_MAX_VISIBLE_LINES = 24;
|
|
25
25
|
export const TASK_DETAIL_RESERVED_ROWS = 8;
|
|
@@ -32,13 +32,23 @@ export const TASK_GRAPH_HORIZONTAL_PAN_COLUMNS = 4;
|
|
|
32
32
|
export const TASK_EXECUTION_MAX_NODES = 1_000;
|
|
33
33
|
export const TASK_EXECUTION_MAX_EDGES = 10_000;
|
|
34
34
|
export const TASK_EXECUTION_MAX_DEGREE = 100;
|
|
35
|
-
/** Bounded
|
|
36
|
-
export const
|
|
35
|
+
/** Bounded parameterized Skill definitions and rendered workflow runs. */
|
|
36
|
+
export const SKILL_MAX_INPUTS = 32;
|
|
37
|
+
export const SKILL_MAX_ENUM_VALUES = 32;
|
|
38
|
+
export const SKILL_MAX_BLUEPRINTS = 100;
|
|
39
|
+
export const SKILL_MAX_LINKS = 500;
|
|
40
|
+
export const SKILL_MAX_RENDERED_BYTES = 1_048_576;
|
|
41
|
+
export const SKILL_RUN_ID_MAX_LENGTH = 64;
|
|
42
|
+
/** Bounded automatic Pi continuations while a focused Papyrus Task remains. */
|
|
37
43
|
export const TASK_DRIVER_MAX_TURNS = 20;
|
|
38
44
|
export const TASK_DRIVER_MAX_UNCHANGED_TURNS = 6;
|
|
39
45
|
export const GRAPH_RENDER_PADDING_X = 2;
|
|
40
46
|
export const GRAPH_RENDER_PADDING_Y = 1;
|
|
41
47
|
export const GRAPH_RENDER_BOX_PADDING = 0;
|
|
48
|
+
/** beautiful-mermaid routed layouts become unsafe on larger task graphs; use bounded line fallback. */
|
|
49
|
+
export const GRAPH_RENDER_MAX_ROUTED_NODES = 48;
|
|
50
|
+
export const GRAPH_RENDER_MAX_ROUTED_EDGES = 96;
|
|
51
|
+
export const GRAPH_RENDER_MAX_FALLBACK_LINES = 200;
|
|
42
52
|
|
|
43
53
|
/** Safe defaults and hard ceilings for graph expansion. */
|
|
44
54
|
export const DEFAULT_GRAPH_DEPTH = 4;
|
|
@@ -58,7 +68,7 @@ export const TASK_RECONCILIATION_INSTRUCTION = [
|
|
|
58
68
|
'• For each current task, ask: "Did we accomplish this task?"',
|
|
59
69
|
"• If yes, run its gates before marking it done; a claim is not verification.",
|
|
60
70
|
"• If no, continue with the next concrete action toward its desired state.",
|
|
61
|
-
"• Address blocked work or explicitly
|
|
71
|
+
"• Address blocked work or explicitly move failed review to rejected with the reason.",
|
|
62
72
|
].join("\n");
|
|
63
73
|
|
|
64
74
|
/** $XDG_DATA_HOME/papyrus/papyrus.db */
|
|
@@ -88,10 +98,12 @@ export const SEED_STATUSES = [
|
|
|
88
98
|
{ name: "draft", kind: "doc" },
|
|
89
99
|
{ name: "active", kind: "doc" },
|
|
90
100
|
{ name: "archived", kind: "doc" },
|
|
91
|
-
{ name: "
|
|
92
|
-
{ name: "
|
|
101
|
+
{ name: "todo", kind: "task" },
|
|
102
|
+
{ name: "in-progress", kind: "task" },
|
|
103
|
+
{ name: "review", kind: "task" },
|
|
104
|
+
{ name: "rejected", kind: "task" },
|
|
93
105
|
{ name: "done", kind: "task" },
|
|
94
|
-
{ name: "
|
|
106
|
+
{ name: "canceled", kind: "task" },
|
|
95
107
|
{ name: "active", kind: "rule" },
|
|
96
108
|
{ name: "deprecated", kind: "rule" },
|
|
97
109
|
{ name: "active", kind: "skill" },
|