Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b8e6e69
docs: revise plan — addDependents merge semantics + cascade flag defi…
dsiddharth2 Apr 16, 2026
77525a5
review: sprint 3 dep-resolution plan re-review — APPROVED
dsiddharth2 Apr 16, 2026
5383e4b
feat(tracker): extend installed.json with installed_as/dependencies/d…
dsiddharth2 Apr 16, 2026
b120b35
review: sprint 3 phase 1 tracker — APPROVED
dsiddharth2 Apr 16, 2026
556fc78
feat(resolver): add DFS dependency resolver
dsiddharth2 Apr 16, 2026
5d6aab2
chore(progress): mark Phase 2 complete (2.1–2.V)
dsiddharth2 Apr 16, 2026
1e3927f
review: sprint 3 phase 2 resolver — APPROVED
dsiddharth2 Apr 16, 2026
92bd4ca
feat(install): wire resolver + TUI scope toggle on dep installs only
dsiddharth2 Apr 16, 2026
9eed459
review: sprint 3 phase 3 install wiring + TUI plan — APPROVED
dsiddharth2 Apr 16, 2026
ffed45c
feat(remove): dependent check, cascade/force, orphan pruning
dsiddharth2 Apr 16, 2026
3c6ff9d
chore(progress): mark Phase 4 complete (4.1-4.V)
dsiddharth2 Apr 16, 2026
6cf1e05
review: sprint 3 phase 4 dependent check + cascade/force + orphan pru…
dsiddharth2 Apr 16, 2026
b9e459d
feat(install): post-install hook notice
dsiddharth2 Apr 16, 2026
e9eb6d7
review: sprint 3 phase 5 post-install hook notice - APPROVED
dsiddharth2 Apr 17, 2026
8b9e958
docs: harvest sprint/dep-resolution — dependency resolution feature +…
dsiddharth2 Apr 17, 2026
a77a7e5
review: harvest sprint/dep-resolution docs - CHANGES NEEDED
Apr 17, 2026
67a0d3c
docs: correct remove command UX - interactive prompts, not CLI flags
dsiddharth2 Apr 17, 2026
a7a0bcd
docs: annotate feedback after harvest fix
dsiddharth2 Apr 17, 2026
853c241
review: harvest re-review sprint/dep-resolution docs — APPROVED
dsiddharth2 Apr 17, 2026
da6ec5a
cleanup: remove fleet control files
dsiddharth2 Apr 17, 2026
45bc4c7
fix(tui): add arrow navigation to install plan and resolve community …
dsiddharth2 Apr 17, 2026
9b12a7d
feat(install): support multi-file downloads for community and officia…
dsiddharth2 Apr 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
319 changes: 234 additions & 85 deletions feedback.md

Large diffs are not rendered by default.

257 changes: 0 additions & 257 deletions plug/PLAN.md

This file was deleted.

14 changes: 13 additions & 1 deletion plug/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,30 @@ plug init
```

### `install`
Directly install a package by name.
Directly install a package by name. If the package has dependencies, `plug` resolves and installs them first, then prompts for confirmation (skip with `--yes`).
```bash
plug install code-review
plug install -g my-vault/api-patterns # Global installation
plug install subagent-driven-development --yes # Auto-confirm dep plan
```

If an installed skill declares `hook:` in its frontmatter, a notice is printed reminding you to add the hook to `settings.json`.

### `remove`
Uninstall a package.
```bash
plug remove code-review
plug remove -g code-review # Remove from global ~/.claude/
plug remove code-review --yes # Auto-prune orphaned dependencies without prompting
```

If other packages depend on the target, `plug` shows an interactive prompt with three choices:
- **Cancel** — abort, nothing changes.
- **Remove all (cascade)** — remove dependent packages first (one level), then remove the target.
- **Force remove** — remove only the target; severs dependent edges without touching dependent packages.

After any successful remove, `plug` checks for orphaned auto-installed dependencies. If any exist, it prompts to remove them. `--yes` skips this confirmation and prunes automatically.

### `list`
List all currently installed packages. Use `--remote` to see all available packages.
```bash
Expand Down
82 changes: 82 additions & 0 deletions plug/docs/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# API Reference

