kibi-opencode 0.3.1 → 0.4.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 CHANGED
@@ -12,22 +12,61 @@ Or via OpenCode's plugin system in `opencode.json`:
12
12
 
13
13
  ```json
14
14
  {
15
- "plugins": ["kibi-opencode"]
15
+ "plugin": ["kibi-opencode"]
16
16
  }
17
17
  ```
18
18
 
19
19
  ## Features
20
20
 
21
- ### Prompt Guidance Injection
21
+ ### Dynamic Contextual Guidance
22
+
23
+ The plugin provides context-aware prompt guidance based on recent edits and workspace state:
24
+
25
+ - **Code edits**: Guidance for querying Kibi by sourceFile, preferring Kibi over comments, and adding `// implements REQ-xxx` traceability
26
+ - **Requirement edits**: Guidance for maintaining separate REQ/SCEN/TEST artifacts and avoiding embedded scenarios
27
+ - **KB doc edits**: Guidance for proper entity relationships and validation
28
+ - **Bootstrap needed**: Detection and nudges for uninitialized repos
29
+
30
+ ### Targeted Validation Checks
31
+
32
+ After KB-document edits, the plugin queues targeted `kibi check` rules to run after sync:
33
+
34
+ - **Requirement/scenario/test/ADR/fact edits**: `kibi check --rules required-fields,no-dangling-refs`
35
+
36
+ Runs in background after sync completes, non-blocking. Can be disabled via `guidance.targetedChecks.enabled: false`.
22
37
 
23
- The plugin injects guidance into OpenCode sessions to improve agent grounding:
38
+ ### Loud `.kb/**` Edit Warnings
24
39
 
40
+ When `guidance.warnOnKbEdits` is enabled (default: `true`), manual edits to files under `.kb/**` trigger prominent warnings:
41
+
42
+ - Logs warning immediately
43
+ - Injects prompt guidance discouraging manual `.kb` edits
44
+ - Directs agents toward MCP/CLI tools (`kb_upsert`, `kb_query`, etc.)
45
+
46
+ ### Session Tracking and Pattern Detection
47
+
48
+ The plugin tracks warning patterns across the session and provides periodic summaries:
49
+
50
+ - **Warning categories**: kb-edit, embedded-scenario-in-req, embedded-test-in-req, long-comment-missed-fact, missing-traceability, bootstrap-needed
51
+ - **Repeated pattern alerts**: Warns when the same anti-pattern occurs 3+ times
52
+ - **Session summaries**: Periodic logs of total warnings and top patterns (default: every 30 minutes)
53
+ - **Top files with warnings**: Tracks which files generate the most guidance
54
+ - **Requirement linting**: Detects embedded scenarios/tests in requirement files
55
+
56
+ Example session summary:
25
57
  ```
26
- Query Kibi before design/implementation work. Prefer kb_query/kb_check for context. Update KB artifacts after relevant changes. Remember symbol traceability requirements.
58
+ session.summary: 12 total warnings
59
+ kb-edit: 3
60
+ missing-traceability: 5
61
+ bootstrap-needed: 1
62
+ embedded-scenario-in-req: 3
63
+ session.patterns: Repeated anti-patterns detected:
64
+ missing-traceability: 5 occurrences
27
65
  ```
28
66
 
29
- - Uses `<!-- kibi-opencode -->` sentinel to prevent duplicate injections
30
- - Respects `prompt.enabled` and overall `enabled` config flags
67
+ ### Prompt Guidance Injection
68
+
69
+ The plugin injects guidance into OpenCode sessions to improve agent grounding. Uses `<!-- kibi-opencode -->` sentinel to prevent duplicate injections and respects `prompt.enabled` and overall `enabled` config flags.
31
70
 
32
71
  ### Bootstrap Command
33
72
 
@@ -64,6 +103,14 @@ Config files (project overrides global):
64
103
  | `sync.debounceMs` | number | `2000` | Debounce window in milliseconds |
65
104
  | `sync.ignore` | string[] | `[]` | Additional paths to ignore |
66
105
  | `sync.relevant` | string[] | `[]` | Additional relevant paths |
