kibi-opencode 0.13.0 → 0.14.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 +4 -4
- package/dist/config.d.ts +1 -1
- package/dist/config.js +2 -2
- package/dist/enforcement-policy.d.ts +71 -0
- package/dist/enforcement-policy.js +269 -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/kibi-checkpoint-runner.d.ts +83 -0
- package/dist/kibi-checkpoint-runner.js +254 -0
- package/dist/plugin.d.ts +1 -0
- package/dist/plugin.js +407 -164
- package/dist/prompt.d.ts +6 -0
- package/dist/prompt.js +25 -0
- package/dist/smart-enforcement.d.ts +6 -2
- package/dist/smart-enforcement.js +7 -1
- package/dist/work-context-resolver.d.ts +21 -0
- package/dist/work-context-resolver.js +197 -0
- package/package.json +1 -1
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,6 +22,7 @@ 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";
|
|
@@ -27,6 +30,7 @@ import { notifyStartup, } from "./startup-notifier.js";
|
|
|
27
30
|
import { sendToast, } from "./toast.js";
|
|
28
31
|
import { announceBriefTui, } from "./tui-brief-delivery.js";
|
|
29
32
|
import { deletePendingBriefMarkers, loadPendingBriefMarkers, } from "./utils/brief-marker.js";
|
|
33
|
+
import { resolveWorkContext, } from "./work-context-resolver.js";
|
|
30
34
|
import * as fs from "node:fs";
|
|
31
35
|
function deriveFileBucket(kind) {
|
|
32
36
|
return kind;
|
|
@@ -125,7 +129,7 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
125
129
|
if (!startup) {
|
|
126
130
|
return {};
|
|
127
131
|
}
|
|
128
|
-
const { cfg, workspaceHealth, posture, currentBranch, cache, runtimeOverlay, scheduler, maintenanceDegraded, getMaintenanceDegraded, getEffectiveMode, latchRuntimeDegraded, } = startup;
|
|
132
|
+
const { cfg, workspaceHealth, posture, currentBranch, cache, runtimeOverlay, scheduler: startupScheduler, maintenanceDegraded, getMaintenanceDegraded, getEffectiveMode, latchRuntimeDegraded, } = startup;
|
|
129
133
|
const hooks = {};
|
|
130
134
|
const initKibiCommandCapability = getInitKibiCommandCapability();
|
|
131
135
|
if (initKibiCommandCapability.supported) {
|
|
@@ -146,12 +150,150 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
146
150
|
const toastedFingerprints = new Set();
|
|
147
151
|
let lastRiskClass = null;
|
|
148
152
|
let lastRiskFilePath = null;
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
+
let lastRiskScopeKey = null;
|
|
154
|
+
const schedulerRegistry = new Map();
|
|
155
|
+
if (startupScheduler) {
|
|
156
|
+
schedulerRegistry.set(path.resolve(input.worktree), startupScheduler);
|
|
157
|
+
}
|
|
158
|
+
const schedulerFactoryGlobals = globalThis;
|
|
159
|
+
const sessionEditStateRegistry = new Map();
|
|
160
|
+
const fileOperationStateRegistry = new Map();
|
|
161
|
+
const checkpointRunnerRegistry = new Map();
|
|
162
|
+
const pathKindCacheRegistry = new Map();
|
|
163
|
+
function resolveScopedWorkContext(filePath) {
|
|
164
|
+
return resolveWorkContext({
|
|
165
|
+
inputDirectory: input.directory,
|
|
166
|
+
inputWorktree: input.worktree,
|
|
167
|
+
...(filePath !== undefined ? { filePath } : {}),
|
|
168
|
+
...(input.sessionId !== undefined ? { sessionId: input.sessionId } : {}),
|
|
169
|
+
...(input.agentIdentity !== undefined
|
|
170
|
+
? { agentIdentity: input.agentIdentity }
|
|
171
|
+
: {}),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
function buildStateScopeKey(context, lane) {
|
|
175
|
+
return buildEnforcementScopeKey({
|
|
176
|
+
sessionId: context.sessionId,
|
|
177
|
+
agentIdentity: context.agentIdentity,
|
|
178
|
+
worktreeRoot: context.worktreeRoot,
|
|
179
|
+
branch: context.branch,
|
|
180
|
+
dirtyRelevantFingerprint: lane,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
function getSessionEditState(context) {
|
|
184
|
+
const key = buildStateScopeKey(context, "session-edits");
|
|
185
|
+
let state = sessionEditStateRegistry.get(key);
|
|
186
|
+
if (!state) {
|
|
187
|
+
state = createSessionEditState({ worktree: context.worktreeRoot });
|
|
188
|
+
sessionEditStateRegistry.set(key, state);
|
|
189
|
+
}
|
|
190
|
+
return state;
|
|
191
|
+
}
|
|
192
|
+
function getFileOperationState(context) {
|
|
193
|
+
const key = buildStateScopeKey(context, "file-operations");
|
|
194
|
+
let state = fileOperationStateRegistry.get(key);
|
|
195
|
+
if (!state) {
|
|
196
|
+
state = createFileOperationState({
|
|
197
|
+
worktree: context.worktreeRoot,
|
|
198
|
+
}); // implements REQ-opencode-file-context-guidance-v1
|
|
199
|
+
fileOperationStateRegistry.set(key, state);
|
|
200
|
+
}
|
|
201
|
+
return state;
|
|
202
|
+
}
|
|
203
|
+
function getCheckpointRunnerForContext(context) {
|
|
204
|
+
const key = buildStateScopeKey(context, "checkpoint-runner");
|
|
205
|
+
let runner = checkpointRunnerRegistry.get(key);
|
|
206
|
+
if (!runner) {
|
|
207
|
+
runner = new KibiCheckpointRunner({
|
|
208
|
+
config: cfg,
|
|
209
|
+
onRunComplete: (meta) => {
|
|
210
|
+
const normalizedReason = meta.reason.endsWith(".trailing")
|
|
211
|
+
? meta.reason.slice(0, -".trailing".length)
|
|
212
|
+
: meta.reason;
|
|
213
|
+
const isSmartEnforcementSync = normalizedReason.startsWith("smart-enforcement.");
|
|
214
|
+
if (meta.exitCode !== 0 && !isSmartEnforcementSync) {
|
|
215
|
+
latchRuntimeDegraded("scheduler_sync_failed");
|
|
216
|
+
}
|
|
217
|
+
if (meta.checkExitCode !== undefined && meta.checkExitCode !== 0) {
|
|
218
|
+
latchRuntimeDegraded("scheduler_check_failed");
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
checkpointRunnerRegistry.set(key, runner);
|
|
223
|
+
}
|
|
224
|
+
return runner;
|
|
225
|
+
}
|
|
226
|
+
function getPathKindCache(context) {
|
|
227
|
+
const key = buildStateScopeKey(context, "path-kind-cache");
|
|
228
|
+
let scopedCache = pathKindCacheRegistry.get(key);
|
|
229
|
+
if (!scopedCache) {
|
|
230
|
+
scopedCache = new Map();
|
|
231
|
+
pathKindCacheRegistry.set(key, scopedCache);
|
|
232
|
+
}
|
|
233
|
+
return scopedCache;
|
|
234
|
+
}
|
|
235
|
+
function getSchedulerForContext(context) {
|
|
236
|
+
if (!cfg.sync.enabled) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
const worktreeRoot = path.resolve(context.worktreeRoot);
|
|
240
|
+
const existing = schedulerRegistry.get(worktreeRoot);
|
|
241
|
+
if (existing) {
|
|
242
|
+
return existing;
|
|
243
|
+
}
|
|
244
|
+
const schedulerFactory = schedulerFactoryGlobals.__kibi_test_scheduler_factory_by_worktree?.get(worktreeRoot) ??
|
|
245
|
+
schedulerFactoryGlobals.__kibi_test_scheduler_factory ??
|
|
246
|
+
createSyncScheduler;
|
|
247
|
+
try {
|
|
248
|
+
const scopedScheduler = schedulerFactory({
|
|
249
|
+
worktree: worktreeRoot,
|
|
250
|
+
config: cfg,
|
|
251
|
+
onRunComplete: (meta) => {
|
|
252
|
+
const normalizedReason = meta.reason.endsWith(".trailing")
|
|
253
|
+
? meta.reason.slice(0, -".trailing".length)
|
|
254
|
+
: meta.reason;
|
|
255
|
+
const isSmartEnforcementSync = normalizedReason.startsWith("smart-enforcement.");
|
|
256
|
+
if (meta.exitCode !== 0 && !isSmartEnforcementSync) {
|
|
257
|
+
latchRuntimeDegraded("scheduler_sync_failed");
|
|
258
|
+
}
|
|
259
|
+
if (meta.checkExitCode !== undefined && meta.checkExitCode !== 0) {
|
|
260
|
+
latchRuntimeDegraded("scheduler_check_failed");
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
schedulerRegistry.set(worktreeRoot, scopedScheduler);
|
|
265
|
+
return scopedScheduler;
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
latchRuntimeDegraded("scheduler_unavailable");
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function buildScopedCacheKey(context, riskClass, fileBucket, dirtyRelevantInputs) {
|
|
273
|
+
const cacheKey = {
|
|
274
|
+
workspaceRoot: context.worktreeRoot,
|
|
275
|
+
branch: context.branch,
|
|
276
|
+
posture: context.posture,
|
|
277
|
+
riskClass,
|
|
278
|
+
fileBucket,
|
|
279
|
+
};
|
|
280
|
+
if (getEffectiveMode() === "hard") {
|
|
281
|
+
cacheKey.scopeKey = buildEnforcementScopeKey({
|
|
282
|
+
sessionId: context.sessionId,
|
|
283
|
+
agentIdentity: context.agentIdentity,
|
|
284
|
+
worktreeRoot: context.worktreeRoot,
|
|
285
|
+
branch: context.branch,
|
|
286
|
+
dirtyRelevantFingerprint: buildDirtyRelevantFingerprint(dirtyRelevantInputs),
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
return cacheKey;
|
|
290
|
+
}
|
|
291
|
+
const rootWorkContext = resolveScopedWorkContext();
|
|
292
|
+
const sessionEditState = getSessionEditState(rootWorkContext);
|
|
293
|
+
const fileOperationState = getFileOperationState(rootWorkContext);
|
|
294
|
+
const scheduler = getSchedulerForContext(rootWorkContext);
|
|
153
295
|
let degradedWarnedOnce = false;
|
|
154
|
-
const pathKindCache =
|
|
296
|
+
const pathKindCache = getPathKindCache(rootWorkContext);
|
|
155
297
|
// Idle-brief state — dedupe via semantic contentHash (persisted envelope is the delivery authority)
|
|
156
298
|
let idleBriefInFlight = false;
|
|
157
299
|
let idleBriefTrailingRerun = false;
|
|
@@ -177,18 +319,21 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
177
319
|
sessionBaselineCursor = nextState.cursor;
|
|
178
320
|
}
|
|
179
321
|
syncSessionBaseline(currentBranch);
|
|
180
|
-
function normalizeSessionPath(filePath) {
|
|
322
|
+
function normalizeSessionPath(filePath, worktree = input.worktree) {
|
|
181
323
|
if (path.isAbsolute(filePath)) {
|
|
182
|
-
const relativePath = path.relative(
|
|
324
|
+
const relativePath = path.relative(worktree, filePath);
|
|
183
325
|
return relativePath.startsWith("..") ? filePath : relativePath;
|
|
184
326
|
}
|
|
185
327
|
return filePath;
|
|
186
328
|
}
|
|
187
|
-
function resolveWorktreePath(filePath) {
|
|
188
|
-
return
|
|
189
|
-
? path.join(
|
|
329
|
+
function resolveWorktreePath(filePath, worktree = input.worktree) {
|
|
330
|
+
return worktree && !path.isAbsolute(filePath)
|
|
331
|
+
? path.join(worktree, filePath)
|
|
190
332
|
: filePath;
|
|
191
333
|
}
|
|
334
|
+
function buildRiskPathScopeKey(context, filePath) {
|
|
335
|
+
return `${buildStateScopeKey(context, "risk")}:${normalizeSessionPath(filePath, context.worktreeRoot)}`;
|
|
336
|
+
}
|
|
192
337
|
function getKbSnapshotFingerprint(worktree, branch) {
|
|
193
338
|
try {
|
|
194
339
|
const snapshotPath = path.join(worktree, ".kb", "branches", branch, "kb.rdf");
|
|
@@ -243,33 +388,33 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
243
388
|
}
|
|
244
389
|
return normalizeSessionPath(directPath);
|
|
245
390
|
}
|
|
246
|
-
function readFileContent(filePath) {
|
|
391
|
+
function readFileContent(filePath, worktree = input.worktree) {
|
|
247
392
|
try {
|
|
248
|
-
return fs.readFileSync(resolveWorktreePath(filePath), "utf-8");
|
|
393
|
+
return fs.readFileSync(resolveWorktreePath(filePath, worktree), "utf-8");
|
|
249
394
|
}
|
|
250
395
|
catch {
|
|
251
396
|
return "";
|
|
252
397
|
}
|
|
253
398
|
}
|
|
254
|
-
function updateRecentEditsFromSession(sessionEdits) {
|
|
399
|
+
function updateRecentEditsFromSession(sessionEdits, scopedPathKindCache) {
|
|
255
400
|
recentEdits = sessionEdits.slice(-MAX_RECENT_EDITS).map((entry) => ({
|
|
256
401
|
path: entry.filePath,
|
|
257
|
-
kind:
|
|
402
|
+
kind: scopedPathKindCache.get(entry.filePath) ?? "unknown",
|
|
258
403
|
timestamp: entry.lastReconciledAt,
|
|
259
404
|
}));
|
|
260
405
|
return recentEdits;
|
|
261
406
|
}
|
|
262
|
-
function deriveRiskContext(filePath) {
|
|
263
|
-
const normalizedFilePath = normalizeSessionPath(filePath);
|
|
264
|
-
const pathAnalysis = analyzePath(normalizedFilePath,
|
|
265
|
-
|
|
266
|
-
const fileContent = readFileContent(normalizedFilePath);
|
|
407
|
+
function deriveRiskContext(context, filePath, scopedPathKindCache) {
|
|
408
|
+
const normalizedFilePath = normalizeSessionPath(filePath, context.worktreeRoot);
|
|
409
|
+
const pathAnalysis = analyzePath(normalizedFilePath, context.worktreeRoot);
|
|
410
|
+
scopedPathKindCache.set(normalizedFilePath, pathAnalysis.kind);
|
|
411
|
+
const fileContent = readFileContent(normalizedFilePath, context.worktreeRoot);
|
|
267
412
|
const hasMustPriority = pathAnalysis.kind === "requirement"
|
|
268
|
-
? isMustPriorityRequirement(normalizedFilePath,
|
|
413
|
+
? isMustPriorityRequirement(normalizedFilePath, context.worktreeRoot)
|
|
269
414
|
: false;
|
|
270
415
|
let precomputedSuggestion = null;
|
|
271
416
|
if (pathAnalysis.kind === "code" && cfg.guidance.commentDetection.enabled) {
|
|
272
|
-
precomputedSuggestion = analyzeCodeFile(resolveWorktreePath(normalizedFilePath), {
|
|
417
|
+
precomputedSuggestion = analyzeCodeFile(resolveWorktreePath(normalizedFilePath, context.worktreeRoot), {
|
|
273
418
|
minLines: cfg.guidance.commentDetection.minLines,
|
|
274
419
|
});
|
|
275
420
|
}
|
|
@@ -287,6 +432,7 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
287
432
|
pathAnalysis.kind === "code" ? precomputedSuggestion : null;
|
|
288
433
|
lastRiskClass = effectiveRiskClass;
|
|
289
434
|
lastRiskFilePath = normalizedFilePath;
|
|
435
|
+
lastRiskScopeKey = buildRiskPathScopeKey(context, normalizedFilePath);
|
|
290
436
|
return {
|
|
291
437
|
effectiveRiskClass,
|
|
292
438
|
pathAnalysis,
|
|
@@ -294,17 +440,17 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
294
440
|
precomputedSuggestion,
|
|
295
441
|
};
|
|
296
442
|
}
|
|
297
|
-
function buildBriefingWorkspaceContext() {
|
|
443
|
+
function buildBriefingWorkspaceContext(context = rootWorkContext, branch = context.branch) {
|
|
298
444
|
return {
|
|
299
|
-
workspaceRoot:
|
|
300
|
-
branch
|
|
301
|
-
directory:
|
|
445
|
+
workspaceRoot: context.worktreeRoot,
|
|
446
|
+
branch,
|
|
447
|
+
directory: context.worktreeRoot,
|
|
302
448
|
...(input.workspace !== undefined ? { workspace: input.workspace } : {}),
|
|
303
449
|
};
|
|
304
450
|
}
|
|
305
|
-
function buildWorkspaceContextForBranch(branch) {
|
|
451
|
+
function buildWorkspaceContextForBranch(branch, context = rootWorkContext) {
|
|
306
452
|
return {
|
|
307
|
-
...buildBriefingWorkspaceContext(),
|
|
453
|
+
...buildBriefingWorkspaceContext(context),
|
|
308
454
|
branch,
|
|
309
455
|
};
|
|
310
456
|
}
|
|
@@ -312,8 +458,8 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
312
458
|
if (!intentResult.eligible ||
|
|
313
459
|
!input.client ||
|
|
314
460
|
getMaintenanceDegraded() ||
|
|
315
|
-
(posture.state !== "root_active" &&
|
|
316
|
-
posture.state !== "hybrid_root_plus_vendored")) {
|
|
461
|
+
((options.postureState ?? posture.state) !== "root_active" &&
|
|
462
|
+
(options.postureState ?? posture.state) !== "hybrid_root_plus_vendored")) {
|
|
317
463
|
return;
|
|
318
464
|
}
|
|
319
465
|
if (options.skipIfCachedResultExists === true &&
|
|
@@ -322,7 +468,7 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
322
468
|
}
|
|
323
469
|
const client = input.client;
|
|
324
470
|
const fingerprint = intentResult.fingerprint;
|
|
325
|
-
const workspaceCtx = buildBriefingWorkspaceContext();
|
|
471
|
+
const workspaceCtx = options.workspaceCtx ?? buildBriefingWorkspaceContext();
|
|
326
472
|
void fetchBriefingResult(client, workspaceCtx, intentResult).then((result) => {
|
|
327
473
|
autoBriefResults.set(fingerprint, result);
|
|
328
474
|
if (!toastedFingerprints.has(fingerprint)) {
|
|
@@ -506,71 +652,63 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
506
652
|
.properties.file;
|
|
507
653
|
if (!filePath)
|
|
508
654
|
return;
|
|
655
|
+
const eventContext = resolveScopedWorkContext(filePath);
|
|
656
|
+
const scopedSessionEditState = getSessionEditState(eventContext);
|
|
657
|
+
const scopedFileOperationState = getFileOperationState(eventContext);
|
|
658
|
+
const scopedPathKindCache = getPathKindCache(eventContext);
|
|
659
|
+
const scopedScheduler = getSchedulerForContext(eventContext);
|
|
660
|
+
const normalizedFilePath = normalizeSessionPath(filePath, eventContext.worktreeRoot);
|
|
509
661
|
// Record lifecycle event into file-operation-state // implements REQ-opencode-file-context-guidance-v1
|
|
510
662
|
const lifecycle = event.type === "file.created"
|
|
511
663
|
? "created"
|
|
512
664
|
: event.type === "file.deleted"
|
|
513
665
|
? "deleted"
|
|
514
666
|
: "edited";
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
const pathAnalysis = analyzePath(
|
|
667
|
+
scopedFileOperationState.recordLifecycle(filePath, lifecycle, Date.now());
|
|
668
|
+
scopedFileOperationState.normalizePath(filePath);
|
|
669
|
+
const pathAnalysis = analyzePath(normalizedFilePath, eventContext.worktreeRoot);
|
|
518
670
|
// For file.deleted: derive path kind without reading content, classify for reminder routing only
|
|
519
671
|
if (lifecycle === "deleted") {
|
|
520
672
|
// Preserve last known semantic risk if path was already tracked during session
|
|
521
|
-
const lastKnownKind =
|
|
673
|
+
const lastKnownKind = scopedPathKindCache.get(normalizedFilePath);
|
|
522
674
|
if (lastKnownKind) {
|
|
523
675
|
// Path was tracked — preserve last known semantic risk for reminder routing
|
|
524
|
-
|
|
676
|
+
scopedPathKindCache.set(normalizedFilePath, pathAnalysis.kind);
|
|
525
677
|
}
|
|
526
678
|
else {
|
|
527
679
|
// Not tracked — classify only for reminder routing, not auto-briefing
|
|
528
|
-
|
|
680
|
+
scopedPathKindCache.set(normalizedFilePath, pathAnalysis.kind);
|
|
529
681
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
const sessionEdits =
|
|
533
|
-
|
|
534
|
-
path: e.filePath,
|
|
535
|
-
kind: pathKindCache.get(e.filePath) ?? "unknown",
|
|
536
|
-
timestamp: e.lastReconciledAt,
|
|
537
|
-
}));
|
|
682
|
+
scopedSessionEditState.recordEventHint(normalizedFilePath, pathAnalysis.kind, Date.now());
|
|
683
|
+
scopedSessionEditState.reconcilePath(normalizedFilePath);
|
|
684
|
+
const sessionEdits = scopedSessionEditState.getSessionEdits();
|
|
685
|
+
updateRecentEditsFromSession(sessionEdits, scopedPathKindCache);
|
|
538
686
|
// Schedule background sync for deleted files that pass shouldHandleFile // implements REQ-opencode-file-context-guidance-v1
|
|
539
687
|
if (cfg.sync.enabled &&
|
|
540
|
-
|
|
541
|
-
fileFilter.shouldHandleFile(
|
|
542
|
-
|
|
688
|
+
scopedScheduler &&
|
|
689
|
+
fileFilter.shouldHandleFile(normalizedFilePath, eventContext.worktreeRoot)) {
|
|
690
|
+
scopedScheduler.scheduleSync("file.deleted", normalizedFilePath);
|
|
543
691
|
}
|
|
544
692
|
return;
|
|
545
693
|
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
const sessionEdits =
|
|
550
|
-
const focusEdit =
|
|
694
|
+
scopedSessionEditState.recordEventHint(normalizedFilePath, pathAnalysis.kind, Date.now());
|
|
695
|
+
scopedSessionEditState.reconcilePath(normalizedFilePath);
|
|
696
|
+
scopedPathKindCache.set(normalizedFilePath, pathAnalysis.kind);
|
|
697
|
+
const sessionEdits = scopedSessionEditState.getSessionEdits();
|
|
698
|
+
const focusEdit = scopedSessionEditState.getFocusEdit();
|
|
551
699
|
// Schedule background sync for file.created/file.edited that pass shouldHandleFile // implements REQ-opencode-file-context-guidance-v1
|
|
552
700
|
if (cfg.sync.enabled &&
|
|
553
|
-
|
|
554
|
-
fileFilter.shouldHandleFile(
|
|
555
|
-
|
|
701
|
+
scopedScheduler &&
|
|
702
|
+
fileFilter.shouldHandleFile(normalizedFilePath, eventContext.worktreeRoot)) {
|
|
703
|
+
scopedScheduler.scheduleSync(lifecycle === "created" ? "file.created" : "file.edited", normalizedFilePath);
|
|
556
704
|
}
|
|
557
|
-
|
|
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");
|
|
563
|
-
}
|
|
564
|
-
catch { }
|
|
705
|
+
const fileContent = readFileContent(normalizedFilePath, eventContext.worktreeRoot);
|
|
565
706
|
const hasMustPriority = pathAnalysis.kind === "requirement"
|
|
566
|
-
? isMustPriorityRequirement(
|
|
707
|
+
? isMustPriorityRequirement(normalizedFilePath, eventContext.worktreeRoot)
|
|
567
708
|
: false;
|
|
568
709
|
let precomputedSuggestion = null;
|
|
569
710
|
if (pathAnalysis.kind === "code" && cfg.guidance.commentDetection.enabled) {
|
|
570
|
-
|
|
571
|
-
? path.join(input.worktree, filePath)
|
|
572
|
-
: filePath;
|
|
573
|
-
precomputedSuggestion = analyzeCodeFile(resolvedPath, {
|
|
711
|
+
precomputedSuggestion = analyzeCodeFile(resolveWorktreePath(normalizedFilePath, eventContext.worktreeRoot), {
|
|
574
712
|
minLines: cfg.guidance.commentDetection.minLines,
|
|
575
713
|
});
|
|
576
714
|
}
|
|
@@ -587,18 +725,20 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
587
725
|
const isAutoBriefRisk = effectiveRiskClass === "behavior_candidate" ||
|
|
588
726
|
effectiveRiskClass === "traceability_candidate";
|
|
589
727
|
lastRiskClass = effectiveRiskClass;
|
|
728
|
+
lastRiskFilePath = normalizedFilePath;
|
|
729
|
+
lastRiskScopeKey = buildRiskPathScopeKey(eventContext, normalizedFilePath);
|
|
590
730
|
logger.info("smart-enforcement.risk", {
|
|
591
731
|
event: "smart_enforcement_risk",
|
|
592
|
-
file:
|
|
732
|
+
file: normalizedFilePath,
|
|
593
733
|
path_kind: pathAnalysis.kind,
|
|
594
734
|
risk_class: effectiveRiskClass,
|
|
595
|
-
posture_state: posture
|
|
735
|
+
posture_state: eventContext.posture,
|
|
596
736
|
maintenance_state: getMaintenanceDegraded()
|
|
597
737
|
? "maintenance_degraded"
|
|
598
738
|
: "maintenance_available",
|
|
599
739
|
under_kb: pathAnalysis.isUnderKb,
|
|
600
740
|
has_must_priority: hasMustPriority,
|
|
601
|
-
posture: posture
|
|
741
|
+
posture: eventContext.posture,
|
|
602
742
|
reason_code: effectiveRiskClass,
|
|
603
743
|
effective_mode: getEffectiveMode(),
|
|
604
744
|
static_degraded: posture.maintenanceDegraded,
|
|
@@ -613,13 +753,13 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
613
753
|
runtimeOverlay.primaryCause === "scheduler_check_failed";
|
|
614
754
|
if (!targetedChecksBlocked &&
|
|
615
755
|
cfg.sync.enabled &&
|
|
616
|
-
|
|
756
|
+
scopedScheduler &&
|
|
617
757
|
cfg.guidance.targetedChecks.enabled) {
|
|
618
758
|
const traceabilityRules = effectiveRiskClass === "traceability_candidate"
|
|
619
759
|
? ["symbol-traceability"]
|
|
620
760
|
: null;
|
|
621
761
|
const kbStructuralRules = effectiveRiskClass === "kb_doc_structural" &&
|
|
622
|
-
fileFilter.shouldHandleFile(
|
|
762
|
+
fileFilter.shouldHandleFile(normalizedFilePath, eventContext.worktreeRoot)
|
|
623
763
|
? [
|
|
624
764
|
"required-fields",
|
|
625
765
|
"no-dangling-refs",
|
|
@@ -633,10 +773,10 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
633
773
|
if (checkRules) {
|
|
634
774
|
logger.info("smart-enforcement.targeted-checks", {
|
|
635
775
|
event: "smart_enforcement_targeted_checks",
|
|
636
|
-
file:
|
|
776
|
+
file: normalizedFilePath,
|
|
637
777
|
risk_class: effectiveRiskClass,
|
|
638
|
-
posture: posture
|
|
639
|
-
posture_state: posture
|
|
778
|
+
posture: eventContext.posture,
|
|
779
|
+
posture_state: eventContext.posture,
|
|
640
780
|
guidance_action: "targeted_checks",
|
|
641
781
|
effective_mode: getEffectiveMode(),
|
|
642
782
|
rules: checkRules,
|
|
@@ -645,43 +785,33 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
645
785
|
merged_degraded: getMaintenanceDegraded(),
|
|
646
786
|
overlay_cause: runtimeOverlay.primaryCause ?? null,
|
|
647
787
|
});
|
|
648
|
-
logger.info(`kibi-opencode: scheduling sync for ${
|
|
649
|
-
|
|
788
|
+
logger.info(`kibi-opencode: scheduling sync for ${normalizedFilePath}`);
|
|
789
|
+
scopedScheduler.scheduleSync(effectiveRiskClass === "traceability_candidate"
|
|
650
790
|
? "smart-enforcement.traceability"
|
|
651
|
-
: "smart-enforcement.kb-doc",
|
|
791
|
+
: "smart-enforcement.kb-doc", normalizedFilePath, checkRules);
|
|
652
792
|
}
|
|
653
793
|
}
|
|
654
|
-
|
|
655
|
-
path: e.filePath,
|
|
656
|
-
kind: pathKindCache.get(e.filePath) ?? "unknown",
|
|
657
|
-
timestamp: e.lastReconciledAt,
|
|
658
|
-
}));
|
|
794
|
+
updateRecentEditsFromSession(sessionEdits, scopedPathKindCache);
|
|
659
795
|
if (effectiveRiskClass === "safe_docs_only" ||
|
|
660
796
|
effectiveRiskClass === "safe_test_only") {
|
|
661
797
|
recentCommentSuggestion = null;
|
|
662
798
|
return;
|
|
663
799
|
}
|
|
664
|
-
const cacheKey =
|
|
665
|
-
workspaceRoot: input.worktree,
|
|
666
|
-
branch: currentBranch,
|
|
667
|
-
posture: posture.state,
|
|
668
|
-
riskClass: effectiveRiskClass,
|
|
669
|
-
fileBucket: deriveFileBucket(pathAnalysis.kind),
|
|
670
|
-
};
|
|
800
|
+
const cacheKey = buildScopedCacheKey(eventContext, effectiveRiskClass, deriveFileBucket(pathAnalysis.kind), [normalizedFilePath, pathAnalysis.kind, effectiveRiskClass]);
|
|
671
801
|
// Always process manual_kb_edit before cache check — this is a critical safety signal
|
|
672
802
|
if (effectiveRiskClass === "manual_kb_edit") {
|
|
673
803
|
hasRecentKbEdit = true;
|
|
674
804
|
if (cfg.guidance.warnOnKbEdits) {
|
|
675
|
-
logger.warn(`kibi-opencode: .kb edit detected for ${
|
|
676
|
-
getSessionTracker().recordWarning("kb-edit",
|
|
805
|
+
logger.warn(`kibi-opencode: .kb edit detected for ${normalizedFilePath}`);
|
|
806
|
+
getSessionTracker().recordWarning("kb-edit", normalizedFilePath, `Manual .kb edit: ${normalizedFilePath}`);
|
|
677
807
|
}
|
|
678
808
|
return;
|
|
679
809
|
}
|
|
680
810
|
// Always emit requirement lint warnings before cache check — these are safety signals
|
|
681
811
|
if (effectiveRiskClass === "req_policy_candidate") {
|
|
682
|
-
const lintWarnings = lintRequirementDoc(
|
|
812
|
+
const lintWarnings = lintRequirementDoc(normalizedFilePath, eventContext.worktreeRoot);
|
|
683
813
|
for (const warning of lintWarnings) {
|
|
684
|
-
getSessionTracker().recordWarning(warning.category,
|
|
814
|
+
getSessionTracker().recordWarning(warning.category, normalizedFilePath, warning.message);
|
|
685
815
|
}
|
|
686
816
|
}
|
|
687
817
|
// Cache check: after critical signals have been emitted
|
|
@@ -690,10 +820,10 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
690
820
|
event: "smart_enforcement_cache",
|
|
691
821
|
cache_hit: true,
|
|
692
822
|
cache_state: "hit",
|
|
693
|
-
file:
|
|
823
|
+
file: normalizedFilePath,
|
|
694
824
|
risk_class: effectiveRiskClass,
|
|
695
|
-
posture: posture
|
|
696
|
-
posture_state: posture
|
|
825
|
+
posture: eventContext.posture,
|
|
826
|
+
posture_state: eventContext.posture,
|
|
697
827
|
});
|
|
698
828
|
if (!isAutoBriefRisk) {
|
|
699
829
|
return;
|
|
@@ -703,10 +833,10 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
703
833
|
event: "smart_enforcement_cache",
|
|
704
834
|
cache_hit: false,
|
|
705
835
|
cache_state: "miss",
|
|
706
|
-
file:
|
|
836
|
+
file: normalizedFilePath,
|
|
707
837
|
risk_class: effectiveRiskClass,
|
|
708
|
-
posture: posture
|
|
709
|
-
posture_state: posture
|
|
838
|
+
posture: eventContext.posture,
|
|
839
|
+
posture_state: eventContext.posture,
|
|
710
840
|
});
|
|
711
841
|
if (effectiveRiskClass === "req_policy_candidate") {
|
|
712
842
|
if (getMaintenanceDegraded()) {
|
|
@@ -715,10 +845,10 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
715
845
|
: logger.info;
|
|
716
846
|
logFn("smart-enforcement.degraded", {
|
|
717
847
|
event: "smart_enforcement_degraded",
|
|
718
|
-
file:
|
|
848
|
+
file: normalizedFilePath,
|
|
719
849
|
risk_class: effectiveRiskClass,
|
|
720
|
-
posture: posture
|
|
721
|
-
posture_state: posture
|
|
850
|
+
posture: eventContext.posture,
|
|
851
|
+
posture_state: eventContext.posture,
|
|
722
852
|
maintenance_state: getMaintenanceDegraded()
|
|
723
853
|
? "maintenance_degraded"
|
|
724
854
|
: "maintenance_available",
|
|
@@ -733,8 +863,8 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
733
863
|
}
|
|
734
864
|
if (!getMaintenanceDegraded() &&
|
|
735
865
|
cfg.sync.enabled &&
|
|
736
|
-
|
|
737
|
-
fileFilter.shouldHandleFile(
|
|
866
|
+
scopedScheduler &&
|
|
867
|
+
fileFilter.shouldHandleFile(normalizedFilePath, eventContext.worktreeRoot)) {
|
|
738
868
|
let checkRules;
|
|
739
869
|
if (cfg.guidance.targetedChecks.enabled) {
|
|
740
870
|
if (hasMustPriority && getEffectiveMode() === "strict") {
|
|
@@ -744,7 +874,7 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
744
874
|
"must-priority-coverage",
|
|
745
875
|
"strict-req-fact-pairing",
|
|
746
876
|
];
|
|
747
|
-
logger.info(`kibi-opencode: must-priority requirement detected, scheduling elevated checks for ${
|
|
877
|
+
logger.info(`kibi-opencode: must-priority requirement detected, scheduling elevated checks for ${normalizedFilePath}`);
|
|
748
878
|
}
|
|
749
879
|
else {
|
|
750
880
|
checkRules = [
|
|
@@ -756,10 +886,10 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
756
886
|
}
|
|
757
887
|
logger.info("smart-enforcement.targeted-checks", {
|
|
758
888
|
event: "smart_enforcement_targeted_checks",
|
|
759
|
-
file:
|
|
889
|
+
file: normalizedFilePath,
|
|
760
890
|
risk_class: effectiveRiskClass,
|
|
761
|
-
posture: posture
|
|
762
|
-
posture_state: posture
|
|
891
|
+
posture: eventContext.posture,
|
|
892
|
+
posture_state: eventContext.posture,
|
|
763
893
|
guidance_action: "targeted_checks",
|
|
764
894
|
effective_mode: getEffectiveMode(),
|
|
765
895
|
rules: checkRules ?? [],
|
|
@@ -768,7 +898,7 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
768
898
|
merged_degraded: getMaintenanceDegraded(),
|
|
769
899
|
overlay_cause: runtimeOverlay.primaryCause ?? null,
|
|
770
900
|
});
|
|
771
|
-
|
|
901
|
+
scopedScheduler.scheduleSync("file.edited", normalizedFilePath, checkRules);
|
|
772
902
|
}
|
|
773
903
|
return;
|
|
774
904
|
}
|
|
@@ -779,10 +909,10 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
779
909
|
: logger.info;
|
|
780
910
|
logFn("smart-enforcement.degraded", {
|
|
781
911
|
event: "smart_enforcement_degraded",
|
|
782
|
-
file:
|
|
912
|
+
file: normalizedFilePath,
|
|
783
913
|
risk_class: effectiveRiskClass,
|
|
784
|
-
posture: posture
|
|
785
|
-
posture_state: posture
|
|
914
|
+
posture: eventContext.posture,
|
|
915
|
+
posture_state: eventContext.posture,
|
|
786
916
|
maintenance_state: getMaintenanceDegraded()
|
|
787
917
|
? "maintenance_degraded"
|
|
788
918
|
: "maintenance_available",
|
|
@@ -803,7 +933,7 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
803
933
|
const suggestion = precomputedSuggestion;
|
|
804
934
|
if (suggestion) {
|
|
805
935
|
recentCommentSuggestion = suggestion;
|
|
806
|
-
const dedupeKey = `${
|
|
936
|
+
const dedupeKey = `${buildRiskPathScopeKey(eventContext, normalizedFilePath)}:${suggestion.suggestionType}:${suggestion.fingerprint}`;
|
|
807
937
|
if (!seenFingerprints.has(dedupeKey)) {
|
|
808
938
|
seenFingerprints.add(dedupeKey);
|
|
809
939
|
const warningCategory = suggestion.suggestionType === "fact"
|
|
@@ -811,8 +941,8 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
811
941
|
: suggestion.suggestionType === "adr"
|
|
812
942
|
? "long-comment-missed-adr"
|
|
813
943
|
: "missing-traceability";
|
|
814
|
-
logger.warn(`kibi-opencode: detected durable ${suggestion.suggestionType} knowledge in ${
|
|
815
|
-
getSessionTracker().recordWarning(warningCategory,
|
|
944
|
+
logger.warn(`kibi-opencode: detected durable ${suggestion.suggestionType} knowledge in ${normalizedFilePath}`);
|
|
945
|
+
getSessionTracker().recordWarning(warningCategory, normalizedFilePath, `Consider routing this ${suggestion.suggestionType} knowledge to Kibi instead of inline comments: ${suggestion.reasoning}`);
|
|
816
946
|
}
|
|
817
947
|
}
|
|
818
948
|
else {
|
|
@@ -827,16 +957,20 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
827
957
|
return;
|
|
828
958
|
}
|
|
829
959
|
const sessionSourceFiles = sessionEdits.map((e) => e.filePath);
|
|
960
|
+
const briefingContext = resolveScopedWorkContext(focusEdit.filePath);
|
|
830
961
|
const intentResult = computeBriefIntent({
|
|
831
962
|
riskClass: effectiveRiskClass,
|
|
832
|
-
posture: posture
|
|
963
|
+
posture: briefingContext.posture,
|
|
833
964
|
maintenanceDegraded: getMaintenanceDegraded(),
|
|
834
965
|
sourceFiles: sessionSourceFiles,
|
|
835
966
|
focusFilePath: focusEdit.filePath,
|
|
836
|
-
worktreeRoot:
|
|
837
|
-
branch:
|
|
967
|
+
worktreeRoot: briefingContext.worktreeRoot,
|
|
968
|
+
branch: briefingContext.branch,
|
|
969
|
+
});
|
|
970
|
+
queueBriefingFetch(intentResult, {
|
|
971
|
+
workspaceCtx: buildBriefingWorkspaceContext(briefingContext),
|
|
972
|
+
postureState: briefingContext.posture,
|
|
838
973
|
});
|
|
839
|
-
queueBriefingFetch(intentResult);
|
|
840
974
|
}
|
|
841
975
|
return;
|
|
842
976
|
};
|
|
@@ -853,31 +987,39 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
853
987
|
cfg.guidance.smartEnforcement.degradedMode === "warn-once" &&
|
|
854
988
|
!degradedWarnedOnce;
|
|
855
989
|
const transformFocusFilePath = getTransformFocusFilePath(transformInput);
|
|
856
|
-
|
|
990
|
+
const promptWorkContext = resolveScopedWorkContext(transformFocusFilePath ?? undefined);
|
|
991
|
+
const promptSessionEditState = getSessionEditState(promptWorkContext);
|
|
992
|
+
const promptFileOperationState = getFileOperationState(promptWorkContext);
|
|
993
|
+
const promptPathKindCache = getPathKindCache(promptWorkContext);
|
|
994
|
+
promptSessionEditState.reconcileKnownPaths();
|
|
857
995
|
if (transformFocusFilePath) {
|
|
858
|
-
|
|
996
|
+
promptSessionEditState.forceEdit(normalizeSessionPath(transformFocusFilePath, promptWorkContext.worktreeRoot));
|
|
859
997
|
}
|
|
860
|
-
const transformSessionEdits =
|
|
861
|
-
const transformFocusEdit =
|
|
998
|
+
const transformSessionEdits = promptSessionEditState.getSessionEdits();
|
|
999
|
+
const transformFocusEdit = promptSessionEditState.getFocusEdit();
|
|
862
1000
|
const transformRecentEdits = transformSessionEdits
|
|
863
1001
|
.slice(-MAX_RECENT_EDITS)
|
|
864
1002
|
.map((e) => ({
|
|
865
1003
|
path: e.filePath,
|
|
866
|
-
kind:
|
|
1004
|
+
kind: promptPathKindCache.get(e.filePath) ?? "unknown",
|
|
867
1005
|
}));
|
|
868
1006
|
const transformPromptFocusEdit = transformFocusEdit
|
|
869
1007
|
? {
|
|
870
1008
|
path: transformFocusEdit.filePath,
|
|
871
|
-
kind:
|
|
1009
|
+
kind: promptPathKindCache.get(transformFocusEdit.filePath) ??
|
|
1010
|
+
"unknown",
|
|
872
1011
|
}
|
|
873
1012
|
: null;
|
|
874
1013
|
const riskContextFilePath = transformFocusEdit?.filePath ?? transformFocusFilePath;
|
|
875
|
-
|
|
1014
|
+
const riskScopeKey = riskContextFilePath
|
|
1015
|
+
? buildRiskPathScopeKey(promptWorkContext, riskContextFilePath)
|
|
1016
|
+
: null;
|
|
1017
|
+
let effectiveRiskClass = riskScopeKey !== null && lastRiskScopeKey === riskScopeKey
|
|
876
1018
|
? lastRiskClass
|
|
877
1019
|
: null;
|
|
878
1020
|
if (riskContextFilePath &&
|
|
879
|
-
(lastRiskClass === null ||
|
|
880
|
-
const riskCtx = deriveRiskContext(riskContextFilePath);
|
|
1021
|
+
(lastRiskClass === null || lastRiskScopeKey !== riskScopeKey)) {
|
|
1022
|
+
const riskCtx = deriveRiskContext(promptWorkContext, riskContextFilePath, promptPathKindCache);
|
|
881
1023
|
effectiveRiskClass = riskCtx.effectiveRiskClass;
|
|
882
1024
|
if (!recentCommentSuggestion && riskCtx.precomputedSuggestion) {
|
|
883
1025
|
recentCommentSuggestion = riskCtx.precomputedSuggestion;
|
|
@@ -891,11 +1033,11 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
891
1033
|
const intentResult = effectiveRiskClass
|
|
892
1034
|
? computeBriefIntent({
|
|
893
1035
|
riskClass: effectiveRiskClass,
|
|
894
|
-
posture: posture
|
|
1036
|
+
posture: promptWorkContext.posture,
|
|
895
1037
|
maintenanceDegraded,
|
|
896
1038
|
sourceFiles: promptSourceFiles,
|
|
897
|
-
worktreeRoot:
|
|
898
|
-
branch:
|
|
1039
|
+
worktreeRoot: promptWorkContext.worktreeRoot,
|
|
1040
|
+
branch: promptWorkContext.branch,
|
|
899
1041
|
...(promptFocusFilePath !== undefined
|
|
900
1042
|
? {
|
|
901
1043
|
focusFilePath: promptFocusFilePath,
|
|
@@ -909,7 +1051,11 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
909
1051
|
const isAutoBriefRisk = effectiveRiskClass === "behavior_candidate" ||
|
|
910
1052
|
effectiveRiskClass === "traceability_candidate";
|
|
911
1053
|
if (!autoBriefResult && isAutoBriefRisk && intentResult) {
|
|
912
|
-
queueBriefingFetch(intentResult, {
|
|
1054
|
+
queueBriefingFetch(intentResult, {
|
|
1055
|
+
skipIfCachedResultExists: true,
|
|
1056
|
+
workspaceCtx: buildBriefingWorkspaceContext(promptWorkContext),
|
|
1057
|
+
postureState: promptWorkContext.posture,
|
|
1058
|
+
});
|
|
913
1059
|
}
|
|
914
1060
|
// Replay latest unread idle brief if available // implements REQ-opencode-kibi-briefing-v4
|
|
915
1061
|
if (input.worktree && currentBranch && input.client) {
|
|
@@ -936,10 +1082,14 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
936
1082
|
}
|
|
937
1083
|
// Steps 3-4: File-operation reminder selection with suppression // implements REQ-opencode-file-context-guidance-v1
|
|
938
1084
|
let fileOperationReminder;
|
|
1085
|
+
let hardGateBlock;
|
|
1086
|
+
let hardGateConsumedPath;
|
|
1087
|
+
let hardGateFingerprint;
|
|
1088
|
+
let hardGateReminderKindsToMark = [];
|
|
939
1089
|
const focusPathForReminder = transformFocusFilePath ?? promptFocusFilePath;
|
|
940
1090
|
if (focusPathForReminder) {
|
|
941
|
-
const normalizedFocusPath =
|
|
942
|
-
const pendingLifecycle =
|
|
1091
|
+
const normalizedFocusPath = promptFileOperationState.normalizePath(focusPathForReminder);
|
|
1092
|
+
const pendingLifecycle = promptFileOperationState.peekPending(normalizedFocusPath);
|
|
943
1093
|
if (pendingLifecycle) {
|
|
944
1094
|
// Check if any reminder kind for this lifecycle has not yet been shown
|
|
945
1095
|
const reminderKindsForLifecycle = pendingLifecycle.lifecycle === "deleted"
|
|
@@ -947,12 +1097,41 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
947
1097
|
: pendingLifecycle.lifecycle === "created"
|
|
948
1098
|
? ["kibi_write", "e2e_write"]
|
|
949
1099
|
: ["e2e_write"];
|
|
950
|
-
const hasUnshownReminder = reminderKindsForLifecycle.some((kind) => !
|
|
1100
|
+
const hasUnshownReminder = reminderKindsForLifecycle.some((kind) => !promptFileOperationState.hasShown(normalizedFocusPath, kind));
|
|
951
1101
|
if (hasUnshownReminder) {
|
|
952
1102
|
// Resolve linked entities and e2e signal
|
|
953
|
-
const linkedEntityResult = getFileLinkedEntityIds(
|
|
954
|
-
const e2eSignal = getE2eCoverageSignal(
|
|
955
|
-
const focusPathKind =
|
|
1103
|
+
const linkedEntityResult = getFileLinkedEntityIds(promptWorkContext.worktreeRoot, focusPathForReminder);
|
|
1104
|
+
const e2eSignal = getE2eCoverageSignal(promptWorkContext.worktreeRoot, focusPathForReminder);
|
|
1105
|
+
const focusPathKind = promptPathKindCache.get(normalizedFocusPath) ?? "unknown";
|
|
1106
|
+
const effectiveMode = getEffectiveMode();
|
|
1107
|
+
let checkpointEvidence = false;
|
|
1108
|
+
let checkpointRunner = null;
|
|
1109
|
+
let checkpointContext = null;
|
|
1110
|
+
const checkpointFingerprint = buildDirtyRelevantFingerprint([
|
|
1111
|
+
normalizedFocusPath,
|
|
1112
|
+
pendingLifecycle.lifecycle,
|
|
1113
|
+
focusPathKind,
|
|
1114
|
+
effectiveRiskClass ?? "safe_docs_only",
|
|
1115
|
+
]);
|
|
1116
|
+
if (effectiveMode === "hard" && promptWorkContext.isAuthoritative) {
|
|
1117
|
+
checkpointRunner = getCheckpointRunnerForContext(promptWorkContext);
|
|
1118
|
+
checkpointContext = {
|
|
1119
|
+
workContext: promptWorkContext,
|
|
1120
|
+
config: cfg,
|
|
1121
|
+
filePath: normalizedFocusPath,
|
|
1122
|
+
maintenanceDegraded,
|
|
1123
|
+
lifecycleEvents: [
|
|
1124
|
+
{
|
|
1125
|
+
normalizedPath: normalizedFocusPath,
|
|
1126
|
+
lifecycle: pendingLifecycle.lifecycle,
|
|
1127
|
+
},
|
|
1128
|
+
],
|
|
1129
|
+
pathKinds: [focusPathKind],
|
|
1130
|
+
linkedEntityResults: [linkedEntityResult],
|
|
1131
|
+
e2eSignals: [e2eSignal],
|
|
1132
|
+
};
|
|
1133
|
+
checkpointEvidence = checkpointRunner.isCheckpointPassed(checkpointFingerprint, checkpointContext);
|
|
1134
|
+
}
|
|
956
1135
|
const reminderResult = deriveFileOperationReminder({
|
|
957
1136
|
normalizedPath: normalizedFocusPath,
|
|
958
1137
|
lifecycle: pendingLifecycle.lifecycle,
|
|
@@ -960,13 +1139,59 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
960
1139
|
linkedEntityResult,
|
|
961
1140
|
e2eSignal,
|
|
962
1141
|
currentSemanticRisk: effectiveRiskClass ?? "safe_docs_only",
|
|
963
|
-
posture: posture
|
|
1142
|
+
posture: promptWorkContext.posture,
|
|
1143
|
+
effectiveMode,
|
|
1144
|
+
resolvedContext: promptWorkContext,
|
|
1145
|
+
checkpointEvidence,
|
|
964
1146
|
});
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1147
|
+
if (reminderResult.policyDecision === "hard_block") {
|
|
1148
|
+
const policyResult = reminderResult.policyResult;
|
|
1149
|
+
hardGateBlock = {
|
|
1150
|
+
shownPaths: "shownPaths" in policyResult ? policyResult.shownPaths : [normalizedFocusPath],
|
|
1151
|
+
remainingCount: "remainingCount" in policyResult ? policyResult.remainingCount : 0,
|
|
1152
|
+
reason: "checkpoint_required",
|
|
1153
|
+
};
|
|
1154
|
+
hardGateConsumedPath = normalizedFocusPath;
|
|
1155
|
+
hardGateFingerprint = checkpointFingerprint;
|
|
1156
|
+
hardGateReminderKindsToMark = reminderResult.reminderKindsToMark;
|
|
1157
|
+
if (checkpointRunner && checkpointContext) {
|
|
1158
|
+
const checkpointContextWithGuidance = {
|
|
1159
|
+
...checkpointContext,
|
|
1160
|
+
hardGuidanceText: reminderResult.lifecycleReminder,
|
|
1161
|
+
};
|
|
1162
|
+
const request = checkpointRunner.requestCheckpoint(checkpointContextWithGuidance, checkpointFingerprint);
|
|
1163
|
+
if (request.kind === "requested") {
|
|
1164
|
+
void checkpointRunner
|
|
1165
|
+
.runCheckpoint(checkpointContextWithGuidance, checkpointFingerprint)
|
|
1166
|
+
.then((result) => {
|
|
1167
|
+
logger.info("smart-enforcement.checkpoint", {
|
|
1168
|
+
event: "smart_enforcement_checkpoint",
|
|
1169
|
+
fingerprint: checkpointFingerprint,
|
|
1170
|
+
result: result.kind,
|
|
1171
|
+
reason: "reason" in result.metadata
|
|
1172
|
+
? result.metadata.reason
|
|
1173
|
+
: undefined,
|
|
1174
|
+
});
|
|
1175
|
+
})
|
|
1176
|
+
.catch((error) => {
|
|
1177
|
+
logger.errorStructuredOnly("smart-enforcement.checkpoint-failed", {
|
|
1178
|
+
event: "smart_enforcement_checkpoint_failed",
|
|
1179
|
+
fingerprint: checkpointFingerprint,
|
|
1180
|
+
error: error instanceof Error
|
|
1181
|
+
? error.message
|
|
1182
|
+
: String(error),
|
|
1183
|
+
});
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
else {
|
|
1189
|
+
fileOperationReminder = {
|
|
1190
|
+
path: normalizedFocusPath,
|
|
1191
|
+
lifecycleReminder: reminderResult.lifecycleReminder,
|
|
1192
|
+
e2eReminder: reminderResult.e2eReminder,
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
970
1195
|
}
|
|
971
1196
|
}
|
|
972
1197
|
}
|
|
@@ -976,10 +1201,10 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
976
1201
|
workspaceHealth,
|
|
977
1202
|
hasRecentKbEdit,
|
|
978
1203
|
recentCommentSuggestion,
|
|
979
|
-
posture: posture
|
|
1204
|
+
posture: promptWorkContext.posture,
|
|
980
1205
|
cache,
|
|
981
|
-
workspaceRoot:
|
|
982
|
-
branch:
|
|
1206
|
+
workspaceRoot: promptWorkContext.worktreeRoot,
|
|
1207
|
+
branch: promptWorkContext.branch,
|
|
983
1208
|
completionReminder: cfg.guidance.smartEnforcement.completionReminder,
|
|
984
1209
|
maintenanceDegraded,
|
|
985
1210
|
degradedMode: cfg.guidance.smartEnforcement.degradedMode,
|
|
@@ -991,12 +1216,13 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
991
1216
|
...(fileOperationReminder !== undefined
|
|
992
1217
|
? { fileOperationReminder }
|
|
993
1218
|
: {}),
|
|
1219
|
+
...(hardGateBlock !== undefined ? { hardGateBlock } : {}),
|
|
994
1220
|
});
|
|
995
1221
|
logger.info("smart-enforcement.guidance", {
|
|
996
1222
|
event: "smart_enforcement_guidance",
|
|
997
1223
|
emitted: guidance.trim() !== "" && guidance.trim() !== SENTINEL,
|
|
998
|
-
posture: posture
|
|
999
|
-
posture_state: posture
|
|
1224
|
+
posture: promptWorkContext.posture,
|
|
1225
|
+
posture_state: promptWorkContext.posture,
|
|
1000
1226
|
guidance_action: guidance.trim() !== "" && guidance.trim() !== SENTINEL
|
|
1001
1227
|
? "emit"
|
|
1002
1228
|
: "skip",
|
|
@@ -1015,8 +1241,8 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
1015
1241
|
logger.info("smart-enforcement.completion-reminder", {
|
|
1016
1242
|
event: "smart_enforcement_completion_reminder",
|
|
1017
1243
|
risk_class: lastRiskClass,
|
|
1018
|
-
posture: posture
|
|
1019
|
-
posture_state: posture
|
|
1244
|
+
posture: promptWorkContext.posture,
|
|
1245
|
+
posture_state: promptWorkContext.posture,
|
|
1020
1246
|
guidance_action: "completion_reminder",
|
|
1021
1247
|
reminder: "kb_check",
|
|
1022
1248
|
static_degraded: posture.maintenanceDegraded,
|
|
@@ -1036,42 +1262,59 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
1036
1262
|
const e2eEmitted = e2eReminderText !== null && guidance.includes(e2eReminderText);
|
|
1037
1263
|
// Mark shown and log only for reminders that were actually emitted
|
|
1038
1264
|
if (lifecycleEmitted) {
|
|
1039
|
-
const kind =
|
|
1265
|
+
const kind = promptFileOperationState.peekPending(focusPathForConsume)
|
|
1266
|
+
?.lifecycle ===
|
|
1040
1267
|
"deleted"
|
|
1041
1268
|
? "kibi_delete"
|
|
1042
1269
|
: "kibi_write";
|
|
1043
|
-
|
|
1270
|
+
promptFileOperationState.markShown(focusPathForConsume, kind);
|
|
1044
1271
|
logger.info("smart-enforcement.file-operation-reminder", {
|
|
1045
1272
|
event: "smart_enforcement_file_operation_reminder",
|
|
1046
1273
|
file: focusPathForConsume,
|
|
1047
|
-
lifecycle:
|
|
1274
|
+
lifecycle: promptFileOperationState.peekPending(focusPathForConsume)
|
|
1048
1275
|
?.lifecycle ?? null,
|
|
1049
|
-
posture_state: posture
|
|
1276
|
+
posture_state: promptWorkContext.posture,
|
|
1050
1277
|
risk_class: effectiveRiskClass,
|
|
1051
1278
|
});
|
|
1052
1279
|
}
|
|
1053
1280
|
if (e2eEmitted) {
|
|
1054
|
-
const kind =
|
|
1281
|
+
const kind = promptFileOperationState.peekPending(focusPathForConsume)
|
|
1282
|
+
?.lifecycle ===
|
|
1055
1283
|
"deleted"
|
|
1056
1284
|
? "e2e_delete"
|
|
1057
1285
|
: "e2e_write";
|
|
1058
|
-
|
|
1059
|
-
const e2eSignalForLog = getE2eCoverageSignal(
|
|
1286
|
+
promptFileOperationState.markShown(focusPathForConsume, kind);
|
|
1287
|
+
const e2eSignalForLog = getE2eCoverageSignal(promptWorkContext.worktreeRoot, focusPathForConsume);
|
|
1060
1288
|
logger.info("smart-enforcement.e2e-reminder", {
|
|
1061
1289
|
event: "smart_enforcement_e2e_reminder",
|
|
1062
1290
|
file: focusPathForConsume,
|
|
1063
|
-
lifecycle:
|
|
1291
|
+
lifecycle: promptFileOperationState.peekPending(focusPathForConsume)
|
|
1064
1292
|
?.lifecycle ?? null,
|
|
1065
1293
|
signal_level: e2eSignalForLog.level,
|
|
1066
|
-
posture_state: posture
|
|
1294
|
+
posture_state: promptWorkContext.posture,
|
|
1067
1295
|
risk_class: effectiveRiskClass,
|
|
1068
1296
|
});
|
|
1069
1297
|
}
|
|
1070
1298
|
// Consume pending only if at least one reminder was emitted
|
|
1071
1299
|
if (lifecycleEmitted || e2eEmitted) {
|
|
1072
|
-
|
|
1300
|
+
promptFileOperationState.consumePending(focusPathForConsume);
|
|
1073
1301
|
}
|
|
1074
1302
|
}
|
|
1303
|
+
if (hardGateBlock &&
|
|
1304
|
+
hardGateConsumedPath &&
|
|
1305
|
+
guidance.includes("🛑 Kibi hard gate blocked")) {
|
|
1306
|
+
for (const kind of hardGateReminderKindsToMark) {
|
|
1307
|
+
promptFileOperationState.markShown(hardGateConsumedPath, kind);
|
|
1308
|
+
}
|
|
1309
|
+
promptFileOperationState.consumePending(hardGateConsumedPath);
|
|
1310
|
+
logger.info("smart-enforcement.hard-gate-consumed", {
|
|
1311
|
+
event: "smart_enforcement_hard_gate_consumed",
|
|
1312
|
+
file: hardGateConsumedPath,
|
|
1313
|
+
fingerprint: hardGateFingerprint ?? null,
|
|
1314
|
+
posture_state: promptWorkContext.posture,
|
|
1315
|
+
risk_class: effectiveRiskClass,
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1075
1318
|
// Latch degraded advisory warning-once state
|
|
1076
1319
|
if (showDegradedAdvisory && guidance.includes("Maintenance degraded")) {
|
|
1077
1320
|
degradedWarnedOnce = true;
|