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
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,25 @@ 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.79

Worker launches, shared ownership and operational evidence now have runtime checks instead of relying on prose alone.

#### Added
- Handoff's `shared-capability-owner` diff rule rejects a capability-level design-system control newly composed by another page adapter when multiple page adapters already compose it directly. A broad acceptance criterion cannot waive the finding; only a ticket or operator ruling that explicitly requires separate compositions can.
- Claude's Agent pre-tool guard refuses a fleet orchestrator's direct launch of an internal `vibe:vibe-*-worker` outside a vibe session, preserving the session record, environment setup, guardrails and adversarial review, PR preflight and SendMessage path supplied by `vibe:vibe`.
- A cross-runtime worker contract enumerates every worker's built-ins and owned MCP operations against Codex and Claude fixtures, including `$vibe` versus `vibe:vibe` invocation and both plugin manifest layouts.
- A Bash pre-tool guard blocks credential workarounds used to obtain evidence and force pushes to any remote, including commands sent through another machine. Proof and scratch work use fresh refs instead of rewriting one.
- The fleet watcher includes atomic Markdown events in its snapshot, so an agent without `SendMessage` can still wake the orchestrator when green status moves.

#### Changed
- When a ticket or PRD says "across all", or the person says "shared", the vibe plan names one owning parent component, every consumer that inherits it and each consumer's approved prototype route. The guardrails reviewer checks that inventory against the import graph.
- Running-app and visual QA compare each consumer page with that page's approved prototype route and flag controls present in the prototype but missing in production, alongside rendered controls that lack a sourced requirement.
- The owned launcher accepts underscore or hyphen MCP operation names, binds both runtime aliases, and compares loaded tools by canonical identity. The Claude change worker keeps `SendMessage` and `Agent` for progress and read-only helpers, while the sole-writer rule still forbids nested writers.
- Worker briefs stop at the first authentication failure and use CI logs or an API for the same evidence, or ask. The no-force-push rule covers every remote and requires unique proof refs.
- When launcher tooling cannot mint a supported publisher grant, main sync may stage a plain noncommitting merge of `origin/main`; the root finishes it through the guarded commit script, and publication still waits for a real grant.
- Instruction text is trimmed so the release adds no words: Markdown under the vibe plugin is about 103 words shorter than in 1.0.78.

### 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.
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.78",
"version": "1.0.79",
"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.78",
"version": "1.0.79",
"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.78` contains 12 skills, 16 agents, and one hook manifest
Version `1.0.79` 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
28 changes: 14 additions & 14 deletions plugins/vibe/agents/vibe-change-worker.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
name: vibe-change-worker
description: The one persistent implementation writer for a vibe session. Keeps the same context, worktree and branch while planning, implementing all frontend/backend/prototype/component work, fixing reviewed issues, repairing existing tests its change broke and listing the missing tests for engineering at handoff; it writes tests only when the person asks for a pull request, in any plain words. Uses graph-first owner and product-decision research, local canonical plans, Storybook and existing checks. Never commits or opens a PR.
model: sonnet
tools: Read, Write, Edit, Grep, Glob, Bash, Skill
tools: Read, Write, Edit, Grep, Glob, Bash, Skill, SendMessage, Agent
---

You are the sole implementation writer for this session. Reuse this underlying
worker/session ID across requests and handoff, not just this role name in a new
context. Work only in the supplied session worktree and branch. Never create
another feature worktree/branch or spawn another implementation/test writer.
The orchestrator queues requests and resumes you serially; independent advisors
and reviewers are read-only.
The orchestrator queues requests serially. Agent may spawn read-only helpers,
never writers. Use SendMessage for parent progress.

## Inputs and continuations

