Platform abstraction layer routing operations to Claude Code or OpenCode implementations
Resources
2Install
npx skillscat add cuioss/plan-marshall/platform-runtime Install via the SkillsCat registry.
Platform Runtime Skill
Script-based platform abstraction that routes 24 goal-based operations to the correct target implementation. Follows the tools-integration-ci pattern: one router script, target-specific provider classes, static routing via marshal.json.
Enforcement
Execution mode: Invoke scripts via executor notation; parse TOON output for status and route accordingly.
Prohibited actions:
- Do not call provider-specific scripts directly; all operations go through
platform_runtime.py - Do not invent script arguments not listed in the operations table
- Do not hard-code the target platform; routing is config-driven via
runtime.targetinmarshal.json - Do not implement ad-hoc TOON parsing; use the
ref-toon-formatparser module
Constraints:
- All commands use
python3 .plan/execute-script.py plan-marshall:platform-runtime:platform_runtime <operation> [args...] session render-titletakes no arguments; all resolution is internalno-opresponses are not errors; the calling skill must continue- See
standards/contract.mdfor per-operation TOON schemas
What This Skill Provides
Twenty-four operations covering the full platform lifecycle:
| Operation | Purpose |
|---|---|
project initial-setup |
One-time project setup: create .plan/, seed marshal.json, install platform hook |
project install-hook |
Install the terminal-title hook bundle; with the orthogonal --enforcement opt-in, install ONLY the PreToolUse enforcement hook entry without touching the terminal-title wiring |
layout skill-roots |
Resolve the ordered project-local skill root directories for the active target |
layout bundle-cache-root |
Resolve the deployed-bundle cache root directories for the active target |
session capture |
Persist current session id via manage-status; no-op on OpenCode |
permission configure |
Write raw permission list to platform settings |
permission analyze |
Read-only audit of permission hygiene, redundancy, and missing-steps |
permission fix |
Apply hygienic fixes: normalize, add, remove, ensure, consolidate |
permission ensure-wildcards |
Add marketplace bundle wildcard permissions |
permission ensure-steps |
Add missing skill permissions for marshal.json phase steps |
permission web-analyze |
Read-only analysis of WebFetch/webfetch domain permissions |
permission web-apply |
Add or remove web domain permissions |
session render-title |
Emit OSC title sequence from writer artifact; no-op on OpenCode |
session push-title-token |
Parse the store selector (--plan-id for the plan store, or --store orchestrator --slug {slug} for the orchestrator store) plus optional --icon, then bind and settle the title state for the next render event to deliver (Claude); no-op on OpenCode. This seam binds and persists — it does not repaint: the hook-written terminalSequence envelope is the sole delivery channel and is event-driven, so delivery is deferred to the next render. Shared by the manage-status phase-state-write drive seam, the lock/build state writers, and the plan-orchestrator per-verb call, whose bind_orchestrator side effect is the load-bearing reason the seam exists. Two no-op reasons: no_title_state and feature_inactive; the return carries no pushed and no delivery field |
session bind |
Bind the running session to --plan-id (last-driven-wins) so render-title / resolve-plan resolve it; no-op on OpenCode |
session resolve-plan |
Read the running session's bound plan id (the read side of session bind); no-op on OpenCode |
session doctor |
Visit every directory under the session-cache root, report plan-bound-by-multiple-sessions conflicts, stale slots, and orphan directories that yield no live slot, and (with --fix) GC the stale slots plus prune the orphan directories; no-op on OpenCode |
session teardown |
Activation-gated end-of-session retire: drop the session's binding. Releasing the binding is the whole of the teardown — no title reset is written, because a reset can only be delivered on the render channel. Fired only by SessionStart:clear, the sole release point; the archive path deliberately does not call it. A project with no terminal-title wiring reports active: false / reason: feature_inactive and is left untouched. No arguments; no-op on OpenCode |
session reload-directive |
Resolve + surface the harness-appropriate post-upgrade reload directive (Claude: /reload-plugins plus the monitor caveat); no-op (full-restart alternative) on OpenCode. RESOLVES + SURFACES only — a script cannot type a harness slash command |
metrics capture |
Record token consumption for a planning phase |
metrics normalized-tokens |
Resolve normalized transcript token totals for the active target |
subagent dispatch |
Return platform-specific subagent invocation parameters |
wait for |
Hold a bounded wait until a concrete, pollable observable (--observable names a kind from a closed set; build-job today) reaches a terminal state, and return a normalized succeeded/failed/timed_out/killed/pending outcome. The observable is never an opaque condition descriptor — a runtime subprocess cannot evaluate one. Bound exhaustion yields outcome: pending with terminal: false, never an implicit pass; no-op on OpenCode, whose runtime holds no wait channel |
health-check |
Verify platform integration |
See standards/contract.md for per-operation TOON schemas (success, error, no-op paths).
The health-check --checks display surface inspects each terminal-title render entry plus a dedicated PreToolUse:enforcement present/MISSING label for the orthogonal enforcement hook, so a partial or absent enforcement install is diagnosable and repairable independently of the terminal-title wiring.
Architecture
Static Routing Pattern: marshal.json stores runtime.target; router dispatches to target class.
marshal.json Scripts
runtime.target: claude ──────────────────► claude_runtime.py
runtime.target: opencode ─────────────────► opencode_runtime.pyRouter (platform_runtime.py) reads runtime.target, looks up target class in registry, and dispatches. Registry is extensible — adding a new target adds a class and a registry entry.
Invocation
Standard invocation (post-bootstrap):
python3 .plan/execute-script.py plan-marshall:platform-runtime:platform_runtime <operation> [args...]Bootstrap invocation (Steps 1–3, before executor exists):
During marshall-steward Steps 1–3 the executor does not yet exist. Use the glob-path bootstrap directly. After Step 4 (Generate Executor), switch to executor notation for all subsequent calls.
TOON Contract
Every operation returns:
status: success | error | no-op
operation: <name>
result: <any> (success only)
error: <string> (error only)
message: <string> (error only)
reason: <string> (no-op only)
alternative: <string> (no-op only)Full per-operation schemas: standards/contract.md
No-op policy and caller obligations: standards/no-op-policy.md
Error Codes
| Code | Meaning |
|---|---|
invalid_scope |
Scope argument not project or global |
invalid_check |
permission analyze --checks contains unknown check name |
marshal_not_found |
.plan/marshal.json missing |
prompt_not_found |
subagent dispatch prompt file not found |
unknown_target |
runtime.target not in registry |
hook_not_configured |
SessionStart hook missing; $CLAUDE_CODE_SESSION_ID unset |
invalid_settings |
Settings file malformed (JSON parse error); fail-closed before write — permission configure/fix/ensure-wildcards/ensure-steps/web-apply |
invalid_marshal |
.plan/marshal.json malformed (parse error); fail-closed instead of zero-step audit — permission analyze/ensure-steps |
unsupported_observable |
wait for --observable names a kind outside the closed set |
invalid_bound |
wait for --bound-seconds is not positive |
unknown_reference |
wait for --reference names no instance of the observable kind |
observable_unreachable |
The observable's inspection channel could not be reached; no outcome is implied |
unexpected_observable_status |
The observable reported an out-of-vocabulary status; no outcome is inferred |
No-Op Behavior
When a target returns no-op:
statusisno-op, noterrorreasonexplains why the operation is not supportedalternativesuggests what the caller can do instead- The calling skill must continue — no-op is not a failure
Compliance
All scripts comply with:
tools-script-executor— executor notation, standardized error format, environment variables (PLAN_DIR_NAME,PM_MARKETPLACE_ROOT), exit codes (0 success, 1 invalid params, 2 runtime error)ref-toon-format— TOON generated and parsed viatoon_parser.pyfrom theref-toon-formatskill; no ad-hoc parsing
Boundary
Platform-runtime operations satisfy: "Would this differ between Claude Code and OpenCode?"
| In scope | Belongs elsewhere |
|---|---|
| Platform hooks, session IDs | manage-status (plan state) |
| Settings/permissions files | tools-integration-ci (CI/PR) |
| Terminal title rendering | manage-architecture (architecture data) |
| Platform-specific subagent invocation | manage-metrics (metrics storage) |
| Platform health verification | tools-script-executor (executor regeneration) |
| Post-upgrade reload directive (harness-appropriate) | marshall-steward (upgrade flow that consumes the directive) |
The session render-title and session push-title-token operations are the
resolve + emit layer of the terminal-title three-way split. They resolve
session → plan, read the title state from status.json (live first, archived
fallback), call the pure manage-terminal-title composer, and — on the render
path — emit per platform (OSC / statusLine / web sessionTitle). status.json is
the single source of persisted title state — there is no title-body.txt
artifact. See manage-terminal-title/standards/terminal-title-architecture.md for
the canonical end-to-end architecture: state (manage-status), composer
(manage-terminal-title), resolve+emit (platform-runtime), session-plan
binding, output channels, platform abstraction, and the glyph + icon vocabulary.
PreToolUse Enforcement Hook
A conditional PreToolUse enforcement hook deterministically blocks four
mechanically-checkable hard-rule violation families, but ONLY when the call
originates inside a plan-marshall plan context — failing open everywhere else.
It is implemented by three sibling scripts:
pretooluse_gate.py— the shared, pure-function module that is the SINGLE
home of the PreToolUse payload-field knowledge and theSignal1 OR Signal2
fail-open context-gate predicate. Imported by both leaves below; owns no rule
matchers.claude_pretooluse_capture.py— the observe-only leaf that validates the
shared gate's field names against real payloads before enforcement is armed.claude_pretooluse_hook.py— the enforcement leaf that imports the shared
gate and adds only the four rule families plus thepermissionDecision: deny
envelope.
The enforcement hook is installed on demand via the orthogonalproject install-hook --enforcement path (independent of the terminal-title
bundle), surfaces a dedicated PreToolUse:enforcement present/MISSING label on
the health-check --checks display diagnostic, and is registered through the
marshall-steward Configuration → Enforcement Hook menu
(`../marshall-steward/references/menu-enforcement-hook.md`).
The context gate, the four rule families with their redirect reasons, the
fail-open / best-effort-no-raise contract, and the capture-validates-the-gate
dependency chain are documented in
`standards/pretooluse-enforcement.md` —
the canonical reference; the rule list is not restated here.