kibi-opencode 2.0.2 → 2.1.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
@@ -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-cli-gc, REQ-cli-check`) 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)
@@ -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
  }
@@ -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.2","mcp":"2.2.0","cli":"2.2.0","core":"0.14.0"}
1
+ {"opencode":"2.1.1","mcp":"3.0.1","cli":"2.5.1","core":"0.15.0"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-opencode",
3
- "version": "2.0.2",
3
+ "version": "2.1.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",
@@ -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.2"
65
+ "kibi-agent-core": "^0.2.0",
66
+ "kibi-runtime": "^2.3.1"
64
67
  },
65
68
  "devDependencies": {
66
69
  "@types/node": "latest",