Expand Down Expand Up @@ -55,17 +55,17 @@ a needed vibe tooling fix returns `BLOCKED`.
Run the prep step in `design-pass.md`: FEATURE_MAP for each screen/host,
required graph `code_symbols`, `code_callers`, `code_importers` and
`blast_radius_tickets`, the component catalog and owning package rules, and
one workflow-memory query when available. Pick the owner by its existing rules:
behavior shared by children belongs in the generic shared parent; domain
wiring stays in its feature package; never copy a component or helper. Look
first for where the same capability already exists for a sibling entity and
mirror its ownership, component and Storybook structure, never its root or
version, where those rules and the repo's docs do not already say how
(`design-pass.md`, Prep step 5). Before you write a new hook, helper, query
key or inline query, `rg` the repo's shared app hooks and helpers (its auth
helpers included) and the existing `use-*` hooks for its endpoint, query key
and subject, and use what exists. URL, storage and route keys come from the
shared constant both hosts import, never a literal in a host adapter.
one workflow-memory query when available. Apply its owner rules: behavior
shared by children belongs in the generic shared parent; domain wiring stays
in its feature package. Where no rule or repo doc decides, mirror a sibling's
ownership and Storybook structure, never its root or version
(`design-pass.md`, Prep step 5). Before a new hook, helper, query or key, `rg`
the repo's shared app hooks and helpers (its auth helpers included) and the
existing `use-*` hooks, then reuse the match. Both hosts import URL, storage
and route keys from one constant.
When a ticket or PRD says "across all" or the person says "shared", the plan
must name one owning parent component and every consumer that inherits it,
plus each consumer's approved prototype route.

Before you delete a feature flag's read, key constant or gate, read the
comments on every ticket linked to it (the live ticket, its PRD, and the
Expand Down
22 changes: 12 additions & 10 deletions plugins/vibe/agents/vibe-guardrails-reviewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,16 +104,18 @@ path merely because another rule describes how its content should look.
8. Shared owner and red flags: every item in `design-pass.md` "Review".
Read the session change log
(`$(git -C <wt> rev-parse --absolute-git-dir)/vibe-changes.md`) for each
request's `Owner` and `Rule`. Flag each red flag (shallow module,
information leakage, temporal decomposition, pass-through, copy) with
`file:line` for every site, and name the parent and the generic slot or
extension point the behavior belongs in. Any component that composes or
inherits from a shared parent keeps only what is specific to it: two
children of one parent that each implement the same behavior is blocking,
and so is a copy
of an existing shared component or domain code inside a design-system
slot. Code that is not in the `Owner` its change log entry names is
advisory. Run these checks and quote what each prints:
request's `Owner` and `Rule`. Report red flags with `file:line` and owner.
Any component that composes or inherits from a shared parent keeps only
what is specific to it: two children of one parent that each implement the
same behavior is blocking. Code outside its logged `Owner` is advisory.
Run and quote:
- Same capability component: treat every `shared-capability-owner` diff
finding as blocking when two or more page-level adapters compose it
instead of one shared parent. Compare the plan's Owner and Consumers
inventory with the import graph; an exception passes only when its cited
ruling explicitly requires separate compositions.
- Red flags: shallow module, information leakage, temporal decomposition,
pass-through and copy.
- Slot without an owner: for each capability whose state the children
share (selection, grouping, collapse, paging), `rg` its state and
handlers (`useState`, a reducer, `selected`, `onSelect`, `toggle`) in
Expand Down
2 changes: 1 addition & 1 deletion plugins/vibe/agents/vibe-seed-pr-worker.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: vibe-seed-pr-worker
description: Handles the git and GitHub side of a vibe-seed-refresh pull request in the target repository for its orchestrator. Modes: open (push, PR from the template), push (push a fix the orchestrator committed and reply on its review threads), status (one-line PR state), sync (stage a merge of main for the orchestrator to commit), enqueue (arm auto-merge into the merge queue), cleanup (remove the fix worktree). Never commits. Returns a short status.
description: "Handles the git and GitHub side of a vibe-seed-refresh pull request in the target repository for its orchestrator. Modes: open (push, PR from the template), push (push a fix the orchestrator committed and reply on its review threads), status (one-line PR state), sync (stage a merge of main for the orchestrator to commit), enqueue (arm auto-merge into the merge queue), cleanup (remove the fix worktree). Never commits. Returns a short status."
model: sonnet
tools: Read, Write, Grep, Glob, Bash
---
Expand Down
6 changes: 4 additions & 2 deletions plugins/vibe/agents/vibe-verify-worker.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,8 +269,10 @@ 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. Inventory the rendered controls against the sourced
requirements and report every control with no source on that surface.
where relevant. For shared scope, compare each consumer page against that
page's approved prototype route; report every control present in the
prototype but missing in production, 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
14 changes: 11 additions & 3 deletions plugins/vibe/evals/codex/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

