Skip to content

wedge stage 10: distribution (skill/plugin install, tools make)#184

Merged
gnanam1990 merged 5 commits into
mainfrom
feat/wedge10-distribution
Jun 13, 2026
Merged

wedge stage 10: distribution (skill/plugin install, tools make)#184
gnanam1990 merged 5 commits into
mainfrom
feat/wedge10-distribution

Conversation

@gnanam1990

@gnanam1990 gnanam1990 commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

Stage 10 — Distribution

ZERO had skills/plugins but no way to install them (skills were discovery-only from a local dir) and no tool scaffolder. This adds a file-backed, local distribution surface so extensibility compounds into an ecosystem. There is no central registry: a "source" is just a git URL or a local path, and every install is checksum-pinned in a lockfile.

What was built

internal/skills/install.goInstall, Remove, Info, lockfile (skills.lock).

  • Fetches a skill from a git URL (shallow clone via an injectable runner) or a local path, locates the SKILL.md (root or a single subdir), and validates its frontmatter through the existing skills parser. Invalid/nameless skills are rejected and nothing is written.
  • Copies the SKILL.md into skills.DefaultDir(env)/<name>/, records a sha256 content hash mapped to its source in skills.lock. Reinstalling surfaces the old→new hash change.
  • A name clash with a different source is refused unless --force; reinstalling from the same source is idempotent.

internal/plugins/install.goInstall, Remove, lockfile (plugins.lock).

  • Same fetch + pin flow, validated against the existing plugin manifest schema (ParseManifest). The whole plugin tree is copied (entry scripts, prompts, skills) so the plugin is runnable through activation — .git and symlinks are skipped so clone metadata and link-escapes never land in the plugins dir.
  • Installed plugins still go through normal activation with permission gating; no install script is run.

internal/tools/scaffold.goScaffold.

  • Generates a minimal, prompt-gated plugin-tool skeleton in the toolbox dir: a plugin.json (schemaVersion 1, one tool, empty parameter schema, command pointing at the entry stub) plus a runnable entry stub carrying a clear TODO. Default runtime is a POSIX shell stub (no interpreter dependency); node/python are also available. After plugin activation the scaffolded tool is loadable like any plugin.

CLI wiring

  • zero skill {add,info,remove} — extended internal/cli/skills.go.
  • zero plugin {add,remove} — extended internal/cli/extensions.go.
  • zero tools {make,list} — new command registered in internal/cli/app.go (dispatch + help), handlers in the new internal/cli/distribution.go.
  • New appDeps resolvers pluginsDir/toolsDir (the user plugins root) added in app.go.

Safety properties

  • No opaque remote code without a fingerprint: every install records a content hash in a lockfile; updates show the hash change.
  • Never executes fetched content: skills are markdown (validated, never run); plugin install scripts are never executed; the scaffolder only writes files. Tests assert a hostile install.sh in a fetched source does not run and is not copied into the skills dir.
  • Network: git fetch inherits the process environment (so proxy/egress settings are honored) and sets GIT_TERMINAL_PROMPT=0 so a missing-credential clone fails fast instead of blocking.

Files

  • internal/skills/install.go + internal/skills/install_test.go
  • internal/plugins/install.go + internal/plugins/install_test.go
  • internal/tools/scaffold.go + internal/tools/scaffold_test.go
  • internal/cli/distribution.go + internal/cli/distribution_test.go
  • internal/cli/app.go, internal/cli/extensions.go, internal/cli/skills.go (subcommand wiring + help)

How it was tested

TDD (RED→GREEN). Per-package unit tests cover the spec's list: local install + hash recording, invalid-source rejection, reinstall hash change + lockfile update, name-clash guard (+--force), manifest validation, scaffold manifest parses against the real schema, list/info/remove reflect installed state, and "install never executes fetched content". Added real-git integration tests (a local file:// repo exercising the default runner end to end, asserting .git does not leak) and a provider-neutrality assertion on the scaffolded manifest. End-to-end smoke through the built binary confirms the full add/list/info/update/remove and make/list lifecycle.

