feat: reaction engine — auto-poll GitHub, match events, execute actions - #1
Merged
Conversation
…ctions
Adds a reactor workspace that periodically polls GitHub Issues/PRs and
captain status, matches events against declarative rules in reactions.json,
and executes actions (delegate to captain, notify CI failures, escalate
stale captains, auto-merge).
New components:
- reactions.json config with 8 built-in rules
- reactor.CLAUDE.md template + reactor-ops skill
- Scripts: poll-github.sh, match-reactions.sh, execute-reaction.sh, reactor-cycle.sh
- CLI: cockpit reactor {add,remove,list,check,status,config,reset}
- cockpit launch --reactor / --all includes reactor
Also includes prior session work:
- Captain hard rules (mandatory crew delegation, close-out reporting)
- OpenSpace-inspired learnings (capture-skill, fix-skill, quality tracking)
- Plugin skills architecture (captain-ops, command-ops, daily-log, reactor-ops)
- Session freshness detection + auto-startup prompts
- Workspace pinning for command/captains/reactor
tu11aa
added a commit
that referenced
this pull request
Apr 21, 2026
* docs(spec): add plugin system phase 3 design — tracker slot Abstract GitHub gh-CLI behind a TrackerDriver interface mirroring runtime (phase 1) and workspace (phase 2). Thin primitive ops only (no cross-provider enrichment). Hybrid CLI: one-shot actions go through cockpit tracker; poll-github.sh stays raw as the canonical GitHub polling script. Normalized cross-tracker vocabulary in reactions.json deferred — tracked as #27 until a 2nd tracker provider ships. * chore: update gitnexus index markers * docs(plan): add tracker slot implementation plan 11-task plan for plugin system phase 3 — tracker abstraction. TDD flow for GitHubDriver + TrackerRegistry + in-memory test driver. Migrates execute-reaction.sh (auto-merge + auto-fix-ci paths) to cockpit tracker. poll-github.sh stays raw per spec §Non-Goals. Plan corrects design-spec error: feedback.ts does NOT use gh issue create (it opens a browser URL); stays untouched in phase 3. * chore: update gitnexus index markers * feat(tracker): add optional tracker field to config types * feat(tracker): add TrackerDriver interface * chore: update gitnexus index markers * feat(tracker): add GitHubDriver implementing TrackerDriver * chore: update gitnexus index markers * feat(tracker): add TrackerRegistry with project-level override * chore: update gitnexus index markers * feat(tracker): add trackers barrel and in-memory test driver * chore: update gitnexus index markers * feat(tracker): add 'cockpit tracker' CLI subcommand * chore: update gitnexus index markers * refactor(reactor): migrate auto-merge and auto-fix-ci to 'cockpit tracker' * chore: update gitnexus index markers * refactor(doctor): probe tracker providers via TrackerRegistry * chore: update gitnexus index markers * docs(readme): document tracker config field and CLI subcommand * fix(tracker): address pre-PR review items - CLI merge-pr --method now errors on invalid value (was silently normalizing to 'squash'). Matches ReactionRule.merge_method typing. - Registry.forProject throws actionable error pointing to reactions.json when owner/repo missing, instead of opaque 'scope.owner required' from GitHubDriver constructor. - Added test for the new missing-repo error path. Reviewer-flagged Important items #1 and #3.
tu11aa
added a commit
that referenced
this pull request
May 19, 2026
… Critical) project/id arrive unsanitized from the socket (dispatch+seed); a crafted '..'/'/'/absolute/NUL value let a confused-deputy read/write arbitrary files as the user (confirmed: wrote outside state root, status=arbitrary read primitive). Fix: safeSegment() rejects separators/traversal/NUL/dot at the single fs chokepoint + assertUnderRoot() defense-in-depth. 26 regression tests; legitimate uuid/hyphenated names unaffected. 115/115.
tu11aa
added a commit
that referenced
this pull request
May 28, 2026
…s fresh-install gap #1) (#144) Replace /Applications/cmux.app/Contents/Resources/bin/cmux hardcoding with resolveCmuxBin() that picks the first available: 1. COCKPIT_CMUX_BIN env var 2. cmuxBin field in ~/.config/cockpit/config.json 3. cmux on PATH (via which) 4. Fallback to ./Applications/... for backward compat Cached after first resolution. All 34 cmux runtime tests pass.
tu11aa
added a commit
that referenced
this pull request
Jun 14, 2026
…297) CC renders its input cursor via native ANSI terminal positioning — NOT a ▌ glyph. Confirmed by hex-capturing a live idle CC session: cursor at position 0 yields e2 9d af c2 a0 (❯ + NBSP) with no ▌ (e2 96 8c) anywhere. cmux read-screen therefore never produces ❯\xa0▌<real-text> when the user presses Ctrl-A/Home on a real draft. Heuristic #1 (/^[▌█]/ skip) is unreachable for that scenario → no false-positive draft clobber. Changes: - Update heuristic #1 comment to explain the ANSI cursor evidence and safety reasoning (instead of the prior inaccurate "nothing has been typed" framing) - Add regression test encoding the captured reality: cursor-at-start of real typed text still returns the draft unchanged (branch 2a per captain review)
6 tasks
tu11aa
added a commit
that referenced
this pull request
Jun 14, 2026
… (#297) * fix(relay): treat CC ghost-suggestion placeholder as empty input (#294) parseDraftFromScreen now correctly ignores Claude Code ghost suggestions that appear in the input box when nothing has been typed: 1. Leading cursor glyph (▌/█ at pos 0): cursor is at the start, meaning input is empty. Any following text is a CC ghost at the cursor position. 2. CC UI placeholder pattern `Press [key] to [action]`: appears in Working state when input is locked — "Press up to edit queued messages" etc. These are UI instructions, never real user-typed content. Without this fix, ghost text → DeferDelivery × 300 → ~5-min stall, then force-delivery backspaced + re-pasted the placeholder as if it were a real draft (writing the ghost text into the captain's input box). Signal chosen after investigation: cmux read-screen is plain text (no ANSI, no --raw flag). Cursor-position not queryable. Leading-cursor heuristic covers idle-state ghost; CC placeholder regex covers Working-state ghost. Both signals confirmed against live captured screen (docs/reports/294-ghost-placeholder-fixture.txt). Real typed drafts still return their text (regression guard for #258/#268). * test(relay): verify heuristic #1 safety for cursor-at-start scenario (#297) CC renders its input cursor via native ANSI terminal positioning — NOT a ▌ glyph. Confirmed by hex-capturing a live idle CC session: cursor at position 0 yields e2 9d af c2 a0 (❯ + NBSP) with no ▌ (e2 96 8c) anywhere. cmux read-screen therefore never produces ❯\xa0▌<real-text> when the user presses Ctrl-A/Home on a real draft. Heuristic #1 (/^[▌█]/ skip) is unreachable for that scenario → no false-positive draft clobber. Changes: - Update heuristic #1 comment to explain the ANSI cursor evidence and safety reasoning (instead of the prior inaccurate "nothing has been typed" framing) - Add regression test encoding the captured reality: cursor-at-start of real typed text still returns the draft unchanged (branch 2a per captain review)
This was referenced Jun 15, 2026
tu11aa
added a commit
that referenced
this pull request
Jul 11, 2026
…store role via sessionId Root cause (corrected — the earlier reconcileLiveness startedAt theory in this branch was wrong and does not explain the live incident): cmux's own hook-sessions record for the live bet2fun-app captain degraded to a bare launchCommand (`["claude"]`, no --append-system-prompt-file) after a crash/ reattach, so runLivenessTick's role-from-template classification read it as role:"unknown". Filtered out of the captain group, the project fell out of `seen`, and the very next tick markEnded() it — "stopped" forever, crews reaped, delivery permanently paused, even though the pid was alive the whole time (verified against the live cmux store data pulled from the running daemon). Two changes: 1. Root cause: sessionId identity now outranks a degraded/unclassifiable launchCommand. A record whose sessionId matches an already-known captain for its project is treated as that captain regardless of what roleFromTemplate currently reads. 2. Fail-safe (stands independently of #1): a captain missing from the runtime snapshot is no longer marked ended on absence alone. markEnded now requires positive evidence — the tracked pid confirmed dead via isPidAlive. If the pid is still alive, or unknown/null, the entry is left alone and a log line is emitted instead. This also gates the #324 reaper, which fires off the same derived state. Updated the two existing tests that encoded the old (buggy) behavior — "absent from snapshot marks ended/reaps even with isPidAlive() => true" — to require genuine death, and added regression tests reproducing the exact incident for both the role-restoration path and the fail-safe.
4 tasks
tu11aa
added a commit
that referenced
this pull request
Jul 11, 2026
…+ no-death-by-absence fail-safe (#568) * fix(#565): reconcileLiveness re-adopts a live captain over a dead prev reconcileLiveness's runtime/agent branch treated an incoming record with an older startedAt than the stored one as stale and discarded it outright — unless lastState was "end". That's correct when prev is alive (guards against a stale duplicate session clobbering a live one, #527), but wrong when prev is already stopped/gone: once a captain's liveness entry is wrongly marked dead, no future runtime signal for that same still-running session can ever win, because its startedAt never changes and will keep losing the ">=" comparison forever. The daemon then never re-adopts the captain, permanently pausing delivery. Reproduced against the live incident data (packages/core/src/liveness.ts) and added a regression test alongside a guard test for the #527 case. * fix(#565): never infer a captain dead from snapshot absence alone; restore role via sessionId Root cause (corrected — the earlier reconcileLiveness startedAt theory in this branch was wrong and does not explain the live incident): cmux's own hook-sessions record for the live bet2fun-app captain degraded to a bare launchCommand (`["claude"]`, no --append-system-prompt-file) after a crash/ reattach, so runLivenessTick's role-from-template classification read it as role:"unknown". Filtered out of the captain group, the project fell out of `seen`, and the very next tick markEnded() it — "stopped" forever, crews reaped, delivery permanently paused, even though the pid was alive the whole time (verified against the live cmux store data pulled from the running daemon). Two changes: 1. Root cause: sessionId identity now outranks a degraded/unclassifiable launchCommand. A record whose sessionId matches an already-known captain for its project is treated as that captain regardless of what roleFromTemplate currently reads. 2. Fail-safe (stands independently of #1): a captain missing from the runtime snapshot is no longer marked ended on absence alone. markEnded now requires positive evidence — the tracked pid confirmed dead via isPidAlive. If the pid is still alive, or unknown/null, the entry is left alone and a log line is emitted instead. This also gates the #324 reaper, which fires off the same derived state. Updated the two existing tests that encoded the old (buggy) behavior — "absent from snapshot marks ended/reaps even with isPidAlive() => true" — to require genuine death, and added regression tests reproducing the exact incident for both the role-restoration path and the fail-safe. * fix(#565): update CLI integration test for the intentional reap-contract change The daemon-direct reap test encoded the OLD contract: "captain absent from the runtime snapshot" alone was sufficient to reap a project's orphaned crews. That contract is what caused #565 — a single snapshot glitch (cmux degrading a session's launchCommand) silently reaped 3 live crews that were actively committing code. The previous commit changed the contract on purpose: reaping now requires positive evidence the tracked pid is actually dead, not just absent from one read. - Updated the existing test's fixture to simulate a genuine close (the snapshot goes empty AND the tracked pid is confirmed dead) — this is what actually happens when a user closes a captain workspace, so the real reap path is preserved and still covered. - Added the inverse case as its own test, reproducing the actual incident: captain absent from the snapshot but its pid still alive → the crew must NOT be reaped and its task stays "working".
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
reactions.json, and executes actions automaticallycockpit reactor {add,remove,list,check,status,config,reset}—addauto-detects GitHub owner/repo from git remoteTest plan
cockpit reactor statusshows rules and gh CLI statuscockpit reactor add <project>auto-detects GitHub remotecockpit reactor check --dry-runpolls without executingcockpit launch --allspawns reactor alongside command and captainscockpit reactor resetclears processed events state