kibi-opencode 0.13.0 → 0.14.0

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