## resolver.js

### `resolve(pkgName, vaultHint?, options?)`

```ts
resolve(pkgName: string, vaultHint?: string | null, options?: { global?: boolean })
: Promise<{ toInstall: string[], alreadySatisfied: string[], cycles: string[] }>
```

Resolves the full dependency graph for `pkgName` and returns an install plan.

| Return field | Type | Description |
|---|---|---|
| `toInstall` | `string[]` | Packages to install, dependency-first. Root package is last. |
| `alreadySatisfied` | `string[]` | Packages already in `installed.json` for the given scope. |
| `cycles` | `string[]` | Package names where a back-edge was detected; not added to `toInstall`. |

`vaultHint` is accepted but not currently used for routing — the full registry+community package map is always built. `options.global` (default `false`) selects which `installed.json` is consulted for the snapshot.

---

## tracker.js (Sprint 3 additions)

### `addDependents(name, newDependents, global?)`

```ts
addDependents(name: string, newDependents: string[], global?: boolean): Promise<void>
```

Merges `newDependents` into `installed[name].dependents` (dedup via `Set`). No-ops if `name` is not in `installed`. Saves.

### `getInstalledRecord(name, global?)`

```ts
getInstalledRecord(name: string, global?: boolean): Promise<object | null>
```

Returns `installed[name]` or `null` if not tracked.

### `prunableOrphans(global?)`

```ts
prunableOrphans(global?: boolean): Promise<string[]>
```

Returns names where `installed_as === 'dependency'` and `dependents.length === 0`. These are candidates for automatic pruning after a remove.

### `removeDependentEdge(fromName, toName, global?)`

```ts
removeDependentEdge(fromName: string, toName: string, global?: boolean): Promise<void>
```

Removes `fromName` from `installed[toName].dependents`. No-ops if `toName` is not tracked. Saves.

---

## frontmatter.js

### `parseFrontmatter(content)`

```ts
parseFrontmatter(content: string): Record<string, string>
```

Parses a YAML fence block from the top of a string. Returns `{}` on absent or malformed frontmatter. Never throws.

`install.js` checks the returned record for `fm.hook || fm.hooks` to decide whether to emit a hook-required notice. The function itself is a pure parser with no install-specific logic.

---

## trackInstall metadata (Sprint 3 additions)

`trackInstall(name, metadata, global?)` now persists three additional optional fields from `metadata`:

| Field | Default | Notes |
|---|---|---|
| `installed_as` | `'explicit'` | `'dependency'` for auto-installed deps |
| `dependencies` | `[]` | Direct dep names at install time |
| `dependents` | `[]` | Populated later via `addDependents` |
113 changes: 113 additions & 0 deletions plug/docs/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Architecture

## installed.json Schema (Sprint 3+)

`installed.json` tracks every installed package under the `installed` key. Each record now carries three additional fields:

```json
{
"installed": {
"senior-engineer": {
"type": "skill",
"vault": "official",
"version": "1.0.0",
"path": ".claude/skills/senior-engineer/",
"installedAt": "2026-04-17T00:00:00.000Z",
"installed_as": "explicit",
"dependencies": [],
"dependents": []
},
"subagent-driven-development": {
"type": "skill",
"vault": "community",
"version": "1.0.0",
"path": ".claude/skills/subagent-driven-development/",
"installedAt": "2026-04-17T00:00:00.000Z",
"installed_as": "explicit",
"dependencies": ["using-git-worktrees", "writing-plans"],
"dependents": []
},
"using-git-worktrees": {
"type": "skill",
"vault": "community",
"version": "1.0.0",
"path": ".claude/skills/using-git-worktrees/",
"installedAt": "2026-04-17T00:00:00.000Z",
"installed_as": "dependency",
"dependencies": [],
"dependents": ["subagent-driven-development"]
}
}
}
```

Field semantics:

| Field | Values | Default |
|-------|--------|---------|
| `installed_as` | `"explicit"` \| `"dependency"` | `"explicit"` |
| `dependencies` | Direct dep names declared at install time | `[]` |
| `dependents` | Packages that required this one | `[]` |

