kibi-opencode 0.19.4 → 2.0.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.
Files changed (38) hide show
  1. package/README.md +8 -6
  2. package/dist/auto-update.d.ts +2 -0
  3. package/dist/auto-update.js +5 -4
  4. package/dist/e2e-coverage-signals.js +3 -3
  5. package/dist/enforcement-policy.d.ts +10 -0
  6. package/dist/enforcement-policy.js +23 -23
  7. package/dist/file-entity-links.d.ts +4 -0
  8. package/dist/file-entity-links.js +6 -4
  9. package/dist/file-filter.d.ts +9 -9
  10. package/dist/file-filter.js +16 -68
  11. package/dist/kibi-bootstrap-alias.d.ts +2 -0
  12. package/dist/kibi-bootstrap-alias.js +14 -0
  13. package/dist/kibi-bootstrap-capability.d.ts +34 -0
  14. package/dist/{init-kibi-capability.js → kibi-bootstrap-capability.js} +26 -16
  15. package/dist/meaningful-change-classifier.d.ts +1 -0
  16. package/dist/meaningful-change-classifier.js +40 -10
  17. package/dist/path-kind.js +21 -39
  18. package/dist/plugin-startup.js +3 -3
  19. package/dist/plugin.d.ts +4 -1
  20. package/dist/plugin.js +81 -374
  21. package/dist/prompt.d.ts +4 -4
  22. package/dist/prompt.js +24 -60
  23. package/dist/repo-posture.js +31 -84
  24. package/dist/risk-classifier.d.ts +3 -3
  25. package/dist/risk-classifier.js +9 -6
  26. package/dist/scheduler.js +2 -2
  27. package/dist/session-edit-state.d.ts +4 -0
  28. package/dist/session-edit-state.js +5 -3
  29. package/dist/smart-enforcement-events.d.ts +100 -0
  30. package/dist/smart-enforcement-events.js +414 -0
  31. package/dist/version-metadata.json +1 -1
  32. package/dist/work-context-resolver.d.ts +10 -0
  33. package/dist/work-context-resolver.js +15 -17
  34. package/dist/workspace-health.js +8 -56
  35. package/package.json +6 -4
  36. package/dist/init-kibi-alias.d.ts +0 -14
  37. package/dist/init-kibi-alias.js +0 -43
  38. package/dist/init-kibi-capability.d.ts +0 -32
package/dist/path-kind.js CHANGED
@@ -2,15 +2,16 @@
2
2
  import path from "node:path";
3
3
  const CODE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".py"];
4
4
  const KB_PREFIX = ".kb";
