kibi-opencode 0.5.2 → 0.5.4

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
@@ -150,6 +150,19 @@ Config files (project overrides global):
150
150
 
151
151
  Per ADR-016, prompt text injection uses only `experimental.chat.system.transform`. The `chat.params` hook is reserved for model option enrichment (temperature, topP, etc.) and never carries prompt text.
152
152
 
153
+ ### Logging Policy
154
+
155
+ The plugin follows a **silent-except-errors** policy for terminal output:
156
+
157
+ | Channel | Terminal | Structured log |
158
+ |---------|----------|---------------|
159
+ | Normal operation (sync success, guidance injection, session summaries) | No | Yes, via `client.app.log()` |
160
+ | Error-class events (bootstrap-needed, sync/check failure, hook/init failure) | Yes, via `console.error` | Yes, via `client.app.log()` |
161
+
162
+ Routine diagnostics route through [`client.app.log()`](https://opencode.ai/docs/plugins/) and never appear in the terminal. Only error-class events break terminal silence. This keeps the developer's workspace clean while preserving full visibility in structured logs for debugging.
163
+
164
+ The `experimental.chat.system.transform` hook handles prompt injection (see [Hook Policy](#hook-policy)). The `chat.params` hook is compatibility-only and never carries prompt text.
165
+
153
166
  ### Hook Modes
154
167
 
155
168
  - `auto`: Use `experimental.chat.system.transform` (primary); `chat.params` is a no-op registration for host compatibility
@@ -190,6 +203,22 @@ Disable specific features while keeping others:
190
203
 
191
204
  This repository's OpenCode setup dogfoods local built artifacts. `opencode.json` starts the local `kibi-mcp` server, `.opencode/plugins/kibi.ts` re-exports `packages/opencode/dist/index.js`, and the published npm package (`kibi-opencode`) remains the distribution artifact for external consumers. See [DEV.md](DEV.md) for the repo-local workflow and rebuild rule.
192
205
 
206
+ ## Troubleshooting
207
+
208
+ If you see a false "workspace needs Kibi bootstrap" warning even though your workspace is already initialized with `.kb/config.json` pointing at relocated `kibi-docs/*` paths, this indicates a stale plugin cache. See [the main troubleshooting docs](../../docs/troubleshooting.md#opencode-shows-workspace-needs-kibi-bootstrap-before-the-tui) for recovery steps.
209
+
210
+ ## Architecture
211
+
212
+ This is a thin bridge layer per ADR-016:
213
+
214
+ XY|This repository's OpenCode setup dogfoods local built artifacts. `opencode.json` starts the local `kibi-mcp` server, `.opencode/plugins/kibi.ts` re-exports `packages/opencode/dist/index.js`, and the published npm package (`kibi-opencode`) remains the distribution artifact for external consumers. See [DEV.md](DEV.md) for the repo-local workflow and rebuild rule.
215
+
216
+ ## Troubleshooting
217
+
218
+ If you see a false "workspace needs Kibi bootstrap" warning even though your workspace is already initialized with `.kb/config.json` pointing at relocated `kibi-docs/*` paths, this indicates a stale plugin cache. See [the main troubleshooting docs](../../docs/troubleshooting.md#opencode-shows-workspace-needs-kibi-bootstrap-before-the-tui) for recovery steps.
219
+
220
+ ## Architecture
221
+
193
222
  ## Architecture
194
223
 
195
224
  This is a thin bridge layer per ADR-016:
@@ -8,5 +8,15 @@ export declare function loadKbSyncPaths(cwd?: string): {
8
8
  facts: string;
9
9
  symbols: string;
10
10
  };
11
+ export interface KbExistenceTarget {
12
+ key: string;
13
+ relativePath: string;
14
+ kind: "dir" | "file";
15
+ }
16
+ export declare function getKbExistenceTargets(cwd?: string): KbExistenceTarget[];
17
+ /** Strip the first path segment containing a glob character and everything
18
+ * after it, returning the directory root to check with existsSync.
19
+ */
20
+ export declare function stripToRoot(p: string): string;
11
21
  export declare function shouldHandleFile(filePath: string, cwd?: string): boolean;
12
22
  export default shouldHandleFile;
@@ -22,16 +22,17 @@ catch {
22
22
  },
23
23
  };
24
24
  }
25
- // Local copy of DEFAULT_SYNC_PATHS to avoid cross-package TS rootDir issues
25
+ // Local copy of DEFAULT_CONFIG.paths to avoid cross-package TS rootDir issues.
26
+ // Must stay in sync with DEFAULT_CONFIG.paths in packages/cli/src/utils/config.ts.
26
27
  const DEFAULT_SYNC_PATHS = {
27
- requirements: "requirements/**/*.md",
28
- scenarios: "scenarios/**/*.md",
29
- tests: "tests/**/*.md",
30
- adr: "adr/**/*.md",
31
- flags: "flags/**/*.md",
32
- events: "events/**/*.md",
33
- facts: "facts/**/*.md",
34
- symbols: "symbols.yaml",
28
+ requirements: "documentation/requirements/**/*.md",
29
+ scenarios: "documentation/scenarios/**/*.md",
30
+ tests: "documentation/tests/**/*.md",
31
+ adr: "documentation/adr/**/*.md",
32
+ flags: "documentation/flags/**/*.md",
33
+ events: "documentation/events/**/*.md",
34
+ facts: "documentation/facts/**/*.md",
35
+ symbols: "documentation/symbols.yaml",
35
36
  };
36
37
  function loadSyncConfigLocal(cwd = process.cwd()) {
37
38
  const configPath = path.join(cwd, ".kb/config.json");
@@ -56,6 +57,53 @@ export function loadKbSyncPaths(cwd = process.cwd()) {
56
57
  const cfg = loadSyncConfigLocal(cwd);
57
58
  return cfg.paths ?? DEFAULT_SYNC_PATHS;
58
59
  }
60
+ // implements REQ-opencode-kibi-plugin-v1
61
+ export function getKbExistenceTargets(cwd = process.cwd()) {
62
+ const paths = loadKbSyncPaths(cwd);
63
+ const keys = [
64
+ "requirements",
65
+ "scenarios",
66
+ "tests",
67
+ "adr",
68
+ "flags",
69
+ "events",
70
+ "facts",
71
+ "symbols",
72
+ ];
73
+ const targets = [];
74
+ for (const key of keys) {
75
+ const raw = paths[key];
76
+ if (!raw)
77
+ continue;
78
+ const isFile = raw.endsWith(".yaml") || raw.endsWith(".yml");
79
+ if (isFile) {
80
+ targets.push({ key, relativePath: raw, kind: "file" });
81
+ }
82
+ else {
83
+ // Contract: trim trailing slashes → normalizePattern → strip first glob segment
84
+ const trimmed = raw.replace(/\/+$/, "");
85
+ const normalized = normalizePattern(trimmed);
86
+ const relativePath = normalized ? stripToRoot(normalized) : ".";
87
+ targets.push({ key, relativePath, kind: "dir" });
88
+ }
89
+ }
90
+ return targets;
91
+ }
92
+ // implements REQ-opencode-kibi-plugin-v1
93
+ /** Strip the first path segment containing a glob character and everything
94
+ * after it, returning the directory root to check with existsSync.
95
+ */
96
+ export function stripToRoot(p) {
97
+ const segments = p.split("/");
98
+ const rootSegments = [];
99
+ for (const seg of segments) {
100
+ if (seg.includes("*") || seg.includes("?") || seg.includes("["))
101
+ break;
102
+ rootSegments.push(seg);
103
+ }
104
+ const result = rootSegments.join("/");
105
+ return result || ".";
106
+ }
59
107
  function normalizePattern(p) {
60
108
  if (!p)
61
109
  return null;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  export interface PluginInput {
2
2
  worktree: string;
3
3
  directory: string;
4
+ client?: {
5
+ app: {
6
+ log: (payload: Record<string, unknown>) => Promise<void>;
7
+ };
8
+ };
4
9
  }
5
10
  interface OpencodeEventPayload {
6
11
  type: string;
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import * as config from "./config.js";
4
4
  import * as fileFilter from "./file-filter.js";
5
5
  import * as logger from "./logger.js";
6
6
  import { analyzePath } from "./path-kind.js";
7
- import { injectPrompt } from "./prompt.js";
7
+ import { buildPrompt, SENTINEL } from "./prompt.js";
8
8
  import { isMustPriorityRequirement } from "./requirement-doc.js";
9
9
  import { createSyncScheduler } from "./scheduler.js";
10
10
  import { getSessionTracker } from "./session-tracker.js";
@@ -56,9 +56,15 @@ const kibiOpencodePlugin = async (input) => {
56
56
  return {};
57
57
  }
58
58
  // Check workspace health for bootstrap nudges
59
+ // Reset the logger client first to avoid leaking a previous invocation's
60
+ // client into this instance, then set the new one if provided.
61
+ logger.resetClient();
62
+ if (input.client) {
63
+ logger.setClient(input.client);
64
+ }
59
65
  const workspaceHealth = checkWorkspaceHealth(input.worktree);
60
66
  if (workspaceHealth.needsBootstrap) {
61
- logger.warn("kibi-opencode: workspace needs Kibi bootstrap");
67
+ logger.error("kibi-opencode: workspace needs Kibi bootstrap");
62
68
  getSessionTracker().recordWarning("bootstrap-needed", input.worktree, "Workspace missing Kibi bootstrap");
63
69
  }
64
70
  // Log session summary periodically (gated on config)
@@ -131,7 +137,7 @@ const kibiOpencodePlugin = async (input) => {
131
137
  : suggestion.suggestionType === "adr"
132
138
  ? "long-comment-missed-adr"
133
139
  : "missing-traceability";
134
- logger.info(`kibi-opencode: detected durable ${suggestion.suggestionType} knowledge in ${filePath}`);
140
+ logger.warn(`kibi-opencode: detected durable ${suggestion.suggestionType} knowledge in ${filePath}`);
135
141
  getSessionTracker().recordWarning(warningCategory, filePath, `Consider routing this ${suggestion.suggestionType} knowledge to Kibi instead of inline comments: ${suggestion.reasoning}`);
136
142
  }
137
143
  }
@@ -172,15 +178,23 @@ const kibiOpencodePlugin = async (input) => {
172
178
  const hookMode = cfg.prompt.hookMode;
173
179
  if (hookMode === "system-transform" || hookMode === "auto") {
174
180
  hooks["experimental.chat.system.transform"] = async (_input, output) => {
175
- const currentSystem = output.system.join("\n");
176
- const injected = injectPrompt(currentSystem, cfg, {
181
+ // Skip if sentinel already present in any existing entry
182
+ if (output.system.some((entry) => entry.includes(SENTINEL))) {
183
+ return;
184
+ }
185
+ // Build only the guidance block and append it; existing entries are preserved
186
+ const guidance = buildPrompt({
177
187
  recentEdits,
178
188
  workspaceHealth,
179
189
  hasRecentKbEdit,
180
190
  recentCommentSuggestion,
181
191
  });
182
- output.system.length = 0;
183
- output.system.push(injected);
192
+ const last = output.system.length > 0
193
+ ? output.system[output.system.length - 1]
194
+ : undefined;
195
+ if (last !== guidance) {
196
+ output.system.push(guidance);
197
+ }
184
198
  };
185
199
  }
186
200
  if (hookMode === "chat-params" || hookMode === "auto") {
package/dist/logger.d.ts CHANGED
@@ -1,3 +1,10 @@
1
+ export interface PluginClient {
2
+ app: {
3
+ log: (payload: Record<string, unknown>) => Promise<void>;
4
+ };
5
+ }
6
+ export declare function setClient(c: PluginClient): void;
7
+ export declare function resetClient(): void;
1
8
  export declare function info(msg: string): void;
2
9
  export declare function warn(msg: string): void;
3
10
  export declare function error(msg: string): void;
package/dist/logger.js CHANGED
@@ -1,11 +1,59 @@
1
1
  // implements REQ-opencode-kibi-plugin-v1
2
+ let client = null;
3
+ // implements REQ-opencode-kibi-plugin-v1
4
+ export function setClient(c) {
5
+ client = c;
6
+ }
7
+ // implements REQ-opencode-kibi-plugin-v1
8
+ export function resetClient() {
9
+ client = null;
10
+ }
11
+ // implements REQ-opencode-kibi-plugin-v1
2
12
  export function info(msg) {
3
- console.log("[kibi-opencode]", msg);
13
+ if (client) {
14
+ void client.app
15
+ .log({
16
+ body: {
17
+ service: "kibi-opencode",
18
+ level: "info",
19
+ message: msg,
20
+ },
21
+ })
22
+ .catch(console.error);
23
+ return;
24
+ }
25
+ // Fallback when no client is available (e.g. during tests or early init)
4
26
  }
27
+ // implements REQ-opencode-kibi-plugin-v1
5
28
  export function warn(msg) {
6
- console.warn("[kibi-opencode]", msg);
29
+ if (client) {
30
+ void client.app
31
+ .log({
32
+ body: {
33
+ service: "kibi-opencode",
34
+ level: "warn",
35
+ message: msg,
36
+ },
37
+ })
38
+ .catch(console.error);
39
+ return;
40
+ }
41
+ // Fallback when no client is available
7
42
  }
8
43
  // implements REQ-opencode-kibi-plugin-v1
9
44
  export function error(msg) {
45
+ // Always emit to console for user visibility
10
46
  console.error("[kibi-opencode]", msg);
47
+ // Also emit to structured logs if client is available
48
+ if (client) {
49
+ void client.app
50
+ .log({
51
+ body: {
52
+ service: "kibi-opencode",
53
+ level: "error",
54
+ message: msg,
55
+ },
56
+ })
57
+ .catch(console.error);
58
+ }
11
59
  }
package/dist/scheduler.js CHANGED
@@ -114,7 +114,7 @@ class WorktreeSyncScheduler {
114
114
  const checkResult = await this.runCheck(this.worktree, checkRules);
115
115
  checkExitCode = checkResult.exitCode;
116
116
  if (checkExitCode !== 0) {
117
- logger.warn(`check.failed ${JSON.stringify({ rules: checkRules, exitCode: checkExitCode })}`);
117
+ logger.error(`check.failed ${JSON.stringify({ rules: checkRules, exitCode: checkExitCode })}`);
118
118
  }
119
119
  else {
120
120
  logger.info(`check.succeeded ${JSON.stringify({ rules: checkRules })}`);
@@ -157,7 +157,7 @@ class WorktreeSyncScheduler {
157
157
  logger.info(`sync.succeeded ${JSON.stringify(meta)}`);
158
158
  }
159
159
  else {
160
- logger.warn(`sync.failed ${JSON.stringify(meta)}`);
160
+ logger.error(`sync.failed ${JSON.stringify(meta)}`);
161
161
  }
162
162
  this.onRunComplete?.(meta);
163
163
  }
@@ -1,7 +1,9 @@
1
1
  import fs from "node:fs";
2
2
  // implements REQ-opencode-kibi-plugin-v1
3
3
  import path from "node:path";
4
+ import { getKbExistenceTargets } from "./file-filter.js";
4
5
  const KB_CONFIG_FILE = ".kb/config.json";
6
+ // Fallback defaults used when .kb/config.json does not exist
5
7
  const KIBI_DOC_DIRS = [
6
8
  "documentation/requirements",
7
9
  "documentation/scenarios",
@@ -12,6 +14,7 @@ const KIBI_DOC_DIRS = [
12
14
  "documentation/facts",
13
15
  "documentation/symbols.yaml",
14
16
  ];
17
+ // implements REQ-opencode-kibi-plugin-v1
15
18
  /**
16
19
  * Analyze workspace health for Kibi bootstrap and initialization.
17
20
  */
@@ -19,10 +22,43 @@ export function checkWorkspaceHealth(cwd) {
19
22
  const configPath = path.join(cwd, KB_CONFIG_FILE);
20
23
  const missingConfig = !fs.existsSync(configPath);
21
24
  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);
25
+ if (missingConfig) {
26
+ // No config file: fall back to hardcoded defaults
27
+ for (const docDir of KIBI_DOC_DIRS) {
28
+ const fullPath = path.join(cwd, docDir);
29
+ if (!fs.existsSync(fullPath)) {
30
+ missingDocDirs.push(docDir);
31
+ }
32
+ }
33
+ }
34
+ else {
35
+ // Config exists: check if user specified custom paths
36
+ let hasUserPaths = false;
37
+ try {
38
+ const raw = JSON.parse(fs.readFileSync(configPath, "utf8"));
39
+ hasUserPaths = Boolean(raw && raw.paths);
40
+ }
41
+ catch {
42
+ hasUserPaths = false;
43
+ }
44
+ if (hasUserPaths) {
45
+ // User has custom paths: resolve targets dynamically
46
+ const targets = getKbExistenceTargets(cwd);
47
+ for (const target of targets) {
48
+ const fullPath = path.join(cwd, target.relativePath);
49
+ if (!fs.existsSync(fullPath)) {
50
+ missingDocDirs.push(target.relativePath);
51
+ }
52
+ }
53
+ }
54
+ else {
55
+ // Config exists but no custom paths: use hardcoded defaults
56
+ for (const docDir of KIBI_DOC_DIRS) {
57
+ const fullPath = path.join(cwd, docDir);
58
+ if (!fs.existsSync(fullPath)) {
59
+ missingDocDirs.push(docDir);
60
+ }
61
+ }
26
62
  }
27
63
  }
28
64
  // Check for any evidence of Kibi usage
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-opencode",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "Kibi OpenCode plugin - thin adapter to integrate Kibi with OpenCode sessions",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",