Skip to content

Releases: ultus-net/opencode-workflow-guard

v1.19.0

Choose a tag to compare

@github-actions github-actions released this 30 Oct 03:40
9e93c89

Minor Changes

  • 5f97b7e: feat: opt-in pre-deployment dogfood gate (Policy 27)

    Gates PR creation on a recorded run of the change on real hardware. Off by
    default, so no existing repository changes behavior.

    • record_dogfood records a run as dogfood evidence bound to the workspace,
      commit hash, and tracked-content worktree fingerprint. A tracked edit or new
      commit after the run makes the evidence stale.
    • Only a pass is durable, and only the newest record per workspace and host
      is kept. A hold stays diagnosable in memory with its taxonomy class and
      never becomes evidence. Recording a hold also discards that host's cached
      passes, so a process restart cannot re-open the gate on an older green run.
    • A pass requires every check row to be pass and at least one check row
      to exist, so the gate cannot be satisfied by a skipped check or by an empty
      observation. A hold must name a taxonomy class.
    • requireDogfood and dogfoodDeployMatrix (or WORKFLOW_GUARD_REQUIRE_DOGFOOD
      and WORKFLOW_GUARD_DOGFOOD_DEPLOY_MATRIX) enable the gate and declare the
      host labels that each need a passing run. Retention and hold eviction are per
      host and scoped to the workspace, so repeated runs in one repository or on one
      host cannot evict another repository's or host's only pass.
    • WORKFLOW_GUARD_DOGFOOD_ACTOR/WORKFLOW_GUARD_DOGFOOD_DEPTH mark a session
      spawned by a repository runner. Every policy still applies there, and the
      marker only refuses record_dogfood, so a tested session cannot certify
      itself.
    • guard_status reports lastDogfood and adds dogfood to
      outstandingRequirements when the gate is unsatisfied.

v1.18.1

Choose a tag to compare

@github-actions github-actions released this 29 Oct 00:19
6a1eae7

Patch Changes

  • c983c70: Report a verification timeout as a timeout, not as a failing test.

    Finalization runs the verify command with a bounded timeout. When the command
    exceeded it, the guard killed the process tree and then reported the outcome
    with the same message it uses for a failing assertion: "all tasks marked done
    but verification is failing". That wording sent the agent looking for a test
    that failed. Nothing had failed; the suite never finished, so the result was
    unknown.

    The two conditions now carry separate codes and reasons. runVerify returns
    timedOut and the limit that was hit, and the dispatcher emits
    verification_timed_out with a message naming the elapsed limit and the remedy
    (verifyTimeoutMs in .opencode/workflow-guard.json, or
    WORKFLOW_GUARD_VERIFY_TIMEOUT_MS). A genuine failure keeps
    verification_failed and its output tail unchanged.

    Both still block finalization. A timeout is not evidence of passing work, so
    allowing it through would be a silent weakening of the verification gate. Only
    the attribution changed.

    Regression coverage asserts five things: a timeout blocks finalization, the
    block reason does not claim a failure, the snipped tail carries no "Verification
    Failed" header, the limit is named at the precision it was set, and the reason
    names a remedy. Four of those fail against the previous message; the block
    itself already held, and must keep holding.

    The remedy is the one lever the agent cannot pull. .opencode/workflow-guard.json
    is tamper-protected, so the reason names both the project setting and the
    environment variable, and the operator applies it. That is deliberate: the guard
    reports what it knows, and does not pretend the agent can widen its own gate.

v1.18.0

Choose a tag to compare

@github-actions github-actions released this 28 Oct 23:22
3df8e45

