diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5820d21a7..fa5cabaa8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,6 +45,28 @@ jobs: pnpm install --frozen-lockfile --ignore-scripts printf '%s' "$PR_TITLE" | pnpm exec commitlint --config commitlint.config.cjs + validate-kit: + name: Validate kit + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Install the sbx CLI + # `sbx kit validate` has no standalone distribution — pull the same linux-amd64 release + # tarball `sbx` itself ships (matches this runner's arch) and lift just the binary out. + env: + SBX_VERSION: "0.38.0" + run: | + curl -fsSL -o sbx.tar.gz \ + "https://github.com/docker/sbx-releases/releases/download/v${SBX_VERSION}/DockerSandboxes-linux-amd64.tar.gz" + tar xzf sbx.tar.gz docker-sbx/sbx + sudo install docker-sbx/sbx /usr/local/bin/sbx + + - name: Validate the kit + run: sbx kit validate kits/aidd + release-please: name: Release Please runs-on: ubuntu-latest @@ -275,6 +297,48 @@ jobs: "/tmp/${PLUGIN}-v${VERSION}.zip" \ --clobber + build-and-push-kit: + name: Build and push kit + needs: [release-please] + if: needs.release-please.outputs.release_created == 'true' + runs-on: ubuntu-latest + # packages: write — `sbx kit push` publishes an OCI artifact to ghcr.io. + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Install the sbx CLI + env: + SBX_VERSION: "0.38.0" + run: | + curl -fsSL -o sbx.tar.gz \ + "https://github.com/docker/sbx-releases/releases/download/v${SBX_VERSION}/DockerSandboxes-linux-amd64.tar.gz" + tar xzf sbx.tar.gz docker-sbx/sbx + sudo install docker-sbx/sbx /usr/local/bin/sbx + + - name: Log in to GitHub Container Registry + # A dedicated PAT, not GITHUB_TOKEN: pushing a brand-new OCI artifact package via + # `sbx kit push` 403s on write_package with GITHUB_TOKEN, even though the same token + # pushes a container image fine elsewhere — GHCR's package-creation check does not + # extend to GITHUB_TOKEN the same way for a non-container OCI artifact. A PAT with the + # `write:packages` scope, stored as the GHCR_PACKAGES_TOKEN secret, sidesteps it. + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GHCR_PACKAGES_TOKEN }} + + - name: Push the kit + # `sbx kit push` reads from the Docker credential store docker/login-action just + # populated for ghcr.io. Tagged both by the release version and `latest`. + run: | + VERSION="${{ needs.release-please.outputs.version }}" + IMAGE="ghcr.io/ai-driven-dev/aidd-sbx-kit" + sbx kit push kits/aidd "${IMAGE}:${VERSION}" + sbx kit push kits/aidd "${IMAGE}:latest" + publish-cli: name: Publish cli needs: [release-please] diff --git a/aidd_docs/memory/codebase-map.md b/aidd_docs/memory/codebase-map.md index 229ec0d28..8cbba5731 100644 --- a/aidd_docs/memory/codebase-map.md +++ b/aidd_docs/memory/codebase-map.md @@ -9,6 +9,7 @@ flowchart TD Root["framework/"] --> Plugins["plugins/"] Root --> Cli["cli/"] Root --> Kanban["kanban/"] + Root --> Kits["kits/"] Root --> Scripts["scripts/"] Root --> Docs["docs/"] Root --> AiddDocs["aidd_docs/"] @@ -24,6 +25,7 @@ flowchart TD | `plugins/` | the product — one dir per plugin, each with `plugins//.claude-plugin/plugin.json` and `skills/`, optionally `agents/`, `commands/`, `hooks/`, `rules/` | | `cli/` | the `aidd` binary. Has its own memory bank and `CLAUDE.md` | | `kanban/` | the task board. A private package with its own lockfile, unwired from the CLI | +| `kits/` | Docker Sandbox mixin kits that install the framework into a sandbox, native `claude plugin` calls, no CLI dependency. `kits//spec.yaml` | | `scripts/` | repository checks and generators, run by lefthook and CI. Tests in `scripts/__tests__/` | | `docs/` | durable docs — architecture, plugin authoring, glossary, maintainer runbook. `prompts-documentation.md` is generated | | `aidd_docs/` | this memory bank, plus `tasks/`, `runs/`, `product/`, `specs/`, `recipes/`, `brainstorm/` | diff --git a/aidd_docs/tasks/2026_10/2026_10_16_docker-sandbox-kit-aidd-678/phase-1.md b/aidd_docs/tasks/2026_10/2026_10_16_docker-sandbox-kit-aidd-678/phase-1.md new file mode 100644 index 000000000..a4aaf8a23 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_16_docker-sandbox-kit-aidd-678/phase-1.md @@ -0,0 +1,88 @@ +--- +status: done +--- + + + +# Instruction: Kit descriptor + +## Architecture projection + +> Tree of the final files. ✅ create · ✏️ modify · ❌ delete + +```txt +. +└── kits/ + └── aidd/ + ├── spec.yaml ✅ create + └── README.md ✅ create +``` + +## User Journey + +```mermaid +flowchart TD + A["sbx kit add ghcr.io/ai-driven-dev/aidd-sbx-kit:"] --> B["sbx recreates the sandbox, applies kits/aidd/spec.yaml"] + B --> C["setup.install: guard on claude CLI, then marketplace add + plugin install loop, aidd-ui excluded"] + C --> D["setup.install: merge extraKnownMarketplaces + enabledPlugins into /etc/claude-code/managed-settings.json"] + D --> E["Session starts: managed-settings.json survives the runtime overwrite of ~/.claude/settings.json"] + E --> F["Agent sees AIDD plugins enabled, aidd-ui still disabled"] +``` + +## Test Scope + + + +```mermaid +--- +title: Test scope +--- +journey + section Setup + Install the sbx CLI from the docker/sbx-releases tarball => sbx on PATH: 5: system + section Happy path + sbx kit validate kits/aidd => exits 0, no schema error: 5: cli + section Edge case - claude CLI absent in the sandbox image + setup.install runs with no claude on PATH => guard prints "aidd: this kit only applies to the claude-code agent" and exits 1, not a silent partial install: 1: system +``` + +## Wireframe + + + +## Tasks to do + +### `1)` Write the kit descriptor + +> `kits/aidd/spec.yaml`, the mixin Docker Sandbox kit v2 descriptor. + +1. `schemaVersion: "2"`, `kind: mixin`, `name: aidd`, `version: "1.0.0"`, `displayName`, `description`, `sourceURL: https://github.com/ai-driven-dev/framework`. +2. `permissions.network.allow: [github.com]` — the marketplace add clones over HTTPS. +3. `setup.install`, step one, run as the sandbox's non-root user: guard on `command -v claude`, exit 1 with a stated reason if absent; `claude plugin marketplace add ai-driven-dev/framework`; loop `claude plugin install "@aidd-framework"` over `aidd-context`, `aidd-refine`, `aidd-dev`, `aidd-vcs`, `aidd-pm`, `aidd-orchestrator` — `aidd-ui` is not in this list. +4. `setup.install`, step two: merge `extraKnownMarketplaces` and `enabledPlugins` from the user's `~/.claude/settings.json` into `/etc/claude-code/managed-settings.json` (create both as `{}`/`/dev/null` when absent, never run the merge as root against the user file, preserve every other key already in the managed file). +5. `agentInstructions.content`: the AIDD SDLC primer (same text already proven in the installed kit instance under `kits-agent-context/aidd.md`). + +### `2)` Write the kit's README + +> `kits/aidd/README.md` — purpose, install command, limits. + +1. State what the kit installs and that it targets Claude Code only. +2. Show `sbx kit add ghcr.io/ai-driven-dev/aidd-sbx-kit:` and `sbx run --kit ./kits/aidd` for local use. +3. State the limit: a mixin kit, not a full sandbox template — it does not provide or replace the agent's own image. + +### `3)` Validate the kit locally + +> Prove the descriptor is well-formed before it ever reaches CI. + +1. Install `sbx` the same way CI will: pull the pinned `docker/sbx-releases` linux-amd64 tarball, extract `docker-sbx/sbx`, install it on `PATH`. +2. Run `sbx kit validate kits/aidd`; fix the descriptor until it exits 0. + +## Test acceptance criteria + + + +| Task | Acceptance criteria | +| ---- | -------------------------------- | +| 1 | `kits/aidd/spec.yaml` parses as a valid Docker Sandbox kit v2 mixin; its install loop names six plugins and never names `aidd-ui` | +| 2 | `kits/aidd/README.md` names the install command and states the Claude-only, mixin-only scope | +| 3 | `sbx kit validate kits/aidd` exits 0 | diff --git a/aidd_docs/tasks/2026_10/2026_10_16_docker-sandbox-kit-aidd-678/phase-2.md b/aidd_docs/tasks/2026_10/2026_10_16_docker-sandbox-kit-aidd-678/phase-2.md new file mode 100644 index 000000000..8d28266dc --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_16_docker-sandbox-kit-aidd-678/phase-2.md @@ -0,0 +1,81 @@ +--- +status: done +--- + + + +# Instruction: Managed-settings regression test + +## Architecture projection + +> Tree of the final files. ✅ create · ✏️ modify · ❌ delete + +```txt +. +└── scripts/ + └── __tests__/ + └── kit-aidd-managed-settings.test.js ✅ create +``` + +## User Journey + +```mermaid +flowchart TD + A["Test reads kits/aidd/spec.yaml"] --> B["Extracts the install-loop plugin list and the managed-settings merge command verbatim"] + B --> C["Asserts aidd-ui is absent from the plugin list"] + C --> D["Runs the merge command against fixture managed.json + user.json in a temp dir"] + D --> E["Asserts the merged managed.json carries the AIDD marketplace and plugins, and keeps any pre-existing unrelated key"] +``` + +## Test Scope + + + +```mermaid +--- +title: Test scope +--- +journey + section Setup + Create a temp dir with a fixture managed.json (holding one unrelated pre-existing key) and a fixture user.json (extraKnownMarketplaces + enabledPlugins) => fixtures ready: 5: system + section Happy path + Run the exact merge command parsed from kits/aidd/spec.yaml against the fixtures => managed.json gains extraKnownMarketplaces.aidd-framework and enabledPlugins with every installed plugin true, aidd-ui absent, the pre-existing key intact: 5: cli + section Edge case - managed.json does not exist yet + No managed.json in the temp dir => the command creates it as {} first => the merge still succeeds: 1: system + section Edge case - user settings file does not exist + No user.json in the temp dir => the merge is a no-op => managed.json is unchanged: 1: system + section Teardown + Remove the temp dir => no fixture left on disk: 5: system +``` + +## Wireframe + + + +## Tasks to do + +### `1)` Extract the live spec's install logic + +> Pin the test against the real file, never a re-typed copy. + +1. Parse `kits/aidd/spec.yaml` with `js-yaml` (already a dev dependency) inside the test. +2. Read `setup.install[0].command` and `setup.install[1].command` as plain strings — nothing else in the test re-implements them. + +### `2)` Write and run the regression test + +> `scripts/__tests__/kit-aidd-managed-settings.test.js`. + +1. Assert `aidd-ui` is not among the plugins named in the install loop; assert the other six are. +2. In a fresh `mkdtemp` directory, write the fixture `managed.json` and `user.json` described in Test Scope. +3. Run the extracted merge command in that directory (substituting its hardcoded paths for the fixture paths), then read back `managed.json`. +4. Assert the merged shape and the two edge cases from Test Scope; clean up the temp directory. +5. Run via `node scripts/check-tests-leave-git-alone.js -- node --test 'scripts/__tests__/kit-aidd-managed-settings.test.js'`. + +## Test acceptance criteria + + + +| Task | Acceptance criteria | +| ---- | -------------------------------- | +| 1 | the test's assertions read from `kits/aidd/spec.yaml` itself, not from a duplicated string | +| 2 | the test fails if `aidd-ui` is ever added to the install loop, and passes against the current `spec.yaml` | diff --git a/aidd_docs/tasks/2026_10/2026_10_16_docker-sandbox-kit-aidd-678/phase-3.md b/aidd_docs/tasks/2026_10/2026_10_16_docker-sandbox-kit-aidd-678/phase-3.md new file mode 100644 index 000000000..06b7f1d67 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_16_docker-sandbox-kit-aidd-678/phase-3.md @@ -0,0 +1,93 @@ +--- +status: done +--- + + + +# Instruction: CI wiring and docs + +## Architecture projection + +> Tree of the final files. ✅ create · ✏️ modify · ❌ delete + +```txt +. +├── .github/ +│ └── workflows/ +│ └── ci.yml ✏️ modify +├── aidd_docs/ +│ └── memory/ +│ └── codebase-map.md ✏️ modify +├── docs/ +│ └── ARCHITECTURE.md ✏️ modify +└── commitlint.config.cjs ✏️ modify +``` + +## User Journey + +```mermaid +flowchart TD + A["Pull request touches kits/"] --> B["validate-kit job: install sbx, sbx kit validate kits/aidd"] + C["release-please merges a release on main"] --> D["build-and-push-kit job: install sbx, docker/login-action with GHCR_PACKAGES_TOKEN"] + D --> E["sbx kit push kits/aidd ghcr.io/ai-driven-dev/aidd-sbx-kit: and :latest"] + E --> F["package visible under the ai-driven-dev GitHub org"] +``` + +## Test Scope + + + +```mermaid +--- +title: Test scope +--- +journey + section Setup + Open the edited ci.yml => workflow file ready to inspect: 5: system + section Happy path + A pull request touching kits/ runs => validate-kit installs sbx and sbx kit validate kits/aidd exits 0: 5: ci + section Edge case - no release was created + release-please runs with no release_created output => build-and-push-kit does not run, and is not reported failed: 1: ci + section Edge case - GHCR_PACKAGES_TOKEN is missing + the secret is absent on the first real release => docker/login-action fails loudly with an auth error, not a silent skip: 1: ci +``` + +## Wireframe + + + +## Tasks to do + +### `1)` Add the PR-time validate job + +> `validate-kit` in `.github/workflows/ci.yml`, runs on every pull request and push, independent of `release-please`. + +1. Install `sbx` from the pinned `docker/sbx-releases` linux-amd64 tarball (same pin as phase 1's local check). +2. Run `sbx kit validate kits/aidd`. + +### `2)` Add the release-time publish job + +> `build-and-push-kit`, `needs: [release-please]`, `if: needs.release-please.outputs.release_created == 'true'`, mirroring `build-and-attach`'s gating. + +1. `permissions: packages: write` on the job (matches the existing `publish-cli` job's grant for its GitHub Packages mirror). +2. Install `sbx` the same way as task 1. +3. `docker/login-action` against `ghcr.io` using a new repository secret `GHCR_PACKAGES_TOKEN` — a personal access token with `write:packages`, not `GITHUB_TOKEN`: pushing a brand-new non-container OCI artifact under `GITHUB_TOKEN` 403s on package creation. **This secret must exist before this job can succeed; creating it is an org-admin action outside this change's scope — state that plainly in the pull request rather than assuming it is already set.** +4. `sbx kit push kits/aidd ghcr.io/ai-driven-dev/aidd-sbx-kit:${{ needs.release-please.outputs.version }}` and again tagged `:latest`. + +### `3)` Update the memory and architecture docs + +> Keep the maps honest now that `kits/` exists. + +1. Add a `kits/` row to `aidd_docs/memory/codebase-map.md`'s Areas table: holds the Docker Sandbox kit descriptors that install the framework into a sandbox. +2. Add one line to `docs/ARCHITECTURE.md` placing `kits/` relative to `plugins/`: a kit packages the CLI-free native install path for one external tool (Docker Sandboxes), not a Claude Code plugin. +3. Add `kit` to `commitlint.config.cjs`'s `scope-enum` list, kept kebab-case and alongside the other path-named scopes. + +## Test acceptance criteria + + + +| Task | Acceptance criteria | +| ---- | -------------------------------- | +| 1 | a pull request touching `kits/` runs `validate-kit` and it exits 0 against the current descriptor | +| 2 | `build-and-push-kit` runs only when `release-please` reports a release, and is absent from the run otherwise | +| 3 | `codebase-map.md` and `ARCHITECTURE.md` each name `kits/` and its relation to `plugins/` | diff --git a/aidd_docs/tasks/2026_10/2026_10_16_docker-sandbox-kit-aidd-678/plan.md b/aidd_docs/tasks/2026_10/2026_10_16_docker-sandbox-kit-aidd-678/plan.md new file mode 100644 index 000000000..9c8f7c43b --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_16_docker-sandbox-kit-aidd-678/plan.md @@ -0,0 +1,47 @@ +--- +objective: "A Docker Sandbox mixin kit installs the AIDD framework (Claude-only, aidd-ui excluded) into a sandbox, survives the runtime's settings overwrite, and publishes to ghcr.io/ai-driven-dev on release." +status: implemented +--- + + + +# Plan: Docker Sandbox kit for AIDD + +## Overview + +| Field | Value | +| ---------- | ----------------------- | +| **Goal** | Ship `kits/aidd/` as a Docker Sandbox v2 mixin kit that installs the AIDD marketplace and its recommended plugins (Claude Code only, `aidd-ui` excluded) into a sandbox, keeps them enabled past the runtime's settings overwrite, and is published to `ghcr.io/ai-driven-dev` on every framework release. | +| **Source** | https://github.com/ai-driven-dev/framework/issues/678 | + +## Phases + +| # | Phase | File | +| --- | -------------------------------- | ---------------------------- | +| 1 | Kit descriptor | [`phase-1.md`](./phase-1.md) | +| 2 | Managed-settings regression test | [`phase-2.md`](./phase-2.md) | +| 3 | CI wiring and docs | [`phase-3.md`](./phase-3.md) | + +## Resources + + + +| Source | Verified | +| ------ | ----------------- | +| https://docs.docker.com/ai/sandboxes/customize/kit-reference/ | `spec.yaml` v2 schema: `schemaVersion`, `kind: mixin`, `setup.install`, `agentInstructions`, `permissions.network` | +| https://docs.docker.com/ai/sandboxes/customize/kits-v2/ | `sbx kit add` on an existing sandbox supports only mixin kits limited to `environment`, `setup.install`, `setup.startup`, `permissions.network.allow` — matches this kit's scope | +| https://docs.docker.com/reference/cli/sbx/kit/push/ | `sbx kit push /:` publishes a kit directory as an OCI artifact | +| https://github.com/crocogiciel/docker-sbx-template (`.github/workflows/cd.yml`, `ci.yml`, `kits/aidd/spec.yaml`) | the only working reference implementation: `sbx` has no standalone distribution (pulled from the `docker/sbx-releases` release tarball); `sbx kit push` of a brand-new OCI artifact 403s under `GITHUB_TOKEN` and needs a dedicated PAT with `write:packages`; the sandbox runtime overwrites `~/.claude/settings.json` at session start, so plugin state must be merged into `/etc/claude-code/managed-settings.json` instead, which the runtime does not overwrite | + +## Decisions + + + +| Decision | Why | +| ---------- | ----- | +| The kit lives at top-level `kits/aidd/`, not under `plugins/` | `plugins/` is the native Claude Code plugin format only; a Docker Sandbox kit is a packaging artifact for a different consumer, a sibling area to `cli/` and `kanban/`, not a plugin | +| Install uses native `claude plugin marketplace add` / `claude plugin install`, not the `aidd` npm CLI | matches the maintainer-validated proof of concept; keeps the sandbox image free of a Node/pnpm dependency for this one step | +| `aidd-ui` is excluded from the install loop by default | explicit maintainer decision on the issue ("leave aidd-ui opt-in while alpha") — the reference implementation installs it and is not a template to copy here | +| Plugin state is merged into `/etc/claude-code/managed-settings.json`, never written to `~/.claude/settings.json` | the sandbox runtime replaces the user settings file at every session start; the managed file is the only location that survives and takes precedence | +| The kit's own `version` in `spec.yaml` is bumped by hand, decoupled from release-please | avoids adding a new release-please package for one mixin kit; the OCI tag it produces is the pinned reference the maintainer asked for | +| `build-and-push-kit` is gated on `release-please`'s `release_created` output, mirroring `build-and-attach`/`build-per-tool` | keeps one release cadence instead of pushing a kit image on every commit to `main` | diff --git a/commitlint.config.cjs b/commitlint.config.cjs index 7014c848c..ad8ee3b2e 100644 --- a/commitlint.config.cjs +++ b/commitlint.config.cjs @@ -27,6 +27,7 @@ module.exports = { "qa", "cli", "kanban", + "kit", "framework", "marketplace", "release-please", diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 3db1a9ebd..a1cc33174 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -21,6 +21,8 @@ flowchart TB Marketplace and plugins version independently. See the [marketplace guide](MARKETPLACE.md) for registration, scopes and updates. +A Docker Sandbox mixin kit (`kits//spec.yaml`) is a separate distribution path outside this chain: it drives a sandbox's own `claude plugin` CLI directly, with no CLI or marketplace-registration step of its own, and is not a plugin. + ## 🧩 Anatomy of a plugin Manifest and skills are required in AIDD; other capabilities are optional. Locations are relative to the plugin directory. diff --git a/kits/aidd/README.md b/kits/aidd/README.md new file mode 100644 index 000000000..d992e111e --- /dev/null +++ b/kits/aidd/README.md @@ -0,0 +1,29 @@ +# AIDD kit + +A Docker Sandbox mixin kit that installs the [AI-Driven Dev (AIDD)](https://github.com/ai-driven-dev/framework) framework into a `claude-code` sandbox: its marketplace plus `aidd-context`, `aidd-refine`, `aidd-dev`, `aidd-vcs`, `aidd-pm` and `aidd-orchestrator`. + +## Install + +On an existing sandbox: + +```bash +sbx kit add ghcr.io/ai-driven-dev/aidd-sbx-kit: +``` + +On a new one: + +```bash +sbx run claude-code --kit ghcr.io/ai-driven-dev/aidd-sbx-kit: +``` + +Against this checkout, before it is published: + +```bash +sbx run claude-code --kit ./kits/aidd +``` + +## Scope + +- Claude Code only: `setup.install` checks for the `claude` CLI and exits if it is absent. +- `aidd-ui`, `aidd-telemetry` and `aidd-qa` are not installed — they are alpha, beta and unproven respectively. Add one by name after install with `claude plugin install @aidd-framework` to opt in. +- A mixin kit, not a full sandbox template: it extends an existing `claude-code` agent image, it does not build or replace one. diff --git a/kits/aidd/spec.yaml b/kits/aidd/spec.yaml new file mode 100644 index 000000000..4dd4d1597 --- /dev/null +++ b/kits/aidd/spec.yaml @@ -0,0 +1,74 @@ +# schemaVersion "2" needs Docker Sandboxes 0.36+; an older sbx rejects +# permissions/setup/agentInstructions. Check with `sbx version`. +schemaVersion: "2" +kind: mixin +name: aidd +version: "1.0.0" +displayName: AI-Driven Dev Framework +description: "Installs the AI-Driven Dev (AIDD) plugins into Claude Code: an SDLC of skills and agents, from idea to reviewed pull request" +sourceURL: "https://github.com/ai-driven-dev/framework" + +permissions: + network: + allow: + - github.com # for `claude plugin marketplace add` + +setup: + install: + # Runs as the agent, not root: `claude plugin install` writes to $HOME/.claude. + - command: | + set -e + command -v claude >/dev/null 2>&1 || { + echo "aidd: this kit only applies to the claude-code agent" >&2 + exit 1 + } + claude plugin marketplace add ai-driven-dev/framework + # aidd-ui/telemetry/qa (alpha/beta/unproven) stay out — opt in by name. + for plugin in \ + aidd-context \ + aidd-refine \ + aidd-dev \ + aidd-vcs \ + aidd-pm \ + aidd-orchestrator + do + claude plugin install "${plugin}@aidd-framework" + done + user: "1000" + description: "Install the AIDD marketplace and its recommended plugins" + + # `claude plugin install` writes ~/.claude/settings.json, but the sandbox + # runtime overwrites that file at every session start. Mirror the install + # into managed-settings.json instead, which the runtime doesn't touch. + # Merged, not overwritten: other kits and the base image share that file. + - command: | + set -e + managed=/etc/claude-code/managed-settings.json + user=/home/agent/.claude/settings.json + mkdir -p "$(dirname "$managed")" + [ -s "$managed" ] || echo '{}' > "$managed" + # Runs as root; only reads the user file, never writes it (would leave it root-owned). + [ -s "$user" ] || user=/dev/null + tmp="$(mktemp)" + jq -s '.[0] * (.[1] + | {extraKnownMarketplaces, enabledPlugins} + | with_entries(select(.value != null)))' \ + "$managed" "$user" > "$tmp" + mv "$tmp" "$managed" + chmod 0644 "$managed" + description: "Keep the plugins enabled once the runtime replaces the user settings" + +agentInstructions: + content: | + The AI-Driven Dev (AIDD) framework is installed as Claude Code plugins. Its + skills are namespaced `/:`, and cover a full SDLC: + + - `/aidd-orchestrator:01-sdlc ""` runs the whole pipeline: frame → + plan → implement → validate → review → challenge → ship. + - Single steps live in `aidd-dev` (`01-plan`, `02-implement`, `05-review`, + `08-debug`, …), `aidd-context` (project onboarding and context artifacts), + `aidd-pm` (backlog, user stories), `aidd-vcs` (commit, pull-request). + + Prefer these skills over an ad-hoc approach when the user asks for AIDD, a + structured SDLC, or names one of the steps above. Run `/plugin` to list + what is available. diff --git a/scripts/__tests__/kit-aidd-managed-settings.test.js b/scripts/__tests__/kit-aidd-managed-settings.test.js new file mode 100644 index 000000000..061b9a1e8 --- /dev/null +++ b/scripts/__tests__/kit-aidd-managed-settings.test.js @@ -0,0 +1,123 @@ +const assert = require("node:assert/strict"); +const { execFileSync } = require("node:child_process"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); +const test = require("node:test"); +const yaml = require("js-yaml"); + +const root = path.resolve(__dirname, "../.."); +const specPath = path.join(root, "kits/aidd/spec.yaml"); + +const RECOMMENDED_PLUGINS = [ + "aidd-context", + "aidd-dev", + "aidd-orchestrator", + "aidd-pm", + "aidd-refine", + "aidd-vcs", +].sort(); + +function loadSpec() { + return yaml.load(fs.readFileSync(specPath, "utf8")); +} + +/** The exact `for plugin in ... do` list the install command loops over, read from the live + * spec rather than re-typed, so a future edit to the loop cannot drift from this test unnoticed. */ +function installedPlugins(installCommand) { + const match = installCommand.match(/for plugin in([\s\S]*?)\ndo\b/); + assert.ok(match, "expected a `for plugin in ... do` loop in the install command"); + return match[1] + .split(/[\s\\]+/) + .map((name) => name.trim()) + .filter(Boolean); +} + +/** Runs the merge command verbatim, pointed at fixture paths instead of its hardcoded + * `/etc/claude-code/managed-settings.json` and `/home/agent/.claude/settings.json`. */ +function runMerge(mergeCommand, managed, user) { + const script = mergeCommand + .replaceAll("/etc/claude-code/managed-settings.json", managed) + .replaceAll("/home/agent/.claude/settings.json", user); + execFileSync("sh", ["-c", script], { stdio: "pipe" }); +} + +function withTempDir(fn) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "aidd-kit-managed-settings-")); + try { + return fn(dir); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } +} + +test("the install loop names the recommended plugins and never aidd-ui", () => { + const spec = loadSpec(); + const plugins = installedPlugins(spec.setup.install[0].command); + + assert.deepEqual([...plugins].sort(), RECOMMENDED_PLUGINS); + assert.ok(!plugins.includes("aidd-ui"), "aidd-ui must stay opt-in, not installed by default"); +}); + +test("the managed-settings merge adds the marketplace and plugins, keeping unrelated keys", () => { + const spec = loadSpec(); + const mergeCommand = spec.setup.install[1].command; + + withTempDir((dir) => { + const managed = path.join(dir, "managed.json"); + const user = path.join(dir, "user.json"); + fs.writeFileSync(managed, JSON.stringify({ unrelated: "kept" })); + fs.writeFileSync( + user, + JSON.stringify({ + extraKnownMarketplaces: { + "aidd-framework": { source: { source: "github", repo: "ai-driven-dev/framework" } }, + }, + enabledPlugins: { + "aidd-context@aidd-framework": true, + "aidd-dev@aidd-framework": true, + }, + }) + ); + + runMerge(mergeCommand, managed, user); + + const merged = JSON.parse(fs.readFileSync(managed, "utf8")); + assert.equal(merged.unrelated, "kept"); + assert.ok(merged.extraKnownMarketplaces["aidd-framework"]); + assert.equal(merged.enabledPlugins["aidd-context@aidd-framework"], true); + assert.equal(merged.enabledPlugins["aidd-ui@aidd-framework"], undefined); + }); +}); + +test("the managed-settings merge creates a missing managed file first", () => { + const spec = loadSpec(); + const mergeCommand = spec.setup.install[1].command; + + withTempDir((dir) => { + const managed = path.join(dir, "nested", "managed.json"); + const user = path.join(dir, "user.json"); + fs.writeFileSync(user, JSON.stringify({ enabledPlugins: { "aidd-dev@aidd-framework": true } })); + + runMerge(mergeCommand, managed, user); + + const merged = JSON.parse(fs.readFileSync(managed, "utf8")); + assert.equal(merged.enabledPlugins["aidd-dev@aidd-framework"], true); + }); +}); + +test("the managed-settings merge is a no-op when the user settings file is absent", () => { + const spec = loadSpec(); + const mergeCommand = spec.setup.install[1].command; + + withTempDir((dir) => { + const managed = path.join(dir, "managed.json"); + const user = path.join(dir, "user.json"); // never created + fs.writeFileSync(managed, JSON.stringify({ unrelated: "kept" })); + + runMerge(mergeCommand, managed, user); + + const merged = JSON.parse(fs.readFileSync(managed, "utf8")); + assert.deepEqual(merged, { unrelated: "kept" }); + }); +});