feat: e2e acceptance script, README and fleet handoff docs
- scripts/e2e-acceptance.sh: offline spec walk (fixture upstream -> vendor add/determinism/collisions/renames -> validate/list/inspect -> update cycle -> employee sync -> idempotence -> protected sync -> deletions -> status drift -> unreachable repo), 61 assertions on tree, lockfile, manifest and exit codes - README: installation, curator guide (selection rules, renames), client guide (three-state semantics, notifications), security model, troubleshooting, scope - docs/fleet-handoff.md: rollout (pinned release, admin-protected config, user-context scheduled task), token rotation, ADR draft superseding the exact-mirror doctrine, fleet verification checklist
This commit is contained in:
@@ -18,3 +18,4 @@
|
||||
{"id":"int-d0f6b2e3946129f47ed8f071902a5c78","kind":"field_change","created_at":"2026-08-22T20:01:15.960626197Z","actor":"m3ta-chiron","issue_id":"agent-lib-j2w.4","extra":{"field":"status","new_value":"closed","old_value":"in_progress"}}
|
||||
{"id":"int-baf99558287967cfd7c3aeb9c0e6354c","kind":"field_change","created_at":"2026-08-23T08:31:08.602480836Z","actor":"m3ta-chiron","issue_id":"agent-lib-j2w.5","extra":{"field":"status","new_value":"closed","old_value":"in_progress"}}
|
||||
{"id":"int-41040467e9dbadf410d0563d3775e728","kind":"field_change","created_at":"2026-08-23T08:36:53.924991634Z","actor":"m3ta-chiron","issue_id":"agent-lib-j2w.6","extra":{"field":"status","new_value":"closed","old_value":"in_progress"}}
|
||||
{"id":"int-06e7ec6923aa26fc3a5eb446c5aa001c","kind":"field_change","created_at":"2026-08-23T08:40:34.06547132Z","actor":"m3ta-chiron","issue_id":"agent-lib-j2w.7","extra":{"field":"status","new_value":"closed","old_value":"in_progress"}}
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
# agent-lib
|
||||
|
||||
One Go binary, two roles: **curator** and **client**.
|
||||
|
||||
- **Curator side** — `agent-lib vendor …` pulls external git repositories into a
|
||||
plain work repository under `external/<source>/<type>/`, pinned by revision,
|
||||
with per-source selection and collision-safe naming. Publication is nothing
|
||||
more than `git commit && git push`.
|
||||
- **Client side** — `agent-lib sync` pulls the work repository and deploys its
|
||||
content into the OpenCode runtime paths. Local modifications win over
|
||||
company updates, self-created items are never touched, and Windows toast
|
||||
notifications tell employees when their edits blocked an update.
|
||||
|
||||
No Nix, no Python, no git subprocess at runtime — a single static binary for
|
||||
windows/amd64, linux/amd64, linux/arm64, darwin/amd64 and darwin/arm64.
|
||||
|
||||
## Installation
|
||||
|
||||
Download the binary for your platform from the
|
||||
[GitHub releases](https://github.com/m3tam3re/agent-lib/releases), verify the
|
||||
checksum, and put it on your `PATH`:
|
||||
|
||||
```sh
|
||||
curl -fsSLO https://github.com/m3tam3re/agent-lib/releases/latest/download/agent-lib_linux_amd64.tar.gz
|
||||
tar xzf agent-lib_linux_amd64.tar.gz
|
||||
sha256sum -c checksums.txt --ignore-missing
|
||||
install -m 0755 agent-lib /usr/local/bin/
|
||||
agent-lib version
|
||||
```
|
||||
|
||||
Fleet-managed machines receive the binary via a pinned rollout — see
|
||||
[docs/fleet-handoff.md](docs/fleet-handoff.md). There is no self-update.
|
||||
|
||||
From source: `go build .` (version is baked in via ldflags; `make build` uses
|
||||
the git description).
|
||||
|
||||
## Curator guide
|
||||
|
||||
The work repository layout:
|
||||
|
||||
```
|
||||
skills/<id>/SKILL.md own skills (folder artifacts)
|
||||
commands/<id>.md own OpenCode commands
|
||||
agents/<id>.md own OpenCode agents
|
||||
mcp/<id>.yaml own MCP fragments (never deployed by the binary)
|
||||
external/<source>/<type>/ vendored content, mirrors the same four types
|
||||
agent-lib.lock.json lockfile v2 — pins every source
|
||||
```
|
||||
|
||||
### Vendoring a source
|
||||
|
||||
```sh
|
||||
agent-lib vendor add superpowers https://github.com/obra/superpowers/tree/main/skills
|
||||
```
|
||||
|
||||
Pasted GitHub/GitLab web-tree URLs are normalized automatically (the tree
|
||||
sub-path becomes the discovery root). `--ref <branch-or-tag>` pins a ref; the
|
||||
resolved revision is pinned in the lockfile. Discovery understands the four
|
||||
standard directories (`skills/`, `commands/`, `agents/`, `mcp/`); skills are
|
||||
folders containing `SKILL.md`, commands and agents are flat `.md` files, MCP
|
||||
fragments are `.yaml`. Everything is read-only scanned — upstream code is
|
||||
never executed.
|
||||
|
||||
### Selection rules
|
||||
|
||||
- Default: vendor **everything** (`mode: all`).
|
||||
- `--include a,b,c` switches the source to **include mode**: only those ids
|
||||
arrive, and new upstream items do **not** arrive automatically.
|
||||
- Hard errors, enforced by `vendor add`, `vendor update` and `validate`:
|
||||
- `all` combined with an include list → error
|
||||
- an exclude list without `all` → error (not supported in v2 selections)
|
||||
- an include entry that no longer exists upstream → error naming the item
|
||||
|
||||
### Collisions and renames
|
||||
|
||||
Every item deploys into a flat per-type namespace. If a vendored id collides
|
||||
with one of your own items (or with another source's item), the operation
|
||||
aborts with a hard error naming both parties — before anything is written.
|
||||
Resolve it with an explicit, reviewable rename:
|
||||
|
||||
```sh
|
||||
agent-lib vendor add superpowers "$URL" --rename review=superpowers-review
|
||||
```
|
||||
|
||||
The on-disk folder always carries exactly the deployed name. Renaming onto
|
||||
another collision is an error.
|
||||
|
||||
### The maintenance loop
|
||||
|
||||
```sh
|
||||
agent-lib vendor diff superpowers # read-only preview: new/changed/deleted vs selection
|
||||
agent-lib vendor update superpowers # fetch latest of the pinned ref, rewrite, report
|
||||
agent-lib vendor update --all # every source, reported per source
|
||||
agent-lib vendor remove superpowers # delete external area + lockfile entry
|
||||
```
|
||||
|
||||
In `all` mode new upstream items arrive automatically (and appear in the
|
||||
update report); in include mode they deliberately do not — `vendor diff`
|
||||
surfaces them as `available upstream, not selected`. Every update re-runs the
|
||||
collision checks and honors the rename map.
|
||||
|
||||
### Auditing
|
||||
|
||||
```sh
|
||||
agent-lib validate # offline: rules + tree/lockfile consistency
|
||||
agent-lib vendor list # sources with pin, mode, item counts
|
||||
agent-lib vendor inspect <name> # full inventory with metadata + warnings
|
||||
```
|
||||
|
||||
All three accept `--json`. The lockfile is deterministic JSON (sorted keys,
|
||||
fixed indentation) — identical reruns produce identical bytes, so git diffs
|
||||
stay clean.
|
||||
|
||||
## Client guide
|
||||
|
||||
Employees run two commands:
|
||||
|
||||
```sh
|
||||
agent-lib sync # pull the work repository and deploy
|
||||
agent-lib status # report local state; exit 1 when updates are blocked
|
||||
```
|
||||
|
||||
Deployment mapping (OpenCode target):
|
||||
|
||||
| Type | Destination |
|
||||
|---|---|
|
||||
| skills | `~/.agents/skills/<id>/` (whole artifact folders) |
|
||||
| commands | `~/.config/opencode/commands/<id>.md` |
|
||||
| agents | `~/.config/opencode/agents/<id>.md` |
|
||||
| mcp | **never deployed** — stays under fleet-controller merge |
|
||||
|
||||
### Sync semantics (three-state)
|
||||
|
||||
Sync plans before it mutates. For every item the planner compares the
|
||||
repository, the local manifest and the on-disk content:
|
||||
|
||||
- new item → **deploy**
|
||||
- managed & unmodified, upstream changed → **update**
|
||||
- managed & locally modified → **skip + warn** — your version stays
|
||||
- on disk but never deployed by us → **leave + warn** — self-created items are safe by construction
|
||||
- upstream deleted & unmodified → **remove**
|
||||
- upstream deleted & modified → **skip + warn** — your version stays
|
||||
|
||||
Sync is idempotent (a second run is a byte-level no-op) and fails cleanly:
|
||||
an unreachable repository never leaves a half-deployed machine.
|
||||
|
||||
### Configuration
|
||||
|
||||
The client config is admin-provided (fleet rollout), JSON at
|
||||
`C:\ProgramData\agent-lib\config.json` on Windows, `/etc/agent-lib/config.json`
|
||||
elsewhere. Override per run with `--config` or `AGENT_LIB_CONFIG`:
|
||||
|
||||
```json
|
||||
{
|
||||
"repo_url": "https://git.example.com/company/agent-content.git",
|
||||
"ref": "main",
|
||||
"token": "<read-only deploy token>",
|
||||
"token_user": "oauth2"
|
||||
}
|
||||
```
|
||||
|
||||
The token is used for basic auth only. It never appears in the lockfile, the
|
||||
manifest, logs or error messages.
|
||||
|
||||
### Notifications
|
||||
|
||||
On Windows, a toast fires when (and only when) a sync kept local
|
||||
modifications or discovered unmanaged items — never for routine syncs. The
|
||||
same warnings are appended to the sync log
|
||||
(`~/.local/state/agent-lib/sync.log` on Linux, `%LocalAppData%\agent-lib`
|
||||
on Windows) and surfaced by `status`. Linux/macOS notification is a no-op.
|
||||
|
||||
`status --json` is machine-readable: deployed revision, repository revision,
|
||||
pending changes, skipped and unrecognized items, and a `problematic_drift`
|
||||
flag that drives the non-zero exit code — suitable for headless drift checks.
|
||||
|
||||
## Security / trust model
|
||||
|
||||
- External sources are **untrusted**. Discovery and vendoring never execute
|
||||
upstream code; only file contents are read.
|
||||
- The shared read-only token limits blast radius; rotation is a single fleet
|
||||
run (see the handoff doc).
|
||||
- MCP fragments from vendored sources are **never activated** by the binary;
|
||||
merging and vault substitution remain an explicit fleet-controller decision.
|
||||
- Binary updates happen exclusively via pinned fleet rollouts — there is no
|
||||
self-update path to abuse.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Cause / fix |
|
||||
|---|---|
|
||||
| `error: … include entry "x" not found upstream` | Typo, or the item was renamed/removed upstream. Fix the lockfile include list, or re-add with a corrected `--include`. |
|
||||
| `error: … collides with own skills "x"` | Deployed-name collision. Add `--rename <upstream>=<deployed>` (add) or fix the rename map (update). |
|
||||
| `validate` reports tree/lockfile divergence | Someone edited `external/` by hand. Re-run `vendor update <source>` to restore the pinned state. |
|
||||
| Skill didn't update, `skipped (local modifications kept): 1` | Working as designed — you edited the deployed item. Restore the company version by deleting the local copy and re-syncing. |
|
||||
| `status` exits 1 | Local modifications block repository updates (or occupy names the repo needs). See `status` output for the item list. |
|
||||
| Sync fails with clone/fetch error | Work repository unreachable, or token expired. Previous local state remains intact; fix config/network and re-run. |
|
||||
| No toast on Windows | Toasts need an interactive user session — the scheduled task must run as the logged-in user, not SYSTEM. |
|
||||
|
||||
## Development
|
||||
|
||||
```sh
|
||||
make build # build with version ldflags
|
||||
make test # unit + black-box e2e (fully offline, local fixture git repos)
|
||||
make lint # go vet + gofmt check
|
||||
./scripts/e2e-acceptance.sh # full spec walk: 60+ assertions, offline
|
||||
```
|
||||
|
||||
Release: `goreleaser release` on a tagged commit builds all five targets with
|
||||
checksums and publishes to GitHub releases. Verify with
|
||||
`goreleaser release --snapshot --clean` or a tagged `--skip=publish` dry run.
|
||||
|
||||
## Scope and non-goals (v2)
|
||||
|
||||
In scope: vendoring with selection/renames into any git work repository;
|
||||
OpenCode-only deployment with three-state protection; toast notifications;
|
||||
goreleaser/GitHub release pipeline.
|
||||
|
||||
Not in scope: client-side selection or profiles, additional targets (Pi,
|
||||
Hermes, Crush, Amp), MCP merge/vault (fleet-controller concern), binary
|
||||
self-update, git submodule sources, migration tooling from the Python-era
|
||||
lockfile.
|
||||
@@ -0,0 +1,137 @@
|
||||
# Fleet handoff — agent-lib v2 rollout
|
||||
|
||||
What az-fleet must roll out, and why. This document is written so the fleet
|
||||
team can implement their role from this file alone.
|
||||
|
||||
## What the binary does (and does not do)
|
||||
|
||||
`agent-lib` is one static Go binary with two roles:
|
||||
|
||||
- **Curator** (maintainer machine): `vendor add/update/diff/remove/list/inspect`
|
||||
plus `validate` — manages external content inside the company work
|
||||
repository. Publication = `git commit && git push` by the maintainer.
|
||||
- **Client** (employee machine): `sync` + `status` — pulls the work repository
|
||||
and deploys skills/commands/agents into the OpenCode paths. Deploys with
|
||||
three-state protection: user-modified managed items are skipped with a
|
||||
warning (never steamrolled), self-created items are never touched,
|
||||
unmodified items update/remove silently.
|
||||
|
||||
The binary handles all content transport itself. It never deploys or merges
|
||||
MCP fragments — `mcp/` and `external/*/mcp/` stay under controller-side merge
|
||||
and vault substitution (clone the work repository as before, now including
|
||||
the external areas).
|
||||
|
||||
## Rollout components
|
||||
|
||||
### 1. Binary via pinned GitHub release
|
||||
|
||||
- Source of truth: GitHub releases of `m3tam3re/agent-lib`.
|
||||
- Artifacts: `agent-lib_{windows_amd64,linux_amd64,linux_arm64,darwin_amd64,darwin_arm64}`
|
||||
plus `checksums.txt` (tar.gz archives; windows is a zip).
|
||||
- Fleet pins an exact release tag (installer-pin pattern already used for
|
||||
other tools). **No self-update exists in the binary** — binary updates are
|
||||
exclusively fleet rollouts. Content updates are pulled by the binary itself.
|
||||
- Verify the checksum during rollout; install to a fixed path
|
||||
(e.g. `C:\Program Files\agent-lib\agent-lib.exe` / `/usr/local/bin/agent-lib`).
|
||||
|
||||
### 2. Admin-protected client config
|
||||
|
||||
Path: `C:\ProgramData\agent-lib\config.json` (Windows, ACL restricted to
|
||||
Administrators + read for Users) or `/etc/agent-lib/config.json` (Linux,
|
||||
root-owned, mode 0644).
|
||||
|
||||
```json
|
||||
{
|
||||
"repo_url": "https://git.example.com/company/agent-content.git",
|
||||
"ref": "main",
|
||||
"token": "<read-only deploy token>",
|
||||
"token_user": "oauth2"
|
||||
}
|
||||
```
|
||||
|
||||
- The token is a **shared read-only** deploy token scoped to the work
|
||||
repository. It is used for HTTP basic auth only and never appears in the
|
||||
lockfile, the manifest, logs, or error output.
|
||||
- Employees must be able to read the config (sync runs in their context) but
|
||||
must not be able to alter it.
|
||||
- Per-run overrides for testing: `--config <path>` or `AGENT_LIB_CONFIG`.
|
||||
|
||||
Local state (manifest, cached bare repo, sync log) lives per-user:
|
||||
`%LocalAppData%\agent-lib` on Windows, `~/.local/state/agent-lib` on Linux
|
||||
(override with `AGENT_LIB_STATE_DIR`).
|
||||
|
||||
### 3. Scheduled task — logged-in user context (required)
|
||||
|
||||
- Schedule `agent-lib sync` per user, **running as the logged-in user, not
|
||||
SYSTEM**. Two reasons:
|
||||
1. Deployment targets (`~/.agents/skills`, `~/.config/opencode/...`) resolve
|
||||
against the user's profile.
|
||||
2. Windows toast notifications require an interactive user session. A task
|
||||
running as SYSTEM produces no visible toasts.
|
||||
- Suggested cadence: every 1–4 hours plus/except at logon. Sync is idempotent
|
||||
and cheap when unchanged.
|
||||
- Optional companion: a drift check running `agent-lib status --json`
|
||||
headless; its non-zero exit (`problematic_drift: true`) means a user's local
|
||||
modifications are blocking company updates — feed it into monitoring.
|
||||
|
||||
## Token rotation procedure
|
||||
|
||||
1. Create/rotate the read-only deploy token on the git host.
|
||||
2. Update `token` in the admin-protected config via the fleet (single run;
|
||||
config is fleet-managed, so this is one playbook task).
|
||||
3. No client-side action: the next scheduled sync picks up the new token
|
||||
(config is read on every run). The old token can be revoked once the
|
||||
fleet run completed.
|
||||
4. A failed sync never damages local state — a revoked token mid-rotation
|
||||
only means "no updates until the config arrives".
|
||||
|
||||
## ADR draft — superseding the "exact mirror" doctrine
|
||||
|
||||
> # ADR-000X: agent-lib three-state sync supersedes exact-mirror deletion
|
||||
>
|
||||
> ## Status
|
||||
> Proposed (supersedes ADR-0006 "exact mirror including deletion")
|
||||
>
|
||||
> ## Context
|
||||
> The previous fleet doctrine required employee machines to be an exact
|
||||
> mirror of the company content repository, including deletions. This
|
||||
> steamrolled legitimate local work: employees' own skills and their local
|
||||
> adjustments to company skills were destroyed by routine syncs.
|
||||
>
|
||||
> agent-lib v2 replaces the mirror sync with a **manifest-based three-state
|
||||
> sync**: a local manifest records every item agent-lib deployed (name, type,
|
||||
> origin, revision, content hash), and the planner compares repository,
|
||||
> manifest and on-disk content per item.
|
||||
>
|
||||
> ## Decision
|
||||
> 1. Managed items that the user modified locally are **skipped** — the local
|
||||
> version wins, a warning is logged and (on Windows) toasted.
|
||||
> 2. Items on disk that agent-lib never deployed are **unmanaged** — left
|
||||
> untouched, reported by `status` as unrecognized.
|
||||
> 3. Upstream deletions remove only the **unmodified** managed copies;
|
||||
> modified copies are kept and warned about.
|
||||
> 4. Everything else (new, changed-unmodified) updates silently; sync is
|
||||
> idempotent and safe to schedule.
|
||||
>
|
||||
> ## Consequences
|
||||
> - Employee machines are no longer exact mirrors by design; drift is
|
||||
> visible (`status`, exit code) instead of silently destroyed.
|
||||
> - "What's in the repo is what I have" holds for everything the user never
|
||||
> touched.
|
||||
> - Remediation for blocked updates is a human decision (restore the company
|
||||
> version by deleting the local copy and re-syncing), not a forced
|
||||
> overwrite.
|
||||
> - The controller-side MCP merge is unaffected: it clones the work
|
||||
> repository (now including `external/*/mcp/`) and continues to own
|
||||
> activation, merging and vault substitution.
|
||||
|
||||
## Verification checklist for the fleet team
|
||||
|
||||
- [ ] Pinned binary installed, `agent-lib version` prints the release version.
|
||||
- [ ] Config at the admin-protected path, readable by users, writable only by
|
||||
admins; token present.
|
||||
- [ ] Scheduled task runs as the logged-in user; toast appears when a
|
||||
modified item is skipped (manual visual check on a Windows client).
|
||||
- [ ] `agent-lib status` exits 0 on a clean machine, non-zero after editing a
|
||||
deployed file (self-test).
|
||||
- [ ] Drift monitoring consumes `status --json` → `problematic_drift`.
|
||||
Executable
+201
@@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env bash
|
||||
# Offline end-to-end acceptance walk of the agent-lib v2 spec:
|
||||
# fixture upstream -> vendor add -> simulated commit -> employee sync ->
|
||||
# local modification -> upstream update cycle -> protected sync -> status.
|
||||
# Asserts filesystem state, lockfile, manifest and exit codes throughout.
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
WORK="$(mktemp -d /tmp/agent-lib-acceptance.XXXXXX)"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
BIN="$WORK/agent-lib"
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
step() { printf '\n\033[1m== %s\033[0m\n' "$*"; }
|
||||
ok() { printf ' \033[32mok\033[0m %s\n' "$*"; PASS=$((PASS+1)); }
|
||||
fail() { printf ' \033[31mFAIL\033[0m %s\n' "$*"; FAIL=$((FAIL+1)); }
|
||||
|
||||
assert_contains() { # file|output needle label
|
||||
if grep -qF "$2" "$1" 2>/dev/null; then ok "$3"; else fail "$3 (missing: $2)"; fi
|
||||
}
|
||||
assert_file() { if [ -e "$1" ]; then ok "$2"; else fail "$2 ($1 missing)"; fi; }
|
||||
assert_no_file() { if [ ! -e "$1" ]; then ok "$2"; else fail "$2 ($1 exists)"; fi; }
|
||||
assert_exit_zero() { if [ "$1" -eq 0 ]; then ok "$2"; else fail "$2 (exit $1)"; fi; }
|
||||
assert_exit_nonzero() { if [ "$1" -ne 0 ]; then ok "$2"; else fail "$2 (exit 0)"; fi; }
|
||||
|
||||
git() { command git -c user.name=acceptance -c user.email=acceptance@test "$@"; }
|
||||
|
||||
step "build binary"
|
||||
(cd "$REPO_ROOT" && go build -o "$BIN" .) || { echo "build failed"; exit 1; }
|
||||
ok "agent-lib built"
|
||||
|
||||
step "fixture upstream repository"
|
||||
UP="$WORK/upstream"
|
||||
mkdir -p "$UP"/{skills/good-skill,skills/broken-skill,commands,agents,mcp}
|
||||
git -C "$UP" init -q -b main
|
||||
printf -- '---\nname: Good Skill\ndescription: works\n---\n# Good\n' > "$UP/skills/good-skill/SKILL.md"
|
||||
printf 'print hi\n' > "$UP/skills/good-skill/helper.py"
|
||||
printf -- '---\nname: Broken\nno colon here\n' > "$UP/skills/broken-skill/SKILL.md"
|
||||
printf -- '---\nname: Review\n---\nbody\n' > "$UP/commands/review.md"
|
||||
printf -- '---\nname: Scout\n---\nbody\n' > "$UP/agents/scout.md"
|
||||
printf 'servers: {}\n' > "$UP/mcp/search.yaml"
|
||||
git -C "$UP" add -A && git -C "$UP" commit -qm "fixture upstream"
|
||||
ok "upstream ready (4 types + broken frontmatter)"
|
||||
|
||||
step "work repository + vendor add"
|
||||
WR="$WORK/work-repo"
|
||||
mkdir -p "$WR/skills/own-skill"
|
||||
git -C "$WR" init -q -b main
|
||||
printf -- '---\nname: Own Skill\n---\n# Own\n' > "$WR/skills/own-skill/SKILL.md"
|
||||
git -C "$WR" add -A && git -C "$WR" commit -qm "own contribution"
|
||||
(cd "$WR" && "$BIN" vendor add superpowers "$UP") > "$WORK/add.log" 2>&1
|
||||
assert_exit_zero $? "vendor add succeeds"
|
||||
assert_contains "$WORK/add.log" "ref: main" "default branch resolved"
|
||||
assert_contains "$WORK/add.log" "warnings" "malformed frontmatter warned, not failed"
|
||||
assert_file "$WR/external/superpowers/skills/good-skill/SKILL.md" "skill vendored"
|
||||
assert_file "$WR/external/superpowers/commands/review.md" "command vendored with extension"
|
||||
assert_file "$WR/external/superpowers/agents/scout.md" "agent vendored"
|
||||
assert_file "$WR/external/superpowers/mcp/search.yaml" "mcp fragment vendored"
|
||||
assert_no_file "$WR/external/superpowers/README.md" "non-content files excluded"
|
||||
assert_contains "$WR/agent-lib.lock.json" '"version": 2' "lockfile v2 written"
|
||||
|
||||
step "lockfile determinism"
|
||||
WR2="$WORK/work-repo-2"
|
||||
mkdir -p "$WR2"
|
||||
(cd "$WR2" && "$BIN" vendor add superpowers "$UP") >/dev/null 2>&1
|
||||
if cmp -s "$WR/agent-lib.lock.json" "$WR2/agent-lib.lock.json"; then ok "identical reruns produce identical bytes"; else fail "lockfile not deterministic"; fi
|
||||
|
||||
step "collision hard error + rename resolution"
|
||||
WR3="$WORK/work-repo-3"
|
||||
mkdir -p "$WR3/skills/good-skill"
|
||||
printf -- '---\nname: My Good\n---\n' > "$WR3/skills/good-skill/SKILL.md"
|
||||
set +e
|
||||
(cd "$WR3" && "$BIN" vendor add sp "$UP") > "$WORK/collide.log" 2>&1; RC=$?
|
||||
set -e
|
||||
assert_exit_nonzero "$RC" "own-vs-vendored collision aborts"
|
||||
assert_contains "$WORK/collide.log" "collides" "collision names the rule"
|
||||
assert_no_file "$WR3/external" "no partial external area"
|
||||
(cd "$WR3" && "$BIN" vendor add sp "$UP" --rename good-skill=sp-good) >/dev/null 2>&1
|
||||
assert_file "$WR3/external/sp/skills/sp-good/SKILL.md" "renamed folder carries deployed name"
|
||||
assert_no_file "$WR3/external/sp/skills/good-skill" "upstream name absent after rename"
|
||||
|
||||
step "validate / list / inspect"
|
||||
(cd "$WR" && "$BIN" validate) > "$WORK/validate.log" 2>&1
|
||||
assert_exit_zero $? "healthy state validates"
|
||||
(cd "$WR" && "$BIN" vendor list --json) > "$WORK/list.json" 2>&1
|
||||
assert_contains "$WORK/list.json" '"mode": "all"' "list --json carries selection mode"
|
||||
(cd "$WR" && "$BIN" vendor inspect superpowers --json) > "$WORK/inspect.json" 2>&1
|
||||
assert_contains "$WORK/inspect.json" "Good Skill" "inspect surfaces frontmatter metadata"
|
||||
|
||||
step "vendor update cycle"
|
||||
mkdir -p "$UP/skills/fresh-skill"
|
||||
printf -- '---\nname: Fresh\n---\n# Fresh\n' > "$UP/skills/fresh-skill/SKILL.md"
|
||||
printf -- '---\nname: Good Skill\ndescription: changed\n---\n# v2\n' > "$UP/skills/good-skill/SKILL.md"
|
||||
rm -rf "$UP/skills/broken-skill"
|
||||
git -C "$UP" add -A && git -C "$UP" commit -qm "upstream: add fresh, change good, remove broken"
|
||||
LOCK_BEFORE="$(sha256sum "$WR/agent-lib.lock.json" | cut -d' ' -f1)"
|
||||
(cd "$WR" && "$BIN" vendor diff superpowers) > "$WORK/diff.log" 2>&1
|
||||
assert_contains "$WORK/diff.log" "fresh-skill" "diff previews upstream changes"
|
||||
LOCK_AFTER_DIFF="$(sha256sum "$WR/agent-lib.lock.json" | cut -d' ' -f1)"
|
||||
[ "$LOCK_BEFORE" = "$LOCK_AFTER_DIFF" ] && ok "diff left lockfile untouched" || fail "diff mutated the lockfile"
|
||||
(cd "$WR" && "$BIN" vendor update superpowers) > "$WORK/update.log" 2>&1
|
||||
assert_exit_zero $? "update succeeds"
|
||||
assert_contains "$WORK/update.log" "fresh-skill (added)" "update reports additions"
|
||||
assert_contains "$WORK/update.log" "good-skill (changed)" "update reports changes"
|
||||
assert_contains "$WORK/update.log" "broken-skill (removed)" "update reports removals"
|
||||
assert_file "$WR/external/superpowers/skills/fresh-skill/SKILL.md" "added item materialized"
|
||||
assert_no_file "$WR/external/superpowers/skills/broken-skill" "removed item gone"
|
||||
(cd "$WR" && "$BIN" validate) >/dev/null 2>&1
|
||||
assert_exit_zero $? "updated tree validates"
|
||||
(cd "$WR" && "$BIN" vendor remove superpowers) >/dev/null 2>&1
|
||||
assert_no_file "$WR/external/superpowers" "remove leaves no residue"
|
||||
(cd "$WR" && "$BIN" vendor add superpowers "$UP") >/dev/null 2>&1
|
||||
git -C "$WR" add -A && git -C "$WR" commit -qm "vendor superpowers (publication = commit && push)"
|
||||
|
||||
step "employee sync"
|
||||
HOME_DIR="$WORK/employee-home"
|
||||
STATE="$WORK/employee-state"
|
||||
mkdir -p "$HOME_DIR" "$STATE"
|
||||
CFG="$WORK/client-config.json"
|
||||
printf '{"repo_url":"%s","ref":"main"}\n' "$WR" > "$CFG"
|
||||
sync_run() {
|
||||
HOME="$HOME_DIR" USERPROFILE="$HOME_DIR" AGENT_LIB_STATE_DIR="$STATE" \
|
||||
"$BIN" sync --config "$CFG" 2>&1
|
||||
}
|
||||
set +e; OUT="$(sync_run)"; RC=$?; set -e
|
||||
assert_exit_zero "$RC" "sync succeeds"
|
||||
printf '%s' "$OUT" > "$WORK/sync1.log"
|
||||
assert_file "$HOME_DIR/.agents/skills/own-skill/SKILL.md" "own skill deployed"
|
||||
assert_file "$HOME_DIR/.agents/skills/fresh-skill/SKILL.md" "vendored skill deployed"
|
||||
assert_file "$HOME_DIR/.config/opencode/commands/review.md" "command deployed"
|
||||
assert_file "$HOME_DIR/.config/opencode/agents/scout.md" "agent deployed"
|
||||
assert_no_file "$HOME_DIR/.config/opencode/mcp" "mcp never deployed"
|
||||
if find "$HOME_DIR" -name '*.yaml' | grep -q .; then fail "no yaml in HOME"; else ok "no mcp yaml anywhere in HOME"; fi
|
||||
assert_contains "$STATE/manifest.json" '"origin": "superpowers"' "manifest records origins"
|
||||
assert_contains "$STATE/manifest.json" '"hash"' "manifest records content hashes"
|
||||
|
||||
step "idempotent second sync"
|
||||
set +e; OUT2="$(sync_run)"; RC2=$?; set -e
|
||||
assert_exit_zero "$RC2" "second sync succeeds"
|
||||
printf '%s' "$OUT2" > "$WORK/sync2.log"
|
||||
assert_contains "$WORK/sync2.log" "kept:" "second run keeps everything"
|
||||
|
||||
step "protected sync: local modification wins"
|
||||
printf -- '---\nname: Own Skill\n---\n# MY EDITS\n' > "$HOME_DIR/.agents/skills/own-skill/SKILL.md"
|
||||
printf -- '---\nname: Own Skill\ndescription: v2\n---\n# Upstream v2\n' > "$WR/skills/own-skill/SKILL.md"
|
||||
git -C "$WR" add -A && git -C "$WR" commit -qm "change own-skill upstream"
|
||||
set +e; OUT3="$(sync_run)"; RC3=$?; set -e
|
||||
assert_exit_zero "$RC3" "sync with local mods does not fail"
|
||||
printf '%s' "$OUT3" > "$WORK/sync3.log"
|
||||
assert_contains "$WORK/sync3.log" "skipped (local modifications kept): 1" "modification skipped with warning"
|
||||
grep -q "MY EDITS" "$HOME_DIR/.agents/skills/own-skill/SKILL.md" && ok "local version kept" || fail "local version overwritten"
|
||||
assert_contains "$STATE/sync.log" "SKIP skills/own-skill" "warning logged with item and reason"
|
||||
|
||||
step "self-created item survives"
|
||||
MINE="$HOME_DIR/.agents/skills/my-private-skill"
|
||||
mkdir -p "$MINE" && printf -- '---\nname: Mine\n---\n' > "$MINE/SKILL.md"
|
||||
set +e; OUT4="$(sync_run)"; RC4=$?; set -e
|
||||
assert_exit_zero "$RC4" "sync tolerates unmanaged items"
|
||||
printf '%s' "$OUT4" > "$WORK/sync4.log"
|
||||
assert_contains "$WORK/sync4.log" "unmanaged items left untouched: 1" "self-created item reported"
|
||||
assert_file "$MINE/SKILL.md" "self-created item untouched"
|
||||
if grep -q "my-private-skill" "$STATE/manifest.json"; then fail "unmanaged item leaked into manifest"; else ok "manifest stays clean"; fi
|
||||
|
||||
step "upstream deletion: unmodified removed, modified kept"
|
||||
rm -rf "$WR/skills/own-skill" "$WR/external/superpowers/agents" "$WR/external/superpowers/commands"
|
||||
printf -- '---\nname: Scout\n---\n# MY AGENT TWEAKS\n' > "$HOME_DIR/.config/opencode/agents/scout.md"
|
||||
git -C "$WR" add -A && git -C "$WR" commit -qm "remove own-skill and vendored agents+commands"
|
||||
set +e; OUT5="$(sync_run)"; RC5=$?; set -e
|
||||
assert_exit_zero "$RC5" "sync with deletions succeeds"
|
||||
printf '%s' "$OUT5" > "$WORK/sync5.log"
|
||||
assert_contains "$WORK/sync5.log" "removed:" "unmodified deletions removed"
|
||||
assert_no_file "$HOME_DIR/.config/opencode/commands/review.md" "unmodified deleted command removed locally"
|
||||
grep -q "MY EDITS" "$HOME_DIR/.agents/skills/own-skill/SKILL.md" && ok "modified skill survives deletion" || fail "modified skill destroyed"
|
||||
grep -q "MY AGENT TWEAKS" "$HOME_DIR/.config/opencode/agents/scout.md" && ok "modified agent survives deletion" || fail "modified agent destroyed"
|
||||
assert_contains "$STATE/sync.log" "upstream deleted but the local copy was modified" "kept deletions warned"
|
||||
|
||||
step "status drift detection"
|
||||
status_run() {
|
||||
HOME="$HOME_DIR" USERPROFILE="$HOME_DIR" AGENT_LIB_STATE_DIR="$STATE" \
|
||||
"$BIN" status --config "$CFG" 2>&1
|
||||
}
|
||||
set +e; OUTS="$(status_run)"; RCS=$?; set -e
|
||||
assert_exit_nonzero "$RCS" "blocked update drift exits non-zero"
|
||||
printf '%s' "$OUTS" > "$WORK/status.log"
|
||||
assert_contains "$WORK/status.log" "drift" "status reports drift"
|
||||
set +e; OUTSJ="$(HOME="$HOME_DIR" USERPROFILE="$HOME_DIR" AGENT_LIB_STATE_DIR="$STATE" "$BIN" status --config "$CFG" --json 2>&1)"; RCSJ=$?; set -e
|
||||
assert_exit_nonzero "$RCSJ" "status --json keeps drift exit code"
|
||||
printf '%s' "$OUTSJ" | grep -q '"problematic_drift": true' && ok "json drift flag set" || fail "json drift flag missing"
|
||||
|
||||
step "unreachable repository keeps state"
|
||||
BEFORE="$(cat "$HOME_DIR/.agents/skills/fresh-skill/SKILL.md")"
|
||||
printf '{"repo_url":"%s","ref":"main"}\n' "$WORK/nonexistent-repo" > "$CFG"
|
||||
set +e; sync_run > "$WORK/unreachable.log" 2>&1; RC6=$?; set -e
|
||||
assert_exit_nonzero "$RC6" "unreachable repo fails cleanly"
|
||||
AFTER="$(cat "$HOME_DIR/.agents/skills/fresh-skill/SKILL.md" 2>/dev/null || true)"
|
||||
[ "$BEFORE" = "$AFTER" ] && ok "previous state intact" || fail "state damaged by failed sync"
|
||||
|
||||
printf '\n\033[1mACCEPTANCE: %d passed, %d failed\033[0m\n' "$PASS" "$FAIL"
|
||||
[ "$FAIL" -eq 0 ]
|
||||
Reference in New Issue
Block a user