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/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
- const sessionEditState = createSessionEditState({ worktree: input.worktree });
149
- const fileOperationState = createFileOperationState({
150
- worktree: input.worktree,
151
- }); // implements REQ-opencode-file-context-guidance-v1
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 = new Map();
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(input.worktree, filePath);
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 input.worktree && !path.isAbsolute(filePath)
188
- ? path.join(input.worktree, filePath)
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: pathKindCache.get(entry.filePath) ?? "unknown",
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, input.worktree);
264
- pathKindCache.set(normalizedFilePath, pathAnalysis.kind);
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, input.worktree)
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: input.worktree,
299
- branch: currentBranch,
300
- directory: input.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 result = await generateIdleBrief(input.client, workspaceCtx, auditDelta, input.sessionId ?? "unknown", sourceFiles.length > 0
401
- ? { sourceFiles, changedEntityIds }
402
- : { changedEntityIds });
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
- fileOperationState.recordLifecycle(filePath, lifecycle, Date.now());
480
- fileOperationState.normalizePath(filePath);
481
- const pathAnalysis = analyzePath(filePath, input.worktree);
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 = pathKindCache.get(filePath);
673
+ const lastKnownKind = scopedPathKindCache.get(normalizedFilePath);
486
674
  if (lastKnownKind) {
487
675
  // Path was tracked — preserve last known semantic risk for reminder routing
488
- pathKindCache.set(filePath, pathAnalysis.kind);
676
+ scopedPathKindCache.set(normalizedFilePath, pathAnalysis.kind);
489
677
  }
490
678
  else {
491
679
  // Not tracked — classify only for reminder routing, not auto-briefing
492
- pathKindCache.set(filePath, pathAnalysis.kind);
680
+ scopedPathKindCache.set(normalizedFilePath, pathAnalysis.kind);
493
681
  }
494
- sessionEditState.recordEventHint(filePath, pathAnalysis.kind, Date.now());
495
- sessionEditState.reconcilePath(filePath);
496
- const sessionEdits = sessionEditState.getSessionEdits();
497
- recentEdits = sessionEdits.slice(-MAX_RECENT_EDITS).map((e) => ({
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
- scheduler &&
505
- fileFilter.shouldHandleFile(filePath, input.worktree)) {
506
- scheduler.scheduleSync("file.deleted", filePath);
688
+ scopedScheduler &&
689
+ fileFilter.shouldHandleFile(normalizedFilePath, eventContext.worktreeRoot)) {
690
+ scopedScheduler.scheduleSync("file.deleted", normalizedFilePath);
507
691
  }
508
692
  return;
509
693
  }
510
- sessionEditState.recordEventHint(filePath, pathAnalysis.kind, Date.now());
511
- sessionEditState.reconcilePath(filePath);
512
- pathKindCache.set(filePath, pathAnalysis.kind);
513
- const sessionEdits = sessionEditState.getSessionEdits();
514
- const focusEdit = sessionEditState.getFocusEdit();
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
- scheduler &&
518
- fileFilter.shouldHandleFile(filePath, input.worktree)) {
519
- scheduler.scheduleSync(lifecycle === "created" ? "file.created" : "file.edited", filePath);
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
- catch { }
705
+ const fileContent = readFileContent(normalizedFilePath, eventContext.worktreeRoot);
529
706
  const hasMustPriority = pathAnalysis.kind === "requirement"
530
- ? isMustPriorityRequirement(filePath, input.worktree)
707
+ ? isMustPriorityRequirement(normalizedFilePath, eventContext.worktreeRoot)
531
708
  : false;
532
709
  let precomputedSuggestion = null;
533
710
  if (pathAnalysis.kind === "code" && cfg.guidance.commentDetection.enabled) {
534
- const resolvedPath = input.worktree && !path.isAbsolute(filePath)
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: filePath,
732
+ file: normalizedFilePath,
557
733
  path_kind: pathAnalysis.kind,
558
734
  risk_class: effectiveRiskClass,
559
- posture_state: posture.state,
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.state,
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
- scheduler &&
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(filePath, input.worktree)
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: filePath,
776
+ file: normalizedFilePath,
601
777
  risk_class: effectiveRiskClass,
602
- posture: posture.state,
603
- posture_state: posture.state,
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 ${filePath}`);
613
- scheduler.scheduleSync(effectiveRiskClass === "traceability_candidate"
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", filePath, checkRules);
791
+ : "smart-enforcement.kb-doc", normalizedFilePath, checkRules);
616
792
  }
617
793
  }
618
- recentEdits = sessionEdits.slice(-MAX_RECENT_EDITS).map((e) => ({
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 ${filePath}`);
640
- getSessionTracker().recordWarning("kb-edit", filePath, `Manual .kb edit: ${filePath}`);
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(filePath, input.worktree);
812
+ const lintWarnings = lintRequirementDoc(normalizedFilePath, eventContext.worktreeRoot);
647
813
  for (const warning of lintWarnings) {
648
- getSessionTracker().recordWarning(warning.category, filePath, warning.message);
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: filePath,
823
+ file: normalizedFilePath,
658
824
  risk_class: effectiveRiskClass,
659
- posture: posture.state,
660
- posture_state: posture.state,
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: filePath,
836
+ file: normalizedFilePath,
671
837
  risk_class: effectiveRiskClass,
672
- posture: posture.state,
673
- posture_state: posture.state,
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: filePath,
848
+ file: normalizedFilePath,
683
849
  risk_class: effectiveRiskClass,
684
- posture: posture.state,
685
- posture_state: posture.state,
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
- scheduler &&
701
- fileFilter.shouldHandleFile(filePath, input.worktree)) {
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 ${filePath}`);
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: filePath,
889
+ file: normalizedFilePath,
724
890
  risk_class: effectiveRiskClass,
725
- posture: posture.state,
726
- posture_state: posture.state,
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
- scheduler?.scheduleSync("file.edited", filePath, checkRules);
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: filePath,
912
+ file: normalizedFilePath,
747
913
  risk_class: effectiveRiskClass,
748
- posture: posture.state,
749
- posture_state: posture.state,
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 = `${filePath}:${suggestion.suggestionType}:${suggestion.fingerprint}`;
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 ${filePath}`);
779
- getSessionTracker().recordWarning(warningCategory, filePath, `Consider routing this ${suggestion.suggestionType} knowledge to Kibi instead of inline comments: ${suggestion.reasoning}`);
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.state,
963
+ posture: briefingContext.posture,
797
964
  maintenanceDegraded: getMaintenanceDegraded(),
798
965
  sourceFiles: sessionSourceFiles,
799
966
  focusFilePath: focusEdit.filePath,
800
- worktreeRoot: input.worktree,
801
- branch: currentBranch,
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
- sessionEditState.reconcileKnownPaths();
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
- sessionEditState.forceEdit(transformFocusFilePath);
996
+ promptSessionEditState.forceEdit(normalizeSessionPath(transformFocusFilePath, promptWorkContext.worktreeRoot));
823
997
  }
