@danypops/papyrus 0.4.0 → 0.6.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 +30 -4
- package/extension/src/domain-tools.ts +18 -5
- package/extension/src/task-detail-format.ts +24 -1
- package/extension/src/task-detail-view.ts +6 -3
- package/extension/src/tasks.ts +29 -5
- package/package.json +1 -1
- package/src/adapters/sqlite-gate-runner.ts +3 -3
- package/src/adapters/sqlite-task-event-store.ts +92 -0
- package/src/cli.ts +51 -7
- package/src/constants.ts +21 -1
- package/src/daemon.ts +20 -4
- package/src/db.ts +77 -27
- package/src/domain/gate.ts +5 -0
- package/src/domain/task-event.ts +104 -0
- package/src/log.ts +6 -0
- package/src/ops.ts +18 -7
- package/src/ports/gate-runner.ts +2 -2
- package/src/ports/task-event-store.ts +43 -0
- package/src/service.ts +72 -17
- package/src/skill-execution.ts +26 -10
- package/src/task-automation.ts +188 -0
- package/src/task-service.ts +140 -54
package/README.md
CHANGED
|
@@ -37,6 +37,8 @@ papyrus tasks plan
|
|
|
37
37
|
papyrus tasks depend <task-id> <prerequisite-id>
|
|
38
38
|
papyrus tasks start <task-id>
|
|
39
39
|
papyrus tasks complete <task-id>
|
|
40
|
+
papyrus tasks automate <task-id> <on|off>
|
|
41
|
+
papyrus automation status
|
|
40
42
|
```
|
|
41
43
|
|
|
42
44
|
For repository work, install the versioned ownership guard once:
|
|
@@ -101,7 +103,7 @@ Internally, application services depend on the `ArtifactStore` and `GateRunner`
|
|
|
101
103
|
|
|
102
104
|
## Interactive frontends
|
|
103
105
|
|
|
104
|
-
- `/tasks` — task lifecycle, gates, dependencies, and nested metadata
|
|
106
|
+
- `/tasks` — task lifecycle, append-only history, gates, dependencies, and nested metadata
|
|
105
107
|
- `/docs` — searchable documents, lifecycle, details, and graph links
|
|
106
108
|
- `/rules` — severity/condition rows, exact injection preview, enable/disable, and task gating
|
|
107
109
|
- `/skills` — trigger/tools rows, invocation into the editor, and artifact templates
|
|
@@ -122,7 +124,7 @@ Run `/tasks` for the interactive task panel:
|
|
|
122
124
|
- successors are never auto-completed; fan-in, fan-out, diamonds, and disconnected DAGs remain explicit
|
|
123
125
|
- inspect deterministic execution layers, readiness, a box-drawn nested hierarchy, composition, dependencies, evidence-bearing checklists, and verification gates
|
|
124
126
|
- lifecycle colors are semantic and redundant with text/glyphs: To-Do grey, in-progress yellow, review blue, rejected orange, done green, and canceled red; `▶` marks active focus
|
|
125
|
-
- Show details keeps Checklist and Validation gates separate from incidental Metadata, then renders relationships as a Unicode box-drawing graph footer; `↑/↓` scrolls and `←/→` pans wide graphs
|
|
127
|
+
- Show details keeps Checklist and Validation gates separate from incidental Metadata, renders bounded post-migration lifecycle history with actor/source/reason and gate evidence, then renders relationships as a Unicode box-drawing graph footer; `↑/↓` scrolls and `←/→` pans wide graphs
|
|
126
128
|
- the compact persistent widget shows bounded open work in containment order and always retains the active focus
|
|
127
129
|
|
|
128
130
|
Authenticated CLI parity covers the changed lifecycle and focus operations:
|
|
@@ -130,6 +132,7 @@ Authenticated CLI parity covers the changed lifecycle and focus operations:
|
|
|
130
132
|
```bash
|
|
131
133
|
papyrus tasks graph --json
|
|
132
134
|
papyrus tasks active --json
|
|
135
|
+
papyrus tasks history <id> --json
|
|
133
136
|
papyrus tasks focus <id> --json
|
|
134
137
|
papyrus tasks start <id> --json
|
|
135
138
|
papyrus tasks submit <id> --json
|
|
@@ -137,8 +140,31 @@ papyrus tasks complete <id> --json
|
|
|
137
140
|
papyrus tasks reject <id> --json
|
|
138
141
|
papyrus tasks retry <id> --json
|
|
139
142
|
papyrus tasks cancel <id> --json
|
|
143
|
+
papyrus tasks automate <id> <on|off> --json
|
|
144
|
+
papyrus automation status --json
|
|
145
|
+
papyrus automation run --json
|
|
140
146
|
```
|
|
141
147
|
|
|
148
|
+
### Opt-in supervised automation
|
|
149
|
+
|
|
150
|
+
Background graph reconciliation is off by default and requires two independent opt-ins: daemon configuration and `automation.enabled` on each Task. Only opted-in Tasks already in `review` are eligible for automatic gate/checklist review; Papyrus never skips the review lifecycle. When one completes, directly dependent opted-in successors that become ready may move from `todo` to `in-progress`. Every completion, rejection, and start is written to append-only history with actor `daemon`, source `automation-reconciler`, reason, and bounded gate evidence.
|
|
151
|
+
|
|
152
|
+
Enable the daemon with a systemd user-service override and restart it:
|
|
153
|
+
|
|
154
|
+
```ini
|
|
155
|
+
[Service]
|
|
156
|
+
Environment=PAPYRUS_AUTOMATION_ENABLED=1
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
systemctl --user edit papyrus.service
|
|
161
|
+
systemctl --user restart papyrus.service
|
|
162
|
+
papyrus tasks automate <task-id> on
|
|
163
|
+
papyrus automation status
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Secure defaults are a 60-second interval, 10 Task transitions per sweep, gate concurrency 1, and a 120-second sweep deadline. Optional environment settings are `PAPYRUS_AUTOMATION_INTERVAL_MS` (10 seconds–1 hour), `PAPYRUS_AUTOMATION_MAX_TASKS` (1–100), `PAPYRUS_AUTOMATION_GATE_CONCURRENCY` (1–4), and `PAPYRUS_AUTOMATION_MAX_RUNTIME_MS` (1 ms–10 minutes). Candidate scans are capped at 1,000 review Tasks, sweeps are single-flight, subprocess gates inherit the sweep deadline, result arrays are bounded by the Task limit, and logs contain counts rather than gate output. `papyrus automation run` uses the same policy and refuses to reconcile while global automation is disabled.
|
|
167
|
+
|
|
142
168
|
Checklist criteria are an item-to-proof map. Every new item requires one or more typed references to inspectable evidence; proof presence does not imply that the evidence passed an executable gate:
|
|
143
169
|
|
|
144
170
|
```ts
|
|
@@ -171,10 +197,10 @@ packed install npm:@danypops/papyrus
|
|
|
171
197
|
~/.pi/agent/npm/node_modules/.bin/papyrus service install
|
|
172
198
|
```
|
|
173
199
|
|
|
174
|
-
Existing databases are never migrated on daemon boot. After upgrading
|
|
200
|
+
Existing databases are never migrated on daemon boot. After upgrading to append-only task history, run the authenticated CLI migration explicitly. A v1 database receives the lifecycle prerequisite and history schema in one transaction; existing tasks receive no fabricated events:
|
|
175
201
|
|
|
176
202
|
```bash
|
|
177
|
-
~/.pi/agent/npm/node_modules/.bin/papyrus migrate task-
|
|
203
|
+
~/.pi/agent/npm/node_modules/.bin/papyrus migrate task-history
|
|
178
204
|
```
|
|
179
205
|
|
|
180
206
|
Until that command succeeds, health reports `migrationRequired` and normal domain operations are rejected with actionable guidance. Migration is not exposed as a Pi tool or MCP action. New empty databases bootstrap directly at the current schema.
|
|
@@ -4,6 +4,7 @@ import type { Artifact } from "../../src/domain/artifact.ts";
|
|
|
4
4
|
import { PROOF_TYPES } from "../../src/domain/checklist.ts";
|
|
5
5
|
import type { GateResult } from "../../src/domain/gate.ts";
|
|
6
6
|
import type { TaskExecutionPlan } from "../../src/task-execution.ts";
|
|
7
|
+
import type { TaskHistoryPage } from "../../src/domain/task-event.ts";
|
|
7
8
|
import type { TaskCompletion, TaskGraph } from "../../src/task-service.ts";
|
|
8
9
|
import type { SkillWorkflowRunResult } from "../../src/skill-execution.ts";
|
|
9
10
|
import { callService } from "./service-client.ts";
|
|
@@ -30,7 +31,7 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
30
31
|
pi.registerTool({
|
|
31
32
|
name: "tasks",
|
|
32
33
|
label: "Tasks",
|
|
33
|
-
description: "Task domain tool. ACTIONS: create, list, show, graph, plan, active, focus, start, submit, complete, reject, retry, cancel, run_gates, set_checklist, depend, contain. Lifecycle is todo → in-progress → review → done, with review failure → rejected and retry → in-progress; canceled is terminal. Active focus is independent and identifies the one task auto-drive continues. Completion runs gates and checklist-proof review, then focuses one deterministic ready successor without claiming effort. Dependency cycles are rejected. Prefer this over low-level papyrus_* tools for task work.",
|
|
34
|
+
description: "Task domain tool. ACTIONS: create, list, show, history, graph, plan, active, focus, start, submit, complete, reject, retry, cancel, run_gates, set_checklist, set_automation, depend, contain. Lifecycle is todo → in-progress → review → done, with review failure → rejected and retry → in-progress; canceled is terminal. Active focus is independent and identifies the one task auto-drive continues. Completion runs gates and checklist-proof review, then focuses one deterministic ready successor without claiming effort. Dependency cycles are rejected. Prefer this over low-level papyrus_* tools for task work.",
|
|
34
35
|
parameters: Type.Object({
|
|
35
36
|
action: Type.String(),
|
|
36
37
|
id: Type.Optional(Type.String()),
|
|
@@ -39,6 +40,11 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
39
40
|
status: Type.Optional(Type.String()),
|
|
40
41
|
text: Type.Optional(Type.String()),
|
|
41
42
|
limit: Type.Optional(Type.Number()),
|
|
43
|
+
cursor: Type.Optional(Type.Number()),
|
|
44
|
+
direction: Type.Optional(Type.Union([Type.Literal("asc"), Type.Literal("desc")])),
|
|
45
|
+
reason: Type.Optional(Type.String()),
|
|
46
|
+
session_id: Type.Optional(Type.String()),
|
|
47
|
+
enabled: Type.Optional(Type.Boolean()),
|
|
42
48
|
labels: Type.Optional(Type.Array(Type.String())),
|
|
43
49
|
extra: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
44
50
|
gates: Type.Optional(Type.Array(Type.Record(Type.String(), Type.Unknown()))),
|
|
@@ -52,8 +58,9 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
52
58
|
async execute(_id, params) {
|
|
53
59
|
try {
|
|
54
60
|
const action = params.action;
|
|
61
|
+
const request = { ...params, actor: "agent", source: "pi-tool" };
|
|
55
62
|
if (action === "create") {
|
|
56
|
-
const artifact = await callService<Record<string, unknown>, Artifact>("tasks.create",
|
|
63
|
+
const artifact = await callService<Record<string, unknown>, Artifact>("tasks.create", request);
|
|
57
64
|
return text(`Created task ${artifactLine(artifact)}`, { artifact });
|
|
58
65
|
}
|
|
59
66
|
if (action === "list") {
|
|
@@ -64,6 +71,11 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
64
71
|
const artifact = await callService<Record<string, unknown>, Artifact>("tasks.show", params);
|
|
65
72
|
return text(`${artifactLine(artifact)}\n\n${artifact.body}`, { artifact });
|
|
66
73
|
}
|
|
74
|
+
if (action === "history") {
|
|
75
|
+
const page = await callService<Record<string, unknown>, TaskHistoryPage>("tasks.history", request);
|
|
76
|
+
const lines = page.events.map((event) => `${event.occurredAt} ${event.type} ${event.fromStatus ?? "∅"} → ${event.toStatus ?? "∅"} · ${event.actor}/${event.source}${event.reason ? ` · ${event.reason}` : ""}`);
|
|
77
|
+
return text(lines.join("\n") || "No recorded history for this task.", { page });
|
|
78
|
+
}
|
|
67
79
|
if (action === "active") {
|
|
68
80
|
const artifact = await callService<Record<string, unknown>, Artifact | null>("tasks.active", params);
|
|
69
81
|
return text(artifact ? `Active: ${artifactLine(artifact)}` : "No active task.", { artifact });
|
|
@@ -92,7 +104,7 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
92
104
|
return text(`Updated checklist: ${artifactLine(artifact)}`, { artifact });
|
|
93
105
|
}
|
|
94
106
|
if (action === "complete") {
|
|
95
|
-
const result = await callService<Record<string, unknown>, TaskCompletion>("tasks.complete",
|
|
107
|
+
const result = await callService<Record<string, unknown>, TaskCompletion>("tasks.complete", request);
|
|
96
108
|
const gates = result.gates.map((gate) => `${gate.passed ? "✓" : "✗"} ${gate.gate.type}: ${gate.gate.target} — ${gate.output}`).join("\n");
|
|
97
109
|
const checklist = result.checklist.map((item) => `${item.accepted ? "✓" : "✗"} proof: ${item.item}${item.reason ? ` — ${item.reason}` : ""}`).join("\n");
|
|
98
110
|
const focused = result.focused ? `\nActive: ${artifactLine(result.focused)}` : "";
|
|
@@ -102,7 +114,7 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
102
114
|
return text(`${result.completed ? "Completed" : "Rejected"}: ${artifactLine(result.artifact)}${focused}${blocked}${checklist ? `\n${checklist}` : ""}${gates ? `\n${gates}` : ""}`, { ...result });
|
|
103
115
|
}
|
|
104
116
|
if (action === "run_gates") {
|
|
105
|
-
const gates = await callService<Record<string, unknown>, GateResult[]>("tasks.run_gates",
|
|
117
|
+
const gates = await callService<Record<string, unknown>, GateResult[]>("tasks.run_gates", request);
|
|
106
118
|
return text(gates.map((gate) => `${gate.passed ? "✓" : "✗"} ${gate.gate.type}: ${gate.gate.target} — ${gate.output}`).join("\n") || "No gates configured.", { gates });
|
|
107
119
|
}
|
|
108
120
|
const operations = {
|
|
@@ -112,12 +124,13 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
112
124
|
reject: "tasks.reject",
|
|
113
125
|
retry: "tasks.retry",
|
|
114
126
|
cancel: "tasks.cancel",
|
|
127
|
+
set_automation: "tasks.set_automation",
|
|
115
128
|
depend: "tasks.depend",
|
|
116
129
|
contain: "tasks.contain",
|
|
117
130
|
} as const;
|
|
118
131
|
const operation = operations[action as keyof typeof operations];
|
|
119
132
|
if (!operation) return text(`Unknown tasks action: ${action}`);
|
|
120
|
-
const artifact = await callService<Record<string, unknown>, Artifact>(operation,
|
|
133
|
+
const artifact = await callService<Record<string, unknown>, Artifact>(operation, request);
|
|
121
134
|
return text(artifactLine(artifact), { artifact });
|
|
122
135
|
} catch (error) {
|
|
123
136
|
return text(`tasks failed: ${error instanceof Error ? error.message : error}`);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Artifact } from "../../src/domain/artifact.ts";
|
|
2
|
+
import type { TaskEvent } from "../../src/domain/task-event.ts";
|
|
2
3
|
import { checklistEntries, type ProofReference } from "../../src/domain/checklist.ts";
|
|
3
4
|
import { formatMetadata } from "./artifact-format.ts";
|
|
4
5
|
|
|
@@ -48,7 +49,28 @@ function gateLines(value: unknown): string[] {
|
|
|
48
49
|
return lines;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
|
|
52
|
+
function historyLines(history: TaskEvent[]): string[] {
|
|
53
|
+
if (history.length === 0) return ["History:", " (no post-migration events recorded)"];
|
|
54
|
+
const lines = ["History:"];
|
|
55
|
+
for (const event of history) {
|
|
56
|
+
const transition = event.fromStatus || event.toStatus ? ` · ${event.fromStatus ?? "∅"} → ${event.toStatus ?? "∅"}` : "";
|
|
57
|
+
const reason = event.reason ? ` · ${event.reason}` : "";
|
|
58
|
+
lines.push(` ${event.occurredAt} · ${event.type}${transition} · ${event.actor}/${event.source}${reason}`);
|
|
59
|
+
if (event.evidence?.result) lines.push(` result: ${event.evidence.result}`);
|
|
60
|
+
if (Array.isArray(event.evidence?.gates)) {
|
|
61
|
+
for (const value of event.evidence.gates) {
|
|
62
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) continue;
|
|
63
|
+
const result = value as Record<string, unknown>;
|
|
64
|
+
const gate = typeof result["gate"] === "object" && result["gate"] !== null ? result["gate"] as Record<string, unknown> : {};
|
|
65
|
+
const passed = result["passed"] === true;
|
|
66
|
+
lines.push(` ${passed ? "✓" : "✗"} ${String(gate["type"] ?? "gate")} · ${String(gate["target"] ?? "unknown")}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return lines;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function taskDetailsText(task: Artifact, relationshipGraphLines: string[] = [], history: TaskEvent[] = []): string {
|
|
52
74
|
let output = `${TASK_STATUS_GLYPHS[task.status] ?? "?"} ${task.title}\n${task.id} [task|${task.status}]`;
|
|
53
75
|
if (task.labels.length > 0) output += `\nLabels: ${task.labels.join(", ")}`;
|
|
54
76
|
output += `\n\n${task.body || "(no body)"}`;
|
|
@@ -60,6 +82,7 @@ export function taskDetailsText(task: Artifact, relationshipGraphLines: string[]
|
|
|
60
82
|
if (Object.keys(metadata).length > 0) {
|
|
61
83
|
output += `\n\nMetadata:\n${formatMetadata(metadata).map((line) => ` ${line}`).join("\n")}`;
|
|
62
84
|
}
|
|
85
|
+
output += `\n\n${historyLines(history).join("\n")}`;
|
|
63
86
|
if (task.edges?.length) {
|
|
64
87
|
const graph = relationshipGraphLines.length > 0 ? relationshipGraphLines.join("\n") : " (graph unavailable)";
|
|
65
88
|
output += `\n\nRelationships:\n Dependencies point prerequisite → dependent.\n${graph}`;
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
TASK_DETAIL_RESERVED_ROWS,
|
|
8
8
|
} from "../../src/constants.ts";
|
|
9
9
|
import type { Artifact } from "../../src/domain/artifact.ts";
|
|
10
|
+
import type { TaskEvent } from "../../src/domain/task-event.ts";
|
|
10
11
|
import type { GraphRenderer } from "../../src/ports/graph-renderer.ts";
|
|
11
12
|
import { projectTaskRelationships } from "../../src/task-relationship-view.ts";
|
|
12
13
|
import type { TaskGraph } from "../../src/task-service.ts";
|
|
@@ -31,13 +32,14 @@ class TaskDetailViewport {
|
|
|
31
32
|
private readonly theme: Theme,
|
|
32
33
|
task: Artifact,
|
|
33
34
|
private readonly graphLines: string[],
|
|
35
|
+
history: TaskEvent[],
|
|
34
36
|
private readonly close: () => void,
|
|
35
37
|
) {
|
|
36
38
|
this.visibleLines = Math.max(
|
|
37
39
|
TASK_DETAIL_MIN_VISIBLE_LINES,
|
|
38
40
|
Math.min(TASK_DETAIL_MAX_VISIBLE_LINES, tui.terminal.rows - TASK_DETAIL_RESERVED_ROWS),
|
|
39
41
|
);
|
|
40
|
-
this.narrative = taskDetailsText({ ...task, edges: undefined });
|
|
42
|
+
this.narrative = taskDetailsText({ ...task, edges: undefined }, [], history);
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
invalidate(): void { this.renderedWidth = 0; }
|
|
@@ -99,13 +101,14 @@ export async function showTaskDetails(
|
|
|
99
101
|
task: Artifact,
|
|
100
102
|
graph?: TaskGraph,
|
|
101
103
|
renderer: GraphRenderer = new BeautifulMermaidRenderer(),
|
|
104
|
+
history: TaskEvent[] = [],
|
|
102
105
|
): Promise<void> {
|
|
103
106
|
const relationshipGraph = renderer.render(projectTaskRelationships(task, graph)).lines;
|
|
104
|
-
const content = taskDetailsText(task, relationshipGraph);
|
|
107
|
+
const content = taskDetailsText(task, relationshipGraph, history);
|
|
105
108
|
if (ctx.mode !== "tui") {
|
|
106
109
|
ctx.ui.notify(content, "info");
|
|
107
110
|
return;
|
|
108
111
|
}
|
|
109
112
|
await ctx.ui.custom<void>((tui, theme, _keybindings, done) =>
|
|
110
|
-
new TaskDetailViewport(tui, theme, task, relationshipGraph, done));
|
|
113
|
+
new TaskDetailViewport(tui, theme, task, relationshipGraph, history, done));
|
|
111
114
|
}
|
package/extension/src/tasks.ts
CHANGED
|
@@ -14,6 +14,7 @@ export { taskDetailsText } from "./task-detail-format.ts";
|
|
|
14
14
|
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
|
+
import type { TaskHistoryPage } from "../../src/domain/task-event.ts";
|
|
17
18
|
import { projectTaskExecution } from "../../src/task-execution.ts";
|
|
18
19
|
import type { TaskCompletion, TaskGraph, TaskStatus } from "../../src/task-service.ts";
|
|
19
20
|
import { TASK_STATUS_PRESENTATION, taskTreeConnector } from "./task-presentation.ts";
|
|
@@ -29,6 +30,12 @@ const STATUS_ACTIONS: Record<string, string[]> = {
|
|
|
29
30
|
|
|
30
31
|
type TaskRow = Artifact;
|
|
31
32
|
|
|
33
|
+
function taskAutomationEnabled(task: Artifact): boolean {
|
|
34
|
+
const automation = task.extra["automation"];
|
|
35
|
+
return typeof automation === "object" && automation !== null && !Array.isArray(automation)
|
|
36
|
+
&& (automation as Record<string, unknown>)["enabled"] === true;
|
|
37
|
+
}
|
|
38
|
+
|
|
32
39
|
export interface TaskHierarchyRow {
|
|
33
40
|
task: TaskRow;
|
|
34
41
|
depth: number;
|
|
@@ -70,7 +77,7 @@ export async function showTasks(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
70
77
|
if (create === "Create a task") {
|
|
71
78
|
const title = await ctx.ui.input("Task title:", "");
|
|
72
79
|
if (title) {
|
|
73
|
-
await callService("tasks.create", { title });
|
|
80
|
+
await callService("tasks.create", { title, actor: "user", source: "tasks-tui" });
|
|
74
81
|
graph = await loadTaskGraph();
|
|
75
82
|
}
|
|
76
83
|
}
|
|
@@ -85,9 +92,11 @@ export async function showTasks(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
85
92
|
if (action.type !== "action" || !action.row) continue;
|
|
86
93
|
|
|
87
94
|
const active = graph.nodes.find((node) => node.task.id === action.row!.id)?.active === true;
|
|
95
|
+
const automationEnabled = taskAutomationEnabled(action.row);
|
|
88
96
|
const choices = [
|
|
89
97
|
"Show details",
|
|
90
98
|
...(!active && action.row.status !== "done" && action.row.status !== "canceled" ? ["Make active"] : []),
|
|
99
|
+
...(action.row.status !== "done" && action.row.status !== "canceled" ? [automationEnabled ? "Disable automation" : "Enable automation"] : []),
|
|
91
100
|
...(action.row.status === "review" ? ["Run gates"] : []),
|
|
92
101
|
...(STATUS_ACTIONS[action.row.status] ?? []),
|
|
93
102
|
];
|
|
@@ -97,7 +106,8 @@ export async function showTasks(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
97
106
|
if (choice === "Show details") {
|
|
98
107
|
const art = await callService<Record<string, unknown>, Artifact | null>("tasks.show", { id: action.row.id });
|
|
99
108
|
if (!art) { ctx.ui.notify("Not found", "error"); continue; }
|
|
100
|
-
await
|
|
109
|
+
const history = await callService<Record<string, unknown>, TaskHistoryPage>("tasks.history", { id: art.id, direction: "desc" });
|
|
110
|
+
await showTaskDetails(ctx, art, graph, undefined, [...history.events].reverse());
|
|
101
111
|
} else if (choice === "Make active") {
|
|
102
112
|
try {
|
|
103
113
|
await callService<Record<string, unknown>, Artifact>("tasks.focus", { id: action.row.id });
|
|
@@ -105,9 +115,23 @@ export async function showTasks(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
105
115
|
} catch (error) {
|
|
106
116
|
ctx.ui.notify(`Focus failed: ${error instanceof Error ? error.message : error}`, "error");
|
|
107
117
|
}
|
|
118
|
+
} else if (choice === "Enable automation" || choice === "Disable automation") {
|
|
119
|
+
try {
|
|
120
|
+
const enabled = choice === "Enable automation";
|
|
121
|
+
const updated = await callService<Record<string, unknown>, Artifact>("tasks.set_automation", {
|
|
122
|
+
id: action.row.id,
|
|
123
|
+
enabled,
|
|
124
|
+
actor: "user",
|
|
125
|
+
source: "tasks-tui",
|
|
126
|
+
});
|
|
127
|
+
action.row.extra = updated.extra;
|
|
128
|
+
ctx.ui.notify(`Automation ${enabled ? "enabled" : "disabled"}: ${action.row.title}`, enabled ? "warning" : "info");
|
|
129
|
+
} catch (error) {
|
|
130
|
+
ctx.ui.notify(`Automation setting failed: ${error instanceof Error ? error.message : error}`, "error");
|
|
131
|
+
}
|
|
108
132
|
} else if (choice === "Run gates") {
|
|
109
133
|
try {
|
|
110
|
-
const results = await callService<Record<string, unknown>, GateResult[]>("tasks.run_gates", { id: action.row.id });
|
|
134
|
+
const results = await callService<Record<string, unknown>, GateResult[]>("tasks.run_gates", { id: action.row.id, actor: "user", source: "tasks-tui" });
|
|
111
135
|
ctx.ui.notify(`Gates:\n${results.map((gate) => `${gate.passed ? "✓" : "✗"} ${gate.gate.type}: ${gate.gate.target} — ${gate.output}`).join("\n")}`, "info");
|
|
112
136
|
} catch (error) {
|
|
113
137
|
ctx.ui.notify(`Gates failed: ${error instanceof Error ? error.message : error}`, "error");
|
|
@@ -126,7 +150,7 @@ export async function showTasks(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
126
150
|
? "tasks.cancel"
|
|
127
151
|
: "tasks.complete";
|
|
128
152
|
if (operation === "tasks.complete") {
|
|
129
|
-
const result = await callService<Record<string, unknown>, TaskCompletion>(operation, { id: action.row.id });
|
|
153
|
+
const result = await callService<Record<string, unknown>, TaskCompletion>(operation, { id: action.row.id, actor: "user", source: "tasks-tui" });
|
|
130
154
|
action.row.status = result.artifact.status;
|
|
131
155
|
const gates = result.gates.map((gate) => `${gate.passed ? "✓" : "✗"} ${gate.gate.type}: ${gate.gate.target}`).join("\n");
|
|
132
156
|
const checklist = result.checklist.map((item) => `${item.accepted ? "✓" : "✗"} proof: ${item.item}`).join("\n");
|
|
@@ -141,7 +165,7 @@ export async function showTasks(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
141
165
|
result.completed ? "info" : "warning",
|
|
142
166
|
);
|
|
143
167
|
} else {
|
|
144
|
-
const updated = await callService<Record<string, unknown>, Artifact>(operation, { id: action.row.id });
|
|
168
|
+
const updated = await callService<Record<string, unknown>, Artifact>(operation, { id: action.row.id, actor: "user", source: "tasks-tui" });
|
|
145
169
|
action.row.status = updated.status;
|
|
146
170
|
ctx.ui.notify(`${updated.id} → [${updated.status}]`, "info");
|
|
147
171
|
}
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Db } from "../db.ts";
|
|
2
|
-
import type { GateResult } from "../domain/gate.ts";
|
|
2
|
+
import type { GateResult, GateRunOptions } from "../domain/gate.ts";
|
|
3
3
|
import type { GateRunner } from "../ports/gate-runner.ts";
|
|
4
4
|
import { runGates, runGatesAsync } from "../ops.ts";
|
|
5
5
|
|
|
@@ -10,7 +10,7 @@ export class SQLiteGateRunner implements GateRunner {
|
|
|
10
10
|
return runGates(this.db, artifactId);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
runAsync(artifactId: string): Promise<GateResult[]> {
|
|
14
|
-
return runGatesAsync(this.db, artifactId);
|
|
13
|
+
runAsync(artifactId: string, options?: GateRunOptions): Promise<GateResult[]> {
|
|
14
|
+
return runGatesAsync(this.db, artifactId, options);
|
|
15
15
|
}
|
|
16
16
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { Db } from "../db.ts";
|
|
2
|
+
import { inTransaction } from "../db.ts";
|
|
3
|
+
import {
|
|
4
|
+
normalizeTaskHistoryQuery,
|
|
5
|
+
validateTaskEvent,
|
|
6
|
+
type AppendTaskEvent,
|
|
7
|
+
type TaskEvent,
|
|
8
|
+
type TaskEventEvidence,
|
|
9
|
+
type TaskEventType,
|
|
10
|
+
type TaskHistoryPage,
|
|
11
|
+
type TaskHistoryQuery,
|
|
12
|
+
type TaskLifecycleStatus,
|
|
13
|
+
} from "../domain/task-event.ts";
|
|
14
|
+
import type { TaskEventStore } from "../ports/task-event-store.ts";
|
|
15
|
+
|
|
16
|
+
interface TaskEventRow {
|
|
17
|
+
id: number;
|
|
18
|
+
task_id: string;
|
|
19
|
+
occurred_at: string;
|
|
20
|
+
event_type: TaskEventType;
|
|
21
|
+
actor: string;
|
|
22
|
+
source: string;
|
|
23
|
+
session_id: string | null;
|
|
24
|
+
reason: string | null;
|
|
25
|
+
from_status: TaskLifecycleStatus | null;
|
|
26
|
+
to_status: TaskLifecycleStatus | null;
|
|
27
|
+
attempt_id: string | null;
|
|
28
|
+
evidence_json: string | null;
|
|
29
|
+
event_schema_version: 1;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function mapRow(row: TaskEventRow): TaskEvent {
|
|
33
|
+
return {
|
|
34
|
+
id: row.id,
|
|
35
|
+
taskId: row.task_id,
|
|
36
|
+
occurredAt: row.occurred_at,
|
|
37
|
+
type: row.event_type,
|
|
38
|
+
actor: row.actor,
|
|
39
|
+
source: row.source,
|
|
40
|
+
...(row.session_id === null ? {} : { sessionId: row.session_id }),
|
|
41
|
+
...(row.reason === null ? {} : { reason: row.reason }),
|
|
42
|
+
...(row.from_status === null ? {} : { fromStatus: row.from_status }),
|
|
43
|
+
...(row.to_status === null ? {} : { toStatus: row.to_status }),
|
|
44
|
+
...(row.attempt_id === null ? {} : { attemptId: row.attempt_id }),
|
|
45
|
+
...(row.evidence_json === null ? {} : { evidence: JSON.parse(row.evidence_json) as TaskEventEvidence }),
|
|
46
|
+
schemaVersion: row.event_schema_version,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class SQLiteTaskEventStore implements TaskEventStore {
|
|
51
|
+
constructor(private readonly db: Db) {}
|
|
52
|
+
|
|
53
|
+
atomic<T>(operation: () => T): T { return inTransaction(this.db, operation); }
|
|
54
|
+
|
|
55
|
+
append(input: AppendTaskEvent): TaskEvent {
|
|
56
|
+
const event = validateTaskEvent(input);
|
|
57
|
+
const result = this.db.prepare(`
|
|
58
|
+
INSERT INTO task_events (
|
|
59
|
+
task_id, occurred_at, event_type, actor, source, session_id, reason,
|
|
60
|
+
from_status, to_status, attempt_id, evidence_json, event_schema_version
|
|
61
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)
|
|
62
|
+
`).run(
|
|
63
|
+
event.taskId,
|
|
64
|
+
new Date().toISOString(),
|
|
65
|
+
event.type,
|
|
66
|
+
event.actor,
|
|
67
|
+
event.source,
|
|
68
|
+
event.sessionId ?? null,
|
|
69
|
+
event.reason ?? null,
|
|
70
|
+
event.fromStatus ?? null,
|
|
71
|
+
event.toStatus ?? null,
|
|
72
|
+
event.attemptId ?? null,
|
|
73
|
+
event.evidence === undefined ? null : JSON.stringify(event.evidence),
|
|
74
|
+
);
|
|
75
|
+
return mapRow(this.db.prepare("SELECT * FROM task_events WHERE id = ?").get(result.lastInsertRowid) as TaskEventRow);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
history(taskId: string, query: TaskHistoryQuery = {}): TaskHistoryPage {
|
|
79
|
+
const { limit, direction, cursor } = normalizeTaskHistoryQuery(query);
|
|
80
|
+
const comparator = direction === "desc" ? "<" : ">";
|
|
81
|
+
const order = direction === "desc" ? "DESC" : "ASC";
|
|
82
|
+
const rows = this.db.prepare(`
|
|
83
|
+
SELECT * FROM task_events
|
|
84
|
+
WHERE task_id = ? ${cursor === undefined ? "" : `AND id ${comparator} ?`}
|
|
85
|
+
ORDER BY occurred_at ${order}, id ${order}
|
|
86
|
+
LIMIT ?
|
|
87
|
+
`).all(...(cursor === undefined ? [taskId, limit + 1] : [taskId, cursor, limit + 1])) as TaskEventRow[];
|
|
88
|
+
const hasMore = rows.length > limit;
|
|
89
|
+
const events = rows.slice(0, limit).map(mapRow);
|
|
90
|
+
return { events, ...(hasMore ? { nextCursor: events.at(-1)!.id } : {}) };
|
|
91
|
+
}
|
|
92
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { serveMain } from "./daemon.ts";
|
|
|
10
10
|
import type { GateResult } from "./domain/gate.ts";
|
|
11
11
|
import type { TaskExecutionPlan } from "./task-execution.ts";
|
|
12
12
|
import type { TaskBlockage, TaskCompletion } from "./task-service.ts";
|
|
13
|
+
import type { TaskAutomationResult, TaskAutomationSettings } from "./task-automation.ts";
|
|
13
14
|
|
|
14
15
|
export interface SystemdUnitOptions {
|
|
15
16
|
bunBin: string;
|
|
@@ -56,11 +57,13 @@ function installService(): void {
|
|
|
56
57
|
const USAGE = `Usage:
|
|
57
58
|
papyrus serve
|
|
58
59
|
papyrus service <install|start|stop|restart|status>
|
|
59
|
-
papyrus migrate task-
|
|
60
|
+
papyrus migrate task-history [--json]
|
|
61
|
+
papyrus automation <status|run> [--json]
|
|
60
62
|
papyrus skills run <id> [--arguments-json <json>] [--run-id <id>] [--json]
|
|
61
63
|
papyrus tasks plan [--json]
|
|
62
64
|
papyrus tasks graph [--json]
|
|
63
65
|
papyrus tasks active [--json]
|
|
66
|
+
papyrus tasks history <id> [--json]
|
|
64
67
|
papyrus tasks focus <id> [--json]
|
|
65
68
|
papyrus tasks complete <id> [--json]
|
|
66
69
|
papyrus tasks start <id> [--json]
|
|
@@ -68,6 +71,7 @@ const USAGE = `Usage:
|
|
|
68
71
|
papyrus tasks reject <id> [--json]
|
|
69
72
|
papyrus tasks retry <id> [--json]
|
|
70
73
|
papyrus tasks cancel <id> [--json]
|
|
74
|
+
papyrus tasks automate <id> <on|off> [--json]
|
|
71
75
|
papyrus tasks depend <id> <prerequisite-id> [--json]`;
|
|
72
76
|
|
|
73
77
|
function usage(): never {
|
|
@@ -106,8 +110,8 @@ function planText(plan: TaskExecutionPlan): string {
|
|
|
106
110
|
export async function runMigrationCli(args: string[], client: TaskCliClient): Promise<string> {
|
|
107
111
|
const json = args.includes("--json");
|
|
108
112
|
const positional = args.filter((arg) => arg !== "--json");
|
|
109
|
-
if (positional.length !== 1 || positional[0] !== "task-
|
|
110
|
-
throw new Error("migrate requires exactly `task-
|
|
113
|
+
if (positional.length !== 1 || positional[0] !== "task-history") {
|
|
114
|
+
throw new Error("migrate requires exactly `task-history`");
|
|
111
115
|
}
|
|
112
116
|
const result = await client.call<Record<string, never>, MigrationResult>("system.migrate", {});
|
|
113
117
|
if (json) return JSON.stringify(result);
|
|
@@ -115,6 +119,20 @@ export async function runMigrationCli(args: string[], client: TaskCliClient): Pr
|
|
|
115
119
|
return `Migrated schema ${result.from} → ${result.to}: ${result.applied.join(", ")}`;
|
|
116
120
|
}
|
|
117
121
|
|
|
122
|
+
export async function runAutomationCli(args: string[], client: TaskCliClient): Promise<string> {
|
|
123
|
+
const json = args.includes("--json");
|
|
124
|
+
const positional = args.filter((argument) => argument !== "--json");
|
|
125
|
+
if (positional.length !== 1 || (positional[0] !== "status" && positional[0] !== "run")) {
|
|
126
|
+
throw new Error("automation requires exactly `status` or `run`");
|
|
127
|
+
}
|
|
128
|
+
if (positional[0] === "status") {
|
|
129
|
+
const status = await client.call<Record<string, never>, TaskAutomationSettings & { inFlight: boolean }>("automation.status", {});
|
|
130
|
+
return json ? JSON.stringify(status) : `Automation: ${status.enabled ? "enabled" : "disabled"} · interval ${status.intervalMs}ms · max ${status.maxTasksPerSweep} tasks · concurrency ${status.gateConcurrency}`;
|
|
131
|
+
}
|
|
132
|
+
const result = await client.call<Record<string, never>, TaskAutomationResult>("automation.reconcile", {});
|
|
133
|
+
return json ? JSON.stringify(result) : `Automation sweep: ${result.examined} examined · ${result.completed} completed · ${result.rejected} rejected · ${result.started} started · ${result.errors.length} errors${result.skipped ? ` · skipped ${result.skipped}` : ""}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
118
136
|
export async function runSkillCli(args: string[], client: TaskCliClient): Promise<string> {
|
|
119
137
|
const json = args.includes("--json");
|
|
120
138
|
const positional: string[] = [];
|
|
@@ -174,6 +192,15 @@ export async function runTaskCli(args: string[], client: TaskCliClient): Promise
|
|
|
174
192
|
human = active ? `Active: ${artifactLabel(active)}` : "No active task.";
|
|
175
193
|
break;
|
|
176
194
|
}
|
|
195
|
+
case "history": {
|
|
196
|
+
if (!id || dependencyId) throw new Error("tasks history requires exactly one task id");
|
|
197
|
+
const page = await client.call<{ id: string; direction: "desc" }, import("./domain/task-event.ts").TaskHistoryPage>("tasks.history", { id, direction: "desc" });
|
|
198
|
+
result = page;
|
|
199
|
+
human = page.events.length === 0
|
|
200
|
+
? `No recorded history for ${id}.`
|
|
201
|
+
: [...page.events].reverse().map((event) => `${event.occurredAt} ${event.type} ${event.fromStatus ?? "∅"} → ${event.toStatus ?? "∅"} · ${event.actor}/${event.source}${event.reason ? ` · ${event.reason}` : ""}`).join("\n");
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
177
204
|
case "focus": {
|
|
178
205
|
if (!id || dependencyId) throw new Error("tasks focus requires exactly one task id");
|
|
179
206
|
const active = await client.call<{ id: string }, CliArtifact>("tasks.focus", { id });
|
|
@@ -202,7 +229,7 @@ export async function runTaskCli(args: string[], client: TaskCliClient): Promise
|
|
|
202
229
|
}
|
|
203
230
|
case "complete": {
|
|
204
231
|
if (!id || dependencyId) throw new Error("tasks complete requires exactly one task id");
|
|
205
|
-
const completion = await client.call<
|
|
232
|
+
const completion = await client.call<Record<string, string>, CliCompletion>("tasks.complete", { id, actor: "user", source: "cli" });
|
|
206
233
|
result = completion;
|
|
207
234
|
const lines = [`${completion.completed ? "Completed" : "Rejected"}: ${artifactLabel(completion.artifact)}`];
|
|
208
235
|
if (completion.focused) lines.push(`Active: ${artifactLabel(completion.focused)}`);
|
|
@@ -215,7 +242,7 @@ export async function runTaskCli(args: string[], client: TaskCliClient): Promise
|
|
|
215
242
|
}
|
|
216
243
|
case "start": {
|
|
217
244
|
if (!id || dependencyId) throw new Error("tasks start requires exactly one task id");
|
|
218
|
-
const artifact = await client.call<
|
|
245
|
+
const artifact = await client.call<Record<string, string>, CliArtifact>("tasks.start", { id, actor: "user", source: "cli" });
|
|
219
246
|
result = artifact;
|
|
220
247
|
human = `Started: ${artifactLabel(artifact)}`;
|
|
221
248
|
break;
|
|
@@ -226,11 +253,23 @@ export async function runTaskCli(args: string[], client: TaskCliClient): Promise
|
|
|
226
253
|
case "cancel": {
|
|
227
254
|
if (!id || dependencyId) throw new Error(`tasks ${action} requires exactly one task id`);
|
|
228
255
|
const operation = `tasks.${action}` as "tasks.submit" | "tasks.reject" | "tasks.retry" | "tasks.cancel";
|
|
229
|
-
const artifact = await client.call<
|
|
256
|
+
const artifact = await client.call<Record<string, string>, CliArtifact>(operation, { id, actor: "user", source: "cli" });
|
|
230
257
|
result = artifact;
|
|
231
258
|
human = `${action[0]!.toUpperCase()}${action.slice(1)}: ${artifactLabel(artifact)}`;
|
|
232
259
|
break;
|
|
233
260
|
}
|
|
261
|
+
case "automate": {
|
|
262
|
+
if (!id || (dependencyId !== "on" && dependencyId !== "off") || positional.length !== 3) throw new Error("tasks automate requires a task id and on or off");
|
|
263
|
+
const artifact = await client.call<Record<string, unknown>, CliArtifact>("tasks.set_automation", {
|
|
264
|
+
id,
|
|
265
|
+
enabled: dependencyId === "on",
|
|
266
|
+
actor: "user",
|
|
267
|
+
source: "cli",
|
|
268
|
+
});
|
|
269
|
+
result = artifact;
|
|
270
|
+
human = `Automation ${dependencyId}: ${artifactLabel(artifact)}`;
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
234
273
|
case "depend": {
|
|
235
274
|
if (!id || !dependencyId || positional.length !== 3) throw new Error("tasks depend requires a task id and prerequisite id");
|
|
236
275
|
const artifact = await client.call<{ id: string; dependency_id: string }, CliArtifact>("tasks.depend", {
|
|
@@ -242,7 +281,7 @@ export async function runTaskCli(args: string[], client: TaskCliClient): Promise
|
|
|
242
281
|
break;
|
|
243
282
|
}
|
|
244
283
|
default:
|
|
245
|
-
throw new Error("tasks action must be active, focus, graph, plan, complete, start, submit, reject, retry, cancel, or depend");
|
|
284
|
+
throw new Error("tasks action must be active, focus, graph, plan, history, complete, start, submit, reject, retry, cancel, automate, or depend");
|
|
246
285
|
}
|
|
247
286
|
return json ? JSON.stringify(result) : human;
|
|
248
287
|
}
|
|
@@ -255,6 +294,11 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
|
|
|
255
294
|
console.log(await runTaskCli(args.slice(1), client));
|
|
256
295
|
return;
|
|
257
296
|
}
|
|
297
|
+
if (command === "automation") {
|
|
298
|
+
const client = await connectPapyrusClient();
|
|
299
|
+
console.log(await runAutomationCli(args.slice(1), client));
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
258
302
|
if (command === "skills") {
|
|
259
303
|
const client = await connectPapyrusClient();
|
|
260
304
|
console.log(await runSkillCli(args.slice(1), client));
|
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 = 3;
|
|
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;
|
|
@@ -15,6 +15,7 @@ export const GATE_COMMAND_TIMEOUT_MS = 30_000;
|
|
|
15
15
|
export const GATE_TEST_TIMEOUT_MS = 60_000;
|
|
16
16
|
export const GATE_OUTPUT_LIMIT = 200;
|
|
17
17
|
export const GATE_MAX_BUFFER_BYTES = 1_048_576;
|
|
18
|
+
export const GATE_FILE_MAX_BYTES = 1_048_576;
|
|
18
19
|
|
|
19
20
|
/** Compact task-context limits keep recurring prompt injection bounded. */
|
|
20
21
|
export const TASK_CONTEXT_CURRENT_LIMIT = 3;
|
|
@@ -42,6 +43,25 @@ export const SKILL_RUN_ID_MAX_LENGTH = 64;
|
|
|
42
43
|
/** Bounded automatic Pi continuations while a focused Papyrus Task remains. */
|
|
43
44
|
export const TASK_DRIVER_MAX_TURNS = 20;
|
|
44
45
|
export const TASK_DRIVER_MAX_UNCHANGED_TURNS = 6;
|
|
46
|
+
/** Append-only Task chronology query and evidence bounds. */
|
|
47
|
+
export const TASK_HISTORY_DEFAULT_LIMIT = 25;
|
|
48
|
+
export const TASK_HISTORY_MAX_LIMIT = 100;
|
|
49
|
+
export const TASK_EVENT_MAX_EVIDENCE_BYTES = 65_536;
|
|
50
|
+
export const TASK_EVENT_ACTOR_MAX_LENGTH = 128;
|
|
51
|
+
export const TASK_EVENT_REASON_MAX_LENGTH = 2_000;
|
|
52
|
+
/** Explicitly opt-in supervised Task graph reconciliation bounds. */
|
|
53
|
+
export const TASK_AUTOMATION_INTERVAL_MS = 60_000;
|
|
54
|
+
export const TASK_AUTOMATION_MIN_INTERVAL_MS = 10_000;
|
|
55
|
+
export const TASK_AUTOMATION_MAX_INTERVAL_MS = 3_600_000;
|
|
56
|
+
export const TASK_AUTOMATION_MAX_TASKS_PER_SWEEP = 10;
|
|
57
|
+
export const TASK_AUTOMATION_HARD_MAX_TASKS_PER_SWEEP = 100;
|
|
58
|
+
export const TASK_AUTOMATION_GATE_CONCURRENCY = 1;
|
|
59
|
+
export const TASK_AUTOMATION_MAX_GATE_CONCURRENCY = 4;
|
|
60
|
+
export const TASK_AUTOMATION_MAX_RUNTIME_MS = 120_000;
|
|
61
|
+
export const TASK_AUTOMATION_HARD_MAX_RUNTIME_MS = 600_000;
|
|
62
|
+
export const TASK_AUTOMATION_MAX_CANDIDATE_SCAN = 1_000;
|
|
63
|
+
export const TASK_AUTOMATION_ERROR_ID_MAX_LENGTH = 128;
|
|
64
|
+
export const TASK_AUTOMATION_ERROR_MESSAGE_MAX_LENGTH = 500;
|
|
45
65
|
export const GRAPH_RENDER_PADDING_X = 2;
|
|
46
66
|
export const GRAPH_RENDER_PADDING_Y = 1;
|
|
47
67
|
export const GRAPH_RENDER_BOX_PADDING = 0;
|