diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 251d486..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,328 +0,0 @@ -name: release - -# Builds the `acyclic` binary for four targets, attaches them to a GitHub -# release, and publishes the npm launcher plus its four platform packages. -# -# Security posture (see packaging/npm/RELEASING.md): -# - Runs only on an annotated version tag or a manual dispatch (dry run -# by default). No PR or branch event can reach the publish job. -# - Every action is pinned to a commit SHA, not a mutable tag. -# - Default token permissions are read-only; each job asks for exactly -# what it needs. Build jobs never see the npm token. -# - Publishing is gated behind the `npm` GitHub environment, so a -# required reviewer must approve before the token is exposed. -# - Binaries carry SLSA build-provenance attestations, and the publish -# job verifies those attestations before wrapping anything in a -# package. npm packages are published with provenance too. -# - Lifecycle scripts are disabled for every npm command. - -on: - push: - tags: - - "v[0-9]+.[0-9]+.[0-9]+" - - "v[0-9]+.[0-9]+.[0-9]+-*" - workflow_dispatch: - inputs: - dry_run: - description: "Build and attest, but run npm publish with --dry-run and skip the GitHub release" - type: boolean - default: true - -permissions: - contents: read - -concurrency: - group: release-${{ github.ref }} - cancel-in-progress: false - -env: - CARGO_TERM_COLOR: always - # acyclic-fs is a git dependency pinned to a commit SHA; cargo's libgit2 - # fetcher can't resolve non-tip SHAs on GitHub, so use the git CLI. - CARGO_NET_GIT_FETCH_WITH_CLI: "true" - # Strip symbols so the npm platform packages stay small. Set here rather - # than in Cargo.toml so local debug-friendly release builds are unchanged. - CARGO_PROFILE_RELEASE_STRIP: symbols - -jobs: - # --------------------------------------------------------------------- - # Confirm the tag, Cargo.toml, and the npm launcher all agree on one - # version before spending any build minutes. - # --------------------------------------------------------------------- - verify: - runs-on: ubuntu-24.04 - outputs: - version: ${{ steps.v.outputs.version }} - dry_run: ${{ steps.v.outputs.dry_run }} - name: ${{ steps.v.outputs.name }} - npm_package: ${{ steps.v.outputs.npm_package }} - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - persist-credentials: false - - - name: Resolve and cross-check version - id: v - env: - REF_TYPE: ${{ github.ref_type }} - REF_NAME: ${{ github.ref_name }} - DRY_RUN_INPUT: ${{ inputs.dry_run }} - run: | - set -euo pipefail - cargo_version="$(awk '/^\[workspace.package\]/{f=1;next} /^\[/{f=0} f && /^version/ {gsub(/[" ]/,"",$3); print $3}' Cargo.toml)" - # The public name and npm package come from product.toml; the npm - # launcher is generated from it at publish time, so the version has - # exactly one source: Cargo.toml. - source scripts/product.sh - bash scripts/check-product-name.sh - echo "Cargo.toml : ${cargo_version}" - echo "product name : ${PRODUCT_NAME}" - echo "npm package : ${PRODUCT_NPM_PACKAGE}" - echo "name=${PRODUCT_NAME}" >> "$GITHUB_OUTPUT" - echo "npm_package=${PRODUCT_NPM_PACKAGE}" >> "$GITHUB_OUTPUT" - - if [ "$REF_TYPE" = "tag" ]; then - tag_version="${REF_NAME#v}" - echo "git tag : ${tag_version}" - [ "$tag_version" = "$cargo_version" ] || { echo "::error::tag $REF_NAME does not match Cargo.toml version $cargo_version"; exit 1; } - dry_run=false - else - dry_run="${DRY_RUN_INPUT:-true}" - fi - echo "version=${cargo_version}" >> "$GITHUB_OUTPUT" - echo "dry_run=${dry_run}" >> "$GITHUB_OUTPUT" - - # --------------------------------------------------------------------- - # Native builds on each target's own runner. No cross toolchains, so the - # binary is exactly what `cargo build --release` produces there. - # --------------------------------------------------------------------- - build: - needs: verify - permissions: - contents: read - id-token: write # sign the provenance attestation - attestations: write # store it - strategy: - fail-fast: true - matrix: - include: - - { os: darwin, cpu: arm64, runner: macos-14, target: aarch64-apple-darwin, exe: "" } - - { os: darwin, cpu: x64, runner: macos-13, target: x86_64-apple-darwin, exe: "" } - - { os: linux, cpu: x64, runner: ubuntu-22.04, target: x86_64-unknown-linux-gnu, exe: "" } - - { os: linux, cpu: arm64, runner: ubuntu-22.04-arm, target: aarch64-unknown-linux-gnu, exe: "" } - - { os: win32, cpu: x64, runner: windows-2022, target: x86_64-pc-windows-msvc, exe: ".exe" } - runs-on: ${{ matrix.runner }} - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - persist-credentials: false - - - name: Install toolchain - run: | - rustup toolchain install stable --profile minimal - rustup default stable - rustup target add ${{ matrix.target }} - - - name: Build - run: cargo build --release --locked -p acyclic --target ${{ matrix.target }} - - - name: Smoke test and checksum - # Windows runners default to pwsh; this script is bash on every target. - shell: bash - env: - VERSION: ${{ needs.verify.outputs.version }} - NAME: ${{ needs.verify.outputs.name }} - run: | - set -euo pipefail - # The cargo target is named acyclic internally; the artifact takes the public name. - bin="target/${{ matrix.target }}/release/acyclic${{ matrix.exe }}" - out="$("$bin" --version)" - echo "$out" - [[ "$out" == *"$VERSION"* ]] || { echo "::error::binary reports '$out', expected version $VERSION"; exit 1; } - [[ "$out" == "$NAME "* ]] || { echo "::error::binary reports '$out', expected it to be named $NAME"; exit 1; } - mkdir -p dist - cp "$bin" "dist/${NAME}-${{ matrix.os }}-${{ matrix.cpu }}${{ matrix.exe }}" - (cd dist && shasum -a 256 "${NAME}-${{ matrix.os }}-${{ matrix.cpu }}${{ matrix.exe }}" > "${NAME}-${{ matrix.os }}-${{ matrix.cpu }}.sha256") - cat dist/*.sha256 - - - name: Attest build provenance - uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0 - with: - subject-path: dist/${{ needs.verify.outputs.name }}-${{ matrix.os }}-${{ matrix.cpu }}${{ matrix.exe }} - - # SPDX SBOM from the locked dependency graph the binary was built from. - # Generated per target so each binary's SBOM attestation names exactly - # the lockfile that produced it; the lists are identical across targets. - - name: Generate SBOM - uses: anchore/sbom-action@3ad7283483fc7af8ff2b4ea19663c2d5ca935e26 # v0.24.2 - with: - file: Cargo.lock - format: spdx-json - output-file: dist/${{ needs.verify.outputs.name }}-${{ matrix.os }}-${{ matrix.cpu }}.spdx.json - upload-artifact: false - upload-release-assets: false - - - name: Attest SBOM - uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0 - with: - subject-path: dist/${{ needs.verify.outputs.name }}-${{ matrix.os }}-${{ matrix.cpu }}${{ matrix.exe }} - sbom-path: dist/${{ needs.verify.outputs.name }}-${{ matrix.os }}-${{ matrix.cpu }}.spdx.json - - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - with: - name: ${{ needs.verify.outputs.name }}-${{ matrix.os }}-${{ matrix.cpu }} - path: dist/ - if-no-files-found: error - retention-days: 7 - - # --------------------------------------------------------------------- - # GitHub release with the raw binaries and a SHA256SUMS file. scripts/install.sh - # consumes these, and a future Homebrew formula will too. - # --------------------------------------------------------------------- - github-release: - needs: [verify, build] - if: needs.verify.outputs.dry_run == 'false' - runs-on: ubuntu-24.04 - permissions: - contents: write - steps: - - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - with: - path: dist - merge-multiple: true - - - name: Assemble SHA256SUMS and the release SBOM - env: - VERSION: ${{ needs.verify.outputs.version }} - NAME: ${{ needs.verify.outputs.name }} - run: | - set -euo pipefail - cd dist - # One SBOM ships with the release (the per-target ones are identical - # and each stays attested against its own binary). Drop the others by - # name: a glob like *-*-*.spdx.json also matches the release SBOM once - # VERSION carries a prerelease suffix (acyclic-0.1.0-rc.1.spdx.json). - mv "${NAME}-linux-x64.spdx.json" "${NAME}-${VERSION}.spdx.json" - for t in darwin-arm64 darwin-x64 linux-x64 linux-arm64; do - rm -f "${NAME}-${t}.spdx.json" - done - cat ./*.sha256 > SHA256SUMS - rm ./*.sha256 - shasum -a 256 "${NAME}-${VERSION}.spdx.json" >> SHA256SUMS - shasum -a 256 -c SHA256SUMS - ls -l - - - uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2 - with: - files: dist/* - fail_on_unmatched_files: true - generate_release_notes: true - - # --------------------------------------------------------------------- - # npm publish. Gated by the `npm` environment; this is the only job that - # can read NPM_TOKEN. Platform packages go first, the launcher last, so a - # half-finished publish never leaves a launcher pointing at missing deps. - # --------------------------------------------------------------------- - npm-publish: - needs: [verify, build] - runs-on: ubuntu-24.04 - environment: npm - permissions: - contents: read - id-token: write # npm provenance + verifying the binary attestations - attestations: read - env: - VERSION: ${{ needs.verify.outputs.version }} - DRY_RUN: ${{ needs.verify.outputs.dry_run }} - NAME: ${{ needs.verify.outputs.name }} - PKG: ${{ needs.verify.outputs.npm_package }} - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - persist-credentials: false - - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: "22" - registry-url: "https://registry.npmjs.org" - - - name: Lock down npm - run: | - npm config set ignore-scripts true - npm config set fund false - npm config set audit false - npm --version - - - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - with: - path: dist - merge-multiple: true - - - name: Verify checksums and build attestations - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - cd dist - shasum -a 256 -c ./*.sha256 - for t in darwin-arm64 darwin-x64 linux-arm64 linux-x64 win32-x64.exe; do - gh attestation verify "./${NAME}-$t" \ - --repo "${GITHUB_REPOSITORY}" \ - --signer-workflow "${GITHUB_REPOSITORY}/.github/workflows/release.yml" - done - # The linux-x64 binary runs on this runner: prove it before publishing. - chmod +x "${NAME}-linux-x64" - "./${NAME}-linux-x64" --version | grep -F "$VERSION" - - - name: Assemble platform packages - run: | - set -euo pipefail - for t in darwin:arm64 darwin:x64 linux:x64 linux:arm64 win32:x64; do - os="${t%%:*}"; cpu="${t##*:}" - exe=""; [ "$os" = "win32" ] && exe=".exe" - packaging/npm/platform-package.sh "$os" "$cpu" "$VERSION" "dist/${NAME}-${os}-${cpu}${exe}" build - done - packaging/npm/launcher-package.sh "$VERSION" build - find build -type f | sort - - - name: Publish platform packages - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - run: | - set -euo pipefail - flags=(--access public --provenance --ignore-scripts) - [ "$DRY_RUN" = "true" ] && flags+=(--dry-run) - for pkg in build/"${PKG}"-*; do - name="$(node -p "require('./$pkg/package.json').name")" - if [ "$DRY_RUN" != "true" ] && npm view "${name}@${VERSION}" version >/dev/null 2>&1; then - echo "${name}@${VERSION} already on the registry, skipping" - continue - fi - echo "::group::publish ${name}@${VERSION}" - (cd "$pkg" && npm publish "${flags[@]}") - echo "::endgroup::" - done - - - name: Publish launcher - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - run: | - set -euo pipefail - flags=(--access public --provenance --ignore-scripts) - [ "$DRY_RUN" = "true" ] && flags+=(--dry-run) - if [ "$DRY_RUN" != "true" ] && npm view "${PKG}@${VERSION}" version >/dev/null 2>&1; then - echo "launcher ${VERSION} already on the registry, skipping" - exit 0 - fi - (cd "build/${PKG}" && npm publish "${flags[@]}") - - - name: Post-publish install check - if: needs.verify.outputs.dry_run == 'false' - run: | - set -euo pipefail - tmp="$(mktemp -d)" - cd "$tmp" - npm init -y >/dev/null - # Fresh install from the public registry, no auth, scripts disabled. - NODE_AUTH_TOKEN= npm install --ignore-scripts "${PKG}@${VERSION}" - "./node_modules/.bin/${NAME}" --version | grep -F "$VERSION" diff --git a/CHANGELOG.md b/CHANGELOG.md index 0278fe1..1a934a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project are documented here. Format loosely follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## Moved + +This repository is archived. The plugin was imported into +[`acyclic-labs/sdk`](https://github.com/acyclic-labs/sdk) as `plugin/` at +commit `e82be0b` of this repository (sdk PR #100, 2026-09-19); everything +after this line is the history that was carried across. Releases now come +from the sdk repository as `plugin-v` tags, and `scripts/install.sh` +here is a shim that runs the installer from there. + ## [Unreleased] Pre-1.0; `main` is the only supported line (see `SECURITY.md`). diff --git a/README.md b/README.md index 7c729fb..12ac152 100644 --- a/README.md +++ b/README.md @@ -1,153 +1,14 @@ -# graphcoder-plugin +# graphcoder-plugin has moved -Checkpoint every agent action, rewind exactly, see the blast radius. The store captures what git can't give back: untracked files, gitignored artifacts, and what a `bash` step wrote. History survives across sessions and is linked to the conversation turn that caused it. +The `acyclic` CLI, daemon and MCP server now live in the Acyclic SDK +repository as [`plugin/`](https://github.com/acyclic-labs/sdk/tree/main/plugin), +where they build against `acyclic-fs` in the same workspace, are qualified by +the same CI, and release as `plugin-v` tags. -**A local product with plugin distribution.** The product is an agent-native state engine that runs on your machine — snapshots, forks, and indexing over your working tree. The plugins are thin adapters that deliver it through Claude Code, Codex, OpenCode, any agent that can run a shell command, and Claude Desktop over MCP. The engine is the moat; the plugins are the channel. +- Source, docs and design history: https://github.com/acyclic-labs/sdk/tree/main/plugin +- Install: `npm i -g @acyclic-labs/plugin`, or + `curl -fsSL https://raw.githubusercontent.com/acyclic-labs/sdk/main/plugin/scripts/install.sh | sh` +- Issues and pull requests: open them on `acyclic-labs/sdk`. -> Status: Launches 1–4 built (Rewind, Timeline, Forks, Safe Mode), acceptance suites green on macOS and Linux, published to npm as `@acyclic-labs/plugin`. Launch 1's release gate is met: snapshot exclusions, a store-growth proof, license scanning, an attested SBOM per binary, `scripts/install.sh`, and a clean-machine install test. What v1 deliberately does not do is prune or purge history; see [Retention and purge](#retention-and-purge). Launch 5 (Monorepo) is spec. The spec lives on the [Acyclic plugins docs page](https://acyclic.dev/docs/plugins). - -## Install - -**Step 1 — get the binary.** Pick one of these; they are alternatives, not a sequence. - -```sh -npm i -g @acyclic-labs/plugin -``` - -```sh -curl -fsSL https://raw.githubusercontent.com/acyclic-labs/graphcoder-plugin/main/scripts/install.sh | sh -``` - -npm ships prebuilt binaries for macOS, Linux, and Windows x64, and is the path that works today. The installer script, which covers macOS and Linux only, downloads the binary for your machine from a GitHub release, checks it against that release's `SHA256SUMS`, and drops it in `~/.local/bin` — no sudo, no package manager. **No release is cut yet, so the script currently exits with a 404 and points you back at npm**; it goes live with the first tag. Set `ACYCLIC_VERSION` to pin a release and `ACYCLIC_INSTALL_DIR` to install elsewhere. - -**Step 2 — wire it into your repo.** - -```sh -cd your-repo -acyclic init # starts the daemon, builds the first snapshot -acyclic install # one of the hosts below; repeat per tool you use -``` - -Releases are built natively per target, carry SLSA build-provenance and SBOM attestations, and ship a `SHA256SUMS` the installer verifies. Cutting one is described in `packaging/npm/RELEASING.md`. - -### Per host - -Three adapter shapes exist. **Capability-based**: Pydantic AI is a framework, not an app, so its adapter is a Python package (`acyclic-pydantic-ai`) whose `Acyclic` capability rides the agent's own lifecycle: a checkpoint before and after every mutating tool call, a turn per `agent.run`, the previous session's brief in the instructions, and rewind/timeline/diff as native tools. **Hook-based** hosts expose a lifecycle-hook API, so a checkpoint is taken automatically around every edit and command; the adapter is checked-in config the whole team inherits. **MCP-based** hosts have no such API; the adapter registers `acyclic mcp`, an MCP server that exposes `checkpoint`/`timeline`/`rewind`/`diff`/`restore`/`turns`/`brief` as tools the model calls explicitly, and the daemon's idle timer (`auto_checkpoint_idle_ms`) catches edits nothing asked to checkpoint. - -| Host | Surface | Shape | Command | What it writes | Verified | -|---|---|---|---|---|---| -| Claude Code | CLI | hooks | `acyclic install claude-code` | `.claude/settings.json` hooks, `/rewind` `/timeline` `/fork` commands, two skills — checked in | live session: `tests/acceptance/claude-e2e.sh` (2.1.270, `claude-opus-5`). Also drives `acyclic mcp` as an MCP client: `mcp-clients-e2e.sh` | -| Codex | CLI | hooks | `acyclic install codex` | `.codex/hooks.json` + AGENTS.md cheatsheet — checked in; trust the hooks once via `/hooks` | live session: `codex-e2e.sh` (0.154.0). MCP client path via `config.toml` overrides: `mcp-clients-e2e.sh` | -| Cursor | desktop app + CLI | hooks + MCP | `acyclic install cursor` | `.cursor/hooks.json`, `.cursor/rules/acyclic.mdc`, `.cursor/mcp.json` — checked in and portable: bare `acyclic` from `PATH`, and the server finds the repo from its working directory | hooks, live session: `cursor-e2e.sh`. MCP: `cursor-agent` lists the tools and calls them, `mcp-clients-e2e.sh` (after `cursor-agent mcp enable acyclic`) | -| Any shell-capable agent | CLI | cheatsheet | `acyclic install agents-md` | AGENTS.md block — checked in | n/a: no host to drive. Checkpoints come from the idle timer, not hooks | -| Claude Desktop | desktop app | MCP | `acyclic install claude-desktop` | `mcpServers.acyclic-` in your global `claude_desktop_config.json`, one entry per repo — **per machine, not checked in**; restart Desktop afterwards | the real app launches the server and completes `initialize` + `tools/list` (checked in its MCP log); server side: `mcp-e2e.sh` on every CI run. A tool call from inside a chat: manual only, see the guide | -| VS Code (Copilot agent mode) | IDE | MCP | `acyclic install vscode` | `.vscode/mcp.json` — checked in and portable: bare `acyclic` from `PATH`, and the server finds the repo from its working directory | server side: `mcp-e2e.sh`; config shape from VS Code's docs, unit-tested. VS Code reading it: not yet | -| OpenCode | CLI | MCP | `acyclic install opencode` | `opencode.json` (`mcp.acyclic`, `type: "local"`, command-as-array) + AGENTS.md cheatsheet — checked in and portable: bare `acyclic` from `PATH` | `opencode mcp list` reports `✓ acyclic connected` against the written config (2026-09-16); server side: `mcp-e2e.sh`. No in-chat tool call yet | -| GitHub Copilot CLI | CLI | MCP | `acyclic install copilot` | `mcpServers.acyclic-` in your global `~/.copilot/mcp-config.json` (`$COPILOT_HOME` moves it), one entry per repo — **per machine, not checked in** | server side: `mcp-e2e.sh`; config shape from GitHub's docs, unit-tested. A live `copilot` session: `mcp-clients-e2e.sh` runs it when the CLI is on `PATH` — not yet exercised here (CLI not installed) | -| Pydantic AI | Python framework | capability (hooks) | `acyclic install pydantic-ai` | AGENTS.md cheatsheet — checked in; adds the `acyclic-pydantic-ai` PyPI package to the project **after a y/N prompt** (`--yes` for scripts). Attach with one line: `Agent(model, capabilities=[Acyclic()])` | real Pydantic AI agent on a scripted model, every run of `run-all.sh`: `pydantic-e2e.sh` (pydantic-ai 2.45.0). Live model: `ACYCLIC_E2E_PYDANTIC_MODEL=` | -| Copilot coding agent (cloud) | GitHub-hosted | MCP | `acyclic install copilot-agent` | `.github/workflows/copilot-setup-steps.yml` — checked in; the MCP entry itself is **printed to paste** into the repo's Copilot settings, since GitHub stores it there rather than in a file | not verified. The agent runs in a GitHub-hosted sandbox, so its checkpoints are that sandbox's timeline, not your local one — see the caveat below | - -The Copilot coding agent is the one host that is not a local integration: it runs on GitHub's infrastructure against its own checkout, so `acyclic` there records that sandbox's work, and nothing it checkpoints reaches your machine unless it lands on a branch. `install copilot-agent` prepares that sandbox and prints the config to paste; it does not pretend to give you local parity. - -"Verified" means what CI or a person has actually run, not what should work. The live sessions pin their model (`ACYCLIC_E2E_CLAUDE_MODEL`, `ACYCLIC_E2E_CURSOR_MODEL`, `ACYCLIC_E2E_CODEX_MODEL` in `common.sh`) because they assert on model-driven behavior — that the agent calls the tool rather than shelling out, and performs the prompt's steps in order — so an unpinned default makes a vendor's model change look like a regression. The Claude gate is pinned to `claude-opus-5`: it is green there and red on `claude-sonnet-5`, which does not complete the three-step prompt. Codex is unpinned (its CLI exposes no way to enumerate valid ids offline). The `*-e2e.sh` live sessions need the host CLI and credentials and run behind `ACYCLIC_E2E=1`; `mcp-e2e.sh` drives `acyclic mcp` with a scripted client and needs only the binary, so it runs on every CI pass. The install-side config merges are unit-tested in `crates/acyclic/src/install.rs`. [`docs/manual-testing.md`](docs/manual-testing.md) is the step-by-step checklist for re-verifying every host by hand after a host upgrade, with the results of the last pass. - -Not yet covered: an `install` writer for Codex's MCP config (TOML), Kimi Code CLI, Windsurf, Zed, JetBrains AI assistants, Gemini CLI, Amazon Q Developer. The `TODO(more hosts)` block above `run()` in `install.rs` is the checklist for adding one: find the host's hook or MCP config from its own docs, reuse `merge_mcp_server_json` when the shape fits, add a unit test that proves other entries survive, then verify against the real app. - -## The public name - -`product.toml` at the repo root holds the public name once. The CLI command, `./config.toml`, the state and config directories, hook commands, skill names, message prefixes, the `_TRACE` and `_HOOK` variables, release asset names, and the npm bin all derive from it at build or packaging time (`crates/acyclic-engine/build.rs`, `scripts/product.sh`, the workflows). Crate names stay `acyclic*` because they are internal. `scripts/install.sh` is fetched standalone and mirrors the name, repo, and npm package; `scripts/check-product-name.sh` fails CI if any of them drifts or if any user-facing Rust string spells the name out. Renaming is: change `product.toml`, update the three mirror lines in `install.sh`, rebuild. - -## Configuration - -`.acyclic/config.toml` is checked in, so the policy ships with the repo. Every key has a safe default; zero config is supported. Machine-level defaults live in `~/.config/acyclic/config.toml`, and the two layers merge key by key — a repo config overrides only the keys it names. - -| Key | Default | What it does | -|---|---|---| -| `exclude` | `[]` | Repo-relative paths (a file, or a directory and everything under it) that never enter a checkpoint: secrets, bulky generated state. A rewind leaves the live copies untouched; `acyclic restore` refuses them. | -| `trash_ttl_days` | `7` | How long a rewound-away tree stays in the store's trash. | -| `commit_every` / `commit_idle_ms` | `25` / `60000` | How often per-tool-call checkpoints are published to the durable store. | -| `auto_checkpoint_idle_ms` | `5000` | Idle-timer safety net: checkpoints changes on its own once the watcher has been quiet this long, for hosts with no lifecycle-hook API (Claude Desktop). `0` disables it. Cheap no-op for hooked hosts, which already drain the watcher themselves. | -| `quiesce_ms` / `quiesce_cap_ms` | `50` / `500` | Watcher quiet window before a capture. | -| `dry_run` / `guarded_paths` | `false` / `[]` | Safe Mode (Launch 4). | -| `[decompose]` / `[merge]` | | Fork decomposition policy and merge limits (Launch 3). | -| `store_dir` | `~/.local/share/acyclic/stores` | Where stores live. Never inside the repo. | - -Speculation is configured separately, in `~/.config/acyclic/speculate.toml` — per developer, never checked in, because turning it on can spend that developer's money. See [Speculation](#speculation). - -`exclude` matches **paths, not names**: `exclude = ["__pycache__"]` excludes a top-level `__pycache__/` and nothing else — it will not exclude `src/__pycache__/`. Name every path you mean (`"src/__pycache__"`), or exclude the directory that contains them. The wrong form fails silently and looks like it worked: the build output is captured anyway, and a Rust `target/` measured 1.4 GB of store and +29 s per build against 14 MB and 35 s with it excluded. - -Adding a path to `exclude` takes effect at the next daemon start; the baseline it builds is scrubbed, and every later checkpoint skips the path. Generations captured before the rule still hold it (see below). - -## Speculation - -The daemon knows things a request does not: it sees a prompt before the agent acts, it knows when a turn closed, and it knows when the watcher went quiet. Those are moments when the machine is idle and the answer to a question nobody has asked yet is already determined. So it computes them then. - -- **The session brief**, when a session ends. It is the most expensive thing the agent waits on — `SessionStart` blocks on it and prints it into the model's context — and it costs a pipeline diff per abandoned branch plus two more. The session that will read it ends long before it is asked for. -- **A turn summary**, when the next turn starts. This one runs a model, so it is the only part of the product that spends money. - -A result is keyed by the generation it describes. Generations are Merkle ids, so a result computed against a tree that has since moved simply never matches the key a later request builds — a stale answer is unreachable rather than guarded against. Nothing speculative runs on the pipeline thread, and nothing speculative is load-bearing: a full queue, a wedged cache or a missing database all fall through to computing the answer the way it was computed before. - -Speculation is **off by default** and configured per developer in `~/.config/acyclic/speculate.toml`, never in the checked-in repo config: whether to spend tokens is a personal decision, not one a teammate inherits from a commit. - -```toml -enabled = true # the free half: precompute, zero tokens -kinds = ["brief", "summary"] # "summary" is the one that spends -command = ["claude", "-p", "--model", "claude-haiku-4-5-20251001"] -max_runs_per_session = 20 # hard ceiling -``` - -**Two gates, not one.** `enabled` alone buys the precompute half. Spending also needs a `command` *and* `"summary"` in `kinds`, so no single boolean can put you on the meter. The command must be on `PATH` or absolute, gets the prompt on stdin, and runs in an empty scratch directory with no filesystem route into the repo — its whole input is a store-computed diff (changed paths and the prompt excerpt, never file contents), so `exclude` governs what it can see. It runs in its own process group, so a timeout kills the children an agent CLI spawns rather than leaking them. - -`acyclic summary` and the `summary` MCP tool read what was produced; neither ever produces one on demand, because a request arriving is not consent to spend. `acyclic status` reports the hit rate and what has been spent: - -``` -speculation: on — precompute + model runs (claude -p --model claude-haiku-4-5-20251001) - 24h: 31 run(s) · 19 of 31 claimed (61%) · median lead 8.2s · 412.0 KB out -``` - -Median lead is the number to watch: it is how far ahead of the request a claimed result landed, and near zero means the trigger is firing too late to be worth anything. Design and rationale: [`docs/design/08-speculation.md`](docs/design/08-speculation.md); the egress note is in [`docs/design/07-compliance.md`](docs/design/07-compliance.md). - -## Retention and purge - -`acyclic status` reports the store size; trash is pruned by TTL. The store itself is never garbage-collected in v1, on purpose. At the pinned `acyclic-fs` revision a generation stays reachable only while it is a workspace head or carries a retention fact (checkpoint label, pin, fork base), retention facts cannot be released, and closure proofs do not follow generation parents. So the fs collector would either destroy every checkpoint but the head or, if every checkpoint were pinned first, never free anything again. Purge-through-history has the same dependency: content cannot be physically removed from a retained generation. Both land when the fs grows a retention-release fact; until then, keep secrets out of the store with `exclude`, which is the compliance control that ships. Details and the upstream ask are in `docs/design/implementation-rewind.md`, Phase 4. - -Known caveats: mtimes are not restored on rewind, a rewind warrants an editor reload, forks and Safe Mode sessions do not see excluded paths, and baseline capture runs at roughly 230 s/GiB on first `init`. - -## Thesis - -Local-first cockpit, agent-summoned muscle. Typing `claude` (or `codex`) starts an ordinary local session — the dev's terminal, their repo, their workflow, unchanged in minute one. The plugin upgrades the substrate the agent works against, not where the agent lives. - -V1 is entirely local: no sandboxes, no managed sessions, no cloud sync. It ships the state layer — snapshots, forks, and indexing — drawn from dVFS and the agent-native VCS. Compute offload and durable sessions arrive in later versions, reached incrementally from the same local session. - -## Architecture - -One engine, thin adapters: - -- **`acyclic` CLI + daemon** — watcher, Merkle-DAG snapshot store, index. Host-agnostic. -- **Per-host adapters** — hook-based for CLIs with a lifecycle-hook API (Claude Code, Codex, Cursor), MCP-based for desktop apps and IDEs without one (Claude Desktop, VS Code; Cursor gets both). Every adapter is a `HostAdapter` in `crates/acyclic/src/install.rs`; the MCP server itself is `crates/acyclic/src/mcp.rs`, a thin translation of each tool call into the same `acyclic-proto::Op` the hooks send. The table under [Install](#per-host) says what each one writes and how far it has been verified; `docs/design/06-installation.md` has the design and the ship decision for the MCP path. - -## Launch plan - -| Launch | Name | Engine increment | Story | Status | -|---|---|---|---|---| -| 1 | Rewind | Merkle snapshot store + host hooks | Never fear letting the agent loose | built (`tests/acceptance/journey.sh`, `crash.sh`, `soak.sh`, `latency.sh`, `claude-e2e.sh`) | -| 2 | Timeline | Turn-linked metadata index | The repo at any point in the conversation | built (`timeline.sh`) | -| 3 | Forks | Copy-on-write materialization | N parallel attempts, pick the winner | built: mounted forks, promote with three-way merge (`forks.sh`, `merge.sh`, `claude-merge-e2e.sh`) | -| 4 | Safe Mode | Session redirection + interposition | Agents on the codebase, not agents' mistakes in it | built, needs the native mount layer (`safe-mode.sh`) | -| 5 | Monorepo | Merkle-aware content + symbol index | The repo that finally works with agents | not started | - -Run everything with `tests/acceptance/run-all.sh`; the live Claude Code scenarios are gated by `ACYCLIC_E2E=1`. - -Full feature lists, user journeys, and technical requirements per launch: [docs/plugins](https://acyclic.dev/docs/plugins). - -## Compliance posture - -**Local-first, and stronger than "no code leaves the machine":** there is no network code in the product at all. No HTTP client is compiled into any crate, so there is no telemetry, no update check, and no egress to enumerate or firewall. Security review is of a local binary, not a vendor. - -Releases carry a SLSA build-provenance attestation and an SPDX SBOM per binary, and every push is scanned for licences, advisories and sources against `deny.toml`. Builds are native per target rather than reproducible, and macOS binaries are not notarized. - -**The shipped control on the snapshot store is `exclude`.** Encryption at rest, purge-through-history, secret scanning and enforced retention have been described as controls but are **not built**; purge and GC are blocked on an upstream retention-release fact, so store retention is currently unbounded. [`docs/design/07-compliance.md`](docs/design/07-compliance.md) separates what is true today from what is intended, claim by claim — read it before making a compliance commitment to anyone. - -## License - -Apache-2.0 (see [LICENSE](LICENSE)). Contributions require DCO sign-off. +This repository is archived. Its history up to the move is preserved here; +`CHANGELOG.md` names the commit that was imported. diff --git a/scripts/install.sh b/scripts/install.sh index cf23cd8..2a97e1d 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1,119 +1,21 @@ #!/bin/sh -# Installer: downloads the prebuilt binary for this machine from a -# GitHub release, verifies it against the release's SHA256SUMS, and installs -# it into a user-writable bin directory. No sudo, no package manager. +# The installer moved with the plugin into acyclic-labs/sdk. This shim keeps +# the old one-liner working by running the current installer from there. # -# curl -fsSL https://raw.githubusercontent.com/acyclic-labs/sdk/main/plugin/scripts/install.sh | sh -# -# Environment: -# ACYCLIC_VERSION release to install, e.g. 0.0.3 (default: the version -# named by plugin/LATEST on the sdk repo's main branch) -# ACYCLIC_INSTALL_DIR where the binary goes (default: $HOME/.local/bin) -# ACYCLIC_RELEASE_URL base URL of a release's assets (default: the GitHub -# release for ACYCLIC_VERSION); file:// works, which is -# how scripts/install-smoke.sh tests this script offline -set -eu - -# This script is fetched on its own, so it cannot read product.toml. These -# four lines mirror it; scripts/check-product-name.sh fails CI if they drift. +# These four lines mirror product.toml so scripts/check-product-name.sh still +# holds; the real installer at the URL below is what consults them. NAME="acyclic" REPO="acyclic-labs/sdk" NPM_PACKAGE="@acyclic-labs/plugin" TAG_PREFIX="plugin-v" -# The sdk repository hosts several release families, so "latest release" is -# not necessarily this product's. main carries the current version in a file. -LATEST_URL="https://raw.githubusercontent.com/$REPO/main/plugin/LATEST" -INSTALL_DIR="${ACYCLIC_INSTALL_DIR:-$HOME/.local/bin}" - -say() { printf '%s\n' "$*" >&2; } -die() { say "install.sh: $*"; exit 1; } - -case "$(uname -s)" in - Darwin) os=darwin ;; - Linux) os=linux ;; - *) die "unsupported OS $(uname -s); see https://github.com/$REPO/releases" ;; -esac -case "$(uname -m)" in - arm64|aarch64) cpu=arm64 ;; - x86_64|amd64) cpu=x64 ;; - *) die "unsupported CPU $(uname -m); see https://github.com/$REPO/releases" ;; -esac -asset="$NAME-$os-$cpu" - -fetch() { - # fetch - if command -v curl >/dev/null 2>&1; then - curl -fsSL --retry 3 -o "$2" "$1" - elif command -v wget >/dev/null 2>&1; then - wget -q -O "$2" "$1" - else - die "need curl or wget" - fi -} - -if [ -n "${ACYCLIC_RELEASE_URL:-}" ]; then - base="${ACYCLIC_RELEASE_URL%/}" +set -eu +url="https://raw.githubusercontent.com/$REPO/main/plugin/scripts/install.sh" +if command -v curl >/dev/null 2>&1; then + script="$(curl -fsSL "$url")" +elif command -v wget >/dev/null 2>&1; then + script="$(wget -qO- "$url")" else - version_wanted="${ACYCLIC_VERSION:-}" - if [ -z "$version_wanted" ]; then - latest_tmp="$(mktemp "${TMPDIR:-/tmp}/$NAME-latest.XXXXXX")" - fetch "$LATEST_URL" "$latest_tmp" || die "cannot read $LATEST_URL; set ACYCLIC_VERSION" - version_wanted="$(tr -d ' \r\n' < "$latest_tmp")" - rm -f "$latest_tmp" - [ -n "$version_wanted" ] || die "$LATEST_URL is empty; set ACYCLIC_VERSION" - fi - base="https://github.com/$REPO/releases/download/${TAG_PREFIX}${version_wanted#v}" -fi - -sha256_of() { - if command -v sha256sum >/dev/null 2>&1; then - sha256sum "$1" | cut -d' ' -f1 - elif command -v shasum >/dev/null 2>&1; then - shasum -a 256 "$1" | cut -d' ' -f1 - else - die "need sha256sum or shasum to verify the download" - fi -} - -tmp="$(mktemp -d "${TMPDIR:-/tmp}/$NAME-install.XXXXXX")" -trap 'rm -rf "$tmp"' EXIT - -# A failure here is nearly always a missing release rather than a broken -# network: no release cut yet, or ACYCLIC_VERSION naming one that does not -# exist. Say which, and name the install path that does not need a release. -missing() { - say "install.sh: cannot fetch $1" - say "" - say "No release asset at that URL. See which releases exist:" - say " https://github.com/$REPO/releases?q=${TAG_PREFIX}" - say "A release must carry both $asset and SHA256SUMS." - say "" - say "To install without a GitHub release:" - say " npm i -g $NPM_PACKAGE" + echo "install.sh: need curl or wget" >&2 exit 1 -} - -say "downloading $asset from $base" -fetch "$base/$asset" "$tmp/$asset" || missing "$base/$asset" -fetch "$base/SHA256SUMS" "$tmp/SHA256SUMS" || missing "$base/SHA256SUMS" - -want="$(awk -v a="$asset" '$2 == a || $2 == "*" a || $2 == "./" a {print $1; exit}' "$tmp/SHA256SUMS")" -[ -n "$want" ] || die "SHA256SUMS has no entry for $asset" -got="$(sha256_of "$tmp/$asset")" -[ "$got" = "$want" ] || die "checksum mismatch for $asset: got $got, want $want" - -mkdir -p "$INSTALL_DIR" -chmod 0755 "$tmp/$asset" -# Atomic replace so a running daemon keeps its old inode until restart. -mv -f "$tmp/$asset" "$INSTALL_DIR/$NAME" - -version="$("$INSTALL_DIR/$NAME" --version 2>/dev/null || true)" -[ -n "$version" ] || die "installed binary does not run on this machine" -say "installed $version to $INSTALL_DIR/$NAME" - -case ":$PATH:" in - *":$INSTALL_DIR:"*) ;; - *) say "note: $INSTALL_DIR is not on your PATH; add it, e.g." - say " export PATH=\"$INSTALL_DIR:\$PATH\"" ;; -esac -say "next: cd your-repo && $NAME init && $NAME install claude-code" +fi +exec sh -c "$script"