Minor Changes

  • 2a68c41: Qualify against OpenCode 2.2.0.

    OpenCode 2.2.0 reorganizes the @opencode/ai export surface. Message is no
    longer reachable from the package root there, and a root named import throws
    SyntaxError: The requested module '@opencode/ai' does not provide an export named 'Message'. A V2 plugin that cannot be evaluated registers no hooks, so
    every policy would be inert while the host looks healthy. This release moves the
    import and the pins to the 2.2.0 package set.

    • Import path. Message moves from the @opencode/ai root to
      @opencode/ai/effect. On 2.2.0 the root no longer re-exports the schema
      surface, so a root named import throws, as quoted above; verified against the
      installed 2.2.0 dist. @opencode/plugin@2.2.0 itself imports Message from
      /effect, so this is the host's own sanctioned path. No call site changes:
      Message.user is still the constructor used at src/lib/v2-plugin.ts:542.
    • Pins. @opencode/plugin and @opencode/ai move to 2.2.0, and
      @opentui/solid to 0.7.0, which @opencode/plugin@2.2.0 declares as an
      exact peer requirement. @opentui/core is deliberately not added as a direct
      dependency: @opentui/solid@0.7.0 already declares it as an exact dependency,
      so an extra pin would duplicate a resolution the tree already settles. The
      @opencode-ai/plugin 1.19.3 V1 pin is unchanged. Of the existing overrides
      entries, solid-js 1.9.17 still applies because @opentui/solid@0.7.0 still
      peer-pins exactly 1.9.12; toml 5.0.1 is unaffected.
    • Todo capability. The registry probe already covers this host, so 2.2.0
      needs no version check. Verified on the live 2.2.0 binary: available with
      experimental.todo: true, unavailable without it.
    • Docs. docs/installation.md carries a 1.18.x / 2.2.0 qualification row
      listing only the seats actually exercised, and the pin-set paragraph now
      explains the @opencode/ai root-versus-/effect boundary and why
      @opentui/core needs no direct pin.
    • Test coverage. test/test-e2e.mts gains two canaries that need no model
      provider: the permission seat denies a real finding and abstains on an
      undecidable push. They run inside the real host setup with the workspace taken
      from the host's own location.directory; the push case passes no client,
      because the point is that simulation skips the remote PR probe. The deny
      canary asserts policy identity (secrets / secret_read), not message prose.

    Verified on OpenCode v2.2.0: npm run typecheck clean, npm test 1370 passed /
    0 failed, npm run test:install 32 passed / 0 failed. The opt-in model-driven
    probes remained unavailable because the provider returned
    Model unavailable before guard behavior could run, which that harness reports
    as live unavailable rather than as a guard failure.

    The zod advisory is also cleared here. GHSA-mrhx-c7xc-m3w5 covers
    zod <4.6.6, and the vulnerable 4.1.8 copy arrived through the exact
    @opencode-ai/plugin@1.19.3 V1 pin while the V2 entrypoint imports zod
    without declaring it. A new zod: 4.6.6 override dedupes both onto one patched
    copy, and npm audit now reports zero vulnerabilities. That override must be
    applied with a clean reinstall: an incremental npm install leaves the old
    resolution in place and fails 14 test:install checks, which reads as an
    incompatibility but is a stale tree.

    One pre-existing issue is deliberately not fixed here, because it needs a real
    binary probe and changes behavior beyond a host qualification:
    src/lib/policy-posture.ts describes the code-mode-surface rule as inert on the
    qualified hosts because they "register no execute tool". That rationale is not
    accurate: the tool is registered, and this repository's own session calls it on
    2.1.3.

Patch Changes

  • 2a68c41: Let the V2 permission seat abstain when remote PR state is unverified.

    Every non-protected git push was denied on OpenCode 2, with a message that contradicted itself. The seat always evaluates the policy core with simulate: true, which deliberately skips the remote GitHub/Azure PR probe. The core then reports needs_approval (remote_state_unchecked), and the seat collapsed every non-allowed status into effect = "deny". The denial therefore demanded the result of a probe the same code path had just skipped.

    needs_approval is a third state meaning "cannot decide here", not a finding. PermissionSeatResult.effect now carries abstain alongside allow and deny. Only a blocked decision denies. An abstention leaves the host's configured effect and message untouched, so a configured ask stays ask, and execute.before runs the real check with the probe. The seat still cannot upgrade an effect.

    The abstention is journaled with decision: "abstain", reason: "deferred_to_execute_before", and the preserved needs_approval decision, so a skipped probe stays readable in the audit trail instead of appearing as a plain allow. The seat's fail-closed catch block is unchanged. guard_why still reports needs_approval, because that is the honest answer for a simulation; this changes the seat, not the policy core.

