From a913bfc2495f9b9ffeaa3352297498a9b9b24a7c Mon Sep 17 00:00:00 2001 From: Daniel Ochoa Date: Wed, 21 Oct 2026 23:26:25 -0500 Subject: [PATCH 1/5] fix(vibe): encode the open 2026-10-22 fleet lessons - liveness.mjs awaited main() above the constants that classify turn errors, so the watcher printed READFAIL for every failed turn; the entry point is last and a contract test refuses the shape anywhere. - NEEDS-REPLY catches a question or a permission, authorization or approval request on any line of an idle writer's final message. - A notLoaded thread whose turn finished reads IDLE with its message, proven with a test against the same App Server reads. - The watcher flags MODEL(...) drift from CODEX_MODEL and CODEX_EFFORT; a new Codex session needs a model as well as an effort. - watch.sh refuses a second copy per work dir. - board.mjs: standing duties with owners (UNOWNED-DUTY; helpers.mjs refuses to stop an owner) and SLOW Codex writers with unresolved human threads and no push for 30 minutes. - executed-check.mjs --kind prd refuses open PRODUCES children; review-page-check.mjs needs code evidence on decisions; session-questions.mjs refuses permission for in-scope work; lessons-pr.mjs finds the open lessons PR. - Short prose for the usage reset rule, grouped bug fixes and work outside the program; vibe 1.0.75 and its changelog. Testing: node --test on each changed suite; each new check shown failing with its fix removed; full suites run on a second machine. Risks: a new Codex session without CODEX_MODEL now refuses to start. --- CHANGELOG.md | 18 +++ plugins/vibe/.claude-plugin/plugin.json | 2 +- plugins/vibe/.codex-plugin/plugin.json | 2 +- plugins/vibe/agents/vibe-change-worker.md | 6 +- plugins/vibe/agents/vibe-ticket-worker.md | 2 +- .../vibe/skills/codex-fleet-monitor/SKILL.md | 9 +- .../codex-fleet-monitor/scripts/classify.sh | 6 +- .../codex-fleet-monitor/scripts/liveness.mjs | 47 +++++-- .../scripts/liveness.test.mjs | 109 +++++++++++++++- .../codex-fleet-monitor/scripts/snapshot.sh | 3 +- .../codex-fleet-monitor/scripts/watch.sh | 14 ++ .../scripts/watcher.test.mjs | 41 ++++++ .../vibe/skills/fleet-orchestrator/SKILL.md | 19 ++- .../references/templates.md | 5 +- .../fleet-orchestrator/scripts/board.mjs | 120 ++++++++++++++++-- .../fleet-orchestrator/scripts/board.test.mjs | 59 +++++++++ .../scripts/codex-session.mjs | 4 +- .../scripts/codex-session.test.mjs | 9 +- .../fleet-orchestrator/scripts/helpers.mjs | 18 +++ .../fleet-orchestrator/scripts/lessons-pr.mjs | 46 +++++++ .../scripts/lessons-pr.test.mjs | 37 ++++++ .../scripts/review-page-check.mjs | 10 +- .../scripts/review-page-check.test.mjs | 12 +- .../skills/handoff/scripts/executed-check.mjs | 33 ++++- .../handoff/scripts/executed-check.test.mjs | 29 ++++- plugins/vibe/skills/vibe/SKILL.md | 4 +- .../skills/vibe/scripts/session-questions.mjs | 17 +++ .../vibe/scripts/session-questions.test.mjs | 24 +++- .../python/test_vibe_1075_lessons_contract.py | 89 +++++++++++++ 29 files changed, 733 insertions(+), 61 deletions(-) create mode 100644 plugins/vibe/skills/fleet-orchestrator/scripts/lessons-pr.mjs create mode 100644 plugins/vibe/skills/fleet-orchestrator/scripts/lessons-pr.test.mjs create mode 100644 plugins/vibe/tools/python/test_vibe_1075_lessons_contract.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 256d7bd7..7fe710bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,24 @@ 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. +- `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. +- 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. + ### vibe v1.0.74 Signal safety after a plugin test ended every process on an operator's machine, the open 2026-10-21 fleet lessons, and Storybook guard rules found by the 2026-10-21 audit (#8953, #8959, #8960, #8962, #8963, #8980). Each lesson has a mechanical check where one fits. 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..ed6c8705 100644 --- a/plugins/vibe/agents/vibe-change-worker.md +++ b/plugins/vibe/agents/vibe-change-worker.md @@ -564,7 +564,11 @@ 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, 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..5461e0af 100644 --- a/plugins/vibe/agents/vibe-ticket-worker.md +++ b/plugins/vibe/agents/vibe-ticket-worker.md @@ -216,7 +216,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..dc2e5da3 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,7 +84,7 @@ 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 | @@ -130,4 +133,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 "