Skip to content

Add Codex plugin sharing a host-agnostic stop-hook core - #6

Merged
vincentmvdm merged 14 commits into
mainfrom
cursor/add-codex-plugin-f61f
Jun 3, 2026
Merged

vincentmvdm merged 14 commits into
mainfrom
cursor/add-codex-plugin-f61f

Conversation

@vincentmvdm

@vincentmvdm vincentmvdm commented Jun 3, 2026 •

Copy link
Copy Markdown
Contributor

Tip

🎬 Watch the walkthrough: Add Codex plugin with host-agnostic stop-hook core

Generated by Mainframe. Mainframe automatically generates videos when new pull requests are opened. Configure here.


What

Adds a Mainframe Codex plugin that works identically to the Cursor plugin: the same share-video skill, the same hosted MCP wiring, and an AFK stop hook. The change is built DRY on a host-agnostic core shared by both hosts (and ready for a future Claude plugin).

How Codex support works (researched from current Codex docs + source)

  • Plugin manifest: Codex has a first-class plugin system with a manifest at .codex-plugin/plugin.json (parallel to Cursor's .cursor-plugin/plugin.json). Unknown top-level fields are ignored, so the shared identity metadata is safe to include.
  • Stop hook: Codex's lifecycle hook engine has a Stop event. The hook reads { transcript_path, stop_hook_active, ... } on stdin and returns { "decision": "block", "reason": ... } to continue an away session with reason as a fresh prompt — the analog of Cursor's followup_message. stop_hook_active is the loop guard.
  • MCP is fully shared: Codex's plugin .mcp.json parser accepts the mcpServers (camelCase) wrapper and remote url (streamable HTTP) servers, so Cursor's existing ./.mcp.json is reused unchanged.
  • Distribution: .agents/plugins/marketplace.json exposes the plugin via a Git-URL source rooted at the repo, so the Codex plugin shares skills/, ./.mcp.json, hooks/, and dist/ with Cursor instead of duplicating them.

Changes

  • Shared core (hooks/core/): transcript.ts now holds only host-agnostic primitives (timestamp + Mainframe-URL parsing, file reading, summary shaping); stop-policy.ts exposes decideStop(summary, stopTimeMs).
  • Cursor (hooks/cursor/): transcript parsing moved out of core/ into cursor/transcript.ts; behavior is unchanged and .cursor-plugin/plugin.json is byte-identical.
  • Codex (hooks/codex/): transcript.ts parses the Codex rollout JSONL ({ timestamp, type, payload }), recognizing event_msg/user_message and response_item/function_call, ignoring unrelated event types, and failing closed on corruption or a missing session_meta. stop-evaluator.ts + stop.ts + hooks.json implement the Stop contract.
  • Generation: tooling/generate.ts emits .codex-plugin/plugin.json and .agents/plugins/marketplace.json from the shared metadata; package surface, drift check, release archive, and mark-executable cover both hosts.
  • Docs: README.md and AGENTS.md updated for the Cursor + Codex scope.

The stop-hook suggestion copy still contains only elapsed hours — never transcript content, prompts, paths, or secrets.

Testing

  • bun run verify — green (66 tests, typecheck, lint, format, generated-drift, build:check).
  • bun run archive:release — green; the tgz contains both plugins' surfaces.
  • End-to-end smoke test of the compiled hooks via stdin:
    • Codex hook returns {"decision":"block","reason":"...hours..."} when the user is away with work and no prior share; returns {} for the loop guard (stop_hook_active: true), non-Stop events, and a null transcript.
    • Cursor hook still returns {"followup_message":"..."} (no regression).

I could not exercise a live Codex install in this environment; the manifest, marketplace, hook, and MCP shapes were validated against the current Codex docs and openai/codex source, plus strict schema tests over the generated files.

Open in Web Open in Cursor 

Codex now installs the Mainframe plugin with the same share-video skill,
hosted MCP wiring, and AFK stop hook as Cursor. The Codex Stop hook returns
`{ decision: "block", reason }` to nudge an away session toward a video
summary, guarded by `stop_hook_active` to avoid continuation loops.

Refactor the stop-hook runtime so hosts share one core:
- `hooks/core/transcript.ts` holds host-agnostic primitives (timestamp and
  Mainframe-URL parsing, file reading, summary shaping).
- Cursor transcript parsing moves to `hooks/cursor/transcript.ts`; Codex
  rollout parsing lives in `hooks/codex/transcript.ts`, ignoring unrelated
  event types while failing closed on corruption or a missing `session_meta`.
- `stop-policy` exposes `decideStop(summary, stopTimeMs)` shared by both hosts.

`tooling/generate.ts` emits `.codex-plugin/plugin.json` and the
`.agents/plugins/marketplace.json` catalog (a Git-URL source rooted at the
repo so both plugins share `skills/`, `./.mcp.json`, and `dist/`). The package
surface, drift check, and release archive now cover both hosts.
Describe installing the Codex plugin via `codex plugin marketplace add` and
note the shared share-video skill, MCP wiring, and stop hook. Update the agent
notes to cover both supported hosts and the generated metadata locations.
Comment thread hooks/codex/transcript.ts Resolved
Comment thread hooks/codex/transcript.ts Resolved
Code-quality pass from the thermo-nuclear review. The file/text read,
size-guard, and summary-shaping skeleton was copy-pasted in both
hooks/cursor/transcript.ts and hooks/codex/transcript.ts, differing only in the
row parser — duplication that a third host would copy again.

Lift that skeleton into core `summarizeTranscript(text, parseRows)` and
`summarizeTranscriptFile(path, parseRows)` parameterized by a named
`TranscriptRowParser`. Each host now contributes only its parser behind a thin
named binding, making "a host is a row parser" the explicit extension point.
`readTranscriptText`, `MAX_TRANSCRIPT_BYTES`, and the summary shaping become
private to core. Also simplify Codex `classifyCodexRow` to a plain string union
instead of payload-free `{ kind }` objects. Behavior is unchanged.
Acts on the second code-quality review (no blockers; DRY sharpening + test
gaps).

- Extract the JSONL "parse or fail closed" scaffold (split, trim, `JSON.parse`,
  object-check) into core `parseJsonlRecords`, so the untrusted-transcript ->
  trusted-rows guarantee lives in one chokepoint for every host. This also
  narrows Cursor's previously over-broad try/catch to the parse step only.
- Add core `runStopHookCli(evaluate)` and make both host stop-evaluators pure
  (no stdin/stdout I/O), separating orchestration from policy.
- Close two Codex test gaps: work state resets per user turn, and a stop-level
  "no work since last user message" negative case.

Behavior unchanged; 68 tests pass and both compiled hooks were smoke-tested.
Follow-up to a code-quality review (APPROVE; no blockers). Add two Codex
parser tests that lock guarantees the review flagged as untested:
- a synthetic `response_item` user row injected after real work does not steal
  the turn (last user time and workHappened stay anchored to the real
  `event_msg/user_message`)
- a latest-user timestamp outside the supported epoch range fails closed to
  `missing-user-time` (so the hook never fires on a garbage timestamp)

Tests only; no behavior change. 70 tests pass.
Addresses two Bugbot findings on the Codex transcript parser:

- Work detection only recognized `function_call`, so sessions whose work ran
  through other Responses-API tool calls (`local_shell_call`,
  `custom_tool_call`, `web_search_call`, ...) reported no work and never
  triggered the share suggestion. Match the whole `*_call` family instead.
- User rows overwrote `lastUserTimeMs` without an ordering check, so a
  backwards timestamp could inflate the away duration and fire the hook
  spuriously. Fail closed on non-decreasing user timestamps, matching the
  Cursor parser and AGENTS.md's fail-closed rule.

Adds tests for both. 72 tests pass.
Self-review of the prior Bugbot fixes: the monotonic user-timestamp guard added
to the Codex parser was a verbatim copy of the Cursor parser's block, so the
same correctness-critical rule lived in two places. Extract it to core
`nextUserTimeMs(rawTimestamp, previousUserTimeMs)` and call it from both parsers,
leaving only the trivial per-host state assignments inline. One definition of
"fail closed when a user timestamp moves backwards" for every host.

Behavior unchanged; 72 tests pass.
@vincentmvdm

Copy link
Copy Markdown
Contributor Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 34f631f. Configure here.

Update the shared plugin description copy and regenerate the Cursor/Codex
manifests and marketplace metadata. Tests assert the new literal.
Drop implementation details (stop hook, MCP wiring, hook trust) from the
Cursor and Codex install sections; describe what the plugin gives users.
Short description: drop the "Mainframe" brand name (it doesn't tell a new
reader what the plugin does) -> "Create and share short video updates from
agent work."

Long description: replace the feature/implementation blurb with the value:
agents now outproduce what anyone can review, so the agent sends you a short
video of what it did, in your own voice and branding, shareable with your team.

Regenerated manifests/marketplace; tests assert the new literals.
Lead with what you get out of it ("have your agents record short videos so you
can stay up to date") rather than dwelling on the problem.
"Have your agents record..." read like an awkward imperative; use the plain
declarative "Your agents record short videos of what they did, so you can..."
Anchor the opener to the delivery moment ("When your agent finishes a task, it
sends you a short video of what it did...") and close on the benefit, replacing
the flatter "Your agents record..." opener.
Lead with the payoff ("watch what your agents did instead of reading through
all of it") and keep the voice/branding/glance/share specifics.
@vincentmvdm
vincentmvdm requested review from atlou and jordansinger June 3, 2026 22:03
@vincentmvdm
vincentmvdm marked this pull request as ready for review June 3, 2026 22:03
@vincentmvdm
vincentmvdm merged commit a87367b into main Jun 3, 2026
4 checks passed
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.

3 participants