kibi-opencode 0.14.0 → 0.15.1

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 (62) hide show
  1. package/README.md +18 -12
  2. package/dist/config.d.ts +0 -3
  3. package/dist/config.js +0 -9
  4. package/dist/e2e-coverage-signals.js +1 -3
  5. package/dist/enforcement-policy.d.ts +2 -0
  6. package/dist/enforcement-policy.js +18 -3
  7. package/dist/enforcement-scope.js +1 -3
  8. package/dist/file-entity-links.js +2 -6
  9. package/dist/file-operation-reminders.d.ts +4 -4
  10. package/dist/file-operation-state.js +4 -2
  11. package/dist/init-kibi-alias.js +0 -1
  12. package/dist/init-kibi-capability.js +1 -5
  13. package/dist/kb-freshness-state.d.ts +62 -0
  14. package/dist/kb-freshness-state.js +215 -0
  15. package/dist/kibi-checkpoint-runner.d.ts +1 -1
  16. package/dist/kibi-checkpoint-runner.js +6 -3
  17. package/dist/knowledge-classifier.js +2 -1
  18. package/dist/meaningful-change-classifier.d.ts +30 -0
  19. package/dist/meaningful-change-classifier.js +103 -0
  20. package/dist/plugin-startup.js +0 -1
  21. package/dist/plugin.js +101 -391
  22. package/dist/prompt.d.ts +6 -5
  23. package/dist/prompt.js +48 -84
  24. package/dist/reconcile-engine.d.ts +15 -1
  25. package/dist/reconcile-engine.js +1 -3
  26. package/dist/scheduler.js +16 -4
  27. package/dist/session-edit-state.js +2 -1
  28. package/dist/source-linked-guidance.js +3 -1
  29. package/dist/startup-notifier.d.ts +7 -1
  30. package/dist/startup-notifier.js +51 -17
  31. package/dist/tui.js +1 -403
  32. package/dist/tui.jsx +1 -113
  33. package/dist/version-metadata.d.ts +13 -0
  34. package/dist/version-metadata.js +78 -0
  35. package/dist/version-metadata.json +1 -0
  36. package/package.json +5 -7
  37. package/dist/brief-delivery-reasons.d.ts +0 -12
  38. package/dist/brief-delivery-reasons.js +0 -132
  39. package/dist/brief-intent.d.ts +0 -41
  40. package/dist/brief-intent.js +0 -142
  41. package/dist/briefing-runtime.d.ts +0 -24
  42. package/dist/briefing-runtime.js +0 -277
  43. package/dist/graph-narrator.d.ts +0 -25
  44. package/dist/graph-narrator.js +0 -408
  45. package/dist/idle-brief-audit.d.ts +0 -36
  46. package/dist/idle-brief-audit.js +0 -186
  47. package/dist/idle-brief-paths.d.ts +0 -6
  48. package/dist/idle-brief-paths.js +0 -120
  49. package/dist/idle-brief-reader.d.ts +0 -37
  50. package/dist/idle-brief-reader.js +0 -163
  51. package/dist/idle-brief-runtime.d.ts +0 -55
  52. package/dist/idle-brief-runtime.js +0 -496
  53. package/dist/idle-brief-store.d.ts +0 -113
  54. package/dist/idle-brief-store.js +0 -262
  55. package/dist/session-fingerprint.d.ts +0 -11
  56. package/dist/session-fingerprint.js +0 -21
  57. package/dist/tui-brief-delivery.d.ts +0 -67
  58. package/dist/tui-brief-delivery.js +0 -279
  59. package/dist/tui-brief-view-model.d.ts +0 -63
  60. package/dist/tui-brief-view-model.js +0 -209
  61. package/dist/utils/brief-marker.d.ts +0 -19
  62. package/dist/utils/brief-marker.js +0 -101
package/dist/plugin.js CHANGED
@@ -1,7 +1,4 @@
1
1
  import * as path from "node:path";
2
- import { loadBriefConfig } from "kibi-cli/brief-config";
3
- import { computeBriefIntent } from "./brief-intent.js";
4
- import { fetchBriefingResult, } from "./briefing-runtime.js";
5
2
  import { analyzeCodeFile, } from "./comment-analysis.js";
6
3
  import { getE2eCoverageSignal } from "./e2e-coverage-signals.js"; // implements REQ-opencode-file-context-guidance-v1
7
4
  import { buildDirtyRelevantFingerprint, buildEnforcementScopeKey, } from "./enforcement-scope.js";
@@ -9,56 +6,26 @@ import { getFileLinkedEntityIds } from "./file-entity-links.js"; // implements R
9
6
  import * as fileFilter from "./file-filter.js";
