@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/papyrus",
3
- "version": "0.60.6",
3
+ "version": "0.60.7",
4
4
  "description": "Daemon-backed graph artifacts, evidence-bearing tasks, rules, skills, and native TUI workflows for Pi",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -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 (!Object.keys(input).every((key) => key === "predicate" || key === "priority" || key === "injection")) {
148
- throw new Error("activation supports only predicate, priority, and injection");
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
- export function evaluateActivation(config: ActivationConfig, context: ActivationContext): ActivationDecision {
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("playbooks update requires --title, --body, --labels-json, --trigger, --steps-json, or --activation-json");
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
  },
@@ -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: { title?: string; body?: string; labelsJson?: string[]; activationJson?: Record<string, unknown> },
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 (flags.title === undefined && flags.body === undefined && flags.labelsJson === undefined && flags.activationJson === undefined)
359
- throw new Error("rules update requires --title, --body, --labels-json, or --activation-json");
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 activation predicates. Returns enabled/disabled decisions, exclusion reasons, priority, injection profile, scope counts, and estimated enabled tokens.",
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,
@@ -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: { type: "object", description: "Typed activation config: {predicate?,priority?,injection?}." } as unknown as {
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
@@ -9,6 +9,7 @@ export type { Artifact, ArtifactEdge } from "./artifact/artifact.ts";
9
9
  export {
10
10
  type ActivationConfig,
11
11
  type ActivationContext,
12
+ type ActivationLabelMatch,
12
13
  type ActivationPredicate,
13
14
  activationConfig,
14
15
  type InjectionProfile,
@@ -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
  ),
@@ -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
- validateActivationConfig,
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 === undefined ? undefined : validateActivationConfig(input.activation, "catalog");
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 activation");
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) return updated;
458
- const activation = input.activation === undefined ? undefined : validateActivationConfig(input.activation, "catalog");
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
- validateActivationConfig,
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 === undefined ? undefined : validateActivationConfig(input.activation, "full");
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 (input.title === undefined && input.body === undefined && input.labels === undefined && input.activation === undefined) {
348
- throw new Error("update requires title, body, labels, or activation");
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 = validateActivationConfig(input.activation, "full");
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;