Gate: gofmt -l . clean, go build ./... succeeds, go test ./... passes.

Deferred / out of scope

  • Release-tarball fetch (the spec listed it as an alternative to shallow clone; clone is implemented and the runner is injectable).
  • Merging plugin-declared skill paths into the skills loader and runtime registration of installed plugin tools remain the separately-tracked Stage 09 / multi-root work, unchanged here.

Summary by CodeRabbit

  • New Features

    • New top-level CLI commands: tools (make, list), skill (add, info, remove), and plugin (add, remove); help updated.
    • Tool scaffolding creates runnable stubs (shell/Node/Python) and a plugin manifest.
    • CLI now respects user-scoped plugins/tools directory defaults.
  • Behavior

    • Installs/removals record deterministic content hashes and lockfiles; force/overwrite rules enforced; list output supports human and --json modes and surfaces loader diagnostics.
  • Tests

    • Extensive end-to-end and unit tests for CLI, install, scaffold flows.

Add a file-backed distribution surface so skills and plugins can be
installed from a git URL or local path, and new plugin-tools can be
scaffolded. No central registry: a source is just a git URL or path.

- internal/skills/install.go: Install/Remove/Info + skills.lock. Validates
  SKILL.md frontmatter via the existing parser, copies it into the skills
  dir, and records a sha256 content hash. Reinstall surfaces the hash
  change; a clash with a different source is refused without --force.
- internal/plugins/install.go: same flow validated against the plugin
  manifest schema, copying the whole plugin tree (minus .git) so the
  plugin is activatable. plugins.lock records source + hash.
- internal/tools/scaffold.go: generate a prompt-gated plugin-tool skeleton
  (manifest + runnable shell/node/python entry stub) in the toolbox dir.
- CLI: zero skill {add,info,remove}, zero plugin {add,remove}, and a new
  zero tools {make,list}; wired in app.go/extensions.go/skills.go.

Safety: fetch honors the process environment (proxy/egress) and disables
git credential prompts; install never executes fetched content (skills are
markdown; plugin install scripts are never run) and installed plugins still
go through normal activation with permission gating.
@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Zero automated PR review

Verdict: No blockers found

Blockers

  • None found.

Validation

  • [pass] Diff hygiene: git diff --check
  • [pass] Tests: go test ./...
  • [pass] Build: go run ./cmd/zero-release build
  • [pass] Smoke build: go run ./cmd/zero-release smoke

Scope

Head: 8ad9f5a5f4a7
Changed files (11): internal/cli/app.go, internal/cli/distribution.go, internal/cli/distribution_test.go, internal/cli/extensions.go, internal/cli/skills.go, internal/plugins/install.go, internal/plugins/install_test.go, internal/skills/install.go, internal/skills/install_test.go, internal/tools/scaffold.go, internal/tools/scaffold_test.go

This deterministic review checks validation status and basic diff hygiene. A human reviewer still owns product judgment and design quality.

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4e0f3af7-809c-41d5-820f-438cdbffd840

📥 Commits

Reviewing files that changed from the base of the PR and between 11d7962 and 8ad9f5a.

📒 Files selected for processing (2)
  • internal/cli/distribution.go
  • internal/plugins/install.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/cli/distribution.go
  • internal/plugins/install.go

Walkthrough

Adds skill and plugin installers with deterministic hashing and lockfiles, a tool scaffold producing plugin manifests and entry stubs, CLI handlers for skill/plugin/tools commands (add/info/remove, make/list) with JSON/text output, tests covering lifecycle and edge cases, and app wiring for user plugin/tool directory resolution.

Changes

Distribution infrastructure for skills, plugins, and tools

