Skip to content

feat(stage7b): dual-target hook engine (shared logic core + CC hooks + opencode plugin) - #2

Merged
thomasluizon merged 1 commit into
mainfrom
chore/stage7b-dual-target-hook-engine
Jul 10, 2026
Merged

feat(stage7b): dual-target hook engine (shared logic core + CC hooks + opencode plugin)#2
thomasluizon merged 1 commit into
mainfrom
chore/stage7b-dual-target-hook-engine

Conversation

@thomasluizon

Copy link
Copy Markdown
Owner

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.json at runtime, which is why the extended genericity gate can guarantee zero leakage. Design per harness-upgrade-plan.md Stage 7 (checklist 7b; sections B + F). Locked design, implemented.

Stacked on chore/stage7a-purge-codex-refresh-core (PR #1, still open). Base is that branch so the diff is 7b-only; retarget to main once 7a merges.

Shared logic core — core/hooks/logic/ (pure, dependency-free)

  • git-action 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.
  • content-scan scanContent + checkLargeBinary — em dash (numeric en-dash allowed), banned phrases, secrets (conservative universal set + project extras), each rule independently path-scoped.
  • proactivity — reminder line + judge-prompt builder + verdict parser + transcript-turn slicer + skill-digest loader.
  • payload — normalize a Claude Code OR opencode payload into one neutral record.
  • scope — dependency-free glob→RegExp (**, *, ?, {a,b}) + scope/exception match.
  • config loadPolicyDEFAULT_POLICY < global < project, JSON only (no YAML at runtime).

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) + a settings.hooks.json wiring snippet. stdin → logic → exit 2 / block.
  • adapters/opencode/plugin/agentic-harness.jstool.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. opencode plugin API verified against the live @opencode-ai/plugin SDK (.opencode/plugin/ singular; tool.execute.before(input, output); block by throw; eventsession.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 via 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 beside the vendored core.
  • CI runs genericity + the hook-engine proof + an adapters-in-sync check.
  • .gitattributes (LF), hooks.policy.example.json, a hooks: block in the config example, and core/hooks/README.md document 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/plugin port lands as a separate cross-linked orbit-ui-mobile PR.

🤖 Generated with Claude Code

@thomasluizon

Copy link
Copy Markdown
Owner Author

Paired Orbit deliverable (7h) landed as thomasluizon/orbit-ui-mobile#441 — ports Orbit's CC hooks to .opencode/plugin/ via the same shared-logic-core + adapter pattern proven here.

@thomasluizon
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
thomasluizon force-pushed the chore/stage7b-dual-target-hook-engine branch from d405bed to d023cdc Compare July 10, 2026 01:23
@thomasluizon
thomasluizon merged commit b5664fa into main Jul 10, 2026
2 checks passed
@thomasluizon
thomasluizon deleted the chore/stage7b-dual-target-hook-engine branch July 10, 2026 01:24
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