kibi-opencode 0.13.0 → 0.15.0

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