EVALS = Path(__file__).resolve().parents[1]
PLUGIN = EVALS.parent
MANIFESTS = (".codex-plugin/plugin.json", ".claude-plugin/plugin.json")


def document(path: Path) -> tuple[dict[str, Any], str]:
Expand All @@ -30,6 +31,15 @@ def document(path: Path) -> tuple[dict[str, Any], str]:
return yaml.safe_load(header), body.strip()


def plugin_version(plugin: Path) -> str:
"""Read the version from either supported runtime's plugin manifest."""
for relative in MANIFESTS:
manifest = plugin / relative
if manifest.is_file():
return str(json.loads(manifest.read_text())["version"])
raise FileNotFoundError("vibe has no Claude Code or Codex plugin manifest")


def role_text(text: str) -> str:
"""Replace fixture identities with roles without changing the scenario intent."""
text = re.sub(r"\b[\w.]+@[\w.]+\.com\b", "product-manager@example.invalid", text)
Expand Down Expand Up @@ -667,9 +677,7 @@ def main() -> int:
(args.plugin / "skills/vibe/SKILL.md").read_bytes()
).hexdigest(),
plugin=str(args.plugin.resolve()),
version=json.loads(
(args.plugin / ".claude-plugin/plugin.json").read_text()
)["version"],
version=plugin_version(args.plugin),
model=args.model,
grader_model=args.grader_model,
concurrency=args.concurrency,
Expand Down
8 changes: 8 additions & 0 deletions plugins/vibe/evals/codex/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ def test_one_case_per_claude_prompt_and_role_names() -> None:
)


@pytest.mark.parametrize("layout", [".codex-plugin", ".claude-plugin"])
def test_plugin_version_accepts_either_runtime_manifest(tmp_path: Path, layout: str) -> None:
manifest = tmp_path / layout / "plugin.json"
manifest.parent.mkdir()
manifest.write_text(json.dumps({"version": "1.2.3"}))
assert runner.plugin_version(tmp_path) == "1.2.3"


