diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b08daf0..0041d172 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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(/!=)` 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=` 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: :`, 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 diff --git a/plugins/vibe/.claude-plugin/plugin.json b/plugins/vibe/.claude-plugin/plugin.json index b3cd03a0..01fa9fa3 100644 --- a/plugins/vibe/.claude-plugin/plugin.json +++ b/plugins/vibe/.claude-plugin/plugin.json @@ -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"} } diff --git a/plugins/vibe/.codex-plugin/plugin.json b/plugins/vibe/.codex-plugin/plugin.json index 6e13cb31..a0162987 100644 --- a/plugins/vibe/.codex-plugin/plugin.json +++ b/plugins/vibe/.codex-plugin/plugin.json @@ -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", diff --git a/plugins/vibe/agents/vibe-change-worker.md b/plugins/vibe/agents/vibe-change-worker.md index 085eb2ee..f6c8a165 100644 --- a/plugins/vibe/agents/vibe-change-worker.md +++ b/plugins/vibe/agents/vibe-change-worker.md @@ -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 diff --git a/plugins/vibe/agents/vibe-ticket-worker.md b/plugins/vibe/agents/vibe-ticket-worker.md index 8d0658b6..fe32a00d 100644 --- a/plugins/vibe/agents/vibe-ticket-worker.md +++ b/plugins/vibe/agents/vibe-ticket-worker.md @@ -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 --plan-body `; + 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 @@ -216,7 +226,7 @@ With `mergeCheck: true`, change nothing else: run `gh pr list --repo closedloop-ai/symphony-alpha --head --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 --last-merge `; +`node ../skills/handoff/scripts/executed-check.mjs --kind plan --file --last-merge `; 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 diff --git a/plugins/vibe/skills/codex-fleet-monitor/SKILL.md b/plugins/vibe/skills/codex-fleet-monitor/SKILL.md index 2b8a51ef..68161153 100644 --- a/plugins/vibe/skills/codex-fleet-monitor/SKILL.md +++ b/plugins/vibe/skills/codex-fleet-monitor/SKILL.md @@ -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= # 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 @@ -63,7 +65,7 @@ The label is what appears in the output lines. scripts/watch.sh [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 `/watcher.json`, and `node scripts/watcher.mjs ` prints `watcher ok` or `watcher MISSING()` (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 ` 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 `/watcher.json`, and `node scripts/watcher.mjs ` prints `watcher ok` or `watcher MISSING()` (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 ` prints the current state once. ## Reading the output @@ -71,6 +73,7 @@ Start it with `run_in_background`. You are notified when it exits. Read its outp |---|---|---| | `session 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(/!=)` | 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) | @@ -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 UNREACHABLE` | ssh failed | check the host | -| `codex-usage under80% ok resets=` / `HIGH(%)` | 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 under80% ok resets=` / `HIGH(%)` | 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 ` / `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 ` | open PR list changed | add the new PR to `prs.txt` and start its review | -| `pr ... 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=`), 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 ... 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. @@ -130,4 +134,4 @@ Every action is an `EVENT recovery ...` line in the events log, which the `recov - Item history lives in `/item-history.txt`; delete it to reset stall timers. Settled check counts live in `/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 `/.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 `/watch.lock`; a second copy exits 3, and a lock whose pid died is taken over. diff --git a/plugins/vibe/skills/codex-fleet-monitor/scripts/classify.sh b/plugins/vibe/skills/codex-fleet-monitor/scripts/classify.sh index 17eae1eb..f7e75d07 100755 --- a/plugins/vibe/skills/codex-fleet-monitor/scripts/classify.sh +++ b/plugins/vibe/skills/codex-fleet-monitor/scripts/classify.sh @@ -1,16 +1,18 @@ #!/bin/zsh # stdin: " ". $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 diff --git a/plugins/vibe/skills/codex-fleet-monitor/scripts/liveness.mjs b/plugins/vibe/skills/codex-fleet-monitor/scripts/liveness.mjs index 8b6b000f..429b7f72 100644 --- a/plugins/vibe/skills/codex-fleet-monitor/scripts/liveness.mjs +++ b/plugins/vibe/skills/codex-fleet-monitor/scripts/liveness.mjs @@ -2,6 +2,7 @@ // A session is "" (reads /.json, which holds a threadId) or "