Layer / File(s) Summary
Skills installation and lockfile management
internal/skills/install.go, internal/skills/install_test.go
Installs/removes/queries skills from git or local sources, validates SKILL.md names, computes sha256: content hashes, persists deterministic skills.lock, enforces name-clash semantics with Force, and exposes Info. Tests cover git/local installs, clobbering, idempotence, and hash tracking.
Plugin installation and lockfile management
internal/plugins/install.go, internal/plugins/install_test.go
Installs/removes plugins from git or local sources by locating/validating plugin.json, derives install ID from manifest, computes deterministic tree hash, persists plugins.lock, copies the tree safely (skip .git, no symlink recreation), canonicalizes local sources, and enforces Force semantics. Tests cover manifest validation, hash updates, git installs, clashes, and full-tree copying.
Tool scaffolding with runtime templates
internal/tools/scaffold.go, internal/tools/scaffold_test.go
Scaffolds local tool plugin skeletons under <Dir>/<name>/, validates names, writes runtime-specific entry stubs (shell/node/python) and plugin.json with a single prompt-gated tool; returns created paths. Tests validate manifest parseability, stub TODO marker, name checks, clobber protection, and runtime templates.
Distribution CLI command handlers
internal/cli/distribution.go, internal/cli/distribution_test.go, internal/cli/extensions.go, internal/cli/skills.go
Adds zero skill (add/info/remove), zero plugin (add/remove), and zero tools (make/list) CLI handlers with shared parsing (--force, --json), redacted pretty JSON or human text, deterministic tooling listing via the plugin loader, help writers, and end-to-end tests for lifecycles, invalid inputs, diagnostics, empty states, and TUI non-interference.
App-level dependency injection and command dispatch
internal/cli/app.go
Adds pluginsDir and toolsDir dependency resolvers, implements defaultUserPluginsDir() resolving the user-scoped plugins root, fills defaults in defaultAppDeps()/fillAppDeps(), routes top-level tools command to the new handlers, and updates top-level help.

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant DistHandler
  participant PluginInstaller
  participant GitRunner
  participant FS
  CLI->>DistHandler: zero plugin add <source> (--force/--json)
  DistHandler->>PluginInstaller: Install(ctx, InstallOptions{Source, Dir, Force})
  PluginInstaller->>GitRunner: clone (for remote sources)
  GitRunner-->>PluginInstaller: local checkout path
  PluginInstaller->>FS: locate plugin.json, read files
  PluginInstaller->>PluginInstaller: hashTree -> sha256:<hex>
  PluginInstaller->>FS: copyTree -> install dir (skip .git, no symlinks)
  PluginInstaller->>FS: write plugins.lock (canonical source + hash)
  PluginInstaller-->>DistHandler: InstallResult (ManifestPath, Hash, Updated)
  DistHandler-->>CLI: formatted output (JSON or human text)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Gitlawb/zero#128: Establishes initial skills command routing; this PR expands that layer with installation and CLI handlers.

Suggested reviewers

  • Vasanthdev2004
  • anandh8x
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: implementing distribution features (skill/plugin installation and tools scaffolding), which aligns with the core objective of this PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/wedge10-distribution

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (1)
internal/tools/scaffold_test.go (1)

14-147: ⚡ Quick win

Cover the non-default runtimes too.

