diff --git a/.github/workflows/core-macos.yml b/.github/workflows/core-macos.yml new file mode 100644 index 00000000..63e29b64 --- /dev/null +++ b/.github/workflows/core-macos.yml @@ -0,0 +1,37 @@ +# The release source gate (release.yml) runs `make test-core` on macos-15 — +# but only on a tag push. Measured 2026-08-19 on v4.1.0 @ 27ad70e2: the +# repaired runner reached the core suite for the first time in five weeks and +# found 5 failures the operator host never shows (3x terminal runtime, a +# KeyError: 'env' in the cell launcher, the resume-pack red). Nothing before +# the tag had ever put the suite on a bare macOS image. This workflow closes +# that loop: the same suite, the same runner, on every pull request and on +# demand — so a tag is a confirmation, not an experiment. +name: Core runtime tests (macOS) + +on: + pull_request: + paths: + - "vibecrafted-core/**" + - "Makefile" + - ".github/workflows/core-macos.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + core-macos: + runs-on: macos-15 + timeout-minutes: 30 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false + + - name: Install uv + uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6 + + # Exactly what release.yml's source-gate runs, so a green check here + # predicts the tag gate instead of the tag discovering the suite. + - name: Run core runtime tests + run: make test-core diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 40acf7ff..f85da9ef 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -61,16 +61,25 @@ jobs: - name: Build the portable release channel run: make portable + # POSIX grep, not ripgrep: this job runs on the bare GitHub macos-15 + # image, which ships neither ripgrep nor shellcheck — the latter is + # why the `brew install shellcheck` step above exists. The earlier + # spelling forced a lookup of a real rg binary on PATH, so this step + # would have failed the whole gate after every test had already + # passed: the exact shape of the v4.0.0 xcrun death, four steps + # later. It arrived in ef700e52 (3.7.1) and every tag since died + # earlier, so it has never once executed. These are literal presence + # assertions over enumerated files, which is what grep is for. - name: Confirm publication boundary for both channels run: | set -euo pipefail test -x scripts/publish-vibecrafted-release.sh test -x scripts/build-portable-release.sh - command rg -n 'Vibecrafted_.*YYYYMMDD|DMG_NAME|\.dmg\.sha256' \ + grep -nE 'Vibecrafted_.*YYYYMMDD|DMG_NAME|\.dmg\.sha256' \ scripts/build-vibecrafted-release.sh \ scripts/publish-vibecrafted-release.sh \ docs/RELEASE_KICKOFF.md - command rg -n 'PORTABLE_NAME|portable\.tar\.gz|portable-output\.json' \ + grep -nE 'PORTABLE_NAME|portable\.tar\.gz|portable-output\.json' \ scripts/build-portable-release.sh \ scripts/publish-vibecrafted-release.sh \ docs/RELEASE_KICKOFF.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 14521cab..dca42e3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,32 @@ Format follows [Keep a Changelog](https://keepachangelog.com/). ## Unreleased +> **4.2.0 scope — measured truths, finished seams.** Release integrity from the +> donor snapshot through to the payload a stranger downloads, and one identity +> order shared by every surface that reads a run. + +### Added + +- `vibecrafted init` carries unfinished work into every session. Each pass + projects this checkout's needs-attention settlement bucket, classifies every + run, and prints the exact command that continues it — newest first, with the + truncated remainder counted out loud. Silent on a clean checkout; an + unreadable ledger degrades to an honest `UNKNOWN` rather than bricking init. + Guardian-owned runs are listed without a command, because each holds a single + automatic attempt that a hand resume would burn. +- `--snapshot-donors` on `scripts/build-vibecrafted-release.sh` builds a release + from a detached worktree at each donor's `HEAD`, so a dirty donor no longer + blocks a cut and never leaves a ghost worktree registration behind. A reaper + folded into the release cleanup trap removes and prunes them. +- `--silence-timeout` on the dispatcher, and a supervisor bound on worker + **stdout silence** rather than wall-clock time. A worker that is slow but + talking is untouched; one blocked in `wait4` now settles through the existing + stall handler, which records `stall_kind=silence|wall_clock` so triage never + has to guess which bound fired. +- `make payload-hygiene` refuses any release payload that names the build host, + reporting the topmost still-host-specific ancestor instead of only the exact + checkout string. + ### Changed - Bare `vibecrafted resume ` (and `--root`) opens a new interactive @@ -13,6 +39,70 @@ Format follows [Keep a Changelog](https://keepachangelog.com/). - Resume pack assembler lives in `aicx_session_chain` (CLI transport + MCP session-chain contract). Catalog rows are evidence, not a Tinder picker. Empty-with-`-p` is `empty_project`, not a silent scanned=0. +- `vibecrafted doctor` judges an installed owner by **where it lands**, not by + what its path is called. Ownership is containment in + `$VIBECRAFTED_RUNTIME_HOME`, resolved directly or through a wrapper's `exec` + target. A new cross-check compares the reported install identity against the + `VERSION` of the root the launcher actually enters, and warns — naming both — + when they disagree, instead of reporting `ok` for a generation nothing runs. +- `workspace_id` mints UUIDv7 and accepts **any** canonical UUID. The wire + contract previously read as a validation rule; a reader that enforced it would + have dropped this repository's own v4 workspace entry from its dashboard. + Chronology comes from `created_at`, never from the id bits. +- The vc-frame default server is the canonical product origin `127.0.0.1:3024` + instead of a tailnet address compiled into the shipped binary. `VC_SERVER_URL` + and `--server` still route to any remote; that is now an operator choice + rather than a build-time one. +- `vibecrafted_core` imports siblings by module path instead of through the + package barrel. Runtime behaviour and lazy exports are unchanged; the importer + graph now names the module that owns each symbol — measured 0 breaking / + 0 structural / 0 diamond cycles, repository health 74 to 80. +- Non-destructive branch push is a duty, not a stop. Force, trunk, delete, and + tag pushes remain the only git-push hard stops, and Mode B worktree workers + stay off remotes entirely. + +### Fixed + +- The delivery proof kernel could not run its own verification subject. The + executor scrubs the environment to `_SAFE_ENV_KEYS` — correctly dropping + `PYTHONPATH` — while the subject was declared as a `-m` module invocation + resolved through the `sys.path` that scrub had just removed. It died with + `ModuleNotFoundError`, the kernel wrote `proof.failed`, and **every run it + judged settled `failed` regardless of the worker's real outcome.** The package + location is now a contract-declared argument instead of ambient state. +- The supervisor heartbeat pulsed identically at 20 seconds and at 3 hours, so a + worker blocked in `wait4` held the supervisor open forever and the finished + `RunState.STALLED` handler was unreachable in production. +- The live dashboard resolved its workspace identity from the repository root + alone, while the runtime stamps runs from the exported + `VIBECRAFTED_WORKSPACE_ID` first. Two implementations of one question, free to + disagree — and a dispatched worker in a worktree, whose root can never equal + its dispatcher's, was structurally invisible to the LIVE RUNS filter. +- `docs/install.sh` exec'd `../install.sh` directly, but that file carries no + executable bit by design, so the shim died with exit 126 on every fresh clone. + It now execs `bash` explicitly, matching the packer contract. +- Chained keychain traps under `set -e`: `_ks_trap_cleanup` returns the + triggering status on purpose, and that non-zero return tore the shell down + before the caller's chained handler ran. Measured on a real failed release + that skipped its own reaper and left two worktree registrations behind. +- Four gates that guarded something real while being structurally unable to see + it break are now capable of failing — including the keychain regression suite, + which ran every child without `set -e`, the exact condition its target bug + requires. + +### Security + +- Signed artifacts no longer carry the operator's disk layout. `runtime_receipt` + dropped two hardcoded build-host absolutes (duplicates where they resolved, + dead entries shipping a private path everywhere else), Rust test-module + fixtures that the packer's `tests/` directory exclusion could not see were + neutralised, and the payload-hygiene gate now fails the build rather than the + reviewer. Measured on the 4.1.0 portable tarball: 5 offenders naming the + checkout, 12 naming the workshop one level above it. +- `install.ps1` is guarded byte-for-byte against the site repository's served + copy, so the two cannot drift apart unnoticed. +- `aicx` runs with a sanitized `PATH` (absolute, non-empty entries, system + fallback), so an implicit-cwd lookup can never pick up a stray binary. ## 4.1.0 — 2026-08-16 diff --git a/Makefile b/Makefile index 5c5630fc..b84da152 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ CARGO_BUILD_ROOT ?= $(INSTALLER_CACHE_HOME)/vibecrafted/build/$(INSTALLER_HOST_T # in-tree cache is never read or written by install lanes. export PYTHONPYCACHEPREFIX ?= $(INSTALLER_CACHE_HOME)/vibecrafted/pycache-$(INSTALLER_HOST_TAG) -.PHONY: help help-dev vibecrafted app dmg dmg-signed release-local notarize release portable publish-release gui-install wizard wizard-dev check test test-core test-skills test-install test-parity test-vc-frame test-iterm2-migrate test-memex test-aicx-sync test-hammerspoon test-keychain-session dispatch-test unified-product-contract-gate install install-auto install-all install-python-tools install-bundle-tools install-tools install-tools-held install-vendored-binaries install-app-binaries install-hammerspoon skills helpers setup-dev dry-run doctor list update uninstall restore migrate migrate-dry init-hooks seed-commit-msg-hooks bundle bundle-check foundations foundations-check semgrep version version-show version-bump bump-patch bump-minor bump-major iterm-plugin iterm-plugin-refresh iterm-plugin-show iterm-plugin-uninstall iterm-plugin-migrate demo demo-full commit-safe test-race-protection skill-new server server-build build-server-release server-check server-test install-server install-server-payload install-server-service server-smoke +.PHONY: help help-dev vibecrafted app dmg dmg-signed release-local notarize release portable publish-release gui-install wizard wizard-dev check test test-core test-skills test-install test-parity test-vc-frame test-iterm2-migrate test-memex test-aicx-sync test-hammerspoon test-keychain-session dispatch-test unified-product-contract-gate payload-hygiene install install-auto install-all install-python-tools install-bundle-tools install-tools install-tools-held install-vendored-binaries install-app-binaries install-hammerspoon skills helpers setup-dev dry-run doctor list update uninstall restore migrate migrate-dry init-hooks seed-commit-msg-hooks bundle bundle-check foundations foundations-check semgrep version version-show version-bump bump-patch bump-minor bump-major iterm-plugin iterm-plugin-refresh iterm-plugin-show iterm-plugin-uninstall iterm-plugin-migrate demo demo-full commit-safe test-race-protection skill-new server server-build build-server-release server-check server-test install-server install-server-payload install-server-service server-smoke help: @printf "\n" @@ -65,24 +65,55 @@ vibecrafted: install RELEASE_SCRIPT := scripts/build-vibecrafted-release.sh PORTABLE_SCRIPT := scripts/build-portable-release.sh KEYS ?= $(HOME)/.keys +# Extra builder flags, e.g. RELEASE_FLAGS=--snapshot-donors to build from +# detached worktrees at each donor HEAD instead of refusing a dirty donor. +# RELEASE_FLAGS reaches the builder as ARGV WORDS, never as shell text. Make +# expands its own variables into the recipe before zsh parses it, so the earlier +# spelling — $(RELEASE_FLAGS) spliced straight into the single-quoted `zsh -ic` +# argument — handed the value to zsh as source. +# +# MEASURED 2026-08-18, and the vector is narrower than it looks: a `;` in the +# value lands AFTER `exec`, so it never runs. Command substitution does, because +# zsh evaluates $(...) and backticks while building the exec's argv: +# RELEASE_FLAGS='--snapshot-donors $(touch /tmp/proof)' -> /tmp/proof exists +# Through the environment and split with zsh's ${=...} the same value arrives as +# four inert argv words and nothing is evaluated. +RELEASE_FLAGS ?= app: - @zsh -ic 'cd "$(CURDIR)" && KEYS="$(KEYS)" exec bash "$(RELEASE_SCRIPT)" --app-only' + @VC_RELEASE_FLAGS='$(RELEASE_FLAGS)' zsh -ic 'cd "$(CURDIR)" && KEYS="$(KEYS)" exec bash "$(RELEASE_SCRIPT)" --app-only $${=VC_RELEASE_FLAGS}' dmg dmg-signed release-local: - @zsh -ic 'cd "$(CURDIR)" && KEYS="$(KEYS)" exec bash "$(RELEASE_SCRIPT)" --no-notarize' + @VC_RELEASE_FLAGS='$(RELEASE_FLAGS)' zsh -ic 'cd "$(CURDIR)" && KEYS="$(KEYS)" exec bash "$(RELEASE_SCRIPT)" --no-notarize $${=VC_RELEASE_FLAGS}' notarize: - @zsh -ic 'cd "$(CURDIR)" && KEYS="$(KEYS)" exec bash "$(RELEASE_SCRIPT)" --notarize-only' + @VC_RELEASE_FLAGS='$(RELEASE_FLAGS)' zsh -ic 'cd "$(CURDIR)" && KEYS="$(KEYS)" exec bash "$(RELEASE_SCRIPT)" --notarize-only $${=VC_RELEASE_FLAGS}' release: - @zsh -ic 'cd "$(CURDIR)" && KEYS="$(KEYS)" exec bash "$(RELEASE_SCRIPT)"' + @VC_RELEASE_FLAGS='$(RELEASE_FLAGS)' zsh -ic 'cd "$(CURDIR)" && KEYS="$(KEYS)" exec bash "$(RELEASE_SCRIPT)" $${=VC_RELEASE_FLAGS}' # The portable channel needs no signing identity and no notary account: it is a # provenance-bound source distribution, so it builds anywhere git and python3 do. portable: @bash "$(PORTABLE_SCRIPT)" +# Ask an artifact that ALREADY EXISTS whether it names the build host. Both +# release scripts run this gate before they sign or publish, but a release is +# expensive and the artifacts from before the gate existed are still on disk — +# so the same question has to be answerable without a rebuild. +# +# make payload-hygiene ARTIFACT=dist/Vibecrafted.app +# make payload-hygiene ARTIFACT=dist/Vibecrafted_4.1.0-20260817-237d2814.dmg +# +# A .dmg is mounted read-only and detached again; nothing is written anywhere. +PAYLOAD_HYGIENE_SCRIPT := scripts/payload-hygiene-artifact.sh +ARTIFACT ?= +payload-hygiene: + @test -n "$(ARTIFACT)" || { \ + printf 'usage: make payload-hygiene ARTIFACT=\n' >&2; \ + exit 2; } + @bash "$(PAYLOAD_HYGIENE_SCRIPT)" "$(ARTIFACT)" + publish-release: @zsh -ic 'cd "$(CURDIR)" && exec bash scripts/publish-vibecrafted-release.sh' diff --git a/docs/RELEASE_CHECKLIST.md b/docs/RELEASE_CHECKLIST.md index e802bf0c..398cf00d 100644 --- a/docs/RELEASE_CHECKLIST.md +++ b/docs/RELEASE_CHECKLIST.md @@ -90,6 +90,70 @@ make semgrep What this proves: the version file, the donors, and the source gates agree before you spend an hour in notarization. +### When a donor is dirty and you are not going to clean it + +The Living Tree keeps donors dirty on purpose, and the builder refuses a dirty +donor because a receipt must not bind a SHA that could move mid-build. Do **not** +hand-roll `git worktree add --detach` into a temp dir: that is where the ghost +registration of 2026-08-11 came from. Use the flag instead: + +```bash +make dmg RELEASE_FLAGS=--snapshot-donors +# or directly: +bash scripts/build-vibecrafted-release.sh --no-notarize --snapshot-donors +``` + +It creates a detached worktree at each donor's HEAD under +`build/unified-release/donor-snapshots/`, builds from those, and reaps them from +the same trap that ends the signing keychain — on success, on failure and on +Ctrl-C. The donor's own working tree, index and stashes are never touched, and +the receipt still binds the donor HEAD, because that is what the snapshot is. + +Two things to know before you use it: the snapshot starts from a **cold cargo +target directory**, so the build is a full rebuild; and the check afterwards is +that each donor is back to the worktree count it had _before_ the build, not +that it has exactly one — donors legitimately carry other agents' worktrees. + +```bash +git -C ../vc-frame worktree list # same entries as before the build +git -C ../vc-terminal worktree list +``` + +**Use it for anything you intend to ship.** `--snapshot-donors` is the only mode +that rebuilds vc-frame's bundled WASM plugins. Those blobs are git-tracked build +output: `make release-binary` builds `--no-plugins` and embeds them with +`include_bytes!`, so without a rebuild the release ships whatever paths the +machine that last ran `make plugins-assets` happened to have. Measured on the +4.1.0 DMG that was 411 occurrences of the operator's home directory inside +`Contents/Helpers/vc-frame` alone. The rebuild adds about a minute and the +snapshot is the only tree we are entitled to regenerate into — doing it to the +living donor would rewrite tracked files another agent may be mid-edit on. + +### The payload gate + +Before a signature is spent, the builder greps the assembled bundle for every +path that exists only on this machine — your home directory, the checkout, both +donors, the snapshots — and refuses to continue if it finds one. There is no +allowlist, on purpose. + +If it fires, the message names each offending file and how many times. Read it +as a real finding: `--remap-path-prefix` only covers rustc, and the payload has +at least four other producers (cc-rs, Swift/xcodebuild, uv's CPython, pip +console scripts). `scripts/payload_hygiene.py` explains each one. + +You can ask the same question of an artifact you already have, without a +rebuild: + +```bash +make payload-hygiene ARTIFACT=dist/Vibecrafted.app +make payload-hygiene ARTIFACT=dist/Vibecrafted_4.1.0-20260817-237d2814.dmg +make payload-hygiene ARTIFACT=dist/Vibecrafted_4.1.0-20260818-c52f1326-portable.tar.gz +``` + +A `.dmg` is attached read-only and detached again; a tarball is extracted into a +temp directory that is removed on every exit path. The artifact is never written +to. + ## 3. Build, sign, notarize ```bash @@ -97,8 +161,12 @@ make release ``` This is `scripts/build-vibecrafted-release.sh` with `KEYS=$HOME/.keys`. -It remaps `RUSTFLAGS` so panic/debug metadata never contain `$HOME` or -the checkout path, and sets `MACOSX_DEPLOYMENT_TARGET=14.0`. +It sets `MACOSX_DEPLOYMENT_TARGET=14.0` and hands every compiler in the build a +prefix map so no debug metadata names this machine: `RUSTFLAGS` for rustc, +`CFLAGS`/`CXXFLAGS` for the C sources cc-rs compiles, and `-debug-prefix-map` +for Swift through xcodebuild. Order matters — rustc applies the **last** match, +so the list runs broadest first. The payload gate above is what checks the +result rather than assuming it. Expected outputs under `dist/`: @@ -196,6 +264,39 @@ and the product-contract / core / installer gates passed on the immutable tag SHA. It does **not** build or upload a DMG. That is intentional. Do not add `contents: write` or `gh release create` to `release.yml`. +### What this gate has actually done — read before you push the tag + +The source gate is not a formality with a green history. It has **failed on +every tag since `v3.5.0`**, and the two repairs below have never been exercised +by a real tag push. + +| Tag | Date | Result | Died on | +| ------ | ---------- | ------- | ---------------------------------------------------- | +| v3.5.0 | 2026-07-12 | success | — | +| v3.7.0 | 2026-07-27 | failure | 1m42s | +| v3.7.1 | 2026-08-14 | failure | 12s | +| v3.7.1 | 2026-08-14 | failure | 12s | +| v4.0.0 | 2026-08-15 | failure | `VCPC033: xcrun is required`, after 483 tests passed | + +Both failures were the same class — a **host tool the runner did not have**, +never a code defect: + +1. `v4.0.0` ran on a Linux runner while the product contract inspects a real + Mach-O fixture. Cured by `54a98b23` (`runs-on: macos-15`), which landed 86 + minutes _after_ that tag failed and has not run since. +2. The final `Confirm publication boundary` step called `rg`, which the + `macos-15` image does not ship either. It arrived in `ef700e52` (3.7.1) and + every tag since died before reaching it, so the gap was invisible to "the + last release worked". Cured in the 4.2.0 flight by moving those two literal + presence assertions to POSIX `grep`, and bound by + `tests/tui/test_release_contract.py::test_tag_gate_only_calls_tools_its_own_runner_provides`. + +Consequence for whoever pushes the next tag: **treat the first run as an +experiment, not a formality.** Push `v4.1.0` at `27ad70e2` first — it is already +merged and stable, `VERSION` and `CHANGELOG.md` both already call it released, +and it has no tag at all — so the never-exercised path gets proven on a version +that is safe to re-cut, before a new one depends on it. + Also required: zero open CodeQL alerts on `main`. ```bash diff --git a/docs/ROADMAP_4.1.1.md b/docs/ROADMAP_4.1.1.md index 2062e8f6..c64d8a17 100644 --- a/docs/ROADMAP_4.1.1.md +++ b/docs/ROADMAP_4.1.1.md @@ -71,3 +71,36 @@ same immutable pack path and digest. - a second durable control plane; - provider-specific summaries that fork repository truth; - replacing native provider resume when an exact resumable session exists. + +## Backlog for the next scaffold (not 4.1.1 scope) — scaffolded as ROADMAP_4.2.0 + +Items surfaced by ground truth on 2026-08-18; each is a small scaffold cut, +none is a release blocker. + +1. **Dirty donors are a release feature, not an operator ritual.** `make release` + refuses dirty donors (`../vc-terminal`, `../vc-frame`); the operator hand-rolls + `git worktree add --detach` snapshots to get past it, and the ghost entry + `snapshot2` (2026-08-11, pointing at a deleted `$TMPDIR/.tmpQUEtCY/snapshot`) + was the residue — no script in either repo ever created a worktree + (`git log --all -S'worktree add' -- scripts Makefile` → docs only). Add + `--snapshot-donors` to `scripts/build-vibecrafted-release.sh`: create detached + worktrees at donor HEADs inside the build work dir, record the SHAs in the + receipt, reap with `git worktree remove --force` + `prune` in the trap, and a + contract test that a run leaves `git worktree list` at exactly one entry. + Superseded by ROADMAP_4.2.0 cut W1-b. +2. **Symlink-free distribution payload as a gate, not a hope.** The 3.7.0 + tarball shipped 4 symlink entries (`vetcoders.zsh -> vetcoders.sh`, + `docs/install.sh -> ../install.sh`, a stray `.antigravitycli/.json` + pointing into an operator `$HOME`), which breaks Windows extraction and + `core.symlinks=false` clones. The portable channel now builds outside the + tree; verify on 4.x that the payload carries zero symlinks and add a + `find -type l` gate beside the env-secret gate. Fold the + in-repo aliases (`runtime`, `skills`, `docs/install.sh`, + `vibecrafted_core/config/vc-frame -> ../../../config/vc-frame`) into that + cut: a package must not depend on repo layout above itself. +3. **Import direction around `vibecrafted_core/__init__.py`.** Loctree audit + (health 93) shows every non-breaking cycle (1 structural, 3 diamond, + 17 lazy) rooted in the package barrel re-exporting from modules that + import back from the package. One deliberate cut on the hub (188 external + importers) clears roughly 40% of the audit list; needs its own wave with + the full Python gates, not a drive-by. diff --git a/docs/ROADMAP_4.2.0.md b/docs/ROADMAP_4.2.0.md new file mode 100644 index 00000000..dadcb281 --- /dev/null +++ b/docs/ROADMAP_4.2.0.md @@ -0,0 +1,511 @@ +# Vibecrafted 4.2.0 roadmap — measured truths, finished seams + +Status: planned (scaffolded 2026-08-18). Not part of the 4.1.0 release contract. + +Plan package (atlas · falsification · tracker · DRIVER · 9 briefs · manifest): +`~/.vibecrafted/artifacts/vetcoders/vibecrafted/2026_0818/plans/roadmap-4.2.0/` +Drive it from `DRIVER.md` there; this file is the repo-facing summary. + +## Thesis + +4.1.0 shipped two channels (DMG + portable) and a durable Workspace identity, but +several truths are still asserted rather than measured, and three product seams +are visibly unfinished. 4.2.0 turns each into a verifier-earned `[x]` or an honest +`[?]`. Only a delivery-verifier flips `[~]→[x]`. + +## Waves + +| Wave | Cut | Title | Vector | Repo | +| ---- | ---- | ----------------------------------------------- | --------- | ---------------------- | +| W0 | W0-a | Verify 4.1.0 payloads symlink/.env/HOME-free | recon | vibecrafted | +| W0 | W0-b | `resume --run-id` e2e on the installed build | e2e | vibecrafted | +| W0 | W0-c | LIVE RUNS dashboard runtime acceptance | e2e | vibecrafted | +| W1 | W1-a | Symlink-free tree: guard + Windows-clone smoke | stabilize | vibecrafted | +| W1 | W1-b | Donor snapshots as a release feature | implement | vibecrafted | +| W1 | W1-c | Serve `install.ps1` | implement | vibecrafted-io | +| W2 | W2-a | Workspaces surface in the vc-frame session rail | implement | vc-frame | +| W2 | W2-b | Vibecrafted.app boundary + chrome polish | implement | vibecrafted + vc-frame | +| W3 | W3-a | Core `__init__` import direction | stabilize | vibecrafted | + +Order: W0 (parallel, read-only) → W1 (parallel, disjoint files) → W2 (parallel) → +W3 (after W1-a). Every wave ends at an operator button (merge / deploy / install). + +## Decisions + +1. Repo tree is symlink-free (landed in #47); a contract test + Windows-clone smoke guard it; projections are produced by installer/packers, never linked. +2. Dirty donors are a release feature (`--snapshot-donors`), not an operator ritual. +3. Windows gets a served entry point (`/install.ps1`, WSL2 hand-off), not a native install. +4. vc-frame shows Workspaces (catalog, `workspace_id`), not physical session names. +5. Runtime acceptance on the installed build is a cut (W0), not a footnote. + +## Implement stage — what landed, 2026-08-18 + +Stage `implement` ran as a single worker (no fleet), so every cut below carries the +executing agent's own authorship, not the brief's planned assignee. + +| Cut | State | Landed SHA(s) | Measured result | +| ---- | ----- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| W0-a | `[!]` | recon only, no commit | Portable payload clean of the operator's **account**; it still carried the **checkout root** in 5 tracked files. **DMG is not clean:** 8 of 2955 files name the build host. Both addressed in the workflow stage — see below. | +| W0-b | `[~]` | recon only, no commit | Installed runtime `4.1.0+g237d2814` contains `18dea346`; `resume --run-id ` fails loudly (exit 1, names the id). Live resume of a real run not exercised from a headless worker. | +| W0-c | `[!]` | recon only, no commit | `catalog.json` present, schema valid, 48 workspaces — but **22 of 48 ids are UUIDv4, including this repo's** (`bda366e0-…-45f1-…`). The plan's UUIDv7 premise is false. | +| W1-a | `[~]` | `838165d6` | Guard added; the clone smoke found a real break and fixed it (see below). | +| W1-b | `[~]` | `cd13e1ca` | `--snapshot-donors` + reaper; proved on the real donors, failure path included. | +| W1-c | `[~]` | `e9f47da1` | Parity guarded. The 404 is a stale deploy branch, not a pipeline limit. | +| W2-a | `[ ]` | — | Not implemented; premise falsified (see W0-c) and acceptance is GUI-only. | +| W2-b | `[~]` | — | Chrome asks already landed in vc-frame `76048ca54`; the menu question is answered below. | +| W3-a | `[~]` | `01e5e18a`, `4918c7fb` | Import cycles 4 → **0**; loctree health 74 → **80**. | + +### Measured findings that changed the plan + +1. **`docs/install.sh` died with exit 126 on every fresh clone.** After #47 it became a + real shim that `exec`s `../install.sh`, but the repository file carries no executable + bit by design — `scripts/build-portable-release.sh:86-88` states exactly that. The shim + now execs `bash` explicitly. +2. **The 4.1.0 DMG leaks operator paths.** `--remap-path-prefix` ran (`/usr/src/operator-home` + is present) yet `/usr/src/vc-frame` and `/usr/src/vc-terminal` are present in _no_ binary: + the donor prefixes were built as `"$REPO_ROOT/../vc-frame"`, and a prefix containing `..` + never matches textually. Fixed in `cd13e1ca`. **The rest is explained as of the workflow + stage** — see "Workflow stage" below. It was never one leak. +3. **`keychain-session.sh` silently dropped the caller's cleanup on any failed release.** + `_ks_trap_cleanup` returns the triggering status by design, and under `set -e` a non-zero + command inside a trap tears the shell down before the chained handler runs. Measured on a + real failed release; fixed in `cd13e1ca`. +4. **`vibecrafted.io` is serving 3.7.0.** `https://vibecrafted.io/VERSION` answers `3.7.0` + and the served `install.sh` is the 3.7.0-era script. The site repo's deploy branch + (`origin/main`) is at 2026-04-14; the 4.1.0 hydration and `install.ps1` live only on an + unmerged branch. The `/install.ps1` 404 is one symptom of that, not the problem. +5. **"Open Console" is not a mislabelled terminal.** The tray menu already carries both + `Open Console` (the Swift main window) and `Open vc-terminal`. Renaming the first would + produce two terminal-sounding items. Kept; a clearer word than "Console" for the Swift + window is a naming call for the operator. +6. **`session-manager` requests no plugin permissions at all** (`request_permission` appears + only in `status-bar` and the test fixture), so W2-a cannot read the catalog from inside the + WASM sandbox without a new consent prompt. The smaller blast radius is a host-side + projection onto `SessionInfo` (`zellij-utils/src/data.rs:1824`). +7. **Five tests were already red before this stage** and stayed red: three + `test_research_launcher.py` settle timeouts, `test_vibecrafted_launcher.py::test_dashboard_subcommand_launches_repo_owned_vc_frame_layout`, + and `vibecrafted-core/tests/test_aicx_session_chain.py::test_resume_pack_never_selects_native_even_with_same_agent`. + All four/one confirmed against a clean `HEAD`. The dashboard one is green as of the + workflow stage; the three research timeouts are diagnosed there and remain red. + +## Workflow stage — what landed, 2026-08-18 + +Stage `workflow` consumed the review report (`revi-260818-160222-61808`) and its +Before-Merge TODO. Three commits: `85cebab5`, `f08e8076`, and the one carrying this +section. + +### W0-a is now a mechanism, not a mystery + +The review attributed the surviving `.cargo/registry` leak to vc-frame's git-tracked +WASM blobs. Measuring the shipped `Vibecrafted_4.1.0-20260817-237d2814.dmg` file by file +found **five** producers, of which `--remap-path-prefix` reaches exactly one: + +| Where | Count | Producer | Lever | +| ---------------------------------------------------------------- | ----------------: | -------------------------------------------------------- | ------------------------------ | +| `Contents/Helpers/vc-frame` | 411 × `$HOME` | git-tracked `assets/plugins/*.wasm` via `include_bytes!` | rebuild the plugins | +| `Contents/Helpers/vc-frame` | 17 × checkout | same blobs | same | +| `Contents/Helpers/vc-terminal.app/…/alacritty` | 277 × donor root | the `..` prefix bug | `canonical_dir`, already fixed | +| `Contents/MacOS/Vibecrafted` | 21 × `$HOME` | cc-rs C sources (`ring`) | `CFLAGS=-ffile-prefix-map` | +| `Contents/MacOS/Vibecrafted` | 51 × checkout | Swift sources + xcodebuild DerivedData | `-debug-prefix-map` | +| `runtime/python/lib/python3.12/_sysconfigdata__darwin_darwin.py` | 27 × checkout | uv's CPython recording its seed prefix | rewrite the literal | +| `runtime/python-site/bin/jsonschema` | 1 × checkout | pip console-script shebang | delete the directory | +| `Contents/MacOS/voc`, `Contents/MacOS/vc-mux-daemon` | 1 × checkout each | `env!("CARGO_MANIFEST_DIR")` probed at runtime | `#[cfg(debug_assertions)]` | + +Two of these are worse than leaks: + +- `runtime/python-site/bin/*` shipped **scripts that cannot run** — their shebang names a + `mktemp` directory the build deletes on its way out. Nothing invokes them (python-site + is on `PYTHONPATH`, never on `PATH`), so the directory is now removed outright. +- `default_command_deck()` and `find_tray_icon()` probed the build machine's checkout at + runtime, and on that machine the path **exists**. The shipped binary therefore preferred + the developer's living checkout — on the one machine where a release gets walked around + before it goes out. + +### The primary defence is a gate, not another flag + +Five producers with five different levers, and the set grows with every new kind of +bundled artifact. `scripts/payload_hygiene.py` reads the finished payload and knows +nothing about how it was made; both release channels run it before they sign or publish, +and `make payload-hygiene ARTIFACT=` asks the same question of anything already on +disk (`.app`, `.dmg`, `.tar.gz`). No allowlist. 2955 files in 1.7 s. + +Its first run found its own weakness: without the donor roots it certified a payload that +carries 277 occurrences of the vc-terminal donor path. A test pins that now. + +### Measured, closed-loop + +- **Plugin rebuild.** All 14 blobs rebuilt inside a real donor snapshot under the release + remaps: `$HOME` 276 → **0**, checkout root 14 → **0**. 60 s. The snapshot's resulting + dirty set is exactly `zellij-utils/assets/plugins/`, which is the whole of the new + `require_clean_repo` allowance. Donor restored: 2 worktrees before and after, 8 stashes + untouched, `git status` clean. +- **Remap precedence.** rustc applies the **last** matching `--remap-path-prefix` + (measured with two overlapping prefixes). `$HOME` was last, so on any host whose + checkout lives under `$HOME` every specific root was dead. Order is now broadest-first. +- **Keychain harness.** `run_child` now runs `set -euo pipefail`; all 61 existing cases + stay green, which is the finding — the suite was blind, not wrong. Two cases added for + the missing cell. Reverting `cd13e1ca` turns exactly one case red: the new one. +- **Barrel self-cycle.** Reproducing ruff PLR0402's rewrite turns the new AST test red at + the exact line, and `loct audit` agrees (`structural: 1`, node `__init__.py`). +- **RELEASE_FLAGS injection.** The vector is command substitution, not `;` — a `;` lands + after `exec` and never runs, while `$(...)` is evaluated while zsh builds the argv. + Both are inert now. + +### Corrections to the review + +- **P2-05 does not survive measurement.** `VIBECRAFTED_HOME` was already redirected to + `tmp_path`; the newest `rese-*` in the operator's real control plane is from 2026-08-13, + not from any test run. The three reds are a **product hang**: the dispatcher blocks in + `wait4` and its `workflow_runtime research` child blocks in the asyncio `kevent` loop, + with no timeout, both reparented to init and alive six minutes later. The stage fixed + the damage (the timeout path now reaps the recorded pgid — 0 orphans after a run, was 6) + and reports the hang rather than smuggling a supervisor rewrite into a review-fix stage. +- **P3-05 declined, with a reason.** Setting `CARGO_TARGET_DIR` to survive the reaper would + break vc-frame's own asset producer: `scripts/plugins-parity.zsh` hardcodes + `$REPO/target/wasm32-wasip1/release`. That is a vc-frame change and belongs to a cut that + can verify it there. + +### Not verified + +- The **Swift** and **cc-rs** prefix maps are wired but need a full signed release to + confirm. The payload gate is what makes that non-optional: the next release fails loudly + if they did not work. +- The plugin rebuild is proven on the blobs and on the snapshot's dirty set, **not** end to + end through a complete `--snapshot-donors` release (cold cargo ×2, signing, notarization). +- Nothing in this stage was pushed, merged, tagged or deployed. + +## Polarize stage — the one truth, 2026-08-18 + +The audit named three candidate axes. Only one of them was a live contradiction +_inside this repository_, so only one was cut. + +**Chosen axis — `workspace_id` is minted as UUIDv7 and accepted as any canonical +UUID.** No consumer may validate, filter, or sort on the version. + +The runtime already implemented this rule and was never wrong: `new_uuid7()` is +the single minting point, `require_uuid()` the single acceptance chokepoint +(fan-in 6, 35 callsites), and it is version-agnostic by construction. What +disagreed was the prose. `docs/runtime/WORKSPACE_IDENTITY.md` — the wire +contract a vc-frame Cut B reader consumes — declared the _kind_ of the three id +fields to be `UUIDv7`, and the `workspace_catalog` module docstring said the +same. Read as a validation rule, that prose is what turns W2-a into the top-2 +risk of this flight. + +Measured this stage, against the live catalog: **57 workspaces, 35 UUIDv7 and 22 +UUIDv4** — and this repository's own entry is +`bda366e0-519f-45f1-8d10-449058491a94`, **version 4**. A v7-only rail drops +Vibecrafted from its own dashboard. + +**Rejected alternatives.** + +- _Migrate the v4 ids to v7 so the doc becomes true._ Rejected: the catalog is + the sole durable identity store. Rewriting durable ids to satisfy a sentence + invalidates every projection keyed on them, for cosmetic gain. +- _Mint v7 **and** validate v7 (the "average" of the two axes)._ Rejected — this + is the exact shape of the failure. Averaging two viable-looking rules here + produces the bug. +- _Cut the cycle-gate axis (`loct audit --json` vs `loct follow` diamonds)._ + Rejected as out of scope for a repo cut: `loct follow` appears nowhere in this + tree as a gate. That conflict lives between two sentences of the W3-a brief, + not between two surfaces of the product. `loct audit --json` stands as the + instrument. +- _Cut the release-scope axis (4.2.0 = integrity spine, W2 deferred)._ Rejected + as **not an agent's call**: whether 4.2.0 waits for W2 is an operator button, + and the audit files it under "needs a human". + +**Aligned surfaces.** `docs/runtime/WORKSPACE_IDENTITY.md` (identity table now +reads `UUID`, plus an explicit _Accepted id rule_ section) · the +`workspace_catalog` module docstring · two regression tests in +`vibecrafted-core/tests/test_workspace_catalog.py` that put a real legacy v4 id +through create → show → select → list and assert list order comes from +`created_at`, never from the id bits. + +**Proof the gate can fail.** `is_uuid` was mutated to require version 7; both new +tests went red with `workspace_id must be a canonical UUID`, and green again on +restore. The guard reproduces the exact regression it exists to stop. + +**Unblocked by this cut.** W2-a may now implement the host-side `SessionInfo` +projection against a stated, tested rule. It remains unimplemented. + +## Polarize stage, loop 2 — the installed owner, 2026-08-18 + +**Chosen axis — an installed owner is where the launcher _lands_, not what its +directory is _named_. `Vibecrafted.app` is a first-class installed owner.** + +`vibecrafted doctor` graded three subsystems against one binary and got three +different answers on this host: + +| Subsystem | Verdict on `~/.local/bin/vibecrafted` | +| --------------------------------------- | ------------------------------------------------------------------ | +| `_launcher_shim_findings` | `fail` — "checkout/legacy bash deck … Reinstall" | +| `server_supervisor` (via `active.json`) | authoritative generation for the launchd plist | +| delivery receipt | `[CLEAN]`, `installed: …/releases/4.1.0+g237d2814/bin/vibecrafted` | + +The cause is not two _ages_ of one install — it is two _layouts_. `make install` +stages `tools/vibecrafted-generation-*` behind the `vibecrafted-current` +symlink; `Vibecrafted.app` (`AppDelegate.swift`) publishes +`releases//` and writes `~/.local/bin` wrappers that `exec` into it. +The doctor recognised only two owners — a uv-tool Python shim, and a bash deck +whose _path string_ contains `vibecrafted-generation-`. The app's own install +matched neither, so the shipped product was told to "reinstall so an installed +owner wins PATH" — advice that reproduces the identical layout and can never be +satisfied. + +Measured, not assumed: `releases/4.1.0+g237d2814/bin/vibecrafted` is a 203 KB +regular file, `resolve()` stays inside itself, and **0** symlinks under that +root escape to the checkout. It is an installed runtime by every property the +check claims to care about. + +The repository's own documentation already stated the correct rule. The +"Checkout-free gate" section of `docs/runtime/INSTALLED_RUNTIME_CAPSULE.md` +says doctor fails "when the public launcher resolves outside +`~/.local/share/vibecrafted`" — containment, not naming. The code implemented a +narrower rule than the doc it was written against, and the doc's _opening_ +paragraph had since drifted the other way ("enter **only** … `vibecrafted-current`"). +This cut restores one rule and makes both surfaces state it. + +**Rejected alternatives** + +| Rejected | Why it loses | +| -------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **The capsule layout is the only installed owner; the app must be repackaged into `tools/vibecrafted-generation-*`** | That is a packaging rewrite of the shipped product to satisfy a string test. The app already audits every runtime entry for executability and refuses symlinks before publishing; it is not less installed for choosing a different directory. | +| **The app channel wins and the capsule is legacy** | Refuted on this host: `tools/vibecrafted-current` is live at `4.1.0+ga7f262d9`, `make install` still writes it, and the generation manifest's digest closure has no counterpart in the app channel. Declaring it legacy would delete a real integrity boundary. | +| **Leave the `fail` and let operators reinstall** | The instruction is false. Reinstalling from the app reproduces byte-for-byte the layout being rejected. A gate whose remedy cannot satisfy it is noise that trains operators to ignore doctor. | +| **Unify the two pointers (`active.json` and `vibecrafted-current`) into one** | The right end state, and out of scope for a cut. It needs the app and the shell installer to agree on a write protocol — implement/marbles work. Polarize names the disagreement and makes doctor surface it; it does not invent the merged pointer. | +| **Also re-point `__version__` resolution at the entered runtime root** | Rewrites version resolution across `staged_tools_sync` (268 tests). The lie is closed by _reporting_ the disagreement, not by silently switching which generation answers. | + +**Aligned surfaces** + +- `vibecrafted_core/doctor.py` — ownership is containment in + `$VIBECRAFTED_RUNTIME_HOME`, resolved either directly or through a wrapper's + `exec` target. The target is believed only after `is_file()` + `X_OK` + + `resolve(strict=True)`, so a wrapper cannot talk its way into a root it does + not enter. New cross-check: the reported install identity is compared against + the `VERSION` of the root the launcher actually enters, and a mismatch is a + `warn` naming both. +- `docs/runtime/INSTALLED_RUNTIME_CAPSULE.md` — the opening paragraph now states + the boundary and both channels instead of contradicting the app. +- `vibecrafted-core/tests/test_doctor.py` — five regression tests: the + app-shaped wrapper is `ok`; a wrapper execing a checkout is `fail`; an `exec` + target that does not exist is `fail`; the version cross-check warns on + disagreement and stays quiet on agreement. + +**Proof the guards bite.** Against the pre-cut doctor, +`test_launcher_shim_finding_ok_for_app_installed_release_launcher` fails +`assert 'fail' == 'ok'`. With the cross-check branch mutated to `elif False`, +`test_launcher_version_warns_…` fails `assert 'ok' == 'warn'`. Both green on +restore. On the live host the check flips `fail` → `ok` and raises the +previously invisible `warn: doctor resolves install identity 4.1.0+ga7f262d9, +but the PATH launcher enters …/releases/4.1.0+g237d2814`. + +**What this does not fix.** One install channel still does not know about the +other. `make install` and the app both claim `~/.local/bin`; last writer wins. +Doctor now reports that instead of hiding it, which is the honest state — the +merge is a wave, not a cut. + +## Polarize stage, loop 3 — one identity resolution order, 2026-08-18 + +**Chosen axis — every surface answers "which workspace is this process?" in the +same order: the exported `VIBECRAFTED_WORKSPACE_ID` first, then the one +canonical catalog by `canonical_root`, both arbitrated by that catalog.** + +The audit filed the LIVE RUNS filter under "silent dashboard fallback". Measured +fresh this stage, it is not a fallback problem — it is a **twin**. The runtime +stamps a run through `resolve_run_workspace_identity`, whose first step is the +exported `VIBECRAFTED_WORKSPACE_ID`. The dashboard resolved its own identity +through `live_dashboard.resolve_workspace_id`, which read only the catalog by +root and never looked at the environment. Two implementations of one question, +free to disagree — and on this host they did. + +**Measured, live, while this stage ran.** `VIBECRAFTED_WORKSPACE_ID` in the +flight's shell is `01a00d7b-3964-77a8-bc53-2f41e4b4e509`; the catalog roots that +workspace at a **pytest temp directory** that no longer exists. 27 run metas +carry a `workspace_id` the catalog does not root where the run ran. The +dashboard, opened in this repository, computed `bda366e0-…` from the root and +showed **1 of 2** live rows: it hid `pola-260818-192800-88430` — the parent run +of this very polarize flight — and it hid `scaf-260818-202208-26610`, a Mode B +worker in a worktree. After the cut both are visible. + +The worktree row is what settles the axis. A worker's worktree root can never +equal its dispatcher's root, so a root-only reader is **structurally** unable to +see a dispatched worker. `workspace_id` is documented as _not_ derived from +root; the dashboard was the surface that had forgotten it. + +**Rejected alternatives.** + +- _Make the dashboard's root lookup win and treat the stamp as advisory._ Kills + Mode B visibility outright and contradicts the wire contract's own line that + `workspace_id` is not derived from root. +- _Trust any exported id without asking the catalog._ Then a variable left in a + shell renames the workspace. The catalog is the sole durable writer; a bare id + is evidence, not identity. +- _Repair the 27 mis-stamped metas / delete the leaked pytest workspace._ Host + state, not repository truth, and rewriting durable run history to make a + reader agree is the wrong direction. The reader was wrong. +- _Change the writer to fall through on a stale export instead of raising._ A + writer creating durable state should refuse loudly; only the reader must + degrade. The role boundary is now stated in the wire contract rather than left + to each surface. +- _Send it back to marbles as a behaviour change._ Loops 1 and 2 both deferred + it on that reasoning. It is a choice between two coexisting identities, which + is precisely the cut polarize owns. + +**Proof the guards bite.** With the env branch disabled, +`test_dashboard_identity_honours_the_exported_workspace_id` fails +`assert 'ws-rooted-here' == 'ws-exported'` and +`test_worktree_worker_stays_visible_in_the_dispatching_workspace` fails +`assert [] == ['pola-…', 'scaf-…']` — the whole flight hidden, exactly the live +shape. With the catalog corroboration dropped, the unknown-id and buried-id +refusals both fail. All green on restore; no probe residue in 1167 scanned files. + +**What this does not fix.** The leaked pytest workspace and the 27 mis-stamped +metas are still in the operator's control plane. Settlement scoping still counts +membership from the raw stamp (it projects zero on this host today, so nothing +is provably misattributed). Neither is repository truth; both are named for DoU. + +## Hydrate stage — the landed ledger, 2026-08-18 + +Stage 10 of the flight. The DoU audit found the engineering sound and the outward +channel dead, so hydrate did the two things that are repository truth — write down +what landed, and stop the payload from contradicting itself — and left every +outward valve as a named operator button. + +### What landed, per cut + +| Cut | State at hydrate | Landing commits | What still stands between it and `[x]` | +| ---- | ---------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| W0-a | `[?]` | `85cebab5` `25fe62b3` `f08e8076` `48b3d9a9` | Portable payload proven clean from `48b3d9a9` (993 files, 0 leaks). No DMG has been rebuilt through the repaired remap and the new gate. | +| W0-b | `[?]` | `0e26b077` | Help and missing-id `rc=1` verified; the live start → stop → `resume --run-id` → `--last` walk needs an installed build newer than `18dea346`. | +| W0-c | `[ ]` | `8e872b17` `289cff14` `fabf0e21` | One identity resolution order landed with a 5-test contract; GUI acceptance and screenshots need a live vc-frame session. | +| W1-a | `[x]` | `838165d6` | Guard plus Windows-clone smoke; re-earned independently by the audit stage. | +| W1-b | `[x]` | `cd13e1ca` `1f6d36c3` | `--snapshot-donors`, the reaper, and the release-script hardening that followed it. | +| W1-c | `[?]` | `e9f47da1` | Byte-parity guard green and the docs honest; `https://vibecrafted.io/install.ps1` still answers 404 until `feat/saas-portal-merge` is merged and deployed in `vibecrafted-io`. | +| W2-a | `[ ]` deferred | — | `workspace_id` reaches 0 of 556 sites in vc-frame; a host-side `SessionInfo` projection is the agreed shape. Deferral is traced but not yet an operator `accept-dou`. | +| W2-b | `[ ]` deferred | — | The tray already carries both `Open Console` and `Open vc-terminal`; the proposed rename would collide. Same deferral. | +| W3-a | `[x]` | `01e5e18a` `4918c7fb` | 0 breaking / 0 structural / 0 diamond cycles, health 74 to 80; re-earned by the audit stage. | + +Carried under the flight without belonging to a single cut: `b18b1483` and +`bcbfc776` (PR #54 review threads and the formatter settling after them), +`e9348458` (autonomy: non-destructive push classified as a duty, authored by +grok), `df9b6337` and the scaffold records `cf20aa4e` `833f770e` `63e5e8d3` +`69101f2c`, and this stage's own commits. + +### What hydrate changed + +- `plugin.json` declared `2.0.0` and `Apache-2.0` while `VERSION` read `4.1.0` + and `LICENSE` opened with `SPDX-License-Identifier: BUSL-1.1`. That file is in + `ALLOWED_TOP_LEVEL` in both `install.sh` and `scripts/distribution_manifest.py`, + so the contradiction shipped to every installed host and into every + distribution archive, sitting next to the LICENSE it disagreed with. It now + states the version and the SPDX identifier it actually ships with, and carries + the same author, homepage, and repository as the generated marketplace + manifest. Two contract tests in `tests/tui/test_distribution_manifest.py` bind + it to `VERSION` and to `LICENSE`; restoring the pre-cut values turns the first + red with `plugin.json version '2.0.0' != VERSION '4.1.0'`. +- `CHANGELOG.md` carried two bullets under `## Unreleased` against 23 commits of + landed work. The section now describes the 4.2.0 scope in Added / Changed / + Fixed / Security. It deliberately stays `Unreleased`: `v4.1.0` was never + tagged, so dating a `## 4.2.0` heading here would repeat exactly the kind of + claim this flight exists to retire. The release stage promotes it with the tag. + +### Corrections to the DoU audit + +- **P1-8, the docs half, is not true of this repository.** The audit reported the + install page never mentioning the desktop product (`dmg`, `--gui`, `Desktop`, + `notariz` all at zero hits). Measured here, `docs/INSTALL.md` opens with a + channel matrix naming the signed `Vibecrafted.app` DMG and its publication + status, carries a `## macOS — the signed desktop app` section with the + `shasum -a 256 -c` walk, documents `make app` and the desktop update path; + `README.md` and `docs/RELEASE_CHECKLIST.md` both enumerate the DMG assets. The + measurement was of the **deployed site**, which is three months stale and lives + in `vibecrafted-io`. The gap is a deploy, not a docs gap — same button as W1-c. +- **P2-10, the brand half, is declined.** The audit called `"Vetcoders"` a + drift from the canonical `VetCoders`. In this repository `Vetcoders` is the + prevailing form at 272 occurrences against 6, it is hardcoded in + `build_marketplace_bundle.plugin_manifest`, and it matches the `vetcoders` + org slug. A 272-site rename is a branding decision for the operator, not a + hydration cut; hydrate changed the version and the licence, and left the + spelling alone. + +### Not verified by hydrate + +No DMG rebuilt, no payload re-scanned, no tag pushed, no site deployed, no +`make install`, no live vc-frame session, no full pytest roots, and no +`make unified-product-contract-gate` — this stage touched neither +`vibecrafted_core` nor `scripts/`, so the gate's trigger condition did not fire. +The tests that do cover the changed surface were run and are green. + +## Release stage — the gate's second missing tool, 2026-08-18 + +The DoU named the top risk precisely: 4.2.0 could be tagged and fail exactly as +4.0.0 did, because the `runs-on: macos-15` cure (`54a98b23`) has never run +against a real tag. Release went looking for what else that untested path would +hit, and found the next mine on it. + +**The final step of the source gate called a tool its own runner does not have.** +`Confirm publication boundary for both channels` invoked `command rg` — which +forces a lookup of a real `rg` binary on `PATH`. The GitHub `macos-15` image +ships no ripgrep: measured 2026-08-18 against the published image manifest +(`actions/runner-images`, `images/macos/macos-15-Readme.md`), zero occurrences, +alongside zero for shellcheck — which this same workflow independently confirms +by having to `brew install shellcheck` before it can lint. In this repository +`rg` exists only inside our own container images (`Dockerfile:40`, +`vibecrafted-vm/Containerfile:118`), never on the runner. + +Under `set -euo pipefail` that step ends the job. So curing `xcrun` would have +moved the failure four steps later, not removed it: every test green, every +build done, and then the gate dies on a missing binary — the exact shape of the +v4.0.0 death. The step arrived in `ef700e52` (3.7.1) and **has never once +executed**, because every tag since died earlier. No amount of "the last release +worked" could surface it. + +### What landed + +| File | What changed | +| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `.github/workflows/release.yml` | The two publication-boundary assertions moved from `command rg -n` to `grep -nE`. Same patterns, same files, same fail-on-no-match semantics — verified locally to still match on both channels — with no tool that has to be installed first. | +| `tests/tui/test_release_contract.py` | Two new tests. One refuses any `run:` line in the tag gate that calls a binary absent from the runner image and not `brew install`ed, and separately requires the shellcheck install wherever `make check` runs. The other pins the boundary step's two patterns and all four files it covers, so rewriting the matcher cannot quietly shrink what it matches. | +| `scripts/hooks/pre-push` | Semgrep now runs under `env -u PYTHONPATH -u PYTHONHOME`, the isolation `pre-commit` has carried for a while. Hydrate measured this one: inside a worker the scanner dies with `ModuleNotFoundError: No module named 'rpds.rpds'` and the push fails on a broken gate rather than on a finding. | +| `templates/hooks/lib/lint-routing.sh` | The same isolation for the shipped husky template's staged and full semgrep helpers. There the crash is worse-behaved, not better: the WARN-mode step counter reports it as a warning, so the gate stops gating without anyone noticing. | +| `docs/RELEASE_CHECKLIST.md` | Section 5 now carries the gate's real run history and both tool gaps, and says plainly that the next tag is an experiment. | + +The shellcheck half of the tooling test is the quieter finding. +`scripts/check_shell.py` falls back to `bash -n` when shellcheck is missing, so +dropping that `brew install` would not fail the release gate — it would keep +reporting green while silently degrading from a linter to a syntax check. + +### Why the version was not bumped and the CHANGELOG stays `Unreleased` + +Hydrate left `## Unreleased` deliberately and release agrees, for a sharper +reason than symmetry: `v4.1.0` has no tag at all while `VERSION` and +`CHANGELOG.md` both call it released (DoU P0-2). Writing a dated `## 4.2.0` +heading on top of that would add a third unanchored version claim to a flight +whose whole purpose is retiring that class of claim. `VERSION` stays `4.1.0` +until the tag that makes it true exists. + +### Mutation evidence + +Both new assertions were driven red before they were trusted, and both first +drafts passed for the wrong reason — worth recording, because the failure mode +generalises. Restoring `command rg` turns the tooling test red naming both +offending lines; removing `brew install shellcheck` turns it red on the +`make check` allowance. The first draft of that second case stayed **green**: +the test read `brew install` out of the raw workflow text, and the explanatory +comment this same cut added to `release.yml` contains that phrase. A test that +reads a whole file also reads the comments written about it. Installs are now +parsed only out of `run:` lines. + +### Not verified by release + +No tag pushed, no release published, no site deployed, no `make install`, no DMG +built or re-scanned, no live vc-frame session, no `resume --run-id` walk. All +operator buttons, and all still open. The `grep -nE` step is proven to match +locally on macOS; it is **not** proven on a GitHub runner, because proving that +requires the tag push this stage does not make. The runner-image measurement is +a live read of one published manifest, not an execution on the image itself. + +## Explicit non-goals + +Native Windows runtime · a second control plane · new vc-frame features beyond the +rail and the 2026-08-16 chrome asks · rewriting the release scripts · merges into trunk, +deploys, or host installs performed by an agent (branch pushes and PR creation are the supervisor's; canonical list: vc-operator/AUTONOMY.md). diff --git a/docs/RUNBOOK.md b/docs/RUNBOOK.md index e3c694c5..e8002b33 100644 --- a/docs/RUNBOOK.md +++ b/docs/RUNBOOK.md @@ -192,9 +192,10 @@ Two standing rules underneath all of the above: editing, never revert others' changes, commit only your own paths in small packs, `[/]` titles, `Authored-By: `. -2. **Buttons** — `git push`, merge, deploy, and anything outward-facing - belongs to the human operator. Workers stop at the button and write a - one-step handoff instead. +2. **Buttons** — force-push, trunk push, merge, deploy, and anything + outward-facing belongs to the human operator. A non-destructive + `git push` of the current feature branch is not a button. Workers stop + at the remaining buttons and write a one-step handoff instead. ## 8. When lost diff --git a/docs/SUBMISSION_FORMS.md b/docs/SUBMISSION_FORMS.md index 51336c9e..d719fdcf 100644 --- a/docs/SUBMISSION_FORMS.md +++ b/docs/SUBMISSION_FORMS.md @@ -18,6 +18,13 @@ Official submit/help surfaces below were re-checked on 2026-04-11 before this pa ## Recommended launch order +0. awesome-agent-orchestrators + Official submit surface: https://github.com/andyrewlee/awesome-agent-orchestrators (fork, add one line, open a PR) + Why zeroth: it is the only entry on this list that costs nothing, waits on no + editorial queue, and is read by exactly our audience. It is also the one + directory where the namesake is already listed and we are not — see the + directory note below. Do this before any paid or curated submission. + 1. There's An AI For That Official submit surface: https://theresanaiforthat.com/launch/ Why first: highest AI-directory traffic, native support for launches and developer tools. @@ -181,6 +188,34 @@ Every public surface should agree on: ## Directory-specific notes +### awesome-agent-orchestrators + +- Submit URL: https://github.com/andyrewlee/awesome-agent-orchestrators +- Mechanism: a curated GitHub awesome-list, ~1.2k stars. Submission is a fork + plus a pull request adding one line in the list's own `Name - sentence.` + format. No fee, no editorial queue, no launch window. +- Why it is first: its topic tags are our exact audience — `claude-code`, + `codex`, `gemini-cli`, `multi-agent-systems`, `parallel-agents`, + `git-worktree`, `agent-orchestration`. Neighbouring entries (ClawTeam, + CompanyHelm, Contrabass, vibe-kanban, constellagent, parallel-code) are the + tools a stranger currently finds instead of us. +- **Positioning risk, measured 2026-08-18.** A near-identical namesake, + `vibecraft.build`, is already listed here and ranks second for the category + query while Vibecrafted returns nothing. The entry must therefore differentiate + in its first clause, not its last: most neighbours orchestrate agents across + worktrees, which is the crowded half of the category. Ours is the lifecycle and + the settlement ledger — the part that says whether the work actually landed. +- Paste-ready line, in the list's format: + + ```text + Vibecrafted - Lifecycle runtime that drives Claude, Codex, and Gemini through an eleven-stage read/write cadence on one shared checkout, settling every stage against a signed delivery ledger instead of an agent's own report. + ``` + +- Prerequisite: land it **after** a GitHub Release actually carries an + installable artifact. An awesome-list PR pointing at a repository whose latest + release is four minors stale is the one version of this submission that can be + rejected on merit. + ### There's An AI For That - Submit URL: https://theresanaiforthat.com/launch/ diff --git a/docs/adr/0002-unified-operator-ownership.md b/docs/adr/0002-unified-operator-ownership.md index 1cf51d89..25f40a86 100644 --- a/docs/adr/0002-unified-operator-ownership.md +++ b/docs/adr/0002-unified-operator-ownership.md @@ -89,7 +89,7 @@ primary shared memory. Installed artifacts (symlinks, configs, KDL, generated profiles under `~/.local/share/vibecrafted` and `~/.vibecrafted`) must never resolve to a repository checkout -(`/Volumes/vc-workspace`, `~/Libraxis` or any git checkout path). Repository paths may appear only +(`/Volumes/`, `~/Libraxis` or any git checkout path). Repository paths may appear only in development receipts, never in runtime links. The host shell receives one reversible path helper and no product logic. diff --git a/docs/design/agents-workshop/Layout-2.md b/docs/design/agents-workshop/Layout-2.md index 0f6472a3..48262810 100644 --- a/docs/design/agents-workshop/Layout-2.md +++ b/docs/design/agents-workshop/Layout-2.md @@ -33,7 +33,7 @@ SESSIONS 5 ┌ grok · vibecrafted ──────────── · resume-codex │ ┌ ❯ Nowy agent ────────────────────────────────────────────────────────────── [Anuluj] ┐ │ 04 ◉ vc-release │ │ ▸ agent [agy] [claude] [codex] «grok» [junie] │ │ · Start here │ │ rytual [init] «resume» [operator] [partner] │ │ - ◉ Agents │ │ sciezka /Volumes/vc-workspace/vetcoders/vibecrafted-suite/vibecrafted │ │ + ◉ Agents │ │ sciezka /srv/vetcoders/vibecrafted │ │ · shell │ │ Enter = interaktywny panel na tym tabie. Nie mux. Nie headless. │ │ · voc │ └─ ↑/↓ wiersz · ←/→ chip · spacja · enter · esc ───────────────────────────────┘ │ 05 ○ vibecrafted-vc_│ │ diff --git a/docs/design/agents-workshop/Layout-5.md b/docs/design/agents-workshop/Layout-5.md index ff6d3164..ed125e34 100644 --- a/docs/design/agents-workshop/Layout-5.md +++ b/docs/design/agents-workshop/Layout-5.md @@ -33,7 +33,7 @@ SESSIONS 5 ┌ grok · vibecrafted ──────────── · resume-codex │ ┌ ❯ Nowy dispatch ─────────────────────────────────────────────────────────── [Anuluj] ┐ │ 04 ◉ vc-release │ │ ▸ agent [agy] [claude] [codex] «grok» [junie] │ │ · Start here │ │ rytual [init] «resume» [operator] [partner] │ │ - ◉ Agents │ │ sciezka /Volumes/vc-workspace/vetcoders/vibecrafted-suite/vibecrafted │ │ + ◉ Agents │ │ sciezka /srv/vetcoders/vibecrafted │ │ · shell │ │ Enter = HEADLESS worker. Bez TTY. Widać go na serwerze / w voc. │ │ · voc │ └─ ten sam chassis co Nowy agent · inne narodziny ─────────────────────────────────────┘ │ 05 ○ vibecrafted-vc_│ │ diff --git a/docs/design/agents-workshop/_render.py b/docs/design/agents-workshop/_render.py index 55361f3a..eba5600c 100755 --- a/docs/design/agents-workshop/_render.py +++ b/docs/design/agents-workshop/_render.py @@ -225,7 +225,7 @@ def chips(items: list[str], selected: str) -> str: def mark(name: str) -> str: return "▸" if focus == name else " " - path = "/Volumes/vc-workspace/vetcoders/vibecrafted-suite/vibecrafted" + path = "/srv/vetcoders/vibecrafted" if focus == "path": path += "█" rows_inner = [ @@ -254,7 +254,7 @@ def box_new_dispatch() -> list[str]: rows_inner = [ " ▸ agent [agy] [claude] [codex] «grok» [junie]", " rytual [init] «resume» [operator] [partner]", - " sciezka /Volumes/vc-workspace/vetcoders/vibecrafted-suite/vibecrafted", + " sciezka /srv/vetcoders/vibecrafted ", " Enter = HEADLESS worker. Bez TTY. Widać go na serwerze / w voc.", ] title = "┌ ❯ Nowy dispatch " diff --git a/docs/design/agents-workshop/preview.html b/docs/design/agents-workshop/preview.html index 0717740d..6fbb93f1 100644 --- a/docs/design/agents-workshop/preview.html +++ b/docs/design/agents-workshop/preview.html @@ -82,7 +82,7 @@ · resume-codex │ ┌ ❯ Nowy agent ────────────────────────────────────────────────────────────── [Anuluj] ┐ │ 04 ◉ vc-release │ │ ▸ agent [agy] [claude] [codex] «grok» [junie] │ │ · Start here │ │ rytual [init] «resume» [operator] [partner] │ │ - ◉ Agents │ │ sciezka /Volumes/vc-workspace/vetcoders/vibecrafted-suite/vibecrafted │ │ + ◉ Agents │ │ sciezka /srv/vetcoders/vibecrafted │ │ · shell │ │ Enter = interaktywny panel na tym tabie. Nie mux. Nie headless. │ │ · voc │ └─ ↑/↓ wiersz · ←/→ chip · spacja · enter · esc ───────────────────────────────┘ │ 05 ○ vibecrafted-vc_│ │ @@ -187,7 +187,7 @@ · resume-codex │ ┌ ❯ Nowy dispatch ─────────────────────────────────────────────────────────── [Anuluj] ┐ │ 04 ◉ vc-release │ │ ▸ agent [agy] [claude] [codex] «grok» [junie] │ │ · Start here │ │ rytual [init] «resume» [operator] [partner] │ │ - ◉ Agents │ │ sciezka /Volumes/vc-workspace/vetcoders/vibecrafted-suite/vibecrafted │ │ + ◉ Agents │ │ sciezka /srv/vetcoders/vibecrafted │ │ · shell │ │ Enter = HEADLESS worker. Bez TTY. Widać go na serwerze / w voc. │ │ · voc │ └─ ten sam chassis co Nowy agent · inne narodziny ─────────────────────────────────────┘ │ 05 ○ vibecrafted-vc_│ │ diff --git a/docs/install.sh b/docs/install.sh index e5305ae2..541b1a94 100755 --- a/docs/install.sh +++ b/docs/install.sh @@ -1,5 +1,13 @@ #!/bin/sh set -eu +# Doc-relative shim for the canonical installer one directory up. +# +# `bash` is explicit on purpose. `scripts/build-portable-release.sh` states the +# contract: "The entrypoint is `bash install.sh`, not `./install.sh`: the packer +# canonicalises modes and the repository file carries no executable bit, so do +# not test for one." A bare `exec "$script_dir/../install.sh"` inherits that +# missing bit and dies with 126 on every fresh clone — which is precisely the +# surface a Windows user meets first. script_dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd) -exec "$script_dir/../install.sh" "$@" +exec bash "$script_dir/../install.sh" "$@" diff --git a/docs/public/_CONTRACT.md b/docs/public/_CONTRACT.md index 07043fac..5d2d477d 100644 --- a/docs/public/_CONTRACT.md +++ b/docs/public/_CONTRACT.md @@ -44,7 +44,7 @@ MUST be unique across the whole tree, lowercase, dash-separated. Never emit: -- Absolute private paths: `/Users/`, `/Volumes/vc-workspace`, `~/Libraxis`. +- Absolute private paths: `/Users/`, `/Volumes/`, `~/Libraxis`. Use `~/.vibecrafted`, `~/.local/share/vibecrafted`, `~/projects/my-app`. - Hostnames/IPs of private infra: `dragon`, Tailscale `100.x.x.x`, `localhost:3025` (the canonical local server example is `http://127.0.0.1:3024`). diff --git a/docs/public/cli/commands.md b/docs/public/cli/commands.md index 64a8ebe8..0388e310 100644 --- a/docs/public/cli/commands.md +++ b/docs/public/cli/commands.md @@ -38,6 +38,19 @@ The interactive first context handoff: loads repository context (history, structural perception, verification) and opens an oriented agent session. Run it once per repository session before dispatching work. +**Resume rides along.** Init also computes this checkout's resume payload and +carries it into the session — you never have to remember to look. If any run +here settled `n` (needs attention), init names it, says who owns it, and prints +the exact command that continues it. Runs the Guardian already owns are +reported without a command, because each holds a single automatic attempt that a +hand resume would burn. A checkout with nothing unfinished adds nothing to the +prompt; an unreadable ledger says so rather than implying "clean". + +The same payload is attached to the init step of **every** pipeline launch +(`vibecrafted `), so a worker opens with unfinished work already +in view. Full inventory on demand: +`vibecrafted settlements list --bucket n --revalidatable`. + ## status ```bash diff --git a/docs/public/dispatch/dispatch-overview.md b/docs/public/dispatch/dispatch-overview.md index 3c60f84f..57bf2b8b 100644 --- a/docs/public/dispatch/dispatch-overview.md +++ b/docs/public/dispatch/dispatch-overview.md @@ -35,7 +35,7 @@ vibecrafted dispatch plan.dispatch.toml --resume Run `--doctor` before every real launch: it parses the plan, checks the schema, and enforces the policy rules (for example, READ cuts must declare a `mutation` policy, and verifier commands must not contain hard-stop commands -like `git push`). `--dry-run` then shows you the exact prompt each worker +like `git push --force` or `git push origin main`). `--dry-run` then shows you the exact prompt each worker would receive — placeholders rendered, briefs inlined, baton attached. ## What the supervisor does per cut diff --git a/docs/public/dispatch/dispatch-schema.md b/docs/public/dispatch/dispatch-schema.md index 4b2376f0..b298928e 100644 --- a/docs/public/dispatch/dispatch-schema.md +++ b/docs/public/dispatch/dispatch-schema.md @@ -182,8 +182,10 @@ expect = { contains = "passed", not_contains = "FAILED", matches = "[0-9]+ passe | `exit_code` | integer | Command exit code equals the value | `run` is required and must not contain hard-stop commands — the parser -refuses `--no-verify`, `git reset --hard`, `git clean`, `git push`, `rm -rf /`, -and release invocations. Outward-facing actions belong to the operator, not +refuses `--no-verify`, `git reset --hard`, `git clean`, destructive remote +push (force / trunk / delete / tags), `rm -rf /`, and release invocations. +A non-destructive `git push origin HEAD` of a feature branch is allowed. +Outward-facing merge, deploy, and publish still belong to the operator, not to a verifier shell. ## Placeholder rendering diff --git a/docs/runtime/ACP.md b/docs/runtime/ACP.md index ee1deeb8..ba1bbe19 100644 --- a/docs/runtime/ACP.md +++ b/docs/runtime/ACP.md @@ -114,10 +114,12 @@ Unknown slash commands fail closed with `stopReason: refusal`. ## Permissions and cancellation -The MVP hard-stop classifier is unchanged. Push, merge, publish, and deploy -intents require an ACP permission request and only explicit `allow_once` -continues. Denial, timeout, and missing decisions fail closed. Accepted -overrides are written through the existing audit event path. +The hard-stop classifier treats force-push, trunk push, merge, publish, and +deploy as operator buttons. A non-destructive feature-branch `git push` +does not request permission. Remaining hard-stop intents require an ACP +permission request and only explicit `allow_once` continues. Denial, +timeout, and missing decisions fail closed. Accepted overrides are written +through the existing audit event path. `session/cancel` signals the session and stops the active child run through the canonical workflow stop API. diff --git a/docs/runtime/AGENT_OPS.md b/docs/runtime/AGENT_OPS.md index 4d3bec55..20849be3 100644 --- a/docs/runtime/AGENT_OPS.md +++ b/docs/runtime/AGENT_OPS.md @@ -254,11 +254,16 @@ seat unless `VIBECRAFTED_WORKER_SESSION` is set. 1. `VIBECRAFTED_WORKER_SESSION` if set — explicit override wins (any name, including one that matches the operator seat). -2. Else `"-workers"` (SPAWN_ROOT / VIBECRAFTED_ROOT / cwd) - — the per-project worker host (e.g. `vibecrafted-workers`, - `vc-frame-workers`), **always** suffixed. The suffix is dash-joined so the - name stays a single token across argv, shell quoting and session-listing - matches (2026-08-17; it used to be space-joined). Bare `basename(--root)` is the +2. Else the workspace-bound worker host `"{label}-{workspace_short8}-w"` + resolved through the workspace catalog (`worker_host_session_name` in + `workspace_catalog.py`, `WORKER_HOST_SUFFIX = "-w"`); emergency + fallback `"-w"` only when the catalog cannot open + (SPAWN_ROOT / VIBECRAFTED_ROOT / cwd). **Always** suffixed. The suffix is a + short dash-joined token so the name stays one argv element across shell + quoting and session-listing matches AND fits the macOS `sockaddr_un` + budget (104 bytes; the older `{label}-{short} workers` form overflowed it — + `legacy_worker_host_session_name()` keeps that token for WES attach only). + See `docs/runtime/WORKSPACE_IDENTITY.md`. Bare `basename(--root)` is the operator's own interactive card in the rail and is never a worker target, so the dispatcher seat plays no part in host resolution. diff --git a/docs/runtime/CONTRACT.md b/docs/runtime/CONTRACT.md index 5cc383ff..f825c618 100644 --- a/docs/runtime/CONTRACT.md +++ b/docs/runtime/CONTRACT.md @@ -245,7 +245,7 @@ Living tree note: - Run required checks. If something is blocked, report the exact blocker and run the closest safe equivalent. - Coordination mode: - You do not need to inspect other agents' plans unless this plan explicitly tells you to. -- Commit is an obligation, not a checkpoint option: ONE commit per round (marbles — one round = one commit), well-formed per the commit-msg hook, on the current branch. Do NOT leave delivered work uncommitted. Do NOT push — push is the operator's button. When the mission spans multiple rounds/units, multi-commit per dispatch is expected. +- Commit is an obligation, not a checkpoint option: ONE commit per round (marbles — one round = one commit), well-formed per the commit-msg hook, on the current branch. Do NOT leave delivered work uncommitted. Non-destructive remote push of the current feature branch (`git push -u origin HEAD`, not force, not trunk) is a duty after that commit. Force-push, trunk push, merge, and deploy stay operator buttons. When the mission spans multiple rounds/units, multi-commit per dispatch is expected. - Pre-handoff baseline is mandatory before handing this plan to another agent: branch, HEAD, git status, changed files, verification result, known failures, unverified surfaces, and exact next instruction/report path. diff --git a/docs/runtime/DELIVERY_PROOF_KERNEL_v1.md b/docs/runtime/DELIVERY_PROOF_KERNEL_v1.md index 9cb1c2c5..d17957ec 100644 --- a/docs/runtime/DELIVERY_PROOF_KERNEL_v1.md +++ b/docs/runtime/DELIVERY_PROOF_KERNEL_v1.md @@ -33,7 +33,7 @@ Rozpoznanie wykonano w checkoutcie: ```text repo: vetcoders/vibecrafted -path: /Volumes/vc-workspace/vetcoders/vibecrafted +path: /srv/checkout/vibecrafted branch: feat/reduce-wrong-assumptions observed HEAD: 15a35e8dc4825e8e1b6869f57212681913da6e8f upstream: origin/feat/reduce-wrong-assumptions @@ -407,7 +407,7 @@ execution: selection_source: omitted resolution_policy: provider-default repo: Loctree/aicx - root: /Volumes/vc-workspace/Loctree/aicx + root: /srv/checkout/aicx branch: fix/example expected_head: upstream_ref: origin/fix/example @@ -484,7 +484,7 @@ identity: parent_pid: resolved_executable: /absolute/path/to/codex argv_sha256: sha256:... - cwd: /Volumes/vc-workspace/vetcoders/codescribe + cwd: /srv/checkout/codescribe ``` `orchestration_session` i `provider.requested_session` mogą przypadkiem mieć ten @@ -551,7 +551,7 @@ proof: producer_id: Loctree/aicx public_surface: aicx extract codex --file --emit session-record argv: [aicx, extract, codex, --file, , --emit, session-record] - cwd: /Volumes/vc-workspace/Loctree/aicx + cwd: /srv/checkout/aicx expected_exit: 0 output: /subject/session_record.json diff --git a/docs/runtime/INSTALLED_RUNTIME_CAPSULE.md b/docs/runtime/INSTALLED_RUNTIME_CAPSULE.md index 60bfd016..7aa5f283 100644 --- a/docs/runtime/INSTALLED_RUNTIME_CAPSULE.md +++ b/docs/runtime/INSTALLED_RUNTIME_CAPSULE.md @@ -2,16 +2,32 @@ The repository is a workshop. The installed generation is the runtime. -`~/.local/bin/vibecrafted` and its `vc-*` aliases enter only the command deck -under: +`~/.local/bin/vibecrafted` and its `vc-*` aliases enter an **installed runtime +root**, never a repository checkout. Ownership is decided by where the launcher +lands, not by the shape of a directory name: ```text -~/.local/share/vibecrafted/tools/vibecrafted-current/ +~/.local/share/vibecrafted/ <- $VIBECRAFTED_RUNTIME_HOME, the boundary + tools/vibecrafted-current/ <- `make install` channel (capsule) + releases// <- Vibecrafted.app channel ``` -`vibecrafted-current` is an atomic symlink to one immutable -`vibecrafted-generation-*` directory. The installer refuses to use a uv tool -shim or repository checkout as the public launcher target. +Two publication channels are live and both are first-class installed owners: + +- **`make install`** stages `tools/vibecrafted-generation-*` and flips the + atomic `vibecrafted-current` symlink onto it. +- **`Vibecrafted.app`** publishes `releases//`, records it in + `active.json` (`vibecrafted.active-runtime.v1`), and writes the `~/.local/bin` + launchers as env preambles ending in `exec '/bin/' "$@"`. + +A launcher qualifies when it resolves inside the runtime home, or when the +executable it `exec`s does — and that target really exists and is executable. +Neither a uv tool shim posing as a deck nor a repository checkout can qualify. + +Both channels writing `~/.local/bin` means both can be installed at once. When +they disagree, `vibecrafted doctor` says so: the version it reports is checked +against the `VERSION` of the runtime root the PATH launcher actually enters, and +a mismatch is a `warn`, not a silent `ok` on a generation nothing runs. ## Generation manifest @@ -59,8 +75,11 @@ Publication fails when: `vibecrafted doctor` repeats the audit against the installed artifact. It also fails when the public launcher resolves outside -`~/.local/share/vibecrafted`, when the manifest is invalid, or when a -manifest-bound file has drifted. +`~/.local/share/vibecrafted` — including through the `exec` target of a +generated wrapper — when the manifest is invalid, or when a manifest-bound file +has drifted. The generation manifest and its digest closure are a property of +the `make install` capsule channel; the app channel is bounded by the app's own +pre-publication executable audit, not by `runtime-manifest.json`. Generations created before this closed verifier inventory are intentionally rejected and must be reinstalled. W4 binds this manifest into the signed release @@ -121,7 +140,7 @@ that is a symlink or is owned by another user is refused, in which case vc-frame keeps its own default rather than accepting a hostile socket home. Worker host session names are single-token for the same reason -(`{label}-{workspace_short}-workers`, see `docs/runtime/WORKSPACE_IDENTITY.md`). +(`{label}-{workspace_short}-w`, see `docs/runtime/WORKSPACE_IDENTITY.md`). ### Install failures are visible diff --git a/docs/runtime/WORKSPACE_IDENTITY.md b/docs/runtime/WORKSPACE_IDENTITY.md index b48693b0..4b2dbc66 100644 --- a/docs/runtime/WORKSPACE_IDENTITY.md +++ b/docs/runtime/WORKSPACE_IDENTITY.md @@ -14,14 +14,62 @@ _Status: control-plane authority · Cut A landed · vc-frame Cut B consumer cont | Field | Kind | Meaning | | ------------------------------ | ------ | ------------------------------------------------------------------------------------------------------------------ | -| `workspace_id` | UUIDv7 | Durable logical Vibecrafted Workspace. **Not** derived from root. Same root may host multiple parallel workspaces. | -| `vibecrafted_session_id` | UUIDv7 | Durable logical session belonging to `workspace_id`. | -| `workspace_instance_id` | UUIDv7 | Concrete runtime materialization of `workspace_id`, bound to an exact `build_id`. | +| `workspace_id` | UUID | Durable logical Vibecrafted Workspace. **Not** derived from root. Same root may host multiple parallel workspaces. | +| `vibecrafted_session_id` | UUID | Durable logical session belonging to `workspace_id`. | +| `workspace_instance_id` | UUID | Concrete runtime materialization of `workspace_id`, bound to an exact `build_id`. | | `run_id` | string | Concrete execution belonging to `workspace_id` + `vibecrafted_session_id`. | | `agent_session_id` | string | Provider-native session (subordinate). | | `runtime_session_id` | string | Runtime tracking id (subordinate). | | vc-frame / Zellij session name | string | Physical pane host (subordinate). Never overload `session_id`. | +### Accepted id rule — mint v7, accept any UUID + +New ids are minted as UUIDv7 (`new_uuid7`) so that lexical order is +chronological. **Acceptance is version-agnostic:** `require_uuid` admits any +canonical RFC 4122 UUID, and every reader — control plane, server/API, +vc-frame — MUST do the same. + +No consumer may validate, filter, or sort on the UUID _version_. The live +catalog is mixed by construction: workspaces minted before v7 became the +default carry v4 ids, including Vibecrafted's own repository +(`bda366e0-519f-45f1-8d10-449058491a94`). A v7-only rail would drop them. + +Ordering that needs chronology reads `created_at`, never the id bits. + +### Identity resolution order — one order, writers and readers alike + +"Which workspace is this process?" has exactly ONE answer path. Every surface +that asks it — the run stamper, the LIVE RUNS dashboard, server projections, +vc-frame — resolves in this order: + +1. **`VIBECRAFTED_WORKSPACE_ID`** — the identity the runtime already resolved + and exported into this process tree. It is honoured only when it names an + **active** workspace in the catalog. +2. **The one canonical catalog by `canonical_root`** — the unique active + workspace rooted here. When several are rooted here, the selected one wins; + an ambiguous root without a selection is an error, never a guess. +3. **Create** (writers only, `create_if_missing`) — readers stop at `None`. + +Two rules follow, and both are load-bearing: + +- **A `workspace_id` the catalog does not hold active is stale evidence, not + identity.** No surface trusts a bare id. What follows the refusal is decided + by role, and only by role: a **writer** about to create durable state + refuses loudly (`WorkspaceNotFound`, or "workspace … is buried; recover it + before launching runs") because a stale export must never silently re-home a + run; a **reader** refuses quietly and continues down the order, because a + dashboard must never go blank over an environment variable. +- **A reader that skips step 1 disagrees with every run its own shell + launched.** `workspace_id` is explicitly _not_ derived from root, so a + root-only reader cannot see a Mode B worker at all: a worktree worker's root + never equals its dispatcher's. Root identity is the fallback for runs that + carry no stamp, not a competing authority. + +Membership questions with stricter semantics stay stricter — settlement +scoping still excludes unstamped runs rather than guessing them (see +_Settlement F/X/N scoping_) — but no surface may believe a stamp the catalog +refuses. + ## build_id Schema: `vibecrafted.build-id.v1` @@ -126,6 +174,16 @@ Env exports for workers: | `VIBECRAFTED_WORKSPACE_INSTANCE_ID` | `workspace_instance_id` | | `VIBECRAFTED_BUILD_ID` | `build_id.rendered` | +These are exports **and** inputs: a child process inherits them and re-enters +the resolution order above at step 1. That is how a Mode B worker in a +worktree stays inside the workspace that dispatched it — and why every reader +must honour step 1 rather than resolve by root on its own. + +Consequence to know: because writers refuse loudly, an inherited export whose +workspace is later buried or dropped from the catalog turns every subsequent +launch in that shell into a hard error. That is deliberate fail-closed +behaviour, not a fallback; clear the variable or recover the workspace. + ## Run metadata fields (new, additive) New runs stamp into `meta.json` / control-plane snapshots: diff --git a/plugin.json b/plugin.json index 491ffe38..bf729325 100644 --- a/plugin.json +++ b/plugin.json @@ -1,9 +1,12 @@ { "name": "vibecrafted", - "version": "2.0.0", - "description": "Vibecrafted release engine and workflows.", + "version": "4.1.0", + "description": "Release engine for AI-built software. Structural mapping, convergence loops, install truth, and launch-ready packaging for AI-generated repos.", "author": { - "name": "Vetcoders" + "name": "Vetcoders", + "email": "hello@vetcoders.io" }, - "license": "Apache-2.0" + "homepage": "https://vibecrafted.io/", + "repository": "https://github.com/vetcoders/vibecrafted", + "license": "BUSL-1.1" } diff --git a/scripts/build-portable-release.sh b/scripts/build-portable-release.sh index ea75fac0..143f1b92 100755 --- a/scripts/build-portable-release.sh +++ b/scripts/build-portable-release.sh @@ -91,6 +91,17 @@ bash -n "$VERIFY_DIR/$ARCHIVE_ROOT_NAME/install.sh" bash "$VERIFY_DIR/$ARCHIVE_ROOT_NAME/install.sh" --help >/dev/null \ || die "packed install.sh cannot print its own usage" +# This channel ships a projection of a commit rather than compiled bytes, so it +# has always been the cleaner of the two — measured on the 4.1.0 tarball, the +# only `/Users|/home` matches were documentation placeholders. That is a fact +# about one build, not a property of the channel: the packer's allowlist can +# grow, and a generated file can arrive carrying an absolute path. Ask the +# extracted tree the same question the DMG channel is asked. +# shellcheck source=/dev/null +. "$REPO_ROOT/scripts/lib/payload-hygiene.sh" +log "asserting the packed payload does not name the build host" +assert_payload_is_anonymous "$VERIFY_DIR/$ARCHIVE_ROOT_NAME" "$PORTABLE_NAME" + ( cd "$DIST_DIR" if command -v shasum >/dev/null 2>&1; then diff --git a/scripts/build-vibecrafted-release.sh b/scripts/build-vibecrafted-release.sh index 0d7d1a79..a9fdc359 100755 --- a/scripts/build-vibecrafted-release.sh +++ b/scripts/build-vibecrafted-release.sh @@ -1,9 +1,54 @@ #!/usr/bin/env bash set -euo pipefail +log() { printf '\n==> %s\n' "$*"; } +die() { printf 'FATAL: %s\n' "$*" >&2; exit 1; } +require() { command -v "$1" >/dev/null 2>&1 || die "$1 is required"; } + +# A --remap-path-prefix whose prefix still contains `..` never matches the path +# the compiler actually sees, because the match is textual. The donor roots used +# to be plain concatenations ("$REPO_ROOT/../vc-terminal"), so both donor remaps +# silently missed every file: measured on the shipped 4.1.0 payload +# (Vibecrafted_4.1.0-20260817-237d2814.dmg, roadmap 4.2.0 cut W0-a), the strings +# `/usr/src/vc-frame` and `/usr/src/vc-terminal` are ABSENT from every binary +# while `/Volumes/<...>/vc-frame` and `/Volumes/<...>/vc-terminal` are present in +# Contents/Helpers/vc-frame, Contents/MacOS/Vibecrafted, Contents/MacOS/voc and +# the bundled alacritty. Resolve the donor roots; never concatenate them. +canonical_dir() { + local target="$1" + (cd "$target" >/dev/null 2>&1 && pwd) || die "missing donor directory: $target" +} + REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -TERMINAL_REPO="${VIBECRAFTED_TERMINAL_REPO:-$REPO_ROOT/../vc-terminal}" -FRAME_REPO="${VIBECRAFTED_FRAME_REPO:-$REPO_ROOT/../vc-frame}" + +MODE="release" +SNAPSHOT_DONORS=0 +for argument in "$@"; do + case "$argument" in + --app-only) MODE="app" ;; + --no-notarize) MODE="dmg" ;; + --notarize-only) MODE="notarize" ;; + --snapshot-donors) SNAPSHOT_DONORS=1 ;; + *) + echo "usage: $0 [--app-only|--no-notarize|--notarize-only] [--snapshot-donors]" >&2 + exit 2 + ;; + esac +done + +# The donor is where the source lives; the repo is what we compile. They differ +# only under --snapshot-donors, where the repo becomes a detached worktree at the +# donor HEAD so a dirty Living Tree donor can still produce an honest receipt. +TERMINAL_DONOR="$(canonical_dir "${VIBECRAFTED_TERMINAL_REPO:-$REPO_ROOT/../vc-terminal}")" +FRAME_DONOR="$(canonical_dir "${VIBECRAFTED_FRAME_REPO:-$REPO_ROOT/../vc-frame}")" +DONOR_SNAPSHOT_ROOT="$REPO_ROOT/build/unified-release/donor-snapshots" +if (( SNAPSHOT_DONORS )); then + TERMINAL_REPO="$DONOR_SNAPSHOT_ROOT/vc-terminal" + FRAME_REPO="$DONOR_SNAPSHOT_ROOT/vc-frame" +else + TERMINAL_REPO="$TERMINAL_DONOR" + FRAME_REPO="$FRAME_DONOR" +fi ICON_SOURCE="${VIBECRAFTED_ICON_SOURCE:-$TERMINAL_REPO/assets/icon/vc-terminal-icon.png}" ICON_REFERENCE="${VIBECRAFTED_ICON_REFERENCE:-$TERMINAL_REPO/assets/icon/terminal.png}" DIST_DIR="${VIBECRAFTED_RELEASE_DIR:-$REPO_ROOT/dist}" @@ -29,27 +74,62 @@ CERT_PASSWORD_FILE="$KEYS/cert_password.txt" SIGNING_KEY="$KEYS/vibecrafted-signing.key" NOTARY_ENV="$KEYS/.notary.env" BUILD_NUMBER="${BUILD_NUMBER:-$(date -u +%Y%m%d%H%M%S)}" -MODE="release" SIGNING_IDENTITY="" TEMP_KEYCHAIN_PATH="" SIGNING_KEYCHAIN_LABEL="vibecrafted-signing-$$" CODESIGN_KEYCHAIN_ARGS=() export MACOSX_DEPLOYMENT_TARGET=14.0 # Release payloads must not remember the operator account, Cargo registry, or -# living checkout locations through Rust panic/debug metadata. -export RUSTFLAGS="--remap-path-prefix=$REPO_ROOT=/usr/src/vibecrafted --remap-path-prefix=$TERMINAL_REPO=/usr/src/vc-terminal --remap-path-prefix=$FRAME_REPO=/usr/src/vc-frame --remap-path-prefix=$HOME=/usr/src/operator-home" - -case "${1:-}" in - --app-only) MODE="app" ;; - --no-notarize) MODE="dmg" ;; - --notarize-only) MODE="notarize" ;; - "") ;; - *) echo "usage: $0 [--app-only|--no-notarize|--notarize-only]" >&2; exit 2 ;; -esac - -log() { printf '\n==> %s\n' "$*"; } -die() { printf 'FATAL: %s\n' "$*" >&2; exit 1; } -require() { command -v "$1" >/dev/null 2>&1 || die "$1 is required"; } +# living checkout locations through compiler metadata. +# +# ORDER IS LOAD-BEARING. rustc applies the LAST matching --remap-path-prefix. +# MEASURED 2026-08-18: +# rustc --remap-path-prefix=$T=/usr/src/OUTER \ +# --remap-path-prefix=$T/inner=/usr/src/INNER $T/inner/main.rs +# reports /usr/src/INNER/main.rs, and swapping the two arguments reports +# /usr/src/OUTER/inner/main.rs. So the list runs BROADEST FIRST: +# * $HOME must precede the checkout and the donors. It used to be last, which +# is correct only by accident on this host — every repository happens to +# live on /Volumes. On any operator whose checkout sits under $HOME, the +# trailing $HOME entry would win and every specific root would be dead. +# * the donor snapshots live under $REPO_ROOT/build/..., so they must follow +# $REPO_ROOT or they would be rewritten as /usr/src/vibecrafted/build/... +# +# The snapshot pair is emitted only when it exists. Without --snapshot-donors +# TERMINAL_REPO IS TERMINAL_DONOR, and the duplicate pair merely pinned its own +# redundancy into the contract test. +PATH_REMAPS=( + "$HOME=/usr/src/operator-home" + "$REPO_ROOT=/usr/src/vibecrafted" + "$TERMINAL_DONOR=/usr/src/vc-terminal" + "$FRAME_DONOR=/usr/src/vc-frame" +) +if (( SNAPSHOT_DONORS )); then + PATH_REMAPS+=( + "$TERMINAL_REPO=/usr/src/vc-terminal" + "$FRAME_REPO=/usr/src/vc-frame" + ) +fi +RUSTFLAGS="" +FILE_PREFIX_MAP="" +SWIFT_PREFIX_MAP="" +for mapping in "${PATH_REMAPS[@]}"; do + RUSTFLAGS+="${RUSTFLAGS:+ }--remap-path-prefix=$mapping" + FILE_PREFIX_MAP+="${FILE_PREFIX_MAP:+ }-ffile-prefix-map=$mapping" + SWIFT_PREFIX_MAP+="${SWIFT_PREFIX_MAP:+ }-debug-prefix-map $mapping" +done +export RUSTFLAGS +# cc-rs compiles the C half of crates such as `ring`, and rustc's remap never +# sees those translation units. MEASURED on the shipped 4.1.0 DMG: +# Contents/MacOS/Vibecrafted carried 21 occurrences of +# $HOME/.cargo/registry/src/.../ring-0.17.14/crypto/... clang's +# -ffile-prefix-map is the same instrument on the C side. +export CFLAGS="${CFLAGS:+$CFLAGS }$FILE_PREFIX_MAP" +export CXXFLAGS="${CXXFLAGS:+$CXXFLAGS }$FILE_PREFIX_MAP" +# The Swift host is built by xcodebuild, which reads none of the above. Same +# payload, 51 occurrences of the checkout root from Swift source locations and +# DerivedData intermediates. Passed to xcodebuild as build settings below. +export SWIFT_PREFIX_MAP # The ephemeral signing keychain is owned by scripts/lib/keychain-session.sh, # which arms its own EXIT/INT/TERM/HUP traps and chains onto whatever this @@ -61,9 +141,24 @@ require() { command -v "$1" >/dev/null 2>&1 || die "$1 is required"; } # host-wide side effect for the entire duration of the release. # shellcheck source=/dev/null . "$REPO_ROOT/scripts/lib/keychain-session.sh" +# shellcheck source=/dev/null +. "$REPO_ROOT/scripts/lib/donor-snapshot.sh" +# shellcheck source=/dev/null +. "$REPO_ROOT/scripts/lib/payload-hygiene.sh" cleanup() { + # Host-wide resources first. The keychain session mutates state that outlives + # this process and affects every application on the machine; the donor + # snapshots are directories under this repo's own build/ and a stale one is + # merely untidy. Reaping first meant a hung `git worktree remove` — an index + # lock on a busy donor is enough — would strand the keychain instead. + # + # In practice keychain_session_begin also arms its own EXIT handler which + # chains ahead of this one, so the keychain is usually already released by the + # time we arrive. That path does not exist when no signing certificate was + # present, which is exactly when this ordering is the only ordering. keychain_session_end "$SIGNING_KEYCHAIN_LABEL" || true + donor_snapshot_reap || true } trap cleanup EXIT INT TERM HUP @@ -119,10 +214,82 @@ LC_ALL=C file -b "$SPOT_MONO_FONT" \ prepare_signing_identity git_sha() { git -C "$1" rev-parse HEAD; } + +# require_clean_repo