kibi-opencode 0.8.0 → 0.10.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 +37 -12
- package/dist/brief-intent.d.ts +41 -0
- package/dist/brief-intent.js +127 -0
- package/dist/briefing-runtime.d.ts +24 -0
- package/dist/briefing-runtime.js +277 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +9 -0
- package/dist/e2e-coverage-signals.d.ts +6 -0
- package/dist/e2e-coverage-signals.js +186 -0
- package/dist/file-entity-links.d.ts +15 -0
- package/dist/file-entity-links.js +254 -0
- package/dist/file-operation-reminders.d.ts +24 -0
- package/dist/file-operation-reminders.js +55 -0
- package/dist/file-operation-state.d.ts +29 -0
- package/dist/file-operation-state.js +113 -0
- package/dist/idle-brief-audit.d.ts +36 -0
- package/dist/idle-brief-audit.js +186 -0
- package/dist/idle-brief-paths.d.ts +6 -0
- package/dist/idle-brief-paths.js +120 -0
- package/dist/idle-brief-reader.d.ts +25 -0
- package/dist/idle-brief-reader.js +142 -0
- package/dist/idle-brief-runtime.d.ts +48 -0
- package/dist/idle-brief-runtime.js +443 -0
- package/dist/idle-brief-store.d.ts +96 -0
- package/dist/idle-brief-store.js +209 -0
- package/dist/index.d.ts +15 -1
- package/dist/index.js +645 -22
- package/dist/init-kibi-alias.d.ts +14 -0
- package/dist/init-kibi-alias.js +38 -0
- package/dist/init-kibi-capability.d.ts +32 -0
- package/dist/init-kibi-capability.js +202 -0
- package/dist/logger.js +9 -3
- package/dist/plugin-startup.d.ts +1 -0
- package/dist/plugin-startup.js +11 -2
- package/dist/prompt.d.ts +18 -3
- package/dist/prompt.js +176 -50
- package/dist/reconcile-engine.d.ts +15 -0
- package/dist/reconcile-engine.js +112 -0
- package/dist/scheduler.d.ts +1 -0
- package/dist/scheduler.js +37 -1
- package/dist/session-edit-state.d.ts +25 -0
- package/dist/session-edit-state.js +177 -0
- package/dist/session-fingerprint.d.ts +11 -0
- package/dist/session-fingerprint.js +21 -0
- package/dist/source-linked-guidance.d.ts +1 -2
- package/dist/source-linked-guidance.js +5 -168
- package/dist/startup-notifier.d.ts +3 -18
- package/dist/startup-notifier.js +42 -36
- package/dist/toast.d.ts +31 -0
- package/dist/toast.js +40 -0
- package/dist/tui-brief-delivery.d.ts +47 -0
- package/dist/tui-brief-delivery.js +138 -0
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -1,18 +1,48 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
+
import { loadBriefConfig } from "kibi-cli/brief-config";
|
|
3
|
+
import { computeBriefIntent } from "./brief-intent.js";
|
|
4
|
+
import { fetchBriefingResult, } from "./briefing-runtime.js";
|
|
2
5
|
import { analyzeCodeFile, } from "./comment-analysis.js";
|
|
6
|
+
import { getE2eCoverageSignal } from "./e2e-coverage-signals.js"; // implements REQ-opencode-file-context-guidance-v1
|
|
7
|
+
import { getFileLinkedEntityIds } from "./file-entity-links.js"; // implements REQ-opencode-file-context-guidance-v1
|
|
3
8
|
import * as fileFilter from "./file-filter.js";
|
|
9
|
+
import { deriveFileOperationReminder } from "./file-operation-reminders.js"; // implements REQ-opencode-file-context-guidance-v1
|
|
10
|
+
import { createFileOperationState, } from "./file-operation-state.js"; // implements REQ-opencode-file-context-guidance-v1
|
|
11
|
+
import { getInitKibiCommandCapability, registerInitKibiCommand, } from "./init-kibi-capability.js";
|
|
12
|
+
import { computeAuditDelta, getAuditTailCursor, guardBranchChanged, } from "./idle-brief-audit.js";
|
|
13
|
+
import { hasTuiSeenBrief, markBriefRead, markBriefTuiSeen, selectLatestUnreadBrief, } from "./idle-brief-reader.js";
|
|
14
|
+
import { generateIdleBrief } from "./idle-brief-runtime.js";
|
|
4
15
|
import * as logger from "./logger.js";
|
|
5
16
|
import { analyzePath } from "./path-kind.js";
|
|
17
|
+
import { runPluginStartup } from "./plugin-startup.js";
|
|
18
|
+
import { resolveCurrentBranch } from "./plugin-startup.js";
|
|
6
19
|
import { SENTINEL, buildPrompt } from "./prompt.js";
|
|
20
|
+
import { reconcileAuditEntries } from "./reconcile-engine.js";
|
|
7
21
|
import { isMustPriorityRequirement } from "./requirement-doc.js";
|
|
8
22
|
import { classifyRisk } from "./risk-classifier.js";
|
|
23
|
+
import { createSessionEditState, } from "./session-edit-state.js";
|
|
24
|
+
import { syncSessionBaselineState, } from "./session-fingerprint.js";
|
|
9
25
|
import { getSessionTracker } from "./session-tracker.js";
|
|
10
|
-
import { notifyStartup } from "./startup-notifier.js";
|
|
11
|
-
import {
|
|
26
|
+
import { notifyStartup, } from "./startup-notifier.js";
|
|
27
|
+
import { sendToast, } from "./toast.js";
|
|
28
|
+
import { deliverBriefTui, } from "./tui-brief-delivery.js";
|
|
12
29
|
import * as fs from "node:fs";
|
|
13
30
|
function deriveFileBucket(kind) {
|
|
14
31
|
return kind;
|
|
15
32
|
}
|
|
33
|
+
function resolveIdleBriefDeliveryDelayMs(worktree) {
|
|
34
|
+
const envValue = Number(process.env.KIBI_OPENCODE_IDLE_BRIEF_DELAY_MS);
|
|
35
|
+
if (Number.isFinite(envValue) && envValue >= 0) {
|
|
36
|
+
return Math.min(60_000, Math.trunc(envValue));
|
|
37
|
+
}
|
|
38
|
+
const sharedPolicy = loadBriefConfig(worktree);
|
|
39
|
+
const configValue = Number(sharedPolicy.tui?.idleDelayMs ?? 1500);
|
|
40
|
+
if (!Number.isFinite(configValue))
|
|
41
|
+
return 1500;
|
|
42
|
+
if (configValue < 0)
|
|
43
|
+
return 0;
|
|
44
|
+
return Math.min(60_000, Math.trunc(configValue));
|
|
45
|
+
}
|
|
16
46
|
const startupNotifyGlobals = globalThis;
|
|
17
47
|
/**
|
|
18
48
|
* Lint requirement documents for embedded scenarios/tests and oversized content.
|
|
@@ -53,28 +83,423 @@ function lintRequirementDoc(filePath, worktree) {
|
|
|
53
83
|
}
|
|
54
84
|
// implements REQ-opencode-kibi-plugin-v1
|
|
55
85
|
const kibiOpencodePlugin = async (input) => {
|
|
86
|
+
const makeToastClient = (client) => {
|
|
87
|
+
const tui = client.tui;
|
|
88
|
+
if (!tui)
|
|
89
|
+
return {};
|
|
90
|
+
const mappedTui = {};
|
|
91
|
+
if (typeof tui.toast === "function") {
|
|
92
|
+
mappedTui.toast = tui.toast.bind(tui);
|
|
93
|
+
}
|
|
94
|
+
if (typeof tui.showToast === "function") {
|
|
95
|
+
mappedTui.showToast = tui.showToast.bind(tui);
|
|
96
|
+
}
|
|
97
|
+
if (typeof tui.clearPrompt === "function") {
|
|
98
|
+
mappedTui.clearPrompt = tui.clearPrompt.bind(tui);
|
|
99
|
+
}
|
|
100
|
+
if (typeof tui.submitPrompt === "function") {
|
|
101
|
+
mappedTui.submitPrompt = tui.submitPrompt.bind(tui);
|
|
102
|
+
}
|
|
103
|
+
return { tui: mappedTui };
|
|
104
|
+
};
|
|
105
|
+
const makeStartupClient = (client) => ({
|
|
106
|
+
...makeToastClient(client),
|
|
107
|
+
app: client.app,
|
|
108
|
+
});
|
|
56
109
|
const startup = await runPluginStartup(input);
|
|
57
110
|
if (!startup) {
|
|
58
111
|
return {};
|
|
59
112
|
}
|
|
60
113
|
const { cfg, workspaceHealth, posture, currentBranch, cache, runtimeOverlay, scheduler, maintenanceDegraded, getMaintenanceDegraded, getEffectiveMode, latchRuntimeDegraded, } = startup;
|
|
61
114
|
const hooks = {};
|
|
115
|
+
const initKibiCommandCapability = getInitKibiCommandCapability();
|
|
116
|
+
if (initKibiCommandCapability.supported) {
|
|
117
|
+
hooks.config = async (configInput) => {
|
|
118
|
+
registerInitKibiCommand(configInput, initKibiCommandCapability);
|
|
119
|
+
};
|
|
120
|
+
}
|
|
62
121
|
// Plugin instance state (not module globals)
|
|
63
122
|
const MAX_RECENT_EDITS = 5;
|
|
64
123
|
let recentEdits = [];
|
|
65
124
|
let hasRecentKbEdit = false;
|
|
66
125
|
let recentCommentSuggestion = null;
|
|
67
126
|
const seenFingerprints = new Set(); // For deduplication
|
|
127
|
+
// NOTE: autoBriefResults is ONLY for prompt-time auto-brief guidance (file.edited flow).
|
|
128
|
+
// Idle-brief runtime (session.idle flow) writes directly to .kb/briefs/ via generateIdleBrief()
|
|
129
|
+
// and MUST NEVER store results in this map or leak into prompt guidance.
|
|
130
|
+
const autoBriefResults = new Map();
|
|
131
|
+
const toastedFingerprints = new Set();
|
|
68
132
|
let lastRiskClass = null;
|
|
133
|
+
let lastRiskFilePath = null;
|
|
134
|
+
const sessionEditState = createSessionEditState({ worktree: input.worktree });
|
|
135
|
+
const fileOperationState = createFileOperationState({
|
|
136
|
+
worktree: input.worktree,
|
|
137
|
+
}); // implements REQ-opencode-file-context-guidance-v1
|
|
69
138
|
let degradedWarnedOnce = false;
|
|
139
|
+
const pathKindCache = new Map();
|
|
140
|
+
// Idle-brief state — dedupe via semantic contentHash (persisted envelope is the delivery authority)
|
|
141
|
+
let idleBriefInFlight = false;
|
|
142
|
+
let idleBriefTrailingRerun = false;
|
|
143
|
+
let idleBriefTimer = null;
|
|
144
|
+
const idleBriefDeliveredHashes = new Set();
|
|
145
|
+
const replayedBriefContentHashes = new Set();
|
|
146
|
+
// Session-local baseline cursor: captured once per session/worktree/branch from the audit-log tail,
|
|
147
|
+
// so the first idle brief in a fresh session only reports post-baseline changes.
|
|
148
|
+
let sessionBaselineCursor = null;
|
|
149
|
+
let sessionBaselineFingerprint = null;
|
|
150
|
+
function syncSessionBaseline(branch) {
|
|
151
|
+
const nextState = syncSessionBaselineState({
|
|
152
|
+
fingerprint: sessionBaselineFingerprint,
|
|
153
|
+
cursor: sessionBaselineCursor,
|
|
154
|
+
}, {
|
|
155
|
+
sessionId: input.sessionId,
|
|
156
|
+
branch,
|
|
157
|
+
worktree: input.worktree,
|
|
158
|
+
}, () => getAuditTailCursor(input.worktree, branch));
|
|
159
|
+
sessionBaselineFingerprint = nextState.fingerprint;
|
|
160
|
+
sessionBaselineCursor = nextState.cursor;
|
|
161
|
+
}
|
|
162
|
+
syncSessionBaseline(currentBranch);
|
|
163
|
+
function normalizeSessionPath(filePath) {
|
|
164
|
+
if (path.isAbsolute(filePath)) {
|
|
165
|
+
const relativePath = path.relative(input.worktree, filePath);
|
|
166
|
+
return relativePath.startsWith("..") ? filePath : relativePath;
|
|
167
|
+
}
|
|
168
|
+
return filePath;
|
|
169
|
+
}
|
|
170
|
+
function resolveWorktreePath(filePath) {
|
|
171
|
+
return input.worktree && !path.isAbsolute(filePath)
|
|
172
|
+
? path.join(input.worktree, filePath)
|
|
173
|
+
: filePath;
|
|
174
|
+
}
|
|
175
|
+
function getKbSnapshotFingerprint(worktree, branch) {
|
|
176
|
+
try {
|
|
177
|
+
const snapshotPath = path.join(worktree, ".kb", "branches", branch, "kb.rdf");
|
|
178
|
+
const stat = fs.statSync(snapshotPath);
|
|
179
|
+
return `${stat.size}:${stat.mtimeMs}`;
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return "missing";
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function buildSyntheticSyncAuditDelta(baseDelta, sourceFiles) {
|
|
186
|
+
const timestamp = new Date().toISOString();
|
|
187
|
+
const fileSource = sourceFiles[0] ?? "workspace-sync";
|
|
188
|
+
const entityId = path.basename(fileSource).replace(/\.md$/, "") || "workspace-sync";
|
|
189
|
+
return {
|
|
190
|
+
...baseDelta,
|
|
191
|
+
hasChanges: true,
|
|
192
|
+
entries: [
|
|
193
|
+
{
|
|
194
|
+
timestamp,
|
|
195
|
+
operation: "upsert",
|
|
196
|
+
entityId,
|
|
197
|
+
payload: {
|
|
198
|
+
kind: "entity",
|
|
199
|
+
entityType: "fact",
|
|
200
|
+
changeKind: "updated",
|
|
201
|
+
title: entityId,
|
|
202
|
+
source: fileSource,
|
|
203
|
+
properties: {
|
|
204
|
+
id: entityId,
|
|
205
|
+
title: entityId,
|
|
206
|
+
source: fileSource,
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function getTransformFocusFilePath(transformInput) {
|
|
214
|
+
if (!transformInput || typeof transformInput !== "object") {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
const inputRecord = transformInput;
|
|
218
|
+
const directPath = inputRecord.focusFilePath ??
|
|
219
|
+
inputRecord.filePath ??
|
|
220
|
+
inputRecord.path ??
|
|
221
|
+
inputRecord.file ??
|
|
222
|
+
inputRecord.focusEdit?.path ??
|
|
223
|
+
inputRecord.focusEdit?.filePath;
|
|
224
|
+
if (typeof directPath !== "string" || directPath.length === 0) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
return normalizeSessionPath(directPath);
|
|
228
|
+
}
|
|
229
|
+
function readFileContent(filePath) {
|
|
230
|
+
try {
|
|
231
|
+
return fs.readFileSync(resolveWorktreePath(filePath), "utf-8");
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
return "";
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function updateRecentEditsFromSession(sessionEdits) {
|
|
238
|
+
recentEdits = sessionEdits.slice(-MAX_RECENT_EDITS).map((entry) => ({
|
|
239
|
+
path: entry.filePath,
|
|
240
|
+
kind: pathKindCache.get(entry.filePath) ?? "unknown",
|
|
241
|
+
timestamp: entry.lastReconciledAt,
|
|
242
|
+
}));
|
|
243
|
+
return recentEdits;
|
|
244
|
+
}
|
|
245
|
+
function deriveRiskContext(filePath) {
|
|
246
|
+
const normalizedFilePath = normalizeSessionPath(filePath);
|
|
247
|
+
const pathAnalysis = analyzePath(normalizedFilePath, input.worktree);
|
|
248
|
+
pathKindCache.set(normalizedFilePath, pathAnalysis.kind);
|
|
249
|
+
const fileContent = readFileContent(normalizedFilePath);
|
|
250
|
+
const hasMustPriority = pathAnalysis.kind === "requirement"
|
|
251
|
+
? isMustPriorityRequirement(normalizedFilePath, input.worktree)
|
|
252
|
+
: false;
|
|
253
|
+
let precomputedSuggestion = null;
|
|
254
|
+
if (pathAnalysis.kind === "code" && cfg.guidance.commentDetection.enabled) {
|
|
255
|
+
precomputedSuggestion = analyzeCodeFile(resolveWorktreePath(normalizedFilePath), {
|
|
256
|
+
minLines: cfg.guidance.commentDetection.minLines,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
const { riskClass } = classifyRisk({
|
|
260
|
+
pathKind: pathAnalysis.kind,
|
|
261
|
+
isUnderKb: pathAnalysis.isUnderKb,
|
|
262
|
+
hasMustPriority,
|
|
263
|
+
hasDurableComment: !!precomputedSuggestion,
|
|
264
|
+
fileContent,
|
|
265
|
+
});
|
|
266
|
+
const effectiveRiskClass = riskClass === "safe_docs_only" && precomputedSuggestion
|
|
267
|
+
? "traceability_candidate"
|
|
268
|
+
: riskClass;
|
|
269
|
+
recentCommentSuggestion =
|
|
270
|
+
pathAnalysis.kind === "code" ? precomputedSuggestion : null;
|
|
271
|
+
lastRiskClass = effectiveRiskClass;
|
|
272
|
+
lastRiskFilePath = normalizedFilePath;
|
|
273
|
+
return {
|
|
274
|
+
effectiveRiskClass,
|
|
275
|
+
pathAnalysis,
|
|
276
|
+
hasMustPriority,
|
|
277
|
+
precomputedSuggestion,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function buildBriefingWorkspaceContext() {
|
|
281
|
+
return {
|
|
282
|
+
workspaceRoot: input.worktree,
|
|
283
|
+
branch: currentBranch,
|
|
284
|
+
directory: input.directory,
|
|
285
|
+
...(input.workspace !== undefined ? { workspace: input.workspace } : {}),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
function buildWorkspaceContextForBranch(branch) {
|
|
289
|
+
return {
|
|
290
|
+
...buildBriefingWorkspaceContext(),
|
|
291
|
+
branch,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function queueBriefingFetch(intentResult, options = {}) {
|
|
295
|
+
if (!intentResult.eligible ||
|
|
296
|
+
!input.client ||
|
|
297
|
+
getMaintenanceDegraded() ||
|
|
298
|
+
(posture.state !== "root_active" &&
|
|
299
|
+
posture.state !== "hybrid_root_plus_vendored")) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (options.skipIfCachedResultExists === true &&
|
|
303
|
+
autoBriefResults.has(intentResult.fingerprint)) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const client = input.client;
|
|
307
|
+
const fingerprint = intentResult.fingerprint;
|
|
308
|
+
const workspaceCtx = buildBriefingWorkspaceContext();
|
|
309
|
+
void fetchBriefingResult(client, workspaceCtx, intentResult).then((result) => {
|
|
310
|
+
autoBriefResults.set(fingerprint, result);
|
|
311
|
+
if (!toastedFingerprints.has(fingerprint)) {
|
|
312
|
+
toastedFingerprints.add(fingerprint);
|
|
313
|
+
void sendToast(makeToastClient(client), {
|
|
314
|
+
message: result.toastMessage,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
}
|
|
70
319
|
hooks.event = async ({ event }) => {
|
|
71
|
-
|
|
320
|
+
const activeBranch = resolveCurrentBranch(input.worktree);
|
|
321
|
+
syncSessionBaseline(activeBranch);
|
|
322
|
+
// Handle session.idle for idle-brief generation. OpenCode can emit idle
|
|
323
|
+
// while an assistant is between tool calls, so debounce until the work
|
|
324
|
+
// burst settles before generating/delivering a brief.
|
|
325
|
+
if (event.type === "session.idle") {
|
|
326
|
+
if (!input.client)
|
|
327
|
+
return;
|
|
328
|
+
const idleBranch = activeBranch;
|
|
329
|
+
const idleWorkspaceRoot = input.worktree;
|
|
330
|
+
const runIdleBrief = async () => {
|
|
331
|
+
if (idleBriefInFlight) {
|
|
332
|
+
idleBriefTrailingRerun = true;
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
idleBriefInFlight = true;
|
|
336
|
+
idleBriefTrailingRerun = false;
|
|
337
|
+
try {
|
|
338
|
+
// Gather session edits
|
|
339
|
+
const sessionEdits = sessionEditState.getSessionEdits();
|
|
340
|
+
const sourceFiles = sessionEdits.map((e) => e.filePath);
|
|
341
|
+
const snapshotBeforeSync = getKbSnapshotFingerprint(idleWorkspaceRoot, idleBranch);
|
|
342
|
+
if (scheduler) {
|
|
343
|
+
scheduler.scheduleSync("session.idle");
|
|
344
|
+
await scheduler.flush();
|
|
345
|
+
}
|
|
346
|
+
const snapshotAfterSync = getKbSnapshotFingerprint(idleWorkspaceRoot, idleBranch);
|
|
347
|
+
const rawAuditDelta = computeAuditDelta(idleWorkspaceRoot, idleBranch, sessionBaselineCursor);
|
|
348
|
+
const auditDelta = rawAuditDelta.hasChanges || snapshotBeforeSync === snapshotAfterSync
|
|
349
|
+
? rawAuditDelta
|
|
350
|
+
: buildSyntheticSyncAuditDelta(rawAuditDelta, sourceFiles);
|
|
351
|
+
if (!auditDelta.hasChanges)
|
|
352
|
+
return;
|
|
353
|
+
// Branch switch guard
|
|
354
|
+
const currentBranchNow = resolveCurrentBranch(input.worktree);
|
|
355
|
+
if (guardBranchChanged(idleBranch, currentBranchNow)) {
|
|
356
|
+
logger.info("idle-brief.branch-changed", {
|
|
357
|
+
event: "idle_brief_branch_changed",
|
|
358
|
+
idleBranch,
|
|
359
|
+
currentBranch: currentBranchNow,
|
|
360
|
+
});
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
// Generate brief
|
|
364
|
+
const workspaceCtx = buildWorkspaceContextForBranch(idleBranch);
|
|
365
|
+
const client = input.client;
|
|
366
|
+
if (!client)
|
|
367
|
+
return;
|
|
368
|
+
const reconciled = reconcileAuditEntries(auditDelta.entries);
|
|
369
|
+
const changedEntityIds = [
|
|
370
|
+
...reconciled.added.map((e) => e.id),
|
|
371
|
+
...reconciled.modified.map((e) => e.id),
|
|
372
|
+
...reconciled.removed.map((e) => e.id),
|
|
373
|
+
];
|
|
374
|
+
const result = await generateIdleBrief(input.client, workspaceCtx, auditDelta, input.sessionId ?? "unknown", sourceFiles.length > 0
|
|
375
|
+
? { sourceFiles, changedEntityIds }
|
|
376
|
+
: { changedEntityIds });
|
|
377
|
+
if (result.success && result.envelope) {
|
|
378
|
+
const envelope = result.envelope;
|
|
379
|
+
// Dedupe by semantic contentHash — persisted envelope is the delivery authority
|
|
380
|
+
const dedupeKey = `${idleWorkspaceRoot}:${idleBranch}:tui:${envelope.contentHash}`;
|
|
381
|
+
if (!idleBriefDeliveredHashes.has(dedupeKey)) {
|
|
382
|
+
idleBriefDeliveredHashes.add(dedupeKey);
|
|
383
|
+
const sharedPolicy = { briefs: loadBriefConfig(input.worktree) };
|
|
384
|
+
const localConfig = {
|
|
385
|
+
autoSubmit: cfg.ux?.briefs?.autoSubmit ?? true,
|
|
386
|
+
};
|
|
387
|
+
if (client) {
|
|
388
|
+
try {
|
|
389
|
+
const deliveryResult = await deliverBriefTui(makeToastClient(client), envelope, sharedPolicy, localConfig);
|
|
390
|
+
const shouldMarkReadAfterTuiDelivery = !sharedPolicy.briefs.channels.vscode;
|
|
391
|
+
if (deliveryResult.delivered &&
|
|
392
|
+
result.briefPath) {
|
|
393
|
+
if (shouldMarkReadAfterTuiDelivery) {
|
|
394
|
+
markBriefRead(idleWorkspaceRoot, result.briefPath);
|
|
395
|
+
}
|
|
396
|
+
markBriefTuiSeen(idleWorkspaceRoot, idleBranch, envelope.contentHash);
|
|
397
|
+
replayedBriefContentHashes.add(envelope.contentHash);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
catch (err) {
|
|
401
|
+
logger.error("idle-brief.delivery-failed", {
|
|
402
|
+
event: "idle_brief_delivery_failed",
|
|
403
|
+
error: err instanceof Error ? err.message : String(err),
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
logger.info("idle-brief.no-brief-generated", {
|
|
411
|
+
event: "idle_brief_no_brief_generated",
|
|
412
|
+
success: result.success,
|
|
413
|
+
hasEnvelope: !!result.envelope,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
logger.error("idle-brief.error", {
|
|
419
|
+
event: "idle_brief_error",
|
|
420
|
+
error: error instanceof Error ? error.message : String(error),
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
finally {
|
|
424
|
+
idleBriefInFlight = false;
|
|
425
|
+
// If trailing rerun was requested, run again
|
|
426
|
+
if (idleBriefTrailingRerun) {
|
|
427
|
+
idleBriefTrailingRerun = false;
|
|
428
|
+
void runIdleBrief();
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
if (idleBriefTimer) {
|
|
433
|
+
clearTimeout(idleBriefTimer);
|
|
434
|
+
}
|
|
435
|
+
idleBriefTimer = setTimeout(() => {
|
|
436
|
+
idleBriefTimer = null;
|
|
437
|
+
void runIdleBrief();
|
|
438
|
+
}, resolveIdleBriefDeliveryDelayMs(idleWorkspaceRoot));
|
|
72
439
|
return;
|
|
440
|
+
}
|
|
441
|
+
// Accept file.created, file.edited, and file.deleted lifecycle events
|
|
442
|
+
const isFileLifecycle = event.type === "file.created" ||
|
|
443
|
+
event.type === "file.edited" ||
|
|
444
|
+
event.type === "file.deleted";
|
|
445
|
+
if (!isFileLifecycle)
|
|
446
|
+
return;
|
|
447
|
+
if (idleBriefTimer) {
|
|
448
|
+
clearTimeout(idleBriefTimer);
|
|
449
|
+
idleBriefTimer = null;
|
|
450
|
+
}
|
|
73
451
|
const filePath = event
|
|
74
452
|
.properties.file;
|
|
75
453
|
if (!filePath)
|
|
76
454
|
return;
|
|
455
|
+
// Record lifecycle event into file-operation-state // implements REQ-opencode-file-context-guidance-v1
|
|
456
|
+
const lifecycle = event.type === "file.created"
|
|
457
|
+
? "created"
|
|
458
|
+
: event.type === "file.deleted"
|
|
459
|
+
? "deleted"
|
|
460
|
+
: "edited";
|
|
461
|
+
fileOperationState.recordLifecycle(filePath, lifecycle, Date.now());
|
|
462
|
+
fileOperationState.normalizePath(filePath);
|
|
77
463
|
const pathAnalysis = analyzePath(filePath, input.worktree);
|
|
464
|
+
// For file.deleted: derive path kind without reading content, classify for reminder routing only
|
|
465
|
+
if (lifecycle === "deleted") {
|
|
466
|
+
// Preserve last known semantic risk if path was already tracked during session
|
|
467
|
+
const lastKnownKind = pathKindCache.get(filePath);
|
|
468
|
+
if (lastKnownKind) {
|
|
469
|
+
// Path was tracked — preserve last known semantic risk for reminder routing
|
|
470
|
+
pathKindCache.set(filePath, pathAnalysis.kind);
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
// Not tracked — classify only for reminder routing, not auto-briefing
|
|
474
|
+
pathKindCache.set(filePath, pathAnalysis.kind);
|
|
475
|
+
}
|
|
476
|
+
sessionEditState.recordEventHint(filePath, pathAnalysis.kind, Date.now());
|
|
477
|
+
sessionEditState.reconcilePath(filePath);
|
|
478
|
+
const sessionEdits = sessionEditState.getSessionEdits();
|
|
479
|
+
recentEdits = sessionEdits.slice(-MAX_RECENT_EDITS).map((e) => ({
|
|
480
|
+
path: e.filePath,
|
|
481
|
+
kind: pathKindCache.get(e.filePath) ?? "unknown",
|
|
482
|
+
timestamp: e.lastReconciledAt,
|
|
483
|
+
}));
|
|
484
|
+
// Schedule background sync for deleted files that pass shouldHandleFile // implements REQ-opencode-file-context-guidance-v1
|
|
485
|
+
if (cfg.sync.enabled &&
|
|
486
|
+
scheduler &&
|
|
487
|
+
fileFilter.shouldHandleFile(filePath, input.worktree)) {
|
|
488
|
+
scheduler.scheduleSync("file.deleted", filePath);
|
|
489
|
+
}
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
sessionEditState.recordEventHint(filePath, pathAnalysis.kind, Date.now());
|
|
493
|
+
sessionEditState.reconcilePath(filePath);
|
|
494
|
+
pathKindCache.set(filePath, pathAnalysis.kind);
|
|
495
|
+
const sessionEdits = sessionEditState.getSessionEdits();
|
|
496
|
+
const focusEdit = sessionEditState.getFocusEdit();
|
|
497
|
+
// Schedule background sync for file.created/file.edited that pass shouldHandleFile // implements REQ-opencode-file-context-guidance-v1
|
|
498
|
+
if (cfg.sync.enabled &&
|
|
499
|
+
scheduler &&
|
|
500
|
+
fileFilter.shouldHandleFile(filePath, input.worktree)) {
|
|
501
|
+
scheduler.scheduleSync(lifecycle === "created" ? "file.created" : "file.edited", filePath);
|
|
502
|
+
}
|
|
78
503
|
let fileContent = "";
|
|
79
504
|
try {
|
|
80
505
|
const resolvedPath = input.worktree && !path.isAbsolute(filePath)
|
|
@@ -105,6 +530,8 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
105
530
|
const effectiveRiskClass = riskClass === "safe_docs_only" && precomputedSuggestion
|
|
106
531
|
? "traceability_candidate"
|
|
107
532
|
: riskClass;
|
|
533
|
+
const isAutoBriefRisk = effectiveRiskClass === "behavior_candidate" ||
|
|
534
|
+
effectiveRiskClass === "traceability_candidate";
|
|
108
535
|
lastRiskClass = effectiveRiskClass;
|
|
109
536
|
logger.info("smart-enforcement.risk", {
|
|
110
537
|
event: "smart_enforcement_risk",
|
|
@@ -143,7 +570,9 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
143
570
|
"required-fields",
|
|
144
571
|
"no-dangling-refs",
|
|
145
572
|
...(pathAnalysis.kind === "fact" ? ["strict-fact-shape"] : []),
|
|
146
|
-
...(pathAnalysis.kind === "requirement"
|
|
573
|
+
...(pathAnalysis.kind === "requirement"
|
|
574
|
+
? ["strict-req-fact-pairing"]
|
|
575
|
+
: []),
|
|
147
576
|
]
|
|
148
577
|
: null;
|
|
149
578
|
const checkRules = traceabilityRules ?? kbStructuralRules;
|
|
@@ -168,15 +597,11 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
168
597
|
: "smart-enforcement.kb-doc", filePath, checkRules);
|
|
169
598
|
}
|
|
170
599
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
});
|
|
177
|
-
if (recentEdits.length > MAX_RECENT_EDITS) {
|
|
178
|
-
recentEdits = recentEdits.slice(-MAX_RECENT_EDITS);
|
|
179
|
-
}
|
|
600
|
+
recentEdits = sessionEdits.slice(-MAX_RECENT_EDITS).map((e) => ({
|
|
601
|
+
path: e.filePath,
|
|
602
|
+
kind: pathKindCache.get(e.filePath) ?? "unknown",
|
|
603
|
+
timestamp: e.lastReconciledAt,
|
|
604
|
+
}));
|
|
180
605
|
if (effectiveRiskClass === "safe_docs_only" ||
|
|
181
606
|
effectiveRiskClass === "safe_test_only") {
|
|
182
607
|
recentCommentSuggestion = null;
|
|
@@ -216,7 +641,9 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
216
641
|
posture: posture.state,
|
|
217
642
|
posture_state: posture.state,
|
|
218
643
|
});
|
|
219
|
-
|
|
644
|
+
if (!isAutoBriefRisk) {
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
220
647
|
}
|
|
221
648
|
logger.info("smart-enforcement.cache", {
|
|
222
649
|
event: "smart_enforcement_cache",
|
|
@@ -266,7 +693,11 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
266
693
|
logger.info(`kibi-opencode: must-priority requirement detected, scheduling elevated checks for ${filePath}`);
|
|
267
694
|
}
|
|
268
695
|
else {
|
|
269
|
-
checkRules = [
|
|
696
|
+
checkRules = [
|
|
697
|
+
"required-fields",
|
|
698
|
+
"no-dangling-refs",
|
|
699
|
+
"strict-req-fact-pairing",
|
|
700
|
+
];
|
|
270
701
|
}
|
|
271
702
|
}
|
|
272
703
|
logger.info("smart-enforcement.targeted-checks", {
|
|
@@ -312,8 +743,7 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
312
743
|
}
|
|
313
744
|
return;
|
|
314
745
|
}
|
|
315
|
-
if (
|
|
316
|
-
effectiveRiskClass === "traceability_candidate") {
|
|
746
|
+
if (isAutoBriefRisk) {
|
|
317
747
|
if (pathAnalysis.kind === "code" &&
|
|
318
748
|
cfg.guidance.commentDetection.enabled) {
|
|
319
749
|
const suggestion = precomputedSuggestion;
|
|
@@ -338,13 +768,28 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
338
768
|
else {
|
|
339
769
|
recentCommentSuggestion = null;
|
|
340
770
|
}
|
|
771
|
+
if (!focusEdit) {
|
|
772
|
+
// No surviving edits (all reverted to baseline) — skip auto-brief fetch
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
const sessionSourceFiles = sessionEdits.map((e) => e.filePath);
|
|
776
|
+
const intentResult = computeBriefIntent({
|
|
777
|
+
riskClass: effectiveRiskClass,
|
|
778
|
+
posture: posture.state,
|
|
779
|
+
maintenanceDegraded: getMaintenanceDegraded(),
|
|
780
|
+
sourceFiles: sessionSourceFiles,
|
|
781
|
+
focusFilePath: focusEdit.filePath,
|
|
782
|
+
worktreeRoot: input.worktree,
|
|
783
|
+
branch: currentBranch,
|
|
784
|
+
});
|
|
785
|
+
queueBriefingFetch(intentResult);
|
|
341
786
|
}
|
|
342
787
|
return;
|
|
343
788
|
};
|
|
344
789
|
if (cfg.prompt.enabled) {
|
|
345
790
|
const hookMode = cfg.prompt.hookMode;
|
|
346
791
|
if (hookMode === "system-transform" || hookMode === "auto") {
|
|
347
|
-
hooks["experimental.chat.system.transform"] = async (
|
|
792
|
+
hooks["experimental.chat.system.transform"] = async (transformInput, output) => {
|
|
348
793
|
// Skip if sentinel already present in any existing entry
|
|
349
794
|
if (output.system.some((entry) => entry.includes(SENTINEL))) {
|
|
350
795
|
return;
|
|
@@ -353,9 +798,134 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
353
798
|
const showDegradedAdvisory = maintenanceDegraded &&
|
|
354
799
|
cfg.guidance.smartEnforcement.degradedMode === "warn-once" &&
|
|
355
800
|
!degradedWarnedOnce;
|
|
356
|
-
|
|
801
|
+
const transformFocusFilePath = getTransformFocusFilePath(transformInput);
|
|
802
|
+
sessionEditState.reconcileKnownPaths();
|
|
803
|
+
if (transformFocusFilePath) {
|
|
804
|
+
sessionEditState.forceEdit(transformFocusFilePath);
|
|
805
|
+
}
|
|
806
|
+
const transformSessionEdits = sessionEditState.getSessionEdits();
|
|
807
|
+
const transformFocusEdit = sessionEditState.getFocusEdit();
|
|
808
|
+
const transformRecentEdits = transformSessionEdits
|
|
809
|
+
.slice(-MAX_RECENT_EDITS)
|
|
810
|
+
.map((e) => ({
|
|
811
|
+
path: e.filePath,
|
|
812
|
+
kind: pathKindCache.get(e.filePath) ?? "unknown",
|
|
813
|
+
}));
|
|
814
|
+
const transformPromptFocusEdit = transformFocusEdit
|
|
815
|
+
? {
|
|
816
|
+
path: transformFocusEdit.filePath,
|
|
817
|
+
kind: pathKindCache.get(transformFocusEdit.filePath) ?? "unknown",
|
|
818
|
+
}
|
|
819
|
+
: null;
|
|
820
|
+
const riskContextFilePath = transformFocusEdit?.filePath ?? transformFocusFilePath;
|
|
821
|
+
let effectiveRiskClass = riskContextFilePath && lastRiskFilePath === riskContextFilePath
|
|
822
|
+
? lastRiskClass
|
|
823
|
+
: null;
|
|
824
|
+
if (riskContextFilePath &&
|
|
825
|
+
(lastRiskClass === null || lastRiskFilePath !== riskContextFilePath)) {
|
|
826
|
+
const riskCtx = deriveRiskContext(riskContextFilePath);
|
|
827
|
+
effectiveRiskClass = riskCtx.effectiveRiskClass;
|
|
828
|
+
if (!recentCommentSuggestion && riskCtx.precomputedSuggestion) {
|
|
829
|
+
recentCommentSuggestion = riskCtx.precomputedSuggestion;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (effectiveRiskClass === null && lastRiskClass !== null) {
|
|
833
|
+
effectiveRiskClass = lastRiskClass;
|
|
834
|
+
}
|
|
835
|
+
const promptSourceFiles = transformSessionEdits.map((entry) => entry.filePath);
|
|
836
|
+
const promptFocusFilePath = transformFocusEdit?.filePath ?? transformFocusFilePath ?? undefined;
|
|
837
|
+
const intentResult = effectiveRiskClass
|
|
838
|
+
? computeBriefIntent({
|
|
839
|
+
riskClass: effectiveRiskClass,
|
|
840
|
+
posture: posture.state,
|
|
841
|
+
maintenanceDegraded,
|
|
842
|
+
sourceFiles: promptSourceFiles,
|
|
843
|
+
worktreeRoot: input.worktree,
|
|
844
|
+
branch: currentBranch,
|
|
845
|
+
...(promptFocusFilePath !== undefined
|
|
846
|
+
? {
|
|
847
|
+
focusFilePath: promptFocusFilePath,
|
|
848
|
+
}
|
|
849
|
+
: {}),
|
|
850
|
+
})
|
|
851
|
+
: null;
|
|
852
|
+
const autoBriefResult = intentResult
|
|
853
|
+
? autoBriefResults.get(intentResult.fingerprint)
|
|
854
|
+
: undefined;
|
|
855
|
+
const isAutoBriefRisk = effectiveRiskClass === "behavior_candidate" ||
|
|
856
|
+
effectiveRiskClass === "traceability_candidate";
|
|
857
|
+
if (!autoBriefResult && isAutoBriefRisk && intentResult) {
|
|
858
|
+
queueBriefingFetch(intentResult, { skipIfCachedResultExists: true });
|
|
859
|
+
}
|
|
860
|
+
// Replay latest unread idle brief if available // implements REQ-opencode-kibi-briefing-v4
|
|
861
|
+
if (input.worktree && currentBranch && input.client) {
|
|
862
|
+
const unreadBrief = selectLatestUnreadBrief(input.worktree, currentBranch);
|
|
863
|
+
if (unreadBrief &&
|
|
864
|
+
!replayedBriefContentHashes.has(unreadBrief.envelope.contentHash) &&
|
|
865
|
+
!hasTuiSeenBrief(input.worktree, currentBranch, unreadBrief.envelope.contentHash)) {
|
|
866
|
+
const sharedPolicy = { briefs: loadBriefConfig(input.worktree) };
|
|
867
|
+
const localConfig = {
|
|
868
|
+
autoSubmit: cfg.ux?.briefs?.autoSubmit ?? true,
|
|
869
|
+
};
|
|
870
|
+
const client = input.client;
|
|
871
|
+
try {
|
|
872
|
+
const deliveryResult = await deliverBriefTui(makeToastClient(client), unreadBrief.envelope, sharedPolicy, localConfig);
|
|
873
|
+
const shouldMarkReadAfterTuiDelivery = !sharedPolicy.briefs.channels.vscode;
|
|
874
|
+
if (deliveryResult.delivered) {
|
|
875
|
+
if (shouldMarkReadAfterTuiDelivery) {
|
|
876
|
+
markBriefRead(input.worktree, unreadBrief.filePath);
|
|
877
|
+
}
|
|
878
|
+
markBriefTuiSeen(input.worktree, currentBranch, unreadBrief.envelope.contentHash);
|
|
879
|
+
replayedBriefContentHashes.add(unreadBrief.envelope.contentHash);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
catch (err) {
|
|
883
|
+
logger.error("idle-brief.replay-failed", {
|
|
884
|
+
event: "idle_brief_replay_failed",
|
|
885
|
+
error: err instanceof Error ? err.message : String(err),
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
// Steps 3-4: File-operation reminder selection with suppression // implements REQ-opencode-file-context-guidance-v1
|
|
891
|
+
let fileOperationReminder;
|
|
892
|
+
const focusPathForReminder = transformFocusFilePath ?? promptFocusFilePath;
|
|
893
|
+
if (focusPathForReminder) {
|
|
894
|
+
const normalizedFocusPath = fileOperationState.normalizePath(focusPathForReminder);
|
|
895
|
+
const pendingLifecycle = fileOperationState.peekPending(normalizedFocusPath);
|
|
896
|
+
if (pendingLifecycle) {
|
|
897
|
+
// Check if any reminder kind for this lifecycle has not yet been shown
|
|
898
|
+
const reminderKindsForLifecycle = pendingLifecycle.lifecycle === "deleted"
|
|
899
|
+
? ["kibi_delete", "e2e_delete"]
|
|
900
|
+
: pendingLifecycle.lifecycle === "created"
|
|
901
|
+
? ["kibi_write", "e2e_write"]
|
|
902
|
+
: ["e2e_write"];
|
|
903
|
+
const hasUnshownReminder = reminderKindsForLifecycle.some((kind) => !fileOperationState.hasShown(normalizedFocusPath, kind));
|
|
904
|
+
if (hasUnshownReminder) {
|
|
905
|
+
// Resolve linked entities and e2e signal
|
|
906
|
+
const linkedEntityResult = getFileLinkedEntityIds(input.worktree, focusPathForReminder);
|
|
907
|
+
const e2eSignal = getE2eCoverageSignal(input.worktree, focusPathForReminder);
|
|
908
|
+
const focusPathKind = pathKindCache.get(normalizedFocusPath) ?? "unknown";
|
|
909
|
+
const reminderResult = deriveFileOperationReminder({
|
|
910
|
+
normalizedPath: normalizedFocusPath,
|
|
911
|
+
lifecycle: pendingLifecycle.lifecycle,
|
|
912
|
+
pathKind: focusPathKind,
|
|
913
|
+
linkedEntityResult,
|
|
914
|
+
e2eSignal,
|
|
915
|
+
currentSemanticRisk: effectiveRiskClass ?? "safe_docs_only",
|
|
916
|
+
posture: posture.state,
|
|
917
|
+
});
|
|
918
|
+
fileOperationReminder = {
|
|
919
|
+
path: normalizedFocusPath,
|
|
920
|
+
lifecycleReminder: reminderResult.lifecycleReminder,
|
|
921
|
+
e2eReminder: reminderResult.e2eReminder,
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
357
926
|
const guidance = buildPrompt({
|
|
358
|
-
recentEdits,
|
|
927
|
+
recentEdits: transformRecentEdits,
|
|
928
|
+
focusEdit: transformPromptFocusEdit,
|
|
359
929
|
workspaceHealth,
|
|
360
930
|
hasRecentKbEdit,
|
|
361
931
|
recentCommentSuggestion,
|
|
@@ -367,7 +937,13 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
367
937
|
maintenanceDegraded,
|
|
368
938
|
degradedMode: cfg.guidance.smartEnforcement.degradedMode,
|
|
369
939
|
showDegradedAdvisory,
|
|
370
|
-
...(
|
|
940
|
+
...(autoBriefResult !== undefined ? { autoBriefResult } : {}),
|
|
941
|
+
...(effectiveRiskClass != null
|
|
942
|
+
? { riskClass: effectiveRiskClass }
|
|
943
|
+
: {}),
|
|
944
|
+
...(fileOperationReminder !== undefined
|
|
945
|
+
? { fileOperationReminder }
|
|
946
|
+
: {}),
|
|
371
947
|
});
|
|
372
948
|
logger.info("smart-enforcement.guidance", {
|
|
373
949
|
event: "smart_enforcement_guidance",
|
|
@@ -402,6 +978,53 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
402
978
|
overlay_cause: runtimeOverlay.primaryCause ?? null,
|
|
403
979
|
});
|
|
404
980
|
}
|
|
981
|
+
// Step 6: After prompt generation, mark reminders as shown if guidance contains the text // implements REQ-opencode-file-context-guidance-v1
|
|
982
|
+
if (fileOperationReminder) {
|
|
983
|
+
const lifecycleReminderText = fileOperationReminder.lifecycleReminder;
|
|
984
|
+
const e2eReminderText = fileOperationReminder.e2eReminder;
|
|
985
|
+
const focusPathForConsume = fileOperationReminder.path;
|
|
986
|
+
// Determine which reminders were actually emitted in guidance
|
|
987
|
+
const lifecycleEmitted = lifecycleReminderText !== null &&
|
|
988
|
+
guidance.includes(lifecycleReminderText);
|
|
989
|
+
const e2eEmitted = e2eReminderText !== null && guidance.includes(e2eReminderText);
|
|
990
|
+
// Mark shown and log only for reminders that were actually emitted
|
|
991
|
+
if (lifecycleEmitted) {
|
|
992
|
+
const kind = fileOperationState.peekPending(focusPathForConsume)?.lifecycle ===
|
|
993
|
+
"deleted"
|
|
994
|
+
? "kibi_delete"
|
|
995
|
+
: "kibi_write";
|
|
996
|
+
fileOperationState.markShown(focusPathForConsume, kind);
|
|
997
|
+
logger.info("smart-enforcement.file-operation-reminder", {
|
|
998
|
+
event: "smart_enforcement_file_operation_reminder",
|
|
999
|
+
file: focusPathForConsume,
|
|
1000
|
+
lifecycle: fileOperationState.peekPending(focusPathForConsume)
|
|
1001
|
+
?.lifecycle ?? null,
|
|
1002
|
+
posture_state: posture.state,
|
|
1003
|
+
risk_class: effectiveRiskClass,
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
if (e2eEmitted) {
|
|
1007
|
+
const kind = fileOperationState.peekPending(focusPathForConsume)?.lifecycle ===
|
|
1008
|
+
"deleted"
|
|
1009
|
+
? "e2e_delete"
|
|
1010
|
+
: "e2e_write";
|
|
1011
|
+
fileOperationState.markShown(focusPathForConsume, kind);
|
|
1012
|
+
const e2eSignalForLog = getE2eCoverageSignal(input.worktree, focusPathForConsume);
|
|
1013
|
+
logger.info("smart-enforcement.e2e-reminder", {
|
|
1014
|
+
event: "smart_enforcement_e2e_reminder",
|
|
1015
|
+
file: focusPathForConsume,
|
|
1016
|
+
lifecycle: fileOperationState.peekPending(focusPathForConsume)
|
|
1017
|
+
?.lifecycle ?? null,
|
|
1018
|
+
signal_level: e2eSignalForLog.level,
|
|
1019
|
+
posture_state: posture.state,
|
|
1020
|
+
risk_class: effectiveRiskClass,
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
// Consume pending only if at least one reminder was emitted
|
|
1024
|
+
if (lifecycleEmitted || e2eEmitted) {
|
|
1025
|
+
fileOperationState.consumePending(focusPathForConsume);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
405
1028
|
// Latch degraded advisory warning-once state
|
|
406
1029
|
if (showDegradedAdvisory && guidance.includes("Maintenance degraded")) {
|
|
407
1030
|
degradedWarnedOnce = true;
|
|
@@ -434,7 +1057,7 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
434
1057
|
setTimeout(callback, delayMs);
|
|
435
1058
|
});
|
|
436
1059
|
scheduleStartupNotify(() => {
|
|
437
|
-
notifyStartup(client, {
|
|
1060
|
+
notifyStartup(makeStartupClient(client), {
|
|
438
1061
|
suppressToast: cfg.ux.toastStartup === false,
|
|
439
1062
|
directory: input.directory,
|
|
440
1063
|
});
|