5
- const KIBI_DOC_PATTERNS = [
6
- "requirements/**",
7
- "scenarios/**",
8
- "tests/**",
9
- "adr/**",
10
- "flags/**",
11
- "events/**",
12
- "facts/**",
13
- "symbols.yaml",
5
+ const KIBI_LANE_KINDS = [
6
+ { prefix: "requirements/", kind: "requirement" },
7
+ { prefix: "scenarios/", kind: "scenario" },
8
+ { prefix: "tests/", kind: "test" },
9
+ { prefix: "adr/", kind: "adr" },
10
+ { prefix: "flags/", kind: "flag" },
11
+ { prefix: "events/", kind: "event" },
12
+ { prefix: "facts/", kind: "fact" },
13
+ { prefix: "symbols.yaml", kind: "symbol" },
14
+ { prefix: "symbol-coordinates.yaml", kind: "symbol" },
14
15
  ];
15
16
  export function analyzePath(
16
17
  // implements REQ-opencode-kibi-plugin-v1
@@ -21,38 +22,19 @@ filePath, cwd = process.cwd()) {
21
22
  let kind = "unknown";
22
23
  let isUnderKb = false;
23
24
  let isKibiDocRelevant = false;
24
- // Check if under .kb/**
25
- if (rel.startsWith(`${KB_PREFIX}/`)) {
26
- kind = "kb";
27
- isUnderKb = true;
28
- }
29
- // Check for Kibi doc paths
30
25
  const normalized = rel.toLowerCase();
31
- for (const pattern of KIBI_DOC_PATTERNS) {
32
- const patternPrefix = pattern.replace(/\*\*/g, "");
33
- const fullPathPattern = `documentation/${patternPrefix}`;
34
- if (normalized.startsWith(fullPathPattern)) {
35
- isKibiDocRelevant = true;
36
- if (kind === "unknown") {
37
- // Map to specific kind based on path
38
- if (patternPrefix.includes("requirements"))
39
- kind = "requirement";
40
- else if (patternPrefix.includes("scenarios"))
41
- kind = "scenario";
42
- else if (patternPrefix.includes("tests"))
43
- kind = "test";
44
- else if (patternPrefix.includes("adr"))
45
- kind = "adr";
46
- else if (patternPrefix.includes("facts"))
47
- kind = "fact";
48
- else if (patternPrefix.includes("events"))
49
- kind = "event";
50
- else if (patternPrefix.includes("flags"))
51
- kind = "flag";
52
- else if (patternPrefix.includes("symbols"))
53
- kind = "symbol";
26
+ if (normalized.startsWith(`${KB_PREFIX}/`)) {
27
+ isUnderKb = true;
28
+ const rest = normalized.slice(KB_PREFIX.length + 1);
29
+ for (const lane of KIBI_LANE_KINDS) {
30
+ if (rest === lane.prefix || rest.startsWith(lane.prefix)) {
31
+ kind = lane.kind;
32
+ isKibiDocRelevant = true;
33
+ break;
54
34
  }
55
- break;
35
+ }
36
+ if (kind === "unknown") {
37
+ kind = "kb";
56
38
  }
57
39
  }
58
40
  if (kind === "unknown") {
@@ -23,7 +23,7 @@ export function resolveCurrentBranch(cwd) {
23
23
  // 1. Check KIBI_BRANCH env var first (highest precedence)
24
24
  const envBranch = process.env.KIBI_BRANCH?.trim();
25
25
  if (envBranch && envBranch.length > 0) {
26
- return envBranch === "master" ? "main" : envBranch;
26
+ return envBranch;
27
27
  }
28
28
  // 2. Fall back to git branch
29
29
  try {
@@ -33,7 +33,7 @@ export function resolveCurrentBranch(cwd) {
33
33
  stdio: ["ignore", "pipe", "ignore"],
34
34
  timeout: 5000,
35
35
  }).trim();
36
- return branch === "master" ? "main" : branch;
36
+ return branch;
37
37
  }
38
38
  catch {
39
39
  return "unknown";
@@ -41,7 +41,7 @@ export function resolveCurrentBranch(cwd) {
41
41
  }
42
42
  function readConfigFingerprint(cwd) {
43
43
  try {
44
- return fs.readFileSync(path.join(cwd, ".kb", "config.json"), "utf-8");
44
+ return fs.readFileSync(path.join(cwd, ".kb", "manifest.json"), "utf-8");
45
45
  }
46
46
  catch {
47
47
  return "missing";
package/dist/plugin.d.ts CHANGED
@@ -1,4 +1,7 @@
1
- import { type OpenCodeConfigHookInput } from "./init-kibi-capability.js";
1
+ import { type OpenCodeConfigHookInput } from "./kibi-bootstrap-capability.js";
2
+ export declare function resetCommentSuggestion(): null;
3
+ export declare function nextRecentCommentSuggestion<T>(inspectingCodeComments: boolean, suggestion: T | null | undefined): T | null;
4
+ export declare function adoptPrecomputedSuggestion<T>(recent: T | null, precomputed: T | null | undefined): T | null;
2
5
  export interface PluginInput {
3
6
  worktree: string;
4
7
  directory: string;
package/dist/plugin.js CHANGED
@@ -7,8 +7,8 @@ import { getFileLinkedEntityIds } from "./file-entity-links.js"; // implements R
7
7
  import * as fileFilter from "./file-filter.js";
8
8
  import { deriveFileOperationReminder } from "./file-operation-reminders.js"; // implements REQ-opencode-file-context-guidance-v1
9
9
  import { createFileOperationState, } from "./file-operation-state.js"; // implements REQ-opencode-file-context-guidance-v1
10
- import { getInitKibiCommandCapability, registerInitKibiCommand, } from "./init-kibi-capability.js";
11
10
  import { createKbFreshnessEvidenceStore, evaluateKbFreshness, } from "./kb-freshness-state.js";
11
+ import { getKibiBootstrapCommandCapability, registerKibiBootstrapCommand, } from "./kibi-bootstrap-capability.js";
12
12
  import { KibiCheckpointRunner, } from "./kibi-checkpoint-runner.js";
13
13
  import * as logger from "./logger.js";
14
14
  import { classifyMeaningfulChange } from "./meaningful-change-classifier.js";
@@ -20,10 +20,23 @@ import { classifyRisk } from "./risk-classifier.js";
20
20
  import { createSyncScheduler } from "./scheduler.js";
21
21
  import { createSessionEditState, } from "./session-edit-state.js";
22
22
  import { getSessionTracker } from "./session-tracker.js";
23
+ import { handleFileLifecycleEvent, handleKbToolEvent, isKbToolEventType, } from "./smart-enforcement-events.js";
23
24
  import { notifyStartup, } from "./startup-notifier.js";
24
25
  import { sendToast, } from "./toast.js";
25
26
  import { readKibiPackageVersions } from "./version-metadata.js";
26
27
  import { resolveWorkContext, } from "./work-context-resolver.js";
28
+ export function resetCommentSuggestion() {
29
+ return null;
30
+ }
31
+ // implements REQ-opencode-file-context-guidance-v1
32
+ export function nextRecentCommentSuggestion(inspectingCodeComments, suggestion) {
33
+ if (inspectingCodeComments && suggestion)
34
+ return suggestion;
35
+ return resetCommentSuggestion();
36
+ }
37
+ export function adoptPrecomputedSuggestion(recent, precomputed) {
38
+ return recent ?? precomputed ?? null;
39
+ }
27
40
  import * as fs from "node:fs";
28
41
  function deriveFileBucket(kind) {
29
42
  return kind;
@@ -100,21 +113,23 @@ const kibiOpencodePlugin = async (input) => {
100
113
  }
101
114
  const { cfg, workspaceHealth, posture, cache, runtimeOverlay, scheduler: startupScheduler, maintenanceDegraded, getMaintenanceDegraded, getEffectiveMode, latchRuntimeDegraded, } = startup;
102
115
  const hooks = {};
103
- const initKibiCommandCapability = getInitKibiCommandCapability();
104
- if (initKibiCommandCapability.supported) {
116
+ const kibiBootstrapCommandCapability = getKibiBootstrapCommandCapability();
117
+ if (kibiBootstrapCommandCapability.supported) {
105
118
  hooks.config = async (configInput) => {
106
- registerInitKibiCommand(configInput, initKibiCommandCapability);
119
+ registerKibiBootstrapCommand(configInput, kibiBootstrapCommandCapability);
107
120
  };
108
121
  }
109
122
  // Plugin instance state (not module globals)
110
123
  const MAX_RECENT_EDITS = 5;
111
- let recentEdits = [];
112
- let hasRecentKbEdit = false;
113
- let recentCommentSuggestion = null;
114
- const seenFingerprints = new Set(); // For deduplication
115
- let lastRiskClass = null;
116
- let lastRiskFilePath = null;
117
- let lastRiskScopeKey = null;
124
+ const enforcementState = {
125
+ recentEdits: [],
126
+ hasRecentKbEdit: false,
127
+ recentCommentSuggestion: null,
128
+ seenFingerprints: new Set(), // For deduplication
129
+ lastRiskClass: null,
130
+ lastRiskFilePath: null,
131
+ lastRiskScopeKey: null,
132
+ };
118
133
  const schedulerRegistry = new Map();
119
134
  if (startupScheduler) {
120
135
  schedulerRegistry.set(path.resolve(input.worktree), startupScheduler);
@@ -298,12 +313,14 @@ const kibiOpencodePlugin = async (input) => {
298
313
  }
299
314
  }
300
315
  function updateRecentEditsFromSession(sessionEdits, scopedPathKindCache) {
301
- recentEdits = sessionEdits.slice(-MAX_RECENT_EDITS).map((entry) => ({
316
+ enforcementState.recentEdits = sessionEdits
317
+ .slice(-MAX_RECENT_EDITS)
318
+ .map((entry) => ({
302
319
  path: entry.filePath,
303
320
  kind: scopedPathKindCache.get(entry.filePath) ?? "unknown",
304
321
  timestamp: entry.lastReconciledAt,
305
322
  }));
306
- return recentEdits;
323
+ return enforcementState.recentEdits;
307
324
  }
308
325
  function deriveRiskContext(context, filePath, scopedPathKindCache) {
309
326
  const normalizedFilePath = normalizeSessionPath(filePath, context.worktreeRoot);
@@ -329,11 +346,11 @@ const kibiOpencodePlugin = async (input) => {
329
346
  const effectiveRiskClass = riskClass === "safe_docs_only" && precomputedSuggestion
330
347
  ? "traceability_candidate"
331
348
  : riskClass;
332
- recentCommentSuggestion =
349
+ enforcementState.recentCommentSuggestion =
333
350
  pathAnalysis.kind === "code" ? precomputedSuggestion : null;
334
- lastRiskClass = effectiveRiskClass;
335
- lastRiskFilePath = normalizedFilePath;
336
- lastRiskScopeKey = buildRiskPathScopeKey(context, normalizedFilePath);
351
+ enforcementState.lastRiskClass = effectiveRiskClass;
352
+ enforcementState.lastRiskFilePath = normalizedFilePath;
353
+ enforcementState.lastRiskScopeKey = buildRiskPathScopeKey(context, normalizedFilePath);
337
354
  return {
338
355
  effectiveRiskClass,
339
356
  pathAnalysis,
@@ -341,354 +358,43 @@ const kibiOpencodePlugin = async (input) => {
341
358
  precomputedSuggestion,
342
359
  };
343
360
  }
361
+ const eventEnv = {
362
+ input,
363
+ cfg,
364
+ cache,
365
+ freshnessStore,
366
+ fileFilter,
367
+ log: logger,
368
+ rootWorkContext,
369
+ state: enforcementState,
370
+ resolveScopedWorkContext,
371
+ getSessionEditState,
372
+ getFileOperationState,
373
+ getPathKindCache,
374
+ getSchedulerForContext,
375
+ normalizeSessionPath,
376
+ resolveWorktreePath,
377
+ buildRiskPathScopeKey,
378
+ buildScopedCacheKey,
379
+ readFileContent,
380
+ updateRecentEditsFromSession,
381
+ recordWarning: (category, filePath, message) => getSessionTracker().recordWarning(category, filePath, message),
382
+ getMaintenanceDegraded,
383
+ getEffectiveMode,
384
+ runtimeOverlay,
385
+ posture,
386
+ lintRequirementDoc,
387
+ };
344
388
  hooks.event = async ({ event }) => {
345
- // Observe KB tool execution events for freshness evidence (best-effort)
346
- const TOOL_EVENT_TYPES = new Set([
347
- "tool.execute.after",
348
- "tool.executed",
349
- "tool.call.completed",
350
- "tool.Execute.after",
351
- "tool.Call.completed",
352
- ]);
353
- if (TOOL_EVENT_TYPES.has(event.type)) {
354
- const props = event.properties ?? {};
355
- const toolName = (props.tool ??
356
- props.toolName ??
357
- props.name ??
358
- props.call?.name ??
359
- props.input?.tool);
360
- if (typeof toolName === "string" && toolName.startsWith("kb_")) {
361
- const scope = {
362
- ...(input.sessionId !== undefined
363
- ? { sessionId: input.sessionId }
364
- : {}),
365
- agentIdentity: input.agentIdentity ?? "unknown",
366
- worktree: input.worktree,
367
- branch: rootWorkContext.branch,
368
- fingerprint: `${input.sessionId ?? ""}-${rootWorkContext.branch}`,
369
- };
370
- try {
371
- freshnessStore.recordToolEvidence(scope, toolName);
372
- logger.info("kb-freshness.tool-evidence", {
373
- event: "kb_freshness_tool_evidence",
374
- tool: toolName,
375
- });
376
- }
377
- catch {
378
- // best-effort, never crash the event handler
379
- }
380
- }
381
- return; // tool events don't need file lifecycle processing
382
- }
383
- // Accept file.created, file.edited, and file.deleted lifecycle events
384
- const isFileLifecycle = event.type === "file.created" ||
385
- event.type === "file.edited" ||
386
- event.type === "file.deleted";
387
- if (!isFileLifecycle)
388
- return;
389
- const filePath = event
390
- .properties.file;
391
- if (!filePath)
392
- return;
393
- const eventContext = resolveScopedWorkContext(filePath);
394
- const scopedSessionEditState = getSessionEditState(eventContext);
395
- const scopedFileOperationState = getFileOperationState(eventContext);
396
- const scopedPathKindCache = getPathKindCache(eventContext);
397
- const scopedScheduler = getSchedulerForContext(eventContext);
398
- const normalizedFilePath = normalizeSessionPath(filePath, eventContext.worktreeRoot);
399
- // Record lifecycle event into file-operation-state // implements REQ-opencode-file-context-guidance-v1
400
- const lifecycle = event.type === "file.created"
401
- ? "created"
402
- : event.type === "file.deleted"
403
- ? "deleted"
404
- : "edited";
405
- scopedFileOperationState.recordLifecycle(filePath, lifecycle, Date.now());
406
- scopedFileOperationState.normalizePath(filePath);
407
- const pathAnalysis = analyzePath(normalizedFilePath, eventContext.worktreeRoot);
408
- // For file.deleted: derive path kind without reading content, classify for reminder routing only
409
- if (lifecycle === "deleted") {
410
- // Preserve last known semantic risk if path was already tracked during session
411
- const lastKnownKind = scopedPathKindCache.get(normalizedFilePath);
412
- if (lastKnownKind) {
413
- // Path was tracked — preserve last known semantic risk for reminder routing
414
- scopedPathKindCache.set(normalizedFilePath, pathAnalysis.kind);
415
- }
416
- else {
417
- // Not tracked — classify only for reminder routing.
418
- scopedPathKindCache.set(normalizedFilePath, pathAnalysis.kind);
419
- }
420
- scopedSessionEditState.recordEventHint(normalizedFilePath, pathAnalysis.kind, Date.now());
421
- scopedSessionEditState.reconcilePath(normalizedFilePath);
422
- const sessionEdits = scopedSessionEditState.getSessionEdits();
423
- updateRecentEditsFromSession(sessionEdits, scopedPathKindCache);
424
- // Schedule background sync for deleted files that pass shouldHandleFile // implements REQ-opencode-file-context-guidance-v1
425
- if (cfg.sync.enabled &&
426
- scopedScheduler &&
427
- fileFilter.shouldHandleFile(normalizedFilePath, eventContext.worktreeRoot)) {
428
- scopedScheduler.scheduleSync("file.deleted", normalizedFilePath);
429
- }
430
- return;
431
- }
432
- scopedSessionEditState.recordEventHint(normalizedFilePath, pathAnalysis.kind, Date.now());
433
- scopedSessionEditState.reconcilePath(normalizedFilePath);
434
- scopedPathKindCache.set(normalizedFilePath, pathAnalysis.kind);
435
- const sessionEdits = scopedSessionEditState.getSessionEdits();
436
- const focusEdit = scopedSessionEditState.getFocusEdit();
437
- // Schedule background sync for file.created/file.edited that pass shouldHandleFile // implements REQ-opencode-file-context-guidance-v1
438
- if (cfg.sync.enabled &&
439
- scopedScheduler &&
440
- fileFilter.shouldHandleFile(normalizedFilePath, eventContext.worktreeRoot)) {
441
- scopedScheduler.scheduleSync(lifecycle === "created" ? "file.created" : "file.edited", normalizedFilePath);
442
- }
443
- const fileContent = readFileContent(normalizedFilePath, eventContext.worktreeRoot);
444
- const hasMustPriority = pathAnalysis.kind === "requirement"
445
- ? isMustPriorityRequirement(normalizedFilePath, eventContext.worktreeRoot)
446
- : false;
447
- let precomputedSuggestion = null;
448
- if (pathAnalysis.kind === "code" && cfg.guidance.commentDetection.enabled) {
449
- precomputedSuggestion = analyzeCodeFile(resolveWorktreePath(normalizedFilePath, eventContext.worktreeRoot), {
450
- minLines: cfg.guidance.commentDetection.minLines,
451
- });
452
- }
453
- const { riskClass } = classifyRisk({
454
- pathKind: pathAnalysis.kind,
455
- isUnderKb: pathAnalysis.isUnderKb,
456
- hasMustPriority,
457
- hasDurableComment: !!precomputedSuggestion,
458
- fileContent,
459
- });
460
- const effectiveRiskClass = riskClass === "safe_docs_only" && precomputedSuggestion
461
- ? "traceability_candidate"
462
- : riskClass;
463
- lastRiskClass = effectiveRiskClass;
464
- lastRiskFilePath = normalizedFilePath;
465
- lastRiskScopeKey = buildRiskPathScopeKey(eventContext, normalizedFilePath);
466
- logger.info("smart-enforcement.risk", {
467
- event: "smart_enforcement_risk",
468
- file: normalizedFilePath,
469
- path_kind: pathAnalysis.kind,
470
- risk_class: effectiveRiskClass,
471
- posture_state: eventContext.posture,
472
- maintenance_state: getMaintenanceDegraded()
473
- ? "maintenance_degraded"
474
- : "maintenance_available",
475
- under_kb: pathAnalysis.isUnderKb,
476
- has_must_priority: hasMustPriority,
477
- posture: eventContext.posture,
478
- reason_code: effectiveRiskClass,
479
- effective_mode: getEffectiveMode(),
480
- static_degraded: posture.maintenanceDegraded,
481
- runtime_degraded: runtimeOverlay.degraded,
482
- merged_degraded: getMaintenanceDegraded(),
483
- overlay_cause: runtimeOverlay.primaryCause ?? null,
484
- });
485
- const targetedChecksBlocked = getMaintenanceDegraded() ||
486
- runtimeOverlay.primaryCause === "sync_disabled" ||
487
- runtimeOverlay.primaryCause === "scheduler_unavailable" ||
488
- runtimeOverlay.primaryCause === "scheduler_sync_failed" ||
489
- runtimeOverlay.primaryCause === "scheduler_check_failed";
490
- if (!targetedChecksBlocked &&
491
- cfg.sync.enabled &&
492
- scopedScheduler &&
493
- cfg.guidance.targetedChecks.enabled) {
494
- const traceabilityRules = effectiveRiskClass === "traceability_candidate"
495
- ? ["symbol-traceability"]
496
- : null;
497
- const kbStructuralRules = effectiveRiskClass === "kb_doc_structural" &&
498
- fileFilter.shouldHandleFile(normalizedFilePath, eventContext.worktreeRoot)
499
- ? [
500
- "required-fields",
501
- "no-dangling-refs",
502
- ...(pathAnalysis.kind === "fact" ? ["strict-fact-shape"] : []),
503
- ...(pathAnalysis.kind === "requirement"
504
- ? ["strict-req-fact-pairing"]
505
- : []),
506
- ]
507
- : null;
508
- const checkRules = traceabilityRules ?? kbStructuralRules;
509
- if (checkRules) {
510
- logger.info("smart-enforcement.targeted-checks", {
511
- event: "smart_enforcement_targeted_checks",
512
- file: normalizedFilePath,
513
- risk_class: effectiveRiskClass,
514
- posture: eventContext.posture,
515
- posture_state: eventContext.posture,
516
- guidance_action: "targeted_checks",
517
- effective_mode: getEffectiveMode(),
518
- rules: checkRules,
519
- static_degraded: posture.maintenanceDegraded,
520
- runtime_degraded: runtimeOverlay.degraded,
521
- merged_degraded: getMaintenanceDegraded(),
522
- overlay_cause: runtimeOverlay.primaryCause ?? null,
523
- });
524
- logger.info(`kibi-opencode: scheduling sync for ${normalizedFilePath}`);
525
- scopedScheduler.scheduleSync(effectiveRiskClass === "traceability_candidate"
526
- ? "smart-enforcement.traceability"
527
- : "smart-enforcement.kb-doc", normalizedFilePath, checkRules);
528
- }
529
- }
530
- updateRecentEditsFromSession(sessionEdits, scopedPathKindCache);
531
- if (effectiveRiskClass === "safe_docs_only" ||
532
- effectiveRiskClass === "safe_test_only") {
533
- recentCommentSuggestion = null;
534
- return;
535
- }
536
- const cacheKey = buildScopedCacheKey(eventContext, effectiveRiskClass, deriveFileBucket(pathAnalysis.kind), [normalizedFilePath, pathAnalysis.kind, effectiveRiskClass]);
537
- // Always process manual_kb_edit before cache check — this is a critical safety signal
538
- if (effectiveRiskClass === "manual_kb_edit") {
539
- hasRecentKbEdit = true;
540
- if (cfg.guidance.warnOnKbEdits) {
541
- logger.warn(`kibi-opencode: .kb edit detected for ${normalizedFilePath}`);
542
- getSessionTracker().recordWarning("kb-edit", normalizedFilePath, `Manual .kb edit: ${normalizedFilePath}`);
543
- }
544
- return;
545
- }
546
- // Always emit requirement lint warnings before cache check — these are safety signals
547
- if (effectiveRiskClass === "req_policy_candidate") {
548
- const lintWarnings = lintRequirementDoc(normalizedFilePath, eventContext.worktreeRoot);
549
- for (const warning of lintWarnings) {
550
- getSessionTracker().recordWarning(warning.category, normalizedFilePath, warning.message);
551
- }
552
- }
553
- // Cache check: after critical signals have been emitted
554
- if (cache.isSatisfied(cacheKey)) {
555
- logger.info("smart-enforcement.cache", {
556
- event: "smart_enforcement_cache",
557
- cache_hit: true,
558
- cache_state: "hit",
559
- file: normalizedFilePath,
560
- risk_class: effectiveRiskClass,
561
- posture: eventContext.posture,
562
- posture_state: eventContext.posture,
563
- });
564
- return;
565
- }
566
- logger.info("smart-enforcement.cache", {
567
- event: "smart_enforcement_cache",
568
- cache_hit: false,
569
- cache_state: "miss",
570
- file: normalizedFilePath,
571
- risk_class: effectiveRiskClass,
572
- posture: eventContext.posture,
573
- posture_state: eventContext.posture,
574
- });
575
- if (effectiveRiskClass === "req_policy_candidate") {
576
- if (getMaintenanceDegraded()) {
577
- const logFn = cfg.guidance.smartEnforcement.degradedMode === "warn-once"
578
- ? logger.warn
579
- : logger.info;
580
- logFn("smart-enforcement.degraded", {
581
- event: "smart_enforcement_degraded",
582
- file: normalizedFilePath,
583
- risk_class: effectiveRiskClass,
584
- posture: eventContext.posture,
585
- posture_state: eventContext.posture,
586
- maintenance_state: getMaintenanceDegraded()
587
- ? "maintenance_degraded"
588
- : "maintenance_available",
589
- reason: runtimeOverlay.primaryCause ?? "non_authoritative_posture",
590
- reason_code: runtimeOverlay.primaryCause ?? "non_authoritative_posture",
591
- static_degraded: posture.maintenanceDegraded,
592
- runtime_degraded: runtimeOverlay.degraded,
593
- merged_degraded: getMaintenanceDegraded(),
594
- overlay_cause: runtimeOverlay.primaryCause ?? null,
595
- effective_mode: getEffectiveMode(),
596
- });
597
- }
598
- if (!getMaintenanceDegraded() &&
599
- cfg.sync.enabled &&
600
- scopedScheduler &&
601
- fileFilter.shouldHandleFile(normalizedFilePath, eventContext.worktreeRoot)) {
602
- let checkRules;
603
- if (cfg.guidance.targetedChecks.enabled) {
604
- if (hasMustPriority && getEffectiveMode() === "strict") {
605
- checkRules = [
606
- "required-fields",
607
- "no-dangling-refs",
608
- "must-priority-coverage",
609
- "strict-req-fact-pairing",
610
- ];
611
- logger.info(`kibi-opencode: must-priority requirement detected, scheduling elevated checks for ${normalizedFilePath}`);
612
- }
613
- else {
614
- checkRules = [
615
- "required-fields",
616
- "no-dangling-refs",
617
- "strict-req-fact-pairing",
618
- ];
619
- }
620
- }
621
- logger.info("smart-enforcement.targeted-checks", {
622
- event: "smart_enforcement_targeted_checks",
623
- file: normalizedFilePath,
624
- risk_class: effectiveRiskClass,
625
- posture: eventContext.posture,
626
- posture_state: eventContext.posture,
627
- guidance_action: "targeted_checks",
628
- effective_mode: getEffectiveMode(),
629
- rules: checkRules ?? [],
630
- static_degraded: posture.maintenanceDegraded,
631
- runtime_degraded: runtimeOverlay.degraded,
632
- merged_degraded: getMaintenanceDegraded(),
633
- overlay_cause: runtimeOverlay.primaryCause ?? null,
634
- });
635
- scopedScheduler.scheduleSync("file.edited", normalizedFilePath, checkRules);
636
- }
389
+ if (isKbToolEventType(event.type)) {
390
+ handleKbToolEvent(eventEnv, event, input.sessionId);
637
391
  return;
638
392
  }
639
- if (effectiveRiskClass === "kb_doc_structural") {
640
- if (getMaintenanceDegraded()) {
641
- const logFn = cfg.guidance.smartEnforcement.degradedMode === "warn-once"
642
- ? logger.warn
643
- : logger.info;
644
- logFn("smart-enforcement.degraded", {
645
- event: "smart_enforcement_degraded",
646
- file: normalizedFilePath,
647
- risk_class: effectiveRiskClass,
648
- posture: eventContext.posture,
649
- posture_state: eventContext.posture,
650
- maintenance_state: getMaintenanceDegraded()
651
- ? "maintenance_degraded"
652
- : "maintenance_available",
653
- reason: runtimeOverlay.primaryCause ?? "non_authoritative_posture",
654
- reason_code: runtimeOverlay.primaryCause ?? "non_authoritative_posture",
655
- static_degraded: posture.maintenanceDegraded,
656
- runtime_degraded: runtimeOverlay.degraded,
657
- merged_degraded: getMaintenanceDegraded(),
658
- overlay_cause: runtimeOverlay.primaryCause ?? null,
659
- effective_mode: getEffectiveMode(),
660
- });
661
- }
662
- return;
663
- }
664
- if (effectiveRiskClass === "behavior_candidate" ||
665
- effectiveRiskClass === "traceability_candidate") {
666
- if (pathAnalysis.kind === "code" &&
667
- cfg.guidance.commentDetection.enabled) {
668
- const suggestion = precomputedSuggestion;
669
- if (suggestion) {
670
- recentCommentSuggestion = suggestion;
671
- const dedupeKey = `${buildRiskPathScopeKey(eventContext, normalizedFilePath)}:${suggestion.suggestionType}:${suggestion.fingerprint}`;
672
- if (!seenFingerprints.has(dedupeKey)) {
673
- seenFingerprints.add(dedupeKey);
674
- const warningCategory = suggestion.suggestionType === "fact"
675
- ? "long-comment-missed-fact"
676
- : suggestion.suggestionType === "adr"
677
- ? "long-comment-missed-adr"
678
- : "missing-traceability";
679
- logger.warn(`kibi-opencode: detected durable ${suggestion.suggestionType} knowledge in ${normalizedFilePath}`);
680
- getSessionTracker().recordWarning(warningCategory, normalizedFilePath, `Consider routing this ${suggestion.suggestionType} knowledge to Kibi instead of inline comments: ${suggestion.reasoning}`);
681
- }
682
- }
683
- else {
684
- recentCommentSuggestion = null;
685
- }
686
- }
687
- else {
688
- recentCommentSuggestion = null;
689
- }
393
+ if (event.type === "file.created" ||
394
+ event.type === "file.edited" ||
395
+ event.type === "file.deleted") {
396
+ handleFileLifecycleEvent(eventEnv, event);
690
397
  }
691
- return;
692
398
  };
693
399
  if (cfg.prompt.enabled) {
694
400
  const hookMode = cfg.prompt.hookMode;
@@ -730,19 +436,20 @@ const kibiOpencodePlugin = async (input) => {
730
436
  const riskScopeKey = riskContextFilePath
731
437
  ? buildRiskPathScopeKey(promptWorkContext, riskContextFilePath)
732
438
  : null;
733
- let effectiveRiskClass = riskScopeKey !== null && lastRiskScopeKey === riskScopeKey
734
- ? lastRiskClass
439
+ let effectiveRiskClass = riskScopeKey !== null &&
440
+ enforcementState.lastRiskScopeKey === riskScopeKey
441
+ ? enforcementState.lastRiskClass
735
442
  : null;
736
443
  if (riskContextFilePath &&
737
- (lastRiskClass === null || lastRiskScopeKey !== riskScopeKey)) {
444
+ (enforcementState.lastRiskClass === null ||
445
+ enforcementState.lastRiskScopeKey !== riskScopeKey)) {
738
446
  const riskCtx = deriveRiskContext(promptWorkContext, riskContextFilePath, promptPathKindCache);
739
447
  effectiveRiskClass = riskCtx.effectiveRiskClass;
740
- if (!recentCommentSuggestion && riskCtx.precomputedSuggestion) {
741
- recentCommentSuggestion = riskCtx.precomputedSuggestion;
742
- }
448
+ enforcementState.recentCommentSuggestion = adoptPrecomputedSuggestion(enforcementState.recentCommentSuggestion, riskCtx.precomputedSuggestion);
743
449
  }
744
- if (effectiveRiskClass === null && lastRiskClass !== null) {
745
- effectiveRiskClass = lastRiskClass;
450
+ if (effectiveRiskClass === null &&
451
+ enforcementState.lastRiskClass !== null) {
452
+ effectiveRiskClass = enforcementState.lastRiskClass;
746
453
  }
747
454
  const promptFocusFilePath = transformFocusEdit?.filePath ?? transformFocusFilePath ?? undefined;
748
455
  // Steps 3-4: File-operation reminder selection with suppression // implements REQ-opencode-file-context-guidance-v1
@@ -900,8 +607,8 @@ const kibiOpencodePlugin = async (input) => {
900
607
  recentEdits: transformRecentEdits,
901
608
  focusEdit: transformPromptFocusEdit,
902
609
  workspaceHealth,
903
- hasRecentKbEdit,
904
- recentCommentSuggestion,
610
+ hasRecentKbEdit: enforcementState.hasRecentKbEdit,
611
+ recentCommentSuggestion: enforcementState.recentCommentSuggestion,
905
612
  posture: promptWorkContext.posture,
906
613
  cache,
907
614
  workspaceRoot: promptWorkContext.worktreeRoot,
@@ -934,8 +641,8 @@ const kibiOpencodePlugin = async (input) => {
934
641
  guidance_action: guidance.trim() !== "" && guidance.trim() !== SENTINEL
935
642
  ? "emit"
936
643
  : "skip",
937
- risk_class: lastRiskClass,
938
- recent_edits: recentEdits.length,
644
+ risk_class: enforcementState.lastRiskClass,
645
+ recent_edits: enforcementState.recentEdits.length,
939
646
  static_degraded: posture.maintenanceDegraded,
940
647
  runtime_degraded: runtimeOverlay.degraded,
941
648
  merged_degraded: maintenanceDegraded,
@@ -948,7 +655,7 @@ const kibiOpencodePlugin = async (input) => {
948
655
  guidance.includes(REMINDER_TEXT)) {
949
656
  logger.info("smart-enforcement.completion-reminder", {
950
657
  event: "smart_enforcement_completion_reminder",
951
- risk_class: lastRiskClass,
658
+ risk_class: enforcementState.lastRiskClass,
952
659
  posture: promptWorkContext.posture,
953
660
  posture_state: promptWorkContext.posture,
954
661
  guidance_action: "completion_reminder",