10
7
  import { deriveFileOperationReminder } from "./file-operation-reminders.js"; // implements REQ-opencode-file-context-guidance-v1
11
8
  import { createFileOperationState, } from "./file-operation-state.js"; // implements REQ-opencode-file-context-guidance-v1
12
- import { KibiCheckpointRunner, } from "./kibi-checkpoint-runner.js";
13
9
  import { getInitKibiCommandCapability, registerInitKibiCommand, } from "./init-kibi-capability.js";
14
- import { computeAuditDelta, getAuditTailCursor, guardBranchChanged, } from "./idle-brief-audit.js";
15
- import { hasTuiSeenBrief, selectLatestUnreadBrief, } from "./idle-brief-reader.js";
16
- import { generateIdleBrief } from "./idle-brief-runtime.js";
10
+ import { createKbFreshnessEvidenceStore, evaluateKbFreshness, } from "./kb-freshness-state.js";
11
+ import { KibiCheckpointRunner, } from "./kibi-checkpoint-runner.js";
17
12
  import * as logger from "./logger.js";
13
+ import { classifyMeaningfulChange } from "./meaningful-change-classifier.js";
18
14
  import { analyzePath } from "./path-kind.js";
19
15
  import { runPluginStartup } from "./plugin-startup.js";
20
- import { resolveCurrentBranch } from "./plugin-startup.js";
21
16
  import { SENTINEL, buildPrompt } from "./prompt.js";
22
- import { reconcileAuditEntries } from "./reconcile-engine.js";
23
17
  import { isMustPriorityRequirement } from "./requirement-doc.js";
24
18
  import { classifyRisk } from "./risk-classifier.js";
25
19
  import { createSyncScheduler } from "./scheduler.js";
26
20
  import { createSessionEditState, } from "./session-edit-state.js";
27
- import { syncSessionBaselineState, } from "./session-fingerprint.js";
28
21
  import { getSessionTracker } from "./session-tracker.js";
29
22
  import { notifyStartup, } from "./startup-notifier.js";
30
- import { sendToast, } from "./toast.js";
31
- import { announceBriefTui, } from "./tui-brief-delivery.js";
32
- import { deletePendingBriefMarkers, loadPendingBriefMarkers, } from "./utils/brief-marker.js";
23
+ import { readKibiPackageVersions } from "./version-metadata.js";
33
24
  import { resolveWorkContext, } from "./work-context-resolver.js";
34
25
  import * as fs from "node:fs";
35
26
  function deriveFileBucket(kind) {
36
27
  return kind;
37
28
  }
38
- function resolveIdleBriefDeliveryDelayMs(worktree) {
39
- const envValue = Number(process.env.KIBI_OPENCODE_IDLE_BRIEF_DELAY_MS);
40
- if (Number.isFinite(envValue) && envValue >= 0) {
41
- return Math.min(60_000, Math.trunc(envValue));
42
- }
43
- const sharedPolicy = loadBriefConfig(worktree);
44
- const configValue = Number(sharedPolicy.tui?.idleDelayMs ?? 1500);
45
- if (!Number.isFinite(configValue))
46
- return 1500;
47
- if (configValue < 0)
48
- return 0;
49
- return Math.min(60_000, Math.trunc(configValue));
50
- }
51
- function readKibiOpencodePackageVersion() {
52
- try {
53
- const packageJson = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
54
- return typeof packageJson.version === "string"
55
- ? packageJson.version
56
- : undefined;
57
- }
58
- catch {
59
- return undefined;
60
- }
61
- }
62
29
  const startupNotifyGlobals = globalThis;