v1.17.0

Choose a tag to compare

@github-actions github-actions released this 23 Oct 04:20
9630844

Minor Changes

  • 495ba8a: Add Policy 25, the live-documentation research floor. After repeated tool failures (default 3) with no live-documentation lookup since, the guard blocks the next mutation or shell call and directs the agent to consult a current source (websearch/webfetch, or an MCP documentation tool) instead of guessing from training data. Reads, greps, and research tools are never blocked.

    The floor is debt-based rather than success-based: an unrelated successful tool does not clear the debt, so a benign command cannot slip a guessed change back through. Only a successful research call clears it. It is enabled by default to force the failover and can be disabled with requireLiveDocs: false / WORKFLOW_GUARD_REQUIRE_LIVE_DOCS=0, or retuned with liveDocFailureThreshold / WORKFLOW_GUARD_LIVE_DOC_THRESHOLD. Decisions are audited (reason: live_docs_research_floor) and block feedback flows to the app log at warn level.

    Note for operators: the acting agent must be permitted to call a research tool, or the floor will (correctly) stall it. Agents whose toolset denies websearch/webfetch and exposes no MCP docs server should be granted one. Both the V1 and V2 entrypoints enforce the floor in parity.

    The gated surface is every mutating call, not just edits and shell: it includes guard_worktree_create, guard_worktree_cleanup, and guard_recovery_restore, so a guard-provided tool cannot be the way past a guard gate. Research recognition matches documented MCP server names (mcp__context7__*, mcp__deepwiki__*, mcp__tavily__*, mcp__brave__*), so a guard inspection tool or generic file reader cannot clear the debt by name accident. An unrecognised docs server can be declared with researchTools / WORKFLOW_GUARD_RESEARCH_TOOLS. A failed research attempt never accrues debt, so a flaky network cannot deepen the block it was meant to clear. An unparseable or non-positive threshold falls back to the default of 3 instead of silently disabling the floor.

    Also add Policy 26, sweeping repository claims vs read coverage. It is observability-only (like Policy 24): when the final response asserts a whole-repository fact ("the entire codebase uses X", "no other callers reference this") but the session has read or authored fewer than 2 distinct files, the mismatch is journaled and logged at info level. The response is never blocked or rewritten, since a search-heavy investigation can legitimately back such a claim with little read coverage. Both entrypoints report it in parity.

v1.16.0

Choose a tag to compare

@github-actions github-actions released this 23 Oct 01:43
c04883f

