kibi-opencode 0.12.1 → 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/graph-narrator.d.ts +25 -0
- package/dist/graph-narrator.js +408 -0
- package/dist/guidance-cache.d.ts +3 -0
- package/dist/guidance-cache.js +1 -1
- package/dist/idle-brief-runtime.d.ts +7 -0
- package/dist/idle-brief-runtime.js +24 -6
- 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 +446 -167
- 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/utils/brief-marker.d.ts +19 -0
- package/dist/utils/brief-marker.js +101 -0
- package/dist/work-context-resolver.d.ts +21 -0
- package/dist/work-context-resolver.js +197 -0
- package/package.json +2 -2
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,12 +22,15 @@ 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";
|
|
27
30
|
import { sendToast, } from "./toast.js";
|
|
28
31
|
import { announceBriefTui, } from "./tui-brief-delivery.js";
|
|
32
|
+
import { deletePendingBriefMarkers, loadPendingBriefMarkers, } from "./utils/brief-marker.js";
|
|
33
|
+
import { resolveWorkContext, } from "./work-context-resolver.js";
|
|
29
34
|
import * as fs from "node:fs";
|
|
30
35
|
function deriveFileBucket(kind) {
|
|
31
36
|
return kind;
|
|
@@ -124,7 +129,7 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
124
129
|
if (!startup) {
|
|
125
130
|
return {};
|
|
126
131
|
}
|
|
127
|
-
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;
|
|
128
133
|
const hooks = {};
|
|
129
134
|
const initKibiCommandCapability = getInitKibiCommandCapability();
|
|
130
135
|
if (initKibiCommandCapability.supported) {
|
|
@@ -145,12 +150,150 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
145
150
|
const toastedFingerprints = new Set();
|
|
146
151
|
let lastRiskClass = null;
|
|
147
152
|
let lastRiskFilePath = null;
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
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);
|
|
152
295
|
let degradedWarnedOnce = false;
|
|
153
|
-
const pathKindCache =
|
|
296
|
+
const pathKindCache = getPathKindCache(rootWorkContext);
|
|
154
297
|
// Idle-brief state — dedupe via semantic contentHash (persisted envelope is the delivery authority)
|
|
155
298
|
let idleBriefInFlight = false;
|
|
156
299
|
let idleBriefTrailingRerun = false;
|
|
@@ -176,18 +319,21 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
176
319
|
sessionBaselineCursor = nextState.cursor;
|
|
177
320
|
}
|
|
178
321
|
syncSessionBaseline(currentBranch);
|
|
179
|
-
function normalizeSessionPath(filePath) {
|
|
322
|
+
function normalizeSessionPath(filePath, worktree = input.worktree) {
|
|
180
323
|
if (path.isAbsolute(filePath)) {
|
|
181
|
-
const relativePath = path.relative(
|
|
324
|
+
const relativePath = path.relative(worktree, filePath);
|
|
182
325
|
return relativePath.startsWith("..") ? filePath : relativePath;
|
|
183
326
|
}
|
|
184
327
|
return filePath;
|
|
185
328
|
}
|
|
186
|
-
function resolveWorktreePath(filePath) {
|
|
187
|
-
return
|
|
188
|
-
? path.join(
|
|
329
|
+
function resolveWorktreePath(filePath, worktree = input.worktree) {
|
|
330
|
+
return worktree && !path.isAbsolute(filePath)
|
|
331
|
+
? path.join(worktree, filePath)
|
|
189
332
|
: filePath;
|
|
190
333
|
}
|
|
334
|
+
function buildRiskPathScopeKey(context, filePath) {
|
|
335
|
+
return `${buildStateScopeKey(context, "risk")}:${normalizeSessionPath(filePath, context.worktreeRoot)}`;
|
|
336
|
+
}
|
|
191
337
|
function getKbSnapshotFingerprint(worktree, branch) {
|
|
192
338
|
try {
|
|
193
339
|
const snapshotPath = path.join(worktree, ".kb", "branches", branch, "kb.rdf");
|
|
@@ -242,33 +388,33 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
242
388
|
}
|
|
243
389
|
return normalizeSessionPath(directPath);
|
|
244
390
|
}
|
|
245
|
-
function readFileContent(filePath) {
|
|
391
|
+
function readFileContent(filePath, worktree = input.worktree) {
|
|
246
392
|
try {
|
|
247
|
-
return fs.readFileSync(resolveWorktreePath(filePath), "utf-8");
|
|
393
|
+
return fs.readFileSync(resolveWorktreePath(filePath, worktree), "utf-8");
|
|
248
394
|
}
|
|
249
395
|
catch {
|
|
250
396
|
return "";
|
|
251
397
|
}
|
|
252
398
|
}
|
|
253
|
-
function updateRecentEditsFromSession(sessionEdits) {
|
|
399
|
+
function updateRecentEditsFromSession(sessionEdits, scopedPathKindCache) {
|
|
254
400
|
recentEdits = sessionEdits.slice(-MAX_RECENT_EDITS).map((entry) => ({
|
|
255
401
|
path: entry.filePath,
|
|
256
|
-
kind:
|
|
402
|
+
kind: scopedPathKindCache.get(entry.filePath) ?? "unknown",
|
|
257
403
|
timestamp: entry.lastReconciledAt,
|
|
258
404
|
}));
|
|
259
405
|
return recentEdits;
|
|
260
406
|
}
|
|
261
|
-
function deriveRiskContext(filePath) {
|
|
262
|
-
const normalizedFilePath = normalizeSessionPath(filePath);
|
|
263
|
-
const pathAnalysis = analyzePath(normalizedFilePath,
|
|
264
|
-
|
|
265
|
-
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);
|
|
266
412
|
const hasMustPriority = pathAnalysis.kind === "requirement"
|
|
267
|
-
? isMustPriorityRequirement(normalizedFilePath,
|
|
413
|
+
? isMustPriorityRequirement(normalizedFilePath, context.worktreeRoot)
|
|
268
414
|
: false;
|
|
269
415
|
let precomputedSuggestion = null;
|
|
270
416
|
if (pathAnalysis.kind === "code" && cfg.guidance.commentDetection.enabled) {
|
|
271
|
-
precomputedSuggestion = analyzeCodeFile(resolveWorktreePath(normalizedFilePath), {
|
|
417
|
+
precomputedSuggestion = analyzeCodeFile(resolveWorktreePath(normalizedFilePath, context.worktreeRoot), {
|
|
272
418
|
minLines: cfg.guidance.commentDetection.minLines,
|
|
273
419
|
});
|
|
274
420
|
}
|
|
@@ -286,6 +432,7 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
286
432
|
pathAnalysis.kind === "code" ? precomputedSuggestion : null;
|
|
287
433
|
lastRiskClass = effectiveRiskClass;
|
|
288
434
|
lastRiskFilePath = normalizedFilePath;
|
|
435
|
+
lastRiskScopeKey = buildRiskPathScopeKey(context, normalizedFilePath);
|
|
289
436
|
return {
|
|
290
437
|
effectiveRiskClass,
|
|
291
438
|
pathAnalysis,
|
|
@@ -293,17 +440,17 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
293
440
|
precomputedSuggestion,
|
|
294
441
|
};
|
|
295
442
|
}
|
|
296
|
-
function buildBriefingWorkspaceContext() {
|
|
443
|
+
function buildBriefingWorkspaceContext(context = rootWorkContext, branch = context.branch) {
|
|
297
444
|
return {
|
|
298
|
-
workspaceRoot:
|
|
299
|
-
branch
|
|
300
|
-
directory:
|
|
445
|
+
workspaceRoot: context.worktreeRoot,
|
|
446
|
+
branch,
|
|
447
|
+
directory: context.worktreeRoot,
|
|
301
448
|
...(input.workspace !== undefined ? { workspace: input.workspace } : {}),
|
|
302
449
|
};
|
|
303
450
|
}
|
|
304
|
-
function buildWorkspaceContextForBranch(branch) {
|
|
451
|
+
function buildWorkspaceContextForBranch(branch, context = rootWorkContext) {
|
|
305
452
|
return {
|
|
306
|
-
...buildBriefingWorkspaceContext(),
|
|
453
|
+
...buildBriefingWorkspaceContext(context),
|
|
307
454
|
branch,
|
|
308
455
|
};
|
|
309
456
|
}
|
|
@@ -311,8 +458,8 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
311
458
|
if (!intentResult.eligible ||
|
|
312
459
|
!input.client ||
|
|
313
460
|
getMaintenanceDegraded() ||
|
|
314
|
-
(posture.state !== "root_active" &&
|
|
315
|
-
posture.state !== "hybrid_root_plus_vendored")) {
|
|
461
|
+
((options.postureState ?? posture.state) !== "root_active" &&
|
|
462
|
+
(options.postureState ?? posture.state) !== "hybrid_root_plus_vendored")) {
|
|
316
463
|
return;
|
|
317
464
|
}
|
|
318
465
|
if (options.skipIfCachedResultExists === true &&
|
|
@@ -321,7 +468,7 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
321
468
|
}
|
|
322
469
|
const client = input.client;
|
|
323
470
|
const fingerprint = intentResult.fingerprint;
|
|
324
|
-
const workspaceCtx = buildBriefingWorkspaceContext();
|
|
471
|
+
const workspaceCtx = options.workspaceCtx ?? buildBriefingWorkspaceContext();
|
|
325
472
|
void fetchBriefingResult(client, workspaceCtx, intentResult).then((result) => {
|
|
326
473
|
autoBriefResults.set(fingerprint, result);
|
|
327
474
|
if (!toastedFingerprints.has(fingerprint)) {
|
|
@@ -354,6 +501,17 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
354
501
|
// Gather session edits
|
|
355
502
|
const sessionEdits = sessionEditState.getSessionEdits();
|
|
356
503
|
const sourceFiles = sessionEdits.map((e) => e.filePath);
|
|
504
|
+
const markerResult = loadPendingBriefMarkers(idleWorkspaceRoot, idleBranch);
|
|
505
|
+
for (const issue of markerResult.issues) {
|
|
506
|
+
logger.warn("idle-brief.marker-invalid", {
|
|
507
|
+
event: "idle_brief_marker_invalid",
|
|
508
|
+
branch: idleBranch,
|
|
509
|
+
filePath: issue.filePath,
|
|
510
|
+
reason: issue.reason,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
const markerEntityIds = markerResult.entityIds;
|
|
514
|
+
const markerRelationships = markerResult.relationships;
|
|
357
515
|
const snapshotBeforeSync = getKbSnapshotFingerprint(idleWorkspaceRoot, idleBranch);
|
|
358
516
|
if (scheduler) {
|
|
359
517
|
const idleSyncBlocked = runtimeOverlay.primaryCause === "scheduler_sync_failed";
|
|
@@ -397,9 +555,33 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
397
555
|
...reconciled.modified.map((e) => e.id),
|
|
398
556
|
...reconciled.removed.map((e) => e.id),
|
|
399
557
|
];
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
558
|
+
const mergedChangedEntityIds = [
|
|
559
|
+
...new Set([...changedEntityIds, ...markerEntityIds]),
|
|
560
|
+
];
|
|
561
|
+
const mergedSourceFiles = [...new Set([...sourceFiles, ...markerEntityIds])];
|
|
562
|
+
const result = await generateIdleBrief(input.client, workspaceCtx, auditDelta, input.sessionId ?? "unknown", mergedSourceFiles.length > 0
|
|
563
|
+
? {
|
|
564
|
+
sourceFiles: mergedSourceFiles,
|
|
565
|
+
changedEntityIds: mergedChangedEntityIds,
|
|
566
|
+
relationships: markerRelationships,
|
|
567
|
+
}
|
|
568
|
+
: mergedChangedEntityIds.length > 0
|
|
569
|
+
? {
|
|
570
|
+
changedEntityIds: mergedChangedEntityIds,
|
|
571
|
+
relationships: markerRelationships,
|
|
572
|
+
}
|
|
573
|
+
: undefined);
|
|
574
|
+
if (result.success) {
|
|
575
|
+
const deleteResult = await deletePendingBriefMarkers(markerResult.markerPaths);
|
|
576
|
+
for (const issue of deleteResult.issues) {
|
|
577
|
+
logger.warn("idle-brief.marker-delete-failed", {
|
|
578
|
+
event: "idle_brief_marker_delete_failed",
|
|
579
|
+
branch: idleBranch,
|
|
580
|
+
filePath: issue.filePath,
|
|
581
|
+
reason: issue.reason,
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
}
|
|
403
585
|
if (result.success && result.envelope) {
|
|
404
586
|
const envelope = result.envelope;
|
|
405
587
|
// Dedupe by semantic contentHash — persisted envelope is the delivery authority
|
|
@@ -470,71 +652,63 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
470
652
|
.properties.file;
|
|
471
653
|
if (!filePath)
|
|
472
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);
|
|
473
661
|
// Record lifecycle event into file-operation-state // implements REQ-opencode-file-context-guidance-v1
|
|
474
662
|
const lifecycle = event.type === "file.created"
|
|
475
663
|
? "created"
|
|
476
664
|
: event.type === "file.deleted"
|
|
477
665
|
? "deleted"
|
|
478
666
|
: "edited";
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
const pathAnalysis = analyzePath(
|
|
667
|
+
scopedFileOperationState.recordLifecycle(filePath, lifecycle, Date.now());
|
|
668
|
+
scopedFileOperationState.normalizePath(filePath);
|
|
669
|
+
const pathAnalysis = analyzePath(normalizedFilePath, eventContext.worktreeRoot);
|
|
482
670
|
// For file.deleted: derive path kind without reading content, classify for reminder routing only
|
|
483
671
|
if (lifecycle === "deleted") {
|
|
484
672
|
// Preserve last known semantic risk if path was already tracked during session
|
|
485
|
-
const lastKnownKind =
|
|
673
|
+
const lastKnownKind = scopedPathKindCache.get(normalizedFilePath);
|
|
486
674
|
if (lastKnownKind) {
|
|
487
675
|
// Path was tracked — preserve last known semantic risk for reminder routing
|
|
488
|
-
|
|
676
|
+
scopedPathKindCache.set(normalizedFilePath, pathAnalysis.kind);
|
|
489
677
|
}
|
|
490
678
|
else {
|
|
491
679
|
// Not tracked — classify only for reminder routing, not auto-briefing
|
|
492
|
-
|
|
680
|
+
scopedPathKindCache.set(normalizedFilePath, pathAnalysis.kind);
|
|
493
681
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
const sessionEdits =
|
|
497
|
-
|
|
498
|
-
path: e.filePath,
|
|
499
|
-
kind: pathKindCache.get(e.filePath) ?? "unknown",
|
|
500
|
-
timestamp: e.lastReconciledAt,
|
|
501
|
-
}));
|
|
682
|
+
scopedSessionEditState.recordEventHint(normalizedFilePath, pathAnalysis.kind, Date.now());
|
|
683
|
+
scopedSessionEditState.reconcilePath(normalizedFilePath);
|
|
684
|
+
const sessionEdits = scopedSessionEditState.getSessionEdits();
|
|
685
|
+
updateRecentEditsFromSession(sessionEdits, scopedPathKindCache);
|
|
502
686
|
// Schedule background sync for deleted files that pass shouldHandleFile // implements REQ-opencode-file-context-guidance-v1
|
|
503
687
|
if (cfg.sync.enabled &&
|
|
504
|
-
|
|
505
|
-
fileFilter.shouldHandleFile(
|
|
506
|
-
|
|
688
|
+
scopedScheduler &&
|
|
689
|
+
fileFilter.shouldHandleFile(normalizedFilePath, eventContext.worktreeRoot)) {
|
|
690
|
+
scopedScheduler.scheduleSync("file.deleted", normalizedFilePath);
|
|
507
691
|
}
|
|
508
692
|
return;
|
|
509
693
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
const sessionEdits =
|
|
514
|
-
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();
|
|
515
699
|
// Schedule background sync for file.created/file.edited that pass shouldHandleFile // implements REQ-opencode-file-context-guidance-v1
|
|
516
700
|
if (cfg.sync.enabled &&
|
|
517
|
-
|
|
518
|
-
fileFilter.shouldHandleFile(
|
|
519
|
-
|
|
520
|
-
}
|
|
521
|
-
let fileContent = "";
|
|
522
|
-
try {
|
|
523
|
-
const resolvedPath = input.worktree && !path.isAbsolute(filePath)
|
|
524
|
-
? path.join(input.worktree, filePath)
|
|
525
|
-
: filePath;
|
|
526
|
-
fileContent = fs.readFileSync(resolvedPath, "utf-8");
|
|
701
|
+
scopedScheduler &&
|
|
702
|
+
fileFilter.shouldHandleFile(normalizedFilePath, eventContext.worktreeRoot)) {
|
|
703
|
+
scopedScheduler.scheduleSync(lifecycle === "created" ? "file.created" : "file.edited", normalizedFilePath);
|
|
527
704
|
}
|
|
528
|
-
|
|
705
|
+
const fileContent = readFileContent(normalizedFilePath, eventContext.worktreeRoot);
|
|
529
706
|
const hasMustPriority = pathAnalysis.kind === "requirement"
|
|
530
|
-
? isMustPriorityRequirement(
|
|
707
|
+
? isMustPriorityRequirement(normalizedFilePath, eventContext.worktreeRoot)
|
|
531
708
|
: false;
|
|
532
709
|
let precomputedSuggestion = null;
|
|
533
710
|
if (pathAnalysis.kind === "code" && cfg.guidance.commentDetection.enabled) {
|
|
534
|
-
|
|
535
|
-
? path.join(input.worktree, filePath)
|
|
536
|
-
: filePath;
|
|
537
|
-
precomputedSuggestion = analyzeCodeFile(resolvedPath, {
|
|
711
|
+
precomputedSuggestion = analyzeCodeFile(resolveWorktreePath(normalizedFilePath, eventContext.worktreeRoot), {
|
|
538
712
|
minLines: cfg.guidance.commentDetection.minLines,
|
|
539
713
|
});
|
|
540
714
|
}
|
|
@@ -551,18 +725,20 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
551
725
|
const isAutoBriefRisk = effectiveRiskClass === "behavior_candidate" ||
|
|
552
726
|
effectiveRiskClass === "traceability_candidate";
|
|
553
727
|
lastRiskClass = effectiveRiskClass;
|
|
728
|
+
lastRiskFilePath = normalizedFilePath;
|
|
729
|
+
lastRiskScopeKey = buildRiskPathScopeKey(eventContext, normalizedFilePath);
|
|
554
730
|
logger.info("smart-enforcement.risk", {
|
|
555
731
|
event: "smart_enforcement_risk",
|
|
556
|
-
file:
|
|
732
|
+
file: normalizedFilePath,
|
|
557
733
|
path_kind: pathAnalysis.kind,
|
|
558
734
|
risk_class: effectiveRiskClass,
|
|
559
|
-
posture_state: posture
|
|
735
|
+
posture_state: eventContext.posture,
|
|
560
736
|
maintenance_state: getMaintenanceDegraded()
|
|
561
737
|
? "maintenance_degraded"
|
|
562
738
|
: "maintenance_available",
|
|
563
739
|
under_kb: pathAnalysis.isUnderKb,
|
|
564
740
|
has_must_priority: hasMustPriority,
|
|
565
|
-
posture: posture
|
|
741
|
+
posture: eventContext.posture,
|
|
566
742
|
reason_code: effectiveRiskClass,
|
|
567
743
|
effective_mode: getEffectiveMode(),
|
|
568
744
|
static_degraded: posture.maintenanceDegraded,
|
|
@@ -577,13 +753,13 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
577
753
|
runtimeOverlay.primaryCause === "scheduler_check_failed";
|
|
578
754
|
if (!targetedChecksBlocked &&
|
|
579
755
|
cfg.sync.enabled &&
|
|
580
|
-
|
|
756
|
+
scopedScheduler &&
|
|
581
757
|
cfg.guidance.targetedChecks.enabled) {
|
|
582
758
|
const traceabilityRules = effectiveRiskClass === "traceability_candidate"
|
|
583
759
|
? ["symbol-traceability"]
|
|
584
760
|
: null;
|
|
585
761
|
const kbStructuralRules = effectiveRiskClass === "kb_doc_structural" &&
|
|
586
|
-
fileFilter.shouldHandleFile(
|
|
762
|
+
fileFilter.shouldHandleFile(normalizedFilePath, eventContext.worktreeRoot)
|
|
587
763
|
? [
|
|
588
764
|
"required-fields",
|
|
589
765
|
"no-dangling-refs",
|
|
@@ -597,10 +773,10 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
597
773
|
if (checkRules) {
|
|
598
774
|
logger.info("smart-enforcement.targeted-checks", {
|
|
599
775
|
event: "smart_enforcement_targeted_checks",
|
|
600
|
-
file:
|
|
776
|
+
file: normalizedFilePath,
|
|
601
777
|
risk_class: effectiveRiskClass,
|
|
602
|
-
posture: posture
|
|
603
|
-
posture_state: posture
|
|
778
|
+
posture: eventContext.posture,
|
|
779
|
+
posture_state: eventContext.posture,
|
|
604
780
|
guidance_action: "targeted_checks",
|
|
605
781
|
effective_mode: getEffectiveMode(),
|
|
606
782
|
rules: checkRules,
|
|
@@ -609,43 +785,33 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
609
785
|
merged_degraded: getMaintenanceDegraded(),
|
|
610
786
|
overlay_cause: runtimeOverlay.primaryCause ?? null,
|
|
611
787
|
});
|
|
612
|
-
logger.info(`kibi-opencode: scheduling sync for ${
|
|
613
|
-
|
|
788
|
+
logger.info(`kibi-opencode: scheduling sync for ${normalizedFilePath}`);
|
|
789
|
+
scopedScheduler.scheduleSync(effectiveRiskClass === "traceability_candidate"
|
|
614
790
|
? "smart-enforcement.traceability"
|
|
615
|
-
: "smart-enforcement.kb-doc",
|
|
791
|
+
: "smart-enforcement.kb-doc", normalizedFilePath, checkRules);
|
|
616
792
|
}
|
|
617
793
|
}
|
|
618
|
-
|
|
619
|
-
path: e.filePath,
|
|
620
|
-
kind: pathKindCache.get(e.filePath) ?? "unknown",
|
|
621
|
-
timestamp: e.lastReconciledAt,
|
|
622
|
-
}));
|
|
794
|
+
updateRecentEditsFromSession(sessionEdits, scopedPathKindCache);
|
|
623
795
|
if (effectiveRiskClass === "safe_docs_only" ||
|
|
624
796
|
effectiveRiskClass === "safe_test_only") {
|
|
625
797
|
recentCommentSuggestion = null;
|
|
626
798
|
return;
|
|
627
799
|
}
|
|
628
|
-
const cacheKey =
|
|
629
|
-
workspaceRoot: input.worktree,
|
|
630
|
-
branch: currentBranch,
|
|
631
|
-
posture: posture.state,
|
|
632
|
-
riskClass: effectiveRiskClass,
|
|
633
|
-
fileBucket: deriveFileBucket(pathAnalysis.kind),
|
|
634
|
-
};
|
|
800
|
+
const cacheKey = buildScopedCacheKey(eventContext, effectiveRiskClass, deriveFileBucket(pathAnalysis.kind), [normalizedFilePath, pathAnalysis.kind, effectiveRiskClass]);
|
|
635
801
|
// Always process manual_kb_edit before cache check — this is a critical safety signal
|
|
636
802
|
if (effectiveRiskClass === "manual_kb_edit") {
|
|
637
803
|
hasRecentKbEdit = true;
|
|
638
804
|
if (cfg.guidance.warnOnKbEdits) {
|
|
639
|
-
logger.warn(`kibi-opencode: .kb edit detected for ${
|
|
640
|
-
getSessionTracker().recordWarning("kb-edit",
|
|
805
|
+
logger.warn(`kibi-opencode: .kb edit detected for ${normalizedFilePath}`);
|
|
806
|
+
getSessionTracker().recordWarning("kb-edit", normalizedFilePath, `Manual .kb edit: ${normalizedFilePath}`);
|
|
641
807
|
}
|
|
642
808
|
return;
|
|
643
809
|
}
|
|
644
810
|
// Always emit requirement lint warnings before cache check — these are safety signals
|
|
645
811
|
if (effectiveRiskClass === "req_policy_candidate") {
|
|
646
|
-
const lintWarnings = lintRequirementDoc(
|
|
812
|
+
const lintWarnings = lintRequirementDoc(normalizedFilePath, eventContext.worktreeRoot);
|
|
647
813
|
for (const warning of lintWarnings) {
|
|
648
|
-
getSessionTracker().recordWarning(warning.category,
|
|
814
|
+
getSessionTracker().recordWarning(warning.category, normalizedFilePath, warning.message);
|
|
649
815
|
}
|
|
650
816
|
}
|
|
651
817
|
// Cache check: after critical signals have been emitted
|
|
@@ -654,10 +820,10 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
654
820
|
event: "smart_enforcement_cache",
|
|
655
821
|
cache_hit: true,
|
|
656
822
|
cache_state: "hit",
|
|
657
|
-
file:
|
|
823
|
+
file: normalizedFilePath,
|
|
658
824
|
risk_class: effectiveRiskClass,
|
|
659
|
-
posture: posture
|
|
660
|
-
posture_state: posture
|
|
825
|
+
posture: eventContext.posture,
|
|
826
|
+
posture_state: eventContext.posture,
|
|
661
827
|
});
|
|
662
828
|
if (!isAutoBriefRisk) {
|
|
663
829
|
return;
|
|
@@ -667,10 +833,10 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
667
833
|
event: "smart_enforcement_cache",
|
|
668
834
|
cache_hit: false,
|
|
669
835
|
cache_state: "miss",
|
|
670
|
-
file:
|
|
836
|
+
file: normalizedFilePath,
|
|
671
837
|
risk_class: effectiveRiskClass,
|
|
672
|
-
posture: posture
|
|
673
|
-
posture_state: posture
|
|
838
|
+
posture: eventContext.posture,
|
|
839
|
+
posture_state: eventContext.posture,
|
|
674
840
|
});
|
|
675
841
|
if (effectiveRiskClass === "req_policy_candidate") {
|
|
676
842
|
if (getMaintenanceDegraded()) {
|
|
@@ -679,10 +845,10 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
679
845
|
: logger.info;
|
|
680
846
|
logFn("smart-enforcement.degraded", {
|
|
681
847
|
event: "smart_enforcement_degraded",
|
|
682
|
-
file:
|
|
848
|
+
file: normalizedFilePath,
|
|
683
849
|
risk_class: effectiveRiskClass,
|
|
684
|
-
posture: posture
|
|
685
|
-
posture_state: posture
|
|
850
|
+
posture: eventContext.posture,
|
|
851
|
+
posture_state: eventContext.posture,
|
|
686
852
|
maintenance_state: getMaintenanceDegraded()
|
|
687
853
|
? "maintenance_degraded"
|
|
688
854
|
: "maintenance_available",
|
|
@@ -697,8 +863,8 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
697
863
|
}
|
|
698
864
|
if (!getMaintenanceDegraded() &&
|
|
699
865
|
cfg.sync.enabled &&
|
|
700
|
-
|
|
701
|
-
fileFilter.shouldHandleFile(
|
|
866
|
+
scopedScheduler &&
|
|
867
|
+
fileFilter.shouldHandleFile(normalizedFilePath, eventContext.worktreeRoot)) {
|
|
702
868
|
let checkRules;
|
|
703
869
|
if (cfg.guidance.targetedChecks.enabled) {
|
|
704
870
|
if (hasMustPriority && getEffectiveMode() === "strict") {
|
|
@@ -708,7 +874,7 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
708
874
|
"must-priority-coverage",
|
|
709
875
|
"strict-req-fact-pairing",
|
|
710
876
|
];
|
|
711
|
-
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}`);
|
|
712
878
|
}
|
|
713
879
|
else {
|
|
714
880
|
checkRules = [
|
|
@@ -720,10 +886,10 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
720
886
|
}
|
|
721
887
|
logger.info("smart-enforcement.targeted-checks", {
|
|
722
888
|
event: "smart_enforcement_targeted_checks",
|
|
723
|
-
file:
|
|
889
|
+
file: normalizedFilePath,
|
|
724
890
|
risk_class: effectiveRiskClass,
|
|
725
|
-
posture: posture
|
|
726
|
-
posture_state: posture
|
|
891
|
+
posture: eventContext.posture,
|
|
892
|
+
posture_state: eventContext.posture,
|
|
727
893
|
guidance_action: "targeted_checks",
|
|
728
894
|
effective_mode: getEffectiveMode(),
|
|
729
895
|
rules: checkRules ?? [],
|
|
@@ -732,7 +898,7 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
732
898
|
merged_degraded: getMaintenanceDegraded(),
|
|
733
899
|
overlay_cause: runtimeOverlay.primaryCause ?? null,
|
|
734
900
|
});
|
|
735
|
-
|
|
901
|
+
scopedScheduler.scheduleSync("file.edited", normalizedFilePath, checkRules);
|
|
736
902
|
}
|
|
737
903
|
return;
|
|
738
904
|
}
|
|
@@ -743,10 +909,10 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
743
909
|
: logger.info;
|
|
744
910
|
logFn("smart-enforcement.degraded", {
|
|
745
911
|
event: "smart_enforcement_degraded",
|
|
746
|
-
file:
|
|
912
|
+
file: normalizedFilePath,
|
|
747
913
|
risk_class: effectiveRiskClass,
|
|
748
|
-
posture: posture
|
|
749
|
-
posture_state: posture
|
|
914
|
+
posture: eventContext.posture,
|
|
915
|
+
posture_state: eventContext.posture,
|
|
750
916
|
maintenance_state: getMaintenanceDegraded()
|
|
751
917
|
? "maintenance_degraded"
|
|
752
918
|
: "maintenance_available",
|
|
@@ -767,7 +933,7 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
767
933
|
const suggestion = precomputedSuggestion;
|
|
768
934
|
if (suggestion) {
|
|
769
935
|
recentCommentSuggestion = suggestion;
|
|
770
|
-
const dedupeKey = `${
|
|
936
|
+
const dedupeKey = `${buildRiskPathScopeKey(eventContext, normalizedFilePath)}:${suggestion.suggestionType}:${suggestion.fingerprint}`;
|
|
771
937
|
if (!seenFingerprints.has(dedupeKey)) {
|
|
772
938
|
seenFingerprints.add(dedupeKey);
|
|
773
939
|
const warningCategory = suggestion.suggestionType === "fact"
|
|
@@ -775,8 +941,8 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
775
941
|
: suggestion.suggestionType === "adr"
|
|
776
942
|
? "long-comment-missed-adr"
|
|
777
943
|
: "missing-traceability";
|
|
778
|
-
logger.warn(`kibi-opencode: detected durable ${suggestion.suggestionType} knowledge in ${
|
|
779
|
-
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}`);
|
|
780
946
|
}
|
|
781
947
|
}
|
|
782
948
|
else {
|
|
@@ -791,16 +957,20 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
791
957
|
return;
|
|
792
958
|
}
|
|
793
959
|
const sessionSourceFiles = sessionEdits.map((e) => e.filePath);
|
|
960
|
+
const briefingContext = resolveScopedWorkContext(focusEdit.filePath);
|
|
794
961
|
const intentResult = computeBriefIntent({
|
|
795
962
|
riskClass: effectiveRiskClass,
|
|
796
|
-
posture: posture
|
|
963
|
+
posture: briefingContext.posture,
|
|
797
964
|
maintenanceDegraded: getMaintenanceDegraded(),
|
|
798
965
|
sourceFiles: sessionSourceFiles,
|
|
799
966
|
focusFilePath: focusEdit.filePath,
|
|
800
|
-
worktreeRoot:
|
|
801
|
-
branch:
|
|
967
|
+
worktreeRoot: briefingContext.worktreeRoot,
|
|
968
|
+
branch: briefingContext.branch,
|
|
969
|
+
});
|
|
970
|
+
queueBriefingFetch(intentResult, {
|
|
971
|
+
workspaceCtx: buildBriefingWorkspaceContext(briefingContext),
|
|
972
|
+
postureState: briefingContext.posture,
|
|
802
973
|
});
|
|
803
|
-
queueBriefingFetch(intentResult);
|
|
804
974
|
}
|
|
805
975
|
return;
|
|
806
976
|
};
|
|
@@ -817,31 +987,39 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
817
987
|
cfg.guidance.smartEnforcement.degradedMode === "warn-once" &&
|
|
818
988
|
!degradedWarnedOnce;
|
|
819
989
|
const transformFocusFilePath = getTransformFocusFilePath(transformInput);
|
|
820
|
-
|
|
990
|
+
const promptWorkContext = resolveScopedWorkContext(transformFocusFilePath ?? undefined);
|
|
991
|
+
const promptSessionEditState = getSessionEditState(promptWorkContext);
|
|
992
|
+
const promptFileOperationState = getFileOperationState(promptWorkContext);
|
|
993
|
+
const promptPathKindCache = getPathKindCache(promptWorkContext);
|
|
994
|
+
promptSessionEditState.reconcileKnownPaths();
|
|
821
995
|
if (transformFocusFilePath) {
|
|
822
|
-
|
|
996
|
+
promptSessionEditState.forceEdit(normalizeSessionPath(transformFocusFilePath, promptWorkContext.worktreeRoot));
|
|
823
997
|
}
|
|
824
|
-
const transformSessionEdits =
|
|
825
|
-
const transformFocusEdit =
|
|
998
|
+
const transformSessionEdits = promptSessionEditState.getSessionEdits();
|
|
999
|
+
const transformFocusEdit = promptSessionEditState.getFocusEdit();
|
|
826
1000
|
const transformRecentEdits = transformSessionEdits
|
|
827
1001
|
.slice(-MAX_RECENT_EDITS)
|
|
828
1002
|
.map((e) => ({
|
|
829
1003
|
path: e.filePath,
|
|
830
|
-
kind:
|
|
1004
|
+
kind: promptPathKindCache.get(e.filePath) ?? "unknown",
|
|
831
1005
|
}));
|
|
832
1006
|
const transformPromptFocusEdit = transformFocusEdit
|
|
833
1007
|
? {
|
|
834
1008
|
path: transformFocusEdit.filePath,
|
|
835
|
-
kind:
|
|
1009
|
+
kind: promptPathKindCache.get(transformFocusEdit.filePath) ??
|
|
1010
|
+
"unknown",
|
|
836
1011
|
}
|
|
837
1012
|
: null;
|
|
838
1013
|
const riskContextFilePath = transformFocusEdit?.filePath ?? transformFocusFilePath;
|
|
839
|
-
|
|
1014
|
+
const riskScopeKey = riskContextFilePath
|
|
1015
|
+
? buildRiskPathScopeKey(promptWorkContext, riskContextFilePath)
|
|
1016
|
+
: null;
|
|
1017
|
+
let effectiveRiskClass = riskScopeKey !== null && lastRiskScopeKey === riskScopeKey
|
|
840
1018
|
? lastRiskClass
|
|
841
1019
|
: null;
|
|
842
1020
|
if (riskContextFilePath &&
|
|
843
|
-
(lastRiskClass === null ||
|
|
844
|
-
const riskCtx = deriveRiskContext(riskContextFilePath);
|
|
1021
|
+
(lastRiskClass === null || lastRiskScopeKey !== riskScopeKey)) {
|
|
1022
|
+
const riskCtx = deriveRiskContext(promptWorkContext, riskContextFilePath, promptPathKindCache);
|
|
845
1023
|
effectiveRiskClass = riskCtx.effectiveRiskClass;
|
|
846
1024
|
if (!recentCommentSuggestion && riskCtx.precomputedSuggestion) {
|
|
847
1025
|
recentCommentSuggestion = riskCtx.precomputedSuggestion;
|
|
@@ -855,11 +1033,11 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
855
1033
|
const intentResult = effectiveRiskClass
|
|
856
1034
|
? computeBriefIntent({
|
|
857
1035
|
riskClass: effectiveRiskClass,
|
|
858
|
-
posture: posture
|
|
1036
|
+
posture: promptWorkContext.posture,
|
|
859
1037
|
maintenanceDegraded,
|
|
860
1038
|
sourceFiles: promptSourceFiles,
|
|
861
|
-
worktreeRoot:
|
|
862
|
-
branch:
|
|
1039
|
+
worktreeRoot: promptWorkContext.worktreeRoot,
|
|
1040
|
+
branch: promptWorkContext.branch,
|
|
863
1041
|
...(promptFocusFilePath !== undefined
|
|
864
1042
|
? {
|
|
865
1043
|
focusFilePath: promptFocusFilePath,
|
|
@@ -873,7 +1051,11 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
873
1051
|
const isAutoBriefRisk = effectiveRiskClass === "behavior_candidate" ||
|
|
874
1052
|
effectiveRiskClass === "traceability_candidate";
|
|
875
1053
|
if (!autoBriefResult && isAutoBriefRisk && intentResult) {
|
|
876
|
-
queueBriefingFetch(intentResult, {
|
|
1054
|
+
queueBriefingFetch(intentResult, {
|
|
1055
|
+
skipIfCachedResultExists: true,
|
|
1056
|
+
workspaceCtx: buildBriefingWorkspaceContext(promptWorkContext),
|
|
1057
|
+
postureState: promptWorkContext.posture,
|
|
1058
|
+
});
|
|
877
1059
|
}
|
|
878
1060
|
// Replay latest unread idle brief if available // implements REQ-opencode-kibi-briefing-v4
|
|
879
1061
|
if (input.worktree && currentBranch && input.client) {
|
|
@@ -900,10 +1082,14 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
900
1082
|
}
|
|
901
1083
|
// Steps 3-4: File-operation reminder selection with suppression // implements REQ-opencode-file-context-guidance-v1
|
|
902
1084
|
let fileOperationReminder;
|
|
1085
|
+
let hardGateBlock;
|
|
1086
|
+
let hardGateConsumedPath;
|
|
1087
|
+
let hardGateFingerprint;
|
|
1088
|
+
let hardGateReminderKindsToMark = [];
|
|
903
1089
|
const focusPathForReminder = transformFocusFilePath ?? promptFocusFilePath;
|
|
904
1090
|
if (focusPathForReminder) {
|
|
905
|
-
const normalizedFocusPath =
|
|
906
|
-
const pendingLifecycle =
|
|
1091
|
+
const normalizedFocusPath = promptFileOperationState.normalizePath(focusPathForReminder);
|
|
1092
|
+
const pendingLifecycle = promptFileOperationState.peekPending(normalizedFocusPath);
|
|
907
1093
|
if (pendingLifecycle) {
|
|
908
1094
|
// Check if any reminder kind for this lifecycle has not yet been shown
|
|
909
1095
|
const reminderKindsForLifecycle = pendingLifecycle.lifecycle === "deleted"
|
|
@@ -911,12 +1097,41 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
911
1097
|
: pendingLifecycle.lifecycle === "created"
|
|
912
1098
|
? ["kibi_write", "e2e_write"]
|
|
913
1099
|
: ["e2e_write"];
|
|
914
|
-
const hasUnshownReminder = reminderKindsForLifecycle.some((kind) => !
|
|
1100
|
+
const hasUnshownReminder = reminderKindsForLifecycle.some((kind) => !promptFileOperationState.hasShown(normalizedFocusPath, kind));
|
|
915
1101
|
if (hasUnshownReminder) {
|
|
916
1102
|
// Resolve linked entities and e2e signal
|
|
917
|
-
const linkedEntityResult = getFileLinkedEntityIds(
|
|
918
|
-
const e2eSignal = getE2eCoverageSignal(
|
|
919
|
-
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
|
+
}
|
|
920
1135
|
const reminderResult = deriveFileOperationReminder({
|
|
921
1136
|
normalizedPath: normalizedFocusPath,
|
|
922
1137
|
lifecycle: pendingLifecycle.lifecycle,
|
|
@@ -924,13 +1139,59 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
924
1139
|
linkedEntityResult,
|
|
925
1140
|
e2eSignal,
|
|
926
1141
|
currentSemanticRisk: effectiveRiskClass ?? "safe_docs_only",
|
|
927
|
-
posture: posture
|
|
1142
|
+
posture: promptWorkContext.posture,
|
|
1143
|
+
effectiveMode,
|
|
1144
|
+
resolvedContext: promptWorkContext,
|
|
1145
|
+
checkpointEvidence,
|
|
928
1146
|
});
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
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
|
+
}
|
|
934
1195
|
}
|
|
935
1196
|
}
|
|
936
1197
|
}
|
|
@@ -940,10 +1201,10 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
940
1201
|
workspaceHealth,
|
|
941
1202
|
hasRecentKbEdit,
|
|
942
1203
|
recentCommentSuggestion,
|
|
943
|
-
posture: posture
|
|
1204
|
+
posture: promptWorkContext.posture,
|
|
944
1205
|
cache,
|
|
945
|
-
workspaceRoot:
|
|
946
|
-
branch:
|
|
1206
|
+
workspaceRoot: promptWorkContext.worktreeRoot,
|
|
1207
|
+
branch: promptWorkContext.branch,
|
|
947
1208
|
completionReminder: cfg.guidance.smartEnforcement.completionReminder,
|
|
948
1209
|
maintenanceDegraded,
|
|
949
1210
|
degradedMode: cfg.guidance.smartEnforcement.degradedMode,
|
|
@@ -955,12 +1216,13 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
955
1216
|
...(fileOperationReminder !== undefined
|
|
956
1217
|
? { fileOperationReminder }
|
|
957
1218
|
: {}),
|
|
1219
|
+
...(hardGateBlock !== undefined ? { hardGateBlock } : {}),
|
|
958
1220
|
});
|
|
959
1221
|
logger.info("smart-enforcement.guidance", {
|
|
960
1222
|
event: "smart_enforcement_guidance",
|
|
961
1223
|
emitted: guidance.trim() !== "" && guidance.trim() !== SENTINEL,
|
|
962
|
-
posture: posture
|
|
963
|
-
posture_state: posture
|
|
1224
|
+
posture: promptWorkContext.posture,
|
|
1225
|
+
posture_state: promptWorkContext.posture,
|
|
964
1226
|
guidance_action: guidance.trim() !== "" && guidance.trim() !== SENTINEL
|
|
965
1227
|
? "emit"
|
|
966
1228
|
: "skip",
|
|
@@ -979,8 +1241,8 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
979
1241
|
logger.info("smart-enforcement.completion-reminder", {
|
|
980
1242
|
event: "smart_enforcement_completion_reminder",
|
|
981
1243
|
risk_class: lastRiskClass,
|
|
982
|
-
posture: posture
|
|
983
|
-
posture_state: posture
|
|
1244
|
+
posture: promptWorkContext.posture,
|
|
1245
|
+
posture_state: promptWorkContext.posture,
|
|
984
1246
|
guidance_action: "completion_reminder",
|
|
985
1247
|
reminder: "kb_check",
|
|
986
1248
|
static_degraded: posture.maintenanceDegraded,
|
|
@@ -1000,42 +1262,59 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
1000
1262
|
const e2eEmitted = e2eReminderText !== null && guidance.includes(e2eReminderText);
|
|
1001
1263
|
// Mark shown and log only for reminders that were actually emitted
|
|
1002
1264
|
if (lifecycleEmitted) {
|
|
1003
|
-
const kind =
|
|
1265
|
+
const kind = promptFileOperationState.peekPending(focusPathForConsume)
|
|
1266
|
+
?.lifecycle ===
|
|
1004
1267
|
"deleted"
|
|
1005
1268
|
? "kibi_delete"
|
|
1006
1269
|
: "kibi_write";
|
|
1007
|
-
|
|
1270
|
+
promptFileOperationState.markShown(focusPathForConsume, kind);
|
|
1008
1271
|
logger.info("smart-enforcement.file-operation-reminder", {
|
|
1009
1272
|
event: "smart_enforcement_file_operation_reminder",
|
|
1010
1273
|
file: focusPathForConsume,
|
|
1011
|
-
lifecycle:
|
|
1274
|
+
lifecycle: promptFileOperationState.peekPending(focusPathForConsume)
|
|
1012
1275
|
?.lifecycle ?? null,
|
|
1013
|
-
posture_state: posture
|
|
1276
|
+
posture_state: promptWorkContext.posture,
|
|
1014
1277
|
risk_class: effectiveRiskClass,
|
|
1015
1278
|
});
|
|
1016
1279
|
}
|
|
1017
1280
|
if (e2eEmitted) {
|
|
1018
|
-
const kind =
|
|
1281
|
+
const kind = promptFileOperationState.peekPending(focusPathForConsume)
|
|
1282
|
+
?.lifecycle ===
|
|
1019
1283
|
"deleted"
|
|
1020
1284
|
? "e2e_delete"
|
|
1021
1285
|
: "e2e_write";
|
|
1022
|
-
|
|
1023
|
-
const e2eSignalForLog = getE2eCoverageSignal(
|
|
1286
|
+
promptFileOperationState.markShown(focusPathForConsume, kind);
|
|
1287
|
+
const e2eSignalForLog = getE2eCoverageSignal(promptWorkContext.worktreeRoot, focusPathForConsume);
|
|
1024
1288
|
logger.info("smart-enforcement.e2e-reminder", {
|
|
1025
1289
|
event: "smart_enforcement_e2e_reminder",
|
|
1026
1290
|
file: focusPathForConsume,
|
|
1027
|
-
lifecycle:
|
|
1291
|
+
lifecycle: promptFileOperationState.peekPending(focusPathForConsume)
|
|
1028
1292
|
?.lifecycle ?? null,
|
|
1029
1293
|
signal_level: e2eSignalForLog.level,
|
|
1030
|
-
posture_state: posture
|
|
1294
|
+
posture_state: promptWorkContext.posture,
|
|
1031
1295
|
risk_class: effectiveRiskClass,
|
|
1032
1296
|
});
|
|
1033
1297
|
}
|
|
1034
1298
|
// Consume pending only if at least one reminder was emitted
|
|
1035
1299
|
if (lifecycleEmitted || e2eEmitted) {
|
|
1036
|
-
|
|
1300
|
+
promptFileOperationState.consumePending(focusPathForConsume);
|
|
1037
1301
|
}
|
|
1038
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
|
+
}
|
|
1039
1318
|
// Latch degraded advisory warning-once state
|
|
1040
1319
|
if (showDegradedAdvisory && guidance.includes("Maintenance degraded")) {
|
|
1041
1320
|
degradedWarnedOnce = true;
|