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.
Files changed (38) hide show
  1. package/README.md +8 -6
  2. package/dist/auto-update.d.ts +2 -0
  3. package/dist/auto-update.js +5 -4
  4. package/dist/e2e-coverage-signals.js +3 -3
  5. package/dist/enforcement-policy.d.ts +10 -0
  6. package/dist/enforcement-policy.js +23 -23
  7. package/dist/file-entity-links.d.ts +4 -0
  8. package/dist/file-entity-links.js +6 -4
  9. package/dist/file-filter.d.ts +9 -9
  10. package/dist/file-filter.js +16 -68
  11. package/dist/kibi-bootstrap-alias.d.ts +2 -0
  12. package/dist/kibi-bootstrap-alias.js +14 -0
  13. package/dist/kibi-bootstrap-capability.d.ts +34 -0
  14. package/dist/{init-kibi-capability.js → kibi-bootstrap-capability.js} +26 -16
  15. package/dist/meaningful-change-classifier.d.ts +1 -0
  16. package/dist/meaningful-change-classifier.js +40 -10
  17. package/dist/path-kind.js +21 -39
  18. package/dist/plugin-startup.js +3 -3
  19. package/dist/plugin.d.ts +4 -1
  20. package/dist/plugin.js +81 -374
  21. package/dist/prompt.d.ts +4 -4
  22. package/dist/prompt.js +24 -60
  23. package/dist/repo-posture.js +31 -84
  24. package/dist/risk-classifier.d.ts +3 -3
  25. package/dist/risk-classifier.js +9 -6
  26. package/dist/scheduler.js +2 -2
  27. package/dist/session-edit-state.d.ts +4 -0
  28. package/dist/session-edit-state.js +5 -3
  29. package/dist/smart-enforcement-events.d.ts +100 -0
  30. package/dist/smart-enforcement-events.js +414 -0
  31. package/dist/version-metadata.json +1 -1
  32. package/dist/work-context-resolver.d.ts +10 -0
  33. package/dist/work-context-resolver.js +15 -17
  34. package/dist/workspace-health.js +8 -56
  35. package/package.json +6 -4
  36. package/dist/init-kibi-alias.d.ts +0 -14
  37. package/dist/init-kibi-alias.js +0 -43
  38. package/dist/init-kibi-capability.d.ts +0 -32