Minor Changes

  • e5fd6a1: Enforce per-task completion evidence for todowrite.

    A task may now enter completed only when the session recorded at least one file mutation inside that task's active window. Previously, a list-wide todowrite could flip every item to completed with no work behind it; the guard only journalled a completion_claim_mismatch audit record and allowed it. That signal is now an enforced policy.

    • Per-task windows, not a session baseline. Each active task records the session mutation count when it entered its current window. One mutation cannot vouch for several completions, so N completions need N mutations counted from the earliest of them, and work done before a task became active never counts toward it. A finished or dropped task releases its window, so reusing a task's text starts clean. Task identity is the task text with the read-only marker stripped, and occurrences are counted rather than matched by last write, so duplicate text cannot hide a new completion.
    • [read-only] escape for review and investigation. Tasks that legitimately change no file carry [read-only] in their text (also [read_only] / [read only]) and complete freely. Because the marker is an annotation rather than part of identity, it can be appended to an already-planned task instead of forcing a re-plan. Every use is journaled as todo_completed_read_only, so relabeling real work as read-only is visible in the audit trail instead of passing silently. Deliberately dropped work uses cancelled.
    • Planned late. A task first observed already completed has no window, so it is scored against the session's mutation history, bounded by the one-mutation-per-completion rule.
    • Block message names the remedy. A rejected write names the offending tasks and states the three legitimate paths: do the work, mark it [read-only], or mark it cancelled.
    • Simulation predicts the gate. guard_why now reports the same verdict the enforcement path would, and does not move any task window.
    • Steering, not just enforcement. The todowrite tool description, the system-prompt guidance, and the compaction injection now state the lifecycle contract: plan before working, keep the list truthful, complete a task only after its work is done, and use the marker for genuinely read-only tasks.
    • guard_status reports taskEvidence (active, completed, tasksWithoutRecordedWork, readOnlyTasks), so the requirement is visible before it blocks anything.

    Todo state is per session and cleared on session.deleted. The completion check never replaces the finalization gates: fresh verification evidence and the review gate remain the authority on whether the work is done.

  • 2818ad0: Adopt five OpenCode 2 plugin APIs that were previously unused, and correct documentation that had drifted from the host.

    ctx.command.transform now registers a server-side /guard-status command, so guard state is readable from every V2 client without the separately installed TUI companion. The report is delivered as a synthetic message, which costs no model turn.

    Read-only subagent roles now lose the mutating tools from their per-request tool snapshot in session.hook("context") (write, edit, patch, apply_patch, shell, bash, execute, todowrite, subagent/task, and the worktree lifecycle tools). Previously they received guidance text and were denied late in execute.before; now the model cannot plan the call at all. The execute.before seat is unchanged and remains the backstop for tools supplied outside the host snapshot. Every pruning is journaled as session.context / read_only_role_tools_pruned.

    guard_worktree_create uses the host worktree API when the host exposes a project id, so the worktree lands in the operator's configured worktree.directory and appears in host inventory. The host's answer is validated independently (existing directory, a registered worktree of this repository, never the primary tree) and any failure falls back to the existing git path. guard_worktree_cleanup still owns removal, because the host's remove has a force flag and no snapshot step, so delegating it could destroy uncommitted work the guard preserves. Cleanup accepts a directory outside the guard's storage only when the host declares it, and it asks the host to refresh its inventory afterwards.

    The MCP policy now widens its guarded server tokens from ctx.mcp.list(), refreshed on mcp.tools.changed. A renamed or enterprise instance of a guarded product is matched without a code change. Widening is one-way: removing a server adds nothing and the static token list stays the floor, because tool names are model-supplied text and the MCP policy is a live-system rail.

    command.executed is now journaled. The previous comment claimed V2 had no such event; the installed 2.0.10 schema has it, and the 2.0.15 host emits it.

    Docs corrected in the same pass: docs/policies.md now lists the V2 hook names alongside the V1 ones (V1 plugin implementations do not run on OpenCode 2), qualifies the todowrite version boundary (absent through 2.0.15, reintroduced in 2.1.x behind experimental.todo), and names session.hook("compaction") as the V2 compaction seat. Stale comments in src/lib/v2-todo.ts and src/lib/v2-plugin.ts now state the version boundary instead of asserting that no todowrite exists.

  • 7842250: The V2 permission.hook("evaluate") seat now enforces instead of only journaling. It maps the host's normalized permission action/resources back onto the policy core and sets effect = "deny" with an attributable message, so a denied shell command or file mutation never starts. Shell resources feed the shell, git, destructive, and workspace policies, read resources feed the secret policy, edit/write/patch/apply_patch resources feed the boundary, tamper, secret, protected-branch, and todo policies, and guarded MCP servers feed the MCP policy. The seat only tightens: an explicit configured deny stays final, unmapped host actions remain with host policy, and a seat failure fails closed. execute.before is kept as the last-resort seat because it is the only one that sees raw tool input.

  • f320ecf: OpenCode 2 policy posture: close a write-confinement escape, add the permission
    seat, ship installable static policies, restore the task/todo distinction, and
    pin the host dependency.

    Security fix. A session could widen its write boundary into another
    repository through a single allowed read, because a path-derived workspace
    candidate was adopted even for an already-bound session. That is the one change
    in this release with a security impact; see the dedicated write-confinement
    changeset for the mechanism and its regression checks.

    The rest:

    • Permission seat. ctx.permission.hook("evaluate") now denies before a
      tool starts, tightening only: a configured deny is already final, so the
      hook never re-permits one.
    • Static policy posture. opencode-workflow-guard policies --print emits a
      host-level policy block for invariants a repository cannot lift. It prints and
      refuses to install, because a plugin that writes the config holding its own
      static rails has moved them back inside the plugin.
    • Todo capability. The todowrite capability is probed from the tool
      registry instead of inferred from history, so OpenCode 2.1.x gates a session
      that never planned while 2.0.x still fails open, now with a once-per-session
      warning and a todoCapability field in guard_status.
    • Pinned host dependency. Direct dependencies no longer float on latest,
      and guard_status reports the qualified host version next to the loaded one.
    • setup writes the right key. OpenCode 2 reads plugins, not the singular
      plugin; the old writer produced a config that looked configured and loaded
      nothing on 2.x. The TUI file is the exception: tui.json keeps singular
      plugin on both majors, because 2.x reads that key and migrates it into
      cli.json.plugins.
    • Subagents inherit the parent's write boundary. A subagent has no direct
      binding of its own, so a path-derived candidate used to look like a first bind
      even when its parent was confined: one allowed foreign read moved the child
      into another repository. Registration now resolves the nearest bound ancestor
      first, so the child stays inside the parent's boundary and the refusal is
      journaled.
    • Adopted current plugin APIs. Server-side /guard-status command, read-only
      tool pruning from the per-request snapshot, native worktree creation with
      host-declared cleanup, and a widen-only MCP inventory.

    Verification status

    Probe-gated, not date-gated. Verified on the installed host:

    Gate Result
    npm run typecheck clean on the final tree
    npm test 1261 passed, 0 failed ...
