Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ All notable changes to the claude-plugins project will be documented in this fil

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Entries are listed newest-first; each plugin section is treated as released when merged to `main`.

### vibe v1.0.80

Targeted validation of the final committed diff is now a required gate before a vibe pull request opens.

#### Added
- Handoff's pull request evidence gate requires a read-only inventory of the final committed diff, with exact files, cases and commands from the repository's own lane tooling. Every inventoried target needs a passing result and SHA, and the validated artifact is posted as a pull request comment.
- E2E selection records a capability probe and chooses containerized, native headless or configured remote execution. Native runs use CI-equivalent quarantine and sandbox settings with retries off, respect the machine-local native-run prohibition and leave host-incompatible visual baselines explicitly to CI.
- The pull request handoff planner dispatches five nonauthoring prep lanes while the persistent writer is running: incremental test inventory, E2E runner warming, stable test lanes, local QA startup and draft PR evidence.

#### Changed
- After an isolated failure is fixed, only that case and newly reached targets rerun. Missing E2E capability is reported with the specs and automatic CI lanes instead of being skipped.
- A carried test result names its source SHA and covered-file hashes. The final gate rejects it when any covered byte changed, preserving the sole-writer rule without turning orchestrator wait time into delivery latency.
- This procedure kept the commits from symphony-alpha pull requests #9202, #9208, #9209 and #9210 green in every post-merge batch lane on 2026-10-22 and 2026-10-23; the batch reds were a pre-existing unrelated flaky test. It did not catch a hidden artifact slug because no test asserted it, so targeted tests do not replace visual QA.
- Instruction text is about 3 words shorter than in 1.0.79.

### vibe v1.0.79

Worker launches, shared ownership and operational evidence now have runtime checks instead of relying on prose alone.
Expand Down
2 changes: 1 addition & 1 deletion plugins/vibe/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "vibe",
"description": "Vibe sessions, handoff, seed refresh, and ClosedLoop ticket workflows",
"version": "1.0.79",
"version": "1.0.80",
"dependencies": ["closedloop-core"],
"author": {"name": "ClosedLoop", "email": "support@closedloop.ai"}
}
2 changes: 1 addition & 1 deletion plugins/vibe/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "vibe",
"version": "1.0.79",
"version": "1.0.80",
"description": "Vibe sessions, handoff, seed refresh, and ClosedLoop ticket workflows",
"author": {"name": "ClosedLoop", "email": "support@closedloop.ai"},
"homepage": "https://github.com/closedloop-ai/claude-plugins",
Expand Down
2 changes: 1 addition & 1 deletion plugins/vibe/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,7 @@ harness switch instead of another worker doing its work.

## Runtime Files

