kibi-opencode 0.5.4 → 0.6.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 +27 -14
- package/dist/config.d.ts +9 -0
- package/dist/config.js +31 -0
- package/dist/guidance-cache.d.ts +75 -0
- package/dist/guidance-cache.js +145 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +428 -56
- package/dist/logger.d.ts +4 -3
- package/dist/logger.js +14 -18
- package/dist/path-kind.d.ts +2 -2
- package/dist/path-kind.js +12 -4
- package/dist/prompt.d.ts +22 -1
- package/dist/prompt.js +297 -121
- package/dist/repo-posture.d.ts +12 -0
- package/dist/repo-posture.js +196 -0
- package/dist/risk-classifier.d.ts +39 -0
- package/dist/risk-classifier.js +111 -0
- package/dist/smart-enforcement.d.ts +41 -0
- package/dist/smart-enforcement.js +48 -0
- package/dist/source-linked-guidance.d.ts +10 -0
- package/dist/source-linked-guidance.js +164 -0
- package/dist/workspace-health.d.ts +2 -0
- package/dist/workspace-health.js +14 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,6 +18,20 @@ Or via OpenCode's plugin system in `opencode.json`:
|
|
|
18
18
|
|
|
19
19
|
## Features
|
|
20
20
|
|
|
21
|
+
### Smart Enforcement
|
|
22
|
+
|
|
23
|
+
The plugin now uses a posture-aware, low-token smart-enforcement model before emitting any guidance:
|
|
24
|
+
|
|
25
|
+
- **Repo posture detection**: distinguishes `root_active`, `root_partial`, `root_uninitialized`, `vendored_only`, and `hybrid_root_plus_vendored`
|
|
26
|
+
- **Risk classification**: separates `safe_docs_only`, `safe_test_only`, `kb_doc_structural`, `req_policy_candidate`, `behavior_candidate`, `traceability_candidate`, and `manual_kb_edit`
|
|
27
|
+
- **Source-linked micro-briefs**: 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 `documentation/symbols.yaml`. Skip on cache hit.
|
|
28
|
+
- **Effective mode gating**: `strict` is only possible for `root_active` and `hybrid_root_plus_vendored` when `requireRootKbForStrict` is enabled; `maintenanceDegraded` overrides everything back to `advisory`
|
|
29
|
+
- **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)
|
|
30
|
+
- **Completion reminder**: when `completionReminder` is enabled, risky code edits append a single prompt-visible `kb_check` reminder exactly once per cached context
|
|
31
|
+
- **Runtime maintenance overlay**: static `maintenanceDegraded` from posture is merged with a latched runtime overlay (sync disabled/unavailable/failing) so degraded state is consistently reflected in prompts, logs, and mode decisions
|
|
32
|
+
- **Advisory in editor, hard in hooks**: OpenCode guidance remains non-blocking; git hooks and KB validation checks stay the durable enforcement boundary
|
|
33
|
+
- **Structured observability**: posture, risk, cache, degraded-mode, targeted-check, and guidance events flow through structured plugin logs
|
|
34
|
+
|
|
21
35
|
### Dynamic Contextual Guidance
|
|
22
36
|
|
|
23
37
|
The plugin provides context-aware prompt guidance based on recent edits and workspace state:
|
|
@@ -31,8 +45,10 @@ The plugin provides context-aware prompt guidance based on recent edits and work
|
|
|
31
45
|
|
|
32
46
|
After KB-document edits, the plugin queues targeted validation rules to run via background sync operations:
|
|
33
47
|
|
|
34
|
-
- **Must-priority requirement edits**: elevated validation including coverage checks
|
|
35
|
-
- **
|
|
48
|
+
- **Must-priority requirement edits**: elevated validation including coverage checks (`must-priority-coverage`)
|
|
49
|
+
- **Traceability candidate code edits**: schedules `symbol-traceability` via reason `smart-enforcement.traceability`
|
|
50
|
+
- **Fact KB doc edits**: includes `strict-fact-shape` validation alongside standard structural checks
|
|
51
|
+
- **Other requirement/scenario/test/ADR/fact edits**: standard validation for `required-fields` and `no-dangling-refs`
|
|
36
52
|
|
|
37
53
|
The plugin inspects requirement frontmatter to detect `priority: must` and schedules elevated validation for critical requirements. Runs in background after sync completes, non-blocking. Can be disabled via `guidance.targetedChecks.enabled: false`.
|
|
38
54
|
|
|
@@ -144,12 +160,21 @@ Config files (project overrides global):
|
|
|
144
160
|
| `guidance.targetedChecks.enabled` | boolean | `true` | Enable post-sync targeted validation checks |
|
|
145
161
|
| `guidance.sessionSummary.enabled` | boolean | `true` | Enable periodic session summary logs |
|
|
146
162
|
| `guidance.sessionSummary.logIntervalMs` | number | `1800000` | Session summary interval (30 min) |
|
|
163
|
+
| `guidance.smartEnforcement.enabled` | boolean | `true` | Enable posture-aware, risk-aware guidance routing |
|
|
164
|
+
| `guidance.smartEnforcement.mode` | string | `"advisory"` | Smart-enforcement mode: `advisory` or `strict` |
|
|
165
|
+
| `guidance.smartEnforcement.preflightTtlMs` | number | `600000` | Guidance/cache TTL for repeated prompt suppression |
|
|
166
|
+
| `guidance.smartEnforcement.idleResetMs` | number | `1800000` | Idle window before smart-enforcement state resets |
|
|
167
|
+
| `guidance.smartEnforcement.degradedMode` | string | `"warn-once"` | Degraded-mode logging policy: `warn-once` or `structured-only` |
|
|
168
|
+
| `guidance.smartEnforcement.requireRootKbForStrict` | boolean | `true` | Only allow strict behavior for authoritative root KB postures |
|
|
169
|
+
| `guidance.smartEnforcement.completionReminder` | boolean | `true` | Emit a single completion-time KB check reminder for risky edits |
|
|
147
170
|
| `logLevel` | string | `"info"` | Log level: `debug`, `info`, `warn`, `error` |
|
|
148
171
|
|
|
149
172
|
### Hook Policy
|
|
150
173
|
|
|
151
174
|
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
175
|
|
|
176
|
+
Smart enforcement keeps the plugin advisory-only: prompt injection can warn, explain, or remind, but it does not block the editor. Hard failures still belong to CLI hooks (`pre-commit`) and explicit check commands.
|
|
177
|
+
|
|
153
178
|
### Logging Policy
|
|
154
179
|
|
|
155
180
|
The plugin follows a **silent-except-errors** policy for terminal output:
|
|
@@ -211,18 +236,6 @@ If you see a false "workspace needs Kibi bootstrap" warning even though your wor
|
|
|
211
236
|
|
|
212
237
|
This is a thin bridge layer per ADR-016:
|
|
213
238
|
|
|
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
|
-
|
|
222
|
-
## Architecture
|
|
223
|
-
|
|
224
|
-
This is a thin bridge layer per ADR-016:
|
|
225
|
-
|
|
226
239
|
- **Agent-visible guidance**: Public MCP tools (`kb_query`, `kb_upsert`, `kb_check`, etc.) and sanctioned slash commands (`/init-kibi`)
|
|
227
240
|
- **Discovery-first workflow**: Agents are guided to use `kb_search` first, then `kb_query`, then reporting tools like `kb_status`, `kb_find_gaps`, `kb_coverage`, and `kb_graph` when needed
|
|
228
241
|
- **Internal maintenance**: Background sync operations handle KB synchronization; agents do NOT run sync commands directly
|
package/dist/config.d.ts
CHANGED
|
@@ -30,6 +30,15 @@ export interface KibiConfig {
|
|
|
30
30
|
enabled: boolean;
|
|
31
31
|
logIntervalMs: number;
|
|
32
32
|
};
|
|
33
|
+
smartEnforcement: {
|
|
34
|
+
enabled: boolean;
|
|
35
|
+
mode: "advisory" | "strict";
|
|
36
|
+
preflightTtlMs: number;
|
|
37
|
+
idleResetMs: number;
|
|
38
|
+
degradedMode: "warn-once" | "structured-only";
|
|
39
|
+
requireRootKbForStrict: boolean;
|
|
40
|
+
completionReminder: boolean;
|
|
41
|
+
};
|
|
33
42
|
};
|
|
34
43
|
logLevel: string;
|
|
35
44
|
}
|
package/dist/config.js
CHANGED
|
@@ -22,6 +22,15 @@ const DEFAULTS = {
|
|
|
22
22
|
enabled: true,
|
|
23
23
|
logIntervalMs: 30 * 60 * 1000, // 30 minutes
|
|
24
24
|
},
|
|
25
|
+
smartEnforcement: {
|
|
26
|
+
enabled: true,
|
|
27
|
+
mode: "advisory",
|
|
28
|
+
preflightTtlMs: 600000, // 10 minutes
|
|
29
|
+
idleResetMs: 1800000, // 30 minutes
|
|
30
|
+
degradedMode: "warn-once",
|
|
31
|
+
requireRootKbForStrict: true,
|
|
32
|
+
completionReminder: true,
|
|
33
|
+
},
|
|
25
34
|
},
|
|
26
35
|
logLevel: "info",
|
|
27
36
|
};
|
|
@@ -117,6 +126,28 @@ function validateAndMerge(obj) {
|
|
|
117
126
|
if (typeof ss.logIntervalMs === "number")
|
|
118
127
|
out.guidance.sessionSummary.logIntervalMs = ss.logIntervalMs;
|
|
119
128
|
}
|
|
129
|
+
if (g.smartEnforcement && typeof g.smartEnforcement === "object") {
|
|
130
|
+
const se = g.smartEnforcement;
|
|
131
|
+
out.guidance.smartEnforcement = {
|
|
132
|
+
...DEFAULTS.guidance.smartEnforcement,
|
|
133
|
+
};
|
|
134
|
+
if (typeof se.enabled === "boolean")
|
|
135
|
+
out.guidance.smartEnforcement.enabled = se.enabled;
|
|
136
|
+
if (se.mode === "advisory" || se.mode === "strict")
|
|
137
|
+
out.guidance.smartEnforcement.mode = se.mode;
|
|
138
|
+
if (typeof se.preflightTtlMs === "number")
|
|
139
|
+
out.guidance.smartEnforcement.preflightTtlMs = se.preflightTtlMs;
|
|
140
|
+
if (typeof se.idleResetMs === "number")
|
|
141
|
+
out.guidance.smartEnforcement.idleResetMs = se.idleResetMs;
|
|
142
|
+
if (se.degradedMode === "warn-once" || se.degradedMode === "structured-only")
|
|
143
|
+
out.guidance.smartEnforcement.degradedMode = se.degradedMode;
|
|
144
|
+
if (typeof se.requireRootKbForStrict === "boolean")
|
|
145
|
+
out.guidance.smartEnforcement.requireRootKbForStrict =
|
|
146
|
+
se.requireRootKbForStrict;
|
|
147
|
+
if (typeof se.completionReminder === "boolean")
|
|
148
|
+
out.guidance.smartEnforcement.completionReminder =
|
|
149
|
+
se.completionReminder;
|
|
150
|
+
}
|
|
120
151
|
}
|
|
121
152
|
return out;
|
|
122
153
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { RepoPosture } from "./repo-posture.js";
|
|
2
|
+
import type { RiskClass } from "./risk-classifier.js";
|
|
3
|
+
/**
|
|
4
|
+
* Cache key uniquely identifies a preflight context by combining
|
|
5
|
+
* workspace root, branch, posture, risk class, and file bucket.
|
|
6
|
+
*/
|
|
7
|
+
export interface CacheKey {
|
|
8
|
+
workspaceRoot: string;
|
|
9
|
+
branch: string;
|
|
10
|
+
posture: RepoPosture;
|
|
11
|
+
riskClass: RiskClass;
|
|
12
|
+
fileBucket: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Cache entry tracking when a preflight was last satisfied.
|
|
16
|
+
*/
|
|
17
|
+
export interface CacheEntry {
|
|
18
|
+
satisfiedAt: number;
|
|
19
|
+
preflightType: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* In-memory cache tracking satisfied preflight checks per unique context.
|
|
23
|
+
*
|
|
24
|
+
* The cache is keyed by (workspaceRoot, branch, posture, riskClass, fileBucket)
|
|
25
|
+
* and tracks when each preflight was last satisfied. Entries expire after
|
|
26
|
+
* a configurable TTL or when context changes (branch switch, posture change,
|
|
27
|
+
* workspace change).
|
|
28
|
+
*
|
|
29
|
+
* v1: in-memory only, no disk persistence.
|
|
30
|
+
*/
|
|
31
|
+
export declare class GuidanceCache {
|
|
32
|
+
private entries;
|
|
33
|
+
private ttlMs;
|
|
34
|
+
private idleResetMs;
|
|
35
|
+
private lastTouchedAt;
|
|
36
|
+
constructor(ttlMs?: number, idleResetMs?: number);
|
|
37
|
+
setTtlMs(ttlMs: number): void;
|
|
38
|
+
setIdleResetMs(idleResetMs: number): void;
|
|
39
|
+
/**
|
|
40
|
+
* Check whether a preflight has been satisfied for the given key
|
|
41
|
+
* and is still within the TTL window.
|
|
42
|
+
*/
|
|
43
|
+
isSatisfied(key: CacheKey): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Record that a preflight has been satisfied for the given key.
|
|
46
|
+
*/
|
|
47
|
+
recordSatisfied(key: CacheKey, preflightType: string): void;
|
|
48
|
+
/**
|
|
49
|
+
* Invalidate all cache entries.
|
|
50
|
+
*/
|
|
51
|
+
invalidate(): void;
|
|
52
|
+
/**
|
|
53
|
+
* Invalidate all entries matching a specific posture.
|
|
54
|
+
*/
|
|
55
|
+
invalidateForPosture(posture: RepoPosture): void;
|
|
56
|
+
/**
|
|
57
|
+
* Invalidate all entries matching a specific branch.
|
|
58
|
+
*/
|
|
59
|
+
invalidateForBranch(branch: string): void;
|
|
60
|
+
/**
|
|
61
|
+
* Invalidate all entries matching a specific workspace root.
|
|
62
|
+
*/
|
|
63
|
+
invalidateForWorkspace(workspaceRoot: string): void;
|
|
64
|
+
/**
|
|
65
|
+
* Get the number of entries currently in the cache (including potentially expired).
|
|
66
|
+
*/
|
|
67
|
+
get size(): number;
|
|
68
|
+
/**
|
|
69
|
+
* Check whether a cache entry has expired based on the configured TTL.
|
|
70
|
+
*/
|
|
71
|
+
private isExpired;
|
|
72
|
+
private resetIfIdle;
|
|
73
|
+
}
|
|
74
|
+
export declare function getGuidanceCache(ttlMs?: number, idleResetMs?: number): GuidanceCache;
|
|
75
|
+
export declare function resetGuidanceCache(ttlMs?: number, idleResetMs?: number): void;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// implements REQ-opencode-smart-enforcement-v1, REQ-opencode-kibi-plugin-v1
|
|
2
|
+
/**
|
|
3
|
+
* Serializes a CacheKey into a deterministic string for use as a Map key.
|
|
4
|
+
*/
|
|
5
|
+
function serializeKey(key) {
|
|
6
|
+
return `${key.workspaceRoot}\0${key.branch}\0${key.posture}\0${key.riskClass}\0${key.fileBucket}`;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* In-memory cache tracking satisfied preflight checks per unique context.
|
|
10
|
+
*
|
|
11
|
+
* The cache is keyed by (workspaceRoot, branch, posture, riskClass, fileBucket)
|
|
12
|
+
* and tracks when each preflight was last satisfied. Entries expire after
|
|
13
|
+
* a configurable TTL or when context changes (branch switch, posture change,
|
|
14
|
+
* workspace change).
|
|
15
|
+
*
|
|
16
|
+
* v1: in-memory only, no disk persistence.
|
|
17
|
+
*/
|
|
18
|
+
export class GuidanceCache {
|
|
19
|
+
// implements REQ-opencode-kibi-plugin-v1
|
|
20
|
+
entries = new Map();
|
|
21
|
+
ttlMs;
|
|
22
|
+
idleResetMs;
|
|
23
|
+
lastTouchedAt;
|
|
24
|
+
constructor(ttlMs = 600000, idleResetMs = Number.POSITIVE_INFINITY) {
|
|
25
|
+
this.ttlMs = ttlMs;
|
|
26
|
+
this.idleResetMs = idleResetMs;
|
|
27
|
+
this.lastTouchedAt = Date.now();
|
|
28
|
+
}
|
|
29
|
+
setTtlMs(ttlMs) {
|
|
30
|
+
this.ttlMs = ttlMs;
|
|
31
|
+
}
|
|
32
|
+
setIdleResetMs(idleResetMs) {
|
|
33
|
+
this.idleResetMs = idleResetMs;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Check whether a preflight has been satisfied for the given key
|
|
37
|
+
* and is still within the TTL window.
|
|
38
|
+
*/
|
|
39
|
+
isSatisfied(key) {
|
|
40
|
+
this.resetIfIdle();
|
|
41
|
+
const serialized = serializeKey(key);
|
|
42
|
+
const entry = this.entries.get(serialized);
|
|
43
|
+
if (!entry)
|
|
44
|
+
return false;
|
|
45
|
+
this.lastTouchedAt = Date.now();
|
|
46
|
+
return !this.isExpired(entry);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Record that a preflight has been satisfied for the given key.
|
|
50
|
+
*/
|
|
51
|
+
recordSatisfied(key, preflightType) {
|
|
52
|
+
this.resetIfIdle();
|
|
53
|
+
const serialized = serializeKey(key);
|
|
54
|
+
this.entries.set(serialized, {
|
|
55
|
+
satisfiedAt: Date.now(),
|
|
56
|
+
preflightType,
|
|
57
|
+
});
|
|
58
|
+
this.lastTouchedAt = Date.now();
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Invalidate all cache entries.
|
|
62
|
+
*/
|
|
63
|
+
invalidate() {
|
|
64
|
+
this.entries.clear();
|
|
65
|
+
this.lastTouchedAt = Date.now();
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Invalidate all entries matching a specific posture.
|
|
69
|
+
*/
|
|
70
|
+
invalidateForPosture(posture) {
|
|
71
|
+
this.resetIfIdle();
|
|
72
|
+
for (const [serialized] of this.entries) {
|
|
73
|
+
// Key format: workspaceRoot\0branch\0posture\0riskClass\0fileBucket
|
|
74
|
+
const parts = serialized.split("\0");
|
|
75
|
+
if (parts[2] === posture) {
|
|
76
|
+
this.entries.delete(serialized);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
this.lastTouchedAt = Date.now();
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Invalidate all entries matching a specific branch.
|
|
83
|
+
*/
|
|
84
|
+
invalidateForBranch(branch) {
|
|
85
|
+
this.resetIfIdle();
|
|
86
|
+
for (const [serialized] of this.entries) {
|
|
87
|
+
const parts = serialized.split("\0");
|
|
88
|
+
if (parts[1] === branch) {
|
|
89
|
+
this.entries.delete(serialized);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
this.lastTouchedAt = Date.now();
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Invalidate all entries matching a specific workspace root.
|
|
96
|
+
*/
|
|
97
|
+
invalidateForWorkspace(workspaceRoot) {
|
|
98
|
+
this.resetIfIdle();
|
|
99
|
+
for (const [serialized] of this.entries) {
|
|
100
|
+
const parts = serialized.split("\0");
|
|
101
|
+
if (parts[0] === workspaceRoot) {
|
|
102
|
+
this.entries.delete(serialized);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
this.lastTouchedAt = Date.now();
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Get the number of entries currently in the cache (including potentially expired).
|
|
109
|
+
*/
|
|
110
|
+
get size() {
|
|
111
|
+
return this.entries.size;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Check whether a cache entry has expired based on the configured TTL.
|
|
115
|
+
*/
|
|
116
|
+
isExpired(entry) {
|
|
117
|
+
return Date.now() - entry.satisfiedAt > this.ttlMs;
|
|
118
|
+
}
|
|
119
|
+
resetIfIdle() {
|
|
120
|
+
const now = Date.now();
|
|
121
|
+
if (now - this.lastTouchedAt > this.idleResetMs) {
|
|
122
|
+
this.entries.clear();
|
|
123
|
+
}
|
|
124
|
+
this.lastTouchedAt = now;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// ── Singleton ───────────────────────────────────────────────────────
|
|
128
|
+
let globalCache = null;
|
|
129
|
+
export function getGuidanceCache(ttlMs, idleResetMs) {
|
|
130
|
+
// implements REQ-opencode-kibi-plugin-v1
|
|
131
|
+
if (!globalCache) {
|
|
132
|
+
globalCache = new GuidanceCache(ttlMs, idleResetMs);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
if (typeof ttlMs === "number")
|
|
136
|
+
globalCache.setTtlMs(ttlMs);
|
|
137
|
+
if (typeof idleResetMs === "number")
|
|
138
|
+
globalCache.setIdleResetMs(idleResetMs);
|
|
139
|
+
}
|
|
140
|
+
return globalCache;
|
|
141
|
+
}
|
|
142
|
+
export function resetGuidanceCache(ttlMs, idleResetMs) {
|
|
143
|
+
// implements REQ-opencode-kibi-plugin-v1
|
|
144
|
+
globalCache = new GuidanceCache(ttlMs, idleResetMs);
|
|
145
|
+
}
|