Read more

v1.15.5

Choose a tag to compare

@github-actions github-actions released this 01 Oct 10:06
232fabc

Patch Changes

  • aeadda1: Reap stale project-memory coordination markers during the daily maintenance pass: markers (*.sqlite.active.*) left behind by processes that died without closing their store previously accumulated until the 100 MB storage ceiling triggered eviction, so a healthy machine collected unbounded zero-byte files. Maintenance now removes provably dead owners opportunistically, keeping the project-memory directory's file count bounded without storage pressure. Markers owned by live or unidentifiable processes are never removed.

v1.15.3

Choose a tag to compare

@github-actions github-actions released this 27 Sep 21:15
1cb167c

Patch Changes

  • d786bd5: Accept workspace-symlink-aliased absolute paths in the workspace boundary: a target that lexically looks outside the workspace root (for example /home/hunter/... where /home is a symlink to var/home and the workspace root lives under /var/home/hunter/...) is no longer blocked as workspace_escape when its canonical resolution is inside the root. Symlinks inside the workspace that point outside stay blocked (including new files under them, via the nearest-existing-ancestor walk), and indeterminate canonicalization still fails closed to the lexical verdict.
  • 35871df: Fix branch-creation staleness classification when the command carries shell redirections: git switch -c feat/x 2>&1 (or 2>/dev/null, > /dev/null) no longer misclassifies the redirection token as an explicit start point and fails closed; redirections are ignored and the HEAD-based or explicit start-point check applies as intended.
  • 93bc6bf: Expand the provider-free e2e harness to verify guard policy decisions via guard_why simulate (protected-branch push, secret-file reads, workspace boundary escape, interactive TTY shell-safety), project configuration loading from .opencode/workflow-guard.json, and the full config-gated 18-tool registration surface through the real OpenCode runtime. Test-only change; no runtime behavior changes.
  • c8021fd: Dynamic shell syntax blocks now name the detected construct and a concrete remedy instead of a generic message: command/process substitution ($( ), backticks, <( ) / >( )) points to rewriting with literal values or separate literal commands and to simulating with guard_why, malformed quoting points to simple balanced quoting, and the IFS/ambiguous-whitespace variants name their construct. The fail-closed decision, block code, and all detectors are unchanged.
  • 0c21118: Seed stale-write freshness observations from allowed shell redirect writes: a file the session just created or rewrote via cmd > file can be edited without a redundant re-read, extending the existing edit/write seeding (LL-003) to shell writers. Redirects to /dev/null, the /tmp/opencode scratch directory, or outside the workspace seed nothing, identical-byte (no-op) redirects do not seed, and the not-read block message now also suggests coordinating when another session may own the file.