63
30
  /**
64
31
  * Lint requirement documents for embedded scenarios/tests and oversized content.
@@ -129,7 +96,7 @@ const kibiOpencodePlugin = async (input) => {
129
96
  if (!startup) {
130
97
  return {};
131
98
  }
132
- const { cfg, workspaceHealth, posture, currentBranch, cache, runtimeOverlay, scheduler: startupScheduler, maintenanceDegraded, getMaintenanceDegraded, getEffectiveMode, latchRuntimeDegraded, } = startup;
99
+ const { cfg, workspaceHealth, posture, cache, runtimeOverlay, scheduler: startupScheduler, maintenanceDegraded, getMaintenanceDegraded, getEffectiveMode, latchRuntimeDegraded, } = startup;
133
100
  const hooks = {};
134
101
  const initKibiCommandCapability = getInitKibiCommandCapability();
135
102
  if (initKibiCommandCapability.supported) {
@@ -143,11 +110,6 @@ const kibiOpencodePlugin = async (input) => {
143
110
  let hasRecentKbEdit = false;
144
111
  let recentCommentSuggestion = null;
145
112
  const seenFingerprints = new Set(); // For deduplication
146
- // NOTE: autoBriefResults is ONLY for prompt-time auto-brief guidance (file.edited flow).
147
- // Idle-brief runtime (session.idle flow) writes directly to .kb/briefs/ via generateIdleBrief()
148
- // and MUST NEVER store results in this map or leak into prompt guidance.
149
- const autoBriefResults = new Map();
150
- const toastedFingerprints = new Set();
151
113
  let lastRiskClass = null;
152
114
  let lastRiskFilePath = null;
153
115
  let lastRiskScopeKey = null;
@@ -160,6 +122,7 @@ const kibiOpencodePlugin = async (input) => {
160
122
  const fileOperationStateRegistry = new Map();
161
123
  const checkpointRunnerRegistry = new Map();
162
124
  const pathKindCacheRegistry = new Map();
125
+ const freshnessStore = createKbFreshnessEvidenceStore();
163
126
  function resolveScopedWorkContext(filePath) {
164
127
  return resolveWorkContext({
165
128
  inputDirectory: input.directory,
@@ -289,36 +252,10 @@ const kibiOpencodePlugin = async (input) => {
289
252
  return cacheKey;
290
253
  }
291
254
  const rootWorkContext = resolveScopedWorkContext();
292
- const sessionEditState = getSessionEditState(rootWorkContext);
293
255
  const fileOperationState = getFileOperationState(rootWorkContext);
294
256
  const scheduler = getSchedulerForContext(rootWorkContext);
295
257
  let degradedWarnedOnce = false;
296
258
  const pathKindCache = getPathKindCache(rootWorkContext);
297
- // Idle-brief state — dedupe via semantic contentHash (persisted envelope is the delivery authority)
298
- let idleBriefInFlight = false;
299
- let idleBriefTrailingRerun = false;
300
- let idleBriefTimer = null;
301
- const idleBriefDeliveredHashes = new Set();
302
- // Session-scoped flag: at most one idle-brief.sync-suppressed breadcrumb per session
303
- let idleSyncSuppressedOnce = false;
304
- const replayedBriefContentHashes = new Set();
305
- // Session-local baseline cursor: captured once per session/worktree/branch from the audit-log tail,
306
- // so the first idle brief in a fresh session only reports post-baseline changes.
307
- let sessionBaselineCursor = null;
308
- let sessionBaselineFingerprint = null;
309
- function syncSessionBaseline(branch) {
310
- const nextState = syncSessionBaselineState({
311
- fingerprint: sessionBaselineFingerprint,
312
- cursor: sessionBaselineCursor,
313
- }, {
314
- sessionId: input.sessionId,
315
- branch,
316
- worktree: input.worktree,
317
- }, () => getAuditTailCursor(input.worktree, branch));
318
- sessionBaselineFingerprint = nextState.fingerprint;
319
- sessionBaselineCursor = nextState.cursor;
320
- }
321
- syncSessionBaseline(currentBranch);
322
259
  function normalizeSessionPath(filePath, worktree = input.worktree) {
323
260
  if (path.isAbsolute(filePath)) {
324
261
  const relativePath = path.relative(worktree, filePath);
@@ -334,44 +271,6 @@ const kibiOpencodePlugin = async (input) => {
334
271
  function buildRiskPathScopeKey(context, filePath) {
335
272
  return `${buildStateScopeKey(context, "risk")}:${normalizeSessionPath(filePath, context.worktreeRoot)}`;
336
273
  }
337
- function getKbSnapshotFingerprint(worktree, branch) {
338
- try {
339
- const snapshotPath = path.join(worktree, ".kb", "branches", branch, "kb.rdf");
340
- const stat = fs.statSync(snapshotPath);
341
- return `${stat.size}:${stat.mtimeMs}`;
342
- }
343
- catch {
344
- return "missing";
345
- }
346
- }
347
- function buildSyntheticSyncAuditDelta(baseDelta, sourceFiles) {
348
- const timestamp = new Date().toISOString();
349
- const fileSource = sourceFiles[0] ?? "workspace-sync";
350
- const entityId = path.basename(fileSource).replace(/\.md$/, "") || "workspace-sync";
351
- return {
352
- ...baseDelta,
353
- hasChanges: true,
354
- entries: [
355
- {
356
- timestamp,
357
- operation: "upsert",
358
- entityId,
359
- payload: {
360
- kind: "entity",
361
- entityType: "fact",
362
- changeKind: "updated",
363
- title: entityId,
364
- source: fileSource,
365
- properties: {
366
- id: entityId,
367
- title: entityId,
368
- source: fileSource,
369
- },
370
- },
371
- },
372
- ],
373
- };
374
- }
375
274
  function getTransformFocusFilePath(transformInput) {
376
275
  if (!transformInput || typeof transformInput !== "object") {
377
276
  return null;
@@ -440,203 +339,44 @@ const kibiOpencodePlugin = async (input) => {
440
339
  precomputedSuggestion,
441
340
  };
442
341
  }
443
- function buildBriefingWorkspaceContext(context = rootWorkContext, branch = context.branch) {
444
- return {
445
- workspaceRoot: context.worktreeRoot,
446
- branch,
447
- directory: context.worktreeRoot,
448
- ...(input.workspace !== undefined ? { workspace: input.workspace } : {}),
449
- };
450
- }
451
- function buildWorkspaceContextForBranch(branch, context = rootWorkContext) {
452
- return {
453
- ...buildBriefingWorkspaceContext(context),
454
- branch,
455
- };
456
- }
457
- function queueBriefingFetch(intentResult, options = {}) {
458
- if (!intentResult.eligible ||
459
- !input.client ||
460
- getMaintenanceDegraded() ||
461
- ((options.postureState ?? posture.state) !== "root_active" &&
462
- (options.postureState ?? posture.state) !== "hybrid_root_plus_vendored")) {
463
- return;
464
- }
465
- if (options.skipIfCachedResultExists === true &&
466
- autoBriefResults.has(intentResult.fingerprint)) {
467
- return;
468
- }
469
- const client = input.client;
470
- const fingerprint = intentResult.fingerprint;
471
- const workspaceCtx = options.workspaceCtx ?? buildBriefingWorkspaceContext();
472
- void fetchBriefingResult(client, workspaceCtx, intentResult).then((result) => {
473
- autoBriefResults.set(fingerprint, result);
474
- if (!toastedFingerprints.has(fingerprint)) {
475
- toastedFingerprints.add(fingerprint);
476
- void sendToast(makeToastClient(client), {
477
- message: result.toastMessage,
478
- });
479
- }
480
- });
481
- }
482
342
  hooks.event = async ({ event }) => {
483
- const activeBranch = resolveCurrentBranch(input.worktree);
484
- syncSessionBaseline(activeBranch);
485
- // Handle session.idle for idle-brief generation. OpenCode can emit idle
486
- // while an assistant is between tool calls, so debounce until the work
487
- // burst settles before generating/delivering a brief.
488
- if (event.type === "session.idle") {
489
- if (!input.client)
490
- return;
491
- const idleBranch = activeBranch;
492
- const idleWorkspaceRoot = input.worktree;
493
- const runIdleBrief = async () => {
494
- if (idleBriefInFlight) {
495
- idleBriefTrailingRerun = true;
496
- return;
497
- }
498
- idleBriefInFlight = true;
499
- idleBriefTrailingRerun = false;
343
+ // Observe KB tool execution events for freshness evidence (best-effort)
344
+ const TOOL_EVENT_TYPES = new Set([
345
+ "tool.execute.after",
346
+ "tool.executed",
347
+ "tool.call.completed",
348
+ "tool.Execute.after",
349
+ "tool.Call.completed",
350
+ ]);
351
+ if (TOOL_EVENT_TYPES.has(event.type)) {
352
+ const props = event.properties ?? {};
353
+ const toolName = (props.tool ??
354
+ props.toolName ??
355
+ props.name ??
356
+ props.call?.name ??
357
+ props.input?.tool);
358
+ if (typeof toolName === "string" && toolName.startsWith("kb_")) {
359
+ const scope = {
360
+ ...(input.sessionId !== undefined
361
+ ? { sessionId: input.sessionId }
362
+ : {}),
363
+ agentIdentity: input.agentIdentity ?? "unknown",
364
+ worktree: input.worktree,
365
+ branch: rootWorkContext.branch,
366
+ fingerprint: `${input.sessionId ?? ""}-${rootWorkContext.branch}`,
367
+ };
500
368
  try {
501
- // Gather session edits
502
- const sessionEdits = sessionEditState.getSessionEdits();
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;
515
- const snapshotBeforeSync = getKbSnapshotFingerprint(idleWorkspaceRoot, idleBranch);
516
- if (scheduler) {
517
- const idleSyncBlocked = runtimeOverlay.primaryCause === "scheduler_sync_failed";
518
- if (!idleSyncBlocked) {
519
- scheduler.scheduleSync("session.idle");
520
- await scheduler.flush();
521
- }
522
- else if (!idleSyncSuppressedOnce) {
523
- idleSyncSuppressedOnce = true;
524
- logger.info("idle-brief.sync-suppressed", {
525
- event: "idle_brief_sync_suppressed",
526
- primaryCause: runtimeOverlay.primaryCause,
527
- });
528
- }
529
- }
530
- const snapshotAfterSync = getKbSnapshotFingerprint(idleWorkspaceRoot, idleBranch);
531
- const rawAuditDelta = computeAuditDelta(idleWorkspaceRoot, idleBranch, sessionBaselineCursor);
532
- const auditDelta = rawAuditDelta.hasChanges || snapshotBeforeSync === snapshotAfterSync
533
- ? rawAuditDelta
534
- : buildSyntheticSyncAuditDelta(rawAuditDelta, sourceFiles);
535
- if (!auditDelta.hasChanges)
536
- return;
537
- // Branch switch guard
538
- const currentBranchNow = resolveCurrentBranch(input.worktree);
539
- if (guardBranchChanged(idleBranch, currentBranchNow)) {
540
- logger.info("idle-brief.branch-changed", {
541
- event: "idle_brief_branch_changed",
542
- idleBranch,
543
- currentBranch: currentBranchNow,
544
- });
545
- return;
546
- }
547
- // Generate brief
548
- const workspaceCtx = buildWorkspaceContextForBranch(idleBranch);
549
- const client = input.client;
550
- if (!client)
551
- return;
552
- const reconciled = reconcileAuditEntries(auditDelta.entries);
553
- const changedEntityIds = [
554
- ...reconciled.added.map((e) => e.id),
555
- ...reconciled.modified.map((e) => e.id),
556
- ...reconciled.removed.map((e) => e.id),
557
- ];
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
- }
585
- if (result.success && result.envelope) {
586
- const envelope = result.envelope;
587
- // Dedupe by semantic contentHash — persisted envelope is the delivery authority
588
- const dedupeKey = `${idleWorkspaceRoot}:${idleBranch}:tui:${envelope.contentHash}`;
589
- if (!idleBriefDeliveredHashes.has(dedupeKey)) {
590
- idleBriefDeliveredHashes.add(dedupeKey);
591
- const sharedPolicy = { briefs: loadBriefConfig(input.worktree) };
592
- if (client) {
593
- try {
594
- const announcementResult = await announceBriefTui(makeToastClient(client), envelope, sharedPolicy);
595
- if (announcementResult.toastDelivered ||
596
- announcementResult.commandPublished) {
597
- replayedBriefContentHashes.add(envelope.contentHash);
598
- }
599
- }
600
- catch (err) {
601
- logger.error("idle-brief.delivery-failed", {
602
- event: "idle_brief_delivery_failed",
603
- error: err instanceof Error ? err.message : String(err),
604
- });
605
- }
606
- }
607
- }
608
- }
609
- else {
610
- logger.info("idle-brief.no-brief-generated", {
611
- event: "idle_brief_no_brief_generated",
612
- success: result.success,
613
- hasEnvelope: !!result.envelope,
614
- });
615
- }
616
- }
617
- catch (error) {
618
- logger.error("idle-brief.error", {
619
- event: "idle_brief_error",
620
- error: error instanceof Error ? error.message : String(error),
369
+ freshnessStore.recordToolEvidence(scope, toolName);
370
+ logger.info("kb-freshness.tool-evidence", {
371
+ event: "kb_freshness_tool_evidence",
372
+ tool: toolName,
621
373
  });
622
374
  }
623
- finally {
624
- idleBriefInFlight = false;
625
- // If trailing rerun was requested, run again
626
- if (idleBriefTrailingRerun) {
627
- idleBriefTrailingRerun = false;
628
- void runIdleBrief();
629
- }
375
+ catch {
376
+ // best-effort, never crash the event handler
630
377
  }
631
- };
632
- if (idleBriefTimer) {
633
- clearTimeout(idleBriefTimer);
634
378
  }
635
- idleBriefTimer = setTimeout(() => {
636
- idleBriefTimer = null;
637
- void runIdleBrief();
638
- }, resolveIdleBriefDeliveryDelayMs(idleWorkspaceRoot));
639
- return;
379
+ return; // tool events don't need file lifecycle processing
640
380
  }
641
381
  // Accept file.created, file.edited, and file.deleted lifecycle events
642
382
  const isFileLifecycle = event.type === "file.created" ||
@@ -644,10 +384,6 @@ const kibiOpencodePlugin = async (input) => {
644
384
  event.type === "file.deleted";
645
385
  if (!isFileLifecycle)
646
386
  return;
647
- if (idleBriefTimer) {
648
- clearTimeout(idleBriefTimer);
649
- idleBriefTimer = null;
650
- }
651
387
  const filePath = event
652
388
  .properties.file;
653
389
  if (!filePath)
@@ -676,7 +412,7 @@ const kibiOpencodePlugin = async (input) => {
676
412
  scopedPathKindCache.set(normalizedFilePath, pathAnalysis.kind);
677
413
  }
678
414
  else {
679
- // Not tracked — classify only for reminder routing, not auto-briefing
415
+ // Not tracked — classify only for reminder routing.
680
416
  scopedPathKindCache.set(normalizedFilePath, pathAnalysis.kind);
681
417
  }
682
418
  scopedSessionEditState.recordEventHint(normalizedFilePath, pathAnalysis.kind, Date.now());
@@ -722,8 +458,6 @@ const kibiOpencodePlugin = async (input) => {
722
458
  const effectiveRiskClass = riskClass === "safe_docs_only" && precomputedSuggestion
723
459
  ? "traceability_candidate"
724
460
  : riskClass;
725
- const isAutoBriefRisk = effectiveRiskClass === "behavior_candidate" ||
726
- effectiveRiskClass === "traceability_candidate";
727
461
  lastRiskClass = effectiveRiskClass;
728
462
  lastRiskFilePath = normalizedFilePath;
729
463
  lastRiskScopeKey = buildRiskPathScopeKey(eventContext, normalizedFilePath);
@@ -825,9 +559,7 @@ const kibiOpencodePlugin = async (input) => {
825
559
  posture: eventContext.posture,
826
560
  posture_state: eventContext.posture,
827
561
  });
828
- if (!isAutoBriefRisk) {
829
- return;
830
- }
562
+ return;
831
563
  }
832
564
  logger.info("smart-enforcement.cache", {
833
565
  event: "smart_enforcement_cache",
@@ -927,7 +659,8 @@ const kibiOpencodePlugin = async (input) => {
927
659
  }
928
660
  return;
929
661
  }
930
- if (isAutoBriefRisk) {
662
+ if (effectiveRiskClass === "behavior_candidate" ||
663
+ effectiveRiskClass === "traceability_candidate") {
931
664
  if (pathAnalysis.kind === "code" &&
932
665
  cfg.guidance.commentDetection.enabled) {
933
666
  const suggestion = precomputedSuggestion;
@@ -952,25 +685,6 @@ const kibiOpencodePlugin = async (input) => {
952
685
  else {
953
686
  recentCommentSuggestion = null;
954
687
  }
955
- if (!focusEdit) {
956
- // No surviving edits (all reverted to baseline) — skip auto-brief fetch
957
- return;
958
- }
959
- const sessionSourceFiles = sessionEdits.map((e) => e.filePath);
960
- const briefingContext = resolveScopedWorkContext(focusEdit.filePath);
961
- const intentResult = computeBriefIntent({
962
- riskClass: effectiveRiskClass,
963
- posture: briefingContext.posture,
964
- maintenanceDegraded: getMaintenanceDegraded(),
965
- sourceFiles: sessionSourceFiles,
966
- focusFilePath: focusEdit.filePath,
967
- worktreeRoot: briefingContext.worktreeRoot,
968
- branch: briefingContext.branch,
969
- });
970
- queueBriefingFetch(intentResult, {
971
- workspaceCtx: buildBriefingWorkspaceContext(briefingContext),
972
- postureState: briefingContext.posture,
973
- });
974
688
  }
975
689
  return;
976
690
  };
@@ -1028,58 +742,7 @@ const kibiOpencodePlugin = async (input) => {
1028
742
  if (effectiveRiskClass === null && lastRiskClass !== null) {
1029
743
  effectiveRiskClass = lastRiskClass;
1030
744
  }
1031
- const promptSourceFiles = transformSessionEdits.map((entry) => entry.filePath);
1032
745
  const promptFocusFilePath = transformFocusEdit?.filePath ?? transformFocusFilePath ?? undefined;
1033
- const intentResult = effectiveRiskClass
1034
- ? computeBriefIntent({
1035
- riskClass: effectiveRiskClass,
1036
- posture: promptWorkContext.posture,
1037
- maintenanceDegraded,
1038
- sourceFiles: promptSourceFiles,
1039
- worktreeRoot: promptWorkContext.worktreeRoot,
1040
- branch: promptWorkContext.branch,
1041
- ...(promptFocusFilePath !== undefined
1042
- ? {
1043
- focusFilePath: promptFocusFilePath,
1044
- }
1045
- : {}),
1046
- })
1047
- : null;
1048
- const autoBriefResult = intentResult
1049
- ? autoBriefResults.get(intentResult.fingerprint)
1050
- : undefined;
1051
- const isAutoBriefRisk = effectiveRiskClass === "behavior_candidate" ||
1052
- effectiveRiskClass === "traceability_candidate";
1053
- if (!autoBriefResult && isAutoBriefRisk && intentResult) {
1054
- queueBriefingFetch(intentResult, {
1055
- skipIfCachedResultExists: true,
1056
- workspaceCtx: buildBriefingWorkspaceContext(promptWorkContext),
1057
- postureState: promptWorkContext.posture,
1058
- });
1059
- }
1060
- // Replay latest unread idle brief if available // implements REQ-opencode-kibi-briefing-v4
1061
- if (input.worktree && currentBranch && input.client) {
1062
- const unreadBrief = selectLatestUnreadBrief(input.worktree, currentBranch);
1063
- if (unreadBrief &&
1064
- !replayedBriefContentHashes.has(unreadBrief.envelope.contentHash) &&
1065
- !hasTuiSeenBrief(input.worktree, currentBranch, unreadBrief.envelope.contentHash)) {
1066
- const sharedPolicy = { briefs: loadBriefConfig(input.worktree) };
1067
- const client = input.client;
1068
- try {
1069
- const announcementResult = await announceBriefTui(makeToastClient(client), unreadBrief.envelope, sharedPolicy);
1070
- if (announcementResult.toastDelivered ||
1071
- announcementResult.commandPublished) {
1072
- replayedBriefContentHashes.add(unreadBrief.envelope.contentHash);
1073
- }
1074
- }
1075
- catch (err) {
1076
- logger.error("idle-brief.replay-failed", {
1077
- event: "idle_brief_replay_failed",
1078
- error: err instanceof Error ? err.message : String(err),
1079
- });
1080
- }
1081
- }
1082
- }
1083
746
  // Steps 3-4: File-operation reminder selection with suppression // implements REQ-opencode-file-context-guidance-v1
1084
747
  let fileOperationReminder;
1085
748
  let hardGateBlock;
@@ -1113,8 +776,10 @@ const kibiOpencodePlugin = async (input) => {
1113
776
  focusPathKind,
1114
777
  effectiveRiskClass ?? "safe_docs_only",
1115
778
  ]);
1116
- if (effectiveMode === "hard" && promptWorkContext.isAuthoritative) {
1117
- checkpointRunner = getCheckpointRunnerForContext(promptWorkContext);
779
+ if (effectiveMode === "hard" &&
780
+ promptWorkContext.isAuthoritative) {
781
+ checkpointRunner =
782
+ getCheckpointRunnerForContext(promptWorkContext);
1118
783
  checkpointContext = {
1119
784
  workContext: promptWorkContext,
1120
785
  config: cfg,
@@ -1147,13 +812,18 @@ const kibiOpencodePlugin = async (input) => {
1147
812
  if (reminderResult.policyDecision === "hard_block") {
1148
813
  const policyResult = reminderResult.policyResult;
1149
814
  hardGateBlock = {
1150
- shownPaths: "shownPaths" in policyResult ? policyResult.shownPaths : [normalizedFocusPath],
1151
- remainingCount: "remainingCount" in policyResult ? policyResult.remainingCount : 0,
815
+ shownPaths: "shownPaths" in policyResult
816
+ ? policyResult.shownPaths
817
+ : [normalizedFocusPath],
818
+ remainingCount: "remainingCount" in policyResult
819
+ ? policyResult.remainingCount
820
+ : 0,
1152
821
  reason: "checkpoint_required",
1153
822
  };
1154
823
  hardGateConsumedPath = normalizedFocusPath;
1155
824
  hardGateFingerprint = checkpointFingerprint;
1156
- hardGateReminderKindsToMark = reminderResult.reminderKindsToMark;
825
+ hardGateReminderKindsToMark =
826
+ reminderResult.reminderKindsToMark;
1157
827
  if (checkpointRunner && checkpointContext) {
1158
828
  const checkpointContextWithGuidance = {
1159
829
  ...checkpointContext,
@@ -1195,6 +865,35 @@ const kibiOpencodePlugin = async (input) => {
1195
865
  }
1196
866
  }
1197
867
  }
868
+ // Build freshness evidence for the current dirty fingerprint
869
+ let freshnessEval;
870
+ const freshnessFingerprint = `${promptWorkContext.sessionId ?? ""}-${promptWorkContext.branch}`;
871
+ const freshnessScope = {
872
+ ...(promptWorkContext.sessionId !== undefined
873
+ ? { sessionId: promptWorkContext.sessionId }
874
+ : {}),
875
+ agentIdentity: promptWorkContext.agentIdentity,
876
+ worktree: promptWorkContext.worktreeRoot,
877
+ branch: promptWorkContext.branch,
878
+ fingerprint: freshnessFingerprint,
879
+ };
880
+ if (transformFocusFilePath) {
881
+ const normalizedFocusPathForFreshness = promptFileOperationState.normalizePath(transformFocusFilePath);
882
+ const focusPathKindForFreshness = promptPathKindCache.get(normalizedFocusPathForFreshness) ??
883
+ "unknown";
884
+ const pendingLifecycleForFreshness = promptFileOperationState.peekPending(normalizedFocusPathForFreshness);
885
+ const effectiveRiskClassForFreshness = effectiveRiskClass ?? "safe_docs_only";
886
+ const meaningfulChange = classifyMeaningfulChange({
887
+ normalizedPath: normalizedFocusPathForFreshness,
888
+ pathKind: focusPathKindForFreshness,
889
+ lifecycle: pendingLifecycleForFreshness?.lifecycle ?? "edited",
890
+ riskClass: effectiveRiskClassForFreshness,
891
+ });
892
+ if (meaningfulChange === "requires-kb-evidence") {
893
+ const freshnessEvidence = freshnessStore.getEvidence(freshnessScope, [normalizedFocusPathForFreshness]);
894
+ freshnessEval = evaluateKbFreshness(freshnessEvidence);
895
+ }
896
+ }
1198
897
  const guidance = buildPrompt({
1199
898
  recentEdits: transformRecentEdits,
1200
899
  focusEdit: transformPromptFocusEdit,
@@ -1209,7 +908,6 @@ const kibiOpencodePlugin = async (input) => {
1209
908
  maintenanceDegraded,
1210
909
  degradedMode: cfg.guidance.smartEnforcement.degradedMode,
1211
910
  showDegradedAdvisory,
1212
- ...(autoBriefResult !== undefined ? { autoBriefResult } : {}),
1213
911
  ...(effectiveRiskClass != null
1214
912
  ? { riskClass: effectiveRiskClass }
1215
913
  : {}),
@@ -1217,6 +915,14 @@ const kibiOpencodePlugin = async (input) => {
1217
915
  ? { fileOperationReminder }
1218
916
  : {}),
1219
917
  ...(hardGateBlock !== undefined ? { hardGateBlock } : {}),
918
+ ...(freshnessEval ? { kbFreshness: freshnessEval } : {}),
919
+ ...(freshnessEval && transformSessionEdits.length > 0
920
+ ? {
921
+ freshnessChangedPaths: [
922
+ ...new Set(transformSessionEdits.map((e) => e.filePath)),
923
+ ].slice(0, 5),
924
+ }
925
+ : {}),
1220
926
  });
1221
927
  logger.info("smart-enforcement.guidance", {
1222
928
  event: "smart_enforcement_guidance",
@@ -1263,8 +969,7 @@ const kibiOpencodePlugin = async (input) => {
1263
969
  // Mark shown and log only for reminders that were actually emitted
1264
970
  if (lifecycleEmitted) {
1265
971
  const kind = promptFileOperationState.peekPending(focusPathForConsume)
1266
- ?.lifecycle ===
1267
- "deleted"
972
+ ?.lifecycle === "deleted"
1268
973
  ? "kibi_delete"
1269
974
  : "kibi_write";
1270
975
  promptFileOperationState.markShown(focusPathForConsume, kind);
@@ -1279,8 +984,7 @@ const kibiOpencodePlugin = async (input) => {
1279
984
  }
1280
985
  if (e2eEmitted) {
1281
986
  const kind = promptFileOperationState.peekPending(focusPathForConsume)
1282
- ?.lifecycle ===
1283
- "deleted"
987
+ ?.lifecycle === "deleted"
1284
988
  ? "e2e_delete"
1285
989
  : "e2e_write";
1286
990
  promptFileOperationState.markShown(focusPathForConsume, kind);
@@ -1347,11 +1051,17 @@ const kibiOpencodePlugin = async (input) => {
1347
1051
  setTimeout(callback, delayMs);
1348
1052
  });
1349
1053
  scheduleStartupNotify(() => {
1350
- const version = readKibiOpencodePackageVersion();
1054
+ const meta = readKibiPackageVersions();
1055
+ const versions = {};
1056
+ for (const key of ["opencode", "mcp", "cli", "core"]) {
1057
+ if (meta[key] !== "unknown")
1058
+ versions[key] = meta[key];
1059
+ }
1351
1060
  notifyStartup(makeStartupClient(client), {
1352
1061
  suppressToast: cfg.ux.toastStartup === false,
1353
1062
  directory: input.directory,
1354
- ...(version ? { version } : {}),
1063
+ ...(Object.keys(versions).length > 0 ? { versions } : {}),
1064
+ versionMetadataSource: meta.source,
1355
1065
  });
1356
1066
  }, 2000);
1357
1067
  }