@danypops/papyrus 0.40.0 → 0.42.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 +9 -12
- package/package.json +1 -1
- package/src/artifact-relationship-view.ts +1 -1
- package/src/cli.ts +12 -178
- package/src/constants.ts +19 -52
- package/src/db.ts +34 -7
- package/src/domain/artifact-event.ts +1 -1
- package/src/domain/blueprint-definition.ts +273 -0
- package/src/domain-services.ts +147 -204
- package/src/modules/logs.ts +1 -1
- package/src/modules/playbooks.ts +9 -7
- package/src/ops.ts +3 -1
- package/src/playbook-definition.ts +75 -29
- package/src/playbook-execution.ts +7 -24
- package/src/ports/artifact-scope-store.ts +1 -1
- package/src/service.ts +5 -21
- package/src/task-service.ts +1 -1
- package/src/vehicle/artifact-trash-vehicle.ts +1 -1
- package/src/vehicle/artifact-vehicle-shared.ts +4 -6
- package/src/vehicle/docs-vehicle.ts +2 -2
- package/src/vehicle/notes-vehicle.ts +2 -2
- package/src/vehicle/papyrus-vehicle.ts +6 -7
- package/src/vehicle/playbooks-vehicle.ts +1 -1
- package/src/vehicle/tasks-vehicle.ts +407 -0
- package/src/workflow-execution.ts +139 -69
- package/src/domain/skill-definition.ts +0 -270
- package/src/modules/skills.ts +0 -158
- package/src/vehicle/skills-vehicle.ts +0 -194
|
@@ -1,270 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
SEED_RELATIONS,
|
|
3
|
-
SKILL_MAX_BLUEPRINTS,
|
|
4
|
-
SKILL_MAX_ENUM_VALUES,
|
|
5
|
-
SKILL_MAX_INPUTS,
|
|
6
|
-
SKILL_MAX_LINKS,
|
|
7
|
-
} from "../constants.ts";
|
|
8
|
-
|
|
9
|
-
export type SkillArgumentValue = string | number | boolean;
|
|
10
|
-
export type SkillInputType = "string" | "number" | "boolean";
|
|
11
|
-
|
|
12
|
-
export interface SkillInputDefinition {
|
|
13
|
-
type: SkillInputType;
|
|
14
|
-
required?: boolean;
|
|
15
|
-
default?: SkillArgumentValue;
|
|
16
|
-
enum?: SkillArgumentValue[];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface SkillDocBlueprint {
|
|
20
|
-
ref: string;
|
|
21
|
-
title: string;
|
|
22
|
-
body?: string;
|
|
23
|
-
subtype?: string;
|
|
24
|
-
labels?: string[];
|
|
25
|
-
extra?: Record<string, unknown>;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface SkillRuleBlueprint {
|
|
29
|
-
ref: string;
|
|
30
|
-
title: string;
|
|
31
|
-
body?: string;
|
|
32
|
-
condition?: string;
|
|
33
|
-
action?: string;
|
|
34
|
-
severity?: "block" | "warn" | "info";
|
|
35
|
-
labels?: string[];
|
|
36
|
-
extra?: Record<string, unknown>;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface SkillTaskBlueprint {
|
|
40
|
-
ref: string;
|
|
41
|
-
title: string;
|
|
42
|
-
body?: string;
|
|
43
|
-
dependsOn?: string[];
|
|
44
|
-
parent?: string;
|
|
45
|
-
labels?: string[];
|
|
46
|
-
extra?: Record<string, unknown>;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* A pipeline step that nests another workflow Skill's run inside this one -- the Jenkins
|
|
51
|
-
* "trigger downstream job and wait" / Ansible "include_tasks" primitive. `skillId` is late-
|
|
52
|
-
* bound: existence and workflow-subtype are checked at execution time (workflow-execution.ts),
|
|
53
|
-
* not here, since this validator has no store access. `dependsOn`/`parent` place this step in
|
|
54
|
-
* the SAME dependency graph as ordinary task blueprints -- a task can depend on a skill-call
|
|
55
|
-
* ref (meaning: depend on every task the nested run creates), and a skill-call's own `parent`
|
|
56
|
-
* contains the nested run's root tasks under an outer task.
|
|
57
|
-
*/
|
|
58
|
-
export interface SkillCallBlueprint {
|
|
59
|
-
ref: string;
|
|
60
|
-
title: string;
|
|
61
|
-
skillId: string;
|
|
62
|
-
arguments?: Record<string, unknown>;
|
|
63
|
-
dependsOn?: string[];
|
|
64
|
-
parent?: string;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export interface SkillBlueprints {
|
|
68
|
-
docs: SkillDocBlueprint[];
|
|
69
|
-
rules: SkillRuleBlueprint[];
|
|
70
|
-
tasks: SkillTaskBlueprint[];
|
|
71
|
-
skills: SkillCallBlueprint[];
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export interface SkillBlueprintLink {
|
|
75
|
-
from: string;
|
|
76
|
-
relation: string;
|
|
77
|
-
to: string;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export interface SkillDefinition {
|
|
81
|
-
version: 1;
|
|
82
|
-
inputs: Record<string, SkillInputDefinition>;
|
|
83
|
-
blueprints: SkillBlueprints;
|
|
84
|
-
links: SkillBlueprintLink[];
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
|
|
88
|
-
const PLACEHOLDER_PATTERN = /{{\s*([A-Za-z][A-Za-z0-9_-]{0,63})\s*}}/g;
|
|
89
|
-
const INPUT_TYPES = new Set<SkillInputType>(["string", "number", "boolean"]);
|
|
90
|
-
const RESERVED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
91
|
-
const RELATIONS = new Set<string>(SEED_RELATIONS);
|
|
92
|
-
|
|
93
|
-
function record(value: unknown, label: string): Record<string, unknown> {
|
|
94
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${label} must be an object`);
|
|
95
|
-
return value as Record<string, unknown>;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function array(value: unknown, label: string): unknown[] {
|
|
99
|
-
if (!Array.isArray(value)) throw new Error(`${label} must be an array`);
|
|
100
|
-
return value;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function string(value: unknown, label: string): string {
|
|
104
|
-
if (typeof value !== "string" || value.length === 0) throw new Error(`${label} must be a non-empty string`);
|
|
105
|
-
return value;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function validateArgumentValue(name: string, type: SkillInputType, value: unknown): SkillArgumentValue {
|
|
109
|
-
if (typeof value !== type || (type === "number" && !Number.isFinite(value))) {
|
|
110
|
-
throw new Error(`skill argument "${name}" must be a ${type}`);
|
|
111
|
-
}
|
|
112
|
-
return value as SkillArgumentValue;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function validateInputs(value: unknown): Record<string, SkillInputDefinition> {
|
|
116
|
-
const source = record(value ?? {}, "skill inputs");
|
|
117
|
-
const entries = Object.entries(source);
|
|
118
|
-
if (entries.length > SKILL_MAX_INPUTS) throw new Error(`skill inputs exceed ${SKILL_MAX_INPUTS}`);
|
|
119
|
-
const result: Record<string, SkillInputDefinition> = {};
|
|
120
|
-
for (const [name, raw] of entries) {
|
|
121
|
-
if (RESERVED_KEYS.has(name)) throw new Error(`reserved skill input name "${name}"`);
|
|
122
|
-
if (!NAME_PATTERN.test(name)) throw new Error(`invalid skill input name "${name}"`);
|
|
123
|
-
const input = record(raw, `skill input "${name}"`);
|
|
124
|
-
if (!INPUT_TYPES.has(input["type"] as SkillInputType)) throw new Error(`skill input "${name}" has unsupported type`);
|
|
125
|
-
const type = input["type"] as SkillInputType;
|
|
126
|
-
if (input["required"] !== undefined && typeof input["required"] !== "boolean") {
|
|
127
|
-
throw new Error(`skill input "${name}" required must be boolean`);
|
|
128
|
-
}
|
|
129
|
-
const normalized: SkillInputDefinition = { type };
|
|
130
|
-
if (input["required"] !== undefined) normalized.required = input["required"] as boolean;
|
|
131
|
-
if (input["default"] !== undefined) normalized.default = validateArgumentValue(name, type, input["default"]);
|
|
132
|
-
if (input["enum"] !== undefined) {
|
|
133
|
-
const values = array(input["enum"], `skill input "${name}" enum`);
|
|
134
|
-
if (values.length === 0 || values.length > SKILL_MAX_ENUM_VALUES) throw new Error(`skill input "${name}" enum must contain 1-${SKILL_MAX_ENUM_VALUES} values`);
|
|
135
|
-
normalized.enum = values.map((entry) => validateArgumentValue(name, type, entry));
|
|
136
|
-
if (normalized.default !== undefined && !normalized.enum.includes(normalized.default)) {
|
|
137
|
-
throw new Error(`skill input "${name}" default must be one of its enum values`);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
result[name] = normalized;
|
|
141
|
-
}
|
|
142
|
-
return result;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function validateBlueprint<T extends { ref: string; title: string }>(value: unknown, kind: string): T {
|
|
146
|
-
const source = record(value, `skill ${kind} blueprint`);
|
|
147
|
-
const ref = string(source["ref"], `skill ${kind} blueprint ref`);
|
|
148
|
-
if (!NAME_PATTERN.test(ref)) throw new Error(`invalid skill blueprint ref "${ref}"`);
|
|
149
|
-
const title = string(source["title"], `skill ${kind} blueprint title`);
|
|
150
|
-
return { ...source, ref, title } as T;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function placeholders(value: unknown, result: Set<string> = new Set()): Set<string> {
|
|
154
|
-
if (typeof value === "string") {
|
|
155
|
-
for (const match of value.matchAll(PLACEHOLDER_PATTERN)) result.add(match[1]!);
|
|
156
|
-
} else if (Array.isArray(value)) {
|
|
157
|
-
for (const entry of value) placeholders(entry, result);
|
|
158
|
-
} else if (typeof value === "object" && value !== null) {
|
|
159
|
-
for (const entry of Object.values(value)) placeholders(entry, result);
|
|
160
|
-
}
|
|
161
|
-
return result;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/** Steps sharing one dependency graph: ordinary tasks and skill-call pipeline steps alike. */
|
|
165
|
-
interface DependentStep {
|
|
166
|
-
ref: string;
|
|
167
|
-
dependsOn?: string[];
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function assertAcyclic(steps: DependentStep[]): void {
|
|
171
|
-
const byRef = new Map(steps.map((step) => [step.ref, step]));
|
|
172
|
-
const visiting = new Set<string>();
|
|
173
|
-
const visited = new Set<string>();
|
|
174
|
-
const visit = (ref: string): void => {
|
|
175
|
-
if (visiting.has(ref)) throw new Error(`skill step dependency cycle includes "${ref}"`);
|
|
176
|
-
if (visited.has(ref)) return;
|
|
177
|
-
visiting.add(ref);
|
|
178
|
-
for (const dependency of byRef.get(ref)?.dependsOn ?? []) visit(dependency);
|
|
179
|
-
visiting.delete(ref);
|
|
180
|
-
visited.add(ref);
|
|
181
|
-
};
|
|
182
|
-
for (const step of steps) visit(step.ref);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function validateSkillCallBlueprint(value: unknown): SkillCallBlueprint {
|
|
186
|
-
const source = record(value, "skill call blueprint");
|
|
187
|
-
const ref = string(source["ref"], "skill call blueprint ref");
|
|
188
|
-
if (!NAME_PATTERN.test(ref)) throw new Error(`invalid skill blueprint ref "${ref}"`);
|
|
189
|
-
const title = string(source["title"], "skill call blueprint title");
|
|
190
|
-
const skillId = string(source["skillId"], "skill call blueprint skillId");
|
|
191
|
-
return { ...source, ref, title, skillId } as SkillCallBlueprint;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
export function validateSkillDefinition(value: unknown): SkillDefinition {
|
|
195
|
-
const source = record(value, "skill definition");
|
|
196
|
-
if (source["version"] !== 1) throw new Error("skill definition version must be 1");
|
|
197
|
-
const inputs = validateInputs(source["inputs"]);
|
|
198
|
-
const rawBlueprints = record(source["blueprints"], "skill blueprints");
|
|
199
|
-
const docs = array(rawBlueprints["docs"] ?? [], "skill doc blueprints").map((entry) => validateBlueprint<SkillDocBlueprint>(entry, "doc"));
|
|
200
|
-
const rules = array(rawBlueprints["rules"] ?? [], "skill rule blueprints").map((entry) => validateBlueprint<SkillRuleBlueprint>(entry, "rule"));
|
|
201
|
-
const tasks = array(rawBlueprints["tasks"] ?? [], "skill task blueprints").map((entry) => validateBlueprint<SkillTaskBlueprint>(entry, "task"));
|
|
202
|
-
const skillCalls = array(rawBlueprints["skills"] ?? [], "skill call blueprints").map(validateSkillCallBlueprint);
|
|
203
|
-
const all = [...docs, ...rules, ...tasks, ...skillCalls];
|
|
204
|
-
if (all.length === 0 || all.length > SKILL_MAX_BLUEPRINTS) throw new Error(`skill blueprints must contain 1-${SKILL_MAX_BLUEPRINTS} artifacts`);
|
|
205
|
-
const refs = new Set<string>();
|
|
206
|
-
for (const blueprint of all) {
|
|
207
|
-
if (refs.has(blueprint.ref)) throw new Error(`duplicate skill blueprint ref "${blueprint.ref}"`);
|
|
208
|
-
refs.add(blueprint.ref);
|
|
209
|
-
}
|
|
210
|
-
// Tasks and skill-call pipeline steps share one dependency graph: a task may depend on a
|
|
211
|
-
// skill-call ref (meaning: depend on every task that nested run creates), and vice versa.
|
|
212
|
-
const stepRefs = new Set<string>([...tasks.map((task) => task.ref), ...skillCalls.map((call) => call.ref)]);
|
|
213
|
-
for (const task of tasks) {
|
|
214
|
-
if (task.dependsOn !== undefined && !Array.isArray(task.dependsOn)) throw new Error(`skill task "${task.ref}" dependsOn must be an array`);
|
|
215
|
-
for (const dependency of task.dependsOn ?? []) {
|
|
216
|
-
if (!stepRefs.has(dependency)) throw new Error(`unknown skill task dependency ref "${dependency}"`);
|
|
217
|
-
}
|
|
218
|
-
// parent stays task-only: containment under a skill-call step's exploded task SET has no
|
|
219
|
-
// single natural parent, so parent must name an actual task blueprint.
|
|
220
|
-
if (task.parent !== undefined && !tasks.some((candidate) => candidate.ref === task.parent)) {
|
|
221
|
-
throw new Error(`unknown skill task parent ref "${task.parent}"`);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
for (const call of skillCalls) {
|
|
225
|
-
if (call.dependsOn !== undefined && !Array.isArray(call.dependsOn)) throw new Error(`skill call "${call.ref}" dependsOn must be an array`);
|
|
226
|
-
for (const dependency of call.dependsOn ?? []) {
|
|
227
|
-
if (!stepRefs.has(dependency)) throw new Error(`unknown skill call dependency ref "${dependency}"`);
|
|
228
|
-
}
|
|
229
|
-
if (call.parent !== undefined && !tasks.some((candidate) => candidate.ref === call.parent)) {
|
|
230
|
-
throw new Error(`unknown skill call parent ref "${call.parent}"`);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
assertAcyclic([...tasks, ...skillCalls]);
|
|
234
|
-
for (const name of placeholders(all)) {
|
|
235
|
-
if (!Object.hasOwn(inputs, name)) throw new Error(`unknown skill input placeholder "${name}"`);
|
|
236
|
-
}
|
|
237
|
-
const links = array(source["links"] ?? [], "skill links").map((entry) => {
|
|
238
|
-
const link = record(entry, "skill link");
|
|
239
|
-
const from = string(link["from"], "skill link from");
|
|
240
|
-
const relation = string(link["relation"], "skill link relation");
|
|
241
|
-
const to = string(link["to"], "skill link to");
|
|
242
|
-
if (!refs.has(from)) throw new Error(`unknown skill blueprint ref "${from}"`);
|
|
243
|
-
if (!refs.has(to)) throw new Error(`unknown skill blueprint ref "${to}"`);
|
|
244
|
-
if (!RELATIONS.has(relation)) throw new Error(`unknown skill link relation "${relation}"`);
|
|
245
|
-
return { from, relation, to };
|
|
246
|
-
});
|
|
247
|
-
if (links.length > SKILL_MAX_LINKS) throw new Error(`skill links exceed ${SKILL_MAX_LINKS}`);
|
|
248
|
-
return { version: 1, inputs, blueprints: { docs, rules, tasks, skills: skillCalls }, links };
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
export function resolveSkillArguments(definition: SkillDefinition, value: unknown): Record<string, SkillArgumentValue> {
|
|
252
|
-
const source = record(value ?? {}, "skill arguments");
|
|
253
|
-
for (const name of Object.keys(source)) {
|
|
254
|
-
if (!Object.hasOwn(definition.inputs, name)) throw new Error(`unknown skill argument "${name}"`);
|
|
255
|
-
}
|
|
256
|
-
const result: Record<string, SkillArgumentValue> = {};
|
|
257
|
-
for (const [name, input] of Object.entries(definition.inputs)) {
|
|
258
|
-
const raw = source[name] ?? input.default;
|
|
259
|
-
if (raw === undefined) {
|
|
260
|
-
if (input.required) throw new Error(`missing required skill argument "${name}"`);
|
|
261
|
-
continue;
|
|
262
|
-
}
|
|
263
|
-
const normalized = validateArgumentValue(name, input.type, raw);
|
|
264
|
-
if (input.enum && !input.enum.includes(normalized)) {
|
|
265
|
-
throw new Error(`skill argument "${name}" must be one of: ${input.enum.join(", ")}`);
|
|
266
|
-
}
|
|
267
|
-
result[name] = normalized;
|
|
268
|
-
}
|
|
269
|
-
return result;
|
|
270
|
-
}
|
package/src/modules/skills.ts
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* modules/skills.ts — Skills as a Papyrus-native registered module
|
|
3
|
-
* (step 5, continued, of the incremental refactor in
|
|
4
|
-
* reducing-papyrus-consumer-change-amplification-with-modules--pvdo).
|
|
5
|
-
*
|
|
6
|
-
* skills.instantiate is intentionally NOT registered here even though its operation name
|
|
7
|
-
* starts with "skills.": when the target template's targetKind is "task" it calls
|
|
8
|
-
* tasks.create() directly instead of the generic instantiateTemplate path — a genuine
|
|
9
|
-
* cross-module concern, same category as rules.injectable (see modules/rules.ts). It
|
|
10
|
-
* stays a composition-root operation in src/service.ts.
|
|
11
|
-
*
|
|
12
|
-
* skills.run depends on the Task-domain ports (TaskEventStore, TaskScopeStore) as
|
|
13
|
-
* constructor parameters. These are shared port contracts every module may depend on,
|
|
14
|
-
* the same way every module already depends on ArtifactStore — not "another module's
|
|
15
|
-
* infrastructure" in the sense of a concrete class. workflow-execution.ts already has this
|
|
16
|
-
* port dependency pre-existing; untangling it is a separate, larger concern than this
|
|
17
|
-
* extraction.
|
|
18
|
-
*/
|
|
19
|
-
import type { AuthorityRegistry } from "../authority-registry.ts";
|
|
20
|
-
import type { Artifact, CreateArtifactInput } from "../domain/artifact.ts";
|
|
21
|
-
import type { ArtifactEventContext } from "../domain/artifact-event.ts";
|
|
22
|
-
import { assignSkillProject, createArtifactTemplate, createSkill, instantiateTemplate, listSkills, showSkill, skillInvocation, transitionSkill, updateSkill } from "../domain-services.ts";
|
|
23
|
-
import type { OperationDefinition } from "../module-registry.ts";
|
|
24
|
-
import type { ArtifactScopeStore } from "../ports/artifact-scope-store.ts";
|
|
25
|
-
import type { ArtifactStore } from "../ports/artifact-store.ts";
|
|
26
|
-
import type { TaskEventStore } from "../ports/task-event-store.ts";
|
|
27
|
-
import type { TaskScopeStore } from "../ports/task-scope-store.ts";
|
|
28
|
-
import type { Tasks, TaskStatus } from "../task-service.ts";
|
|
29
|
-
import { instantiateSkillWorkflow } from "../workflow-execution.ts";
|
|
30
|
-
|
|
31
|
-
const MODULE_ID = "skills";
|
|
32
|
-
|
|
33
|
-
type OperationInput = Record<string, unknown>;
|
|
34
|
-
|
|
35
|
-
function string(input: OperationInput, key: string): string {
|
|
36
|
-
const value = input[key];
|
|
37
|
-
if (typeof value !== "string" || value.length === 0) throw new Error(`${key} is required`);
|
|
38
|
-
return value;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function optionalString(input: OperationInput, key: string): string | undefined {
|
|
42
|
-
const value = input[key];
|
|
43
|
-
if (value === undefined) return undefined;
|
|
44
|
-
if (typeof value !== "string") throw new Error(`${key} must be a string`);
|
|
45
|
-
return value;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function optionalNumber(input: OperationInput, key: string): number | undefined {
|
|
49
|
-
const value = input[key];
|
|
50
|
-
if (value === undefined) return undefined;
|
|
51
|
-
if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`${key} must be a number`);
|
|
52
|
-
return value;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const eventContext = (input: OperationInput) => ({
|
|
56
|
-
actor: optionalString(input, "actor"),
|
|
57
|
-
source: optionalString(input, "source"),
|
|
58
|
-
sessionId: optionalString(input, "session_id") ?? optionalString(input, "sessionId"),
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
const eventContextFor = (input: OperationInput, source: string) => {
|
|
62
|
-
const context = eventContext(input);
|
|
63
|
-
return { ...context, source: context.source ?? source };
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const artifactFilter = (input: OperationInput) => ({
|
|
67
|
-
status: optionalString(input, "status"),
|
|
68
|
-
text: optionalString(input, "text"),
|
|
69
|
-
limit: optionalNumber(input, "limit"),
|
|
70
|
-
projectRoot: optionalString(input, "project_root"),
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
export interface SkillsModuleDeps {
|
|
74
|
-
artifacts: ArtifactStore;
|
|
75
|
-
events: TaskEventStore;
|
|
76
|
-
scopes: TaskScopeStore;
|
|
77
|
-
/** Docs/Rules/Skills project scoping (distinct from `scopes`, which is Task-run project scoping for skills.run's materialized blueprint tasks). */
|
|
78
|
-
artifactScopes: ArtifactScopeStore;
|
|
79
|
-
authority: AuthorityRegistry;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function normalizeCreateInput(input: OperationInput): CreateArtifactInput {
|
|
83
|
-
const { template_id, ...rest } = input;
|
|
84
|
-
return { ...rest, templateId: typeof template_id === "string" ? template_id : undefined } as CreateArtifactInput;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* skills.instantiate's own branching logic (compatibility-template creation vs. a
|
|
89
|
-
* task-target template's tasks.create() call) -- shared between service.ts's raw RPC
|
|
90
|
-
* forwarder and skills-vehicle.ts's Vehicle operation, the two real callers, instead
|
|
91
|
-
* of reimplemented in each. Takes `tasks: Tasks` directly rather than through
|
|
92
|
-
* SkillsModuleDeps: a genuine cross-module dependency, the same category as
|
|
93
|
-
* rules.injectable and the module comment's own reason skills.instantiate isn't
|
|
94
|
-
* registered as an operation here.
|
|
95
|
-
*/
|
|
96
|
-
export interface InstantiateSkillDeps {
|
|
97
|
-
artifacts: ArtifactStore;
|
|
98
|
-
tasks: Tasks;
|
|
99
|
-
authority: AuthorityRegistry;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function instantiateSkillOrTemplate(deps: InstantiateSkillDeps, input: OperationInput, context?: ArtifactEventContext): Artifact {
|
|
103
|
-
const templateId = string(input, "template_id");
|
|
104
|
-
const template = deps.artifacts.get(templateId);
|
|
105
|
-
// Note ownership for a non-task template target is enforced inside instantiateTemplate's
|
|
106
|
-
// own rejectsNoteTemplate for the non-task branch below -- nothing else currently claims
|
|
107
|
-
// an unresolved (pre-template-resolution) kind, so there is no check to perform here.
|
|
108
|
-
if (template?.extra["targetKind"] !== "task") return instantiateTemplate(deps.artifacts, templateId, normalizeCreateInput(input), deps.authority, context);
|
|
109
|
-
return deps.tasks.create({
|
|
110
|
-
title: optionalString(input, "title") as string,
|
|
111
|
-
body: optionalString(input, "body"),
|
|
112
|
-
status: optionalString(input, "status") as TaskStatus | undefined,
|
|
113
|
-
labels: input["labels"] as string[] | undefined,
|
|
114
|
-
extra: input["extra"] as Record<string, unknown> | undefined,
|
|
115
|
-
templateId,
|
|
116
|
-
projectRoot: string(input, "project_root"),
|
|
117
|
-
projectSource: "cwd",
|
|
118
|
-
}, context);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** Registers every skills.* operation except skills.instantiate (see module comment). Behavior is unchanged from the prior inline handlers in src/service.ts. */
|
|
122
|
-
/** This module's own operation names, the single source of truth src/service.ts's EXPECTED_OPERATION_NAMES spreads in rather than re-listing by hand. skills.instantiate is deliberately absent -- see the module comment above. */
|
|
123
|
-
export const SKILLS_OPERATION_NAMES = [
|
|
124
|
-
"skills.create", "skills.create_template", "skills.list", "skills.show", "skills.invoke", "skills.run", "skills.enable", "skills.disable", "skills.assign_project", "skills.update",
|
|
125
|
-
] as const;
|
|
126
|
-
|
|
127
|
-
export function skillsOperations({ artifacts, events, scopes, artifactScopes, authority }: SkillsModuleDeps): OperationDefinition[] {
|
|
128
|
-
const define = <Input, Output>(name: string, execute: (input: Input) => Output): OperationDefinition<Input, Output> => ({
|
|
129
|
-
name, moduleId: MODULE_ID, execute,
|
|
130
|
-
});
|
|
131
|
-
return [
|
|
132
|
-
define("skills.create", (input: OperationInput) => createSkill(artifacts, artifactScopes, {
|
|
133
|
-
title: string(input, "title"), body: optionalString(input, "body"), trigger: optionalString(input, "trigger"),
|
|
134
|
-
steps: input["steps"] as string[] | undefined, tools: input["tools"] as string[] | undefined,
|
|
135
|
-
definition: input["definition"],
|
|
136
|
-
labels: input["labels"] as string[] | undefined, extra: input["extra"] as Record<string, unknown> | undefined,
|
|
137
|
-
projectRoot: optionalString(input, "project_root"),
|
|
138
|
-
}, authority, eventContext(input))),
|
|
139
|
-
define("skills.create_template", (input: OperationInput) => createArtifactTemplate(artifacts, artifactScopes, {
|
|
140
|
-
title: string(input, "title"), targetKind: string(input, "target_kind"), defaults: input["defaults"] as Record<string, unknown> | undefined,
|
|
141
|
-
required: input["required"] as string[] | undefined, body: optionalString(input, "body"), labels: input["labels"] as string[] | undefined,
|
|
142
|
-
projectRoot: optionalString(input, "project_root"),
|
|
143
|
-
}, authority, eventContext(input))),
|
|
144
|
-
define("skills.list", (input: OperationInput) => listSkills(artifacts, artifactScopes, artifactFilter(input))),
|
|
145
|
-
define("skills.show", (input: OperationInput) => showSkill(artifacts, string(input, "id"))),
|
|
146
|
-
define("skills.invoke", (input: OperationInput) => skillInvocation(artifacts, string(input, "id"))),
|
|
147
|
-
define("skills.run", (input: OperationInput) => instantiateSkillWorkflow(artifacts, string(input, "id"), {
|
|
148
|
-
runId: optionalString(input, "run_id") ?? optionalString(input, "runId"),
|
|
149
|
-
arguments: input["arguments"] as Record<string, unknown> | undefined,
|
|
150
|
-
}, { events, scopes, projectRoot: string(input, "project_root"), context: eventContextFor(input, "skill-run") })),
|
|
151
|
-
define("skills.enable", (input: OperationInput) => transitionSkill(artifacts, string(input, "id"), "enable", eventContext(input))),
|
|
152
|
-
define("skills.disable", (input: OperationInput) => transitionSkill(artifacts, string(input, "id"), "disable", eventContext(input))),
|
|
153
|
-
define("skills.assign_project", (input: OperationInput) => assignSkillProject(artifacts, artifactScopes, string(input, "id"), optionalString(input, "project_root"))),
|
|
154
|
-
define("skills.update", (input: OperationInput) => updateSkill(artifacts, string(input, "id"), {
|
|
155
|
-
title: optionalString(input, "title"), body: optionalString(input, "body"), labels: input["labels"] as string[] | undefined,
|
|
156
|
-
}, eventContext(input))),
|
|
157
|
-
];
|
|
158
|
-
}
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Skills projected as a real VehicleRegistry: one VehicleOperation per real action.
|
|
3
|
-
* Wraps modules/skills.ts's operation definitions plus skills.instantiate (composition-
|
|
4
|
-
* root-only in the module -- see instantiateSkillOrTemplate's own doc comment).
|
|
5
|
-
* remove/restore/remove_subtree are not duplicated here -- see ./artifact-trash-vehicle.ts.
|
|
6
|
-
*
|
|
7
|
-
* skills.run's output carries its own `content` block (see @danypops/vehicle-core's
|
|
8
|
-
* WithVehicleContent) built from the same execution-DAG summary pi-papyrus's hand-rolled
|
|
9
|
-
* tool used to build client-side -- the model reads a summary, not the raw node/layer/
|
|
10
|
-
* cycleId structure.
|
|
11
|
-
*/
|
|
12
|
-
import { bindVehicleOperation, defineVehicleOperation } from "@danypops/vehicle-core";
|
|
13
|
-
import type { VehicleRegistry } from "@danypops/vehicle-server";
|
|
14
|
-
import type { AuthorityRegistry } from "../authority-registry.ts";
|
|
15
|
-
import { listSkills } from "../domain-services.ts";
|
|
16
|
-
import { instantiateSkillOrTemplate, skillsOperations } from "../modules/skills.ts";
|
|
17
|
-
import type { ArtifactScopeStore } from "../ports/artifact-scope-store.ts";
|
|
18
|
-
import type { ArtifactStore } from "../ports/artifact-store.ts";
|
|
19
|
-
import type { TaskEventStore } from "../ports/task-event-store.ts";
|
|
20
|
-
import type { TaskScopeStore } from "../ports/task-scope-store.ts";
|
|
21
|
-
import type { Tasks } from "../task-service.ts";
|
|
22
|
-
import type { WorkflowRunResult } from "../workflow-execution.ts";
|
|
23
|
-
import { buildWorkflowRunContent, looseObjectSchema, numberProp, passthroughOutput, resolveArtifactIdWidened, stringProp } from "./artifact-vehicle-shared.ts";
|
|
24
|
-
|
|
25
|
-
const OWNER = "skills";
|
|
26
|
-
const LIMITS = { defaultTimeoutMs: 5_000, maxTimeoutMs: 30_000, maxRequestBytes: 65_536, maxResponseBytes: 262_144 };
|
|
27
|
-
|
|
28
|
-
export interface SkillsVehicleDeps {
|
|
29
|
-
artifacts: ArtifactStore;
|
|
30
|
-
events: TaskEventStore;
|
|
31
|
-
scopes: TaskScopeStore;
|
|
32
|
-
artifactScopes: ArtifactScopeStore;
|
|
33
|
-
authority: AuthorityRegistry;
|
|
34
|
-
/** Only for skills.instantiate's task-target branch -- see instantiateSkillOrTemplate. */
|
|
35
|
-
tasks: Tasks;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function resolveSkillId(artifacts: ArtifactStore, scopes: ArtifactScopeStore, projectRoot: string | undefined, id: unknown, name: unknown): string {
|
|
39
|
-
if (typeof id === "string" && id.length > 0) return id;
|
|
40
|
-
if (typeof name !== "string" || name.length === 0) throw new Error("id or name is required");
|
|
41
|
-
return resolveArtifactIdWidened(
|
|
42
|
-
name,
|
|
43
|
-
() => listSkills(artifacts, scopes, { text: name, projectRoot }),
|
|
44
|
-
projectRoot === undefined ? undefined : () => listSkills(artifacts, scopes, { text: name }),
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function resolveTemplateId(artifacts: ArtifactStore, scopes: ArtifactScopeStore, projectRoot: string | undefined, id: unknown, name: unknown): string | undefined {
|
|
49
|
-
if (typeof id === "string" && id.length > 0) return id;
|
|
50
|
-
if (typeof name !== "string" || name.length === 0) return undefined;
|
|
51
|
-
return resolveArtifactIdWidened(
|
|
52
|
-
name,
|
|
53
|
-
() => listSkills(artifacts, scopes, { text: name, projectRoot }),
|
|
54
|
-
projectRoot === undefined ? undefined : () => listSkills(artifacts, scopes, { text: name }),
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function registerSkillsVehicleOperations(registry: VehicleRegistry, deps: SkillsVehicleDeps): void {
|
|
59
|
-
const { artifacts, events, scopes, artifactScopes, authority, tasks } = deps;
|
|
60
|
-
const moduleOperations = new Map(skillsOperations({ artifacts, events, scopes, artifactScopes, authority }).map((op) => [op.name, op]));
|
|
61
|
-
const call = (name: string, input: Record<string, unknown>): unknown => moduleOperations.get(name)!.execute(input);
|
|
62
|
-
|
|
63
|
-
const define = (
|
|
64
|
-
action: string,
|
|
65
|
-
description: string,
|
|
66
|
-
effect: "read" | "local-write",
|
|
67
|
-
properties: Record<string, { type: string; enum?: readonly string[] }>,
|
|
68
|
-
required: readonly string[],
|
|
69
|
-
resolve: (input: Record<string, unknown>) => Record<string, unknown>,
|
|
70
|
-
execute?: (input: Record<string, unknown>) => unknown,
|
|
71
|
-
): void => {
|
|
72
|
-
const operation = defineVehicleOperation({
|
|
73
|
-
name: `skills.${action}`,
|
|
74
|
-
version: 1,
|
|
75
|
-
description,
|
|
76
|
-
input: looseObjectSchema(properties, required),
|
|
77
|
-
output: passthroughOutput,
|
|
78
|
-
permissions: ["skills:read", "skills:write"],
|
|
79
|
-
effect,
|
|
80
|
-
idempotency: { mode: effect === "read" ? "safe" : "unsafe" },
|
|
81
|
-
limits: LIMITS,
|
|
82
|
-
});
|
|
83
|
-
registry.register(OWNER, bindVehicleOperation(operation, () => async (context) => (execute ?? ((input: Record<string, unknown>) => call(`skills.${action}`, input)))(resolve(context.input))));
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
define(
|
|
87
|
-
"create",
|
|
88
|
-
"Creates a Skill -- a parameterized Task/Rule/Doc bundle, distinct from a prompt-only skill. project_root is optional (omitted = unscoped).",
|
|
89
|
-
"local-write",
|
|
90
|
-
{ title: stringProp, body: stringProp, trigger: stringProp, steps: { type: "array" }, tools: { type: "array" }, definition: { type: "object" }, labels: { type: "array" }, extra: { type: "object" }, project_root: stringProp, actor: stringProp, source: stringProp, session_id: stringProp },
|
|
91
|
-
["title"],
|
|
92
|
-
(input) => input,
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
define(
|
|
96
|
-
"create_template",
|
|
97
|
-
"Creates a compatibility artifact-template (defaults/required fields for a target kind), distinct from a workflow Skill.",
|
|
98
|
-
"local-write",
|
|
99
|
-
{ title: stringProp, target_kind: stringProp, defaults: { type: "object" }, required: { type: "array" }, body: stringProp, labels: { type: "array" }, project_root: stringProp, actor: stringProp, source: stringProp, session_id: stringProp },
|
|
100
|
-
["title", "target_kind"],
|
|
101
|
-
(input) => input,
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
define(
|
|
105
|
-
"list",
|
|
106
|
-
"Lists Skills matching an optional status/text filter, scoped to project_root when given.",
|
|
107
|
-
"read",
|
|
108
|
-
{ status: stringProp, text: stringProp, limit: numberProp, project_root: stringProp },
|
|
109
|
-
[],
|
|
110
|
-
(input) => input,
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
define(
|
|
114
|
-
"show",
|
|
115
|
-
"Shows one Skill by id or title.",
|
|
116
|
-
"read",
|
|
117
|
-
{ id: stringProp, name: stringProp, project_root: stringProp },
|
|
118
|
-
[],
|
|
119
|
-
(input) => ({ ...input, id: resolveSkillId(artifacts, artifactScopes, input.project_root as string | undefined, input.id, input.name) }),
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
define(
|
|
123
|
-
"invoke",
|
|
124
|
-
"Renders a Skill's own preview text with no side effects.",
|
|
125
|
-
"read",
|
|
126
|
-
{ id: stringProp, name: stringProp, project_root: stringProp },
|
|
127
|
-
[],
|
|
128
|
-
(input) => ({ ...input, id: resolveSkillId(artifacts, artifactScopes, input.project_root as string | undefined, input.id, input.name) }),
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
define(
|
|
132
|
-
"run",
|
|
133
|
-
"Validates arguments and atomically creates one scoped workflow run: real Tasks/Rules/Docs wired with dependsOn, one step surfacing at a time as it becomes focused -- no text dump. project_root is required here (no ambient cwd server-side); pass it explicitly.",
|
|
134
|
-
"local-write",
|
|
135
|
-
{ id: stringProp, name: stringProp, run_id: stringProp, arguments: { type: "object" }, project_root: stringProp, actor: stringProp, source: stringProp, session_id: stringProp },
|
|
136
|
-
["project_root"],
|
|
137
|
-
(input) => ({ ...input, id: resolveSkillId(artifacts, artifactScopes, input.project_root as string | undefined, input.id, input.name) }),
|
|
138
|
-
(input) => {
|
|
139
|
-
const run = call("skills.run", input) as WorkflowRunResult;
|
|
140
|
-
const content = buildWorkflowRunContent(artifacts, `Created Skill run ${run.runId}: ${run.created.tasks.length} tasks, ${run.created.rules.length} rules, ${run.created.docs.length} docs.`, run);
|
|
141
|
-
return { ...run, content: [content] };
|
|
142
|
-
},
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
define(
|
|
146
|
-
"enable",
|
|
147
|
-
"Enables a Skill.",
|
|
148
|
-
"local-write",
|
|
149
|
-
{ id: stringProp, name: stringProp, project_root: stringProp, actor: stringProp, source: stringProp, session_id: stringProp },
|
|
150
|
-
[],
|
|
151
|
-
(input) => ({ ...input, id: resolveSkillId(artifacts, artifactScopes, input.project_root as string | undefined, input.id, input.name) }),
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
define(
|
|
155
|
-
"disable",
|
|
156
|
-
"Disables a Skill.",
|
|
157
|
-
"local-write",
|
|
158
|
-
{ id: stringProp, name: stringProp, project_root: stringProp, actor: stringProp, source: stringProp, session_id: stringProp },
|
|
159
|
-
[],
|
|
160
|
-
(input) => ({ ...input, id: resolveSkillId(artifacts, artifactScopes, input.project_root as string | undefined, input.id, input.name) }),
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
define(
|
|
164
|
-
"instantiate",
|
|
165
|
-
"Instantiates a compatibility artifact-template (template_id/template_name) -- a task-target template calls tasks.create() directly; any other target creates a plain artifact. project_root is required here (no ambient cwd server-side).",
|
|
166
|
-
"local-write",
|
|
167
|
-
{ template_id: stringProp, template_name: stringProp, title: stringProp, body: stringProp, status: stringProp, labels: { type: "array" }, extra: { type: "object" }, subtype: stringProp, kind: stringProp, project_root: stringProp, actor: stringProp, source: stringProp, session_id: stringProp },
|
|
168
|
-
["title", "project_root"],
|
|
169
|
-
(input) => {
|
|
170
|
-
const templateId = resolveTemplateId(artifacts, artifactScopes, input.project_root as string | undefined, input.template_id, input.template_name);
|
|
171
|
-
if (!templateId) throw new Error("template_id or template_name is required");
|
|
172
|
-
return { ...input, template_id: templateId };
|
|
173
|
-
},
|
|
174
|
-
(input) => instantiateSkillOrTemplate({ artifacts, tasks, authority }, input, { actor: input.actor as string | undefined, source: input.source as string | undefined, sessionId: (input.session_id ?? input.sessionId) as string | undefined }),
|
|
175
|
-
);
|
|
176
|
-
|
|
177
|
-
define(
|
|
178
|
-
"assign_project",
|
|
179
|
-
"Reassigns a Skill's project_root, or unscopes it when project_root is omitted.",
|
|
180
|
-
"local-write",
|
|
181
|
-
{ id: stringProp, name: stringProp, project_root: stringProp },
|
|
182
|
-
[],
|
|
183
|
-
(input) => ({ ...input, id: resolveSkillId(artifacts, artifactScopes, undefined, input.id, input.name) }),
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
define(
|
|
187
|
-
"update",
|
|
188
|
-
"Changes a Skill's title/body/labels (at least one required). Refused for a read-only external projection.",
|
|
189
|
-
"local-write",
|
|
190
|
-
{ id: stringProp, name: stringProp, title: stringProp, body: stringProp, labels: { type: "array" }, project_root: stringProp, actor: stringProp, source: stringProp, session_id: stringProp },
|
|
191
|
-
[],
|
|
192
|
-
(input) => ({ ...input, id: resolveSkillId(artifacts, artifactScopes, input.project_root as string | undefined, input.id, input.name) }),
|
|
193
|
-
);
|
|
194
|
-
}
|