feat(stage7b): dual-target hook engine (shared logic core + CC hooks + opencode plugin) - #2
Merged
Conversation
Owner
Author
|
Paired Orbit deliverable (7h) landed as thomasluizon/orbit-ui-mobile#441 — ports Orbit's CC hooks to |
thomasluizon
changed the base branch from
chore/stage7a-purge-codex-refresh-core
to
main
July 10, 2026 01:21
…+ opencode plugin)
Stage 7b of the harness-upgrade plan: enforceable invariants written ONCE,
tool-neutrally, and enforced in BOTH Claude Code and opencode off one shared
logic core. No SDLC policy is a constant anywhere in the engine; it all comes
from hooks.policy.json at runtime, which is why the extended genericity gate can
guarantee zero leakage.
Shared logic core (core/hooks/logic/, pure + dependency-free):
- git-action.mjs evaluateGitCommand(command, policy, ctx): protected-ref push
(explicit + bare-on-protected via injected resolver), the git bypass flags,
no-gpg-sign, branch-name pattern (+ exceptions), ticket ref, forbidden
trailers, large-binary add. Every literal comes from policy; none is baked in.
- content-scan.mjs scanContent(text, policy, filePath) + checkLargeBinary:
em dash (numeric en-dash allowed), banned phrases, secrets (conservative
universal default set + project extras), per-rule path scopes/exceptions.
- proactivity.mjs reminder line + judge-prompt builder + verdict parser +
transcript-turn slicer + skill-digest loader (the pure guard building blocks).
- payload.mjs normalize a Claude Code OR opencode payload into one neutral
record so the logic never learns a tool's field names.
- scope.mjs dependency-free glob->RegExp (**, *, ?, {a,b}) + path-scope match.
- config.mjs loadPolicy(startDir): DEFAULT_POLICY < global < project (JSON only,
no YAML at runtime). Defaults are universally-safe only.
Template library (core/hooks/templates.mjs): 11 parameterized invariants with
config bindings, defaults, path-scope/exception support, and a gateTable() for
the (later) decomposition gate.
Two thin adapters off the one core:
- adapters/claude-code/hooks/: git-guardrails, content-guard, proactivity-
reminder (UserPromptSubmit), proactivity-guard (Stop, model-configurable),
+ settings.hooks.json wiring snippet. Read stdin -> logic -> exit 2 / block.
- adapters/opencode/plugin/agentic-harness.js: tool.execute.before -> throw
(FULL enforcement parity), event/session.idle guard (best-effort; opencode
cannot rewind a finished turn). Core resolved via the runtime project
directory (bundling-proof) with import.meta.url fallbacks.
Stack-detected lint/analyzer generators (core/hooks/lint-generators/):
detect + eslint/roslyn/ruff generators + strongestLayerFor() routing a
code-level policy to a real linter rule, content-scan hook only as fallback.
Folded in (deferred from 7a): adapters/claude-code/workflows/audit.mjs - the
genericized Workflow-audit accelerator, all repos/surfaces/ladders/checklists
driven by args (Workflow scripts have no fs/Node access), zero project constants.
Wiring:
- check-genericity.mjs now also scans the authored engine adapters.
- gen-adapters.mjs scopes its wipe to skills/+agents/ so the authored engine
dirs survive regeneration.
- install.mjs vendors the engine dirs (hooks/workflows -> .claude, plugin ->
.opencode) beside the vendored core.
- CI runs check-genericity + the hook-engine proof + an adapters-in-sync check.
- .gitattributes pins engine files to LF; hooks.policy.example.json + a hooks:
block in workflow.config.example.yaml document the policy surface; README +
core/hooks/README.md document the engine.
Proof (scripts/test-hook-engine.mjs, CI-gated): 42 assertions. Vendors the pack
into a temp project and runs the REAL CC hook AND the REAL opencode plugin - a
push to a protected branch is blocked by both, a feature-branch push allowed by
both, an em dash caught by both - proving the dual-target wiring end to end over
one logic core. This starts 7h (the paired Orbit .opencode/plugin port lands as
a separate cross-linked orbit-ui-mobile PR).
Stacked on the 7a branch (chore/stage7a-purge-codex-refresh-core, unmerged);
retarget to main once 7a merges.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
thomasluizon
force-pushed
the
chore/stage7b-dual-target-hook-engine
branch
from
July 10, 2026 01:23
d405bed to
d023cdc
Compare
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.
Stage 7b — dual-target hook engine
Enforceable invariants written once, tool-neutrally, and enforced in both Claude Code and opencode off a single shared logic core. No SDLC policy is a constant anywhere in the engine; it all comes from
hooks.policy.jsonat runtime, which is why the extended genericity gate can guarantee zero leakage. Design perharness-upgrade-plan.mdStage 7 (checklist 7b; sections B + F). Locked design, implemented.Shared logic core —
core/hooks/logic/(pure, dependency-free)evaluateGitCommand(command, policy, ctx)— protected-ref push (explicit + bare-on-protected via an injected branch resolver), the git bypass flags, no-gpg-sign, branch-name pattern (+ exceptions), ticket ref, forbidden trailers, large-binary add. Every literal is a policy field; none is baked in.scanContent+checkLargeBinary— em dash (numeric en-dash allowed), banned phrases, secrets (conservative universal set + project extras), each rule independently path-scoped.**,*,?,{a,b}) + scope/exception match.loadPolicy—DEFAULT_POLICY < global < project, JSON only (no YAML at runtime).Template library —
core/hooks/templates.mjs11 parameterized invariants with config bindings, defaults, path-scope/exception support, and a
gateTable()for the later decomposition gate.Two thin adapters off the one core
adapters/claude-code/hooks/— git-guardrails, content-guard, proactivity-reminder (UserPromptSubmit), proactivity-guard (Stop, model-configurable) + asettings.hooks.jsonwiring snippet. stdin → logic →exit 2/ block.adapters/opencode/plugin/agentic-harness.js—tool.execute.before→ throw (full enforcement parity);event/session.idleguard (best-effort — opencode cannot rewind a finished turn). Core resolved via the runtime projectdirectory(bundling-proof) withimport.meta.urlfallbacks. opencode plugin API verified against the live@opencode-ai/pluginSDK (.opencode/plugin/singular;tool.execute.before(input, output); block by throw;event→session.idle).Strongest-layer generators —
core/hooks/lint-generators/detect+ eslint/roslyn/ruff generators +strongestLayerFor()routing a code-level policy to a real linter rule where the stack supports it; the content-scan hook is the fallback only where no linter can express it.Folded in (deferred from 7a)
adapters/claude-code/workflows/audit.mjs— the genericized Workflow-audit accelerator. All repos/surfaces/ladders/checklists arrive viaargs(Workflow scripts have no fs/Node access), zero project constants.Wiring
check-genericity.mjsnow also scans the authored engine adapters.gen-adapters.mjsscopes its wipe toskills/+agents/so the authored engine dirs survive regeneration.install.mjsvendors the engine dirs beside the vendored core..gitattributes(LF),hooks.policy.example.json, ahooks:block in the config example, andcore/hooks/README.mddocument the surface.Proof —
scripts/test-hook-engine.mjs(CI-gated, 42 assertions)Vendors the pack into a temp project and runs the real CC hook and the real opencode plugin: a push to a protected branch is blocked by both, a feature-branch push allowed by both, an em dash caught by both — the dual-target wiring proven end to end over one logic core. This starts 7h; the paired Orbit
.opencode/pluginport lands as a separate cross-linkedorbit-ui-mobilePR.🤖 Generated with Claude Code