The suite only exercises the default shell scaffold, but runtimeSpecFor has separate Node and Python branches with different filenames, commands, and modes. A small table-driven test over RuntimeShell, RuntimeNode, and RuntimePython would catch template/manifest drift in the branches users can actually select.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tools/scaffold_test.go` around lines 14 - 147, Add a table-driven
test that calls tools.Scaffold with each runtime variant (RuntimeShell,
RuntimeNode, RuntimePython) to exercise the separate branches in runtimeSpecFor;
for each runtime, assert Scaffold returns no error, that result.ManifestPath and
result.EntryPath exist and live under the plugin dir, that plugins.ParseManifest
succeeds for the generated manifest, and that the tool command/args reference
the entry (reuse commandReferencesEntry). Keep assertions the same as in
TestScaffoldCreatesManifestAndStub but loop over runtimes (or create a new Test
like TestScaffoldCoversAllRuntimes) and set ScaffoldOptions.Runtime accordingly
to catch template/manifest drift across the Node and Python branches.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/cli/distribution.go`:
- Around line 340-343: The "Next steps" scaffold message incorrectly suggests
"zero plugins list"; update the string literal "Run `zero plugins list` to
confirm it loads." to "Run `zero tools list` to confirm it loads." so the hint
checks the tool discovery path (especially when deps.toolsDir() !=
deps.pluginsDir()); locate and change the message in the Next steps block
generated in distribution.go.
- Around line 372-386: The toolbox listing currently calls plugins.Load and
ignores non-fatal diagnostics (result.Diagnostics), causing broken plugins to
silently disappear; update the runToolsList flow to surface those diagnostics by
checking result.Diagnostics after plugins.Load and either (a) include them in
the JSON response (extend the payload passed to writePrettyJSON to include
Diagnostics and use redaction.RedactValue on the whole payload) or (b) for text
output, print/redact result.Diagnostics to stderr (or return an error via
writeAppError) so users see loader warnings; reference plugins.Load,
result.Diagnostics, toolboxItems, writePrettyJSON, writeAppError and
formatToolboxList to locate where to add the diagnostics handling.
- Around line 148-168: parseNameCommandArgs returns a JSON flag that the remove
handlers currently ignore; update runSkillRemove (and the analogous
plugin/remove handlers referenced) to capture the returned jsonFlag (the second
return value from parseNameCommandArgs) and handle it: either reject it by
returning a clear app/usage error when jsonFlag is true (e.g.,
writeAppError(stderr, "flag --json is not supported for remove commands",
exitUsage)) or implement a JSON response path that emits a machine-readable
success/failure object instead of the human text message; apply the same change
to the other remove handlers (the plugin remove function and the other remove
blocks mentioned) so --json is either honored or explicitly disallowed
consistently.

In `@internal/plugins/install.go`:
- Around line 120-145: The installer currently hashes only the manifest via
hashContent(data) but copies the full filtered plugin tree with
copyTree(pluginDir, target), causing mismatches when non-manifest files change;
update the install flow to compute the hash over the exact same filtered tree
that copyTree installs (i.e., walk pluginDir using the same include/exclude
rules used by copyTree and feed the concatenated/ordered contents into the
hasher instead of only hashing `data`), keep using LockEntry{Source: source,
Hash: hash} but set Hash to this tree-hash, and ensure writeLock/readLock logic
remains unchanged; also add a regression test that installs a plugin, modifies a
nested file (e.g., a tool script or prompt), reinstalls and asserts the lock
hash changed and Updated=true so nested-file changes are detected.

In `@internal/skills/install.go`:
- Around line 87-129: The installers compare the raw CLI source string which
makes local paths appear different (relative vs absolute); canonicalize local
filesystem sources before storing or comparing. Detect when options.Source
refers to a local path (not a remote git URL), resolve it to an absolute,
symlink-evaluated canonical path (e.g., via filepath.Abs +
filepath.EvalSymlinks) and use that canonicalSource in place of options.Source
for fetchSource, for the hash/manifest handling, and when writing/reading lock
entries (the values compared with previous.Source). Apply the same change in
internal/skills/install.go (refs: source, fetchDir, fetchSource,
previous.Source, ReadLock) and internal/plugins/install.go so that clash
detection always uses the normalized local path.
- Around line 132-149: Current install flow in internal/skills/install.go writes
directly into target (using target, skillFileName) after RemoveAll, then updates
lock (lock, LockEntry, writeLock), risking loss if process crashes; change to a
staged-swap: create a temp sibling directory (e.g., target+".tmp" or use
os.MkdirTemp under dir), write the SKILL.md and any files into that temp dir,
fsync files and directory, update the in-memory lock entry
(lock[name]=LockEntry{...}) and write the new lockfile to a temp file as well,
fsync it, then atomically rename the temp dir to target and rename the temp lock
to the real lock file so on-disk content and lockfile swap together; ensure any
cleanup of old temp dirs on failure and use appropriate perms and error handling
around fsync/rename operations.

---

