@danypops/papyrus 0.44.12 → 0.45.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/package.json +1 -1
- package/src/cli/task-command.ts +2 -1
- package/src/domain/task-event.ts +1 -0
- package/src/handlers/discuss.ts +13 -2
- package/src/handlers/tasks.ts +11 -2
- package/src/modules/tasks.ts +2 -0
- package/src/service.ts +1 -0
- package/src/task/task-service.ts +20 -4
package/package.json
CHANGED
package/src/cli/task-command.ts
CHANGED
|
@@ -747,7 +747,7 @@ const startCommand = buildCommand({
|
|
|
747
747
|
docs: { brief: "Lifecycle transition: todo -> in-progress" },
|
|
748
748
|
});
|
|
749
749
|
|
|
750
|
-
function buildSimpleTransitionCommand(action: "submit" | "reject" | "retry" | "cancel", brief: string) {
|
|
750
|
+
function buildSimpleTransitionCommand(action: "submit" | "reject" | "retry" | "cancel" | "reopen", brief: string) {
|
|
751
751
|
return buildCommand({
|
|
752
752
|
func: async function (this: TaskContext, flags: { sessionId?: string }, id: string) {
|
|
753
753
|
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>(`tasks.${action}` as OperationName, {
|
|
@@ -868,6 +868,7 @@ const app = buildApplication(
|
|
|
868
868
|
reject: buildSimpleTransitionCommand("reject", "Lifecycle transition: review -> rejected"),
|
|
869
869
|
retry: buildSimpleTransitionCommand("retry", "Lifecycle transition: rejected -> in-progress"),
|
|
870
870
|
cancel: buildSimpleTransitionCommand("cancel", "Lifecycle transition to canceled"),
|
|
871
|
+
reopen: buildSimpleTransitionCommand("reopen", "Lifecycle transition: canceled -> todo"),
|
|
871
872
|
"cancel-subtree": cancelSubtreeCommand,
|
|
872
873
|
depend: buildDependencyCommand(
|
|
873
874
|
"tasks.depend",
|
package/src/domain/task-event.ts
CHANGED
package/src/handlers/discuss.ts
CHANGED
|
@@ -17,6 +17,7 @@ import type { VehicleContentBlock } from "@danypops/vehicle-core";
|
|
|
17
17
|
import type { VehicleRegistry } from "@danypops/vehicle-server";
|
|
18
18
|
import type { Artifact } from "../artifact/artifact.ts";
|
|
19
19
|
import type { ArtifactStore } from "../artifact/artifact-store.ts";
|
|
20
|
+
import { DISCUSSION_OPTION_DESCRIPTION_MAX_LENGTH, DISCUSSION_OPTION_MAX_LENGTH, DISCUSSION_OPTIONS_MAX_COUNT } from "../constants.ts";
|
|
20
21
|
import type { DiscussionAndRounds, Discussions } from "../discussion/discussion-service.ts";
|
|
21
22
|
import { DISCUSSION_SUBTYPE, type DiscussionRound } from "../domain/discussion.ts";
|
|
22
23
|
import { discussOperations } from "../modules/discuss.ts";
|
|
@@ -118,6 +119,16 @@ function defaultActorToAgent(input: Record<string, unknown>): void {
|
|
|
118
119
|
|
|
119
120
|
const optionsUnionSchema = { type: "array" } as const;
|
|
120
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Shared suffix for open/reply's own description -- interpolates the real, enforced bounds
|
|
124
|
+
* (domain/discussion.ts's validateDiscussionOptions) rather than a hand-typed number that can
|
|
125
|
+
* silently drift out of sync with the actual limit. A prior version of this description stated
|
|
126
|
+
* the option count bound but not the per-option/per-description character bound at all --
|
|
127
|
+
* confirmed live: a caller had no way to know a 250-character description would be rejected
|
|
128
|
+
* until it already had been (see discuss-vehicle.test.ts's oversized-description regression).
|
|
129
|
+
*/
|
|
130
|
+
const OPTION_BOUNDS_TEXT = `Each option is at most ${DISCUSSION_OPTION_MAX_LENGTH} characters (up to ${DISCUSSION_OPTIONS_MAX_COUNT} total); each description is at most ${DISCUSSION_OPTION_DESCRIPTION_MAX_LENGTH} characters.`;
|
|
131
|
+
|
|
121
132
|
export function registerDiscussVehicleOperations(registry: VehicleRegistry, discussions: Discussions, artifacts: ArtifactStore): void {
|
|
122
133
|
const moduleOperations = new Map(discussOperations(discussions).map((op) => [op.name, op]));
|
|
123
134
|
const call = <Output>(name: string, input: Record<string, unknown>): Output => moduleOperations.get(name)!.execute(input) as Output;
|
|
@@ -152,7 +163,7 @@ export function registerDiscussVehicleOperations(registry: VehicleRegistry, disc
|
|
|
152
163
|
|
|
153
164
|
define(
|
|
154
165
|
"open",
|
|
155
|
-
|
|
166
|
+
`Opens a new Discussion and starts round 1. Optionally poses a structured choice via options (2-10 entries) + options_mode ('single' mutually exclusive, 'multi' allows several) -- each option a bare string (self-evident) or {title, description} (a real tradeoff worth spelling out; description REQUIRED once there are 3+ options). ${OPTION_BOUNDS_TEXT} Optionally blocks one or more Tasks immediately via blocks_task_ids/blocks_task_names. Pass live:true to get a human's answer synchronously in this same call, via an interactive prompt -- only takes effect with an interactive UI available, otherwise degrades silently to the normal durably-recorded round.`,
|
|
156
167
|
"local-write",
|
|
157
168
|
{
|
|
158
169
|
title: stringProp,
|
|
@@ -182,7 +193,7 @@ export function registerDiscussVehicleOperations(registry: VehicleRegistry, disc
|
|
|
182
193
|
|
|
183
194
|
define(
|
|
184
195
|
"reply",
|
|
185
|
-
|
|
196
|
+
`Adds a round to an existing Discussion. Refused once deferred or settled -- resume first. Answers a currently pending posed choice via \`selected\` (validated against it), or poses a new choice via options/options_mode. ${OPTION_BOUNDS_TEXT} Prefer \`name\` over \`id\`. Pass live:true to get a human's answer synchronously in this same call, via the pending choice's picker if one was posed, otherwise a freeform question -- only takes effect with an interactive UI available, otherwise degrades silently to the normal durably-recorded round.`,
|
|
186
197
|
"local-write",
|
|
187
198
|
{
|
|
188
199
|
id: stringProp,
|
package/src/handlers/tasks.ts
CHANGED
|
@@ -244,7 +244,7 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
244
244
|
|
|
245
245
|
define(
|
|
246
246
|
"update",
|
|
247
|
-
"Recovers an accidentally-terminal task via status=todo + reason, or changes title/body/labels, without rewriting real history. Never touches gates -- use set_gates.",
|
|
247
|
+
"Recovers an accidentally-terminal task via status=todo + reason (only a task whose status was terminal at its own creation -- not one that reached canceled/rejected through a real, later transition; use tasks.reopen for that), or changes title/body/labels, without rewriting real history. Never touches gates -- use set_gates.",
|
|
248
248
|
"local-write",
|
|
249
249
|
{
|
|
250
250
|
id: stringProp,
|
|
@@ -453,7 +453,16 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
453
453
|
);
|
|
454
454
|
define(
|
|
455
455
|
"cancel",
|
|
456
|
-
"Lifecycle transition to canceled (terminal) from todo/in-progress/review/rejected.",
|
|
456
|
+
"Lifecycle transition to canceled (terminal) from todo/in-progress/review/rejected. Reversible via tasks.reopen if this turns out to be premature.",
|
|
457
|
+
"local-write",
|
|
458
|
+
{ id: stringProp, name: stringProp, reason: stringProp, session_id: stringProp, project_root: stringProp },
|
|
459
|
+
[],
|
|
460
|
+
resolveIdAndScope,
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
define(
|
|
464
|
+
"reopen",
|
|
465
|
+
"Lifecycle transition: canceled -> todo. For a task legitimately canceled through a normal transition (e.g. a deliberate pause/park) that should resume -- distinct from tasks.update's status:todo path, which only recovers a task that was terminal at its own creation (a caller mistake), never one canceled/rejected later.",
|
|
457
466
|
"local-write",
|
|
458
467
|
{ id: stringProp, name: stringProp, reason: stringProp, session_id: stringProp, project_root: stringProp },
|
|
459
468
|
[],
|
package/src/modules/tasks.ts
CHANGED
|
@@ -86,6 +86,7 @@ export const TASKS_OPERATION_NAMES = [
|
|
|
86
86
|
"tasks.reject",
|
|
87
87
|
"tasks.retry",
|
|
88
88
|
"tasks.cancel",
|
|
89
|
+
"tasks.reopen",
|
|
89
90
|
"tasks.cancel_subtree",
|
|
90
91
|
"tasks.depend",
|
|
91
92
|
"tasks.undepend",
|
|
@@ -201,6 +202,7 @@ export function tasksOperations(tasks: Tasks, artifacts: ArtifactStore, sessionI
|
|
|
201
202
|
define("tasks.reject", (input: OperationInput) => tasks.transition(string(input, "id"), "reject", eventContext(input))),
|
|
202
203
|
define("tasks.retry", (input: OperationInput) => tasks.transition(string(input, "id"), "retry", eventContext(input))),
|
|
203
204
|
define("tasks.cancel", (input: OperationInput) => tasks.transition(string(input, "id"), "cancel", eventContext(input))),
|
|
205
|
+
define("tasks.reopen", (input: OperationInput) => tasks.transition(string(input, "id"), "reopen", eventContext(input))),
|
|
204
206
|
define("tasks.cancel_subtree", (input: OperationInput) => tasks.cancelSubtree(string(input, "id"), eventContext(input))),
|
|
205
207
|
define("tasks.depend", (input: OperationInput) =>
|
|
206
208
|
tasks.depend(string(input, "id"), string(input, "dependency_id"), eventContext(input)),
|
package/src/service.ts
CHANGED
|
@@ -371,6 +371,7 @@ function handlers(
|
|
|
371
371
|
"tasks.reject": forwardToModule("tasks.reject"),
|
|
372
372
|
"tasks.retry": forwardToModule("tasks.retry"),
|
|
373
373
|
"tasks.cancel": forwardToModule("tasks.cancel"),
|
|
374
|
+
"tasks.reopen": forwardToModule("tasks.reopen"),
|
|
374
375
|
"tasks.cancel_subtree": forwardToModule("tasks.cancel_subtree"),
|
|
375
376
|
"tasks.depend": forwardToModule("tasks.depend"),
|
|
376
377
|
"tasks.undepend": forwardToModule("tasks.undepend"),
|
package/src/task/task-service.ts
CHANGED
|
@@ -78,7 +78,7 @@ export interface CreateTaskInput {
|
|
|
78
78
|
projectSource?: TaskScopeSource;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
export type TaskTransition = "start" | "submit" | "reject" | "retry" | "cancel";
|
|
81
|
+
export type TaskTransition = "start" | "submit" | "reject" | "retry" | "cancel" | "reopen";
|
|
82
82
|
|
|
83
83
|
export interface TaskBlockage {
|
|
84
84
|
artifact: Artifact;
|
|
@@ -134,6 +134,17 @@ const TASK_TRANSITIONS: Record<TaskTransition, { from: TaskStatus[]; to: TaskSta
|
|
|
134
134
|
reject: { from: ["review"], to: "rejected" },
|
|
135
135
|
retry: { from: ["rejected"], to: "in-progress" },
|
|
136
136
|
cancel: { from: ["todo", "in-progress", "review", "rejected"], to: "canceled" },
|
|
137
|
+
/**
|
|
138
|
+
* Distinct from tasks.update's status:todo path (recoverCreation, below): that path only ever
|
|
139
|
+
* recovers a task whose entire history is a single "created" event already at a terminal status
|
|
140
|
+
* (a creation-time mistake) -- it deliberately refuses a task that reached canceled through a
|
|
141
|
+
* real, later transition. reopen is the missing counterpart for exactly that case: a task
|
|
142
|
+
* legitimately canceled (e.g. a deliberate "pause/park", since there is no direct
|
|
143
|
+
* in-progress -> todo transition) can be brought back to todo and driven through the normal
|
|
144
|
+
* lifecycle again, without rewriting its real history the way recoverCreation's own
|
|
145
|
+
* terminal-at-creation check exists to prevent.
|
|
146
|
+
*/
|
|
147
|
+
reopen: { from: ["canceled"], to: "todo" },
|
|
137
148
|
};
|
|
138
149
|
|
|
139
150
|
export class Tasks {
|
|
@@ -511,9 +522,14 @@ export class Tasks {
|
|
|
511
522
|
this.focusStore.set(id, context.sessionId);
|
|
512
523
|
}
|
|
513
524
|
const updated = this.artifacts.setStatus(id, transition.to)!;
|
|
514
|
-
const eventType = {
|
|
515
|
-
|
|
516
|
-
|
|
525
|
+
const eventType = {
|
|
526
|
+
start: "started",
|
|
527
|
+
submit: "submitted",
|
|
528
|
+
reject: "review_rejected",
|
|
529
|
+
retry: "retried",
|
|
530
|
+
cancel: "canceled",
|
|
531
|
+
reopen: "reopened",
|
|
532
|
+
}[action] as AppendTaskEvent["type"];
|
|
517
533
|
this.appendEvent({ taskId: id, type: eventType, fromStatus: task.status as TaskStatus, toStatus: transition.to }, context);
|
|
518
534
|
if (action === "start" || action === "retry") this.propagateProgressToAncestors(id, context);
|
|
519
535
|
if (action === "retry") this.focusStore.set(id, context.sessionId);
|