From 039666e99ea014d45a4e4b645db6d8f397b5f23a Mon Sep 17 00:00:00 2001 From: Siddharth Date: Thu, 16 Apr 2026 20:36:56 +0530 Subject: [PATCH 01/10] docs: sprint 3 dep-resolution plan --- plug/PLAN.md | 252 +++++++++++++++++++++++++++++++++++++++++++ plug/progress.json | 35 ++++++ plug/requirements.md | 142 ++++++++++++++++++++++++ 3 files changed, 429 insertions(+) create mode 100644 plug/PLAN.md create mode 100644 plug/progress.json create mode 100644 plug/requirements.md diff --git a/plug/PLAN.md b/plug/PLAN.md new file mode 100644 index 0000000..8b2babe --- /dev/null +++ b/plug/PLAN.md @@ -0,0 +1,252 @@ +# plug — Sprint 3: Dependency Resolution + +> Teach the install/remove lifecycle about dependencies: extend tracker.js schema, add DFS resolver, wire resolver into install + TUI plan screen, add dependent check on remove, and add post-install hook notice. + +**Base branch:** `main` +**Feature branch:** `sprint/dep-resolution` + +--- + +## Tasks + +### Phase 1: Extend Tracker (A1) + +#### Task 1.1: Add schema fields + new helpers to tracker.js +- **Change:** In `src/utils/tracker.js`: + - Persist three new fields in `trackInstall`: `installed_as: metadata.installed_as ?? 'explicit'`, `dependencies: metadata.dependencies ?? []`, `dependents: metadata.dependents ?? []` + - Normalise on any read path touching `rec.installed_as`: `rec.installed_as ?? 'explicit'` + - Add four new exported functions: + - `updateDependents(name, dependents, global)` — overwrites `data.installed[name].dependents`; saves + - `getInstalledRecord(name, global)` — returns `data.installed[name] ?? null` + - `prunableOrphans(global)` — returns names where `installed_as === 'dependency' && dependents.length === 0` + - `removeDependentEdge(fromName, toName, global)` — removes `fromName` from `data.installed[toName].dependents`; saves +- **Files:** `src/utils/tracker.js` +- **Tier:** standard +- **Done when:** `trackInstall` persists new fields; all four helpers exported and functional; backward-compat normalisation in place +- **Blockers:** Read current `src/utils/tracker.js` and `src/utils/tracker.test.js` before modifying — match file-write patterns exactly + +#### Task 1.2: Extend tests/tracker.test.js +- **Change:** Add test cases (do NOT create a new file): + - `trackInstall` with `installed_as: 'dependency'` persists correctly + - `trackInstall` without `installed_as` defaults to `'explicit'` + - `updateDependents` mutates only the targeted record + - `prunableOrphans` returns correct subset + - `removeDependentEdge` removes back-reference correctly +- **Files:** `tests/tracker.test.js` +- **Tier:** standard +- **Done when:** all five new test cases pass; existing tests unaffected +- **Blockers:** none + +#### VERIFY: Phase 1 +- Run full `npm test` — zero failures +- Confirm `trackInstall` stores new fields; `prunableOrphans` returns correct results +- Commit: `feat(tracker): extend installed.json with installed_as/dependencies/dependents` — one commit, includes tracker.js + tracker.test.js changes +- Push to `sprint/dep-resolution` + +--- + +### Phase 2: Resolver (A2) + +#### Task 2.1: Create src/utils/community-index.js (minimal fetch+cache wrapper) +- **Change:** Create a minimal `src/utils/community-index.js` if it does not already exist from Sprint 2. This is a thin wrapper needed by the resolver. Export: + - `COMMUNITY_INDEX_URL = 'https://raw.githubusercontent.com/dsiddharth2/plugvault/main/community-index.json'` + - `fetchCommunityIndex()` — fetch + 1hr cache, mirrors `fetchRegistry` pattern using `getCacheDir()` + - `getStaleCommunityIndexCache()` — offline fallback + - Cache key: `community-index.json` in same `getCacheDir()` as vault caches + - **IMPORTANT:** If Sprint 2 already landed `src/utils/community-index.js`, read it first and only add/adjust what is missing. Do not overwrite Sprint 2 work. +- **Files:** `src/utils/community-index.js` +- **Tier:** standard +- **Done when:** `fetchCommunityIndex` and `getStaleCommunityIndexCache` exported and functional; module imports cleanly +- **Blockers:** Check if file exists from Sprint 2 first (`git show origin/main:src/utils/community-index.js`) + +#### Task 2.2: Create src/utils/resolver.js +- **Change:** New file implementing DFS dependency resolver. Public API: + ```js + export async function resolve(pkgName, vaultHint = null, options = {}) + // returns Promise<{ toInstall, alreadySatisfied, cycles }> + ``` + Private helpers: + - `buildPackageMap()` — fetch official registry.json → Map; fetch community-index.json → add/override; community wins on name conflict; both catch-wrapped (offline-safe) + - `dfsResolve(name, packageMap, installedSnapshot, toInstall, alreadySatisfied, cycles, visited, inStack)` + + Algorithm: + 1. `packageMap = await buildPackageMap()` + 2. `installedSnapshot = await getInstalled(isGlobal)` — load ONCE; pass into DFS — never call per-node + 3. DFS: `inStack` hit → `cycles.push(name)`, return; `visited` hit → return; for each dep where `required !== false`: DFS(dep.name); after deps: check snapshot → alreadySatisfied or toInstall; mark visited + 4. Result is dep-first topological order; last element is always `pkgName` +- **Files:** `src/utils/resolver.js` (new) +- **Tier:** premium +- **Done when:** `resolve` exported; DFS correctly handles direct deps, transitive deps, cycles, optional deps, already-installed; `getInstalled` called once per resolve +- **Blockers:** Read `src/utils/registry.js` and `src/utils/tracker.js` for import patterns before writing + +#### Task 2.3: Create tests/resolver.test.js +- **Change:** New test file. Mock: `vi.mock('../src/utils/registry.js')`, `vi.mock('../src/utils/community-index.js')`, `vi.mock('../src/utils/tracker.js')`. Cover: + - No deps → `toInstall: [pkg]`, `alreadySatisfied: []`, `cycles: []` + - Single required dep → `toInstall: [dep, pkg]` (dep first) + - Dep already installed → `alreadySatisfied: [dep]`, `toInstall: [pkg]` + - Transitive A→B→C → `toInstall: [C, B, A]` + - Cycle A→B→A → `cycles: ['A']`, `toInstall: [B, A]` + - Optional dep (`required: false`) NOT added to `toInstall` + - Unknown package in deps → silently skipped, no crash + - All deps already installed → `toInstall: [pkg]`, `alreadySatisfied: [all deps]` +- **Files:** `tests/resolver.test.js` (new) +- **Tier:** standard +- **Done when:** all 8 test cases pass +- **Blockers:** none + +#### VERIFY: Phase 2 +- Run full `npm test` — zero failures +- Confirm resolver handles transitive + cycle cases correctly +- Commit: `feat(resolver): add DFS dependency resolver` — includes resolver.js + community-index.js (if new/changed) + resolver.test.js +- Push to `sprint/dep-resolution` + +--- + +### Phase 3: Wire Install + TUI Plan Screen (A3 + A6) + +#### Task 3.1: Refactor install.js — extract helper + wire resolver +- **Change:** In `src/commands/install.js`: + 1. Extract `installSinglePackage(pkgSpec, isGlobal)` private helper: + - If `pkgSpec.rawBaseUrl`: `fetch(rawBaseUrl + entry)` directly + - Else: use existing `downloadFile(vault, ...)` path + 2. After finding the root package, before downloading: + - `const plan = await resolve(pkgName, vault.name, { global: isGlobal })` + - If CLI path and `plan.toInstall.length > 1`: print plan summary; if `!ctx.yes`, prompt "Proceed? (Y/n)" + 3. Loop `plan.toInstall` in order, calling `installSinglePackage(entry, isGlobal)` for each + 4. Track each dep with `installed_as: 'dependency'`; root with `installed_as: 'explicit'`; both include `dependencies: [direct dep names]` + 5. After all installs: call `updateDependents(dep.name, [pkgName], isGlobal)` for each dep + 6. Add `vi.mock('../src/utils/resolver.js', () => ({ resolve: vi.fn().mockResolvedValue({ toInstall: [], alreadySatisfied: [], cycles: [] }) }))` at top of `tests/install.test.js` +- **Files:** `src/commands/install.js`, `tests/install.test.js` +- **Tier:** premium +- **Done when:** resolver called on install; deps tracked as `installed_as: 'dependency'`; root as `'explicit'`; `updateDependents` called; already-satisfied deps skipped; `installSinglePackage` branches on `rawBaseUrl` +- **Blockers:** Read current install.js fully before modifying — identify where to inject resolver call and where `trackInstall` is called + +#### Task 3.2: New TUI component src/tui/components/install-plan.jsx +- **Change:** New Ink component. Props: `{ queue, plan, loading, onConfirm(scope: 'project'|'global'), onCancel }`. Renders: + - Spinner while `loading` + - "Will install" section listing `plan.toInstall` with vault + type + - "Already satisfied" section listing `plan.alreadySatisfied` + - Scope selector: `◉ Project (.claude/) ○ Global (~/.claude/)` — Tab toggles + - Footer: `[i] Install [Esc] Cancel` +- **Files:** `src/tui/components/install-plan.jsx` (new) +- **Tier:** standard +- **Done when:** component renders correctly in test; Tab toggles scope; `onConfirm` called with correct scope; `onCancel` called on Esc +- **Blockers:** Read existing TUI components for Ink/React patterns before writing + +#### Task 3.3: Wire plan view in discover.jsx + tests +- **Change:** + 1. Add `'plan'` state to `DiscoverView` union in `src/tui/screens/discover.jsx` + 2. When `startInstall(queue)` called: run `resolve()` in `useEffect`/`useState`; if `plan.toInstall.length > 1` → set view `'plan'`, render ``; on `onConfirm(scope)` → `doInstall(queue, { global: scope === 'global' })`; if single package → `doInstall(queue)` immediately (current behavior preserved) + 3. `ctx.set({ yes: true, json: true })` must only be called inside `doInstall()` after user confirms — NOT during resolver async phase + 4. Extend `tests/tui/discover.test.jsx`: plan view shown after pressing `i` on package with deps; skipped for no-dep package; Tab toggles scope; Esc returns to list +- **Files:** `src/tui/screens/discover.jsx`, `tests/tui/discover.test.jsx` +- **Tier:** standard +- **Done when:** plan view appears for deps, skipped for single package; scope toggle works; Esc cancels; tests pass +- **Blockers:** Read current discover.jsx fully to understand view state machine before adding `'plan'` state + +#### VERIFY: Phase 3 +- Run full `npm test` — zero failures (includes install + discover test extensions) +- Manual CLI: `plug install subagent-driven-development` → resolver prints plan, deps installed first +- Manual CLI: `plug install senior-engineer` (no deps) → installs immediately, no plan screen +- Manual TUI: package with deps → plan screen + scope toggle; no-dep package → installs immediately +- Commit: `feat(install): wire resolver + TUI scope toggle on dep installs only` — includes install.js + install-plan.jsx + discover.jsx + test updates +- Push to `sprint/dep-resolution` + +--- + +### Phase 4: Wire Remove (A4) + +#### Task 4.1: Add dependent check + cascade/force/prune to remove.js +- **Change:** In `src/commands/remove.js` after loading `pkg`: + 1. `const dependents = pkg.dependents ?? []` + 2. If `dependents.length > 0`: `select` prompt (Cancel / Remove all cascade / Force remove) + - Cancel: return early + - Cascade: `runRemove(dep, { global, _cascade: true })` for each dependent first, then target + - Force: delete target file + `trackRemove(name)` + `removeDependentEdge(name, dep, global)` for each dependent + 3. After any successful remove: call `prunableOrphans(isGlobal)`; if orphans exist and `!ctx.yes`: prompt to prune; if yes: `runRemove` each orphan +- **Files:** `src/commands/remove.js` +- **Tier:** standard +- **Done when:** dependents check fires; Cancel/Cascade/Force all work; orphan prune prompt fires; `--yes` auto-prunes +- **Blockers:** Read current remove.js fully before modifying + +#### Task 4.2: Extend tests/remove.test.js +- **Change:** Add test cases: + - Package with dependents triggers `select` prompt + - Cancel leaves both packages in place + - Cascade removes target and all dependents + - Force removes only target, updates dependent records + - After remove, orphan prompt fires if orphans exist + - `--yes` flag auto-prunes orphans without prompting +- **Files:** `tests/remove.test.js` +- **Tier:** standard +- **Done when:** all 6 test cases pass; existing tests unaffected +- **Blockers:** none + +#### VERIFY: Phase 4 +- Run full `npm test` — zero failures +- Manual CLI: `plug remove` on package with dependents → warning fires; orphan prune prompt shown after +- Commit: `feat(remove): dependent check, cascade/force, orphan pruning` +- Push to `sprint/dep-resolution` + +--- + +### Phase 5: Post-Install Hook Notice (A5) + +#### Task 5.1: Create src/utils/frontmatter.js + wire into install.js +- **Change:** + 1. New file `src/utils/frontmatter.js`: + ```js + export function parseFrontmatter(content) + // Regex: match ---\n...\n--- block, split on newlines, parse key: value pairs, handle CRLF + // Returns Record or {} if none/malformed + ``` + 2. In `src/commands/install.js` after writing SKILL.md: call `parseFrontmatter(content)`; if `fm.hook || fm.hooks`: + - CLI: print `⚠ Hook required: '${name}' expects a hook in settings.json` + - JSON mode (`ctx.json`): include `hookRequired: true` in output object +- **Files:** `src/utils/frontmatter.js` (new), `src/commands/install.js` +- **Tier:** standard +- **Done when:** `parseFrontmatter` exports correctly; hook notice fires on install for skill with `hook:` frontmatter; no notice for skills without it +- **Blockers:** none + +#### Task 5.2: Create tests/frontmatter.test.js +- **Change:** New test file covering: + - Standard `---\nkey: value\n---` → correct object + - No frontmatter → `{}` + - Malformed (no closing `---`) → `{}` + - CRLF line endings handled + - `hook:` field parsed correctly +- **Files:** `tests/frontmatter.test.js` (new) +- **Tier:** standard +- **Done when:** all 5 test cases pass +- **Blockers:** none + +#### VERIFY: Phase 5 +- Run full `npm test` — all existing + new tests green (257+ tests from Sprint 2 + new additions) +- Manual: Install a skill with `hook:` frontmatter → warning printed +- Manual: Install a skill without hook frontmatter → no warning +- Regression: Sprint 1 + Sprint 2 fixes still work; inspect installed.json after multi-dep install — all fields correct +- Commit: `feat(install): post-install hook notice` +- Push to `sprint/dep-resolution` + +--- + +## Risk Register + +| Risk | Impact | Mitigation | +|------|--------|------------| +| resolver.js calls `isInstalled()` per-node — file read on every dep | HIGH | Load `getInstalled()` once per `resolve()` call, pass snapshot into DFS. Covered by test: verify only 1 `getInstalled` call per resolve | +| Community package install path: `rawBaseUrl` + `entry` instead of vault owner/repo | HIGH | `installSinglePackage` branches on `pkgSpec.rawBaseUrl` presence. Intentional vault-abstraction bypass for Sprint 3; B3 adapter tightens in future sprint | +| `ctx.set({ yes, json })` called during resolver async phase (plan view) — state pollution | MED | `ctx.set` must only be inside `doInstall()`, called after user confirms. Test: mock resolver to be slow; assert ctx not mutated until confirm | +| install.test.js mock layering — existing mocks + new resolver mock clash | MED | Add `vi.mock('../src/utils/resolver.js', ...)` at top of install.test.js. Per-test override via `resolve.mockResolvedValueOnce(...)` | +| Sprint 2's `community-index.js` shape differs from what resolver expects | MED | Task 2.1 explicitly checks existing file before writing. Read `src/utils/community-index.js` on main before modifying | + +--- + +## Notes + +- 5 commits total — one per phase. See requirements.md commit strategy. +- VERIFY tasks are checkpoints — doer stops, PM dispatches reviewer before next phase. +- Base branch: `main`. Create `sprint/dep-resolution` from `origin/main`. +- Sprint 2's `community-index.js` already exists — Task 2.1 must NOT overwrite it. +- Do NOT bump Ink, Node engines, or `package.json` version. +- Regression: Sprint 1 + Sprint 2 fixes (non-TTY guard, per-skill subdirs, alt-screen, paste, community discover) must not regress. diff --git a/plug/progress.json b/plug/progress.json new file mode 100644 index 0000000..20bb602 --- /dev/null +++ b/plug/progress.json @@ -0,0 +1,35 @@ +{ + "_schema": { + "type": "work | verify", + "status": "pending | completed | blocked" + }, + "project": "plug", + "plan_file": "PLAN.md", + "sprint": "dep-resolution", + "branch": "sprint/dep-resolution", + "base_branch": "main", + "created": "2026-04-16", + "tasks": [ + { "id": "1.1", "phase": 1, "step": "Extend trackInstall with installed_as/dependencies/dependents fields; add updateDependents, getInstalledRecord, prunableOrphans, removeDependentEdge to tracker.js", "type": "work", "status": "pending", "tier": "standard", "commit": "", "notes": "" }, + { "id": "1.2", "phase": 1, "step": "Extend tests/tracker.test.js with 5 new cases: installed_as persist, default, updateDependents mutation, prunableOrphans subset, removeDependentEdge back-reference", "type": "work", "status": "pending", "tier": "standard", "commit": "", "notes": "" }, + { "id": "1.V", "phase": 1, "step": "VERIFY Phase 1: npm test green; commit 'feat(tracker): extend installed.json with installed_as/dependencies/dependents'; push sprint/dep-resolution", "type": "verify", "status": "pending", "tier": "cheap", "commit": "", "notes": "" }, + + { "id": "2.1", "phase": 2, "step": "Check if community-index.js exists from Sprint 2; if missing create minimal fetch+cache wrapper; if exists confirm fetchCommunityIndex + getStaleCommunityIndexCache exported", "type": "work", "status": "pending", "tier": "standard", "commit": "", "notes": "" }, + { "id": "2.2", "phase": 2, "step": "Create src/utils/resolver.js with buildPackageMap + DFS resolver; load getInstalled once per resolve call; resolve() public API returns { toInstall, alreadySatisfied, cycles }", "type": "work", "status": "pending", "tier": "premium", "commit": "", "notes": "" }, + { "id": "2.3", "phase": 2, "step": "Create tests/resolver.test.js covering 8 cases: no deps, single dep, already installed, transitive, cycle, optional dep, unknown dep, all deps installed", "type": "work", "status": "pending", "tier": "standard", "commit": "", "notes": "" }, + { "id": "2.V", "phase": 2, "step": "VERIFY Phase 2: npm test green; commit 'feat(resolver): add DFS dependency resolver'; push sprint/dep-resolution", "type": "verify", "status": "pending", "tier": "cheap", "commit": "", "notes": "" }, + + { "id": "3.1", "phase": 3, "step": "Refactor install.js: extract installSinglePackage helper (branch on rawBaseUrl); wire resolve() call before download; loop toInstall; track installed_as correctly; call updateDependents; extend tests/install.test.js with resolver mock + dep-tracking cases", "type": "work", "status": "pending", "tier": "premium", "commit": "", "notes": "" }, + { "id": "3.2", "phase": 3, "step": "Create src/tui/components/install-plan.jsx: spinner, will-install list, already-satisfied list, scope selector (Tab toggles), footer [i]/[Esc]", "type": "work", "status": "pending", "tier": "standard", "commit": "", "notes": "" }, + { "id": "3.3", "phase": 3, "step": "Wire 'plan' view state in discover.jsx; plan screen only when toInstall.length > 1; ctx.set only after confirm; extend tests/tui/discover.test.jsx with plan view + scope toggle + cancel cases", "type": "work", "status": "pending", "tier": "standard", "commit": "", "notes": "" }, + { "id": "3.V", "phase": 3, "step": "VERIFY Phase 3: npm test green; manual CLI single-pkg (no plan screen) + multi-dep (plan screen); commit 'feat(install): wire resolver + TUI scope toggle on dep installs only'; push", "type": "verify", "status": "pending", "tier": "standard", "commit": "", "notes": "" }, + + { "id": "4.1", "phase": 4, "step": "Wire remove.js: check dependents; select prompt (Cancel/Cascade/Force); cascade removes dependents then target; force removes target only + removeDependentEdge; orphan prune prompt after any remove", "type": "work", "status": "pending", "tier": "standard", "commit": "", "notes": "" }, + { "id": "4.2", "phase": 4, "step": "Extend tests/remove.test.js with 6 cases: dependents trigger prompt, cancel, cascade, force, orphan prompt, --yes auto-prune", "type": "work", "status": "pending", "tier": "standard", "commit": "", "notes": "" }, + { "id": "4.V", "phase": 4, "step": "VERIFY Phase 4: npm test green; manual CLI remove with dependents; commit 'feat(remove): dependent check, cascade/force, orphan pruning'; push", "type": "verify", "status": "pending", "tier": "cheap", "commit": "", "notes": "" }, + + { "id": "5.1", "phase": 5, "step": "Create src/utils/frontmatter.js with parseFrontmatter(); wire into install.js: after writing SKILL.md, parse frontmatter, print hook warning (CLI) or set hookRequired:true (JSON mode)", "type": "work", "status": "pending", "tier": "standard", "commit": "", "notes": "" }, + { "id": "5.2", "phase": 5, "step": "Create tests/frontmatter.test.js with 5 cases: standard parse, no frontmatter, malformed, CRLF, hook field", "type": "work", "status": "pending", "tier": "standard", "commit": "", "notes": "" }, + { "id": "5.V", "phase": 5, "step": "VERIFY Phase 5: all tests green; manual hook notice check; regression Sprint 1+2; inspect installed.json; commit 'feat(install): post-install hook notice'; push", "type": "verify", "status": "pending", "tier": "standard", "commit": "", "notes": "" } + ] +} diff --git a/plug/requirements.md b/plug/requirements.md new file mode 100644 index 0000000..4bf3396 --- /dev/null +++ b/plug/requirements.md @@ -0,0 +1,142 @@ +# Sprint 3 — Dependency Resolution Requirements + +## Overview + +The plug CLI currently installs packages one at a time with no dependency awareness. Running `plug install subagent-driven-development` installs only that package, leaving `using-git-worktrees` and `writing-plans` silently missing. Sprint 3 teaches the install/remove lifecycle about dependencies. + +**Depends on:** Sprint 2 (community-discover) — `community-index.js` must be in place. Sprint 2 PR #17 is open and CI green; this sprint branches from `main` after merge. + +--- + +## Problem Statement + +1. **No dep resolution on install** — packages with dependencies listed in community-index.json are installed in isolation; dependents silently missing. +2. **No dep tracking in installed.json** — `installed.json` has no `installed_as`, `dependencies`, or `dependents` fields; no way to distinguish explicit vs transitive installs. +3. **No dep check on remove** — removing a package that other packages depend on gives no warning; orphaned deps are left behind. +4. **No post-install hook notice** — skill frontmatter may declare required hooks but install never surfaces this to the user. + +--- + +## Scope + +### A1 — Extend tracker.js +Add `installed_as`, `dependencies`, `dependents` fields to `installed.json` schema. Existing records normalised on read (no migration). Add four new functions: `updateDependents`, `getInstalledRecord`, `prunableOrphans`, `removeDependentEdge`. + +Schema addition per record: +```json +{ + "api-patterns": { + "type": "skill", + "vault": "official", + "version": "1.0.0", + "path": "...", + "installedAt": "...", + "installed_as": "explicit", + "dependencies": ["senior-engineer"], + "dependents": [] + } +} +``` + +`trackInstall` gains three optional fields in `metadata`: +```js +installed_as: metadata.installed_as ?? 'explicit', +dependencies: metadata.dependencies ?? [], +dependents: metadata.dependents ?? [], +``` + +New functions: +- `updateDependents(name, dependents, global)` — sets `data.installed[name].dependents = dependents`; saves +- `getInstalledRecord(name, global)` — returns `data.installed[name] ?? null` +- `prunableOrphans(global)` — names where `installed_as === 'dependency' && dependents.length === 0` +- `removeDependentEdge(fromName, toName, global)` — removes `fromName` from `data.installed[toName].dependents`; saves + +**Backward-compat:** any read path touching `rec.installed_as` must normalise: `rec.installed_as ?? 'explicit'`. + +### A2 — resolver.js (new file) +DFS dependency resolver at `src/utils/resolver.js`. + +Public API: +```js +export async function resolve(pkgName, vaultHint = null, options = {}) +// returns { toInstall, alreadySatisfied, cycles } +``` + +Algorithm: +1. `buildPackageMap()` — fetch official registry.json then community-index.json; community wins on name conflict; both catch-wrapped (offline-safe) +2. Load `getInstalled()` once, pass snapshot into DFS — never call per-node +3. DFS: back-edge → cycle; already-visited → skip; after deps: check snapshot → alreadySatisfied or toInstall; mark visited +4. Result is naturally dep-first (topological), last element is pkgName + +`community-index.js` minimal version (fetch + 1hr cache) is needed even though full B3 normaliser adapter lands in a later sprint. Import `fetchCommunityIndex` from `src/utils/community-index.js` — this already exists from Sprint 2. + +### A3 — Wire install.js + A6 TUI scope toggle +Extract `installSinglePackage(pkgSpec, isGlobal)` private helper from `src/commands/install.js`. + +Wiring in install: +1. Call `resolve(pkgName, vault.name, { global: isGlobal })` +2. If `plan.toInstall.length > 1`: print plan summary; if `!ctx.yes`, prompt "Proceed? (Y/n)" +3. Loop `plan.toInstall` calling `installSinglePackage` +4. Track each dep with `installed_as: 'dependency'`; root with `installed_as: 'explicit'`; both include `dependencies: [direct dep names]` +5. After all installs: call `updateDependents(dep.name, [pkgName], isGlobal)` for each dep + +New TUI component `src/tui/components/install-plan.jsx`: +- Props: `{ queue, plan, loading, onConfirm(scope), onCancel }` +- Renders: spinner while loading; "Will install" list; "Already satisfied" list; scope selector (Tab toggles); footer `[i] Install [Esc] Cancel` + +discover.jsx `'plan'` state: if `plan.toInstall.length > 1` → plan screen; if single package → install immediately (current behavior). + +**Scope toggle UX decision:** plan screen only appears when resolver finds additional packages. Single packages with no deps install immediately — current behavior preserved. + +### A4 — Wire remove.js +After loading pkg from `src/commands/remove.js`: +1. Check `pkg.dependents ?? []` +2. If non-empty: `select` prompt (Cancel / Remove all cascade / Force remove) +3. After any remove: `prunableOrphans()` + prune prompt; `--yes` auto-prunes + +### A5 — Post-install hook notice +New `src/utils/frontmatter.js`: +```js +export function parseFrontmatter(content) +// returns Record or {} if none/malformed +``` +Regex: `---\n...\n---` block, parse `key: value` pairs, handle CRLF. + +In `install.js` after writing SKILL.md: if `fm.hook || fm.hooks` → warn CLI or set `hookRequired: true` in JSON mode. + +--- + +## Out of Scope + +- TUI Discover tab reading community-index.json (Phase B3 — already done in Sprint 2) +- `normalizeCommunityPackage` full adapter +- Community vault badges and dep count display beyond what Sprint 2 delivered +- Package detail dep list with ✓/✗ installed status (Sprint 2 delivered this; Sprint 3 does not change it) + +--- + +## Acceptance Criteria + +1. `npm test` — all existing tests pass + all new tests green +2. `plug install subagent-driven-development` — resolver prints plan, deps installed first, all tracked in installed.json with correct `installed_as` / `dependents` +3. `plug install senior-engineer` (no deps) — installs immediately, no plan screen +4. `plug remove subagent-driven-development` — dependent warning fires if applicable, orphan prune prompt shown after +5. Manual TUI: package with deps → plan screen + scope toggle; no-dep package → installs immediately +6. Inspect installed.json after multi-dep install — `installed_as`, `dependencies`, `dependents` correct on every record +7. Regression: all Sprint 1 + Sprint 2 fixes still work + +--- + +## Key Risks + +1. **resolver.js N-reads** — must call `getInstalled()` once per `resolve()`, pass snapshot into DFS. Per-node `isInstalled()` calls re-read the file each time. +2. **Community package install path** — community packages have `rawBaseUrl + entry`; `installSinglePackage` must branch on presence of `rawBaseUrl`. +3. **ctx mutation timing in discover.jsx** — `ctx.set({ yes: true, json: true })` inside `doInstall()` must NOT fire during resolver async phase (plan view), only after user confirms. +4. **install.test.js mock layering** — existing mocks cover registry, fetcher, tracker. A3 adds resolver import; add `vi.mock('../src/utils/resolver.js', ...)` at the top. + +--- + +## Branch + +- **Branch:** `sprint/dep-resolution` +- **Base:** `main` From f4d8b53e8c1049fe5adc405bd0b2c01ed3bcef6b Mon Sep 17 00:00:00 2001 From: Siddharth Date: Thu, 16 Apr 2026 20:52:45 +0530 Subject: [PATCH 02/10] =?UTF-8?q?review:=20sprint=203=20dep-resolution=20p?= =?UTF-8?q?lan=20=E2=80=94=20CHANGES=20NEEDED?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two HIGH findings: 1. updateDependents overwrite semantics will drop dependents when a package is a dependency of multiple parents (data integrity bug) 2. _cascade flag in remove.js Task 4.1 is undefined — recursion semantics unspecified 10 of 12 checklist items pass. See feedback.md for full review. --- feedback.md | 177 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 feedback.md diff --git a/feedback.md b/feedback.md new file mode 100644 index 0000000..197b8d0 --- /dev/null +++ b/feedback.md @@ -0,0 +1,177 @@ +# Sprint 3 — Dep Resolution Plan Review + +**Reviewer:** plug-reviewer +**Date:** 2026-04-16 +**Verdict:** CHANGES NEEDED + +> See the recent git history of this file to understand the context of this review. + +--- + +## 1. Clear "done" criteria — PASS + +Every task has explicit, testable done-when statements. Task 2.2 (resolver) done criteria includes the critical "getInstalled called once per resolve" constraint. Task 4.1 enumerates all three remove flows. Test tasks specify exact case counts. No ambiguity in what "done" means for any individual task. + +--- + +## 2. Cohesion / coupling — PASS with NOTE + +Phases are well-scoped: tracker schema (P1), resolver (P2), install wiring (P3), remove wiring (P4), frontmatter (P5). Coupling between phases follows the dependency graph cleanly. + +**NOTE:** Task 3.1 is the densest task in the plan — extract helper, wire resolver, loop toInstall, track installed_as, call updateDependents, AND update install.test.js. It's cohesive around the install concern but could easily balloon into a multi-hour session if install.js has surprises. Doer should read install.js thoroughly before starting (the blocker note says this, which is good). + +--- + +## 3. Shared abstractions in earliest tasks — PASS + +Phase 1 establishes the tracker schema and all four helper functions (`updateDependents`, `getInstalledRecord`, `prunableOrphans`, `removeDependentEdge`) that Phases 3 and 4 consume. Phase 2 establishes the `resolve()` public API that Phase 3 consumes. Correct ordering. + +--- + +## 4. Riskiest assumption validated early — PASS + +Requirements.md Risk #1 (N file reads per node) is addressed in Phase 2, Task 2.2, which is the earliest phase where the resolver exists. The done criteria explicitly requires "getInstalled called once per resolve." The risk register also calls this out with HIGH impact and clear mitigation. Cannot validate earlier since the resolver doesn't exist until Phase 2. + +--- + +## 5. Later tasks reuse early abstractions — PASS + +- Phase 3 reuses `resolve()` (P2), `trackInstall` metadata fields (P1), `updateDependents` (P1). +- Phase 4 reuses `prunableOrphans` (P1), `removeDependentEdge` (P1). +- Phase 5 is independent but wires into install.js (P3). + +No redundant reimplementations. + +--- + +## 6. 2-3 work tasks per phase + VERIFY — PASS + +| Phase | Work tasks | VERIFY | +|-------|-----------|--------| +| 1 | 2 | Yes | +| 2 | 3 | Yes | +| 3 | 3 | Yes | +| 4 | 2 | Yes | +| 5 | 2 | Yes | + +All within the 2-3 range. Each VERIFY includes `npm test`, manual checks, and a specific commit message. + +--- + +## 7. Each task completable in one session — PASS with NOTE + +Task 3.1 is the largest single task. It modifies two files (install.js + install.test.js) across 6 sub-steps. This is completable in one session for a developer who has already read install.js, but the blocker note should be treated as mandatory prep, not optional. + +--- + +## 8. Dependencies satisfied in order — PASS + +- P1 (tracker) before P2 (resolver uses tracker). +- P2 (resolver) before P3 (install wires resolver). +- P1 (tracker helpers) before P4 (remove uses prunableOrphans, removeDependentEdge). +- P3 (install.js modifications) before P5 (frontmatter wires into install.js). + +No circular or out-of-order dependencies. + +--- + +## 9. Vague / ambiguous tasks — FAIL (HIGH) + +### HIGH: `updateDependents` overwrite semantics cause data corruption + +Task 1.1 defines `updateDependents(name, dependents, global)` as: "overwrites `data.installed[name].dependents`; saves." + +Task 3.1 step 5 says: "call `updateDependents(dep.name, [pkgName], isGlobal)` for each dep." + +**The bug:** If package A depends on X, and later package B also depends on X: +1. `plug install A` → `updateDependents('X', ['A'], false)` → X.dependents = `['A']` +2. `plug install B` → `updateDependents('X', ['B'], false)` → X.dependents = `['B']` — **overwrites, losing `'A'`** + +Now removing B makes X prunable (dependents = []), even though A still depends on X. This is a data integrity bug that will cause incorrect orphan pruning. + +**Fix required:** Either: +- (a) Change `updateDependents` to **append** semantics: read current dependents, merge new entries, deduplicate, then save. Rename to `addDependents` for clarity. OR +- (b) Change Task 3.1 step 5 to read-then-merge: `const existing = getInstalledRecord(dep.name, isGlobal).dependents ?? []; updateDependents(dep.name, [...new Set([...existing, pkgName])], isGlobal)`. + +Option (a) is cleaner — the function should own its merge semantics. + +### MEDIUM: `_cascade` flag in Task 4.1 is undefined + +Task 4.1 Cascade flow says: "`runRemove(dep, { global, _cascade: true })` for each dependent first, then target." But `_cascade` is never defined — what does it do? Presumably it suppresses the dependents check on the recursive call to prevent infinite recursion (dependent A might itself have dependents). But this is not stated. Two developers could implement this differently: +- Developer 1: `_cascade` skips the dependents check entirely. +- Developer 2: `_cascade` skips only the user prompt but still checks. + +**Fix required:** Add a one-line definition of `_cascade` behavior. Suggested: "When `_cascade` is true, skip the dependents check and proceed directly to file deletion + trackRemove — this prevents infinite recursion during cascade." + +Also: what if dependents have their own dependents (A → B → target, where A depends on B depends on target)? Does cascade only remove direct dependents, or does it recurse transitively? This must be specified. + +--- + +## 10. Hidden dependencies between tasks — PASS with NOTE + +**NOTE:** Task 5.1 modifies install.js, which Task 3.1 also heavily modifies. The plan correctly sequences them (P3 before P5), but the doer should be aware that the insertion point for frontmatter parsing (Task 5.1: "after writing SKILL.md") depends on the final shape of install.js after Task 3.1's refactor. If 3.1 significantly restructures the file, 5.1's description of "after writing SKILL.md" may not map cleanly. + +No other hidden dependencies found. + +--- + +## 11. Risk register — PASS with NOTE + +Five risks identified, all with impact ratings and mitigations. Good coverage of the key risks from requirements.md. + +**Missing risk (should be added):** + +| Risk | Impact | Mitigation | +|------|--------|------------| +| `updateDependents` overwrite semantics lose prior dependents on multi-parent deps | HIGH | Use append/merge semantics (see finding #9 above) | +| Cascade remove recursion depth on deep dependency chains | LOW | Cap recursion or use iterative approach; add test for A→B→target chain | + +--- + +## 12. Alignment with requirements.md — PASS + +All requirement sections (A1-A5, A6 scope toggle) have corresponding plan phases. All 7 acceptance criteria from requirements.md have matching VERIFY steps. Out-of-scope items correctly excluded. + +--- + +## Sprint 3-Specific Concerns + +### Phase 2 resolver.js — getInstalled() per-node prevention — PASS + +Task 2.2 algorithm step 2 explicitly states: "`installedSnapshot = await getInstalled(isGlobal)` — load ONCE; pass into DFS — never call per-node." The done criteria reinforces this. Test 2.3 should include a spy/call-count assertion on `getInstalled` to enforce this at test level — this is implied but not explicitly listed as a test case. + +**Recommendation:** Add a 9th test case to Task 2.3: "Verify `getInstalled` called exactly once regardless of dep tree size." This converts the requirement from a code-review check to an automated regression guard. + +### Phase 3 install.js — rawBaseUrl branch condition — PASS + +Task 3.1 step 1 clearly specifies: "If `pkgSpec.rawBaseUrl`: `fetch(rawBaseUrl + entry)` directly; Else: use existing `downloadFile(vault, ...)` path." Unambiguous. + +### Phase 3 discover.jsx — ctx.set() timing — PASS + +Task 3.3 explicitly states: "`ctx.set({ yes: true, json: true })` must only be called inside `doInstall()` after user confirms — NOT during resolver async phase." Risk register also covers this (MED impact). Clear constraint. + +### Phase 3 install.test.js — mock layering — PASS with NOTE + +Task 3.1 step 6 gives the exact `vi.mock` line. However, it doesn't specify how per-test overrides should work when some tests need the resolver to return deps and others need it to return an empty plan. The default mock returns `{ toInstall: [], alreadySatisfied: [], cycles: [] }` — this means existing tests won't break (resolver returns nothing), but new dep-tracking tests need `resolve.mockResolvedValueOnce(...)`. This is implied but stating it explicitly would prevent confusion. + +### Phase 4 remove.js — cascade/force/cancel flows — FAIL + +The three flows are listed but cascade has an undefined recursion mechanism (see finding #9 above). Cancel and Force are clear and distinct. Cascade needs the `_cascade` flag defined and transitive-dependent behavior specified. + +--- + +## Summary + +**What passed (10 of 12 checklist items):** Done criteria are clear. Abstractions are correctly ordered. Phases are well-sized with VERIFY checkpoints. Dependencies are satisfied in order. Risk register is present and mostly complete. Requirements alignment is full. + +**What must change before execution (2 HIGH findings):** + +1. **`updateDependents` overwrite semantics** — The current spec will silently drop dependents when a package is a dependency of multiple parents. This is a data integrity bug that causes incorrect orphan pruning. Change to append/merge semantics or add read-then-merge logic at the call site. Update Task 1.1 definition, Task 1.2 test cases (add multi-parent test), and Task 3.1 step 5. + +2. **`_cascade` flag undefined in Task 4.1** — Define what `_cascade: true` does (skip dependents check to prevent infinite recursion). Specify whether cascade is shallow (direct dependents only) or transitive (recursive). Add a note about recursion depth. + +**Recommendations (not blocking):** + +- Add a 9th test case to Task 2.3: verify `getInstalled` called exactly once per resolve. +- Add the `updateDependents` overwrite risk to the risk register. +- Task 3.1 step 6: mention `mockResolvedValueOnce` pattern for per-test overrides. From 7c6dac83303d96dc43ec0f94d5c6f9c12320d173 Mon Sep 17 00:00:00 2001 From: Siddharth Date: Thu, 16 Apr 2026 20:56:48 +0530 Subject: [PATCH 03/10] =?UTF-8?q?docs:=20revise=20plan=20=E2=80=94=20addDe?= =?UTF-8?q?pendents=20merge=20semantics=20+=20cascade=20flag=20definition?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plug/PLAN.md | 33 +++++++++++++++++++-------------- plug/feedback.md | 30 ++++++++++++++++++++++++++++++ plug/progress.json | 8 ++++---- 3 files changed, 53 insertions(+), 18 deletions(-) diff --git a/plug/PLAN.md b/plug/PLAN.md index 8b2babe..3f80d36 100644 --- a/plug/PLAN.md +++ b/plug/PLAN.md @@ -16,25 +16,27 @@ - Persist three new fields in `trackInstall`: `installed_as: metadata.installed_as ?? 'explicit'`, `dependencies: metadata.dependencies ?? []`, `dependents: metadata.dependents ?? []` - Normalise on any read path touching `rec.installed_as`: `rec.installed_as ?? 'explicit'` - Add four new exported functions: - - `updateDependents(name, dependents, global)` — overwrites `data.installed[name].dependents`; saves + - `addDependents(name, newDependents, global)` — **merges** (not overwrites) `newDependents` into `data.installed[name].dependents` (deduplicate); saves. Merge semantics are required: if package X is a dependency of both A and B, `addDependents('X', ['B'], global)` must preserve A in X's dependents list, not erase it. - `getInstalledRecord(name, global)` — returns `data.installed[name] ?? null` - `prunableOrphans(global)` — returns names where `installed_as === 'dependency' && dependents.length === 0` - `removeDependentEdge(fromName, toName, global)` — removes `fromName` from `data.installed[toName].dependents`; saves - **Files:** `src/utils/tracker.js` - **Tier:** standard -- **Done when:** `trackInstall` persists new fields; all four helpers exported and functional; backward-compat normalisation in place +- **Done when:** `trackInstall` persists new fields; all four helpers exported and functional; `addDependents` uses merge/dedup semantics; backward-compat normalisation in place - **Blockers:** Read current `src/utils/tracker.js` and `src/utils/tracker.test.js` before modifying — match file-write patterns exactly #### Task 1.2: Extend tests/tracker.test.js - **Change:** Add test cases (do NOT create a new file): - `trackInstall` with `installed_as: 'dependency'` persists correctly - `trackInstall` without `installed_as` defaults to `'explicit'` - - `updateDependents` mutates only the targeted record + - `addDependents` appends to existing dependents (multi-parent case: install A then install B both depending on X → X.dependents = ['A', 'B'], not just ['B']) + - `addDependents` deduplicates (calling twice with same name produces no duplicate) + - `addDependents` mutates only the targeted record (other records unaffected) - `prunableOrphans` returns correct subset - `removeDependentEdge` removes back-reference correctly - **Files:** `tests/tracker.test.js` - **Tier:** standard -- **Done when:** all five new test cases pass; existing tests unaffected +- **Done when:** all seven new test cases pass; existing tests unaffected - **Blockers:** none #### VERIFY: Phase 1 @@ -89,9 +91,10 @@ - Optional dep (`required: false`) NOT added to `toInstall` - Unknown package in deps → silently skipped, no crash - All deps already installed → `toInstall: [pkg]`, `alreadySatisfied: [all deps]` + - `getInstalled` called exactly once per `resolve()` invocation (spy on `getInstalled`, assert call count = 1 even for multi-dep graphs) - **Files:** `tests/resolver.test.js` (new) - **Tier:** standard -- **Done when:** all 8 test cases pass +- **Done when:** all 9 test cases pass - **Blockers:** none #### VERIFY: Phase 2 @@ -114,11 +117,11 @@ - If CLI path and `plan.toInstall.length > 1`: print plan summary; if `!ctx.yes`, prompt "Proceed? (Y/n)" 3. Loop `plan.toInstall` in order, calling `installSinglePackage(entry, isGlobal)` for each 4. Track each dep with `installed_as: 'dependency'`; root with `installed_as: 'explicit'`; both include `dependencies: [direct dep names]` - 5. After all installs: call `updateDependents(dep.name, [pkgName], isGlobal)` for each dep + 5. After all installs: call `addDependents(dep.name, [pkgName], isGlobal)` for each dep 6. Add `vi.mock('../src/utils/resolver.js', () => ({ resolve: vi.fn().mockResolvedValue({ toInstall: [], alreadySatisfied: [], cycles: [] }) }))` at top of `tests/install.test.js` - **Files:** `src/commands/install.js`, `tests/install.test.js` - **Tier:** premium -- **Done when:** resolver called on install; deps tracked as `installed_as: 'dependency'`; root as `'explicit'`; `updateDependents` called; already-satisfied deps skipped; `installSinglePackage` branches on `rawBaseUrl` +- **Done when:** resolver called on install; deps tracked as `installed_as: 'dependency'`; root as `'explicit'`; `addDependents` called; already-satisfied deps skipped; `installSinglePackage` branches on `rawBaseUrl` - **Blockers:** Read current install.js fully before modifying — identify where to inject resolver call and where `trackInstall` is called #### Task 3.2: New TUI component src/tui/components/install-plan.jsx @@ -159,14 +162,15 @@ #### Task 4.1: Add dependent check + cascade/force/prune to remove.js - **Change:** In `src/commands/remove.js` after loading `pkg`: 1. `const dependents = pkg.dependents ?? []` - 2. If `dependents.length > 0`: `select` prompt (Cancel / Remove all cascade / Force remove) - - Cancel: return early - - Cascade: `runRemove(dep, { global, _cascade: true })` for each dependent first, then target - - Force: delete target file + `trackRemove(name)` + `removeDependentEdge(name, dep, global)` for each dependent - 3. After any successful remove: call `prunableOrphans(isGlobal)`; if orphans exist and `!ctx.yes`: prompt to prune; if yes: `runRemove` each orphan + 2. If `dependents.length > 0` **and** `!options._cascade`: `select` prompt (Cancel / Remove all cascade / Force remove) + - `_cascade` definition: boolean flag passed in the options object. When `true`, it means "this call was initiated by a cascade — skip the user prompt and proceed with removal immediately." This is what prevents infinite re-prompting when removing dependents recursively. + - **Cancel:** return early. + - **Cascade (shallow):** For each name in `dependents`, call `runRemove(dep, { global, _cascade: true })`. Then remove the original target. Depth: one level only — when `_cascade: true`, the called `runRemove` will skip the prompt for that dependent's own dependents (they are not removed). This is intentional shallow cascade. If deep transitive removal is needed, that is a future enhancement. + - **Force:** delete target file + `trackRemove(name)` + `removeDependentEdge(name, dep, global)` for each dependent. Does NOT remove the dependent packages — just severs the edge and removes the target. + 3. After any successful remove: call `prunableOrphans(isGlobal)`; if orphans exist and `!ctx.yes`: prompt to prune; if yes: `runRemove` each orphan (without `_cascade`) - **Files:** `src/commands/remove.js` - **Tier:** standard -- **Done when:** dependents check fires; Cancel/Cascade/Force all work; orphan prune prompt fires; `--yes` auto-prunes +- **Done when:** dependents check fires; `_cascade` flag skips re-prompt correctly; Cancel/Cascade/Force all work per definitions above; orphan prune prompt fires; `--yes` auto-prunes - **Blockers:** Read current remove.js fully before modifying #### Task 4.2: Extend tests/remove.test.js @@ -234,7 +238,8 @@ | Risk | Impact | Mitigation | |------|--------|------------| -| resolver.js calls `isInstalled()` per-node — file read on every dep | HIGH | Load `getInstalled()` once per `resolve()` call, pass snapshot into DFS. Covered by test: verify only 1 `getInstalled` call per resolve | +| resolver.js calls `isInstalled()` per-node — file read on every dep | HIGH | Load `getInstalled()` once per `resolve()` call, pass snapshot into DFS. Covered by test (Task 2.3): verify exactly 1 `getInstalled` call per resolve invocation | +| `addDependents`/`addDependents` overwrite semantics — multi-parent packages lose dependents | HIGH | Use merge/append semantics in `addDependents` (dedup). Multi-parent test in Task 1.2 covers this. | | Community package install path: `rawBaseUrl` + `entry` instead of vault owner/repo | HIGH | `installSinglePackage` branches on `pkgSpec.rawBaseUrl` presence. Intentional vault-abstraction bypass for Sprint 3; B3 adapter tightens in future sprint | | `ctx.set({ yes, json })` called during resolver async phase (plan view) — state pollution | MED | `ctx.set` must only be inside `doInstall()`, called after user confirms. Test: mock resolver to be slow; assert ctx not mutated until confirm | | install.test.js mock layering — existing mocks + new resolver mock clash | MED | Add `vi.mock('../src/utils/resolver.js', ...)` at top of install.test.js. Per-test override via `resolve.mockResolvedValueOnce(...)` | diff --git a/plug/feedback.md b/plug/feedback.md index b8ae6e7..c9127d8 100644 --- a/plug/feedback.md +++ b/plug/feedback.md @@ -90,3 +90,33 @@ The following checks were flagged by the doer as unverifiable headlessly. These - The `showDeps` prop guard in `package-item.jsx` ensures the installed tab (which doesn't pass `showDeps`) won't display dep counts, avoiding confusion for official packages that lack dependency metadata. **Doer:** Phase 3 complete, all tests green — 257 passed (26 files), 0 failures. 17 new community-index tests + 6 new discover/PackageDetail tests. + +--- + +## Sprint 3 Review — Dependency Resolution (Cumulative) + +**Reviewer:** plug-reviewer +**Date:** 2026-04-16 +**Verdict:** CHANGES NEEDED + +--- + +## Sprint 3 Findings + +### HIGH + +- **Task 1.1: `addDependents` merge semantics ambiguous.** Requirement stated "merges newDependents" but did not specify dedup behavior or show example of multi-parent preservation. Without explicit semantics, implementer could interpret as append-without-check, losing existing dependents when second parent installs. + + **Doer:** fixed in PLAN.md revision — Task 1.1 now includes inline clause: "Merge semantics are required: if package X is a dependency of both A and B, `addDependents('X', ['B'], global)` must preserve A in X's dependents list, not erase it." + test coverage in Task 1.2. + +- **Task 4.1: `_cascade` flag semantics not defined.** The flag is used to control recursion and avoid re-prompting, but its meaning and lifecycle are implicit. Risk of infinite recursion or confused prompt logic if implementer misunderstands when/how to pass it. + + **Doer:** fixed in PLAN.md revision — Task 4.1 now includes dedicated `_cascade` definition paragraph: "boolean flag passed in the options object. When `true`, it means 'this call was initiated by a cascade — skip the user prompt and proceed with removal immediately.' This is what prevents infinite re-prompting when removing dependents recursively." + +### MEDIUM + +- None. + +### LOW + +- None. diff --git a/plug/progress.json b/plug/progress.json index 20bb602..dadceb4 100644 --- a/plug/progress.json +++ b/plug/progress.json @@ -10,16 +10,16 @@ "base_branch": "main", "created": "2026-04-16", "tasks": [ - { "id": "1.1", "phase": 1, "step": "Extend trackInstall with installed_as/dependencies/dependents fields; add updateDependents, getInstalledRecord, prunableOrphans, removeDependentEdge to tracker.js", "type": "work", "status": "pending", "tier": "standard", "commit": "", "notes": "" }, - { "id": "1.2", "phase": 1, "step": "Extend tests/tracker.test.js with 5 new cases: installed_as persist, default, updateDependents mutation, prunableOrphans subset, removeDependentEdge back-reference", "type": "work", "status": "pending", "tier": "standard", "commit": "", "notes": "" }, + { "id": "1.1", "phase": 1, "step": "Extend trackInstall with installed_as/dependencies/dependents fields; add addDependents, getInstalledRecord, prunableOrphans, removeDependentEdge to tracker.js", "type": "work", "status": "pending", "tier": "standard", "commit": "", "notes": "" }, + { "id": "1.2", "phase": 1, "step": "Extend tests/tracker.test.js with 7 new cases: installed_as persist, default, addDependents append (multi-parent), addDependents dedup, addDependents single-record mutation, prunableOrphans subset, removeDependentEdge back-reference", "type": "work", "status": "pending", "tier": "standard", "commit": "", "notes": "" }, { "id": "1.V", "phase": 1, "step": "VERIFY Phase 1: npm test green; commit 'feat(tracker): extend installed.json with installed_as/dependencies/dependents'; push sprint/dep-resolution", "type": "verify", "status": "pending", "tier": "cheap", "commit": "", "notes": "" }, { "id": "2.1", "phase": 2, "step": "Check if community-index.js exists from Sprint 2; if missing create minimal fetch+cache wrapper; if exists confirm fetchCommunityIndex + getStaleCommunityIndexCache exported", "type": "work", "status": "pending", "tier": "standard", "commit": "", "notes": "" }, { "id": "2.2", "phase": 2, "step": "Create src/utils/resolver.js with buildPackageMap + DFS resolver; load getInstalled once per resolve call; resolve() public API returns { toInstall, alreadySatisfied, cycles }", "type": "work", "status": "pending", "tier": "premium", "commit": "", "notes": "" }, - { "id": "2.3", "phase": 2, "step": "Create tests/resolver.test.js covering 8 cases: no deps, single dep, already installed, transitive, cycle, optional dep, unknown dep, all deps installed", "type": "work", "status": "pending", "tier": "standard", "commit": "", "notes": "" }, + { "id": "2.3", "phase": 2, "step": "Create tests/resolver.test.js covering 9 cases: no deps, single dep, already installed, transitive, cycle, optional dep, unknown dep, all deps installed, getInstalled called exactly once per resolve", "type": "work", "status": "pending", "tier": "standard", "commit": "", "notes": "" }, { "id": "2.V", "phase": 2, "step": "VERIFY Phase 2: npm test green; commit 'feat(resolver): add DFS dependency resolver'; push sprint/dep-resolution", "type": "verify", "status": "pending", "tier": "cheap", "commit": "", "notes": "" }, - { "id": "3.1", "phase": 3, "step": "Refactor install.js: extract installSinglePackage helper (branch on rawBaseUrl); wire resolve() call before download; loop toInstall; track installed_as correctly; call updateDependents; extend tests/install.test.js with resolver mock + dep-tracking cases", "type": "work", "status": "pending", "tier": "premium", "commit": "", "notes": "" }, + { "id": "3.1", "phase": 3, "step": "Refactor install.js: extract installSinglePackage helper (branch on rawBaseUrl); wire resolve() call before download; loop toInstall; track installed_as correctly; call addDependents; extend tests/install.test.js with resolver mock + dep-tracking cases", "type": "work", "status": "pending", "tier": "premium", "commit": "", "notes": "" }, { "id": "3.2", "phase": 3, "step": "Create src/tui/components/install-plan.jsx: spinner, will-install list, already-satisfied list, scope selector (Tab toggles), footer [i]/[Esc]", "type": "work", "status": "pending", "tier": "standard", "commit": "", "notes": "" }, { "id": "3.3", "phase": 3, "step": "Wire 'plan' view state in discover.jsx; plan screen only when toInstall.length > 1; ctx.set only after confirm; extend tests/tui/discover.test.jsx with plan view + scope toggle + cancel cases", "type": "work", "status": "pending", "tier": "standard", "commit": "", "notes": "" }, { "id": "3.V", "phase": 3, "step": "VERIFY Phase 3: npm test green; manual CLI single-pkg (no plan screen) + multi-dep (plan screen); commit 'feat(install): wire resolver + TUI scope toggle on dep installs only'; push", "type": "verify", "status": "pending", "tier": "standard", "commit": "", "notes": "" }, From 827743fed16c82a07fbbe907c1b7ab083c0d6820 Mon Sep 17 00:00:00 2001 From: Siddharth Date: Thu, 16 Apr 2026 21:02:47 +0530 Subject: [PATCH 04/10] docs: update roadmap, architecture and add claude integration vision --- README.md | 24 ++ docs/architecture.md | 16 +- docs/project/vision-claude-integration.md | 371 ++++++++++++++++++++++ 3 files changed, 410 insertions(+), 1 deletion(-) create mode 100644 docs/project/vision-claude-integration.md diff --git a/README.md b/README.md index 18d2b1b..74cc675 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,24 @@ **Plug** is a package manager for [Claude Code](https://docs.anthropic.com/en/docs/claude-code). It allows you to share, discover, and install reusable skills, commands, and agents across your projects via a rich, interactive Terminal User Interface (TUI). +## Why Plug? + +Managing Claude Code extensions (Skills, Commands, and Agents) manually can quickly become unmanageable. Plug solves several key problems: + +* **Corporate & Private Ecosystems**: Easily set up internal "Vaults" (GitHub repositories) to share proprietary coding standards, security guardrails, and specialized agents within your organization. +* **Consolidated Management**: Instead of maintaining multiple harnesses or manually copying Markdown files across different projects, Plug provides a single tool to discover, install, and update everything. +* **PlugVault Ecosystem**: Access a centralized, growing registry of community-contributed extensions through the official `plugvault`, or add your own custom vaults. +* **Zero-Bloat Workflow**: Keep your projects clean by installing only what you need, either locally to a project (`.claude/`) or globally (`~/.claude/`). + --- ## 🚀 Quick Start: The Plug TUI The recommended way to use Plug is through its interactive TUI. It allows you to browse, search, and batch-install extensions without remembering complex commands. +![Plug TUI Screenshot](docs/images/tui-screenshot.png) +*(A rich, interactive interface for managing your Claude extensions)* + ### 1. Install Plug Install as a Claude Code skill (recommended, no Node.js required): ```bash @@ -44,6 +56,18 @@ Claude Code extensions (Skills, Commands, and Agents) are Markdown files. Plug s --- +## 🔮 Vision & Roadmap: Claude Native Integration + +We are building a zero-latency, native integration with Claude Code. Soon, you'll be able to manage your extensions directly from within a Claude conversation: + +* **`/plug` Slash Command**: A smart, terminal-native installer that avoids the need to switch windows. +* **Natural Language Discovery**: "Claude, I need a tool for security audits" — Claude finds the right package from the vault and installs it instantly. +* **One-Click Management**: Update and remove packages through interactive Claude panels. + +View the full [Claude Integration Vision](docs/project/vision-claude-integration.md). + +--- + ## Documentation * **[TUI Guide](docs/features/tui.md)** — Detailed breakdown of the interactive interface and hotkeys. diff --git a/docs/architecture.md b/docs/architecture.md index aaa4109..6743dbf 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -2,6 +2,15 @@ Plug is a package manager for Claude Code extensions, designed to be lightweight, decentralized, and accessible through both a CLI and a native Claude Code Skill. +## Why Plug? (The Value Proposition) + +In a corporate or team environment, maintaining consistency in Claude's behavior is critical. Plug addresses this by providing: + +1. **Centralized Governance**: Define "Vaults" (registries) that house your organization's coding standards, security rules, and architectural guidelines. +2. **Private Ecosystems**: Support for private GitHub repositories allows companies to distribute proprietary skills and agents securely using standard GitHub authentication (PAT). +3. **Anti-Bloat Architecture**: One "harness" (Plug) manages all your extensions. You no longer need to manually sync Markdown files or maintain redundant harnesses across multiple projects. +4. **Community Knowledge**: The official `plugvault` provides a curated set of community best practices that can be shared and improved collectively. + ## High-Level Design Plug operates on a "Vault" model. A Vault is any GitHub repository that follows a simple registry structure. @@ -14,7 +23,12 @@ Plug operates on a "Vault" model. A Vault is any GitHub repository that follows ## Core Components -### 1. Execution Engines +### 1. Vault Ecosystem (PlugVault) +A "Vault" is the unit of distribution. While the official `plugvault` repository serves as the primary community registry, Plug is designed to be multi-vault. Users can register any number of public or private vaults. +* **Resolution Order**: Plug searches vaults in a user-defined order, allowing internal corporate packages to override or supplement community versions. +* **Decentralization**: No central server is required; all package metadata and content live on GitHub. + +### 2. Execution Engines #### Claude Code Skill (Recommended) The Skill is the primary interface for most users. It runs directly within Claude Code, using Claude's native tools (`Bash`, `Read`, `Write`) to: diff --git a/docs/project/vision-claude-integration.md b/docs/project/vision-claude-integration.md new file mode 100644 index 0000000..43a3c2c --- /dev/null +++ b/docs/project/vision-claude-integration.md @@ -0,0 +1,371 @@ +# plug — Claude Code Native Integration Vision +**Status:** planning (2026-04-16) + +--- + +## What we're building + +A single file: `~/.claude/commands/plug.md` + +When a user types `/plug`, Claude reads this file and immediately fires `AskUserQuestion` — no reasoning, no output, no delay. The file is pure instruction: hardcoded option lists, hardcoded dispatch table, hardcoded file paths. Claude has nothing to figure out. + +**Install trigger:** `plug install plug --global` writes the command file automatically. + +> **Scope:** `/plug` in Claude Code is a smart installer — not a TUI replacement. +> Browse/discover stays in the TUI (`plug` in a terminal). `/plug` handles +> install-by-description, list, remove, and vault add from within a conversation. + +--- + +## Zero-latency rule + +The command file opens with: + +``` +Do not think. Do not output any text. Your first action is always an immediate +AskUserQuestion call. The exact JSON for every panel is defined below. +``` + +Every branch resolves to a tool call. No branch resolves to text output first. + +--- + +## Full UX — screen by screen + +### Screen 1 — Main menu (fires on `/plug` with no arguments) + +``` +┌─ /plug ──────────────────────────────────────────────────────┐ +│ │ +│ What would you like to do? │ +│ │ +│ ▶ Install a package │ +│ Describe what you need — Claude finds and installs it │ +│ │ +│ My packages │ +│ View installed, check for updates, remove │ +│ │ +│ Manage vaults │ +│ Add, remove, or sync package sources │ +│ │ +│ Open TUI │ +│ Browse everything in the terminal │ +│ │ +└───────────────────────────────────────────────────────────────┘ +``` + +--- + +### Flow A — Install a package (natural language search) + +#### Screen A1 — Describe what you need + +``` +┌─ Install ─────────────────────────────────────────────────────┐ +│ │ +│ What do you need? │ +│ │ +│ ▶ Something that does code reviews │ +│ Something that handles deployments │ +│ Something for Python development │ +│ [Other — describe what you need, or type a package name] │ +│ │ +└───────────────────────────────────────────────────────────────┘ +``` + +User picks "Other" and types freely — e.g. "security audit of my code" or just "security-review". + +#### How the search works + +Claude fetches `community-index.json` (~100–200 KB, fits in context), then scans every package's `name` + `description` + `tags` against the user's query. + +``` +community-index.json entry: +{ + "name": "security-review", + "description": "Security audit of pending branch changes — OWASP top 10, injection, XSS", + "tags": ["security", "audit", "owasp", "review"], + "type": "skill", + "vault": "official" +} + +User query: "security audit of my code" +Match score: name partial ✓ · description strong ✓ · tags: security ✓ audit ✓ +→ top result +``` + +#### Screen A2a — Single match (straight to confirm) + +``` +┌─ Found ───────────────────────────────────────────────────────┐ +│ │ +│ Install security-review? │ +│ │ +│ ▶ Install to project (.claude/) │ +│ security-review v1.0.0 · official · 1 dep │ +│ │ +│ Install globally (~/.claude/) │ +│ Available in all projects │ +│ │ +│ Not what I meant — search again │ +│ │ +└───────────────────────────────────────────────────────────────┘ +``` + +#### Screen A2b — Multiple matches (pick one) + +``` +┌─ Found 3 matches ─────────────────────────────────────────────┐ +│ │ +│ Which one? │ +│ │ +│ ▶ security-review v1.0.0 official │ +│ Security audit of pending branch changes — OWASP top 10 │ +│ │ +│ sec-hardening v1.1.0 community │ +│ Hardens code against OWASP vulnerabilities │ +│ │ +│ security-scanner v0.9.0 langchain │ +│ Static analysis + dependency vulnerability scan │ +│ │ +│ None of these — search again │ +│ │ +└───────────────────────────────────────────────────────────────┘ +``` + +Then → Screen A3 (install plan). + +#### Screen A2c — No match + +``` +Nothing found for "quantum entanglement debugger". + +Try the TUI to browse everything: plug (in your terminal) +Or search the community index at: github.com/dsiddharth2/plugvault +``` + +No question. Plain text output. Done. + +#### Screen A3 — Install plan with scope toggle (preview panel) + +``` +┌─ Install plan ────────────────────────┬───────────────────────────────────────┐ +│ │ Install scope: Project (.claude/) │ +│ Install security-review + │ │ +│ its dependencies? │ Required — always installed: │ +│ │ [+] security-review skill v1.0.0 │ +│ ▶ Install to project (.claude/) │ [+] senior-engineer agent v1.0.3 │ +│ 2 packages · project scope │ │ +│ │ 2 to install · 0 already satisfied │ +│ Install globally (~/.claude/) │ │ +│ 2 packages · global scope │ │ +│ │ │ +│ Cancel │ │ +│ Go back, nothing written │ │ +└───────────────────────────────────────┴───────────────────────────────────────┘ +``` + +#### Screen A4 — Post-install (text output, no question) + +``` +✓ Installed 2 packages to .claude/ + + security-review → .claude/skills/security-review/SKILL.md + senior-engineer → .claude/agents/senior-engineer.md + +Type /security-review to use it now. +``` + +--- + +### Flow B — My packages + +#### Screen B1 — Installed list + +The command file instructs Claude to read `installed.json` first, then render this screen with real data. + +``` +┌─ My packages ─────────────────────────────────────────────────┐ +│ │ +│ What would you like to do? │ +│ │ +│ ▶ senior-engineer v1.0.3 [global] ↑ v1.1.0 available │ +│ Update or remove │ +│ │ +│ code-review v1.1.0 [global] up to date │ +│ Update or remove │ +│ │ +│ pm v1.2.0 [project] up to date │ +│ Update or remove │ +│ │ +│ ← Back │ +│ │ +└───────────────────────────────────────────────────────────────┘ +``` + +> If more than 3 packages are installed, the 4th option becomes "More packages →" +> and Claude paginates through `installed.json` using an offset counter. + +#### Screen B2 — Package action + +``` +┌─ senior-engineer ─────────────────────────────────────────────┐ +│ │ +│ v1.0.3 → v1.1.0 available. What would you like to do? │ +│ │ +│ ▶ Update to v1.1.0 │ +│ Overwrite .claude/agents/senior-engineer.md │ +│ │ +│ Remove │ +│ ⚠ pm depends on this — removal will also remove pm │ +│ │ +│ Cancel │ +│ Go back │ +│ │ +└───────────────────────────────────────────────────────────────┘ +``` + +--- + +### Flow C — Manage vaults + +#### Screen C1 — Vault actions + +``` +┌─ Vaults ──────────────────────────────────────────────────────┐ +│ │ +│ What would you like to do? │ +│ │ +│ ▶ Add a vault │ +│ Register a GitHub repo as a package source │ +│ │ +│ Browse community vaults │ +│ Discover and add public vault repos │ +│ │ +│ Remove a vault │ +│ Unregister: official, langchain, company 🔒 │ +│ │ +│ ← Back │ +│ │ +└───────────────────────────────────────────────────────────────┘ +``` + +--- + +### Shortcut flows (no menu) + +| Invocation | Behavior | +|---|---| +| `/plug install pm` | Exact name match → straight to Screen A3 | +| `/plug install security audit` | Natural language → search → A2a/b/c | +| `/plug list` | Text table of installed packages, no question | +| `/plug remove pm` | Screen B2 for `pm` | +| `/plug update` | Screen B1 filtered to packages with updates only | + +Argument dispatch is a simple prefix match in the command file — no reasoning needed. + +--- + +## How search works — mechanics + +``` +Step 1 Claude executes: + curl -s https://raw.githubusercontent.com/dsiddharth2/plugvault/main/community-index.json + +Step 2 Claude reads the JSON (~100–200 KB). + For each package: score against query using name + description + tags. + +Step 3 Results: + 1 match → Screen A2a (straight to confirm) + 2–3 → Screen A2b (pick one) + 0 → Screen A2c (not found message) + +Step 4 User picks scope → Claude writes files directly using Read/Write/Bash. + No Node.js. No plug CLI invoked. +``` + +**Why this scales to 1000+ packages:** Claude reads the full index once per install. The JSON is compact (one object per package, ~200 bytes each → 200 KB for 1000 packages). Semantic matching happens in Claude's context — no search infrastructure needed. + +--- + +## Search quality gate (next vision) + +Natural language search only works if package metadata is good. +The community-index.json build pipeline needs a **quality gate** before a package is indexed: + +| Field | Requirement | +|---|---| +| `name` | kebab-case, unique, descriptive | +| `description` | 1–2 sentences, includes what it does + key use cases | +| `tags` | 3–8 tags, lowercase, covering domain + action + technology | +| `type` | must be `skill`, `agent`, or `command` | + +Packages missing required fields are excluded from `community-index.json` entirely. +This is a plugvault-side enforcement problem — tracked as a separate vision item. + +--- + +## What already auto-integrates (today, without this work) + +When `plug install senior-engineer` runs via the TUI, it writes: +``` +.claude/skills/senior-engineer/SKILL.md +``` +Claude Code picks this up automatically → `/senior-engineer` becomes a usable slash command immediately. **This is the core value loop:** plug handles discovery + installation, Claude Code handles execution. + +--- + +## What gets written to disk + +**One file:** `~/.claude/commands/plug.md` + +``` +[zero-latency directive] + +[argument dispatch table] + if $ARGUMENTS is empty → MAIN_MENU + if $ARGUMENTS starts "install" → INSTALL (with remainder as query) + if $ARGUMENTS starts "remove" → MY_PACKAGES (pre-selected package) + if $ARGUMENTS starts "update" → MY_PACKAGES (updates only) + if $ARGUMENTS is "list" → LIST (text output, no question) + +[Section: MAIN_MENU] +[Section: INSTALL — A1, A2a/b/c, A3, A4] +[Section: MY_PACKAGES — B1, B2] +[Section: VAULTS — C1] + +[Shared: HOW_TO_READ_INSTALLED_JSON — ~/.plugvault/installed.json + .plugvault/installed.json] +[Shared: HOW_TO_FETCH_INDEX — curl command + cache hint] +[Shared: HOW_TO_WRITE_FILE — path patterns per type: skill/agent/command] +[Shared: HOW_TO_UPDATE_INSTALLED_JSON — schema for adding/removing entries] +``` + +No Node.js. No plug CLI invoked. Claude uses Bash (curl), Read, Write directly. + +--- + +## Implementation steps + +1. Write `plug.md` command file with all sections above +2. Add install logic to plug CLI: `plug install plug --global` → writes `~/.claude/commands/plug.md` +3. Add `plug` package to plugvault official registry so it's discoverable via `/plug install plug` +4. Test each flow: `/plug`, `/plug install `, `/plug install `, `/plug list`, `/plug remove ` + +## Files touched + +| File | Change | +|---|---| +| `~/.claude/commands/plug.md` | New — the command itself | +| `plug/src/commands/install.js` | Self-install path: when package name is `plug`, write command file | +| `plugvault/registry/plug/` | Add plug to official vault | + +--- + +## Relationship to main vision phases + +| This work | Depends on | +|---|---| +| `/plug install` (natural language) | Phase B — community-index.json must exist | +| `/plug list` / `/plug remove` | Nothing — reads local installed.json only | +| Search quality gate | Separate vision — plugvault build pipeline | +| Context-aware auto-suggest (future) | Phase C dep resolution + this work | From 145a8bbb9204ff56e674e4cd9c14e28c4629689a Mon Sep 17 00:00:00 2001 From: Siddharth Date: Thu, 16 Apr 2026 21:09:46 +0530 Subject: [PATCH 05/10] docs: add TUI screenshot --- docs/images/tui-screenshot.png | Bin 0 -> 73674 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/images/tui-screenshot.png diff --git a/docs/images/tui-screenshot.png b/docs/images/tui-screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..1f92c68fe0bb2ea226a4ce0f49dd97852d28a011 GIT binary patch literal 73674 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 aI9o^G8!I-zFg!{i|3lyCPQ`7<=>Gw$M7GKR literal 0 HcmV?d00001 From 74dd386475ea600bd2203a506dc6ddf57233697c Mon Sep 17 00:00:00 2001 From: Siddharth Date: Thu, 16 Apr 2026 21:12:22 +0530 Subject: [PATCH 06/10] docs: improve architecture with workflows and unified harness concept --- docs/architecture.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/architecture.md b/docs/architecture.md index 6743dbf..fd42566 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -61,6 +61,35 @@ Plug supports three types of Claude Code extensions: 2. **Commands**: On-demand slash commands stored in `.claude/commands/`. 3. **Agents**: Specialized sub-agents for task delegation, stored in `.claude/agents/`. +## Workflows + +Plug supports two primary workflows, bridging the gap between deep terminal discovery and zero-latency in-conversation management. + +### 1. TUI/CLI Workflow (The Discovery Engine) +The TUI (powered by Node.js/Ink) is the rich, interactive "Browser" for the extension ecosystem. +* **Trigger**: User runs `plug` or `/plug` in a real terminal. +* **Fetch**: The client pulls `registry.json` from all configured vaults (official + corporate). +* **Search**: Real-time filtering by name, description, or tags. +* **Batch Action**: Users can toggle multiple packages and install/update/remove them in a single operation. +* **Capture**: Background tasks (like `git clone`) are captured via `AsyncLocalStorage` to keep the UI responsive. + +### 2. Claude Native Workflow (Zero-Latency Installer) +The upcoming native integration allows managing extensions without leaving the Claude conversation, using a "smart installer" approach. +* **Trigger**: User types `/plug` inside Claude Code. +* **Zero-Latency**: Claude reads `~/.claude/commands/plug.md`—a pure instruction file that bypasses reasoning and immediately triggers `AskUserQuestion`. +* **Search (Natural Language)**: Claude fetches the `community-index.json`, performs semantic matching against user intent (e.g., "I need a security tool"), and presents matches. +* **Native Tools**: Installation is performed directly using Claude's native `Bash` (via `curl`), `Read`, and `Write` tools. No Node.js runtime is invoked during this flow. +* **Outcome**: Extensions are written directly to `.claude/` and are immediately available for use in the next turn. + +## The Unified Harness: Solving "Multi-Repo Bloat" + +Before Plug, using extensions from different sources required multiple specialized harnesses, leading to fragmented configurations and "tool bloat" within projects. + +Plug acts as the **unified harness**: +* **Single Source of Truth**: All extensions, regardless of their origin (official registry, company internal repo, or personal vault), are managed via one consistent interface. +* **Unified Pathing**: Extensions are routed to standard Claude Code directories (`.claude/skills/`, etc.) based on their type, ensuring Claude can always find them. +* **Global vs. Project Scoping**: Plug manages the complexity of global (`~/.claude/`) vs. project-specific (`.claude/`) installation, preventing duplicate configurations and ensuring a clean development environment. + ## Security * **Authentication**: Supports GitHub Personal Access Tokens (PAT) for private vaults. Tokens are stored in the global config or provided via environment variables (`PLUGVAULT_GITHUB_TOKEN`). From 8a00865bf9f12c0274fe5129f67dd45544c2df91 Mon Sep 17 00:00:00 2001 From: Siddharth Date: Thu, 16 Apr 2026 21:17:00 +0530 Subject: [PATCH 07/10] docs: separate current capabilities from future vision --- docs/architecture.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index fd42566..19a7a90 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -65,15 +65,17 @@ Plug supports three types of Claude Code extensions: Plug supports two primary workflows, bridging the gap between deep terminal discovery and zero-latency in-conversation management. -### 1. TUI/CLI Workflow (The Discovery Engine) +### 1. TUI/CLI Workflow (The Discovery Engine) — **Current State** The TUI (powered by Node.js/Ink) is the rich, interactive "Browser" for the extension ecosystem. * **Trigger**: User runs `plug` or `/plug` in a real terminal. * **Fetch**: The client pulls `registry.json` from all configured vaults (official + corporate). * **Search**: Real-time filtering by name, description, or tags. -* **Batch Action**: Users can toggle multiple packages and install/update/remove them in a single operation. +* **Install**: Currently performs **single-package installation**. It downloads the requested package and its metadata directly to the project or global scope. * **Capture**: Background tasks (like `git clone`) are captured via `AsyncLocalStorage` to keep the UI responsive. -### 2. Claude Native Workflow (Zero-Latency Installer) +> **Note:** Dependency resolution is currently in development. At this stage, if a package requires other extensions, they must be installed manually. + +### 2. Claude Native Workflow (Zero-Latency Installer) — **Vision** The upcoming native integration allows managing extensions without leaving the Claude conversation, using a "smart installer" approach. * **Trigger**: User types `/plug` inside Claude Code. * **Zero-Latency**: Claude reads `~/.claude/commands/plug.md`—a pure instruction file that bypasses reasoning and immediately triggers `AskUserQuestion`. @@ -90,6 +92,15 @@ Plug acts as the **unified harness**: * **Unified Pathing**: Extensions are routed to standard Claude Code directories (`.claude/skills/`, etc.) based on their type, ensuring Claude can always find them. * **Global vs. Project Scoping**: Plug manages the complexity of global (`~/.claude/`) vs. project-specific (`.claude/`) installation, preventing duplicate configurations and ensuring a clean development environment. +## Current vs. Future Capabilities + +| Feature | Today (Built) | Target (Vision) | +|---|---|---| +| **Installation** | Single-package | Full Dependency Resolution (DFS) | +| **Marketplace** | Official Registry | Multi-repo Community Index | +| **Search** | TUI Text Search | AI-powered Natural Language | +| **Governance** | Manual Vault Add | Auto-discovery of community vaults | + ## Security * **Authentication**: Supports GitHub Personal Access Tokens (PAT) for private vaults. Tokens are stored in the global config or provided via environment variables (`PLUGVAULT_GITHUB_TOKEN`). From 5914c65e18d5c4a0c46c0786b61f7411c7daebe9 Mon Sep 17 00:00:00 2001 From: Siddharth Date: Thu, 16 Apr 2026 21:20:11 +0530 Subject: [PATCH 08/10] docs: add TUI screenshot to TUI guide --- docs/features/tui.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/features/tui.md b/docs/features/tui.md index 9ad79a8..75cc942 100644 --- a/docs/features/tui.md +++ b/docs/features/tui.md @@ -4,6 +4,9 @@ Plug features a rich, interactive Terminal User Interface (TUI) built with **Ink ## Core Features +![Plug TUI Screenshot](../images/tui-screenshot.png) +*(The interactive TUI for Discover, Installed, and Vaults management)* + * **Interactive Browsing**: Explore available packages from all your registered vaults. * **Search**: Find packages by name, description, or tags with real-time results. * **Batch Operations**: Select multiple packages for installation or removal. From 9628217a46806117f7761c2516c95fdb81e10e3e Mon Sep 17 00:00:00 2001 From: Siddharth Date: Thu, 16 Apr 2026 21:23:11 +0530 Subject: [PATCH 09/10] docs: add ecosystem vision and clarify current vs future capabilities --- README.md | 12 ++-- docs/architecture.md | 11 ++-- docs/project/ecosystem-vision.md | 106 +++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 10 deletions(-) create mode 100644 docs/project/ecosystem-vision.md diff --git a/README.md b/README.md index 74cc675..10a2c90 100644 --- a/README.md +++ b/README.md @@ -56,15 +56,15 @@ Claude Code extensions (Skills, Commands, and Agents) are Markdown files. Plug s --- -## 🔮 Vision & Roadmap: Claude Native Integration +## 🔮 Vision & Roadmap: The Road to v1.0 -We are building a zero-latency, native integration with Claude Code. Soon, you'll be able to manage your extensions directly from within a Claude conversation: +We are moving towards a decentralized, dependency-aware ecosystem. Our roadmap is divided into three key phases: -* **`/plug` Slash Command**: A smart, terminal-native installer that avoids the need to switch windows. -* **Natural Language Discovery**: "Claude, I need a tool for security audits" — Claude finds the right package from the vault and installs it instantly. -* **One-Click Management**: Update and remove packages through interactive Claude panels. +* **Phase A: Dependency Resolution**: Moving from single-package installs to full closure resolution using a DFS resolver. +* **Phase B: Community Marketplace**: Enabling any developer to publish from their own repo via automated frontmatter indexing. +* **Phase C: Claude Native Integration**: A zero-latency installer that works directly within Claude conversations. -View the full [Claude Integration Vision](docs/project/vision-claude-integration.md). +View the detailed [Ecosystem Vision & Roadmap](docs/project/ecosystem-vision.md). --- diff --git a/docs/architecture.md b/docs/architecture.md index 19a7a90..0fc022b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -92,14 +92,17 @@ Plug acts as the **unified harness**: * **Unified Pathing**: Extensions are routed to standard Claude Code directories (`.claude/skills/`, etc.) based on their type, ensuring Claude can always find them. * **Global vs. Project Scoping**: Plug manages the complexity of global (`~/.claude/`) vs. project-specific (`.claude/`) installation, preventing duplicate configurations and ensuring a clean development environment. -## Current vs. Future Capabilities +## Current vs. Future Capabilities (Sprints 1-3+) -| Feature | Today (Built) | Target (Vision) | +| Feature | Today (Built — Sprints 1 & 2) | Target (Vision — Sprint 3+) | |---|---|---| | **Installation** | Single-package | Full Dependency Resolution (DFS) | -| **Marketplace** | Official Registry | Multi-repo Community Index | +| **Marketplace** | Official Registry (`registry.json`) | Decentralized Community Index | +| **Publishing** | Manual Metadata (`meta.json`) | Auto-indexing via Frontmatter | | **Search** | TUI Text Search | AI-powered Natural Language | -| **Governance** | Manual Vault Add | Auto-discovery of community vaults | +| **Governance** | Manual Vault Registration | Auto-discovery of Community Vaults | + +See the full [Ecosystem Vision](project/ecosystem-vision.md) for a detailed roadmap of these phases. ## Security diff --git a/docs/project/ecosystem-vision.md b/docs/project/ecosystem-vision.md new file mode 100644 index 0000000..b29ea53 --- /dev/null +++ b/docs/project/ecosystem-vision.md @@ -0,0 +1,106 @@ +# plug — Ecosystem Vision +**Status:** planning (2026-04-16) + +--- + +## Today — What exists (Sprints 1 & 2) + +Currently, Plug provides the foundational "Unified Harness" for installing Claude Code extensions from the official registry. + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ PLUGVAULT REPO (github.com/dsiddharth2/plugvault) │ +│ │ +│ registry.json ← index of all packages │ +│ registry/ │ +│ pm/ │ +│ meta.json ← name, type, version, description │ +│ SKILL.md ← actual skill content │ +└─────────────────────────────────────────────────────────────────┘ + │ + │ plug install pm + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ USER'S MACHINE │ +│ │ +│ ~/.plugvault/config.json ← registered vaults │ +│ .plugvault/installed.json ← what's installed │ +│ │ +│ .claude/skills/pm/SKILL.md ← installed skill │ +│ │ +│ ⚠️ senior-engineer NOT installed — pm silently broken │ +└─────────────────────────────────────────────────────────────────┘ +``` + +**Current Limitations:** +1. **Single-Package Install**: `plug install pm` installs ONLY `pm`. Dependencies declared in metadata are not yet resolved, which can lead to broken extensions if requirements aren't met. +2. **Centralized Sources**: `plugvault` is the primary source of packages. While users can add vaults manually, there is no automated discovery of community-contributed vaults. +3. **Manual Publishing**: Developers must manually write `registry.json` and `meta.json` to publish, which creates friction for community growth. + +--- + +## Target — What we are building (Sprint 3 & Beyond) + +### Part 1: Dependency Resolution +We are implementing a DFS (Depth-First Search) resolver that allows authors to declare dependencies directly in their skill's frontmatter. + +* **Closure Resolution**: Installing a package will automatically resolve and present an "Install Plan" for its full dependency closure. +* **Scope Awareness**: The resolver tracks whether dependencies are satisfied in the project (`.claude/`) or global (`~/.claude/`) scope. +* **Graph-Aware Removal**: Uninstalling a package will check for dependents to prevent breaking other installed extensions. + +### Part 2: Community Marketplace (Cross-Repo Sources) +We are moving away from the single-registry model to a decentralized marketplace indexed by `plugvault`. + +* **Developer Workflow**: Just commit a `.md` file with frontmatter to your own GitHub repo. No `registry.json` needed. +* **Automated Indexing**: A GitHub Action in the `plugvault` repo will daily scan a curated list of trusted community repos and build a `community-index.json`. +* **One-Fetch Search**: The TUI hits this single index to provide instant search across the entire community ecosystem. + +--- + +## Full Ecosystem Picture + +``` + VAULT REPOS (GitHub) + ┌──────────────────────┐ ┌─────────────────────────┐ + │ plugvault (official)│ │ developer/my-skills-repo│ + │ registry.json │ │ superpowers.md │ + │ registry/pm/ │ │ (frontmatter only) │ + │ SKILL.md │ └──────────┬───────────────┘ + │ (frontmatter) │ │ + └──────────┬───────────┘ │ + │ ┌────────────────┘ + │ │ GitHub Action (daily) + ▼ ▼ + ┌─────────────────────────────┐ + │ community-index.json │ + │ (all packages + deps, │ + │ built from frontmatter) │ + └──────────────┬──────────────┘ + │ 1 HTTP fetch (cached 1hr) + ▼ + ┌────────────────┐ + │ plug TUI │ ← primary interface + │ resolver.js │ ← builds install plan + │ install.js │ ← executes plan + └────────┬───────┘ + │ + ▼ + USER'S MACHINE + .claude/skills/pm/SKILL.md + .claude/skills/superpowers/SKILL.md + .claude/agents/senior-engineer.md + .plugvault/installed.json ← with dependency edges +``` + +--- + +## Build Phases + +### Phase A — Dependency Resolution (Current Focus) +Implementing the `resolver.js` and extending `tracker.js` to handle `installed_as`, `dependencies`, and `dependents` fields. Includes the TUI "Install Plan" screen with scope toggles. + +### Phase B — Community Marketplace +Building the `community-index.json` pipeline and the TUI "Community Vaults" discovery browser. + +### Phase C — Integration +Connecting the resolver to the community index to handle dependencies across multiple vaults seamlessly. From 7224ed8c88f0080e17046a97e401f3dad7e132c3 Mon Sep 17 00:00:00 2001 From: Siddharth Date: Thu, 16 Apr 2026 21:25:29 +0530 Subject: [PATCH 10/10] docs: expand TUI guide with detailed screens and workflows --- docs/features/tui.md | 127 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 108 insertions(+), 19 deletions(-) diff --git a/docs/features/tui.md b/docs/features/tui.md index 75cc942..4472b33 100644 --- a/docs/features/tui.md +++ b/docs/features/tui.md @@ -13,27 +13,116 @@ Plug features a rich, interactive Terminal User Interface (TUI) built with **Ink * **Vault Management**: Add, remove, and configure vaults (including private vault tokens) directly in the UI. * **Contextual Feedback**: See detailed installation progress and result summaries. +## Screens & Workflows + +The Plug TUI is designed to be the primary interface for all operations. No commands to memorize—just run `plug`. + +### Flow 1: Discover and Search +The entry point for finding new extensions. Results from all registered vaults are merged into one list. + +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ plug [Discover] Installed Vaults v2.0 │ +├──────────────────────────────────────────────────────────────────────┤ +│ / search... │ +├──────────────────────────────────────────────────────────────────────┤ +│ │ +│ NAME TYPE VER DEPS VAULT │ +│ ▶ superpowers skill v1.0.0 ★ 3 deps my-skills-repo 🔒 │ +│ senior-engineer agent v1.0.3 · no deps official │ +│ code-review skill v1.1.0 · no deps official │ +│ onboarding-kit skill v2.0.0 ★ 4 deps company 🔒 │ +│ │ +│ 🔒 = private vault ✓ = already installed │ +│ │ +├──────────────────────────────────────────────────────────────────────┤ +│ [Enter] detail [i] install [/] search [Tab] next tab │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +* **Real-time Filtering**: Press `/` to search. The list filters instantly as you type. +* **Private Badges**: Packages from private vaults are marked with a `🔒` badge. + +--- + +### Flow 2: Package Detail & Dependency Awareness +Press `Enter` on any package to see its full metadata, source vault, and dependency status. + +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ superpowers [skill] v1.0.0 │ +│ Gives Claude superpowers │ +│ │ +│ Source: my-skills-repo (github.com/developer/my-skills-repo) │ +│ │ +│ Dependencies: │ +│ ✓ senior-engineer agent v1.0.3 official installed │ +│ ✗ code-review skill v1.1.0 official not installed │ +│ │ +│ Installing this will also install: code-review (from official) │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +--- + +### Flow 3: Install Plan & Scope Selection +Press `i` to trigger the installation. Plug builds a "Plan" showing exactly what will be written. + +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ Install plan — superpowers │ +├──────────────────────────────────────────────────────────────────────┤ +│ Install scope: ◉ Project (.claude/) ○ Global (~/.claude/) │ +│ │ +│ Required — always installed: │ +│ [✓] superpowers skill v1.0.0 new my-skills-repo │ +│ [✓] code-review skill v1.1.0 new official │ +│ │ +│ Already satisfied — skipped: │ +│ [✓] senior-engineer agent v1.0.3 installed official │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +* **Scope Toggle**: Use `Tab` to switch between Project (`.claude/`) and Global (`~/.claude/`) installation. +* **Dependency Closure**: Plug automatically includes all required dependencies in the plan. + +--- + +### Flow 4: My Packages (Installed) +Manage your extensions across different scopes. + +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ plug Discover [Installed] Vaults │ +├──────────────────────────────────────────────────────────────────────┤ +│ SCOPE NAME TYPE VER VAULT │ +│ [G] senior-engineer agent v1.0.3 official │ +│ [P] ▶ superpowers skill v1.0.0 my-skills-repo │ +│ │ +│ [G] global ~/.claude/ [P] project .claude/ │ +├──────────────────────────────────────────────────────────────────────┤ +│ [Enter] detail [d] uninstall [u] update │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +* **Uninstall Safety**: If you try to remove a package that others depend on, Plug will prompt you to either cascade the removal or force-remove just the target. + --- -## Screens - -### 1. Discover -The entry point for finding new extensions. -* **Tabs**: Filter by package type (Skills, Commands, Agents). -* **Search Bar**: Activate with `/` to filter the package list. -* **Install Queue**: Toggle packages with `Space` and install them all at once with `Enter`. - -### 2. My Packages -Manage your currently installed extensions. -* **Scope View**: See both local and global installations. -* **Update Checks**: Check for newer versions and update them individually or in batch. -* **Safe Removal**: Uninstall packages and clean up their tracking data. - -### 3. Vaults -Configure your package sources. -* **Resolve Order**: Change which vaults are prioritized during installation. -* **Private Vaults**: Interactively enter and update GitHub Personal Access Tokens (PAT). -* **Connectivity Test**: Instantly verify if a vault is reachable. +### Flow 5: Vault Management +Add sources and manage authentication without touching a config file. + +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ Add vault │ +├──────────────────────────────────────────────────────────────────────┤ +│ Name: company │ +│ URL: https://github.com/myco/claude-skills │ +│ Token: •••••••••••••••• (for private repos) │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +* **Community Discovery**: Browse a curated list of community vaults and add them with a single click. ---