@danypops/papyrus 0.45.3 → 0.46.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 +10 -0
- package/package.json +1 -1
- package/src/authority-registry.ts +2 -1
- package/src/cli/task-command.ts +89 -4
- package/src/constants.ts +6 -1
- package/src/db.ts +68 -1
- package/src/docs/docs-service.ts +192 -0
- package/src/domain/task-lease.ts +6 -0
- package/src/domain/task-scope.ts +16 -0
- package/src/domain-service-shared.ts +162 -0
- package/src/handlers/docs.ts +1 -1
- package/src/handlers/playbooks.ts +3 -1
- package/src/handlers/rules.ts +5 -3
- package/src/handlers/shared.ts +16 -1
- package/src/handlers/tasks.ts +129 -14
- package/src/index.ts +1 -1
- package/src/modules/docs.ts +2 -2
- package/src/modules/notes.ts +2 -2
- package/src/modules/playbooks.ts +6 -4
- package/src/modules/rules.ts +4 -2
- package/src/modules/tasks.ts +19 -0
- package/src/playbook/playbook-definition.ts +1 -1
- package/src/{domain-services.ts → playbook/playbook-service.ts} +53 -453
- package/src/rules/rules-service.ts +224 -0
- package/src/service.ts +7 -2
- package/src/stores/sqlite-task-create-request-store.ts +47 -0
- package/src/stores/sqlite-task-scope-store.ts +154 -15
- package/src/stores/task-create-request-store.ts +45 -0
- package/src/stores/task-scope-store.ts +91 -1
- package/src/task/task-service.ts +131 -13
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared, kind-agnostic CRUD composition helpers used by docs/docs-service.ts,
|
|
3
|
+
* rules/rules-service.ts, and playbook/playbook-service.ts. Split out of the former
|
|
4
|
+
* domain-services.ts (which combined all three domains in one 849-line file) so each
|
|
5
|
+
* domain's own service file only imports the cross-domain pieces it actually needs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Artifact } from "./artifact/artifact.ts";
|
|
9
|
+
import type { ArtifactEventContext } from "./artifact/artifact-event.ts";
|
|
10
|
+
import type { ArtifactScopeStore } from "./artifact/artifact-scope-store.ts";
|
|
11
|
+
import type { ArtifactStore } from "./artifact/artifact-store.ts";
|
|
12
|
+
import {
|
|
13
|
+
ARTIFACT_BODY_MAX_LENGTH,
|
|
14
|
+
ARTIFACT_LABEL_MAX_COUNT,
|
|
15
|
+
ARTIFACT_LABEL_MAX_LENGTH,
|
|
16
|
+
ARTIFACT_SCOPE_MAX_ARTIFACTS,
|
|
17
|
+
ARTIFACT_TITLE_MAX_LENGTH,
|
|
18
|
+
} from "./constants.ts";
|
|
19
|
+
import { normalizeProjectRoot } from "./domain/task-scope.ts";
|
|
20
|
+
|
|
21
|
+
export interface UpdateContentInput {
|
|
22
|
+
title?: string;
|
|
23
|
+
body?: string;
|
|
24
|
+
labels?: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function requireContentUpdateFields(input: UpdateContentInput): void {
|
|
28
|
+
if (input.title === undefined && input.body === undefined && input.labels === undefined) {
|
|
29
|
+
throw new Error("update requires title, body, or labels");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function assertTitleBounds(title: string | undefined): void {
|
|
34
|
+
if (title !== undefined && (title.trim().length === 0 || title.length > ARTIFACT_TITLE_MAX_LENGTH)) {
|
|
35
|
+
throw new Error(`title must be between 1 and ${ARTIFACT_TITLE_MAX_LENGTH} characters`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function assertBodyBounds(body: string | undefined): void {
|
|
40
|
+
if (body !== undefined && body.length > ARTIFACT_BODY_MAX_LENGTH)
|
|
41
|
+
throw new Error(`body cannot exceed ${ARTIFACT_BODY_MAX_LENGTH} characters`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function assertLabelsBounds(labels: string[] | undefined): void {
|
|
45
|
+
if (labels === undefined) return;
|
|
46
|
+
if (labels.length > ARTIFACT_LABEL_MAX_COUNT) throw new Error(`labels cannot exceed ${ARTIFACT_LABEL_MAX_COUNT} entries`);
|
|
47
|
+
if (labels.some((label) => label.length === 0 || label.length > ARTIFACT_LABEL_MAX_LENGTH)) {
|
|
48
|
+
throw new Error(`each label must be between 1 and ${ARTIFACT_LABEL_MAX_LENGTH} characters`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ListFilter {
|
|
53
|
+
status?: string;
|
|
54
|
+
text?: string;
|
|
55
|
+
limit?: number;
|
|
56
|
+
/** When supplied, results are limited to artifacts scoped to this project (or the unscoped bucket, for an empty string is not accepted -- use assignArtifactProject's own validation). */
|
|
57
|
+
projectRoot?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Shared by listDocuments/listRules/listPlaybooks: when filter.projectRoot is given, resolve
|
|
62
|
+
* via ArtifactScopeStore first and post-filter by kind/status/text (mirrors Tasks.list's
|
|
63
|
+
* established scoped-listing shape); otherwise fall back to the existing unscoped query
|
|
64
|
+
* path unchanged, so every caller that predates project scoping keeps working exactly as
|
|
65
|
+
* before.
|
|
66
|
+
*/
|
|
67
|
+
export function listScoped(
|
|
68
|
+
artifacts: ArtifactStore,
|
|
69
|
+
scopes: ArtifactScopeStore,
|
|
70
|
+
kind: string,
|
|
71
|
+
filter: ListFilter,
|
|
72
|
+
excludeSubtype?: string,
|
|
73
|
+
): Artifact[] {
|
|
74
|
+
if (filter.projectRoot === undefined)
|
|
75
|
+
return artifacts.query({ kind, excludeSubtype, status: filter.status, text: filter.text, limit: filter.limit });
|
|
76
|
+
const limit = filter.limit ?? ARTIFACT_SCOPE_MAX_ARTIFACTS;
|
|
77
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > ARTIFACT_SCOPE_MAX_ARTIFACTS) {
|
|
78
|
+
throw new Error(`list limit must be between 1 and ${ARTIFACT_SCOPE_MAX_ARTIFACTS}`);
|
|
79
|
+
}
|
|
80
|
+
const projectRoot = normalizeProjectRoot(filter.projectRoot);
|
|
81
|
+
const ids = scopes.ids(projectRoot, ARTIFACT_SCOPE_MAX_ARTIFACTS);
|
|
82
|
+
const text = filter.text?.toLowerCase();
|
|
83
|
+
return ids
|
|
84
|
+
.map((id) => artifacts.get(id))
|
|
85
|
+
.filter((artifact): artifact is Artifact => artifact?.kind === kind && artifact.subtype !== excludeSubtype)
|
|
86
|
+
.filter((artifact) => filter.status === undefined || artifact.status === filter.status)
|
|
87
|
+
.filter((artifact) => text === undefined || artifact.title.toLowerCase().includes(text) || artifact.body.toLowerCase().includes(text))
|
|
88
|
+
.sort((left, right) => right.updated_at.localeCompare(left.updated_at) || left.id.localeCompare(right.id))
|
|
89
|
+
.slice(0, limit);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function requireKind(artifacts: ArtifactStore, id: string, kind: string): Artifact {
|
|
93
|
+
const artifact = artifacts.get(id);
|
|
94
|
+
if (!artifact) throw new Error(`${kind} artifact "${id}" not found`);
|
|
95
|
+
if (artifact.kind !== kind) throw new Error(`artifact "${id}" is not a ${kind}`);
|
|
96
|
+
return artifact;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Shared shape for a kind's own declarative status-transition table -- Document and Task
|
|
101
|
+
* already independently converged on this exact shape; Rule/Playbook used an inline ternary
|
|
102
|
+
* instead only because they happened to have just two states. See validateTransitionFrom's
|
|
103
|
+
* own comment for why this is split from runTransition rather than always combined.
|
|
104
|
+
*/
|
|
105
|
+
export type TransitionTable<Action extends string, Status extends string> = Record<Action, { from: Status[]; to: Status }>;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The from/to-table lookup+validation half of a transition, split out from runTransition
|
|
109
|
+
* (below) so a caller with its own side effects gated on "is this action even valid from the
|
|
110
|
+
* current status" (e.g. Tasks.transition's dependency-blocking check and focus-store
|
|
111
|
+
* bookkeeping, which must run -- or not -- before the status write itself) can call this
|
|
112
|
+
* directly instead of duplicating the same lookup+throw three times. Every other caller
|
|
113
|
+
* (Document/Rule/Playbook, none of which have that ordering constraint) uses runTransition
|
|
114
|
+
* instead, which does this same check plus the write in one call.
|
|
115
|
+
*/
|
|
116
|
+
export function validateTransitionFrom<Action extends string, Status extends string>(
|
|
117
|
+
kind: string,
|
|
118
|
+
action: Action,
|
|
119
|
+
status: string,
|
|
120
|
+
table: TransitionTable<Action, Status>,
|
|
121
|
+
): { from: Status[]; to: Status } {
|
|
122
|
+
const transition = table[action];
|
|
123
|
+
if (!transition.from.includes(status as Status)) throw new Error(`cannot ${action} ${kind} from ${status}`);
|
|
124
|
+
return transition;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Shared by transitionDocument/transitionRule/transitionPlaybook: validates the requested
|
|
129
|
+
* action is legal from the artifact's current status per its own kind's transition table, then
|
|
130
|
+
* writes the resulting status. Any authority/ownership guard (e.g. requireMutableDocument,
|
|
131
|
+
* requireLocallyOwnedContent) is the caller's own responsibility, resolved on `artifact`
|
|
132
|
+
* *before* calling this -- kept out of this primitive so it stays usable by a kind with no
|
|
133
|
+
* such guard (Rule, Playbook) without a no-op parameter every call site has to pass.
|
|
134
|
+
*/
|
|
135
|
+
export function runTransition<Action extends string, Status extends string>(
|
|
136
|
+
artifacts: ArtifactStore,
|
|
137
|
+
artifact: Artifact,
|
|
138
|
+
kind: string,
|
|
139
|
+
action: Action,
|
|
140
|
+
table: TransitionTable<Action, Status>,
|
|
141
|
+
context?: ArtifactEventContext,
|
|
142
|
+
): Artifact {
|
|
143
|
+
const transition = validateTransitionFrom(kind, action, artifact.status, table);
|
|
144
|
+
return artifacts.setStatus(artifact.id, transition.to, context)!;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Shared by assignRuleProject/assignPlaybookProject. assignDocumentProject has its own body -- it must reject Notes via requireDocument, not the generic requireKind this helper uses. */
|
|
148
|
+
export function assignArtifactProject(
|
|
149
|
+
artifacts: ArtifactStore,
|
|
150
|
+
scopes: ArtifactScopeStore,
|
|
151
|
+
id: string,
|
|
152
|
+
kind: string,
|
|
153
|
+
projectRoot: string | undefined,
|
|
154
|
+
): Artifact {
|
|
155
|
+
requireKind(artifacts, id, kind);
|
|
156
|
+
scopes.assign(
|
|
157
|
+
id,
|
|
158
|
+
projectRoot === undefined ? undefined : normalizeProjectRoot(projectRoot),
|
|
159
|
+
projectRoot === undefined ? "unscoped" : "explicit",
|
|
160
|
+
);
|
|
161
|
+
return artifacts.get(id)!;
|
|
162
|
+
}
|
package/src/handlers/docs.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { VehicleRegistry } from "@danypops/vehicle-server";
|
|
|
7
7
|
import type { ArtifactScopeStore } from "../artifact/artifact-scope-store.ts";
|
|
8
8
|
import type { ArtifactStore } from "../artifact/artifact-store.ts";
|
|
9
9
|
import type { AuthorityRegistry } from "../authority-registry.ts";
|
|
10
|
-
import { listDocuments } from "../
|
|
10
|
+
import { listDocuments } from "../docs/docs-service.ts";
|
|
11
11
|
import { docsOperations } from "../modules/docs.ts";
|
|
12
12
|
import { booleanProp, createOperationDefiner, numberProp, resolveArtifactIdWidened, stringProp, validationError } from "./shared.ts";
|
|
13
13
|
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
import type { VehicleRegistry } from "@danypops/vehicle-server";
|
|
22
22
|
import type { ArtifactScopeStore } from "../artifact/artifact-scope-store.ts";
|
|
23
23
|
import type { ArtifactStore } from "../artifact/artifact-store.ts";
|
|
24
|
-
import { listPlaybooks } from "../domain-services.ts";
|
|
25
24
|
import { playbooksOperations } from "../modules/playbooks.ts";
|
|
26
25
|
import type { PlaybookInvocationResult, PlaybookMissingArguments } from "../playbook/playbook-execution.ts";
|
|
26
|
+
import { listPlaybooks } from "../playbook/playbook-service.ts";
|
|
27
27
|
import type { SessionIdentity } from "../session-identity/session-identity-service.ts";
|
|
28
28
|
import type { TaskEventStore } from "../stores/task-event-store.ts";
|
|
29
29
|
import type { TaskScopeStore } from "../stores/task-scope-store.ts";
|
|
@@ -92,8 +92,10 @@ export function registerPlaybooksVehicleOperations(registry: VehicleRegistry, de
|
|
|
92
92
|
steps: { type: "array" },
|
|
93
93
|
tools: { type: "array" },
|
|
94
94
|
arguments: { type: "array" },
|
|
95
|
+
subtype: stringProp,
|
|
95
96
|
labels: { type: "array" },
|
|
96
97
|
extra: { type: "object" },
|
|
98
|
+
template_id: stringProp,
|
|
97
99
|
project_root: stringProp,
|
|
98
100
|
actor: stringProp,
|
|
99
101
|
source: stringProp,
|
package/src/handlers/rules.ts
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
import type { VehicleRegistry } from "@danypops/vehicle-server";
|
|
8
8
|
import type { ArtifactScopeStore } from "../artifact/artifact-scope-store.ts";
|
|
9
9
|
import type { ArtifactStore } from "../artifact/artifact-store.ts";
|
|
10
|
-
import { listRules } from "../domain-services.ts";
|
|
11
10
|
import { rulesOperations } from "../modules/rules.ts";
|
|
11
|
+
import { listRules } from "../rules/rules-service.ts";
|
|
12
12
|
import { booleanProp, createOperationDefiner, numberProp, resolveArtifactIdWidened, stringProp, validationError } from "./shared.ts";
|
|
13
13
|
|
|
14
14
|
const OWNER = "rules";
|
|
@@ -49,7 +49,7 @@ export function registerRulesVehicleOperations(registry: VehicleRegistry, artifa
|
|
|
49
49
|
|
|
50
50
|
define(
|
|
51
51
|
"create",
|
|
52
|
-
"Creates a Rule -- a standing constraint injected into the agent system prompt while active. project_root is optional (omitted = unscoped). The response includes combinedLength (condition+action+body character count) and a non-blocking warning once it exceeds the ~600-character soft target (hard-rejected past 4000).",
|
|
52
|
+
"Creates a Rule -- a standing constraint injected into the agent system prompt while active. Plain Rules start active; a template_id creates an inert draft that must pass the template's completionRequired fields through rules.enable. project_root is optional (omitted = unscoped). The response includes combinedLength (condition+action+body character count) and a non-blocking warning once it exceeds the ~600-character soft target (hard-rejected past 4000).",
|
|
53
53
|
"local-write",
|
|
54
54
|
{
|
|
55
55
|
title: stringProp,
|
|
@@ -57,8 +57,10 @@ export function registerRulesVehicleOperations(registry: VehicleRegistry, artifa
|
|
|
57
57
|
condition: stringProp,
|
|
58
58
|
rule_action: stringProp,
|
|
59
59
|
severity: { type: "string", enum: ["block", "warn", "info"] },
|
|
60
|
+
subtype: stringProp,
|
|
60
61
|
labels: { type: "array" } as unknown as { type: string },
|
|
61
62
|
extra: { type: "object" } as unknown as { type: string },
|
|
63
|
+
template_id: stringProp,
|
|
62
64
|
project_root: stringProp,
|
|
63
65
|
actor: stringProp,
|
|
64
66
|
source: stringProp,
|
|
@@ -100,7 +102,7 @@ export function registerRulesVehicleOperations(registry: VehicleRegistry, artifa
|
|
|
100
102
|
|
|
101
103
|
define(
|
|
102
104
|
"enable",
|
|
103
|
-
"Enables a Rule so it starts injecting into the agent system prompt.",
|
|
105
|
+
"Enables a Rule so it starts injecting into the agent system prompt. A template-derived draft is enabled only after every field path in its source template's completionRequired array is present.",
|
|
104
106
|
"local-write",
|
|
105
107
|
{ id: stringProp, name: stringProp, project_root: stringProp, actor: stringProp, source: stringProp, session_id: stringProp },
|
|
106
108
|
[],
|
package/src/handlers/shared.ts
CHANGED
|
@@ -18,6 +18,7 @@ import type { Artifact } from "../artifact/artifact.ts";
|
|
|
18
18
|
import type { ArtifactStore } from "../artifact/artifact-store.ts";
|
|
19
19
|
import { PlaybookCompositionError } from "../playbook/playbook-definition.ts";
|
|
20
20
|
import { InvalidSessionSecretError } from "../session-identity/session-identity-service.ts";
|
|
21
|
+
import { TaskCreateIdempotencyConflictError } from "../stores/task-create-request-store.ts";
|
|
21
22
|
import { TaskDependencyCycleError, TaskExecutionBoundExceededError, type TaskExecutionPlan } from "../task/task-execution.ts";
|
|
22
23
|
|
|
23
24
|
/**
|
|
@@ -112,6 +113,17 @@ export function classifyTaskExecutionBounds<T>(run: () => T): T {
|
|
|
112
113
|
}
|
|
113
114
|
}
|
|
114
115
|
|
|
116
|
+
export function classifyTaskCreateIdempotency<T>(run: () => T): T {
|
|
117
|
+
try {
|
|
118
|
+
return run();
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (error instanceof TaskCreateIdempotencyConflictError) {
|
|
121
|
+
throw new VehicleError("idempotency-key-conflict", error.message, { category: "conflict" });
|
|
122
|
+
}
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
115
127
|
/** A self-dependency or dependency-cycle rejection (tasks.depend/undepend/create) is an ordinary, expected validation failure, not an unexpected crash. */
|
|
116
128
|
export function classifyTaskDependencyCycles<T>(run: () => T): T {
|
|
117
129
|
try {
|
|
@@ -235,7 +247,10 @@ export function buildWorkflowRunContent(
|
|
|
235
247
|
return { type: "text", text };
|
|
236
248
|
}
|
|
237
249
|
|
|
238
|
-
export type OperationSchemaProperties = Record<
|
|
250
|
+
export type OperationSchemaProperties = Record<
|
|
251
|
+
string,
|
|
252
|
+
{ type: string; enum?: readonly string[]; description?: string; [key: string]: unknown }
|
|
253
|
+
>;
|
|
239
254
|
|
|
240
255
|
export type DefineOperation = (
|
|
241
256
|
action: string,
|
package/src/handlers/tasks.ts
CHANGED
|
@@ -19,18 +19,21 @@
|
|
|
19
19
|
*
|
|
20
20
|
* remove/remove_subtree/restore are not duplicated here -- see ./artifact-trash-vehicle.ts.
|
|
21
21
|
*/
|
|
22
|
-
import type
|
|
22
|
+
import { VehicleError, type VehicleLimits } from "@danypops/vehicle-core";
|
|
23
23
|
import type { VehicleRegistry } from "@danypops/vehicle-server";
|
|
24
24
|
import type { ArtifactStore } from "../artifact/artifact-store.ts";
|
|
25
|
-
import { GATE_TIMEOUT_MAX_MS } from "../constants.ts";
|
|
25
|
+
import { GATE_TIMEOUT_MAX_MS, TASK_CREATE_IDEMPOTENCY_KEY_MAX_LENGTH } from "../constants.ts";
|
|
26
|
+
import { PROOF_TYPES } from "../domain/checklist.ts";
|
|
27
|
+
import { GATE_TYPES } from "../domain/gate.ts";
|
|
26
28
|
import type { TaskViewMode } from "../domain/task-scope.ts";
|
|
27
29
|
import { tasksOperations } from "../modules/tasks.ts";
|
|
28
30
|
import type { SessionIdentity } from "../session-identity/session-identity-service.ts";
|
|
29
31
|
import type { TaskExecutionPlan } from "../task/task-execution.ts";
|
|
30
|
-
import type
|
|
32
|
+
import { type TaskCompletion, TaskProjectAmbiguousError, TaskProjectNotFoundError, type Tasks } from "../task/task-service.ts";
|
|
31
33
|
import {
|
|
32
34
|
booleanProp,
|
|
33
35
|
classifySessionAuthorization,
|
|
36
|
+
classifyTaskCreateIdempotency,
|
|
34
37
|
classifyTaskDependencyCycles,
|
|
35
38
|
classifyTaskExecutionBounds,
|
|
36
39
|
createOperationDefiner,
|
|
@@ -69,9 +72,63 @@ const GATE_OPERATION_LIMITS: VehicleLimits = {
|
|
|
69
72
|
maxResponseBytes: 262_144,
|
|
70
73
|
};
|
|
71
74
|
|
|
72
|
-
const objectProp = { type: "object" } as
|
|
73
|
-
const arrayProp = { type: "array" } as
|
|
74
|
-
const _boolProp = { type: "boolean" } as
|
|
75
|
+
const objectProp = { type: "object" } as const;
|
|
76
|
+
const arrayProp = { type: "array" } as const;
|
|
77
|
+
const _boolProp = { type: "boolean" } as const;
|
|
78
|
+
|
|
79
|
+
const gateProp = {
|
|
80
|
+
type: "array",
|
|
81
|
+
description: "Validation gates run by tasks.run_gates and tasks.complete.",
|
|
82
|
+
items: {
|
|
83
|
+
type: "object",
|
|
84
|
+
properties: {
|
|
85
|
+
type: { type: "string", enum: GATE_TYPES, description: "Gate evaluator." },
|
|
86
|
+
target: { type: "string", minLength: 1, description: "Path, command, text target, or test command." },
|
|
87
|
+
expect: { type: "string", description: "Optional expected text/result." },
|
|
88
|
+
timeoutMs: { type: "integer", minimum: 1_000, maximum: GATE_TIMEOUT_MAX_MS, description: "Command/test timeout override." },
|
|
89
|
+
},
|
|
90
|
+
required: ["type", "target"],
|
|
91
|
+
additionalProperties: false,
|
|
92
|
+
},
|
|
93
|
+
examples: [
|
|
94
|
+
[{ type: "file-exists", target: "dist/index.js" }],
|
|
95
|
+
[{ type: "command", target: "bun run typecheck", timeoutMs: 60_000 }],
|
|
96
|
+
[{ type: "contains", target: "README.md", expect: "Retry semantics" }],
|
|
97
|
+
[{ type: "test", target: "bun test" }],
|
|
98
|
+
],
|
|
99
|
+
} as const;
|
|
100
|
+
|
|
101
|
+
const checklistProp = {
|
|
102
|
+
type: "object",
|
|
103
|
+
description: "Map from completion criterion text to one or more typed proof references. An empty map clears the checklist.",
|
|
104
|
+
additionalProperties: {
|
|
105
|
+
type: "object",
|
|
106
|
+
properties: {
|
|
107
|
+
proof: {
|
|
108
|
+
type: "array",
|
|
109
|
+
minItems: 1,
|
|
110
|
+
items: {
|
|
111
|
+
type: "object",
|
|
112
|
+
properties: {
|
|
113
|
+
type: { type: "string", enum: PROOF_TYPES },
|
|
114
|
+
target: { type: "string", minLength: 1 },
|
|
115
|
+
expect: { type: "string" },
|
|
116
|
+
},
|
|
117
|
+
required: ["type", "target"],
|
|
118
|
+
additionalProperties: false,
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
required: ["proof"],
|
|
123
|
+
additionalProperties: false,
|
|
124
|
+
},
|
|
125
|
+
examples: [
|
|
126
|
+
{
|
|
127
|
+
"tests pass": { proof: [{ type: "test", target: "bun test", expect: "0 failures" }] },
|
|
128
|
+
"documentation updated": { proof: [{ type: "file", target: "README.md" }] },
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
} as const;
|
|
75
132
|
|
|
76
133
|
export interface TasksVehicleDeps {
|
|
77
134
|
tasks: Tasks;
|
|
@@ -194,10 +251,26 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
194
251
|
*/
|
|
195
252
|
const call = (name: string, input: Record<string, unknown>): unknown =>
|
|
196
253
|
classifySessionAuthorization(() =>
|
|
197
|
-
|
|
254
|
+
classifyTaskCreateIdempotency(() =>
|
|
255
|
+
classifyTaskExecutionBounds(() => classifyTaskDependencyCycles(() => moduleOperations.get(name)!.execute(input))),
|
|
256
|
+
),
|
|
198
257
|
);
|
|
199
258
|
const define = createOperationDefiner(registry, OWNER, "tasks", ["tasks:read", "tasks:write"], call);
|
|
200
259
|
|
|
260
|
+
const resolveProject = (reference: string) => {
|
|
261
|
+
try {
|
|
262
|
+
return tasks.resolveProject(reference);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
if (error instanceof TaskProjectNotFoundError) {
|
|
265
|
+
throw new VehicleError("task-project-not-found", error.message, { category: "not_found" });
|
|
266
|
+
}
|
|
267
|
+
if (error instanceof TaskProjectAmbiguousError) {
|
|
268
|
+
throw new VehicleError("task-project-ambiguous", error.message, { category: "conflict" });
|
|
269
|
+
}
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
201
274
|
/** Shared by every action taking a single id/name: resolves root_task_name first, then name -> id against the final scope. */
|
|
202
275
|
const resolveIdAndScope = (input: Record<string, unknown>): Record<string, unknown> => {
|
|
203
276
|
const projectRoot = input.project_root as string | undefined;
|
|
@@ -221,14 +294,21 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
221
294
|
status: stringProp,
|
|
222
295
|
labels: arrayProp,
|
|
223
296
|
extra: objectProp,
|
|
224
|
-
gates:
|
|
225
|
-
checklist:
|
|
297
|
+
gates: gateProp,
|
|
298
|
+
checklist: checklistProp,
|
|
226
299
|
template_id: stringProp,
|
|
227
300
|
parent_id: stringProp,
|
|
228
301
|
parent_name: stringProp,
|
|
229
302
|
depends_on: arrayProp,
|
|
230
303
|
depends_on_names: arrayProp,
|
|
231
304
|
project_root: stringProp,
|
|
305
|
+
idempotency_key: {
|
|
306
|
+
type: "string",
|
|
307
|
+
minLength: 1,
|
|
308
|
+
maxLength: TASK_CREATE_IDEMPOTENCY_KEY_MAX_LENGTH,
|
|
309
|
+
description:
|
|
310
|
+
"Optional retry key, scoped by caller and canonical project root. Reusing it with the same payload returns the original response; a different payload is rejected.",
|
|
311
|
+
},
|
|
232
312
|
session_id: stringProp,
|
|
233
313
|
},
|
|
234
314
|
["title", "project_root"],
|
|
@@ -244,6 +324,12 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
244
324
|
const dependsOn = resolveArrayField(artifacts, tasks, filter, input.depends_on, input.depends_on_names);
|
|
245
325
|
return { ...input, ...(parentId ? { parent_id: parentId } : {}), ...(dependsOn ? { depends_on: dependsOn } : {}) };
|
|
246
326
|
},
|
|
327
|
+
(input, context) =>
|
|
328
|
+
call("tasks.create", {
|
|
329
|
+
...input,
|
|
330
|
+
idempotency_key: input.idempotency_key ?? context.idempotencyKey,
|
|
331
|
+
idempotency_caller: context.principal?.id ?? "anonymous",
|
|
332
|
+
}),
|
|
247
333
|
);
|
|
248
334
|
|
|
249
335
|
define(
|
|
@@ -340,6 +426,35 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
340
426
|
resolveIdAndScope,
|
|
341
427
|
);
|
|
342
428
|
|
|
429
|
+
define(
|
|
430
|
+
"projects",
|
|
431
|
+
"Lists registered Task project scopes with stable ids, names, aliases, and canonical roots. Use resolve_project before a task operation when the user supplied a human project name.",
|
|
432
|
+
"read",
|
|
433
|
+
{ query: stringProp, limit: numberProp },
|
|
434
|
+
[],
|
|
435
|
+
(input) => input,
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
define(
|
|
439
|
+
"resolve_project",
|
|
440
|
+
"Resolves one case-insensitive exact Task project id, name, alias, or canonical root. Fails closed on unknown or ambiguous references and returns the canonical project_root for subsequent task operations.",
|
|
441
|
+
"read",
|
|
442
|
+
{ name: stringProp },
|
|
443
|
+
["name"],
|
|
444
|
+
(input) => input,
|
|
445
|
+
(input) => resolveProject(input.name as string),
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
define(
|
|
449
|
+
"register_project",
|
|
450
|
+
"Registers a Task project name and aliases. Pass project to update/rename/move an existing registration while preserving its stable id and old name as an alias.",
|
|
451
|
+
"local-write",
|
|
452
|
+
{ project_root: stringProp, name: stringProp, aliases: arrayProp, project: stringProp },
|
|
453
|
+
["project_root"],
|
|
454
|
+
(input) => input,
|
|
455
|
+
(input) => call("tasks.register_project", input),
|
|
456
|
+
);
|
|
457
|
+
|
|
343
458
|
define(
|
|
344
459
|
"scope",
|
|
345
460
|
"Describes the current task-view scope selection for project_root.",
|
|
@@ -531,7 +646,7 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
531
646
|
"set_checklist",
|
|
532
647
|
"Replaces a Task's evidence-bearing checklist (proof requirements) in full.",
|
|
533
648
|
"local-write",
|
|
534
|
-
{ id: stringProp, name: stringProp, checklist:
|
|
649
|
+
{ id: stringProp, name: stringProp, checklist: checklistProp, project_root: stringProp },
|
|
535
650
|
["checklist"],
|
|
536
651
|
resolveIdAndScope,
|
|
537
652
|
);
|
|
@@ -539,7 +654,7 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
539
654
|
"set_gates",
|
|
540
655
|
"Replaces a Task's gate commands in full. Each gate is {type, target, expect?, timeoutMs?} -- timeoutMs overrides the default per-type command timeout (30s)/test timeout (60s) for a legitimately slower gate, up to a bounded ceiling.",
|
|
541
656
|
"local-write",
|
|
542
|
-
{ id: stringProp, name: stringProp, gates:
|
|
657
|
+
{ id: stringProp, name: stringProp, gates: gateProp, project_root: stringProp },
|
|
543
658
|
["gates"],
|
|
544
659
|
resolveIdAndScope,
|
|
545
660
|
);
|
|
@@ -637,7 +752,7 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
637
752
|
|
|
638
753
|
define(
|
|
639
754
|
"claim",
|
|
640
|
-
"Claims this Task's lease under owner (defaults to session_id). Throws if a different owner already holds one.",
|
|
755
|
+
"Claims this Task's lease under owner (defaults to session_id). Returns taskName (the reusable artifact alias) and taskTitle instead of exposing its backend UUID. Throws if a different owner already holds one.",
|
|
641
756
|
"local-write",
|
|
642
757
|
{
|
|
643
758
|
id: stringProp,
|
|
@@ -657,7 +772,7 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
657
772
|
);
|
|
658
773
|
define(
|
|
659
774
|
"heartbeat_lease",
|
|
660
|
-
"Extends this Task's lease -- needs the exact owner/token claim() returned.",
|
|
775
|
+
"Extends this Task's lease -- needs the exact owner/token claim() returned and returns the reusable taskName plus taskTitle.",
|
|
661
776
|
"local-write",
|
|
662
777
|
{
|
|
663
778
|
id: stringProp,
|
|
@@ -687,7 +802,7 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
687
802
|
);
|
|
688
803
|
define(
|
|
689
804
|
"lease",
|
|
690
|
-
"Shows this Task's current lease, if any.",
|
|
805
|
+
"Shows this Task's current lease, if any, identified by reusable taskName plus taskTitle rather than its backend UUID.",
|
|
691
806
|
"read",
|
|
692
807
|
{ id: stringProp, name: stringProp, project_root: stringProp },
|
|
693
808
|
[],
|
package/src/index.ts
CHANGED
|
@@ -24,7 +24,7 @@ export type { DisplayGraph, DisplayGraphEdge, DisplayGraphNode, RenderedGraph }
|
|
|
24
24
|
export type { GateResult } from "./domain/gate.ts";
|
|
25
25
|
export type { NoteHistoryPage } from "./domain/note-event.ts";
|
|
26
26
|
export type { TaskEvent, TaskHistoryPage } from "./domain/task-event.ts";
|
|
27
|
-
export type { TaskLease } from "./domain/task-lease.ts";
|
|
27
|
+
export type { TaskLease, TaskLeaseView } from "./domain/task-lease.ts";
|
|
28
28
|
export type { TaskViewSelection } from "./domain/task-scope.ts";
|
|
29
29
|
export { NOTE_DISPOSITIONS } from "./note/note-service.ts";
|
|
30
30
|
export type { PlaybookInvocationResult, PlaybookMissingArguments } from "./playbook/playbook-execution.ts";
|
package/src/modules/docs.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* (step 5, continued, of the incremental refactor in
|
|
4
4
|
* reducing-papyrus-consumer-change-amplification-with-modules--pvdo).
|
|
5
5
|
*
|
|
6
|
-
* Imports only src/
|
|
6
|
+
* Imports only src/docs/docs-service.ts's Doc functions, which are already generic
|
|
7
7
|
* ArtifactStore-based with no other module's concrete class dependency.
|
|
8
8
|
*/
|
|
9
9
|
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
showDocument,
|
|
21
21
|
transitionDocument,
|
|
22
22
|
updateDocument,
|
|
23
|
-
} from "../
|
|
23
|
+
} from "../docs/docs-service.ts";
|
|
24
24
|
import type { OperationDefinition } from "../module-registry.ts";
|
|
25
25
|
import { type OperationInput, optionalBoolean, optionalNumber, optionalString, string } from "./operation-input.ts";
|
|
26
26
|
|
package/src/modules/notes.ts
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* table via NOTE_SUBTYPE) and has exactly six operations — the smallest real module to
|
|
8
8
|
* prove the OperationRegistry shape against before extracting Tasks or Docs.
|
|
9
9
|
*
|
|
10
|
-
* This module does not import another module's infrastructure (src/task-service.ts,
|
|
11
|
-
* src/
|
|
10
|
+
* This module does not import another module's infrastructure (src/task/task-service.ts,
|
|
11
|
+
* src/docs/docs-service.ts, etc.) — only its own src/note/note-service.ts and the shared
|
|
12
12
|
* OperationInput parsing helpers, matching the "module code does not import another
|
|
13
13
|
* module's infrastructure" constraint.
|
|
14
14
|
*/
|
package/src/modules/playbooks.ts
CHANGED
|
@@ -6,13 +6,15 @@
|
|
|
6
6
|
* declare Doc/Rule blueprints and typed arguments and nested pipeline calls. playbooks.invoke
|
|
7
7
|
* recycles the shared blueprint materialization engine (playbook/playbook-execution.ts compiles a
|
|
8
8
|
* Playbook's steps and composition tree into a BlueprintDefinition, then hands off to
|
|
9
|
-
* playbook/workflow-execution.ts's shared core). See
|
|
9
|
+
* playbook/workflow-execution.ts's shared core). See playbook/playbook-service.ts and
|
|
10
10
|
* playbook/playbook-definition.ts for the full rationale.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { summarizeArtifact } from "../artifact/artifact.ts";
|
|
14
14
|
import type { ArtifactScopeStore } from "../artifact/artifact-scope-store.ts";
|
|
15
15
|
import type { ArtifactStore } from "../artifact/artifact-store.ts";
|
|
16
|
+
import type { OperationDefinition } from "../module-registry.ts";
|
|
17
|
+
import { invokePlaybook } from "../playbook/playbook-execution.ts";
|
|
16
18
|
import {
|
|
17
19
|
assignPlaybookProject,
|
|
18
20
|
containPlaybook,
|
|
@@ -25,9 +27,7 @@ import {
|
|
|
25
27
|
uncontainPlaybook,
|
|
26
28
|
undependPlaybook,
|
|
27
29
|
updatePlaybook,
|
|
28
|
-
} from "../
|
|
29
|
-
import type { OperationDefinition } from "../module-registry.ts";
|
|
30
|
-
import { invokePlaybook } from "../playbook/playbook-execution.ts";
|
|
30
|
+
} from "../playbook/playbook-service.ts";
|
|
31
31
|
import type { SessionIdentity } from "../session-identity/session-identity-service.ts";
|
|
32
32
|
import type { TaskEventStore } from "../stores/task-event-store.ts";
|
|
33
33
|
import type { TaskScopeStore } from "../stores/task-scope-store.ts";
|
|
@@ -108,8 +108,10 @@ export function playbooksOperations({
|
|
|
108
108
|
steps: input.steps,
|
|
109
109
|
tools: input.tools as string[] | undefined,
|
|
110
110
|
arguments: input.arguments,
|
|
111
|
+
subtype: optionalString(input, "subtype"),
|
|
111
112
|
labels: input.labels as string[] | undefined,
|
|
112
113
|
extra: input.extra as Record<string, unknown> | undefined,
|
|
114
|
+
templateId: optionalString(input, "template_id") ?? optionalString(input, "templateId"),
|
|
113
115
|
projectRoot: optionalString(input, "project_root"),
|
|
114
116
|
},
|
|
115
117
|
eventContext(input),
|
package/src/modules/rules.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { type Artifact, summarizeArtifact } from "../artifact/artifact.ts";
|
|
15
15
|
import type { ArtifactScopeStore } from "../artifact/artifact-scope-store.ts";
|
|
16
16
|
import type { ArtifactStore } from "../artifact/artifact-store.ts";
|
|
17
|
+
import type { OperationDefinition } from "../module-registry.ts";
|
|
17
18
|
import {
|
|
18
19
|
assignRuleProject,
|
|
19
20
|
createRule,
|
|
@@ -25,8 +26,7 @@ import {
|
|
|
25
26
|
showRule,
|
|
26
27
|
transitionRule,
|
|
27
28
|
updateRule,
|
|
28
|
-
} from "../
|
|
29
|
-
import type { OperationDefinition } from "../module-registry.ts";
|
|
29
|
+
} from "../rules/rules-service.ts";
|
|
30
30
|
import { type OperationInput, optionalBoolean, optionalNumber, optionalString, string } from "./operation-input.ts";
|
|
31
31
|
|
|
32
32
|
const MODULE_ID = "rules";
|
|
@@ -92,8 +92,10 @@ export function rulesOperations(artifacts: ArtifactStore, scopes: ArtifactScopeS
|
|
|
92
92
|
condition: optionalString(input, "condition"),
|
|
93
93
|
action: optionalString(input, "rule_action") ?? optionalString(input, "governance_action"),
|
|
94
94
|
severity: optionalString(input, "severity") as "block" | "warn" | "info" | undefined,
|
|
95
|
+
subtype: optionalString(input, "subtype"),
|
|
95
96
|
labels: input.labels as string[] | undefined,
|
|
96
97
|
extra: input.extra as Record<string, unknown> | undefined,
|
|
98
|
+
templateId: optionalString(input, "template_id") ?? optionalString(input, "templateId"),
|
|
97
99
|
projectRoot: optionalString(input, "project_root"),
|
|
98
100
|
},
|
|
99
101
|
eventContext(input),
|
package/src/modules/tasks.ts
CHANGED
|
@@ -68,6 +68,9 @@ export const TASKS_OPERATION_NAMES = [
|
|
|
68
68
|
"tasks.plan",
|
|
69
69
|
"tasks.show",
|
|
70
70
|
"tasks.history",
|
|
71
|
+
"tasks.projects",
|
|
72
|
+
"tasks.resolve_project",
|
|
73
|
+
"tasks.register_project",
|
|
71
74
|
"tasks.scope",
|
|
72
75
|
"tasks.set_scope",
|
|
73
76
|
"tasks.assign_project",
|
|
@@ -133,6 +136,10 @@ export function tasksOperations(tasks: Tasks, artifacts: ArtifactStore, sessionI
|
|
|
133
136
|
projectSource: "cwd",
|
|
134
137
|
},
|
|
135
138
|
eventContext(input),
|
|
139
|
+
{
|
|
140
|
+
key: optionalString(input, "idempotency_key"),
|
|
141
|
+
caller: optionalString(input, "idempotency_caller"),
|
|
142
|
+
},
|
|
136
143
|
),
|
|
137
144
|
),
|
|
138
145
|
define("tasks.update", (input: OperationInput) =>
|
|
@@ -161,6 +168,18 @@ export function tasksOperations(tasks: Tasks, artifacts: ArtifactStore, sessionI
|
|
|
161
168
|
direction: optionalString(input, "direction") as TaskEventDirection | undefined,
|
|
162
169
|
}),
|
|
163
170
|
),
|
|
171
|
+
define("tasks.projects", (input: OperationInput) => tasks.projects(optionalString(input, "query"), optionalNumber(input, "limit"))),
|
|
172
|
+
define("tasks.resolve_project", (input: OperationInput) => tasks.resolveProject(string(input, "name"))),
|
|
173
|
+
define("tasks.register_project", (input: OperationInput) =>
|
|
174
|
+
tasks.registerProject(
|
|
175
|
+
{
|
|
176
|
+
projectRoot: string(input, "project_root"),
|
|
177
|
+
name: optionalString(input, "name"),
|
|
178
|
+
aliases: optionalStringArray(input, "aliases"),
|
|
179
|
+
},
|
|
180
|
+
optionalString(input, "project"),
|
|
181
|
+
),
|
|
182
|
+
),
|
|
164
183
|
define("tasks.scope", (input: OperationInput) => tasks.scopeSelection(string(input, "project_root"))),
|
|
165
184
|
define("tasks.set_scope", (input: OperationInput) =>
|
|
166
185
|
tasks.setView(string(input, "project_root"), string(input, "scope") as TaskViewMode, optionalString(input, "root_task_id")),
|
|
@@ -35,7 +35,7 @@ import type {
|
|
|
35
35
|
TaskBlueprint,
|
|
36
36
|
} from "../domain/blueprint-definition.ts";
|
|
37
37
|
import { validateBlueprintDefinition } from "../domain/blueprint-definition.ts";
|
|
38
|
-
import type { PlaybookArgument, PlaybookStep } from "
|
|
38
|
+
import type { PlaybookArgument, PlaybookStep } from "./playbook-service.ts";
|
|
39
39
|
|
|
40
40
|
/** A non-composing edge touching a playbook node, to be mirrored onto that node's generated root task once real task ids exist -- e.g. a Rule `gates` this playbook, or this playbook `references`/`documents` a Doc. Direction is preserved exactly: `from`/`to` name whichever side is NOT the playbook, and `ownerIsFrom` says which side the playbook (now the generated root task) occupies. */
|
|
41
41
|
export interface PlaybookExternalLink {
|