Version `1.0.79` contains 12 skills, 16 agents, and one hook manifest
Version `1.0.80` contains 12 skills, 16 agents, and one hook manifest
(`hooks/hooks.json`). It has no standalone commands, root-level shell scripts,
or production Python tools under
`tools/python/`; that directory contains the skill-contract test modules and
Expand Down
7 changes: 6 additions & 1 deletion plugins/vibe/agents/vibe-environment-worker.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,8 @@ changes and gates fields.
never push to make it so.
2. If an open pull request already exists for the branch
(`gh pr list --repo closedloop-ai/symphony-alpha --head <branch> --state open --json url,number`),
create nothing. On the first dispatch return `DONE` with its URL. A
create nothing. Ensure it has the verified commit's `Targeted pre-PR
evidence` comment, posting it per step 4 if absent. Then return its URL. A
dispatch from handoff's "Changes on the open pull request" (section 10)
updates it instead, as "Updating an open pull request" below says.
3. Run `node ../skills/handoff/scripts/handoff-inventory.mjs --worktree "<wt>" --phase pull-request --overlap --ticket "<gitdir>/vibe-live-ticket.md"`
Expand Down Expand Up @@ -405,6 +406,10 @@ changes and gates fields.
space, and one pull request's body can overwrite another's. The same holds for every body or payload file this
mode writes. Rerun step 3 with `--body <file> --runs "<gitdir>/vibe-pr-evidence.json" --commit <published commit>`:
`blocking.prBody` or `blocking.prEvidence` false creates nothing (`NEEDS_CHANGE`).
Capture the new URL, render the validated evidence with
`node ../skills/handoff/scripts/pr-evidence-comment.mjs --evidence "<gitdir>/vibe-pr-evidence.json" --output "<gitdir>/pr-evidence-<live ticket slug>.md"`,
and post it with `gh pr comment <url> --body-file <file>`. Do not return
`DONE` until the comment lists the inventory, result SHAs, probe and tier.
5. Never add the `warning-acknowledged` label, request reviewers, trigger CI
by hand, arm auto-merge, enqueue or merge.

Expand Down
93 changes: 47 additions & 46 deletions plugins/vibe/agents/vibe-verify-worker.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ worktree/branch.

## Inputs and references

The owned worktree, mode (`checks`, `footprint`, `coverage`, or
The owned worktree, mode (`checks`, `pre-pr`, `footprint`, `coverage`, or
`control`), inventory, acceptance criteria and the sole writer's local plan and
missing tests list.
Read the actual table at the same session decision table path and affected/
Expand Down Expand Up @@ -58,54 +58,55 @@ automatic technical approval question for the vibe coder. Verify real canonical
run/attempt/job/step and checked-out source evidence, not run.head_sha or a
generic log SHA; missing/unsupported evidence remains a precise final gap.

Validation is targeted, never a broad or full local suite. Run:
Validation is targeted. Run nonwriting Biome on changed files, source gates
and `pnpm typecheck:affected`. Only the tests closedloop-graph's
`code_tests_for` returns for each changed file, plus
`diffRules.requiredTests` and `requiredChecks`, through their owning package
commands. Run a required
`flagStates` test on and off. If graph coverage is empty or unknown, use the
repository's related-test command, never a wider suite.
Commands come from `.vibe/path-gates.json` or the `checks` of its
`.vibe/storybook-rules.json`; record flag pairs with `flag: "on"` or `flag: "off"`.

- Biome WITHOUT write/fix flags on the changed files.
- The source gates.
- `pnpm typecheck:affected` (the changed packages and their dependents).
- Only the tests closedloop-graph's `code_tests_for` returns for each changed
file (any file a change, fix or main merge touched, conflict resolutions
included), plus the inventory's `diffRules.requiredTests` and
`requiredChecks`, each through its owning package's runner on those test
files alone (for example
`pnpm --filter <package> exec vitest run <test files>`). A required test
whose path is a command comes from the repo's `.vibe/path-gates.json` or
the `checks` of its `.vibe/storybook-rules.json` (required whenever a story
changes): run that command and record it in `testRuns` with the command as
its `path`.
A required test with `flagStates` covers a host of a shared component the
diff changed: run it with the flag on and with it off, and record each run
with `flag: "on"` or `flag: "off"`.
Never run `pnpm test`, `pnpm verify`, `pnpm test:affected`,
`pnpm test:lanes --exec`, a whole lane or package suite locally. Select over
the whole branch diff. Record import and E2E-reference selections in
`testSelection`, and commands, cases, flags, results and SHAs in `testRuns` of
`<gitdir>/vibe-pr-evidence.json`. `blocking.prEvidence` names gaps; skipped CI
is not a run or permission to write one.

Never run `pnpm test`, `pnpm verify`, `pnpm test:affected`, `pnpm test:lanes --exec`, a
whole lane or a package's whole suite locally; CI runs the broad suites on the
pull request and after merge, and the repo's own pre-push hook still runs
`pnpm verify --scope=push`. A requested pull request opens once these targeted
checks pass. Select over the whole branch diff against the base. Where
`code_tests_for` returns no test list for a file (`found: false`, empty or
unknown), use the repo's related-test command (for example
`vitest related <file> --run`), never a wider suite; also select e2e specs by
reference across every e2e root (routes, request fields, test IDs, accessible
names, channels and flag keys the file touches). Record both as `testSelection`
and every run as `testRuns` in `<gitdir>/vibe-pr-evidence.json`;
`blocking.prEvidence` names what is missing. Skipped CI is not a run. Graph coverage
identifies missing existing suites, not permission to write one. Three more
rules:
In `pre-pr` mode, read the final committed diff, never the plan, and write
`targetedInventory` in that file as a read-only actor. Run `pnpm test:lanes`
for the repository-owned lane names and commands. Inventory exact commands,
files and cases for every selected Vitest package, importing test, changed or
importing Storybook story, referenced web/Desktop E2E spec and matching
post-merge-only lane. Run that set once. After a writer fixes an isolated
failure, refresh the inventory and rerun only that failed case and any target
the fix newly reaches, never the whole set. Record every result and SHA. Carry
an earlier pass only when its covered-file hashes match final HEAD.

- Every e2e spec the session touched, and every spec that covers a touched
screen (`code_tests_for`, and `rg` of the screen's route and test IDs under
every e2e root: any directory named `e2e`, `e2e-*` or `*-e2e`), runs at handoff through the repo's containerized path (the
`local-ci` skill, `dagger call e-2-e`), never natively on the person's Mac,
and only those specs, never the whole e2e suite, all in one locked run per
head: the lock refuses a second entry from the same worktree (exit 76). The containerized stack
is one per machine (one compose project name, fixed ports and shared
Dagger cache volumes), so start it only through
`node ../skills/vibe/scripts/e2e-lock.mjs run --head <sha> <the e2e command>`, which
waits until no other session's stack is running and runs only that clean commit. Its exit 75 means the
stack stayed busy past the wait: record that limitation with the holder it
prints, never a pass, and never start the stack around the lock.
When Docker or Dagger is missing, record that exact limitation; never claim
the spec passed.
For targeted E2E, record the repository's capability probe and choose:

1. Tier A: its container path (`run-containerized.sh` or Dagger) only when
Docker is present and reachable and reported memory and disk meet the
repository's documented thresholds.
2. Tier B: otherwise run the same specs natively, headless, against the local
stack with the CI sandbox identity, quarantine exclusion and retries off.
Do not run host-browser visual-regression specs against Linux baselines;
list them as `not run locally, covered by CI`.
3. Tier C: otherwise use the operator's configured remote runner and its
tested-head lock.

`git config --bool vibe.noNativeE2e` is the machine-local operator override;
when true, skip Tier B and use C or report no tier. If no tier is available,
list every unrun spec and its automatic CI lane. Never silently skip E2E.
Three more rules:

- Select every E2E spec the session touched or that references a touched
screen's route, test IDs, accessible names, channels, fields or flag keys
across every E2E root. Run only those specs through the chosen tier and the
repository's serialization lock; a busy or unavailable runner is a named
limitation, never a pass.
- Before you claim that CI runs, or ran, a check or test, read
`.github/pr-test-event-tiers.md` and cite the tier of the job that runs
it. A `batch-only` job does not run on the pull request, and its `skipped`
Expand Down
9 changes: 8 additions & 1 deletion plugins/vibe/skills/handoff/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,10 @@ on those tests, then step 7's commit and its push and publication through
`vibe-environment-worker` in redeploy mode. The pull request does not open
until those tests pass, their independent review is clean and step 9's gate passes.

During that turn, run `node scripts/parallel-prep.mjs --writer-state running
--pull-request` and dispatch each lane. Prep may draft evidence, warm E2E,
hash stable results and ready hidden QA, but never edit source or start a writer.

From the request on, every brief in this handoff carries the `pull-request`
phase. No brief may say both that the person takes the work to a pull request and that no tests are authored.

Expand Down Expand Up @@ -776,7 +780,10 @@ lookup resolved to exactly one user.

On a requested pull request, skip assign mode: the ticket stays with the
person, In Progress. Before dispatch, require the refreshed pinned-base
Storybook train check. Then dispatch
Storybook train check. Dispatch `vibe-verify-worker` in `pre-pr` mode on final
HEAD. It records read-only inventory, results, SHAs, E2E probe and tier in
`<gitdir>/vibe-pr-evidence.json`; after a fix only failed cases rerun.
Then dispatch
`vibe-environment-worker` in `pull-request` mode (its own `pull-request`
record action, an exclusive record turn with no record writes) with the
worktree, the live ticket slug, the confirmed summary, the final verified
Expand Down
54 changes: 54 additions & 0 deletions plugins/vibe/skills/handoff/scripts/e2e-tier.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Select and validate the local E2E execution tier recorded in
// vibe-pr-evidence.json. Repository-owned lane tooling supplies the probe;
// this module only applies its documented thresholds and operator override.

/** Return the strongest available E2E tier, or null when CI must cover it. */
export function selectE2eTier(capability = {}) {
const container = capability.container ?? {};
const finite = (...values) => values.every(Number.isFinite);
const containerReady = container.dockerPresent === true && container.dockerReachable === true
&& finite(container.memoryGiB, container.diskGiB, container.minimumMemoryGiB, container.minimumDiskGiB)
&& container.memoryGiB >= container.minimumMemoryGiB && container.diskGiB >= container.minimumDiskGiB;
if (containerReady) return { tier: "A", reason: "the repository capability probe passed" };
if (capability.native?.available === true && capability.native?.forbidden !== true) {
return { tier: "B", reason: "the container probe failed and native headless E2E is allowed" };
}
if (capability.remote?.available === true) return { tier: "C", reason: "a configured remote runner is available" };
return { tier: null, reason: "no permitted local or remote E2E tier is available" };
}

/** Problems in the recorded capability probe and chosen tier. */
export function tierEvidenceProblems(capability, e2eSpecs = []) {
if (!capability || typeof capability !== "object") return ["E2E capability probe and tier are missing"];
const problems = [];
const container = capability.container ?? {};
for (const key of ["dockerPresent", "dockerReachable", "memoryGiB", "diskGiB", "minimumMemoryGiB", "minimumDiskGiB", "command"]) {
if (container[key] === undefined || container[key] === "") problems.push(`E2E container probe is missing ${key}`);
}
const expected = selectE2eTier(capability).tier;
if ((capability.selectedTier ?? null) !== expected) {
problems.push(`E2E tier ${capability.selectedTier ?? "none"} does not match selected tier ${expected ?? "none"}`);
}
if (expected === "B") {
if (capability.native?.headless !== true || capability.native?.localStack !== true) problems.push("native E2E must be headless against the local stack");
if (capability.native?.retries !== 0) problems.push("native E2E must run with retries off");
if (capability.native?.quarantineExcluded !== true) problems.push("native E2E must record the CI quarantine exclusion");
if (!capability.native?.sandboxIdentity && !capability.native?.sandboxIdentityNotRequired) {
problems.push("native E2E must name the CI sandbox identity or why the repository needs none");
}
const visual = e2eSpecs.filter((spec) => /visual[-_.]?regression|screenshots?/i.test(spec));
const held = new Set(capability.notRunLocally ?? []);
for (const spec of visual.filter((spec) => !held.has(spec))) {
problems.push(`${spec}: visual-regression coverage must be listed as not run locally, covered by CI`);
}
if (visual.length > 0 && (!Array.isArray(capability.ciCoverage) || capability.ciCoverage.length === 0)) problems.push("native visual-regression gaps need the CI job that covers them");
}
if (capability.native?.forbidden === true && !capability.native?.forbiddenSource) problems.push("the native E2E operator override needs its configured source");
if (expected === "C" && !capability.remote?.command) problems.push("remote E2E tier needs its configured runner command");
if (expected === null && e2eSpecs.length > 0) {
const unavailable = new Set(capability.unavailableSpecs ?? []);
for (const spec of e2eSpecs.filter((spec) => !unavailable.has(spec))) problems.push(`${spec}: unavailable E2E spec is not left explicitly for CI`);
if (!Array.isArray(capability.ciCoverage) || capability.ciCoverage.length === 0) problems.push("unavailable E2E needs the CI jobs that cover it");
}
return problems;
}
31 changes: 31 additions & 0 deletions plugins/vibe/skills/handoff/scripts/e2e-tier.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import assert from "node:assert/strict";
import test from "node:test";
import { selectE2eTier, tierEvidenceProblems } from "./e2e-tier.mjs";

const container = {
dockerPresent: true,
dockerReachable: true,
memoryGiB: 24,
diskGiB: 80,
minimumMemoryGiB: 20,
minimumDiskGiB: 64,
command: "pnpm prepush --all --probe",
};

test("tier selection prefers capable containers, then native, then a configured remote", () => {
const base = { container, native: { available: true, forbidden: false }, remote: { available: true } };
assert.equal(selectE2eTier(base).tier, "A");
assert.equal(selectE2eTier({ ...base, container: { ...container, memoryGiB: 12 } }).tier, "B");
assert.equal(selectE2eTier({ ...base, container: { ...container, dockerReachable: false }, native: { available: true, forbidden: true } }).tier, "C");
assert.equal(selectE2eTier({ ...base, container: { ...container, dockerPresent: false }, native: { available: true, forbidden: true }, remote: { available: false } }).tier, null);
});

test("the evidence records the probe, selected tier and honest no-tier or native visual gaps", () => {
const base = { container, native: { available: true, forbidden: false }, remote: { available: true }, selectedTier: "A" };
assert.deepEqual(tierEvidenceProblems(base, ["e2e/sessions.spec.ts"]), []);
const native = { ...base, container: { ...container, dockerReachable: false }, selectedTier: "B",
native: { available: true, forbidden: false, headless: true, localStack: true, retries: 0, quarantineExcluded: true, sandboxIdentityNotRequired: "fixture" } };
assert.match(tierEvidenceProblems(native, ["e2e/visual-regression.spec.ts"]).join(), /visual-regression.*covered by CI/);
assert.deepEqual(tierEvidenceProblems({ ...native, notRunLocally: ["e2e/visual-regression.spec.ts"], ciCoverage: ["visual regression"] }, ["e2e/visual-regression.spec.ts"]), []);
assert.deepEqual(tierEvidenceProblems({ ...base, container: { ...container, dockerPresent: false }, native: { available: true, forbidden: true, forbiddenSource: "git config vibe.noNativeE2e" }, remote: { available: false }, selectedTier: null, unavailableSpecs: ["e2e/sessions.spec.ts"], ciCoverage: ["Browser E2E"] }, ["e2e/sessions.spec.ts"]), []);
});
Loading
Loading