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
26 changes: 25 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,30 @@ 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.78

The fleet runs from one fleet home, and the open 2026-10-22 lessons each get a check where one fits.

#### Added
- One fleet home (`FLEET_HOME`, default `~/fleet-orchestrator`) holds what every orchestrator chat reuses at its top (`config.env`, `standing-rulings.md`, the lessons ledger, `handover.md` and `current`, the active program) and each program's state under `programs/<slug>/`. `fleet-home.mjs start` begins a program that reads nothing of an earlier one; `resume` lists what a new chat reads, the Claude writers to relaunch from their saved prompts and a watcher another chat still holds; `migrate` turns a flat work dir into a home. Every fleet and monitor script resolves the home through `work-dir.mjs`, with session, request and remote state dirs derived per program (`REMOTE_FLEET_HOME` on the other machine). A new chat once inherited only part of a program: its resume log and ledger lived outside the work dir and its Codex writers' state in other directories.
- `board.mjs status` fails on state written outside the home: program files at its top, a symlink leaving it, a per-program key in its `config.env`, a shared remote state dir, a session in `sessions.txt` that `fleet.sh` did not start, and Codex thread state outside `sessions/`.
- The watcher never adds or reports a PR another chat owns: one in `prs-ignore.txt` or in another program's `prs.txt` in the same fleet home, so it is never `STALE-PR` here. Discovery once added a PR another chat was shepherding.
- Every test result names the commit it ran: `pre-pr-lanes.mjs run --head <sha>` runs a lane and records it under that SHA, `record` needs `--head`, and `e2e-lock.mjs run` needs `--head`; each refuses when the worktree's HEAD is another commit or a tracked file is modified (`tested-head.mjs`) and fails when HEAD moves during the run. A worker's container runs once tested an old commit for an hour after every checkout failed, and passes were reported for a pull request never tested.
- Handoff's diff rules reject a newly enabled control unless the change ledger cites the ticket acceptance criterion or ruling that names that control on that surface. The guardrails review opens each citation, and running-app and visual QA inventory rendered controls against the sourced requirements.
- A changed visual baseline requires a counted `Baseline inventory:` and one sourced `Baseline element:` entry per new element. `pr-preflight.mjs` discovers baseline files from the actual Git diff and rejects missing ledgers, while green watch and guardrails compare each declared element with the image diff; "plausibly intended" is not a source.
- `pr-preflight.mjs` runs before a pull request opens (the environment worker's pull-request mode): the title with the ` (#n)` suffix fits 72 characters, and a body line claiming a result is identical or matches carries its measured count. A green-watch fix failed the metadata gate on a 73-character title, and another merged on a "pixel-identical" baseline about 3,900 pixels off.
- `hooks/merge-guard.mjs`, a PreToolUse hook, refuses `gh pr merge` (a merge, an enqueue or armed auto-merge) on a pull request another account authored until the operator approves it (`MERGE_AUTHOR_OK=<author>`). An orchestrator once enqueued a teammate's pull request on a worker's misattribution.

#### Changed
- The single review round (operator ruling) holds only in a program's delivery phase: once core work is done it is retired and every valid finding is fixed in its pull request, every round. `shepherd.mjs --round-one-fixed-at`, `review-followup.mjs plan` and a `later` result in `review-threads.mjs` now need the single-review ruling in force (`--single-review-ruling`, `singleReviewRuling`).
- The green-watch fixer is a vibe writer, so its pull request passes vibe's gates, and it attributes reds from `gh pr view --json author,headRefName`. The guardrails reviewer blocks a changed literal whose old value remains in the repository and a test locator widened to a non-unique page-wide match; a green-watch fix left both.
- `pre-pr-lanes.mjs` requires every lane the lane report names, e2e lanes included, and reads lane lines that start with the step's variables; `--skip` needs `--skip-ruling`. A pull request merged with two Desktop E2E reds because the check skipped e2e lanes by default.
- fleet-orchestrator: review threads are findings to verify on their merits, never human rulings, since scheduled review bots post under teammates' accounts; the green watch is a Sonnet watcher that starts an Opus fixer only for a red that needs attribution or a fix, and files another author's red only after that PR's ticket, PRD and design rulings show the change was not asked for (a requested change is accepted and cited); once core work is done the full e2e suites run on main before the batches find the reds, and an e2e fix PR gets auto-merge only after its specs and the lane's latest red set pass on the CI-identical runner (operator rulings).
- Instruction text the new checks enforce is trimmed so the release adds no words: Markdown under the vibe plugin is about 7 words shorter than in 1.0.77.

#### Fixed
- The v1.0.77 entry said Markdown was about 930 words shorter than in 1.0.75; the merged change is about 830 shorter. The count was taken before the release's last commits. `instruction-words.mjs` (fleet-orchestrator) now measures the net change between two refs and, with `--changelog`, fails when an entry states a number the measurement does not match; the lessons loop runs it at the final head.

### vibe v1.0.77

The last 2026-10-22 lessons and a trim that brings 1.0.76 and 1.0.77 together to fewer instruction words than 1.0.75.
Expand All @@ -17,7 +41,7 @@ The last 2026-10-22 lessons and a trim that brings 1.0.76 and 1.0.77 together to
- `pre-pr-lanes.mjs` refuses to open a pull request until each test lane the repository's lane report names has a passing run recorded at the head (operator ruling); the environment worker, handoff and fleet QA prep run it.

#### Changed
- Instruction text is trimmed: prose a script now enforces keeps only the command and its reason, and repeated rules are stated once. Markdown under the vibe plugin is about 930 words shorter than in 1.0.75.
- Instruction text is trimmed: prose a script now enforces keeps only the command and its reason, and repeated rules are stated once. Markdown under the vibe plugin is about 830 words shorter than in 1.0.75.

### vibe v1.0.76

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.77",
"version": "1.0.78",
"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.77",
"version": "1.0.78",
"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
5 changes: 3 additions & 2 deletions plugins/vibe/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -523,8 +523,9 @@ harness switch instead of another worker doing its work.

## Runtime Files

Version `1.0.68` contains 12 skills and 16 agents. It has no standalone
commands, hooks, root-level shell scripts, or production Python tools under
Version `1.0.78` 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
`vibe_contract_text.py`, the text helpers they share.
Runtime helpers and tests live alongside their owning skills, including vibe
Expand Down
11 changes: 5 additions & 6 deletions plugins/vibe/agents/vibe-change-worker.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,12 +167,11 @@ Raising and recording it is neither instruction inflation nor manufactured
ambiguity: those rules protect settled rulings, and an unexamined mechanism is
not one.

Build the outcome only along data directions that are already allowed.
Every new user-visible element needs a source that names it: ticket or PRD
text naming that element, a prototype that shows it, or the person's or an
operator's ruling. A ticket's list of capabilities or action types names no
new control, menu item or screen. Absorbing an existing ticket's work is fine;
record each absorbed ticket on the live ticket and in the pull request body.
Build the outcome only along data directions that are already allowed. A newly enabled control needs a
ticket acceptance criterion or ruling naming that control on that surface;
"preserve X" in one mode does not add X in another. Record `Control: <file>:
<prop or mount>: <surface>: <ticket AC or ruling>`; `diff-rules.mjs` blocks
the diff without it. A capability list names no control, menu item or screen.
Matching a prototype or design never removes what main ships; `diff-rules.mjs`
blocks a removed mount or shipped string without a `Removal:` line citing the
human source that retires it.
Expand Down
6 changes: 4 additions & 2 deletions plugins/vibe/agents/vibe-environment-worker.md
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,8 @@ changes and gates fields.
invention:
- Summary: the confirmed summary with the live ticket's slug and its
"Keys to see this work", then every `overlap`, `assertionChanges`,
`contractChanges` and answered `asks` entry.
`contractChanges` and answered `asks` entry, plus the change log's
`Control:`, `Baseline inventory:` and `Baseline element:` ledger lines.
- Breaking changes: from Production impact and every `refusals` entry;
"none" only when Production impact supports it and `refusals` is empty.
- UI Feature Flag: the heading alone, unless the change adds a net-new
Expand All @@ -392,7 +393,8 @@ changes and gates fields.
inline, briefly. `blocking.prBody` refuses a line naming one.
4. Title: `<live ticket slug>: <plain imperative summary>`, at most 72
characters including the ` (#NNNN)` GitHub appends, with no mention of AI
tools. First `node ../skills/vibe/scripts/pre-pr-lanes.mjs check --worktree "<wt>" --report <the repo's lane report output>`
tools; `node ../skills/vibe/scripts/pr-preflight.mjs --title "<title>" --body-file <file> --worktree "<wt>" --base <inventory.baseCommit>`
must pass. First `node ../skills/vibe/scripts/pre-pr-lanes.mjs check --worktree "<wt>" --report <the repo's lane report output>`
must pass (each test lane the report names has a passing run recorded at
HEAD; `BLOCKED` with its `MISSING-LANE` lines otherwise). Create it with
`gh pr create --repo closedloop-ai/symphony-alpha --base main --head <branch> --title "<title>" --body-file <file>`,
Expand Down
24 changes: 13 additions & 11 deletions plugins/vibe/agents/vibe-guardrails-reviewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,9 @@ path merely because another rule describes how its content should look.
file whose exports only forward to the new owner (a pass-through or
wrapper), a re-export alias, an export with no remaining importer, a
prop no caller passes, and a loop or `each` table over one remaining
arm are each blocking, cited at `file:line`.
arm are each blocking, cited at `file:line`. So is a changed literal or
contract value whose old value `rg` still finds anywhere in the repo,
and a test locator widened to a page-wide match that is not unique.
9. Tests and checks: an added or deleted test file (`*.test.*`, `*.spec.*`,
`__tests__/`, `e2e/`, or a snapshot or fixture a test reads), a changed
snapshot, or a changed test that is not a pure repair is blocking in the
Expand Down Expand Up @@ -295,16 +297,16 @@ path merely because another rule describes how its content should look.
change log or ticket claims. Match each to the person's words, a ticket or
PRD statement, a human comment, or a shipped rule found through the graph.
If its only source is an agent's own rationale, it is a blocking
`engineering decision` finding. A detailed explanation written by an
agent is still not a product source, and neither is the session's own
ticket, PLN, plan, table or change log. This covers every new user-reachable
capability (a write path, action or control), refusal rule (a
PRD display cap is not one), new visual element or state presentation (held
to the copy rule),
numeric limit, criterion worded more strongly than its source and removed
accessibility behavior; a shared-module edit lists its callers in the
change log. A spec or criterion edited to match the code is blocking
without a human source (`spec-change` diff rule).
`engineering decision` finding. Session artifacts are not a product source.
This covers every new
user-reachable capability (a write path, action or control), refusal rule
(a PRD display cap is not one), visual element, numeric limit, criterion
worded more strongly than its source and removed accessibility. Open every
`Control:` source: it must name that control on that surface; preservation
elsewhere is insufficient. Compare each changed visual baseline with its
counted `Baseline inventory:` and cited `Baseline element:` lines; omission
or "plausibly intended" is blocking. A spec change without a human source
is blocking (`spec-change`).
16. Tech debt and flag keys (`guardrails.md`, "No new tech debt" and
"Feature flags"): each item of a kind that section lists with no
`Debt:` reason is blocking: a mechanism checks 1, 7, 8 or 12 find, a
Expand Down
7 changes: 4 additions & 3 deletions plugins/vibe/agents/vibe-verify-worker.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ rules:
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 <the e2e command>`, which
waits until no other session's stack is running. Its exit 75 means the
`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
Expand Down Expand Up @@ -269,7 +269,8 @@ session touched:
`pnpm control console --level pageerror` and `pnpm control network`. Add
`pnpm control api GET <path>` for the record the screen shows, and
`pnpm control ipc <namespace.method>` read calls for a Desktop screen,
where relevant.
where relevant. Inventory the rendered controls against the sourced
requirements and report every control with no source on that surface.
4. When a baseline exists (an earlier control capture of the same entry on
the merge-base or current main, never a commit that is not an ancestor),
return its artifact paths beside the new ones and say what changed. When the session recorded reference images
Expand Down
24 changes: 24 additions & 0 deletions plugins/vibe/hooks/command-segments.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Splits a Bash command line into its simple commands for the PreToolUse
// guards: each segment's leading NAME=value assignments and its words, with
// wrappers (sudo, env, exec and the like) removed so words[0] is the program.

import path from "node:path";

const SEPARATOR = /\|\||&&|[;|&\n]|\$\(|`|\(|\)/;
const ENV_ASSIGN = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/;
const WRAPPERS = new Set(["sudo", "command", "exec", "nohup", "time", "builtin", "env"]);

/** `{ env, words }` for each simple command in `command`. */
export function commandSegments(command) {
return command.split(SEPARATOR).map((segment) => {
const words = segment.trim().split(/\s+/).filter((word) => word && !/[<>]/.test(word)).map((word) => word.replace(/^["']|["']$/g, ""));
const env = {};
while (words.length > 0) {
const assignment = ENV_ASSIGN.exec(words[0]);
if (assignment) env[assignment[1]] = assignment[2];
else if (!WRAPPERS.has(path.basename(words[0]))) break;
words.shift();
}
return { env, words };
});
}
4 changes: 4 additions & 0 deletions plugins/vibe/hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/foreground-guard.mjs\""
},
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/merge-guard.mjs\""
}
]
}
Expand Down
8 changes: 2 additions & 6 deletions plugins/vibe/hooks/kill-guard.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,11 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { commandSegments } from "./command-segments.mjs";

const SEPARATOR = /\|\||&&|[;|&\n]|\$\(|`|\(|\)/;
const SIGNAL_OPTION = /^-(?:[A-Za-z][A-Za-z0-9+-]*|\d+)$/;
const SUBSTITUTED_KILL = /(?:^|[\s;&|(])kill\b[^;&|\n]*(?:\$\(|`)/;
const BY_NAME = /^p?kill(?:all)?$/;
const ENV_ASSIGN = /^[A-Za-z_][A-Za-z0-9_]*=/;
const WRAPPERS = new Set(["sudo", "command", "exec", "nohup", "time", "builtin", "env"]);

/** The file recording the pids each session called its own. */
export function ledgerFile(home = os.homedir()) {
Expand All @@ -36,9 +34,7 @@ export function killProblems(command, isOwn) {
const problems = SUBSTITUTED_KILL.test(command)
? ["kill with a command substitution signals whatever a search matched; kill only a pid you recorded as your own"]
: [];
for (const segment of command.split(SEPARATOR)) {
const words = segment.trim().split(/\s+/).filter((word) => word && !/[<>]/.test(word)).map((word) => word.replace(/^["']|["']$/g, ""));
while (words.length > 0 && (ENV_ASSIGN.test(words[0]) || WRAPPERS.has(path.basename(words[0])))) words.shift();
for (const { words } of commandSegments(command)) {
const name = path.basename(words[0] ?? "");
if (name !== "kill" && BY_NAME.test(name)) {
problems.push(`${name} signals by pattern, which can end other sessions' processes; record your own pid when you start a process and run kill <pid>`);
Expand Down
Loading
Loading