kibi-opencode 1.0.0 → 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.
- package/dist/auto-update.d.ts +2 -0
- package/dist/auto-update.js +5 -4
- package/dist/enforcement-policy.d.ts +10 -0
- package/dist/enforcement-policy.js +16 -6
- package/dist/file-entity-links.d.ts +4 -0
- package/dist/file-entity-links.js +5 -1
- package/dist/kibi-bootstrap-capability.d.ts +2 -0
- package/dist/kibi-bootstrap-capability.js +14 -5
- package/dist/meaningful-change-classifier.d.ts +1 -0
- package/dist/meaningful-change-classifier.js +13 -2
- package/dist/plugin.d.ts +3 -0
- package/dist/plugin.js +77 -370
- package/dist/session-edit-state.d.ts +4 -0
- package/dist/session-edit-state.js +5 -3
- package/dist/smart-enforcement-events.d.ts +100 -0
- package/dist/smart-enforcement-events.js +414 -0
- package/dist/version-metadata.json +1 -1
- package/dist/work-context-resolver.d.ts +10 -0
- package/dist/work-context-resolver.js +13 -15
- package/package.json +2 -2
package/dist/plugin.js
CHANGED
|
@@ -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;
|
|
@@ -108,13 +121,15 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
108
121
|
}
|
|
109
122
|
// Plugin instance state (not module globals)
|
|
110
123
|
const MAX_RECENT_EDITS = 5;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
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
|
-
|
|
346
|
-
|
|
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 (
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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 &&
|
|
734
|
-
|
|
439
|
+
let effectiveRiskClass = riskScopeKey !== null &&
|
|
440
|
+
enforcementState.lastRiskScopeKey === riskScopeKey
|
|
441
|
+
? enforcementState.lastRiskClass
|
|
735
442
|
: null;
|
|
736
443
|
if (riskContextFilePath &&
|
|
737
|
-
(lastRiskClass === null ||
|
|
444
|
+
(enforcementState.lastRiskClass === null ||
|
|
445
|
+
enforcementState.lastRiskScopeKey !== riskScopeKey)) {
|
|
738
446
|
const riskCtx = deriveRiskContext(promptWorkContext, riskContextFilePath, promptPathKindCache);
|
|
739
447
|
effectiveRiskClass = riskCtx.effectiveRiskClass;
|
|
740
|
-
|
|
741
|
-
recentCommentSuggestion = riskCtx.precomputedSuggestion;
|
|
742
|
-
}
|
|
448
|
+
enforcementState.recentCommentSuggestion = adoptPrecomputedSuggestion(enforcementState.recentCommentSuggestion, riskCtx.precomputedSuggestion);
|
|
743
449
|
}
|
|
744
|
-
if (effectiveRiskClass === null &&
|
|
745
|
-
|
|
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",
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
export type EditEventKind = string;
|
|
2
|
+
export declare function pushEventHintIfKind(hints: Array<{
|
|
3
|
+
kind: string;
|
|
4
|
+
timestamp: number;
|
|
5
|
+
}>, kind: string | undefined, timestamp: number): void;
|
|
2
6
|
export interface SessionEditEntry {
|
|
3
7
|
/** Relative file path (relative to worktree root). */
|
|
4
8
|
filePath: string;
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import * as crypto from "node:crypto";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
export function pushEventHintIfKind(hints, kind, timestamp) {
|
|
5
|
+
if (kind)
|
|
6
|
+
hints.push({ kind, timestamp });
|
|
7
|
+
}
|
|
4
8
|
// ---------------------------------------------------------------------------
|
|
5
9
|
// Constants
|
|
6
10
|
// ---------------------------------------------------------------------------
|
|
@@ -162,9 +166,7 @@ export function createSessionEditState(opts) {
|
|
|
162
166
|
const abs = resolveToAbsolute(rel);
|
|
163
167
|
entry.currentHash = hashFile(abs);
|
|
164
168
|
entry.lastReconciledAt = timestamp ?? now();
|
|
165
|
-
|
|
166
|
-
entry.eventHints.push({ kind, timestamp: timestamp ?? now() });
|
|
167
|
-
}
|
|
169
|
+
pushEventHintIfKind(entry.eventHints, kind, timestamp ?? now());
|
|
168
170
|
}
|
|
169
171
|
return {
|
|
170
172
|
recordEventHint,
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { CommentAnalysisResult } from "./comment-analysis.js";
|
|
2
|
+
import type { FileOperationState } from "./file-operation-state.js";
|
|
3
|
+
import type { CacheKey } from "./guidance-cache.js";
|
|
4
|
+
import type { KbFreshnessScope } from "./kb-freshness-state.js";
|
|
5
|
+
import type { PathKind } from "./path-kind.js";
|
|
6
|
+
import type { RuntimeDegradedOverlay } from "./plugin-startup.js";
|
|
7
|
+
import type { RiskClass } from "./risk-classifier.js";
|
|
8
|
+
import type { SyncScheduler } from "./scheduler.js";
|
|
9
|
+
import type { SessionEditState } from "./session-edit-state.js";
|
|
10
|
+
import type { WarningCategory } from "./session-tracker.js";
|
|
11
|
+
import type { WorkContext } from "./work-context-resolver.js";
|
|
12
|
+
export interface PluginEventInput {
|
|
13
|
+
sessionId?: string;
|
|
14
|
+
agentIdentity?: string;
|
|
15
|
+
worktree: string;
|
|
16
|
+
directory: string;
|
|
17
|
+
}
|
|
18
|
+
export interface PluginEvent {
|
|
19
|
+
type: string;
|
|
20
|
+
properties?: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
/** Mutable cross-event state shared by the event and chat hooks. */
|
|
23
|
+
export interface SmartEnforcementState {
|
|
24
|
+
recentEdits: Array<{
|
|
25
|
+
path: string;
|
|
26
|
+
kind: PathKind;
|
|
27
|
+
timestamp: number;
|
|
28
|
+
}>;
|
|
29
|
+
hasRecentKbEdit: boolean;
|
|
30
|
+
recentCommentSuggestion: CommentAnalysisResult | null;
|
|
31
|
+
seenFingerprints: Set<string>;
|
|
32
|
+
lastRiskClass: RiskClass | null;
|
|
33
|
+
lastRiskFilePath: string | null;
|
|
34
|
+
lastRiskScopeKey: string | null;
|
|
35
|
+
}
|
|
36
|
+
export interface KibiEventEnv {
|
|
37
|
+
input: PluginEventInput;
|
|
38
|
+
cfg: {
|
|
39
|
+
sync: {
|
|
40
|
+
enabled: boolean;
|
|
41
|
+
};
|
|
42
|
+
guidance: {
|
|
43
|
+
warnOnKbEdits: boolean;
|
|
44
|
+
commentDetection: {
|
|
45
|
+
enabled: boolean;
|
|
46
|
+
minLines: number;
|
|
47
|
+
};
|
|
48
|
+
targetedChecks: {
|
|
49
|
+
enabled: boolean;
|
|
50
|
+
};
|
|
51
|
+
smartEnforcement: {
|
|
52
|
+
degradedMode: string;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
cache: {
|
|
57
|
+
isSatisfied(key: CacheKey): boolean;
|
|
58
|
+
};
|
|
59
|
+
freshnessStore: {
|
|
60
|
+
recordToolEvidence(scope: KbFreshnessScope, tool: string): void;
|
|
61
|
+
};
|
|
62
|
+
fileFilter: {
|
|
63
|
+
shouldHandleFile(filePath: string, root: string): boolean;
|
|
64
|
+
};
|
|
65
|
+
log: {
|
|
66
|
+
info(...args: unknown[]): void;
|
|
67
|
+
warn(...args: unknown[]): void;
|
|
68
|
+
};
|
|
69
|
+
rootWorkContext: WorkContext;
|
|
70
|
+
state: SmartEnforcementState;
|
|
71
|
+
resolveScopedWorkContext(filePath?: string): WorkContext;
|
|
72
|
+
getSessionEditState(context: WorkContext): SessionEditState;
|
|
73
|
+
getFileOperationState(context: WorkContext): FileOperationState;
|
|
74
|
+
getPathKindCache(context: WorkContext): Map<string, PathKind>;
|
|
75
|
+
getSchedulerForContext(context: WorkContext): SyncScheduler | null;
|
|
76
|
+
normalizeSessionPath(filePath: string, worktree?: string): string;
|
|
77
|
+
resolveWorktreePath(filePath: string, worktree?: string): string;
|
|
78
|
+
buildRiskPathScopeKey(context: WorkContext, filePath: string): string;
|
|
79
|
+
buildScopedCacheKey(context: WorkContext, riskClass: RiskClass, fileBucket: string, dirtyRelevantInputs: Iterable<string | null | undefined>): CacheKey;
|
|
80
|
+
readFileContent(filePath: string, worktree?: string): string;
|
|
81
|
+
updateRecentEditsFromSession(sessionEdits: Array<{
|
|
82
|
+
filePath: string;
|
|
83
|
+
lastReconciledAt: number;
|
|
84
|
+
}>, scopedPathKindCache: Map<string, PathKind>): SmartEnforcementState["recentEdits"];
|
|
85
|
+
recordWarning(category: WarningCategory | string, filePath: string, message: string): void;
|
|
86
|
+
getMaintenanceDegraded(): boolean;
|
|
87
|
+
getEffectiveMode(): string;
|
|
88
|
+
posture: {
|
|
89
|
+
maintenanceDegraded: boolean;
|
|
90
|
+
state: string;
|
|
91
|
+
};
|
|
92
|
+
runtimeOverlay: RuntimeDegradedOverlay;
|
|
93
|
+
lintRequirementDoc(filePath: string, worktreeRoot: string): Array<{
|
|
94
|
+
category: string;
|
|
95
|
+
message: string;
|
|
96
|
+
}>;
|
|
97
|
+
}
|
|
98
|
+
export declare function isKbToolEventType(eventType: string): boolean;
|
|
99
|
+
export declare function handleKbToolEvent(env: KibiEventEnv, event: PluginEvent, sessionId: string | undefined): void;
|
|
100
|
+
export declare function handleFileLifecycleEvent(env: KibiEventEnv, event: PluginEvent): void;
|