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.
- package/README.md +18 -12
- package/dist/config.d.ts +0 -3
- package/dist/config.js +0 -9
- package/dist/e2e-coverage-signals.js +1 -3
- package/dist/enforcement-policy.d.ts +2 -0
- package/dist/enforcement-policy.js +18 -3
- package/dist/enforcement-scope.js +1 -3
- package/dist/file-entity-links.js +2 -6
- package/dist/file-operation-reminders.d.ts +4 -4
- package/dist/file-operation-state.js +4 -2
- package/dist/init-kibi-alias.js +0 -1
- package/dist/init-kibi-capability.js +1 -5
- package/dist/kb-freshness-state.d.ts +62 -0
- package/dist/kb-freshness-state.js +215 -0
- package/dist/kibi-checkpoint-runner.d.ts +1 -1
- package/dist/kibi-checkpoint-runner.js +6 -3
- package/dist/knowledge-classifier.js +2 -1
- package/dist/meaningful-change-classifier.d.ts +30 -0
- package/dist/meaningful-change-classifier.js +103 -0
- package/dist/plugin-startup.js +0 -1
- package/dist/plugin.js +101 -391
- package/dist/prompt.d.ts +6 -5
- package/dist/prompt.js +48 -84
- package/dist/reconcile-engine.d.ts +15 -1
- package/dist/reconcile-engine.js +1 -3
- package/dist/scheduler.js +16 -4
- package/dist/session-edit-state.js +2 -1
- package/dist/source-linked-guidance.js +3 -1
- package/dist/startup-notifier.d.ts +7 -1
- package/dist/startup-notifier.js +51 -17
- package/dist/tui.js +1 -403
- package/dist/tui.jsx +1 -113
- package/dist/version-metadata.d.ts +13 -0
- package/dist/version-metadata.js +78 -0
- package/dist/version-metadata.json +1 -0
- package/package.json +5 -7
- package/dist/brief-delivery-reasons.d.ts +0 -12
- package/dist/brief-delivery-reasons.js +0 -132
- package/dist/brief-intent.d.ts +0 -41
- package/dist/brief-intent.js +0 -142
- package/dist/briefing-runtime.d.ts +0 -24
- package/dist/briefing-runtime.js +0 -277
- package/dist/graph-narrator.d.ts +0 -25
- package/dist/graph-narrator.js +0 -408
- package/dist/idle-brief-audit.d.ts +0 -36
- package/dist/idle-brief-audit.js +0 -186
- package/dist/idle-brief-paths.d.ts +0 -6
- package/dist/idle-brief-paths.js +0 -120
- package/dist/idle-brief-reader.d.ts +0 -37
- package/dist/idle-brief-reader.js +0 -163
- package/dist/idle-brief-runtime.d.ts +0 -55
- package/dist/idle-brief-runtime.js +0 -496
- package/dist/idle-brief-store.d.ts +0 -113
- package/dist/idle-brief-store.js +0 -262
- package/dist/session-fingerprint.d.ts +0 -11
- package/dist/session-fingerprint.js +0 -21
- package/dist/tui-brief-delivery.d.ts +0 -67
- package/dist/tui-brief-delivery.js +0 -279
- package/dist/tui-brief-view-model.d.ts +0 -63
- package/dist/tui-brief-view-model.js +0 -209
- package/dist/utils/brief-marker.d.ts +0 -19
- 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 {
|
|
15
|
-
import {
|
|
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 {
|
|
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,
|
|
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
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
const
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
|
|
624
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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" &&
|
|
1117
|
-
|
|
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
|
|
1151
|
-
|
|
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 =
|
|
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
|
|
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
|
-
...(
|
|
1063
|
+
...(Object.keys(versions).length > 0 ? { versions } : {}),
|
|
1064
|
+
versionMetadataSource: meta.source,
|
|
1355
1065
|
});
|
|
1356
1066
|
}, 2000);
|
|
1357
1067
|
}
|