v1.15.2

Choose a tag to compare

@github-actions github-actions released this 27 Sep 03:58
599e5c6

Patch Changes

  • ec96e30: Resolve every workspace-dependent guard decision from the call's own context and the session's own workspace binding instead of one process-global slot: concurrent loop sessions sharing a single guard process no longer re-anchor each other's boundary checks, git push/branch evaluation, guard_status, or record_review bindings (session workspaces are registered from tool-call arguments or host-provided per-call worktrees, and a plugin-instance default can no longer re-anchor an already-bound session).

    Bind secondary-review verdicts per exact worktree: getLastReviewResultForWorkspace prefers an exact worktree-path match, then a same-repo review whose tracked-content fingerprint matches the consuming worktree, and only then falls back to repository-level recency — so two loops sharing one repository stop displacing each other's approvals between record_review and gh pr create. record_review without any resolvable reviewed workspace (no explicit directory, no session binding, no per-call worktree) is now rejected with an actionable cause instead of silently binding to a latched root.

    Make the PR-creation lockfile gate section-aware for package.json: it fires when a section the lockfile records changed (dependencies, devDependencies, optionalDependencies, peerDependencies, peerDependenciesMeta, engines, packageManager, overrides, resolutions, workspaces, pnpm, name, version). Scripts-only or metadata-only manifest edits — which regenerate package-lock.json byte-identical — no longer block PR creation with an unsatisfiable demand. Cargo.toml/go.mod keep the conservative manifest-changed trigger.

v1.15.1

Choose a tag to compare

@github-actions github-actions released this 22 Sep 08:23
03fbdcf

Patch Changes

  • 14a07f1: Branch creation freshness gate honors explicit fresh start points

    git switch -c <name> origin/main (and git checkout -b <name> origin/main)
    no longer fails when the current branch is behind the remote default: a start
    point that already contains origin/HEAD makes the new branch fresh by
    construction, so the HEAD-based staleness check does not apply to it.
    Classifiable start points are the literal ref or SHA operand of the creation
    flag (including -c<name>-style attached forms and valueless flags around
    them); variables, quoted or otherwise indeterminate tokens, and unknown or
    value-taking flags fail closed to the previous check. Creations without a
    start point are unchanged.

    Block reasons now name what was actually checked — the current branch and its
    distance from the remote default — and remedies that work under the guard
    (rebase or ff-merge the branch, or branch from the remote default directly),
    instead of suggesting git pull on main, which does not address a stale
    non-main branch and previously kept the block in place even after the local
    main ref was updated.

  • 2cbeab1: TUI companion: drop the duplicate home-footer badge on OpenCode 2

    The V2 companion claimed both home.footer.status and prompt.footer.status,
    and OpenCode 2 mounts both footers on the home screen, so the
    Workflow Guard 🛡️ badge rendered twice there (once in the prompt footer row
    next to the location path, once in the bottom status bar next to the version).
    The companion now claims only prompt.footer.status, which is mounted on every
    screen (home and session composers), so the badge renders exactly once; the
    home footer bar returns to its builtin contents (MCP/plugin indicators,
    version).

v1.15.0

Choose a tag to compare

@github-actions github-actions released this 22 Sep 07:40
d3bfb09