@@ -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
+ }
@@ -1 +1 @@
1
- {"opencode":"0.19.4","mcp":"0.22.0","cli":"0.19.0","core":"0.10.0"}
1
+ {"opencode":"2.0.0","mcp":"2.0.0","cli":"2.0.0","core":"0.12.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 (true) {
56
+ while (current !== undefined) {
53
57
  const dotGitPath = join(current, ".git");
54
58
  const dotGitStats = safeStat(dotGitPath);
55
59
  if (dotGitStats?.isFile()) {
@@ -74,15 +78,12 @@ function findGitMetadata(candidatePath, searchBoundary) {
74
78
  if (boundary !== null && current === boundary) {
75
79
  return null;
76
80
  }
77
- const parent = dirname(current);
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
- return existsSync(join(root, ".kb", "config.json"));
86
+ return existsSync(join(root, ".kb", "manifest.json"));
86
87
  }
87
88
  function uniqueResolved(paths) {
88
89
  const seen = new Set();
@@ -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 (true) {
119
+ while (current !== undefined) {
119
120
  if (hasRootKbConfig(current)) {
120
121
  roots.push(current);
121
122
  }
122
- const parent = dirname(current);
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
  }
@@ -157,7 +155,7 @@ function resolveBranch(git) {
157
155
  return "HEAD";
158
156
  }
159
157
  const branch = ref.slice("refs/heads/".length);
160
- return branch === "master" ? "main" : branch;
158
+ return branch;
161
159
  }
162
160
  function normalizeRepoRelativePath(fromRoot, targetPath) {
163
161
  const rawRelativePath = relative(fromRoot, targetPath);
@@ -3,18 +3,7 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { getKbExistenceTargets } from "./file-filter.js";
5
5
  import { detectPosture } from "./repo-posture.js";
6
- const KB_CONFIG_FILE = ".kb/config.json";
7
- // Fallback defaults used when .kb/config.json does not exist
8
- const KIBI_DOC_DIRS = [
9
- "documentation/requirements",
10
- "documentation/scenarios",
11
- "documentation/tests",
12
- "documentation/adr",
13
- "documentation/flags",
14
- "documentation/events",
15
- "documentation/facts",
16
- "documentation/symbols.yaml",
17
- ];
6
+ const KB_MANIFEST_FILE = ".kb/manifest.json";
18
7
  // implements REQ-opencode-kibi-plugin-v1
19
8
  /**
20
9
  * Analyze workspace health for Kibi bootstrap and initialization.
@@ -22,56 +11,19 @@ const KIBI_DOC_DIRS = [
22
11
  * bootstrap-needs to the posture result.
23
12
  */
24
13
  export function checkWorkspaceHealth(cwd) {
25
- // Use posture detection for root-scoped classification
26
14
  const posture = detectPosture(cwd);
27
- const configPath = path.join(cwd, KB_CONFIG_FILE);
28
- const missingConfig = !fs.existsSync(configPath);
15
+ const manifestPath = path.join(cwd, KB_MANIFEST_FILE);
16
+ const missingConfig = !fs.existsSync(manifestPath);
29
17
  const missingDocDirs = [];
30
- if (missingConfig) {
31
- // No config file: fall back to hardcoded defaults
32
- for (const docDir of KIBI_DOC_DIRS) {
33
- const fullPath = path.resolve(cwd, docDir);
34
- if (!fs.existsSync(fullPath)) {
35
- missingDocDirs.push(docDir);
36
- }
18
+ const targets = getKbExistenceTargets(cwd);
19
+ for (const target of targets) {
20
+ const fullPath = path.resolve(cwd, target.relativePath);
21
+ if (!fs.existsSync(fullPath)) {
22
+ missingDocDirs.push(target.relativePath);
37
23
  }
38
24
  }
39
- else {
40
- // Config exists: check if user specified custom paths
41
- let hasUserPaths = false;
42
- try {
43
- const raw = JSON.parse(fs.readFileSync(configPath, "utf8"));
44
- hasUserPaths = Boolean(raw?.paths);
45
- }
46
- catch {
47
- hasUserPaths = false;
48
- }
49
- if (hasUserPaths) {
50
- // User has custom paths: resolve targets dynamically
51
- const targets = getKbExistenceTargets(cwd);
52
- for (const target of targets) {
53
- const fullPath = path.resolve(cwd, target.relativePath);
54
- if (!fs.existsSync(fullPath)) {
55
- missingDocDirs.push(target.relativePath);
56
- }
57
- }
58
- }
59
- else {
60
- // Config exists but no custom paths: use hardcoded defaults
61
- for (const docDir of KIBI_DOC_DIRS) {
62
- const fullPath = path.resolve(cwd, docDir);
63
- if (!fs.existsSync(fullPath)) {
64
- missingDocDirs.push(docDir);
65
- }
66
- }
67
- }
68
- }
69
- // Check for any evidence of Kibi usage
70
25
  const kbDir = path.join(cwd, ".kb");
71
26
  const hasKbEvidence = fs.existsSync(kbDir) && fs.readdirSync(kbDir).length > 0;
72
- // Restore lenient threshold for repos that have a config but are missing a few dirs.
73
- // Uninitialized repos always need bootstrap; partial repos fall back to the legacy
74
- // >2 missing dirs threshold so small gaps (e.g. unused flags/events) do not nag.
75
27
  const needsBootstrap = posture.state === "root_uninitialized" ||
76
28
  (posture.state === "root_partial" && missingDocDirs.length > 2);
77
29
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-opencode",
3
- "version": "0.19.4",
3
+ "version": "2.0.0",
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,11 @@
37
37
  "default": "./dist/tui.js"
38
38
  }
39
39
  },
40
- "files": ["dist"],
40
+ "files": [
41
+ "dist"
42
+ ],
41
43
  "engines": {
42
- "node": ">=18"
44
+ "node": ">=22"
43
45
  },
44
46
  "license": "AGPL-3.0-or-later",
45
47
  "repository": {
@@ -60,7 +62,7 @@
60
62
  "@opencode-ai/plugin": "^1.4.7",
61
63
  "@opentui/core": "^0.1.99",
62
64
  "@opentui/solid": "^0.1.99",
63
- "kibi-cli": "^0.19.0"
65
+ "kibi-runtime": "^2.0.0"
64
66
  },
65
67
  "devDependencies": {
66
68
  "@types/node": "latest",
@@ -1,14 +0,0 @@
1
- /**
2
- * Builds the canonical native alias for the Kibi MCP bootstrap workflow.
3
- * This is a thin wrapper over the MCP-defined workflow, preserving all
4
- * semantic markers while removing namespacing and keeping text concise.
5
- *
6
- * Markers (MUST PRESERVE):
7
- * - "at most 4 bounded questions"
8
- * - "kb_autopilot_generate"
9
- * - "preview" or "approval"
10
- * - "kb_upsert"
11
- * - "kb_check"
12
- * - "sequential" or similar ordering language
13
- */
14
- export declare function buildInitKibiAlias(): string;