opencode-workflow-guard 1.10.1 → 1.12.0

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 CHANGED
@@ -42,7 +42,7 @@ WORKFLOW_GUARD_LIVE_E2E=1 npm run test:install # Also run policy probes with Ope
42
42
 
43
43
  The plugin enforces **24 deterministic policy rules** across four main pillars:
44
44
 
45
- * **Branch & History Protection:** Enforces feature-branch workflows (blocks edits/commits on `main`/`master`), hard-blocks direct or forced pushes to protected branches, enforces mergeability pre-flight checks, and requires PR changelogs / changesets.
45
+ * **Branch & History Protection:** Enforces feature-branch workflows (blocks edits/commits on `main`/`master`), hard-blocks direct or forced pushes to protected branches, enforces mergeability pre-flight checks, enforces secondary review approval before PR creation by default, and requires PR changelogs / changesets.
46
46
  * **Task & Verification Discipline:** Gates file mutations on active `todowrite` tasks with subagent inheritance, blocks concurrent direct edits to the same canonical file across sessions, requires same-session fresh reads before `edit`/`write` can replace existing files, blocks silent task deletion, enforces fresh test verification evidence before task completion, and journals completion claims vs. verification mismatches.
47
47
  * **Secrets & Workspace Confinement:** Confines edit tools and recognized shell/git mutations within the workspace root (including symlink and `../` escape checks), scrubs sensitive environment variables in agent subshells, and redacts `.env` reads with safe schema masks. Arbitrary executables are not OS-sandboxed; use containers or filesystem isolation when hard confinement is required.
48
48
  * **Destructive CLI & Environment Safety:** Intercepts destructive cloud/database/infrastructure commands, stops script laundering and interpreter evasion, blocks TTY hangs (`vim`, `nano`, `sudo`), and guards against dangerous package manager flags.
@@ -108,6 +108,22 @@ OpenCode's model/provider request timeout is separate from Workflow Guard. In Op
108
108
 
109
109
  ---
110
110
 
111
+ ## Secondary Review Requirement
112
+
113
+ Workflow Guard enforces secondary review approval (`requireReview`) by default during PR creation (`gh pr create` and `az repos pr create`). Before a pull request can be opened, an agent must invoke a secondary review subagent and record passing review approval using `record_review` evaluated against `guard_review_rubric`.
114
+
115
+ To opt out of the PR secondary review gate for a project, set `requireReview: false` in `.opencode/workflow-guard.json`:
116
+
117
+ ```json
118
+ {
119
+ "requireReview": false
120
+ }
121
+ ```
122
+
123
+ Or set `WORKFLOW_GUARD_REQUIRE_REVIEW=0` in your environment. Conversely, `WORKFLOW_GUARD_REQUIRE_REVIEW=1` enforces the review gate regardless of repository configuration.
124
+
125
+ ---
126
+
111
127
  ## Recommended Companion Configuration
112
128
 
113
129
  For defense in depth, pair `opencode-workflow-guard` with OpenCode's native permission rules in `opencode.json` (see [OpenCode Permissions](https://opencode.ai/docs/permissions/)):
package/docs/policies.md CHANGED
@@ -108,11 +108,13 @@ It is a policy and enforcement layer, not an agent harness. Policies may constra
108
108
 
109
109
  ### 15. Compaction State & Policy Context Preservation
110
110
  - Integrates with OpenCode's `experimental.session.compacting` hook to inject bounded active policy state into `output.context` before context summarization:
111
+ - Caps injected continuity context at 8,000 characters, preserving operational guard state first and filling the remaining budget with active tasks, review follow-ups, and fresh local project memory.
111
112
  - Active `todowrite` tasks with status badges and subagent hierarchy attribution.
112
113
  - Active Git branch name and protected branch status.
113
114
  - Test verification status (passed/failed, test command, and commit hash).
114
115
  - Secondary review verdicts (reviewer name and approval status).
115
116
  - Uncommitted mutation counts.
117
+ - Compacted project-memory context points back to `project_memory_search` for deeper historical retrieval instead of attempting to embed the full durable store.
116
118
  - Preserves policy-relevant context across session compactions without assigning, prioritizing, or sequencing tasks for the model.
117
119
 
118
120
  ### 16. TUI Visual Feedback & Static Status Badge
@@ -155,8 +157,13 @@ It is a policy and enforcement layer, not an agent harness. Policies may constra
155
157
  - Mismatches (failing verification, stale evidence due to mutations after the verify run, or no evidence at all) are journaled to the audit trail and logged at info level. This is **observability, not gating**: the response is never blocked, but a confident wrap-up cannot silently contradict a failing or absent verification state.
156
158
  - Uses the `experimental.text.complete` hook; claim detection is a conservative phrase heuristic to avoid false positives on casual wording.
157
159
 
158
- ### Tool Description Honesty
159
- - Via the `tool.definition` hook, the guard enriches the `todowrite` tool's description with its replacement-list lifecycle and finalization gates (including verification evidence required after the last mutation), so the model is not surprised by preventable lifecycle blocks. Other tools are untouched, and the enrichment is idempotent.
160
+ ### Tool Description Honesty & Cross-Model Adherence
161
+ - Via the `tool.definition` hook, the guard enriches tool descriptions to keep them honest and proactive across all model families (OpenAI, Gemini, Claude, and open-weights models):
162
+ - `todowrite`: Explains its replacement-list lifecycle, finalization verification gate, and directs models to `guard_next_tasks` for roadmap discovery and `guard_status` for gate checks.
163
+ - Mutating tools (`edit`, `write`, `apply_patch`): States preconditions (active todo, feature branch, prior read).
164
+ - `task`: Guides orchestrator agents to isolate parallel mutations with `guard_worktree_create` and conduct secondary reviews via `guard_review_rubric` and `record_review`.
165
+ - Via the `experimental.chat.system.transform` hook, the guard provides concise operational guidelines in the system prompt detailing available guard tools (`guard_next_tasks`, `guard_status`, `guard_review_rubric`, `guard_worktree_create`, `project_memory_*`, `learning_*`, `guard_why`), heavily encouraging open models and Gemini/Claude to proactively use workflow discipline features throughout development.
166
+ - Companion tools feature proactive action triggers ("Proactively call...", "Recommended at session start..."), and `guard_status` returns actionable `recommendedActions` directing models to the exact next step.
160
167
 
161
168
  ---
162
169
 
@@ -181,7 +188,7 @@ Besides enforcement hooks, the guard registers companion tools in OpenCode:
181
188
 
182
189
  ### Inspection Tools
183
190
  - `guard_status`, `guard_audit`, `guard_why`, `guard_review_rubric`, `record_review`, `guard_review_followups`, and `guard_review_followup_resolve` provide runtime introspection of guardrail state, audit entries, block explanations, reviewer rubric definitions, reviewer decisions, and durable local review debt. `guard_status` also exposes the effective Ralph opt-in, iteration budget, and calling-session outcome when applicable.
