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 +3 -2
- package/dist/hook-telemetry.d.ts +15 -0
- package/dist/hook-telemetry.js +55 -0
- package/dist/kibi-checkpoint-runner.js +5 -5
- package/dist/plugin.d.ts +5 -0
- package/dist/plugin.js +11 -0
- package/dist/prompt.js +14 -4
- package/dist/risk-classifier.js +1 -1
- package/dist/smart-enforcement-events.js +9 -2
- package/dist/version-metadata.js +1 -1
- package/dist/version-metadata.json +1 -1
- package/package.json +6 -3
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-
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
package/dist/risk-classifier.js
CHANGED
|
@@ -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
|
|
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,
|
|
57
|
+
env.freshnessStore.recordToolEvidence(scope, evidenceTool);
|
|
51
58
|
env.log.info("kb-freshness.tool-evidence", {
|
|
52
59
|
event: "kb_freshness_tool_evidence",
|
|
53
|
-
tool:
|
|
60
|
+
tool: evidenceTool,
|
|
54
61
|
});
|
|
55
62
|
}
|
|
56
63
|
catch {
|
package/dist/version-metadata.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync as fsReadFileSync } from "node:fs";
|
|
2
|
-
// implements REQ-opencode-
|
|
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
|
+
{"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
|
|
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": [
|
|
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-
|
|
65
|
+
"kibi-agent-core": "^0.2.0",
|
|
66
|
+
"kibi-runtime": "^2.3.0"
|
|
64
67
|
},
|
|
65
68
|
"devDependencies": {
|
|
66
69
|
"@types/node": "latest",
|