kibi-opencode 2.0.1 → 2.1.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.
package/README.md CHANGED
@@ -52,7 +52,8 @@ The plugin now uses a posture-aware, low-token smart-enforcement model before em
52
52
 
53
53
  - **Repo posture detection**: distinguishes `root_active`, `root_partial`, `root_uninitialized`, `vendored_only`, and `hybrid_root_plus_vendored`
54
54
  - **Risk classification**: separates `safe_docs_only`, `safe_test_only`, `kb_doc_structural`, `req_policy_candidate`, `behavior_candidate`, `traceability_candidate`, and `manual_kb_edit`
55
- - **Source-linked Kibi links**: risky code edits (`behavior_candidate`, `traceability_candidate`) prepend a concise list of existing Kibi links (e.g., `- Existing Kibi links: REQ-001, REQ-002`) when 1-3 concrete source-linked KB hits are found in `.kb/symbols.yaml`. Skip on cache hit.
55
+ - **Source-linked Kibi links**: risky code edits (`behavior_candidate`, `traceability_candidate`) prepend a concise list of existing Kibi links (e.g., `- Existing Kibi links: REQ-cli-gc, REQ-cli-check`) when 1-3 concrete source-linked KB hits are found in `.kb/symbols.yaml`. Skip on cache hit. When the lead requirement is current, one more bullet says what it must keep true (one linked fact) and the decision behind it (its ADR), built with the same `kibi-agent-core/snippets` grounding as the Claude Code, Cursor, Codex, and ZCode hooks; a superseded or retired requirement gets no such bullet.
56
+ - **Opt-in tool telemetry**: with `KIBI_DIAGNOSTIC_MODE=1`, the `tool.execute.after` hook appends `interface: "hook"` rows with `host: "opencode"` to `.kb/usage.log` for `kb_search`/`kb_query` and other Kibi calls (MCP tools such as `kibi_kb_search`, or `kibi <route>` in bash) and for edited files with the requirements they implement, keyed by the OpenCode session id. `kibi usage-metrics` reads them for the `lookup_before_first_edit` metric. Nothing is written without the variable.
56
57
  - **Start-task risky cue**: authoritative risky edits also add a compact Kibi context cue so agents can start with explicit source-linked guidance before acting, while staying inside the same single prompt block and token budget.
57
58
  - **Effective mode gating**: `advisory` (default) never blocks; `strict` escalates checks and reminders for `root_active` and `hybrid_root_plus_vendored` postures when `requireRootKbForStrict` is enabled; `hard` fails closed for authoritative roots and linked git worktrees, injecting capability-based recovery steps until the Kibi checkpoint passes. `maintenanceDegraded` overrides everything back to `advisory`.
58
59
  - **Low-token prompt policy**: docs-only and test-only edits avoid unnecessary discovery prompts; vendored-only repos suppress operational bootstrap nudges; at most one contextual block is injected per prompt (≤120 words, ≤5 bullets)
@@ -303,7 +304,7 @@ Disable specific features while keeping others:
303
304
 
304
305
  ## Troubleshooting
305
306
 