184
- - **Priority-Ranked Review Gate:** `guard_review_rubric` includes P0-P3 severity tiers. When recording reviews via `record_review`, approvals that include active P0 (blocker) or P1 (major defect) issues are rejected until blockers are resolved.
191
+ - **Priority-Ranked Review Gate:** Secondary review approval (`requireReview`) is enforced by default during PR preflight (`gh pr create` / `az repos pr create`). Before creating a pull request, a secondary reviewer subagent must evaluate the changes using `guard_review_rubric` and record approval with `record_review`. Approvals that include active P0 (blocker) or P1 (major defect) issues are rejected until blockers are resolved. Projects can opt out via `.opencode/workflow-guard.json` (`{ "requireReview": false }`) or environment variable `WORKFLOW_GUARD_REQUIRE_REVIEW=0`. Conversely, `WORKFLOW_GUARD_REQUIRE_REVIEW=1` forces the review requirement regardless of project config.
185
192
  - **Durable Lower-Priority Follow-ups:** Accepted review summaries containing a P2 or P3 finding are persisted in the project's local Workflow Guard SQLite database. Open findings are included in compacted agent context and remain open across sessions until explicitly resolved with `guard_review_followup_resolve`; they are not silently discarded with the review session.
186
193
  - **Privacy-Safe Local Telemetry:** Guard decisions remain in the local audit JSONL. Tool after-hook observations add `callID` and duration records for local diagnosis and performance analysis; the current hook API does not distinguish success from every failure path, so these records do not claim a success outcome. Command and patch bodies are never written to the journal; only their byte count and SHA-256 fingerprint are retained for correlation.
187
194
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-workflow-guard",
3
- "version": "1.10.1",
3
+ "version": "1.12.0",
4
4
  "description": "Deterministic workflow and safety policy enforcement for OpenCode",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -53,6 +53,51 @@ export function extractReviewFollowups(summary: string): Array<{ severity: "P2"
53
53
  .map((finding) => ({ severity: finding.severity, summary: finding.line }));
54
54
  }
55
55
 
56
+ export function buildWorkflowGuardSystemGuidance(options: {
57
+ projectMemoryEnabled?: boolean;
58
+ learningEnabled?: boolean;
59
+ recoveryCheckpointsEnabled?: boolean;
60
+ isReadOnly?: boolean;
61
+ }): string {
62
+ const sections: string[] = [
63
+ "## Workflow Guard & Operational Tools",
64
+ "Workflow Guard enforces safe engineering discipline and provides proactive workflow tools:",
65
+ "- Task Planning: Proactively call `guard_next_tasks` at session start or when planning work to discover repository roadmap, plan, and TODO items. Check `guard_status` to inspect branch protection, mutation count, and outstanding verification/review gates.",
66
+ ];
67
+
68
+ if (!options.isReadOnly) {
69
+ sections.push(
70
+ "- Review & Verification: Before completing tasks or creating PRs, run tests to produce fresh verification evidence, retrieve the review rubric via `guard_review_rubric` (5 core axes: test integrity, task completeness, cleanliness, security, platform), and spawn a reviewer subagent via `task` that records its verdict with `record_review`.",
71
+ "- Worktree Isolation: When orchestrating parallel subagent tasks with `task`, call `guard_worktree_create` to prevent concurrent file conflicts, and `guard_worktree_cleanup` when finished.",
72
+ );
73
+ }
74
+
75
+ sections.push(
76
+ "- Technical Debt: Inspect open P2/P3 review follow-ups with `guard_review_followups` during planning; resolve fixed items with `guard_review_followup_resolve`.",
77
+ "- Policy Simulation: If uncertain whether a command or file edit is permitted, simulate it with `guard_why`.",
78
+ );
79
+
80
+ if (options.projectMemoryEnabled) {
81
+ sections.push(
82
+ "- Project Memory: Proactively call `project_memory_search` before architectural changes or complex implementations. Record durable lessons, architectural decisions, and repo constraints with `project_memory_record`.",
83
+ );
84
+ }
85
+
86
+ if (options.learningEnabled) {
87
+ sections.push(
88
+ "- Socratic Learning: When facing non-trivial architectural trade-offs, design choices, or debugging breakthroughs, call `learning_checkpoint`. If selected (intervene: true), ask one concise question via `question` before proceeding, and capture observed understanding with `learning_record`.",
89
+ );
90
+ }
91
+
92
+ if (options.recoveryCheckpointsEnabled) {
93
+ sections.push(
94
+ "- Recovery: If workspace state needs rollback to the pre-run checkpoint, use `guard_recovery_restore`.",
95
+ );
96
+ }
97
+
98
+ return sections.join("\n");
99
+ }
100
+
56
101
  export function createCustomTools(options: {
57
102
  effectiveRoot: string;
58
103
  projectMemoryEnabled: boolean;
@@ -80,7 +125,7 @@ export function createCustomTools(options: {
80
125
  }),
81
126
  } : {}),