Older records without these fields are normalised on read: any code path touching `rec.installed_as` uses `rec.installed_as ?? 'explicit'`. No migration is needed.

## DFS Resolver (`src/utils/resolver.js`)

The resolver performs a depth-first traversal of the dependency graph to produce an ordered install plan.

**Entry point:**
```js
resolve(pkgName, vaultHint = null, options = {})
// returns Promise<{ toInstall: string[], alreadySatisfied: string[], cycles: string[] }>
```

**Algorithm:**

1. `buildPackageMap()` — fetches official registry(ies) then the community index into a single `Map<name, pkgData>`. Community entries win on name conflict. Both sources are catch-wrapped (offline-safe); a missing source is skipped.
2. `getInstalled(isGlobal)` is called **once** per `resolve()` invocation. The snapshot is passed into DFS — never called per-node.
3. DFS (`dfsResolve`):
- Back-edge (`inStack` hit) → push to `cycles`, return.
- Already-visited → return.
- For each required dep (deps with `required !== false`; unknown deps silently skipped): recurse.
- After all deps: check snapshot → push to `alreadySatisfied` or `toInstall`.
- Mark visited.
4. Result: `toInstall` is in dependency-first order; the root package is always last.

Optional deps (`required: false`) are silently skipped. Unknown package names in dep lists are silently skipped (offline-safe).

## Install-Plan TUI Component (`src/tui/components/install-plan.jsx`)

Props: `{ queue, plan, loading, onConfirm(scope: 'project'|'global'), onCancel }`

States:
- While `loading`: spinner.
- Ready: "Will install" section (from `plan.toInstall`), "Already satisfied" section (`plan.alreadySatisfied`), scope selector (`Tab` toggles between `project` and `global`), footer `[i] Install [Esc] Cancel`.

`onConfirm` is called with the selected scope string. `onCancel` is called on Esc.

The component is rendered by `DiscoverView` when a package has dependencies (`plan.toInstall.length > 1`). Single-package installs bypass the plan screen entirely and call `doInstall` immediately.

`ctx.set({ yes: true, json: true })` is called inside `doInstall()` only — never during the resolver async phase — to prevent context state pollution while the plan view is displayed.

## Frontmatter Parser (`src/utils/frontmatter.js`)

```js
parseFrontmatter(content)
// returns Record<string, string> — {} on no frontmatter or malformed input
```

Parses a YAML fence block at the top of a file:

```
---
key: value
hook: pre-tool-use
---
```

- Regex: `^---\r?\n([\s\S]*?)\r?\n---` — non-greedy, handles CRLF.
- Key-value split uses `indexOf(':')` so colons in values are preserved (e.g., `hook: pre-tool-use:v2`).
- Lines without a colon are skipped; empty keys are skipped.
- Any regex miss returns `{}` — no exception thrown.

Used by `install.js` after writing the SKILL.md file to disk. Only invoked for `type === 'skill'` packages.
39 changes: 39 additions & 0 deletions plug/docs/decisions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Decisions & Trade-offs

## DFS over topological sort

