kibi-opencode 0.14.0 → 0.15.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 +14 -0
- package/dist/enforcement-policy.d.ts +2 -0
- package/dist/enforcement-policy.js +10 -0
- package/dist/kb-freshness-state.d.ts +62 -0
- package/dist/kb-freshness-state.js +211 -0
- package/dist/meaningful-change-classifier.d.ts +30 -0
- package/dist/meaningful-change-classifier.js +104 -0
- package/dist/plugin.js +73 -13
- package/dist/prompt.d.ts +5 -0
- package/dist/prompt.js +29 -2
- package/dist/startup-notifier.d.ts +7 -1
- package/dist/startup-notifier.js +51 -17
- package/dist/version-metadata.d.ts +13 -0
- package/dist/version-metadata.js +78 -0
- package/dist/version-metadata.json +1 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -33,6 +33,20 @@ The plugin now uses a posture-aware, low-token smart-enforcement model before em
|
|
|
33
33
|
- **Three-mode enforcement**: `advisory` by default keeps guidance non-blocking; opt-in `strict` adds elevated validation cues; `hard` blocks the prompt for authoritative workspaces until the checkpoint cycle succeeds. Git hooks and KB validation checks remain the durable enforcement boundary for all modes.
|
|
34
34
|
- **Structured observability**: posture, risk, cache, degraded-mode, targeted-check, and guidance events flow through structured plugin logs
|
|
35
35
|
|
|
36
|
+
### KB Freshness Contract
|
|
37
|
+
|
|
38
|
+
Meaningful code, test, requirement, scenario, and symbol changes now require explicit
|
|
39
|
+
KB impact resolution before task completion. When source or documentation files are
|
|
40
|
+
modified, the plugin evaluates KB freshness evidence and surfaces a visible
|
|
41
|
+
`🧠 **Kibi freshness required**` block when KB impact is unresolved.
|
|
42
|
+
|
|
43
|
+
**Allowed resolutions:**
|
|
44
|
+
- **KB updated**: Run source-linked discovery via `kb_search`, create or update entities
|
|
45
|
+
with `kb_upsert` (or remove with `kb_delete`), then run `kb_check`.
|
|
46
|
+
- **No KB impact**: Include a no-impact rationale in the final report after source-linked
|
|
47
|
+
discovery via `kb_search` or `kb_query(sourceFile=...)` and `kb_check`.
|
|
48
|
+
- **Deferred/Failed**: Do not claim task completion.
|
|
49
|
+
|
|
36
50
|
### Dynamic Contextual Guidance
|
|
37
51
|
|
|
38
52
|
The plugin provides context-aware prompt guidance based on recent edits and workspace state:
|
|
@@ -5,6 +5,7 @@ import type { RepoPosture } from "./repo-posture.js";
|
|
|
5
5
|
import type { RiskClass } from "./risk-classifier.js";
|
|
6
6
|
import type { EffectiveMode } from "./smart-enforcement.js";
|
|
7
7
|
import type { WorkContext } from "./work-context-resolver.js";
|
|
8
|
+
import type { KbFreshnessEvidence } from "./kb-freshness-state.js";
|
|
8
9
|
export interface PolicyLinkedEntityResult {
|
|
9
10
|
ids: string[];
|
|
10
11
|
source: "symbols" | "doc-path" | "none";
|
|
@@ -20,6 +21,7 @@ export type CheckpointEvidence = boolean | {
|
|
|
20
21
|
kbStatus?: boolean;
|
|
21
22
|
kbCheck?: boolean;
|
|
22
23
|
kbUpsert?: boolean;
|
|
24
|
+
freshness?: KbFreshnessEvidence;
|
|
23
25
|
};
|
|
24
26
|
export interface EnforcementPolicyInput {
|
|
25
27
|
/** Resolved work context for the current file/prompt cycle. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { evaluateKbFreshness } from "./kb-freshness-state.js";
|
|
1
2
|
const NO_E2E_SIGNAL = {
|
|
2
3
|
level: "none",
|
|
3
4
|
evidence: [],
|
|
@@ -31,6 +32,11 @@ function normalizeCheckpointEvidence(evidence) {
|
|
|
31
32
|
if (!evidence) {
|
|
32
33
|
return false;
|
|
33
34
|
}
|
|
35
|
+
// Freshness evidence is more authoritative than legacy booleans
|
|
36
|
+
if (evidence.freshness) {
|
|
37
|
+
return evaluateKbFreshness(evidence.freshness).allowsCompletion;
|
|
38
|
+
}
|
|
39
|
+
// Legacy boolean check
|
|
34
40
|
return (evidence.hasCheckpoint === true ||
|
|
35
41
|
evidence.kbSearch === true ||
|
|
36
42
|
evidence.sourceFileQuery === true ||
|
|
@@ -180,6 +186,10 @@ function hardBlockText(events) {
|
|
|
180
186
|
"- Run `kb_status` if branch or snapshot freshness matters.",
|
|
181
187
|
`- ${deletionCleanup}`,
|
|
182
188
|
"- Run `kb_check` before completing the task.",
|
|
189
|
+
"KB freshness resolution:",
|
|
190
|
+
"- **KB updated**: run `kb_search` for discovery, then `kb_upsert`/`kb_delete` for mutations, then `kb_check`.",
|
|
191
|
+
"- **No KB impact**: provide a no-impact rationale in your final report after source-linked discovery via `kb_search` or `kb_query(sourceFile=...)` and `kb_check`.",
|
|
192
|
+
"- **Deferred/failed**: do not claim task completion."
|
|
183
193
|
].join("\n"),
|
|
184
194
|
shownPaths,
|
|
185
195
|
remainingCount,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KB freshness state machine, evidence model, and in-memory evidence store.
|
|
3
|
+
*
|
|
4
|
+
* This is Wave 1 — the foundation module. No MCP tools, no filesystem or
|
|
5
|
+
* `.kb/` persistence, no entity mutation. Pure in-memory state.
|
|
6
|
+
*
|
|
7
|
+
* @module kb-freshness-state
|
|
8
|
+
*/
|
|
9
|
+
export type KbFreshnessDecision = "updated" | "no-impact" | "deferred" | "failed";
|
|
10
|
+
export type KbFreshnessStateKind = "clean" | "unknown-impact" | "evidence-required" | "updated" | "no-impact-accepted" | "deferred" | "check-failed";
|
|
11
|
+
export interface KbFreshnessEvidence {
|
|
12
|
+
sessionId?: string;
|
|
13
|
+
agentIdentity: string;
|
|
14
|
+
worktree: string;
|
|
15
|
+
branch: string;
|
|
16
|
+
fingerprint: string;
|
|
17
|
+
changedFiles: string[];
|
|
18
|
+
kbStatus: boolean;
|
|
19
|
+
sourceLinkedDiscovery: boolean;
|
|
20
|
+
kbMutation: boolean;
|
|
21
|
+
kbCheck: boolean;
|
|
22
|
+
decision?: KbFreshnessDecision;
|
|
23
|
+
rationale?: string;
|
|
24
|
+
checkRules?: string[];
|
|
25
|
+
}
|
|
26
|
+
export interface KbFreshnessEvaluation {
|
|
27
|
+
state: KbFreshnessStateKind;
|
|
28
|
+
requiresEvidence: boolean;
|
|
29
|
+
allowsCompletion: boolean;
|
|
30
|
+
reason: string;
|
|
31
|
+
missingEvidence: string[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Evaluate KB freshness from collected evidence.
|
|
35
|
+
*
|
|
36
|
+
* Rules (in priority order):
|
|
37
|
+
* 1. No changed files → `clean`, allows completion.
|
|
38
|
+
* 2. Changed files but no decision → `evidence-required`, blocks completion.
|
|
39
|
+
* 3. `updated` → requires `kbMutation` + `kbCheck`.
|
|
40
|
+
* 4. `no-impact` → requires `rationale` + `sourceLinkedDiscovery` + `kbCheck`.
|
|
41
|
+
* 5. `deferred` → blocks completion.
|
|
42
|
+
* 6. `failed` → `check-failed`, blocks completion.
|
|
43
|
+
*/
|
|
44
|
+
export declare function evaluateKbFreshness(evidence: KbFreshnessEvidence): KbFreshnessEvaluation;
|
|
45
|
+
export interface KbFreshnessScope {
|
|
46
|
+
sessionId?: string;
|
|
47
|
+
agentIdentity: string;
|
|
48
|
+
worktree: string;
|
|
49
|
+
branch: string;
|
|
50
|
+
fingerprint: string;
|
|
51
|
+
}
|
|
52
|
+
export interface KbFreshnessEvidenceStore {
|
|
53
|
+
recordToolEvidence(scope: KbFreshnessScope, toolName: string): void;
|
|
54
|
+
recordNoImpact(scope: KbFreshnessScope, rationale: string): void;
|
|
55
|
+
getEvidence(scope: KbFreshnessScope, changedFiles: string[]): KbFreshnessEvidence;
|
|
56
|
+
resetScope(scope: KbFreshnessScope): void;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Create an in-memory evidence store scoped by
|
|
60
|
+
* (sessionId | agentIdentity | worktree | branch | fingerprint).
|
|
61
|
+
*/
|
|
62
|
+
export declare function createKbFreshnessEvidenceStore(): KbFreshnessEvidenceStore;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KB freshness state machine, evidence model, and in-memory evidence store.
|
|
3
|
+
*
|
|
4
|
+
* This is Wave 1 — the foundation module. No MCP tools, no filesystem or
|
|
5
|
+
* `.kb/` persistence, no entity mutation. Pure in-memory state.
|
|
6
|
+
*
|
|
7
|
+
* @module kb-freshness-state
|
|
8
|
+
*/
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Evaluation
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
/**
|
|
13
|
+
* Evaluate KB freshness from collected evidence.
|
|
14
|
+
*
|
|
15
|
+
* Rules (in priority order):
|
|
16
|
+
* 1. No changed files → `clean`, allows completion.
|
|
17
|
+
* 2. Changed files but no decision → `evidence-required`, blocks completion.
|
|
18
|
+
* 3. `updated` → requires `kbMutation` + `kbCheck`.
|
|
19
|
+
* 4. `no-impact` → requires `rationale` + `sourceLinkedDiscovery` + `kbCheck`.
|
|
20
|
+
* 5. `deferred` → blocks completion.
|
|
21
|
+
* 6. `failed` → `check-failed`, blocks completion.
|
|
22
|
+
*/
|
|
23
|
+
export function evaluateKbFreshness(evidence) {
|
|
24
|
+
// Rule 1: No changed files
|
|
25
|
+
if (evidence.changedFiles.length === 0) {
|
|
26
|
+
return {
|
|
27
|
+
state: "clean",
|
|
28
|
+
requiresEvidence: false,
|
|
29
|
+
allowsCompletion: true,
|
|
30
|
+
reason: "No files changed",
|
|
31
|
+
missingEvidence: [],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
// Rule 2: No decision recorded yet
|
|
35
|
+
if (evidence.decision === undefined) {
|
|
36
|
+
return {
|
|
37
|
+
state: "evidence-required",
|
|
38
|
+
requiresEvidence: true,
|
|
39
|
+
allowsCompletion: false,
|
|
40
|
+
reason: "Session modified files without KB tool evidence",
|
|
41
|
+
missingEvidence: [],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
// Rule 5: Deferred
|
|
45
|
+
if (evidence.decision === "deferred") {
|
|
46
|
+
return {
|
|
47
|
+
state: "deferred",
|
|
48
|
+
requiresEvidence: false,
|
|
49
|
+
allowsCompletion: false,
|
|
50
|
+
reason: "KB freshness check was explicitly deferred",
|
|
51
|
+
missingEvidence: [],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
// Rule 6: Failed
|
|
55
|
+
if (evidence.decision === "failed") {
|
|
56
|
+
return {
|
|
57
|
+
state: "check-failed",
|
|
58
|
+
requiresEvidence: false,
|
|
59
|
+
allowsCompletion: false,
|
|
60
|
+
reason: "KB freshness check failed",
|
|
61
|
+
missingEvidence: [],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
// Rule 3: Updated
|
|
65
|
+
if (evidence.decision === "updated") {
|
|
66
|
+
const missingEvidence = [];
|
|
67
|
+
if (!evidence.kbMutation) {
|
|
68
|
+
missingEvidence.push("kbMutation");
|
|
69
|
+
}
|
|
70
|
+
if (!evidence.kbCheck) {
|
|
71
|
+
missingEvidence.push("kbCheck");
|
|
72
|
+
}
|
|
73
|
+
if (missingEvidence.length > 0) {
|
|
74
|
+
return {
|
|
75
|
+
state: "evidence-required",
|
|
76
|
+
requiresEvidence: true,
|
|
77
|
+
allowsCompletion: false,
|
|
78
|
+
reason: "KB was updated but required evidence (kbMutation, kbCheck) is missing",
|
|
79
|
+
missingEvidence,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
state: "updated",
|
|
84
|
+
requiresEvidence: false,
|
|
85
|
+
allowsCompletion: true,
|
|
86
|
+
reason: "KB updated and verified via kb_check",
|
|
87
|
+
missingEvidence: [],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// Rule 4: No-impact
|
|
91
|
+
if (evidence.decision === "no-impact") {
|
|
92
|
+
const missingEvidence = [];
|
|
93
|
+
if (!evidence.rationale || evidence.rationale === "") {
|
|
94
|
+
missingEvidence.push("rationale");
|
|
95
|
+
}
|
|
96
|
+
if (!evidence.sourceLinkedDiscovery) {
|
|
97
|
+
missingEvidence.push("sourceLinkedDiscovery");
|
|
98
|
+
}
|
|
99
|
+
if (!evidence.kbCheck) {
|
|
100
|
+
missingEvidence.push("kbCheck");
|
|
101
|
+
}
|
|
102
|
+
if (missingEvidence.length > 0) {
|
|
103
|
+
return {
|
|
104
|
+
state: "evidence-required",
|
|
105
|
+
requiresEvidence: true,
|
|
106
|
+
allowsCompletion: false,
|
|
107
|
+
reason: "No-impact claim needs rationale, sourceLinkedDiscovery, and kbCheck",
|
|
108
|
+
missingEvidence,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
state: "no-impact-accepted",
|
|
113
|
+
requiresEvidence: false,
|
|
114
|
+
allowsCompletion: true,
|
|
115
|
+
reason: "No-impact accepted with rationale and KB check passed",
|
|
116
|
+
missingEvidence: [],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// Exhaustive guard — all decision branches handled above
|
|
120
|
+
const _exhaustive = evidence.decision;
|
|
121
|
+
throw new Error(`Unexpected decision: ${_exhaustive}`);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Map of tool names → evidence side effects.
|
|
125
|
+
* Each entry lists the evidence fields to set when that tool is used.
|
|
126
|
+
*/
|
|
127
|
+
const TOOL_EFFECTS = {
|
|
128
|
+
kb_status: ["kbStatus"],
|
|
129
|
+
kb_search: ["sourceLinkedDiscovery"],
|
|
130
|
+
kb_query: ["sourceLinkedDiscovery"],
|
|
131
|
+
kb_graph: ["sourceLinkedDiscovery"],
|
|
132
|
+
kb_find_gaps: ["sourceLinkedDiscovery"],
|
|
133
|
+
kb_coverage: ["sourceLinkedDiscovery"],
|
|
134
|
+
kb_upsert: ["kbMutation"],
|
|
135
|
+
kb_delete: ["kbMutation"],
|
|
136
|
+
kb_check: ["kbCheck"],
|
|
137
|
+
};
|
|
138
|
+
function buildScopeKey(scope) {
|
|
139
|
+
const parts = [
|
|
140
|
+
scope.sessionId ?? "",
|
|
141
|
+
scope.agentIdentity,
|
|
142
|
+
scope.worktree,
|
|
143
|
+
scope.branch,
|
|
144
|
+
scope.fingerprint,
|
|
145
|
+
];
|
|
146
|
+
return parts.join("|");
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Create an in-memory evidence store scoped by
|
|
150
|
+
* (sessionId | agentIdentity | worktree | branch | fingerprint).
|
|
151
|
+
*/
|
|
152
|
+
export function createKbFreshnessEvidenceStore() {
|
|
153
|
+
const store = new Map();
|
|
154
|
+
function getOrCreate(scope) {
|
|
155
|
+
const key = buildScopeKey(scope);
|
|
156
|
+
if (!store.has(key)) {
|
|
157
|
+
store.set(key, {
|
|
158
|
+
kbStatus: false,
|
|
159
|
+
sourceLinkedDiscovery: false,
|
|
160
|
+
kbMutation: false,
|
|
161
|
+
kbCheck: false,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return store.get(key);
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
recordToolEvidence(scope, toolName) {
|
|
168
|
+
const record = getOrCreate(scope);
|
|
169
|
+
const effects = TOOL_EFFECTS[toolName];
|
|
170
|
+
if (!effects) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
for (const field of effects) {
|
|
174
|
+
if (field === "kbMutation") {
|
|
175
|
+
record.kbMutation = true;
|
|
176
|
+
record.decision = "updated";
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
record[field] = true;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
recordNoImpact(scope, rationale) {
|
|
184
|
+
const record = getOrCreate(scope);
|
|
185
|
+
record.decision = "no-impact";
|
|
186
|
+
record.rationale = rationale;
|
|
187
|
+
},
|
|
188
|
+
getEvidence(scope, changedFiles) {
|
|
189
|
+
const record = getOrCreate(scope);
|
|
190
|
+
return {
|
|
191
|
+
...(scope.sessionId ? { sessionId: scope.sessionId } : {}),
|
|
192
|
+
agentIdentity: scope.agentIdentity,
|
|
193
|
+
worktree: scope.worktree,
|
|
194
|
+
branch: scope.branch,
|
|
195
|
+
fingerprint: scope.fingerprint,
|
|
196
|
+
changedFiles,
|
|
197
|
+
kbStatus: record.kbStatus,
|
|
198
|
+
sourceLinkedDiscovery: record.sourceLinkedDiscovery,
|
|
199
|
+
kbMutation: record.kbMutation,
|
|
200
|
+
kbCheck: record.kbCheck,
|
|
201
|
+
...(record.decision !== undefined ? { decision: record.decision } : {}),
|
|
202
|
+
...(record.rationale !== undefined ? { rationale: record.rationale } : {}),
|
|
203
|
+
...(record.checkRules !== undefined ? { checkRules: record.checkRules } : {}),
|
|
204
|
+
};
|
|
205
|
+
},
|
|
206
|
+
resetScope(scope) {
|
|
207
|
+
const key = buildScopeKey(scope);
|
|
208
|
+
store.delete(key);
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { PathKind } from "./path-kind.js";
|
|
2
|
+
import type { FileLifecycle } from "./file-operation-state.js";
|
|
3
|
+
import type { RiskClass } from "./risk-classifier.js";
|
|
4
|
+
/**
|
|
5
|
+
* Classification for whether a file change requires KB freshness evidence.
|
|
6
|
+
*
|
|
7
|
+
* - "requires-kb-evidence": The change is meaningful for KB freshness — the KB
|
|
8
|
+
* snapshot should be considered stale and needs a re-sync.
|
|
9
|
+
* - "advisory": The change is low-risk (lockfiles, docs-only) — advisory only.
|
|
10
|
+
* - "ignored": The change is in a build artifact or third-party path — no KB
|
|
11
|
+
* impact.
|
|
12
|
+
*/
|
|
13
|
+
export type MeaningfulChangeClass = "requires-kb-evidence" | "advisory" | "ignored";
|
|
14
|
+
/**
|
|
15
|
+
* Classify whether a file change requires KB freshness evidence.
|
|
16
|
+
*
|
|
17
|
+
* Rules (first match wins):
|
|
18
|
+
* 1. Ignored paths (build artifacts, vendor dirs) → `ignored`
|
|
19
|
+
* 2. Advisory paths (lockfiles) → `advisory`
|
|
20
|
+
* 3. Meaningful pathKinds (code, requirements, KB docs, etc.) → `requires-kb-evidence`
|
|
21
|
+
* 4. Risk-based upgrade (behavior/traceability/req_policy candidates) → `requires-kb-evidence`
|
|
22
|
+
* 5. `safe_docs_only` risk + `unknown` pathKind → `advisory`
|
|
23
|
+
* 6. Default → `requires-kb-evidence` (safety default)
|
|
24
|
+
*/
|
|
25
|
+
export declare function classifyMeaningfulChange(params: {
|
|
26
|
+
normalizedPath: string;
|
|
27
|
+
pathKind: PathKind;
|
|
28
|
+
lifecycle: FileLifecycle;
|
|
29
|
+
riskClass?: RiskClass;
|
|
30
|
+
}): MeaningfulChangeClass;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Directories whose contents are always ignored for KB freshness.
|
|
2
|
+
const IGNORED_DIRECTORIES = [
|
|
3
|
+
".git",
|
|
4
|
+
".sisyphus",
|
|
5
|
+
"node_modules",
|
|
6
|
+
"vendor",
|
|
7
|
+
"third_party",
|
|
8
|
+
"dist",
|
|
9
|
+
"coverage",
|
|
10
|
+
];
|
|
11
|
+
// Filenames that are advisory-only (lockfiles).
|
|
12
|
+
const ADVISORY_FILENAMES = new Set([
|
|
13
|
+
"bun.lock",
|
|
14
|
+
"bun.lockb",
|
|
15
|
+
"package-lock.json",
|
|
16
|
+
"pnpm-lock.yaml",
|
|
17
|
+
"yarn.lock",
|
|
18
|
+
]);
|
|
19
|
+
// PathKind values that always require KB evidence.
|
|
20
|
+
const MEANINGFUL_PATH_KINDS = new Set([
|
|
21
|
+
"code",
|
|
22
|
+
"requirement",
|
|
23
|
+
"scenario",
|
|
24
|
+
"test",
|
|
25
|
+
"adr",
|
|
26
|
+
"fact",
|
|
27
|
+
"flag",
|
|
28
|
+
"event",
|
|
29
|
+
"symbol",
|
|
30
|
+
"kb",
|
|
31
|
+
]);
|
|
32
|
+
// Risk classes that upgrade the classification to requires-kb-evidence.
|
|
33
|
+
const UPGRADE_RISK_CLASSES = new Set([
|
|
34
|
+
"behavior_candidate",
|
|
35
|
+
"traceability_candidate",
|
|
36
|
+
"req_policy_candidate",
|
|
37
|
+
]);
|
|
38
|
+
function isIgnoredPath(normalizedPath, pathKind) {
|
|
39
|
+
// Check extension-based ignored patterns.
|
|
40
|
+
if (normalizedPath.endsWith(".tsbuildinfo") ||
|
|
41
|
+
normalizedPath.endsWith(".map")) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
// Check directory-based ignored patterns.
|
|
45
|
+
for (const dir of IGNORED_DIRECTORIES) {
|
|
46
|
+
if (normalizedPath.startsWith(`${dir}/`) ||
|
|
47
|
+
normalizedPath.includes(`/${dir}/`)) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Special handling for .kb/ — only ignored when pathKind is NOT "kb".
|
|
52
|
+
// Actual KB entity files (pathKind=kb) should still require evidence.
|
|
53
|
+
if (pathKind !== "kb") {
|
|
54
|
+
if (normalizedPath.startsWith(".kb/") ||
|
|
55
|
+
normalizedPath.includes("/.kb/")) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
function getFilename(normalizedPath) {
|
|
62
|
+
const lastSlash = normalizedPath.lastIndexOf("/");
|
|
63
|
+
return lastSlash === -1
|
|
64
|
+
? normalizedPath
|
|
65
|
+
: normalizedPath.slice(lastSlash + 1);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Classify whether a file change requires KB freshness evidence.
|
|
69
|
+
*
|
|
70
|
+
* Rules (first match wins):
|
|
71
|
+
* 1. Ignored paths (build artifacts, vendor dirs) → `ignored`
|
|
72
|
+
* 2. Advisory paths (lockfiles) → `advisory`
|
|
73
|
+
* 3. Meaningful pathKinds (code, requirements, KB docs, etc.) → `requires-kb-evidence`
|
|
74
|
+
* 4. Risk-based upgrade (behavior/traceability/req_policy candidates) → `requires-kb-evidence`
|
|
75
|
+
* 5. `safe_docs_only` risk + `unknown` pathKind → `advisory`
|
|
76
|
+
* 6. Default → `requires-kb-evidence` (safety default)
|
|
77
|
+
*/
|
|
78
|
+
export function classifyMeaningfulChange(params) {
|
|
79
|
+
const { normalizedPath, pathKind, lifecycle: _lifecycle, riskClass } = params;
|
|
80
|
+
// 1. IGNORED paths (for freshness contract)
|
|
81
|
+
if (isIgnoredPath(normalizedPath, pathKind)) {
|
|
82
|
+
return "ignored";
|
|
83
|
+
}
|
|
84
|
+
// 2. ADVISORY paths (low-risk lockfiles)
|
|
85
|
+
const filename = getFilename(normalizedPath);
|
|
86
|
+
if (ADVISORY_FILENAMES.has(filename)) {
|
|
87
|
+
return "advisory";
|
|
88
|
+
}
|
|
89
|
+
// 3. REQUIRES-KB-EVIDENCE by pathKind
|
|
90
|
+
if (MEANINGFUL_PATH_KINDS.has(pathKind)) {
|
|
91
|
+
return "requires-kb-evidence";
|
|
92
|
+
}
|
|
93
|
+
// 4. RISK-BASED UPGRADE
|
|
94
|
+
if (riskClass) {
|
|
95
|
+
if (UPGRADE_RISK_CLASSES.has(riskClass)) {
|
|
96
|
+
return "requires-kb-evidence";
|
|
97
|
+
}
|
|
98
|
+
if (riskClass === "safe_docs_only" && pathKind === "unknown") {
|
|
99
|
+
return "advisory";
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// 5. DEFAULT — safety default for unknown paths
|
|
103
|
+
return "requires-kb-evidence";
|
|
104
|
+
}
|
package/dist/plugin.js
CHANGED
|
@@ -27,8 +27,11 @@ import { createSessionEditState, } from "./session-edit-state.js";
|
|
|
27
27
|
import { syncSessionBaselineState, } from "./session-fingerprint.js";
|
|
28
28
|
import { getSessionTracker } from "./session-tracker.js";
|
|
29
29
|
import { notifyStartup, } from "./startup-notifier.js";
|
|
30
|
+
import { readKibiPackageVersions } from "./version-metadata.js";
|
|
30
31
|
import { sendToast, } from "./toast.js";
|
|
31
32
|
import { announceBriefTui, } from "./tui-brief-delivery.js";
|
|
33
|
+
import { createKbFreshnessEvidenceStore, evaluateKbFreshness, } from "./kb-freshness-state.js";
|
|
34
|
+
import { classifyMeaningfulChange } from "./meaningful-change-classifier.js";
|
|
32
35
|
import { deletePendingBriefMarkers, loadPendingBriefMarkers, } from "./utils/brief-marker.js";
|
|
33
36
|
import { resolveWorkContext, } from "./work-context-resolver.js";
|
|
34
37
|
import * as fs from "node:fs";
|
|
@@ -48,17 +51,6 @@ function resolveIdleBriefDeliveryDelayMs(worktree) {
|
|
|
48
51
|
return 0;
|
|
49
52
|
return Math.min(60_000, Math.trunc(configValue));
|
|
50
53
|
}
|
|
51
|
-
function readKibiOpencodePackageVersion() {
|
|
52
|
-
try {
|
|
53
|
-
const packageJson = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
54
|
-
return typeof packageJson.version === "string"
|
|
55
|
-
? packageJson.version
|
|
56
|
-
: undefined;
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
return undefined;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
54
|
const startupNotifyGlobals = globalThis;
|
|
63
55
|
/**
|
|
64
56
|
* Lint requirement documents for embedded scenarios/tests and oversized content.
|
|
@@ -160,6 +152,7 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
160
152
|
const fileOperationStateRegistry = new Map();
|
|
161
153
|
const checkpointRunnerRegistry = new Map();
|
|
162
154
|
const pathKindCacheRegistry = new Map();
|
|
155
|
+
const freshnessStore = createKbFreshnessEvidenceStore();
|
|
163
156
|
function resolveScopedWorkContext(filePath) {
|
|
164
157
|
return resolveWorkContext({
|
|
165
158
|
inputDirectory: input.directory,
|
|
@@ -482,6 +475,37 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
482
475
|
hooks.event = async ({ event }) => {
|
|
483
476
|
const activeBranch = resolveCurrentBranch(input.worktree);
|
|
484
477
|
syncSessionBaseline(activeBranch);
|
|
478
|
+
// Observe KB tool execution events for freshness evidence (best-effort)
|
|
479
|
+
const TOOL_EVENT_TYPES = new Set([
|
|
480
|
+
"tool.execute.after", "tool.executed", "tool.call.completed",
|
|
481
|
+
"tool.Execute.after", "tool.Call.completed",
|
|
482
|
+
]);
|
|
483
|
+
if (TOOL_EVENT_TYPES.has(event.type)) {
|
|
484
|
+
const props = event.properties ?? {};
|
|
485
|
+
const toolName = (props.tool ?? props.toolName ?? props.name ??
|
|
486
|
+
props.call?.name ??
|
|
487
|
+
props.input?.tool);
|
|
488
|
+
if (typeof toolName === "string" && toolName.startsWith("kb_")) {
|
|
489
|
+
const scope = {
|
|
490
|
+
...(input.sessionId !== undefined ? { sessionId: input.sessionId } : {}),
|
|
491
|
+
agentIdentity: input.agentIdentity ?? "unknown",
|
|
492
|
+
worktree: input.worktree,
|
|
493
|
+
branch: activeBranch,
|
|
494
|
+
fingerprint: `${input.sessionId ?? ""}-${activeBranch}`,
|
|
495
|
+
};
|
|
496
|
+
try {
|
|
497
|
+
freshnessStore.recordToolEvidence(scope, toolName);
|
|
498
|
+
logger.info("kb-freshness.tool-evidence", {
|
|
499
|
+
event: "kb_freshness_tool_evidence",
|
|
500
|
+
tool: toolName,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
catch {
|
|
504
|
+
// best-effort, never crash the event handler
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return; // tool events don't need file lifecycle processing
|
|
508
|
+
}
|
|
485
509
|
// Handle session.idle for idle-brief generation. OpenCode can emit idle
|
|
486
510
|
// while an assistant is between tool calls, so debounce until the work
|
|
487
511
|
// burst settles before generating/delivering a brief.
|
|
@@ -1195,6 +1219,32 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
1195
1219
|
}
|
|
1196
1220
|
}
|
|
1197
1221
|
}
|
|
1222
|
+
// Build freshness evidence for the current dirty fingerprint
|
|
1223
|
+
let freshnessEval;
|
|
1224
|
+
const freshnessFingerprint = `${promptWorkContext.sessionId ?? ""}-${promptWorkContext.branch}`;
|
|
1225
|
+
const freshnessScope = {
|
|
1226
|
+
...(promptWorkContext.sessionId !== undefined ? { sessionId: promptWorkContext.sessionId } : {}),
|
|
1227
|
+
agentIdentity: promptWorkContext.agentIdentity,
|
|
1228
|
+
worktree: promptWorkContext.worktreeRoot,
|
|
1229
|
+
branch: promptWorkContext.branch,
|
|
1230
|
+
fingerprint: freshnessFingerprint,
|
|
1231
|
+
};
|
|
1232
|
+
if (transformFocusFilePath) {
|
|
1233
|
+
const normalizedFocusPathForFreshness = promptFileOperationState.normalizePath(transformFocusFilePath);
|
|
1234
|
+
const focusPathKindForFreshness = promptPathKindCache.get(normalizedFocusPathForFreshness) ?? "unknown";
|
|
1235
|
+
const pendingLifecycleForFreshness = promptFileOperationState.peekPending(normalizedFocusPathForFreshness);
|
|
1236
|
+
const effectiveRiskClassForFreshness = effectiveRiskClass ?? "safe_docs_only";
|
|
1237
|
+
const meaningfulChange = classifyMeaningfulChange({
|
|
1238
|
+
normalizedPath: normalizedFocusPathForFreshness,
|
|
1239
|
+
pathKind: focusPathKindForFreshness,
|
|
1240
|
+
lifecycle: pendingLifecycleForFreshness?.lifecycle ?? "edited",
|
|
1241
|
+
riskClass: effectiveRiskClassForFreshness,
|
|
1242
|
+
});
|
|
1243
|
+
if (meaningfulChange === "requires-kb-evidence") {
|
|
1244
|
+
const freshnessEvidence = freshnessStore.getEvidence(freshnessScope, [normalizedFocusPathForFreshness]);
|
|
1245
|
+
freshnessEval = evaluateKbFreshness(freshnessEvidence);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1198
1248
|
const guidance = buildPrompt({
|
|
1199
1249
|
recentEdits: transformRecentEdits,
|
|
1200
1250
|
focusEdit: transformPromptFocusEdit,
|
|
@@ -1217,6 +1267,10 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
1217
1267
|
? { fileOperationReminder }
|
|
1218
1268
|
: {}),
|
|
1219
1269
|
...(hardGateBlock !== undefined ? { hardGateBlock } : {}),
|
|
1270
|
+
...(freshnessEval ? { kbFreshness: freshnessEval } : {}),
|
|
1271
|
+
...(freshnessEval && transformSessionEdits.length > 0
|
|
1272
|
+
? { freshnessChangedPaths: [...new Set(transformSessionEdits.map(e => e.filePath))].slice(0, 5) }
|
|
1273
|
+
: {}),
|
|
1220
1274
|
});
|
|
1221
1275
|
logger.info("smart-enforcement.guidance", {
|
|
1222
1276
|
event: "smart_enforcement_guidance",
|
|
@@ -1347,11 +1401,17 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
1347
1401
|
setTimeout(callback, delayMs);
|
|
1348
1402
|
});
|
|
1349
1403
|
scheduleStartupNotify(() => {
|
|
1350
|
-
const
|
|
1404
|
+
const meta = readKibiPackageVersions();
|
|
1405
|
+
const versions = {};
|
|
1406
|
+
for (const key of ["opencode", "mcp", "cli", "core"]) {
|
|
1407
|
+
if (meta[key] !== "unknown")
|
|
1408
|
+
versions[key] = meta[key];
|
|
1409
|
+
}
|
|
1351
1410
|
notifyStartup(makeStartupClient(client), {
|
|
1352
1411
|
suppressToast: cfg.ux.toastStartup === false,
|
|
1353
1412
|
directory: input.directory,
|
|
1354
|
-
...(
|
|
1413
|
+
...(Object.keys(versions).length > 0 ? { versions } : {}),
|
|
1414
|
+
versionMetadataSource: meta.source,
|
|
1355
1415
|
});
|
|
1356
1416
|
}, 2000);
|
|
1357
1417
|
}
|
package/dist/prompt.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { PathKind } from "./path-kind.js";
|
|
|
7
7
|
import type { RepoPosture } from "./repo-posture.js";
|
|
8
8
|
import type { RiskClass } from "./risk-classifier.js";
|
|
9
9
|
import type { WorkspaceHealth } from "./workspace-health.js";
|
|
10
|
+
import type { KbFreshnessEvaluation } from "./kb-freshness-state.js";
|
|
10
11
|
declare const SENTINEL = "<!-- kibi-opencode -->";
|
|
11
12
|
export declare function buildAutoBriefingGuidance(autoBriefResult: BriefingRuntimeResult | undefined, completionReminder: boolean): string | null;
|
|
12
13
|
export interface PromptContext {
|
|
@@ -53,6 +54,10 @@ export interface PromptContext {
|
|
|
53
54
|
remainingCount: number;
|
|
54
55
|
reason?: string;
|
|
55
56
|
};
|
|
57
|
+
/** KB freshness evaluation from evidence store */
|
|
58
|
+
kbFreshness?: KbFreshnessEvaluation;
|
|
59
|
+
/** Paths changed for freshness evaluation (capped at 5) */
|
|
60
|
+
freshnessChangedPaths?: string[];
|
|
56
61
|
}
|
|
57
62
|
export declare function postureGuidance(posture: RepoPosture, capability?: InitKibiCommandCapability): string | null;
|
|
58
63
|
/**
|
package/dist/prompt.js
CHANGED
|
@@ -110,7 +110,7 @@ function buildBootstrapRequiredBody(capability = getInitKibiCommandCapability())
|
|
|
110
110
|
const commandBullet = capability.supported
|
|
111
111
|
? "- When the Kibi OpenCode plugin is active and native injection is supported, use `/init-kibi` as the canonical short alias; `/kibi:init-kibi:mcp` remains the namespaced MCP fallback."
|
|
112
112
|
: "- This host does not support native `/init-kibi` injection. Kibi must fail closed and does not register a fake native alias; use `/kibi:init-kibi:mcp` instead.";
|
|
113
|
-
return `This repository does not appear to have Kibi initialized. Agents should:\n${commandBullet}\n- The workflow uses \`kb_autopilot_generate\` for read-only synthesis; always preview and get approval before writes.\n- Ask the user/operator to run setup or repair outside this session if bootstrap is insufficient.\n\nUse public MCP tools only: \`kb_autopilot_generate\`, \`kb_search\`, \`kb_query\`, \`kb_status\`, \`kb_find_gaps\`, \`kb_coverage\`, \`kb_graph\`, \`kb_upsert\`, \`kb_delete\`, \`kb_check\`.`;
|
|
113
|
+
return `This repository does not appear to have Kibi initialized. Agents should:\n${commandBullet}\n- The workflow uses \`kb_autopilot_generate\` for read-only synthesis; always preview and get approval before writes.\n- Ask the user/operator to run setup or repair outside this session if bootstrap is insufficient.\n- For comprehensive Kibi usage guidance, use \`kb_skills_load\` with skill id \`kibi-usage\`.\n\nUse public MCP tools only: \`kb_autopilot_generate\`, \`kb_search\`, \`kb_query\`, \`kb_status\`, \`kb_find_gaps\`, \`kb_coverage\`, \`kb_graph\`, \`kb_upsert\`, \`kb_delete\`, \`kb_check\`, \`kb_skills_load\`.`;
|
|
114
114
|
}
|
|
115
115
|
function buildHardGateBlock(block) {
|
|
116
116
|
const pathLines = block.shownPaths.map((path) => `- \`${path}\``);
|
|
@@ -240,6 +240,7 @@ ${buildBootstrapRequiredBody(capability)}`;
|
|
|
240
240
|
context.cache &&
|
|
241
241
|
context.workspaceRoot &&
|
|
242
242
|
context.branch &&
|
|
243
|
+
!context.kbFreshness?.requiresEvidence &&
|
|
243
244
|
riskClass) {
|
|
244
245
|
const focusEdit = getFocusEdit(context);
|
|
245
246
|
const key = {
|
|
@@ -455,6 +456,26 @@ The Kibi workspace is in a maintenance-degraded state. Guidance remains advisory
|
|
|
455
456
|
posture !== "root_partial") {
|
|
456
457
|
finalBlock = `${finalBlock}\n- Kibi impact evidence is required before completion/commit: run \`kb_check\` before completing this task.`;
|
|
457
458
|
}
|
|
459
|
+
// Prepend KB freshness block when evidence is required
|
|
460
|
+
if (context.kbFreshness?.requiresEvidence) {
|
|
461
|
+
const changedListing = context.freshnessChangedPaths?.length
|
|
462
|
+
? `Changed: ${context.freshnessChangedPaths.slice(0, 5).map(p => `\`${p}\``).join(", ")}`
|
|
463
|
+
: null;
|
|
464
|
+
const freshnessLines = [
|
|
465
|
+
"🧠 **Kibi freshness required**",
|
|
466
|
+
changedListing,
|
|
467
|
+
`State: ${context.kbFreshness.state}. ${context.kbFreshness.reason}`,
|
|
468
|
+
...(context.kbFreshness.missingEvidence.length > 0
|
|
469
|
+
? [`Missing: ${context.kbFreshness.missingEvidence.join(", ")}`]
|
|
470
|
+
: []),
|
|
471
|
+
"Resolution:",
|
|
472
|
+
"- **KB updated**: run `kb_search` for discovery, then `kb_upsert`/`kb_delete` for mutations, then `kb_check`.",
|
|
473
|
+
"- **No KB impact**: include a no-impact rationale in your final report after source-linked discovery via `kb_search`/`kb_query(sourceFile=...)` and `kb_check`.",
|
|
474
|
+
].filter(Boolean).join("\n");
|
|
475
|
+
finalBlock = finalBlock
|
|
476
|
+
? `${freshnessLines}\n\n${finalBlock}`
|
|
477
|
+
: freshnessLines;
|
|
478
|
+
}
|
|
458
479
|
// Return: sentinel + one targeted block (or just sentinel if no block)
|
|
459
480
|
return finalBlock
|
|
460
481
|
? `${SENTINEL}\n\n${finalBlock}`
|
|
@@ -531,7 +552,13 @@ Dogfood note for this repo: OpenCode here uses local built \`kibi-mcp\` and \`ki
|
|
|
531
552
|
6. **Validate**: Run kb_check after KB mutations to catch violations early.
|
|
532
553
|
7. **Before completion/commit**: Kibi impact evidence is required before completion/commit. If extraction output changes, refresh documentation/symbols.yaml and do not revert that update as scope creep.
|
|
533
554
|
|
|
534
|
-
**Public Kibi tools only:** kb_autopilot_generate, kb_search, kb_query, kb_status, kb_find_gaps, kb_coverage, kb_graph, kb_upsert, kb_delete, kb_check
|
|
555
|
+
**Public Kibi tools only:** kb_autopilot_generate, kb_search, kb_query, kb_status, kb_find_gaps, kb_coverage, kb_graph, kb_upsert, kb_delete, kb_check, kb_skills_load.
|
|
556
|
+
|
|
557
|
+
For comprehensive Kibi usage guidance (relationships, fact lanes, workflows), use \`kb_skills_load\` with skill id \`kibi-usage\`.
|
|
558
|
+
|
|
559
|
+
Do not invoke Kibi CLI commands directly from the agent.
|
|
560
|
+
|
|
561
|
+
${buildInitKibiBootstrapReference(capability)}`;
|
|
535
562
|
}
|
|
536
563
|
/**
|
|
537
564
|
* Build prompt with contextual guidance based on posture, risk class, and cache state.
|
|
@@ -6,8 +6,14 @@ export type StartupNotifierClient = ToastCapableClient & {
|
|
|
6
6
|
};
|
|
7
7
|
};
|
|
8
8
|
export type StartupNotifierConfig = {
|
|
9
|
-
|
|
9
|
+
versions?: {
|
|
10
|
+
opencode?: string;
|
|
11
|
+
mcp?: string;
|
|
12
|
+
cli?: string;
|
|
13
|
+
core?: string;
|
|
14
|
+
};
|
|
10
15
|
suppressToast?: boolean;
|
|
11
16
|
directory?: string;
|
|
17
|
+
versionMetadataSource?: string;
|
|
12
18
|
};
|
|
13
19
|
export declare function notifyStartup(client: StartupNotifierClient, cfg: StartupNotifierConfig): void;
|
package/dist/startup-notifier.js
CHANGED
|
@@ -1,10 +1,30 @@
|
|
|
1
1
|
import { sendToast, } from "./toast.js";
|
|
2
|
+
const VERSION_ORDER = ["opencode", "mcp", "cli", "core"];
|
|
2
3
|
// implements REQ-opencode-kibi-plugin-v1
|
|
3
4
|
export function notifyStartup(client, cfg) {
|
|
4
5
|
const message = "kibi-opencode started";
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
// Build toast message from known versions in order
|
|
7
|
+
const versionParts = [];
|
|
8
|
+
if (cfg.versions) {
|
|
9
|
+
for (const key of VERSION_ORDER) {
|
|
10
|
+
const val = cfg.versions[key];
|
|
11
|
+
if (val !== undefined) {
|
|
12
|
+
versionParts.push(`${key} v${val}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const displayMessage = versionParts.length > 0
|
|
17
|
+
? `${message} (${versionParts.join(", ")})`
|
|
7
18
|
: message;
|
|
19
|
+
// Compute unknown versions (keys from VERSION_ORDER not present in cfg.versions)
|
|
20
|
+
const unknownVersions = [];
|
|
21
|
+
if (cfg.versions) {
|
|
22
|
+
for (const key of VERSION_ORDER) {
|
|
23
|
+
if (cfg.versions[key] === undefined) {
|
|
24
|
+
unknownVersions.push(key);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
8
28
|
const toastPayload = {
|
|
9
29
|
variant: "success",
|
|
10
30
|
title: "Kibi OpenCode",
|
|
@@ -18,31 +38,36 @@ export function notifyStartup(client, cfg) {
|
|
|
18
38
|
...(cfg.directory ? { directory: cfg.directory } : {}),
|
|
19
39
|
};
|
|
20
40
|
if (result.status === "delivered") {
|
|
21
|
-
void client.app
|
|
41
|
+
void client.app
|
|
42
|
+
.log({
|
|
22
43
|
body: {
|
|
23
44
|
...base,
|
|
24
45
|
level: "info",
|
|
25
46
|
message: "startup toast delivered",
|
|
26
47
|
transport: result.transport,
|
|
27
48
|
},
|
|
28
|
-
})
|
|
49
|
+
})
|
|
50
|
+
.catch(() => {
|
|
29
51
|
// Advisory log failure stays silent
|
|
30
52
|
});
|
|
31
53
|
}
|
|
32
54
|
else if (result.status === "unavailable") {
|
|
33
|
-
void client.app
|
|
55
|
+
void client.app
|
|
56
|
+
.log({
|
|
34
57
|
body: {
|
|
35
58
|
...base,
|
|
36
59
|
level: "info",
|
|
37
60
|
message: "startup toast unavailable",
|
|
38
61
|
reason: result.reason,
|
|
39
62
|
},
|
|
40
|
-
})
|
|
63
|
+
})
|
|
64
|
+
.catch(() => {
|
|
41
65
|
// Advisory log failure stays silent
|
|
42
66
|
});
|
|
43
67
|
}
|
|
44
68
|
else if (result.status === "failed") {
|
|
45
|
-
void client.app
|
|
69
|
+
void client.app
|
|
70
|
+
.log({
|
|
46
71
|
body: {
|
|
47
72
|
...base,
|
|
48
73
|
level: "warn",
|
|
@@ -51,21 +76,30 @@ export function notifyStartup(client, cfg) {
|
|
|
51
76
|
reason: result.reason,
|
|
52
77
|
...(result.error ? { error: result.error } : {}),
|
|
53
78
|
},
|
|
54
|
-
})
|
|
79
|
+
})
|
|
80
|
+
.catch(() => {
|
|
55
81
|
// Advisory log failure stays silent
|
|
56
82
|
});
|
|
57
83
|
}
|
|
58
84
|
});
|
|
59
85
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
86
|
+
// Structured startup log
|
|
87
|
+
const logBody = {
|
|
88
|
+
service: "kibi-opencode",
|
|
89
|
+
level: "info",
|
|
90
|
+
message,
|
|
91
|
+
};
|
|
92
|
+
if (cfg.versions && Object.keys(cfg.versions).length > 0) {
|
|
93
|
+
logBody.versions = cfg.versions;
|
|
94
|
+
logBody.unknownVersions = unknownVersions;
|
|
95
|
+
}
|
|
96
|
+
if (cfg.versionMetadataSource) {
|
|
97
|
+
logBody.versionMetadataSource = cfg.versionMetadataSource;
|
|
98
|
+
}
|
|
99
|
+
if (cfg.directory) {
|
|
100
|
+
logBody.directory = cfg.directory;
|
|
101
|
+
}
|
|
102
|
+
void Promise.resolve(client.app.log({ body: logBody })).catch(() => {
|
|
69
103
|
// Advisory log failure stays silent
|
|
70
104
|
});
|
|
71
105
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface KibiPackageVersions {
|
|
2
|
+
opencode: string;
|
|
3
|
+
mcp: string;
|
|
4
|
+
cli: string;
|
|
5
|
+
core: string;
|
|
6
|
+
source: "generated-dist" | "workspace-packages" | "unknown";
|
|
7
|
+
missing: string[];
|
|
8
|
+
}
|
|
9
|
+
export interface ReadKibiPackageVersionsOptions {
|
|
10
|
+
baseUrl?: string | URL;
|
|
11
|
+
readFileSync?: (path: string | URL, encoding: "utf-8") => string;
|
|
12
|
+
}
|
|
13
|
+
export declare function readKibiPackageVersions(options?: ReadKibiPackageVersionsOptions): KibiPackageVersions;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { readFileSync as fsReadFileSync } from "node:fs";
|
|
2
|
+
// implements REQ-opencode-toast-package-versions-v1
|
|
3
|
+
export function readKibiPackageVersions(options) {
|
|
4
|
+
const baseUrl = options?.baseUrl ?? import.meta.url;
|
|
5
|
+
const readFs = options?.readFileSync ?? fsReadFileSync;
|
|
6
|
+
// Tier 1: generated dist JSON (build-time artifact)
|
|
7
|
+
try {
|
|
8
|
+
const distUrl = new URL("./version-metadata.json", baseUrl);
|
|
9
|
+
const raw = readFs(distUrl, "utf-8");
|
|
10
|
+
const parsed = JSON.parse(raw);
|
|
11
|
+
if (typeof parsed.opencode === "string" &&
|
|
12
|
+
typeof parsed.mcp === "string" &&
|
|
13
|
+
typeof parsed.cli === "string" &&
|
|
14
|
+
typeof parsed.core === "string") {
|
|
15
|
+
return {
|
|
16
|
+
opencode: parsed.opencode,
|
|
17
|
+
mcp: parsed.mcp,
|
|
18
|
+
cli: parsed.cli,
|
|
19
|
+
core: parsed.core,
|
|
20
|
+
source: "generated-dist",
|
|
21
|
+
missing: [],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// fall through
|
|
27
|
+
}
|
|
28
|
+
// Tier 2: workspace package.json files
|
|
29
|
+
try {
|
|
30
|
+
const entries = [
|
|
31
|
+
["opencode", "../package.json"],
|
|
32
|
+
["mcp", "../../mcp/package.json"],
|
|
33
|
+
["cli", "../../cli/package.json"],
|
|
34
|
+
["core", "../../core/package.json"],
|
|
35
|
+
];
|
|
36
|
+
const versions = {};
|
|
37
|
+
const missing = [];
|
|
38
|
+
for (const [name, relPath] of entries) {
|
|
39
|
+
try {
|
|
40
|
+
const pkgUrl = new URL(relPath, baseUrl);
|
|
41
|
+
const raw = readFs(pkgUrl, "utf-8");
|
|
42
|
+
const pkg = JSON.parse(raw);
|
|
43
|
+
versions[name] =
|
|
44
|
+
typeof pkg.version === "string" ? pkg.version : "unknown";
|
|
45
|
+
if (typeof pkg.version !== "string") {
|
|
46
|
+
missing.push(name);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
versions[name] = "unknown";
|
|
51
|
+
missing.push(name);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Only claim workspace-packages if at least one resolved
|
|
55
|
+
if (Object.values(versions).some((v) => v !== "unknown")) {
|
|
56
|
+
return {
|
|
57
|
+
opencode: versions.opencode ?? "unknown",
|
|
58
|
+
mcp: versions.mcp ?? "unknown",
|
|
59
|
+
cli: versions.cli ?? "unknown",
|
|
60
|
+
core: versions.core ?? "unknown",
|
|
61
|
+
source: "workspace-packages",
|
|
62
|
+
missing: missing.length > 0 ? missing : [],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// fall through
|
|
68
|
+
}
|
|
69
|
+
// Tier 3: all unknown — never throws
|
|
70
|
+
return {
|
|
71
|
+
opencode: "unknown",
|
|
72
|
+
mcp: "unknown",
|
|
73
|
+
cli: "unknown",
|
|
74
|
+
core: "unknown",
|
|
75
|
+
source: "unknown",
|
|
76
|
+
missing: ["opencode", "mcp", "cli", "core"],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"opencode":"0.15.0","mcp":"0.14.2","cli":"0.11.1","core":"0.5.3"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kibi-opencode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.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",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"access": "public"
|
|
53
53
|
},
|
|
54
54
|
"scripts": {
|
|
55
|
-
"build": "tsc -p tsconfig.json && bun run scripts/build-tui.ts && tsc -p tsconfig.tui-types.json",
|
|
55
|
+
"build": "tsc -p tsconfig.json && bun run scripts/generate-version-metadata.ts && bun run scripts/build-tui.ts && tsc -p tsconfig.tui-types.json",
|
|
56
56
|
"build:tui": "bun run scripts/build-tui.ts && tsc -p tsconfig.tui-types.json",
|
|
57
57
|
"dev": "tsc -p tsconfig.json --watch",
|
|
58
58
|
"clean": "rm -rf dist",
|
|
@@ -62,10 +62,10 @@
|
|
|
62
62
|
"@opencode-ai/plugin": "^1.4.7",
|
|
63
63
|
"@opentui/core": "^0.1.99",
|
|
64
64
|
"@opentui/solid": "^0.1.99",
|
|
65
|
-
"kibi-cli": "^0.11.
|
|
65
|
+
"kibi-cli": "^0.11.1"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@types/node": "latest",
|
|
69
69
|
"typescript": "^5.0.0"
|
|
70
70
|
}
|
|
71
|
-
}
|
|
71
|
+
}
|