/**
* modules/playbooks.ts — Playbooks as a Papyrus-native registered module.
*
* A Playbook is prose-first, whole-artifact composition (contains/depends_on between real
* Playbook artifacts) rather than a raw JSON blueprint -- but its own step list can also
* declare Doc/Rule blueprints and typed arguments and nested pipeline calls. playbooks.invoke
* recycles the shared blueprint materialization engine (playbook/playbook-execution.ts compiles a
* Playbook's steps and composition tree into a BlueprintDefinition, then hands off to
* playbook/workflow-execution.ts's shared core). See playbook/playbook-service.ts and
* playbook/playbook-definition.ts for the full rationale.
*/
import { summarizeArtifact } from "../artifact/artifact.ts";
import { activationContextFromInput } from "../artifact/artifact-activation.ts";
import type { ArtifactScopeStore } from "../artifact/artifact-scope-store.ts";
import type { ArtifactStore } from "../artifact/artifact-store.ts";
import type { OperationDefinition } from "../module-registry.ts";
import { invokePlaybook } from "../playbook/playbook-execution.ts";
import {
addPlaybookGroup,
addPlaybookProject,
assignPlaybookProject,
containPlaybook,
createPlaybook,
dependPlaybook,
listActivatedPlaybooks,
listPlaybooks,
playbookInvocation,
playbookScope,
removePlaybookGroup,
removePlaybookProject,
replacePlaybookGroups,
replacePlaybookProjects,
setPlaybookGlobal,
setPlaybookNone,
showPlaybook,
transitionPlaybook,
uncontainPlaybook,
undependPlaybook,
updatePlaybook,
} from "../playbook/playbook-service.ts";
import type { ProjectRegistryStore } from "../project-registry/project-registry-store.ts";
import type { ScopeGroupStore } from "../scope-group/scope-group-store.ts";
import type { SessionIdentity } from "../session-identity/session-identity-service.ts";
import type { TaskEventSink } from "../task/event/task-event-store.ts";
import type { TaskScopeAssigner } from "../task/scope/task-scope-store.ts";
import type { Tasks } from "../task/task-service.ts";
import { type OperationInput, optionalBoolean, optionalNumber, optionalString, string } from "./operation-input.ts";
const MODULE_ID = "playbooks";
const eventContext = (input: OperationInput) => ({
actor: optionalString(input, "actor"),
source: optionalString(input, "source"),
sessionId: optionalString(input, "session_id") ?? optionalString(input, "sessionId"),
});
const eventContextFor = (input: OperationInput, source: string) => {
const context = eventContext(input);
return { ...context, source: context.source ?? source };
};
/**
* applicable=true switches project_root's meaning from exact-membership audit listing to
* applicable listing (global Playbooks plus Playbooks whose membership includes it) -- see
* ListFilter's own doc comment on projectRoot vs applicableToProjectRoot. Used by pi-papyrus's
* before_agent_start to inject only Playbooks applicable to ctx.cwd, not every active Playbook
* across every project.
*/
const artifactFilter = (input: OperationInput) => {
const projectRoot = optionalString(input, "project_root");
const applicable = optionalBoolean(input, "applicable") === true;
if (applicable && projectRoot === undefined) throw new Error("applicable requires project_root");
return {
status: optionalString(input, "status"),
text: optionalString(input, "text"),
limit: optionalNumber(input, "limit"),
...(applicable ? { applicableToProjectRoot: projectRoot } : { projectRoot }),
};
};
/** This module's own operation names, the single source of truth src/service.ts's EXPECTED_OPERATION_NAMES spreads in rather than re-listing by hand. */
export const PLAYBOOKS_OPERATION_NAMES = [
"playbooks.create",
"playbooks.list",
"playbooks.show",
"playbooks.invoke",
"playbooks.preview",
"playbooks.enable",
"playbooks.disable",
"playbooks.assign_project",
"playbooks.scope",
"playbooks.set_global",
"playbooks.set_none",
"playbooks.add_project",
"playbooks.remove_project",
"playbooks.replace_projects",
"playbooks.add_group",
"playbooks.remove_group",
"playbooks.replace_groups",
"playbooks.update",
"playbooks.contain",
"playbooks.uncontain",
"playbooks.depend",
"playbooks.undepend",
] as const;
export interface PlaybooksModuleDeps {
artifacts: ArtifactStore;
events: TaskEventSink;
scopes: TaskScopeAssigner;
/** Docs/Rules/Skills/Playbooks project scoping (distinct from `scopes`, which is Task-run project scoping for playbooks.invoke's materialized tasks). */
artifactScopes: ArtifactScopeStore;
/** Used for exactly one thing: focusing the entry task after a successful invoke -- the one safety-checked Tasks operation this module needs, not bulk graph construction (that goes straight through artifacts/events/scopes in playbook/playbook-execution.ts, mirroring playbook/workflow-execution.ts). */
tasks: Tasks;
/** Guards the same session_secret check tasks.focus's own operation enforces (guardFocusMutation in modules/tasks.ts) -- invoke's internal tasks.focus() call goes straight through the Tasks class, bypassing that operation wrapper entirely, so the check must be applied here instead of silently skipped. */
sessionIdentity: SessionIdentity;
registry: ProjectRegistryStore;
scopeGroups: ScopeGroupStore;
}
export function playbooksOperations({
artifacts,
events,
scopes,
artifactScopes,
tasks,
sessionIdentity,
registry,
scopeGroups,
}: PlaybooksModuleDeps): OperationDefinition[] {
const define = (name: string, execute: (input: Input) => Output): OperationDefinition => ({
name,
moduleId: MODULE_ID,
execute,
});
return [
define("playbooks.create", (input: OperationInput) =>
createPlaybook(
artifacts,
artifactScopes,
{
title: string(input, "title"),
body: optionalString(input, "body"),
trigger: optionalString(input, "trigger"),
steps: input.steps,
tools: input.tools as string[] | undefined,
arguments: input.arguments,
subtype: optionalString(input, "subtype"),
labels: input.labels as string[] | undefined,
extra: input.extra as Record | undefined,
activation: input.activation,
activationEnabled: optionalBoolean(input, "activation_enabled"),
templateId: optionalString(input, "template_id") ?? optionalString(input, "templateId"),
projectRoot: optionalString(input, "project_root"),
projectReferences: input.projects as string[] | undefined,
},
eventContext(input),
registry,
),
),
define("playbooks.list", (input: OperationInput) => {
const filter = artifactFilter(input);
const projectRoot = optionalString(input, "project_root");
const activeTask =
optionalBoolean(input, "activated") === true
? tasks.active({ projectRoot, sessionId: optionalString(input, "session_id") ?? optionalString(input, "sessionId") })
: undefined;
const playbooks =
optionalBoolean(input, "activated") === true
? listActivatedPlaybooks(artifacts, artifactScopes, filter, {
...activationContextFromInput(input.activation_context),
projectRoot,
taskStatus: activeTask?.status,
taskLabels: activeTask?.labels,
})
: listPlaybooks(artifacts, artifactScopes, filter);
return optionalBoolean(input, "full") === true ? playbooks : playbooks.map(summarizeArtifact);
}),
define("playbooks.show", (input: OperationInput) => showPlaybook(artifacts, string(input, "id"))),
define("playbooks.preview", (input: OperationInput) =>
playbookInvocation(artifacts, string(input, "id"), input.arguments as Record | undefined),
),
define("playbooks.invoke", (input: OperationInput) => {
const result = invokePlaybook(
artifacts,
string(input, "id"),
{
runId: optionalString(input, "run_id") ?? optionalString(input, "runId"),
arguments: input.arguments as Record | undefined,
},
{
events,
scopes,
artifactScopes,
projectRoot: optionalString(input, "project_root"),
context: eventContextFor(input, "playbook-run"),
},
);
if ("missingArguments" in result) return result;
const focusContext = eventContextFor(input, "playbook-run");
sessionIdentity.assertAuthorized(focusContext.sessionId, optionalString(input, "session_secret"));
tasks.focus(result.entryTaskId, focusContext);
return result;
}),
define("playbooks.enable", (input: OperationInput) =>
transitionPlaybook(artifacts, string(input, "id"), "enable", eventContext(input)),
),
define("playbooks.disable", (input: OperationInput) =>
transitionPlaybook(artifacts, string(input, "id"), "disable", eventContext(input)),
),
define("playbooks.assign_project", (input: OperationInput) =>
assignPlaybookProject(artifacts, artifactScopes, string(input, "id"), optionalString(input, "project_root")),
),
define("playbooks.scope", (input: OperationInput) => playbookScope(artifacts, artifactScopes, string(input, "id"))),
define("playbooks.set_global", (input: OperationInput) => setPlaybookGlobal(artifacts, artifactScopes, string(input, "id"))),
define("playbooks.set_none", (input: OperationInput) => setPlaybookNone(artifacts, artifactScopes, string(input, "id"))),
define("playbooks.add_group", (input: OperationInput) =>
addPlaybookGroup(artifacts, artifactScopes, scopeGroups, string(input, "id"), string(input, "group")),
),
define("playbooks.remove_group", (input: OperationInput) =>
removePlaybookGroup(artifacts, artifactScopes, scopeGroups, string(input, "id"), string(input, "group")),
),
define("playbooks.replace_groups", (input: OperationInput) =>
replacePlaybookGroups(artifacts, artifactScopes, scopeGroups, string(input, "id"), (input.groups as string[] | undefined) ?? []),
),
define("playbooks.add_project", (input: OperationInput) =>
addPlaybookProject(artifacts, artifactScopes, registry, string(input, "id"), string(input, "project")),
),
define("playbooks.remove_project", (input: OperationInput) =>
removePlaybookProject(artifacts, artifactScopes, registry, string(input, "id"), string(input, "project")),
),
define("playbooks.replace_projects", (input: OperationInput) =>
replacePlaybookProjects(artifacts, artifactScopes, registry, string(input, "id"), (input.projects as string[] | undefined) ?? []),
),
define("playbooks.update", (input: OperationInput) =>
updatePlaybook(
artifacts,
string(input, "id"),
{
title: optionalString(input, "title"),
body: optionalString(input, "body"),
labels: input.labels as string[] | undefined,
trigger: optionalString(input, "trigger"),
steps: input.steps,
activation: input.activation,
activationEnabled: optionalBoolean(input, "activation_enabled"),
},
eventContext(input),
),
),
define("playbooks.contain", (input: OperationInput) =>
containPlaybook(artifacts, string(input, "parent_id"), string(input, "child_id"), eventContext(input)),
),
define("playbooks.uncontain", (input: OperationInput) =>
uncontainPlaybook(artifacts, string(input, "parent_id"), string(input, "child_id"), eventContext(input)),
),
define("playbooks.depend", (input: OperationInput) =>
dependPlaybook(artifacts, string(input, "id"), string(input, "dependency_id"), eventContext(input)),
),
define("playbooks.undepend", (input: OperationInput) =>
undependPlaybook(artifacts, string(input, "id"), string(input, "dependency_id"), eventContext(input)),
),
];
}