opencode-workflow-guard 1.8.5 → 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 +10 -6
- package/src/lib/types.ts +3 -1
- package/src/policies/continuation.ts +20 -2
- package/src/policies/todo.ts +25 -0
- package/src/workflow-guard-ui.ts +12 -79
- package/src/workflow-guard.ts +3 -2
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,13 +24,14 @@ 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";
|
|
30
31
|
import { buildReviewRubric } from "./review.ts";
|
|
31
32
|
import { createGitWorktree, cleanupGitWorktree } from "./worktree.ts";
|
|
32
33
|
import { restoreRecoveryCheckpoint } from "./checkpoint.ts";
|
|
33
|
-
import { getRecentAuditEntries } from "./audit.ts";
|
|
34
|
+
import { audit, getRecentAuditEntries } from "./audit.ts";
|
|
34
35
|
import { guardToolCallImpl } from "./guard-dispatcher.ts";
|
|
35
36
|
import { discoverPlanningSources } from "../policies/planning.ts";
|
|
36
37
|
import { secretIn } from "../policies/secrets.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({
|
|
@@ -154,14 +156,16 @@ export function createCustomTools(options: {
|
|
|
154
156
|
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.",
|
|
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
|
-
const
|
|
158
|
-
|
|
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 } });
|
|
160
|
+
const parentSessionID = await runWithRuntimeState(effectiveRoot, client, () => fetchParentSessionID(toolContext.sessionID));
|
|
161
|
+
if (!parentSessionID) { auditVerdict("rejected", "missing_parent_session"); return "[workflow-guard] Review rejected: record_review must be called from a secondary/subagent session."; }
|
|
159
162
|
const axesRefs = ["test integrity", "task completeness", "cleanliness", "security", "platform"];
|
|
160
163
|
const referenced = axesRefs.filter((axis) => args.summary.toLowerCase().includes(axis));
|
|
161
|
-
if (referenced.length < 3) return `[workflow-guard] Review rejected: summary must reference the review axes (found ${referenced.length}/5). Call guard_review_rubric to get the rubric, evaluate each axis, and include findings per axis in the summary.`;
|
|
162
|
-
if (args.passed && /(?:^|\s)(?:\[p[01]\]|p[01]\s*:\s*(?:blocker|defect|vulnerability|error|bug|issue)|p[01]\s+blocker)/i.test(args.summary)) return "[workflow-guard] Review rejected: cannot record approval when P0 or P1 blockers are flagged in findings. Resolve all P0/P1 issues before approving or record review with passed=false.";
|
|
164
|
+
if (referenced.length < 3) { auditVerdict("rejected", "insufficient_rubric_axes"); return `[workflow-guard] Review rejected: summary must reference the review axes (found ${referenced.length}/5). Call guard_review_rubric to get the rubric, evaluate each axis, and include findings per axis in the summary.`; }
|
|
165
|
+
if (args.passed && /(?:^|\s)(?:\[p[01]\]|p[01]\s*:\s*(?:blocker|defect|vulnerability|error|bug|issue)|p[01]\s+blocker)/i.test(args.summary)) { auditVerdict("rejected", "approval_contains_blocker"); return "[workflow-guard] Review rejected: cannot record approval when P0 or P1 blockers are flagged in findings. Resolve all P0/P1 issues before approving or record review with passed=false."; }
|
|
163
166
|
recordReviewResult(args.reviewer, args.summary, args.passed, parentSessionID, toolContext.worktree || toolContext.directory);
|
|
164
167
|
if (followupStore && !secretIn(args.summary)) for (const finding of extractReviewFollowups(args.summary)) recordReviewFollowup(followupStore, { severity: finding.severity, summary: finding.summary, reviewer: args.reviewer, sessionID: toolContext.sessionID, commit: getCurrentGitCommitHash(effectiveRoot) });
|
|
168
|
+
auditVerdict(args.passed ? "approved" : "changes_requested", args.passed ? "approved" : "changes_requested");
|
|
165
169
|
return args.passed ? `[workflow-guard] Review recorded as APPROVED by ${args.reviewer}.` : `[workflow-guard] Review recorded as CHANGES REQUESTED by ${args.reviewer}.`;
|
|
166
170
|
},
|
|
167
171
|
}),
|
package/src/lib/types.ts
CHANGED
|
@@ -9,7 +9,7 @@ export interface TodoSdkClient {
|
|
|
9
9
|
};
|
|
10
10
|
session?: {
|
|
11
11
|
todo?: (opts: { path: { id: string } }) => Promise<{ data?: unknown }>;
|
|
12
|
-
get?: (opts: { path: { id: string } }) => Promise<{ data?: { parentID?: unknown } }>;
|
|
12
|
+
get?: (opts: { path: { id: string } }) => Promise<{ data?: { parentID?: unknown; title?: unknown } }>;
|
|
13
13
|
promptAsync?: (opts: {
|
|
14
14
|
path: { id: string };
|
|
15
15
|
body: { messageID?: string; parts: Array<{ type: "text"; text: string; synthetic?: boolean }> };
|
|
@@ -36,6 +36,7 @@ export interface ProjectConfig {
|
|
|
36
36
|
recoveryCheckpoints?: boolean;
|
|
37
37
|
projectMemory?: boolean;
|
|
38
38
|
learning?: boolean;
|
|
39
|
+
titleSettleWorkaround?: boolean;
|
|
39
40
|
maxSubagentMutations?: number;
|
|
40
41
|
maxLearningInterventions?: number;
|
|
41
42
|
}
|
|
@@ -83,6 +84,7 @@ export interface AuditEntry {
|
|
|
83
84
|
durationMs?: number;
|
|
84
85
|
};
|
|
85
86
|
allowLive?: boolean;
|
|
87
|
+
reviewVerdict?: "approved" | "changes_requested" | "rejected";
|
|
86
88
|
};
|
|
87
89
|
}
|
|
88
90
|
|
|
@@ -3,6 +3,9 @@ import { getSdkClient } from "../lib/state.ts";
|
|
|
3
3
|
import { effectiveTodosWithOwner, hasActiveTodo } from "./todo.ts";
|
|
4
4
|
|
|
5
5
|
const MAX_CONSECUTIVE_CONTINUATIONS = 3;
|
|
6
|
+
const TITLE_SETTLE_MS = 250;
|
|
7
|
+
const TITLE_SETTLE_ATTEMPTS = 8;
|
|
8
|
+
const DEFAULT_SESSION_TITLE = /^(?:New session - |Child session - )\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
6
9
|
type ContinuationState = {
|
|
7
10
|
counts: Map<string, number>;
|
|
8
11
|
generatedMessageIDs: Map<string, Set<string>>;
|
|
@@ -10,6 +13,20 @@ type ContinuationState = {
|
|
|
10
13
|
};
|
|
11
14
|
const states = new WeakMap<object, ContinuationState>();
|
|
12
15
|
|
|
16
|
+
export async function waitForSessionTitle(sessionID: string): Promise<void> {
|
|
17
|
+
const session = getSdkClient()?.session;
|
|
18
|
+
if (typeof session?.get !== "function") return;
|
|
19
|
+
for (let attempt = 0; attempt < TITLE_SETTLE_ATTEMPTS; attempt++) {
|
|
20
|
+
try {
|
|
21
|
+
const current = (await session.get({ path: { id: sessionID } }))?.data;
|
|
22
|
+
if (current?.parentID || typeof current?.title !== "string" || !DEFAULT_SESSION_TITLE.test(current.title)) return;
|
|
23
|
+
} catch {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (attempt + 1 < TITLE_SETTLE_ATTEMPTS) await new Promise((resolve) => setTimeout(resolve, TITLE_SETTLE_MS));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
13
30
|
function getState(): ContinuationState | undefined {
|
|
14
31
|
const client = getSdkClient();
|
|
15
32
|
if (!client || (typeof client !== "object" && typeof client !== "function")) return undefined;
|
|
@@ -21,13 +38,14 @@ function getState(): ContinuationState | undefined {
|
|
|
21
38
|
return state;
|
|
22
39
|
}
|
|
23
40
|
|
|
24
|
-
export async function continueUnfinishedSession(sessionID: string): Promise<boolean> {
|
|
41
|
+
export async function continueUnfinishedSession(sessionID: string, settleTitle = false): Promise<boolean> {
|
|
25
42
|
const state = getState();
|
|
26
43
|
if (!state || state.inFlight.has(sessionID)) return false;
|
|
27
44
|
state.inFlight.add(sessionID);
|
|
28
45
|
try {
|
|
29
46
|
const effective = await effectiveTodosWithOwner(sessionID);
|
|
30
|
-
if (!effective ||
|
|
47
|
+
if (!effective || effective.ownerSessionID !== sessionID || !hasActiveTodo(effective.todos)) return false;
|
|
48
|
+
if (settleTitle) await waitForSessionTitle(sessionID);
|
|
31
49
|
|
|
32
50
|
const count = state.counts.get(sessionID) ?? 0;
|
|
33
51
|
if (count >= MAX_CONSECUTIVE_CONTINUATIONS) return false;
|
package/src/policies/todo.ts
CHANGED
|
@@ -97,14 +97,39 @@ export function validateTodoLifecycle(
|
|
|
97
97
|
return s === "pending" || s === "in_progress";
|
|
98
98
|
});
|
|
99
99
|
if (activeExisting.length > 0) {
|
|
100
|
+
const activeNew = newTodos.filter((t) => ACTIVE_TODO_STATUSES.has(String(t.status ?? "")));
|
|
101
|
+
const reviewObligation = /\b(?:secondary\s+review|re-?review|reviewer|(?:run|perform|request|obtain|complete)\s+(?:an?\s+)?review)\b/i;
|
|
102
|
+
const remediationAction = /\b(?:address|fix|resolve|remediate)\b/i;
|
|
103
|
+
const reviewReference = /\b(?:secondary\s+review|re-?review|reviewer|review)\b/i;
|
|
104
|
+
const remediationSubject = /\b(?:findings?|feedback|comments?|issues?)\b/i;
|
|
105
|
+
const isReviewObligation = (content: string) => reviewObligation.test(content) && !(remediationAction.test(content) && reviewReference.test(content) && remediationSubject.test(content));
|
|
100
106
|
const newContentCounts = new Map<string, number>();
|
|
101
107
|
for (const todo of newTodos) {
|
|
102
108
|
const content = String(todo.content ?? "").trim();
|
|
103
109
|
newContentCounts.set(content, (newContentCounts.get(content) ?? 0) + 1);
|
|
104
110
|
}
|
|
111
|
+
const retainedReviewCounts = new Map<string, number>();
|
|
112
|
+
for (const todo of activeNew) {
|
|
113
|
+
const content = String(todo.content ?? "").trim();
|
|
114
|
+
retainedReviewCounts.set(content, (retainedReviewCounts.get(content) ?? 0) + 1);
|
|
115
|
+
}
|
|
116
|
+
let retainedReviewObligations = 0;
|
|
117
|
+
for (const todo of activeExisting) {
|
|
118
|
+
const content = String(todo.content ?? "").trim();
|
|
119
|
+
const remaining = retainedReviewCounts.get(content) ?? 0;
|
|
120
|
+
if (remaining > 0 && isReviewObligation(content)) {
|
|
121
|
+
retainedReviewCounts.set(content, remaining - 1);
|
|
122
|
+
retainedReviewObligations++;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
let replacementReviewObligations = activeNew.filter((t) => isReviewObligation(String(t.content ?? ""))).length - retainedReviewObligations;
|
|
105
126
|
const missing = activeExisting.find((todo) => {
|
|
106
127
|
const content = String(todo.content ?? "").trim();
|
|
107
128
|
const remaining = newContentCounts.get(content) ?? 0;
|
|
129
|
+
if (remaining === 0 && isReviewObligation(content) && replacementReviewObligations > 0) {
|
|
130
|
+
replacementReviewObligations--;
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
108
133
|
if (remaining === 0) return true;
|
|
109
134
|
newContentCounts.set(content, remaining - 1);
|
|
110
135
|
return false;
|
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,11 +30,10 @@ 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";
|
|
35
|
+
|
|
36
|
+
export function readProjectOption(root: string, option: ProjectToggle): boolean {
|
|
39
37
|
const path = projectConfigPath(root);
|
|
40
38
|
if (!existsSync(path)) return option === "projectMemory";
|
|
41
39
|
const errors: ParseError[] = [];
|
|
@@ -44,7 +42,7 @@ export function readProjectOption(root: string, option: "recoveryCheckpoints" |
|
|
|
44
42
|
return option === "projectMemory" ? config?.projectMemory !== false : config?.[option] === true;
|
|
45
43
|
}
|
|
46
44
|
|
|
47
|
-
function writeProjectOption(root: string, option:
|
|
45
|
+
function writeProjectOption(root: string, option: ProjectToggle, enabled: boolean): string {
|
|
48
46
|
const path = projectConfigPath(root);
|
|
49
47
|
const raw = existsSync(path) ? readFileSync(path, "utf8") : "{}\n";
|
|
50
48
|
const errors: ParseError[] = [];
|
|
@@ -72,29 +70,11 @@ export function writeRecoveryCheckpointsOption(root: string, enabled: boolean):
|
|
|
72
70
|
return writeProjectOption(root, "recoveryCheckpoints", enabled);
|
|
73
71
|
}
|
|
74
72
|
|
|
75
|
-
export function
|
|
76
|
-
|
|
77
|
-
if (reason === undefined) sessionBlockedReasons.clear();
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function getLastBlockedReason(): string | undefined {
|
|
81
|
-
return lastBlockedReason;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function formatBadge(sessionID?: string): { text: string; isBlocked: boolean } {
|
|
85
|
-
const reason = sessionID ? sessionBlockedReasons.get(sessionID) : lastBlockedReason;
|
|
86
|
-
if (!reason) {
|
|
87
|
-
return { text: BADGE_ACTIVE, isBlocked: false };
|
|
88
|
-
}
|
|
89
|
-
const cleanReason = reason.replace(/^\[workflow-guard\]\s*/i, "").replace(/^Blocked(?::|\s+)\s*/i, "");
|
|
90
|
-
const shortReason = cleanReason.length > 30 ? cleanReason.slice(0, 27) + "..." : cleanReason;
|
|
91
|
-
return { text: `[Workflow Guard: Blocked: ${shortReason}]`, isBlocked: true };
|
|
73
|
+
export function formatBadge(): { text: string; isBlocked: boolean } {
|
|
74
|
+
return { text: BADGE_ACTIVE, isBlocked: false };
|
|
92
75
|
}
|
|
93
76
|
|
|
94
77
|
export const WorkflowGuardTui: TuiPlugin = async (api) => {
|
|
95
|
-
const badgeNodes = new Map<string, BaseRenderable>();
|
|
96
|
-
const blockedBadgeTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
97
|
-
|
|
98
78
|
api.keymap.registerLayer({
|
|
99
79
|
commands: [{
|
|
100
80
|
name: "workflow-guard.project-options",
|
|
@@ -107,6 +87,7 @@ export const WorkflowGuardTui: TuiPlugin = async (api) => {
|
|
|
107
87
|
const recovery = readProjectOption(root, "recoveryCheckpoints");
|
|
108
88
|
const memory = readProjectOption(root, "projectMemory");
|
|
109
89
|
const learning = readProjectOption(root, "learning");
|
|
90
|
+
const titleSettle = readProjectOption(root, "titleSettleWorkaround");
|
|
110
91
|
api.ui.dialog.replace(() => api.ui.DialogSelect({
|
|
111
92
|
title: "Workflow Guard Project Options",
|
|
112
93
|
current: undefined,
|
|
@@ -114,9 +95,10 @@ export const WorkflowGuardTui: TuiPlugin = async (api) => {
|
|
|
114
95
|
{ title: `Recovery checkpoints: ${recovery ? "On" : "Off"}`, value: "recoveryCheckpoints", description: "Toggle durable pre-run Git checkpoints" },
|
|
115
96
|
{ title: `Project memory: ${memory ? "On" : "Off"}`, value: "projectMemory", description: "Toggle durable local project memory" },
|
|
116
97
|
{ title: `Learner mode: ${learning ? "On" : "Off"}`, value: "learning", description: "Toggle evidence-based learning tools" },
|
|
98
|
+
{ title: `Title settle workaround: ${titleSettle ? "On" : "Off"}`, value: "titleSettleWorkaround", description: "Delay automatic continuation while OpenCode generates a session title" },
|
|
117
99
|
],
|
|
118
100
|
onSelect: (option) => {
|
|
119
|
-
const key = option.value as
|
|
101
|
+
const key = option.value as ProjectToggle;
|
|
120
102
|
const enabled = !readProjectOption(root, key);
|
|
121
103
|
const path = writeProjectOption(root, key, enabled);
|
|
122
104
|
api.ui.dialog.clear();
|
|
@@ -128,67 +110,18 @@ export const WorkflowGuardTui: TuiPlugin = async (api) => {
|
|
|
128
110
|
bindings: [],
|
|
129
111
|
});
|
|
130
112
|
|
|
131
|
-
function scheduleBadgeReset(sessionID?: string): void {
|
|
132
|
-
const key = sessionID ?? "home";
|
|
133
|
-
const existing = blockedBadgeTimers.get(key);
|
|
134
|
-
if (existing) clearTimeout(existing);
|
|
135
|
-
blockedBadgeTimers.set(key, setTimeout(() => {
|
|
136
|
-
blockedBadgeTimers.delete(key);
|
|
137
|
-
if (sessionID) sessionBlockedReasons.delete(sessionID);
|
|
138
|
-
else lastBlockedReason = undefined;
|
|
139
|
-
const node = badgeNodes.get(key);
|
|
140
|
-
if (node) {
|
|
141
|
-
setProp(node, "content", BADGE_ACTIVE);
|
|
142
|
-
setProp(node, "fg", api.theme.current.success);
|
|
143
|
-
}
|
|
144
|
-
}, BLOCKED_BADGE_DURATION_MS));
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
try {
|
|
148
|
-
api.event.on("tui.toast.show", (e) => {
|
|
149
|
-
const title = e.properties.title;
|
|
150
|
-
const msg = e.properties.message;
|
|
151
|
-
if (title === "Workflow Guard Blocked" && typeof msg === "string") {
|
|
152
|
-
const route = api.route.current;
|
|
153
|
-
const sessionID =
|
|
154
|
-
route.name === "session" && typeof route.params?.sessionID === "string"
|
|
155
|
-
? route.params.sessionID
|
|
156
|
-
: undefined;
|
|
157
|
-
if (sessionID) sessionBlockedReasons.set(sessionID, msg);
|
|
158
|
-
else lastBlockedReason = msg;
|
|
159
|
-
const key = sessionID ?? "home";
|
|
160
|
-
const node = badgeNodes.get(key);
|
|
161
|
-
if (node) {
|
|
162
|
-
const badge = formatBadge(sessionID);
|
|
163
|
-
setProp(node, "content", badge.text);
|
|
164
|
-
setProp(node, "fg", badge.isBlocked ? api.theme.current.warning : api.theme.current.success);
|
|
165
|
-
}
|
|
166
|
-
scheduleBadgeReset(sessionID);
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
} catch {}
|
|
170
|
-
|
|
171
113
|
api.slots.register({
|
|
172
114
|
order: 1,
|
|
173
115
|
slots: {
|
|
174
116
|
home_prompt_right() {
|
|
175
117
|
const theme = api.theme.current;
|
|
176
118
|
const badge = formatBadge();
|
|
177
|
-
|
|
178
|
-
badgeNodes.set("home", node as unknown as BaseRenderable);
|
|
179
|
-
return node;
|
|
119
|
+
return text({ fg: theme.success }, [badge.text]);
|
|
180
120
|
},
|
|
181
121
|
session_prompt_right() {
|
|
182
122
|
const theme = api.theme.current;
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
route.name === "session" && typeof route.params?.sessionID === "string"
|
|
186
|
-
? route.params.sessionID
|
|
187
|
-
: undefined;
|
|
188
|
-
const badge = formatBadge(sessionID);
|
|
189
|
-
const node = text({ fg: badge.isBlocked ? theme.warning : theme.success }, [badge.text]);
|
|
190
|
-
if (sessionID) badgeNodes.set(sessionID, node as unknown as BaseRenderable);
|
|
191
|
-
return node;
|
|
123
|
+
const badge = formatBadge();
|
|
124
|
+
return text({ fg: theme.success }, [badge.text]);
|
|
192
125
|
},
|
|
193
126
|
},
|
|
194
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 =
|
|
@@ -650,7 +650,8 @@ export const WorkflowGuard: Plugin = async (ctx) => {
|
|
|
650
650
|
if (recoveryRun !== undefined) {
|
|
651
651
|
finalizeRecoveryCheckpoint(effectiveRoot, sessionID, recoveryRun);
|
|
652
652
|
}
|
|
653
|
-
|
|
653
|
+
const settleTitle = getProjectConfig(effectiveRoot).titleSettleWorkaround === true;
|
|
654
|
+
await runWithRuntimeState(effectiveRoot, ctx.client, () => continueUnfinishedSession(sessionID, settleTitle));
|
|
654
655
|
}
|
|
655
656
|
}
|
|
656
657
|
if (event?.type === "message.updated") {
|