Skip to content

feat: reaction engine — auto-poll GitHub, match events, execute actions - #1

Merged
tu11aa merged 1 commit into
mainfrom
feature/reaction-engine
Apr 6, 2026
Merged

feat: reaction engine — auto-poll GitHub, match events, execute actions#1
tu11aa merged 1 commit into
mainfrom
feature/reaction-engine

Conversation

@tu11aa

@tu11aa tu11aa commented Apr 6, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds a reactor workspace that periodically polls GitHub Issues/PRs and captain status, matches events against declarative rules in reactions.json, and executes actions automatically
  • New CLI: cockpit reactor {add,remove,list,check,status,config,reset}add auto-detects GitHub owner/repo from git remote
  • 8 built-in reaction rules: issue delegation, CI failure notification, review feedback routing, stale captain escalation, auto-merge (opt-in)
  • Also includes prior session work: captain hard rules, OpenSpace-inspired learnings system, plugin skills architecture, session freshness, workspace pinning

Test plan

  • cockpit reactor status shows rules and gh CLI status
  • cockpit reactor add <project> auto-detects GitHub remote
  • cockpit reactor check --dry-run polls without executing
  • cockpit launch --all spawns reactor alongside command and captains
  • cockpit reactor reset clears processed events state

…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
tu11aa merged commit a75cf52 into main Apr 6, 2026
@tu11aa
tu11aa deleted the feature/reaction-engine branch April 6, 2026 03:06
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 tu11aa mentioned this pull request May 5, 2026
2 tasks
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)
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)
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.
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".
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant