@zenspc/pi-pstack 0.1.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.
Files changed (128) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +49 -0
  3. package/agents/comment-sicko.md +34 -0
  4. package/agents/poteto-agent.md +13 -0
  5. package/extensions/pstack/config.ts +228 -0
  6. package/extensions/pstack/index.ts +211 -0
  7. package/package.json +57 -0
  8. package/skills/architect/SKILL.md +82 -0
  9. package/skills/architect/references/design-red-flags.md +33 -0
  10. package/skills/architect/references/rationale-template.md +35 -0
  11. package/skills/architect/references/runner-prompt.md +20 -0
  12. package/skills/arena/SKILL.md +70 -0
  13. package/skills/automate-me/SKILL.md +114 -0
  14. package/skills/blast-radius/SKILL.md +49 -0
  15. package/skills/bro/SKILL.md +6 -0
  16. package/skills/create-verification-skill/SKILL.md +43 -0
  17. package/skills/create-verification-skill/references/feature-map-example/README.md +47 -0
  18. package/skills/create-verification-skill/references/feature-map-example/create-note.md +39 -0
  19. package/skills/create-verification-skill/references/feature-map-example/search.md +45 -0
  20. package/skills/figure-it-out/SKILL.md +54 -0
  21. package/skills/how/SKILL.md +134 -0
  22. package/skills/how/references/critic-prompt.md +59 -0
  23. package/skills/how/references/critique-rubric.md +58 -0
  24. package/skills/how/references/explainer-prompt.md +55 -0
  25. package/skills/how/references/explorer-prompt.md +52 -0
  26. package/skills/interrogate/SKILL.md +109 -0
  27. package/skills/interrogate/references/code-quality-review.md +47 -0
  28. package/skills/interrogate/references/lead-judgment.md +58 -0
  29. package/skills/interrogate/references/reviewer-prompt.md +72 -0
  30. package/skills/interrogate/references/rubric.md +77 -0
  31. package/skills/maintain-verification-skill/SKILL.md +38 -0
  32. package/skills/no-comments/SKILL.md +23 -0
  33. package/skills/poteto-mode/SKILL.md +140 -0
  34. package/skills/poteto-mode/playbooks/authoring-a-skill.md +12 -0
  35. package/skills/poteto-mode/playbooks/autonomous-run.md +13 -0
  36. package/skills/poteto-mode/playbooks/autopilot-full.md +13 -0
  37. package/skills/poteto-mode/playbooks/autopilot-stack.md +31 -0
  38. package/skills/poteto-mode/playbooks/babysit.md +27 -0
  39. package/skills/poteto-mode/playbooks/bug-fix.md +17 -0
  40. package/skills/poteto-mode/playbooks/eval.md +27 -0
  41. package/skills/poteto-mode/playbooks/feature.md +21 -0
  42. package/skills/poteto-mode/playbooks/hillclimb.md +21 -0
  43. package/skills/poteto-mode/playbooks/investigation.md +14 -0
  44. package/skills/poteto-mode/playbooks/multi-phase-plan.md +3 -0
  45. package/skills/poteto-mode/playbooks/opening-a-pr.md +35 -0
  46. package/skills/poteto-mode/playbooks/orchestrate.md +113 -0
  47. package/skills/poteto-mode/playbooks/pause-safely.md +10 -0
  48. package/skills/poteto-mode/playbooks/perf-issue.md +24 -0
  49. package/skills/poteto-mode/playbooks/prototype.md +14 -0
  50. package/skills/poteto-mode/playbooks/refactoring.md +16 -0
  51. package/skills/poteto-mode/playbooks/runtime-forensics.md +11 -0
  52. package/skills/poteto-mode/playbooks/session-pickup.md +13 -0
  53. package/skills/poteto-mode/playbooks/shipping.md +40 -0
  54. package/skills/poteto-mode/playbooks/trace-forensics.md +14 -0
  55. package/skills/poteto-mode/playbooks/visual-parity.md +11 -0
  56. package/skills/poteto-mode/playbooks/worktree-cleanup.md +14 -0
  57. package/skills/poteto-mode/references/bugbot-triage.md +142 -0
  58. package/skills/poteto-mode/references/plan.md +105 -0
  59. package/skills/poteto-mode/scripts/bootstrap.ts +62 -0
  60. package/skills/poteto-mode/scripts/bun.lock +67 -0
  61. package/skills/poteto-mode/scripts/orch/orch.test.ts +634 -0
  62. package/skills/poteto-mode/scripts/orch/orch.ts +578 -0
  63. package/skills/poteto-mode/scripts/orch/store.ts +1607 -0
  64. package/skills/poteto-mode/scripts/package.json +16 -0
  65. package/skills/poteto-mode/scripts/watch-pr/cli.test.ts +224 -0
  66. package/skills/poteto-mode/scripts/watch-pr/cli.ts +223 -0
  67. package/skills/poteto-mode/scripts/watch-pr/fakes.test-helper.ts +118 -0
  68. package/skills/poteto-mode/scripts/watch-pr/github.test.ts +306 -0
  69. package/skills/poteto-mode/scripts/watch-pr/github.ts +699 -0
  70. package/skills/poteto-mode/scripts/watch-pr/policy.test.ts +420 -0
  71. package/skills/poteto-mode/scripts/watch-pr/policy.ts +832 -0
  72. package/skills/poteto-mode/scripts/watch-pr/render.ts +169 -0
  73. package/skills/poteto-mode/scripts/watch-pr/tsconfig.json +13 -0
  74. package/skills/poteto-mode/scripts/watch-pr/types.compile.ts +93 -0
  75. package/skills/poteto-mode/scripts/watch-pr/types.ts +401 -0
  76. package/skills/poteto-mode/scripts/watch-pr/watch-pr +6 -0
  77. package/skills/poteto-mode/scripts/worktree-audit.sh +85 -0
  78. package/skills/principle-boundary-discipline/SKILL.md +33 -0
  79. package/skills/principle-build-the-lever/SKILL.md +22 -0
  80. package/skills/principle-encode-lessons-in-structure/SKILL.md +30 -0
  81. package/skills/principle-exhaust-the-design-space/SKILL.md +20 -0
  82. package/skills/principle-experience-first/SKILL.md +18 -0
  83. package/skills/principle-fix-root-causes/SKILL.md +22 -0
  84. package/skills/principle-foundational-thinking/SKILL.md +20 -0
  85. package/skills/principle-guard-the-context-window/SKILL.md +16 -0
  86. package/skills/principle-laziness-protocol/SKILL.md +17 -0
  87. package/skills/principle-make-operations-idempotent/SKILL.md +23 -0
  88. package/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +21 -0
  89. package/skills/principle-minimize-reader-load/SKILL.md +22 -0
  90. package/skills/principle-model-the-domain/SKILL.md +25 -0
  91. package/skills/principle-never-block-on-the-human/SKILL.md +22 -0
  92. package/skills/principle-outcome-oriented-execution/SKILL.md +21 -0
  93. package/skills/principle-prove-it-works/SKILL.md +32 -0
  94. package/skills/principle-redesign-from-first-principles/SKILL.md +15 -0
  95. package/skills/principle-separate-before-serializing-shared-state/SKILL.md +15 -0
  96. package/skills/principle-sequence-verifiable-units/SKILL.md +21 -0
  97. package/skills/principle-subtract-before-you-add/SKILL.md +21 -0
  98. package/skills/principle-type-system-discipline/SKILL.md +30 -0
  99. package/skills/recall/SKILL.md +40 -0
  100. package/skills/reflect/SKILL.md +77 -0
  101. package/skills/reflect/references/divergent-reviewer.md +43 -0
  102. package/skills/reflect/references/judgment-reviewer.md +42 -0
  103. package/skills/reflect/references/synthesizer.md +56 -0
  104. package/skills/reflect/references/tooling-reviewer.md +57 -0
  105. package/skills/setup-pstack/SKILL.md +28 -0
  106. package/skills/show-me-your-work/SKILL.md +89 -0
  107. package/skills/show-me-your-work/references/decision-log-template.tsv +1 -0
  108. package/skills/show-me-your-work/scripts/log.sh +40 -0
  109. package/skills/swarm/SKILL.md +45 -0
  110. package/skills/tdd/SKILL.md +43 -0
  111. package/skills/teach/SKILL.md +20 -0
  112. package/skills/technical-writing/SKILL.md +129 -0
  113. package/skills/typescript-best-practices/SKILL.md +28 -0
  114. package/skills/typescript-best-practices/references/patterns.md +292 -0
  115. package/skills/unslop/SKILL.md +80 -0
  116. package/skills/why/SKILL.md +229 -0
  117. package/skills/why/references/epistemics.md +144 -0
  118. package/skills/why/references/investigator-prompt.md +103 -0
  119. package/skills/why/references/source-playbook.md +17 -0
  120. package/skills/why/references/sources/code-archaeology.md +88 -0
  121. package/skills/why/references/sources/databricks.md +70 -0
  122. package/skills/why/references/sources/datadog.md +99 -0
  123. package/skills/why/references/sources/incident-postmortem.md +15 -0
  124. package/skills/why/references/sources/linear.md +48 -0
  125. package/skills/why/references/sources/notion.md +55 -0
  126. package/skills/why/references/sources/sentry.md +100 -0
  127. package/skills/why/references/sources/slack.md +54 -0
  128. 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,49 @@
