@danypops/papyrus 0.60.4 → 0.60.6
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 +96 -0
- package/src/artifact/artifact-activation.ts +239 -0
- package/src/cli/activation-command.ts +81 -0
- package/src/cli/playbooks-command.ts +47 -4
- package/src/cli/rules-command.ts +37 -5
- package/src/cli.ts +8 -0
- package/src/constants.ts +2 -0
- package/src/handlers/activation.ts +39 -0
- package/src/handlers/playbooks.ts +12 -1
- package/src/handlers/registry.ts +2 -0
- package/src/handlers/rules.ts +4 -0
- package/src/index.ts +7 -0
- package/src/modules/playbooks.ts +19 -1
- package/src/modules/rules.ts +2 -0
- package/src/playbook/playbook-service.ts +36 -3
- package/src/rules/rules-service.ts +35 -7
- package/src/service.ts +21 -3
package/package.json
CHANGED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { ARTIFACT_SCOPE_MAX_ARTIFACTS, CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN } from "../constants.ts";
|
|
2
|
+
import { passesRuleRunScope, previewRule } from "../rules/rules-service.ts";
|
|
3
|
+
import type { Artifact } from "./artifact.ts";
|
|
4
|
+
import { type ActivationContext, activationConfig, evaluateActivation, type InjectionProfile } from "./artifact-activation.ts";
|
|
5
|
+
import type { ArtifactScopeMode, ArtifactScopeStore } from "./artifact-scope-store.ts";
|
|
6
|
+
import type { ArtifactStore } from "./artifact-store.ts";
|
|
7
|
+
|
|
8
|
+
export interface ActivationAuditEntry {
|
|
9
|
+
id: string;
|
|
10
|
+
kind: "rule" | "playbook";
|
|
11
|
+
title: string;
|
|
12
|
+
status: string;
|
|
13
|
+
scopeMode: ArtifactScopeMode;
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
reason: string;
|
|
16
|
+
priority: number;
|
|
17
|
+
injection: InjectionProfile;
|
|
18
|
+
estimatedTokens: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ActivationAudit {
|
|
22
|
+
projectRoot: string;
|
|
23
|
+
entries: ActivationAuditEntry[];
|
|
24
|
+
summary: {
|
|
25
|
+
total: number;
|
|
26
|
+
enabled: number;
|
|
27
|
+
disabled: number;
|
|
28
|
+
global: number;
|
|
29
|
+
explicit: number;
|
|
30
|
+
hidden: number;
|
|
31
|
+
estimatedEnabledTokens: number;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function estimatedTokens(artifacts: ArtifactStore, artifact: Artifact): number {
|
|
36
|
+
const text =
|
|
37
|
+
artifact.kind === "rule"
|
|
38
|
+
? previewRule(artifacts, artifact.id)
|
|
39
|
+
: `• ${artifact.title} (when: ${typeof artifact.extra.trigger === "string" ? artifact.extra.trigger : "manual invocation"})`;
|
|
40
|
+
return Math.ceil(text.length / CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function auditArtifactActivation(
|
|
44
|
+
artifacts: ArtifactStore,
|
|
45
|
+
scopes: ArtifactScopeStore,
|
|
46
|
+
projectRoot: string,
|
|
47
|
+
activeTaskId: string | undefined,
|
|
48
|
+
context: ActivationContext,
|
|
49
|
+
): ActivationAudit {
|
|
50
|
+
const rows = [
|
|
51
|
+
...artifacts.query({ kind: "rule", limit: ARTIFACT_SCOPE_MAX_ARTIFACTS }),
|
|
52
|
+
...artifacts.query({ kind: "playbook", limit: ARTIFACT_SCOPE_MAX_ARTIFACTS }),
|
|
53
|
+
] as Artifact[];
|
|
54
|
+
const entries = rows
|
|
55
|
+
.map((artifact): ActivationAuditEntry => {
|
|
56
|
+
const scope = scopes.scope(artifact.id);
|
|
57
|
+
const config = activationConfig(artifact.extra, artifact.kind === "rule" ? "full" : "catalog");
|
|
58
|
+
let decision: { enabled: boolean; reason: string };
|
|
59
|
+
if (artifact.status !== "active") decision = { enabled: false, reason: `lifecycle status is ${artifact.status}` };
|
|
60
|
+
else if (!scopes.appliesToProjectRoot(artifact.id, projectRoot))
|
|
61
|
+
decision = { enabled: false, reason: `scope ${scope.mode} does not apply` };
|
|
62
|
+
else if (artifact.kind === "rule" && artifact.subtype === "artifact-template") {
|
|
63
|
+
decision = { enabled: false, reason: "rule is an artifact template" };
|
|
64
|
+
} else if (artifact.kind === "rule" && !passesRuleRunScope(artifact, activeTaskId)) {
|
|
65
|
+
decision = { enabled: false, reason: "run ownership does not apply" };
|
|
66
|
+
} else decision = evaluateActivation(config, { ...context, projectRoot });
|
|
67
|
+
return {
|
|
68
|
+
id: artifact.id,
|
|
69
|
+
kind: artifact.kind as "rule" | "playbook",
|
|
70
|
+
title: artifact.title,
|
|
71
|
+
status: artifact.status,
|
|
72
|
+
scopeMode: scope.mode,
|
|
73
|
+
...decision,
|
|
74
|
+
priority: config.priority,
|
|
75
|
+
injection: config.injection,
|
|
76
|
+
estimatedTokens: estimatedTokens(artifacts, artifact),
|
|
77
|
+
};
|
|
78
|
+
})
|
|
79
|
+
.sort(
|
|
80
|
+
(left, right) =>
|
|
81
|
+
Number(right.enabled) - Number(left.enabled) || right.priority - left.priority || left.title.localeCompare(right.title),
|
|
82
|
+
);
|
|
83
|
+
return {
|
|
84
|
+
projectRoot,
|
|
85
|
+
entries,
|
|
86
|
+
summary: {
|
|
87
|
+
total: entries.length,
|
|
88
|
+
enabled: entries.filter((entry) => entry.enabled).length,
|
|
89
|
+
disabled: entries.filter((entry) => !entry.enabled).length,
|
|
90
|
+
global: entries.filter((entry) => entry.scopeMode === "all").length,
|
|
91
|
+
explicit: entries.filter((entry) => entry.scopeMode === "explicit").length,
|
|
92
|
+
hidden: entries.filter((entry) => entry.scopeMode === "none").length,
|
|
93
|
+
estimatedEnabledTokens: entries.filter((entry) => entry.enabled).reduce((sum, entry) => sum + entry.estimatedTokens, 0),
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
export const ACTIVATION_MAX_DEPTH = 8;
|
|
2
|
+
export const ACTIVATION_MAX_NODES = 64;
|
|
3
|
+
export const ACTIVATION_MAX_VALUES = 32;
|
|
4
|
+
export const ACTIVATION_VALUE_MAX_LENGTH = 256;
|
|
5
|
+
|
|
6
|
+
export const ACTIVATION_FIELDS = [
|
|
7
|
+
"project.root",
|
|
8
|
+
"task.status",
|
|
9
|
+
"task.labels",
|
|
10
|
+
"languages",
|
|
11
|
+
"file.extensions",
|
|
12
|
+
"tool.name",
|
|
13
|
+
"operation.name",
|
|
14
|
+
"session.capabilities",
|
|
15
|
+
] as const;
|
|
16
|
+
export type ActivationField = (typeof ACTIVATION_FIELDS)[number];
|
|
17
|
+
|
|
18
|
+
export const ACTIVATION_OPERATORS = ["eq", "in", "contains_any", "contains_all", "exists"] as const;
|
|
19
|
+
export type ActivationOperator = (typeof ACTIVATION_OPERATORS)[number];
|
|
20
|
+
export type InjectionProfile = "full" | "catalog" | "on-demand";
|
|
21
|
+
|
|
22
|
+
export type ActivationPredicate =
|
|
23
|
+
| { field: ActivationField; operator: ActivationOperator; value: string | string[] | boolean }
|
|
24
|
+
| { all: ActivationPredicate[] }
|
|
25
|
+
| { any: ActivationPredicate[] }
|
|
26
|
+
| { not: ActivationPredicate };
|
|
27
|
+
|
|
28
|
+
export interface ActivationConfig {
|
|
29
|
+
predicate?: ActivationPredicate;
|
|
30
|
+
priority: number;
|
|
31
|
+
injection: InjectionProfile;
|
|
32
|
+
invalid?: true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ActivationContext {
|
|
36
|
+
projectRoot?: string;
|
|
37
|
+
taskStatus?: string;
|
|
38
|
+
taskLabels?: string[];
|
|
39
|
+
languages?: string[];
|
|
40
|
+
fileExtensions?: string[];
|
|
41
|
+
toolName?: string;
|
|
42
|
+
operationName?: string;
|
|
43
|
+
sessionCapabilities?: string[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ActivationDecision {
|
|
47
|
+
enabled: boolean;
|
|
48
|
+
reason: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function optionalContextString(value: unknown, label: string): string | undefined {
|
|
52
|
+
return value === undefined ? undefined : boundedString(value, label);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function optionalContextStrings(value: unknown, label: string): string[] | undefined {
|
|
56
|
+
return value === undefined ? undefined : stringValues(value, label);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Parses caller-supplied activation signals. Project and active-Task fields are supplied server-side and intentionally cannot be spoofed through this object. */
|
|
60
|
+
export function activationContextFromInput(value: unknown): ActivationContext {
|
|
61
|
+
if (value === undefined) return {};
|
|
62
|
+
const input = record(value, "activation_context");
|
|
63
|
+
if (
|
|
64
|
+
!Object.keys(input).every((key) =>
|
|
65
|
+
["languages", "file_extensions", "tool_name", "operation_name", "session_capabilities"].includes(key),
|
|
66
|
+
)
|
|
67
|
+
) {
|
|
68
|
+
throw new Error("activation_context contains an unsupported field");
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
languages: optionalContextStrings(input.languages, "activation_context.languages"),
|
|
72
|
+
fileExtensions: optionalContextStrings(input.file_extensions, "activation_context.file_extensions"),
|
|
73
|
+
toolName: optionalContextString(input.tool_name, "activation_context.tool_name"),
|
|
74
|
+
operationName: optionalContextString(input.operation_name, "activation_context.operation_name"),
|
|
75
|
+
sessionCapabilities: optionalContextStrings(input.session_capabilities, "activation_context.session_capabilities"),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const fieldSet = new Set<string>(ACTIVATION_FIELDS);
|
|
80
|
+
const operatorSet = new Set<string>(ACTIVATION_OPERATORS);
|
|
81
|
+
const arrayFields = new Set<ActivationField>(["task.labels", "languages", "file.extensions", "session.capabilities"]);
|
|
82
|
+
|
|
83
|
+
function record(value: unknown, label: string): Record<string, unknown> {
|
|
84
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${label} must be an object`);
|
|
85
|
+
return value as Record<string, unknown>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function boundedString(value: unknown, label: string): string {
|
|
89
|
+
if (typeof value !== "string" || value.length === 0 || value.length > ACTIVATION_VALUE_MAX_LENGTH) {
|
|
90
|
+
throw new Error(`${label} must be a string between 1 and ${ACTIVATION_VALUE_MAX_LENGTH} characters`);
|
|
91
|
+
}
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function stringValues(value: unknown, label: string): string[] {
|
|
96
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > ACTIVATION_MAX_VALUES) {
|
|
97
|
+
throw new Error(`${label} must contain 1-${ACTIVATION_MAX_VALUES} strings`);
|
|
98
|
+
}
|
|
99
|
+
return value.map((entry, index) => boundedString(entry, `${label}[${index}]`));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface ValidationState {
|
|
103
|
+
nodes: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function validatePredicate(value: unknown, state: ValidationState, depth: number): ActivationPredicate {
|
|
107
|
+
if (depth > ACTIVATION_MAX_DEPTH) throw new Error(`activation predicate depth cannot exceed ${ACTIVATION_MAX_DEPTH}`);
|
|
108
|
+
state.nodes++;
|
|
109
|
+
if (state.nodes > ACTIVATION_MAX_NODES) throw new Error(`activation predicate cannot exceed ${ACTIVATION_MAX_NODES} nodes`);
|
|
110
|
+
const input = record(value, "activation predicate");
|
|
111
|
+
const keys = Object.keys(input);
|
|
112
|
+
if (keys.length === 1 && keys[0] === "all") {
|
|
113
|
+
if (!Array.isArray(input.all) || input.all.length === 0) throw new Error("activation all must be a non-empty array");
|
|
114
|
+
return { all: input.all.map((entry) => validatePredicate(entry, state, depth + 1)) };
|
|
115
|
+
}
|
|
116
|
+
if (keys.length === 1 && keys[0] === "any") {
|
|
117
|
+
if (!Array.isArray(input.any) || input.any.length === 0) throw new Error("activation any must be a non-empty array");
|
|
118
|
+
return { any: input.any.map((entry) => validatePredicate(entry, state, depth + 1)) };
|
|
119
|
+
}
|
|
120
|
+
if (keys.length === 1 && keys[0] === "not") return { not: validatePredicate(input.not, state, depth + 1) };
|
|
121
|
+
if (!keys.every((key) => key === "field" || key === "operator" || key === "value")) {
|
|
122
|
+
throw new Error("activation predicate must be exactly one all/any/not group or one field/operator/value test");
|
|
123
|
+
}
|
|
124
|
+
const field = boundedString(input.field, "activation field");
|
|
125
|
+
if (!fieldSet.has(field)) throw new Error(`unsupported activation field "${field}"`);
|
|
126
|
+
const operator = boundedString(input.operator, "activation operator");
|
|
127
|
+
if (!operatorSet.has(operator)) throw new Error(`unsupported activation operator "${operator}"`);
|
|
128
|
+
const typedField = field as ActivationField;
|
|
129
|
+
const typedOperator = operator as ActivationOperator;
|
|
130
|
+
let typedValue: string | string[] | boolean;
|
|
131
|
+
if (typedOperator === "exists") {
|
|
132
|
+
if (typeof input.value !== "boolean") throw new Error("activation exists value must be a boolean");
|
|
133
|
+
typedValue = input.value;
|
|
134
|
+
} else if (typedOperator === "eq") {
|
|
135
|
+
typedValue = boundedString(input.value, "activation eq value");
|
|
136
|
+
} else {
|
|
137
|
+
typedValue = stringValues(input.value, `activation ${typedOperator} value`);
|
|
138
|
+
}
|
|
139
|
+
if ((typedOperator === "contains_any" || typedOperator === "contains_all") && !arrayFields.has(typedField)) {
|
|
140
|
+
throw new Error(`activation operator ${typedOperator} requires an array-valued field`);
|
|
141
|
+
}
|
|
142
|
+
return { field: typedField, operator: typedOperator, value: typedValue };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function validateActivationConfig(value: unknown, defaultInjection: InjectionProfile = "full"): ActivationConfig {
|
|
146
|
+
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");
|
|
149
|
+
}
|
|
150
|
+
const priority = input.priority === undefined ? 0 : input.priority;
|
|
151
|
+
if (!Number.isInteger(priority) || (priority as number) < -1000 || (priority as number) > 1000) {
|
|
152
|
+
throw new Error("activation priority must be an integer between -1000 and 1000");
|
|
153
|
+
}
|
|
154
|
+
const injection = input.injection === undefined ? defaultInjection : input.injection;
|
|
155
|
+
if (injection !== "full" && injection !== "catalog" && injection !== "on-demand") {
|
|
156
|
+
throw new Error("activation injection must be full, catalog, or on-demand");
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
...(input.predicate === undefined ? {} : { predicate: validatePredicate(input.predicate, { nodes: 0 }, 1) }),
|
|
160
|
+
priority: priority as number,
|
|
161
|
+
injection,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function activationConfig(extra: Record<string, unknown>, defaultInjection: InjectionProfile = "full"): ActivationConfig {
|
|
166
|
+
if (extra.activation === undefined) return { priority: 0, injection: defaultInjection };
|
|
167
|
+
try {
|
|
168
|
+
return validateActivationConfig(extra.activation, defaultInjection);
|
|
169
|
+
} catch {
|
|
170
|
+
return { priority: 0, injection: defaultInjection, invalid: true };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function contextValue(field: ActivationField, context: ActivationContext): string | string[] | undefined {
|
|
175
|
+
switch (field) {
|
|
176
|
+
case "project.root":
|
|
177
|
+
return context.projectRoot;
|
|
178
|
+
case "task.status":
|
|
179
|
+
return context.taskStatus;
|
|
180
|
+
case "task.labels":
|
|
181
|
+
return context.taskLabels;
|
|
182
|
+
case "languages":
|
|
183
|
+
return context.languages;
|
|
184
|
+
case "file.extensions":
|
|
185
|
+
return context.fileExtensions;
|
|
186
|
+
case "tool.name":
|
|
187
|
+
return context.toolName;
|
|
188
|
+
case "operation.name":
|
|
189
|
+
return context.operationName;
|
|
190
|
+
case "session.capabilities":
|
|
191
|
+
return context.sessionCapabilities;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function evaluatePredicate(predicate: ActivationPredicate, context: ActivationContext): ActivationDecision {
|
|
196
|
+
if ("all" in predicate) {
|
|
197
|
+
for (const child of predicate.all) {
|
|
198
|
+
const decision = evaluatePredicate(child, context);
|
|
199
|
+
if (!decision.enabled) return decision;
|
|
200
|
+
}
|
|
201
|
+
return { enabled: true, reason: "enabled" };
|
|
202
|
+
}
|
|
203
|
+
if ("any" in predicate) {
|
|
204
|
+
const decisions = predicate.any.map((child) => evaluatePredicate(child, context));
|
|
205
|
+
if (decisions.some((decision) => decision.enabled)) return { enabled: true, reason: "enabled" };
|
|
206
|
+
return { enabled: false, reason: decisions[0]?.reason ?? "activation any did not match" };
|
|
207
|
+
}
|
|
208
|
+
if ("not" in predicate) {
|
|
209
|
+
const decision = evaluatePredicate(predicate.not, context);
|
|
210
|
+
return decision.enabled ? { enabled: false, reason: "activation not matched" } : { enabled: true, reason: "enabled" };
|
|
211
|
+
}
|
|
212
|
+
const actual = contextValue(predicate.field, context);
|
|
213
|
+
if (predicate.operator === "exists") {
|
|
214
|
+
const exists = actual !== undefined && (!Array.isArray(actual) || actual.length > 0);
|
|
215
|
+
return exists === predicate.value
|
|
216
|
+
? { enabled: true, reason: "enabled" }
|
|
217
|
+
: { enabled: false, reason: `activation ${predicate.field} existence did not match` };
|
|
218
|
+
}
|
|
219
|
+
if (actual === undefined) return { enabled: false, reason: `activation context field ${predicate.field} is unavailable` };
|
|
220
|
+
const actualValues = Array.isArray(actual) ? actual : [actual];
|
|
221
|
+
if (predicate.operator === "eq") {
|
|
222
|
+
return actualValues.includes(predicate.value as string)
|
|
223
|
+
? { enabled: true, reason: "enabled" }
|
|
224
|
+
: { enabled: false, reason: `activation ${predicate.field} did not equal ${predicate.value as string}` };
|
|
225
|
+
}
|
|
226
|
+
const expected = predicate.value as string[];
|
|
227
|
+
const matches =
|
|
228
|
+
predicate.operator === "contains_all"
|
|
229
|
+
? expected.every((entry) => actualValues.includes(entry))
|
|
230
|
+
: expected.some((entry) => actualValues.includes(entry));
|
|
231
|
+
return matches
|
|
232
|
+
? { enabled: true, reason: "enabled" }
|
|
233
|
+
: { enabled: false, reason: `activation ${predicate.field} did not match ${predicate.operator}` };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function evaluateActivation(config: ActivationConfig, context: ActivationContext): ActivationDecision {
|
|
237
|
+
if (config.invalid) return { enabled: false, reason: "invalid activation configuration" };
|
|
238
|
+
return config.predicate === undefined ? { enabled: true, reason: "enabled" } : evaluatePredicate(config.predicate, context);
|
|
239
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { CommandContext } from "@stricli/core";
|
|
2
|
+
import { buildApplication, buildCommand, buildRouteMap } from "@stricli/core";
|
|
3
|
+
import type { PapyrusClient } from "../client.ts";
|
|
4
|
+
import { runStricliToString } from "./stricli-run.ts";
|
|
5
|
+
|
|
6
|
+
type ActivationClient = Pick<PapyrusClient, "call">;
|
|
7
|
+
interface ActivationCliContext extends CommandContext {
|
|
8
|
+
readonly client: ActivationClient;
|
|
9
|
+
readonly json: boolean;
|
|
10
|
+
readonly callerProjectRoot: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ActivationAuditOutput {
|
|
14
|
+
summary: {
|
|
15
|
+
total: number;
|
|
16
|
+
enabled: number;
|
|
17
|
+
disabled: number;
|
|
18
|
+
global: number;
|
|
19
|
+
explicit: number;
|
|
20
|
+
hidden: number;
|
|
21
|
+
estimatedEnabledTokens: number;
|
|
22
|
+
};
|
|
23
|
+
entries: Array<{ enabled: boolean; kind: string; title: string; reason: string }>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseObject(value: string): Record<string, unknown> {
|
|
27
|
+
const parsed = JSON.parse(value) as unknown;
|
|
28
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("value must be a JSON object");
|
|
29
|
+
return parsed as Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const auditCommand = buildCommand({
|
|
33
|
+
func: async function (
|
|
34
|
+
this: ActivationCliContext,
|
|
35
|
+
flags: { projectRoot?: string; activationContextJson?: Record<string, unknown>; sessionId?: string },
|
|
36
|
+
) {
|
|
37
|
+
const result = await this.client.call<Record<string, unknown>, ActivationAuditOutput>("activation.audit", {
|
|
38
|
+
project_root: flags.projectRoot ?? this.callerProjectRoot,
|
|
39
|
+
activation_context: flags.activationContextJson,
|
|
40
|
+
session_id: flags.sessionId,
|
|
41
|
+
});
|
|
42
|
+
if (this.json) this.process.stdout.write(JSON.stringify(result));
|
|
43
|
+
else {
|
|
44
|
+
const summary = result.summary;
|
|
45
|
+
const lines = [
|
|
46
|
+
`${summary.enabled}/${summary.total} enabled; ${summary.disabled} disabled; ${summary.estimatedEnabledTokens} estimated tokens`,
|
|
47
|
+
`scope: ${summary.global} global, ${summary.explicit} explicit, ${summary.hidden} hidden`,
|
|
48
|
+
...result.entries.map((entry) => `${entry.enabled ? "enabled" : "disabled"} [${entry.kind}] ${entry.title} — ${entry.reason}`),
|
|
49
|
+
];
|
|
50
|
+
this.process.stdout.write(lines.join("\n"));
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
parameters: {
|
|
54
|
+
flags: {
|
|
55
|
+
projectRoot: { brief: "Project root (defaults to caller cwd)", kind: "parsed", parse: String, placeholder: "path", optional: true },
|
|
56
|
+
activationContextJson: {
|
|
57
|
+
brief: "Trusted activation context as JSON",
|
|
58
|
+
kind: "parsed",
|
|
59
|
+
parse: parseObject,
|
|
60
|
+
placeholder: "json",
|
|
61
|
+
optional: true,
|
|
62
|
+
},
|
|
63
|
+
sessionId: { brief: "Agent session id", kind: "parsed", parse: String, placeholder: "id", optional: true },
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
docs: { brief: "Audit Rule and Playbook activation decisions for a project context" },
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const app = buildApplication(buildRouteMap({ routes: { audit: auditCommand }, docs: { brief: "Conditional activation operations" } }), {
|
|
70
|
+
name: "activation",
|
|
71
|
+
scanner: { caseStyle: "allow-kebab-for-camel" },
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export async function runActivationCli(args: string[], client: ActivationClient, projectRoot: string = process.cwd()): Promise<string> {
|
|
75
|
+
const json = args.includes("--json");
|
|
76
|
+
return runStricliToString(
|
|
77
|
+
app,
|
|
78
|
+
args.filter((arg) => arg !== "--json"),
|
|
79
|
+
{ client, json, callerProjectRoot: projectRoot },
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -51,6 +51,7 @@ const createCommand = buildCommand({
|
|
|
51
51
|
toolsJson?: string[];
|
|
52
52
|
labelsJson?: string[];
|
|
53
53
|
extraJson?: Record<string, unknown>;
|
|
54
|
+
activationJson?: Record<string, unknown>;
|
|
54
55
|
argumentsJson?: unknown[] | Record<string, unknown>;
|
|
55
56
|
projectRoot?: string;
|
|
56
57
|
projectsJson?: string[];
|
|
@@ -64,6 +65,7 @@ const createCommand = buildCommand({
|
|
|
64
65
|
tools: flags.toolsJson,
|
|
65
66
|
labels: flags.labelsJson,
|
|
66
67
|
extra: flags.extraJson,
|
|
68
|
+
activation: flags.activationJson,
|
|
67
69
|
arguments: flags.argumentsJson,
|
|
68
70
|
project_root: flags.projectRoot,
|
|
69
71
|
projects: flags.projectsJson,
|
|
@@ -85,6 +87,13 @@ const createCommand = buildCommand({
|
|
|
85
87
|
toolsJson: { brief: "JSON string array of tool names", kind: "parsed", parse: parseStringArray, placeholder: "json", optional: true },
|
|
86
88
|
labelsJson: { brief: "JSON string array of labels", kind: "parsed", parse: parseStringArray, placeholder: "json", optional: true },
|
|
87
89
|
extraJson: { brief: "JSON object of extra fields", kind: "parsed", parse: parseObject, placeholder: "json", optional: true },
|
|
90
|
+
activationJson: {
|
|
91
|
+
brief: "Typed activation config as JSON: {predicate?,priority?,injection?}",
|
|
92
|
+
kind: "parsed",
|
|
93
|
+
parse: parseObject,
|
|
94
|
+
placeholder: "json",
|
|
95
|
+
optional: true,
|
|
96
|
+
},
|
|
88
97
|
argumentsJson: {
|
|
89
98
|
brief: "JSON array of declared argument definitions",
|
|
90
99
|
kind: "parsed",
|
|
@@ -108,7 +117,15 @@ const createCommand = buildCommand({
|
|
|
108
117
|
const listCommand = buildCommand({
|
|
109
118
|
func: async function (
|
|
110
119
|
this: PlaybooksContext,
|
|
111
|
-
flags: {
|
|
120
|
+
flags: {
|
|
121
|
+
status?: string;
|
|
122
|
+
text?: string;
|
|
123
|
+
limit?: number;
|
|
124
|
+
projectRoot?: string;
|
|
125
|
+
applicable?: boolean;
|
|
126
|
+
activated?: boolean;
|
|
127
|
+
activationContextJson?: Record<string, unknown>;
|
|
128
|
+
},
|
|
112
129
|
) {
|
|
113
130
|
const rows = await this.client.call<Record<string, unknown>, CliArtifact[]>("playbooks.list", {
|
|
114
131
|
status: flags.status,
|
|
@@ -116,6 +133,8 @@ const listCommand = buildCommand({
|
|
|
116
133
|
limit: flags.limit,
|
|
117
134
|
project_root: flags.projectRoot,
|
|
118
135
|
applicable: flags.applicable,
|
|
136
|
+
activated: flags.activated,
|
|
137
|
+
activation_context: flags.activationContextJson,
|
|
119
138
|
});
|
|
120
139
|
render.call(this, rows, rows.length === 0 ? "No playbooks found." : rows.map((row) => artifactLabel(row)).join("\n"));
|
|
121
140
|
},
|
|
@@ -130,6 +149,14 @@ const listCommand = buildCommand({
|
|
|
130
149
|
kind: "boolean",
|
|
131
150
|
optional: true,
|
|
132
151
|
},
|
|
152
|
+
activated: { brief: "Evaluate typed activation predicates", kind: "boolean", optional: true },
|
|
153
|
+
activationContextJson: {
|
|
154
|
+
brief: "Trusted activation context as JSON",
|
|
155
|
+
kind: "parsed",
|
|
156
|
+
parse: parseObject,
|
|
157
|
+
placeholder: "json",
|
|
158
|
+
optional: true,
|
|
159
|
+
},
|
|
133
160
|
},
|
|
134
161
|
},
|
|
135
162
|
docs: { brief: "List Playbooks" },
|
|
@@ -388,7 +415,14 @@ const replaceGroupsCommand = buildCommand({
|
|
|
388
415
|
const updateCommand = buildCommand({
|
|
389
416
|
func: async function (
|
|
390
417
|
this: PlaybooksContext,
|
|
391
|
-
flags: {
|
|
418
|
+
flags: {
|
|
419
|
+
title?: string;
|
|
420
|
+
body?: string;
|
|
421
|
+
labelsJson?: string[];
|
|
422
|
+
trigger?: string;
|
|
423
|
+
stepsJson?: unknown[] | Record<string, unknown>;
|
|
424
|
+
activationJson?: Record<string, unknown>;
|
|
425
|
+
},
|
|
392
426
|
id: string,
|
|
393
427
|
) {
|
|
394
428
|
if (
|
|
@@ -396,9 +430,10 @@ const updateCommand = buildCommand({
|
|
|
396
430
|
flags.body === undefined &&
|
|
397
431
|
flags.labelsJson === undefined &&
|
|
398
432
|
flags.trigger === undefined &&
|
|
399
|
-
flags.stepsJson === undefined
|
|
433
|
+
flags.stepsJson === undefined &&
|
|
434
|
+
flags.activationJson === undefined
|
|
400
435
|
)
|
|
401
|
-
throw new Error("playbooks update requires --title, --body, --labels-json, --trigger, or --
|
|
436
|
+
throw new Error("playbooks update requires --title, --body, --labels-json, --trigger, --steps-json, or --activation-json");
|
|
402
437
|
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>("playbooks.update", {
|
|
403
438
|
id,
|
|
404
439
|
title: flags.title,
|
|
@@ -406,6 +441,7 @@ const updateCommand = buildCommand({
|
|
|
406
441
|
labels: flags.labelsJson,
|
|
407
442
|
trigger: flags.trigger,
|
|
408
443
|
steps: flags.stepsJson,
|
|
444
|
+
activation: flags.activationJson,
|
|
409
445
|
});
|
|
410
446
|
render.call(this, artifact, artifactLabel(artifact));
|
|
411
447
|
},
|
|
@@ -422,6 +458,13 @@ const updateCommand = buildCommand({
|
|
|
422
458
|
placeholder: "json",
|
|
423
459
|
optional: true,
|
|
424
460
|
},
|
|
461
|
+
activationJson: {
|
|
462
|
+
brief: "Replacement typed activation config as JSON",
|
|
463
|
+
kind: "parsed",
|
|
464
|
+
parse: parseObject,
|
|
465
|
+
placeholder: "json",
|
|
466
|
+
optional: true,
|
|
467
|
+
},
|
|
425
468
|
},
|
|
426
469
|
positional: { kind: "tuple", parameters: [{ brief: "Playbook id", parse: String, placeholder: "id" }] },
|
|
427
470
|
},
|
package/src/cli/rules-command.ts
CHANGED
|
@@ -41,6 +41,7 @@ const createCommand = buildCommand({
|
|
|
41
41
|
severity?: string;
|
|
42
42
|
labelsJson?: string[];
|
|
43
43
|
extraJson?: Record<string, unknown>;
|
|
44
|
+
activationJson?: Record<string, unknown>;
|
|
44
45
|
projectRoot?: string;
|
|
45
46
|
},
|
|
46
47
|
) {
|
|
@@ -52,6 +53,7 @@ const createCommand = buildCommand({
|
|
|
52
53
|
severity: flags.severity,
|
|
53
54
|
labels: flags.labelsJson,
|
|
54
55
|
extra: flags.extraJson,
|
|
56
|
+
activation: flags.activationJson,
|
|
55
57
|
project_root: flags.projectRoot,
|
|
56
58
|
});
|
|
57
59
|
render.call(this, artifact, `Created rule: ${artifactLabel(artifact)}`);
|
|
@@ -65,6 +67,13 @@ const createCommand = buildCommand({
|
|
|
65
67
|
severity: { brief: "block|warn|info", kind: "parsed", parse: String, placeholder: "severity", optional: true },
|
|
66
68
|
labelsJson: { brief: "JSON string array of labels", kind: "parsed", parse: parseStringArray, placeholder: "json", optional: true },
|
|
67
69
|
extraJson: { brief: "JSON object of extra fields", kind: "parsed", parse: parseObject, placeholder: "json", optional: true },
|
|
70
|
+
activationJson: {
|
|
71
|
+
brief: "Typed activation config as JSON: {predicate?,priority?,injection?}",
|
|
72
|
+
kind: "parsed",
|
|
73
|
+
parse: parseObject,
|
|
74
|
+
placeholder: "json",
|
|
75
|
+
optional: true,
|
|
76
|
+
},
|
|
68
77
|
projectRoot: { brief: "Project scope", kind: "parsed", parse: String, placeholder: "path", optional: true },
|
|
69
78
|
},
|
|
70
79
|
},
|
|
@@ -319,25 +328,41 @@ const gateCommand = buildCommand({
|
|
|
319
328
|
});
|
|
320
329
|
|
|
321
330
|
const injectableCommand = buildCommand({
|
|
322
|
-
func: async function (this: RulesContext) {
|
|
331
|
+
func: async function (this: RulesContext, flags: { activationContextJson?: Record<string, unknown> }) {
|
|
323
332
|
const rows = await this.client.call<Record<string, unknown>, CliArtifact[]>("rules.injectable", {
|
|
324
333
|
project_root: this.callerProjectRoot,
|
|
334
|
+
activation_context: flags.activationContextJson,
|
|
325
335
|
});
|
|
326
336
|
render.call(this, rows, rows.length === 0 ? "No injectable rules." : rows.map((row) => row.title).join("\n"));
|
|
327
337
|
},
|
|
328
|
-
parameters: {
|
|
338
|
+
parameters: {
|
|
339
|
+
flags: {
|
|
340
|
+
activationContextJson: {
|
|
341
|
+
brief: "Trusted activation context as JSON",
|
|
342
|
+
kind: "parsed",
|
|
343
|
+
parse: parseObject,
|
|
344
|
+
placeholder: "json",
|
|
345
|
+
optional: true,
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
},
|
|
329
349
|
docs: { brief: "List Rules currently injectable into the agent system prompt for this project" },
|
|
330
350
|
});
|
|
331
351
|
|
|
332
352
|
const updateCommand = buildCommand({
|
|
333
|
-
func: async function (
|
|
334
|
-
|
|
335
|
-
|
|
353
|
+
func: async function (
|
|
354
|
+
this: RulesContext,
|
|
355
|
+
flags: { title?: string; body?: string; labelsJson?: string[]; activationJson?: Record<string, unknown> },
|
|
356
|
+
id: string,
|
|
357
|
+
) {
|
|
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");
|
|
336
360
|
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>("rules.update", {
|
|
337
361
|
id,
|
|
338
362
|
title: flags.title,
|
|
339
363
|
body: flags.body,
|
|
340
364
|
labels: flags.labelsJson,
|
|
365
|
+
activation: flags.activationJson,
|
|
341
366
|
});
|
|
342
367
|
render.call(this, artifact, artifactLabel(artifact));
|
|
343
368
|
},
|
|
@@ -346,6 +371,13 @@ const updateCommand = buildCommand({
|
|
|
346
371
|
title: { brief: "New title", kind: "parsed", parse: String, placeholder: "text", optional: true },
|
|
347
372
|
body: { brief: "New body", kind: "parsed", parse: String, placeholder: "text", optional: true },
|
|
348
373
|
labelsJson: { brief: "JSON string array of labels", kind: "parsed", parse: parseStringArray, placeholder: "json", optional: true },
|
|
374
|
+
activationJson: {
|
|
375
|
+
brief: "Replacement typed activation config as JSON",
|
|
376
|
+
kind: "parsed",
|
|
377
|
+
parse: parseObject,
|
|
378
|
+
placeholder: "json",
|
|
379
|
+
optional: true,
|
|
380
|
+
},
|
|
349
381
|
},
|
|
350
382
|
positional: { kind: "tuple", parameters: [{ brief: "Rule id", parse: String, placeholder: "id" }] },
|
|
351
383
|
},
|
package/src/cli.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { execFileSync } from "node:child_process";
|
|
|
3
3
|
import { copyFileSync, existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { createNodeServiceInstallDeps, generateSystemdUnit, installUserService, type ServiceSpec } from "@danypops/vehicle-server/service";
|
|
6
|
+
import { runActivationCli } from "./cli/activation-command.ts";
|
|
6
7
|
import { runArtifactCli } from "./cli/artifact-command.ts";
|
|
7
8
|
import { runBatchCli } from "./cli/batch-command.ts";
|
|
8
9
|
import { runBindersCli } from "./cli/binders-command.ts";
|
|
@@ -107,6 +108,7 @@ const USAGE = `Usage:
|
|
|
107
108
|
papyrus gates run <id> [--json]
|
|
108
109
|
papyrus graph-projection apply --batch-json <json> [--json]
|
|
109
110
|
papyrus graph-projection checkpoint --producer-id <id> [--json]
|
|
111
|
+
papyrus activation audit [--project-root <path>] [--activation-context-json <json>] [--session-id <id>] [--json]
|
|
110
112
|
papyrus artifact create --kind <kind> [--title <title>] [--status <status>] [--subtype <subtype>] [--body <body>] [--labels-json <json>] [--extra-json <json>] [--template-id <id>] [--json]
|
|
111
113
|
papyrus artifact query [--kind <kind>] [--status <status>] [--text <query>] [--limit <count>] [--json]
|
|
112
114
|
papyrus artifact show <id> [--depth <n>] [--max-nodes <n>] [--json]
|
|
@@ -363,6 +365,7 @@ export function runIdMigrationCli(args: string[]): string {
|
|
|
363
365
|
}
|
|
364
366
|
|
|
365
367
|
export {
|
|
368
|
+
runActivationCli,
|
|
366
369
|
runArtifactCli,
|
|
367
370
|
runBatchCli,
|
|
368
371
|
runBindersCli,
|
|
@@ -466,6 +469,11 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
|
|
|
466
469
|
console.log(await runRulesCli(args.slice(1), client));
|
|
467
470
|
return;
|
|
468
471
|
}
|
|
472
|
+
if (command === "activation") {
|
|
473
|
+
const client = await connectPapyrusClient();
|
|
474
|
+
console.log(await runActivationCli(args.slice(1), client));
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
469
477
|
if (command === "artifact") {
|
|
470
478
|
const client = await connectPapyrusClient();
|
|
471
479
|
console.log(await runArtifactCli(args.slice(1), client));
|
package/src/constants.ts
CHANGED
|
@@ -72,6 +72,8 @@ export const CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN = 4;
|
|
|
72
72
|
* this long cannot also risk a real JavaScript call-stack overflow independent of this bound.
|
|
73
73
|
*/
|
|
74
74
|
export const CONTEXT_TREE_MAX_NODES = 50_000;
|
|
75
|
+
/** Hard aggregate ceiling for Papyrus's recurring Rules + Playbooks + Task system-prompt injection. */
|
|
76
|
+
export const PAPYRUS_CONTEXT_INJECTION_MAX_TOKENS = 8192;
|
|
75
77
|
|
|
76
78
|
/**
|
|
77
79
|
* A Papyrus Rule's condition+action+body is injected into EVERY relevant turn's system
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { VehicleRegistry } from "@danypops/vehicle-server";
|
|
2
|
+
import { activationContextFromInput } from "../artifact/artifact-activation.ts";
|
|
3
|
+
import { auditArtifactActivation } from "../artifact/artifact-activation-audit.ts";
|
|
4
|
+
import type { ArtifactScopeStore } from "../artifact/artifact-scope-store.ts";
|
|
5
|
+
import type { ArtifactStore } from "../artifact/artifact-store.ts";
|
|
6
|
+
import type { Tasks } from "../task/task-service.ts";
|
|
7
|
+
import { createOperationDefiner, stringProp } from "./shared.ts";
|
|
8
|
+
|
|
9
|
+
export function registerActivationVehicleOperations(
|
|
10
|
+
registry: VehicleRegistry,
|
|
11
|
+
artifacts: ArtifactStore,
|
|
12
|
+
scopes: ArtifactScopeStore,
|
|
13
|
+
tasks: Tasks,
|
|
14
|
+
): void {
|
|
15
|
+
const define = createOperationDefiner(registry, "activation", "activation", ["rules:read", "playbooks:read"], (name, input) => {
|
|
16
|
+
if (name !== "activation.audit") throw new Error(`unknown activation operation ${name}`);
|
|
17
|
+
const projectRoot = input.project_root as string;
|
|
18
|
+
const sessionId = input.session_id as string | undefined;
|
|
19
|
+
const activeTask = tasks.active({ projectRoot, sessionId });
|
|
20
|
+
return auditArtifactActivation(artifacts, scopes, projectRoot, activeTask?.id, {
|
|
21
|
+
...activationContextFromInput(input.activation_context),
|
|
22
|
+
projectRoot,
|
|
23
|
+
taskStatus: activeTask?.status,
|
|
24
|
+
taskLabels: activeTask?.labels,
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
define(
|
|
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.",
|
|
30
|
+
"read",
|
|
31
|
+
{
|
|
32
|
+
project_root: stringProp,
|
|
33
|
+
activation_context: { type: "object", description: "Trusted turn signals used by typed activation predicates." },
|
|
34
|
+
session_id: stringProp,
|
|
35
|
+
},
|
|
36
|
+
["project_root"],
|
|
37
|
+
(input) => input,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -105,6 +105,7 @@ 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
109
|
template_id: stringProp,
|
|
109
110
|
project_root: stringProp,
|
|
110
111
|
projects: { type: "array" },
|
|
@@ -123,7 +124,16 @@ export function registerPlaybooksVehicleOperations(registry: VehicleRegistry, de
|
|
|
123
124
|
"list",
|
|
124
125
|
"Lists Playbooks matching an optional status/text filter. project_root alone scopes to EXACT membership in that project (audit semantics); project_root plus applicable:true instead lists every Playbook APPLICABLE to it (global Playbooks plus Playbooks whose membership includes it) -- what pi-papyrus's before_agent_start uses to inject only relevant Playbooks. Returns a lean summary (no body/steps) by default -- pass full: true for the complete artifact.",
|
|
125
126
|
"read",
|
|
126
|
-
{
|
|
127
|
+
{
|
|
128
|
+
status: stringProp,
|
|
129
|
+
text: stringProp,
|
|
130
|
+
limit: numberProp,
|
|
131
|
+
project_root: stringProp,
|
|
132
|
+
applicable: booleanProp,
|
|
133
|
+
activated: booleanProp,
|
|
134
|
+
activation_context: { type: "object", description: "Trusted turn signals used by typed activation predicates." },
|
|
135
|
+
full: booleanProp,
|
|
136
|
+
},
|
|
127
137
|
[],
|
|
128
138
|
(input) => input,
|
|
129
139
|
);
|
|
@@ -316,6 +326,7 @@ export function registerPlaybooksVehicleOperations(registry: VehicleRegistry, de
|
|
|
316
326
|
labels: { type: "array" },
|
|
317
327
|
trigger: stringProp,
|
|
318
328
|
steps: { type: "array" },
|
|
329
|
+
activation: { type: "object", description: "Replacement typed activation config." },
|
|
319
330
|
actor: stringProp,
|
|
320
331
|
source: stringProp,
|
|
321
332
|
session_id: stringProp,
|
package/src/handlers/registry.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type { SessionIdentity } from "../session-identity/session-identity-servi
|
|
|
19
19
|
import type { TaskEventStore } from "../task/event/task-event-store.ts";
|
|
20
20
|
import type { TaskScopeStore } from "../task/scope/task-scope-store.ts";
|
|
21
21
|
import type { Tasks } from "../task/task-service.ts";
|
|
22
|
+
import { registerActivationVehicleOperations } from "./activation.ts";
|
|
22
23
|
import { registerArtifactTrashOperations } from "./artifact-trash.ts";
|
|
23
24
|
import { registerBatchVehicleOperation } from "./batch.ts";
|
|
24
25
|
import { registerBindersVehicleOperations } from "./binders.ts";
|
|
@@ -57,6 +58,7 @@ export function createPapyrusVehicleRegistry(deps: PapyrusVehicleDeps): VehicleR
|
|
|
57
58
|
// session_id, a correlation id, not a secret -- see session-identity-service.ts).
|
|
58
59
|
registry.setExposeHandlerFailureDetails(true);
|
|
59
60
|
registerNotesVehicleOperations(registry, deps.notes, deps.artifacts);
|
|
61
|
+
registerActivationVehicleOperations(registry, deps.artifacts, deps.scopes, deps.tasks);
|
|
60
62
|
registerBindersVehicleOperations(registry, deps.artifacts, deps.scopes, deps.projectRegistry, deps.scopeGroups);
|
|
61
63
|
registerRulesVehicleOperations(registry, deps.artifacts, deps.scopes, deps.projectRegistry, deps.scopeGroups);
|
|
62
64
|
registerDocsVehicleOperations(registry, deps.artifacts, deps.scopes, deps.authority, deps.projectRegistry, deps.scopeGroups);
|
package/src/handlers/rules.ts
CHANGED
|
@@ -77,6 +77,9 @@ 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 {
|
|
81
|
+
type: string;
|
|
82
|
+
},
|
|
80
83
|
template_id: stringProp,
|
|
81
84
|
project_root: stringProp,
|
|
82
85
|
projects: { type: "array" } as unknown as { type: string },
|
|
@@ -314,6 +317,7 @@ export function registerRulesVehicleOperations(
|
|
|
314
317
|
title: stringProp,
|
|
315
318
|
body: stringProp,
|
|
316
319
|
labels: { type: "array" } as unknown as { type: string },
|
|
320
|
+
activation: { type: "object", description: "Replacement typed activation config." } as unknown as { type: string },
|
|
317
321
|
project_root: stringProp,
|
|
318
322
|
actor: stringProp,
|
|
319
323
|
source: stringProp,
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
export type { Artifact, ArtifactEdge } from "./artifact/artifact.ts";
|
|
9
|
+
export {
|
|
10
|
+
type ActivationConfig,
|
|
11
|
+
type ActivationContext,
|
|
12
|
+
type ActivationPredicate,
|
|
13
|
+
activationConfig,
|
|
14
|
+
type InjectionProfile,
|
|
15
|
+
} from "./artifact/artifact-activation.ts";
|
|
9
16
|
export { projectArtifactRelationships } from "./artifact/artifact-relationship-view.ts";
|
|
10
17
|
export type { ArtifactStore } from "./artifact/artifact-store.ts";
|
|
11
18
|
export {
|
package/src/modules/playbooks.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { summarizeArtifact } from "../artifact/artifact.ts";
|
|
14
|
+
import { activationContextFromInput } from "../artifact/artifact-activation.ts";
|
|
14
15
|
import type { ArtifactScopeStore } from "../artifact/artifact-scope-store.ts";
|
|
15
16
|
import type { ArtifactStore } from "../artifact/artifact-store.ts";
|
|
16
17
|
import type { OperationDefinition } from "../module-registry.ts";
|
|
@@ -22,6 +23,7 @@ import {
|
|
|
22
23
|
containPlaybook,
|
|
23
24
|
createPlaybook,
|
|
24
25
|
dependPlaybook,
|
|
26
|
+
listActivatedPlaybooks,
|
|
25
27
|
listPlaybooks,
|
|
26
28
|
playbookInvocation,
|
|
27
29
|
playbookScope,
|
|
@@ -147,6 +149,7 @@ export function playbooksOperations({
|
|
|
147
149
|
subtype: optionalString(input, "subtype"),
|
|
148
150
|
labels: input.labels as string[] | undefined,
|
|
149
151
|
extra: input.extra as Record<string, unknown> | undefined,
|
|
152
|
+
activation: input.activation,
|
|
150
153
|
templateId: optionalString(input, "template_id") ?? optionalString(input, "templateId"),
|
|
151
154
|
projectRoot: optionalString(input, "project_root"),
|
|
152
155
|
projectReferences: input.projects as string[] | undefined,
|
|
@@ -156,7 +159,21 @@ export function playbooksOperations({
|
|
|
156
159
|
),
|
|
157
160
|
),
|
|
158
161
|
define("playbooks.list", (input: OperationInput) => {
|
|
159
|
-
const
|
|
162
|
+
const filter = artifactFilter(input);
|
|
163
|
+
const projectRoot = optionalString(input, "project_root");
|
|
164
|
+
const activeTask =
|
|
165
|
+
optionalBoolean(input, "activated") === true
|
|
166
|
+
? tasks.active({ projectRoot, sessionId: optionalString(input, "session_id") ?? optionalString(input, "sessionId") })
|
|
167
|
+
: undefined;
|
|
168
|
+
const playbooks =
|
|
169
|
+
optionalBoolean(input, "activated") === true
|
|
170
|
+
? listActivatedPlaybooks(artifacts, artifactScopes, filter, {
|
|
171
|
+
...activationContextFromInput(input.activation_context),
|
|
172
|
+
projectRoot,
|
|
173
|
+
taskStatus: activeTask?.status,
|
|
174
|
+
taskLabels: activeTask?.labels,
|
|
175
|
+
})
|
|
176
|
+
: listPlaybooks(artifacts, artifactScopes, filter);
|
|
160
177
|
return optionalBoolean(input, "full") === true ? playbooks : playbooks.map(summarizeArtifact);
|
|
161
178
|
}),
|
|
162
179
|
define("playbooks.show", (input: OperationInput) => showPlaybook(artifacts, string(input, "id"))),
|
|
@@ -225,6 +242,7 @@ export function playbooksOperations({
|
|
|
225
242
|
labels: input.labels as string[] | undefined,
|
|
226
243
|
trigger: optionalString(input, "trigger"),
|
|
227
244
|
steps: input.steps,
|
|
245
|
+
activation: input.activation,
|
|
228
246
|
},
|
|
229
247
|
eventContext(input),
|
|
230
248
|
),
|
package/src/modules/rules.ts
CHANGED
|
@@ -120,6 +120,7 @@ export function rulesOperations(
|
|
|
120
120
|
subtype: optionalString(input, "subtype"),
|
|
121
121
|
labels: input.labels as string[] | undefined,
|
|
122
122
|
extra: input.extra as Record<string, unknown> | undefined,
|
|
123
|
+
activation: input.activation,
|
|
123
124
|
templateId: optionalString(input, "template_id") ?? optionalString(input, "templateId"),
|
|
124
125
|
projectRoot: optionalString(input, "project_root"),
|
|
125
126
|
projectReferences: input.projects as string[] | undefined,
|
|
@@ -182,6 +183,7 @@ export function rulesOperations(
|
|
|
182
183
|
title: optionalString(input, "title"),
|
|
183
184
|
body: optionalString(input, "body"),
|
|
184
185
|
labels: input.labels as string[] | undefined,
|
|
186
|
+
activation: input.activation,
|
|
185
187
|
},
|
|
186
188
|
eventContext(input),
|
|
187
189
|
),
|
|
@@ -24,6 +24,13 @@
|
|
|
24
24
|
|
|
25
25
|
import type { Artifact } from "../artifact/artifact.ts";
|
|
26
26
|
import { requireLocallyOwnedContent } from "../artifact/artifact.ts";
|
|
27
|
+
import {
|
|
28
|
+
type ActivationContext,
|
|
29
|
+
activationConfig,
|
|
30
|
+
evaluateActivation,
|
|
31
|
+
type InjectionProfile,
|
|
32
|
+
validateActivationConfig,
|
|
33
|
+
} from "../artifact/artifact-activation.ts";
|
|
27
34
|
import type { ArtifactEventContext } from "../artifact/artifact-event.ts";
|
|
28
35
|
import type { ArtifactScope, ArtifactScopeStore } from "../artifact/artifact-scope-store.ts";
|
|
29
36
|
import type { ArtifactStore } from "../artifact/artifact-store.ts";
|
|
@@ -226,6 +233,7 @@ export interface CreatePlaybookInput {
|
|
|
226
233
|
subtype?: string;
|
|
227
234
|
labels?: string[];
|
|
228
235
|
extra?: Record<string, unknown>;
|
|
236
|
+
activation?: unknown;
|
|
229
237
|
templateId?: string;
|
|
230
238
|
projectRoot?: string;
|
|
231
239
|
/** 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. */
|
|
@@ -246,6 +254,7 @@ export type PlaybookTransition = "enable" | "disable";
|
|
|
246
254
|
export interface UpdatePlaybookInput extends UpdateContentInput {
|
|
247
255
|
trigger?: string;
|
|
248
256
|
steps?: unknown;
|
|
257
|
+
activation?: unknown;
|
|
249
258
|
}
|
|
250
259
|
|
|
251
260
|
const PLAYBOOK_TRANSITIONS: TransitionTable<PlaybookTransition, string> = {
|
|
@@ -266,6 +275,7 @@ export function createPlaybook(
|
|
|
266
275
|
const projectRoot = input.projectRoot === undefined ? undefined : normalizeProjectRoot(input.projectRoot);
|
|
267
276
|
const declaredArguments = validatePlaybookArguments(input.arguments);
|
|
268
277
|
const declaredSteps = validatePlaybookSteps(input.steps);
|
|
278
|
+
const activation = input.activation === undefined ? undefined : validateActivationConfig(input.activation, "catalog");
|
|
269
279
|
const playbook = artifacts.create(
|
|
270
280
|
{
|
|
271
281
|
kind: "playbook",
|
|
@@ -276,6 +286,7 @@ export function createPlaybook(
|
|
|
276
286
|
labels: input.labels,
|
|
277
287
|
extra: {
|
|
278
288
|
...(input.extra ?? {}),
|
|
289
|
+
...(activation === undefined ? {} : { activation }),
|
|
279
290
|
...(input.trigger ? { trigger: input.trigger } : {}),
|
|
280
291
|
...(declaredSteps ? { steps: declaredSteps } : {}),
|
|
281
292
|
...(input.tools ? { tools: input.tools } : {}),
|
|
@@ -297,6 +308,25 @@ export function listPlaybooks(artifacts: ArtifactStore, scopes: ArtifactScopeSto
|
|
|
297
308
|
return listScoped(artifacts, scopes, "playbook", filter);
|
|
298
309
|
}
|
|
299
310
|
|
|
311
|
+
export function listActivatedPlaybooks(
|
|
312
|
+
artifacts: ArtifactStore,
|
|
313
|
+
scopes: ArtifactScopeStore,
|
|
314
|
+
filter: ListFilter,
|
|
315
|
+
context: ActivationContext,
|
|
316
|
+
): Artifact[] {
|
|
317
|
+
return listPlaybooks(artifacts, scopes, filter).filter(
|
|
318
|
+
(playbook) => evaluateActivation(activationConfig(playbook.extra, "catalog"), context).enabled,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function playbookActivationDecision(
|
|
323
|
+
playbook: Artifact,
|
|
324
|
+
context: ActivationContext,
|
|
325
|
+
): { enabled: boolean; reason: string; priority: number; injection: InjectionProfile } {
|
|
326
|
+
const config = activationConfig(playbook.extra, "catalog");
|
|
327
|
+
return { ...evaluateActivation(config, context), priority: config.priority, injection: config.injection };
|
|
328
|
+
}
|
|
329
|
+
|
|
300
330
|
export function assignPlaybookProject(
|
|
301
331
|
artifacts: ArtifactStore,
|
|
302
332
|
scopes: ArtifactScopeStore,
|
|
@@ -409,9 +439,10 @@ export function updatePlaybook(artifacts: ArtifactStore, id: string, input: Upda
|
|
|
409
439
|
input.body === undefined &&
|
|
410
440
|
input.labels === undefined &&
|
|
411
441
|
input.trigger === undefined &&
|
|
412
|
-
input.steps === undefined
|
|
442
|
+
input.steps === undefined &&
|
|
443
|
+
input.activation === undefined
|
|
413
444
|
) {
|
|
414
|
-
throw new Error("update requires title, body, labels, trigger, or
|
|
445
|
+
throw new Error("update requires title, body, labels, trigger, steps, or activation");
|
|
415
446
|
}
|
|
416
447
|
assertTitleBounds(input.title);
|
|
417
448
|
assertBodyBounds(input.body);
|
|
@@ -423,13 +454,15 @@ export function updatePlaybook(artifacts: ArtifactStore, id: string, input: Upda
|
|
|
423
454
|
const hasContentFields = input.title !== undefined || input.body !== undefined || input.labels !== undefined;
|
|
424
455
|
const updated = hasContentFields ? artifacts.updateContent(playbook.id, input, context) : playbook;
|
|
425
456
|
if (!updated) throw new Error(`playbook "${id}" not found`);
|
|
426
|
-
if (declaredSteps === undefined && input.trigger === undefined) return updated;
|
|
457
|
+
if (declaredSteps === undefined && input.trigger === undefined && input.activation === undefined) return updated;
|
|
458
|
+
const activation = input.activation === undefined ? undefined : validateActivationConfig(input.activation, "catalog");
|
|
427
459
|
const withExtra = artifacts.setExtra(
|
|
428
460
|
updated.id,
|
|
429
461
|
{
|
|
430
462
|
...updated.extra,
|
|
431
463
|
...(input.trigger !== undefined ? { trigger: input.trigger } : {}),
|
|
432
464
|
...(declaredSteps !== undefined ? { steps: declaredSteps } : {}),
|
|
465
|
+
...(activation !== undefined ? { activation } : {}),
|
|
433
466
|
},
|
|
434
467
|
context,
|
|
435
468
|
);
|
|
@@ -7,6 +7,13 @@
|
|
|
7
7
|
|
|
8
8
|
import type { Artifact } from "../artifact/artifact.ts";
|
|
9
9
|
import { requireLocallyOwnedContent } from "../artifact/artifact.ts";
|
|
10
|
+
import {
|
|
11
|
+
type ActivationContext,
|
|
12
|
+
activationConfig,
|
|
13
|
+
evaluateActivation,
|
|
14
|
+
type InjectionProfile,
|
|
15
|
+
validateActivationConfig,
|
|
16
|
+
} from "../artifact/artifact-activation.ts";
|
|
10
17
|
import type { ArtifactEventContext } from "../artifact/artifact-event.ts";
|
|
11
18
|
import type { ArtifactScope, ArtifactScopeStore } from "../artifact/artifact-scope-store.ts";
|
|
12
19
|
import type { ArtifactStore } from "../artifact/artifact-store.ts";
|
|
@@ -23,7 +30,6 @@ import {
|
|
|
23
30
|
removeArtifactScopeProject,
|
|
24
31
|
replaceArtifactScopeGroups,
|
|
25
32
|
replaceArtifactScopeProjects,
|
|
26
|
-
requireContentUpdateFields,
|
|
27
33
|
requireKind,
|
|
28
34
|
runTransition,
|
|
29
35
|
setArtifactScopeNone,
|
|
@@ -43,6 +49,7 @@ export interface CreateRuleInput {
|
|
|
43
49
|
subtype?: string;
|
|
44
50
|
labels?: string[];
|
|
45
51
|
extra?: Record<string, unknown>;
|
|
52
|
+
activation?: unknown;
|
|
46
53
|
templateId?: string;
|
|
47
54
|
projectRoot?: string;
|
|
48
55
|
/** 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. */
|
|
@@ -135,6 +142,7 @@ export function createRule(
|
|
|
135
142
|
registry?: ProjectRegistryStore,
|
|
136
143
|
): Artifact {
|
|
137
144
|
assertRuleTextWithinBounds(input.condition, input.action, input.body);
|
|
145
|
+
const activation = input.activation === undefined ? undefined : validateActivationConfig(input.activation, "full");
|
|
138
146
|
if (input.projectReferences !== undefined && input.projectReferences.length > 0 && registry === undefined) {
|
|
139
147
|
throw new Error("projectReferences requires a project registry");
|
|
140
148
|
}
|
|
@@ -149,6 +157,7 @@ export function createRule(
|
|
|
149
157
|
labels: input.labels,
|
|
150
158
|
extra: {
|
|
151
159
|
...(input.extra ?? {}),
|
|
160
|
+
...(activation === undefined ? {} : { activation }),
|
|
152
161
|
...(input.condition ? { condition: input.condition } : {}),
|
|
153
162
|
...(input.action ? { action: input.action } : {}),
|
|
154
163
|
severity: input.severity ?? "info",
|
|
@@ -271,7 +280,7 @@ export function removeRuleGroup(
|
|
|
271
280
|
}
|
|
272
281
|
|
|
273
282
|
/** The extra.scope run-gating check alone -- a Rule with no extra.scope always passes this; one with a skill-run/playbook-run scope passes only while its run owns activeTaskId. Both a workflow-definition target's own run scope ("skill-run", written by workflow-execution.ts's runWorkflowSteps for that target kind) and a Playbook's own run scope ("playbook-run", same call for a Playbook target) are recognized -- confirmed live that only "skill-run" was ever checked here, silently breaking Playbook-run-scoped rule injection since Playbook gained its own doc/rule structured steps. */
|
|
274
|
-
function
|
|
283
|
+
export function passesRuleRunScope(rule: Artifact, activeTaskId: string | undefined): boolean {
|
|
275
284
|
const scope = rule.extra.scope;
|
|
276
285
|
if (scope === undefined) return true;
|
|
277
286
|
if (typeof scope !== "object" || scope === null || Array.isArray(scope)) return false;
|
|
@@ -294,13 +303,23 @@ export function listInjectableRules(
|
|
|
294
303
|
scopes: ArtifactScopeStore,
|
|
295
304
|
projectRoot: string | undefined,
|
|
296
305
|
activeTaskId?: string,
|
|
306
|
+
context: ActivationContext = {},
|
|
297
307
|
): Artifact[] {
|
|
298
308
|
return artifacts.query({ kind: "rule", status: "active" }).filter((rule) => {
|
|
299
309
|
if (rule.subtype === "artifact-template") return false;
|
|
300
|
-
|
|
310
|
+
if (!passesRuleRunScope(rule, activeTaskId) || !scopes.appliesToProjectRoot(rule.id, projectRoot)) return false;
|
|
311
|
+
return evaluateActivation(activationConfig(rule.extra, "full"), { ...context, projectRoot }).enabled;
|
|
301
312
|
});
|
|
302
313
|
}
|
|
303
314
|
|
|
315
|
+
export function ruleActivationDecision(
|
|
316
|
+
rule: Artifact,
|
|
317
|
+
context: ActivationContext,
|
|
318
|
+
): { enabled: boolean; reason: string; priority: number; injection: InjectionProfile } {
|
|
319
|
+
const config = activationConfig(rule.extra, "full");
|
|
320
|
+
return { ...evaluateActivation(config, context), priority: config.priority, injection: config.injection };
|
|
321
|
+
}
|
|
322
|
+
|
|
304
323
|
export function showRule(artifacts: ArtifactStore, id: string): Artifact {
|
|
305
324
|
requireKind(artifacts, id, "rule");
|
|
306
325
|
return artifacts.get(id, { tree: true })!;
|
|
@@ -319,11 +338,15 @@ export function transitionRule(artifacts: ArtifactStore, id: string, action: Rul
|
|
|
319
338
|
return runTransition(artifacts, rule, "rule", action, RULE_TRANSITIONS, context);
|
|
320
339
|
}
|
|
321
340
|
|
|
322
|
-
export
|
|
341
|
+
export interface UpdateRuleInput extends UpdateContentInput {
|
|
342
|
+
activation?: unknown;
|
|
343
|
+
}
|
|
323
344
|
|
|
324
345
|
/** 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. */
|
|
325
346
|
export function updateRule(artifacts: ArtifactStore, id: string, input: UpdateRuleInput, context?: ArtifactEventContext): Artifact {
|
|
326
|
-
|
|
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");
|
|
349
|
+
}
|
|
327
350
|
assertTitleBounds(input.title);
|
|
328
351
|
assertLabelsBounds(input.labels);
|
|
329
352
|
const rule = requireLocallyOwnedContent(requireKind(artifacts, id, "rule"));
|
|
@@ -332,9 +355,14 @@ export function updateRule(artifacts: ArtifactStore, id: string, input: UpdateRu
|
|
|
332
355
|
const action = typeof rule.extra.action === "string" ? rule.extra.action : undefined;
|
|
333
356
|
assertRuleTextWithinBounds(condition, action, input.body);
|
|
334
357
|
}
|
|
335
|
-
const
|
|
358
|
+
const hasContent = input.title !== undefined || input.body !== undefined || input.labels !== undefined;
|
|
359
|
+
const updated = hasContent ? artifacts.updateContent(id, input, context) : rule;
|
|
336
360
|
if (!updated) throw new Error(`rule "${id}" not found`);
|
|
337
|
-
return updated;
|
|
361
|
+
if (input.activation === undefined) return updated;
|
|
362
|
+
const activation = validateActivationConfig(input.activation, "full");
|
|
363
|
+
const withActivation = artifacts.setExtra(updated.id, { ...updated.extra, activation }, context);
|
|
364
|
+
if (!withActivation) throw new Error(`rule "${id}" not found`);
|
|
365
|
+
return withActivation;
|
|
338
366
|
}
|
|
339
367
|
|
|
340
368
|
export function gateTaskWithRule(artifacts: ArtifactStore, ruleId: string, taskId: string, context?: ArtifactEventContext): Artifact {
|
package/src/service.ts
CHANGED
|
@@ -4,6 +4,8 @@ import type { DaemonDiagnosis } from "@danypops/vehicle-server/daemon-lifecycle"
|
|
|
4
4
|
import { createVehicleHttpApp } from "@danypops/vehicle-server/http";
|
|
5
5
|
import type { Logger } from "@danypops/vehicle-server/logging";
|
|
6
6
|
import type { CreateArtifactInput } from "./artifact/artifact.ts";
|
|
7
|
+
import { activationContextFromInput } from "./artifact/artifact-activation.ts";
|
|
8
|
+
import { auditArtifactActivation } from "./artifact/artifact-activation-audit.ts";
|
|
7
9
|
import type { ArtifactEventReader } from "./artifact/artifact-event-reader.ts";
|
|
8
10
|
import type { ArtifactScopeStore } from "./artifact/artifact-scope-store.ts";
|
|
9
11
|
import type { ArtifactStore } from "./artifact/artifact-store.ts";
|
|
@@ -80,6 +82,7 @@ const COMPOSITION_ROOT_OPERATION_NAMES = [
|
|
|
80
82
|
"artifact.restore",
|
|
81
83
|
"artifact.trash_status",
|
|
82
84
|
"artifact.trash_list",
|
|
85
|
+
"activation.audit",
|
|
83
86
|
"graph.link",
|
|
84
87
|
"graph.unlink",
|
|
85
88
|
"graph.tree",
|
|
@@ -400,11 +403,26 @@ function handlers(
|
|
|
400
403
|
const id = string(input, "id");
|
|
401
404
|
return artifacts.get(id)?.kind === "task" ? tasks.runGates(id, eventContextFor(input, "gates-api")) : gates.runAsync(id);
|
|
402
405
|
},
|
|
406
|
+
"activation.audit": (input) => {
|
|
407
|
+
const filter = taskFilter(input);
|
|
408
|
+
if (!filter.projectRoot) throw new Error("project_root is required");
|
|
409
|
+
const activeTask = tasks.active(filter);
|
|
410
|
+
return auditArtifactActivation(artifacts, artifactScopes, filter.projectRoot, activeTask?.id, {
|
|
411
|
+
...activationContextFromInput(input.activation_context),
|
|
412
|
+
projectRoot: filter.projectRoot,
|
|
413
|
+
taskStatus: activeTask?.status,
|
|
414
|
+
taskLabels: activeTask?.labels,
|
|
415
|
+
});
|
|
416
|
+
},
|
|
403
417
|
"rules.injectable": (input) => {
|
|
404
418
|
const filter = taskFilter(input);
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
419
|
+
const activeTask = tasks.active(filter);
|
|
420
|
+
return listInjectableRules(artifacts, artifactScopes, filter.projectRoot, activeTask?.id, {
|
|
421
|
+
...activationContextFromInput(input.activation_context),
|
|
422
|
+
projectRoot: filter.projectRoot,
|
|
423
|
+
taskStatus: activeTask?.status,
|
|
424
|
+
taskLabels: activeTask?.labels,
|
|
425
|
+
}).map(({ id, title, body, extra }) => ({ id, title, body, extra }));
|
|
408
426
|
},
|
|
409
427
|
"tasks.create": forwardToModule("tasks.create"),
|
|
410
428
|
"tasks.update": forwardToModule("tasks.update"),
|