@danypops/papyrus 0.21.1 → 0.21.2
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.ts +10 -1
- package/src/domain/gate.ts +34 -1
- package/src/modules/tasks.ts +2 -1
- package/src/service.ts +1 -0
- package/src/task-service.ts +12 -2
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -157,6 +157,7 @@ const USAGE = `Usage:
|
|
|
157
157
|
papyrus tasks show <id> [--json]
|
|
158
158
|
papyrus tasks run-gates <id> [--json]
|
|
159
159
|
papyrus tasks set-checklist <id> --checklist-json <json> [--json]
|
|
160
|
+
papyrus tasks set-gates <id> --gates-json <json> [--json]
|
|
160
161
|
papyrus tasks context [--scope <project|graph|all>] [--root-task-id <id>] [--session-id <id>] [--json]
|
|
161
162
|
|
|
162
163
|
A "--session-id" scopes Task Focus to one agent session; omit it to use the shared "global" Focus (today's behavior).
|
|
@@ -1322,6 +1323,14 @@ export async function runTaskCli(args: string[], client: TaskCliClient, projectR
|
|
|
1322
1323
|
human = `Updated checklist: ${artifactLabel(artifact)}`;
|
|
1323
1324
|
break;
|
|
1324
1325
|
}
|
|
1326
|
+
case "set-gates": {
|
|
1327
|
+
if (!id || dependencyId) throw new Error("tasks set-gates requires exactly one task id");
|
|
1328
|
+
if (!gates) throw new Error("tasks set-gates requires --gates-json");
|
|
1329
|
+
const artifact = await client.call<Record<string, unknown>, CliArtifact>("tasks.set_gates", { id, gates });
|
|
1330
|
+
result = artifact;
|
|
1331
|
+
human = `Updated gates: ${artifactLabel(artifact)}`;
|
|
1332
|
+
break;
|
|
1333
|
+
}
|
|
1325
1334
|
case "context": {
|
|
1326
1335
|
if (id) throw new Error("tasks context accepts no positional arguments");
|
|
1327
1336
|
const summary = await client.call<Record<string, unknown>, string | null>("tasks.context", {
|
|
@@ -1478,7 +1487,7 @@ export async function runTaskCli(args: string[], client: TaskCliClient, projectR
|
|
|
1478
1487
|
break;
|
|
1479
1488
|
}
|
|
1480
1489
|
default:
|
|
1481
|
-
throw new Error("tasks action must be create, list, show, active, focused, focus, pause, unpause, clear-focus, update, graph, plan, context, history, scope, assign-project, complete, start, submit, reject, retry, cancel, depend, undepend, contain, uncontain, run-gates, or set-
|
|
1490
|
+
throw new Error("tasks action must be create, list, show, active, focused, focus, pause, unpause, clear-focus, update, graph, plan, context, history, scope, assign-project, complete, start, submit, reject, retry, cancel, depend, undepend, contain, uncontain, run-gates, set-checklist, or set-gates");
|
|
1482
1491
|
}
|
|
1483
1492
|
return json ? JSON.stringify(result) : human;
|
|
1484
1493
|
}
|
package/src/domain/gate.ts
CHANGED
|
@@ -1,9 +1,42 @@
|
|
|
1
|
+
export const GATE_TYPES = ["file-exists", "command", "contains", "test"] as const;
|
|
2
|
+
export type GateType = typeof GATE_TYPES[number];
|
|
3
|
+
|
|
1
4
|
export interface Gate {
|
|
2
|
-
type:
|
|
5
|
+
type: GateType;
|
|
3
6
|
target: string;
|
|
4
7
|
expect?: string;
|
|
5
8
|
}
|
|
6
9
|
|
|
10
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
11
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Validates a Gate[] the same way validateChecklist validates a Checklist -- gates previously had
|
|
16
|
+
* no validation at all (create() assigned `input.gates` to extra verbatim), and no way to change
|
|
17
|
+
* them after creation except by re-typing the whole task, silently accepted by "tasks update"
|
|
18
|
+
* (which only ever reads title/body/labels/status) as if it had worked. See Tasks.setGates.
|
|
19
|
+
*/
|
|
20
|
+
export function validateGates(value: unknown): Gate[] {
|
|
21
|
+
if (!Array.isArray(value)) throw new Error("gates must be an array");
|
|
22
|
+
return value.map((entry, index) => {
|
|
23
|
+
if (!isRecord(entry) || !GATE_TYPES.includes(entry["type"] as GateType)) {
|
|
24
|
+
throw new Error(`gate at index ${index} requires a valid type (${GATE_TYPES.join(", ")})`);
|
|
25
|
+
}
|
|
26
|
+
if (typeof entry["target"] !== "string" || entry["target"].trim().length === 0) {
|
|
27
|
+
throw new Error(`gate at index ${index} requires a non-empty target`);
|
|
28
|
+
}
|
|
29
|
+
if (entry["expect"] !== undefined && typeof entry["expect"] !== "string") {
|
|
30
|
+
throw new Error(`gate at index ${index} expect must be a string`);
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
type: entry["type"] as GateType,
|
|
34
|
+
target: entry["target"],
|
|
35
|
+
...(typeof entry["expect"] === "string" ? { expect: entry["expect"] } : {}),
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
7
40
|
export interface GateRunOptions {
|
|
8
41
|
/** Absolute Unix epoch deadline for the full gate sequence. */
|
|
9
42
|
deadlineMs?: number;
|
package/src/modules/tasks.ts
CHANGED
|
@@ -89,7 +89,7 @@ export const TASKS_OPERATION_NAMES = [
|
|
|
89
89
|
"tasks.create", "tasks.update", "tasks.list", "tasks.graph", "tasks.plan", "tasks.show", "tasks.history",
|
|
90
90
|
"tasks.scope", "tasks.set_scope", "tasks.assign_project", "tasks.active", "tasks.focused", "tasks.focus",
|
|
91
91
|
"tasks.pause", "tasks.unpause", "tasks.clear_focus", "tasks.start", "tasks.submit", "tasks.complete",
|
|
92
|
-
"tasks.run_gates", "tasks.set_checklist", "tasks.context", "tasks.reject", "tasks.retry", "tasks.cancel",
|
|
92
|
+
"tasks.run_gates", "tasks.set_checklist", "tasks.set_gates", "tasks.context", "tasks.reject", "tasks.retry", "tasks.cancel",
|
|
93
93
|
"tasks.depend", "tasks.undepend", "tasks.contain", "tasks.uncontain", "tasks.reap_stale_focus",
|
|
94
94
|
] as const;
|
|
95
95
|
|
|
@@ -157,6 +157,7 @@ export function tasksOperations(tasks: Tasks, artifacts: ArtifactStore, sessionI
|
|
|
157
157
|
define("tasks.complete", (input: OperationInput) => tasks.completeAsync(string(input, "id"), eventContext(input))),
|
|
158
158
|
define("tasks.run_gates", (input: OperationInput) => tasks.runGates(string(input, "id"), eventContext(input))),
|
|
159
159
|
define("tasks.set_checklist", (input: OperationInput) => tasks.setChecklist(string(input, "id"), input["checklist"] as Checklist)),
|
|
160
|
+
define("tasks.set_gates", (input: OperationInput) => tasks.setGates(string(input, "id"), input["gates"] as Parameters<Tasks["setGates"]>[1])),
|
|
160
161
|
define("tasks.context", (input: OperationInput) => taskContext(
|
|
161
162
|
artifacts,
|
|
162
163
|
tasks.active(taskFilter(input))?.id,
|
package/src/service.ts
CHANGED
|
@@ -323,6 +323,7 @@ function handlers(
|
|
|
323
323
|
"tasks.complete": forwardToModule("tasks.complete"),
|
|
324
324
|
"tasks.run_gates": forwardToModule("tasks.run_gates"),
|
|
325
325
|
"tasks.set_checklist": forwardToModule("tasks.set_checklist"),
|
|
326
|
+
"tasks.set_gates": forwardToModule("tasks.set_gates"),
|
|
326
327
|
"tasks.context": forwardToModule("tasks.context"),
|
|
327
328
|
"tasks.reject": forwardToModule("tasks.reject"),
|
|
328
329
|
"tasks.retry": forwardToModule("tasks.retry"),
|
package/src/task-service.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
import type { Artifact } from "./domain/artifact.ts";
|
|
13
13
|
import { checklistEntries, validateChecklist, type Checklist, type ProofReference } from "./domain/checklist.ts";
|
|
14
14
|
import { isDiscussionArtifact, readDiscussionExtra } from "./domain/discussion.ts";
|
|
15
|
-
import type
|
|
15
|
+
import { validateGates, type Gate, type GateResult } from "./domain/gate.ts";
|
|
16
16
|
import type { AppendTaskEvent, TaskEventContext, TaskHistoryPage, TaskHistoryQuery, TaskLifecycleStatus } from "./domain/task-event.ts";
|
|
17
17
|
import { normalizeProjectRoot, taskScopeLabel, type TaskScopeSource, type TaskViewMode, type TaskViewSelection } from "./domain/task-scope.ts";
|
|
18
18
|
import type { ArtifactStore } from "./ports/artifact-store.ts";
|
|
@@ -141,7 +141,7 @@ export class Tasks {
|
|
|
141
141
|
if (input.parentId) this.require(input.parentId);
|
|
142
142
|
for (const dependency of input.dependsOn ?? []) this.require(dependency);
|
|
143
143
|
const extra: Record<string, unknown> = { ...(input.extra ?? {}) };
|
|
144
|
-
if (input.gates !== undefined) extra["gates"] = input.gates;
|
|
144
|
+
if (input.gates !== undefined) extra["gates"] = validateGates(input.gates);
|
|
145
145
|
if (input.checklist !== undefined) extra["checklist"] = validateChecklist(input.checklist);
|
|
146
146
|
const projectRoot = input.projectRoot === undefined ? undefined : normalizeProjectRoot(input.projectRoot);
|
|
147
147
|
if (input.parentId && this.scopes.get(input.parentId)?.projectRoot !== projectRoot) {
|
|
@@ -459,6 +459,16 @@ export class Tasks {
|
|
|
459
459
|
return this.artifacts.setExtra(id, { ...task.extra, checklist: validateChecklist(checklist) })!;
|
|
460
460
|
}
|
|
461
461
|
|
|
462
|
+
/**
|
|
463
|
+
* The only way to change a task's gates after creation. "tasks update" (title/body/labels/
|
|
464
|
+
* status only) silently ignored a `gates` field with no error at all -- a real incident (see
|
|
465
|
+
* GateRunOptions.cwd's doc comment for the crash this masked while debugging).
|
|
466
|
+
*/
|
|
467
|
+
setGates(id: string, gates: Gate[]): Artifact {
|
|
468
|
+
const task = this.require(id);
|
|
469
|
+
return this.artifacts.setExtra(id, { ...task.extra, gates: validateGates(gates) })!;
|
|
470
|
+
}
|
|
471
|
+
|
|
462
472
|
depend(id: string, dependencyId: string, context: TaskEventContext = {}): Artifact {
|
|
463
473
|
return this.events.atomic(() => {
|
|
464
474
|
this.require(id);
|