kibi-opencode 0.9.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 +36 -12
- package/dist/brief-intent.d.ts +15 -4
- package/dist/brief-intent.js +63 -25
- package/dist/briefing-runtime.js +2 -1
- 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 +14 -1
- package/dist/index.js +626 -50
- 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 +15 -3
- package/dist/prompt.js +103 -33
- 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.js +42 -31
- package/dist/toast.d.ts +21 -22
- package/dist/toast.js +36 -14
- 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,21 +1,48 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
+
import { loadBriefConfig } from "kibi-cli/brief-config";
|
|
2
3
|
import { computeBriefIntent } from "./brief-intent.js";
|
|
3
4
|
import { fetchBriefingResult, } from "./briefing-runtime.js";
|
|
4
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
|
|
5
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";
|
|
6
15
|
import * as logger from "./logger.js";
|
|
7
16
|
import { analyzePath } from "./path-kind.js";
|
|
17
|
+
import { runPluginStartup } from "./plugin-startup.js";
|
|
18
|
+
import { resolveCurrentBranch } from "./plugin-startup.js";
|
|
8
19
|
import { SENTINEL, buildPrompt } from "./prompt.js";
|
|
20
|
+
import { reconcileAuditEntries } from "./reconcile-engine.js";
|
|
9
21
|
import { isMustPriorityRequirement } from "./requirement-doc.js";
|
|
10
22
|
import { classifyRisk } from "./risk-classifier.js";
|
|
23
|
+
import { createSessionEditState, } from "./session-edit-state.js";
|
|
24
|
+
import { syncSessionBaselineState, } from "./session-fingerprint.js";
|
|
11
25
|
import { getSessionTracker } from "./session-tracker.js";
|
|
12
|
-
import { notifyStartup } from "./startup-notifier.js";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
26
|
+
import { notifyStartup, } from "./startup-notifier.js";
|
|
27
|
+
import { sendToast, } from "./toast.js";
|
|
28
|
+
import { deliverBriefTui, } from "./tui-brief-delivery.js";
|
|
15
29
|
import * as fs from "node:fs";
|
|
16
30
|
function deriveFileBucket(kind) {
|
|
17
31
|
return kind;
|
|
18
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
|
+
}
|
|
19
46
|
const startupNotifyGlobals = globalThis;
|
|
20
47
|
/**
|
|
21
48
|
* Lint requirement documents for embedded scenarios/tests and oversized content.
|
|
@@ -56,32 +83,423 @@ function lintRequirementDoc(filePath, worktree) {
|
|
|
56
83
|
}
|
|
57
84
|
// implements REQ-opencode-kibi-plugin-v1
|
|
58
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
|
+
});
|
|
59
109
|
const startup = await runPluginStartup(input);
|
|
60
110
|
if (!startup) {
|
|
61
111
|
return {};
|
|
62
112
|
}
|
|
63
113
|
const { cfg, workspaceHealth, posture, currentBranch, cache, runtimeOverlay, scheduler, maintenanceDegraded, getMaintenanceDegraded, getEffectiveMode, latchRuntimeDegraded, } = startup;
|
|
64
114
|
const hooks = {};
|
|
115
|
+
const initKibiCommandCapability = getInitKibiCommandCapability();
|
|
116
|
+
if (initKibiCommandCapability.supported) {
|
|
117
|
+
hooks.config = async (configInput) => {
|
|
118
|
+
registerInitKibiCommand(configInput, initKibiCommandCapability);
|
|
119
|
+
};
|
|
120
|
+
}
|
|
65
121
|
// Plugin instance state (not module globals)
|
|
66
122
|
const MAX_RECENT_EDITS = 5;
|
|
67
123
|
let recentEdits = [];
|
|
68
124
|
let hasRecentKbEdit = false;
|
|
69
125
|
let recentCommentSuggestion = null;
|
|
70
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.
|
|
71
130
|
const autoBriefResults = new Map();
|
|
72
131
|
const toastedFingerprints = new Set();
|
|
73
132
|
let lastRiskClass = null;
|
|
74
|
-
let
|
|
75
|
-
|
|
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
|
|
76
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
|
+
}
|
|
77
319
|
hooks.event = async ({ event }) => {
|
|
78
|
-
|
|
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));
|
|
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)
|
|
79
446
|
return;
|
|
447
|
+
if (idleBriefTimer) {
|
|
448
|
+
clearTimeout(idleBriefTimer);
|
|
449
|
+
idleBriefTimer = null;
|
|
450
|
+
}
|
|
80
451
|
const filePath = event
|
|
81
452
|
.properties.file;
|
|
82
453
|
if (!filePath)
|
|
83
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);
|
|
84
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
|
+
}
|
|
85
503
|
let fileContent = "";
|
|
86
504
|
try {
|
|
87
505
|
const resolvedPath = input.worktree && !path.isAbsolute(filePath)
|
|
@@ -115,7 +533,6 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
115
533
|
const isAutoBriefRisk = effectiveRiskClass === "behavior_candidate" ||
|
|
116
534
|
effectiveRiskClass === "traceability_candidate";
|
|
117
535
|
lastRiskClass = effectiveRiskClass;
|
|
118
|
-
lastEditedFilePath = filePath;
|
|
119
536
|
logger.info("smart-enforcement.risk", {
|
|
120
537
|
event: "smart_enforcement_risk",
|
|
121
538
|
file: filePath,
|
|
@@ -153,7 +570,9 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
153
570
|
"required-fields",
|
|
154
571
|
"no-dangling-refs",
|
|
155
572
|
...(pathAnalysis.kind === "fact" ? ["strict-fact-shape"] : []),
|
|
156
|
-
...(pathAnalysis.kind === "requirement"
|
|
573
|
+
...(pathAnalysis.kind === "requirement"
|
|
574
|
+
? ["strict-req-fact-pairing"]
|
|
575
|
+
: []),
|
|
157
576
|
]
|
|
158
577
|
: null;
|
|
159
578
|
const checkRules = traceabilityRules ?? kbStructuralRules;
|
|
@@ -178,15 +597,11 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
178
597
|
: "smart-enforcement.kb-doc", filePath, checkRules);
|
|
179
598
|
}
|
|
180
599
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
});
|
|
187
|
-
if (recentEdits.length > MAX_RECENT_EDITS) {
|
|
188
|
-
recentEdits = recentEdits.slice(-MAX_RECENT_EDITS);
|
|
189
|
-
}
|
|
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
|
+
}));
|
|
190
605
|
if (effectiveRiskClass === "safe_docs_only" ||
|
|
191
606
|
effectiveRiskClass === "safe_test_only") {
|
|
192
607
|
recentCommentSuggestion = null;
|
|
@@ -278,7 +693,11 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
278
693
|
logger.info(`kibi-opencode: must-priority requirement detected, scheduling elevated checks for ${filePath}`);
|
|
279
694
|
}
|
|
280
695
|
else {
|
|
281
|
-
checkRules = [
|
|
696
|
+
checkRules = [
|
|
697
|
+
"required-fields",
|
|
698
|
+
"no-dangling-refs",
|
|
699
|
+
"strict-req-fact-pairing",
|
|
700
|
+
];
|
|
282
701
|
}
|
|
283
702
|
}
|
|
284
703
|
logger.info("smart-enforcement.targeted-checks", {
|
|
@@ -349,45 +768,28 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
349
768
|
else {
|
|
350
769
|
recentCommentSuggestion = null;
|
|
351
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);
|
|
352
776
|
const intentResult = computeBriefIntent({
|
|
353
777
|
riskClass: effectiveRiskClass,
|
|
354
778
|
posture: posture.state,
|
|
355
779
|
maintenanceDegraded: getMaintenanceDegraded(),
|
|
356
|
-
|
|
780
|
+
sourceFiles: sessionSourceFiles,
|
|
781
|
+
focusFilePath: focusEdit.filePath,
|
|
357
782
|
worktreeRoot: input.worktree,
|
|
358
783
|
branch: currentBranch,
|
|
359
784
|
});
|
|
360
|
-
|
|
361
|
-
if (intentResult.eligible &&
|
|
362
|
-
input.client &&
|
|
363
|
-
!getMaintenanceDegraded() &&
|
|
364
|
-
(posture.state === "root_active" ||
|
|
365
|
-
posture.state === "hybrid_root_plus_vendored")) {
|
|
366
|
-
const client = input.client;
|
|
367
|
-
const fingerprint = intentResult.fingerprint;
|
|
368
|
-
const workspaceCtx = {
|
|
369
|
-
workspaceRoot: input.worktree,
|
|
370
|
-
branch: currentBranch,
|
|
371
|
-
directory: input.directory,
|
|
372
|
-
...(input.workspace !== undefined ? { workspace: input.workspace } : {}),
|
|
373
|
-
};
|
|
374
|
-
void fetchBriefingResult(client, workspaceCtx, intentResult).then((result) => {
|
|
375
|
-
autoBriefResults.set(fingerprint, result);
|
|
376
|
-
if (!toastedFingerprints.has(fingerprint)) {
|
|
377
|
-
toastedFingerprints.add(fingerprint);
|
|
378
|
-
void sendToast(client, { message: result.toastMessage }).catch(() => {
|
|
379
|
-
// toast delivery failure is non-fatal
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
});
|
|
383
|
-
}
|
|
785
|
+
queueBriefingFetch(intentResult);
|
|
384
786
|
}
|
|
385
787
|
return;
|
|
386
788
|
};
|
|
387
789
|
if (cfg.prompt.enabled) {
|
|
388
790
|
const hookMode = cfg.prompt.hookMode;
|
|
389
791
|
if (hookMode === "system-transform" || hookMode === "auto") {
|
|
390
|
-
hooks["experimental.chat.system.transform"] = async (
|
|
792
|
+
hooks["experimental.chat.system.transform"] = async (transformInput, output) => {
|
|
391
793
|
// Skip if sentinel already present in any existing entry
|
|
392
794
|
if (output.system.some((entry) => entry.includes(SENTINEL))) {
|
|
393
795
|
return;
|
|
@@ -396,12 +798,134 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
396
798
|
const showDegradedAdvisory = maintenanceDegraded &&
|
|
397
799
|
cfg.guidance.smartEnforcement.degradedMode === "warn-once" &&
|
|
398
800
|
!degradedWarnedOnce;
|
|
399
|
-
const
|
|
400
|
-
|
|
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)
|
|
401
854
|
: undefined;
|
|
402
|
-
|
|
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
|
+
}
|
|
403
926
|
const guidance = buildPrompt({
|
|
404
|
-
recentEdits,
|
|
927
|
+
recentEdits: transformRecentEdits,
|
|
928
|
+
focusEdit: transformPromptFocusEdit,
|
|
405
929
|
workspaceHealth,
|
|
406
930
|
hasRecentKbEdit,
|
|
407
931
|
recentCommentSuggestion,
|
|
@@ -414,7 +938,12 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
414
938
|
degradedMode: cfg.guidance.smartEnforcement.degradedMode,
|
|
415
939
|
showDegradedAdvisory,
|
|
416
940
|
...(autoBriefResult !== undefined ? { autoBriefResult } : {}),
|
|
417
|
-
...(
|
|
941
|
+
...(effectiveRiskClass != null
|
|
942
|
+
? { riskClass: effectiveRiskClass }
|
|
943
|
+
: {}),
|
|
944
|
+
...(fileOperationReminder !== undefined
|
|
945
|
+
? { fileOperationReminder }
|
|
946
|
+
: {}),
|
|
418
947
|
});
|
|
419
948
|
logger.info("smart-enforcement.guidance", {
|
|
420
949
|
event: "smart_enforcement_guidance",
|
|
@@ -449,6 +978,53 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
449
978
|
overlay_cause: runtimeOverlay.primaryCause ?? null,
|
|
450
979
|
});
|
|
451
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
|
+
}
|
|
452
1028
|
// Latch degraded advisory warning-once state
|
|
453
1029
|
if (showDegradedAdvisory && guidance.includes("Maintenance degraded")) {
|
|
454
1030
|
degradedWarnedOnce = true;
|
|
@@ -481,7 +1057,7 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
481
1057
|
setTimeout(callback, delayMs);
|
|
482
1058
|
});
|
|
483
1059
|
scheduleStartupNotify(() => {
|
|
484
|
-
notifyStartup(client, {
|
|
1060
|
+
notifyStartup(makeStartupClient(client), {
|
|
485
1061
|
suppressToast: cfg.ux.toastStartup === false,
|
|
486
1062
|
directory: input.directory,
|
|
487
1063
|
});
|