The resolver uses iterative DFS rather than a two-pass topological sort (Kahn's algorithm). The dependency graphs for Claude Code skills are small and shallow; DFS produces a valid dep-first ordering in a single pass, handles cycle detection inline via an `inStack` set, and requires no auxiliary data structures (in-degree map, queue). Kahn's would add code for no practical gain at this scale.

## `addDependents` merge semantics

`addDependents(name, newDependents, global)` **merges** into the existing `dependents` array (with dedup via `Set`) rather than replacing it.

Replacing would corrupt multi-parent packages. If package X is a dependency of both A and B:
- After installing A: `X.dependents = ['A']`
- Installing B calls `addDependents('X', ['B'])` → must yield `['A', 'B']`, not `['B']`

Overwrite semantics would silently erase A from X's dependents, causing a false orphan detection when A is later removed.

## Hook notice is print-only

The post-install hook notice (`⚠ Hook required: '<name>' expects a hook in settings.json`) is a warning, not an automatic action. `plug` does not modify `settings.json`.

Rationale: hook configuration is settings-file surgery that can break the user's existing hooks or add hooks with wrong event names. Silently mutating `settings.json` without user review was judged higher risk than a missed notice. The user retains full control.

## JSON mode vs CLI mode split for hook notice

In CLI mode, the notice is printed to stdout as a chalk-yellow warning. In JSON mode (`--json`), the output object gains `hookRequired: true` (conditionally — absent when false).

Mixing a human-readable warning string into JSON output would break machine consumers. The boolean field lets callers (CI scripts, wrapper tools) detect and act on the hook requirement without parsing prose.

## Shallow cascade on remove

The "Remove all (cascade)" interactive choice removes direct dependents one level deep. When a dependent is removed as part of a cascade, it does not further cascade into its own dependents. Deep transitive cascade is deferred to a future sprint.

Rationale: deep cascade is hard to reason about and easy to trigger accidentally. Shallow cascade covers the common case (A depends on X; removing X cascades to A) with a predictable blast radius. Users who need deeper removal can repeat the command.

## Community wins on registry name conflict

In `buildPackageMap()`, community-index entries are loaded after official vault entries and overwrite on name collision. This allows community packages to shadow official ones when both registries contain the same name.

Rationale: community packages are typically more specialised or customised variants; a community author explicitly publishing under the same name is expressing an intentional override.
64 changes: 64 additions & 0 deletions plug/docs/features/dependency-resolution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Dependency Resolution

Sprint 3 taught the install/remove lifecycle about dependencies. Packages can declare dependencies; `plug` resolves and installs them automatically.

## Install

When you run `plug install <package>`, the resolver checks whether the target package has dependencies:

- **No dependencies** — installs immediately, same behaviour as before Sprint 3.
- **Has dependencies** — builds an install plan and presents it before proceeding:
- CLI: prints a summary of packages to install and packages already satisfied; prompts "Proceed? (Y/n)" unless `--yes` is set.
- TUI: shows the **Install Plan** screen listing "Will install" and "Already satisfied" sections. Tab toggles between project scope (`.claude/`) and global scope (`~/.claude/`). Press `i` to confirm or Esc to cancel.

Dependencies are installed **before** the root package (depth-first order). Each dependency is tracked as `installed_as: "dependency"`; the root package is tracked as `installed_as: "explicit"`.

After all installs, each dependency's `dependents` list is updated to include the root package name.

### Scope toggle (TUI only)

The plan screen exposes a scope selector that controls whether the entire install (root + deps) lands in the project scope or globally. The selector only appears when the resolver finds additional packages to install.

### JSON mode

```
plug install <package> --json
```

The JSON output includes `hookRequired: true` when the installed skill declares a `hook:` or `hooks:` field in its frontmatter.

## Remove

```
plug remove <package>
plug remove <package> --yes # Auto-prune orphans without prompting
```

When removing a package that other packages depend on, `plug` shows an interactive prompt:

```
'<package>' is required by: <dependent>, <dependent>. How do you want to proceed?
> Cancel
Remove all (cascade) — removes dependents first, then this package
Force remove — removes only this package, severs dependent edges
```

| Choice | Behaviour |
|--------|-----------|
| Cancel | Aborts; nothing is changed. |
| Remove all (cascade) | Removes each dependent package first (one level deep), then removes the target. |
| Force remove | Removes only the target; severs dependent edges without removing dependent packages. |

`--cascade` and `--force` are **not** CLI flags. They are choices in the interactive prompt that appears when dependents are detected.

**Orphan pruning:** after any successful remove, `plug` checks for installed-as-dependency packages whose `dependents` list is now empty. If any exist, it prompts to prune them. `--yes` (a global flag) auto-prunes without prompting.

## Post-install hook notice

If an installed skill's frontmatter declares `hook:` or `hooks:`, `plug` prints:

```
⚠ Hook required: '<name>' expects a hook in settings.json
```

This is a notice only — `plug` does not modify `settings.json`. The user must wire the hook manually.
Loading
Loading