kibi-opencode 0.19.4 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -6
- package/dist/auto-update.d.ts +2 -0
- package/dist/auto-update.js +5 -4
- package/dist/e2e-coverage-signals.js +3 -3
- package/dist/enforcement-policy.d.ts +10 -0
- package/dist/enforcement-policy.js +23 -23
- package/dist/file-entity-links.d.ts +4 -0
- package/dist/file-entity-links.js +6 -4
- package/dist/file-filter.d.ts +9 -9
- package/dist/file-filter.js +16 -68
- package/dist/kibi-bootstrap-alias.d.ts +2 -0
- package/dist/kibi-bootstrap-alias.js +14 -0
- package/dist/kibi-bootstrap-capability.d.ts +34 -0
- package/dist/{init-kibi-capability.js → kibi-bootstrap-capability.js} +26 -16
- package/dist/meaningful-change-classifier.d.ts +1 -0
- package/dist/meaningful-change-classifier.js +40 -10
- package/dist/path-kind.js +21 -39
- package/dist/plugin-startup.js +3 -3
- package/dist/plugin.d.ts +4 -1
- package/dist/plugin.js +81 -374
- package/dist/prompt.d.ts +4 -4
- package/dist/prompt.js +24 -60
- package/dist/repo-posture.js +31 -84
- package/dist/risk-classifier.d.ts +3 -3
- package/dist/risk-classifier.js +9 -6
- package/dist/scheduler.js +2 -2
- 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 +15 -17
- package/dist/workspace-health.js +8 -56
- package/package.json +6 -4
- package/dist/init-kibi-alias.d.ts +0 -14
- package/dist/init-kibi-alias.js +0 -43
- package/dist/init-kibi-capability.d.ts +0 -32
package/dist/path-kind.js
CHANGED
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
const CODE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".py"];
|
|
4
4
|
const KB_PREFIX = ".kb";
|
|
5
|
-
const
|
|
6
|
-
"requirements
|
|
7
|
-
"scenarios
|
|
8
|
-
"tests
|
|
9
|
-
"adr
|
|
10
|
-
"flags
|
|
11
|
-
"events
|
|
12
|
-
"facts
|
|
13
|
-
"symbols.yaml",
|
|
5
|
+
const KIBI_LANE_KINDS = [
|
|
6
|
+
{ prefix: "requirements/", kind: "requirement" },
|
|
7
|
+
{ prefix: "scenarios/", kind: "scenario" },
|
|
8
|
+
{ prefix: "tests/", kind: "test" },
|
|
9
|
+
{ prefix: "adr/", kind: "adr" },
|
|
10
|
+
{ prefix: "flags/", kind: "flag" },
|
|
11
|
+
{ prefix: "events/", kind: "event" },
|
|
12
|
+
{ prefix: "facts/", kind: "fact" },
|
|
13
|
+
{ prefix: "symbols.yaml", kind: "symbol" },
|
|
14
|
+
{ prefix: "symbol-coordinates.yaml", kind: "symbol" },
|
|
14
15
|
];
|
|
15
16
|
export function analyzePath(
|
|
16
17
|
// implements REQ-opencode-kibi-plugin-v1
|
|
@@ -21,38 +22,19 @@ filePath, cwd = process.cwd()) {
|
|
|
21
22
|
let kind = "unknown";
|
|
22
23
|
let isUnderKb = false;
|
|
23
24
|
let isKibiDocRelevant = false;
|
|
24
|
-
// Check if under .kb/**
|
|
25
|
-
if (rel.startsWith(`${KB_PREFIX}/`)) {
|
|
26
|
-
kind = "kb";
|
|
27
|
-
isUnderKb = true;
|
|
28
|
-
}
|
|
29
|
-
// Check for Kibi doc paths
|
|
30
25
|
const normalized = rel.toLowerCase();
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
kind = "requirement";
|
|
40
|
-
else if (patternPrefix.includes("scenarios"))
|
|
41
|
-
kind = "scenario";
|
|
42
|
-
else if (patternPrefix.includes("tests"))
|
|
43
|
-
kind = "test";
|
|
44
|
-
else if (patternPrefix.includes("adr"))
|
|
45
|
-
kind = "adr";
|
|
46
|
-
else if (patternPrefix.includes("facts"))
|
|
47
|
-
kind = "fact";
|
|
48
|
-
else if (patternPrefix.includes("events"))
|
|
49
|
-
kind = "event";
|
|
50
|
-
else if (patternPrefix.includes("flags"))
|
|
51
|
-
kind = "flag";
|
|
52
|
-
else if (patternPrefix.includes("symbols"))
|
|
53
|
-
kind = "symbol";
|
|
26
|
+
if (normalized.startsWith(`${KB_PREFIX}/`)) {
|
|
27
|
+
isUnderKb = true;
|
|
28
|
+
const rest = normalized.slice(KB_PREFIX.length + 1);
|
|
29
|
+
for (const lane of KIBI_LANE_KINDS) {
|
|
30
|
+
if (rest === lane.prefix || rest.startsWith(lane.prefix)) {
|
|
31
|
+
kind = lane.kind;
|
|
32
|
+
isKibiDocRelevant = true;
|
|
33
|
+
break;
|
|
54
34
|
}
|
|
55
|
-
|
|
35
|
+
}
|
|
36
|
+
if (kind === "unknown") {
|
|
37
|
+
kind = "kb";
|
|
56
38
|
}
|
|
57
39
|
}
|
|
58
40
|
if (kind === "unknown") {
|
package/dist/plugin-startup.js
CHANGED
|
@@ -23,7 +23,7 @@ export function resolveCurrentBranch(cwd) {
|
|
|
23
23
|
// 1. Check KIBI_BRANCH env var first (highest precedence)
|
|
24
24
|
const envBranch = process.env.KIBI_BRANCH?.trim();
|
|
25
25
|
if (envBranch && envBranch.length > 0) {
|
|
26
|
-
return envBranch
|
|
26
|
+
return envBranch;
|
|
27
27
|
}
|
|
28
28
|
// 2. Fall back to git branch
|
|
29
29
|
try {
|
|
@@ -33,7 +33,7 @@ export function resolveCurrentBranch(cwd) {
|
|
|
33
33
|
stdio: ["ignore", "pipe", "ignore"],
|
|
34
34
|
timeout: 5000,
|
|
35
35
|
}).trim();
|
|
36
|
-
return branch
|
|
36
|
+
return branch;
|
|
37
37
|
}
|
|
38
38
|
catch {
|
|
39
39
|
return "unknown";
|
|
@@ -41,7 +41,7 @@ export function resolveCurrentBranch(cwd) {
|
|
|
41
41
|
}
|
|
42
42
|
function readConfigFingerprint(cwd) {
|
|
43
43
|
try {
|
|
44
|
-
return fs.readFileSync(path.join(cwd, ".kb", "
|
|
44
|
+
return fs.readFileSync(path.join(cwd, ".kb", "manifest.json"), "utf-8");
|
|
45
45
|
}
|
|
46
46
|
catch {
|
|
47
47
|
return "missing";
|
package/dist/plugin.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { type OpenCodeConfigHookInput } from "./
|
|
1
|
+
import { type OpenCodeConfigHookInput } from "./kibi-bootstrap-capability.js";
|
|
2
|
+
export declare function resetCommentSuggestion(): null;
|
|
3
|
+
export declare function nextRecentCommentSuggestion<T>(inspectingCodeComments: boolean, suggestion: T | null | undefined): T | null;
|
|
4
|
+
export declare function adoptPrecomputedSuggestion<T>(recent: T | null, precomputed: T | null | undefined): T | null;
|
|
2
5
|
export interface PluginInput {
|
|
3
6
|
worktree: string;
|
|
4
7
|
directory: string;
|
package/dist/plugin.js
CHANGED
|
@@ -7,8 +7,8 @@ import { getFileLinkedEntityIds } from "./file-entity-links.js"; // implements R
|
|
|
7
7
|
import * as fileFilter from "./file-filter.js";
|
|
8
8
|
import { deriveFileOperationReminder } from "./file-operation-reminders.js"; // implements REQ-opencode-file-context-guidance-v1
|
|
9
9
|
import { createFileOperationState, } from "./file-operation-state.js"; // implements REQ-opencode-file-context-guidance-v1
|
|
10
|
-
import { getInitKibiCommandCapability, registerInitKibiCommand, } from "./init-kibi-capability.js";
|
|
11
10
|
import { createKbFreshnessEvidenceStore, evaluateKbFreshness, } from "./kb-freshness-state.js";
|
|
11
|
+
import { getKibiBootstrapCommandCapability, registerKibiBootstrapCommand, } from "./kibi-bootstrap-capability.js";
|
|
12
12
|
import { KibiCheckpointRunner, } from "./kibi-checkpoint-runner.js";
|
|
13
13
|
import * as logger from "./logger.js";
|
|
14
14
|
import { classifyMeaningfulChange } from "./meaningful-change-classifier.js";
|
|
@@ -20,10 +20,23 @@ import { classifyRisk } from "./risk-classifier.js";
|
|
|
20
20
|
import { createSyncScheduler } from "./scheduler.js";
|
|
21
21
|
import { createSessionEditState, } from "./session-edit-state.js";
|
|
22
22
|
import { getSessionTracker } from "./session-tracker.js";
|
|
23
|
+
import { handleFileLifecycleEvent, handleKbToolEvent, isKbToolEventType, } from "./smart-enforcement-events.js";
|
|
23
24
|
import { notifyStartup, } from "./startup-notifier.js";
|
|
24
25
|
import { sendToast, } from "./toast.js";
|
|
25
26
|
import { readKibiPackageVersions } from "./version-metadata.js";
|
|
26
27
|
import { resolveWorkContext, } from "./work-context-resolver.js";
|
|
28
|
+
export function resetCommentSuggestion() {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
// implements REQ-opencode-file-context-guidance-v1
|
|
32
|
+
export function nextRecentCommentSuggestion(inspectingCodeComments, suggestion) {
|
|
33
|
+
if (inspectingCodeComments && suggestion)
|
|
34
|
+
return suggestion;
|
|
35
|
+
return resetCommentSuggestion();
|
|
36
|
+
}
|
|
37
|
+
export function adoptPrecomputedSuggestion(recent, precomputed) {
|
|
38
|
+
return recent ?? precomputed ?? null;
|
|
39
|
+
}
|
|
27
40
|
import * as fs from "node:fs";
|
|
28
41
|
function deriveFileBucket(kind) {
|
|
29
42
|
return kind;
|
|
@@ -100,21 +113,23 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
100
113
|
}
|
|
101
114
|
const { cfg, workspaceHealth, posture, cache, runtimeOverlay, scheduler: startupScheduler, maintenanceDegraded, getMaintenanceDegraded, getEffectiveMode, latchRuntimeDegraded, } = startup;
|
|
102
115
|
const hooks = {};
|
|
103
|
-
const
|
|
104
|
-
if (
|
|
116
|
+
const kibiBootstrapCommandCapability = getKibiBootstrapCommandCapability();
|
|
117
|
+
if (kibiBootstrapCommandCapability.supported) {
|
|
105
118
|
hooks.config = async (configInput) => {
|
|
106
|
-
|
|
119
|
+
registerKibiBootstrapCommand(configInput, kibiBootstrapCommandCapability);
|
|
107
120
|
};
|
|
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",
|