opencode-workflow-guard 1.8.6 → 1.8.7
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 +1 -1
- package/package.json +1 -1
- package/src/lib/custom-tools.ts +4 -2
- package/src/workflow-guard-ui.ts +5 -76
- package/src/workflow-guard.ts +1 -1
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ opencode plugin "opencode-workflow-guard@$VERSION" --global --force
|
|
|
23
23
|
|
|
24
24
|
*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
25
|
|
|
26
|
-
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
|
|
26
|
+
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
27
|
|
|
28
28
|

|
|
29
29
|
|
package/package.json
CHANGED
package/src/lib/custom-tools.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
isReviewRequired,
|
|
25
25
|
recordMutation,
|
|
26
26
|
recordReviewResult,
|
|
27
|
+
runWithRuntimeState,
|
|
27
28
|
} from "./state.ts";
|
|
28
29
|
import { loadProjectConfig } from "./project-config.ts";
|
|
29
30
|
import { detectVerifyCommand, getCurrentGitCommitHash } from "./verify.ts";
|
|
@@ -53,8 +54,9 @@ export function createCustomTools(options: {
|
|
|
53
54
|
followupStore: ProjectMemoryStore | undefined;
|
|
54
55
|
portableMemoryPath: string;
|
|
55
56
|
learningInterventions: Map<string, number>;
|
|
57
|
+
client: unknown;
|
|
56
58
|
}) {
|
|
57
|
-
const { effectiveRoot, projectMemoryEnabled, learningEnabled, projectMemory, followupStore, portableMemoryPath, learningInterventions } = options;
|
|
59
|
+
const { effectiveRoot, projectMemoryEnabled, learningEnabled, projectMemory, followupStore, portableMemoryPath, learningInterventions, client } = options;
|
|
58
60
|
return {
|
|
59
61
|
...(getProjectConfig(effectiveRoot).recoveryCheckpoints === true ? {
|
|
60
62
|
guard_recovery_restore: tool({
|
|
@@ -155,7 +157,7 @@ export function createCustomTools(options: {
|
|
|
155
157
|
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") },
|
|
156
158
|
execute: async (args, toolContext) => {
|
|
157
159
|
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 } });
|
|
158
|
-
const parentSessionID = await fetchParentSessionID(toolContext.sessionID);
|
|
160
|
+
const parentSessionID = await runWithRuntimeState(effectiveRoot, client, () => fetchParentSessionID(toolContext.sessionID));
|
|
159
161
|
if (!parentSessionID) { auditVerdict("rejected", "missing_parent_session"); return "[workflow-guard] Review rejected: record_review must be called from a secondary/subagent session."; }
|
|
160
162
|
const axesRefs = ["test integrity", "task completeness", "cleanliness", "security", "platform"];
|
|
161
163
|
const referenced = axesRefs.filter((axis) => args.summary.toLowerCase().includes(axis));
|
package/src/workflow-guard-ui.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui";
|
|
2
|
-
import type { BaseRenderable } from "@opentui/core";
|
|
3
2
|
import type { JSX } from "@opentui/solid";
|
|
4
3
|
import { createElement, insert, setProp } from "@opentui/solid";
|
|
5
4
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
@@ -31,9 +30,6 @@ function text(props: Record<string, unknown>, children: Child[]) {
|
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
const BADGE_ACTIVE = "Workflow Guard 🛡️";
|
|
34
|
-
const BLOCKED_BADGE_DURATION_MS = 5_000;
|
|
35
|
-
let lastBlockedReason: string | undefined;
|
|
36
|
-
const sessionBlockedReasons = new Map<string, string>();
|
|
37
33
|
|
|
38
34
|
type ProjectToggle = "recoveryCheckpoints" | "projectMemory" | "learning" | "titleSettleWorkaround";
|
|
39
35
|
|
|
@@ -74,29 +70,11 @@ export function writeRecoveryCheckpointsOption(root: string, enabled: boolean):
|
|
|
74
70
|
return writeProjectOption(root, "recoveryCheckpoints", enabled);
|
|
75
71
|
}
|
|
76
72
|
|
|
77
|
-
export function
|
|
78
|
-
|
|
79
|
-
if (reason === undefined) sessionBlockedReasons.clear();
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export function getLastBlockedReason(): string | undefined {
|
|
83
|
-
return lastBlockedReason;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function formatBadge(sessionID?: string): { text: string; isBlocked: boolean } {
|
|
87
|
-
const reason = sessionID ? sessionBlockedReasons.get(sessionID) : lastBlockedReason;
|
|
88
|
-
if (!reason) {
|
|
89
|
-
return { text: BADGE_ACTIVE, isBlocked: false };
|
|
90
|
-
}
|
|
91
|
-
const cleanReason = reason.replace(/^\[workflow-guard\]\s*/i, "").replace(/^Blocked(?::|\s+)\s*/i, "");
|
|
92
|
-
const shortReason = cleanReason.length > 30 ? cleanReason.slice(0, 27) + "..." : cleanReason;
|
|
93
|
-
return { text: `[Workflow Guard: Blocked: ${shortReason}]`, isBlocked: true };
|
|
73
|
+
export function formatBadge(): { text: string; isBlocked: boolean } {
|
|
74
|
+
return { text: BADGE_ACTIVE, isBlocked: false };
|
|
94
75
|
}
|
|
95
76
|
|
|
96
77
|
export const WorkflowGuardTui: TuiPlugin = async (api) => {
|
|
97
|
-
const badgeNodes = new Map<string, BaseRenderable>();
|
|
98
|
-
const blockedBadgeTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
99
|
-
|
|
100
78
|
api.keymap.registerLayer({
|
|
101
79
|
commands: [{
|
|
102
80
|
name: "workflow-guard.project-options",
|
|
@@ -132,67 +110,18 @@ export const WorkflowGuardTui: TuiPlugin = async (api) => {
|
|
|
132
110
|
bindings: [],
|
|
133
111
|
});
|
|
134
112
|
|
|
135
|
-
function scheduleBadgeReset(sessionID?: string): void {
|
|
136
|
-
const key = sessionID ?? "home";
|
|
137
|
-
const existing = blockedBadgeTimers.get(key);
|
|
138
|
-
if (existing) clearTimeout(existing);
|
|
139
|
-
blockedBadgeTimers.set(key, setTimeout(() => {
|
|
140
|
-
blockedBadgeTimers.delete(key);
|
|
141
|
-
if (sessionID) sessionBlockedReasons.delete(sessionID);
|
|
142
|
-
else lastBlockedReason = undefined;
|
|
143
|
-
const node = badgeNodes.get(key);
|
|
144
|
-
if (node) {
|
|
145
|
-
setProp(node, "content", BADGE_ACTIVE);
|
|
146
|
-
setProp(node, "fg", api.theme.current.success);
|
|
147
|
-
}
|
|
148
|
-
}, BLOCKED_BADGE_DURATION_MS));
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
try {
|
|
152
|
-
api.event.on("tui.toast.show", (e) => {
|
|
153
|
-
const title = e.properties.title;
|
|
154
|
-
const msg = e.properties.message;
|
|
155
|
-
if (title === "Workflow Guard Blocked" && typeof msg === "string") {
|
|
156
|
-
const route = api.route.current;
|
|
157
|
-
const sessionID =
|
|
158
|
-
route.name === "session" && typeof route.params?.sessionID === "string"
|
|
159
|
-
? route.params.sessionID
|
|
160
|
-
: undefined;
|
|
161
|
-
if (sessionID) sessionBlockedReasons.set(sessionID, msg);
|
|
162
|
-
else lastBlockedReason = msg;
|
|
163
|
-
const key = sessionID ?? "home";
|
|
164
|
-
const node = badgeNodes.get(key);
|
|
165
|
-
if (node) {
|
|
166
|
-
const badge = formatBadge(sessionID);
|
|
167
|
-
setProp(node, "content", badge.text);
|
|
168
|
-
setProp(node, "fg", badge.isBlocked ? api.theme.current.warning : api.theme.current.success);
|
|
169
|
-
}
|
|
170
|
-
scheduleBadgeReset(sessionID);
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
} catch {}
|
|
174
|
-
|
|
175
113
|
api.slots.register({
|
|
176
114
|
order: 1,
|
|
177
115
|
slots: {
|
|
178
116
|
home_prompt_right() {
|
|
179
117
|
const theme = api.theme.current;
|
|
180
118
|
const badge = formatBadge();
|
|
181
|
-
|
|
182
|
-
badgeNodes.set("home", node as unknown as BaseRenderable);
|
|
183
|
-
return node;
|
|
119
|
+
return text({ fg: theme.success }, [badge.text]);
|
|
184
120
|
},
|
|
185
121
|
session_prompt_right() {
|
|
186
122
|
const theme = api.theme.current;
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
route.name === "session" && typeof route.params?.sessionID === "string"
|
|
190
|
-
? route.params.sessionID
|
|
191
|
-
: undefined;
|
|
192
|
-
const badge = formatBadge(sessionID);
|
|
193
|
-
const node = text({ fg: badge.isBlocked ? theme.warning : theme.success }, [badge.text]);
|
|
194
|
-
if (sessionID) badgeNodes.set(sessionID, node as unknown as BaseRenderable);
|
|
195
|
-
return node;
|
|
123
|
+
const badge = formatBadge();
|
|
124
|
+
return text({ fg: theme.success }, [badge.text]);
|
|
196
125
|
},
|
|
197
126
|
},
|
|
198
127
|
});
|
package/src/workflow-guard.ts
CHANGED
|
@@ -371,7 +371,7 @@ export const WorkflowGuard: Plugin = async (ctx) => {
|
|
|
371
371
|
const toolOutcomes = new ToolOutcomeTracker();
|
|
372
372
|
|
|
373
373
|
return {
|
|
374
|
-
tool: createCustomTools({ effectiveRoot, projectMemoryEnabled, learningEnabled, projectMemory, followupStore, portableMemoryPath, learningInterventions }),
|
|
374
|
+
tool: createCustomTools({ effectiveRoot, projectMemoryEnabled, learningEnabled, projectMemory, followupStore, portableMemoryPath, learningInterventions, client: ctx.client }),
|
|
375
375
|
|
|
376
376
|
"tool.execute.before": async (input, output) => {
|
|
377
377
|
const toolWorktree =
|