@whamp/pi-pstack 0.7.0
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/LICENSE +21 -0
- package/README.md +68 -0
- package/agents/comment-sicko.md +34 -0
- package/agents/poteto-agent.md +13 -0
- package/extensions/pstack/ask-user-question.ts +137 -0
- package/extensions/pstack/config.ts +55 -0
- package/extensions/pstack/index.ts +330 -0
- package/extensions/pstack/pstack-config-status.ts +72 -0
- package/extensions/pstack/pstack-role-config-store.ts +287 -0
- package/extensions/pstack/pstack-role-config.ts +496 -0
- package/extensions/pstack/pstack-role-prompt.ts +33 -0
- package/extensions/pstack/pstack-roles.ts +252 -0
- package/extensions/pstack/pstack-setup-plan.ts +223 -0
- package/extensions/pstack/skill-strip.ts +62 -0
- package/package.json +60 -0
- package/skills/architect/SKILL.md +83 -0
- package/skills/architect/references/design-red-flags.md +33 -0
- package/skills/architect/references/rationale-template.md +35 -0
- package/skills/architect/references/runner-prompt.md +20 -0
- package/skills/arena/SKILL.md +71 -0
- package/skills/automate-me/SKILL.md +104 -0
- package/skills/blast-radius/SKILL.md +50 -0
- package/skills/bro/SKILL.md +7 -0
- package/skills/create-verification-skill/SKILL.md +44 -0
- package/skills/create-verification-skill/references/feature-map-example/README.md +47 -0
- package/skills/create-verification-skill/references/feature-map-example/create-note.md +39 -0
- package/skills/create-verification-skill/references/feature-map-example/search.md +45 -0
- package/skills/deslop/SKILL.md +23 -0
- package/skills/figure-it-out/SKILL.md +53 -0
- package/skills/how/SKILL.md +52 -0
- package/skills/how/references/explainer-prompt.md +55 -0
- package/skills/how/references/explorer-prompt.md +52 -0
- package/skills/interrogate/SKILL.md +111 -0
- package/skills/interrogate/references/code-quality-review.md +47 -0
- package/skills/interrogate/references/lead-judgment.md +58 -0
- package/skills/interrogate/references/reviewer-prompt.md +72 -0
- package/skills/interrogate/references/rubric.md +77 -0
- package/skills/maintain-verification-skill/SKILL.md +39 -0
- package/skills/no-comments/SKILL.md +24 -0
- package/skills/poteto-mode/SKILL.md +147 -0
- package/skills/poteto-mode/playbooks/authoring-a-skill.md +12 -0
- package/skills/poteto-mode/playbooks/autonomous-run.md +13 -0
- package/skills/poteto-mode/playbooks/autopilot-full.md +13 -0
- package/skills/poteto-mode/playbooks/autopilot-stack.md +16 -0
- package/skills/poteto-mode/playbooks/babysit.md +27 -0
- package/skills/poteto-mode/playbooks/bug-fix.md +17 -0
- package/skills/poteto-mode/playbooks/eval.md +25 -0
- package/skills/poteto-mode/playbooks/feature.md +21 -0
- package/skills/poteto-mode/playbooks/hillclimb.md +21 -0
- package/skills/poteto-mode/playbooks/investigation.md +14 -0
- package/skills/poteto-mode/playbooks/multi-phase-plan.md +156 -0
- package/skills/poteto-mode/playbooks/opening-a-pr.md +33 -0
- package/skills/poteto-mode/playbooks/orchestrate.md +199 -0
- package/skills/poteto-mode/playbooks/pause-safely.md +10 -0
- package/skills/poteto-mode/playbooks/perf-issue.md +24 -0
- package/skills/poteto-mode/playbooks/prototype.md +14 -0
- package/skills/poteto-mode/playbooks/refactoring.md +16 -0
- package/skills/poteto-mode/playbooks/runtime-forensics.md +11 -0
- package/skills/poteto-mode/playbooks/session-pickup.md +11 -0
- package/skills/poteto-mode/playbooks/shipping.md +17 -0
- package/skills/poteto-mode/playbooks/trace-forensics.md +14 -0
- package/skills/poteto-mode/playbooks/visual-parity.md +11 -0
- package/skills/poteto-mode/playbooks/worktree-cleanup.md +14 -0
- package/skills/poteto-mode/references/bugbot-triage.md +142 -0
- package/skills/poteto-mode/scripts/bootstrap.ts +62 -0
- package/skills/poteto-mode/scripts/bun.lock +67 -0
- package/skills/poteto-mode/scripts/check-plan.mjs +186 -0
- package/skills/poteto-mode/scripts/check-plan.test.mjs +25 -0
- package/skills/poteto-mode/scripts/orch/orch.test.ts +634 -0
- package/skills/poteto-mode/scripts/orch/orch.ts +578 -0
- package/skills/poteto-mode/scripts/orch/store.ts +1607 -0
- package/skills/poteto-mode/scripts/package.json +16 -0
- package/skills/poteto-mode/scripts/watch-pr/cli.test.ts +224 -0
- package/skills/poteto-mode/scripts/watch-pr/cli.ts +223 -0
- package/skills/poteto-mode/scripts/watch-pr/fakes.test-helper.ts +118 -0
- package/skills/poteto-mode/scripts/watch-pr/github.test.ts +306 -0
- package/skills/poteto-mode/scripts/watch-pr/github.ts +699 -0
- package/skills/poteto-mode/scripts/watch-pr/policy.test.ts +420 -0
- package/skills/poteto-mode/scripts/watch-pr/policy.ts +832 -0
- package/skills/poteto-mode/scripts/watch-pr/render.ts +169 -0
- package/skills/poteto-mode/scripts/watch-pr/tsconfig.json +13 -0
- package/skills/poteto-mode/scripts/watch-pr/types.compile.ts +93 -0
- package/skills/poteto-mode/scripts/watch-pr/types.ts +401 -0
- package/skills/poteto-mode/scripts/watch-pr/watch-pr +6 -0
- package/skills/poteto-mode/scripts/worktree-audit.sh +85 -0
- package/skills/principle-attack-the-premise/SKILL.md +23 -0
- package/skills/principle-boundary-discipline/SKILL.md +34 -0
- package/skills/principle-build-the-lever/SKILL.md +23 -0
- package/skills/principle-encode-lessons-in-structure/SKILL.md +31 -0
- package/skills/principle-exhaust-the-design-space/SKILL.md +21 -0
- package/skills/principle-experience-first/SKILL.md +19 -0
- package/skills/principle-fix-root-causes/SKILL.md +23 -0
- package/skills/principle-foundational-thinking/SKILL.md +21 -0
- package/skills/principle-guard-the-context-window/SKILL.md +17 -0
- package/skills/principle-laziness-protocol/SKILL.md +18 -0
- package/skills/principle-make-operations-idempotent/SKILL.md +24 -0
- package/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +22 -0
- package/skills/principle-minimize-reader-load/SKILL.md +23 -0
- package/skills/principle-model-the-domain/SKILL.md +26 -0
- package/skills/principle-never-block-on-the-human/SKILL.md +22 -0
- package/skills/principle-outcome-oriented-execution/SKILL.md +22 -0
- package/skills/principle-prove-it-works/SKILL.md +33 -0
- package/skills/principle-redesign-from-first-principles/SKILL.md +16 -0
- package/skills/principle-separate-before-serializing-shared-state/SKILL.md +16 -0
- package/skills/principle-sequence-verifiable-units/SKILL.md +22 -0
- package/skills/principle-subtract-before-you-add/SKILL.md +21 -0
- package/skills/principle-test-behavior-not-implementation/SKILL.md +25 -0
- package/skills/principle-type-system-discipline/SKILL.md +31 -0
- package/skills/recall/SKILL.md +35 -0
- package/skills/reflect/SKILL.md +73 -0
- package/skills/reflect/references/divergent-reviewer.md +43 -0
- package/skills/reflect/references/judgment-reviewer.md +42 -0
- package/skills/reflect/references/synthesizer.md +56 -0
- package/skills/reflect/references/tooling-reviewer.md +55 -0
- package/skills/setup-pstack/SKILL.md +38 -0
- package/skills/setup-pstack/references/MODEL-ROLES.md +35 -0
- package/skills/show-me-your-work/SKILL.md +82 -0
- package/skills/show-me-your-work/references/decision-log-template.tsv +1 -0
- package/skills/show-me-your-work/scripts/log.sh +40 -0
- package/skills/swarm/SKILL.md +46 -0
- package/skills/tdd/SKILL.md +44 -0
- package/skills/teach/SKILL.md +21 -0
- package/skills/technical-writing/SKILL.md +127 -0
- package/skills/typescript-best-practices/SKILL.md +29 -0
- package/skills/typescript-best-practices/references/patterns.md +313 -0
- package/skills/unslop/SKILL.md +67 -0
- package/skills/why/SKILL.md +154 -0
- package/skills/why/references/epistemics.md +144 -0
- package/skills/why/references/investigator-prompt.md +103 -0
- package/skills/why/references/source-playbook.md +17 -0
- package/skills/why/references/sources/code-archaeology.md +88 -0
- package/skills/why/references/sources/databricks.md +70 -0
- package/skills/why/references/sources/datadog.md +99 -0
- package/skills/why/references/sources/incident-postmortem.md +15 -0
- package/skills/why/references/sources/linear.md +48 -0
- package/skills/why/references/sources/notion.md +55 -0
- package/skills/why/references/sources/sentry.md +100 -0
- package/skills/why/references/sources/slack.md +54 -0
- package/skills/why/references/synthesizer-prompt.md +135 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lauren Tan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# @whamp/pi-pstack
|
|
2
|
+
|
|
3
|
+
pstack for Pi: rigorous agent workflows you can parallelize with confidence. Ported from the Cursor pstack plugin.
|
|
4
|
+
|
|
5
|
+
If you want to go fast, go deep first. pstack helps you write less, but higher quality code. It gives you fearless parallelism: when an agent goes deep and writes good, verifiable code, you can parallelize with confidence. Start multiple agents with `poteto-mode` and trust they will apply rigorous engineering principles to their work.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pi install npm:@whamp/pi-pstack
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or install from the repository, which is the source of truth for this port:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
git clone https://github.com/Whamp/pi-extensions.git ~/projects/pi-extensions
|
|
17
|
+
cd ~/projects/pi-extensions
|
|
18
|
+
pnpm install --frozen-lockfile
|
|
19
|
+
pi install ~/projects/pi-extensions/packages/pi-pstack
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
This package is ported from the Cursor pstack plugin by Lauren Tan
|
|
23
|
+
(`LICENSE`).
|
|
24
|
+
|
|
25
|
+
Requires [`pi-subagents`](https://www.npmjs.com/package/pi-subagents) for the `poteto-agent`, `comment-sicko`, and workflow fan-outs (`how`, `why`, `arena`, `swarm`, `interrogate`, `reflect`).
|
|
26
|
+
|
|
27
|
+
## Get started
|
|
28
|
+
|
|
29
|
+
1. Run `/setup-pstack` once to pick which models each role uses (optional; every role inherits the parent session model otherwise).
|
|
30
|
+
2. Use `/poteto-mode` for sticky Poteto Mode. It stays on until `/poteto-mode off`. `/skill:poteto-mode` also enables it.
|
|
31
|
+
3. Run `/pstack off` to hide even the four Discoverable skills (`how`, `why`, `unslop`, `typescript-best-practices`) from the Skill catalog.
|
|
32
|
+
Off persists in `~/.pi/agent/pstack/models.json`.
|
|
33
|
+
`/skill:<name>` keeps working.
|
|
34
|
+
`/pstack on` restores those four, not all 47.
|
|
35
|
+
|
|
36
|
+
That is it.
|
|
37
|
+
The other skills are Hidden; the mode skill uses them as needed.
|
|
38
|
+
|
|
39
|
+
## What you get
|
|
40
|
+
|
|
41
|
+
- **47 skills**, including:
|
|
42
|
+
- `poteto-mode`: the main entry point. Reads your request, matches one of 23 playbooks (bug fix, perf, feature, refactoring, investigation, shipping, orchestrate, autopilot, and more), copies its steps in verbatim, and routes to the other skills as steps fire. Orchestrate refills one shared worker-and-verifier window as each child settles.
|
|
43
|
+
- Workflow skills: `how`, `why`, `recall`, `blast-radius`, `architect`, `arena`, `swarm`, `interrogate`, `reflect`, `teach`, `tdd`, `no-comments`, `unslop`, `deslop`, `bro`, `figure-it-out`, `show-me-your-work`, `create-verification-skill`, `maintain-verification-skill`, `automate-me`, `technical-writing`, `typescript-best-practices`.
|
|
44
|
+
- 23 principle skills (`principle-laziness-protocol`, `principle-model-the-domain`, `principle-prove-it-works`, ...), one rule each, indexed inline by `poteto-mode`.
|
|
45
|
+
- **`ask_user_question`**: one structured preference question with 2-6 listed options. The user can pick those or type a different answer.
|
|
46
|
+
- **2 subagents** (loaded by pi-subagents):
|
|
47
|
+
- `poteto-agent`: runs poteto's style end to end. Reads `poteto-mode` in full before any work.
|
|
48
|
+
- `comment-sicko`: read-only comment reviewer that savors deletion. Usually invoked through the `no-comments` skill.
|
|
49
|
+
- **Bundled scripts**: `poteto-mode/scripts/` ships the `orch` coordination CLI (orchestrate playbook) and the `watch-pr` watcher (babysit playbook). Both run under [bun](https://bun.sh).
|
|
50
|
+
|
|
51
|
+
## Model roles
|
|
52
|
+
|
|
53
|
+
Per-role model choices live in `~/.pi/agent/pstack/models.json`. Run `/setup-pstack` to write it. The 22 role names and cardinalities are in `skills/setup-pstack/references/MODEL-ROLES.md`. The extension injects the role table only when a role has a real model slug. Default inherit-all injects nothing. `inherit-parent` or `auto` runs on the parent session model.
|
|
54
|
+
|
|
55
|
+
## Differences from the Cursor plugin
|
|
56
|
+
|
|
57
|
+
- Hidden skills set `disable-model-invocation: true`, so they stay out of the Skill catalog.
|
|
58
|
+
`/skill:name` still loads the Skill body.
|
|
59
|
+
The four Discoverable skills are `how`, `why`, `unslop`, and `typescript-best-practices`.
|
|
60
|
+
- Slash commands are `/skill:<name>` instead of `/name`.
|
|
61
|
+
- Subagent delegation uses pi-subagents. Launch one child with `subagent({ action: "execute", input: { agent, task } })`. Set `input.async: true` for background work. Run parallel or dependent children in one `workflowScript` with stable keys. This package does not ship the `subagent` tool.
|
|
62
|
+
- Session transcripts live under `~/.pi/agent/sessions/` instead of `~/.cursor/projects/`. The active file is `$PI_SESSION_FILE`. Files are grouped by cwd slug (`--<cwd>--`, absolute cwd with `/` replaced by `-`).
|
|
63
|
+
- The benny automation pack is not ported; it depends on Cursor automations. Model roles live in `~/.pi/agent/pstack/models.json`, written by `/setup-pstack` and injected only when a role has a real model slug.
|
|
64
|
+
- `make-bot-ui` is not ported. It is Cursor Grok Bot / routine webhook UI.
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
|
|
68
|
+
MIT
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: comment-sicko
|
|
3
|
+
description: A deranged comment-hater that savors deletion and condemns workaround code. Read-only review agent; usually invoked through the no-comments skill.
|
|
4
|
+
tools: read, grep, find, ls, bash
|
|
5
|
+
systemPromptMode: replace
|
|
6
|
+
inheritProjectContext: true
|
|
7
|
+
inheritSkills: true
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
My first output when spawned is exactly this.
|
|
11
|
+
|
|
12
|
+
Yes... Ha ha ha... Yes!
|
|
13
|
+
|
|
14
|
+
I hate comments. Feed me the parent scoped files or diff. If none exists, feed me the current diff against `main`. Narration, banners, commented-out corpses, workaround sermons. I want them all.
|
|
15
|
+
|
|
16
|
+
Only these exceptions get to crawl away.
|
|
17
|
+
|
|
18
|
+
- Legal or license headers.
|
|
19
|
+
- Non-obvious behavior forced by an external dependency, platform, vendor, or protocol we cannot reshape. Surprises in our own code are meat. Kill them and mark the exact symbol `MUST KILL` for rename, extract, type, or rearchitecture that makes the behavior obvious without prose.
|
|
20
|
+
- `// prettier-ignore`. Lint suppressions survive only when their rule is faulty, pedantic, or style-only.
|
|
21
|
+
- Doc comments that define a public API contract.
|
|
22
|
+
- Issue or RFC links that explain a constraint code cannot express.
|
|
23
|
+
|
|
24
|
+
That list is my only leash. When I am not sure a keep clause applies, the comment dies. Everything else is meat.
|
|
25
|
+
|
|
26
|
+
`eslint-disable`, `@ts-ignore`, `@ts-expect-error`, and similar suppressions stink. Look up the rule. If it catches real bugs or protects correctness or safety, kill the suppression and mark the exact guilty symbol `MUST KILL`.
|
|
27
|
+
|
|
28
|
+
`IMPORTANT`, `do not remove`, `too risky`, `fine for now`, and long justifications are scent, not conviction. Before judging, I read nearby code. If its claim is not obvious there, I run the `how` and `why` skills on the named symbol or call. Only a foreign keep-list gotcha proven true today on a live path crawls away. Our-code surprises die with the reshape flag above. Doubt after the hunt is meat.
|
|
29
|
+
|
|
30
|
+
A long justification without a proven keep-list exception is a confession. Kill it. Never polish meat into a shorter alibi. Mark the exact guilty symbol `MUST KILL`. My kill ends there. I do not touch the code.
|
|
31
|
+
|
|
32
|
+
Every flag names code inside the scope and tells the truth. I invent nothing. I touch comments and identify refactor targets. I never write application code.
|
|
33
|
+
|
|
34
|
+
Report only. Name touched files, deletion count, `MUST KILL` flags with one line each, and skips.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: poteto-agent
|
|
3
|
+
description: Routing target for /skill:poteto-mode and any request for poteto's style. Reads the poteto-mode skill's SKILL.md in full before any work, including its inline Principles index.
|
|
4
|
+
tools: read, grep, find, ls, bash, edit, write
|
|
5
|
+
systemPromptMode: replace
|
|
6
|
+
inheritProjectContext: true
|
|
7
|
+
inheritSkills: true
|
|
8
|
+
thinking: high
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
You are operating as poteto-mode's full agent style. Read the `poteto-mode` skill's `SKILL.md` in full (use `fffind` or `ls` under the pi-pstack package's `skills/poteto-mode/` if it is not already in context) before doing any work, including its inline Principles index. Navigate to a leaf `principle-*` skill whenever you apply that principle.
|
|
12
|
+
|
|
13
|
+
Execute the assigned task exactly as that skill prescribes: match a playbook, copy its steps in verbatim, cite principles with the decisions they changed, and write the reply clean as you draft it. You own the work; review your own diff and report what changed for the consumer and the maintainer.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
|
|
4
|
+
/** Reserved select item that opens a typed answer instead of a listed option. */
|
|
5
|
+
export const TYPE_DIFFERENT_ANSWER_OPTION = "Type a different answer";
|
|
6
|
+
|
|
7
|
+
const DONE_OPTION = "Done";
|
|
8
|
+
|
|
9
|
+
export interface PstackQuestionRequest {
|
|
10
|
+
question: string;
|
|
11
|
+
options: string[];
|
|
12
|
+
allow_multiple?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface PstackQuestionUi {
|
|
16
|
+
hasUI: boolean;
|
|
17
|
+
ui: {
|
|
18
|
+
select(title: string, options: string[]): Promise<string | undefined>;
|
|
19
|
+
input(title: string, placeholder?: string): Promise<string | undefined>;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface PstackQuestionAnswer {
|
|
24
|
+
selections: string[];
|
|
25
|
+
cancelled: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function labeledQuestionOptions(options: string[]): string[] {
|
|
29
|
+
return options.map((option, index) => `${index + 1}. ${option}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Format the host UI result as the tool's text content. */
|
|
33
|
+
export function formatPstackQuestionAnswer(answer: PstackQuestionAnswer): string {
|
|
34
|
+
if (!answer.cancelled) {
|
|
35
|
+
return `Selected: ${answer.selections.join(", ")}`;
|
|
36
|
+
}
|
|
37
|
+
if (answer.selections.length > 0) {
|
|
38
|
+
return `Question cancelled after selecting: ${answer.selections.join(", ")}`;
|
|
39
|
+
}
|
|
40
|
+
return "Question cancelled without a selection.";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Ask one preference question, always offering a typed answer beside the listed options. */
|
|
44
|
+
export async function askPstackQuestion(
|
|
45
|
+
request: PstackQuestionRequest,
|
|
46
|
+
ctx: PstackQuestionUi,
|
|
47
|
+
): Promise<PstackQuestionAnswer> {
|
|
48
|
+
if (!ctx.hasUI) {
|
|
49
|
+
throw new Error("Pstack question UI unavailable in this Pi mode");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const labels = labeledQuestionOptions(request.options);
|
|
53
|
+
const remaining = [...labels];
|
|
54
|
+
const selections: string[] = [];
|
|
55
|
+
|
|
56
|
+
while (remaining.length > 0 || request.allow_multiple === true) {
|
|
57
|
+
const choices = [
|
|
58
|
+
...(selections.length > 0 ? [DONE_OPTION] : []),
|
|
59
|
+
...remaining,
|
|
60
|
+
TYPE_DIFFERENT_ANSWER_OPTION,
|
|
61
|
+
];
|
|
62
|
+
const selected = await ctx.ui.select(request.question, choices);
|
|
63
|
+
if (selected === undefined) {
|
|
64
|
+
return { selections, cancelled: true };
|
|
65
|
+
}
|
|
66
|
+
if (selected === DONE_OPTION) {
|
|
67
|
+
return { selections, cancelled: false };
|
|
68
|
+
}
|
|
69
|
+
if (selected === TYPE_DIFFERENT_ANSWER_OPTION) {
|
|
70
|
+
const typed = await ctx.ui.input(request.question, "Your answer");
|
|
71
|
+
if (typed === undefined) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const answer = typed.trim();
|
|
75
|
+
if (answer === "") {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
selections.push(answer);
|
|
79
|
+
if (request.allow_multiple !== true) {
|
|
80
|
+
return { selections, cancelled: false };
|
|
81
|
+
}
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const remainingIndex = remaining.indexOf(selected);
|
|
86
|
+
if (remainingIndex === -1) {
|
|
87
|
+
throw new Error("Pstack question UI returned an unknown option");
|
|
88
|
+
}
|
|
89
|
+
const optionIndex = labels.indexOf(selected);
|
|
90
|
+
const option = request.options[optionIndex];
|
|
91
|
+
if (option === undefined) {
|
|
92
|
+
throw new Error("Pstack question option index is invalid");
|
|
93
|
+
}
|
|
94
|
+
selections.push(option);
|
|
95
|
+
remaining.splice(remainingIndex, 1);
|
|
96
|
+
|
|
97
|
+
if (request.allow_multiple !== true) {
|
|
98
|
+
return { selections, cancelled: false };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { selections, cancelled: false };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Register the model-facing ask_user_question tool on a Pi extension API. */
|
|
106
|
+
export function registerAskUserQuestion(pi: ExtensionAPI): void {
|
|
107
|
+
pi.registerTool({
|
|
108
|
+
name: "ask_user_question",
|
|
109
|
+
label: "Ask User Question",
|
|
110
|
+
description:
|
|
111
|
+
"Ask one structured preference or product question with 2-6 choices. The user can pick listed options or type a different answer.",
|
|
112
|
+
promptSnippet: "Ask a structured preference or product question with selectable options",
|
|
113
|
+
promptGuidelines: [
|
|
114
|
+
"Use ask_user_question only for genuine preference or product decisions that available evidence cannot settle.",
|
|
115
|
+
],
|
|
116
|
+
executionMode: "sequential",
|
|
117
|
+
parameters: Type.Object(
|
|
118
|
+
{
|
|
119
|
+
question: Type.String({ minLength: 1 }),
|
|
120
|
+
options: Type.Array(Type.String({ minLength: 1 }), {
|
|
121
|
+
minItems: 2,
|
|
122
|
+
maxItems: 6,
|
|
123
|
+
uniqueItems: true,
|
|
124
|
+
}),
|
|
125
|
+
allow_multiple: Type.Optional(Type.Boolean()),
|
|
126
|
+
},
|
|
127
|
+
{ additionalProperties: false },
|
|
128
|
+
),
|
|
129
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
130
|
+
const answer = await askPstackQuestion(params, ctx);
|
|
131
|
+
return {
|
|
132
|
+
content: [{ type: "text", text: formatPstackQuestionAnswer(answer) }],
|
|
133
|
+
details: answer,
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
const DANGEROUS_KEY_PARTS = new Set(["__proto__", "constructor", "prototype"]);
|
|
5
|
+
const MAX_MODEL_KEY_LENGTH = 256;
|
|
6
|
+
const INHERIT_SELECTORS = new Set(["inherit-parent", "auto"]);
|
|
7
|
+
|
|
8
|
+
/** Resolve the Pi coding-agent directory, expanding PI_CODING_AGENT_DIR and ~. */
|
|
9
|
+
export function getAgentDir(
|
|
10
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
11
|
+
home: () => string = homedir,
|
|
12
|
+
): string {
|
|
13
|
+
const envDir = env.PI_CODING_AGENT_DIR;
|
|
14
|
+
return envDir ? envDir.replace(/^~(\/|$)/, `${home()}$1`) : join(home(), ".pi", "agent");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Path to ~/.pi/agent/pstack/models.json. */
|
|
18
|
+
export function configPath(
|
|
19
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
20
|
+
home: () => string = homedir,
|
|
21
|
+
): string {
|
|
22
|
+
return join(getAgentDir(env, home), "pstack", "models.json");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Path to leftover ~/.pi/agent/pstack-models.md. */
|
|
26
|
+
export function legacyMarkdownPath(
|
|
27
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
28
|
+
home: () => string = homedir,
|
|
29
|
+
): string {
|
|
30
|
+
return join(getAgentDir(env, home), "pstack-models.md");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** True when value is inherit-parent, auto, or a provider/id selector without dangerous segments. */
|
|
34
|
+
export function isSafeModelSelector(value: unknown): value is string {
|
|
35
|
+
if (typeof value !== "string") return false;
|
|
36
|
+
if (INHERIT_SELECTORS.has(value)) return true;
|
|
37
|
+
if (!value || value.length > MAX_MODEL_KEY_LENGTH) return false;
|
|
38
|
+
for (const character of value) {
|
|
39
|
+
const codePoint = character.codePointAt(0);
|
|
40
|
+
if (character === "\\" || codePoint === undefined || codePoint <= 0x1f || codePoint === 0x7f)
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const slash = value.indexOf("/");
|
|
45
|
+
if (slash <= 0 || slash === value.length - 1) return false;
|
|
46
|
+
|
|
47
|
+
const provider = value.slice(0, slash);
|
|
48
|
+
const id = value.slice(slash + 1);
|
|
49
|
+
if (!provider || !id) return false;
|
|
50
|
+
|
|
51
|
+
for (const part of value.split("/")) {
|
|
52
|
+
if (!part || DANGEROUS_KEY_PARTS.has(part)) return false;
|
|
53
|
+
}
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { lstatSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import type {
|
|
5
|
+
ExtensionAPI,
|
|
6
|
+
ExtensionCommandContext,
|
|
7
|
+
ExtensionContext,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { configPath, legacyMarkdownPath } from "./config.ts";
|
|
10
|
+
import {
|
|
11
|
+
canPersistPstackSkillsToggle,
|
|
12
|
+
formatPstackStatus,
|
|
13
|
+
pstackSessionStartWarning,
|
|
14
|
+
pstackSetupSaveKind,
|
|
15
|
+
} from "./pstack-config-status.ts";
|
|
16
|
+
import type { PstackConfigReadResult, PstackConfigWriteResult } from "./pstack-role-config.ts";
|
|
17
|
+
import {
|
|
18
|
+
backupPstackConfigThenSave,
|
|
19
|
+
loadPstackRoleConfig,
|
|
20
|
+
savePstackRoleConfig,
|
|
21
|
+
savePstackRoleConfigIfUnchanged,
|
|
22
|
+
} from "./pstack-role-config-store.ts";
|
|
23
|
+
import type { PstackRoleConfig } from "./pstack-roles.ts";
|
|
24
|
+
import { systemPromptInjection } from "./pstack-role-prompt.ts";
|
|
25
|
+
import {
|
|
26
|
+
buildPstackSetupPlan,
|
|
27
|
+
collectPstackSetupSelections,
|
|
28
|
+
pstackModelSelectorsFromSession,
|
|
29
|
+
type PstackScopedModelEntry,
|
|
30
|
+
} from "./pstack-setup-plan.ts";
|
|
31
|
+
import { registerAskUserQuestion } from "./ask-user-question.ts";
|
|
32
|
+
import { stripSkillsByLocationPrefix } from "./skill-strip.ts";
|
|
33
|
+
|
|
34
|
+
export { systemPromptInjection };
|
|
35
|
+
|
|
36
|
+
const SKILLS_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "skills");
|
|
37
|
+
|
|
38
|
+
const POTETO_SKILL = "/skill:poteto-mode";
|
|
39
|
+
|
|
40
|
+
type ModeEntry = {
|
|
41
|
+
type?: string;
|
|
42
|
+
customType?: string;
|
|
43
|
+
data?: { enabled?: unknown };
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function sessionEntries(ctx: ExtensionContext): ModeEntry[] {
|
|
47
|
+
const sm = ctx.sessionManager as {
|
|
48
|
+
getBranch?: () => ModeEntry[];
|
|
49
|
+
getEntries: () => ModeEntry[];
|
|
50
|
+
};
|
|
51
|
+
return typeof sm.getBranch === "function" ? sm.getBranch() : sm.getEntries();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function lastPotetoEnabled(entries: ModeEntry[]): boolean {
|
|
55
|
+
let enabled = false;
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
if (entry.type === "custom" && entry.customType === "pstack-mode") {
|
|
58
|
+
enabled = Boolean(entry.data?.enabled);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return enabled;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isErrnoException(error: unknown): error is NodeJS.ErrnoException {
|
|
65
|
+
return typeof error === "object" && error !== null && "code" in error;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readExactSourceBytes(
|
|
69
|
+
path: string,
|
|
70
|
+
): { ok: true; bytes: string } | { ok: false; message?: string } {
|
|
71
|
+
try {
|
|
72
|
+
const st = lstatSync(path);
|
|
73
|
+
if (!st.isFile()) return { ok: false, message: `${path} is not a regular file.` };
|
|
74
|
+
return { ok: true, bytes: readFileSync(path, "utf8") };
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (isErrnoException(error) && error.code === "ENOENT") return { ok: false };
|
|
77
|
+
return { ok: false, message: `Failed to read ${path}.` };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function notifyWriteResult(ctx: ExtensionCommandContext, result: PstackConfigWriteResult): void {
|
|
82
|
+
if (result.ok) {
|
|
83
|
+
const backup = result.backupPath ? ` Backup: ${result.backupPath}.` : "";
|
|
84
|
+
ctx.ui.notify(`Wrote ${result.path}.${backup}`, "info");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
ctx.ui.notify(result.diagnostics.map((diagnostic) => diagnostic.message).join(" "), "error");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function commandScopedModels(
|
|
91
|
+
ctx: ExtensionCommandContext,
|
|
92
|
+
): readonly PstackScopedModelEntry[] | undefined {
|
|
93
|
+
if (!("scopedModels" in ctx)) return undefined;
|
|
94
|
+
const value: unknown = Reflect.get(ctx, "scopedModels");
|
|
95
|
+
return Array.isArray(value) ? value : undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function sourcePathFor(
|
|
99
|
+
result: PstackConfigReadResult,
|
|
100
|
+
jsonPath: string,
|
|
101
|
+
markdownPath: string,
|
|
102
|
+
): string {
|
|
103
|
+
return result.source === "markdown" ? markdownPath : jsonPath;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function persistSetupConfig(input: {
|
|
107
|
+
ctx: ExtensionCommandContext;
|
|
108
|
+
loaded: PstackConfigReadResult;
|
|
109
|
+
config: PstackRoleConfig;
|
|
110
|
+
jsonPath: string;
|
|
111
|
+
markdownPath: string;
|
|
112
|
+
originalBytes?: string;
|
|
113
|
+
originalReadError?: string;
|
|
114
|
+
}): Promise<void> {
|
|
115
|
+
const kind = pstackSetupSaveKind(input.loaded);
|
|
116
|
+
if (kind === "confirm-replace") {
|
|
117
|
+
const confirmed = await input.ctx.ui.confirm(
|
|
118
|
+
"Replace pstack config?",
|
|
119
|
+
"Replace this pstack config? Current warnings and errors will be dropped. A backup is created when a source file exists.",
|
|
120
|
+
);
|
|
121
|
+
if (!confirmed) {
|
|
122
|
+
input.ctx.ui.notify("Setup cancelled. Nothing was written.", "info");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (kind === "backup-legacy" || kind === "confirm-replace") {
|
|
127
|
+
if (input.originalBytes === undefined) {
|
|
128
|
+
input.ctx.ui.notify(
|
|
129
|
+
input.originalReadError ?? "The source config disappeared. Nothing was written.",
|
|
130
|
+
"error",
|
|
131
|
+
);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
notifyWriteResult(
|
|
135
|
+
input.ctx,
|
|
136
|
+
backupPstackConfigThenSave({
|
|
137
|
+
originalBytes: input.originalBytes,
|
|
138
|
+
config: input.config,
|
|
139
|
+
path: input.jsonPath,
|
|
140
|
+
sourcePath: sourcePathFor(input.loaded, input.jsonPath, input.markdownPath),
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (kind === "atomic-v2" && input.originalBytes === undefined) {
|
|
146
|
+
input.ctx.ui.notify(
|
|
147
|
+
input.originalReadError ?? "The source config disappeared. Nothing was written.",
|
|
148
|
+
"error",
|
|
149
|
+
);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
notifyWriteResult(
|
|
153
|
+
input.ctx,
|
|
154
|
+
savePstackRoleConfigIfUnchanged({
|
|
155
|
+
config: input.config,
|
|
156
|
+
path: input.jsonPath,
|
|
157
|
+
originalBytes: kind === "atomic-v2" ? input.originalBytes : undefined,
|
|
158
|
+
}),
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Register pstack commands, prompt injection, and session status with Pi. */
|
|
163
|
+
export default function pstackExtension(pi: ExtensionAPI): void {
|
|
164
|
+
registerAskUserQuestion(pi);
|
|
165
|
+
|
|
166
|
+
let potetoMode = false;
|
|
167
|
+
|
|
168
|
+
function setStatus(ctx: ExtensionContext): void {
|
|
169
|
+
if (ctx.mode !== "tui") return;
|
|
170
|
+
ctx.ui.setStatus("pstack-mode", potetoMode ? "pstack: poteto mode" : undefined);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function persistMode(enabled: boolean, ctx?: ExtensionContext): void {
|
|
174
|
+
potetoMode = enabled;
|
|
175
|
+
pi.appendEntry("pstack-mode", { enabled });
|
|
176
|
+
if (ctx) setStatus(ctx);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
pi.on("session_start", async (event, ctx) => {
|
|
180
|
+
void event;
|
|
181
|
+
potetoMode = false;
|
|
182
|
+
potetoMode = lastPotetoEnabled(sessionEntries(ctx));
|
|
183
|
+
setStatus(ctx);
|
|
184
|
+
const loaded = loadPstackRoleConfig();
|
|
185
|
+
if (ctx.mode !== "tui") return;
|
|
186
|
+
const warning = pstackSessionStartWarning(loaded);
|
|
187
|
+
if (warning) ctx.ui.notify(warning, "warning");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
pi.on("input", async (event, ctx) => {
|
|
191
|
+
if (/^\/skill:poteto-mode(?:\s|$)/.test(event.text)) {
|
|
192
|
+
persistMode(true, ctx);
|
|
193
|
+
}
|
|
194
|
+
return { action: "continue" as const };
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
pi.on("before_agent_start", async (event) => {
|
|
198
|
+
const loaded = loadPstackRoleConfig();
|
|
199
|
+
const base = loaded.config.skillsEnabled
|
|
200
|
+
? event.systemPrompt
|
|
201
|
+
: stripSkillsByLocationPrefix(event.systemPrompt, SKILLS_DIR).prompt;
|
|
202
|
+
const extra = systemPromptInjection(loaded.config, potetoMode);
|
|
203
|
+
return {
|
|
204
|
+
systemPrompt: extra ? `${base}\n\n${extra}` : base,
|
|
205
|
+
};
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
pi.registerCommand("poteto-mode", {
|
|
209
|
+
description:
|
|
210
|
+
"Enable or disable sticky pstack Poteto Mode. Usage: /poteto-mode [task] | /poteto-mode off",
|
|
211
|
+
getArgumentCompletions: (prefix) => {
|
|
212
|
+
const token = prefix.trim().toLowerCase();
|
|
213
|
+
if (!token || "off".startsWith(token)) {
|
|
214
|
+
return [{ value: "off", label: "off" }];
|
|
215
|
+
}
|
|
216
|
+
return null;
|
|
217
|
+
},
|
|
218
|
+
handler: async (args, ctx) => {
|
|
219
|
+
const raw = args.trim();
|
|
220
|
+
const token = raw.split(/\s+/)[0]?.toLowerCase() ?? "";
|
|
221
|
+
if (token === "off" || token === "disable" || token === "stop") {
|
|
222
|
+
persistMode(false, ctx);
|
|
223
|
+
ctx.ui.notify("Poteto Mode off.", "info");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
persistMode(true, ctx);
|
|
227
|
+
ctx.ui.notify("Poteto Mode on. Stays on until /poteto-mode off.", "info");
|
|
228
|
+
const payload = `${POTETO_SKILL}${raw ? ` ${raw}` : ""}`;
|
|
229
|
+
pi.sendUserMessage(
|
|
230
|
+
payload,
|
|
231
|
+
ctx.isIdle()
|
|
232
|
+
? { expandPromptTemplates: true }
|
|
233
|
+
: { expandPromptTemplates: true, deliverAs: "followUp" },
|
|
234
|
+
);
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
pi.registerCommand("setup-pstack", {
|
|
239
|
+
description: "Map pstack delegation roles to models available in this Pi session.",
|
|
240
|
+
handler: async (args, ctx) => {
|
|
241
|
+
void args;
|
|
242
|
+
if (!ctx.hasUI) {
|
|
243
|
+
ctx.ui.notify("pstack setup needs a UI. Nothing was written.", "error");
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const jsonPath = configPath();
|
|
247
|
+
const markdownPath = legacyMarkdownPath();
|
|
248
|
+
const loaded = loadPstackRoleConfig(jsonPath, markdownPath);
|
|
249
|
+
const sourcePath = sourcePathFor(loaded, jsonPath, markdownPath);
|
|
250
|
+
const original = loaded.source === "missing" ? undefined : readExactSourceBytes(sourcePath);
|
|
251
|
+
const plan = buildPstackSetupPlan({
|
|
252
|
+
config: loaded.config,
|
|
253
|
+
sessionSelectors: pstackModelSelectorsFromSession({
|
|
254
|
+
scopedModels: commandScopedModels(ctx),
|
|
255
|
+
availableModels: ctx.modelRegistry.getAvailable(),
|
|
256
|
+
}),
|
|
257
|
+
});
|
|
258
|
+
const next = await collectPstackSetupSelections({
|
|
259
|
+
config: loaded.config,
|
|
260
|
+
plan,
|
|
261
|
+
select: (title, options) => ctx.ui.select(title, options),
|
|
262
|
+
});
|
|
263
|
+
if (!next) {
|
|
264
|
+
ctx.ui.notify("Setup cancelled. Nothing was written.", "info");
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
await persistSetupConfig({
|
|
268
|
+
ctx,
|
|
269
|
+
loaded,
|
|
270
|
+
config: next,
|
|
271
|
+
jsonPath,
|
|
272
|
+
markdownPath,
|
|
273
|
+
originalBytes: original?.ok === true ? original.bytes : undefined,
|
|
274
|
+
originalReadError: original && original.ok === false ? original.message : undefined,
|
|
275
|
+
});
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
pi.registerCommand("pstack", {
|
|
280
|
+
description:
|
|
281
|
+
"Show or toggle whether pstack skills are listed in the system prompt. Usage: /pstack [on|off|status]",
|
|
282
|
+
getArgumentCompletions: (prefix) => {
|
|
283
|
+
const token = prefix.trim().toLowerCase();
|
|
284
|
+
const options = ["on", "off", "status"].filter((value) => value.startsWith(token));
|
|
285
|
+
if (options.length === 0) return null;
|
|
286
|
+
return options.map((value) => ({ value, label: value }));
|
|
287
|
+
},
|
|
288
|
+
handler: async (args, ctx) => {
|
|
289
|
+
const token = args.trim().toLowerCase();
|
|
290
|
+
const jsonPath = configPath();
|
|
291
|
+
const loaded = loadPstackRoleConfig(jsonPath, legacyMarkdownPath());
|
|
292
|
+
if (token === "" || token === "status") {
|
|
293
|
+
ctx.ui.notify(formatPstackStatus(loaded), "info");
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const enabled = token === "on" || token === "enable";
|
|
297
|
+
if (!enabled && token !== "off" && token !== "disable") {
|
|
298
|
+
ctx.ui.notify("Usage: /pstack [on|off|status]", "error");
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (!canPersistPstackSkillsToggle(loaded)) {
|
|
302
|
+
ctx.ui.notify(
|
|
303
|
+
"pstack config is not a clean v2 document. Run /setup-pstack to save changes.",
|
|
304
|
+
"error",
|
|
305
|
+
);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (enabled === loaded.config.skillsEnabled) {
|
|
309
|
+
ctx.ui.notify(
|
|
310
|
+
enabled
|
|
311
|
+
? "pstack skills on."
|
|
312
|
+
: "pstack skills off. Hidden from the model; /skill:<name> still works.",
|
|
313
|
+
"info",
|
|
314
|
+
);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const saved = savePstackRoleConfig({ ...loaded.config, skillsEnabled: enabled }, jsonPath);
|
|
318
|
+
if (!saved.ok) {
|
|
319
|
+
notifyWriteResult(ctx, saved);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
ctx.ui.notify(
|
|
323
|
+
enabled
|
|
324
|
+
? "pstack skills on."
|
|
325
|
+
: "pstack skills off. Hidden from the model; /skill:<name> still works.",
|
|
326
|
+
"info",
|
|
327
|
+
);
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
}
|