@danypops/papyrus 0.45.2 → 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 +135 -16
- 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 +25 -2
- 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,17 +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 {
|
|
34
|
+
booleanProp,
|
|
32
35
|
classifySessionAuthorization,
|
|
36
|
+
classifyTaskCreateIdempotency,
|
|
33
37
|
classifyTaskDependencyCycles,
|
|
34
38
|
classifyTaskExecutionBounds,
|
|
35
39
|
createOperationDefiner,
|
|
@@ -68,9 +72,63 @@ const GATE_OPERATION_LIMITS: VehicleLimits = {
|
|
|
68
72
|
maxResponseBytes: 262_144,
|
|
69
73
|
};
|
|
70
74
|
|
|
71
|
-
const objectProp = { type: "object" } as
|
|
72
|
-
const arrayProp = { type: "array" } as
|
|
73
|
-
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;
|
|
74
132
|
|
|
75
133
|
export interface TasksVehicleDeps {
|
|
76
134
|
tasks: Tasks;
|
|
@@ -147,6 +205,9 @@ const readSchemaProps = {
|
|
|
147
205
|
labels: arrayProp,
|
|
148
206
|
};
|
|
149
207
|
|
|
208
|
+
/** list-only: opts into full Artifact bodies instead of the lean summarizeArtifact() default (modules/tasks.ts). */
|
|
209
|
+
const listSchemaProps = { ...readSchemaProps, full: booleanProp };
|
|
210
|
+
|
|
150
211
|
/** Same gate/checklist narrative lines the removed tool built client-side. */
|
|
151
212
|
function completionContentText(labels: Map<string, string>, result: TaskCompletion): string {
|
|
152
213
|
const gates = result.gates.map((gate) => `${gate.passed ? "✓" : "✗"} ${gate.gate.type}: ${gate.gate.target} — ${gate.output}`).join("\n");
|
|
@@ -190,10 +251,26 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
190
251
|
*/
|
|
191
252
|
const call = (name: string, input: Record<string, unknown>): unknown =>
|
|
192
253
|
classifySessionAuthorization(() =>
|
|
193
|
-
|
|
254
|
+
classifyTaskCreateIdempotency(() =>
|
|
255
|
+
classifyTaskExecutionBounds(() => classifyTaskDependencyCycles(() => moduleOperations.get(name)!.execute(input))),
|
|
256
|
+
),
|
|
194
257
|
);
|
|
195
258
|
const define = createOperationDefiner(registry, OWNER, "tasks", ["tasks:read", "tasks:write"], call);
|
|
196
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
|
+
|
|
197
274
|
/** Shared by every action taking a single id/name: resolves root_task_name first, then name -> id against the final scope. */
|
|
198
275
|
const resolveIdAndScope = (input: Record<string, unknown>): Record<string, unknown> => {
|
|
199
276
|
const projectRoot = input.project_root as string | undefined;
|
|
@@ -217,14 +294,21 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
217
294
|
status: stringProp,
|
|
218
295
|
labels: arrayProp,
|
|
219
296
|
extra: objectProp,
|
|
220
|
-
gates:
|
|
221
|
-
checklist:
|
|
297
|
+
gates: gateProp,
|
|
298
|
+
checklist: checklistProp,
|
|
222
299
|
template_id: stringProp,
|
|
223
300
|
parent_id: stringProp,
|
|
224
301
|
parent_name: stringProp,
|
|
225
302
|
depends_on: arrayProp,
|
|
226
303
|
depends_on_names: arrayProp,
|
|
227
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
|
+
},
|
|
228
312
|
session_id: stringProp,
|
|
229
313
|
},
|
|
230
314
|
["title", "project_root"],
|
|
@@ -240,6 +324,12 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
240
324
|
const dependsOn = resolveArrayField(artifacts, tasks, filter, input.depends_on, input.depends_on_names);
|
|
241
325
|
return { ...input, ...(parentId ? { parent_id: parentId } : {}), ...(dependsOn ? { depends_on: dependsOn } : {}) };
|
|
242
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
|
+
}),
|
|
243
333
|
);
|
|
244
334
|
|
|
245
335
|
define(
|
|
@@ -263,9 +353,9 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
263
353
|
|
|
264
354
|
define(
|
|
265
355
|
"list",
|
|
266
|
-
"Lists Tasks matching an optional status/text/labels filter, scoped to project_root. project_root is required (no ambient cwd server-side).",
|
|
356
|
+
"Lists Tasks matching an optional status/text/labels filter, scoped to project_root. project_root is required (no ambient cwd server-side). Returns a lean summary (no body/extra) unless full: true is passed.",
|
|
267
357
|
"read",
|
|
268
|
-
|
|
358
|
+
listSchemaProps,
|
|
269
359
|
["project_root"],
|
|
270
360
|
(input) => {
|
|
271
361
|
const rootTaskId = resolveRootTaskId(artifacts, tasks, input.project_root as string, input.root_task_id, input.root_task_name);
|
|
@@ -336,6 +426,35 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
336
426
|
resolveIdAndScope,
|
|
337
427
|
);
|
|
338
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
|
+
|
|
339
458
|
define(
|
|
340
459
|
"scope",
|
|
341
460
|
"Describes the current task-view scope selection for project_root.",
|
|
@@ -527,7 +646,7 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
527
646
|
"set_checklist",
|
|
528
647
|
"Replaces a Task's evidence-bearing checklist (proof requirements) in full.",
|
|
529
648
|
"local-write",
|
|
530
|
-
{ id: stringProp, name: stringProp, checklist:
|
|
649
|
+
{ id: stringProp, name: stringProp, checklist: checklistProp, project_root: stringProp },
|
|
531
650
|
["checklist"],
|
|
532
651
|
resolveIdAndScope,
|
|
533
652
|
);
|
|
@@ -535,7 +654,7 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
535
654
|
"set_gates",
|
|
536
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.",
|
|
537
656
|
"local-write",
|
|
538
|
-
{ id: stringProp, name: stringProp, gates:
|
|
657
|
+
{ id: stringProp, name: stringProp, gates: gateProp, project_root: stringProp },
|
|
539
658
|
["gates"],
|
|
540
659
|
resolveIdAndScope,
|
|
541
660
|
);
|
|
@@ -633,7 +752,7 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
633
752
|
|
|
634
753
|
define(
|
|
635
754
|
"claim",
|
|
636
|
-
"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.",
|
|
637
756
|
"local-write",
|
|
638
757
|
{
|
|
639
758
|
id: stringProp,
|
|
@@ -653,7 +772,7 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
653
772
|
);
|
|
654
773
|
define(
|
|
655
774
|
"heartbeat_lease",
|
|
656
|
-
"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.",
|
|
657
776
|
"local-write",
|
|
658
777
|
{
|
|
659
778
|
id: stringProp,
|
|
@@ -683,7 +802,7 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
683
802
|
);
|
|
684
803
|
define(
|
|
685
804
|
"lease",
|
|
686
|
-
"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.",
|
|
687
806
|
"read",
|
|
688
807
|
{ id: stringProp, name: stringProp, project_root: stringProp },
|
|
689
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),
|