824
- const transformSessionEdits = sessionEditState.getSessionEdits();
825
- const transformFocusEdit = sessionEditState.getFocusEdit();
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: pathKindCache.get(e.filePath) ?? "unknown",
1004
+ kind: promptPathKindCache.get(e.filePath) ?? "unknown",
831
1005
  }));
832
1006
  const transformPromptFocusEdit = transformFocusEdit
833
1007
  ? {
834
1008
  path: transformFocusEdit.filePath,
835
- kind: pathKindCache.get(transformFocusEdit.filePath) ?? "unknown",
1009
+ kind: promptPathKindCache.get(transformFocusEdit.filePath) ??
1010
+ "unknown",
836
1011
  }
837
1012
  : null;
838
1013
  const riskContextFilePath = transformFocusEdit?.filePath ?? transformFocusFilePath;
839
- let effectiveRiskClass = riskContextFilePath && lastRiskFilePath === riskContextFilePath
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 || lastRiskFilePath !== riskContextFilePath)) {
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.state,
1036
+ posture: promptWorkContext.posture,
859
1037
  maintenanceDegraded,
860
1038
  sourceFiles: promptSourceFiles,
861
- worktreeRoot: input.worktree,
862
- branch: currentBranch,
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, { skipIfCachedResultExists: true });
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 = fileOperationState.normalizePath(focusPathForReminder);
906
- const pendingLifecycle = fileOperationState.peekPending(normalizedFocusPath);
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) => !fileOperationState.hasShown(normalizedFocusPath, 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(input.worktree, focusPathForReminder);
918
- const e2eSignal = getE2eCoverageSignal(input.worktree, focusPathForReminder);
919
- const focusPathKind = pathKindCache.get(normalizedFocusPath) ?? "unknown";
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.state,
1142
+ posture: promptWorkContext.posture,
1143
+ effectiveMode,
1144
+ resolvedContext: promptWorkContext,
1145
+ checkpointEvidence,
928
1146
  });
929
- fileOperationReminder = {
930
- path: normalizedFocusPath,
931
- lifecycleReminder: reminderResult.lifecycleReminder,
932
- e2eReminder: reminderResult.e2eReminder,
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.state,
1204
+ posture: promptWorkContext.posture,
944
1205
  cache,
945
- workspaceRoot: input.worktree,
946
- branch: currentBranch,
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.state,
963
- posture_state: posture.state,
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.state,
983
- posture_state: posture.state,
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 = fileOperationState.peekPending(focusPathForConsume)?.lifecycle ===
1265
+ const kind = promptFileOperationState.peekPending(focusPathForConsume)
1266
+ ?.lifecycle ===
1004
1267
  "deleted"
1005
1268
  ? "kibi_delete"
1006
1269
  : "kibi_write";
1007
- fileOperationState.markShown(focusPathForConsume, kind);
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: fileOperationState.peekPending(focusPathForConsume)
1274
+ lifecycle: promptFileOperationState.peekPending(focusPathForConsume)
1012
1275
  ?.lifecycle ?? null,
1013
- posture_state: posture.state,
1276
+ posture_state: promptWorkContext.posture,
1014
1277
  risk_class: effectiveRiskClass,
1015
1278
  });
1016
1279
  }
1017
1280
  if (e2eEmitted) {
1018
- const kind = fileOperationState.peekPending(focusPathForConsume)?.lifecycle ===
1281
+ const kind = promptFileOperationState.peekPending(focusPathForConsume)
1282
+ ?.lifecycle ===
1019
1283
  "deleted"
1020
1284
  ? "e2e_delete"
1021
1285
  : "e2e_write";
1022
- fileOperationState.markShown(focusPathForConsume, kind);
1023
- const e2eSignalForLog = getE2eCoverageSignal(input.worktree, focusPathForConsume);
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: fileOperationState.peekPending(focusPathForConsume)
1291
+ lifecycle: promptFileOperationState.peekPending(focusPathForConsume)
1028
1292
  ?.lifecycle ?? null,
1029
1293
  signal_level: e2eSignalForLog.level,
1030
- posture_state: posture.state,
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
- fileOperationState.consumePending(focusPathForConsume);
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;