@rol1n/pi-kit 0.1.1 → 0.1.3

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 (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +176 -39
  3. package/README_zh.md +211 -0
  4. package/ask-u-questions/README.md +2 -2
  5. package/continue-signal/README.md +2 -2
  6. package/custom-providers/README.md +2 -2
  7. package/image-generate/README.md +4 -2
  8. package/memory-log/README.md +81 -14
  9. package/memory-log/config.ts +1 -1
  10. package/memory-log/hooks/compaction-hook.ts +58 -15
  11. package/memory-log/hooks/compaction-trigger.ts +24 -97
  12. package/memory-log/hooks/observer-trigger.ts +2 -1
  13. package/memory-log/index.ts +4 -0
  14. package/memory-log/ledger/index.ts +1 -0
  15. package/memory-log/ledger/recall.ts +111 -0
  16. package/memory-log/ledger/render.ts +3 -1
  17. package/memory-log/ledger/serialize.ts +2 -4
  18. package/memory-log/ledger/types.ts +4 -1
  19. package/memory-log/runtime.ts +0 -5
  20. package/memory-log/tools/recall.ts +60 -0
  21. package/model-vision/README.md +2 -2
  22. package/model-vision/image.ts +84 -0
  23. package/model-vision/index.ts +124 -0
  24. package/model-vision/network.ts +82 -0
  25. package/model-vision/requests.ts +151 -0
  26. package/multi-harness/README.md +4 -4
  27. package/oai-subquota/README.md +2 -2
  28. package/package.json +19 -24
  29. package/prompt-snipaste/README.md +19 -5
  30. package/prompt-snipaste/index.ts +4 -4
  31. package/prompt-snipaste/snippets.ts +28 -149
  32. package/prompt-snipaste/types.ts +1 -0
  33. package/screenshot/README.md +2 -2
  34. package/screenshot/index.ts +126 -0
  35. package/scripts/check-package.mjs +54 -0
  36. package/skills/worktree-studio/SKILL.md +99 -0
  37. package/srp-theme/README.md +2 -2
  38. package/subagent/README.md +4 -4
  39. package/tui-asr/README.md +4 -2
  40. package/web-reach/README.md +2 -2
  41. package/windows-wheel-fix/README.md +2 -2
  42. /package/{prompt-snipaste/prompts → prompts}/ask-questions.md +0 -0
  43. /package/{prompt-snipaste/prompts → prompts}/delegate-exploration.md +0 -0
  44. /package/{prompt-snipaste/prompts → prompts}/diagnose-report.md +0 -0
  45. /package/{prompt-snipaste/prompts → prompts}/frontend-tech-preset.md +0 -0
  46. /package/{prompt-snipaste/prompts → prompts}/orchestrator-mode.md +0 -0
  47. /package/{prompt-snipaste/prompts → prompts}/session-kickoff.md +0 -0
  48. /package/{prompt-snipaste/prompts → prompts}/verify-not-assume.md +0 -0
@@ -1,20 +1,32 @@
1
1
  # prompt-snipaste
2
2
 
3
- Select prompt snippets to prepend or append to the next user message, with live preview and a session transcript card. Bundles 7 built-in prompt snippets under `prompts/`.
3
+ A generic composer for Pi prompt templates: select snippets to prepend or append to the next user message, with live preview and a session transcript card. It ships no private template library and requires no particular prompt names.
4
4
 
5
5
  ## Install
6
6
 
7
7
  ```sh
8
- pi install npm:@rol1n/prompt-snipaste
8
+ pi install npm:@rol1n/pi-kit
9
9
  ```
10
10
 
11
- Also included in `@rol1n/pi-kit`.
11
+ Included and loaded by default in `@rol1n/pi-kit`. See [resource selection](../README.md#resource-selection) to disable this entry or load only selected extensions.
12
12
 
13
13
  ## Use
14
14
 
15
- Open with `/snipaste` or `Alt+S`. Navigate with arrow keys, toggle with `Space`, preview with `Tab`, confirm with `Enter`, and cancel with `Escape`.
15
+ Open with `/snipaste` or `Alt+S`. Navigate with arrow keys, toggle with `Space`, preview with `Tab`, confirm with `Enter`, and cancel with `Escape`. A confirmed selection applies once, to the next user message.
16
16
 
17
- ## Frontmatter
17
+ ## Prompt sources and filtering
18
+
19
+ Pi owns discovery and filtering. The selector reads only prompt commands exposed by `pi.getCommands()`, including templates from personal configuration, trusted projects, and other packages. It does not scan package directories, read settings itself, or fall back to disabled templates.
20
+
21
+ - Ordinary Markdown and standard Pi templates work without any snippet-specific metadata.
22
+ - Add or change prompts through Pi's normal prompt directories or settings, then run `/reload`.
23
+ - Package `prompts` filters are respected. With no enabled templates, the menu displays a notice and injects nothing.
24
+ - Same-named files from different sources have independent identities based on their full source paths.
25
+ - The kit's root [`prompts/`](../prompts/) directory is a separate resource. Disabling it does not disable the selector; disabling the selector does not disable native template commands.
26
+
27
+ The selector composes template bodies as text; it does **not** invoke slash commands or expand positional argument placeholders such as `$1`. Use Pi's normal `/template args` invocation when argument expansion is needed.
28
+
29
+ ## Optional frontmatter
18
30
 
19
31
  ```markdown
20
32
  ---
@@ -25,3 +37,5 @@ order: 100
25
37
  ---
26
38
  Check correctness, error handling, and missing validation.
27
39
  ```
40
+
41
+ All fields are optional. `name` overrides the menu label, `description` describes the template, `placement` defaults to `append`, and `order` defaults to `100`. Prepend snippets come first, followed by append snippets; each group is sorted by numeric order and name.
@@ -20,11 +20,11 @@ export default function promptSnipaste(pi: ExtensionAPI) {
20
20
  return;
21
21
  }
22
22
 
23
- snippets = loadSnippets(ctx.cwd, pi);
23
+ snippets = loadSnippets(pi);
24
24
  enabled = new Set([...enabled].filter((id) => snippets.some((s) => s.id === id)));
25
25
 
26
26
  if (snippets.length === 0) {
27
- ctx.ui.notify("No Markdown files found in prompt directories", "warning");
27
+ ctx.ui.notify("No enabled prompt templates. Add prompts in Pi settings, then /reload.", "warning");
28
28
  updateWidget(ctx);
29
29
  return;
30
30
  }
@@ -50,14 +50,14 @@ export default function promptSnipaste(pi: ExtensionAPI) {
50
50
  pi.on("session_start", (_event, ctx) => {
51
51
  enabled = new Set();
52
52
  pendingTurnSnippets = null;
53
- snippets = loadSnippets(ctx.cwd, pi);
53
+ snippets = loadSnippets(pi);
54
54
  updateWidget(ctx);
55
55
  });
56
56
 
57
57
  pi.on("input", async (_event, ctx) => {
58
58
  if (enabled.size === 0) return undefined;
59
59
 
60
- snippets = loadSnippets(ctx.cwd, pi);
60
+ snippets = loadSnippets(pi);
61
61
  const active = snippets.filter((s) => enabled.has(s.id));
62
62
  enabled = new Set();
63
63
  updateWidget(ctx);
@@ -1,10 +1,6 @@
1
- import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { basename, dirname, isAbsolute, join, resolve } from "node:path";
4
- import { fileURLToPath } from "node:url";
5
-
6
- const BUNDLED_PROMPTS_DIR = join(dirname(fileURLToPath(import.meta.url)), "prompts");
7
- import { CONFIG_DIR_NAME, getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
1
+ import { readFileSync } from "node:fs";
2
+ import { basename, resolve } from "node:path";
3
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
8
4
  import type { Snippet } from "./types.ts";
9
5
 
10
6
  export function parseSnippet(filename: string, raw: string): Snippet | null {
@@ -48,159 +44,42 @@ export function parseSnippet(filename: string, raw: string): Snippet | null {
48
44
  };
49
45
  }
50
46
 
51
- function resolveSnippetPath(rawPath: string, cwd: string): string {
52
- const trimmed = rawPath.trim();
53
- if (trimmed === "~" || trimmed.startsWith("~/") || (process.platform === "win32" && trimmed.startsWith("~\\"))) {
54
- return resolve(homedir(), trimmed.replace(/^~[/\\]?/, ""));
55
- }
56
- if (isAbsolute(trimmed)) {
57
- return resolve(trimmed);
58
- }
59
- const fromCwd = resolve(cwd, trimmed);
60
- if (existsSync(fromCwd)) return fromCwd;
61
- const fromAgent = resolve(getAgentDir(), trimmed);
62
- if (existsSync(fromAgent)) return fromAgent;
63
- return fromCwd;
64
- }
65
-
66
- function getSettingsPrompts(cwd: string, pi?: ExtensionAPI): string[] {
67
- const results: string[] = [];
68
-
69
- if (pi) {
70
- try {
71
- const settings = pi.getSettings();
72
- if (Array.isArray(settings?.prompts)) {
73
- results.push(...settings.prompts);
74
- return results;
75
- }
76
- } catch {
77
- // pi.getSettings() may throw before runner binds
78
- }
79
- }
80
-
81
- const settingsFiles = [
82
- join(getAgentDir(), "settings.json"),
83
- join(cwd, CONFIG_DIR_NAME, "settings.json"),
84
- ];
85
-
86
- for (const sf of settingsFiles) {
87
- if (!existsSync(sf)) continue;
88
- try {
89
- const content = readFileSync(sf, "utf-8");
90
- const parsed = JSON.parse(content.replace(/^\uFEFF/, ""));
91
- if (Array.isArray(parsed?.prompts)) {
92
- for (const p of parsed.prompts) {
93
- if (typeof p === "string" && !results.includes(p)) {
94
- results.push(p);
95
- }
96
- }
97
- }
98
- } catch {
99
- // ignore JSON parse errors
100
- }
101
- }
47
+ const UNREADABLE_PROMPT_CODES = new Set(["ENOENT", "ENOTDIR", "EISDIR", "EACCES", "EPERM"]);
102
48
 
103
- return results;
104
- }
105
-
106
- export function loadSnippets(cwd: string, pi?: ExtensionAPI): Snippet[] {
49
+ /**
50
+ * Pi owns prompt discovery, trust, and resource filtering. Consume only its
51
+ * enabled prompt commands; never scan directories or revive excluded templates.
52
+ */
53
+ export function loadSnippets(pi: Pick<ExtensionAPI, "getCommands">): Snippet[] {
107
54
  const seen = new Map<string, Snippet>();
108
- const excludedNames = new Set<string>();
109
55
 
110
- const loadedPromptPaths = pi?.getCommands()
111
- .filter((command) => command.source === "prompt")
112
- .map((command) => command.sourceInfo.path);
113
- const defaultDirs = loadedPromptPaths ? [] : [
114
- BUNDLED_PROMPTS_DIR,
115
- join(getAgentDir(), "prompts"),
116
- join(cwd, CONFIG_DIR_NAME, "prompts"),
117
- ];
118
- const settingsPrompts = loadedPromptPaths ?? getSettingsPrompts(cwd, pi);
56
+ for (const command of pi.getCommands()) {
57
+ if (command.source !== "prompt") continue;
58
+ const path = resolve(command.sourceInfo.path);
59
+ if (!path.toLowerCase().endsWith(".md") || seen.has(path)) continue;
119
60
 
120
- const processDirectory = (dir: string) => {
121
- if (!existsSync(dir)) return;
122
- let files: string[];
61
+ let raw: string;
123
62
  try {
124
- files = readdirSync(dir);
63
+ raw = readFileSync(path, "utf-8");
125
64
  } catch (error) {
126
- if (!(error instanceof Error) || !("code" in error)) throw error;
127
- console.error(`Failed to read prompt directory ${dir}: ${error.message}`);
128
- return;
65
+ if (!(error instanceof Error) || !("code" in error) || !UNREADABLE_PROMPT_CODES.has(String(error.code))) throw error;
66
+ console.error(`prompt-snipaste: cannot read enabled prompt ${path}: ${error.message}`);
67
+ continue;
129
68
  }
130
- for (const file of files) {
131
- if (!file.endsWith(".md")) continue;
132
- if (excludedNames.has(file)) continue;
133
- const fullPath = join(dir, file);
134
- try {
135
- const snippet = parseSnippet(file, readFileSync(fullPath, "utf-8"));
136
- if (snippet) seen.set(file, snippet);
137
- } catch (error) {
138
- if (!(error instanceof Error) || !("code" in error)) throw error;
139
- console.error(`Failed to read prompt ${fullPath}: ${error.message}`);
140
- }
69
+ const snippet = parseSnippet(basename(path), raw);
70
+ if (snippet) {
71
+ // Source paths, not basenames, keep same-named templates independent.
72
+ seen.set(path, {
73
+ ...snippet,
74
+ id: path,
75
+ description: command.description ?? snippet.description,
76
+ });
141
77
  }
142
- };
143
-
144
- const processFile = (filePath: string) => {
145
- if (!existsSync(filePath) || !filePath.endsWith(".md")) return;
146
- const filename = basename(filePath);
147
- if (excludedNames.has(filename)) return;
148
- try {
149
- const snippet = parseSnippet(filename, readFileSync(filePath, "utf-8"));
150
- if (snippet) seen.set(filename, snippet);
151
- } catch (error) {
152
- if (!(error instanceof Error) || !("code" in error)) throw error;
153
- console.error(`Failed to read prompt file ${filePath}: ${error.message}`);
154
- }
155
- };
156
-
157
- // Collect exclusions first
158
- for (const raw of settingsPrompts) {
159
- if (typeof raw !== "string") continue;
160
- const trimmed = raw.trim();
161
- if (trimmed.startsWith("-") || trimmed.startsWith("!")) {
162
- const clean = trimmed.slice(1).trim();
163
- excludedNames.add(basename(clean));
164
- }
165
- }
166
-
167
- // 1. Load default prompt directories
168
- for (const dir of defaultDirs) {
169
- processDirectory(dir);
170
- }
171
-
172
- // 2. Load Pi's resolved prompts, or explicit settings paths without a runtime.
173
- for (const raw of settingsPrompts) {
174
- if (typeof raw !== "string") continue;
175
- let trimmed = raw.trim();
176
- if (trimmed.startsWith("-") || trimmed.startsWith("!")) continue;
177
- if (trimmed.startsWith("+")) trimmed = trimmed.slice(1).trim();
178
- if (!trimmed) continue;
179
-
180
- const resolved = resolveSnippetPath(trimmed, cwd);
181
- if (!existsSync(resolved)) continue;
182
-
183
- try {
184
- const stat = statSync(resolved);
185
- if (stat.isDirectory()) {
186
- processDirectory(resolved);
187
- } else if (stat.isFile() && resolved.endsWith(".md")) {
188
- processFile(resolved);
189
- }
190
- } catch (error) {
191
- if (!(error instanceof Error) || !("code" in error)) throw error;
192
- console.error(`Failed to inspect prompt path ${resolved}: ${error.message}`);
193
- }
194
- }
195
-
196
- // Remove any excluded items
197
- for (const ex of excludedNames) {
198
- seen.delete(ex);
199
78
  }
200
79
 
201
- return Array.from(seen.values()).sort((a, b) => {
80
+ return [...seen.values()].sort((a, b) => {
202
81
  if (a.placement !== b.placement) return a.placement === "prepend" ? -1 : 1;
203
82
  if (a.order !== b.order) return a.order - b.order;
204
- return a.name.localeCompare(b.name);
83
+ return a.name.localeCompare(b.name) || a.id.localeCompare(b.id);
205
84
  });
206
85
  }
@@ -1,4 +1,5 @@
1
1
  export interface Snippet {
2
+ /** Absolute source path when loaded from Pi; stable across menu refreshes. */
2
3
  id: string;
3
4
  name: string;
4
5
  description: string;
@@ -8,7 +8,7 @@ Searches standard and localized `Pictures/Screenshots` and OneDrive screenshot d
8
8
  ## Install
9
9
 
10
10
  ```sh
11
- pi install npm:@rol1n/screenshot
11
+ pi install npm:@rol1n/pi-kit
12
12
  ```
13
13
 
14
- Available as a standalone package (not enabled in `@rol1n/pi-kit` by default).
14
+ Included and loaded by default in `@rol1n/pi-kit`. See [resource selection](../README.md#resource-selection) to disable this entry or load only selected extensions.
@@ -0,0 +1,126 @@
1
+ /**
2
+ * screenshot — Insert the latest Windows/WSL system screenshot path into Pi's editor.
3
+ */
4
+
5
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
6
+ import { extname, join } from "node:path";
7
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
8
+
9
+ const SCREENSHOT_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp"]);
10
+ const SKIP_USER_DIRS = /^(Public|Default|Default User|All Users|desktop\.ini)$/i;
11
+
12
+ function isExpectedFileSystemError(error: unknown): boolean {
13
+ return error instanceof Error && "code" in error &&
14
+ ["ENOENT", "ENOTDIR", "EACCES", "EPERM"].includes(String(error.code));
15
+ }
16
+
17
+ function isWindows(): boolean {
18
+ return process.platform === "win32";
19
+ }
20
+
21
+ function isWSL(): boolean {
22
+ if (process.platform !== "linux") return false;
23
+ if (process.env.WSL_DISTRO_NAME || process.env.WSLENV || existsSync("/mnt/c/Users")) return true;
24
+ try {
25
+ return /microsoft|wsl/i.test(readFileSync("/proc/version", "utf-8"));
26
+ } catch (error) {
27
+ if (isExpectedFileSystemError(error)) return false;
28
+ throw error;
29
+ }
30
+ }
31
+
32
+ export function supportsScreenshots(): boolean {
33
+ return isWindows() || isWSL();
34
+ }
35
+
36
+ function findScreenshotDirs(): string[] {
37
+ const dirs = new Set<string>();
38
+ const addIfDirectory = (path: string) => {
39
+ try {
40
+ if (existsSync(path) && statSync(path).isDirectory()) dirs.add(path);
41
+ } catch (error) {
42
+ if (!isExpectedFileSystemError(error)) throw error;
43
+ }
44
+ };
45
+
46
+ if (isWindows()) {
47
+ const profile = process.env.USERPROFILE || process.env.HOME;
48
+ if (profile) {
49
+ addIfDirectory(join(profile, "Pictures", "Screenshots"));
50
+ addIfDirectory(join(profile, "Pictures", "屏幕截图"));
51
+ addIfDirectory(join(profile, "OneDrive", "Pictures", "Screenshots"));
52
+ addIfDirectory(join(profile, "OneDrive", "图片", "屏幕截图"));
53
+ addIfDirectory(join(profile, "OneDrive", "Pictures", "屏幕截图"));
54
+ }
55
+ }
56
+
57
+ if (isWSL()) {
58
+ const usersRoot = "/mnt/c/Users";
59
+ try {
60
+ for (const entry of readdirSync(usersRoot, { withFileTypes: true })) {
61
+ if (!entry.isDirectory() || SKIP_USER_DIRS.test(entry.name)) continue;
62
+ const profile = join(usersRoot, entry.name);
63
+ addIfDirectory(join(profile, "Pictures", "Screenshots"));
64
+ addIfDirectory(join(profile, "Pictures", "屏幕截图"));
65
+ addIfDirectory(join(profile, "OneDrive", "Pictures", "Screenshots"));
66
+ addIfDirectory(join(profile, "OneDrive", "图片", "屏幕截图"));
67
+ }
68
+ } catch (error) {
69
+ if (!isExpectedFileSystemError(error)) throw error;
70
+ }
71
+ }
72
+
73
+ return [...dirs];
74
+ }
75
+
76
+ export function findLatestScreenshot(): string | null {
77
+ let latestPath: string | null = null;
78
+ let latestMtime = -1;
79
+
80
+ for (const dir of findScreenshotDirs()) {
81
+ let files: string[];
82
+ try {
83
+ files = readdirSync(dir);
84
+ } catch (error) {
85
+ if (isExpectedFileSystemError(error)) continue;
86
+ throw error;
87
+ }
88
+
89
+ for (const file of files) {
90
+ if (!SCREENSHOT_EXTENSIONS.has(extname(file).toLowerCase())) continue;
91
+ const path = join(dir, file);
92
+ try {
93
+ const stat = statSync(path);
94
+ if (stat.isFile() && stat.mtimeMs > latestMtime) {
95
+ latestPath = path;
96
+ latestMtime = stat.mtimeMs;
97
+ }
98
+ } catch (error) {
99
+ if (!isExpectedFileSystemError(error)) throw error;
100
+ }
101
+ }
102
+ }
103
+
104
+ return latestPath;
105
+ }
106
+
107
+ export default function (pi: ExtensionAPI) {
108
+ pi.registerCommand("screenshot", {
109
+ description: "Insert the latest Windows/WSL system screenshot path into the editor",
110
+ handler: async (_args, ctx: ExtensionContext) => {
111
+ if (!supportsScreenshots()) {
112
+ ctx.ui.notify("Current environment is not Windows or WSL; cannot locate Windows screenshots.", "warning");
113
+ return;
114
+ }
115
+
116
+ const path = findLatestScreenshot();
117
+ if (!path) {
118
+ ctx.ui.notify("No Windows screenshots found.", "warning");
119
+ return;
120
+ }
121
+
122
+ ctx.ui.pasteToEditor(`${path} `);
123
+ ctx.ui.notify(`Referenced screenshot: ${path.split(/[\\/]/).pop()}`, "info");
124
+ },
125
+ });
126
+ }
@@ -0,0 +1,54 @@
1
+ import assert from "node:assert/strict";
2
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
7
+ const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
8
+
9
+ assert.equal(manifest.name, "@rol1n/pi-kit");
10
+ assert.match(manifest.version, /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/, "Expected a release version in package.json");
11
+ assert.equal(manifest.type, "module");
12
+ assert.equal(manifest.license, "MIT");
13
+ assert.ok(manifest.keywords.includes("pi-package"), "Missing pi-package discovery keyword");
14
+ assert.ok(Array.isArray(manifest.files), "Expected a files allowlist");
15
+
16
+ // Validate the hand-maintained manifest; never generate or rewrite it.
17
+ const extensions = readdirSync(root, { withFileTypes: true })
18
+ .filter((entry) => entry.isDirectory() && existsSync(join(root, entry.name, "index.ts")))
19
+ .map((entry) => entry.name)
20
+ .sort();
21
+ assert.ok(extensions.length > 0, "No extension directories found");
22
+ assert.deepEqual(
23
+ [...manifest.pi.extensions].sort(),
24
+ extensions.map((name) => `./${name}/index.ts`),
25
+ "pi.extensions must declare every top-level extension exactly once",
26
+ );
27
+ for (const name of extensions) {
28
+ assert.ok(existsSync(join(root, name, "README.md")), `Missing ${name}/README.md`);
29
+ assert.ok(manifest.files.includes(name), `files must include the complete ${name} directory`);
30
+ }
31
+
32
+ assert.deepEqual(manifest.pi.prompts, ["./prompts"], "pi.prompts must declare the independent root prompts directory");
33
+ assert.ok(manifest.files.includes("prompts"), "files must include the root prompts directory");
34
+ assert.ok(existsSync(join(root, "prompts")), "Missing root prompts directory");
35
+ assert.deepEqual(manifest.pi.skills, ["./skills"], "pi.skills must declare the root skills directory");
36
+ assert.ok(manifest.files.includes("skills"), "files must include the root skills directory");
37
+ const skills = readdirSync(join(root, "skills"), { withFileTypes: true })
38
+ .filter((entry) => entry.isDirectory() && existsSync(join(root, "skills", entry.name, "SKILL.md")))
39
+ .map((entry) => entry.name);
40
+ assert.ok(skills.length > 0, "Expected at least one skill directory containing SKILL.md");
41
+ assert.deepEqual(manifest.pi.themes, []);
42
+ for (const name of ["@earendil-works/pi-ai", "@earendil-works/pi-agent-core", "@earendil-works/pi-coding-agent", "@earendil-works/pi-tui", "typebox"]) {
43
+ assert.equal(manifest.peerDependencies[name], "*", `${name} must remain a host-provided peer`);
44
+ assert.ok(!manifest.dependencies?.[name], `Do not install a separate copy of ${name}`);
45
+ }
46
+ for (const exclusion of ["!**/test/**", "!**/tests/**", "!**/*.{test,spec}.{ts,js,mjs,cjs}", "!**/.env", "!**/.env.*"]) {
47
+ assert.ok(manifest.files.includes(exclusion), `Missing package exclusion: ${exclusion}`);
48
+ }
49
+ for (const path of ["LICENSE", "README.md", "README_zh.md", "scripts/check-package.mjs"]) {
50
+ assert.ok(existsSync(join(root, path)), `Missing ${path}`);
51
+ }
52
+
53
+ const promptCount = readdirSync(join(root, "prompts")).filter((name) => name.endsWith(".md")).length;
54
+ console.log(`Checked ${manifest.name}@${manifest.version}: ${extensions.length} extensions, ${promptCount} prompts, ${skills.length} skill${skills.length === 1 ? "" : "s"}.`);
@@ -0,0 +1,99 @@
1
+ ---
2
+ name: worktree-studio
3
+ description: Orchestrate isolated Git-worktree experiments and compare independently implemented alternatives with real diffs and verification. Use for The Road Not Taken, competing implementation approaches, parallel worktree exploration, or accepting/rejecting agent-produced worktrees. Owns planning, confirmation, directory layout, worker delegation, evidence collection, and comparison; does not auto-integrate or discard results.
4
+ ---
5
+
6
+ # Worktree Studio — The Road Not Taken
7
+
8
+ Separate the workflow from the runtime. Git manages directories; this skill manages the experiment; a delegation tool runs each task in the specified `cwd`. `multi_harness` is one runtime, not a requirement for creating worktrees. Ordinary `subagent` or another available runtime may be used with equivalent explicit directory and lifecycle controls.
9
+
10
+ ## 1. Define and confirm the experiment
11
+
12
+ - Use experiments only when evidence from genuinely viable alternatives will resolve uncertainty. Do not manufacture alternatives for a routine change.
13
+ - Define the common goal, concrete acceptance criteria, each approach, and the number of alternatives. Do not hard-code two workers or require different harnesses.
14
+ - Choose a reproducible baseline and name workers by purpose (for example `cache-codex`, `batch-pi`). Identify write overlap and dependencies.
15
+ - Confirm the plan with the user **before creating worktrees or launching paid workers**. Explain expected workers/models, directories, resource costs, and retained artifacts. Timeouts are not spending limits; these workers may have full filesystem permissions.
16
+ - Respect existing user approval without repeating questions already answered. Do not interpret approval to explore as approval to merge, commit, install dependencies, or delete results.
17
+
18
+ ## 2. Inspect the baseline without disturbing changes
19
+
20
+ Use Git with argument-safe paths, for example:
21
+
22
+ ```sh
23
+ git -C <source> rev-parse --show-toplevel
24
+ git -C <source> rev-parse HEAD
25
+ git -C <source> status --porcelain=v1 --untracked-files=normal
26
+ git -C <source> worktree list --porcelain
27
+ ```
28
+
29
+ Read relevant project instructions and code. Record the exact baseline SHA, source directory, and acceptance commands in a plan file outside worker directories.
30
+
31
+ A dirty source cannot be reproduced from `HEAD`. Stop and explain the discrepancy. Ask the user to resolve the state, or explicitly approve comparing the committed baseline while leaving local changes untouched. Never automatically stash, stage, commit, reset, or copy ignored/secret files. Do not claim an experiment includes uncommitted context when it does not.
32
+
33
+ ## 3. Lay out isolated worktrees
34
+
35
+ Choose a fresh experiment directory outside the source checkout and never reuse a nonempty path. Record a mapping of approach → worker name → harness/model → absolute worktree path.
36
+
37
+ For each alternative:
38
+
39
+ ```sh
40
+ git -C <source> worktree add --detach <absolute-new-path> <baseline-sha>
41
+ ```
42
+
43
+ If creation partially fails, keep successful worktrees and report their paths. Do not force-remove them. Do not use Git locks or branches shared between workers.
44
+
45
+ Ignored dependencies/build outputs are not copied by Git. Establish what can be verified without installing anything; ask before dependency installation or version changes. A worktree is not a security sandbox.
46
+
47
+ ## 4. Delegate one well-scoped task per directory
48
+
49
+ Use any suitable runtime. With `multi_harness`, each worker is an equal CLI peer (`pi`, `codex`, `claude`, or `opencode`), not a mode:
50
+
51
+ ```json
52
+ {"action":"spawn","name":"cache-codex","harness":"codex","cwd":"/absolute/experiment/cache","task":"Implement the cache approach. Baseline: <sha>. Acceptance: <criteria and commands>. Work only in this directory. Do not commit, merge, switch branches, delete worktrees, modify the source checkout, read secrets, or install dependencies without permission. Report changes, commands actually executed, results, blockers, and trade-offs."}
53
+ ```
54
+
55
+ - Give every worker the **same goal and acceptance criteria**, with a different assigned approach where appropriate.
56
+ - Include explicit paths, scope, no-go operations, expected artifacts, and verification commands in the task.
57
+ - Native TUIs stay open after each turn. Completion arrives as a pushed notification. Do not wait in bash loops, repeatedly capture panes, or continually call `status`.
58
+ - Treat `harness-done "summary"` as a summary submission; settlement hooks produce the authoritative turn-completion event. A worker's success claim is not independent verification.
59
+ - To clarify, repair, or request more evidence, send a follow-up to the same idle worker:
60
+
61
+ ```json
62
+ {"action":"send","target":"cache-codex","task":"Run the missing acceptance check in your existing directory. Report the exact command, result, and remaining limitations; do not integrate or commit."}
63
+ ```
64
+
65
+ Use `status` for a user-requested snapshot or diagnosis, not as a waiting loop. If a timeout or trust/permission dialog blocks work, surface it; do not silently bypass additional security controls.
66
+
67
+ ## 5. Collect evidence and independently accept/reject
68
+
69
+ After notifications arrive, inspect **every** worktree. Keep machine evidence separate from worker summaries:
70
+
71
+ ```sh
72
+ git -C <worker-dir> rev-parse HEAD
73
+ git -C <worker-dir> status --short --untracked-files=normal
74
+ git -C <worker-dir> diff --no-ext-diff --stat <baseline-sha>
75
+ git -C <worker-dir> diff --no-ext-diff --binary <baseline-sha>
76
+ git -C <worker-dir> ls-files --others --exclude-standard -z
77
+ git -C <worker-dir> log --oneline <baseline-sha>..HEAD
78
+ ```
79
+
80
+ - Save tracked diffs and inspect untracked deliverables separately: ordinary `git diff` omits new untracked files. Never stage files merely to make a diff appear complete.
81
+ - Detect unexpected commits, branch changes, deleted files, dependency edits, out-of-scope changes, and modifications to the original checkout.
82
+ - Run comparable acceptance checks yourself when safe. Record command, directory, exit/result, and environment limitations. Do not mark an unrun or permission-blocked check as passed.
83
+ - Review correctness, maintainability, scope, performance evidence, compatibility, operational risks, and costs when reported. Do not fabricate absent pricing or infer free usage from zero/missing cost.
84
+ - For UI outputs, inspect desktop/mobile rendering if browser access is available; otherwise state that visual validation was not performed.
85
+
86
+ ## 6. Report, recommend, and preserve
87
+
88
+ Write a comparison report in the experiment directory:
89
+
90
+ 1. Goal, baseline SHA, source path, acceptance criteria.
91
+ 2. Route/worker/harness/model/directory mapping.
92
+ 3. Per-route implementation summary, tracked diff, untracked artifacts, unexpected commits, independent test results, and limitations.
93
+ 4. Comparison table: accepted criteria, failures, trade-offs, evidence gaps, resource/cost observations.
94
+ 5. Recommendation with reasons, or an explicit statement that evidence is insufficient.
95
+ 6. Paths to preserved worktrees, runtime sessions/logs, and pending decisions.
96
+
97
+ Ask before integrating any result. Do not automatically choose a winner, merge, cherry-pick, commit, or delete worktrees.
98
+
99
+ When the user wants runtime cleanup, `multi_harness({"action":"close","target":"cache-codex"})` closes an idle TUI without deleting working files. For active work, use `stop` first. Explain that stopping a worker is not the same as removing its Git worktree. Any later removal needs separate explicit approval after artifact review.
@@ -5,10 +5,10 @@ Incremental Pi terminal presentation enhancement: adds a `belowEditor` last-prom
5
5
  ## Install
6
6
 
7
7
  ```sh
8
- pi install npm:@rol1n/srp-theme
8
+ pi install npm:@rol1n/pi-kit
9
9
  ```
10
10
 
11
- Also included in `@rol1n/pi-kit`.
11
+ Included and loaded by default in `@rol1n/pi-kit`. See [resource selection](../README.md#resource-selection) to disable this entry or load only selected extensions.
12
12
 
13
13
  ## Commands
14
14
 
@@ -5,10 +5,10 @@ Delegate tasks to independent, full-powered Pi workers. The `subagent` tool retu
5
5
  ## Install
6
6
 
7
7
  ```sh
8
- pi install npm:@rol1n/subagent
8
+ pi install npm:@rol1n/pi-kit
9
9
  ```
10
10
 
11
- Also included in `@rol1n/pi-kit`. The parent and workers require the `pi` executable on PATH and their normal Pi authentication.
11
+ Included and loaded by default in `@rol1n/pi-kit`. See [resource selection](../README.md#resource-selection) to disable this entry or load only selected extensions. The parent and workers require the `pi` executable on PATH and their normal Pi authentication.
12
12
 
13
13
  ## Use
14
14
 
@@ -30,6 +30,6 @@ Also included in `@rol1n/pi-kit`. The parent and workers require the `pi` execut
30
30
 
31
31
  Without a usable multiplexer, the worker backend falls back to a background process. Native Windows supports PowerShell launch commands and Zellij panes; tmux is for POSIX environments. Session transcripts and task artifacts are stored under `~/.pi/agent/`.
32
32
 
33
- ## Other delegation packages
33
+ ## Other delegation tools
34
34
 
35
- Use [multi-harness](https://github.com/RolinShmily/pi-kit/tree/main/multi-harness) for Codex, Claude, or OpenCode peers. That package shares multiplexer helpers (`mux.ts`) while exposing a separate tool.
35
+ Use [multi-harness](https://github.com/RolinShmily/pi-kit/tree/main/multi-harness) for Codex, Claude, or OpenCode peers. It shares multiplexer helpers (`mux.ts`) within the same kit while exposing a separate tool.
package/tui-asr/README.md CHANGED
@@ -5,10 +5,12 @@ Streaming voice dictation for Pi's terminal editor using Aliyun DashScope Parafo
5
5
  ## Install
6
6
 
7
7
  ```sh
8
- pi install npm:@rol1n/tui-asr
8
+ pi install npm:@rol1n/pi-kit
9
9
  ```
10
10
 
11
- Also included in `@rol1n/pi-kit`. Installing this package automatically installs `@rol1n/custom-providers`.
11
+ Included and loaded by default in `@rol1n/pi-kit`. See [resource selection](../README.md#resource-selection) to disable this entry or load only selected extensions.
12
+
13
+ Uses `custom-providers` from the same kit; required provider registration is initialized automatically.
12
14
 
13
15
  ## Commands and shortcuts
14
16
 
@@ -5,10 +5,10 @@ Keyless web search (`web_search` via Exa MCP) and multi-tier page extraction (`w
5
5
  ## Install
6
6
 
7
7
  ```sh
8
- pi install npm:@rol1n/web-reach
8
+ pi install npm:@rol1n/pi-kit
9
9
  ```
10
10
 
11
- Also included in `@rol1n/pi-kit`.
11
+ Included and loaded by default in `@rol1n/pi-kit`. See [resource selection](../README.md#resource-selection) to disable this entry or load only selected extensions.
12
12
 
13
13
  ## Configuration
14
14
 
@@ -6,7 +6,7 @@ Restores mouse-wheel reporting for Pi under Windows ConPTY / Zellij ([earendil-w
6
6
  ## Install
7
7
 
8
8
  ```sh
9
- pi install npm:@rol1n/windows-wheel-fix
9
+ pi install npm:@rol1n/pi-kit
10
10
  ```
11
11
 
12
- Also included in `@rol1n/pi-kit`.
12
+ Included and loaded by default in `@rol1n/pi-kit`. See [resource selection](../README.md#resource-selection) to disable this entry or load only selected extensions.