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/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 { buildPrompt, SENTINEL } from "./prompt.js";
9
+ import { SENTINEL, buildPrompt } from "./prompt.js";
10
+ import { detectPosture } from "./repo-posture.js";
8
11
  import { isMustPriorityRequirement } from "./requirement-doc.js";
9
- import { createSyncScheduler } from "./scheduler.js";
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
- const schedulerOpts = {
90
- worktree: input.worktree,
91
- config: cfg,
92
- };
93
- scheduler = createSyncScheduler(schedulerOpts);
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
- if (pathAnalysis.isUnderKb && cfg.guidance.warnOnKbEdits) {
104
- hasRecentKbEdit = true;
105
- logger.warn(`kibi-opencode: .kb edit detected for ${filePath}`);
106
- getSessionTracker().recordWarning("kb-edit", filePath, `Manual .kb edit: ${filePath}`);
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
- if (pathAnalysis.kind === "requirement") {
109
- const lintWarnings = lintRequirementDoc(filePath, input.worktree);
110
- for (const warning of lintWarnings) {
111
- getSessionTracker().recordWarning(warning.category, filePath, warning.message);
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 (pathAnalysis.kind === "code" && cfg.guidance.commentDetection.enabled) {
124
- const resolvedPath = input.worktree && !path.isAbsolute(filePath)
125
- ? path.join(input.worktree, filePath)
126
- : filePath;
127
- const suggestion = analyzeCodeFile(resolvedPath, {
128
- minLines: cfg.guidance.commentDetection.minLines,
129
- });
130
- if (suggestion) {
131
- recentCommentSuggestion = suggestion;
132
- const dedupeKey = `${filePath}:${suggestion.suggestionType}:${suggestion.fingerprint}`;
133
- if (!seenFingerprints.has(dedupeKey)) {
134
- seenFingerprints.add(dedupeKey);
135
- const warningCategory = suggestion.suggestionType === "fact"
136
- ? "long-comment-missed-fact"
137
- : suggestion.suggestionType === "adr"
138
- ? "long-comment-missed-adr"
139
- : "missing-traceability";
140
- logger.warn(`kibi-opencode: detected durable ${suggestion.suggestionType} knowledge in ${filePath}`);
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
- else {
145
- recentCommentSuggestion = null;
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
- else {
149
- recentCommentSuggestion = null;
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
- if (!cfg.sync.enabled)
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
- if (!fileFilter.shouldHandleFile(filePath, input.worktree))
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
- let checkRules;
156
- if (cfg.guidance.targetedChecks.enabled) {
157
- if (pathAnalysis.kind === "requirement") {
158
- if (isMustPriorityRequirement(filePath, input.worktree)) {
159
- checkRules = [
160
- "required-fields",
161
- "no-dangling-refs",
162
- "must-priority-coverage",
163
- ];
164
- logger.info(`kibi-opencode: must-priority requirement detected, scheduling elevated checks for ${filePath}`);
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
- checkRules = ["required-fields", "no-dangling-refs"];
489
+ recentCommentSuggestion = null;
168
490
  }
169
491
  }
170
- else if (["scenario", "test", "adr", "fact"].includes(pathAnalysis.kind)) {
171
- checkRules = ["required-fields", "no-dangling-refs"];
492
+ else {
493
+ recentCommentSuggestion = null;
172
494
  }
495
+ return;
173
496
  }
174
- logger.info(`kibi-opencode: scheduling sync for ${filePath}`);
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
  }
@@ -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: string): PathAnalysis;
7
+ export declare function analyzePath(filePath: string, cwd?: string): PathAnalysis;