1
+ # @zenspc/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:@zenspc/pi-pstack
11
+ ```
12
+
13
+ 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`).
14
+
15
+ ## Get started
16
+
17
+ Two steps:
18
+
19
+ 1. Run `/setup-pstack` once to pick which models each role uses (optional; every role inherits the parent session model otherwise).
20
+ 2. Use `/poteto-mode` for sticky Poteto Mode. It stays on until `/poteto-mode off`. `/skill:poteto-mode` also enables it.
21
+
22
+ That is it. The other skills are situational; the mode skill uses them as needed.
23
+
24
+ ## What you get
25
+
26
+ - **44 skills**, including:
27
+ - `poteto-mode`: the main entry point. Reads your request, matches one of 22 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.
28
+ - Workflow skills: `how`, `why`, `recall`, `blast-radius`, `architect`, `arena`, `swarm`, `interrogate`, `reflect`, `teach`, `tdd`, `no-comments`, `unslop`, `bro`, `figure-it-out`, `show-me-your-work`, `create-verification-skill`, `maintain-verification-skill`, `automate-me`, `technical-writing`, `typescript-best-practices`.
29
+ - 21 principle skills (`principle-laziness-protocol`, `principle-model-the-domain`, `principle-prove-it-works`, ...), one rule each, indexed inline by `poteto-mode`.
30
+ - **2 subagents** (loaded by pi-subagents):
31
+ - `poteto-agent`: runs poteto's style end to end. Reads `poteto-mode` in full before any work.
32
+ - `comment-sicko`: read-only comment reviewer that savors deletion. Usually invoked through the `no-comments` skill.
33
+ - **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).
34
+
35
+ ## Model roles
36
+
37
+ Per-role model choices live in `~/.pi/agent/pstack/models.json`. Run `/setup-pstack` to write it. The extension injects the role table every turn. `inherit-parent` or `auto` runs on the parent session model.
38
+
39
+ ## Differences from the Cursor plugin
40
+
41
+ - Skills follow the Agent Skills standard directly; no `disable-model-invocation` or Cursor-only frontmatter.
42
+ - Slash commands are `/skill:<name>` instead of `/name`.
43
+ - Subagent delegation uses pi-subagents (`subagent({ agent, task })`) instead of Cursor's Task tool. This package does not ship a replacement `subagent` tool.
44
+ - 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 `-`).
45
+ - 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 every turn by the extension.
46
+
47
+ ## License
48
+
49
+ 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,228 @@
1
+ import {
2
+ existsSync,
3
+ lstatSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ renameSync,
7
+ unlinkSync,
8
+ writeFileSync,
9
+ } from "node:fs";
10
+ import { homedir } from "node:os";
11
+ import { basename, dirname, join } from "node:path";
12
+
13
+ export const ROLE_NAMES = [
14
+ "feature, refactoring",
15
+ "bug-fix",
16
+ "perf-issue",
17
+ "hillclimb",
18
+ "judgment and prose",
19
+ "hardest tasks",
20
+ "how explorer",
21
+ "how explainer",
22
+ "how critics",
23
+ "why investigators",
24
+ "why synthesizer",
25
+ "reflect tooling",
26
+ "reflect judgment, divergent, synthesizer",
27
+ "arena runners",
28
+ "arena cross-judge pool",
29
+ "swarm workers",
30
+ "architect runners",
31
+ "interrogate reviewers",
32
+ ] as const;
33
+
34
+ export type RoleName = (typeof ROLE_NAMES)[number];
35
+ export type RoleValue = string | string[];
36
+ export interface PstackConfig {
37
+ version: 1;
38
+ roles: Record<string, RoleValue>;
39
+ }
40
+
41
+ export const LIST_ROLES: ReadonlySet<RoleName> = new Set([
42
+ "how critics",
43
+ "arena runners",
44
+ "arena cross-judge pool",
45
+ "architect runners",
46
+ "interrogate reviewers",
47
+ ]);
48
+
49
+ const ROLE_NAME_SET: ReadonlySet<string> = new Set(ROLE_NAMES);
50
+ const DANGEROUS_KEY_PARTS = new Set(["__proto__", "constructor", "prototype"]);
51
+ const MAX_CONFIG_BYTES = 100_000;
52
+ const MAX_MODEL_KEY_LENGTH = 256;
53
+ const INHERIT_SELECTORS = new Set(["inherit-parent", "auto"]);
54
+
55
+ export function getAgentDir(env: NodeJS.ProcessEnv = process.env, home: () => string = homedir): string {
56
+ const envDir = env.PI_CODING_AGENT_DIR;
57
+ return envDir
58
+ ? envDir.replace(/^~(\/|$)/, `${home()}$1`)
59
+ : join(home(), ".pi", "agent");
60
+ }
61
+
62
+ export function configPath(env: NodeJS.ProcessEnv = process.env, home: () => string = homedir): string {
63
+ return join(getAgentDir(env, home), "pstack", "models.json");
64
+ }
65
+
66
+ export function legacyMarkdownPath(env: NodeJS.ProcessEnv = process.env, home: () => string = homedir): string {
67
+ return join(getAgentDir(env, home), "pstack-models.md");
68
+ }
69
+
70
+ export function defaultConfig(): PstackConfig {
71
+ const roles: Record<string, RoleValue> = Object.create(null);
72
+ for (const role of ROLE_NAMES) {
73
+ roles[role] = "inherit-parent";
74
+ }
75
+ return { version: 1, roles };
76
+ }
77
+
78
+ export function isSafeModelSelector(value: unknown): value is string {
79
+ if (typeof value !== "string") return false;
80
+ if (INHERIT_SELECTORS.has(value)) return true;
81
+ if (!value || value.length > MAX_MODEL_KEY_LENGTH) return false;
82
+ if (/[\u0000-\u001f\u007f\\]/.test(value)) return false;
83
+
84
+ const slash = value.indexOf("/");
85
+ if (slash <= 0 || slash === value.length - 1) return false;
86
+
87
+ const provider = value.slice(0, slash);
88
+ const id = value.slice(slash + 1);
89
+ if (!provider || !id) return false;
90
+
91
+ for (const part of value.split("/")) {
92
+ if (!part || DANGEROUS_KEY_PARTS.has(part)) return false;
93
+ }
94
+ return true;
95
+ }
96
+
97
+ function parseRoleValue(value: unknown): RoleValue | undefined {
98
+ if (typeof value === "string") {
99
+ return isSafeModelSelector(value) ? value : undefined;
100
+ }
101
+ if (!Array.isArray(value)) return undefined;
102
+ const selectors = value.filter(isSafeModelSelector);
103
+ if (selectors.length === 0) return undefined;
104
+ return selectors;
105
+ }
106
+
107
+ export function parseConfig(raw: unknown): PstackConfig {
108
+ const fallback = defaultConfig();
109
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return fallback;
110
+
111
+ const record = raw as { version?: unknown; roles?: unknown };
112
+ if (record.version !== 1 || !record.roles || typeof record.roles !== "object" || Array.isArray(record.roles)) {
113
+ return fallback;
114
+ }
115
+
116
+ const roles: Record<string, RoleValue> = Object.create(null);
117
+ for (const role of ROLE_NAMES) {
118
+ roles[role] = fallback.roles[role];
119
+ }
120
+ for (const [key, value] of Object.entries(record.roles as Record<string, unknown>)) {
121
+ if (!ROLE_NAME_SET.has(key)) continue;
122
+ const parsed = parseRoleValue(value);
123
+ if (parsed === undefined) continue;
124
+ roles[key] = parsed;
125
+ }
126
+ return { version: 1, roles };
127
+ }
128
+
129
+ export function loadConfig(path: string = configPath()): PstackConfig {
130
+ try {
131
+ if (!existsSync(path)) return defaultConfig();
132
+ const st = lstatSync(path);
133
+ if (!st.isFile()) return defaultConfig();
134
+ if (st.size > MAX_CONFIG_BYTES) return defaultConfig();
135
+ const text = readFileSync(path, "utf8");
136
+ if (text.length > MAX_CONFIG_BYTES) return defaultConfig();
137
+ return parseConfig(JSON.parse(text));
138
+ } catch {
139
+ return defaultConfig();
140
+ }
141
+ }
142
+
143
+ export function saveConfig(config: PstackConfig, path: string = configPath()): boolean {
144
+ const clean = parseConfig(config);
145
+ const body = `${JSON.stringify({ version: 1, roles: clean.roles }, null, 2)}\n`;
146
+ const dir = dirname(path);
147
+ const tmp = join(dir, `.${basename(path)}.${process.pid}.${Date.now()}.tmp`);
148
+ try {
149
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
150
+ if (existsSync(path)) {
151
+ const st = lstatSync(path);
152
+ if (!st.isFile()) return false;
153
+ }
154
+ writeFileSync(tmp, body, { encoding: "utf8", mode: 0o600 });
155
+ renameSync(tmp, path);
156
+ return true;
157
+ } catch {
158
+ try {
159
+ if (existsSync(tmp)) unlinkSync(tmp);
160
+ } catch {
161
+ // best-effort temp cleanup
162
+ }
163
+ return false;
164
+ }
165
+ }
166
+
167
+ export function parseLegacyMarkdown(text: string): PstackConfig {
168
+ const roles: Record<string, RoleValue> = Object.create(null);
169
+ for (const line of text.split(/\r?\n/)) {
170
+ const trimmed = line.trim();
171
+ if (!trimmed || trimmed.startsWith("#")) continue;
172
+ const colon = trimmed.indexOf(":");
173
+ if (colon <= 0) continue;
174
+ const name = trimmed.slice(0, colon).trim();
175
+ if (!ROLE_NAME_SET.has(name)) continue;
176
+ const parts = trimmed
177
+ .slice(colon + 1)
178
+ .split(",")
179
+ .map((part) => part.trim())
180
+ .filter(Boolean);
181
+ const parsed = parseRoleValue(parts.length <= 1 ? (parts[0] ?? "") : parts);
182
+ if (parsed === undefined) continue;
183
+ roles[name] = parsed;
184
+ }
185
+ return parseConfig({ version: 1, roles });
186
+ }
187
+
188
+ function isRegularFile(path: string): boolean {
189
+ try {
190
+ return existsSync(path) && lstatSync(path).isFile();
191
+ } catch {
192
+ return false;
193
+ }
194
+ }
195
+
196
+ export function migrateLegacyMarkdownIfNeeded(
197
+ jsonPath: string = configPath(),
198
+ markdownPath: string = legacyMarkdownPath(),
199
+ ): PstackConfig | undefined {
200
+ if (existsSync(jsonPath)) return undefined;
201
+ if (!isRegularFile(markdownPath)) return undefined;
202
+ try {
203
+ const migrated = parseLegacyMarkdown(readFileSync(markdownPath, "utf8"));
204
+ if (!saveConfig(migrated, jsonPath)) return undefined;
205
+ return migrated;
206
+ } catch {
207
+ return undefined;
208
+ }
209
+ }
210
+
211
+ export function modelsForRole(config: PstackConfig, role: string): string[] {
212
+ const value = config.roles[role];
213
+ const list = Array.isArray(value) ? value : value ? [value] : [];
214
+ return list.filter((selector) => !INHERIT_SELECTORS.has(selector));
215
+ }
216
+
217
+ export function formatRoleTable(config: PstackConfig): string {
218
+ const lines: string[] = [];
219
+ for (const role of ROLE_NAMES) {
220
+ const models = modelsForRole(config, role);
221
+ if (models.length === 0) continue;
222
+ lines.push(`${role}: ${models.join(", ")}`);
223
+ }
224
+ if (lines.length === 0) {
225
+ return "Pstack model roles: all inherit the parent session model.";
226
+ }
227
+ return lines.join("\n");
228
+ }
@@ -0,0 +1,211 @@
1
+ import { existsSync } from "node:fs";
2
+ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import {
4
+ LIST_ROLES,
5
+ ROLE_NAMES,
6
+ type RoleName,
7
+ type RoleValue,
8
+ configPath,
9
+ defaultConfig,
10
+ formatRoleTable,
11
+ isSafeModelSelector,
12
+ loadConfig,
13
+ migrateLegacyMarkdownIfNeeded,
14
+ saveConfig,
15
+ } from "./config.ts";
16
+
17
+ const POTETO_SKILL = "/skill:poteto-mode";
18
+ const POTETO_PROMPT =
19
+ "Pstack Poteto Mode is on for this session. Follow skills/poteto-mode/SKILL.md: match a playbook, copy its steps, delegate through pi-subagents, verify real behavior, name only principles that changed a decision. /poteto-mode off disables it.";
20
+
21
+ type ModeEntry = {
22
+ type?: string;
23
+ customType?: string;
24
+ data?: { enabled?: unknown };
25
+ };
26
+
27
+ function sessionEntries(ctx: ExtensionContext): ModeEntry[] {
28
+ const sm = ctx.sessionManager as {
29
+ getBranch?: () => ModeEntry[];
30
+ getEntries: () => ModeEntry[];
31
+ };
32
+ return typeof sm.getBranch === "function" ? sm.getBranch() : sm.getEntries();
33
+ }
34
+
35
+ function lastPotetoEnabled(entries: ModeEntry[]): boolean {
36
+ let enabled = false;
37
+ for (const entry of entries) {
38
+ if (entry.type === "custom" && entry.customType === "pstack-mode") {
39
+ enabled = Boolean(entry.data?.enabled);
40
+ }
41
+ }
42
+ return enabled;
43
+ }
44
+
45
+ function stripCurrentMark(choice: string): string {
46
+ return choice.endsWith(" (current)") ? choice.slice(0, -" (current)".length) : choice;
47
+ }
48
+
49
+ function labeledChoices(choices: string[], current: RoleValue | undefined): string[] {
50
+ const currents = new Set(Array.isArray(current) ? current : current ? [current] : []);
51
+ return choices.map((choice) => (currents.has(choice) ? `${choice} (current)` : choice));
52
+ }
53
+
54
+ function modelChoices(ctx: ExtensionCommandContext): string[] {
55
+ const models =
56
+ ctx.scopedModels.length > 0
57
+ ? ctx.scopedModels.map((entry) => entry.model)
58
+ : ctx.modelRegistry.getAvailable();
59
+ const ids: string[] = [];
60
+ const seen = new Set<string>();
61
+ for (const model of models) {
62
+ const key = `${model.provider}/${model.id}`;
63
+ if (!isSafeModelSelector(key) || seen.has(key)) continue;
64
+ seen.add(key);
65
+ ids.push(key);
66
+ }
67
+ return ["inherit-parent", "auto", ...ids];
68
+ }
69
+
70
+ function isInheritSelector(value: string): boolean {
71
+ return value === "inherit-parent" || value === "auto";
72
+ }
73
+
74
+ export default function pstackExtension(pi: ExtensionAPI): void {
75
+ let potetoMode = false;
76
+
77
+ function setStatus(ctx: ExtensionContext): void {
78
+ if (ctx.mode !== "tui") return;
79
+ ctx.ui.setStatus("pstack-mode", potetoMode ? "pstack: poteto mode" : undefined);
80
+ }
81
+
82
+ function persistMode(enabled: boolean, ctx?: ExtensionContext): void {
83
+ potetoMode = enabled;
84
+ pi.appendEntry("pstack-mode", { enabled });
85
+ if (ctx) setStatus(ctx);
86
+ }
87
+
88
+ pi.on("session_start", async (_event, ctx) => {
89
+ potetoMode = false;
90
+ potetoMode = lastPotetoEnabled(sessionEntries(ctx));
91
+ setStatus(ctx);
92
+ try {
93
+ migrateLegacyMarkdownIfNeeded();
94
+ } catch {
95
+ // ignore migration errors
96
+ }
97
+ });
98
+
99
+ pi.on("input", async (event, ctx) => {
100
+ if (/^\/skill:poteto-mode(?:\s|$)/.test(event.text)) {
101
+ persistMode(true, ctx);
102
+ }
103
+ return { action: "continue" as const };
104
+ });
105
+
106
+ pi.on("before_agent_start", async (event) => {
107
+ const parts = [formatRoleTable(loadConfig())];
108
+ if (potetoMode) parts.push(POTETO_PROMPT);
109
+ return {
110
+ systemPrompt: `${event.systemPrompt}\n\n${parts.join("\n\n")}`,
111
+ };
112
+ });
113
+
114
+ pi.registerCommand("poteto-mode", {
115
+ description: "Enable or disable sticky pstack Poteto Mode. Usage: /poteto-mode [task] | /poteto-mode off",
116
+ getArgumentCompletions: (prefix) => {
117
+ const token = prefix.trim().toLowerCase();
118
+ if (!token || "off".startsWith(token)) {
119
+ return [{ value: "off", label: "off" }];
120
+ }
121
+ return null;
122
+ },
123
+ handler: async (args, ctx) => {
124
+ const raw = args.trim();
125
+ const token = raw.split(/\s+/)[0]?.toLowerCase() ?? "";
126
+ if (token === "off" || token === "disable" || token === "stop") {
127
+ persistMode(false, ctx);
128
+ ctx.ui.notify("Poteto Mode off.", "info");
129
+ return;
130
+ }
131
+ persistMode(true, ctx);
132
+ ctx.ui.notify("Poteto Mode on. Stays on until /poteto-mode off.", "info");
133
+ const payload = `${POTETO_SKILL}${raw ? ` ${raw}` : ""}`;
134
+ pi.sendUserMessage(
135
+ payload,
136
+ ctx.isIdle()
137
+ ? { expandPromptTemplates: true }
138
+ : { expandPromptTemplates: true, deliverAs: "followUp" },
139
+ );
140
+ },
141
+ });
142
+
143
+ pi.registerCommand("setup-pstack", {
144
+ description: "Map pstack delegation roles to models available in this Pi session.",
145
+ handler: async (_args, ctx) => {
146
+ try {
147
+ migrateLegacyMarkdownIfNeeded();
148
+ } catch {
149
+ // ignore
150
+ }
151
+ const path = configPath();
152
+ const config = loadConfig();
153
+ if (!ctx.hasUI) {
154
+ if (!existsSync(path) && !saveConfig(defaultConfig(), path)) {
155
+ ctx.ui.notify(`Failed to write ${path}`, "error");
156
+ return;
157
+ }
158
+ ctx.ui.notify(`Wrote ${path}`, "info");
159
+ return;
160
+ }
161
+
162
+ const choices = modelChoices(ctx);
163
+ for (const role of ROLE_NAMES) {
164
+ const next = LIST_ROLES.has(role)
165
+ ? await pickListRole(ctx, role, choices, config.roles[role])
166
+ : await pickScalarRole(ctx, role, choices, config.roles[role]);
167
+ if (next === undefined) break;
168
+ config.roles[role] = next;
169
+ }
170
+
171
+ if (!saveConfig(config, path)) {
172
+ ctx.ui.notify(`Failed to write ${path}`, "error");
173
+ return;
174
+ }
175
+ ctx.ui.notify(`Wrote ${path}`, "info");
176
+ },
177
+ });
178
+ }
179
+
180
+ async function pickScalarRole(
181
+ ctx: ExtensionCommandContext,
182
+ role: RoleName,
183
+ choices: string[],
184
+ current: RoleValue | undefined,
185
+ ): Promise<RoleValue | undefined> {
186
+ const choice = await ctx.ui.select(`Model for ${role}`, labeledChoices(choices, current));
187
+ if (!choice) return undefined;
188
+ return stripCurrentMark(choice);
189
+ }
190
+
191
+ async function pickListRole(
192
+ ctx: ExtensionCommandContext,
193
+ role: RoleName,
194
+ choices: string[],
195
+ current: RoleValue | undefined,
196
+ ): Promise<RoleValue | undefined> {
197
+ const first = await ctx.ui.select(`Model for ${role}`, labeledChoices(choices, current));
198
+ if (!first) return undefined;
199
+ const selected = stripCurrentMark(first);
200
+ if (isInheritSelector(selected)) return selected;
201
+
202
+ const picked = [selected];
203
+ while (true) {
204
+ const next = await ctx.ui.select("Add another model for this role?", ["done", ...choices]);
205
+ if (!next) return undefined;
206
+ if (next === "done") return picked;
207
+ const value = stripCurrentMark(next);
208
+ if (isInheritSelector(value)) return value;
209
+ picked.push(value);
210
+ }
211
+ }
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@zenspc/pi-pstack",
3
+ "version": "0.1.0",
4
+ "description": "pstack for Pi: rigorous agent workflows you can parallelize with confidence - poteto-mode playbooks, engineering principles, multi-model review panels, and subagents.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "dhairyaar",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/zenspc/pi-extensions.git",
11
+ "directory": "packages/pi-pstack"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/zenspc/pi-extensions/issues"
15
+ },
16
+ "homepage": "https://github.com/zenspc/pi-extensions/tree/master/packages/pi-pstack#readme",
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "keywords": [
21
+ "pi-package",
22
+ "pi-extension",
23
+ "skills",
24
+ "workflow",
25
+ "principles",
26
+ "poteto-mode",
27
+ "subagents",
28
+ "unslop"
29
+ ],
30
+ "files": [
31
+ "extensions/",
32
+ "!extensions/**/*.test.ts",
33
+ "skills/",
34
+ "agents/",
35
+ "README.md",
36
+ "LICENSE"
37
+ ],
38
+ "peerDependencies": {
39
+ "@earendil-works/pi-coding-agent": "*"
40
+ },
41
+ "pi": {
42
+ "extensions": [
43
+ "./extensions/pstack/index.ts"
44
+ ],
45
+ "skills": [
46
+ "./skills"
47
+ ],
48
+ "subagents": {
49
+ "agents": [
50
+ "./agents"
51
+ ]
52
+ }
53
+ },
54
+ "scripts": {
55
+ "test": "node --experimental-strip-types --test extensions/pstack/*.test.ts"
56
+ }
57
+ }