@danypops/pi-papyrus 0.57.5 → 0.57.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/extension/src/context/activation-context.ts +43 -0
- package/extension/src/context/context-budget.ts +20 -1
- package/extension/src/context/context-hub-contribution.ts +9 -1
- package/extension/src/context/context-injection-telemetry.ts +89 -10
- package/extension/src/index.ts +8 -2
- package/extension/src/playbook/playbook-bridge.ts +30 -8
- package/package.json +2 -2
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { extname, join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface ActivationContextInput {
|
|
5
|
+
languages?: string[];
|
|
6
|
+
file_extensions?: string[];
|
|
7
|
+
session_capabilities?: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const LANGUAGE_MARKERS: ReadonlyArray<{ language: string; files: string[] }> = [
|
|
11
|
+
{ language: "typescript", files: ["tsconfig.json"] },
|
|
12
|
+
{ language: "javascript", files: ["package.json"] },
|
|
13
|
+
{ language: "rust", files: ["Cargo.toml"] },
|
|
14
|
+
{ language: "go", files: ["go.mod"] },
|
|
15
|
+
{ language: "python", files: ["pyproject.toml", "requirements.txt"] },
|
|
16
|
+
{ language: "java", files: ["pom.xml", "build.gradle", "build.gradle.kts"] },
|
|
17
|
+
{ language: "kotlin", files: ["build.gradle.kts"] },
|
|
18
|
+
{ language: "swift", files: ["Package.swift"] },
|
|
19
|
+
{ language: "zig", files: ["build.zig"] },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const PATH_TOKEN = /(?:^|[\s"'`(])([^\s"'`()]+\.[A-Za-z0-9]{1,10})(?=$|[\s"'`),:])/g;
|
|
23
|
+
const MAX_EXTENSIONS = 32;
|
|
24
|
+
const MAX_CAPABILITIES = 32;
|
|
25
|
+
|
|
26
|
+
/** Derives only bounded, deterministic turn signals. It never executes project code or interprets free-form predicates. */
|
|
27
|
+
export function buildActivationContext(cwd: string, prompt: string, selectedTools: readonly string[] = []): ActivationContextInput {
|
|
28
|
+
const languages = LANGUAGE_MARKERS.filter((entry) => entry.files.some((file) => existsSync(join(cwd, file))))
|
|
29
|
+
.map((entry) => entry.language)
|
|
30
|
+
.sort();
|
|
31
|
+
const extensions = new Set<string>();
|
|
32
|
+
for (const match of prompt.matchAll(PATH_TOKEN)) {
|
|
33
|
+
const extension = extname(match[1]!).toLowerCase();
|
|
34
|
+
if (extension) extensions.add(extension);
|
|
35
|
+
if (extensions.size >= MAX_EXTENSIONS) break;
|
|
36
|
+
}
|
|
37
|
+
const capabilities = [...new Set(selectedTools.filter((tool) => tool.length > 0))].sort().slice(0, MAX_CAPABILITIES);
|
|
38
|
+
return {
|
|
39
|
+
...(languages.length > 0 ? { languages } : {}),
|
|
40
|
+
...(extensions.size > 0 ? { file_extensions: [...extensions].sort() } : {}),
|
|
41
|
+
...(capabilities.length > 0 ? { session_capabilities: capabilities } : {}),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import type { ContextSegmentItem } from "@danypops/jittor";
|
|
4
4
|
import { type Artifact, CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN, CONTEXT_TREE_MAX_NODES, type TaskGraph } from "@danypops/papyrus";
|
|
5
|
+
import { playbookInjectionPreview } from "../playbook/playbook-bridge.ts";
|
|
5
6
|
import { ruleInjectionPreview } from "../rules/rules.ts";
|
|
6
7
|
import { discoverSkillDirectories, type SkillCatalogFootprint, scanSkillCatalogFootprint } from "./skill-catalog-footprint.ts";
|
|
7
8
|
|
|
@@ -27,6 +28,10 @@ export interface ContextBudget {
|
|
|
27
28
|
totalCharacters: number;
|
|
28
29
|
totalEstimatedTokens: number;
|
|
29
30
|
};
|
|
31
|
+
playbooks: {
|
|
32
|
+
entries: Array<{ title: string; estimatedTokens: number }>;
|
|
33
|
+
totalEstimatedTokens: number;
|
|
34
|
+
};
|
|
30
35
|
skills: SkillCatalogFootprint;
|
|
31
36
|
totalEstimatedTokens: number;
|
|
32
37
|
}
|
|
@@ -61,12 +66,26 @@ export function computeContextBudget(
|
|
|
61
66
|
rules: ReadonlyArray<Pick<Artifact, "id" | "title" | "body" | "extra">>,
|
|
62
67
|
cwd: string,
|
|
63
68
|
homeDirectory: string = homedir(),
|
|
69
|
+
playbooks: ReadonlyArray<Pick<Artifact, "title" | "extra">> = [],
|
|
64
70
|
): ContextBudget {
|
|
65
71
|
const settingsSkills = readSettingsSkillPaths(`${homeDirectory}/.pi/agent/settings.json`);
|
|
66
72
|
const directories = discoverSkillDirectories(homeDirectory, cwd, settingsSkills);
|
|
67
73
|
const skills = scanSkillCatalogFootprint(directories);
|
|
68
74
|
const ruleBudget = computeRuleBudget(rules);
|
|
69
|
-
|
|
75
|
+
const playbookEntries = playbooks.map((playbook) => ({
|
|
76
|
+
title: playbook.title,
|
|
77
|
+
estimatedTokens: Math.ceil(playbookInjectionPreview(playbook).length / CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN),
|
|
78
|
+
}));
|
|
79
|
+
const playbookBudget = {
|
|
80
|
+
entries: playbookEntries,
|
|
81
|
+
totalEstimatedTokens: playbookEntries.reduce((sum, entry) => sum + entry.estimatedTokens, 0),
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
rules: ruleBudget,
|
|
85
|
+
playbooks: playbookBudget,
|
|
86
|
+
skills,
|
|
87
|
+
totalEstimatedTokens: ruleBudget.totalEstimatedTokens + playbookBudget.totalEstimatedTokens + skills.totalEstimatedTokens,
|
|
88
|
+
};
|
|
70
89
|
}
|
|
71
90
|
|
|
72
91
|
/** Sums a possibly-nested item tree's tokens recursively -- every node's own contribution, not just top-level items. */
|
|
@@ -17,6 +17,7 @@ export function papyrusContextSegment(
|
|
|
17
17
|
ruleBudget: ContextBudget["rules"],
|
|
18
18
|
taskItems: ContextSegmentItem[],
|
|
19
19
|
skills: SkillCatalogFootprint,
|
|
20
|
+
playbookBudget?: ContextBudget["playbooks"],
|
|
20
21
|
): ContextSegment {
|
|
21
22
|
const items: ContextSegmentItem[] = [];
|
|
22
23
|
if (ruleBudget.entries.length > 0) {
|
|
@@ -26,6 +27,13 @@ export function papyrusContextSegment(
|
|
|
26
27
|
children: ruleBudget.entries.map((entry) => ({ label: entry.title, estimatedTokens: entry.estimatedTokens })),
|
|
27
28
|
});
|
|
28
29
|
}
|
|
30
|
+
if ((playbookBudget?.entries.length ?? 0) > 0) {
|
|
31
|
+
items.push({
|
|
32
|
+
label: "Available Playbooks",
|
|
33
|
+
estimatedTokens: playbookBudget!.totalEstimatedTokens,
|
|
34
|
+
children: playbookBudget!.entries.map((entry) => ({ label: entry.title, estimatedTokens: entry.estimatedTokens })),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
29
37
|
if (taskItems.length > 0) {
|
|
30
38
|
items.push({ label: "Open Tasks", estimatedTokens: sumItemTree(taskItems), children: taskItems });
|
|
31
39
|
}
|
|
@@ -38,7 +46,7 @@ export function papyrusContextSegment(
|
|
|
38
46
|
}
|
|
39
47
|
return {
|
|
40
48
|
key: "papyrus",
|
|
41
|
-
label: "Papyrus (Rules, Tasks, Skills)",
|
|
49
|
+
label: "Papyrus (Rules, Playbooks, Tasks, Skills)",
|
|
42
50
|
estimatedTokens: items.reduce((sum, item) => sum + item.estimatedTokens, 0),
|
|
43
51
|
confidence: "exact-cooperative",
|
|
44
52
|
...(items.length > 0 ? { items } : {}),
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
type Artifact,
|
|
4
|
+
activationConfig,
|
|
5
|
+
CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN,
|
|
6
|
+
PAPYRUS_CONTEXT_INJECTION_MAX_TOKENS,
|
|
7
|
+
PAPYRUS_CONTEXT_INJECTION_SCHEMA,
|
|
8
|
+
} from "@danypops/papyrus";
|
|
3
9
|
import { playbookInjectionPreview } from "../playbook/playbook-bridge.ts";
|
|
4
10
|
import { ruleInjectionPreview } from "../rules/rules.ts";
|
|
5
11
|
|
|
@@ -14,8 +20,8 @@ export interface PapyrusContextInjectionObservation {
|
|
|
14
20
|
sequence: number;
|
|
15
21
|
producerId: string;
|
|
16
22
|
before: ContextPayloadSize;
|
|
17
|
-
rules: ContextPayloadSize & { count: number };
|
|
18
|
-
playbooks: ContextPayloadSize & { count: number };
|
|
23
|
+
rules: ContextPayloadSize & { count: number; omitted: number };
|
|
24
|
+
playbooks: ContextPayloadSize & { count: number; omitted: number };
|
|
19
25
|
tasks: ContextPayloadSize;
|
|
20
26
|
injected: ContextPayloadSize;
|
|
21
27
|
after: ContextPayloadSize;
|
|
@@ -34,9 +40,75 @@ export interface BuildContextInjectionInput {
|
|
|
34
40
|
sequence: number;
|
|
35
41
|
producerId: string;
|
|
36
42
|
previousFingerprint?: string;
|
|
43
|
+
maxEstimatedTokens?: number;
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
const encoder = new TextEncoder();
|
|
47
|
+
const RULE_HEADER = "\n\n## Active rules (Papyrus)\n\n";
|
|
48
|
+
const PLAYBOOK_HEADER = "\n\n## Available playbooks (Papyrus)\n\n";
|
|
49
|
+
const TASK_HEADER = "\n\n## Open tasks (Papyrus)\n\n";
|
|
50
|
+
|
|
51
|
+
function ruleText(rule: Pick<Artifact, "title" | "body" | "extra">): string {
|
|
52
|
+
const config = activationConfig(rule.extra, "full");
|
|
53
|
+
const condition = typeof rule.extra.condition === "string" ? ` (when: ${rule.extra.condition})` : "";
|
|
54
|
+
if (config.injection === "catalog") return `• ${rule.title}${condition}`;
|
|
55
|
+
return ruleInjectionPreview(rule);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface InjectionCandidate {
|
|
59
|
+
kind: "rule" | "playbook";
|
|
60
|
+
text: string;
|
|
61
|
+
priority: number;
|
|
62
|
+
stableKey: string;
|
|
63
|
+
index: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function selectWithinBudget(
|
|
67
|
+
input: BuildContextInjectionInput,
|
|
68
|
+
taskBlock: string,
|
|
69
|
+
): { ruleIndexes: Set<number>; playbookIndexes: Set<number> } {
|
|
70
|
+
const maxCharacters = (input.maxEstimatedTokens ?? PAPYRUS_CONTEXT_INJECTION_MAX_TOKENS) * CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN;
|
|
71
|
+
let used = taskBlock.length;
|
|
72
|
+
let hasRule = false;
|
|
73
|
+
let hasPlaybook = false;
|
|
74
|
+
const candidates: InjectionCandidate[] = [
|
|
75
|
+
...input.rules.map((rule, index) => ({
|
|
76
|
+
kind: "rule" as const,
|
|
77
|
+
text: ruleText(rule),
|
|
78
|
+
priority: activationConfig(rule.extra, "full").priority,
|
|
79
|
+
stableKey: `${rule.title}\u0000${index}`,
|
|
80
|
+
index,
|
|
81
|
+
})),
|
|
82
|
+
...input.playbooks.map((playbook, index) => ({
|
|
83
|
+
kind: "playbook" as const,
|
|
84
|
+
text: playbookInjectionPreview(playbook),
|
|
85
|
+
priority: activationConfig(playbook.extra, "catalog").priority,
|
|
86
|
+
stableKey: `${playbook.title}\u0000${index}`,
|
|
87
|
+
index,
|
|
88
|
+
})),
|
|
89
|
+
].filter((candidate) => {
|
|
90
|
+
const source = candidate.kind === "rule" ? input.rules[candidate.index] : input.playbooks[candidate.index];
|
|
91
|
+
return activationConfig(source!.extra, candidate.kind === "rule" ? "full" : "catalog").injection !== "on-demand";
|
|
92
|
+
});
|
|
93
|
+
candidates.sort((left, right) => right.priority - left.priority || left.stableKey.localeCompare(right.stableKey));
|
|
94
|
+
const ruleIndexes = new Set<number>();
|
|
95
|
+
const playbookIndexes = new Set<number>();
|
|
96
|
+
for (const candidate of candidates) {
|
|
97
|
+
const firstOfKind = candidate.kind === "rule" ? !hasRule : !hasPlaybook;
|
|
98
|
+
const overhead = firstOfKind ? (candidate.kind === "rule" ? RULE_HEADER.length + 1 : PLAYBOOK_HEADER.length + 1) : 1;
|
|
99
|
+
const cost = candidate.text.length + overhead;
|
|
100
|
+
if (used + cost > maxCharacters) continue;
|
|
101
|
+
used += cost;
|
|
102
|
+
if (candidate.kind === "rule") {
|
|
103
|
+
ruleIndexes.add(candidate.index);
|
|
104
|
+
hasRule = true;
|
|
105
|
+
} else {
|
|
106
|
+
playbookIndexes.add(candidate.index);
|
|
107
|
+
hasPlaybook = true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { ruleIndexes, playbookIndexes };
|
|
111
|
+
}
|
|
40
112
|
|
|
41
113
|
function size(value: string): ContextPayloadSize {
|
|
42
114
|
return { characters: value.length, bytes: encoder.encode(value).byteLength };
|
|
@@ -49,11 +121,14 @@ export function buildContextInjection(input: BuildContextInjectionInput): {
|
|
|
49
121
|
taskBlock: string;
|
|
50
122
|
observation: PapyrusContextInjectionObservation;
|
|
51
123
|
} {
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
const
|
|
124
|
+
const taskBlock = input.taskSummary ? `${TASK_HEADER}${input.taskSummary}\n` : "";
|
|
125
|
+
const selected = selectWithinBudget(input, taskBlock);
|
|
126
|
+
const selectedRules = input.rules.filter((_rule, index) => selected.ruleIndexes.has(index));
|
|
127
|
+
const selectedPlaybooks = input.playbooks.filter((_playbook, index) => selected.playbookIndexes.has(index));
|
|
128
|
+
const ruleContent = selectedRules.map(ruleText).join("\n");
|
|
129
|
+
const ruleBlock = ruleContent ? `${RULE_HEADER}${ruleContent}\n` : "";
|
|
130
|
+
const playbookContent = selectedPlaybooks.map(playbookInjectionPreview).join("\n");
|
|
131
|
+
const playbookBlock = playbookContent ? `${PLAYBOOK_HEADER}${playbookContent}\n` : "";
|
|
57
132
|
const injected = `${ruleBlock}${playbookBlock}${taskBlock}`;
|
|
58
133
|
const prompt = `${input.basePrompt}${injected}`;
|
|
59
134
|
const fingerprint = createHash("sha256").update(injected).digest("hex");
|
|
@@ -70,8 +145,12 @@ export function buildContextInjection(input: BuildContextInjectionInput): {
|
|
|
70
145
|
sequence: input.sequence,
|
|
71
146
|
producerId: input.producerId,
|
|
72
147
|
before: size(input.basePrompt),
|
|
73
|
-
rules: { ...size(ruleBlock), count: input.rules.length },
|
|
74
|
-
playbooks: {
|
|
148
|
+
rules: { ...size(ruleBlock), count: selectedRules.length, omitted: input.rules.length - selectedRules.length },
|
|
149
|
+
playbooks: {
|
|
150
|
+
...size(playbookBlock),
|
|
151
|
+
count: selectedPlaybooks.length,
|
|
152
|
+
omitted: input.playbooks.length - selectedPlaybooks.length,
|
|
153
|
+
},
|
|
75
154
|
tasks: size(taskBlock),
|
|
76
155
|
injected: injectedSize,
|
|
77
156
|
after: afterSize,
|
package/extension/src/index.ts
CHANGED
|
@@ -34,6 +34,7 @@ import { AutoRotatingWindow, renderCardRow, type WidgetSection } from "malevich-
|
|
|
34
34
|
import { Type } from "typebox";
|
|
35
35
|
import { formatMetadata } from "./artifact/artifact-format.ts";
|
|
36
36
|
import { BoundedPoll } from "./bounded-poll.ts";
|
|
37
|
+
import { buildActivationContext } from "./context/activation-context.ts";
|
|
37
38
|
import { buildTaskItemTree, computeContextBudget } from "./context/context-budget.ts";
|
|
38
39
|
import { PAPYRUS_CONTEXT_HUB_PRODUCER_NAME, papyrusContextSegment } from "./context/context-hub-contribution.ts";
|
|
39
40
|
import { buildContextInjection } from "./context/context-injection-telemetry.ts";
|
|
@@ -917,15 +918,20 @@ export default async function (pi: ExtensionAPI) {
|
|
|
917
918
|
let result: { systemPrompt: string } | undefined;
|
|
918
919
|
try {
|
|
919
920
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
921
|
+
const activationContext = buildActivationContext(ctx.cwd, event.prompt, event.systemPromptOptions.selectedTools);
|
|
920
922
|
const [rules, playbooks, summary, taskGraph] = await Promise.all([
|
|
921
923
|
callService<Record<string, unknown>, Array<Pick<Artifact, "id" | "title" | "body" | "extra">>>("rules.injectable", {
|
|
922
924
|
project_root: ctx.cwd,
|
|
923
925
|
session_id: sessionId,
|
|
926
|
+
activation_context: activationContext,
|
|
924
927
|
}),
|
|
925
928
|
callService<Record<string, unknown>, Array<Pick<Artifact, "title" | "extra">>>("playbooks.list", {
|
|
926
929
|
status: "active",
|
|
927
930
|
project_root: ctx.cwd,
|
|
928
931
|
applicable: true,
|
|
932
|
+
activated: true,
|
|
933
|
+
activation_context: activationContext,
|
|
934
|
+
session_id: sessionId,
|
|
929
935
|
limit: PLAYBOOK_BRIDGE_MAX_PLAYBOOKS,
|
|
930
936
|
}),
|
|
931
937
|
callService<Record<string, unknown>, string | null>("tasks.context", {
|
|
@@ -951,13 +957,13 @@ export default async function (pi: ExtensionAPI) {
|
|
|
951
957
|
// Context Hub contribution is best-effort observability for /context -- its own failure
|
|
952
958
|
// must never block this turn's actual rules/tasks injection above.
|
|
953
959
|
try {
|
|
954
|
-
const { rules: ruleBudget, skills } = computeContextBudget(rules, ctx.cwd);
|
|
960
|
+
const { rules: ruleBudget, playbooks: playbookBudget, skills } = computeContextBudget(rules, ctx.cwd, undefined, playbooks);
|
|
955
961
|
pi.events.emit(CONTEXT_HUB_CONTRIBUTION_CHANNEL, {
|
|
956
962
|
schema: CONTEXT_HUB_CONTRIBUTION_SCHEMA,
|
|
957
963
|
observedAt: Date.now(),
|
|
958
964
|
sequence: ++contextHubContributionSequence,
|
|
959
965
|
producerName: PAPYRUS_CONTEXT_HUB_PRODUCER_NAME,
|
|
960
|
-
segment: papyrusContextSegment(ruleBudget, buildTaskItemTree(taskGraph), skills),
|
|
966
|
+
segment: papyrusContextSegment(ruleBudget, buildTaskItemTree(taskGraph), skills, playbookBudget),
|
|
961
967
|
});
|
|
962
968
|
} catch {
|
|
963
969
|
// Malformed/unreachable daemon data for this turn's contribution -- drop it silently.
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import type { Artifact } from "@danypops/papyrus";
|
|
22
22
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
23
|
+
import { buildActivationContext } from "../context/activation-context.ts";
|
|
23
24
|
import { callService } from "../service-client.ts";
|
|
24
25
|
|
|
25
26
|
export const PLAYBOOK_BRIDGE_MAX_PLAYBOOKS = 100;
|
|
@@ -33,8 +34,19 @@ function slugify(title: string): string {
|
|
|
33
34
|
return slug.length > 0 ? slug : "playbook";
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
async function activePlaybooks(): Promise<Artifact[]> {
|
|
37
|
-
return callService<Record<string, unknown>, Artifact[]>("playbooks.list", {
|
|
37
|
+
async function activePlaybooks(projectRoot?: string, capabilities: readonly string[] = []): Promise<Artifact[]> {
|
|
38
|
+
return callService<Record<string, unknown>, Artifact[]>("playbooks.list", {
|
|
39
|
+
status: "active",
|
|
40
|
+
limit: PLAYBOOK_BRIDGE_MAX_PLAYBOOKS,
|
|
41
|
+
...(projectRoot === undefined
|
|
42
|
+
? {}
|
|
43
|
+
: {
|
|
44
|
+
project_root: projectRoot,
|
|
45
|
+
applicable: true,
|
|
46
|
+
activated: true,
|
|
47
|
+
activation_context: buildActivationContext(projectRoot, "", capabilities),
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
38
50
|
}
|
|
39
51
|
|
|
40
52
|
/** Exported for direct testing without a real ExtensionAPI. */
|
|
@@ -54,8 +66,11 @@ export function playbookInjectionPreview(playbook: Pick<Artifact, "title" | "ext
|
|
|
54
66
|
}
|
|
55
67
|
|
|
56
68
|
/** Exported for direct testing without a real ExtensionAPI: what would be registered right now. */
|
|
57
|
-
export async function planPlaybookCommandRegistrations(
|
|
58
|
-
|
|
69
|
+
export async function planPlaybookCommandRegistrations(
|
|
70
|
+
projectRoot?: string,
|
|
71
|
+
capabilities: readonly string[] = [],
|
|
72
|
+
): Promise<Array<{ name: string; id: string; title: string; trigger: string }>> {
|
|
73
|
+
const playbooks = await activePlaybooks(projectRoot, capabilities);
|
|
59
74
|
const usedNames = new Set<string>();
|
|
60
75
|
return playbooks.map((playbook) => {
|
|
61
76
|
let name = playbookCommandName(playbook.title);
|
|
@@ -67,14 +82,21 @@ export async function planPlaybookCommandRegistrations(): Promise<Array<{ name:
|
|
|
67
82
|
}
|
|
68
83
|
|
|
69
84
|
export function registerPlaybookBridge(pi: ExtensionAPI): void {
|
|
70
|
-
const refresh = async () => {
|
|
85
|
+
const refresh = async (projectRoot?: string) => {
|
|
71
86
|
try {
|
|
72
|
-
const registrations = await planPlaybookCommandRegistrations();
|
|
87
|
+
const registrations = await planPlaybookCommandRegistrations(projectRoot, pi.getActiveTools?.() ?? []);
|
|
73
88
|
for (const { name, id, title, trigger } of registrations) {
|
|
74
89
|
pi.registerCommand(name, {
|
|
75
90
|
description: trigger,
|
|
76
91
|
handler: async (_args, ctx) => {
|
|
77
92
|
try {
|
|
93
|
+
if (ctx.cwd) {
|
|
94
|
+
const currentlyActive = await activePlaybooks(ctx.cwd, pi.getActiveTools?.() ?? []);
|
|
95
|
+
if (!currentlyActive.some((playbook) => playbook.id === id)) {
|
|
96
|
+
ctx.ui.notify(`"${title}" is not enabled for this project context`, "error");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
78
100
|
// Re-fetched live, not captured at registration time: a lingering stale
|
|
79
101
|
// command (renamed or disabled since, since registerCommand can't be
|
|
80
102
|
// unregistered) must fail cleanly, never run deleted/stale content.
|
|
@@ -103,8 +125,8 @@ export function registerPlaybookBridge(pi: ExtensionAPI): void {
|
|
|
103
125
|
// "no new/updated playbook commands this cycle", not a broken session start.
|
|
104
126
|
}
|
|
105
127
|
};
|
|
106
|
-
pi.on("resources_discover", async () => {
|
|
107
|
-
await refresh();
|
|
128
|
+
pi.on("resources_discover", async (event) => {
|
|
129
|
+
await refresh(event?.cwd);
|
|
108
130
|
return {};
|
|
109
131
|
});
|
|
110
132
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-papyrus",
|
|
3
|
-
"version": "0.57.
|
|
3
|
+
"version": "0.57.6",
|
|
4
4
|
"description": "Pi host extension for Papyrus: native tools, TUI panels, and context injection over the daemon-backed graph store",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": ["pi-package"],
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@danypops/jittor": "^0.19.2",
|
|
22
|
-
"@danypops/papyrus": "^0.60.
|
|
22
|
+
"@danypops/papyrus": "^0.60.5",
|
|
23
23
|
"@danypops/vehicle-client": "^0.10.3",
|
|
24
24
|
"@danypops/vehicle-core": "^0.18.5",
|
|
25
25
|
"@danypops/vehicle-server": "^0.25.2",
|