def test_every_model_grader_has_a_deterministic_or_wording_route() -> None:
deterministic = {
"explains-both-endings",
Expand Down
31 changes: 31 additions & 0 deletions plugins/vibe/hooks/agent-guard.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/usr/bin/env node
// PreToolUse guard for Agent: an orchestrator starts a Claude vibe writer by
// invoking the vibe:vibe skill, never by dispatching one of its internal
// workers. Direct dispatch skips the session record, environment setup,
// guardrails and adversarial review, PR preflight and SendMessage.

import { readFileSync } from "node:fs";
import { pathToFileURL } from "node:url";
import { inVibeSession } from "./foreground-guard.mjs";

const VIBE_WORKER = /^vibe:vibe-[a-z0-9-]+-worker$/;

/** The refusal for a direct internal vibe-worker launch, or null. */
export function agentLaunchProblem(input) {
const subagent = input?.subagent_type;
if (typeof subagent !== "string" || !VIBE_WORKER.test(subagent)) return null;
return `do not launch ${subagent} directly: invoke \`vibe:vibe\` in a Claude agent so the writer gets its session record, environment setup, guardrails and adversarial review, PR preflight and SendMessage path`;
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
try {
const event = JSON.parse(readFileSync(0, "utf8") || "{}");
const problem = event?.tool_name === "Agent" ? agentLaunchProblem(event.tool_input) : null;
const cwd = typeof event.cwd === "string" ? event.cwd : process.cwd();
if (problem && !inVibeSession(cwd)) {
process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: problem } })}\n`);
}
} catch (error) {
process.stderr.write(`agent-guard: ${error instanceof Error ? error.message : String(error)}\n`);
}
}
32 changes: 32 additions & 0 deletions plugins/vibe/hooks/agent-guard.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import assert from "node:assert/strict";
import { execFileSync, spawnSync } from "node:child_process";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";
import { agentLaunchProblem } from "./agent-guard.mjs";

const HERE = path.dirname(fileURLToPath(import.meta.url));
const GUARD = path.join(HERE, "agent-guard.mjs");

test("an orchestrator cannot launch a vibe worker directly outside a vibe session", (t) => {
const dir = mkdtempSync(path.join(os.tmpdir(), "vibe-agent-guard-"));
t.after(() => rmSync(dir, { recursive: true, force: true }));
const run = (subagentType, cwd = dir) => spawnSync(process.execPath, [GUARD], {
input: JSON.stringify({ tool_name: "Agent", cwd, tool_input: { subagent_type: subagentType, prompt: "Implement the ticket" } }),
encoding: "utf8",
});

assert.match(agentLaunchProblem({ subagent_type: "vibe:vibe-change-worker" }), /invoke `vibe:vibe`/);
const denied = JSON.parse(run("vibe:vibe-change-worker").stdout).hookSpecificOutput;
assert.equal(denied.permissionDecision, "deny");
assert.match(denied.permissionDecisionReason, /session record, environment setup, guardrails and adversarial review, PR preflight and SendMessage path/);
assert.equal(run("general-purpose").stdout, "", "a Claude agent may invoke the vibe skill");

execFileSync("git", ["init", "--quiet", "--initial-branch=main", dir]);
const gitDir = execFileSync("git", ["-C", dir, "rev-parse", "--absolute-git-dir"], { encoding: "utf8" }).trim();
writeFileSync(path.join(gitDir, "vibe-session.json"), "{}\n");
assert.equal(run("vibe:vibe-change-worker", dir).stdout, "", "the vibe skill may dispatch its own worker inside a session");
assert.ok(readFileSync(path.join(HERE, "hooks.json"), "utf8").includes('"matcher": "Agent"'), "the Agent hook is registered");
});
13 changes: 13 additions & 0 deletions plugins/vibe/hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/merge-guard.mjs\""
},
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/shell-safety-guard.mjs\""
}
]
},
{
"matcher": "Agent",
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/agent-guard.mjs\""
}
]
}
Expand Down
40 changes: 40 additions & 0 deletions plugins/vibe/hooks/shell-safety-guard.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env node
// PreToolUse guard for Bash: workers may not invent credential routes to fetch
// evidence or force-push any remote. Both mistakes can escape a written rule,
// especially through an ssh command aimed at another machine.

import { readFileSync } from "node:fs";
import { pathToFileURL } from "node:url";

const TOKEN = /(?:\b(?:gh_token|github_token|[a-z][a-z0-9_]*token)\b|\$\{?[a-z][a-z0-9_]*token\}?|--password(?:-stdin)?\b|(?:^|\s)-p(?:\s|$)|["']?auth["']?\s*:)/i;
const CREDENTIAL_ROUTE = /(?:\bDOCKER_CONFIG\b|\bdocker\s+login\b|["']?auths["']?\s*:)/i;
const FORCE_PUSH = /\bgit\b(?:(?![;&|\n]).)*?\bpush\b(?:(?![;&|\n]).)*(?:\s-f(?:\s|$)|--force(?:-with-lease)?\b|(?:^|\s)\+[^\s]+)/i;

/** The credential-safety refusal for a shell command, or null. */
export function credentialProblem(command) {
if (!CREDENTIAL_ROUTE.test(command) || !TOKEN.test(command)) return null;
return "credential workaround refused: after the first authentication failure, use CI logs or the API for the same evidence, or stop and ask; never write, export or route around the operator's credentials";
}

/** The force-push refusal for a shell command, or null. */
export function forcePushProblem(command) {
if (!FORCE_PUSH.test(command)) return null;
return "force push refused: never rewrite any remote, including another machine or a scratch ref; create a fresh proof/<sha>-<n> ref and push normally";
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
try {
const event = JSON.parse(readFileSync(0, "utf8") || "{}");
const command = event?.tool_input?.command;
if (event?.tool_name === "Bash" && typeof command === "string") {
const problem = credentialProblem(command) ?? forcePushProblem(command);
if (problem) {
process.stdout.write(`${JSON.stringify({ hookSpecificOutput: {
hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: problem,
} })}\n`);
}
}
} catch (error) {
process.stderr.write(`shell-safety-guard: ${error instanceof Error ? error.message : String(error)}\n`);
}
}
Loading
Loading