Reviewed GitHub issue and project context.
UnderstandDrafted consolidated comment with design decisions.
EditExpanded comment with detailed implementation instructions.
Edit>git statusOn branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
Analyze GitHub issue(s): https://github.com/badlogic/pi-mono/issues/645 this is an issue by me and you. we already went through all of this. i think its finally time to implement this garbage. analyze the issue and the codebase, tell me if you have any open questions so we can finalize the details, then go to town
For each issue:
Read the issue in full, including all comments and linked issues/PRs.
For bugs:
For feature requests:
Do NOT implement unless explicitly asked. Analyze and propose only.
Preparing to create GitHub issue
I'm focusing on drafting a GitHub issue to capture the user's concrete task without implementing or modifying the README, as per instructions. I'll view the current issue details for context.
>gh issue view 645 --json title,body,comments,labels,state{"body":"## Summary\n\nSupersedes #326 and #562. Comprehensive plan for extension package management, loading, and hot reload.\n\n---\n\n## Architecture Overview\n\n```\n┌─────────────────┐ ┌────────────────┐ ┌──────────────────┐\n│ PackageManager │────▶│ Loader │────▶│ AgentSession │\n│ │ │ │ │ │\n│ - Resolves │ │ - Loads from │ │ - Uses loaded │\n│ sources to │ │ paths │ │ content │\n│ paths │ │ - Applies │ │ - reload() takes │\n│ - Reads pi │ │ filters │ │ LoadResult │\n│ field from │ │ - Returns │ │ - Calls runner │\n│ package.json │ │ LoadResult │ │ initializer │\n└─────────────────┘ └────────────────┘ └──────────────────┘\n```\n\n---\n\n## Phase 1: Package Management\n\n### Sources\n\n| Source | Example | Global Install Location | Local Install Location |\n|--------|---------|------------------------|------------------------|\n| npm | `npm:@foo/bar@1.0.0` | Global node_modules (`npm root -g`) | `<cwd>/.pi/npm/node_modules/` |\n| git | `git:github.com/user/repo@v1` | `~/.pi/agent/git/<host>/<path>/` | `<cwd>/.pi/git/<host>/<path>/` |\n| local | `./my-extension/` | Used directly | Used directly |\n\n### Commands\n\n```bash\n# Global (default)\npi install npm:@foo/bar@1.0.0 # npm install -g @foo/bar@1.0.0\npi install git:github.com/u/r@v1 # clone to ~/.pi/agent/git/github.com/u/r/\npi install ./my-extension/ # add to settings.json, npm install if package.json exists\n\n# Local (-l flag)\npi install -l npm:@foo/bar@1.0.0 # add to .pi/npm/package.json, npm install\npi install -l git:github.com/u/r@v1\n\n# Management\npi remove npm:@foo/bar # uninstall + remove from settings\npi update # update all unpinned packages\npi update npm:@foo/bar # update specific package\n```\n\n### settings.json\n\nSource of truth for what to load:\n\n```json\n{\n \"extensions\": [\n \"npm:@foo/bar@1.0.0\",\n \"git:github.com/user/repo@v1.0\",\n \"./local-extension/\"\n ]\n}\n```\n\n- Global: `~/.pi/agent/settings.json`\n- Local: `<cwd>/.pi/settings.json` (with `-l` flag)\n- Local overrides/merges with global (existing behavior)\n\n### Version Pinning\n\n- `@version` pinned → `pi update` is no-op\n- No version → `pi update` pulls latest\n\n### Package Format\n\n**Auto-discovery (no package.json needed):**\n- `extensions/*.ts` or `extensions/*/index.ts`\n- `skills/*.md` or `skills/*/SKILL.md`\n- `prompts/*.md`\n- `themes/*.json`\n\n**Explicit declaration via package.json `pi` field:**\n```json\n{\n \"name\": \"@foo/pi-extension-pack\",\n \"pi\": {\n \"extensions\": [\"./src/index.ts\", \"./src/other.ts\"],\n \"skills\": [\"./skills/\"],\n \"prompts\": [\"./prompts/\"],\n \"themes\": [\"./themes/dark.json\"]\n }\n}\n```\n\nPackageManager reads the `pi` field and returns those paths. If no `pi` field, returns the directory for default discovery rules.\n\n### PackageManager API\n\n```typescript\ninterface PackageManager {\n resolve(onMissing?: (source: string) => Promise<\"install\" | \"skip\" | \"error\">): Promise<ResolvedPaths>;\n install(source: string, options?: { local?: boolean }): Promise<void>;\n remove(source: string, options?: { local?: boolean }): Promise<void>;\n update(source?: string): Promise<void>;\n}\n\ninterface ResolvedPaths {\n extensions: string[]; // files or folders\n skills: string[];\n prompts: string[];\n themes: string[];\n agentsMd: string[];\n systemMd: string[];\n}\n```\n\n---\n\n## Phase 2: Loader\n\n### Loader API\n\n```typescript\ntype LoaderConfig = ResolvedPaths & { filters?: LoaderFilters };\n\n// Static config OR async callback (re-reads settings on each load)\ntype LoaderConfigProvider = LoaderConfig | (() => LoaderConfig | Promise<LoaderConfig>);\n\nfunction createLoader(configOrProvider: LoaderConfigProvider): Loader;\n\ninterface Loader {\n load(): Promise<LoadResult>;\n}\n```\n\n### LoaderFilters\n\n```typescript\ninterface LoaderFilters {\n extensions?: { enabled?: boolean; include?: string[]; exclude?: string[] };\n skills?: {\n enabled?: boolean;\n sources?: { codexUser?: boolean; claudeUser?: boolean; claudeProject?: boolean; piUser?: boolean; piProject?: boolean };\n include?: string[];\n exclude?: string[];\n };\n prompts?: { enabled?: boolean; include?: string[]; exclude?: string[] };\n themes?: { enabled?: boolean; include?: string[]; exclude?: string[] };\n}\n```\n\n### LoadResult\n\n```typescript\ninterface LoadResult {\n extensions: LoadExtensionsResult;\n skills: Skill[];\n skillWarnings: SkillWarning[];\n promptTemplates: PromptTemplate[];\n themes: Theme[];\n contextFiles: Array<{ path: string; content: string }>;\n}\n```\n\n---\n\n## Phase 3: Bootstrap Flow (main.ts)\n\nExtensions can register CLI flags via `pi.registerFlag()`. This requires loading extensions BEFORE full CLI parsing.\n\n```typescript\n// 1. First pass parse - get extension paths, --no-extensions, etc.\nconst firstPass = parseArgs(args);\n\n// 2. Setup managers\nconst settingsManager = SettingsManager.create(cwd, agentDir);\nconst packageManager = createPackageManager({ cwd, agentDir });\n\n// 3. Create loader with config callback (captures firstPass for filters)\nconst loader = createLoader(async () => {\n const paths = await packageManager.resolve();\n return {\n ...paths,\n filters: {\n extensions: { enabled: !firstPass.noExtensions, additional: firstPass.extensions },\n skills: { enabled: !firstPass.noSkills },\n },\n };\n});\n\n// 4. Load early to discover extension flags\nconst loaded = await loader.load();\n\n// 5. Collect extension flags\nconst extensionFlags = new Map();\nfor (const ext of loaded.extensions.extensions) {\n for (const [name, flag] of ext.flags) {\n extensionFlags.set(name, { type: flag.type });\n }\n}\n\n// 6. Second pass parse with extension flags\nconst parsed = parseArgs(args, extensionFlags);\n\n// 7. Set flag values in runtime\nfor (const [name, value] of parsed.unknownFlags) {\n loaded.extensions.runtime.flagValues.set(name, value);\n}\n\n// 8. Create session\nconst session = await AgentSession.create({ ...loaded, model, sessionManager, settingsManager, modelRegistry, cwd });\n\n// 9. Run mode\nconst mode = new InteractiveMode(session, loader, parsed);\nawait mode.run();\n```\n\n---\n\n## Phase 4: Runner Initialization\n\nThe ExtensionRunner requires mode-specific initialization:\n\n1. `runner.initialize(actions, contextActions, commandContextActions, uiContext)`\n2. `runner.onError(handler)`\n3. `setupExtensionShortcuts(runner)` (interactive only)\n4. `runner.emit({ type: \"session_start\" })`\n\n**Solution: Runner Initializer Callback**\n\nSession stores a runner initializer provided by the mode. Called on create and reload.\n\n```typescript\ntype RunnerInitializer = (runner: ExtensionRunner) => void | Promise<void>;\n\nclass InteractiveMode {\n constructor(session, loader, parsed) {\n session.setRunnerInitializer(async (runner) => {\n runner.initialize(actions, contextActions, commandContextActions, uiContext);\n runner.onError((e) => this.showExtensionError(...));\n this.setupExtensionShortcuts(runner);\n });\n }\n \n async handleReload() {\n const newLoaded = await this.loader.load();\n // Transfer flag values from old runtime to new\n const oldRuntime = this.session.extensionRunner?.runtime;\n if (oldRuntime) {\n for (const [k, v] of oldRuntime.flagValues) {\n newLoaded.extensions.runtime.flagValues.set(k, v);\n }\n }\n // Session handles: shutdown → create runner → call initializer → session_start\n await this.session.reload(newLoaded);\n this.applyThemes(newLoaded.themes);\n }\n}\n```\n\n---\n\n## Phase 5: AgentSession Reload\n\n```typescript\nclass AgentSession {\n private runnerInitializer?: RunnerInitializer;\n \n setRunnerInitializer(init: RunnerInitializer) {\n this.runnerInitializer = init;\n if (this._extensionRunner && init) {\n init(this._extensionRunner).then(() => this._extensionRunner?.emit({ type: \"session_start\" }));\n }\n }\n \n async reload(loaded: LoadResult): Promise<void> {\n // 1. Shutdown old extensions\n await this._extensionRunner?.emit({ type: \"session_shutdown\" });\n \n // 2. Create new runner\n this._extensionRunner = new ExtensionRunner(loaded.extensions.extensions, loaded.extensions.runtime, ...);\n \n // 3. Update content\n this._skills = loaded.skills;\n this._promptTemplates = loaded.promptTemplates;\n // etc.\n \n // 4. Rebuild system prompt\n this._baseSystemPrompt = this._rebuildSystemPrompt(this.getActiveToolNames());\n this.agent.setSystemPrompt(this._baseSystemPrompt);\n \n // 5. Call mode initializer + emit session_start\n if (this.runnerInitializer) await this.runnerInitializer(this._extensionRunner);\n await this._extensionRunner.emit({ type: \"session_start\" });\n }\n}\n```\n\n---\n\n## Phase 6: Watch Mode\n\n`--watch` flag for auto-reload. Watches all paths from loader config. Debounces changes (100-200ms).\n\n---\n\n## CLI Flag Behavior on Reload\n\n- **CLI flags are fixed at startup** (`--no-extensions`, `--no-skills`, `-e`, etc.)\n- **settings.json changes are picked up** on reload\n- **New extension flags**: values undefined (CLI already parsed). Restart to use new flags.\n- **Flag values transfer**: old runtime values copied to new runtime\n\n---\n\n## Implementation Order\n\n1. Extract loader from sdk.ts\n2. Implement PackageManager with npm/git resolution\n3. Refactor main.ts to use new bootstrap flow\n4. Add RunnerInitializer pattern to AgentSession\n5. Implement session.reload()\n6. Package management commands (`pi install/remove/update`)\n7. Interactive mode `/reload` command\n8. Watch mode (`--watch` flag)\n","comments":[{"id":"IC_kwDOPbFNk87euJAd","author":{"login":"badlogic"},"authorAssociation":"OWNER","body":"## Files to Read Before Implementation\n\nBefore starting work on this issue, read the following files in full:\n\n### Core\n- `packages/coding-agent/src/core/sdk.ts` - Current session creation, discovery logic to extract\n- `packages/coding-agent/src/core/agent-session.ts` - Where reload() will be added\n- `packages/coding-agent/src/core/settings-manager.ts` - Settings structure, filters\n- `packages/coding-agent/src/core/extensions/runner.ts` - ExtensionRunner, initialize() pattern\n- `packages/coding-agent/src/core/extensions/index.ts` - Extension loading, discovery\n\n### Modes\n- `packages/coding-agent/src/main.ts` - Bootstrap flow, CLI parsing, two-pass pattern\n- `packages/coding-agent/src/modes/interactive/interactive-mode.ts` - Runner initialization, mode-specific setup\n- `packages/coding-agent/src/modes/print-mode.ts` - Simpler runner initialization\n- `packages/coding-agent/src/modes/rpc/rpc-mode.ts` - RPC runner initialization\n\n### Loading\n- `packages/coding-agent/src/core/skills.ts` - Skill loading, filters\n- `packages/coding-agent/src/core/prompt-templates.ts` - Prompt template loading\n- `packages/coding-agent/src/core/system-prompt.ts` - Context file loading (AGENTS.md)\n\n### CLI\n- `packages/coding-agent/src/cli/args.ts` - CLI argument parsing, extension flags\n\n### Docs\n- `packages/coding-agent/docs/extensions.md` - Extension discovery rules, package.json format\n- `packages/coding-agent/README.md` - Overview of extension locations","createdAt":"2026-01-12T01:55:40Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/issues/645#issuecomment-3736637469","viewerDidAuthor":true},{"id":"IC_kwDOPbFNk87gX7j8","author":{"login":"ben-vargas"},"authorAssociation":"CONTRIBUTOR","body":"Would still love to have the ability to have skills recursively traverse discovery similar to AGENTS.md recursion from #751 - any timeline for this rework? I am just having to maintain/use a dev branch that rebases and reapplies #751 on top of each release... would love to get back on to main version.","createdAt":"2026-01-17T21:36:28Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/issues/645#issuecomment-3764369660","viewerDidAuthor":false}],"labels":[{"id":"LA_kwDOPbFNk88AAAACQ8TbnQ","name":"pkg:coding-agent","description":"Coding agent CLI","color":"0e8a16"}],"state":"OPEN","title":"Extension package management and hot reload"}Planning parallel reading of linked issues and files
The plan is to fully read all comments and code in linked issues #326, #562, and #751, plus relevant files without offsets, using parallel reads to speed this up. Next, I'll sequence these operations using parallel calls to issue views and file reads.
>gh issue view 326 --json title,body,comments,labels,state{"body":"## Summary\n\nImplement a unified system for loading extensions (hooks, tools, skills, themes) from multiple sources:\n\n- **Local files/directories**: `./my-hook.ts`, `~/hooks/`\n- **npm packages**: `npm:pkg@version`, `npm:@scope/pkg@1.0.0`\n- **Git repositories**: `git:url@tag`, `git:url#branch`\n\n## Key Features\n\n- **Consistent entry points**: `index.ts` for hooks/tools, `SKILL.md` for skills, `*.theme.json` for themes\n- **Extension packs**: Pull in collections, filter to subset with glob patterns (`[\"audit-*\", \"!audit-debug\"]`)\n- **Settings hierarchy**: Global (`~/.pi/agent/settings.json`) + Project (`<cwd>/.pi/settings.json`) with additive paths\n- **Installation locations**:\n - CLI flags (`--hook`, `--tool`, etc.) → `/tmp/pi-extensions/` (ephemeral)\n - Global settings → `~/.pi/agent/<type>/` (permanent)\n - Project settings → `<cwd>/.pi/<type>/` (project-local)\n- **Atomic installation**: `<target>.installing/` renamed on success to prevent corruption from Ctrl+C\n- **Management commands**:\n - `pi install <type> <source>` - add to settings.json + install (global default, `-p` for project)\n - `pi remove <type> <name>` - remove from settings.json + delete\n - `pi update [types...]` - update npm/git extensions\n- **Runtime flags**: `--hook`, `--tool`, `--skill`, `--theme` (add sources), `--hooks`, `--custom-tools`, `--skills`, `--themes` (filter), `--no-hooks`, `--no-custom-tools`, `--no-skills` (disable)\n\n## Design Document\n\nFull design and implementation plan: [docs/extension-loading.md](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/extension-loading.md)","comments":[{"id":"IC_kwDOPbFNk87cJomA","author":{"login":"paulbettner"},"authorAssociation":"CONTRIBUTOR","body":"This is such a great idea. \n\nI'd love if this can also support for fully custom agents with their own system prompts and memory files as well, if that's possible.","createdAt":"2025-12-27T00:24:58Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/issues/326#issuecomment-3693513088","viewerDidAuthor":false},{"id":"IC_kwDOPbFNk87c7nkE","author":{"login":"badlogic"},"authorAssociation":"OWNER","body":"I kind of hate that custom tools and hooks are so separate from each other. Often, a tool has a companion hook. E.g. todo tool. Agent gets to modify todo list of the session. A separate hook lets the user do /todos to show the current status if they want to. \n\nE.g. tool writes state to session upon agent invocations:\nhttps://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/examples/custom-tools/todo/index.ts\n\nHook parses last state from session and displays it with custom UI:\nhttps://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/examples/hooks/todo/index.ts\n\nZero code sharing. Cumbersome on-disk folders.\n\nI kind of feel that tools should actually just be a call to `registerCustomTool()` on the `HookApi`. That would vastly simplify all the extension points (custom tools, hook events, hook slash commands, hook message renderers, etc.): into a single concept: hook.\n\nThe only downside to this is that the proposed filtering above would then no longer be file name based, which I kind of like, but instead would be runtime based (e.g. must load all hooks to know all custom tools, can only filter then). Which kind of makes it less obvious what's going on. It would also mean that if the setup code for custom-tools in a hook has side-effects, they'd be happening irrespective of whether the tool is then actually available to the agent or not based on the filters.\n\nThoughts? @mitsuhiko @aliou @nicobailon ?","createdAt":"2026-01-03T02:26:37Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[{"content":"EYES","users":{"totalCount":1}}],"url":"https://github.com/badlogic/pi-mono/issues/326#issuecomment-3706616068","viewerDidAuthor":true},{"id":"IC_kwDOPbFNk87c9n_b","author":{"login":"mrexodia"},"authorAssociation":"CONTRIBUTOR","body":"There was an initial implementation that worked pretty well here: https://github.com/can1357/oh-my-pi/tree/archive/plugin, but @can1357 has since decided to hard fork pi instead because he ran into extensibility limitations","createdAt":"2026-01-03T15:42:33Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/issues/326#issuecomment-3707142107","viewerDidAuthor":false},{"id":"IC_kwDOPbFNk87c-Hep","author":{"login":"aliou"},"authorAssociation":"CONTRIBUTOR","body":"Def like the idea of having a single place where everything is defined at once.\n\n> It would also mean that if the setup code for custom-tools in a hook has side-effects, they'd be happening irrespective of whether the tool is then actually available to the agent or not based on the filters.\n\nTo avoid this, could we not keep / require the `CustomToolFactory` to be defined / exported for each tool in the hook?\n\nBut thinking more about it, should we even allow filtering on tools defined in hooks? I feel like if those are tightly coupled with one needing the other, it might now really make sense to disable one and not the other.\n\nAs an example, I'm playing with `read_thread` / `find_thread` and having a hook write a custom entry with some metadata. If i disable the commands, there's no real need for the hook anymore imho. (this is not a super solid argument but haven't thought of other usecases yet)\n\n\n","createdAt":"2026-01-03T18:45:09Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/issues/326#issuecomment-3707271081","viewerDidAuthor":false},{"id":"IC_kwDOPbFNk87c-RCO","author":{"login":"badlogic"},"authorAssociation":"OWNER","body":"@mrexodia oh wow, he went full in on the hard fork. I don't even understand some of the features he implemented. Time to pack it up, folks :D","createdAt":"2026-01-03T19:49:35Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[{"content":"LAUGH","users":{"totalCount":2}}],"url":"https://github.com/badlogic/pi-mono/issues/326#issuecomment-3707310222","viewerDidAuthor":true},{"id":"IC_kwDOPbFNk87c-mC0","author":{"login":"mitsuhiko"},"authorAssociation":"CONTRIBUTOR","body":"> I kind of feel that tools should actually just be a call to registerCustomTool() on the HookApi. That would vastly simplify all the extension points (custom tools, hook events, hook slash commands, hook message renderers, etc.): into a single concept: hook.\n\nI'm definitely in favor but not sure about the file system part yet. amp loads tools by just being on the path that that caused one of my vibe coded skills to constantly load a chrome whenever I spawned Chrome. That wasn't a great experience because i could not figure out what did it.","createdAt":"2026-01-03T21:58:42Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/issues/326#issuecomment-3707396276","viewerDidAuthor":false},{"id":"IC_kwDOPbFNk87c-56y","author":{"login":"badlogic"},"authorAssociation":"OWNER","body":"@mitsuhiko What would you suggest as an alternative? fwiw, pi will tell you exactly what hooks/tools/skills get loaded on session start, which should help boil this kind of stuff down. Could also have a pi doctor which displays that info in even more detail.","createdAt":"2026-01-04T00:26:33Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/issues/326#issuecomment-3707477682","viewerDidAuthor":true},{"id":"IC_kwDOPbFNk87c-89K","author":{"login":"mitsuhiko"},"authorAssociation":"CONTRIBUTOR","body":"I don't have one. You're probably best off with sticking to that plan.","createdAt":"2026-01-04T00:46:34Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/issues/326#issuecomment-3707490122","viewerDidAuthor":false},{"id":"IC_kwDOPbFNk87c-9p9","author":{"login":"mrexodia"},"authorAssociation":"CONTRIBUTOR","body":"I guess I will move the discussion here instead of on Discord.\n\nWhat about doing `.pi/agent/extensions/<extension-id>/{hooks/tools/skills/subagents}`? \n\nAnd then virtually merge everything in `.pi/agent/{hooks/tools/skills/subagents/xyz(custom supporting files for new concepts)}`\n\nThat separates the concern of the extension loading mechanism from refactoring how hooks/tools are handled. For me it's best if an extension is just a folder that I can copy, without having to deal with any versioning/whatever magic JSON. For git you can `git clone` in that path, for npm it would be a symlink etc. and there are no restrictions on what the extension can put in its own folder. Updating extensions could be handled by pi too, since it can resolve the symlink to figure out the npm package name and check for a `.git` and fetch to see if there is a new tag/commit (depending on whether currently checked out on a branch or a tag).","createdAt":"2026-01-04T00:51:45Z","includesCreatedEdit":true,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/issues/326#issuecomment-3707492989","viewerDidAuthor":false},{"id":"IC_kwDOPbFNk87c--Dz","author":{"login":"badlogic"},"authorAssociation":"OWNER","body":"edit: \n\nI'm kind of gravitating towards the following:\n\n- extension is a unit to be installed (copy files to known folder, or pull in from NPM, or git)\n- extension can contain multiple of\n - hooks\n - custom tools (rolled into hooks\n - skills\n - themes\n- extensions have a package.json, with a custom field ala \"pi\": { themes: [\"path.json\", ...], hooks: [\"path.ts\", ...], skills: [\"path/\", ...]}\n\nExtensions are what you use for complex integrations that consist of multiple parts, not just a single .md/.ts/.json file. For complex hooks/custom tool combos and theme/skill packs, this is nice, because you can:\n\n- pull in dependencies via package.json\n- split up your source files\n- have multiple themes in a single package\n- have multiple skills in a single package\n\nAll versioned and easily pullable from NPM or a git repo. I kind of gravitate towards requiring version pining, as such extensions being mindlessly updated can wreck real havoc. But I digress.\n\nWe want to keep supporting the lower effort variants as well:\n- ~/.pi/agent/skills/someskill.md or ~/.pi/agent/skills/otherskill/SKILL.md + additional files\n- ~/.pi/agent/hooks/somehook.ts, or ~/.pi/agent/hooks/otherhook/ with a package.json defining the entry point in \"pi\" as shown above\n- ~/.pi/agent/themes/mytheme.json or ~/.pi/agent/themes/othertheme/*.json\n\nextensions would go into ~/.pi/agent/extensions/, each extension gets its own subdir by source type:\n\n- ~/.pi/agent/extensions/myextension, just plain files, sourced from wherever, not managed by pi except for loading/filtering\n- ~/.pi/agent/extensions/git/some/url/\n- ~/.pi/agent/extensions/npm/@optionalscope/packagename/version\n\npi would then have cli flags to manage extensions:\n\n- `pi inspect npm:@scope/package/@version|latest` or `pi inspect git:github.com/badlogic/foo@tag|commit`: reads package.json from the remote and lists what's in the extension. sucks, because tools are not rolled into hooks, which means we can't enumerate the tools without loading the hook .ts entry point ... so maybe tools still need to be their own thing.\n- `pi install|update|remove` whatever\n\nFiltering also needs to be a thing. I'm super unhappy with the complexity of it as outlined in the humongous vibed design doc.\n\nI guess I need a good night's sleep and start from scratch.","createdAt":"2026-01-04T00:55:04Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/issues/326#issuecomment-3707494643","viewerDidAuthor":true},{"id":"IC_kwDOPbFNk87c-_bb","author":{"login":"badlogic"},"authorAssociation":"OWNER","body":"@mrexodia so, for extensions pulled from npm, I'd actually not go through the global node_modules, but instead just pull the pakcage from npm unzip it into the extensions/<extension-id> folder, and run npm install. I find it icky to pollute a global package registry on disk of the user with stuff. Also, symlinks on Windows do kinda exist, but are wonky as well, and I'd rather not have to deal with that stuff. Sadly, I can't just ignore Windows, as I need it occassionally myself still.\n\n> What about doing .pi/agent/extensions/<extension-id>/{hooks/tools/skills/subagents}?\n\nYeah, that's basically the first variation of an extension outlined above. I do think a magic package.json file is still kind of needed to specify entry points and dependencies an extension might rely on that must also be installed. I would like to have both options: self-manage the folders/files however I see fit for some use cases (e.g. project specific extensions just pushed into the git repo), and extensions i can just pull down via pi install npm:whatever or pi install git:whatever.\n\nI guess I should just build it, see how it feels, throw it at you folks and get some feedback. Who'd have thunk building a package manager would need multiple comments on a GitHub issue.","createdAt":"2026-01-04T01:05:21Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[{"content":"ROCKET","users":{"totalCount":2}}],"url":"https://github.com/badlogic/pi-mono/issues/326#issuecomment-3707500251","viewerDidAuthor":true},{"id":"IC_kwDOPbFNk87dDx_g","author":{"login":"badlogic"},"authorAssociation":"OWNER","body":"Unification has been achieved https://github.com/badlogic/pi-mono/issues/454\n\nWith that out of the way, the remainder (publishing and pulling extensions from NPM) should be rather simple. To be continued.","createdAt":"2026-01-05T02:45:13Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[{"content":"THUMBS_UP","users":{"totalCount":1}},{"content":"HOORAY","users":{"totalCount":2}}],"url":"https://github.com/badlogic/pi-mono/issues/326#issuecomment-3708755936","viewerDidAuthor":true},{"id":"IC_kwDOPbFNk87dq78g","author":{"login":"Whamp"},"authorAssociation":"CONTRIBUTOR","body":"I've been following the unification work here and in #454, and think it is a huge improvement. I noticed Prompts (as in /prompts not system prompts) are curently missing from the proposed extension types list, which currently covers Hooks, Tools, Skills, and Themes.\n\nI think It would be a significant UX win to bundle prompts into extension loading/auto-discover and suite of commands like `/review`, `/debug`, or `/test` becomes available rather than requiring users to manually copy Markdown files into their local `prompts/` directory especially if the prompts are manual triggers for specific tools/skills in the extension.\n\nApologies if this was already implied by the discussion above but I know the skills unification into extensions is still ongoing, at least I think so, and I thought this would be a natural addition. \n","createdAt":"2026-01-07T13:58:37Z","includesCreatedEdit":true,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/issues/326#issuecomment-3719020320","viewerDidAuthor":false},{"id":"IC_kwDOPbFNk87et4Zp","author":{"login":"badlogic"},"authorAssociation":"OWNER","body":"Superseded by #645","createdAt":"2026-01-12T00:58:17Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/issues/326#issuecomment-3736569449","viewerDidAuthor":true}],"labels":[{"id":"LA_kwDOPbFNk88AAAACQ8TbnQ","name":"pkg:coding-agent","description":"Coding agent CLI","color":"0e8a16"}],"state":"CLOSED","title":"Unified extension loading system for hooks, tools, skills, and themes"}>gh issue view 562 --json title,body,comments,labels,state{"body":"Prompt templates, skills, themes, extensions, AGENTS.md, SYSTEM.md reloading via\n\n- /reload: manual\n- --watch: watch all extension locations for changes and auto-reload\n\nSee also https://github.com/badlogic/pi-mono/pull/508 which outlines this for prompt templates, skill, and themes\n\nDepends on #326 to be done. Do not implement before that issue is closed.","comments":[{"id":"IC_kwDOPbFNk87et4aH","author":{"login":"badlogic"},"authorAssociation":"OWNER","body":"Superseded by #645","createdAt":"2026-01-12T00:58:19Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/issues/562#issuecomment-3736569479","viewerDidAuthor":true}],"labels":[],"state":"CLOSED","title":"Extensions reloading"}>gh issue view 751 --json title,body,comments,labels,state{"body":"Extensions can now provide additional skill directories via the `skills_discover` event, which fires during skill discovery before the session starts.\n\n## Use Cases\n\n- Traversing ancestor directories for shared skills (monorepos, nested projects)\n- Dynamic skill directories based on project structure\n- Team/organization skill repositories\n- Environment-specific skill loading\n\n## Changes\n\n### New Extension Event\n- `skills_discover` event fires during skill discovery\n- Extensions return `{ additionalDirectories: string[] }` to add custom skill locations\n- Supports `~` expansion for home directory paths\n- Skills from extension directories are tagged with `source: \"extension\"`\n\n### Implementation\n- Add `SkillsDiscoverEvent` and `SkillsDiscoverResult` types\n- Add `emitSkillsDiscover()` method to `ExtensionRunner`\n- Reorder `sdk.ts` to load extensions before skill discovery\n- Add `extensionDirectories` option to `loadSkills()`\n- Guard: hook is not called when `options.skills` is explicitly provided (e.g., `--no-skills` or SDK usage)\n\n### Documentation\n- Updated `docs/skills.md` with \"Extension Hook\" section\n- Updated `docs/extensions.md` (exports)\n- Added example extension: `examples/extensions/recursive-skills.ts`\n\n### Example Usage\n\n```typescript\nexport default function(pi: ExtensionAPI) {\n pi.on(\"skills_discover\", (event, ctx) => {\n // event.cwd contains the current working directory\n return {\n additionalDirectories: [\n \"~/shared-skills\",\n \"/path/to/team/skills\"\n ]\n };\n });\n}\n```\n\n## Testing\n\n- Added tests in `test/sdk-skills.test.ts`:\n - Verifies skills from `skills_discover` are loaded with correct source\n - Verifies hook is not called when skills are explicitly provided\n- All 201 tests pass\n\n## Commits\n\n1. `feat: add skills_discover extension hook for custom skill directories` - Core implementation\n2. `fix(coding-agent): guard skills_discover when skills provided` - Skip hook when skills explicitly set\n3. `feat(coding-agent): add recursive skills discovery example` - Example extension","comments":[{"id":"IC_kwDOPbFNk87f3RfK","author":{"login":"badlogic"},"authorAssociation":"OWNER","body":"See #562 I have a different plan for this.","createdAt":"2026-01-15T16:42:48Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/pull/751#issuecomment-3755808714","viewerDidAuthor":true},{"id":"IC_kwDOPbFNk87f3UF0","author":{"login":"ben-vargas"},"authorAssociation":"CONTRIBUTOR","body":"Hh sweet, looking forward to it! Thanks Mario.","createdAt":"2026-01-15T16:44:13Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/pull/751#issuecomment-3755819380","viewerDidAuthor":false},{"id":"IC_kwDOPbFNk87f3Uf5","author":{"login":"badlogic"},"authorAssociation":"OWNER","body":"Eeks, wrong issue, I mean this one: https://github.com/badlogic/pi-mono/issues/645","createdAt":"2026-01-15T16:44:27Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/pull/751#issuecomment-3755821049","viewerDidAuthor":true},{"id":"IC_kwDOPbFNk87f3VTD","author":{"login":"ben-vargas"},"authorAssociation":"CONTRIBUTOR","body":"Yeah, I got to the right one following the references :)","createdAt":"2026-01-15T16:44:54Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/pull/751#issuecomment-3755824323","viewerDidAuthor":false}],"labels":[],"state":"CLOSED","title":"feat(coding-agent): add skills_discover extension hook for custom skill directories"}Planning full file reads for analysis
Resolving large file read conflict
The file exceeds 50KB, so to retrieve it fully I’ll read the remaining part using offsets despite instructions against offsets for full reads, since the size requires chunked reading. I’ll continue from line 1521 to get the complete file.
Continuing file read with offset
Proceeding to read the remaining agent-session file starting at offset 1521 without a limit, expecting to fetch additional segments if needed due to size.
Continuing file read at offset 1529
Continuing interactive mode implementation
Noting missing docs
Requesting remaining file content
Planning extension loader analysis
I'm preparing to read the extension loader code to understand how to extract it from the SDK per issue #645 and identify related implementation details. Next, I'll locate and inspect the loader file in the core extensions folder.
>ls /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/core/extensionsindex.ts
loader.ts
runner.ts
types.ts
wrapper.tsReviewing loader and types definitions
I'm reading loader.ts and types.ts fully to gather all necessary type definitions and understand their relationships for accurate typing and integration.
Analyzing package manager feature proposal
I'm outlining a design for a package manager with loader, reload, and watch features, identifying needed interface and main.ts changes plus runner reload support, while pinpointing open questions on settings storage, npm handling, and CLI integration. Next, I'll verify current extension loading and reload gaps by reviewing prompt templates and skills loading files.
Summarizing feature scope and open questions
I'm detailing #645 tasks covering package manager, loader, bootstrap flow, session reload, CLI commands, and watch mode, plus noting current extension/skill/prompt loading and absence of reload. I highlight unclear points on recursive skill discovery, compatibility with existing extension loading, settings merging semantics, npm install location, and theme loading, preparing precise questions for clarification.
Summarizing theme loading and extension management analysis
I've analyzed theme loading anchored by a custom themes directory and noted it would require new package-level loading support. The current extension loader focuses on local paths without package management. I'll outline the issue context, related tickets, current loader architecture, propose new PackageManager and Loader modules, and suggest CLI commands for package install/update/remove with options for organizing commands separately.
Detailing implementation approach and open questions
The current system lacks a unified loader abstraction, package manager, reload support, and CLI commands for package management. I propose creating a new core loader module to handle loading extensions, skills, prompts, themes, and context files from resolved paths with filtering, reusing existing extension loader logic where possible. A new PackageManager module would manage npm/git/local package sources and settings, handle installs/updates, and coordinate with SettingsManager for source persistence. Reload support would move initialization logic into AgentSession with a reload method triggering rebuilds of prompts, skills, and tools. Watch mode should gather relevant paths and run in interactive mode only.
Open questions include whether to install npm packages globally or in a local directory, how to migrate settings from paths to package sources, whether to retain automatic extension discovery alongside package manager resolution, how to handle overrides merging in SettingsManager, the scope of files and manifests the loader should handle (including SYSTEM.md and APPEND_SYSTEM.md), how and where to add CLI subcommands for install/remove/update, version pinning logic for git packages, and how to integrate new loaders with existing SDK/session runner initialization and event bus reuse. The response also notes affected files and the need for documentation updates.
Summary of issue #645 and linked items
Current code behavior
Concise implementation approach Phase 1: PackageManager
Phase 2: Loader
Phase 3: Bootstrap flow
Phase 4: Runner initializer and AgentSession reload
Phase 5: Reload and watch
Phase 6: Package management CLI
Affected files and changes needed
Open questions to finalize
If these are confirmed, I can move to implementation.
Extend the pi manifest handling to include prompts and themes and maybe context file paths. Current PiManifest includes skills and themes but loader only uses extensions. Add prompts and context entries to the manifest shape.
i think this also needs some talking about
Providing example and clarifications on settings and manifest
I'm preparing a concise example showing how local settings.json merges with system and appends for point 4. I'll outline that the manifest includes entries for prompts, themes, skills, and context, clarifying that the pi field controls priority or inclusion. I'll also confirm skill recursion is handled and raise an open question about default behavior for auto-discovery and settings merging, noting that watch mode applies in all cases.
Answers and details
Project .pi/settings.json: { "extensions": ["./project-ext"], "skills": { "includeSkills": ["docker-*"] } }
With current deep merge behavior, arrays override, not merge. Effective: extensions: ["./project-ext"] skills.includeSkills: ["docker-*"]
If you want additive merge for extensions, we need custom handling so project extensions are appended to global instead of replacing. Same question applies to skills includeSkills and ignoredSkills. Do you want additive merge or override for each array.
If loader owns it: Option A. Keep current behavior
Option B. Loader returns explicit prompt overrides
Option C. Hybrid
Given your note, Option A or C is minimal. Option B is a more complete centralization but adds more decisions to loader.
Skills recursion Yes. Current loadSkills searches ~/.pi/agent/skills and /.pi/skills recursively already. It also loads Codex and Claude locations with their specific formats.
Git update I will implement: if source has tag or commit, skip update when already at that ref. If no version, update to latest.
Manifest handling discussion Today PiManifest in extensions/loader.ts only checks pi.extensions, but the interface already includes themes and skills. It does not include prompts or context files.
We need to decide what pi field means for non extensions. Proposed options:
Option 1. pi field means explicit paths only, no auto discovery inside the package
Option 2. pi field is an augmentation, not a replacement
Option 3. pi field only for extensions, other types always use auto discovery
Given your earlier issue text, it sounds like Option 1 is intended. If no pi field, fall back to default discovery for extensions, skills, prompts, themes. If pi field exists, use only those entries.
Also need to decide path formats:
Open questions
Once you confirm, I will proceed to implementation.
as for merging extensions arrays, yeah that makes sense, merge local and global, warn on conflicts (same extension different version via npm or git for example).
what about themes and prompt templates?
that can stay as is, packaged extensions won't ship SYSTEM.md or AGENTS.md or APPEND_SYSTEM.md. that said, wouldn't it make sense to construct the system in such a way that the loader is responsible for ingesting and providing all the dynamic parts? extensions, skills, themes, prompt templates, AGENTS.md, APPEND_SYSTEM.md, SYSTEM.md? the default loader would just implement the current behaviour of looking in the default locations. A custom loader can provide those things whichever way it likes, no? Then main uses the default loader and passes that to createAgentSession in the SDK?
So i think that's also part of what the default loader will do. resolve skills in known locations, resolve skills from extension packages, provide them all as a nice list of SKILL.md files to load to createAgentSession?
approved
manifest handling i don't think we should include context files in manifests, but we must include prompts.
pi field means explicit paths only (either folders to which parts of auto-disocvery applies, or concrete files), full auto-discovery (like we do in ~/.pi/agent) only for extension packages without a pi field. those must then follow the folder and file layouts expected in ~/.pi/agent
path formats:
Is this consistent? got a better proposal?
open questions:
let's keep discussing based on the above, possibly give feedback and a proposal of the full thing
Proposing SDK loader and settings changes
I’ll clarify the SKILL.md vs SKILLS.md mismatch by standardizing on SKILL.md as the code uses that. For settings, only extension arrays merge with conflict warnings; themes and prompts don’t have array settings currently. Skills settings will remove include/ignored filters. I propose a new Loader interface returning a load result that includes discovered extensions, skills, prompts, themes, context files, and system prompt files, used as input to createAgentSession via options for preloaded data. The default loader will handle discovery as now. SettingsManager and the settings schema need updates to reflect skill settings removing include/ignored. For merging, extensions arrays merge locally and globally with warnings on conflicts; we should consider merging extension sources from the CLI with similar warnings. Custom skill directories remain and should merge additively with warnings on duplicates since filtering is removed. This balanced approach avoids ambiguous merges and improves extensibility.
Confirming manifest support and naming consistency
I’ll note that prompts and themes support manifests as files or directories with rules, with themes and prompts loaded globally and non-recursively for listing and application. I’ll confirm consistently using SKILL.md (not SKILLS.md) and suggest minor settings tweaks to align with these standards.
Notes and proposal based on your latest guidance
Clarification needed
Settings merge and removal of skill filters
Extensions array merging and conflicts
Themes and prompt templates in settings
Loader ownership of all dynamic parts This fits well. I suggest we formalize a Loader result that includes:
Default loader behavior
Custom loader behavior
SDK changes
Manifest handling proposal
Path formats based on your list This is consistent, with one change for naming.
If you want prompts or themes to be recursive within package manifests, say so. Otherwise I will keep non recursive to match the folder rules.
Open questions to finalize
Once these are locked, I can proceed to implementation.
A yes, SKILL.md
I want to get rid of the source flags. if people want to use skills from another harness, they should symlink or use customDirectories. i'm ok with this breaking change. Merging of global and project customDirectories with dedup is fine too. I have a feeling we won't need enabled. it's not used anywhere either afaikt! i.e. SettingsManager.getSkillsEnabled(). so that means skills can become a simple array like extensions as well? which references local files SKILL.md or folders (recursively find SKILL.mds), in addition to discovery in ~/.pi/agent/skills
themes and prompt templates, yes makes sense, tho i feel we should harmonize this with extensions and skills above? I kind of feel there's a general theme here we can apply to all 4 things: extensions, skills, prompts themes?
we auto-discovery in ~/.pi/agent and /.pi/
Then in addition to that auto discovery, settings.json can have
skills: ["folder", "file"] prompts: ["folder", "file"] themes: ["folder", "file"] extensions: ["folder", "file", "npm", "git"]
and the same rules as for auto-disovery apply? any skill, theme or prompt declared in an extension package (either by folder layout or via pi field in packages.json)
if people want to disable skills or extensions loading, they can use --no-skills and --no-extensions, no need to make the config super complicated
I think the loader should have getters to get the currently loaded extension runtimes, skill objects, prompttemplate objects, theme objects, contextFiles, systemPromptSource, and appendPromptSources
Loader then also has a reload() method that reloads all the things it was configured to load, subsequent getXXX() calls return the new objects. does that make sense?
the sdk then does ... nothing much. it just takes a loader instance which it binds to the AgentSession is creates, so the agent session can reload things at any time
so the loader really loads everything, including context files, system.md, etc. does that make sense? tui can also get access to the loader to set things up
new proposal
Proposing loader API with breaking change
I'm summarizing by proposing a concrete loader API design that includes a breaking change and asking for confirmation before implementation. The response avoids em dashes and remains concise.
Proposal aligned with your updated direction
Summary of behavior
Auto discovery rules
Paths listed in settings.json
Extension packages
Loader shape I propose a Loader interface like:
type LoaderConfig = { cwd: string; agentDir: string; settingsManager: SettingsManager; packageManager: PackageManager; filters?: { extensions?: { enabled?: boolean; include?: string[]; exclude?: string[] }; skills?: { enabled?: boolean }; prompts?: { enabled?: boolean }; themes?: { enabled?: boolean }; }; };
type LoadResult = { extensions: LoadExtensionsResult; skills: Skill[]; skillWarnings: SkillWarning[]; promptTemplates: PromptTemplate[]; themes: ThemeInfo[] | Theme[]; contextFiles: Array<{ path: string; content: string }>; systemPromptSource?: string; appendSystemPromptSource?: string; };
interface Loader { load(): Promise; getResult(): LoadResult | undefined; }
Notes
SDK changes
Questions to confirm
If this matches, I will proceed to implement exactly this shape.
that loader interface is fucking garbage.
i think what we need is something more like Resources interface
interface ResourceLoader { getExtensions(): ExtensionLoaderResult (prev LoadExtensionsResult) getSkills(): { skills: Skill[], warnings: SkillWarning[] } getPrompts(): { prompts: PromptTemplate[], warnings PromptTemplateWarning[] } getThemes(): { themes: Theme[], warnings: ThemeWarnings[] } getAgentsFiles(): { agentsFiles: { path: string, content: string }[] } getSystemPrompt(): string? getAppendSystemPrompt(): string[]? async reload(): ?? }
then we have a DefaultResourceLoader that implemnents our default semantics, people can subclass it, or wrap it?
and createAgentSession takes a ResourceLoader, and AgentSession exposes the ResourceLoader and has a reload() method that rewires everything, and possibly emits an event to tui or other parties when reloading has finished?
I want this to be as simple and clean as possible
Preparing concise revised proposal
I'm drafting a concise revised proposal that includes missing warning types and asks for clarifications while avoiding em dashes.
Understood. Here is a cleaner proposal aligned with your ResourceLoader shape.
Proposed interfaces
export interface ResourceLoader { getExtensions(): LoadExtensionsResult; getSkills(): { skills: Skill[]; warnings: SkillWarning[] }; getPrompts(): { prompts: PromptTemplate[]; warnings: PromptTemplateWarning[] }; getThemes(): { themes: Theme[]; warnings: ThemeWarning[] }; getAgentsFiles(): { agentsFiles: Array<{ path: string; content: string }> }; getSystemPrompt(): string | undefined; getAppendSystemPrompt(): string[]; // one or more fragments reload(): Promise<void>;}Notes
DefaultResourceLoader behavior
SDK and AgentSession changes
Open questions
If this is good, I will proceed and adjust the analysis for warnings types.
errors: Array<{ path: string; error: string }>
i think this shape is what we want for warnings/errors in general? maybe we just have warningsAndErrors, and add a type field "warning" | "error", an optional path (in case the resource wasn't laoded from disk), and a text: string?
Proposing warnings and errors format
I suggest a consistent shape for warnings and errors as objects with a code string and a message string. These should integrate into validation functions and UI feedback components. Please confirm this approach before I proceed.
Proposed unified diagnostics shape
export interface ResourceDiagnostic { type: "warning" | "error"; message: string; path?: string;}ResourceLoader getters return { data, diagnostics } for each resource type, or we add a single getDiagnostics that aggregates them.
Example per resource:
getSkills(): { skills: Skill[]; diagnostics: ResourceDiagnostic[] }This replaces skillWarnings and any future prompt or theme warnings.
Confirm if you want per resource diagnostics or one aggregated list.
DefaultResourceLoader behavior
note that we also have --extension and other cli flags that may add additional resources, no?
Confirming extension flags plan
I plan to extend the loader to accept extraPaths for extensions, skills, prompts, themes, and system prompt sources, appending from CLI input plus custom loader input. I'll ask for confirmation before proceeding.
Yes. DefaultResourceLoader should take CLI overrides too.
Proposal
For now only extensionSources and disable flags are used by CLI. This keeps it extensible without more CLI flags.
Confirm if you want CLI to also accept explicit prompt, theme, skill sources.
does shouldn't be overrides though, should they be? the string[] at least, no? those are in addition to the auto discovery shit?
Confirming additive semantics and discussion scope
Yes, the semantics are additive. I'll provide updated semantics and ask about --skills while keeping instructions not to implement but just discuss.
Agree. CLI lists should be additive.
Updated behavior
Confirm if you want --skills and --themes and --prompts flags to exist, or only extensions for now.
no aggreation, each getter returns diagnostics just for that resource tpe.
read @packages/coding-agent/src/cli/args.ts to see if we forgot something?
CLI args we need to update or remove
Proposal update for ResourceLoader + CLI
If you want to repurpose --skills to accept explicit paths, confirm and I will adjust parseArgs and help accordingly.
so, we already have --extension, i think we should have --skill (takes a SKILL.md) --theme (takes a theme.json) --prompt-template (takes a prompt template .md).
then we have --no-skills, --no-extensions --no-theme and --no-prompt-templates, and if we also specify --skill, --theme, --prompt-template, --extension, those get added back.
is that consistent?
Confirming parseArgs changes and ResourceLoader interface
I'll confirm the parseArgs updates are consistent and ask for final approval on the ResourceLoader interface. Then I'll provide the updated implementation plan.
Yes, consistent.
I will add:
Disable flags:
Behavior:
If this is final, I will proceed to implementation.
not yet, we now still need to figure out how package manager factors into all of this.
in settings, or via --extension, what if a user specifics npm: or git:? what should happen?
Proposing package source handling
Suggest storing source URLs with schemes in settings for the package manager to resolve to paths. For CLI -e with schemes like npm: or git:, propose ephemeral installs unless already present; install commands should modify settings to add sources. Clarify local versus global install behavior and whether -e triggers install prompts or loads existing packages.
We need to define how source specs are handled in two contexts: settings.json and CLI flags.
Proposed behavior
Open decisions A. Should CLI npm or git sources be auto installed into global locations, without touching settings.json.
B. Should CLI npm or git sources trigger a settings change, same as pi install.
I suggest A for flags, B for install command. That keeps flags ephemeral and install explicit.
Please confirm:
Also decide whether --skill, --prompt-template, --theme should accept npm or git or only filesystem paths. If we allow npm or git, the package must include pi field or follow auto discovery rules.
npm: and git: are only for extensions, which themselves then can contain extension entry points, skills, prompts, and themes
Confirming readiness for next steps
Understood.
Behavior update
This means --skill, --theme, --prompt-template only accept filesystem paths. Same for settings.json entries in skills, prompts, themes.
Anything else to finalize before implementation.
i think they should go into a /tmp folder just for that run?
the bigger question is: is package manager auto-invoked at any point? or is the user supposed to use the additional cli args to manually trigger pulling in an extension specified in settings.json?
Proposing package manager auto-invoke questions
I suggest confirming if the loader should automatically resolve and install missing packages based on user settings or remain manual. Should CLI runs leverage a temporary directory like /tmp for transient installs? Would the user prefer explicit prompts before any install or fully automatic handling?
Proposal
CLI npm or git
Auto invocation of PackageManager
Optional behavior
Confirm if auto install on startup is acceptable, or if you want a prompt or a flag to opt out.
well, installing via package manager may take a while, so whoever constructs the default loader may also want to show visual feedback in the tui ... so we need a kind of progress thing.
Proposing package manager events for progress hook
I'm suggesting implementing progress indication via package manager events or callbacks to efficiently track installation or update status without external tools.
We can add progress callbacks to PackageManager and ResourceLoader.
Proposal
type PackageProgress = { source: string; phase: "resolve" | "install" | "update" | "done"; message?: string };Interactive mode can pass a handler that updates a loader or status line.
If you want, I can add a lightweight interface like:
interface ResourceLoaderProgress { phase: "extensions" | "skills" | "prompts" | "themes" | "context" | "system" | "package"; message: string;}Confirm which shape you want.
what does package manager do concretely? where does it put the things?
Concrete PackageManager behavior proposal
Global install locations
npm root -g to find global node_modules. If source is npm:@scope/pkg@1.2.3, install via npm install -g @scope/pkg@1.2.3, then resolve path via node_modules/@scope/pkg.~/.pi/agent/git/<host>/<path>/<ref?> or similar. Use ~/.pi/agent/git/github.com/user/repo/ and checkout ref. If no ref, track default branch and update via fetch + reset.Local install locations
Temp installs for CLI flags
Actions
Pinning
If this matches, I will proceed.
what does package manager return to the loader then? would it itself be a resource loader?
Clarifying package manager role
The package manager returns resolved directory paths along with metadata but does not load resources itself; loaders consume these paths to fetch content. Progress reporting depends on the package manager implementation and usage context.
PackageManager is not a ResourceLoader. It is a dependency of DefaultResourceLoader.
PackageManager returns resolved paths for extension sources:
DefaultResourceLoader then applies extension discovery rules inside those directories, or uses pi field if present.
So the chain is: settings.json or CLI sources -> DefaultResourceLoader -> PackageManager.resolve -> resolved directories -> extension discovery -> ExtensionLoaderResult, skills, prompts, themes.
If you want PackageManager to expose a higher level helper like resolveExtensionSources to return arrays of paths, I can add that, but it still is not a ResourceLoader.
ok. what if the project local settings.json has npm in it? will that also be installed to the global npm modules?
Confirming global npm install decision
The npm modules will be installed globally following the npm root -g path, even if project local settings.json specifies npm. This behavior can be changed by adding a local install flag, but currently global install is the default. Should I proceed with this approach or adjust it?
With the current decision to use npm root -g, yes. Any npm: source in project settings would still install into the global npm modules directory.
If you want project settings to install into a project local cache instead, I can add a local mode. That would need a rule like: project settings use /.pi/npm/node_modules while global settings use npm root -g.
what did the original issue say about that?
Referencing issue content
The answer will note that the table distinguishes between global node_modules and local .pi/npm modules based on the issue summary.
Issue #645 specifies:
npm root -g.<cwd>/.pi/npm/node_modules/.So if we follow the issue, project settings would be local only when using the -l flag in install. Otherwise global.
ignore the -l flag and other shit we might add as an interface to the lci later. all that will do is modify the settings.json, either global or local. the rest will then be handled by the default "pipeline". no?
Confirming pipeline and install location
The pipeline will run as planned. Should global installation proceed even if local settings include npm? Current understanding is yes, global install is still needed unless otherwise specified.
Yes. The CLI install or other management commands only mutate settings.json, either global or local. After that, the default loader pipeline handles resolve and install on startup or reload. With npm root -g, both global and project settings will install into the global node_modules path unless we introduce a separate local install location later.
soo, can we install to ./pi/npm/node_modules easily? in case we find a npm: extension in the project local settings.json?
Confirming npm install approach
Suggest installing with npm using a prefix for local installs (npm --prefix /.pi/npm install) and npm root -g for global; ask which approach is preferred. Also mention branching on the settings origin to handle global defaults and consider handling CLI extension temp paths.
Yes. Use npm install --prefix <cwd>/.pi/npm <pkg> and resolve path from <cwd>/.pi/npm/node_modules/<pkg>. We can detect origin of the source. If it comes from project settings, use local install dir. If it comes from global settings, use npm root -g.
Confirm if you want this split behavior now.
confirmed.
so back to when package-manager is triggered. read @packages/coding-agent/src/main.ts where in this flow would we do that? in the defaultresourceloader? which i guess we construct on startup to get extension cli flags?
Confirming default loader use
The default loader handles detecting the package manager and initializing with settings, no-extensions flags, and CLI extension sources; it parses once, creates the loader, then calls reload to load extensions and flags with progress tracking. Confirm if this approach is acceptable.
Yes. The default ResourceLoader owns PackageManager and should be used in both passes.
Flow in main.ts:
This keeps the package manager inside the default loader and ensures it runs during the early extension flag discovery.
Confirm if you want step 5 to reuse the same loaded result or to reload after setting flags.
well, flags can't modify anything wrt to resources, can theyß
Acknowledging request
Ready for the next input; will indicate when to reload after responding.
Extension flags do not affect resource loading. We can load once in the first pass, then only apply flag values to the runtime before creating the session. No second reload needed unless you want to re-read settings.json after parsing non extension flags.
ok, and a user of DefaultResourceLoader would not have to cnstruct their own package manager, correct?
Confirming resource loader design
The plan is to have DefaultResourceLoader.create build the package manager internally while allowing injection for tests, with ResourceLoader exposed in the SDK.
Correct. DefaultResourceLoader should create and own its PackageManager internally. We can still accept an optional PackageManager for tests, but callers should not need to construct one.
alright. what about models.json? and keybindings.json? do we have more .json files that could live in ~/.pi/agent or /.pi? what do we have atm? i think resourceloader should handle all those things, no?
Identifying config source
I'm reading the main config file to locate the list of configuration paths or patterns, aiming to find relevant config file names or locations before listing or inspecting them.
Clarifying resource loading scope
The ResourceLoader should focus on runtime session-related resources like extensions, skills, prompts, themes, agents, and system prompts, excluding models, keybindings, and auth config since they're outside session-loaded resources. Keybindings likely live in the keybindings manager and can be read there if needed, so checking keybindings.json with a read tool in that context makes sense for safety.
Clarifying ResourceLoader scope and config files
ResourceLoader should only handle session-visible resources like extensions, skills, prompts, themes, context, and system prompts. Core config files such as models.json, auth.json, keybindings.json, and settings.json belong elsewhere and are not managed by ResourceLoader.
Defining ownership of core config files
Models.json, auth.json, and keybindings.json are managed respectively by ModelRegistry, AuthStorage, and KeybindingsManager; ResourceLoader should not handle them.
Current config and data files
Global (~/.pi/agent):
Project (/.pi):
Project context files outside .pi:
Recommendation ResourceLoader should only handle runtime resources that affect the session prompt and extension system:
It should not handle:
If you want ResourceLoader to handle settings.json too, that is a larger refactor. My suggestion is to keep settings in SettingsManager and let DefaultResourceLoader read from it.
no, that sounds good to me. alright, does it make sense to encode ALL our decisions above into a new comment on the issue 645 that describes everything in detail?
Confirming overview comment offer
I'll confirm that encoding all decisions into a detailed comment on issue 645 makes sense and offer to draft and post it, asking for approval before posting while noting no implementation will happen now.
Yes. A single comment on #645 that consolidates the final design and decisions will prevent drift. If you want, I can post it as a structured checklist covering ResourceLoader API, DefaultResourceLoader behavior, package manager rules, settings layout, CLI flags, and reload flow.
do that please. i suggest you first write to a local file, then upload that as a comment via gh cli
Drafting concise issue comment
I'm preparing a clear technical comment listing key decisions: ResourceLoader interface, diagnostic updates, default discovery changes, merged settings arrays with conflict checks, new CLI flags replacing --skills filter, npm root and local install behavior, temp use for CLI npm/git in extensions, and the two-pass reload flow. I'll avoid em dashes and ornate language for clarity before saving and posting the comment via GitHub CLI.
>gh issue comment 645 -F /Users/badlogic/workspaces/pi-mono/tmp/issue-645-comment.mdhttps://github.com/badlogic/pi-mono/issues/645#issuecomment-3770700174Comment posted: https://github.com/badlogic/pi-mono/issues/645#issuecomment-3770700174
If you want any edits to the comment, say which parts to adjust.
could you implement this based on just that comment? i HIGHLY doubt that ... add all the detalis needed, so the comment is self-conatined, contains all shapes, all files you need to read and modify, etc.
Planning detailed comment update
I'm clarifying that updating the external comment requires reading and modifying local files first, then posting a new GitHub issue comment rather than editing the existing one. I'll include specifics like which files to change, interface definitions, and flow details in the new comment.
>gh issue comment 645 -F /Users/badlogic/workspaces/pi-mono/tmp/issue-645-comment-v2.mdhttps://github.com/badlogic/pi-mono/issues/645#issuecomment-3770703260Posted expanded comment: https://github.com/badlogic/pi-mono/issues/645#issuecomment-3770703260
If you want any adjustments, list them and I will update with a follow-up comment.
export interface ResourceLoader { getExtensions(): LoadExtensionsResult; getSkills(): { skills: Skill[]; warnings: SkillWarning[] }; getPrompts(): { prompts: PromptTemplate[]; warnings: PromptTemplateWarning[] }; getThemes(): { themes: Theme[]; warnings: ThemeWarning[] }; getAgentsFiles(): { agentsFiles: Array<{ path: string; content: string }> }; getSystemPrompt(): string | undefined; getAppendSystemPrompt(): string[]; // one or more fragments reload(): Promise<void>;}export interface ResourceDiagnostic { type: "warning" | "error"; message: string; path?: string;}getSkills(): { skills: Skill[]; diagnostics: ResourceDiagnostic[] }type PackageProgress = { source: string; phase: "resolve" | "install" | "update" | "done"; message?: string };interface ResourceLoaderProgress { phase: "extensions" | "skills" | "prompts" | "themes" | "context" | "system" | "package"; message: string;}