kibi-opencode 0.13.0 → 0.15.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 +18 -4
- package/dist/config.d.ts +1 -1
- package/dist/config.js +2 -2
- package/dist/enforcement-policy.d.ts +73 -0
- package/dist/enforcement-policy.js +279 -0
- package/dist/enforcement-scope.d.ts +15 -0
- package/dist/enforcement-scope.js +36 -0
- package/dist/file-operation-reminders.d.ts +13 -0
- package/dist/file-operation-reminders.js +24 -37
- package/dist/guidance-cache.d.ts +3 -0
- package/dist/guidance-cache.js +1 -1
- package/dist/kb-freshness-state.d.ts +62 -0
- package/dist/kb-freshness-state.js +211 -0
- package/dist/kibi-checkpoint-runner.d.ts +83 -0
- package/dist/kibi-checkpoint-runner.js +254 -0
- package/dist/meaningful-change-classifier.d.ts +30 -0
- package/dist/meaningful-change-classifier.js +104 -0
- package/dist/plugin.d.ts +1 -0
- package/dist/plugin.js +480 -177
- package/dist/prompt.d.ts +11 -0
- package/dist/prompt.js +54 -2
- package/dist/smart-enforcement.d.ts +6 -2
- package/dist/smart-enforcement.js +7 -1
- package/dist/startup-notifier.d.ts +7 -1
- package/dist/startup-notifier.js +51 -17
- package/dist/version-metadata.d.ts +13 -0
- package/dist/version-metadata.js +78 -0
- package/dist/version-metadata.json +1 -0
- package/dist/work-context-resolver.d.ts +21 -0
- package/dist/work-context-resolver.js +197 -0
- package/package.json +4 -4
package/dist/plugin.js
CHANGED
|
@@ -4,10 +4,12 @@ import { computeBriefIntent } from "./brief-intent.js";
|
|
|
4
4
|
import { fetchBriefingResult, } from "./briefing-runtime.js";
|
|
5
5
|
import { analyzeCodeFile, } from "./comment-analysis.js";
|
|
6
6
|
import { getE2eCoverageSignal } from "./e2e-coverage-signals.js"; // implements REQ-opencode-file-context-guidance-v1
|
|
7
|
+
import { buildDirtyRelevantFingerprint, buildEnforcementScopeKey, } from "./enforcement-scope.js";
|
|
7
8
|
import { getFileLinkedEntityIds } from "./file-entity-links.js"; // implements REQ-opencode-file-context-guidance-v1
|
|
8
9
|
import * as fileFilter from "./file-filter.js";
|
|
9
10
|
import { deriveFileOperationReminder } from "./file-operation-reminders.js"; // implements REQ-opencode-file-context-guidance-v1
|
|
10
11
|
import { createFileOperationState, } from "./file-operation-state.js"; // implements REQ-opencode-file-context-guidance-v1
|
|
12
|
+
import { KibiCheckpointRunner, } from "./kibi-checkpoint-runner.js";
|
|
11
13
|
import { getInitKibiCommandCapability, registerInitKibiCommand, } from "./init-kibi-capability.js";
|
|
12
14
|
import { computeAuditDelta, getAuditTailCursor, guardBranchChanged, } from "./idle-brief-audit.js";
|
|
13
15
|
import { hasTuiSeenBrief, selectLatestUnreadBrief, } from "./idle-brief-reader.js";
|
|
@@ -20,13 +22,18 @@ import { SENTINEL, buildPrompt } from "./prompt.js";
|
|
|
20
22
|
import { reconcileAuditEntries } from "./reconcile-engine.js";
|
|
21
23
|
import { isMustPriorityRequirement } from "./requirement-doc.js";
|
|
22
24
|
import { classifyRisk } from "./risk-classifier.js";
|
|
25
|
+
import { createSyncScheduler } from "./scheduler.js";
|
|
23
26
|
import { createSessionEditState, } from "./session-edit-state.js";
|
|
24
27
|
import { syncSessionBaselineState, } from "./session-fingerprint.js";
|
|
25
28
|
import { getSessionTracker } from "./session-tracker.js";
|
|
26
29
|
import { notifyStartup, } from "./startup-notifier.js";
|
|
30
|
+
import { readKibiPackageVersions } from "./version-metadata.js";
|
|
27
31
|
import { sendToast, } from "./toast.js";
|
|
28
32
|
import { announceBriefTui, } from "./tui-brief-delivery.js";
|
|
33
|
+
import { createKbFreshnessEvidenceStore, evaluateKbFreshness, } from "./kb-freshness-state.js";
|
|
34
|
+
import { classifyMeaningfulChange } from "./meaningful-change-classifier.js";
|
|
29
35
|
import { deletePendingBriefMarkers, loadPendingBriefMarkers, } from "./utils/brief-marker.js";
|
|
36
|
+
import { resolveWorkContext, } from "./work-context-resolver.js";
|
|
30
37
|
import * as fs from "node:fs";
|
|
31
38
|
function deriveFileBucket(kind) {
|
|
32
39
|
return kind;
|
|
@@ -44,17 +51,6 @@ function resolveIdleBriefDeliveryDelayMs(worktree) {
|
|
|
44
51
|
return 0;
|
|
45
52
|
return Math.min(60_000, Math.trunc(configValue));
|
|
46
53
|
}
|
|
47
|
-
function readKibiOpencodePackageVersion() {
|
|
48
|
-
try {
|
|
49
|
-
const packageJson = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
50
|
-
return typeof packageJson.version === "string"
|
|
51
|
-
? packageJson.version
|
|
52
|
-
: undefined;
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
return undefined;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
54
|
const startupNotifyGlobals = globalThis;
|
|
59
55
|
/**
|
|
60
56
|
* Lint requirement documents for embedded scenarios/tests and oversized content.
|
|
@@ -125,7 +121,7 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
125
121
|
if (!startup) {
|
|
126
122
|
return {};
|
|
127
123
|
}
|
|
128
|
-
const { cfg, workspaceHealth, posture, currentBranch, cache, runtimeOverlay, scheduler, maintenanceDegraded, getMaintenanceDegraded, getEffectiveMode, latchRuntimeDegraded, } = startup;
|
|
124
|
+
const { cfg, workspaceHealth, posture, currentBranch, cache, runtimeOverlay, scheduler: startupScheduler, maintenanceDegraded, getMaintenanceDegraded, getEffectiveMode, latchRuntimeDegraded, } = startup;
|
|
129
125
|
const hooks = {};
|
|
130
126
|
const initKibiCommandCapability = getInitKibiCommandCapability();
|
|
131
127
|
if (initKibiCommandCapability.supported) {
|
|
@@ -146,12 +142,151 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
146
142
|
const toastedFingerprints = new Set();
|
|
147
143
|
let lastRiskClass = null;
|
|
148
144
|
let lastRiskFilePath = null;
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
145
|
+
let lastRiskScopeKey = null;
|
|
146
|
+
const schedulerRegistry = new Map();
|
|
147
|
+
if (startupScheduler) {
|
|
148
|
+
schedulerRegistry.set(path.resolve(input.worktree), startupScheduler);
|
|
149
|
+
}
|
|
150
|
+
const schedulerFactoryGlobals = globalThis;
|
|
151
|
+
const sessionEditStateRegistry = new Map();
|
|
152
|
+
const fileOperationStateRegistry = new Map();
|
|
153
|
+
const checkpointRunnerRegistry = new Map();
|
|
154
|
+
const pathKindCacheRegistry = new Map();
|
|
155
|
+
const freshnessStore = createKbFreshnessEvidenceStore();
|
|
156
|
+
function resolveScopedWorkContext(filePath) {
|
|
157
|
+
return resolveWorkContext({
|
|
158
|
+
inputDirectory: input.directory,
|
|
159
|
+
inputWorktree: input.worktree,
|
|
160
|
+
...(filePath !== undefined ? { filePath } : {}),
|
|
161
|
+
...(input.sessionId !== undefined ? { sessionId: input.sessionId } : {}),
|
|
162
|
+
...(input.agentIdentity !== undefined
|
|
163
|
+
? { agentIdentity: input.agentIdentity }
|
|
164
|
+
: {}),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
function buildStateScopeKey(context, lane) {
|
|
168
|
+
return buildEnforcementScopeKey({
|
|
169
|
+
sessionId: context.sessionId,
|
|
170
|
+
agentIdentity: context.agentIdentity,
|
|
171
|
+
worktreeRoot: context.worktreeRoot,
|
|
172
|
+
branch: context.branch,
|
|
173
|
+
dirtyRelevantFingerprint: lane,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
function getSessionEditState(context) {
|
|
177
|
+
const key = buildStateScopeKey(context, "session-edits");
|
|
178
|
+
let state = sessionEditStateRegistry.get(key);
|
|
179
|
+
if (!state) {
|
|
180
|
+
state = createSessionEditState({ worktree: context.worktreeRoot });
|
|
181
|
+
sessionEditStateRegistry.set(key, state);
|
|
182
|
+
}
|
|
183
|
+
return state;
|
|
184
|
+
}
|
|
185
|
+
function getFileOperationState(context) {
|
|
186
|
+
const key = buildStateScopeKey(context, "file-operations");
|
|
187
|
+
let state = fileOperationStateRegistry.get(key);
|
|
188
|
+
if (!state) {
|
|
189
|
+
state = createFileOperationState({
|
|
190
|
+
worktree: context.worktreeRoot,
|
|
191
|
+
}); // implements REQ-opencode-file-context-guidance-v1
|
|
192
|
+
fileOperationStateRegistry.set(key, state);
|
|
193
|
+
}
|
|
194
|
+
return state;
|
|
195
|
+
}
|
|
196
|
+
function getCheckpointRunnerForContext(context) {
|
|
197
|
+
const key = buildStateScopeKey(context, "checkpoint-runner");
|
|
198
|
+
let runner = checkpointRunnerRegistry.get(key);
|
|
199
|
+
if (!runner) {
|
|
200
|
+
runner = new KibiCheckpointRunner({
|
|
201
|
+
config: cfg,
|
|
202
|
+
onRunComplete: (meta) => {
|
|
203
|
+
const normalizedReason = meta.reason.endsWith(".trailing")
|
|
204
|
+
? meta.reason.slice(0, -".trailing".length)
|
|
205
|
+
: meta.reason;
|
|
206
|
+
const isSmartEnforcementSync = normalizedReason.startsWith("smart-enforcement.");
|
|
207
|
+
if (meta.exitCode !== 0 && !isSmartEnforcementSync) {
|
|
208
|
+
latchRuntimeDegraded("scheduler_sync_failed");
|
|
209
|
+
}
|
|
210
|
+
if (meta.checkExitCode !== undefined && meta.checkExitCode !== 0) {
|
|
211
|
+
latchRuntimeDegraded("scheduler_check_failed");
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
checkpointRunnerRegistry.set(key, runner);
|
|
216
|
+
}
|
|
217
|
+
return runner;
|
|
218
|
+
}
|
|
219
|
+
function getPathKindCache(context) {
|
|
220
|
+
const key = buildStateScopeKey(context, "path-kind-cache");
|
|
221
|
+
let scopedCache = pathKindCacheRegistry.get(key);
|
|
222
|
+
if (!scopedCache) {
|
|
223
|
+
scopedCache = new Map();
|
|
224
|
+
pathKindCacheRegistry.set(key, scopedCache);
|
|
225
|
+
}
|
|
226
|
+
return scopedCache;
|
|
227
|
+
}
|
|
228
|
+
function getSchedulerForContext(context) {
|
|
229
|
+
if (!cfg.sync.enabled) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
const worktreeRoot = path.resolve(context.worktreeRoot);
|
|
233
|
+
const existing = schedulerRegistry.get(worktreeRoot);
|
|
234
|
+
if (existing) {
|
|
235
|
+
return existing;
|
|
236
|
+
}
|
|
237
|
+
const schedulerFactory = schedulerFactoryGlobals.__kibi_test_scheduler_factory_by_worktree?.get(worktreeRoot) ??
|
|
238
|
+
schedulerFactoryGlobals.__kibi_test_scheduler_factory ??
|
|
239
|
+
createSyncScheduler;
|
|
240
|
+
try {
|
|
241
|
+
const scopedScheduler = schedulerFactory({
|
|
242
|
+
worktree: worktreeRoot,
|
|
243
|
+
config: cfg,
|
|
244
|
+
onRunComplete: (meta) => {
|
|
245
|
+
const normalizedReason = meta.reason.endsWith(".trailing")
|
|
246
|
+
? meta.reason.slice(0, -".trailing".length)
|
|
247
|
+
: meta.reason;
|
|
248
|
+
const isSmartEnforcementSync = normalizedReason.startsWith("smart-enforcement.");
|
|
249
|
+
if (meta.exitCode !== 0 && !isSmartEnforcementSync) {
|
|
250
|
+
latchRuntimeDegraded("scheduler_sync_failed");
|
|
251
|
+
}
|
|
252
|
+
if (meta.checkExitCode !== undefined && meta.checkExitCode !== 0) {
|
|
253
|
+
latchRuntimeDegraded("scheduler_check_failed");
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
schedulerRegistry.set(worktreeRoot, scopedScheduler);
|
|
258
|
+
return scopedScheduler;
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
latchRuntimeDegraded("scheduler_unavailable");
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function buildScopedCacheKey(context, riskClass, fileBucket, dirtyRelevantInputs) {
|
|
266
|
+
const cacheKey = {
|
|
267
|
+
workspaceRoot: context.worktreeRoot,
|
|
268
|
+
branch: context.branch,
|
|
269
|
+
posture: context.posture,
|
|
270
|
+
riskClass,
|
|
271
|
+
fileBucket,
|
|
272
|
+
};
|
|
273
|
+
if (getEffectiveMode() === "hard") {
|
|
274
|
+
cacheKey.scopeKey = buildEnforcementScopeKey({
|
|
275
|
+
sessionId: context.sessionId,
|
|
276
|
+
agentIdentity: context.agentIdentity,
|
|
277
|
+
worktreeRoot: context.worktreeRoot,
|
|
278
|
+
branch: context.branch,
|
|
279
|
+
dirtyRelevantFingerprint: buildDirtyRelevantFingerprint(dirtyRelevantInputs),
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
return cacheKey;
|
|
283
|
+
}
|
|
284
|
+
const rootWorkContext = resolveScopedWorkContext();
|
|
285
|
+
const sessionEditState = getSessionEditState(rootWorkContext);
|
|
286
|
+
const fileOperationState = getFileOperationState(rootWorkContext);
|
|
287
|
+
const scheduler = getSchedulerForContext(rootWorkContext);
|
|
153
288
|
let degradedWarnedOnce = false;
|
|
154
|
-
const pathKindCache =
|
|
289
|
+
const pathKindCache = getPathKindCache(rootWorkContext);
|
|
155
290
|
// Idle-brief state — dedupe via semantic contentHash (persisted envelope is the delivery authority)
|
|
156
291
|
let idleBriefInFlight = false;
|
|
157
292
|
let idleBriefTrailingRerun = false;
|
|
@@ -177,18 +312,21 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
177
312
|
sessionBaselineCursor = nextState.cursor;
|
|
178
313
|
}
|
|
179
314
|
syncSessionBaseline(currentBranch);
|
|
180
|
-
function normalizeSessionPath(filePath) {
|
|
315
|
+
function normalizeSessionPath(filePath, worktree = input.worktree) {
|
|
181
316
|
if (path.isAbsolute(filePath)) {
|
|
182
|
-
const relativePath = path.relative(
|
|
317
|
+
const relativePath = path.relative(worktree, filePath);
|
|
183
318
|
return relativePath.startsWith("..") ? filePath : relativePath;
|
|
184
319
|
}
|
|
185
320
|
return filePath;
|
|
186
321
|
}
|
|
187
|
-
function resolveWorktreePath(filePath) {
|
|
188
|
-
return
|
|
189
|
-
? path.join(
|
|
322
|
+
function resolveWorktreePath(filePath, worktree = input.worktree) {
|
|
323
|
+
return worktree && !path.isAbsolute(filePath)
|
|
324
|
+
? path.join(worktree, filePath)
|
|
190
325
|
: filePath;
|
|
191
326
|
}
|
|
327
|
+
function buildRiskPathScopeKey(context, filePath) {
|
|
328
|
+
return `${buildStateScopeKey(context, "risk")}:${normalizeSessionPath(filePath, context.worktreeRoot)}`;
|
|
329
|
+
}
|
|
192
330
|
function getKbSnapshotFingerprint(worktree, branch) {
|
|
193
331
|
try {
|
|
194
332
|
const snapshotPath = path.join(worktree, ".kb", "branches", branch, "kb.rdf");
|
|
@@ -243,33 +381,33 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
243
381
|
}
|
|
244
382
|
return normalizeSessionPath(directPath);
|
|
245
383
|
}
|
|
246
|
-
function readFileContent(filePath) {
|
|
384
|
+
function readFileContent(filePath, worktree = input.worktree) {
|
|
247
385
|
try {
|
|
248
|
-
return fs.readFileSync(resolveWorktreePath(filePath), "utf-8");
|
|
386
|
+
return fs.readFileSync(resolveWorktreePath(filePath, worktree), "utf-8");
|
|
249
387
|
}
|
|
250
388
|
catch {
|
|
251
389
|
return "";
|
|
252
390
|
}
|
|
253
391
|
}
|
|
254
|
-
function updateRecentEditsFromSession(sessionEdits) {
|
|
392
|
+
function updateRecentEditsFromSession(sessionEdits, scopedPathKindCache) {
|
|
255
393
|
recentEdits = sessionEdits.slice(-MAX_RECENT_EDITS).map((entry) => ({
|
|
256
394
|
path: entry.filePath,
|
|
257
|
-
kind:
|
|
395
|
+
kind: scopedPathKindCache.get(entry.filePath) ?? "unknown",
|
|
258
396
|
timestamp: entry.lastReconciledAt,
|
|
259
397
|
}));
|
|
260
398
|
return recentEdits;
|
|
261
399
|
}
|
|
262
|
-
function deriveRiskContext(filePath) {
|
|
263
|
-
const normalizedFilePath = normalizeSessionPath(filePath);
|
|
264
|
-
const pathAnalysis = analyzePath(normalizedFilePath,
|
|
265
|
-
|
|
266
|
-
const fileContent = readFileContent(normalizedFilePath);
|
|
400
|
+
function deriveRiskContext(context, filePath, scopedPathKindCache) {
|
|
401
|
+
const normalizedFilePath = normalizeSessionPath(filePath, context.worktreeRoot);
|
|
402
|
+
const pathAnalysis = analyzePath(normalizedFilePath, context.worktreeRoot);
|
|
403
|
+
scopedPathKindCache.set(normalizedFilePath, pathAnalysis.kind);
|
|
404
|
+
const fileContent = readFileContent(normalizedFilePath, context.worktreeRoot);
|
|
267
405
|
const hasMustPriority = pathAnalysis.kind === "requirement"
|
|
268
|
-
? isMustPriorityRequirement(normalizedFilePath,
|
|
406
|
+
? isMustPriorityRequirement(normalizedFilePath, context.worktreeRoot)
|
|
269
407
|
: false;
|
|
270
408
|
let precomputedSuggestion = null;
|
|
271
409
|
if (pathAnalysis.kind === "code" && cfg.guidance.commentDetection.enabled) {
|
|
272
|
-
precomputedSuggestion = analyzeCodeFile(resolveWorktreePath(normalizedFilePath), {
|
|
410
|
+
precomputedSuggestion = analyzeCodeFile(resolveWorktreePath(normalizedFilePath, context.worktreeRoot), {
|
|
273
411
|
minLines: cfg.guidance.commentDetection.minLines,
|
|
274
412
|
});
|
|
275
413
|
}
|
|
@@ -287,6 +425,7 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
287
425
|
pathAnalysis.kind === "code" ? precomputedSuggestion : null;
|
|
288
426
|
lastRiskClass = effectiveRiskClass;
|
|
289
427
|
lastRiskFilePath = normalizedFilePath;
|
|
428
|
+
lastRiskScopeKey = buildRiskPathScopeKey(context, normalizedFilePath);
|
|
290
429
|
return {
|
|
291
430
|
effectiveRiskClass,
|
|
292
431
|
pathAnalysis,
|
|
@@ -294,17 +433,17 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
294
433
|
precomputedSuggestion,
|
|
295
434
|
};
|
|
296
435
|
}
|
|
297
|
-
function buildBriefingWorkspaceContext() {
|
|
436
|
+
function buildBriefingWorkspaceContext(context = rootWorkContext, branch = context.branch) {
|
|
298
437
|
return {
|
|
299
|
-
workspaceRoot:
|
|
300
|
-
branch
|
|
301
|
-
directory:
|
|
438
|
+
workspaceRoot: context.worktreeRoot,
|
|
439
|
+
branch,
|
|
440
|
+
directory: context.worktreeRoot,
|
|
302
441
|
...(input.workspace !== undefined ? { workspace: input.workspace } : {}),
|
|
303
442
|
};
|
|
304
443
|
}
|
|
305
|
-
function buildWorkspaceContextForBranch(branch) {
|
|
444
|
+
function buildWorkspaceContextForBranch(branch, context = rootWorkContext) {
|
|
306
445
|
return {
|
|
307
|
-
...buildBriefingWorkspaceContext(),
|
|
446
|
+
...buildBriefingWorkspaceContext(context),
|
|
308
447
|
branch,
|
|
309
448
|
};
|
|
310
449
|
}
|
|
@@ -312,8 +451,8 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
312
451
|
if (!intentResult.eligible ||
|
|
313
452
|
!input.client ||
|
|
314
453
|
getMaintenanceDegraded() ||
|
|
315
|
-
(posture.state !== "root_active" &&
|
|
316
|
-
posture.state !== "hybrid_root_plus_vendored")) {
|
|
454
|
+
((options.postureState ?? posture.state) !== "root_active" &&
|
|
455
|
+
(options.postureState ?? posture.state) !== "hybrid_root_plus_vendored")) {
|
|
317
456
|
return;
|
|
318
457
|
}
|
|
319
458
|
if (options.skipIfCachedResultExists === true &&
|
|
@@ -322,7 +461,7 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
322
461
|
}
|
|
323
462
|
const client = input.client;
|
|
324
463
|
const fingerprint = intentResult.fingerprint;
|
|
325
|
-
const workspaceCtx = buildBriefingWorkspaceContext();
|
|
464
|
+
const workspaceCtx = options.workspaceCtx ?? buildBriefingWorkspaceContext();
|
|
326
465
|
void fetchBriefingResult(client, workspaceCtx, intentResult).then((result) => {
|
|
327
466
|
autoBriefResults.set(fingerprint, result);
|
|
328
467
|
if (!toastedFingerprints.has(fingerprint)) {
|
|
@@ -336,6 +475,37 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
336
475
|
hooks.event = async ({ event }) => {
|
|
337
476
|
const activeBranch = resolveCurrentBranch(input.worktree);
|
|
338
477
|
syncSessionBaseline(activeBranch);
|
|
478
|
+
// Observe KB tool execution events for freshness evidence (best-effort)
|
|
479
|
+
const TOOL_EVENT_TYPES = new Set([
|
|
480
|
+
"tool.execute.after", "tool.executed", "tool.call.completed",
|
|
481
|
+
"tool.Execute.after", "tool.Call.completed",
|
|
482
|
+
]);
|
|
483
|
+
if (TOOL_EVENT_TYPES.has(event.type)) {
|
|
484
|
+
const props = event.properties ?? {};
|
|
485
|
+
const toolName = (props.tool ?? props.toolName ?? props.name ??
|
|
486
|
+
props.call?.name ??
|
|
487
|
+
props.input?.tool);
|
|
488
|
+
if (typeof toolName === "string" && toolName.startsWith("kb_")) {
|
|
489
|
+
const scope = {
|
|
490
|
+
...(input.sessionId !== undefined ? { sessionId: input.sessionId } : {}),
|
|
491
|
+
agentIdentity: input.agentIdentity ?? "unknown",
|
|
492
|
+
worktree: input.worktree,
|
|
493
|
+
branch: activeBranch,
|
|
494
|
+
fingerprint: `${input.sessionId ?? ""}-${activeBranch}`,
|
|
495
|
+
};
|
|
496
|
+
try {
|
|
497
|
+
freshnessStore.recordToolEvidence(scope, toolName);
|
|
498
|
+
logger.info("kb-freshness.tool-evidence", {
|
|
499
|
+
event: "kb_freshness_tool_evidence",
|
|
500
|
+
tool: toolName,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
catch {
|
|
504
|
+
// best-effort, never crash the event handler
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return; // tool events don't need file lifecycle processing
|
|
508
|
+
}
|
|
339
509
|
// Handle session.idle for idle-brief generation. OpenCode can emit idle
|
|
340
510
|
// while an assistant is between tool calls, so debounce until the work
|
|
341
511
|
// burst settles before generating/delivering a brief.
|
|
@@ -506,71 +676,63 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
506
676
|
.properties.file;
|
|
507
677
|
if (!filePath)
|
|
508
678
|
return;
|
|
679
|
+
const eventContext = resolveScopedWorkContext(filePath);
|
|
680
|
+
const scopedSessionEditState = getSessionEditState(eventContext);
|
|
681
|
+
const scopedFileOperationState = getFileOperationState(eventContext);
|
|
682
|
+
const scopedPathKindCache = getPathKindCache(eventContext);
|
|
683
|
+
const scopedScheduler = getSchedulerForContext(eventContext);
|
|
684
|
+
const normalizedFilePath = normalizeSessionPath(filePath, eventContext.worktreeRoot);
|
|
509
685
|
// Record lifecycle event into file-operation-state // implements REQ-opencode-file-context-guidance-v1
|
|
510
686
|
const lifecycle = event.type === "file.created"
|
|
511
687
|
? "created"
|
|
512
688
|
: event.type === "file.deleted"
|
|
513
689
|
? "deleted"
|
|
514
690
|
: "edited";
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
const pathAnalysis = analyzePath(
|
|
691
|
+
scopedFileOperationState.recordLifecycle(filePath, lifecycle, Date.now());
|
|
692
|
+
scopedFileOperationState.normalizePath(filePath);
|
|
693
|
+
const pathAnalysis = analyzePath(normalizedFilePath, eventContext.worktreeRoot);
|
|
518
694
|
// For file.deleted: derive path kind without reading content, classify for reminder routing only
|
|
519
695
|
if (lifecycle === "deleted") {
|
|
520
696
|
// Preserve last known semantic risk if path was already tracked during session
|
|
521
|
-
const lastKnownKind =
|
|
697
|
+
const lastKnownKind = scopedPathKindCache.get(normalizedFilePath);
|
|
522
698
|
if (lastKnownKind) {
|
|
523
699
|
// Path was tracked — preserve last known semantic risk for reminder routing
|
|
524
|
-
|
|
700
|
+
scopedPathKindCache.set(normalizedFilePath, pathAnalysis.kind);
|
|
525
701
|
}
|
|
526
702
|
else {
|
|
527
703
|
// Not tracked — classify only for reminder routing, not auto-briefing
|
|
528
|
-
|
|
704
|
+
scopedPathKindCache.set(normalizedFilePath, pathAnalysis.kind);
|
|
529
705
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
const sessionEdits =
|
|
533
|
-
|
|
534
|
-
path: e.filePath,
|
|
535
|
-
kind: pathKindCache.get(e.filePath) ?? "unknown",
|
|
536
|
-
timestamp: e.lastReconciledAt,
|
|
537
|
-
}));
|
|
706
|
+
scopedSessionEditState.recordEventHint(normalizedFilePath, pathAnalysis.kind, Date.now());
|
|
707
|
+
scopedSessionEditState.reconcilePath(normalizedFilePath);
|
|
708
|
+
const sessionEdits = scopedSessionEditState.getSessionEdits();
|
|
709
|
+
updateRecentEditsFromSession(sessionEdits, scopedPathKindCache);
|
|
538
710
|
// Schedule background sync for deleted files that pass shouldHandleFile // implements REQ-opencode-file-context-guidance-v1
|
|
539
711
|
if (cfg.sync.enabled &&
|
|
540
|
-
|
|
541
|
-
fileFilter.shouldHandleFile(
|
|
542
|
-
|
|
712
|
+
scopedScheduler &&
|
|
713
|
+
fileFilter.shouldHandleFile(normalizedFilePath, eventContext.worktreeRoot)) {
|
|
714
|
+
scopedScheduler.scheduleSync("file.deleted", normalizedFilePath);
|
|
543
715
|
}
|
|
544
716
|
return;
|
|
545
717
|
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
const sessionEdits =
|
|
550
|
-
const focusEdit =
|
|
718
|
+
scopedSessionEditState.recordEventHint(normalizedFilePath, pathAnalysis.kind, Date.now());
|
|
719
|
+
scopedSessionEditState.reconcilePath(normalizedFilePath);
|
|
720
|
+
scopedPathKindCache.set(normalizedFilePath, pathAnalysis.kind);
|
|
721
|
+
const sessionEdits = scopedSessionEditState.getSessionEdits();
|
|
722
|
+
const focusEdit = scopedSessionEditState.getFocusEdit();
|
|
551
723
|
// Schedule background sync for file.created/file.edited that pass shouldHandleFile // implements REQ-opencode-file-context-guidance-v1
|
|
552
724
|
if (cfg.sync.enabled &&
|
|
553
|
-
|
|
554
|
-
fileFilter.shouldHandleFile(
|
|
555
|
-
|
|
556
|
-
}
|
|
557
|
-
let fileContent = "";
|
|
558
|
-
try {
|
|
559
|
-
const resolvedPath = input.worktree && !path.isAbsolute(filePath)
|
|
560
|
-
? path.join(input.worktree, filePath)
|
|
561
|
-
: filePath;
|
|
562
|
-
fileContent = fs.readFileSync(resolvedPath, "utf-8");
|
|
725
|
+
scopedScheduler &&
|
|
726
|
+
fileFilter.shouldHandleFile(normalizedFilePath, eventContext.worktreeRoot)) {
|
|
727
|
+
scopedScheduler.scheduleSync(lifecycle === "created" ? "file.created" : "file.edited", normalizedFilePath);
|
|
563
728
|
}
|
|
564
|
-
|
|
729
|
+
const fileContent = readFileContent(normalizedFilePath, eventContext.worktreeRoot);
|
|
565
730
|
const hasMustPriority = pathAnalysis.kind === "requirement"
|
|
566
|
-
? isMustPriorityRequirement(
|
|
731
|
+
? isMustPriorityRequirement(normalizedFilePath, eventContext.worktreeRoot)
|
|
567
732
|
: false;
|
|
568
733
|
let precomputedSuggestion = null;
|
|
569
734
|
if (pathAnalysis.kind === "code" && cfg.guidance.commentDetection.enabled) {
|
|
570
|
-
|
|
571
|
-
? path.join(input.worktree, filePath)
|
|
572
|
-
: filePath;
|
|
573
|
-
precomputedSuggestion = analyzeCodeFile(resolvedPath, {
|
|
735
|
+
precomputedSuggestion = analyzeCodeFile(resolveWorktreePath(normalizedFilePath, eventContext.worktreeRoot), {
|
|
574
736
|
minLines: cfg.guidance.commentDetection.minLines,
|
|
575
737
|
});
|
|
576
738
|
}
|
|
@@ -587,18 +749,20 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
587
749
|
const isAutoBriefRisk = effectiveRiskClass === "behavior_candidate" ||
|
|
588
750
|
effectiveRiskClass === "traceability_candidate";
|
|
589
751
|
lastRiskClass = effectiveRiskClass;
|
|
752
|
+
lastRiskFilePath = normalizedFilePath;
|
|
753
|
+
lastRiskScopeKey = buildRiskPathScopeKey(eventContext, normalizedFilePath);
|
|
590
754
|
logger.info("smart-enforcement.risk", {
|
|
591
755
|
event: "smart_enforcement_risk",
|
|
592
|
-
file:
|
|
756
|
+
file: normalizedFilePath,
|
|
593
757
|
path_kind: pathAnalysis.kind,
|
|
594
758
|
risk_class: effectiveRiskClass,
|
|
595
|
-
posture_state: posture
|
|
759
|
+
posture_state: eventContext.posture,
|
|
596
760
|
maintenance_state: getMaintenanceDegraded()
|
|
597
761
|
? "maintenance_degraded"
|
|
598
762
|
: "maintenance_available",
|
|
599
763
|
under_kb: pathAnalysis.isUnderKb,
|
|
600
764
|
has_must_priority: hasMustPriority,
|
|
601
|
-
posture: posture
|
|
765
|
+
posture: eventContext.posture,
|
|
602
766
|
reason_code: effectiveRiskClass,
|
|
603
767
|
effective_mode: getEffectiveMode(),
|
|
604
768
|
static_degraded: posture.maintenanceDegraded,
|
|
@@ -613,13 +777,13 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
613
777
|
runtimeOverlay.primaryCause === "scheduler_check_failed";
|
|
614
778
|
if (!targetedChecksBlocked &&
|
|
615
779
|
cfg.sync.enabled &&
|
|
616
|
-
|
|
780
|
+
scopedScheduler &&
|
|
617
781
|
cfg.guidance.targetedChecks.enabled) {
|
|
618
782
|
const traceabilityRules = effectiveRiskClass === "traceability_candidate"
|
|
619
783
|
? ["symbol-traceability"]
|
|
620
784
|
: null;
|
|
621
785
|
const kbStructuralRules = effectiveRiskClass === "kb_doc_structural" &&
|
|
622
|
-
fileFilter.shouldHandleFile(
|
|
786
|
+
fileFilter.shouldHandleFile(normalizedFilePath, eventContext.worktreeRoot)
|
|
623
787
|
? [
|
|
624
788
|
"required-fields",
|
|
625
789
|
"no-dangling-refs",
|
|
@@ -633,10 +797,10 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
633
797
|
if (checkRules) {
|
|
634
798
|
logger.info("smart-enforcement.targeted-checks", {
|
|
635
799
|
event: "smart_enforcement_targeted_checks",
|
|
636
|
-
file:
|
|
800
|
+
file: normalizedFilePath,
|
|
637
801
|
risk_class: effectiveRiskClass,
|
|
638
|
-
posture: posture
|
|
639
|
-
posture_state: posture
|
|
802
|
+
posture: eventContext.posture,
|
|
803
|
+
posture_state: eventContext.posture,
|
|
640
804
|
guidance_action: "targeted_checks",
|
|
641
805
|
effective_mode: getEffectiveMode(),
|
|
642
806
|
rules: checkRules,
|
|
@@ -645,43 +809,33 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
645
809
|
merged_degraded: getMaintenanceDegraded(),
|
|
646
810
|
overlay_cause: runtimeOverlay.primaryCause ?? null,
|
|
647
811
|
});
|
|
648
|
-
logger.info(`kibi-opencode: scheduling sync for ${
|
|
649
|
-
|
|
812
|
+
logger.info(`kibi-opencode: scheduling sync for ${normalizedFilePath}`);
|
|
813
|
+
scopedScheduler.scheduleSync(effectiveRiskClass === "traceability_candidate"
|
|
650
814
|
? "smart-enforcement.traceability"
|
|
651
|
-
: "smart-enforcement.kb-doc",
|
|
815
|
+
: "smart-enforcement.kb-doc", normalizedFilePath, checkRules);
|
|
652
816
|
}
|
|
653
817
|
}
|
|
654
|
-
|
|
655
|
-
path: e.filePath,
|
|
656
|
-
kind: pathKindCache.get(e.filePath) ?? "unknown",
|
|
657
|
-
timestamp: e.lastReconciledAt,
|
|
658
|
-
}));
|
|
818
|
+
updateRecentEditsFromSession(sessionEdits, scopedPathKindCache);
|
|
659
819
|
if (effectiveRiskClass === "safe_docs_only" ||
|
|
660
820
|
effectiveRiskClass === "safe_test_only") {
|
|
661
821
|
recentCommentSuggestion = null;
|
|
662
822
|
return;
|
|
663
823
|
}
|
|
664
|
-
const cacheKey =
|
|
665
|
-
workspaceRoot: input.worktree,
|
|
666
|
-
branch: currentBranch,
|
|
667
|
-
posture: posture.state,
|
|
668
|
-
riskClass: effectiveRiskClass,
|
|
669
|
-
fileBucket: deriveFileBucket(pathAnalysis.kind),
|
|
670
|
-
};
|
|
824
|
+
const cacheKey = buildScopedCacheKey(eventContext, effectiveRiskClass, deriveFileBucket(pathAnalysis.kind), [normalizedFilePath, pathAnalysis.kind, effectiveRiskClass]);
|
|
671
825
|
// Always process manual_kb_edit before cache check — this is a critical safety signal
|
|
672
826
|
if (effectiveRiskClass === "manual_kb_edit") {
|
|
673
827
|
hasRecentKbEdit = true;
|
|
674
828
|
if (cfg.guidance.warnOnKbEdits) {
|
|
675
|
-
logger.warn(`kibi-opencode: .kb edit detected for ${
|
|
676
|
-
getSessionTracker().recordWarning("kb-edit",
|
|
829
|
+
logger.warn(`kibi-opencode: .kb edit detected for ${normalizedFilePath}`);
|
|
830
|
+
getSessionTracker().recordWarning("kb-edit", normalizedFilePath, `Manual .kb edit: ${normalizedFilePath}`);
|
|
677
831
|
}
|
|
678
832
|
return;
|
|
679
833
|
}
|
|
680
834
|
// Always emit requirement lint warnings before cache check — these are safety signals
|
|
681
835
|
if (effectiveRiskClass === "req_policy_candidate") {
|
|
682
|
-
const lintWarnings = lintRequirementDoc(
|
|
836
|
+
const lintWarnings = lintRequirementDoc(normalizedFilePath, eventContext.worktreeRoot);
|
|
683
837
|
for (const warning of lintWarnings) {
|
|
684
|
-
getSessionTracker().recordWarning(warning.category,
|
|
838
|
+
getSessionTracker().recordWarning(warning.category, normalizedFilePath, warning.message);
|
|
685
839
|
}
|
|
686
840
|
}
|
|
687
841
|
// Cache check: after critical signals have been emitted
|
|
@@ -690,10 +844,10 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
690
844
|
event: "smart_enforcement_cache",
|
|
691
845
|
cache_hit: true,
|
|
692
846
|
cache_state: "hit",
|
|
693
|
-
file:
|
|
847
|
+
file: normalizedFilePath,
|
|
694
848
|
risk_class: effectiveRiskClass,
|
|
695
|
-
posture: posture
|
|
696
|
-
posture_state: posture
|
|
849
|
+
posture: eventContext.posture,
|
|
850
|
+
posture_state: eventContext.posture,
|
|
697
851
|
});
|
|
698
852
|
if (!isAutoBriefRisk) {
|
|
699
853
|
return;
|
|
@@ -703,10 +857,10 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
703
857
|
event: "smart_enforcement_cache",
|
|
704
858
|
cache_hit: false,
|
|
705
859
|
cache_state: "miss",
|
|
706
|
-
file:
|
|
860
|
+
file: normalizedFilePath,
|
|
707
861
|
risk_class: effectiveRiskClass,
|
|
708
|
-
posture: posture
|
|
709
|
-
posture_state: posture
|
|
862
|
+
posture: eventContext.posture,
|
|
863
|
+
posture_state: eventContext.posture,
|
|
710
864
|
});
|
|
711
865
|
if (effectiveRiskClass === "req_policy_candidate") {
|
|
712
866
|
if (getMaintenanceDegraded()) {
|
|
@@ -715,10 +869,10 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
715
869
|
: logger.info;
|
|
716
870
|
logFn("smart-enforcement.degraded", {
|
|
717
871
|
event: "smart_enforcement_degraded",
|
|
718
|
-
file:
|
|
872
|
+
file: normalizedFilePath,
|
|
719
873
|
risk_class: effectiveRiskClass,
|
|
720
|
-
posture: posture
|
|
721
|
-
posture_state: posture
|
|
874
|
+
posture: eventContext.posture,
|
|
875
|
+
posture_state: eventContext.posture,
|
|
722
876
|
maintenance_state: getMaintenanceDegraded()
|
|
723
877
|
? "maintenance_degraded"
|
|
724
878
|
: "maintenance_available",
|
|
@@ -733,8 +887,8 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
733
887
|
}
|
|
734
888
|
if (!getMaintenanceDegraded() &&
|
|
735
889
|
cfg.sync.enabled &&
|
|
736
|
-
|
|
737
|
-
fileFilter.shouldHandleFile(
|
|
890
|
+
scopedScheduler &&
|
|
891
|
+
fileFilter.shouldHandleFile(normalizedFilePath, eventContext.worktreeRoot)) {
|
|
738
892
|
let checkRules;
|
|
739
893
|
if (cfg.guidance.targetedChecks.enabled) {
|
|
740
894
|
if (hasMustPriority && getEffectiveMode() === "strict") {
|
|
@@ -744,7 +898,7 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
744
898
|
"must-priority-coverage",
|
|
745
899
|
"strict-req-fact-pairing",
|
|
746
900
|
];
|
|
747
|
-
logger.info(`kibi-opencode: must-priority requirement detected, scheduling elevated checks for ${
|
|
901
|
+
logger.info(`kibi-opencode: must-priority requirement detected, scheduling elevated checks for ${normalizedFilePath}`);
|
|
748
902
|
}
|
|
749
903
|
else {
|
|
750
904
|
checkRules = [
|
|
@@ -756,10 +910,10 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
756
910
|
}
|
|
757
911
|
logger.info("smart-enforcement.targeted-checks", {
|
|
758
912
|
event: "smart_enforcement_targeted_checks",
|
|
759
|
-
file:
|
|
913
|
+
file: normalizedFilePath,
|
|
760
914
|
risk_class: effectiveRiskClass,
|
|
761
|
-
posture: posture
|
|
762
|
-
posture_state: posture
|
|
915
|
+
posture: eventContext.posture,
|
|
916
|
+
posture_state: eventContext.posture,
|
|
763
917
|
guidance_action: "targeted_checks",
|
|
764
918
|
effective_mode: getEffectiveMode(),
|
|
765
919
|
rules: checkRules ?? [],
|
|
@@ -768,7 +922,7 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
768
922
|
merged_degraded: getMaintenanceDegraded(),
|
|
769
923
|
overlay_cause: runtimeOverlay.primaryCause ?? null,
|
|
770
924
|
});
|
|
771
|
-
|
|
925
|
+
scopedScheduler.scheduleSync("file.edited", normalizedFilePath, checkRules);
|
|
772
926
|
}
|
|
773
927
|
return;
|
|
774
928
|
}
|
|
@@ -779,10 +933,10 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
779
933
|
: logger.info;
|
|
780
934
|
logFn("smart-enforcement.degraded", {
|
|
781
935
|
event: "smart_enforcement_degraded",
|
|
782
|
-
file:
|
|
936
|
+
file: normalizedFilePath,
|
|
783
937
|
risk_class: effectiveRiskClass,
|
|
784
|
-
posture: posture
|
|
785
|
-
posture_state: posture
|
|
938
|
+
posture: eventContext.posture,
|
|
939
|
+
posture_state: eventContext.posture,
|
|
786
940
|
maintenance_state: getMaintenanceDegraded()
|
|
787
941
|
? "maintenance_degraded"
|
|
788
942
|
: "maintenance_available",
|
|
@@ -803,7 +957,7 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
803
957
|
const suggestion = precomputedSuggestion;
|
|
804
958
|
if (suggestion) {
|
|
805
959
|
recentCommentSuggestion = suggestion;
|
|
806
|
-
const dedupeKey = `${
|
|
960
|
+
const dedupeKey = `${buildRiskPathScopeKey(eventContext, normalizedFilePath)}:${suggestion.suggestionType}:${suggestion.fingerprint}`;
|
|
807
961
|
if (!seenFingerprints.has(dedupeKey)) {
|
|
808
962
|
seenFingerprints.add(dedupeKey);
|
|
809
963
|
const warningCategory = suggestion.suggestionType === "fact"
|
|
@@ -811,8 +965,8 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
811
965
|
: suggestion.suggestionType === "adr"
|
|
812
966
|
? "long-comment-missed-adr"
|
|
813
967
|
: "missing-traceability";
|
|
814
|
-
logger.warn(`kibi-opencode: detected durable ${suggestion.suggestionType} knowledge in ${
|
|
815
|
-
getSessionTracker().recordWarning(warningCategory,
|
|
968
|
+
logger.warn(`kibi-opencode: detected durable ${suggestion.suggestionType} knowledge in ${normalizedFilePath}`);
|
|
969
|
+
getSessionTracker().recordWarning(warningCategory, normalizedFilePath, `Consider routing this ${suggestion.suggestionType} knowledge to Kibi instead of inline comments: ${suggestion.reasoning}`);
|
|
816
970
|
}
|
|
817
971
|
}
|
|
818
972
|
else {
|
|
@@ -827,16 +981,20 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
827
981
|
return;
|
|
828
982
|
}
|
|
829
983
|
const sessionSourceFiles = sessionEdits.map((e) => e.filePath);
|
|
984
|
+
const briefingContext = resolveScopedWorkContext(focusEdit.filePath);
|
|
830
985
|
const intentResult = computeBriefIntent({
|
|
831
986
|
riskClass: effectiveRiskClass,
|
|
832
|
-
posture: posture
|
|
987
|
+
posture: briefingContext.posture,
|
|
833
988
|
maintenanceDegraded: getMaintenanceDegraded(),
|
|
834
989
|
sourceFiles: sessionSourceFiles,
|
|
835
990
|
focusFilePath: focusEdit.filePath,
|
|
836
|
-
worktreeRoot:
|
|
837
|
-
branch:
|
|
991
|
+
worktreeRoot: briefingContext.worktreeRoot,
|
|
992
|
+
branch: briefingContext.branch,
|
|
993
|
+
});
|
|
994
|
+
queueBriefingFetch(intentResult, {
|
|
995
|
+
workspaceCtx: buildBriefingWorkspaceContext(briefingContext),
|
|
996
|
+
postureState: briefingContext.posture,
|
|
838
997
|
});
|
|
839
|
-
queueBriefingFetch(intentResult);
|
|
840
998
|
}
|
|
841
999
|
return;
|
|
842
1000
|
};
|
|
@@ -853,31 +1011,39 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
853
1011
|
cfg.guidance.smartEnforcement.degradedMode === "warn-once" &&
|
|
854
1012
|
!degradedWarnedOnce;
|
|
855
1013
|
const transformFocusFilePath = getTransformFocusFilePath(transformInput);
|
|
856
|
-
|
|
1014
|
+
const promptWorkContext = resolveScopedWorkContext(transformFocusFilePath ?? undefined);
|
|
1015
|
+
const promptSessionEditState = getSessionEditState(promptWorkContext);
|
|
1016
|
+
const promptFileOperationState = getFileOperationState(promptWorkContext);
|
|
1017
|
+
const promptPathKindCache = getPathKindCache(promptWorkContext);
|
|
1018
|
+
promptSessionEditState.reconcileKnownPaths();
|
|
857
1019
|
if (transformFocusFilePath) {
|
|
858
|
-
|
|
1020
|
+
promptSessionEditState.forceEdit(normalizeSessionPath(transformFocusFilePath, promptWorkContext.worktreeRoot));
|
|
859
1021
|
}
|
|
860
|
-
const transformSessionEdits =
|
|
861
|
-
const transformFocusEdit =
|
|
1022
|
+
const transformSessionEdits = promptSessionEditState.getSessionEdits();
|
|
1023
|
+
const transformFocusEdit = promptSessionEditState.getFocusEdit();
|
|
862
1024
|
const transformRecentEdits = transformSessionEdits
|
|
863
1025
|
.slice(-MAX_RECENT_EDITS)
|
|
864
1026
|
.map((e) => ({
|
|
865
1027
|
path: e.filePath,
|
|
866
|
-
kind:
|
|
1028
|
+
kind: promptPathKindCache.get(e.filePath) ?? "unknown",
|
|
867
1029
|
}));
|
|
868
1030
|
const transformPromptFocusEdit = transformFocusEdit
|
|
869
1031
|
? {
|
|
870
1032
|
path: transformFocusEdit.filePath,
|
|
871
|
-
kind:
|
|
1033
|
+
kind: promptPathKindCache.get(transformFocusEdit.filePath) ??
|
|
1034
|
+
"unknown",
|
|
872
1035
|
}
|
|
873
1036
|
: null;
|
|
874
1037
|
const riskContextFilePath = transformFocusEdit?.filePath ?? transformFocusFilePath;
|
|
875
|
-
|
|
1038
|
+
const riskScopeKey = riskContextFilePath
|
|
1039
|
+
? buildRiskPathScopeKey(promptWorkContext, riskContextFilePath)
|
|
1040
|
+
: null;
|
|
1041
|
+
let effectiveRiskClass = riskScopeKey !== null && lastRiskScopeKey === riskScopeKey
|
|
876
1042
|
? lastRiskClass
|
|
877
1043
|
: null;
|
|
878
1044
|
if (riskContextFilePath &&
|
|
879
|
-
(lastRiskClass === null ||
|
|
880
|
-
const riskCtx = deriveRiskContext(riskContextFilePath);
|
|
1045
|
+
(lastRiskClass === null || lastRiskScopeKey !== riskScopeKey)) {
|
|
1046
|
+
const riskCtx = deriveRiskContext(promptWorkContext, riskContextFilePath, promptPathKindCache);
|
|
881
1047
|
effectiveRiskClass = riskCtx.effectiveRiskClass;
|
|
882
1048
|
if (!recentCommentSuggestion && riskCtx.precomputedSuggestion) {
|
|
883
1049
|
recentCommentSuggestion = riskCtx.precomputedSuggestion;
|
|
@@ -891,11 +1057,11 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
891
1057
|
const intentResult = effectiveRiskClass
|
|
892
1058
|
? computeBriefIntent({
|
|
893
1059
|
riskClass: effectiveRiskClass,
|
|
894
|
-
posture: posture
|
|
1060
|
+
posture: promptWorkContext.posture,
|
|
895
1061
|
maintenanceDegraded,
|
|
896
1062
|
sourceFiles: promptSourceFiles,
|
|
897
|
-
worktreeRoot:
|
|
898
|
-
branch:
|
|
1063
|
+
worktreeRoot: promptWorkContext.worktreeRoot,
|
|
1064
|
+
branch: promptWorkContext.branch,
|
|
899
1065
|
...(promptFocusFilePath !== undefined
|
|
900
1066
|
? {
|
|
901
1067
|
focusFilePath: promptFocusFilePath,
|
|
@@ -909,7 +1075,11 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
909
1075
|
const isAutoBriefRisk = effectiveRiskClass === "behavior_candidate" ||
|
|
910
1076
|
effectiveRiskClass === "traceability_candidate";
|
|
911
1077
|
if (!autoBriefResult && isAutoBriefRisk && intentResult) {
|
|
912
|
-
queueBriefingFetch(intentResult, {
|
|
1078
|
+
queueBriefingFetch(intentResult, {
|
|
1079
|
+
skipIfCachedResultExists: true,
|
|
1080
|
+
workspaceCtx: buildBriefingWorkspaceContext(promptWorkContext),
|
|
1081
|
+
postureState: promptWorkContext.posture,
|
|
1082
|
+
});
|
|
913
1083
|
}
|
|
914
1084
|
// Replay latest unread idle brief if available // implements REQ-opencode-kibi-briefing-v4
|
|
915
1085
|
if (input.worktree && currentBranch && input.client) {
|
|
@@ -936,10 +1106,14 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
936
1106
|
}
|
|
937
1107
|
// Steps 3-4: File-operation reminder selection with suppression // implements REQ-opencode-file-context-guidance-v1
|
|
938
1108
|
let fileOperationReminder;
|
|
1109
|
+
let hardGateBlock;
|
|
1110
|
+
let hardGateConsumedPath;
|
|
1111
|
+
let hardGateFingerprint;
|
|
1112
|
+
let hardGateReminderKindsToMark = [];
|
|
939
1113
|
const focusPathForReminder = transformFocusFilePath ?? promptFocusFilePath;
|
|
940
1114
|
if (focusPathForReminder) {
|
|
941
|
-
const normalizedFocusPath =
|
|
942
|
-
const pendingLifecycle =
|
|
1115
|
+
const normalizedFocusPath = promptFileOperationState.normalizePath(focusPathForReminder);
|
|
1116
|
+
const pendingLifecycle = promptFileOperationState.peekPending(normalizedFocusPath);
|
|
943
1117
|
if (pendingLifecycle) {
|
|
944
1118
|
// Check if any reminder kind for this lifecycle has not yet been shown
|
|
945
1119
|
const reminderKindsForLifecycle = pendingLifecycle.lifecycle === "deleted"
|
|
@@ -947,12 +1121,41 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
947
1121
|
: pendingLifecycle.lifecycle === "created"
|
|
948
1122
|
? ["kibi_write", "e2e_write"]
|
|
949
1123
|
: ["e2e_write"];
|
|
950
|
-
const hasUnshownReminder = reminderKindsForLifecycle.some((kind) => !
|
|
1124
|
+
const hasUnshownReminder = reminderKindsForLifecycle.some((kind) => !promptFileOperationState.hasShown(normalizedFocusPath, kind));
|
|
951
1125
|
if (hasUnshownReminder) {
|
|
952
1126
|
// Resolve linked entities and e2e signal
|
|
953
|
-
const linkedEntityResult = getFileLinkedEntityIds(
|
|
954
|
-
const e2eSignal = getE2eCoverageSignal(
|
|
955
|
-
const focusPathKind =
|
|
1127
|
+
const linkedEntityResult = getFileLinkedEntityIds(promptWorkContext.worktreeRoot, focusPathForReminder);
|
|
1128
|
+
const e2eSignal = getE2eCoverageSignal(promptWorkContext.worktreeRoot, focusPathForReminder);
|
|
1129
|
+
const focusPathKind = promptPathKindCache.get(normalizedFocusPath) ?? "unknown";
|
|
1130
|
+
const effectiveMode = getEffectiveMode();
|
|
1131
|
+
let checkpointEvidence = false;
|
|
1132
|
+
let checkpointRunner = null;
|
|
1133
|
+
let checkpointContext = null;
|
|
1134
|
+
const checkpointFingerprint = buildDirtyRelevantFingerprint([
|
|
1135
|
+
normalizedFocusPath,
|
|
1136
|
+
pendingLifecycle.lifecycle,
|
|
1137
|
+
focusPathKind,
|
|
1138
|
+
effectiveRiskClass ?? "safe_docs_only",
|
|
1139
|
+
]);
|
|
1140
|
+
if (effectiveMode === "hard" && promptWorkContext.isAuthoritative) {
|
|
1141
|
+
checkpointRunner = getCheckpointRunnerForContext(promptWorkContext);
|
|
1142
|
+
checkpointContext = {
|
|
1143
|
+
workContext: promptWorkContext,
|
|
1144
|
+
config: cfg,
|
|
1145
|
+
filePath: normalizedFocusPath,
|
|
1146
|
+
maintenanceDegraded,
|
|
1147
|
+
lifecycleEvents: [
|
|
1148
|
+
{
|
|
1149
|
+
normalizedPath: normalizedFocusPath,
|
|
1150
|
+
lifecycle: pendingLifecycle.lifecycle,
|
|
1151
|
+
},
|
|
1152
|
+
],
|
|
1153
|
+
pathKinds: [focusPathKind],
|
|
1154
|
+
linkedEntityResults: [linkedEntityResult],
|
|
1155
|
+
e2eSignals: [e2eSignal],
|
|
1156
|
+
};
|
|
1157
|
+
checkpointEvidence = checkpointRunner.isCheckpointPassed(checkpointFingerprint, checkpointContext);
|
|
1158
|
+
}
|
|
956
1159
|
const reminderResult = deriveFileOperationReminder({
|
|
957
1160
|
normalizedPath: normalizedFocusPath,
|
|
958
1161
|
lifecycle: pendingLifecycle.lifecycle,
|
|
@@ -960,26 +1163,98 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
960
1163
|
linkedEntityResult,
|
|
961
1164
|
e2eSignal,
|
|
962
1165
|
currentSemanticRisk: effectiveRiskClass ?? "safe_docs_only",
|
|
963
|
-
posture: posture
|
|
1166
|
+
posture: promptWorkContext.posture,
|
|
1167
|
+
effectiveMode,
|
|
1168
|
+
resolvedContext: promptWorkContext,
|
|
1169
|
+
checkpointEvidence,
|
|
964
1170
|
});
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1171
|
+
if (reminderResult.policyDecision === "hard_block") {
|
|
1172
|
+
const policyResult = reminderResult.policyResult;
|
|
1173
|
+
hardGateBlock = {
|
|
1174
|
+
shownPaths: "shownPaths" in policyResult ? policyResult.shownPaths : [normalizedFocusPath],
|
|
1175
|
+
remainingCount: "remainingCount" in policyResult ? policyResult.remainingCount : 0,
|
|
1176
|
+
reason: "checkpoint_required",
|
|
1177
|
+
};
|
|
1178
|
+
hardGateConsumedPath = normalizedFocusPath;
|
|
1179
|
+
hardGateFingerprint = checkpointFingerprint;
|
|
1180
|
+
hardGateReminderKindsToMark = reminderResult.reminderKindsToMark;
|
|
1181
|
+
if (checkpointRunner && checkpointContext) {
|
|
1182
|
+
const checkpointContextWithGuidance = {
|
|
1183
|
+
...checkpointContext,
|
|
1184
|
+
hardGuidanceText: reminderResult.lifecycleReminder,
|
|
1185
|
+
};
|
|
1186
|
+
const request = checkpointRunner.requestCheckpoint(checkpointContextWithGuidance, checkpointFingerprint);
|
|
1187
|
+
if (request.kind === "requested") {
|
|
1188
|
+
void checkpointRunner
|
|
1189
|
+
.runCheckpoint(checkpointContextWithGuidance, checkpointFingerprint)
|
|
1190
|
+
.then((result) => {
|
|
1191
|
+
logger.info("smart-enforcement.checkpoint", {
|
|
1192
|
+
event: "smart_enforcement_checkpoint",
|
|
1193
|
+
fingerprint: checkpointFingerprint,
|
|
1194
|
+
result: result.kind,
|
|
1195
|
+
reason: "reason" in result.metadata
|
|
1196
|
+
? result.metadata.reason
|
|
1197
|
+
: undefined,
|
|
1198
|
+
});
|
|
1199
|
+
})
|
|
1200
|
+
.catch((error) => {
|
|
1201
|
+
logger.errorStructuredOnly("smart-enforcement.checkpoint-failed", {
|
|
1202
|
+
event: "smart_enforcement_checkpoint_failed",
|
|
1203
|
+
fingerprint: checkpointFingerprint,
|
|
1204
|
+
error: error instanceof Error
|
|
1205
|
+
? error.message
|
|
1206
|
+
: String(error),
|
|
1207
|
+
});
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
else {
|
|
1213
|
+
fileOperationReminder = {
|
|
1214
|
+
path: normalizedFocusPath,
|
|
1215
|
+
lifecycleReminder: reminderResult.lifecycleReminder,
|
|
1216
|
+
e2eReminder: reminderResult.e2eReminder,
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
970
1219
|
}
|
|
971
1220
|
}
|
|
972
1221
|
}
|
|
1222
|
+
// Build freshness evidence for the current dirty fingerprint
|
|
1223
|
+
let freshnessEval;
|
|
1224
|
+
const freshnessFingerprint = `${promptWorkContext.sessionId ?? ""}-${promptWorkContext.branch}`;
|
|
1225
|
+
const freshnessScope = {
|
|
1226
|
+
...(promptWorkContext.sessionId !== undefined ? { sessionId: promptWorkContext.sessionId } : {}),
|
|
1227
|
+
agentIdentity: promptWorkContext.agentIdentity,
|
|
1228
|
+
worktree: promptWorkContext.worktreeRoot,
|
|
1229
|
+
branch: promptWorkContext.branch,
|
|
1230
|
+
fingerprint: freshnessFingerprint,
|
|
1231
|
+
};
|
|
1232
|
+
if (transformFocusFilePath) {
|
|
1233
|
+
const normalizedFocusPathForFreshness = promptFileOperationState.normalizePath(transformFocusFilePath);
|
|
1234
|
+
const focusPathKindForFreshness = promptPathKindCache.get(normalizedFocusPathForFreshness) ?? "unknown";
|
|
1235
|
+
const pendingLifecycleForFreshness = promptFileOperationState.peekPending(normalizedFocusPathForFreshness);
|
|
1236
|
+
const effectiveRiskClassForFreshness = effectiveRiskClass ?? "safe_docs_only";
|
|
1237
|
+
const meaningfulChange = classifyMeaningfulChange({
|
|
1238
|
+
normalizedPath: normalizedFocusPathForFreshness,
|
|
1239
|
+
pathKind: focusPathKindForFreshness,
|
|
1240
|
+
lifecycle: pendingLifecycleForFreshness?.lifecycle ?? "edited",
|
|
1241
|
+
riskClass: effectiveRiskClassForFreshness,
|
|
1242
|
+
});
|
|
1243
|
+
if (meaningfulChange === "requires-kb-evidence") {
|
|
1244
|
+
const freshnessEvidence = freshnessStore.getEvidence(freshnessScope, [normalizedFocusPathForFreshness]);
|
|
1245
|
+
freshnessEval = evaluateKbFreshness(freshnessEvidence);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
973
1248
|
const guidance = buildPrompt({
|
|
974
1249
|
recentEdits: transformRecentEdits,
|
|
975
1250
|
focusEdit: transformPromptFocusEdit,
|
|
976
1251
|
workspaceHealth,
|
|
977
1252
|
hasRecentKbEdit,
|
|
978
1253
|
recentCommentSuggestion,
|
|
979
|
-
posture: posture
|
|
1254
|
+
posture: promptWorkContext.posture,
|
|
980
1255
|
cache,
|
|
981
|
-
workspaceRoot:
|
|
982
|
-
branch:
|
|
1256
|
+
workspaceRoot: promptWorkContext.worktreeRoot,
|
|
1257
|
+
branch: promptWorkContext.branch,
|
|
983
1258
|
completionReminder: cfg.guidance.smartEnforcement.completionReminder,
|
|
984
1259
|
maintenanceDegraded,
|
|
985
1260
|
degradedMode: cfg.guidance.smartEnforcement.degradedMode,
|
|
@@ -991,12 +1266,17 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
991
1266
|
...(fileOperationReminder !== undefined
|
|
992
1267
|
? { fileOperationReminder }
|
|
993
1268
|
: {}),
|
|
1269
|
+
...(hardGateBlock !== undefined ? { hardGateBlock } : {}),
|
|
1270
|
+
...(freshnessEval ? { kbFreshness: freshnessEval } : {}),
|
|
1271
|
+
...(freshnessEval && transformSessionEdits.length > 0
|
|
1272
|
+
? { freshnessChangedPaths: [...new Set(transformSessionEdits.map(e => e.filePath))].slice(0, 5) }
|
|
1273
|
+
: {}),
|
|
994
1274
|
});
|
|
995
1275
|
logger.info("smart-enforcement.guidance", {
|
|
996
1276
|
event: "smart_enforcement_guidance",
|
|
997
1277
|
emitted: guidance.trim() !== "" && guidance.trim() !== SENTINEL,
|
|
998
|
-
posture: posture
|
|
999
|
-
posture_state: posture
|
|
1278
|
+
posture: promptWorkContext.posture,
|
|
1279
|
+
posture_state: promptWorkContext.posture,
|
|
1000
1280
|
guidance_action: guidance.trim() !== "" && guidance.trim() !== SENTINEL
|
|
1001
1281
|
? "emit"
|
|
1002
1282
|
: "skip",
|
|
@@ -1015,8 +1295,8 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
1015
1295
|
logger.info("smart-enforcement.completion-reminder", {
|
|
1016
1296
|
event: "smart_enforcement_completion_reminder",
|
|
1017
1297
|
risk_class: lastRiskClass,
|
|
1018
|
-
posture: posture
|
|
1019
|
-
posture_state: posture
|
|
1298
|
+
posture: promptWorkContext.posture,
|
|
1299
|
+
posture_state: promptWorkContext.posture,
|
|
1020
1300
|
guidance_action: "completion_reminder",
|
|
1021
1301
|
reminder: "kb_check",
|
|
1022
1302
|
static_degraded: posture.maintenanceDegraded,
|
|
@@ -1036,42 +1316,59 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
1036
1316
|
const e2eEmitted = e2eReminderText !== null && guidance.includes(e2eReminderText);
|
|
1037
1317
|
// Mark shown and log only for reminders that were actually emitted
|
|
1038
1318
|
if (lifecycleEmitted) {
|
|
1039
|
-
const kind =
|
|
1319
|
+
const kind = promptFileOperationState.peekPending(focusPathForConsume)
|
|
1320
|
+
?.lifecycle ===
|
|
1040
1321
|
"deleted"
|
|
1041
1322
|
? "kibi_delete"
|
|
1042
1323
|
: "kibi_write";
|
|
1043
|
-
|
|
1324
|
+
promptFileOperationState.markShown(focusPathForConsume, kind);
|
|
1044
1325
|
logger.info("smart-enforcement.file-operation-reminder", {
|
|
1045
1326
|
event: "smart_enforcement_file_operation_reminder",
|
|
1046
1327
|
file: focusPathForConsume,
|
|
1047
|
-
lifecycle:
|
|
1328
|
+
lifecycle: promptFileOperationState.peekPending(focusPathForConsume)
|
|
1048
1329
|
?.lifecycle ?? null,
|
|
1049
|
-
posture_state: posture
|
|
1330
|
+
posture_state: promptWorkContext.posture,
|
|
1050
1331
|
risk_class: effectiveRiskClass,
|
|
1051
1332
|
});
|
|
1052
1333
|
}
|
|
1053
1334
|
if (e2eEmitted) {
|
|
1054
|
-
const kind =
|
|
1335
|
+
const kind = promptFileOperationState.peekPending(focusPathForConsume)
|
|
1336
|
+
?.lifecycle ===
|
|
1055
1337
|
"deleted"
|
|
1056
1338
|
? "e2e_delete"
|
|
1057
1339
|
: "e2e_write";
|
|
1058
|
-
|
|
1059
|
-
const e2eSignalForLog = getE2eCoverageSignal(
|
|
1340
|
+
promptFileOperationState.markShown(focusPathForConsume, kind);
|
|
1341
|
+
const e2eSignalForLog = getE2eCoverageSignal(promptWorkContext.worktreeRoot, focusPathForConsume);
|
|
1060
1342
|
logger.info("smart-enforcement.e2e-reminder", {
|
|
1061
1343
|
event: "smart_enforcement_e2e_reminder",
|
|
1062
1344
|
file: focusPathForConsume,
|
|
1063
|
-
lifecycle:
|
|
1345
|
+
lifecycle: promptFileOperationState.peekPending(focusPathForConsume)
|
|
1064
1346
|
?.lifecycle ?? null,
|
|
1065
1347
|
signal_level: e2eSignalForLog.level,
|
|
1066
|
-
posture_state: posture
|
|
1348
|
+
posture_state: promptWorkContext.posture,
|
|
1067
1349
|
risk_class: effectiveRiskClass,
|
|
1068
1350
|
});
|
|
1069
1351
|
}
|
|
1070
1352
|
// Consume pending only if at least one reminder was emitted
|
|
1071
1353
|
if (lifecycleEmitted || e2eEmitted) {
|
|
1072
|
-
|
|
1354
|
+
promptFileOperationState.consumePending(focusPathForConsume);
|
|
1073
1355
|
}
|
|
1074
1356
|
}
|
|
1357
|
+
if (hardGateBlock &&
|
|
1358
|
+
hardGateConsumedPath &&
|
|
1359
|
+
guidance.includes("🛑 Kibi hard gate blocked")) {
|
|
1360
|
+
for (const kind of hardGateReminderKindsToMark) {
|
|
1361
|
+
promptFileOperationState.markShown(hardGateConsumedPath, kind);
|
|
1362
|
+
}
|
|
1363
|
+
promptFileOperationState.consumePending(hardGateConsumedPath);
|
|
1364
|
+
logger.info("smart-enforcement.hard-gate-consumed", {
|
|
1365
|
+
event: "smart_enforcement_hard_gate_consumed",
|
|
1366
|
+
file: hardGateConsumedPath,
|
|
1367
|
+
fingerprint: hardGateFingerprint ?? null,
|
|
1368
|
+
posture_state: promptWorkContext.posture,
|
|
1369
|
+
risk_class: effectiveRiskClass,
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1075
1372
|
// Latch degraded advisory warning-once state
|
|
1076
1373
|
if (showDegradedAdvisory && guidance.includes("Maintenance degraded")) {
|
|
1077
1374
|
degradedWarnedOnce = true;
|
|
@@ -1104,11 +1401,17 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
1104
1401
|
setTimeout(callback, delayMs);
|
|
1105
1402
|
});
|
|
1106
1403
|
scheduleStartupNotify(() => {
|
|
1107
|
-
const
|
|
1404
|
+
const meta = readKibiPackageVersions();
|
|
1405
|
+
const versions = {};
|
|
1406
|
+
for (const key of ["opencode", "mcp", "cli", "core"]) {
|
|
1407
|
+
if (meta[key] !== "unknown")
|
|
1408
|
+
versions[key] = meta[key];
|
|
1409
|
+
}
|
|
1108
1410
|
notifyStartup(makeStartupClient(client), {
|
|
1109
1411
|
suppressToast: cfg.ux.toastStartup === false,
|
|
1110
1412
|
directory: input.directory,
|
|
1111
|
-
...(
|
|
1413
|
+
...(Object.keys(versions).length > 0 ? { versions } : {}),
|
|
1414
|
+
versionMetadataSource: meta.source,
|
|
1112
1415
|
});
|
|
1113
1416
|
}, 2000);
|
|
1114
1417
|
}
|