Nitpick comments:
In `@internal/tools/scaffold_test.go`:
- Around line 14-147: Add a table-driven test that calls tools.Scaffold with
each runtime variant (RuntimeShell, RuntimeNode, RuntimePython) to exercise the
separate branches in runtimeSpecFor; for each runtime, assert Scaffold returns
no error, that result.ManifestPath and result.EntryPath exist and live under the
plugin dir, that plugins.ParseManifest succeeds for the generated manifest, and
that the tool command/args reference the entry (reuse commandReferencesEntry).
Keep assertions the same as in TestScaffoldCreatesManifestAndStub but loop over
runtimes (or create a new Test like TestScaffoldCoversAllRuntimes) and set
ScaffoldOptions.Runtime accordingly to catch template/manifest drift across the
Node and Python branches.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1c041b89-80d4-4a55-97ca-9b6f366180a3

📥 Commits

Reviewing files that changed from the base of the PR and between 59c3326 and b8d41be.

📒 Files selected for processing (11)
  • internal/cli/app.go
  • internal/cli/distribution.go
  • internal/cli/distribution_test.go
  • internal/cli/extensions.go
  • internal/cli/skills.go
  • internal/plugins/install.go
  • internal/plugins/install_test.go
  • internal/skills/install.go
  • internal/skills/install_test.go
  • internal/tools/scaffold.go
  • internal/tools/scaffold_test.go

Comment thread internal/cli/distribution.go
Comment thread internal/cli/distribution.go
Comment thread internal/cli/distribution.go Outdated
Comment thread internal/plugins/install.go Outdated
Comment thread internal/skills/install.go
Comment thread internal/skills/install.go
The test used a whitespace-only source dir name to force the no-usable-name rejection, which is not creatable on Windows (Smoke failure). Switched to a dir base that validSkillName rejects on every platform (contains "..") while staying creatable on Windows; the blank frontmatter still falls back to the unusable dir base, exercising the same rejection.
@gnanam1990

Copy link
Copy Markdown
Collaborator Author

Addressed the review (verified each against current code; gate green: gofmt clean, go build ./... and go test ./... pass).

Fixed:

  • plugins/install.go — install now hashes the same filtered tree copyTree installs (path + exec bit + bytes, .git/symlinks skipped, sorted), not just plugin.json, so a change to any nested file (tool script, prompt, bundled skill) is reflected in the lock hash and reported as an update. Added a regression that changes only a nested file and asserts Updated + hash change.
  • cli/distribution.gotools list now surfaces plugins.Load diagnostics in both text ("Tool diagnostics:") and JSON (diagnostics field), matching plugins list, so a broken toolbox plugin no longer vanishes silently. Added a text+JSON test.
  • cli/distribution.goskill remove / plugin remove now reject --json with a usage error instead of silently ignoring it. Added a test.
  • cli/distribution.go — post-scaffold hint now points at zero tools list (the command that proves the scaffolded tool loads), not zero plugins list.
  • skills/install.go + plugins/install.go — local install sources are canonicalized (abs + symlink-resolved) before storing/comparing, so the same local source via relative vs absolute spelling is no longer a false clash. Added regression tests in both packages.
  • tools/scaffold_test.go — added a table-driven test covering all three runtimes (shell/node/python) so manifest/template drift in the non-default branches is caught.

Skipped:

  • Staged-swap reinstall (skills + plugins) — skipped to keep the change minimal. A correct, litter-free staged swap requires either skipping dot-dirs in the loader (out of scope; loaders aren't in this PR's changed set) or stale-temp cleanup logic; staging under the scanned dir without that would add a new crash-path failure mode (a leftover temp dir read as a duplicate plugin/skill). The current in-place flow already clears-then-writes so it never mixes old/new files.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
internal/cli/distribution.go (1)

449-451: 💤 Low value

Simplify loop with variadic append.

Per staticcheck S1011, the loop can be replaced with a single variadic append.