306
- If you see a false "workspace needs Kibi bootstrap" warning even though `.kb/manifest.json` and the canonical knowledge lanes exist, this usually means a stale plugin cache. Leftover `.kb/config.json` custom paths are ignored. See [the main troubleshooting docs](../../docs/troubleshooting.md#opencode-shows-workspace-needs-kibi-bootstrap-before-the-tui) for recovery steps.
307
+ If you see a false "workspace needs Kibi bootstrap" warning even though `.kb/manifest.json` and the canonical knowledge lanes exist, this usually means a stale plugin cache. Leftover `.kb/config.json` custom paths are ignored. See [the troubleshooting guide](https://looted.github.io/kibi/guide/troubleshooting.html#opencode-shows-workspace-needs-kibi-bootstrap-before-the-tui) for recovery steps.
307
308
 
308
309
  ## Architecture
309
310
 
@@ -0,0 +1,15 @@
1
+ export type OpencodeToolCall = {
2
+ /** Kibi workspace root (the session's worktree). */
3
+ worktree: string;
4
+ tool: string | undefined;
5
+ args: unknown;
6
+ sessionId: string | undefined;
7
+ startedAt: Date;
8
+ };
9
+ /**
10
+ * Opt-in (`KIBI_DIAGNOSTIC_MODE`) hook rows for the lookup-before-first-edit
11
+ * telemetry metric, written through the shared kibi-agent-core format: Kibi
12
+ * lookups through MCP or the CLI, and edits with the requirements the edited
13
+ * files implement (`implements` links in the configured symbols manifest).
14
+ */
15
+ export declare function recordOpencodeToolTelemetry(call: OpencodeToolCall, env?: NodeJS.ProcessEnv): void;
@@ -0,0 +1,55 @@
1
+ // implements REQ-opencode-kibi-plugin-v1, REQ-kibi-telemetry-acceptance-gate-v2
2
+ import { existsSync } from "node:fs";
3
+ import * as path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { appendHookUsageRows, editTraces, hookTelemetryEnabled, kbUsageTrace, readPackageVersion, } from "kibi-agent-core/hook-usage-log";
6
+ import { extractEditedPaths, toWorkspacePath, } from "kibi-agent-core/path-policy";
7
+ import { getFileLinkedTargetsByType } from "./file-entity-links.js";
8
+ /** OpenCode's built-in file-editing tools. */
9
+ const EDIT_TOOLS = new Set(["edit", "write", "patch", "multiedit"]);
10
+ let cachedPackageVersion;
11
+ function packageVersion() {
12
+ // dist/hook-telemetry.js sits one level below the package root.
13
+ if (cachedPackageVersion !== undefined)
14
+ return cachedPackageVersion;
15
+ cachedPackageVersion = readPackageVersion(path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json"));
16
+ return cachedPackageVersion;
17
+ }
18
+ // implements REQ-opencode-kibi-plugin-v1, REQ-kibi-telemetry-acceptance-gate-v2
19
+ /**
20
+ * Opt-in (`KIBI_DIAGNOSTIC_MODE`) hook rows for the lookup-before-first-edit
21
+ * telemetry metric, written through the shared kibi-agent-core format: Kibi
22
+ * lookups through MCP or the CLI, and edits with the requirements the edited
23
+ * files implement (`implements` links in the configured symbols manifest).
24
+ */
25
+ export function recordOpencodeToolTelemetry(call, env = process.env) {
26
+ if (!hookTelemetryEnabled(env))
27
+ return;
28
+ try {
29
+ recordToolCall(call, env);
30
+ }
31
+ catch {
32
+ // Telemetry is best effort and must never disturb the tool call.
33
+ }
34
+ }
35
+ function recordToolCall(call, env) {
36
+ if (!existsSync(path.join(call.worktree, ".kb")))
37
+ return;
38
+ const kbUsage = kbUsageTrace(call.tool, call.args);
39
+ let traces = kbUsage ? [kbUsage] : [];
40
+ if (!kbUsage && call.tool !== undefined && EDIT_TOOLS.has(call.tool)) {
41
+ const relativePaths = extractEditedPaths(call.args)
42
+ .map((rawPath) => toWorkspacePath(call.worktree, rawPath, call.worktree)?.relative)
43
+ .filter((relative) => relative !== undefined);
44
+ traces = editTraces(relativePaths, (relativePath) => getFileLinkedTargetsByType(call.worktree, path.join(call.worktree, relativePath), ["implements"]));
45
+ }
46
+ appendHookUsageRows({
47
+ host: "opencode",
48
+ packageVersion: packageVersion(),
49
+ workspaceRoot: call.worktree,
50
+ event: "tool.execute.after",
51
+ sessionId: call.sessionId,
52
+ hostTool: call.tool,
53
+ startedAt: call.startedAt,
54
+ }, traces, env);
55
+ }
@@ -1,4 +1,4 @@
1
- // implements REQ-opencode-worktree-hard-enforcement-v1
1
+ // implements REQ-opencode-smart-enforcement-v1
2
2
  import { DEFAULTS } from "./config.js";
3
3
  import { computeEnforcementPolicy, } from "./enforcement-policy.js";
4
4
  import { buildEnforcementScopeKey } from "./enforcement-scope.js";
@@ -10,7 +10,7 @@ const NO_E2E_SIGNAL = {
10
10
  evidence: [],
11
11
  reminderText: null,
12
12
  };
13
- // implements REQ-opencode-worktree-hard-enforcement-v1
13
+ // implements REQ-opencode-smart-enforcement-v1
14
14
  export class KibiCheckpointRunner {
15
15
  config;
16
16
  runSync;
@@ -31,7 +31,7 @@ export class KibiCheckpointRunner {
31
31
  this.setTimeoutFn = options.setTimeoutFn ?? setTimeout;
32
32
  this.clearTimeoutFn = options.clearTimeoutFn ?? clearTimeout;
33
33
  }
34
- // implements REQ-opencode-worktree-hard-enforcement-v1
34
+ // implements REQ-opencode-smart-enforcement-v1
35
35
  requestCheckpoint(context, fingerprint) {
36
36
  const normalizedFingerprint = normalizeFingerprint(fingerprint);
37
37
  const metadata = this.baseMetadata(context, normalizedFingerprint);
@@ -63,7 +63,7 @@ export class KibiCheckpointRunner {
63
63
  this.requested.set(metadata.scopeKey, requested);
64
64
  return { kind: "requested", metadata: requested.metadata };
65
65
  }
66
- // implements REQ-opencode-worktree-hard-enforcement-v1
66
+ // implements REQ-opencode-smart-enforcement-v1
67
67
  isCheckpointPassed(fingerprint, context) {
68
68
  const normalizedFingerprint = normalizeFingerprint(fingerprint);
69
69
  if (context) {
@@ -72,7 +72,7 @@ export class KibiCheckpointRunner {
72
72
  return ((this.passedScopeKeysByFingerprint.get(normalizedFingerprint)?.size ??
73
73
  0) > 0);
74
74
  }
75
- // implements REQ-opencode-worktree-hard-enforcement-v1
75
+ // implements REQ-opencode-smart-enforcement-v1
76
76
  async runCheckpoint(context, fingerprint) {
77
77
  const normalizedFingerprint = normalizeFingerprint(fingerprint);
78
78
  const metadata = this.baseMetadata(context, normalizedFingerprint);
package/dist/plugin.d.ts CHANGED
@@ -50,6 +50,11 @@ export interface Hooks {
50
50
  config?: (input: OpenCodeConfigHookInput) => void | Promise<void>;
51
51
  "experimental.chat.system.transform"?: (input: unknown, output: SystemTransformOutput) => void | Promise<void>;
52
52
  "chat.params"?: (input: unknown, output: unknown) => void | Promise<void>;
53
+ "tool.execute.after"?: (input: {
54
+ tool?: string;
55
+ sessionID?: string;
56
+ args?: unknown;
57
+ } | undefined, output?: unknown) => void | Promise<void>;
53
58
  }
54
59
  export type Plugin = (input: PluginInput) => Hooks | Promise<Hooks>;
55
60
  declare const kibiOpencodePlugin: Plugin;
package/dist/plugin.js CHANGED
@@ -7,6 +7,7 @@ import { getFileLinkedEntityIds } from "./file-entity-links.js"; // implements R
7
7
  import * as fileFilter from "./file-filter.js";
8
8
  import { deriveFileOperationReminder } from "./file-operation-reminders.js"; // implements REQ-opencode-file-context-guidance-v1
9
9
  import { createFileOperationState, } from "./file-operation-state.js"; // implements REQ-opencode-file-context-guidance-v1
10
+ import { recordOpencodeToolTelemetry } from "./hook-telemetry.js";
10
11
  import { createKbFreshnessEvidenceStore, evaluateKbFreshness, } from "./kb-freshness-state.js";
11
12
  import { getKibiBootstrapCommandCapability, registerKibiBootstrapCommand, } from "./kibi-bootstrap-capability.js";
12
13
  import { KibiCheckpointRunner, } from "./kibi-checkpoint-runner.js";
@@ -385,6 +386,16 @@ const kibiOpencodePlugin = async (input) => {
385
386
  posture,
386
387
  lintRequirementDoc,
387
388
  };
389
+ // Opt-in usage rows for the lookup-before-first-edit telemetry metric.
390
+ hooks["tool.execute.after"] = async (toolCall) => {
391
+ recordOpencodeToolTelemetry({
392
+ worktree: rootWorkContext.worktreeRoot,
393
+ tool: toolCall?.tool,
394
+ args: toolCall?.args,
395
+ sessionId: toolCall?.sessionID,
396
+ startedAt: new Date(),
397
+ });
398
+ };
388
399
  hooks.event = async ({ event }) => {
389
400
  if (isKbToolEventType(event.type)) {
390
401
  handleKbToolEvent(eventEnv, event, input.sessionId);
package/dist/prompt.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as path from "node:path";
2
+ import { createEntitySummarizer, requirementGroundingLines, } from "kibi-agent-core/snippets";
2
3
  import { isPluginEnabled } from "./config.js";
3
4
  import { getKibiBootstrapCommandCapability, } from "./kibi-bootstrap-capability.js";
4
5
  import { getSourceLinkedRequirementIds } from "./source-linked-guidance.js";
@@ -119,7 +120,7 @@ Do not read or edit \`.kb/\` directly. Follow typed Kibi status \`nextActions\`
119
120
  };
120
121
  // ── Posture overrides ──────────────────────────────────────────────────
121
122
  export function postureGuidance(posture, capability = getKibiBootstrapCommandCapability()) {
122
- // implements REQ-opencode-prompt-injection
123
+ // implements REQ-opencode-guidance-injection
123
124
  switch (posture) {
124
125
  case "vendored_only":
125
126
  // Minimal guidance only, no bootstrap nags
@@ -279,7 +280,16 @@ If you're adding long explanatory comments, consider routing that knowledge to:
279
280
  ? editedPath
280
281
  : path.join(context.workspaceRoot, editedPath);
281
282
  const linkedIds = getSourceLinkedRequirementIds(context.workspaceRoot, absEdited);
282
- if (linkedIds.length >= 1 && linkedIds.length <= 3) {
283
+ const leadId = linkedIds[0];
284
+ if (leadId !== undefined && linkedIds.length <= 3) {
285
+ // What the lead requirement must keep true and the decision behind
286
+ // it, from the shared kibi-agent-core projection. One fact keeps
287
+ // the bullet inside the prompt's word budget; retired requirements
288
+ // contribute nothing.
289
+ const grounding = requirementGroundingLines(leadId, createEntitySummarizer(context.workspaceRoot), { maxFacts: 1 });
290
+ if (grounding.length > 0) {
291
+ selectedBlock = insertBulletAfterHeader(selectedBlock, `- ${grounding.join(" ")}`);
292
+ }
283
293
  selectedBlock = insertBulletAfterHeader(selectedBlock, `- Existing Kibi links: ${linkedIds.join(", ")}`);
284
294
  }
285
295
  }
@@ -433,7 +443,7 @@ This keeps domain truths centralized, searchable, and contradiction-safe.`;
433
443
  Your recent code edit contains a comment that looks like a **technical decision** (tradeoffs, rationale, or architecture choices).
434
444
 
435
445
  **Action**: Instead of inline comments, route this to an ADR entity:
436
- - Create \`.kb/adr/ADR-xxx.md\` documenting the decision
446
+ - Create \`.kb/adr/ADR-<decision>.md\` documenting the decision
437
447
  - Include context, options considered, and the chosen approach
438
448
  - Link to constrained code symbols
439
449
 
@@ -444,7 +454,7 @@ This preserves decision context for future maintainers.`;
444
454
  Your recent code edit contains a comment that looks like **behavior intent** (system capabilities or user-facing requirements).
445
455
 
446
456
  **Action**: Instead of inline comments, route this to a REQ entity:
447
- - Create \`.kb/requirements/REQ-xxx.md\` with the behavior description
457
+ - Create \`.kb/requirements/REQ-<area>-<behavior>.md\` (named by the behavior, never the next number) with the behavior description
448
458
  - Add SCEN and TEST entities for specification and verification
449
459
  - Link code: production uses \`implements\` (symbol→req) for ownership; test code uses \`executable_for\`; \`covered_by\` is coverage evidence only
450
460
 
@@ -82,7 +82,7 @@ export function classifyRisk(params) {
82
82
  if (hasDurableComment || !hasTraceability) {
83
83
  const reasons = [];
84
84
  if (!hasTraceability) {
85
- reasons.push("Code file contains exports without // implements REQ-xxx annotation");
85
+ reasons.push("Code file contains exports without // implements REQ-<area>-<behavior> annotation");
86
86
  }
87
87
  if (hasDurableComment) {
88
88
  reasons.push("Durable knowledge comment detected — traceability review recommended");
@@ -46,11 +46,18 @@ export function handleKbToolEvent(env, event, sessionId) {
46
46
  branch: env.rootWorkContext.branch,
47
47
  fingerprint: `${env.input.sessionId ?? ""}-${env.rootWorkContext.branch}`,
48
48
  };
49
+ // A dry-run upsert only validates and is not a KB mutation.
50
+ const args = (props.args ??
51
+ props.input?.args ??
52
+ props.call?.args);
53
+ const evidenceTool = toolName === "kb_upsert" && args?.dryRun === true
54
+ ? "kb_validate_upsert"
55
+ : toolName;
49
56
  try {
50
- env.freshnessStore.recordToolEvidence(scope, toolName);
57
+ env.freshnessStore.recordToolEvidence(scope, evidenceTool);
51
58
  env.log.info("kb-freshness.tool-evidence", {
52
59
  event: "kb_freshness_tool_evidence",
53
- tool: toolName,
60
+ tool: evidenceTool,
54
61
  });
55
62
  }
56
63
  catch {
@@ -1,5 +1,5 @@
1
1
  import { readFileSync as fsReadFileSync } from "node:fs";
2
- // implements REQ-opencode-toast-package-versions-v1
2
+ // implements REQ-opencode-kibi-plugin-v1
3
3
  export function readKibiPackageVersions(options) {
4
4
  const baseUrl = options?.baseUrl ?? import.meta.url;
5
5
  const readFs = options?.readFileSync ?? fsReadFileSync;
@@ -1 +1 @@
1
- {"opencode":"2.0.1","mcp":"2.1.0","cli":"2.1.0","core":"0.13.0"}
1
+ {"opencode":"2.1.0","mcp":"3.0.0","cli":"2.5.0","core":"0.15.0"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-opencode",
3
- "version": "2.0.1",
3
+ "version": "2.1.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,7 +37,9 @@
37
37
  "default": "./dist/tui.js"
38
38
  }
39
39
  },
40
- "files": ["dist"],
40
+ "files": [
41
+ "dist"
42
+ ],
41
43
  "engines": {
42
44
  "node": ">=22"
43
45
  },
@@ -60,7 +62,8 @@
60
62
  "@opencode-ai/plugin": "^1.4.7",
61
63
  "@opentui/core": "^0.1.99",
62
64
  "@opentui/solid": "^0.1.99",
63
- "kibi-runtime": "^2.0.1"
65
+ "kibi-agent-core": "^0.2.0",
66
+ "kibi-runtime": "^2.3.0"
64
67
  },
65
68
  "devDependencies": {
66
69
  "@types/node": "latest",