106
+ | `guidance.dynamic` | boolean | `true` | Enable dynamic contextual guidance |
107
+ | `guidance.warnOnKbEdits` | boolean | `true` | Enable loud warnings for .kb/** edits |
108
+ | `guidance.factFirstDomainRouting` | boolean | `true` | Enable FACT-first domain routing suggestions |
109
+ | `guidance.commentDetection.enabled` | boolean | `true` | Enable comment content analysis |
110
+ | `guidance.commentDetection.minLines` | number | `6` | Minimum lines to trigger comment analysis |
111
+ | `guidance.targetedChecks.enabled` | boolean | `true` | Enable post-sync targeted validation checks |
112
+ | `guidance.sessionSummary.enabled` | boolean | `true` | Enable periodic session summary logs |
113
+ | `guidance.sessionSummary.logIntervalMs` | number | `1800000` | Session summary interval (30 min) |
67
114
  | `logLevel` | string | `"info"` | Log level: `debug`, `info`, `warn`, `error` |
68
115
 
69
116
  ### Hook Policy
package/dist/config.d.ts CHANGED
@@ -15,6 +15,22 @@ export interface KibiConfig {
15
15
  toastSuccesses: boolean;
16
16
  toastCooldownMs: number;
17
17
  };
18
+ guidance: {
19
+ dynamic: boolean;
20
+ warnOnKbEdits: boolean;
21
+ factFirstDomainRouting: boolean;
22
+ commentDetection: {
23
+ enabled: boolean;
24
+ minLines: number;
25
+ };
26
+ targetedChecks: {
27
+ enabled: boolean;
28
+ };
29
+ sessionSummary: {
30
+ enabled: boolean;
31
+ logIntervalMs: number;
32
+ };
33
+ };
18
34
  logLevel: string;
19
35
  }
20
36
  declare const DEFAULTS: KibiConfig;
package/dist/config.js CHANGED
@@ -7,6 +7,22 @@ const DEFAULTS = {
7
7
  prompt: { enabled: true, hookMode: "auto" },
8
8
  sync: { enabled: true, debounceMs: 2000, ignore: [], relevant: [] },
9
9
  ux: { toastFailures: true, toastSuccesses: false, toastCooldownMs: 10000 },
10
+ guidance: {
11
+ dynamic: true,
12
+ warnOnKbEdits: true,
13
+ factFirstDomainRouting: true,
14
+ commentDetection: {
15
+ enabled: true,
16
+ minLines: 6,
17
+ },
18
+ targetedChecks: {
19
+ enabled: true,
20
+ },
21
+ sessionSummary: {
22
+ enabled: true,
23
+ logIntervalMs: 30 * 60 * 1000, // 30 minutes
24
+ },
25
+ },
10
26
  logLevel: "info",
11
27
  };
12
28
  function readJsonIfExists(filePath) {
@@ -70,6 +86,38 @@ function validateAndMerge(obj) {
70
86
  }
71
87
  if (typeof src.logLevel === "string")
72
88
  out.logLevel = src.logLevel;
89
+ if (src.guidance && typeof src.guidance === "object") {
90
+ const g = src.guidance;
91
+ out.guidance = { ...DEFAULTS.guidance };
92
+ if (typeof g.dynamic === "boolean")
93
+ out.guidance.dynamic = g.dynamic;
94
+ if (typeof g.warnOnKbEdits === "boolean")
95
+ out.guidance.warnOnKbEdits = g.warnOnKbEdits;
96
+ if (typeof g.factFirstDomainRouting === "boolean")
97
+ out.guidance.factFirstDomainRouting = g.factFirstDomainRouting;
98
+ if (g.commentDetection && typeof g.commentDetection === "object") {
99
+ const cd = g.commentDetection;
100
+ out.guidance.commentDetection = { ...DEFAULTS.guidance.commentDetection };
101
+ if (typeof cd.enabled === "boolean")
102
+ out.guidance.commentDetection.enabled = cd.enabled;
103
+ if (typeof cd.minLines === "number")
104
+ out.guidance.commentDetection.minLines = cd.minLines;
105
+ }
106
+ if (g.targetedChecks && typeof g.targetedChecks === "object") {
107
+ const tc = g.targetedChecks;
108
+ out.guidance.targetedChecks = { ...DEFAULTS.guidance.targetedChecks };
109
+ if (typeof tc.enabled === "boolean")
110
+ out.guidance.targetedChecks.enabled = tc.enabled;
111
+ }
112
+ if (g.sessionSummary && typeof g.sessionSummary === "object") {
113
+ const ss = g.sessionSummary;
114
+ out.guidance.sessionSummary = { ...DEFAULTS.guidance.sessionSummary };
115
+ if (typeof ss.enabled === "boolean")
116
+ out.guidance.sessionSummary.enabled = ss.enabled;
117
+ if (typeof ss.logIntervalMs === "number")
118
+ out.guidance.sessionSummary.logIntervalMs = ss.logIntervalMs;
119
+ }
120
+ }
73
121
  return out;
74
122
  }
75
123
  // implements REQ-opencode-kibi-plugin-v1
package/dist/index.d.ts CHANGED
@@ -1,9 +1,4 @@
1
- import * as config from "./config";
2
- import * as fileFilter from "./file-filter";
3
- import { SENTINEL, injectPrompt } from "./prompt";
4
- import { createSyncScheduler } from "./scheduler";
5
1
  import type { Hooks, Plugin, PluginInput } from "@opencode-ai/plugin";
6
2
  export type { Plugin, PluginInput, Hooks };
7
3
  declare const kibiOpencodePlugin: Plugin;
8
4
  export default kibiOpencodePlugin;
9
- export { config, fileFilter, createSyncScheduler, injectPrompt, SENTINEL };
package/dist/index.js CHANGED
@@ -1,18 +1,80 @@
1
1
  import * as config from "./config";
2
2
  import * as fileFilter from "./file-filter";
3
3
  import * as logger from "./logger";
4
- import { SENTINEL, injectPrompt } from "./prompt";
4
+ import { analyzePath } from "./path-kind";
5
+ import { injectPrompt } from "./prompt";
5
6
  import { createSyncScheduler } from "./scheduler";
7
+ import { getSessionTracker } from "./session-tracker";
8
+ import { checkWorkspaceHealth } from "./workspace-health";
9
+ import * as fs from "node:fs";
10
+ /**
11
+ * Lint requirement document for anti-patterns.
12
+ */
13
+ function lintRequirementDoc(filePath, worktree) {
14
+ const warnings = [];
15
+ try {
16
+ const resolvedPath = worktree && !filePath.startsWith("/")
17
+ ? `${worktree}/${filePath}`
18
+ : filePath;
19
+ const content = fs.readFileSync(resolvedPath, "utf-8");
20
+ // Check for embedded scenarios (Given/When/Then patterns)
21
+ if (/given\s+.*when\s+.*then/i.test(content)) {
22
+ warnings.push({
23
+ category: "embedded-scenario-in-req",
24
+ message: `Requirement file ${filePath} appears to contain embedded scenario (Given/When/Then). Consider extracting to a separate SCEN entity.`,
25
+ });
26
+ }
27
+ // Check for embedded tests (assert/verify patterns)
28
+ if (/\b(assert|verify|expected\s+to|should\s+return)\b/i.test(content)) {
29
+ warnings.push({
30
+ category: "embedded-test-in-req",
31
+ message: `Requirement file ${filePath} appears to contain embedded test assertions. Consider extracting to a separate TEST entity.`,
32
+ });
33
+ }
34
+ // Check for very long requirement that might need splitting
35
+ const lines = content.split("\n");
36
+ const contentLines = lines.filter((l) => l.trim() && !l.startsWith("---") && !l.startsWith("#"));
37
+ if (contentLines.length > 50) {
38
+ warnings.push({
39
+ category: "missing-traceability",
40
+ message: `Requirement file ${filePath} is very long (${contentLines.length} content lines). Consider splitting into multiple requirements or extracting scenarios/tests.`,
41
+ });
42
+ }
43
+ }
44
+ catch {
45
+ // Ignore read errors
46
+ }
47
+ return warnings;
48
+ }
6
49
  let scheduler = null;
7
- let cfg = null;
50
+ let cfg;
51
+ // Track recent edits for contextual guidance
52
+ const MAX_RECENT_EDITS = 5;
53
+ let recentEdits = [];
54
+ let hasRecentKbEdit = false;
8
55
  // implements REQ-opencode-kibi-plugin-v1