-	for _, line := range formatToolboxDiagnostics(diagnostics) {
-		lines = append(lines, line)
-	}
+	lines = append(lines, formatToolboxDiagnostics(diagnostics)...)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/distribution.go` around lines 449 - 451, Replace the explicit
loop that appends each element from formatToolboxDiagnostics(diagnostics) into
lines with a variadic append to simplify the code: use lines = append(lines,
formatToolboxDiagnostics(diagnostics)...), referencing the existing identifiers
lines, formatToolboxDiagnostics and diagnostics to locate and update the code.

Source: Linters/SAST tools

internal/plugins/install.go (1)

507-509: 💤 Low value

Comment describes "length-prefixed" but implementation uses null-delimited.

The header is null-delimited (path\x00exec\x00), not length-prefixed. The approach is still collision-resistant since filesystem paths cannot contain null bytes, but the comment is misleading.

-			// Length-prefixed header keeps file boundaries unambiguous so two trees
-			// cannot collide by shifting bytes across the path/content split.
+			// Null-delimited header keeps file boundaries unambiguous (paths cannot
+			// contain null bytes) so two trees cannot collide by shifting bytes.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/plugins/install.go` around lines 507 - 509, The comment wrongly
calls the header "length-prefixed" while the code constructs a null-delimited
header via header := fmt.Sprintf("%s\x00%d\x00", filepath.ToSlash(rel),
executable); update the comment to accurately describe the format
(null-delimited path and executable marker) and why it remains
collision-resistant (filesystem paths cannot contain NUL), or alternatively
change the construction to a true length-prefixed format if that was the
original intent; reference the header variable, filepath.ToSlash(rel),
executable, and the fmt.Sprintf call when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@internal/cli/distribution.go`:
- Around line 449-451: Replace the explicit loop that appends each element from
formatToolboxDiagnostics(diagnostics) into lines with a variadic append to
simplify the code: use lines = append(lines,
formatToolboxDiagnostics(diagnostics)...), referencing the existing identifiers
lines, formatToolboxDiagnostics and diagnostics to locate and update the code.

In `@internal/plugins/install.go`:
- Around line 507-509: The comment wrongly calls the header "length-prefixed"
while the code constructs a null-delimited header via header :=
fmt.Sprintf("%s\x00%d\x00", filepath.ToSlash(rel), executable); update the
comment to accurately describe the format (null-delimited path and executable
marker) and why it remains collision-resistant (filesystem paths cannot contain
NUL), or alternatively change the construction to a true length-prefixed format
if that was the original intent; reference the header variable,
filepath.ToSlash(rel), executable, and the fmt.Sprintf call when making the
change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e22b7b76-9eb5-4307-b811-4f0f89f517da

📥 Commits

Reviewing files that changed from the base of the PR and between b90e1ca and 4a5e63c.

📒 Files selected for processing (7)
  • internal/cli/distribution.go
  • internal/cli/distribution_test.go
  • internal/plugins/install.go
  • internal/plugins/install_test.go
  • internal/skills/install.go
  • internal/skills/install_test.go
  • internal/tools/scaffold_test.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • internal/cli/distribution_test.go
  • internal/skills/install.go
  • internal/tools/scaffold_test.go
  • internal/plugins/install_test.go
  • internal/skills/install_test.go

Two Windows Smoke failures from the canonical-source review fix:
- TestInstallSameLocalSourceDifferentSpellingIsNotAClash (skills + plugins) used filepath.Rel(cwd, tempdir), which errors on Windows when the temp dir (C:) and repo (D:) are on different drives. Switched to a same-directory alternate spelling (redundant /./), which still exercises canonicalSource without a cross-drive relative path.
- TestRunSkillAddInfoRemove asserted the raw source path, but canonicalSource resolves symlinks/8.3-short-paths, so the stored+displayed source differs on Windows. Normalize the source via EvalSymlinks up front so the assertion matches on every platform.
@gnanam1990

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@gnanam1990

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit approvable ?

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check current review state and open comments on PR `#184`
gh pr view 184 --repo Gitlawb/zero --json state,reviewDecision,reviews,comments | jq '{state,reviewDecision,reviews: (.reviews // [] | map({author:.author.login, state:.state, submittedAt:.submittedAt})), commentCount: (.comments // [] | length)}'

