opencode-workflow-guard 1.12.1 → 1.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/docs/installation.md +4 -3
- package/docs/policies.md +7 -2
- package/docs/troubleshooting.md +18 -0
- package/package.json +4 -2
- package/src/lib/guard-dispatcher.ts +6 -2
- package/src/lib/types.ts +1 -0
- package/src/lib/v2-plugin.ts +649 -0
- package/src/lib/verify.ts +11 -0
- package/src/policies/boundary.ts +26 -5
- package/src/policies/continuation.ts +20 -1
- package/src/policies/git.ts +42 -1
- package/src/policies/tamper.ts +17 -0
- package/src/workflow-guard-ui.ts +111 -14
- package/src/workflow-guard.ts +26 -5
- package/test/test-e2e.mts +129 -27
- package/test/test.mts +71 -1
package/README.md
CHANGED
|
@@ -14,15 +14,18 @@ Workflow Guard is deliberately **not an agent harness**: it can constrain action
|
|
|
14
14
|
|
|
15
15
|
### 1. Installation
|
|
16
16
|
|
|
17
|
-
Install the current published version globally
|
|
17
|
+
Install the current published version globally:
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
20
|
VERSION=$(npm view opencode-workflow-guard version)
|
|
21
|
-
opencode plugin "opencode-workflow-guard@$VERSION"
|
|
21
|
+
opencode plugin add "opencode-workflow-guard@$VERSION" # OpenCode 2.x
|
|
22
|
+
opencode plugin "opencode-workflow-guard@$VERSION" --global --force # OpenCode 1.x
|
|
22
23
|
```
|
|
23
24
|
|
|
24
25
|
*Requires OpenCode >= 1.18.* OpenCode detects the package's server and TUI targets and updates both global configs. Use the same command after a release to upgrade; the explicit version gives OpenCode a fresh package-cache key. Restart OpenCode after installation. See [docs/installation.md](docs/installation.md) for details and links to the OpenCode plugin documentation.
|
|
25
26
|
|
|
27
|
+
The plugin supports both generations from one package: **OpenCode 1.x** loads the `server()` entrypoint, and **OpenCode 2.x** loads the `setup()` entrypoint (`@opencode/plugin`). Policies, tools, and configuration behave the same on both; V2 load tests run against a real OpenCode 2 binary in `npm run test:install`.
|
|
28
|
+
|
|
26
29
|
For the optional TUI badge, configure `"opencode-workflow-guard"` in `tui.json`. OpenCode resolves the package's exported `./tui` entrypoint automatically for TUI plugins. The companion keeps a static `Workflow Guard` shield in the prompt bar; warning toasts provide block details without changing agent behavior. Do not place the TUI module under the server `plugins/` directory.
|
|
27
30
|
|
|
28
31
|

|
package/docs/installation.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
## Requirements
|
|
8
8
|
|
|
9
|
-
- **OpenCode >= 1.18** (requires the `GET /session/:id/todo` endpoint and V1 `PluginModule` loader).
|
|
9
|
+
- **OpenCode >= 1.18** (V1: requires the `GET /session/:id/todo` endpoint and the V1 `PluginModule` loader) or **OpenCode 2.x** (V2: loads through the `@opencode/plugin` API; the current todo list is reconstructed from the session message history because V2 has no todo endpoint).
|
|
10
10
|
- **Node.js >= 22.18** or **Bun** (for running test scripts directly).
|
|
11
11
|
|
|
12
12
|
---
|
|
@@ -19,10 +19,11 @@ Install the current published version with OpenCode's plugin command:
|
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
21
|
VERSION=$(npm view opencode-workflow-guard version)
|
|
22
|
-
opencode plugin "opencode-workflow-guard@$VERSION"
|
|
22
|
+
opencode plugin add "opencode-workflow-guard@$VERSION" # OpenCode 2.x
|
|
23
|
+
opencode plugin "opencode-workflow-guard@$VERSION" --global --force # OpenCode 1.x
|
|
23
24
|
```
|
|
24
25
|
|
|
25
|
-
OpenCode documents `opencode plugin <
|
|
26
|
+
OpenCode 2 documents `opencode plugin add <package>` as the command to install a plugin and add it to the global configuration; OpenCode 1 documents `opencode plugin <module>` with `--global` and `--force`. Workflow Guard exposes both server and TUI targets, so OpenCode detects both and updates their global configuration files (OpenCode 2 uses one global `cli.json` for terminal-client settings). Restart OpenCode after installation because configuration is loaded at startup. See the [OpenCode plugin CLI documentation](https://opencode.ai/docs/cli/#plugin).
|
|
26
27
|
|
|
27
28
|
Use the same two commands to upgrade after a Workflow Guard release. Keep the explicit version in OpenCode's configuration rather than relying on a bare package name or `@latest`: OpenCode caches npm plugins, and an already-populated `@latest` cache key may continue resolving to the version that originally populated it. An explicit new version creates a new package-cache key without manually deleting OpenCode's cache.
|
|
28
29
|
|
package/docs/policies.md
CHANGED
|
@@ -12,7 +12,7 @@ It is a policy and enforcement layer, not an agent harness. Policies may constra
|
|
|
12
12
|
- **Pre-edit Gate:** All file-editing tools (`edit`, `write`, `apply_patch`) are blocked until the session has an active task list in OpenCode's native todo system (`todowrite`, persisted at `GET /session/:id/todo`).
|
|
13
13
|
- **Flexible Task Execution:** Tasks can be completed in any order as work concludes without artificial sequential blockers, maintaining maximum pair-programming DX.
|
|
14
14
|
- **No Silent Deletion:** Each `todowrite` update replaces the complete list. Active tasks must remain in subsequent updates until they are explicitly marked `completed` or `cancelled`.
|
|
15
|
-
- **No Silent Early Exit:** When an owning session becomes `session.idle` with unfinished native todos, the guard asynchronously asks OpenCode to resume the existing run. This enforces the session's existing lifecycle state: Workflow Guard does not choose the next task, reorder work, create work, or delegate it. Ordinary automatic continuations are capped at three consecutive attempts and the budget resets on genuine user input. The independently opt-in `ralphMode` uses `ralphMaxIterations` (1-100, default 10) instead; genuine user input terminates an active Ralph run, and unfinished work at the budget limit reports `budget_exhausted` rather than inferring a policy block. Ralph remains disabled by default under every operation profile. Use OpenCode's native `question` tool when user feedback or a decision is required; question and permission interactions wait inside the active run rather than producing an idle completion.
|
|
15
|
+
- **No Silent Early Exit:** When an owning session becomes `session.idle` with unfinished native todos, the guard asynchronously asks OpenCode to resume the existing run. This enforces the session's existing lifecycle state: Workflow Guard does not choose the next task, reorder work, create work, or delegate it. Ordinary automatic continuations are capped at three consecutive attempts and the budget resets on genuine user input. User-initiated interrupts (Esc) surface as `MessageAbortedError` and suspend automatic continuation for that session until genuine user input arrives, mirroring the ralph `user_stopped` semantics. The independently opt-in `ralphMode` uses `ralphMaxIterations` (1-100, default 10) instead; genuine user input terminates an active Ralph run, and unfinished work at the budget limit reports `budget_exhausted` rather than inferring a policy block. Ralph remains disabled by default under every operation profile. Use OpenCode's native `question` tool when user feedback or a decision is required; question and permission interactions wait inside the active run rather than producing an idle completion.
|
|
16
16
|
- **Subagent Inheritance & Role Confinement:** Subagents automatically inherit active tasks from their parent session via the `parentID` hierarchy. Subagents spawned with read-only roles (`reviewer`, `planner`, `advisor`, `critic`, `explorer`, `scout`, `evaluator`) are hard-confined: file mutations, lifecycle tools, and mutating shell commands are strictly blocked.
|
|
17
17
|
- **Handoff Safety:** An idle subagent whose effective todos are inherited from its parent is not auto-continued. Inherited work belongs to the parent and can be handed back normally.
|
|
18
18
|
- **Durable Planning Context Discovery:** `guard_next_tasks` prefers a repository-root `TODO.md`; when it is absent, it reads conventional `ROADMAP.md`, `PLAN.md`, `TASKS.md`, `BACKLOG.md`, their `docs/` counterparts, and Markdown files under `docs/plans/`. The tool exposes candidate planning context without selecting, prioritizing, or sequencing tasks. These files never replace OpenCode's native runtime todo state.
|
|
@@ -60,6 +60,8 @@ It is a policy and enforcement layer, not an agent harness. Policies may constra
|
|
|
60
60
|
- Direct edits of the same paths through the `edit`/`write`/`apply_patch` tools
|
|
61
61
|
- Shell commands invoking `opencode auth`, `opencode config`, `opencode permission`, or `opencode run --auto`
|
|
62
62
|
- Evasion-normalized: quote-concatenation (`open''code.json`), escapes (`open\c\ode`), and glob wildcards (`opencode.jso?`) are stripped before matching.
|
|
63
|
+
- Collaboration commands (`gh issue`, `gh pr`, `glab issue`, `glab pr`, `az repos pr`) and document content mentioning guarded paths are not tampering: quoted arguments are command data, not shell syntax (redirect analysis runs on the quote-stripped residue), and the redirect heuristic applies to shell commands only (write/edit content is guarded by the target path check instead).
|
|
64
|
+
- Plan files under `.opencode/plans/` are exempt: opencode's plan mode writes agent-authored plan markdown there, and plans are content, not configuration. Everything else under `.opencode/` (including the directory itself) stays protected.
|
|
63
65
|
- **Read-only access is allowed** (`cat`, `less`, `grep`, `head`, `tail` on config files) - only modification attempts trigger the guard.
|
|
64
66
|
|
|
65
67
|
### 7. Feature-Branch Workflow
|
|
@@ -70,17 +72,19 @@ It is a policy and enforcement layer, not an agent harness. Policies may constra
|
|
|
70
72
|
### 8. Workspace Boundary Guard
|
|
71
73
|
- File modification tools (`edit`, `write`, `apply_patch`) and recognized shell mutations (redirection `>`, `>>`, `&>`, `>&`, `tee`, `sed -i`, `cp`/`mv`/`ln`, `touch`, `mkdir`, `rm`, `truncate`, `dd of=`, `git apply`/`git am`) are validated to ensure detected targets cannot escape the current workspace root via `../` traversal, symlinks, absolute paths, or shell expansions (`~`, `~user`, `$HOME`). Unresolvable `$VARIABLE` references fail closed. This is command-level enforcement, not filesystem sandboxing; arbitrary programs and dynamically computed paths require the OS isolation described under Known Limits.
|
|
72
74
|
- `tee` flag parsing generically skips option flags (`-a`, `--append`, `-ai`, `--`) and validates all specified target files against the boundary.
|
|
75
|
+
- Embedded non-shell comparisons are not mutations: a bare `>` whose target is purely numeric (`WHERE count > 5`, awk `$3 > 100`) and `>=`/`==` operators are treated as comparison operands, not redirects. Real redirects are unaffected (`> src/a.ts`, `>>`, `&>`, `>&`, fd targets like `/dev/null` still follow their existing rules).
|
|
73
76
|
- `mv` sources are validated too: moving a file from outside the workspace (or a protected settings/plugin file to an innocuous name) is blocked, since `mv` mutates the source.
|
|
74
77
|
- External repository git writes (`git -C /other-repo commit`, `git --git-dir=/other-repo/.git push`, `GIT_DIR=/other-repo/.git git push`, `GIT_WORK_TREE=...`) are also confined to the workspace.
|
|
75
78
|
- The boundary has **no override**: `WORKFLOW_GUARD_ALLOW_LIVE=1` covers live-system commands only and never weakens the workspace confinement.
|
|
76
79
|
|
|
77
80
|
### 9. Script-Laundering Guard
|
|
78
|
-
- Content written via `edit`, `write`, or `apply_patch` is scanned for destructive CLI commands
|
|
81
|
+
- Content written via `edit`, `write`, or `apply_patch` is scanned for destructive CLI commands.
|
|
79
82
|
- Prevents agents from bypassing shell guards by writing destructive commands to script files (e.g. `write deploy.sh` -> `bash deploy.sh`).
|
|
80
83
|
|
|
81
84
|
### 10. Evidence-Based Verification
|
|
82
85
|
- When the agent attempts to mark **every** task completed (finalizing the request), the guard requires passing verification evidence.
|
|
83
86
|
- Verification (`WORKFLOW_GUARD_VERIFY` env, project `verifyCommand`, or auto-detected test/typecheck runner across ecosystems: `package.json` (`npm test`, `npm run typecheck`, `npm run check`, `npm run build`), `Cargo.toml` (`cargo test`), `go.mod` (`go test ./...`), `pytest.ini`/`pyproject.toml` (`pytest`), or `deno.json` (`deno test`)) executes in an isolated environment with scrubbed credentials, output caps, token-efficient stdout/stderr snipping, and strict timeout protection. Environment configuration takes precedence over project configuration.
|
|
87
|
+
- The verification timeout defaults to 30 seconds and can be raised per project (`verifyTimeoutMs` in `.opencode/workflow-guard.json`) or per environment (`WORKFLOW_GUARD_VERIFY_TIMEOUT_MS`, which wins over project config); non-positive values are ignored and fall back to the default. Raise it for suites that legitimately exceed the default - for example this repository's own unit suite.
|
|
84
88
|
- Project `postEditValidators` can run faster targeted checks after a direct file edit actually changes the target. Each entry accepts a workspace-relative glob `pattern` (`*`, `**`, and `?` wildcards; brace expansion and character classes are not supported), a non-empty `command`, and an optional positive `timeoutMs`; invalid entries are reported as advisory feedback rather than silently ignored. Matching commands run from the workspace root with the same scrubbed verification environment. Validator failures and timeouts are appended to the edit result as advisory feedback and do not replace the blocking whole-project finalization verification. Multi-file `apply_patch` calls validate every changed target.
|
|
85
89
|
- With project `recoveryCheckpoints: true`, or the `autonomous` operation profile unless explicitly overridden, genuine root-session user runs capture a private Git checkpoint before agent work starts. Checkpoints preserve tracked and untracked state without using the user's stash list; subagents and generated continuation prompts do not replace them. At `session.idle`, the run's resulting workspace fingerprint is recorded before automatic continuation. `guard_recovery_restore` is provenance-bound to the same root session and refuses restoration after any intervening workspace change. Operation profiles only set optional workflow defaults and cannot weaken core safety policies.
|
|
86
90
|
- Automatic continuation remains synthetic. By default, an eligible root session with unfinished owned todos briefly waits for OpenCode's native asynchronous title generation before that continuation is sent; set `titleSettleWorkaround: false` to opt out. The bounded wait never rewrites titles or provider/model parameters.
|
|
@@ -135,6 +139,7 @@ It is a policy and enforcement layer, not an agent harness. Policies may constra
|
|
|
135
139
|
### 20. Merged Branch & Base Freshness Guard
|
|
136
140
|
- Blocks pushing to branches already merged or associated with closed PRs in GitHub or Azure DevOps.
|
|
137
141
|
- Blocks creating fresh feature branches when the local base branch is behind the remote, prompting the agent to pull latest changes first.
|
|
142
|
+
- Tag push refspecs are exempt: publishing an existing tag (`git push origin v1.2.0`, `git push origin refs/tags/v1.2.0`) and creating tags (`git tag v1.2.0`, `git tag -a -m ...`) are release operations, not branch mutations - only tag deletions (`git tag -d`, `git push origin :refs/tags/v1.2.0`, `--delete`) stay blocked.
|
|
138
143
|
|
|
139
144
|
### 21. Documentation Review & Synchronization Guard
|
|
140
145
|
- Ensures that relevant documentation is updated whenever changes introduce new features, policies, or public tools before PR creation. Configurable via `.opencode/workflow-guard.json` (`requireDocumentation: true`) or `WORKFLOW_GUARD_REQUIRE_DOCS=1`.
|
package/docs/troubleshooting.md
CHANGED
|
@@ -82,3 +82,21 @@ opencode plugin "opencode-workflow-guard@$VERSION" --global --force
|
|
|
82
82
|
OpenCode documents `opencode plugin <module>` as installing a plugin and updating its config, with `--global` for global config and `--force` to replace an existing plugin version: https://opencode.ai/docs/cli/#plugin. Its plugin documentation also describes npm plugins as OpenCode-managed, cached installations: https://opencode.ai/docs/plugins/#how-plugins-are-installed.
|
|
83
83
|
|
|
84
84
|
After the command reports `Detected server + tui targets`, confirm it reports replacements/additions for both the OpenCode and TUI config files, then restart OpenCode. Manual cache deletion should not be necessary.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
### 10. TUI companion crashes with `Keymap.Provider is missing` (OpenCode V2)
|
|
89
|
+
|
|
90
|
+
- **Symptoms:** On opencode v2, the TUI companion plugin fails during setup with `Keymap.Provider is missing` and does not load.
|
|
91
|
+
- **Root Cause:** `ctx.keymap.layer` resolves a Solid context (`useContext(KeymapContext)`) and only works inside the TUI's `Keymap.Provider` render tree; the API documents layers as "owned by the calling component". Plugin `setup()` runs as a plain async call outside that tree, so registering a layer there always throws.
|
|
92
|
+
- **Solution:** Fixed in 1.13.2: the layer is registered from an `app`-slot claim (`ctx.ui.slot({ append: "app", render: () => { ...; return null } })`). Slot renders execute in a reactive scope under the provider, and the same session-panel pattern applies to any plugin API that requires component ownership. If you maintain a TUI plugin that registers keymap layers, commands, or reactive state, register them from a slot render (or a rendered component), never from `setup()`.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
### 11. Reading tool parts from V2 sessions (integrators)
|
|
97
|
+
|
|
98
|
+
- **Symptoms:** Code that parses recorded tool calls (message parts, the `part` DB table, or `session.message.content.updated` events) finds no tool name on OpenCode V2, or sees tool names that do not exist in OpenCode's builtin tool list.
|
|
99
|
+
- **Root Cause:** Two independent things:
|
|
100
|
+
1. V2 message parts record the tool name in the `tool` field; V1 used `name`. Code reading `part.name` silently gets `undefined` on V2.
|
|
101
|
+
2. V2's builtin tool registry contains only `read`, `glob`, `grep`, `edit`, `write`, `patch`, `shell`, `webfetch`, `websearch`, `question`, `skill`, `subagent`, and `execute`. There is no builtin `todowrite` (the V1 name), and the V1 `task` subagent tool was renamed to `subagent`. `todowrite`/`bash` tool parts observed in V2 sessions come from ACP agents (for example pi) that supply their own tools; those tools are not in the server-side registry and cannot be reached by `ctx.tool.transform`.
|
|
102
|
+
- **Solution:** Read the tool name as `part.tool ?? part.name` and treat tool-part names as agent-reported, not as the builtin registry. `ToolEditor.update` ignores missing IDs, so enriching a tool that does not exist is a harmless no-op.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-workflow-guard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.2",
|
|
4
4
|
"description": "Deterministic workflow and safety policy enforcement for OpenCode",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -48,8 +48,10 @@
|
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@opencode-ai/plugin": "latest",
|
|
51
|
+
"@opencode/plugin": "latest",
|
|
51
52
|
"@opentui/solid": "latest",
|
|
52
|
-
"jsonc-parser": "latest"
|
|
53
|
+
"jsonc-parser": "latest",
|
|
54
|
+
"@opencode/ai": "latest"
|
|
53
55
|
},
|
|
54
56
|
"devDependencies": {
|
|
55
57
|
"@changesets/cli": "latest",
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
getGitStatusSummary,
|
|
35
35
|
getGitWorktreeFingerprint,
|
|
36
36
|
runVerify,
|
|
37
|
+
resolveVerifyTimeoutMs,
|
|
37
38
|
snipVerifyOutput,
|
|
38
39
|
} from "./verify.ts";
|
|
39
40
|
import { detectShellMutation, extractPatchPaths, guardShellMutation, isPathOutsideWorkspace } from "../policies/boundary.ts";
|
|
@@ -55,6 +56,7 @@ import {
|
|
|
55
56
|
onProtectedBranch,
|
|
56
57
|
parseGitInvocation,
|
|
57
58
|
pushedProtectedBranchIn,
|
|
59
|
+
tagRefspecIn,
|
|
58
60
|
} from "../policies/git.ts";
|
|
59
61
|
import { extractInterpreterPayload, outsideWritePathInPayload, secretPathInPayload, writePathsInPayload } from "../policies/interpreter.ts";
|
|
60
62
|
import { mcpMutationTool } from "../policies/mcp.ts";
|
|
@@ -180,7 +182,7 @@ export async function guardToolCallImpl(
|
|
|
180
182
|
const isFresh = verifyResult !== undefined && verifyResult.passed && verifyResult.command === command && isEvidenceFresh(verificationEvidence(verifyResult, sessionID), currentSubject, mutationTimestamp);
|
|
181
183
|
if (!isFresh) {
|
|
182
184
|
if (context?.simulate) return block("verification", "verification_required", `Blocked: finalization requires fresh passing verification (${command}).`);
|
|
183
|
-
const result = await runVerify(command, currentRoot);
|
|
185
|
+
const result = await runVerify(command, currentRoot, resolveVerifyTimeoutMs(currentRoot));
|
|
184
186
|
recordVerifyResult(command, result, sessionID, currentRoot);
|
|
185
187
|
if (!result.passed) {
|
|
186
188
|
const tail = snipVerifyOutput(result.output, result.passed).slice(-500);
|
|
@@ -248,7 +250,6 @@ export async function guardToolCallImpl(
|
|
|
248
250
|
}
|
|
249
251
|
}
|
|
250
252
|
}
|
|
251
|
-
for (const content of extractEditContent(input)) if (isSettingsTamper(content)) return block("tamper", "settings_tamper", PROTECTED_PATH_REASON);
|
|
252
253
|
if (onProtectedBranch(currentRoot)) {
|
|
253
254
|
logPolicyBlock(`[workflow-guard] blocked ${toolName}: on protected branch ${currentGitBranch(currentRoot)}`);
|
|
254
255
|
return block("git", "protected_branch", branchGuardReason());
|
|
@@ -329,6 +330,7 @@ export async function guardToolCallImpl(
|
|
|
329
330
|
}
|
|
330
331
|
}
|
|
331
332
|
for (const invocation of gitInvocations) {
|
|
333
|
+
if (tagRefspecIn(`git ${invocation.rest}`, invocation.repoDir)) continue;
|
|
332
334
|
if (GIT_WRITE_RE.test(`git ${invocation.rest}`) && onProtectedBranch(invocation.repoDir)) {
|
|
333
335
|
logPolicyBlock(`[workflow-guard] blocked git write on protected branch: ${command.slice(0, 120)}`);
|
|
334
336
|
return block("git", "protected_branch", branchGuardReason());
|
|
@@ -397,6 +399,7 @@ export async function guardToolCallImpl(
|
|
|
397
399
|
for (const invocation of gitInvocations) {
|
|
398
400
|
const pushText = `git ${invocation.rest}`;
|
|
399
401
|
if (!/\bgit\s+push\b/.test(pushText)) continue;
|
|
402
|
+
if (tagRefspecIn(pushText, invocation.repoDir)) continue;
|
|
400
403
|
const pushedBranch = pushedProtectedBranchIn(pushText, invocation.repoDir);
|
|
401
404
|
if (pushedBranch) {
|
|
402
405
|
logPolicyBlock(`[workflow-guard] blocked push to protected branch '${pushedBranch}': ${command}`);
|
|
@@ -405,6 +408,7 @@ export async function guardToolCallImpl(
|
|
|
405
408
|
}
|
|
406
409
|
for (const invocation of gitInvocations) {
|
|
407
410
|
if (!/\bgit\s+push\b/.test(`git ${invocation.rest}`)) continue;
|
|
411
|
+
if (tagRefspecIn(`git ${invocation.rest}`, invocation.repoDir)) continue;
|
|
408
412
|
if (onProtectedBranch(invocation.repoDir)) {
|
|
409
413
|
logPolicyBlock(`[workflow-guard] blocked push from protected branch: ${command}`);
|
|
410
414
|
return block("git", "protected_branch", branchGuardReason());
|
package/src/lib/types.ts
CHANGED