9
56
  const kibiOpencodePlugin = async (input) => {
10
57
  // Load config
11
- cfg = config.loadConfig(input.directory);
58
+ const loadedCfg = config.loadConfig(input.directory);
59
+ cfg = loadedCfg;
12
60
  if (!cfg.enabled) {
13
61
  logger.info("kibi-opencode: disabled via config");
14
62
  return {};
15
63
  }
64
+ // Check workspace health for bootstrap nudges
65
+ const workspaceHealth = checkWorkspaceHealth(input.worktree);
66
+ if (workspaceHealth.needsBootstrap) {
67
+ logger.warn("kibi-opencode: workspace needs Kibi bootstrap");
68
+ getSessionTracker().recordWarning("bootstrap-needed", input.worktree, "Workspace missing Kibi bootstrap");
69
+ }
70
+ // Log session summary periodically (gated on config)
71
+ if (cfg.guidance.sessionSummary.enabled) {
72
+ const tracker = getSessionTracker();
73
+ if (tracker.isSessionExpired(cfg.guidance.sessionSummary.logIntervalMs)) {
74
+ tracker.logSummary();
75
+ tracker.reset();
76
+ }
77
+ }
16
78
  logger.info("kibi-opencode: setting up hooks");
17
79
  const hooks = {};
18
80
  // Setup file-edit triggered sync via event hook
@@ -25,13 +87,48 @@ const kibiOpencodePlugin = async (input) => {
25
87
  hooks.event = async ({ event }) => {
26
88
  if (event.type !== "file.edited")
27
89
  return;
28
- const filePath = event.properties.file;
90
+ const filePath = event
91
+ .properties.file;
29
92
  if (!filePath)
30
93
  return;
94
+ // Analyze path for tracking and classification
95
+ const pathAnalysis = analyzePath(filePath, input.worktree);
96
+ // Check for .kb edit (loud warning) — gated on guidance.warnOnKbEdits
97
+ if (pathAnalysis.isUnderKb && cfg.guidance.warnOnKbEdits) {
98
+ hasRecentKbEdit = true;
99
+ logger.warn(`kibi-opencode: .kb edit detected for ${filePath}`);
100
+ getSessionTracker().recordWarning("kb-edit", filePath, `Manual .kb edit: ${filePath}`);
101
+ }
102
+ // Lint requirement docs for anti-patterns
103
+ if (pathAnalysis.kind === "requirement") {
104
+ const lintWarnings = lintRequirementDoc(filePath, input.worktree);
105
+ for (const warning of lintWarnings) {
106
+ getSessionTracker().recordWarning(warning.category, filePath, warning.message);
107
+ }
108
+ }
109
+ // Track recent edits
110
+ const now = Date.now();
111
+ recentEdits.push({
112
+ path: filePath,
113
+ kind: pathAnalysis.kind,
114
+ timestamp: now,
115
+ });
116
+ // Keep only recent edits
117
+ if (recentEdits.length > MAX_RECENT_EDITS) {
118
+ recentEdits = recentEdits.slice(-MAX_RECENT_EDITS);
119
+ }
120
+ // Only schedule sync for relevant files (not .kb)
31
121
  if (!fileFilter.shouldHandleFile(filePath, input.worktree))
32
122
  return;
123
+ // Determine targeted checks based on edit type (gated on guidance.targetedChecks.enabled)
124
+ let checkRules;
125
+ if (cfg.guidance.targetedChecks.enabled) {
126
+ if (["requirement", "scenario", "test", "adr", "fact"].includes(pathAnalysis.kind)) {
127
+ checkRules = ["required-fields", "no-dangling-refs"];
128
+ }
129
+ }
33
130
  logger.info(`kibi-opencode: scheduling sync for ${filePath}`);
34
- scheduler.scheduleSync("file.edited", filePath);
131
+ scheduler?.scheduleSync("file.edited", filePath, checkRules);
35
132
  };
36
133
  }
37
134
  // Setup prompt injection hook
@@ -40,7 +137,11 @@ const kibiOpencodePlugin = async (input) => {
40
137
  if (hookMode === "system-transform" || hookMode === "auto") {
41
138
  hooks["experimental.chat.system.transform"] = async (_input, output) => {
42
139
  const currentSystem = output.system.join("\n");
43
- const injected = injectPrompt(currentSystem, cfg);
140
+ const injected = injectPrompt(currentSystem, cfg, {
141
+ recentEdits,
142
+ workspaceHealth,
143
+ hasRecentKbEdit,
144
+ });
44
145
  output.system.length = 0;
45
146
  output.system.push(injected);
46
147
  };
@@ -61,4 +162,3 @@ const kibiOpencodePlugin = async (input) => {
61
162
  return hooks;
62
163
  };
63
164
  export default kibiOpencodePlugin;
64
- export { config, fileFilter, createSyncScheduler, injectPrompt, SENTINEL };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Heuristic classifier for routing durable knowledge prose to appropriate Kibi entity types.
3
+ * This is guidance-only and does not auto-create entities.
4
+ */
5
+ export type KnowledgeSuggestion = {
6
+ type: "fact" | "req" | "adr" | "scenario" | "test";
7
+ confidence: "low" | "medium" | "high";
8
+ reasoning: string;
9
+ };
10
+ /**
11
+ * Analyze a comment or prose block and suggest most appropriate entity type.
12
+ */
13
+ export declare function classifyKnowledge(text: string): KnowledgeSuggestion | null;
@@ -0,0 +1,137 @@
1
+ // implements REQ-opencode-kibi-plugin-v1
2
+ /**
3
+ * Cues for FACT entities (domain invariants, properties, limits, cardinalities)
4
+ */
5
+ const FACT_CUES = [
6
+ "must be unique",
7
+ "at most",
8
+ "exactly one",
9
+ "default",
10
+ "default is",
11
+ "expires after",
12
+ "cannot exceed",
13
+ "maximum of",
14
+ "minimum of",
15
+ "always",
16
+ "never",
17
+ "state is",
18
+ "invariant",
19
+ "property value",
20
+ "cardinality",
21
+ "limit is",
22
+ "uniqueness constraint",
23
+ ];
24
+ /**
25
+ * Cues for REQ entities (system behavior, capabilities, obligations)
26
+ */
27
+ const REQ_CUES = [
28
+ "system must",
29
+ "user can",
30
+ "user should",
31
+ "should allow",
32
+ "shall",
33
+ "must support",
34
+ "capability",
35
+ "permission",
36
+ ];
37
+ /**
38
+ * Cues for ADR entities (technical decisions, tradeoffs, rationale)
39
+ */
40
+ const ADR_CUES = [
41
+ "decision",
42
+ "tradeoff",
43
+ "trade-off",
44
+ "we chose",
45
+ "because",
46
+ "rationale",
47
+ "constraint",
48
+ "architecture decision",
49
+ "design decision",
50
+ ];
51
+ /**
52
+ * Cues for SCENARIO entities (behavior examples, flows)
53
+ */
54
+ const SCENARIO_CUES = [
55
+ "given",
56
+ "then",
57
+ "user flow",
58
+ "example interaction",
59
+ "acceptance criteria",
60
+ ];
61
+ /**
62
+ * Cues for TEST entities (verification language, assertions)
63
+ */
64
+ const TEST_CUES = [
65
+ "verify",
66
+ "assert",
67
+ "expected",
68
+ "test case",
69
+ "asserts that",
70
+ "should verify",
71
+ ];
72
+ /**
73
+ * Analyze a comment or prose block and suggest most appropriate entity type.
74
+ */
75
+ export function classifyKnowledge(text) {
76
+ if (!text || text.trim().length < 50) {
77
+ return null;
78
+ }
79
+ const lower = text.toLowerCase();
80
+ let bestMatch = null;
81
+ let maxMatches = 0;
82
+ function scoreMatches(cues, target) {
83
+ return cues.filter((cue) => lower.includes(cue)).length;
84
+ }
85
+ const factScore = scoreMatches(FACT_CUES, text);
86
+ const reqScore = scoreMatches(REQ_CUES, text);
87
+ const adrScore = scoreMatches(ADR_CUES, text);
88
+ const scenarioScore = scoreMatches(SCENARIO_CUES, text);
89
+ const testScore = scoreMatches(TEST_CUES, text);
90
+ // Determine the best match with some tie-breaking logic
91
+ // Confidence: 3+ matches = high, 1-2 matches = medium, 0 = no result
92
+ if (factScore > maxMatches) {
93
+ maxMatches = factScore;
94
+ bestMatch = {
95
+ type: "fact",
96
+ confidence: factScore >= 3 ? "high" : "medium",
97
+ reasoning: 'Contains domain invariant or property cues like "must be unique", "at most", or "default is"',
98
+ };
99
+ }
100
+ if (reqScore > maxMatches) {
101
+ maxMatches = reqScore;
102
+ bestMatch = {
103
+ type: "req",
104
+ confidence: reqScore >= 3 ? "high" : "medium",
105
+ reasoning: 'Contains system behavior or obligation cues like "system must", "user can", or "shall"',
106
+ };
107
+ }
108
+ if (adrScore > maxMatches) {
109
+ maxMatches = adrScore;
110
+ bestMatch = {
111
+ type: "adr",
112
+ confidence: adrScore >= 3 ? "high" : "medium",
113
+ reasoning: 'Contains decision or tradeoff cues like "we chose", "because", or "constraint"',
114
+ };
115
+ }
116
+ if (scenarioScore > maxMatches) {
117
+ maxMatches = scenarioScore;
118
+ bestMatch = {
119
+ type: "scenario",
120
+ confidence: scenarioScore >= 3 ? "high" : "medium",
121
+ reasoning: 'Contains behavior example cues like "given/when/then" or "user flow"',
122
+ };
123
+ }
124
+ if (testScore > maxMatches) {
125
+ maxMatches = testScore;
126
+ bestMatch = {
127
+ type: "test",
128
+ confidence: testScore >= 3 ? "high" : "medium",
129
+ reasoning: 'Contains verification cues like "verify", "assert", or "expected"',
130
+ };
131
+ }
132
+ // Return best match (any match with at least 1 cue is medium+ confidence)
133
+ if (bestMatch) {
134
+ return bestMatch;
135
+ }
136
+ return null;
137
+ }
@@ -0,0 +1,7 @@
1
+ export type PathKind = "code" | "requirement" | "scenario" | "test" | "adr" | "fact" | "kb" | "unknown";
2
+ export interface PathAnalysis {
3
+ kind: PathKind;
4
+ isUnderKb: boolean;
5
+ isKibiDocRelevant: boolean;
6
+ }
7
+ export declare function analyzePath(filePath: string, cwd: string): PathAnalysis;
@@ -0,0 +1,64 @@
1
+ // implements REQ-opencode-kibi-plugin-v1
2
+ import path from "node:path";
3
+ const CODE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx"];
4
+ const KB_PREFIX = ".kb";
5
+ const KIBI_DOC_PATTERNS = [
6
+ "requirements/**",
7
+ "scenarios/**",
8
+ "tests/**",
9
+ "adr/**",
10
+ "flags/**",
11
+ "events/**",
12
+ "facts/**",
13
+ "symbols.yaml",
14
+ ];
15
+ export function analyzePath(filePath, cwd) {
16
+ const rel = path.isAbsolute(filePath)
17
+ ? path.relative(cwd, filePath).split(path.sep).join("/")
18
+ : filePath.split(path.sep).join("/");
19
+ let kind = "unknown";
20
+ let isUnderKb = false;
21
+ let isKibiDocRelevant = false;
22
+ // Check if under .kb/**
23
+ if (rel.startsWith(`${KB_PREFIX}/`)) {
24
+ kind = "kb";
25
+ isUnderKb = true;
26
+ }
27
+ // Check for Kibi doc paths
28
+ const normalized = rel.toLowerCase();
29
+ for (const pattern of KIBI_DOC_PATTERNS) {
30
+ const patternPrefix = pattern.replace(/\*\*/g, "");
31
+ const fullPathPattern = `documentation/${patternPrefix}`;
32
+ if (normalized.startsWith(fullPathPattern)) {
33
+ isKibiDocRelevant = true;
34
+ if (kind === "unknown") {
35
+ // Map to specific kind based on path
36
+ if (patternPrefix.includes("requirements"))
37
+ kind = "requirement";
38
+ else if (patternPrefix.includes("scenarios"))
39
+ kind = "scenario";
40
+ else if (patternPrefix.includes("tests"))
41
+ kind = "test";
42
+ else if (patternPrefix.includes("adr"))
43
+ kind = "adr";
44
+ else if (patternPrefix.includes("facts"))
45
+ kind = "fact";
46
+ else if (patternPrefix.includes("events"))
47
+ kind = "fact"; // events map to fact for routing
48
+ else if (patternPrefix.includes("flags"))
49
+ kind = "fact"; // flags map to fact for routing
50
+ else if (patternPrefix.includes("symbols"))
51
+ kind = "fact";
52
+ }
53
+ break;
54
+ }
55
+ }
56
+ // Check for code files
57
+ if (kind === "unknown") {
58
+ const ext = path.extname(rel).toLowerCase();
59
+ if (CODE_EXTENSIONS.includes(ext)) {
60
+ kind = "code";
61
+ }
62
+ }
63
+ return { kind, isUnderKb, isKibiDocRelevant };
64
+ }
package/dist/prompt.d.ts CHANGED
@@ -1,5 +1,21 @@
1
1
  import type { KibiConfig } from "./config";
2
+ import type { PathKind } from "./path-kind";
3
+ import type { WorkspaceHealth } from "./workspace-health";
2
4
  declare const SENTINEL = "<!-- kibi-opencode -->";
3
- export declare function buildPrompt(): string;
4
- export declare function injectPrompt(current: string, config: KibiConfig): string;
5
+ export interface PromptContext {
6
+ recentEdits: Array<{
7
+ path: string;
8
+ kind: PathKind;
9
+ }>;
10
+ workspaceHealth?: WorkspaceHealth;
11
+ hasRecentKbEdit?: boolean;
12
+ }
13
+ /**
14
+ * Build prompt with contextual guidance based on recent edits and workspace state.
15
+ */
16
+ export declare function buildPrompt(context?: PromptContext): string;
17
+ /**
18
+ * Inject prompt guidance if not already present.
19
+ */
20
+ export declare function injectPrompt(current: string, config: KibiConfig, context?: PromptContext): string;
5
21
  export { SENTINEL };
package/dist/prompt.js CHANGED
@@ -1,6 +1,109 @@
1
1
  import { isPluginEnabled } from "./config";
2
2
  const SENTINEL = "<!-- kibi-opencode -->";
3
- const GUIDANCE = `${SENTINEL}
3
+ /**
4
+ * Build prompt guidance block based on path kind.
5
+ */
6
+ function buildContextualGuidance(context) {
7
+ const parts = [SENTINEL];
8
+ // 1. Check for recent .kb edits (loud warning)
9
+ if (context.hasRecentKbEdit) {
10
+ parts.push(`
11
+ ⚠️ **WARNING: Do not edit .kb/** files manually.**
12
+
13
+ The Kibi knowledge base is managed through MCP and CLI tools. Direct manual edits to files under .kb/** can cause inconsistencies and should be avoided.
14
+
15
+ Instead:
16
+ - Use kb_upsert to create/update entities
17
+ - Use kb_query to inspect the KB
18
+ - Use kb_check to validate consistency
19
+ `);
20
+ }
21
+ // 2. Check for bootstrap/health issues
22
+ if (context.workspaceHealth?.needsBootstrap) {
23
+ parts.push(`
24
+ 🔧 **Bootstrap required**
25
+
26
+ This repository does not appear to have Kibi initialized. Consider running:
27
+ - \`/init-kibi\` for retroactive bootstrap of existing repos
28
+ - \`kibi init\` for new repos
29
+ - \`kibi doctor\` to verify your environment
30
+ `);
31
+ }
32
+ // 3. Analyze recent edits and provide targeted guidance
33
+ const codeEdits = context.recentEdits.filter((e) => e.kind === "code");
34
+ const reqEdits = context.recentEdits.filter((e) => e.kind === "requirement");
35
+ const kbDocEdits = context.recentEdits.filter((e) => ["requirement", "scenario", "test", "adr", "fact"].includes(e.kind));
36
+ // Code edit guidance
37
+ if (codeEdits.length > 0) {
38
+ parts.push(`
39
+ 📝 **Code changes detected**
40
+
41
+ Before implementing or explaining code:
42
+ 1. **Query Kibi first** - Run kb_query by sourceFile to find related requirements, ADRs, tests, and symbols.
43
+ 2. **Prefer Kibi over comments** - Store durable knowledge in KB entities instead of inline comments.
44
+ 3. **Add traceability** - Add \`// implements REQ-xxx\` to every new or modified function/class so the pre-commit hook can verify coverage.
45
+
46
+ If you're adding long explanatory comments, consider routing that knowledge to:
47
+ - \`FACT\` for domain invariants, properties, limits, cardinalities
48
+ - \`ADR\` for technical decisions, tradeoffs, rationale
49
+ - \`REQ\` for system behavior requirements
50
+ - \`SCEN\` for behavior examples and flows
51
+ - \`TEST\` for verification intent
52
+ `);
53
+ }
54
+ // Requirement edit guidance
55
+ if (reqEdits.length > 0) {
56
+ parts.push(`
57
+ 📋 **Requirement changes detected**
58
+
59
+ When editing requirements:
60
+ 1. **Keep artifacts separate** - Do not embed scenarios or tests inside requirement files.
61
+ 2. **Add verification** - Create or update linked \`SCEN\` and \`TEST\` entities.
62
+ 3. **Check coverage** - For \`priority: must\` requirements, ensure both scenario and test coverage.
63
+
64
+ Preferred structure:
65
+ - \`REQ-xxx.md\` contains the requirement statement
66
+ - \`SCEN-xxx.md\` specifies behavior via Given/When/Then
67
+ - \`TEST-xxx.md\` verifies the requirement
68
+ `);
69
+ }
70
+ // KB doc edit guidance (requirement, scenario, test, ADR, fact)
71
+ if (kbDocEdits.length > 0 && reqEdits.length === 0) {
72
+ parts.push(`
73
+ 📚 **Kibi documentation changes detected**
74
+
75
+ When editing KB documentation:
76
+ 1. **Maintain traceability** - Link entities using relationships: specified_by (req→scenario), verified_by (req→test), etc.
77
+ 2. **Validate** - Run \`kibi check\` after making changes to catch integrity issues.
78
+ 3. **Follow entity patterns** - Ensure each entity has proper frontmatter with required fields.
79
+ `);
80
+ }
81
+ // Only include general Kibi workflow if no specific context (beyond the sentinel)
82
+ if (parts.length === 1) {
83
+ parts.push(`This project uses Kibi (via MCP). Prefer storing durable knowledge in Kibi over code comments.
84
+
85
+ Before changing behavior: query Kibi by sourceFile, id, type, or tags; do not rely on undocumented tools.
86
+
87
+ Keep changed symbols traceable: add \`// implements REQ-xxx\` to every new or modified function/class so the pre-commit hook can verify coverage.
88
+
89
+ Run kb_check after KB mutations.
90
+
91
+ **Kibi-first workflow:**
92
+ 1. **Discover**: Run kb_query with filters (sourceFile, type, tags) to find related requirements, ADRs, tests, and symbols.
93
+ 2. **Document intent**: If you are about to explain code, STOP. Route that explanation to kb_upsert instead of inline comments.
94
+ 3. **Link during work**: When creating KB entities, include relationship rows: specified_by (req→scenario), verified_by (req→test), implements (symbol→req), covered_by (symbol→test).
95
+ 4. **Validate**: Run kb_check after KB mutations to catch violations early.
96
+
97
+ **Public Kibi tools only:** kb_query, kb_upsert, kb_delete, kb_check.
98
+
99
+ Bootstrap existing repos: use \`/init-kibi\` to run the retroactive initialization workflow.`);
100
+ }
101
+ return parts.join("\n\n").trim();
102
+ }
103
+ /**
104
+ * Build the static guidance block (original behavior).
105
+ */
106
+ const BASE_GUIDANCE = `${SENTINEL}
4
107
  This project uses Kibi (via MCP). Prefer storing durable knowledge in Kibi over code comments.
5
108
 
6
109
  Before changing behavior: query Kibi by sourceFile, id, type, or tags; do not rely on undocumented tools.
@@ -18,18 +121,25 @@ Run kb_check after KB mutations.
18
121
  **Public Kibi tools only:** kb_query, kb_upsert, kb_delete, kb_check.
19
122
 
20
123
  Bootstrap existing repos: use \`/init-kibi\` to run the retroactive initialization workflow.`;
21
- // implements REQ-opencode-kibi-plugin-v1
22
- export function buildPrompt() {
23
- return GUIDANCE.trim();
124
+ /**
125
+ * Build prompt with contextual guidance based on recent edits and workspace state.
126
+ */
127
+ export function buildPrompt(context) {
128
+ if (!context) {
129
+ return BASE_GUIDANCE.trim();
130
+ }
131
+ return buildContextualGuidance(context).trim();
24
132
  }
25
- // implements REQ-opencode-kibi-plugin-v1
26
- export function injectPrompt(current, config) {
133
+ /**
134
+ * Inject prompt guidance if not already present.
135
+ */
136
+ export function injectPrompt(current, config, context) {
27
137
  if (!config.prompt.enabled || !isPluginEnabled(config)) {
28
138
  return current;
29
139
  }
30
140
  if (current.includes(SENTINEL)) {
31
141
  return current;
32
142
  }
33
- return `${current}\n\n${buildPrompt()}`;
143
+ return `${current}\n\n${buildPrompt(context)}`;
34
144
  }
35
145
  export { SENTINEL };
@@ -1,5 +1,5 @@
1
1
  import type { KibiConfig } from "./config";
2
- type TimeoutHandle = ReturnType<typeof setTimeout>;
2
+ export type TimeoutHandle = ReturnType<typeof setTimeout>;
3
3
  export interface SyncRunMetadata {
4
4
  reason: string;
5
5
  worktree: string;
@@ -7,25 +7,35 @@ export interface SyncRunMetadata {
7
7
  debounceWindowMs: number;
8
8
  durationMs: number;
9
9
  exitCode: number;
10
+ checkExitCode?: number;
11
+ checkRules?: string[];
10
12
  }
11
- type SyncRunner = (worktree: string) => Promise<{
13
+ export type SyncRunner = (worktree: string) => Promise<{
14
+ exitCode: number;
15
+ }>;
16
+ export type CheckRunner = (worktree: string, rules: string[]) => Promise<{
12
17
  exitCode: number;
13
18
  }>;
14
19
  export interface SchedulerOptions {
15
20
  worktree: string;
16
21
  config: KibiConfig;
17
22
  runSync?: SyncRunner;
23
+ runCheck?: CheckRunner;
18
24
  now?: () => number;
19
25
  setTimeoutFn?: (fn: () => void, ms: number) => TimeoutHandle;
20
26
  clearTimeoutFn?: (handle: TimeoutHandle) => void;
21
27
  onRunComplete?: (meta: SyncRunMetadata) => void;
22
28
  enableToolExecuteAfterHint?: boolean;
23
29
  }
30
+ export type PendingTrigger = {
31
+ reason: string;
32
+ filePath?: string;
33
+ checkRules?: string[];
34
+ };
24
35
  export interface SyncScheduler {
25
- scheduleSync(reason: string, filePath?: string): void;
36
+ scheduleSync(reason: string, filePath?: string, checkRules?: string[]): void;
26
37
  onFileEdited(filePath: string): void;
27
38
  onToolExecuteAfter(reason?: string): void;
28
39
  dispose(): void;
29
40
  }
30
41
  export declare function createSyncScheduler(opts: SchedulerOptions): SyncScheduler;
31
- export {};
package/dist/scheduler.js CHANGED
@@ -8,6 +8,7 @@ class WorktreeSyncScheduler {
8
8
  setTimeoutFn;
9
9
  clearTimeoutFn;
10
10
  runSync;
11
+ runCheck;
11
12
  config;
12
13
  onRunComplete;
13
14
  explicitToolAfterHint;
@@ -24,10 +25,11 @@ class WorktreeSyncScheduler {
24
25
  this.setTimeoutFn = opts.setTimeoutFn ?? setTimeout;
25
26
  this.clearTimeoutFn = opts.clearTimeoutFn ?? clearTimeout;
26
27
  this.runSync = opts.runSync ?? runKibiSync;
28
+ this.runCheck = opts.runCheck ?? runKibiCheck;
27
29
  this.onRunComplete = opts.onRunComplete;
28
30
  this.explicitToolAfterHint = Boolean(opts.enableToolExecuteAfterHint);
29
31
  }
30
- scheduleSync(reason, filePath) {
32
+ scheduleSync(reason, filePath, checkRules) {
31
33
  if (!this.config.sync.enabled)
32
34
  return;
33
35
  if (reason === "file.edited") {
@@ -37,7 +39,7 @@ class WorktreeSyncScheduler {
37
39
  return;
38
40
  this.lastFileEditedAt = this.now();
39
41
  }
40
- this.pending = { reason, filePath };
42
+ this.pending = { reason, filePath, checkRules };
41
43
  if (this.timer)
42
44
  this.clearTimeoutFn(this.timer);
43
45
  this.timer = this.setTimeoutFn(() => {
@@ -88,7 +90,7 @@ class WorktreeSyncScheduler {
88
90
  }
89
91
  this.startRun(trigger);
90
92
  }
91
- startRun(trigger) {
93
+ async startRun(trigger) {
92
94
  this.inFlight = true;
93
95
  const startedAt = this.now();
94
96
  logger.info(`sync.started ${JSON.stringify({
@@ -97,29 +99,49 @@ class WorktreeSyncScheduler {
97
99
  filePath: trigger.filePath,
98
100
  debounceWindowMs: this.config.sync.debounceMs,
99
101
  })}`);
100
- void this.runSync(this.worktree)
101
- .then(({ exitCode }) => {
102
- this.emitCompletion(trigger, startedAt, exitCode);
103
- })
104
- .catch((err) => {
102
+ let syncExitCode = 0;
103
+ let checkExitCode;
104
+ let checkRules;
105
+ try {
106
+ const syncResult = await this.runSync(this.worktree);
107
+ syncExitCode = syncResult.exitCode;
108
+ // Run targeted checks if sync succeeded and rules specified
109
+ if (syncExitCode === 0 &&
110
+ trigger.checkRules &&
111
+ trigger.checkRules.length > 0) {
112
+ checkRules = trigger.checkRules;
113
+ logger.info(`check.started ${JSON.stringify({ rules: checkRules })}`);
114
+ const checkResult = await this.runCheck(this.worktree, checkRules);
115
+ checkExitCode = checkResult.exitCode;
116
+ if (checkExitCode !== 0) {
117
+ logger.warn(`check.failed ${JSON.stringify({ rules: checkRules, exitCode: checkExitCode })}`);
118
+ }
119
+ else {
120
+ logger.info(`check.succeeded ${JSON.stringify({ rules: checkRules })}`);
121
+ }
122
+ }
123
+ }
124
+ catch (err) {
105
125
  const message = err instanceof Error ? err.message : String(err);
106
126
  logger.error(`sync.failed ${message}`);
107
- this.emitCompletion(trigger, startedAt, 1);
108
- })
109
- .finally(() => {
127
+ syncExitCode = 1;
128
+ }
129
+ finally {
130
+ this.emitCompletion(trigger, startedAt, syncExitCode, checkExitCode, checkRules);
110
131
  this.inFlight = false;
111
- if (!this.dirty)
112
- return;
113
- const trailing = this.trailing ?? { reason: "sync.trailing" };
114
- this.dirty = false;
115
- this.trailing = null;
116
- this.startRun({
117
- reason: `${trailing.reason}.trailing`,
118
- filePath: trailing.filePath,
119
- });
120
- });
132
+ if (this.dirty) {
133
+ const trailing = this.trailing ?? { reason: "sync.trailing" };
134
+ this.dirty = false;
135
+ this.trailing = null;
136
+ void this.startRun({
137
+ reason: `${trailing.reason}.trailing`,
138
+ filePath: trailing.filePath,
139
+ checkRules: trailing.checkRules,
140
+ });
141
+ }
142
+ }
121
143
  }
122
- emitCompletion(trigger, startedAt, exitCode) {
144
+ emitCompletion(trigger, startedAt, exitCode, checkExitCode, checkRules) {
123
145
  const durationMs = Math.max(0, this.now() - startedAt);
124
146
  const meta = {
125
147
  reason: trigger.reason,
@@ -128,6 +150,8 @@ class WorktreeSyncScheduler {
128
150
  debounceWindowMs: this.config.sync.debounceMs,
129
151
  durationMs,
130
152
  exitCode,
153
+ checkExitCode,
154
+ checkRules,
131
155
  };
132
156
  if (exitCode === 0) {
133
157
  logger.info(`sync.succeeded ${JSON.stringify(meta)}`);
@@ -145,6 +169,14 @@ async function runKibiSync(worktree) {
145
169
  });
146
170
  });
147
171
  }
172
+ async function runKibiCheck(worktree, rules) {
173
+ return new Promise((resolve) => {
174
+ const rulesArg = rules.join(",");
175
+ exec(`kibi check --rules ${rulesArg}`, { cwd: worktree }, (error) => {
176
+ resolve({ exitCode: error ? (error.code ?? 1) : 0 });
177
+ });
178
+ });
179
+ }
148
180
  // implements REQ-opencode-kibi-plugin-v1
149
181
  export function createSyncScheduler(opts) {
150
182
  return new WorktreeSyncScheduler(opts);
@@ -0,0 +1,49 @@
1
+ export type WarningCategory = "kb-edit" | "embedded-scenario-in-req" | "embedded-test-in-req" | "long-comment-missed-fact" | "long-comment-missed-adr" | "missing-traceability" | "bootstrap-needed";
2
+ export interface WarningEvent {
3
+ category: WarningCategory;
4
+ path: string;
5
+ timestamp: number;
6
+ message: string;
7
+ }
8
+ export interface SessionSummary {
9
+ totalWarnings: number;
10
+ warningsByCategory: Record<WarningCategory, number>;
11
+ repeatedPatterns: Array<{
12
+ category: WarningCategory;
13
+ count: number;
14
+ }>;
15
+ topFilesWithWarnings: Array<{
16
+ path: string;
17
+ count: number;
18
+ }>;
19
+ }
20
+ declare class SessionTracker {
21
+ private warnings;
22
+ private sessionStart;
23
+ constructor();
24
+ /**
25
+ * Record a warning event.
26
+ */
27
+ recordWarning(category: WarningCategory, path: string, message: string): void;
28
+ /**
29
+ * Generate a session summary of warnings and patterns.
30
+ */
31
+ generateSummary(): SessionSummary;
32
+ /**
33
+ * Log a summary at session end or on demand.
34
+ */
35
+ logSummary(): void;
36
+ /**
37
+ * Reset the session.
38
+ */
39
+ reset(): void;
40
+ /**
41
+ * Check if session has expired.
42
+ */
43
+ isSessionExpired(intervalMs?: number): boolean;
44
+ private logWarning;
45
+ private checkRepeatedPattern;
46
+ }
47
+ export declare function getSessionTracker(): SessionTracker;
48
+ export declare function resetSessionTracker(): void;
49
+ export { SessionTracker };
@@ -0,0 +1,132 @@
1
+ // implements REQ-opencode-kibi-plugin-v1
2
+ import * as logger from "./logger";
3
+ const WARNING_THRESHOLD_REPEAT = 3;
4
+ const SESSION_DURATION_MS = 30 * 60 * 1000; // 30 minutes
5
+ class SessionTracker {
6
+ warnings = [];
7
+ sessionStart;
8
+ constructor() {
9
+ this.sessionStart = Date.now();
10
+ }
11
+ /**
12
+ * Record a warning event.
13
+ */
14
+ recordWarning(category, path, message) {
15
+ const event = {
16
+ category,
17
+ path,
18
+ timestamp: Date.now(),
19
+ message,
20
+ };
21
+ this.warnings.push(event);
22
+ // Log with category prefix for richer filtering
23
+ this.logWarning(category, message);
24
+ // Check for repeated patterns
25
+ this.checkRepeatedPattern(category);
26
+ }
27
+ /**
28
+ * Generate a session summary of warnings and patterns.
29
+ */
30
+ generateSummary() {
31
+ const byCategory = {
32
+ "kb-edit": 0,
33
+ "embedded-scenario-in-req": 0,
34
+ "embedded-test-in-req": 0,
35
+ "long-comment-missed-fact": 0,
36
+ "long-comment-missed-adr": 0,
37
+ "missing-traceability": 0,
38
+ "bootstrap-needed": 0,
39
+ };
40
+ const byFile = {};
41
+ for (const warning of this.warnings) {
42
+ byCategory[warning.category]++;
43
+ byFile[warning.path] = (byFile[warning.path] || 0) + 1;
44
+ }
45
+ const repeatedPatterns = Object.entries(byCategory)
46
+ .filter(([, count]) => count >= WARNING_THRESHOLD_REPEAT)
47
+ .map(([category, count]) => ({
48
+ category: category,
49
+ count,
50
+ }))
51
+ .sort((a, b) => b.count - a.count);
52
+ const topFiles = Object.entries(byFile)
53
+ .sort((a, b) => b[1] - a[1])
54
+ .slice(0, 5)
55
+ .map(([path, count]) => ({ path, count }));
56
+ return {
57
+ totalWarnings: this.warnings.length,
58
+ warningsByCategory: byCategory,
59
+ repeatedPatterns,
60
+ topFilesWithWarnings: topFiles,
61
+ };
62
+ }
63
+ /**
64
+ * Log a summary at session end or on demand.
65
+ */
66
+ logSummary() {
67
+ const summary = this.generateSummary();
68
+ if (summary.totalWarnings === 0) {
69
+ logger.info("session.summary: No warnings recorded");
70
+ return;
71
+ }
72
+ logger.info(`session.summary: ${summary.totalWarnings} total warnings`);
73
+ for (const [category, count] of Object.entries(summary.warningsByCategory)) {
74
+ if (count > 0) {
75
+ logger.info(` ${category}: ${count}`);
76
+ }
77
+ }
78
+ if (summary.repeatedPatterns.length > 0) {
79
+ logger.warn("session.patterns: Repeated anti-patterns detected:");
80
+ for (const pattern of summary.repeatedPatterns) {
81
+ logger.warn(` ${pattern.category}: ${pattern.count} occurrences`);
82
+ }
83
+ }
84
+ }
85
+ /**
86
+ * Reset the session.
87
+ */
88
+ reset() {
89
+ this.warnings = [];
90
+ this.sessionStart = Date.now();
91
+ }
92
+ /**
93
+ * Check if session has expired.
94
+ */
95
+ isSessionExpired(intervalMs = SESSION_DURATION_MS) {
96
+ return Date.now() - this.sessionStart > intervalMs;
97
+ }
98
+ logWarning(category, message) {
99
+ const prefix = `[${category}]`;
100
+ switch (category) {
101
+ case "kb-edit":
102
+ logger.warn(`${prefix} ${message}`);
103
+ break;
104
+ case "bootstrap-needed":
105
+ logger.warn(`${prefix} ${message}`);
106
+ break;
107
+ case "missing-traceability":
108
+ logger.info(`${prefix} ${message}`);
109
+ break;
110
+ default:
111
+ logger.info(`${prefix} ${message}`);
112
+ }
113
+ }
114
+ checkRepeatedPattern(category) {
115
+ const count = this.warnings.filter((w) => w.category === category).length;
116
+ if (count === WARNING_THRESHOLD_REPEAT) {
117
+ logger.warn(`pattern.repeat: ${category} has occurred ${count} times. Consider addressing this pattern systematically.`);
118
+ }
119
+ }
120
+ }
121
+ // Singleton instance
122
+ let globalTracker = null;
123
+ export function getSessionTracker() {
124
+ if (!globalTracker) {
125
+ globalTracker = new SessionTracker();
126
+ }
127
+ return globalTracker;
128
+ }
129
+ export function resetSessionTracker() {
130
+ globalTracker = new SessionTracker();
131
+ }
132
+ export { SessionTracker };
@@ -0,0 +1,10 @@
1
+ export interface WorkspaceHealth {
2
+ needsBootstrap: boolean;
3
+ missingConfig: boolean;
4
+ missingDocDirs: string[];
5
+ hasKbEvidence: boolean;
6
+ }
7
+ /**
8
+ * Analyze workspace health for Kibi bootstrap and initialization.
9
+ */
10
+ export declare function checkWorkspaceHealth(cwd: string): WorkspaceHealth;
@@ -0,0 +1,39 @@
1
+ import fs from "node:fs";
2
+ // implements REQ-opencode-kibi-plugin-v1
3
+ import path from "node:path";
4
+ const KB_CONFIG_FILE = ".kb/config.json";
5
+ const KIBI_DOC_DIRS = [
6
+ "documentation/requirements",
7
+ "documentation/scenarios",
8
+ "documentation/tests",
9
+ "documentation/adr",
10
+ "documentation/flags",
11
+ "documentation/events",
12
+ "documentation/facts",
13
+ "documentation/symbols.yaml",
14
+ ];
15
+ /**
16
+ * Analyze workspace health for Kibi bootstrap and initialization.
17
+ */
18
+ export function checkWorkspaceHealth(cwd) {
19
+ const configPath = path.join(cwd, KB_CONFIG_FILE);
20
+ const missingConfig = !fs.existsSync(configPath);
21
+ const missingDocDirs = [];
22
+ for (const docDir of KIBI_DOC_DIRS) {
23
+ const fullPath = path.join(cwd, docDir);
24
+ if (!fs.existsSync(fullPath)) {
25
+ missingDocDirs.push(docDir);
26
+ }
27
+ }
28
+ // Check for any evidence of Kibi usage
29
+ const kbDir = path.join(cwd, ".kb");
30
+ const hasKbEvidence = fs.existsSync(kbDir) && fs.readdirSync(kbDir).length > 0;
31
+ // If missing config or more than 2 doc dirs are missing, suggest bootstrap
32
+ const needsBootstrap = missingConfig || missingDocDirs.length > 2;
33
+ return {
34
+ needsBootstrap,
35
+ missingConfig,
36
+ missingDocDirs,
37
+ hasKbEvidence,
38
+ };
39
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-opencode",
3
- "version": "0.3.1",
3
+ "version": "0.4.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",
@@ -10,6 +10,26 @@
10
10
  "types": "./dist/index.d.ts",
11
11
  "import": "./dist/index.js",
12
12
  "default": "./dist/index.js"
13
+ },
14
+ "./config": {
15
+ "types": "./dist/config.d.ts",
16
+ "import": "./dist/config.js",
17
+ "default": "./dist/config.js"
18
+ },
19
+ "./prompt": {
20
+ "types": "./dist/prompt.d.ts",
21
+ "import": "./dist/prompt.js",
22
+ "default": "./dist/prompt.js"
23
+ },
24
+ "./scheduler": {
25
+ "types": "./dist/scheduler.d.ts",
26
+ "import": "./dist/scheduler.js",
27
+ "default": "./dist/scheduler.js"
28
+ },
29
+ "./file-filter": {
30
+ "types": "./dist/file-filter.d.ts",
31
+ "import": "./dist/file-filter.js",
32
+ "default": "./dist/file-filter.js"
13
33
  }
14
34
  },
15
35
  "files": [