kibi-opencode 0.5.4 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -14
- package/dist/config.d.ts +9 -0
- package/dist/config.js +31 -0
- package/dist/guidance-cache.d.ts +75 -0
- package/dist/guidance-cache.js +145 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +428 -56
- package/dist/logger.d.ts +4 -3
- package/dist/logger.js +14 -18
- package/dist/path-kind.d.ts +2 -2
- package/dist/path-kind.js +12 -4
- package/dist/prompt.d.ts +22 -1
- package/dist/prompt.js +297 -121
- package/dist/repo-posture.d.ts +12 -0
- package/dist/repo-posture.js +196 -0
- package/dist/risk-classifier.d.ts +39 -0
- package/dist/risk-classifier.js +111 -0
- package/dist/smart-enforcement.d.ts +41 -0
- package/dist/smart-enforcement.js +48 -0
- package/dist/source-linked-guidance.d.ts +10 -0
- package/dist/source-linked-guidance.js +164 -0
- package/dist/workspace-health.d.ts +2 -0
- package/dist/workspace-health.js +14 -6
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,15 +1,44 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
1
2
|
import * as path from "node:path";
|
|
2
3
|
import { analyzeCodeFile, } from "./comment-analysis.js";
|
|
3
4
|
import * as config from "./config.js";
|
|
4
5
|
import * as fileFilter from "./file-filter.js";
|
|
6
|
+
import { getGuidanceCache } from "./guidance-cache.js";
|
|
5
7
|
import * as logger from "./logger.js";
|
|
6
8
|
import { analyzePath } from "./path-kind.js";
|
|
7
|
-
import {
|
|
9
|
+
import { SENTINEL, buildPrompt } from "./prompt.js";
|
|
10
|
+
import { detectPosture } from "./repo-posture.js";
|
|
8
11
|
import { isMustPriorityRequirement } from "./requirement-doc.js";
|
|
9
|
-
import {
|
|
12
|
+
import { classifyRisk } from "./risk-classifier.js";
|
|
13
|
+
import { createSyncScheduler as importedCreateSyncScheduler, } from "./scheduler.js";
|
|
10
14
|
import { getSessionTracker } from "./session-tracker.js";
|
|
15
|
+
import { computeEffectiveMode, } from "./smart-enforcement.js";
|
|
11
16
|
import { checkWorkspaceHealth } from "./workspace-health.js";
|
|
12
17
|
import * as fs from "node:fs";
|
|
18
|
+
function deriveFileBucket(kind) {
|
|
19
|
+
return kind;
|
|
20
|
+
}
|
|
21
|
+
function resolveCurrentBranch(cwd) {
|
|
22
|
+
try {
|
|
23
|
+
return execSync("git rev-parse --abbrev-ref HEAD", {
|
|
24
|
+
cwd,
|
|
25
|
+
encoding: "utf8",
|
|
26
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
27
|
+
}).trim();
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return "unknown";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function readConfigFingerprint(cwd) {
|
|
34
|
+
try {
|
|
35
|
+
return fs.readFileSync(path.join(cwd, ".kb", "config.json"), "utf-8");
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return "missing";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const workspaceCacheState = new Map();
|
|
13
42
|
/**
|
|
14
43
|
* Lint requirement documents for embedded scenarios/tests and oversized content.
|
|
15
44
|
*/
|
|
@@ -75,6 +104,92 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
75
104
|
tracker.reset();
|
|
76
105
|
}
|
|
77
106
|
}
|
|
107
|
+
const posture = detectPosture(input.worktree);
|
|
108
|
+
const currentBranch = resolveCurrentBranch(input.worktree);
|
|
109
|
+
const configFingerprint = readConfigFingerprint(input.worktree);
|
|
110
|
+
const cache = getGuidanceCache(cfg.guidance.smartEnforcement.preflightTtlMs, cfg.guidance.smartEnforcement.idleResetMs);
|
|
111
|
+
const previousCacheState = workspaceCacheState.get(input.worktree);
|
|
112
|
+
if (previousCacheState) {
|
|
113
|
+
if (previousCacheState.branch !== currentBranch) {
|
|
114
|
+
cache.invalidateForBranch(previousCacheState.branch);
|
|
115
|
+
}
|
|
116
|
+
if (previousCacheState.posture !== posture.state ||
|
|
117
|
+
previousCacheState.configFingerprint !== configFingerprint) {
|
|
118
|
+
cache.invalidateForWorkspace(input.worktree);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
workspaceCacheState.set(input.worktree, {
|
|
122
|
+
branch: currentBranch,
|
|
123
|
+
posture: posture.state,
|
|
124
|
+
configFingerprint,
|
|
125
|
+
});
|
|
126
|
+
// Session-local runtime degraded overlay (latched, never cleared)
|
|
127
|
+
const runtimeOverlay = {
|
|
128
|
+
degraded: false,
|
|
129
|
+
causes: [],
|
|
130
|
+
};
|
|
131
|
+
let degradedWarnedOnce = false;
|
|
132
|
+
function latchRuntimeDegraded(cause) {
|
|
133
|
+
if (!runtimeOverlay.degraded) {
|
|
134
|
+
runtimeOverlay.degraded = true;
|
|
135
|
+
runtimeOverlay.primaryCause = cause;
|
|
136
|
+
runtimeOverlay.causes.push(cause);
|
|
137
|
+
logger.info("smart-enforcement.degraded", {
|
|
138
|
+
event: "smart_enforcement_degraded",
|
|
139
|
+
overlay_cause: cause,
|
|
140
|
+
runtime_degraded: true,
|
|
141
|
+
static_degraded: posture.maintenanceDegraded,
|
|
142
|
+
merged_degraded: getMaintenanceDegraded(),
|
|
143
|
+
maintenance_state: getMaintenanceDegraded()
|
|
144
|
+
? "maintenance_degraded"
|
|
145
|
+
: "maintenance_available",
|
|
146
|
+
effective_mode: getEffectiveMode(),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
else if (!runtimeOverlay.causes.includes(cause)) {
|
|
150
|
+
runtimeOverlay.causes.push(cause);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function getMaintenanceDegraded() {
|
|
154
|
+
return posture.maintenanceDegraded || runtimeOverlay.degraded;
|
|
155
|
+
}
|
|
156
|
+
function getEffectiveMode() {
|
|
157
|
+
return computeEffectiveMode({
|
|
158
|
+
mode: cfg.guidance.smartEnforcement.mode,
|
|
159
|
+
requireRootKbForStrict: cfg.guidance.smartEnforcement.requireRootKbForStrict,
|
|
160
|
+
posture: posture.state,
|
|
161
|
+
maintenanceDegraded: getMaintenanceDegraded(),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
// Compute effective smart-enforcement mode from config + posture + runtime overlay
|
|
165
|
+
// Latch startup-level runtime degraded causes
|
|
166
|
+
if (posture.state === "vendored_only" ||
|
|
167
|
+
posture.state === "root_uninitialized" ||
|
|
168
|
+
posture.state === "root_partial") {
|
|
169
|
+
latchRuntimeDegraded("non_authoritative_posture");
|
|
170
|
+
}
|
|
171
|
+
if (!cfg.sync.enabled) {
|
|
172
|
+
latchRuntimeDegraded("sync_disabled");
|
|
173
|
+
}
|
|
174
|
+
const maintenanceDegraded = getMaintenanceDegraded();
|
|
175
|
+
logger.info("smart-enforcement.posture", {
|
|
176
|
+
event: "smart_enforcement_posture",
|
|
177
|
+
posture: posture.state,
|
|
178
|
+
posture_state: posture.state,
|
|
179
|
+
maintenance_state: maintenanceDegraded
|
|
180
|
+
? "maintenance_degraded"
|
|
181
|
+
: "maintenance_available",
|
|
182
|
+
needs_bootstrap: workspaceHealth.needsBootstrap,
|
|
183
|
+
posture_reason: posture.reason,
|
|
184
|
+
reason_code: posture.reason,
|
|
185
|
+
smart_enforcement_mode: cfg.guidance.smartEnforcement.mode,
|
|
186
|
+
effective_mode: getEffectiveMode(),
|
|
187
|
+
static_degraded: posture.maintenanceDegraded,
|
|
188
|
+
runtime_degraded: runtimeOverlay.degraded,
|
|
189
|
+
merged_degraded: maintenanceDegraded,
|
|
190
|
+
overlay_cause: runtimeOverlay.primaryCause ?? null,
|
|
191
|
+
branch: currentBranch,
|
|
192
|
+
});
|
|
78
193
|
logger.info("kibi-opencode: setting up hooks");
|
|
79
194
|
const hooks = {};
|
|
80
195
|
// Plugin instance state (not module globals)
|
|
@@ -83,14 +198,30 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
83
198
|
let hasRecentKbEdit = false;
|
|
84
199
|
let recentCommentSuggestion = null;
|
|
85
200
|
const seenFingerprints = new Set(); // For deduplication
|
|
201
|
+
let lastRiskClass = null;
|
|
202
|
+
const createSyncScheduler = globalThis.__kibi_test_scheduler_factory ?? importedCreateSyncScheduler;
|
|
86
203
|
// Create scheduler only if sync is enabled
|
|
87
204
|
let scheduler = null;
|
|
88
205
|
if (cfg.sync.enabled) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
206
|
+
try {
|
|
207
|
+
const schedulerOpts = {
|
|
208
|
+
worktree: input.worktree,
|
|
209
|
+
config: cfg,
|
|
210
|
+
onRunComplete: (meta) => {
|
|
211
|
+
if (meta.exitCode !== 0) {
|
|
212
|
+
latchRuntimeDegraded("scheduler_sync_failed");
|
|
213
|
+
}
|
|
214
|
+
if (meta.checkExitCode !== undefined && meta.checkExitCode !== 0) {
|
|
215
|
+
latchRuntimeDegraded("scheduler_check_failed");
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
scheduler = createSyncScheduler(schedulerOpts);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
latchRuntimeDegraded("scheduler_unavailable");
|
|
223
|
+
scheduler = null;
|
|
224
|
+
}
|
|
94
225
|
}
|
|
95
226
|
hooks.event = async ({ event }) => {
|
|
96
227
|
if (event.type !== "file.edited")
|
|
@@ -100,15 +231,96 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
100
231
|
if (!filePath)
|
|
101
232
|
return;
|
|
102
233
|
const pathAnalysis = analyzePath(filePath, input.worktree);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
234
|
+
let fileContent = "";
|
|
235
|
+
try {
|
|
236
|
+
const resolvedPath = input.worktree && !path.isAbsolute(filePath)
|
|
237
|
+
? path.join(input.worktree, filePath)
|
|
238
|
+
: filePath;
|
|
239
|
+
fileContent = fs.readFileSync(resolvedPath, "utf-8");
|
|
107
240
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
241
|
+
catch { }
|
|
242
|
+
const hasMustPriority = pathAnalysis.kind === "requirement"
|
|
243
|
+
? isMustPriorityRequirement(filePath, input.worktree)
|
|
244
|
+
: false;
|
|
245
|
+
let precomputedSuggestion = null;
|
|
246
|
+
if (pathAnalysis.kind === "code" && cfg.guidance.commentDetection.enabled) {
|
|
247
|
+
const resolvedPath = input.worktree && !path.isAbsolute(filePath)
|
|
248
|
+
? path.join(input.worktree, filePath)
|
|
249
|
+
: filePath;
|
|
250
|
+
precomputedSuggestion = analyzeCodeFile(resolvedPath, {
|
|
251
|
+
minLines: cfg.guidance.commentDetection.minLines,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
const { riskClass } = classifyRisk({
|
|
255
|
+
pathKind: pathAnalysis.kind,
|
|
256
|
+
isUnderKb: pathAnalysis.isUnderKb,
|
|
257
|
+
hasMustPriority,
|
|
258
|
+
hasDurableComment: !!precomputedSuggestion,
|
|
259
|
+
fileContent,
|
|
260
|
+
});
|
|
261
|
+
const effectiveRiskClass = riskClass === "safe_docs_only" && precomputedSuggestion
|
|
262
|
+
? "traceability_candidate"
|
|
263
|
+
: riskClass;
|
|
264
|
+
lastRiskClass = effectiveRiskClass;
|
|
265
|
+
logger.info("smart-enforcement.risk", {
|
|
266
|
+
event: "smart_enforcement_risk",
|
|
267
|
+
file: filePath,
|
|
268
|
+
path_kind: pathAnalysis.kind,
|
|
269
|
+
risk_class: effectiveRiskClass,
|
|
270
|
+
posture_state: posture.state,
|
|
271
|
+
maintenance_state: getMaintenanceDegraded()
|
|
272
|
+
? "maintenance_degraded"
|
|
273
|
+
: "maintenance_available",
|
|
274
|
+
under_kb: pathAnalysis.isUnderKb,
|
|
275
|
+
has_must_priority: hasMustPriority,
|
|
276
|
+
posture: posture.state,
|
|
277
|
+
reason_code: effectiveRiskClass,
|
|
278
|
+
effective_mode: getEffectiveMode(),
|
|
279
|
+
static_degraded: posture.maintenanceDegraded,
|
|
280
|
+
runtime_degraded: runtimeOverlay.degraded,
|
|
281
|
+
merged_degraded: getMaintenanceDegraded(),
|
|
282
|
+
overlay_cause: runtimeOverlay.primaryCause ?? null,
|
|
283
|
+
});
|
|
284
|
+
const targetedChecksBlocked = getMaintenanceDegraded() ||
|
|
285
|
+
runtimeOverlay.primaryCause === "sync_disabled" ||
|
|
286
|
+
runtimeOverlay.primaryCause === "scheduler_unavailable" ||
|
|
287
|
+
runtimeOverlay.primaryCause === "scheduler_sync_failed" ||
|
|
288
|
+
runtimeOverlay.primaryCause === "scheduler_check_failed";
|
|
289
|
+
if (!targetedChecksBlocked &&
|
|
290
|
+
cfg.sync.enabled &&
|
|
291
|
+
scheduler &&
|
|
292
|
+
cfg.guidance.targetedChecks.enabled) {
|
|
293
|
+
const traceabilityRules = effectiveRiskClass === "traceability_candidate"
|
|
294
|
+
? ["symbol-traceability"]
|
|
295
|
+
: null;
|
|
296
|
+
const kbStructuralRules = effectiveRiskClass === "kb_doc_structural" &&
|
|
297
|
+
fileFilter.shouldHandleFile(filePath, input.worktree)
|
|
298
|
+
? [
|
|
299
|
+
"required-fields",
|
|
300
|
+
"no-dangling-refs",
|
|
301
|
+
...(pathAnalysis.kind === "fact" ? ["strict-fact-shape"] : []),
|
|
302
|
+
]
|
|
303
|
+
: null;
|
|
304
|
+
const checkRules = traceabilityRules ?? kbStructuralRules;
|
|
305
|
+
if (checkRules) {
|
|
306
|
+
logger.info("smart-enforcement.targeted-checks", {
|
|
307
|
+
event: "smart_enforcement_targeted_checks",
|
|
308
|
+
file: filePath,
|
|
309
|
+
risk_class: effectiveRiskClass,
|
|
310
|
+
posture: posture.state,
|
|
311
|
+
posture_state: posture.state,
|
|
312
|
+
guidance_action: "targeted_checks",
|
|
313
|
+
effective_mode: getEffectiveMode(),
|
|
314
|
+
rules: checkRules,
|
|
315
|
+
static_degraded: posture.maintenanceDegraded,
|
|
316
|
+
runtime_degraded: runtimeOverlay.degraded,
|
|
317
|
+
merged_degraded: getMaintenanceDegraded(),
|
|
318
|
+
overlay_cause: runtimeOverlay.primaryCause ?? null,
|
|
319
|
+
});
|
|
320
|
+
logger.info(`kibi-opencode: scheduling sync for ${filePath}`);
|
|
321
|
+
scheduler.scheduleSync(effectiveRiskClass === "traceability_candidate"
|
|
322
|
+
? "smart-enforcement.traceability"
|
|
323
|
+
: "smart-enforcement.kb-doc", filePath, checkRules);
|
|
112
324
|
}
|
|
113
325
|
}
|
|
114
326
|
const now = Date.now();
|
|
@@ -120,59 +332,169 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
120
332
|
if (recentEdits.length > MAX_RECENT_EDITS) {
|
|
121
333
|
recentEdits = recentEdits.slice(-MAX_RECENT_EDITS);
|
|
122
334
|
}
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
getSessionTracker().recordWarning(warningCategory, filePath, `Consider routing this ${suggestion.suggestionType} knowledge to Kibi instead of inline comments: ${suggestion.reasoning}`);
|
|
142
|
-
}
|
|
335
|
+
if (effectiveRiskClass === "safe_docs_only" ||
|
|
336
|
+
effectiveRiskClass === "safe_test_only") {
|
|
337
|
+
recentCommentSuggestion = null;
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const cacheKey = {
|
|
341
|
+
workspaceRoot: input.worktree,
|
|
342
|
+
branch: currentBranch,
|
|
343
|
+
posture: posture.state,
|
|
344
|
+
riskClass: effectiveRiskClass,
|
|
345
|
+
fileBucket: deriveFileBucket(pathAnalysis.kind),
|
|
346
|
+
};
|
|
347
|
+
// Always process manual_kb_edit before cache check — this is a critical safety signal
|
|
348
|
+
if (effectiveRiskClass === "manual_kb_edit") {
|
|
349
|
+
hasRecentKbEdit = true;
|
|
350
|
+
if (cfg.guidance.warnOnKbEdits) {
|
|
351
|
+
logger.warn(`kibi-opencode: .kb edit detected for ${filePath}`);
|
|
352
|
+
getSessionTracker().recordWarning("kb-edit", filePath, `Manual .kb edit: ${filePath}`);
|
|
143
353
|
}
|
|
144
|
-
|
|
145
|
-
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
// Always emit requirement lint warnings before cache check — these are safety signals
|
|
357
|
+
if (effectiveRiskClass === "req_policy_candidate") {
|
|
358
|
+
const lintWarnings = lintRequirementDoc(filePath, input.worktree);
|
|
359
|
+
for (const warning of lintWarnings) {
|
|
360
|
+
getSessionTracker().recordWarning(warning.category, filePath, warning.message);
|
|
146
361
|
}
|
|
147
362
|
}
|
|
148
|
-
|
|
149
|
-
|
|
363
|
+
// Cache check: after critical signals have been emitted
|
|
364
|
+
if (cache.isSatisfied(cacheKey)) {
|
|
365
|
+
logger.info("smart-enforcement.cache", {
|
|
366
|
+
event: "smart_enforcement_cache",
|
|
367
|
+
cache_hit: true,
|
|
368
|
+
cache_state: "hit",
|
|
369
|
+
file: filePath,
|
|
370
|
+
risk_class: effectiveRiskClass,
|
|
371
|
+
posture: posture.state,
|
|
372
|
+
posture_state: posture.state,
|
|
373
|
+
});
|
|
374
|
+
return;
|
|
150
375
|
}
|
|
151
|
-
|
|
376
|
+
logger.info("smart-enforcement.cache", {
|
|
377
|
+
event: "smart_enforcement_cache",
|
|
378
|
+
cache_hit: false,
|
|
379
|
+
cache_state: "miss",
|
|
380
|
+
file: filePath,
|
|
381
|
+
risk_class: effectiveRiskClass,
|
|
382
|
+
posture: posture.state,
|
|
383
|
+
posture_state: posture.state,
|
|
384
|
+
});
|
|
385
|
+
if (effectiveRiskClass === "req_policy_candidate") {
|
|
386
|
+
if (getMaintenanceDegraded()) {
|
|
387
|
+
const logFn = cfg.guidance.smartEnforcement.degradedMode === "warn-once"
|
|
388
|
+
? logger.warn
|
|
389
|
+
: logger.info;
|
|
390
|
+
logFn("smart-enforcement.degraded", {
|
|
391
|
+
event: "smart_enforcement_degraded",
|
|
392
|
+
file: filePath,
|
|
393
|
+
risk_class: effectiveRiskClass,
|
|
394
|
+
posture: posture.state,
|
|
395
|
+
posture_state: posture.state,
|
|
396
|
+
maintenance_state: getMaintenanceDegraded()
|
|
397
|
+
? "maintenance_degraded"
|
|
398
|
+
: "maintenance_available",
|
|
399
|
+
reason: runtimeOverlay.primaryCause ?? "non_authoritative_posture",
|
|
400
|
+
reason_code: runtimeOverlay.primaryCause ?? "non_authoritative_posture",
|
|
401
|
+
static_degraded: posture.maintenanceDegraded,
|
|
402
|
+
runtime_degraded: runtimeOverlay.degraded,
|
|
403
|
+
merged_degraded: getMaintenanceDegraded(),
|
|
404
|
+
overlay_cause: runtimeOverlay.primaryCause ?? null,
|
|
405
|
+
effective_mode: getEffectiveMode(),
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
if (!getMaintenanceDegraded() &&
|
|
409
|
+
cfg.sync.enabled &&
|
|
410
|
+
scheduler &&
|
|
411
|
+
fileFilter.shouldHandleFile(filePath, input.worktree)) {
|
|
412
|
+
let checkRules;
|
|
413
|
+
if (cfg.guidance.targetedChecks.enabled) {
|
|
414
|
+
if (hasMustPriority && getEffectiveMode() === "strict") {
|
|
415
|
+
checkRules = [
|
|
416
|
+
"required-fields",
|
|
417
|
+
"no-dangling-refs",
|
|
418
|
+
"must-priority-coverage",
|
|
419
|
+
];
|
|
420
|
+
logger.info(`kibi-opencode: must-priority requirement detected, scheduling elevated checks for ${filePath}`);
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
checkRules = ["required-fields", "no-dangling-refs"];
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
logger.info("smart-enforcement.targeted-checks", {
|
|
427
|
+
event: "smart_enforcement_targeted_checks",
|
|
428
|
+
file: filePath,
|
|
429
|
+
risk_class: effectiveRiskClass,
|
|
430
|
+
posture: posture.state,
|
|
431
|
+
posture_state: posture.state,
|
|
432
|
+
guidance_action: "targeted_checks",
|
|
433
|
+
effective_mode: getEffectiveMode(),
|
|
434
|
+
rules: checkRules ?? [],
|
|
435
|
+
static_degraded: posture.maintenanceDegraded,
|
|
436
|
+
runtime_degraded: runtimeOverlay.degraded,
|
|
437
|
+
merged_degraded: getMaintenanceDegraded(),
|
|
438
|
+
overlay_cause: runtimeOverlay.primaryCause ?? null,
|
|
439
|
+
});
|
|
440
|
+
scheduler?.scheduleSync("file.edited", filePath, checkRules);
|
|
441
|
+
}
|
|
152
442
|
return;
|
|
153
|
-
|
|
443
|
+
}
|
|
444
|
+
if (effectiveRiskClass === "kb_doc_structural") {
|
|
445
|
+
if (getMaintenanceDegraded()) {
|
|
446
|
+
const logFn = cfg.guidance.smartEnforcement.degradedMode === "warn-once"
|
|
447
|
+
? logger.warn
|
|
448
|
+
: logger.info;
|
|
449
|
+
logFn("smart-enforcement.degraded", {
|
|
450
|
+
event: "smart_enforcement_degraded",
|
|
451
|
+
file: filePath,
|
|
452
|
+
risk_class: effectiveRiskClass,
|
|
453
|
+
posture: posture.state,
|
|
454
|
+
posture_state: posture.state,
|
|
455
|
+
maintenance_state: getMaintenanceDegraded()
|
|
456
|
+
? "maintenance_degraded"
|
|
457
|
+
: "maintenance_available",
|
|
458
|
+
reason: runtimeOverlay.primaryCause ?? "non_authoritative_posture",
|
|
459
|
+
reason_code: runtimeOverlay.primaryCause ?? "non_authoritative_posture",
|
|
460
|
+
static_degraded: posture.maintenanceDegraded,
|
|
461
|
+
runtime_degraded: runtimeOverlay.degraded,
|
|
462
|
+
merged_degraded: getMaintenanceDegraded(),
|
|
463
|
+
overlay_cause: runtimeOverlay.primaryCause ?? null,
|
|
464
|
+
effective_mode: getEffectiveMode(),
|
|
465
|
+
});
|
|
466
|
+
}
|
|
154
467
|
return;
|
|
155
|
-
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
468
|
+
}
|
|
469
|
+
if (effectiveRiskClass === "behavior_candidate" ||
|
|
470
|
+
effectiveRiskClass === "traceability_candidate") {
|
|
471
|
+
if (pathAnalysis.kind === "code" &&
|
|
472
|
+
cfg.guidance.commentDetection.enabled) {
|
|
473
|
+
const suggestion = precomputedSuggestion;
|
|
474
|
+
if (suggestion) {
|
|
475
|
+
recentCommentSuggestion = suggestion;
|
|
476
|
+
const dedupeKey = `${filePath}:${suggestion.suggestionType}:${suggestion.fingerprint}`;
|
|
477
|
+
if (!seenFingerprints.has(dedupeKey)) {
|
|
478
|
+
seenFingerprints.add(dedupeKey);
|
|
479
|
+
const warningCategory = suggestion.suggestionType === "fact"
|
|
480
|
+
? "long-comment-missed-fact"
|
|
481
|
+
: suggestion.suggestionType === "adr"
|
|
482
|
+
? "long-comment-missed-adr"
|
|
483
|
+
: "missing-traceability";
|
|
484
|
+
logger.warn(`kibi-opencode: detected durable ${suggestion.suggestionType} knowledge in ${filePath}`);
|
|
485
|
+
getSessionTracker().recordWarning(warningCategory, filePath, `Consider routing this ${suggestion.suggestionType} knowledge to Kibi instead of inline comments: ${suggestion.reasoning}`);
|
|
486
|
+
}
|
|
165
487
|
}
|
|
166
488
|
else {
|
|
167
|
-
|
|
489
|
+
recentCommentSuggestion = null;
|
|
168
490
|
}
|
|
169
491
|
}
|
|
170
|
-
else
|
|
171
|
-
|
|
492
|
+
else {
|
|
493
|
+
recentCommentSuggestion = null;
|
|
172
494
|
}
|
|
495
|
+
return;
|
|
173
496
|
}
|
|
174
|
-
|
|
175
|
-
scheduler?.scheduleSync("file.edited", filePath, checkRules);
|
|
497
|
+
return;
|
|
176
498
|
};
|
|
177
499
|
if (cfg.prompt.enabled) {
|
|
178
500
|
const hookMode = cfg.prompt.hookMode;
|
|
@@ -182,13 +504,63 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
182
504
|
if (output.system.some((entry) => entry.includes(SENTINEL))) {
|
|
183
505
|
return;
|
|
184
506
|
}
|
|
507
|
+
const maintenanceDegraded = getMaintenanceDegraded();
|
|
508
|
+
const showDegradedAdvisory = maintenanceDegraded &&
|
|
509
|
+
cfg.guidance.smartEnforcement.degradedMode === "warn-once" &&
|
|
510
|
+
!degradedWarnedOnce;
|
|
185
511
|
// Build only the guidance block and append it; existing entries are preserved
|
|
186
512
|
const guidance = buildPrompt({
|
|
187
513
|
recentEdits,
|
|
188
514
|
workspaceHealth,
|
|
189
515
|
hasRecentKbEdit,
|
|
190
516
|
recentCommentSuggestion,
|
|
517
|
+
posture: posture.state,
|
|
518
|
+
riskClass: lastRiskClass ?? undefined,
|
|
519
|
+
cache,
|
|
520
|
+
workspaceRoot: input.worktree,
|
|
521
|
+
branch: currentBranch,
|
|
522
|
+
completionReminder: cfg.guidance.smartEnforcement.completionReminder,
|
|
523
|
+
maintenanceDegraded,
|
|
524
|
+
degradedMode: cfg.guidance.smartEnforcement.degradedMode,
|
|
525
|
+
showDegradedAdvisory,
|
|
526
|
+
});
|
|
527
|
+
logger.info("smart-enforcement.guidance", {
|
|
528
|
+
event: "smart_enforcement_guidance",
|
|
529
|
+
emitted: guidance.trim() !== "" && guidance.trim() !== SENTINEL,
|
|
530
|
+
posture: posture.state,
|
|
531
|
+
posture_state: posture.state,
|
|
532
|
+
guidance_action: guidance.trim() !== "" && guidance.trim() !== SENTINEL
|
|
533
|
+
? "emit"
|
|
534
|
+
: "skip",
|
|
535
|
+
risk_class: lastRiskClass,
|
|
536
|
+
recent_edits: recentEdits.length,
|
|
537
|
+
static_degraded: posture.maintenanceDegraded,
|
|
538
|
+
runtime_degraded: runtimeOverlay.degraded,
|
|
539
|
+
merged_degraded: maintenanceDegraded,
|
|
540
|
+
overlay_cause: runtimeOverlay.primaryCause ?? null,
|
|
191
541
|
});
|
|
542
|
+
// Emit completion-reminder log only when prompt-visible reminder text is present
|
|
543
|
+
const REMINDER_TEXT = "Run `kb_check` before completing this task.";
|
|
544
|
+
if (cfg.guidance.smartEnforcement.completionReminder &&
|
|
545
|
+
!maintenanceDegraded &&
|
|
546
|
+
guidance.includes(REMINDER_TEXT)) {
|
|
547
|
+
logger.info("smart-enforcement.completion-reminder", {
|
|
548
|
+
event: "smart_enforcement_completion_reminder",
|
|
549
|
+
risk_class: lastRiskClass,
|
|
550
|
+
posture: posture.state,
|
|
551
|
+
posture_state: posture.state,
|
|
552
|
+
guidance_action: "completion_reminder",
|
|
553
|
+
reminder: "kb_check",
|
|
554
|
+
static_degraded: posture.maintenanceDegraded,
|
|
555
|
+
runtime_degraded: runtimeOverlay.degraded,
|
|
556
|
+
merged_degraded: maintenanceDegraded,
|
|
557
|
+
overlay_cause: runtimeOverlay.primaryCause ?? null,
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
// Latch degraded advisory warning-once state
|
|
561
|
+
if (showDegradedAdvisory && guidance.includes("Maintenance degraded")) {
|
|
562
|
+
degradedWarnedOnce = true;
|
|
563
|
+
}
|
|
192
564
|
const last = output.system.length > 0
|
|
193
565
|
? output.system[output.system.length - 1]
|
|
194
566
|
: undefined;
|
package/dist/logger.d.ts
CHANGED
|
@@ -3,8 +3,9 @@ export interface PluginClient {
|
|
|
3
3
|
log: (payload: Record<string, unknown>) => Promise<void>;
|
|
4
4
|
};
|
|
5
5
|
}
|
|
6
|
+
export type LogMetadata = Record<string, unknown>;
|
|
6
7
|
export declare function setClient(c: PluginClient): void;
|
|
7
8
|
export declare function resetClient(): void;
|
|
8
|
-
export declare function info(msg: string): void;
|
|
9
|
-
export declare function warn(msg: string): void;
|
|
10
|
-
export declare function error(msg: string): void;
|
|
9
|
+
export declare function info(msg: string, metadata?: LogMetadata): void;
|
|
10
|
+
export declare function warn(msg: string, metadata?: LogMetadata): void;
|
|
11
|
+
export declare function error(msg: string, metadata?: LogMetadata): void;
|
package/dist/logger.js
CHANGED
|
@@ -8,16 +8,20 @@ export function setClient(c) {
|
|
|
8
8
|
export function resetClient() {
|
|
9
9
|
client = null;
|
|
10
10
|
}
|
|
11
|
+
function buildBody(level, message, metadata) {
|
|
12
|
+
return {
|
|
13
|
+
service: "kibi-opencode",
|
|
14
|
+
level,
|
|
15
|
+
message,
|
|
16
|
+
...(metadata ?? {}),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
11
19
|
// implements REQ-opencode-kibi-plugin-v1
|
|
12
|
-
export function info(msg) {
|
|
20
|
+
export function info(msg, metadata) {
|
|
13
21
|
if (client) {
|
|
14
22
|
void client.app
|
|
15
23
|
.log({
|
|
16
|
-
body:
|
|
17
|
-
service: "kibi-opencode",
|
|
18
|
-
level: "info",
|
|
19
|
-
message: msg,
|
|
20
|
-
},
|
|
24
|
+
body: buildBody("info", msg, metadata),
|
|
21
25
|
})
|
|
22
26
|
.catch(console.error);
|
|
23
27
|
return;
|
|
@@ -25,15 +29,11 @@ export function info(msg) {
|
|
|
25
29
|
// Fallback when no client is available (e.g. during tests or early init)
|
|
26
30
|
}
|
|
27
31
|
// implements REQ-opencode-kibi-plugin-v1
|
|
28
|
-
export function warn(msg) {
|
|
32
|
+
export function warn(msg, metadata) {
|
|
29
33
|
if (client) {
|
|
30
34
|
void client.app
|
|
31
35
|
.log({
|
|
32
|
-
body:
|
|
33
|
-
service: "kibi-opencode",
|
|
34
|
-
level: "warn",
|
|
35
|
-
message: msg,
|
|
36
|
-
},
|
|
36
|
+
body: buildBody("warn", msg, metadata),
|
|
37
37
|
})
|
|
38
38
|
.catch(console.error);
|
|
39
39
|
return;
|
|
@@ -41,18 +41,14 @@ export function warn(msg) {
|
|
|
41
41
|
// Fallback when no client is available
|
|
42
42
|
}
|
|
43
43
|
// implements REQ-opencode-kibi-plugin-v1
|
|
44
|
-
export function error(msg) {
|
|
44
|
+
export function error(msg, metadata) {
|
|
45
45
|
// Always emit to console for user visibility
|
|
46
46
|
console.error("[kibi-opencode]", msg);
|
|
47
47
|
// Also emit to structured logs if client is available
|
|
48
48
|
if (client) {
|
|
49
49
|
void client.app
|
|
50
50
|
.log({
|
|
51
|
-
body:
|
|
52
|
-
service: "kibi-opencode",
|
|
53
|
-
level: "error",
|
|
54
|
-
message: msg,
|
|
55
|
-
},
|
|
51
|
+
body: buildBody("error", msg, metadata),
|
|
56
52
|
})
|
|
57
53
|
.catch(console.error);
|
|
58
54
|
}
|
package/dist/path-kind.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export type PathKind = "code" | "requirement" | "scenario" | "test" | "adr" | "fact" | "kb" | "unknown";
|
|
1
|
+
export type PathKind = "code" | "requirement" | "scenario" | "test" | "adr" | "fact" | "flag" | "event" | "symbol" | "kb" | "unknown";
|
|
2
2
|
export interface PathAnalysis {
|
|
3
3
|
kind: PathKind;
|
|
4
4
|
isUnderKb: boolean;
|
|
5
5
|
isKibiDocRelevant: boolean;
|
|
6
6
|
}
|
|
7
|
-
export declare function analyzePath(filePath: string, cwd
|
|
7
|
+
export declare function analyzePath(filePath: string, cwd?: string): PathAnalysis;
|