echo "---OPEN REVIEW THREADS---"
gh api graphql -f query='
{
  repository(owner: "Gitlawb", name: "zero") {
    pullRequest(number: 184) {
      reviewThreads(first: 50) {
        nodes {
          isResolved
          isOutdated
          comments(first: 1) {
            nodes {
              author { login }
              body
              path
              line
            }
          }
        }
      }
    }
  }
}' | jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false and .isOutdated == false) | {path: .comments.nodes[0].path, line: .comments.nodes[0].line, author: .comments.nodes[0].author.login, body: .comments.nodes[0].body[:200]}'

Length of output: 1721



@gnanam1990 — Yes. Here's the rundown:

Open thread: The only unresolved thread is the staged-swap reinstall concern on internal/skills/install.go line 152. Your deferral reasoning is sound:

  • A correct staged swap requires loader awareness of dot-dirs (out of scope for this PR).
  • Staging under the scanned directory without that would create a crash-path failure mode (leftover temp read as a duplicate plugin/skill).
  • The current in-place clear-then-write is already functionally correct; it never interleaves old and new files.

I'm withdrawing that concern and approving.

[approve]

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: Approved

Reviewed the skill/plugin distribution and tool scaffold changes. No blockers found.

What I checked:

  • Plugin and skill installs validate manifests/frontmatter before writing install state.
  • Local sources are canonicalized for clash detection; git/local installs never execute fetched content.
  • Plugin hashing covers installed tree content and skips .git/symlinks; skill hashing covers the installed SKILL.md content.
  • Remove paths validate IDs/names before deleting install targets.
  • Scaffolded tools default to prompt-gated permissions and generate data only.

Note: the non-staged install replacement tradeoff is acceptable for this PR after the latest discussion/fixes; it does not introduce execution of fetched content, and CI/CodeRabbit are green.

Validation run locally on the PR head:

  • git diff --check origin/main...HEAD
  • go test ./...
  • go run ./cmd/zero-release build
  • go run ./cmd/zero-release smoke

@gnanam1990

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit review..if approvable then approve it

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
gh pr view 184 --repo Gitlawb/zero --json state,reviewDecision,reviews | \
  jq '{state, reviewDecision, reviews: (.reviews // [] | map({author:.author.login, state:.state, submittedAt:.submittedAt}))}'

echo "---OPEN UNRESOLVED THREADS---"
gh api graphql -f query='
{
  repository(owner: "Gitlawb", name: "zero") {
    pullRequest(number: 184) {
      reviewThreads(first: 50) {
        nodes {
          isResolved
          isOutdated
          comments(first: 1) {
            nodes {
              author { login }
              body
              path
              line
            }
          }
        }
      }
    }
  }
}' | jq '[.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false and .isOutdated == false)] | length as $count | {openThreadCount: $count, threads: map({path: .comments.nodes[0].path, line: .comments.nodes[0].line, author: .comments.nodes[0].author.login, body: .comments.nodes[0].body[:200]})}'

Length of output: 1856


🏁 Script executed:

#!/bin/bash
# Get the thread node ID so we can resolve it
gh api graphql -f query='
{
  repository(owner: "Gitlawb", name: "zero") {
    pullRequest(number: 184) {
      reviewThreads(first: 50) {
        nodes {
          id
          isResolved
          isOutdated
          comments(first: 1) {
            nodes {
              author { login }
              path
              line
            }
          }
        }
      }
    }
  }
}' | jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false and .isOutdated == false and .comments.nodes[0].author.login == "coderabbitai") | .id'

Length of output: 687


🏁 Script executed:

#!/bin/bash
gh api graphql -f query='
mutation {
  resolveReviewThread(input: {threadId: "PRRT_kwDOSqaaUs6JMOPb"}) {
    thread {
      id
      isResolved
    }
  }
}'

Length of output: 305



@gnanam1990 — Open thread resolved. All fixes are solid: hashing covers the full filtered tree, tools list surfaces diagnostics, --json is correctly rejected on remove commands, local sources are canonicalized, and runtime coverage tests are in place. Approving now.

(ᵔᴥᵔ)

[approve]

@gnanam1990

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit approve

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown
✅ Action performed

Comments resolved and changes approved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants