@danypops/papyrus 0.11.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/extension/src/domain-tools.ts +1 -1
- package/package.json +1 -1
- package/src/cli.ts +16 -6
- package/src/domain/task-event.ts +1 -0
- package/src/service.ts +1 -0
- package/src/task-service.ts +33 -2
package/README.md
CHANGED
|
@@ -168,6 +168,7 @@ papyrus tasks pause --json
|
|
|
168
168
|
papyrus tasks unpause --json
|
|
169
169
|
papyrus tasks clear-focus --json
|
|
170
170
|
papyrus tasks update <id> --title "Revised title" --body "Revised body" --json
|
|
171
|
+
papyrus tasks update <id> --status todo --reason "created with legacy default" --json
|
|
171
172
|
papyrus tasks start <id> --json
|
|
172
173
|
papyrus tasks submit <id> --json
|
|
173
174
|
papyrus tasks complete <id> --json
|
|
@@ -201,7 +202,7 @@ Papyrus also injects an Alef-style reconciliation block at `before_agent_start`
|
|
|
201
202
|
|
|
202
203
|
After assembling each system-prompt addition, Papyrus emits a versioned `papyrus.context-injection.v1` observation on Pi's shared extension event bus. It contains only exact byte/character sizes, Rule count, a labeled token estimate, prompt share, sequence, and a SHA-256 payload fingerprint; Rule/Task text, prompts, project paths, and credentials are never included. Jittor can persist and assess these observations without Papyrus maintaining a second telemetry store.
|
|
203
204
|
|
|
204
|
-
Task edits mutate the existing Papyrus-owned Task identity and append an `updated` event; title, body, and labels can be revised without canceling the Task or creating a replacement. Lifecycle, relationships, gates, checklist metadata, scope, and focus remain intact.
|
|
205
|
+
Task edits mutate the existing Papyrus-owned Task identity and append an `updated` event; title, body, and labels can be revised without canceling the Task or creating a replacement. Lifecycle, relationships, gates, checklist metadata, scope, and focus remain intact. The same `update` action provides a narrowly guarded recovery for Tasks accidentally created terminal by a legacy default: `status=todo` requires an audit reason, cannot be combined with content edits, only applies when `created` is the sole lifecycle event, and appends `creation_recovered` rather than rewriting history.
|
|
205
206
|
|
|
206
207
|
## Why
|
|
207
208
|
|
|
@@ -32,7 +32,7 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
32
32
|
pi.registerTool({
|
|
33
33
|
name: "tasks",
|
|
34
34
|
label: "Tasks",
|
|
35
|
-
description: "Task domain tool. ACTIONS: create, update, list, show, history, scope, set_scope, assign_project, graph, plan, active, focused, focus, pause, unpause, clear_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.",
|
|
35
|
+
description: "Task domain tool. ACTIONS: create, update, list, show, history, scope, set_scope, assign_project, graph, plan, active, focused, focus, pause, unpause, clear_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. update can recover a Task accidentally created terminal by setting status=todo with a reason, but cannot rewrite legitimate lifecycle history. 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.",
|
|
36
36
|
parameters: Type.Object({
|
|
37
37
|
action: Type.String(),
|
|
38
38
|
id: Type.Optional(Type.String()),
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -75,7 +75,7 @@ const USAGE = `Usage:
|
|
|
75
75
|
papyrus tasks scope [project|all|graph <root-id>] [--json]
|
|
76
76
|
papyrus tasks assign-project <id> [project-root] [--json]
|
|
77
77
|
papyrus tasks focus <id> [--json]
|
|
78
|
-
papyrus tasks update <id> [--title <title>] [--body <body>] [--labels-json <json>] [--json]
|
|
78
|
+
papyrus tasks update <id> [--title <title>] [--body <body>] [--labels-json <json>] [--status todo --reason <reason>] [--json]
|
|
79
79
|
papyrus tasks complete <id> [--json]
|
|
80
80
|
papyrus tasks start <id> [--json]
|
|
81
81
|
papyrus tasks submit <id> [--json]
|
|
@@ -237,16 +237,21 @@ export async function runNoteCli(args: string[], client: TaskCliClient, projectR
|
|
|
237
237
|
export async function runTaskCli(args: string[], client: TaskCliClient, projectRoot: string = process.cwd()): Promise<string> {
|
|
238
238
|
const json = args.includes("--json");
|
|
239
239
|
const positional: string[] = [];
|
|
240
|
-
const updateInput: { title?: string; body?: string; labels?: string[] } = {};
|
|
240
|
+
const updateInput: { title?: string; body?: string; labels?: string[]; status?: "todo" } = {};
|
|
241
|
+
let reason: string | undefined;
|
|
241
242
|
for (let index = 0; index < args.length; index++) {
|
|
242
243
|
const argument = args[index]!;
|
|
243
244
|
if (argument === "--json") continue;
|
|
244
|
-
if (argument === "--title" || argument === "--body" || argument === "--labels-json") {
|
|
245
|
+
if (argument === "--title" || argument === "--body" || argument === "--labels-json" || argument === "--status" || argument === "--reason") {
|
|
245
246
|
const value = args[++index];
|
|
246
247
|
if (value === undefined) throw new Error(`${argument} requires a value`);
|
|
247
248
|
if (argument === "--title") updateInput.title = value;
|
|
248
249
|
else if (argument === "--body") updateInput.body = value;
|
|
249
|
-
else
|
|
250
|
+
else if (argument === "--reason") reason = value;
|
|
251
|
+
else if (argument === "--status") {
|
|
252
|
+
if (value !== "todo") throw new Error("--status only supports todo for accidental creation recovery");
|
|
253
|
+
updateInput.status = value;
|
|
254
|
+
} else {
|
|
250
255
|
const parsed = JSON.parse(value) as unknown;
|
|
251
256
|
if (!Array.isArray(parsed) || parsed.some((entry) => typeof entry !== "string")) throw new Error("--labels-json requires a JSON string array");
|
|
252
257
|
updateInput.labels = parsed as string[];
|
|
@@ -256,6 +261,7 @@ export async function runTaskCli(args: string[], client: TaskCliClient, projectR
|
|
|
256
261
|
positional.push(argument);
|
|
257
262
|
}
|
|
258
263
|
const [action, id, dependencyId] = positional;
|
|
264
|
+
if (reason !== undefined && action !== "update") throw new Error("--reason is only supported by tasks update");
|
|
259
265
|
let result: unknown;
|
|
260
266
|
let human: string;
|
|
261
267
|
switch (action) {
|
|
@@ -291,8 +297,12 @@ export async function runTaskCli(args: string[], client: TaskCliClient, projectR
|
|
|
291
297
|
}
|
|
292
298
|
case "update": {
|
|
293
299
|
if (!id || dependencyId) throw new Error("tasks update requires exactly one task id");
|
|
294
|
-
if (Object.keys(updateInput).length === 0) throw new Error("tasks update requires --title, --body,
|
|
295
|
-
|
|
300
|
+
if (Object.keys(updateInput).length === 0) throw new Error("tasks update requires --title, --body, --labels-json, or --status todo");
|
|
301
|
+
if (updateInput.status !== undefined && !reason?.trim()) throw new Error("tasks update --status requires --reason");
|
|
302
|
+
if (reason !== undefined && updateInput.status === undefined) throw new Error("tasks update --reason requires --status todo");
|
|
303
|
+
const artifact = await client.call<Record<string, unknown>, CliArtifact>("tasks.update", {
|
|
304
|
+
id, ...updateInput, ...(reason ? { reason } : {}), actor: "user", source: "cli",
|
|
305
|
+
});
|
|
296
306
|
result = artifact;
|
|
297
307
|
human = `Updated: ${artifactLabel(artifact)}`;
|
|
298
308
|
break;
|
package/src/domain/task-event.ts
CHANGED
package/src/service.ts
CHANGED
|
@@ -269,6 +269,7 @@ function handlers(
|
|
|
269
269
|
...(input["title"] !== undefined ? { title: optionalString(input, "title")! } : {}),
|
|
270
270
|
...(input["body"] !== undefined ? { body: optionalString(input, "body")! } : {}),
|
|
271
271
|
...(input["labels"] !== undefined ? { labels: optionalStringArray(input, "labels")! } : {}),
|
|
272
|
+
...(input["status"] !== undefined ? { status: string(input, "status") as "todo" } : {}),
|
|
272
273
|
}, eventContext(input)),
|
|
273
274
|
"tasks.list": (input) => tasks.list(taskFilter(input)),
|
|
274
275
|
"tasks.graph": (input) => tasks.graph(taskFilter(input)),
|
package/src/task-service.ts
CHANGED
|
@@ -24,6 +24,7 @@ export interface UpdateTaskInput {
|
|
|
24
24
|
title?: string;
|
|
25
25
|
body?: string;
|
|
26
26
|
labels?: string[];
|
|
27
|
+
status?: "todo";
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export interface TaskFilter {
|
|
@@ -148,7 +149,7 @@ export class Tasks {
|
|
|
148
149
|
title: input.title,
|
|
149
150
|
body: input.body,
|
|
150
151
|
subtype: input.subtype,
|
|
151
|
-
status: input.status,
|
|
152
|
+
status: input.status ?? "todo",
|
|
152
153
|
labels: input.labels,
|
|
153
154
|
extra,
|
|
154
155
|
templateId: input.templateId,
|
|
@@ -161,9 +162,39 @@ export class Tasks {
|
|
|
161
162
|
});
|
|
162
163
|
}
|
|
163
164
|
|
|
165
|
+
private recoverCreation(id: string, context: TaskEventContext): Artifact {
|
|
166
|
+
if (!context.reason?.trim()) throw new Error("creation recovery requires an audit reason");
|
|
167
|
+
return this.events.atomic(() => {
|
|
168
|
+
const task = this.require(id);
|
|
169
|
+
if (task.status !== "done" && task.status !== "canceled") throw new Error(`cannot recover task creation from ${task.status}`);
|
|
170
|
+
const history = this.events.history(id, { direction: "asc", limit: 2 });
|
|
171
|
+
const created = history.events[0];
|
|
172
|
+
if (history.events.length !== 1 || history.nextCursor !== undefined || created?.type !== "created" || created.toStatus !== task.status) {
|
|
173
|
+
throw new Error("task was not terminal at creation");
|
|
174
|
+
}
|
|
175
|
+
const recovered = this.artifacts.setStatus(id, "todo");
|
|
176
|
+
if (!recovered) throw new Error(`task "${id}" not found`);
|
|
177
|
+
this.appendEvent({
|
|
178
|
+
taskId: id,
|
|
179
|
+
type: "creation_recovered",
|
|
180
|
+
fromStatus: task.status as TaskStatus,
|
|
181
|
+
toStatus: "todo",
|
|
182
|
+
evidence: { result: "terminal-at-creation" },
|
|
183
|
+
}, context);
|
|
184
|
+
return recovered;
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
164
188
|
update(id: string, input: UpdateTaskInput, context: TaskEventContext = {}): Artifact {
|
|
189
|
+
if (input.status !== undefined) {
|
|
190
|
+
if (input.status !== "todo") throw new Error("task status updates only support recovering creation to todo");
|
|
191
|
+
if (input.title !== undefined || input.body !== undefined || input.labels !== undefined) {
|
|
192
|
+
throw new Error("task creation recovery cannot be combined with content updates");
|
|
193
|
+
}
|
|
194
|
+
return this.recoverCreation(id, context);
|
|
195
|
+
}
|
|
165
196
|
const fields = (["title", "body", "labels"] as const).filter((field) => input[field] !== undefined);
|
|
166
|
-
if (fields.length === 0) throw new Error("task update requires title, body, or labels");
|
|
197
|
+
if (fields.length === 0) throw new Error("task update requires title, body, or labels; status todo is only valid for creation recovery");
|
|
167
198
|
if (input.title !== undefined && (input.title.trim().length === 0 || input.title.length > TASK_TITLE_MAX_LENGTH)) {
|
|
168
199
|
throw new Error(`title must be between 1 and ${TASK_TITLE_MAX_LENGTH} characters`);
|
|
169
200
|
}
|