82
127
  guard_next_tasks: tool({
83
- description: "Load durable repository task context when deciding what to work on next. Prefers TODO.md; if absent, discovers conventional roadmap, plan, tasks, backlog, and docs/plans Markdown files.",
128
+ description: "Load durable repository task context when deciding what to work on next or planning upcoming steps. Proactively call at session start or during planning to discover roadmap, plan, tasks, backlog, and TODO.md files.",
84
129
  args: {},
85
130
  execute: async () => {
86
131
  const sources = discoverPlanningSources(effectiveRoot);
@@ -89,12 +134,12 @@ export function createCustomTools(options: {
89
134
  }),
90
135
  ...(projectMemoryEnabled ? {
91
136
  project_memory_search: tool({
92
- description: "Search current durable knowledge for this project. Returns concise typed records with provenance; superseded records are excluded.",
137
+ description: "Search current durable knowledge for this project, including past architectural decisions, constraints, and conventions. Proactively call before starting complex architectural changes, refactors, or new feature implementations.",
93
138
  args: { query: tool.schema.string() },
94
139
  execute: async (args) => projectMemory ? JSON.stringify(args.query.length <= 500 ? searchProjectMemory(projectMemory, args.query, 8) : [], null, 2) : "[workflow-guard] Project memory unavailable; core guard enforcement remains active.",
95
140
  }),
96
141
  project_memory_record: tool({
97
- description: "Record a durable project fact, decision, constraint, or lesson when it will matter in future sessions. Do not record transient tool output, secrets, or speculative hypotheses.",
142
+ description: "Record a durable project fact, decision, constraint, or lesson when it will matter in future sessions. Proactively call after making important architectural choices, discovering tricky repo constraints, or establishing new patterns.",
98
143
  args: { kind: tool.schema.string().describe("fact, decision, constraint, or lesson"), content: tool.schema.string(), paths: tool.schema.array(tool.schema.string()), supersedes: tool.schema.string().optional() },
99
144
  execute: async (args, toolContext) => {
100
145
  if (!projectMemory) return "[workflow-guard] Project memory unavailable; core guard enforcement remains active.";
@@ -105,7 +150,7 @@ export function createCustomTools(options: {
105
150
  },
106
151
  }),
107
152
  project_memory_export: tool({
108
- description: "Explicitly promote selected durable project-memory records to the human-readable repo-local .opencode/memory/project-memory.jsonl file. Nothing is exported automatically.",
153
+ description: "Explicitly promote selected durable project-memory records to the human-readable repo-local .opencode/memory/project-memory.jsonl file. Call when durable knowledge should be committed and shared with the team.",
109
154
  args: { ids: tool.schema.array(tool.schema.string()) },
110
155
  execute: async (args) => {
111
156
  if (!projectMemory) return "[workflow-guard] Project memory unavailable; core guard enforcement remains active.";
@@ -114,12 +159,12 @@ export function createCustomTools(options: {
114
159
  return `[workflow-guard] Exported ${count} project-memory record(s) to .opencode/memory/project-memory.jsonl.`;
115
160
  },
116
161
  }),
117
- project_memory_import: tool({ description: "Import the fixed repo-local .opencode/memory/project-memory.jsonl file into this project's local working-memory index.", args: {}, execute: async () => projectMemory ? `[workflow-guard] Imported ${importProjectKnowledge(projectMemory, portableMemoryPath, (content) => secretIn(content) !== undefined)} new project-memory record(s).` : "[workflow-guard] Project memory unavailable; core guard enforcement remains active." }),
162
+ project_memory_import: tool({ description: "Import the repo-local .opencode/memory/project-memory.jsonl file into this project's local working-memory index. Call to synchronize local project memory with shared repository records.", args: {}, execute: async () => projectMemory ? `[workflow-guard] Imported ${importProjectKnowledge(projectMemory, portableMemoryPath, (content) => secretIn(content) !== undefined)} new project-memory record(s).` : "[workflow-guard] Project memory unavailable; core guard enforcement remains active." }),
118
163
  } : {}),
119
164
  ...(learningEnabled ? {
120
- learning_profile: tool({ description: "Inspect the local evidence-based learner profile so teaching can build on demonstrated knowledge without assuming unobserved concepts are gaps.", args: {}, execute: async () => JSON.stringify(loadLearnerProfile(), null, 2) }),
165
+ learning_profile: tool({ description: "Inspect the local evidence-based learner profile so explanations and teaching can build on demonstrated knowledge without assuming unobserved concepts are gaps.", args: {}, execute: async () => JSON.stringify(loadLearnerProfile(), null, 2) }),
121
166
  learning_checkpoint: tool({
122
- description: "At a high-value design decision, debugging moment, or important new concept, rank candidate Socratic learning opportunities. If one is selected, ask one concise question before continuing the work; do not turn routine syntax into a lesson.",
167
+ description: "At a high-value design decision, debugging moment, or important new concept, rank candidate Socratic learning opportunities. Proactively call when encountering non-trivial architectural trade-offs, design choices, or tricky bugs. If selected (intervene: true), ask one concise question via question before continuing; do not turn routine syntax into a lesson.",
123
168
  args: { opportunities: tool.schema.array(tool.schema.object({ type: tool.schema.string().describe("design, debugging, or new-concept"), concept: tool.schema.string().describe("Transferable engineering concept"), relevance: tool.schema.number().describe("Current-task relevance from 0 to 1"), consequence: tool.schema.number().describe("Decision consequence from 0 to 1") })) },
124
169
  execute: async (args, toolContext) => {
125
170
  const valid = args.opportunities.slice(0, 20).filter((candidate): candidate is typeof candidate & { type: "design" | "debugging" | "new-concept" } => (candidate.type === "design" || candidate.type === "debugging" || candidate.type === "new-concept") && candidate.concept.length > 0 && candidate.concept.length <= 100 && candidate.relevance >= 0 && candidate.relevance <= 1 && candidate.consequence >= 0 && candidate.consequence <= 1);
@@ -131,7 +176,7 @@ export function createCustomTools(options: {
131
176
  },
132
177
  }),
133
178
  learning_record: tool({
134
- description: "Record concise evidence from a real Socratic interaction after observing the learner's reasoning. Record what was demonstrated, not a grade or an inferred deficit.",
179
+ description: "Record concise evidence from a real Socratic interaction after observing the learner's reasoning and response to a learning checkpoint question. Record what was demonstrated, not a grade or an inferred deficit.",
135
180
  args: { concept: tool.schema.string(), kind: tool.schema.string().describe("exposed, developing, demonstrated, independent, critique, or needs-reinforcement"), summary: tool.schema.string().describe("Short factual description of observed reasoning") },
136
181
  execute: async (args, toolContext) => {
137
182
  const validKinds = new Set(["exposed", "developing", "demonstrated", "independent", "critique", "needs-reinforcement"]);
@@ -151,7 +196,7 @@ export function createCustomTools(options: {
151
196
  }),
152
197
  } : {}),
153
198
  guard_status: tool({
154
- description: "Inspect active guardrails, current branch protection, and verification/review status.", args: {},
199
+ description: "Inspect active guardrails, current branch protection, mutation count, outstanding verification/review requirements, and ralph status. Proactively call at session start and before completing tasks or creating PRs.", args: {},
155
200
  execute: async (_args, toolContext) => {
156
201
  const root = effectiveRoot; const branch = currentGitBranch(root) ?? "unknown"; const isProtected = onProtectedBranch(root); const lastV = getLastVerifyResultForWorkspace(root); const lastR = getLastReviewResultForWorkspace(root); const lastMut = getWorkspaceMutationTimestamp(root); const cfg = loadProjectConfig(root);
157
202
  const subject = { workspace: projectRootKey(root), commitHash: getCurrentGitCommitHash(root), worktreeFingerprint: getGitWorktreeFingerprint(root) };
@@ -167,14 +212,32 @@ export function createCustomTools(options: {
167
212
  ...(reviewRequired && !reviewFresh ? ["review"] : []),
168
213
  ...(documentationRequired && !branchHasDocumentationChange(root) ? ["documentation"] : []),
169
214
  ];
215
+ const recommendedActions: string[] = [];
216
+ if (outstandingRequirements.includes("verification")) {
217
+ recommendedActions.push(verifyCommand ? `Execute test verification command '${verifyCommand}' to produce fresh verification evidence.` : "Run project tests to establish fresh verification evidence.");
218
+ }
219
+ if (outstandingRequirements.includes("review")) {
220
+ recommendedActions.push("Call guard_review_rubric to retrieve the 5-axis review rubric, then spawn a secondary reviewer subagent via task to evaluate changes and record verdict with record_review.");
221
+ }
222
+ if (outstandingRequirements.includes("documentation")) {
223
+ recommendedActions.push("Update documentation files (e.g. README.md, docs/) to satisfy the documentation gate before opening a PR.");
224
+ }
225
+ let openFollowupsCount = 0;
226
+ try { openFollowupsCount = followupStore ? listReviewFollowups(followupStore, "open", 10).length : 0; } catch {}
227
+ if (openFollowupsCount > 0) {
228
+ recommendedActions.push(`Inspect ${openFollowupsCount} open P2/P3 review follow-up(s) with guard_review_followups and resolve with guard_review_followup_resolve once addressed.`);
229
+ }
230
+ if (recommendedActions.length === 0) {
231
+ recommendedActions.push("All operational requirements are satisfied for task finalization and PR creation.");
232
+ }
170
233
  const ralphOutcome = runWithRuntimeState(root, client, () => getRalphOutcome(toolContext.sessionID));
171
- return JSON.stringify({ workspaceRoot: root, branch, onProtectedBranch: isProtected, outstandingRequirements, lastMutationTimestamp: lastMut, mutationCount: getWorkspaceMutationCount(root), lastVerify: lastV && verifyEvidence ? { command: lastV.command, passed: lastV.passed, fresh: verifyFresh, evidenceId: verifyEvidence.id, commitHash: lastV.commitHash } : null, lastReview: lastR && reviewEvidenceRecord ? { reviewer: lastR.reviewer, passed: lastR.passed, summary: lastR.summary, fresh: reviewFresh, evidenceId: reviewEvidenceRecord.id } : null, ralph: { enabled: isRalphModeEnabled(root), maxIterations: getRalphMaxIterations(root), outcome: ralphOutcome ?? null }, projectConfig: { profile: getOperationProfile(root), protectedBranches: cfg.protectedBranches ?? ["main", "master"], verifyCommand: verifyCommand ?? null, requireReview: reviewRequired, requireDocumentation: documentationRequired, recoveryCheckpoints: isRecoveryCheckpointsEnabled(root) } }, null, 2);
234
+ return JSON.stringify({ workspaceRoot: root, branch, onProtectedBranch: isProtected, outstandingRequirements, recommendedActions, lastMutationTimestamp: lastMut, mutationCount: getWorkspaceMutationCount(root), lastVerify: lastV && verifyEvidence ? { command: lastV.command, passed: lastV.passed, fresh: verifyFresh, evidenceId: verifyEvidence.id, commitHash: lastV.commitHash } : null, lastReview: lastR && reviewEvidenceRecord ? { reviewer: lastR.reviewer, passed: lastR.passed, summary: lastR.summary, fresh: reviewFresh, evidenceId: reviewEvidenceRecord.id } : null, ralph: { enabled: isRalphModeEnabled(root), maxIterations: getRalphMaxIterations(root), outcome: ralphOutcome ?? null }, projectConfig: { profile: getOperationProfile(root), protectedBranches: cfg.protectedBranches ?? ["main", "master"], verifyCommand: verifyCommand ?? null, requireReview: reviewRequired, requireDocumentation: documentationRequired, recoveryCheckpoints: isRecoveryCheckpointsEnabled(root) } }, null, 2);
172
235
  },
173
236
  }),
174
- guard_audit: tool({ description: "View recent audit entries recorded by opencode-workflow-guard.", args: { limit: tool.schema.number().optional().describe("Maximum entries to return (default 10)") }, execute: async (args) => JSON.stringify(getRecentAuditEntries(typeof args?.limit === "number" ? Math.min(args.limit, 50) : 10), null, 2) }),
175
- guard_why: tool({ description: "Simulate and return the structured policy decision for a specific tool call or command.", args: { tool: tool.schema.string().describe("Tool name (e.g. bash, edit, write, read, apply_patch)"), input: tool.schema.record(tool.schema.string(), tool.schema.any()).optional().describe("Tool input arguments") }, execute: async (args, toolContext) => JSON.stringify(await runWithRuntimeState(effectiveRoot, client, () => guardToolCallImpl(args.tool, args.input ?? {}, { sessionID: toolContext.sessionID, worktree: toolContext.worktree, directory: toolContext.directory, simulate: true })), null, 2) }),
237
+ guard_audit: tool({ description: "View recent audit entries recorded by opencode-workflow-guard. Use to diagnose policy decisions, blocked commands, or outcome telemetry.", args: { limit: tool.schema.number().optional().describe("Maximum entries to return (default 10)") }, execute: async (args) => JSON.stringify(getRecentAuditEntries(typeof args?.limit === "number" ? Math.min(args.limit, 50) : 10), null, 2) }),
238
+ guard_why: tool({ description: "Simulate and return the structured policy decision for a specific tool call or command. Proactively use before executing questionable or complex commands to check if they would be blocked by guard policies.", args: { tool: tool.schema.string().describe("Tool name (e.g. bash, edit, write, read, apply_patch)"), input: tool.schema.record(tool.schema.string(), tool.schema.any()).optional().describe("Tool input arguments") }, execute: async (args, toolContext) => JSON.stringify(await runWithRuntimeState(effectiveRoot, client, () => guardToolCallImpl(args.tool, args.input ?? {}, { sessionID: toolContext.sessionID, worktree: toolContext.worktree, directory: toolContext.directory, simulate: true })), null, 2) }),
176
239
  record_review: tool({
177
- description: "Record a secondary reviewer agent's approval or critique of the current changes. The summary must reference the 5 core review axes from guard_review_rubric.",
240
+ description: "Record a secondary reviewer agent's approval or critique of the current changes. The summary must reference the 5 core review axes from guard_review_rubric (test integrity, task completeness, cleanliness, security, platform).",
178
241
  args: { reviewer: tool.schema.string().describe("Identifier/name of the reviewer subagent"), summary: tool.schema.string().describe("Review findings summary across the 5 core review axes"), passed: tool.schema.boolean().describe("True if change is approved, false if changes requested") },
179
242
  execute: async (args, toolContext) => {
180
243
  const auditVerdict = (verdict: "approved" | "changes_requested" | "rejected", reason: string) => audit({ ts: new Date().toISOString(), sessionID: toolContext.sessionID, tool: "record_review.verdict", decision: verdict === "rejected" ? "block" : "allow", phase: "event", reason, evidence: { reviewVerdict: verdict } });
@@ -190,10 +253,10 @@ export function createCustomTools(options: {
190
253
  return args.passed ? `[workflow-guard] Review recorded as APPROVED by ${args.reviewer}.` : `[workflow-guard] Review recorded as CHANGES REQUESTED by ${args.reviewer}.`;
191
254
  },
192
255
  }),
193
- guard_review_followups: tool({ description: "List durable local P2/P3 review follow-ups for this project. Open findings are technical debt that should be addressed rather than indefinitely deferred.", args: {}, execute: async () => JSON.stringify(followupStore ? listReviewFollowups(followupStore) : [], null, 2) }),
256
+ guard_review_followups: tool({ description: "List durable local P2/P3 review follow-ups (technical debt) for this project. Proactively check during planning or before final verification to address open findings.", args: {}, execute: async () => JSON.stringify(followupStore ? listReviewFollowups(followupStore) : [], null, 2) }),
194
257
  guard_review_followup_resolve: tool({ description: "Resolve a durable local review follow-up after the underlying issue has been fixed and verified.", args: { id: tool.schema.string() }, execute: async (args) => followupStore && resolveReviewFollowup(followupStore, args.id) ? `[workflow-guard] Review follow-up ${args.id} resolved.` : `[workflow-guard] Review follow-up ${args.id} was not open or was not found.` }),
195
258
  guard_review_rubric: tool({
196
- description: "Get the secondary-review rubric for the current branch diff. The orchestrator calls this, spawns a reviewer subagent with the rubric as the prompt, then the reviewer records its verdict via record_review.", args: { base: tool.schema.string().optional().describe("Base ref to diff against (default: origin/main, origin/master, main)") },
259
+ description: "Get the secondary-review rubric for the current branch diff across 5 core review axes (test integrity, task completeness, cleanliness, security, platform). The orchestrator should call this before finalizing work or creating a PR, then spawn a reviewer subagent with the rubric as the prompt, which records its verdict via record_review.", args: { base: tool.schema.string().optional().describe("Base ref to diff against (default: origin/main, origin/master, main)") },
197
260
  execute: async (args) => {
198
261
  const sanitizedBase = typeof args.base === "string" && !args.base.startsWith("-") && !/[\s;'"\0]/.test(args.base) ? args.base : undefined;
199
262
  const bases = sanitizedBase ? [sanitizedBase] : ["origin/main", "origin/master", "main", "master"];
@@ -204,7 +267,7 @@ export function createCustomTools(options: {
204
267
  },
205
268
  }),
206
269
  guard_worktree_create: tool({
207
- description: "Create an isolated git worktree directory for concurrent subagent execution.", args: { branch: tool.schema.string().describe("Branch name for the isolated worktree (e.g. 'feat/subagent-task')"), baseBranch: tool.schema.string().optional().describe("Base branch to branch off of (defaults to HEAD)") },
270
+ description: "Create an isolated git worktree directory for concurrent subagent execution. Proactively call when launching parallel tasks/subagents to prevent file collision and maintain branch isolation.", args: { branch: tool.schema.string().describe("Branch name for the isolated worktree (e.g. 'feat/subagent-task')"), baseBranch: tool.schema.string().optional().describe("Base branch to branch off of (defaults to HEAD)") },
208
271
  execute: async (args, toolContext) => {
209
272
  const todos = await effectiveTodos(toolContext.sessionID); if (todos !== undefined && !hasActiveTodo(todos)) return "[workflow-guard] Blocked: worktree creation with no active todo item. Break the request down with todowrite first, then create worktrees.";
210
273
  const toolRoot = toolContext.worktree || toolContext.directory || effectiveRoot; const res = createGitWorktree(args.branch, args.baseBranch ?? "HEAD", toolRoot); if (!res.success) return `[workflow-guard] Failed to create worktree: ${res.error}`;
@@ -212,7 +275,7 @@ export function createCustomTools(options: {
212
275
  },
213
276
  }),
214
277
  guard_worktree_cleanup: tool({
215
- description: "Commit a final snapshot and remove an isolated git worktree directory.", args: { worktreePath: tool.schema.string().describe("Path of the worktree directory to clean up") },
278
+ description: "Commit a final snapshot and remove an isolated git worktree directory. Call to clean up worktrees created with guard_worktree_create after subagent tasks complete.", args: { worktreePath: tool.schema.string().describe("Path of the worktree directory to clean up") },
216
279
  execute: async (args, toolContext) => {
217
280
  const todos = await effectiveTodos(toolContext.sessionID); if (todos !== undefined && !hasActiveTodo(todos)) return "[workflow-guard] Blocked: worktree cleanup with no active todo item. Break the request down with todowrite first, then clean up worktrees.";
218
281
  const toolRoot = toolContext.worktree || toolContext.directory || effectiveRoot; const res = cleanupGitWorktree(args.worktreePath, toolRoot); if (!res.success) return `[workflow-guard] Failed to clean up worktree: ${res.error}`;
package/src/lib/state.ts CHANGED
@@ -218,9 +218,11 @@ export function getProjectConfig(root: string): ProjectConfig {
218
218
  }
219
219
 
220
220
  export function isReviewRequired(root: string): boolean {
221
- if (process.env.WORKFLOW_GUARD_REQUIRE_REVIEW === "1") return true;
221
+ const env = process.env.WORKFLOW_GUARD_REQUIRE_REVIEW?.toLowerCase();
222
+ if (env === "0" || env === "false" || env === "off") return false;
223
+ if (env === "1" || env === "true" || env === "on") return true;
222
224
  const cfg = getProjectConfig(root);
223
- return cfg.requireReview === true;
225
+ return cfg.requireReview !== false;
224
226
  }
225
227
 
226
228
  export function isDocumentationRequired(root: string): boolean {
@@ -97,7 +97,7 @@ export async function continueUnfinishedSession(sessionID: string, settleTitle =
97
97
  messageID,
98
98
  parts: [{
99
99
  type: "text",
100
- text: `Workflow Guard: unfinished todos remain. Continue working through them.${tasksSummary}\nIf you need user input, use the question tool instead of ending the run.`,
100
+ text: `Workflow Guard: unfinished todos remain. Continue working through them.${tasksSummary}\nIf you need user input, use the question tool instead of ending the run. Check guard_status for outstanding verification/review requirements, and guard_review_rubric before final completion.`,
101
101
  synthetic: true,
102
102
  }],
103
103
  },
@@ -18,10 +18,10 @@ import { beginReadObservation, clearReadFingerprints, recordSuccessfulRead, stal
18
18
  import { ToolInvocationLifecycle } from "./lib/tool-lifecycle.ts";
19
19
  import { ToolOutcomeTracker, type ToolOutcomePart } from "./lib/tool-outcomes.ts";
20
20
  import { guardToolCallImpl, isReadOnlyRole } from "./lib/guard-dispatcher.ts";
21
- import { createCustomTools } from "./lib/custom-tools.ts";
21
+ import { createCustomTools, buildWorkflowGuardSystemGuidance } from "./lib/custom-tools.ts";
22
22
  import type { PolicyDecision, TodoSdkClient } from "./lib/types.ts";
23
23
  export { isReadOnlyRole } from "./lib/guard-dispatcher.ts";
24
- export { extractReviewFollowups } from "./lib/custom-tools.ts";
24
+ export { extractReviewFollowups, buildWorkflowGuardSystemGuidance } from "./lib/custom-tools.ts";
25
25
 
26
26
  // ── Types ────────────────────────────────────────────────────────────────────
27
27
  export type {
@@ -345,6 +345,24 @@ async function emitBlockFeedback(message: string): Promise<void> {
345
345
  await showBlockToast(message);
346
346
  }
347
347
 
348
+ export function buildCompactionContext(operationalState: string, priorityBlocks: string[], maxChars = 8_000): string {
349
+ if (!Number.isFinite(maxChars) || maxChars < 0 || !Number.isInteger(maxChars)) {
350
+ throw new RangeError("maxChars must be a non-negative finite integer");
351
+ }
352
+ const truncate = (value: string, limit: number): string => {
353
+ let end = Math.min(value.length, limit);
354
+ if (end > 0 && end < value.length && /[\uD800-\uDBFF]/.test(value[end - 1])) end -= 1;
355
+ return value.slice(0, end);
356
+ };
357
+ let context = truncate(operationalState, maxChars);
358
+ for (const block of priorityBlocks) {
359
+ const remaining = maxChars - context.length - 2;
360
+ if (remaining <= 0) break;
361
+ context += `\n\n${truncate(block, remaining)}`;
362
+ }
363
+ return context;
364
+ }
365
+
348
366
  export const WorkflowGuard: Plugin = async (ctx) => {
349
367
  // Honor worktree if present (e.g. opencode worktrees or devcontainers)
350
368
  // so worktree plugins cannot punch through boundary gates. When the host
@@ -416,7 +434,7 @@ export const WorkflowGuard: Plugin = async (ctx) => {
416
434
  await emitBlockFeedback(reason);
417
435
  const failureCount = input.sessionID ? toolOutcomes.getFailureCount(input.sessionID) : 0;
418
436
  const circuitBreakerSuffix = failureCount >= 2
419
- ? "\n\n[Workflow Guard Circuit Breaker: Repeated failures detected in this session. Stop attempting alternative workarounds or shell laundering. Address the required step above directly.]"
437
+ ? "\n\n[Workflow Guard Circuit Breaker: Repeated failures detected in this session. Stop attempting alternative workarounds or shell laundering. Address the required step above directly, or inspect policy details with guard_status or guard_why.]"
420
438
  : "";
421
439
  throw new Error(`[workflow-guard] ${reason}${circuitBreakerSuffix}`);
422
440
  }
@@ -484,7 +502,7 @@ export const WorkflowGuard: Plugin = async (ctx) => {
484
502
  : getLastReviewResult();
485
503
  const mutationCountVal = sessionID ? sessionMutationCount : getMutationCount();
486
504
 
487
- const contextBlocks: string[] = [];
505
+ const priorityContextBlocks: string[] = [];
488
506
 
489
507
  if (active && active.length > 0) {
490
508
  const lines = active.slice(0, 20).map(
@@ -497,7 +515,7 @@ export const WorkflowGuard: Plugin = async (ctx) => {
497
515
  : sessionID
498
516
  ? ` (Session: ${sessionID})`
499
517
  : "";
500
- contextBlocks.push(
518
+ priorityContextBlocks.push(
501
519
  `## Active Tasks${attribution}\n` +
502
520
  lines.join("\n") +
503
521
  "\nComplete tasks efficiently - mark finished items as completed and address remaining ones.",
@@ -523,9 +541,9 @@ export const WorkflowGuard: Plugin = async (ctx) => {
523
541
  try { openFollowups = followupStore ? listReviewFollowups(followupStore, "open", 8) : []; } catch {}
524
542
  if (openFollowups.length > 0) {
525
543
  stateLines.push(`- Open Review Follow-ups: ${openFollowups.length} local P2/P3 item(s)`);
526
- contextBlocks.push(`## Review Follow-ups\n${openFollowups.map((item) => `- [${item.severity}:${item.id.slice(0, 8)}] ${item.summary.slice(0, 300)}`).join("\n")}\nTreat these as durable technical debt: address relevant items when practical and resolve them explicitly after verification.`);
544
+ priorityContextBlocks.push(`## Review Follow-ups\n${openFollowups.map((item) => `- [${item.severity}:${item.id.slice(0, 8)}] ${item.summary.slice(0, 300)}`).join("\n")}\nTreat these as durable technical debt: address relevant items when practical and resolve them explicitly after verification.`);
527
545
  }
528
- contextBlocks.push(stateLines.join("\n"));
546
+ const operationalState = stateLines.join("\n");
529
547
  let projectKnowledge: ReturnType<typeof getRecentProjectMemory> = [];
530
548
  try {
531
549
  const candidates = (projectMemory ? getRecentProjectMemory(projectMemory, 8) : [])
@@ -535,11 +553,11 @@ export const WorkflowGuard: Plugin = async (ctx) => {
535
553
  } catch {}
536
554
  if (projectKnowledge.length > 0) {
537
555
  const lines = projectKnowledge.map((memory) => `- [${memory.kind}:${memory.id.slice(0, 8)}] ${memory.content.slice(0, 300)}`);
538
- contextBlocks.push(`## Project Memory\n${lines.join("\n")}\nTreat these as historical project knowledge; verify against current repository state when relevant files have changed.`);
556
+ priorityContextBlocks.push(`## Project Memory\nUse project_memory_search when deeper historical context is needed. Treat these as historical project knowledge; verify against current repository state when relevant files have changed.\n${lines.join("\n")}`);
539
557
  }
540
558
 
541
559
  if (Array.isArray(output?.context)) {
542
- output.context.push(contextBlocks.join("\n\n"));
560
+ output.context.push(buildCompactionContext(operationalState, priorityContextBlocks));
543
561
  }
544
562
  } catch {}
545
563
  },
@@ -611,13 +629,14 @@ export const WorkflowGuard: Plugin = async (ctx) => {
611
629
  // Keep tool descriptions honest: todowrite's description reflects lifecycle
612
630
  // and finalization gates so the model is not surprised by preventable blocks.
613
631
  // Similarly, mutating tools reflect their active-todo, feature-branch, and prior-read requirements.
632
+ // Subagent orchestration tools reflect worktree isolation and review expectations.
614
633
  "tool.definition": async (input, output) => {
615
634
  if (input.toolID === "todowrite") {
616
635
  const description = typeof output.description === "string" ? output.description : "";
617
636
  if (description.includes("verification evidence")) return;
618
637
  output.description =
619
638
  description +
620
- "\n\nWorkflow Guard lifecycle: each todowrite call replaces the complete task list. Preserve every pending/in_progress task in subsequent updates until you explicitly mark it completed or cancelled; do not omit active tasks when adding new work. Marking every task completed triggers the finalization gate - fresh verification evidence (test run) is required after the last mutation, and protected-branch/conflict checks apply.";
639
+ "\n\nWorkflow Guard lifecycle: each todowrite call replaces the complete task list. Preserve every pending/in_progress task in subsequent updates until you explicitly mark it completed or cancelled; do not omit active tasks when adding new work. Marking every task completed triggers the finalization gate - fresh verification evidence (test run) is required after the last mutation, and protected-branch/conflict checks apply. Proactively call guard_next_tasks when planning work to discover existing roadmap/TODOs, and guard_status to check outstanding requirements.";
621
640
  return;
622
641
  }
623
642
  if (EDIT_TOOL_NAMES.has(input.toolID)) {
@@ -628,6 +647,30 @@ export const WorkflowGuard: Plugin = async (ctx) => {
628
647
  "\n\nWorkflow Guard requirement: Modifications require an active task in todowrite (status pending or in_progress), a feature branch (edits on main/master are blocked), and a prior read of existing files in the current session.";
629
648
  return;
630
649
  }
650
+ if (input.toolID === "task") {
651
+ const description = typeof output.description === "string" ? output.description : "";
652
+ if (description.includes("Workflow Guard subagent guidance")) return;
653
+ output.description =
654
+ description +
655
+ "\n\nWorkflow Guard subagent guidance: When spawning parallel worker subagents, use guard_worktree_create to isolate file mutations in separate worktrees. When conducting code reviews before task completion or PR creation, fetch the rubric with guard_review_rubric and provide it to a reviewer subagent to evaluate and record via record_review.";
656
+ return;
657
+ }
658
+ },
659
+
660
+ "experimental.chat.system.transform": async (input, output) => {
661
+ try {
662
+ if (!Array.isArray(output?.system)) return;
663
+ const existing = output.system.join("\n");
664
+ if (existing.includes("## Workflow Guard & Operational Tools")) return;
665
+ const isReadOnly = (input as { agent?: string })?.agent ? isReadOnlyRole((input as { agent?: string }).agent!) : false;
666
+ const guidance = buildWorkflowGuardSystemGuidance({
667
+ projectMemoryEnabled,
668
+ learningEnabled,
669
+ recoveryCheckpointsEnabled: isRecoveryCheckpointsEnabled(effectiveRoot),
670
+ isReadOnly,
671
+ });
672
+ output.system.push(guidance);
673
+ } catch {}
631
674
  },
632
675
 
633
676
  "chat.message": async (input) => {
package/test/test.mts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  setWorkspaceRoot,
13
13
  setSdkClient,
14
14
  WorkflowGuard,
15
+ buildCompactionContext,
15
16
  detectVerifyCommand,
16
17
  runVerify,
17
18
  getCleanEnv,
@@ -62,6 +63,7 @@ import {
62
63
  checkLockfileSync,
63
64
  branchHasDocumentationChange,
64
65
  isDocumentationRequired,
66
+ isReviewRequired,
65
67
  getOperationProfile,
66
68
  isRecoveryCheckpointsEnabled,
67
69
  getSubagentMutationBudget,
@@ -241,6 +243,7 @@ check("allow git push origin feature/x", !(await shell("git push origin feature/
241
243
  check("allow push to main-backup (ref-like path)", !(await shell("git push origin main-backup")));
242
244
 
243
245
  console.log("- Policy 3: PR changelog (GitHub & Azure DevOps) -");
246
+ process.env.WORKFLOW_GUARD_REQUIRE_REVIEW = "0";
244
247
  check("block gh pr create without changelog", blocked(await shell("gh pr create --title t --body 'no changes here'")));
245
248
  check("allow gh pr create with Changelog: body", !(await shell("gh pr create --title t --body 'Changelog: fixed stuff'")));
246
249
  check("allow gh pr create with Summary release information", !(await shell("gh pr create --title t --body '## Summary\n- Fix stale tool outcome tracking'")));
@@ -341,6 +344,8 @@ check("unrelated shell workdir cannot supply PR changelog evidence", blocked(awa
341
344
  spawnSync("git", ["worktree", "remove", "--force", changesetWorktree], { cwd: changesetRepo });
342
345
  rmSync(changesetRepo, { recursive: true, force: true });
343
346
 
347
+ delete process.env.WORKFLOW_GUARD_REQUIRE_REVIEW;
348
+
344
349
  console.log("- Policy 4: destructive commands -");
345
350
  check("block kubectl delete", blocked(await shell("kubectl delete pod foo")));
346
351
  check("block helm uninstall", blocked(await shell("helm uninstall my-release")));
@@ -561,6 +566,13 @@ check("multi-target tee with an outside path is blocked", blocked(await call("ba
561
566
  check("tee --append within workspace is allowed with todos", !(await call("bash", { command: "echo x | tee --append src/a.ts" }, { sessionID: "s-active" })));
562
567
 
563
568
  console.log("- Compaction focus preservation & TUI toast -");
569
+ const boundedCompaction = buildCompactionContext("## Operational Guard State\n- critical", ["## Active Tasks\n" + "a".repeat(10_000), "## Project Memory\nshould-not-fit"], 500);
570
+ check("compaction packer enforces its total context budget", boundedCompaction.length === 500);
571
+ check("compaction packer preserves mandatory operational state before optional context", boundedCompaction.startsWith("## Operational Guard State\n- critical") && !boundedCompaction.includes("should-not-fit"));
572
+ check("compaction packer preserves the largest valid Unicode prefix", buildCompactionContext("critical \u{1F680}state", [], 10) === "critical ");
573
+ let invalidCompactionBudgetRejected = false;
574
+ try { buildCompactionContext("critical", [], Number.NaN); } catch (error) { invalidCompactionBudgetRejected = error instanceof RangeError; }
575
+ check("compaction packer rejects invalid public budgets", invalidCompactionBudgetRejected);
564
576
  let toasts: unknown[] = [];
565
577
  const toastClient = {
566
578
  session: fakeClient.session,
@@ -584,6 +596,8 @@ if (typeof compactFn === "function") {
584
596
  await compactFn({ sessionID: "s-active" } as any, compactOutput as any);
585
597
  }
586
598
  check("compaction hook injects active tasks into output.context", compactOutput.context.length > 0 && (compactOutput.context[0]?.includes("Active Tasks") ?? false));
599
+ check("compaction hook keeps injected continuity context bounded", compactOutput.context.every((context) => context.length <= 8000));
600
+ check("compaction hook preserves operational guard state within its budget", compactOutput.context.some((context) => context.includes("## Operational Guard State") && context.includes("Git Branch:")));
587
601
 
588
602
  // Blocked tool call emits one in-app warning via tui.showToast.
589
603
  toasts = [];
@@ -1802,6 +1816,44 @@ setWorkspaceRoot(root);
1802
1816
  reloadProjectConfig(root);
1803
1817
  resetReviewState();
1804
1818
 
1819
+ // Default requireReview behavior: true by default, opt-out with config false or env 0
1820
+ const defaultReviewRepo = mkdtempSync(join(tmpdir(), "wg-default-review-"));
1821
+ mkdirSync(join(defaultReviewRepo, ".opencode"), { recursive: true });
1822
+ setWorkspaceRoot(defaultReviewRepo);
1823
+ reloadProjectConfig(defaultReviewRepo);
1824
+ check("requireReview defaults to true when unconfigured", isReviewRequired(defaultReviewRepo) === true);
1825
+ resetReviewState();
1826
+ check(
1827
+ "PR creation blocked by default without recorded review in unconfigured repo",
1828
+ blocked(await shell("gh pr create --title t --body 'Changelog: update'")),
1829
+ );
1830
+ writeFileSync(join(defaultReviewRepo, ".opencode", "workflow-guard.json"), JSON.stringify({ requireReview: false }));
1831
+ reloadProjectConfig(defaultReviewRepo);
1832
+ check("explicit requireReview: false disables review gate", isReviewRequired(defaultReviewRepo) === false);
1833
+ check(
1834
+ "PR creation allowed when requireReview is explicitly false",
1835
+ !(await shell("gh pr create --title t --body 'Changelog: update'")),
1836
+ );
1837
+ process.env.WORKFLOW_GUARD_REQUIRE_REVIEW = "1";
1838
+ check("WORKFLOW_GUARD_REQUIRE_REVIEW=1 forces review even when config is false", isReviewRequired(defaultReviewRepo) === true);
1839
+ check(
1840
+ "PR creation blocked when WORKFLOW_GUARD_REQUIRE_REVIEW=1 overrides config false",
1841
+ blocked(await shell("gh pr create --title t --body 'Changelog: update'")),
1842
+ );
1843
+ writeFileSync(join(defaultReviewRepo, ".opencode", "workflow-guard.json"), JSON.stringify({ requireReview: true }));
1844
+ reloadProjectConfig(defaultReviewRepo);
1845
+ process.env.WORKFLOW_GUARD_REQUIRE_REVIEW = "0";
1846
+ check("WORKFLOW_GUARD_REQUIRE_REVIEW=0 overrides config true", isReviewRequired(defaultReviewRepo) === false);
1847
+ check(
1848
+ "PR creation allowed when WORKFLOW_GUARD_REQUIRE_REVIEW=0 overrides config true",
1849
+ !(await shell("gh pr create --title t --body 'Changelog: update'")),
1850
+ );
1851
+ delete process.env.WORKFLOW_GUARD_REQUIRE_REVIEW;
1852
+ rmSync(defaultReviewRepo, { recursive: true, force: true });
1853
+ setWorkspaceRoot(root);
1854
+ reloadProjectConfig(root);
1855
+ resetReviewState();
1856
+
1805
1857
  // 12. Custom Plugin Tools & Event Auditing
1806
1858
  console.log("- Custom Tools & Event Auditing -");
1807
1859
  const customPlugin = await (defaultExport?.server ?? WorkflowGuard)({
@@ -1857,6 +1909,10 @@ check("guard_status exposes subject-bound evidence references", parsedStatus.las
1857
1909
  check("guard_status exposes effective operation profile without TUI state", parsedStatus.projectConfig.profile === "interactive" && typeof parsedStatus.projectConfig.recoveryCheckpoints === "boolean");
1858
1910
  check("guard_status exposes disabled-by-default Ralph accountability state", parsedStatus.ralph?.enabled === false && parsedStatus.ralph?.maxIterations === 10 && parsedStatus.ralph?.outcome === null);
1859
1911
  check("guard_status exposes bounded outstanding requirements", Array.isArray(parsedStatus.outstandingRequirements) && parsedStatus.outstandingRequirements.every((requirement: unknown) => ["verification", "review", "documentation"].includes(String(requirement))));
1912
+ check("guard_status exposes actionable recommended actions", Array.isArray(parsedStatus.recommendedActions) && parsedStatus.recommendedActions.length > 0);
1913
+ check("guard_next_tasks description includes proactive trigger", Boolean(customPlugin.tool?.guard_next_tasks?.description.includes("Proactively call")));
1914
+ check("project_memory_search description includes proactive trigger", Boolean(customPlugin.tool?.project_memory_search?.description?.includes("Proactively call")));
1915
+ check("guard_review_rubric description includes orchestrator trigger", Boolean(customPlugin.tool?.guard_review_rubric?.description?.includes("orchestrator should call")));
1860
1916
 
1861
1917
  const requirementsRoot = mkdtempSync(join(tmpdir(), "wg-status-requirements-"));
1862
1918
  mkdirSync(join(requirementsRoot, ".opencode"), { recursive: true });
@@ -2191,6 +2247,7 @@ check(
2191
2247
  // Now update README.md or docs
2192
2248
  writeFileSync(join(docRepo, "README.md"), "# Updated Docs\n");
2193
2249
  spawnSync("git", ["commit", "-am", "docs: update README"], { cwd: docRepo });
2250
+ recordReviewResult("reviewer-agent", "LGTM - 5 axes verified", true, undefined, docRepo);
2194
2251
  check("branchHasDocumentationChange returns true once docs are modified", branchHasDocumentationChange(docRepo));
2195
2252
  check(
2196
2253
  "gh pr create passes once documentation update is present",
@@ -3165,6 +3222,26 @@ const editIdemDef = { description: editDef.description, parameters: {} };
3165
3222
  await defPlugin["tool.definition"]?.({ toolID: "edit" } as any, editIdemDef);
3166
3223
  check("tool.definition edit enrichment is idempotent", editIdemDef.description === editDef.description);
3167
3224
 
3225
+ const taskDef = { description: "Launch subagents.", parameters: {} };
3226
+ await defPlugin["tool.definition"]?.({ toolID: "task" } as any, taskDef);
3227
+ check("tool.definition enriches task tool with subagent and review guidance", taskDef.description.includes("Workflow Guard subagent guidance") && taskDef.description.includes("guard_worktree_create") && taskDef.description.includes("guard_review_rubric"));
3228
+ const taskIdemDef = { description: taskDef.description, parameters: {} };
3229
+ await defPlugin["tool.definition"]?.({ toolID: "task" } as any, taskIdemDef);
3230
+ check("tool.definition task enrichment is idempotent", taskIdemDef.description === taskDef.description);
3231
+ check("tool.definition todowrite enriches with planning and status guidance", todoDef.description.includes("guard_next_tasks") && todoDef.description.includes("guard_status"));
3232
+
3233
+ // experimental.chat.system.transform provides cross-model workflow guidance
3234
+ const systemOutput = { system: ["Base system prompt"] };
3235
+ await defPlugin["experimental.chat.system.transform"]?.({ model: { id: "gemini-flash" } as any }, systemOutput);
3236
+ check("experimental.chat.system.transform adds workflow guard guidance", systemOutput.system.length === 2 && systemOutput.system[1].includes("## Workflow Guard & Operational Tools") && systemOutput.system[1].includes("guard_next_tasks") && systemOutput.system[1].includes("guard_review_rubric"));
3237
+ const systemOutputIdem = { system: [...systemOutput.system] };
3238
+ await defPlugin["experimental.chat.system.transform"]?.({ model: { id: "gemini-flash" } as any }, systemOutputIdem);
3239
+ check("experimental.chat.system.transform is idempotent", systemOutputIdem.system.length === 2);
3240
+
3241
+ const readOnlySystemOutput = { system: ["Base system prompt"] };
3242
+ await defPlugin["experimental.chat.system.transform"]?.({ agent: "reviewer", model: { id: "claude-sonnet" } as any } as any, readOnlySystemOutput);
3243
+ check("experimental.chat.system.transform respects read-only roles", !readOnlySystemOutput.system[1].includes("spawn a reviewer subagent"));
3244
+
3168
3245
  // Socratic learning engine: evidence is explicit and interventions favor
3169
3246
  // relevant gaps without repeatedly interrupting demonstrated knowledge.
3170
3247
  const learner = createLearnerProfile();
@@ -3244,6 +3321,7 @@ const learningPlugin = await WorkflowGuard({ directory: root, worktree: root, cl
3244
3321
  check("learning mode registers profile tool when explicitly enabled", !!learningPlugin.tool?.learning_profile);
3245
3322
  check("learning mode registers adaptive checkpoint tool", !!learningPlugin.tool?.learning_checkpoint);
3246
3323
  check("learning mode registers evidence recorder", !!learningPlugin.tool?.learning_record);
3324
+ check("learning_checkpoint description includes proactive trigger", Boolean(learningPlugin.tool?.learning_checkpoint?.description?.includes("Proactively call")));
3247
3325
  const checkpointResult = JSON.parse(await (learningPlugin.tool as any).learning_checkpoint.execute({
3248
3326
  opportunities: [{ type: "design", concept: "application-state", relevance: 1, consequence: 0.9 }],
3249
3327
  }, { sessionID: "s-learning-tools" }));