Minor Changes

  • 4797e62: The loaded guard version is now visible: guard_status reports it as pluginVersion, and the startup app log states Workflow Guard v<version> plugin initialized. This closes the silent-stale-installation trap where OpenCode resolves a configured bare plugin name through Node's parent-directory lookup from its configuration location — a copy in a parent node_modules (for example a pnpm-managed home directory) shadows both OpenCode's plugin cache and a global npm install -g, so updates to the wrong surface leave an old version running. docs/installation.md and docs/troubleshooting.md now document the shadowing behavior, how to verify which copy actually loaded, and how to update it (opencode service restart reloads the background service).
  • 3ec7161: record_review supports an opt-in strict recorder mode: setting requireSubagentReview: true in .opencode/workflow-guard.json (or WORKFLOW_GUARD_REQUIRE_SUBAGENT_REVIEW=1) rejects approvals recorded by the root session itself, so a passing verdict must be recorded from a subagent session with a parent. This closes the root-session self-approval path that content binding intentionally leaves open for relay flows with read-only reviewer toolsets; changes-requested findings remain relayable, the default behavior is unchanged, the audit entry records reason subagent_recorder_required, and WORKFLOW_GUARD_REQUIRE_SUBAGENT_REVIEW=0 disables the requirement even when project config enables it. guard_status echoes the effective requirement in its projectConfig summary.
  • aae4ca6: guard_status now includes a read-only gitHygiene snapshot: local branches already merged into the mainline (capped list plus true count, excluding the current and unmerged branches), prunable worktree admin entries (clearable with guard_worktree_cleanup / cleanupGitWorktree or git worktree prune), and the current branch's base-behind distance when a remote base exists. Hygiene itself stays manual — the guard reports accumulated git state, it never prunes, fetches, or deletes branches on its own.

Patch Changes

  • 99dd23f: Workspace-boundary blocks now attribute the unresolvable-variable cause: a mutation, redirect, or patch target containing an unresolvable $VARIABLE reference (for example mkdir -p $D/sub or > $CACHE/out.txt) is indeterminate without executing shell semantics and is treated as outside the workspace, but the block message now says so — "contains an unresolvable variable reference … use a literal workspace-relative path" — instead of the generic escape message, across the edit/write path, apply_patch, and shell mutations. Deterministically expandable forms (~, $HOME) and literal paths inside the workspace keep the generic messages; the fail-closed behavior itself is unchanged. docs/policies.md and docs/troubleshooting.md document the rule and the remedy.
  • e1065bf: guard_review_rubric diff invocations keep a trailing -- after the revision so a configured base ref can never parse as a pathspec (defense-in-depth on top of the revision-before--- fix), and the default-bases fallback loop (origin/main → origin/master → main → master, then HEAD~1) is directly asserted: a repository with no origin remote embeds the real branch diff from the local main branch.
  • 634d748: Scratch-directory cause attribution: /tmp/opencode is the OpenCode environment's designated agent scratch space, but the workspace boundary is workspace-absolute and does not cover it, so writes there stay blocked. Block messages for targets under it (edit/write, apply_patch, and shell mutations) now name the directory and point to the remedy — write scratch as an untracked file inside the workspace instead of outside it — instead of the generic escape message that read as a guard bug (six of the device audit's boundary blocks were this class). The enforcement decision is unchanged.
  • 2cb9fd8: Stale file-claim takeover for a missed idle release: a direct-edit claim created before its owner's last recorded idle timestamp cannot belong to an in-flight tool call, so when another session hits that conflict every provably stale claim of that owner is released (each re-checked after the owner lookup) with an audited stale_claim_takeover entry instead of blocking every session until the owner is deleted or the service restarts. Owners still mid-session (no idle timestamp), clients without a session lookup, and failed lookups keep the block (fail-closed), and guard_why remains read-only. Claims now record their creation timestamp, and a same-session re-claim refreshes the stored claim so its release matches the in-flight call.