@danypops/papyrus 0.60.6 → 0.60.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/artifact/artifact-activation-audit.ts +5 -1
- package/src/artifact/artifact-activation.ts +60 -5
- package/src/cli/playbooks-command.ts +30 -3
- package/src/cli/rules-command.ts +39 -4
- package/src/handlers/activation.ts +1 -1
- package/src/handlers/playbooks.ts +3 -1
- package/src/handlers/rules.ts +6 -1
- package/src/index.ts +1 -0
- package/src/modules/playbooks.ts +2 -0
- package/src/modules/rules.ts +2 -0
- package/src/playbook/playbook-service.ts +16 -8
- package/src/rules/rules-service.ts +20 -8
package/package.json
CHANGED
|
@@ -13,6 +13,8 @@ export interface ActivationAuditEntry {
|
|
|
13
13
|
scopeMode: ArtifactScopeMode;
|
|
14
14
|
enabled: boolean;
|
|
15
15
|
reason: string;
|
|
16
|
+
activationEnabled: boolean;
|
|
17
|
+
labelMatch?: "any" | "all";
|
|
16
18
|
priority: number;
|
|
17
19
|
injection: InjectionProfile;
|
|
18
20
|
estimatedTokens: number;
|
|
@@ -63,7 +65,7 @@ export function auditArtifactActivation(
|
|
|
63
65
|
decision = { enabled: false, reason: "rule is an artifact template" };
|
|
64
66
|
} else if (artifact.kind === "rule" && !passesRuleRunScope(artifact, activeTaskId)) {
|
|
65
67
|
decision = { enabled: false, reason: "run ownership does not apply" };
|
|
66
|
-
} else decision = evaluateActivation(config, { ...context, projectRoot });
|
|
68
|
+
} else decision = evaluateActivation(config, { ...context, projectRoot }, artifact.labels);
|
|
67
69
|
return {
|
|
68
70
|
id: artifact.id,
|
|
69
71
|
kind: artifact.kind as "rule" | "playbook",
|
|
@@ -71,6 +73,8 @@ export function auditArtifactActivation(
|
|
|
71
73
|
status: artifact.status,
|
|
72
74
|
scopeMode: scope.mode,
|
|
73
75
|
...decision,
|
|
76
|
+
activationEnabled: config.enabled,
|
|
77
|
+
...(config.labels === undefined ? {} : { labelMatch: config.labels }),
|
|
74
78
|
priority: config.priority,
|
|
75
79
|
injection: config.injection,
|
|
76
80
|
estimatedTokens: estimatedTokens(artifacts, artifact),
|
|
@@ -18,6 +18,7 @@ export type ActivationField = (typeof ACTIVATION_FIELDS)[number];
|
|
|
18
18
|
export const ACTIVATION_OPERATORS = ["eq", "in", "contains_any", "contains_all", "exists"] as const;
|
|
19
19
|
export type ActivationOperator = (typeof ACTIVATION_OPERATORS)[number];
|
|
20
20
|
export type InjectionProfile = "full" | "catalog" | "on-demand";
|
|
21
|
+
export type ActivationLabelMatch = "any" | "all";
|
|
21
22
|
|
|
22
23
|
export type ActivationPredicate =
|
|
23
24
|
| { field: ActivationField; operator: ActivationOperator; value: string | string[] | boolean }
|
|
@@ -26,7 +27,11 @@ export type ActivationPredicate =
|
|
|
26
27
|
| { not: ActivationPredicate };
|
|
27
28
|
|
|
28
29
|
export interface ActivationConfig {
|
|
30
|
+
/** Persisted manual switch, independent of lifecycle and project scope. Missing values default true. */
|
|
31
|
+
enabled: boolean;
|
|
29
32
|
predicate?: ActivationPredicate;
|
|
33
|
+
/** Opt-in bridge to the artifact label system: compare this artifact's direct labels with the active Task's labels. */
|
|
34
|
+
labels?: ActivationLabelMatch;
|
|
30
35
|
priority: number;
|
|
31
36
|
injection: InjectionProfile;
|
|
32
37
|
invalid?: true;
|
|
@@ -144,8 +149,18 @@ function validatePredicate(value: unknown, state: ValidationState, depth: number
|
|
|
144
149
|
|
|
145
150
|
export function validateActivationConfig(value: unknown, defaultInjection: InjectionProfile = "full"): ActivationConfig {
|
|
146
151
|
const input = record(value, "activation");
|
|
147
|
-
if (
|
|
148
|
-
|
|
152
|
+
if (
|
|
153
|
+
!Object.keys(input).every(
|
|
154
|
+
(key) => key === "enabled" || key === "predicate" || key === "labels" || key === "priority" || key === "injection",
|
|
155
|
+
)
|
|
156
|
+
) {
|
|
157
|
+
throw new Error("activation supports only enabled, predicate, labels, priority, and injection");
|
|
158
|
+
}
|
|
159
|
+
const enabled = input.enabled === undefined ? true : input.enabled;
|
|
160
|
+
if (typeof enabled !== "boolean") throw new Error("activation enabled must be a boolean");
|
|
161
|
+
const labels = input.labels;
|
|
162
|
+
if (labels !== undefined && labels !== "any" && labels !== "all") {
|
|
163
|
+
throw new Error("activation labels must be any or all");
|
|
149
164
|
}
|
|
150
165
|
const priority = input.priority === undefined ? 0 : input.priority;
|
|
151
166
|
if (!Number.isInteger(priority) || (priority as number) < -1000 || (priority as number) > 1000) {
|
|
@@ -156,21 +171,44 @@ export function validateActivationConfig(value: unknown, defaultInjection: Injec
|
|
|
156
171
|
throw new Error("activation injection must be full, catalog, or on-demand");
|
|
157
172
|
}
|
|
158
173
|
return {
|
|
174
|
+
enabled,
|
|
159
175
|
...(input.predicate === undefined ? {} : { predicate: validatePredicate(input.predicate, { nodes: 0 }, 1) }),
|
|
176
|
+
...(labels === undefined ? {} : { labels: labels as ActivationLabelMatch }),
|
|
160
177
|
priority: priority as number,
|
|
161
178
|
injection,
|
|
162
179
|
};
|
|
163
180
|
}
|
|
164
181
|
|
|
165
182
|
export function activationConfig(extra: Record<string, unknown>, defaultInjection: InjectionProfile = "full"): ActivationConfig {
|
|
166
|
-
if (extra.activation === undefined) return { priority: 0, injection: defaultInjection };
|
|
183
|
+
if (extra.activation === undefined) return { enabled: true, priority: 0, injection: defaultInjection };
|
|
167
184
|
try {
|
|
168
185
|
return validateActivationConfig(extra.activation, defaultInjection);
|
|
169
186
|
} catch {
|
|
170
|
-
return { priority: 0, injection: defaultInjection, invalid: true };
|
|
187
|
+
return { enabled: false, priority: 0, injection: defaultInjection, invalid: true };
|
|
171
188
|
}
|
|
172
189
|
}
|
|
173
190
|
|
|
191
|
+
/** Validates a replacement config while allowing the ergonomic standalone flag to override its enabled field. */
|
|
192
|
+
export function validateActivationInput(
|
|
193
|
+
value: unknown,
|
|
194
|
+
enabled: boolean | undefined,
|
|
195
|
+
defaultInjection: InjectionProfile,
|
|
196
|
+
): ActivationConfig | undefined {
|
|
197
|
+
if (value === undefined && enabled === undefined) return undefined;
|
|
198
|
+
const input = value === undefined ? {} : record(value, "activation");
|
|
199
|
+
return validateActivationConfig(enabled === undefined ? input : { ...input, enabled }, defaultInjection);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Applies the ergonomic standalone activation flag while retaining predicate/profile/priority settings. */
|
|
203
|
+
export function activationConfigWithEnabled(
|
|
204
|
+
extra: Record<string, unknown>,
|
|
205
|
+
enabled: boolean,
|
|
206
|
+
defaultInjection: InjectionProfile,
|
|
207
|
+
): ActivationConfig {
|
|
208
|
+
const current = extra.activation === undefined ? {} : record(extra.activation, "activation");
|
|
209
|
+
return validateActivationConfig({ ...current, enabled }, defaultInjection);
|
|
210
|
+
}
|
|
211
|
+
|
|
174
212
|
function contextValue(field: ActivationField, context: ActivationContext): string | string[] | undefined {
|
|
175
213
|
switch (field) {
|
|
176
214
|
case "project.root":
|
|
@@ -233,7 +271,24 @@ function evaluatePredicate(predicate: ActivationPredicate, context: ActivationCo
|
|
|
233
271
|
: { enabled: false, reason: `activation ${predicate.field} did not match ${predicate.operator}` };
|
|
234
272
|
}
|
|
235
273
|
|
|
236
|
-
|
|
274
|
+
function normalizeLabel(value: string): string {
|
|
275
|
+
return value.trim().toLowerCase();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function evaluateActivation(
|
|
279
|
+
config: ActivationConfig,
|
|
280
|
+
context: ActivationContext,
|
|
281
|
+
artifactLabels: readonly string[] = [],
|
|
282
|
+
): ActivationDecision {
|
|
237
283
|
if (config.invalid) return { enabled: false, reason: "invalid activation configuration" };
|
|
284
|
+
if (!config.enabled) return { enabled: false, reason: "activation flag is disabled" };
|
|
285
|
+
if (config.labels !== undefined) {
|
|
286
|
+
const expected = [...new Set(artifactLabels.map(normalizeLabel).filter(Boolean))];
|
|
287
|
+
if (expected.length === 0) return { enabled: false, reason: "activation label matching has no artifact labels" };
|
|
288
|
+
const actual = new Set((context.taskLabels ?? []).map(normalizeLabel).filter(Boolean));
|
|
289
|
+
if (actual.size === 0) return { enabled: false, reason: "activation context field task.labels is unavailable" };
|
|
290
|
+
const matches = config.labels === "all" ? expected.every((label) => actual.has(label)) : expected.some((label) => actual.has(label));
|
|
291
|
+
if (!matches) return { enabled: false, reason: `activation artifact labels did not match ${config.labels}` };
|
|
292
|
+
}
|
|
238
293
|
return config.predicate === undefined ? { enabled: true, reason: "enabled" } : evaluatePredicate(config.predicate, context);
|
|
239
294
|
}
|
|
@@ -18,6 +18,12 @@ function parseStringArray(value: string): string[] {
|
|
|
18
18
|
return parsed as string[];
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
function parseBoolean(value: string): boolean {
|
|
22
|
+
if (value === "true") return true;
|
|
23
|
+
if (value === "false") return false;
|
|
24
|
+
throw new Error("value must be true or false");
|
|
25
|
+
}
|
|
26
|
+
|
|
21
27
|
function parseObject(value: string): Record<string, unknown> {
|
|
22
28
|
const parsed = JSON.parse(value) as unknown;
|
|
23
29
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("value must be a JSON object");
|
|
@@ -52,6 +58,7 @@ const createCommand = buildCommand({
|
|
|
52
58
|
labelsJson?: string[];
|
|
53
59
|
extraJson?: Record<string, unknown>;
|
|
54
60
|
activationJson?: Record<string, unknown>;
|
|
61
|
+
activationEnabled?: boolean;
|
|
55
62
|
argumentsJson?: unknown[] | Record<string, unknown>;
|
|
56
63
|
projectRoot?: string;
|
|
57
64
|
projectsJson?: string[];
|
|
@@ -66,6 +73,7 @@ const createCommand = buildCommand({
|
|
|
66
73
|
labels: flags.labelsJson,
|
|
67
74
|
extra: flags.extraJson,
|
|
68
75
|
activation: flags.activationJson,
|
|
76
|
+
activation_enabled: flags.activationEnabled,
|
|
69
77
|
arguments: flags.argumentsJson,
|
|
70
78
|
project_root: flags.projectRoot,
|
|
71
79
|
projects: flags.projectsJson,
|
|
@@ -88,12 +96,19 @@ const createCommand = buildCommand({
|
|
|
88
96
|
labelsJson: { brief: "JSON string array of labels", kind: "parsed", parse: parseStringArray, placeholder: "json", optional: true },
|
|
89
97
|
extraJson: { brief: "JSON object of extra fields", kind: "parsed", parse: parseObject, placeholder: "json", optional: true },
|
|
90
98
|
activationJson: {
|
|
91
|
-
brief: "Typed activation config as JSON: {predicate?,priority?,injection?}",
|
|
99
|
+
brief: "Typed activation config as JSON: {enabled?,predicate?,labels?,priority?,injection?}",
|
|
92
100
|
kind: "parsed",
|
|
93
101
|
parse: parseObject,
|
|
94
102
|
placeholder: "json",
|
|
95
103
|
optional: true,
|
|
96
104
|
},
|
|
105
|
+
activationEnabled: {
|
|
106
|
+
brief: "Persisted manual activation flag (true|false)",
|
|
107
|
+
kind: "parsed",
|
|
108
|
+
parse: parseBoolean,
|
|
109
|
+
placeholder: "boolean",
|
|
110
|
+
optional: true,
|
|
111
|
+
},
|
|
97
112
|
argumentsJson: {
|
|
98
113
|
brief: "JSON array of declared argument definitions",
|
|
99
114
|
kind: "parsed",
|
|
@@ -422,6 +437,7 @@ const updateCommand = buildCommand({
|
|
|
422
437
|
trigger?: string;
|
|
423
438
|
stepsJson?: unknown[] | Record<string, unknown>;
|
|
424
439
|
activationJson?: Record<string, unknown>;
|
|
440
|
+
activationEnabled?: boolean;
|
|
425
441
|
},
|
|
426
442
|
id: string,
|
|
427
443
|
) {
|
|
@@ -431,9 +447,12 @@ const updateCommand = buildCommand({
|
|
|
431
447
|
flags.labelsJson === undefined &&
|
|
432
448
|
flags.trigger === undefined &&
|
|
433
449
|
flags.stepsJson === undefined &&
|
|
434
|
-
flags.activationJson === undefined
|
|
450
|
+
flags.activationJson === undefined &&
|
|
451
|
+
flags.activationEnabled === undefined
|
|
435
452
|
)
|
|
436
|
-
throw new Error(
|
|
453
|
+
throw new Error(
|
|
454
|
+
"playbooks update requires --title, --body, --labels-json, --trigger, --steps-json, --activation-json, or --activation-enabled",
|
|
455
|
+
);
|
|
437
456
|
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>("playbooks.update", {
|
|
438
457
|
id,
|
|
439
458
|
title: flags.title,
|
|
@@ -442,6 +461,7 @@ const updateCommand = buildCommand({
|
|
|
442
461
|
trigger: flags.trigger,
|
|
443
462
|
steps: flags.stepsJson,
|
|
444
463
|
activation: flags.activationJson,
|
|
464
|
+
activation_enabled: flags.activationEnabled,
|
|
445
465
|
});
|
|
446
466
|
render.call(this, artifact, artifactLabel(artifact));
|
|
447
467
|
},
|
|
@@ -465,6 +485,13 @@ const updateCommand = buildCommand({
|
|
|
465
485
|
placeholder: "json",
|
|
466
486
|
optional: true,
|
|
467
487
|
},
|
|
488
|
+
activationEnabled: {
|
|
489
|
+
brief: "Persisted manual activation flag (true|false)",
|
|
490
|
+
kind: "parsed",
|
|
491
|
+
parse: parseBoolean,
|
|
492
|
+
placeholder: "boolean",
|
|
493
|
+
optional: true,
|
|
494
|
+
},
|
|
468
495
|
},
|
|
469
496
|
positional: { kind: "tuple", parameters: [{ brief: "Playbook id", parse: String, placeholder: "id" }] },
|
|
470
497
|
},
|
package/src/cli/rules-command.ts
CHANGED
|
@@ -20,6 +20,12 @@ function parseStringArray(value: string): string[] {
|
|
|
20
20
|
return parsed as string[];
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
function parseBoolean(value: string): boolean {
|
|
24
|
+
if (value === "true") return true;
|
|
25
|
+
if (value === "false") return false;
|
|
26
|
+
throw new Error("value must be true or false");
|
|
27
|
+
}
|
|
28
|
+
|
|
23
29
|
function parseObject(value: string): Record<string, unknown> {
|
|
24
30
|
const parsed = JSON.parse(value) as unknown;
|
|
25
31
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("value must be a JSON object");
|
|
@@ -42,6 +48,7 @@ const createCommand = buildCommand({
|
|
|
42
48
|
labelsJson?: string[];
|
|
43
49
|
extraJson?: Record<string, unknown>;
|
|
44
50
|
activationJson?: Record<string, unknown>;
|
|
51
|
+
activationEnabled?: boolean;
|
|
45
52
|
projectRoot?: string;
|
|
46
53
|
},
|
|
47
54
|
) {
|
|
@@ -54,6 +61,7 @@ const createCommand = buildCommand({
|
|
|
54
61
|
labels: flags.labelsJson,
|
|
55
62
|
extra: flags.extraJson,
|
|
56
63
|
activation: flags.activationJson,
|
|
64
|
+
activation_enabled: flags.activationEnabled,
|
|
57
65
|
project_root: flags.projectRoot,
|
|
58
66
|
});
|
|
59
67
|
render.call(this, artifact, `Created rule: ${artifactLabel(artifact)}`);
|
|
@@ -68,12 +76,19 @@ const createCommand = buildCommand({
|
|
|
68
76
|
labelsJson: { brief: "JSON string array of labels", kind: "parsed", parse: parseStringArray, placeholder: "json", optional: true },
|
|
69
77
|
extraJson: { brief: "JSON object of extra fields", kind: "parsed", parse: parseObject, placeholder: "json", optional: true },
|
|
70
78
|
activationJson: {
|
|
71
|
-
brief: "Typed activation config as JSON: {predicate?,priority?,injection?}",
|
|
79
|
+
brief: "Typed activation config as JSON: {enabled?,predicate?,labels?,priority?,injection?}",
|
|
72
80
|
kind: "parsed",
|
|
73
81
|
parse: parseObject,
|
|
74
82
|
placeholder: "json",
|
|
75
83
|
optional: true,
|
|
76
84
|
},
|
|
85
|
+
activationEnabled: {
|
|
86
|
+
brief: "Persisted manual activation flag (true|false)",
|
|
87
|
+
kind: "parsed",
|
|
88
|
+
parse: parseBoolean,
|
|
89
|
+
placeholder: "boolean",
|
|
90
|
+
optional: true,
|
|
91
|
+
},
|
|
77
92
|
projectRoot: { brief: "Project scope", kind: "parsed", parse: String, placeholder: "path", optional: true },
|
|
78
93
|
},
|
|
79
94
|
},
|
|
@@ -352,17 +367,30 @@ const injectableCommand = buildCommand({
|
|
|
352
367
|
const updateCommand = buildCommand({
|
|
353
368
|
func: async function (
|
|
354
369
|
this: RulesContext,
|
|
355
|
-
flags: {
|
|
370
|
+
flags: {
|
|
371
|
+
title?: string;
|
|
372
|
+
body?: string;
|
|
373
|
+
labelsJson?: string[];
|
|
374
|
+
activationJson?: Record<string, unknown>;
|
|
375
|
+
activationEnabled?: boolean;
|
|
376
|
+
},
|
|
356
377
|
id: string,
|
|
357
378
|
) {
|
|
358
|
-
if (
|
|
359
|
-
|
|
379
|
+
if (
|
|
380
|
+
flags.title === undefined &&
|
|
381
|
+
flags.body === undefined &&
|
|
382
|
+
flags.labelsJson === undefined &&
|
|
383
|
+
flags.activationJson === undefined &&
|
|
384
|
+
flags.activationEnabled === undefined
|
|
385
|
+
)
|
|
386
|
+
throw new Error("rules update requires --title, --body, --labels-json, --activation-json, or --activation-enabled");
|
|
360
387
|
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>("rules.update", {
|
|
361
388
|
id,
|
|
362
389
|
title: flags.title,
|
|
363
390
|
body: flags.body,
|
|
364
391
|
labels: flags.labelsJson,
|
|
365
392
|
activation: flags.activationJson,
|
|
393
|
+
activation_enabled: flags.activationEnabled,
|
|
366
394
|
});
|
|
367
395
|
render.call(this, artifact, artifactLabel(artifact));
|
|
368
396
|
},
|
|
@@ -378,6 +406,13 @@ const updateCommand = buildCommand({
|
|
|
378
406
|
placeholder: "json",
|
|
379
407
|
optional: true,
|
|
380
408
|
},
|
|
409
|
+
activationEnabled: {
|
|
410
|
+
brief: "Persisted manual activation flag (true|false)",
|
|
411
|
+
kind: "parsed",
|
|
412
|
+
parse: parseBoolean,
|
|
413
|
+
placeholder: "boolean",
|
|
414
|
+
optional: true,
|
|
415
|
+
},
|
|
381
416
|
},
|
|
382
417
|
positional: { kind: "tuple", parameters: [{ brief: "Rule id", parse: String, placeholder: "id" }] },
|
|
383
418
|
},
|
|
@@ -26,7 +26,7 @@ export function registerActivationVehicleOperations(
|
|
|
26
26
|
});
|
|
27
27
|
define(
|
|
28
28
|
"audit",
|
|
29
|
-
"Audits every Rule and Playbook against lifecycle, project/scope-group applicability, run ownership, and typed
|
|
29
|
+
"Audits every Rule and Playbook against lifecycle, project/scope-group applicability, run ownership, the persisted activation flag, artifact-label matching, and typed predicates. Returns enabled/disabled decisions, exclusion reasons, activation settings, priority, injection profile, scope counts, and estimated enabled tokens.",
|
|
30
30
|
"read",
|
|
31
31
|
{
|
|
32
32
|
project_root: stringProp,
|
|
@@ -105,7 +105,8 @@ export function registerPlaybooksVehicleOperations(registry: VehicleRegistry, de
|
|
|
105
105
|
subtype: stringProp,
|
|
106
106
|
labels: { type: "array" },
|
|
107
107
|
extra: { type: "object" },
|
|
108
|
-
activation: { type: "object", description: "Typed activation config: {predicate?,priority?,injection?}." },
|
|
108
|
+
activation: { type: "object", description: "Typed activation config: {enabled?,predicate?,labels?,priority?,injection?}." },
|
|
109
|
+
activation_enabled: { ...booleanProp, description: "Persisted manual activation flag; defaults true." },
|
|
109
110
|
template_id: stringProp,
|
|
110
111
|
project_root: stringProp,
|
|
111
112
|
projects: { type: "array" },
|
|
@@ -327,6 +328,7 @@ export function registerPlaybooksVehicleOperations(registry: VehicleRegistry, de
|
|
|
327
328
|
trigger: stringProp,
|
|
328
329
|
steps: { type: "array" },
|
|
329
330
|
activation: { type: "object", description: "Replacement typed activation config." },
|
|
331
|
+
activation_enabled: { ...booleanProp, description: "Persisted manual activation flag; retains other activation settings." },
|
|
330
332
|
actor: stringProp,
|
|
331
333
|
source: stringProp,
|
|
332
334
|
session_id: stringProp,
|
package/src/handlers/rules.ts
CHANGED
|
@@ -77,9 +77,13 @@ export function registerRulesVehicleOperations(
|
|
|
77
77
|
subtype: stringProp,
|
|
78
78
|
labels: { type: "array" } as unknown as { type: string },
|
|
79
79
|
extra: { type: "object" } as unknown as { type: string },
|
|
80
|
-
activation: {
|
|
80
|
+
activation: {
|
|
81
|
+
type: "object",
|
|
82
|
+
description: "Typed activation config: {enabled?,predicate?,labels?,priority?,injection?}.",
|
|
83
|
+
} as unknown as {
|
|
81
84
|
type: string;
|
|
82
85
|
},
|
|
86
|
+
activation_enabled: { ...booleanProp, description: "Persisted manual activation flag; defaults true." },
|
|
83
87
|
template_id: stringProp,
|
|
84
88
|
project_root: stringProp,
|
|
85
89
|
projects: { type: "array" } as unknown as { type: string },
|
|
@@ -318,6 +322,7 @@ export function registerRulesVehicleOperations(
|
|
|
318
322
|
body: stringProp,
|
|
319
323
|
labels: { type: "array" } as unknown as { type: string },
|
|
320
324
|
activation: { type: "object", description: "Replacement typed activation config." } as unknown as { type: string },
|
|
325
|
+
activation_enabled: { ...booleanProp, description: "Persisted manual activation flag; retains other activation settings." },
|
|
321
326
|
project_root: stringProp,
|
|
322
327
|
actor: stringProp,
|
|
323
328
|
source: stringProp,
|
package/src/index.ts
CHANGED
package/src/modules/playbooks.ts
CHANGED
|
@@ -150,6 +150,7 @@ export function playbooksOperations({
|
|
|
150
150
|
labels: input.labels as string[] | undefined,
|
|
151
151
|
extra: input.extra as Record<string, unknown> | undefined,
|
|
152
152
|
activation: input.activation,
|
|
153
|
+
activationEnabled: optionalBoolean(input, "activation_enabled"),
|
|
153
154
|
templateId: optionalString(input, "template_id") ?? optionalString(input, "templateId"),
|
|
154
155
|
projectRoot: optionalString(input, "project_root"),
|
|
155
156
|
projectReferences: input.projects as string[] | undefined,
|
|
@@ -243,6 +244,7 @@ export function playbooksOperations({
|
|
|
243
244
|
trigger: optionalString(input, "trigger"),
|
|
244
245
|
steps: input.steps,
|
|
245
246
|
activation: input.activation,
|
|
247
|
+
activationEnabled: optionalBoolean(input, "activation_enabled"),
|
|
246
248
|
},
|
|
247
249
|
eventContext(input),
|
|
248
250
|
),
|
package/src/modules/rules.ts
CHANGED
|
@@ -121,6 +121,7 @@ export function rulesOperations(
|
|
|
121
121
|
labels: input.labels as string[] | undefined,
|
|
122
122
|
extra: input.extra as Record<string, unknown> | undefined,
|
|
123
123
|
activation: input.activation,
|
|
124
|
+
activationEnabled: optionalBoolean(input, "activation_enabled"),
|
|
124
125
|
templateId: optionalString(input, "template_id") ?? optionalString(input, "templateId"),
|
|
125
126
|
projectRoot: optionalString(input, "project_root"),
|
|
126
127
|
projectReferences: input.projects as string[] | undefined,
|
|
@@ -184,6 +185,7 @@ export function rulesOperations(
|
|
|
184
185
|
body: optionalString(input, "body"),
|
|
185
186
|
labels: input.labels as string[] | undefined,
|
|
186
187
|
activation: input.activation,
|
|
188
|
+
activationEnabled: optionalBoolean(input, "activation_enabled"),
|
|
187
189
|
},
|
|
188
190
|
eventContext(input),
|
|
189
191
|
),
|
|
@@ -27,9 +27,10 @@ import { requireLocallyOwnedContent } from "../artifact/artifact.ts";
|
|
|
27
27
|
import {
|
|
28
28
|
type ActivationContext,
|
|
29
29
|
activationConfig,
|
|
30
|
+
activationConfigWithEnabled,
|
|
30
31
|
evaluateActivation,
|
|
31
32
|
type InjectionProfile,
|
|
32
|
-
|
|
33
|
+
validateActivationInput,
|
|
33
34
|
} from "../artifact/artifact-activation.ts";
|
|
34
35
|
import type { ArtifactEventContext } from "../artifact/artifact-event.ts";
|
|
35
36
|
import type { ArtifactScope, ArtifactScopeStore } from "../artifact/artifact-scope-store.ts";
|
|
@@ -234,6 +235,7 @@ export interface CreatePlaybookInput {
|
|
|
234
235
|
labels?: string[];
|
|
235
236
|
extra?: Record<string, unknown>;
|
|
236
237
|
activation?: unknown;
|
|
238
|
+
activationEnabled?: boolean;
|
|
237
239
|
templateId?: string;
|
|
238
240
|
projectRoot?: string;
|
|
239
241
|
/** Bounded exact registered project references (id/name/alias/root) -- fail-closed unlike projectRoot's auto-register-by-root legacy form. Takes precedence over projectRoot when both are given. */
|
|
@@ -255,6 +257,7 @@ export interface UpdatePlaybookInput extends UpdateContentInput {
|
|
|
255
257
|
trigger?: string;
|
|
256
258
|
steps?: unknown;
|
|
257
259
|
activation?: unknown;
|
|
260
|
+
activationEnabled?: boolean;
|
|
258
261
|
}
|
|
259
262
|
|
|
260
263
|
const PLAYBOOK_TRANSITIONS: TransitionTable<PlaybookTransition, string> = {
|
|
@@ -275,7 +278,7 @@ export function createPlaybook(
|
|
|
275
278
|
const projectRoot = input.projectRoot === undefined ? undefined : normalizeProjectRoot(input.projectRoot);
|
|
276
279
|
const declaredArguments = validatePlaybookArguments(input.arguments);
|
|
277
280
|
const declaredSteps = validatePlaybookSteps(input.steps);
|
|
278
|
-
const activation = input.activation
|
|
281
|
+
const activation = validateActivationInput(input.activation, input.activationEnabled, "catalog");
|
|
279
282
|
const playbook = artifacts.create(
|
|
280
283
|
{
|
|
281
284
|
kind: "playbook",
|
|
@@ -315,7 +318,7 @@ export function listActivatedPlaybooks(
|
|
|
315
318
|
context: ActivationContext,
|
|
316
319
|
): Artifact[] {
|
|
317
320
|
return listPlaybooks(artifacts, scopes, filter).filter(
|
|
318
|
-
(playbook) => evaluateActivation(activationConfig(playbook.extra, "catalog"), context).enabled,
|
|
321
|
+
(playbook) => evaluateActivation(activationConfig(playbook.extra, "catalog"), context, playbook.labels).enabled,
|
|
319
322
|
);
|
|
320
323
|
}
|
|
321
324
|
|
|
@@ -324,7 +327,7 @@ export function playbookActivationDecision(
|
|
|
324
327
|
context: ActivationContext,
|
|
325
328
|
): { enabled: boolean; reason: string; priority: number; injection: InjectionProfile } {
|
|
326
329
|
const config = activationConfig(playbook.extra, "catalog");
|
|
327
|
-
return { ...evaluateActivation(config, context), priority: config.priority, injection: config.injection };
|
|
330
|
+
return { ...evaluateActivation(config, context, playbook.labels), priority: config.priority, injection: config.injection };
|
|
328
331
|
}
|
|
329
332
|
|
|
330
333
|
export function assignPlaybookProject(
|
|
@@ -440,9 +443,10 @@ export function updatePlaybook(artifacts: ArtifactStore, id: string, input: Upda
|
|
|
440
443
|
input.labels === undefined &&
|
|
441
444
|
input.trigger === undefined &&
|
|
442
445
|
input.steps === undefined &&
|
|
443
|
-
input.activation === undefined
|
|
446
|
+
input.activation === undefined &&
|
|
447
|
+
input.activationEnabled === undefined
|
|
444
448
|
) {
|
|
445
|
-
throw new Error("update requires title, body, labels, trigger, steps, or
|
|
449
|
+
throw new Error("update requires title, body, labels, trigger, steps, activation, or activationEnabled");
|
|
446
450
|
}
|
|
447
451
|
assertTitleBounds(input.title);
|
|
448
452
|
assertBodyBounds(input.body);
|
|
@@ -454,8 +458,12 @@ export function updatePlaybook(artifacts: ArtifactStore, id: string, input: Upda
|
|
|
454
458
|
const hasContentFields = input.title !== undefined || input.body !== undefined || input.labels !== undefined;
|
|
455
459
|
const updated = hasContentFields ? artifacts.updateContent(playbook.id, input, context) : playbook;
|
|
456
460
|
if (!updated) throw new Error(`playbook "${id}" not found`);
|
|
457
|
-
if (declaredSteps === undefined && input.trigger === undefined && input.activation === undefined
|
|
458
|
-
|
|
461
|
+
if (declaredSteps === undefined && input.trigger === undefined && input.activation === undefined && input.activationEnabled === undefined)
|
|
462
|
+
return updated;
|
|
463
|
+
const activation =
|
|
464
|
+
input.activation === undefined && input.activationEnabled !== undefined
|
|
465
|
+
? activationConfigWithEnabled(updated.extra, input.activationEnabled, "catalog")
|
|
466
|
+
: validateActivationInput(input.activation, input.activationEnabled, "catalog");
|
|
459
467
|
const withExtra = artifacts.setExtra(
|
|
460
468
|
updated.id,
|
|
461
469
|
{
|
|
@@ -10,9 +10,10 @@ import { requireLocallyOwnedContent } from "../artifact/artifact.ts";
|
|
|
10
10
|
import {
|
|
11
11
|
type ActivationContext,
|
|
12
12
|
activationConfig,
|
|
13
|
+
activationConfigWithEnabled,
|
|
13
14
|
evaluateActivation,
|
|
14
15
|
type InjectionProfile,
|
|
15
|
-
|
|
16
|
+
validateActivationInput,
|
|
16
17
|
} from "../artifact/artifact-activation.ts";
|
|
17
18
|
import type { ArtifactEventContext } from "../artifact/artifact-event.ts";
|
|
18
19
|
import type { ArtifactScope, ArtifactScopeStore } from "../artifact/artifact-scope-store.ts";
|
|
@@ -50,6 +51,7 @@ export interface CreateRuleInput {
|
|
|
50
51
|
labels?: string[];
|
|
51
52
|
extra?: Record<string, unknown>;
|
|
52
53
|
activation?: unknown;
|
|
54
|
+
activationEnabled?: boolean;
|
|
53
55
|
templateId?: string;
|
|
54
56
|
projectRoot?: string;
|
|
55
57
|
/** Bounded exact registered project references (id/name/alias/root) -- fail-closed unlike projectRoot's auto-register-by-root legacy form. Takes precedence over projectRoot when both are given. */
|
|
@@ -142,7 +144,7 @@ export function createRule(
|
|
|
142
144
|
registry?: ProjectRegistryStore,
|
|
143
145
|
): Artifact {
|
|
144
146
|
assertRuleTextWithinBounds(input.condition, input.action, input.body);
|
|
145
|
-
const activation = input.activation
|
|
147
|
+
const activation = validateActivationInput(input.activation, input.activationEnabled, "full");
|
|
146
148
|
if (input.projectReferences !== undefined && input.projectReferences.length > 0 && registry === undefined) {
|
|
147
149
|
throw new Error("projectReferences requires a project registry");
|
|
148
150
|
}
|
|
@@ -308,7 +310,7 @@ export function listInjectableRules(
|
|
|
308
310
|
return artifacts.query({ kind: "rule", status: "active" }).filter((rule) => {
|
|
309
311
|
if (rule.subtype === "artifact-template") return false;
|
|
310
312
|
if (!passesRuleRunScope(rule, activeTaskId) || !scopes.appliesToProjectRoot(rule.id, projectRoot)) return false;
|
|
311
|
-
return evaluateActivation(activationConfig(rule.extra, "full"), { ...context, projectRoot }).enabled;
|
|
313
|
+
return evaluateActivation(activationConfig(rule.extra, "full"), { ...context, projectRoot }, rule.labels).enabled;
|
|
312
314
|
});
|
|
313
315
|
}
|
|
314
316
|
|
|
@@ -317,7 +319,7 @@ export function ruleActivationDecision(
|
|
|
317
319
|
context: ActivationContext,
|
|
318
320
|
): { enabled: boolean; reason: string; priority: number; injection: InjectionProfile } {
|
|
319
321
|
const config = activationConfig(rule.extra, "full");
|
|
320
|
-
return { ...evaluateActivation(config, context), priority: config.priority, injection: config.injection };
|
|
322
|
+
return { ...evaluateActivation(config, context, rule.labels), priority: config.priority, injection: config.injection };
|
|
321
323
|
}
|
|
322
324
|
|
|
323
325
|
export function showRule(artifacts: ArtifactStore, id: string): Artifact {
|
|
@@ -340,12 +342,19 @@ export function transitionRule(artifacts: ArtifactStore, id: string, action: Rul
|
|
|
340
342
|
|
|
341
343
|
export interface UpdateRuleInput extends UpdateContentInput {
|
|
342
344
|
activation?: unknown;
|
|
345
|
+
activationEnabled?: boolean;
|
|
343
346
|
}
|
|
344
347
|
|
|
345
348
|
/** A Rule's body update stays under the same combined condition+action+body ceiling as creation -- a permanent per-turn injection cost doesn't get looser just because it's an edit, not a create. */
|
|
346
349
|
export function updateRule(artifacts: ArtifactStore, id: string, input: UpdateRuleInput, context?: ArtifactEventContext): Artifact {
|
|
347
|
-
if (
|
|
348
|
-
|
|
350
|
+
if (
|
|
351
|
+
input.title === undefined &&
|
|
352
|
+
input.body === undefined &&
|
|
353
|
+
input.labels === undefined &&
|
|
354
|
+
input.activation === undefined &&
|
|
355
|
+
input.activationEnabled === undefined
|
|
356
|
+
) {
|
|
357
|
+
throw new Error("update requires title, body, labels, activation, or activationEnabled");
|
|
349
358
|
}
|
|
350
359
|
assertTitleBounds(input.title);
|
|
351
360
|
assertLabelsBounds(input.labels);
|
|
@@ -358,8 +367,11 @@ export function updateRule(artifacts: ArtifactStore, id: string, input: UpdateRu
|
|
|
358
367
|
const hasContent = input.title !== undefined || input.body !== undefined || input.labels !== undefined;
|
|
359
368
|
const updated = hasContent ? artifacts.updateContent(id, input, context) : rule;
|
|
360
369
|
if (!updated) throw new Error(`rule "${id}" not found`);
|
|
361
|
-
if (input.activation === undefined) return updated;
|
|
362
|
-
const activation =
|
|
370
|
+
if (input.activation === undefined && input.activationEnabled === undefined) return updated;
|
|
371
|
+
const activation =
|
|
372
|
+
input.activation === undefined
|
|
373
|
+
? activationConfigWithEnabled(updated.extra, input.activationEnabled!, "full")
|
|
374
|
+
: validateActivationInput(input.activation, input.activationEnabled, "full")!;
|
|
363
375
|
const withActivation = artifacts.setExtra(updated.id, { ...updated.extra, activation }, context);
|
|
364
376
|
if (!withActivation) throw new Error(`rule "${id}" not found`);
|
|
365
377
|
return withActivation;
|