kibi-opencode 1.0.0 → 2.0.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/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/tui.js +2 -2
- 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 +3 -5
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Kibi — repo-local, per-branch, queryable long-term memory for software projects
|
|
3
|
+
Copyright (C) 2026 Piotr Franczyk
|
|
4
|
+
|
|
5
|
+
This program is free software: you can redistribute it and/or modify
|
|
6
|
+
it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
(at your option) any later version.
|
|
9
|
+
|
|
10
|
+
This program is distributed in the hope that it will be useful,
|
|
11
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
GNU Affero General Public License for more details.
|
|
14
|
+
|
|
15
|
+
You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
*/
|
|
18
|
+
import { analyzeCodeFile } from "./comment-analysis.js";
|
|
19
|
+
import { analyzePath } from "./path-kind.js";
|
|
20
|
+
import { isMustPriorityRequirement } from "./requirement-doc.js";
|
|
21
|
+
import { classifyRisk } from "./risk-classifier.js";
|
|
22
|
+
const TOOL_EVENT_TYPES = new Set([
|
|
23
|
+
"tool.execute.after",
|
|
24
|
+
"tool.executed",
|
|
25
|
+
"tool.call.completed",
|
|
26
|
+
"tool.Execute.after",
|
|
27
|
+
"tool.Call.completed",
|
|
28
|
+
]);
|
|
29
|
+
// implements REQ-opencode-kibi-plugin-v1
|
|
30
|
+
export function isKbToolEventType(eventType) {
|
|
31
|
+
return TOOL_EVENT_TYPES.has(eventType);
|
|
32
|
+
}
|
|
33
|
+
// implements REQ-opencode-kibi-plugin-v1
|
|
34
|
+
export function handleKbToolEvent(env, event, sessionId) {
|
|
35
|
+
const props = (event.properties ?? {});
|
|
36
|
+
const toolName = (props.tool ??
|
|
37
|
+
props.toolName ??
|
|
38
|
+
props.name ??
|
|
39
|
+
props.call?.name ??
|
|
40
|
+
props.input?.tool);
|
|
41
|
+
if (typeof toolName === "string" && toolName.startsWith("kb_")) {
|
|
42
|
+
const scope = {
|
|
43
|
+
...(sessionId !== undefined ? { sessionId } : {}),
|
|
44
|
+
agentIdentity: env.input.agentIdentity ?? "unknown",
|
|
45
|
+
worktree: env.input.worktree,
|
|
46
|
+
branch: env.rootWorkContext.branch,
|
|
47
|
+
fingerprint: `${env.input.sessionId ?? ""}-${env.rootWorkContext.branch}`,
|
|
48
|
+
};
|
|
49
|
+
try {
|
|
50
|
+
env.freshnessStore.recordToolEvidence(scope, toolName);
|
|
51
|
+
env.log.info("kb-freshness.tool-evidence", {
|
|
52
|
+
event: "kb_freshness_tool_evidence",
|
|
53
|
+
tool: toolName,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// best-effort, never crash the event handler
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function resolveFileEvent(env, event) {
|
|
62
|
+
const filePath = event
|
|
63
|
+
.properties.file;
|
|
64
|
+
if (!filePath)
|
|
65
|
+
return null;
|
|
66
|
+
const eventContext = env.resolveScopedWorkContext(filePath);
|
|
67
|
+
const scopedSessionEditState = env.getSessionEditState(eventContext);
|
|
68
|
+
const scopedFileOperationState = env.getFileOperationState(eventContext);
|
|
69
|
+
const scopedPathKindCache = env.getPathKindCache(eventContext);
|
|
70
|
+
const scopedScheduler = env.getSchedulerForContext(eventContext);
|
|
71
|
+
const normalizedFilePath = env.normalizeSessionPath(filePath, eventContext.worktreeRoot);
|
|
72
|
+
const lifecycle = event.type === "file.created"
|
|
73
|
+
? "created"
|
|
74
|
+
: event.type === "file.deleted"
|
|
75
|
+
? "deleted"
|
|
76
|
+
: "edited";
|
|
77
|
+
return {
|
|
78
|
+
eventContext,
|
|
79
|
+
scopedSessionEditState,
|
|
80
|
+
scopedFileOperationState,
|
|
81
|
+
scopedPathKindCache,
|
|
82
|
+
scopedScheduler,
|
|
83
|
+
normalizedFilePath,
|
|
84
|
+
lifecycle,
|
|
85
|
+
focusEdit: undefined,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// implements REQ-opencode-file-context-guidance-v1
|
|
89
|
+
// implements REQ-opencode-kibi-plugin-v1
|
|
90
|
+
export function handleFileLifecycleEvent(env, event) {
|
|
91
|
+
const resolution = resolveFileEvent(env, event);
|
|
92
|
+
if (!resolution)
|
|
93
|
+
return;
|
|
94
|
+
const { eventContext, scopedSessionEditState, scopedFileOperationState, scopedPathKindCache, scopedScheduler, normalizedFilePath, lifecycle, } = resolution;
|
|
95
|
+
scopedFileOperationState.recordLifecycle(filePathOf(resolution), resolution.lifecycle, Date.now());
|
|
96
|
+
scopedFileOperationState.normalizePath(filePathOf(resolution));
|
|
97
|
+
const pathAnalysis = analyzePath(normalizedFilePath, eventContext.worktreeRoot);
|
|
98
|
+
if (lifecycle === "deleted") {
|
|
99
|
+
handleDeletedFile(env, {
|
|
100
|
+
eventContext,
|
|
101
|
+
scopedSessionEditState,
|
|
102
|
+
scopedPathKindCache,
|
|
103
|
+
scopedScheduler,
|
|
104
|
+
normalizedFilePath,
|
|
105
|
+
pathKind: pathAnalysis.kind,
|
|
106
|
+
});
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
scopedSessionEditState.recordEventHint(normalizedFilePath, pathAnalysis.kind, Date.now());
|
|
110
|
+
scopedSessionEditState.reconcilePath(normalizedFilePath);
|
|
111
|
+
scopedPathKindCache.set(normalizedFilePath, pathAnalysis.kind);
|
|
112
|
+
const sessionEdits = scopedSessionEditState.getSessionEdits();
|
|
113
|
+
const focusEdit = scopedSessionEditState.getFocusEdit();
|
|
114
|
+
if (env.cfg.sync.enabled &&
|
|
115
|
+
scopedScheduler &&
|
|
116
|
+
env.fileFilter.shouldHandleFile(normalizedFilePath, eventContext.worktreeRoot)) {
|
|
117
|
+
scopedScheduler.scheduleSync(lifecycle === "created" ? "file.created" : "file.edited", normalizedFilePath);
|
|
118
|
+
}
|
|
119
|
+
const fileContent = env.readFileContent(normalizedFilePath, eventContext.worktreeRoot);
|
|
120
|
+
const hasMustPriority = pathAnalysis.kind === "requirement"
|
|
121
|
+
? isMustPriorityRequirement(normalizedFilePath, eventContext.worktreeRoot)
|
|
122
|
+
: false;
|
|
123
|
+
let precomputedSuggestion = null;
|
|
124
|
+
if (pathAnalysis.kind === "code" &&
|
|
125
|
+
env.cfg.guidance.commentDetection.enabled) {
|
|
126
|
+
precomputedSuggestion = analyzeCodeFile(env.resolveWorktreePath(normalizedFilePath, eventContext.worktreeRoot), {
|
|
127
|
+
minLines: env.cfg.guidance.commentDetection.minLines,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const { riskClass } = classifyRisk({
|
|
131
|
+
pathKind: pathAnalysis.kind,
|
|
132
|
+
isUnderKb: pathAnalysis.isUnderKb,
|
|
133
|
+
hasMustPriority,
|
|
134
|
+
hasDurableComment: !!precomputedSuggestion,
|
|
135
|
+
fileContent,
|
|
136
|
+
});
|
|
137
|
+
const effectiveRiskClass = riskClass === "safe_docs_only" && precomputedSuggestion
|
|
138
|
+
? "traceability_candidate"
|
|
139
|
+
: riskClass;
|
|
140
|
+
env.state.lastRiskClass = effectiveRiskClass;
|
|
141
|
+
env.state.lastRiskFilePath = normalizedFilePath;
|
|
142
|
+
env.state.lastRiskScopeKey = env.buildRiskPathScopeKey(eventContext, normalizedFilePath);
|
|
143
|
+
env.log.info("smart-enforcement.risk", {
|
|
144
|
+
event: "smart_enforcement_risk",
|
|
145
|
+
file: normalizedFilePath,
|
|
146
|
+
path_kind: pathAnalysis.kind,
|
|
147
|
+
risk_class: effectiveRiskClass,
|
|
148
|
+
posture_state: eventContext.posture,
|
|
149
|
+
maintenance_state: env.getMaintenanceDegraded()
|
|
150
|
+
? "maintenance_degraded"
|
|
151
|
+
: "maintenance_available",
|
|
152
|
+
under_kb: pathAnalysis.isUnderKb,
|
|
153
|
+
has_must_priority: hasMustPriority,
|
|
154
|
+
posture: eventContext.posture,
|
|
155
|
+
reason_code: effectiveRiskClass,
|
|
156
|
+
effective_mode: env.getEffectiveMode(),
|
|
157
|
+
static_degraded: env.posture.maintenanceDegraded,
|
|
158
|
+
runtime_degraded: env.runtimeOverlay.degraded,
|
|
159
|
+
merged_degraded: env.getMaintenanceDegraded(),
|
|
160
|
+
overlay_cause: env.runtimeOverlay.primaryCause ?? null,
|
|
161
|
+
});
|
|
162
|
+
const targetedChecksBlocked = env.getMaintenanceDegraded() ||
|
|
163
|
+
env.runtimeOverlay.primaryCause === "sync_disabled" ||
|
|
164
|
+
env.runtimeOverlay.primaryCause === "scheduler_unavailable" ||
|
|
165
|
+
env.runtimeOverlay.primaryCause === "scheduler_sync_failed" ||
|
|
166
|
+
env.runtimeOverlay.primaryCause === "scheduler_check_failed";
|
|
167
|
+
scheduleTargetedChecksForRisk(env, {
|
|
168
|
+
eventContext,
|
|
169
|
+
scopedScheduler,
|
|
170
|
+
normalizedFilePath,
|
|
171
|
+
pathKind: pathAnalysis.kind,
|
|
172
|
+
effectiveRiskClass,
|
|
173
|
+
targetedChecksBlocked,
|
|
174
|
+
});
|
|
175
|
+
env.updateRecentEditsFromSession(sessionEdits, scopedPathKindCache);
|
|
176
|
+
if (effectiveRiskClass === "safe_docs_only" ||
|
|
177
|
+
effectiveRiskClass === "safe_test_only") {
|
|
178
|
+
env.state.recentCommentSuggestion = null;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const cacheKey = env.buildScopedCacheKey(eventContext, effectiveRiskClass, fileBucketOf(pathAnalysis.kind), [normalizedFilePath, pathAnalysis.kind, effectiveRiskClass]);
|
|
182
|
+
// Always process manual_kb_edit before cache check — critical safety signal
|
|
183
|
+
if (effectiveRiskClass === "manual_kb_edit") {
|
|
184
|
+
handleManualKbEdit(env, normalizedFilePath);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
// Always emit requirement lint warnings before cache check — safety signals
|
|
188
|
+
if (effectiveRiskClass === "req_policy_candidate") {
|
|
189
|
+
const lintWarnings = env.lintRequirementDoc(normalizedFilePath, eventContext.worktreeRoot);
|
|
190
|
+
for (const warning of lintWarnings) {
|
|
191
|
+
env.recordWarning(warning.category, normalizedFilePath, warning.message);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (env.cache.isSatisfied(cacheKey)) {
|
|
195
|
+
env.log.info("smart-enforcement.cache", {
|
|
196
|
+
event: "smart_enforcement_cache",
|
|
197
|
+
cache_hit: true,
|
|
198
|
+
cache_state: "hit",
|
|
199
|
+
file: normalizedFilePath,
|
|
200
|
+
risk_class: effectiveRiskClass,
|
|
201
|
+
posture: eventContext.posture,
|
|
202
|
+
posture_state: eventContext.posture,
|
|
203
|
+
});
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
env.log.info("smart-enforcement.cache", {
|
|
207
|
+
event: "smart_enforcement_cache",
|
|
208
|
+
cache_hit: false,
|
|
209
|
+
cache_state: "miss",
|
|
210
|
+
file: normalizedFilePath,
|
|
211
|
+
risk_class: effectiveRiskClass,
|
|
212
|
+
posture: eventContext.posture,
|
|
213
|
+
posture_state: eventContext.posture,
|
|
214
|
+
});
|
|
215
|
+
if (effectiveRiskClass === "req_policy_candidate") {
|
|
216
|
+
handleReqPolicyCandidate(env, {
|
|
217
|
+
eventContext,
|
|
218
|
+
scopedScheduler,
|
|
219
|
+
normalizedFilePath,
|
|
220
|
+
hasMustPriority,
|
|
221
|
+
});
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (effectiveRiskClass === "kb_doc_structural") {
|
|
225
|
+
warnDegradedFor(env, "kb_doc_structural", normalizedFilePath, eventContext.posture);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (effectiveRiskClass === "behavior_candidate" ||
|
|
229
|
+
effectiveRiskClass === "traceability_candidate") {
|
|
230
|
+
emitBehaviorSuggestions(env, {
|
|
231
|
+
eventContext,
|
|
232
|
+
normalizedFilePath,
|
|
233
|
+
pathKind: pathAnalysis.kind,
|
|
234
|
+
commentDetectionEnabled: env.cfg.guidance.commentDetection.enabled,
|
|
235
|
+
precomputedSuggestion,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function filePathOf(resolution) {
|
|
240
|
+
return resolution.normalizedFilePath;
|
|
241
|
+
}
|
|
242
|
+
function fileBucketOf(kind) {
|
|
243
|
+
return kind;
|
|
244
|
+
}
|
|
245
|
+
function handleDeletedFile(env, args) {
|
|
246
|
+
const { eventContext, scopedSessionEditState, scopedPathKindCache, scopedScheduler, normalizedFilePath, pathKind, } = args;
|
|
247
|
+
// Preserve last known semantic risk if path was already tracked during session
|
|
248
|
+
const lastKnownKind = scopedPathKindCache.get(normalizedFilePath);
|
|
249
|
+
if (lastKnownKind) {
|
|
250
|
+
// Path was tracked — preserve last known semantic risk for reminder routing
|
|
251
|
+
scopedPathKindCache.set(normalizedFilePath, pathKind);
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
// Not tracked — classify only for reminder routing.
|
|
255
|
+
scopedPathKindCache.set(normalizedFilePath, pathKind);
|
|
256
|
+
}
|
|
257
|
+
scopedSessionEditState.recordEventHint(normalizedFilePath, pathKind, Date.now());
|
|
258
|
+
scopedSessionEditState.reconcilePath(normalizedFilePath);
|
|
259
|
+
const sessionEdits = scopedSessionEditState.getSessionEdits();
|
|
260
|
+
env.updateRecentEditsFromSession(sessionEdits, scopedPathKindCache);
|
|
261
|
+
// Schedule background sync for deleted files that pass shouldHandleFile
|
|
262
|
+
if (env.cfg.sync.enabled &&
|
|
263
|
+
scopedScheduler &&
|
|
264
|
+
env.fileFilter.shouldHandleFile(normalizedFilePath, eventContext.worktreeRoot)) {
|
|
265
|
+
scopedScheduler.scheduleSync("file.deleted", normalizedFilePath);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function scheduleTargetedChecksForRisk(env, args) {
|
|
269
|
+
const { eventContext, scopedScheduler, normalizedFilePath, pathKind, effectiveRiskClass, targetedChecksBlocked, } = args;
|
|
270
|
+
if (!targetedChecksBlocked &&
|
|
271
|
+
env.cfg.sync.enabled &&
|
|
272
|
+
scopedScheduler &&
|
|
273
|
+
env.cfg.guidance.targetedChecks.enabled) {
|
|
274
|
+
const traceabilityRules = effectiveRiskClass === "traceability_candidate"
|
|
275
|
+
? ["symbol-traceability"]
|
|
276
|
+
: null;
|
|
277
|
+
const kbStructuralRules = effectiveRiskClass === "kb_doc_structural" &&
|
|
278
|
+
env.fileFilter.shouldHandleFile(normalizedFilePath, eventContext.worktreeRoot)
|
|
279
|
+
? [
|
|
280
|
+
"required-fields",
|
|
281
|
+
"no-dangling-refs",
|
|
282
|
+
...(pathKind === "fact" ? ["strict-fact-shape"] : []),
|
|
283
|
+
...(pathKind === "requirement" ? ["strict-req-fact-pairing"] : []),
|
|
284
|
+
]
|
|
285
|
+
: null;
|
|
286
|
+
const checkRules = traceabilityRules ?? kbStructuralRules;
|
|
287
|
+
if (checkRules) {
|
|
288
|
+
env.log.info("smart-enforcement.targeted-checks", {
|
|
289
|
+
event: "smart_enforcement_targeted_checks",
|
|
290
|
+
file: normalizedFilePath,
|
|
291
|
+
risk_class: effectiveRiskClass,
|
|
292
|
+
posture: eventContext.posture,
|
|
293
|
+
posture_state: eventContext.posture,
|
|
294
|
+
guidance_action: "targeted_checks",
|
|
295
|
+
effective_mode: env.getEffectiveMode(),
|
|
296
|
+
rules: checkRules,
|
|
297
|
+
static_degraded: env.posture.maintenanceDegraded,
|
|
298
|
+
runtime_degraded: env.runtimeOverlay.degraded,
|
|
299
|
+
merged_degraded: env.getMaintenanceDegraded(),
|
|
300
|
+
overlay_cause: env.runtimeOverlay.primaryCause ?? null,
|
|
301
|
+
});
|
|
302
|
+
env.log.info(`kibi-opencode: scheduling sync for ${normalizedFilePath}`);
|
|
303
|
+
scopedScheduler.scheduleSync(effectiveRiskClass === "traceability_candidate"
|
|
304
|
+
? "smart-enforcement.traceability"
|
|
305
|
+
: "smart-enforcement.kb-doc", normalizedFilePath, checkRules);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function handleManualKbEdit(env, normalizedFilePath) {
|
|
310
|
+
env.state.hasRecentKbEdit = true;
|
|
311
|
+
if (env.cfg.guidance.warnOnKbEdits) {
|
|
312
|
+
env.log.warn(`kibi-opencode: .kb edit detected for ${normalizedFilePath}`);
|
|
313
|
+
env.recordWarning("kb-edit", normalizedFilePath, `Manual .kb edit: ${normalizedFilePath}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function lintRequirementPolicy(env, normalizedFilePath, worktreeRoot) {
|
|
317
|
+
const lintWarnings = env.lintRequirementDoc(normalizedFilePath, worktreeRoot);
|
|
318
|
+
for (const warning of lintWarnings) {
|
|
319
|
+
env.recordWarning(warning.category, normalizedFilePath, warning.message);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function warnDegradedFor(env, riskClass, normalizedFilePath, posture) {
|
|
323
|
+
if (!env.getMaintenanceDegraded())
|
|
324
|
+
return;
|
|
325
|
+
const logFn = env.cfg.guidance.smartEnforcement.degradedMode === "warn-once"
|
|
326
|
+
? env.log.warn
|
|
327
|
+
: env.log.info;
|
|
328
|
+
logFn("smart-enforcement.degraded", {
|
|
329
|
+
event: "smart_enforcement_degraded",
|
|
330
|
+
file: normalizedFilePath,
|
|
331
|
+
risk_class: riskClass,
|
|
332
|
+
posture,
|
|
333
|
+
posture_state: posture,
|
|
334
|
+
maintenance_state: env.getMaintenanceDegraded()
|
|
335
|
+
? "maintenance_degraded"
|
|
336
|
+
: "maintenance_available",
|
|
337
|
+
reason: env.runtimeOverlay.primaryCause ?? "non_authoritative_posture",
|
|
338
|
+
reason_code: env.runtimeOverlay.primaryCause ?? "non_authoritative_posture",
|
|
339
|
+
static_degraded: false,
|
|
340
|
+
runtime_degraded: env.runtimeOverlay.degraded,
|
|
341
|
+
merged_degraded: env.getMaintenanceDegraded(),
|
|
342
|
+
overlay_cause: env.runtimeOverlay.primaryCause ?? null,
|
|
343
|
+
effective_mode: env.getEffectiveMode(),
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
function handleReqPolicyCandidate(env, args) {
|
|
347
|
+
const { eventContext, scopedScheduler, normalizedFilePath, hasMustPriority } = args;
|
|
348
|
+
warnDegradedFor(env, "req_policy_candidate", normalizedFilePath, {
|
|
349
|
+
posture: eventContext.posture,
|
|
350
|
+
});
|
|
351
|
+
if (!env.getMaintenanceDegraded() &&
|
|
352
|
+
env.cfg.sync.enabled &&
|
|
353
|
+
scopedScheduler &&
|
|
354
|
+
env.fileFilter.shouldHandleFile(normalizedFilePath, eventContext.worktreeRoot)) {
|
|
355
|
+
let checkRules;
|
|
356
|
+
if (env.cfg.guidance.targetedChecks.enabled) {
|
|
357
|
+
if (hasMustPriority && env.getEffectiveMode() === "strict") {
|
|
358
|
+
checkRules = [
|
|
359
|
+
"required-fields",
|
|
360
|
+
"no-dangling-refs",
|
|
361
|
+
"must-priority-coverage",
|
|
362
|
+
"strict-req-fact-pairing",
|
|
363
|
+
];
|
|
364
|
+
env.log.info(`kibi-opencode: must-priority requirement detected, scheduling elevated checks for ${normalizedFilePath}`);
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
checkRules = [
|
|
368
|
+
"required-fields",
|
|
369
|
+
"no-dangling-refs",
|
|
370
|
+
"strict-req-fact-pairing",
|
|
371
|
+
];
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
env.log.info("smart-enforcement.targeted-checks", {
|
|
375
|
+
event: "smart_enforcement_targeted_checks",
|
|
376
|
+
file: normalizedFilePath,
|
|
377
|
+
risk_class: "req_policy_candidate",
|
|
378
|
+
posture: eventContext.posture,
|
|
379
|
+
posture_state: eventContext.posture,
|
|
380
|
+
guidance_action: "targeted_checks",
|
|
381
|
+
effective_mode: env.getEffectiveMode(),
|
|
382
|
+
rules: checkRules ?? [],
|
|
383
|
+
static_degraded: env.posture.maintenanceDegraded,
|
|
384
|
+
runtime_degraded: env.runtimeOverlay.degraded,
|
|
385
|
+
merged_degraded: env.getMaintenanceDegraded(),
|
|
386
|
+
overlay_cause: env.runtimeOverlay.primaryCause ?? null,
|
|
387
|
+
});
|
|
388
|
+
scopedScheduler.scheduleSync("file.edited", normalizedFilePath, checkRules);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
function emitBehaviorSuggestions(env, args) {
|
|
392
|
+
const { eventContext, normalizedFilePath, pathKind, precomputedSuggestion } = args;
|
|
393
|
+
const inspectingCodeComments = pathKind === "code" && env.cfg.guidance.commentDetection.enabled;
|
|
394
|
+
const suggestion = nextRecentCommentSuggestion(inspectingCodeComments, precomputedSuggestion);
|
|
395
|
+
env.state.recentCommentSuggestion = suggestion;
|
|
396
|
+
if (suggestion) {
|
|
397
|
+
const dedupeKey = `${env.buildRiskPathScopeKey(eventContext, normalizedFilePath)}:${suggestion.suggestionType}:${suggestion.fingerprint}`;
|
|
398
|
+
if (!env.state.seenFingerprints.has(dedupeKey)) {
|
|
399
|
+
env.state.seenFingerprints.add(dedupeKey);
|
|
400
|
+
const warningCategory = suggestion.suggestionType === "fact"
|
|
401
|
+
? "long-comment-missed-fact"
|
|
402
|
+
: suggestion.suggestionType === "adr"
|
|
403
|
+
? "long-comment-missed-adr"
|
|
404
|
+
: "missing-traceability";
|
|
405
|
+
env.log.warn(`kibi-opencode: detected durable ${suggestion.suggestionType} knowledge in ${normalizedFilePath}`);
|
|
406
|
+
env.recordWarning(warningCategory, normalizedFilePath, `Consider routing this ${suggestion.suggestionType} knowledge to Kibi instead of inline comments: ${suggestion.reasoning}`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function nextRecentCommentSuggestion(inspectingCodeComments, suggestion) {
|
|
411
|
+
if (inspectingCodeComments && suggestion)
|
|
412
|
+
return suggestion;
|
|
413
|
+
return null;
|
|
414
|
+
}
|
package/dist/tui.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"opencode":"
|
|
1
|
+
{"opencode":"2.0.1","mcp":"2.1.0","cli":"2.1.0","core":"0.13.0"}
|
|
@@ -17,5 +17,15 @@ export interface WorkContext {
|
|
|
17
17
|
sessionId: string | undefined;
|
|
18
18
|
agentIdentity: string;
|
|
19
19
|
}
|
|
20
|
+
export interface GitMetadata {
|
|
21
|
+
worktreeRoot: string;
|
|
22
|
+
gitDir: string;
|
|
23
|
+
commonGitDir: string;
|
|
24
|
+
isLinkedWorktree: boolean;
|
|
25
|
+
}
|
|
26
|
+
export declare function nextAncestorDirectory(current: string): string | undefined;
|
|
27
|
+
export declare function authorityRootFromLinkedGitDir(gitDir: string): string | null;
|
|
28
|
+
export declare function ancestorKbRoots(start: string): string[];
|
|
29
|
+
export declare function resolveBranch(git: GitMetadata | null): string;
|
|
20
30
|
declare const resolveWorkContext: (input: ResolveWorkContextInput) => WorkContext;
|
|
21
31
|
export { resolveWorkContext };
|
|
@@ -46,10 +46,14 @@ function directorySearchStart(candidatePath) {
|
|
|
46
46
|
const stats = safeStat(resolved);
|
|
47
47
|
return stats?.isDirectory() ? resolved : dirname(resolved);
|
|
48
48
|
}
|
|
49
|
+
export function nextAncestorDirectory(current) {
|
|
50
|
+
const parent = dirname(current);
|
|
51
|
+
return parent === current ? undefined : parent;
|
|
52
|
+
}
|
|
49
53
|
function findGitMetadata(candidatePath, searchBoundary) {
|
|
50
54
|
let current = directorySearchStart(candidatePath);
|
|
51
55
|
const boundary = searchBoundary ? resolve(searchBoundary) : null;
|
|
52
|
-
while (
|
|
56
|
+
while (current !== undefined) {
|
|
53
57
|
const dotGitPath = join(current, ".git");
|
|
54
58
|
const dotGitStats = safeStat(dotGitPath);
|
|
55
59
|
if (dotGitStats?.isFile()) {
|
|
@@ -74,12 +78,9 @@ function findGitMetadata(candidatePath, searchBoundary) {
|
|
|
74
78
|
if (boundary !== null && current === boundary) {
|
|
75
79
|
return null;
|
|
76
80
|
}
|
|
77
|
-
|
|
78
|
-
if (parent === current) {
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
current = parent;
|
|
81
|
+
current = nextAncestorDirectory(current);
|
|
82
82
|
}
|
|
83
|
+
return null;
|
|
83
84
|
}
|
|
84
85
|
function hasRootKbConfig(root) {
|
|
85
86
|
return existsSync(join(root, ".kb", "manifest.json"));
|
|
@@ -103,7 +104,7 @@ function uniqueResolved(paths) {
|
|
|
103
104
|
function authorityRootFromCommonGitDir(commonGitDir) {
|
|
104
105
|
return basename(commonGitDir) === ".git" ? dirname(commonGitDir) : null;
|
|
105
106
|
}
|
|
106
|
-
function authorityRootFromLinkedGitDir(gitDir) {
|
|
107
|
+
export function authorityRootFromLinkedGitDir(gitDir) {
|
|
107
108
|
const normalizedGitDir = resolve(gitDir);
|
|
108
109
|
const marker = `${sep}.git${sep}worktrees${sep}`;
|
|
109
110
|
const markerIndex = normalizedGitDir.indexOf(marker);
|
|
@@ -112,19 +113,16 @@ function authorityRootFromLinkedGitDir(gitDir) {
|
|
|
112
113
|
}
|
|
113
114
|
return normalizedGitDir.slice(0, markerIndex);
|
|
114
115
|
}
|
|
115
|
-
function ancestorKbRoots(start) {
|
|
116
|
+
export function ancestorKbRoots(start) {
|
|
116
117
|
const roots = [];
|
|
117
118
|
let current = resolve(start);
|
|
118
|
-
while (
|
|
119
|
+
while (current !== undefined) {
|
|
119
120
|
if (hasRootKbConfig(current)) {
|
|
120
121
|
roots.push(current);
|
|
121
122
|
}
|
|
122
|
-
|
|
123
|
-
if (parent === current) {
|
|
124
|
-
return roots;
|
|
125
|
-
}
|
|
126
|
-
current = parent;
|
|
123
|
+
current = nextAncestorDirectory(current);
|
|
127
124
|
}
|
|
125
|
+
return roots;
|
|
128
126
|
}
|
|
129
127
|
function resolveAuthorityRoot(git, inputDirectory) {
|
|
130
128
|
if (!git.isLinkedWorktree) {
|
|
@@ -141,7 +139,7 @@ function resolveAuthorityRoot(git, inputDirectory) {
|
|
|
141
139
|
candidates[0] ??
|
|
142
140
|
git.worktreeRoot);
|
|
143
141
|
}
|
|
144
|
-
function resolveBranch(git) {
|
|
142
|
+
export function resolveBranch(git) {
|
|
145
143
|
if (!git) {
|
|
146
144
|
return "unknown";
|
|
147
145
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kibi-opencode",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Kibi OpenCode plugin - thin adapter to integrate Kibi with OpenCode sessions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -37,9 +37,7 @@
|
|
|
37
37
|
"default": "./dist/tui.js"
|
|
38
38
|
}
|
|
39
39
|
},
|
|
40
|
-
"files": [
|
|
41
|
-
"dist"
|
|
42
|
-
],
|
|
40
|
+
"files": ["dist"],
|
|
43
41
|
"engines": {
|
|
44
42
|
"node": ">=22"
|
|
45
43
|
},
|
|
@@ -62,7 +60,7 @@
|
|
|
62
60
|
"@opencode-ai/plugin": "^1.4.7",
|
|
63
61
|
"@opentui/core": "^0.1.99",
|
|
64
62
|
"@opentui/solid": "^0.1.99",
|
|
65
|
-
"kibi-runtime": "^
|
|
63
|
+
"kibi-runtime": "^2.0.1"
|
|
66
64
|
},
|
|
67
65
|
"devDependencies": {
|
|
68
66
|
"@types/node": "latest",
|