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

The open fleet lessons from the 2026-10-21 and 2026-10-22 orchestration runs, each with a mechanical check where one fits, and a watcher bug found while checking them.

#### Fixed
- `liveness.mjs` run as a command (as the watcher runs it) printed `READFAIL` for every failed turn: its entry point awaited `main()` above the constants that classify turn errors, so classifying one threw. The entry point is last, a command-level test reads a usage-limit turn as `LIMIT`, and a contract test refuses a top-level `const`, `let` or `class` below a top-level `await main()` in any plugin script.
- A watcher `IDLE` line says `NEEDS-REPLY` when any line of the final message asks a question or asks for permission, authorization or approval, not only when the message ends in one. A writer that asked "May I ...?" and ended on a status sentence waited unread.

#### Added
- A thread the App Server unloaded (`notLoaded`) after its turn finished reads `IDLE` with its final message, the same as a loaded one; a test proves it, so no code change was needed.
- `liveness.mjs` adds `MODEL(<model>/<effort>!=<want>)` to a session whose thread runs on another model or effort than `CODEX_MODEL` and `CODEX_EFFORT`, and `fleet.sh` refuses a new Codex session without a model as well as an effort.
- A watched PR line carries `threads=<n>` unresolved review threads and ends `BLOCKED(unresolved threads)` while an open PR has any, apart from its review decision and checks; approved PRs with auto-merge armed once sat blocked for hours.
- `watch.sh` holds a lock per work dir; a second copy (one started with `&`) exits 3 and says to start it with the harness's background option, and a lock whose pid died is taken over.
- fleet-orchestrator: `board.mjs duty` gives each standing duty (main's post-merge health, a PR shepherd, the watcher) an owner, `status` prints `UNOWNED-DUTY`, and `helpers.mjs stopped` refuses a helper that owns one. `status` also prints `SLOW` for a Codex writer 30 minutes without a push while its PR has unresolved human threads, the cue to hand it to a Claude writer.
- `executed-check.mjs` takes `--kind plan|prd`; a PRD also needs `--children` and is refused while any PRODUCES child is not DONE or CANCELED.
- `review-page-check.mjs` refuses a decision row without `checked in code: <file>:<line>`, and vibe's `session-questions.mjs ask` refuses a request for permission to do in-scope work (a test repair, a main merge, a push, a fix); the change worker does that work and reports it.
- `lessons-pr.mjs` names an open lessons PR to push onto before another is opened.
- Every PLN vibe publishes starts with a `Self-approved` line (`session-plan.mjs`), since vibe sets a PLN's status but never its approver; the ticket worker sets `APPROVED` only on a plan with a human approver or that line. `rulings-check.mjs --plan` reads a PLN's `get-document` JSON: an `APPROVED` or `EXECUTED` plan with no human approver fails without the line, and any plan with no human approver fails on each out-of-scope, non-goal, excluded, deferred, descoped, obsolete, not-covered or GAP item that cites no ticket AC, PRD or operator ruling. The ticket worker runs it before setting a PLN `APPROVED`, handoff runs it on every linked PLN, and fleet-orchestrator runs it at dispatch and QA prep and states the operator's scope-source ruling; takeover and resume prompts make the incoming writer trace every inherited exclusion to a human source or build or escalate it.
- fleet-orchestrator prose: near a Codex usage limit, tell the operator with the available resets and, when they are away, move the busiest writers to Claude writers; group bug fixes into one umbrella ticket with PRs split by code area; work outside the program goes to its owner as a ticket.

### code-review v3.13.0

#### Fixed
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.74",
"version": "1.0.75",
"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.74",
"version": "1.0.75",
"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
7 changes: 6 additions & 1 deletion plugins/vibe/agents/vibe-change-worker.md
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,12 @@ handoff the missing tests list: `STORIES_READY` names the new Storybook
components' story IDs and continues the same request; `NEEDS_STORYBOOK`
asks for placement advice and continues the same request; `NEEDS_REVIEW` waits for handoff's implementation/coverage review;
`NEEDS_PERSON` is only a researched necessary product question or legitimate
human-only action, never a test or engineering choice; `NEEDS_COMMIT` waits
human-only action, never a test or engineering choice. Never stop for
authorization on in-scope work (repairing tests your change broke, a main
merge, a push, an in-scope fix): do it and report it afterwards; only a flag
key, a product question, dropping or narrowing scope, a destructive action
or a credential waits, and
`session-questions.mjs ask` refuses any other permission question. `NEEDS_COMMIT` waits
for the orchestrator; `DONE` means the whole requested feature passed its
quick checks in the loop, or its checks and reviews at handoff, and always
carries the Acceptance criteria block: each touched criterion's ID, quoted
Expand Down
12 changes: 11 additions & 1 deletion plugins/vibe/agents/vibe-ticket-worker.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,16 @@ F. Status. The PLN's status follows the work:
external dependency evidence. A ticket that still says the collector,
selector, monitor or other dependency is missing keeps that task open and
the PLN `DRAFT`; return `NEEDS_CHANGE` rather than approving it.
You never set a PLN's approver, so the `APPROVED` you set means built
and checked, never a human approval: set it only when `get-document`
shows a human `approverId`, or when the published text starts with the
`Self-approved` line `session-plan.mjs` renders (version an older text
in D first). Before setting it, save the PLN's
`get-document` JSON and its latest text and run
`node ../skills/handoff/scripts/rulings-check.mjs --plan <json> --plan-body <text>`;
each exclusion it names (out of scope, deferred, not covered, a GAP line)
goes back to the writer as `NEEDS_CHANGE`: cite the ticket AC, PRD or
operator ruling behind it, or bring the work back into scope.

## Create mode

Expand Down Expand Up @@ -216,7 +226,7 @@ With `mergeCheck: true`, change nothing else: run
`gh pr list --repo closedloop-ai/symphony-alpha --head <session branch> --state merged --json number,url,mergedAt`
from the worktree. When it lists a merged pull request and the PLN is not
`EXECUTED`, run
`node ../skills/handoff/scripts/executed-check.mjs --file <its saved latest text> --last-merge <mergedAt>`;
`node ../skills/handoff/scripts/executed-check.mjs --kind plan --file <its saved latest text> --last-merge <mergedAt>`;
when it fails, append a version adding the section it names (the session's
flag keys, each read now from the flag service), and rerun it. Only once it
passes, set `EXECUTED` through step F (with `expectedStatus` the status you
Expand Down
12 changes: 8 additions & 4 deletions plugins/vibe/skills/codex-fleet-monitor/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ WATCH_NEW_PR_REPOS="owner/repo" # optional: list open PRs here to spot new ones
PR_SEARCH="head:feature/" # optional: search filter for that list
BOT_LOGIN_REGEX="vercel|codex|github-actions|copilot|bot" # authors not counted as human feedback
# USAGE_HIGH_PERCENT=80 # usage below this prints one band (default MOVE_AT_PERCENT, else 80)
# CODEX_MODEL=<model> # with CODEX_EFFORT, a thread on another model or effort gets a MODEL(...) word
# CODEX_EFFORT=high
RECOVERY_BACKOFF_SECONDS="120 300 600" # wait before each resume of a transient failure; one retry per value
# CONTINUE_PROMPT_FILE=/path/to/continue.md # replaces the built-in continue prompt (REMOTE_CONTINUE_PROMPT_FILE on REMOTE_HOST)
# AUTO_RESTART_DAEMON=1 # allow restarting the App Server daemon for threads stuck in systemError; off by default
Expand Down Expand Up @@ -63,14 +65,15 @@ The label is what appears in the output lines.
scripts/watch.sh <workDir> [intervalSeconds]
```

Start it with `run_in_background`. You are notified when it exits. Read its output, handle every line, then start it again; it only watches while running. An `IDLE` line includes the writer's final message tail and says `NEEDS-REPLY` when it ends in a question or permission request; classify that message before re-arming. `watch.sh` records its pid, each tick and its exit in `<workDir>/watcher.json`, and `node scripts/watcher.mjs <workDir>` prints `watcher ok` or `watcher MISSING(<why>)` (exit 1): check it before ending any turn that handled an event, and re-arm on `MISSING`. Editing `sessions.txt` or `prs.txt` takes effect on the next re-arm. `scripts/snapshot.sh <workDir>` prints the current state once.
Start it with `run_in_background`, never with `&`; a second copy for the same work dir exits 3 and says how to re-arm. You are notified when it exits. Read its output, handle every line, then start it again; it only watches while running. An `IDLE` line includes the writer's final message tail, the same once the App Server unloads the thread (`notLoaded`), and says `NEEDS-REPLY` when any of its lines asks a question or asks for permission, authorization or approval; classify that message before re-arming. `watch.sh` records its pid, each tick and its exit in `<workDir>/watcher.json`, and `node scripts/watcher.mjs <workDir>` prints `watcher ok` or `watcher MISSING(<why>)` (exit 1): check it before ending any turn that handled an event, and re-arm on `MISSING`. Editing `sessions.txt` or `prs.txt` takes effect on the next re-arm. `scripts/snapshot.sh <workDir>` prints the current state once.

## Reading the output

| Line | Meaning | Usual action |
|---|---|---|
| `session <where> <name> RUN` | turn in progress and producing items | none |
| `session ... IDLE` | turn completed; the session is waiting for you | read its last message, give it the next turn |
| `session ... MODEL(<model>/<effort>!=<want>)` | the thread runs on another model or effort than `CODEX_MODEL` and `CODEX_EFFORT` | restart it on the configured one; other models were far slower |
| `session ... STALLED(...)` | turn open but silent | check for a usage limit or a hung command; tell the user if it needs them |
| `session ... FAILED(LIMIT)` | turn errored on a usage limit | tell the user right away; a reset or second account is theirs to do |
| `session ... FAILED(TRANSIENT)` | turn errored on a transient provider failure: model at capacity, rate limit, 5xx or a dropped connection | none; recovery resumes it after the backoff (watch the `recovery` line) |
Expand All @@ -81,13 +84,14 @@ Start it with `run_in_background`. You are notified when it exits. Read its outp
| `supervisor ... STALE(>Ns)` / `MISSING` / `UNREADABLE` | no heartbeat for 3 intervals (a hung or dead supervisor), the lock exists with no heartbeat, or the heartbeat cannot be parsed | read `supervisor.log` there; restart the supervisor |
| `supervisor ... RSS-HIGH(>NMB)` | the supervisor's RSS is over `SUPERVISOR_RSS_CAP_MB` | read its hourly summary lines; restart it and report the leak |
| `session remote <host> UNREACHABLE` | ssh failed | check the host |
| `codex-usage <local\|remote> under80% ok resets=<n>` / `HIGH(<band>%)` | Codex account usage on that machine: one band below `USAGE_HIGH_PERCENT` (80 here) used, so ordinary use is not an event, then the used percent floored to 10; `resets` is the count of available limit resets | none below it; on `HIGH`, warn the user with the rate |
| `codex-usage <local\|remote> under80% ok resets=<n>` / `HIGH(<band>%)` | Codex account usage on that machine: one band below `USAGE_HIGH_PERCENT` (80 here) used, so ordinary use is not an event, then the used percent floored to 10; `resets` is the count of available limit resets | none below it; on `HIGH`, warn the user at once with the band and `resets`, since applying a reset is theirs; if they are away, move the busiest writers to Claude writers |
| `codex-usage ... LIMIT` / `LIMIT-STOPPED` | usage limit reached; `LIMIT` means ordinary usage is still allowed, `LIMIT-STOPPED` means sessions cannot run | tell the user right away; a reset or second account is theirs to do |
| `codex-usage ... LIMIT-SPENDING-CREDITS` | limit reached and turns are now consuming paid credits | tell the user at once and stop sending turns unless they approve paid credits |
| `codex-usage ... unknown` / `unreadable READFAIL` | the App Server answered without rate limits, or it or the remote host could not be read | check `codex` on that machine |
| `requests <local\|remote> <count> <newest file>` / `requests ... UNREADABLE` | a session dropped (or you removed) a delegated-work request file | hand the new request to a Claude worker, then steer its result back to the session that asked |
| `open-prs <repo> <numbers>` | open PR list changed | add the new PR to `prs.txt` and start its review |
| `pr ... <state> <head> <reviewDecision> <mergeable> comments= reviews= inline= checks=` | any field changed | new head: re-review; count up: read the feedback; `CONFLICTING`: tell the owning session to merge the base branch; checks: act on failures. Check counts change only once settled; while any check is pending the last settled counts show with ` (running)` |
| `pr ... BLOCKED(unresolved threads)` | an open PR has unresolved review threads (`threads=<n>`), which block its merge even when it is approved with auto-merge armed | treat each thread as work: route it to the owning writer now, and never read the block as a missing approval |
| `pr ... <state> <head> <reviewDecision> <mergeable> comments= reviews= inline= threads= checks=` | any field changed | new head: re-review; count up: read the feedback; `CONFLICTING`: tell the owning session to merge the base branch; checks: act on failures. Check counts change only once settled; while any check is pending the last settled counts show with ` (running)` |

Surface everything that needs a human to the user immediately: limits, paid-credit spending, stalls, failures, and new human review comments.

Expand Down Expand Up @@ -130,4 +134,4 @@ Every action is an `EVENT recovery ...` line in the events log, which the `recov
- Item history lives in `<workDir>/item-history.txt`; delete it to reset stall timers. Settled check counts live in `<workDir>/checks-*.txt`. The `keep-*`, `merge-*` and `checks-*` files of a PR are removed once it leaves `prs.txt`.
- On the remote host the scripts are copied to `<REMOTE_STATE_DIR>/.fleet-monitor/` on every snapshot.
- A failed or empty `gh` read repeats that field's last value, so an API blip is not an event.
- Don't run two copies on the same work dir.
- `watch.sh` holds `<workDir>/watch.lock`; a second copy exits 3, and a lock whose pid died is taken over.
6 changes: 4 additions & 2 deletions plugins/vibe/skills/codex-fleet-monitor/scripts/classify.sh
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
#!/bin/zsh
# stdin: "<where> <stateName> <turnStatus> <progress> <flag>". $1: item history file. $2: stall seconds.
# A running turn whose progress (its newest item) has not changed for $2 seconds is STALLED (a Codex usage
# limit leaves the turn open and silent, so "turn is open" alone is not liveness).
# limit leaves the turn open and silent, so "turn is open" alone is not liveness). A MODEL(...) word that
# liveness.mjs put first in the tail (a thread on another model or effort) is kept for every state.
H=$1; STALL=${2:-720}; touch $H; now=$(date +%s)
while read where n st items flag tail; do
prev=$(grep "^$where:$n " $H | awk '{print $2, $3}'); pi=${prev% *}; pt=${prev#* }
if [ -z "$prev" ] || [ "$pi" != "$items" ]; then pt=$now; fi
grep -v "^$where:$n " $H > $H.tmp; echo "$where:$n $items $pt" >> $H.tmp; mv $H.tmp $H
drift=; if [[ $tail == MODEL\(* ]]; then drift=${tail%% *}; tail=${tail#$drift}; tail=${tail# }; fi
if [ "$flag" != "-" ]; then s="FAILED($flag)"
elif [ "$st" = inProgress ] && [ $((now - pt)) -ge $STALL ]; then s="STALLED(no progress $((STALL / 60))m+)"
elif [ "$st" = inProgress ]; then s=RUN
elif [ "$st" = completed ]; then s="IDLE${tail:+ $tail}"
else s="STOPPED($st)"; fi
echo "session $where $n $s"
echo "session $where $n $s${drift:+ $drift}"
done
Loading
Loading