kibi-opencode 0.8.0 → 0.9.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 +5 -4
- package/dist/brief-intent.d.ts +30 -0
- package/dist/brief-intent.js +89 -0
- package/dist/briefing-runtime.d.ts +24 -0
- package/dist/briefing-runtime.js +276 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +50 -3
- package/dist/prompt.d.ts +3 -0
- package/dist/prompt.js +74 -18
- package/dist/startup-notifier.d.ts +3 -18
- package/dist/startup-notifier.js +3 -8
- package/dist/toast.d.ts +32 -0
- package/dist/toast.js +18 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -115,11 +115,12 @@ OpenCode exposes Kibi MCP prompts as slash commands. The \`/init-kibi\` command
|
|
|
115
115
|
|
|
116
116
|
### Start-Task Briefing
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
When the plugin detects an authoritative risky edit (`behavior_candidate` or `traceability_candidate` risk class), it automatically fetches a Kibi briefing from a background worker session via the `file.edited` event path. Auto-briefing is no longer deferred and provides immediate project context before you act.
|
|
119
119
|
|
|
120
|
-
-
|
|
121
|
-
- The
|
|
122
|
-
-
|
|
120
|
+
- **Automatic delivery**: Briefings appear in a toast notification and inside the guidance block headed `🧠 **Kibi briefing available**`.
|
|
121
|
+
- **Contextual richness**: The briefing includes a summary and key source-linked bullets generated by the `kb_briefing_generate` MCP tool.
|
|
122
|
+
- **TL;DR fallback**: If a full briefing is unavailable, a summary is provided with a cue to use the manual command.
|
|
123
|
+
- **Manual command**: Use `/brief-kibi` at any time to trigger an on-demand briefing if auto-delivery is skipped or fails.
|
|
123
124
|
|
|
124
125
|
### Discovery-first MCP guidance
|
|
125
126
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { RepoPosture } from "./repo-posture.js";
|
|
2
|
+
import type { RiskClass } from "./risk-classifier.js";
|
|
3
|
+
export interface BriefIntentParams {
|
|
4
|
+
riskClass: RiskClass;
|
|
5
|
+
posture: RepoPosture;
|
|
6
|
+
maintenanceDegraded: boolean;
|
|
7
|
+
workspaceRoot: string;
|
|
8
|
+
branch: string;
|
|
9
|
+
editedFilePath: string | undefined;
|
|
10
|
+
seedIds?: string[];
|
|
11
|
+
}
|
|
12
|
+
export interface BriefIntentResult {
|
|
13
|
+
eligible: boolean;
|
|
14
|
+
reason: string;
|
|
15
|
+
fingerprint: string;
|
|
16
|
+
sourceFiles: string[];
|
|
17
|
+
seedIds: string[];
|
|
18
|
+
}
|
|
19
|
+
export interface BriefIntentInputs {
|
|
20
|
+
riskClass: RiskClass;
|
|
21
|
+
posture: RepoPosture;
|
|
22
|
+
maintenanceDegraded: boolean;
|
|
23
|
+
worktreeRoot: string;
|
|
24
|
+
branch: string;
|
|
25
|
+
editedFile: string | undefined;
|
|
26
|
+
seedIds?: string[];
|
|
27
|
+
}
|
|
28
|
+
export declare function deriveBriefIntent(params: BriefIntentParams): BriefIntentResult;
|
|
29
|
+
export declare function computeBriefIntent(// implements REQ-opencode-kibi-briefing-v2
|
|
30
|
+
inputs: BriefIntentInputs): BriefIntentResult;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// implements REQ-opencode-kibi-briefing-v2, REQ-opencode-smart-enforcement-v1
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { getSourceLinkedRequirementIds } from "./source-linked-guidance.js";
|
|
4
|
+
const ELIGIBLE_RISK_CLASSES = new Set([
|
|
5
|
+
"behavior_candidate",
|
|
6
|
+
"traceability_candidate",
|
|
7
|
+
]);
|
|
8
|
+
const STRICT_ELIGIBLE_POSTURES = new Set([
|
|
9
|
+
"root_active",
|
|
10
|
+
"hybrid_root_plus_vendored",
|
|
11
|
+
]);
|
|
12
|
+
function hasEditedFilePath(editedFilePath) {
|
|
13
|
+
return typeof editedFilePath === "string" && editedFilePath.length > 0;
|
|
14
|
+
}
|
|
15
|
+
function deriveSeedIds(params) {
|
|
16
|
+
if (!hasEditedFilePath(params.editedFilePath)) {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
if (params.seedIds !== undefined) {
|
|
20
|
+
return params.seedIds.slice(0, 3);
|
|
21
|
+
}
|
|
22
|
+
const absoluteEditedPath = path.isAbsolute(params.editedFilePath)
|
|
23
|
+
? params.editedFilePath
|
|
24
|
+
: path.join(params.workspaceRoot, params.editedFilePath);
|
|
25
|
+
return getSourceLinkedRequirementIds(params.workspaceRoot, absoluteEditedPath).slice(0, 3);
|
|
26
|
+
}
|
|
27
|
+
// implements REQ-opencode-kibi-briefing-v2, REQ-opencode-smart-enforcement-v1
|
|
28
|
+
export function deriveBriefIntent(params) {
|
|
29
|
+
const fingerprint = `brief:${params.workspaceRoot}\0${params.branch}\0${params.editedFilePath ?? ""}\0${params.riskClass}`;
|
|
30
|
+
const sourceFiles = hasEditedFilePath(params.editedFilePath)
|
|
31
|
+
? [params.editedFilePath]
|
|
32
|
+
: [];
|
|
33
|
+
const seedIds = deriveSeedIds(params);
|
|
34
|
+
if (!hasEditedFilePath(params.editedFilePath)) {
|
|
35
|
+
return {
|
|
36
|
+
eligible: false,
|
|
37
|
+
reason: "Ineligible: edited file path is missing",
|
|
38
|
+
fingerprint,
|
|
39
|
+
sourceFiles,
|
|
40
|
+
seedIds,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (!ELIGIBLE_RISK_CLASSES.has(params.riskClass)) {
|
|
44
|
+
return {
|
|
45
|
+
eligible: false,
|
|
46
|
+
reason: `Ineligible: riskClass ${params.riskClass} is not auto-brief eligible`,
|
|
47
|
+
fingerprint,
|
|
48
|
+
sourceFiles,
|
|
49
|
+
seedIds,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (!STRICT_ELIGIBLE_POSTURES.has(params.posture)) {
|
|
53
|
+
return {
|
|
54
|
+
eligible: false,
|
|
55
|
+
reason: `Ineligible: posture ${params.posture} is not authoritative`,
|
|
56
|
+
fingerprint,
|
|
57
|
+
sourceFiles,
|
|
58
|
+
seedIds,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (params.maintenanceDegraded) {
|
|
62
|
+
return {
|
|
63
|
+
eligible: false,
|
|
64
|
+
reason: "Ineligible: maintenance is degraded",
|
|
65
|
+
fingerprint,
|
|
66
|
+
sourceFiles,
|
|
67
|
+
seedIds,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
eligible: true,
|
|
72
|
+
reason: "Eligible for auto-briefing",
|
|
73
|
+
fingerprint,
|
|
74
|
+
sourceFiles,
|
|
75
|
+
seedIds,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export function computeBriefIntent(// implements REQ-opencode-kibi-briefing-v2
|
|
79
|
+
inputs) {
|
|
80
|
+
return deriveBriefIntent({
|
|
81
|
+
riskClass: inputs.riskClass,
|
|
82
|
+
posture: inputs.posture,
|
|
83
|
+
maintenanceDegraded: inputs.maintenanceDegraded,
|
|
84
|
+
workspaceRoot: inputs.worktreeRoot,
|
|
85
|
+
branch: inputs.branch,
|
|
86
|
+
editedFilePath: inputs.editedFile,
|
|
87
|
+
...(inputs.seedIds !== undefined ? { seedIds: inputs.seedIds } : {}),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { BriefIntentResult } from "./brief-intent.js";
|
|
2
|
+
export type BriefingWorkspaceCtx = {
|
|
3
|
+
workspaceRoot: string;
|
|
4
|
+
branch: string;
|
|
5
|
+
directory?: string;
|
|
6
|
+
workspace?: string;
|
|
7
|
+
ttlMs?: number;
|
|
8
|
+
};
|
|
9
|
+
export type BriefingCitation = {
|
|
10
|
+
id: string;
|
|
11
|
+
type?: string;
|
|
12
|
+
title?: string;
|
|
13
|
+
source?: string;
|
|
14
|
+
textRef?: string;
|
|
15
|
+
};
|
|
16
|
+
export type BriefingRuntimeResult = {
|
|
17
|
+
state: "ready" | "tldr_fallback" | "no_briefing";
|
|
18
|
+
promptBlock: string;
|
|
19
|
+
tldr: string;
|
|
20
|
+
citations: BriefingCitation[];
|
|
21
|
+
showManualCue: boolean;
|
|
22
|
+
toastMessage: string;
|
|
23
|
+
};
|
|
24
|
+
export declare function fetchBriefingResult(client: unknown, workspaceCtx: BriefingWorkspaceCtx, intentResult: BriefIntentResult): Promise<BriefingRuntimeResult>;
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
// implements REQ-opencode-kibi-briefing-v2
|
|
2
|
+
const DEFAULT_TTL_MS = 300_000;
|
|
3
|
+
const WORKER_TITLE = "Kibi Auto Brief Worker";
|
|
4
|
+
const READY_TOAST = "Kibi brief ready — summary added to guidance.";
|
|
5
|
+
const TLDR_FALLBACK_TOAST = "Kibi brief summary added — use /brief-kibi for full details.";
|
|
6
|
+
const UNAVAILABLE_TOAST = "Kibi brief unavailable — keeping /brief-kibi manual path.";
|
|
7
|
+
const PROMPT_INSTRUCTION = "Call only kb_briefing_generate once with the provided sourceFiles and seedIds. If briefingState is ready, copy only cited fields. If briefingState is no_briefing, return empty promptBlock/citations and keep manual cue availability. Never invent claims.";
|
|
8
|
+
const PROMPT_FORMAT = {
|
|
9
|
+
type: "json_schema",
|
|
10
|
+
schema: {
|
|
11
|
+
type: "object",
|
|
12
|
+
properties: {
|
|
13
|
+
briefingState: { type: "string" },
|
|
14
|
+
tldr: { type: "string" },
|
|
15
|
+
promptBlock: { type: "string" },
|
|
16
|
+
citations: { type: "array", items: { type: "object" } },
|
|
17
|
+
activationState: { type: "string" },
|
|
18
|
+
confidence: { type: "string" },
|
|
19
|
+
freshness: { type: "string" },
|
|
20
|
+
},
|
|
21
|
+
required: ["briefingState"],
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
const workerSessionIds = new Map();
|
|
25
|
+
const workerSessionPromises = new Map();
|
|
26
|
+
const resultCache = new Map();
|
|
27
|
+
const inFlightResults = new Map();
|
|
28
|
+
function noBriefingResult() {
|
|
29
|
+
return {
|
|
30
|
+
state: "no_briefing",
|
|
31
|
+
promptBlock: "",
|
|
32
|
+
tldr: "",
|
|
33
|
+
citations: [],
|
|
34
|
+
showManualCue: true,
|
|
35
|
+
toastMessage: UNAVAILABLE_TOAST,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function workspaceSessionKey(workspaceCtx) {
|
|
39
|
+
return `${workspaceCtx.workspaceRoot}\0${workspaceCtx.branch}`;
|
|
40
|
+
}
|
|
41
|
+
function asRecord(value) {
|
|
42
|
+
return typeof value === "object" && value !== null
|
|
43
|
+
? value
|
|
44
|
+
: null;
|
|
45
|
+
}
|
|
46
|
+
function asString(value) {
|
|
47
|
+
return typeof value === "string" ? value : "";
|
|
48
|
+
}
|
|
49
|
+
function sanitizePromptBlock(value) {
|
|
50
|
+
return asString(value).trim();
|
|
51
|
+
}
|
|
52
|
+
function sanitizeCitations(value) {
|
|
53
|
+
if (!Array.isArray(value)) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
const citations = [];
|
|
57
|
+
for (const item of value) {
|
|
58
|
+
const record = asRecord(item);
|
|
59
|
+
if (!record) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const id = asString(record.id).trim();
|
|
63
|
+
if (!id) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const citation = { id };
|
|
67
|
+
const type = asString(record.type).trim();
|
|
68
|
+
const title = asString(record.title).trim();
|
|
69
|
+
const source = asString(record.source).trim();
|
|
70
|
+
const textRef = asString(record.textRef).trim();
|
|
71
|
+
if (type) {
|
|
72
|
+
citation.type = type;
|
|
73
|
+
}
|
|
74
|
+
if (title) {
|
|
75
|
+
citation.title = title;
|
|
76
|
+
}
|
|
77
|
+
if (source) {
|
|
78
|
+
citation.source = source;
|
|
79
|
+
}
|
|
80
|
+
if (textRef) {
|
|
81
|
+
citation.textRef = textRef;
|
|
82
|
+
}
|
|
83
|
+
citations.push(citation);
|
|
84
|
+
}
|
|
85
|
+
return citations;
|
|
86
|
+
}
|
|
87
|
+
function hasBriefingState(value) {
|
|
88
|
+
const record = asRecord(value);
|
|
89
|
+
return record !== null && "briefingState" in record;
|
|
90
|
+
}
|
|
91
|
+
function extractParts(response) {
|
|
92
|
+
const root = asRecord(response);
|
|
93
|
+
if (!root) {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
const data = asRecord(root.data);
|
|
97
|
+
const parts = data?.parts ?? root.parts;
|
|
98
|
+
return Array.isArray(parts) ? parts : [];
|
|
99
|
+
}
|
|
100
|
+
function parsePromptPayload(response) {
|
|
101
|
+
const parts = extractParts(response);
|
|
102
|
+
for (let index = parts.length - 1; index >= 0; index -= 1) {
|
|
103
|
+
const part = asRecord(parts[index]);
|
|
104
|
+
if (!part || part.type !== "text") {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const text = asString(part.text);
|
|
108
|
+
if (!text) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const parsed = JSON.parse(text);
|
|
113
|
+
if (hasBriefingState(parsed)) {
|
|
114
|
+
return parsed;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Ignore malformed text parts and continue searching from the end.
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
function normalizeResult(payload) {
|
|
124
|
+
if (!payload) {
|
|
125
|
+
return noBriefingResult();
|
|
126
|
+
}
|
|
127
|
+
const briefingState = asString(payload.briefingState).trim();
|
|
128
|
+
const tldr = asString(payload.tldr).trim();
|
|
129
|
+
const promptBlock = sanitizePromptBlock(payload.promptBlock);
|
|
130
|
+
if (briefingState === "ready" && promptBlock) {
|
|
131
|
+
return {
|
|
132
|
+
state: "ready",
|
|
133
|
+
promptBlock,
|
|
134
|
+
tldr,
|
|
135
|
+
citations: sanitizeCitations(payload.citations),
|
|
136
|
+
showManualCue: false,
|
|
137
|
+
toastMessage: READY_TOAST,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (briefingState === "ready" && tldr) {
|
|
141
|
+
return {
|
|
142
|
+
state: "tldr_fallback",
|
|
143
|
+
promptBlock: `- ${tldr}\n- Full details: run /brief-kibi.`,
|
|
144
|
+
tldr,
|
|
145
|
+
citations: [],
|
|
146
|
+
showManualCue: true,
|
|
147
|
+
toastMessage: TLDR_FALLBACK_TOAST,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return noBriefingResult();
|
|
151
|
+
}
|
|
152
|
+
function getSessionApi(client) {
|
|
153
|
+
const root = asRecord(client);
|
|
154
|
+
const session = asRecord(root?.session);
|
|
155
|
+
if (!session) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
const create = session.create;
|
|
159
|
+
const prompt = session.prompt;
|
|
160
|
+
if (typeof create !== "function" || typeof prompt !== "function") {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
create: create,
|
|
165
|
+
prompt: prompt,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function extractSessionId(response) {
|
|
169
|
+
const root = asRecord(response);
|
|
170
|
+
if (!root) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
const directId = asString(root.id).trim();
|
|
174
|
+
if (directId) {
|
|
175
|
+
return directId;
|
|
176
|
+
}
|
|
177
|
+
const data = asRecord(root.data);
|
|
178
|
+
const dataId = asString(data?.id).trim();
|
|
179
|
+
return dataId || null;
|
|
180
|
+
}
|
|
181
|
+
async function getWorkerSessionId(sessionApi, workspaceCtx) {
|
|
182
|
+
const key = workspaceSessionKey(workspaceCtx);
|
|
183
|
+
const existing = workerSessionIds.get(key);
|
|
184
|
+
if (existing) {
|
|
185
|
+
return existing;
|
|
186
|
+
}
|
|
187
|
+
const pending = workerSessionPromises.get(key);
|
|
188
|
+
if (pending) {
|
|
189
|
+
return pending;
|
|
190
|
+
}
|
|
191
|
+
const promise = (async () => {
|
|
192
|
+
const response = await sessionApi.create({
|
|
193
|
+
directory: workspaceCtx.workspaceRoot,
|
|
194
|
+
title: WORKER_TITLE,
|
|
195
|
+
});
|
|
196
|
+
const sessionId = extractSessionId(response);
|
|
197
|
+
if (!sessionId) {
|
|
198
|
+
throw new Error("Failed to resolve worker session ID");
|
|
199
|
+
}
|
|
200
|
+
workerSessionIds.set(key, sessionId);
|
|
201
|
+
return sessionId;
|
|
202
|
+
})().finally(() => {
|
|
203
|
+
workerSessionPromises.delete(key);
|
|
204
|
+
});
|
|
205
|
+
workerSessionPromises.set(key, promise);
|
|
206
|
+
return promise;
|
|
207
|
+
}
|
|
208
|
+
async function loadBriefingResult(sessionApi, workspaceCtx, intentResult) {
|
|
209
|
+
const sessionID = await getWorkerSessionId(sessionApi, workspaceCtx);
|
|
210
|
+
const response = await sessionApi.prompt({
|
|
211
|
+
sessionID,
|
|
212
|
+
tools: { kb_briefing_generate: true },
|
|
213
|
+
format: PROMPT_FORMAT,
|
|
214
|
+
parts: [
|
|
215
|
+
{
|
|
216
|
+
type: "text",
|
|
217
|
+
text: PROMPT_INSTRUCTION,
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
type: "text",
|
|
221
|
+
text: JSON.stringify({
|
|
222
|
+
sourceFiles: intentResult.sourceFiles,
|
|
223
|
+
seedIds: intentResult.seedIds,
|
|
224
|
+
}),
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
});
|
|
228
|
+
return normalizeResult(parsePromptPayload(response));
|
|
229
|
+
}
|
|
230
|
+
// implements REQ-opencode-kibi-briefing-v2
|
|
231
|
+
export async function fetchBriefingResult(client, workspaceCtx, intentResult) {
|
|
232
|
+
const ttlMs = workspaceCtx.ttlMs ?? DEFAULT_TTL_MS;
|
|
233
|
+
const cached = resultCache.get(intentResult.fingerprint);
|
|
234
|
+
const now = Date.now();
|
|
235
|
+
if (cached && now - cached.timestamp <= ttlMs) {
|
|
236
|
+
return cached.result;
|
|
237
|
+
}
|
|
238
|
+
if (cached) {
|
|
239
|
+
resultCache.delete(intentResult.fingerprint);
|
|
240
|
+
}
|
|
241
|
+
const pending = inFlightResults.get(intentResult.fingerprint);
|
|
242
|
+
if (pending) {
|
|
243
|
+
return pending;
|
|
244
|
+
}
|
|
245
|
+
const sessionApi = getSessionApi(client);
|
|
246
|
+
if (!sessionApi || !intentResult.eligible) {
|
|
247
|
+
const result = noBriefingResult();
|
|
248
|
+
resultCache.set(intentResult.fingerprint, {
|
|
249
|
+
result,
|
|
250
|
+
timestamp: now,
|
|
251
|
+
});
|
|
252
|
+
return result;
|
|
253
|
+
}
|
|
254
|
+
const promise = (async () => {
|
|
255
|
+
try {
|
|
256
|
+
const result = await loadBriefingResult(sessionApi, workspaceCtx, intentResult);
|
|
257
|
+
resultCache.set(intentResult.fingerprint, {
|
|
258
|
+
result,
|
|
259
|
+
timestamp: Date.now(),
|
|
260
|
+
});
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
const result = noBriefingResult();
|
|
265
|
+
resultCache.set(intentResult.fingerprint, {
|
|
266
|
+
result,
|
|
267
|
+
timestamp: Date.now(),
|
|
268
|
+
});
|
|
269
|
+
return result;
|
|
270
|
+
}
|
|
271
|
+
})().finally(() => {
|
|
272
|
+
inFlightResults.delete(intentResult.fingerprint);
|
|
273
|
+
});
|
|
274
|
+
inFlightResults.set(intentResult.fingerprint, promise);
|
|
275
|
+
return promise;
|
|
276
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
+
import { computeBriefIntent } from "./brief-intent.js";
|
|
3
|
+
import { fetchBriefingResult, } from "./briefing-runtime.js";
|
|
2
4
|
import { analyzeCodeFile, } from "./comment-analysis.js";
|
|
3
5
|
import * as fileFilter from "./file-filter.js";
|
|
4
6
|
import * as logger from "./logger.js";
|
|
@@ -9,6 +11,7 @@ import { classifyRisk } from "./risk-classifier.js";
|
|
|
9
11
|
import { getSessionTracker } from "./session-tracker.js";
|
|
10
12
|
import { notifyStartup } from "./startup-notifier.js";
|
|
11
13
|
import { runPluginStartup } from "./plugin-startup.js";
|
|
14
|
+
import { sendToast } from "./toast.js";
|
|
12
15
|
import * as fs from "node:fs";
|
|
13
16
|
function deriveFileBucket(kind) {
|
|
14
17
|
return kind;
|
|
@@ -65,7 +68,11 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
65
68
|
let hasRecentKbEdit = false;
|
|
66
69
|
let recentCommentSuggestion = null;
|
|
67
70
|
const seenFingerprints = new Set(); // For deduplication
|
|
71
|
+
const autoBriefResults = new Map();
|
|
72
|
+
const toastedFingerprints = new Set();
|
|
68
73
|
let lastRiskClass = null;
|
|
74
|
+
let lastEditedFilePath = null;
|
|
75
|
+
let lastBriefFingerprint = null;
|
|
69
76
|
let degradedWarnedOnce = false;
|
|
70
77
|
hooks.event = async ({ event }) => {
|
|
71
78
|
if (event.type !== "file.edited")
|
|
@@ -105,7 +112,10 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
105
112
|
const effectiveRiskClass = riskClass === "safe_docs_only" && precomputedSuggestion
|
|
106
113
|
? "traceability_candidate"
|
|
107
114
|
: riskClass;
|
|
115
|
+
const isAutoBriefRisk = effectiveRiskClass === "behavior_candidate" ||
|
|
116
|
+
effectiveRiskClass === "traceability_candidate";
|
|
108
117
|
lastRiskClass = effectiveRiskClass;
|
|
118
|
+
lastEditedFilePath = filePath;
|
|
109
119
|
logger.info("smart-enforcement.risk", {
|
|
110
120
|
event: "smart_enforcement_risk",
|
|
111
121
|
file: filePath,
|
|
@@ -216,7 +226,9 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
216
226
|
posture: posture.state,
|
|
217
227
|
posture_state: posture.state,
|
|
218
228
|
});
|
|
219
|
-
|
|
229
|
+
if (!isAutoBriefRisk) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
220
232
|
}
|
|
221
233
|
logger.info("smart-enforcement.cache", {
|
|
222
234
|
event: "smart_enforcement_cache",
|
|
@@ -312,8 +324,7 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
312
324
|
}
|
|
313
325
|
return;
|
|
314
326
|
}
|
|
315
|
-
if (
|
|
316
|
-
effectiveRiskClass === "traceability_candidate") {
|
|
327
|
+
if (isAutoBriefRisk) {
|
|
317
328
|
if (pathAnalysis.kind === "code" &&
|
|
318
329
|
cfg.guidance.commentDetection.enabled) {
|
|
319
330
|
const suggestion = precomputedSuggestion;
|
|
@@ -338,6 +349,38 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
338
349
|
else {
|
|
339
350
|
recentCommentSuggestion = null;
|
|
340
351
|
}
|
|
352
|
+
const intentResult = computeBriefIntent({
|
|
353
|
+
riskClass: effectiveRiskClass,
|
|
354
|
+
posture: posture.state,
|
|
355
|
+
maintenanceDegraded: getMaintenanceDegraded(),
|
|
356
|
+
editedFile: filePath,
|
|
357
|
+
worktreeRoot: input.worktree,
|
|
358
|
+
branch: currentBranch,
|
|
359
|
+
});
|
|
360
|
+
lastBriefFingerprint = intentResult.fingerprint;
|
|
361
|
+
if (intentResult.eligible &&
|
|
362
|
+
input.client &&
|
|
363
|
+
!getMaintenanceDegraded() &&
|
|
364
|
+
(posture.state === "root_active" ||
|
|
365
|
+
posture.state === "hybrid_root_plus_vendored")) {
|
|
366
|
+
const client = input.client;
|
|
367
|
+
const fingerprint = intentResult.fingerprint;
|
|
368
|
+
const workspaceCtx = {
|
|
369
|
+
workspaceRoot: input.worktree,
|
|
370
|
+
branch: currentBranch,
|
|
371
|
+
directory: input.directory,
|
|
372
|
+
...(input.workspace !== undefined ? { workspace: input.workspace } : {}),
|
|
373
|
+
};
|
|
374
|
+
void fetchBriefingResult(client, workspaceCtx, intentResult).then((result) => {
|
|
375
|
+
autoBriefResults.set(fingerprint, result);
|
|
376
|
+
if (!toastedFingerprints.has(fingerprint)) {
|
|
377
|
+
toastedFingerprints.add(fingerprint);
|
|
378
|
+
void sendToast(client, { message: result.toastMessage }).catch(() => {
|
|
379
|
+
// toast delivery failure is non-fatal
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
}
|
|
341
384
|
}
|
|
342
385
|
return;
|
|
343
386
|
};
|
|
@@ -353,6 +396,9 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
353
396
|
const showDegradedAdvisory = maintenanceDegraded &&
|
|
354
397
|
cfg.guidance.smartEnforcement.degradedMode === "warn-once" &&
|
|
355
398
|
!degradedWarnedOnce;
|
|
399
|
+
const autoBriefResult = lastBriefFingerprint != null
|
|
400
|
+
? autoBriefResults.get(lastBriefFingerprint)
|
|
401
|
+
: undefined;
|
|
356
402
|
// Build only the guidance block and append it; existing entries are preserved
|
|
357
403
|
const guidance = buildPrompt({
|
|
358
404
|
recentEdits,
|
|
@@ -367,6 +413,7 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
367
413
|
maintenanceDegraded,
|
|
368
414
|
degradedMode: cfg.guidance.smartEnforcement.degradedMode,
|
|
369
415
|
showDegradedAdvisory,
|
|
416
|
+
...(autoBriefResult !== undefined ? { autoBriefResult } : {}),
|
|
370
417
|
...(lastRiskClass != null ? { riskClass: lastRiskClass } : {}),
|
|
371
418
|
});
|
|
372
419
|
logger.info("smart-enforcement.guidance", {
|
package/dist/prompt.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CommentAnalysisResult } from "./comment-analysis.js";
|
|
2
|
+
import type { BriefingRuntimeResult } from "./briefing-runtime.js";
|
|
2
3
|
import type { KibiConfig } from "./config.js";
|
|
3
4
|
import type { GuidanceCache } from "./guidance-cache.js";
|
|
4
5
|
import type { PathKind } from "./path-kind.js";
|
|
@@ -32,6 +33,8 @@ export interface PromptContext {
|
|
|
32
33
|
degradedMode?: "warn-once" | "structured-only";
|
|
33
34
|
/** Whether to show the degraded advisory block this invocation */
|
|
34
35
|
showDegradedAdvisory?: boolean;
|
|
36
|
+
/** Stored auto-brief runtime result for the current fingerprint */
|
|
37
|
+
autoBriefResult?: BriefingRuntimeResult;
|
|
35
38
|
}
|
|
36
39
|
export declare function postureGuidance(posture: RepoPosture): string | null;
|
|
37
40
|
/**
|
package/dist/prompt.js
CHANGED
|
@@ -4,7 +4,9 @@ import { getSourceLinkedRequirementIds } from "./source-linked-guidance.js";
|
|
|
4
4
|
const SENTINEL = "<!-- kibi-opencode -->";
|
|
5
5
|
// ── Token budget enforcement ───────────────────────────────────────────
|
|
6
6
|
const MAX_BULLETS = 5;
|
|
7
|
+
const MAX_AUTO_BRIEF_BULLETS_WITH_REMINDER = 4;
|
|
7
8
|
const MAX_WORDS = 117; // Reserve 3 words for sentinel so total injected prompt stays ≤ 120
|
|
9
|
+
const AUTO_BRIEF_HEADER = "🧠 **Kibi briefing available**";
|
|
8
10
|
const AUTHORITATIVE_POSTURES = [
|
|
9
11
|
"root_active",
|
|
10
12
|
"hybrid_root_plus_vendored",
|
|
@@ -15,14 +17,14 @@ function countWords(text) {
|
|
|
15
17
|
function countBullets(lines) {
|
|
16
18
|
return lines.filter((l) => l.startsWith("-")).length;
|
|
17
19
|
}
|
|
18
|
-
function enforceBudget(block) {
|
|
20
|
+
function enforceBudget(block, maxBullets = MAX_BULLETS) {
|
|
19
21
|
const lines = block.split("\n");
|
|
20
|
-
if (countBullets(lines) >
|
|
21
|
-
// Trim to budget: keep header + first
|
|
22
|
+
if (countBullets(lines) > maxBullets || countWords(block) > MAX_WORDS) {
|
|
23
|
+
// Trim to budget: keep header + first maxBullets bullet lines
|
|
22
24
|
const header = [];
|
|
23
25
|
const bullets = [];
|
|
24
26
|
for (const line of lines) {
|
|
25
|
-
if (line.startsWith("-") && bullets.length <
|
|
27
|
+
if (line.startsWith("-") && bullets.length < maxBullets) {
|
|
26
28
|
bullets.push(line);
|
|
27
29
|
}
|
|
28
30
|
else if (!line.startsWith("-")) {
|
|
@@ -45,6 +47,34 @@ function insertBulletAfterHeader(block, bullet) {
|
|
|
45
47
|
return `${block}\n${bullet}`;
|
|
46
48
|
return `${block.slice(0, headerEnd + 1)}${bullet}\n${block.slice(headerEnd + 1)}`;
|
|
47
49
|
}
|
|
50
|
+
// implements REQ-opencode-kibi-briefing-v2
|
|
51
|
+
function buildAutoBriefingGuidance(autoBriefResult, completionReminder) {
|
|
52
|
+
if (!autoBriefResult)
|
|
53
|
+
return null;
|
|
54
|
+
if (autoBriefResult.state === "ready") {
|
|
55
|
+
const promptBlock = autoBriefResult.promptBlock.trim();
|
|
56
|
+
if (!promptBlock)
|
|
57
|
+
return null;
|
|
58
|
+
const maxBullets = completionReminder
|
|
59
|
+
? MAX_AUTO_BRIEF_BULLETS_WITH_REMINDER
|
|
60
|
+
: MAX_BULLETS;
|
|
61
|
+
const briefingLines = promptBlock
|
|
62
|
+
.split("\n")
|
|
63
|
+
.map((line) => line.trim())
|
|
64
|
+
.filter((line) => line.startsWith("-"))
|
|
65
|
+
.slice(0, maxBullets);
|
|
66
|
+
if (briefingLines.length === 0)
|
|
67
|
+
return null;
|
|
68
|
+
return `${AUTO_BRIEF_HEADER}\n${briefingLines.join("\n")}`;
|
|
69
|
+
}
|
|
70
|
+
if (autoBriefResult.state === "tldr_fallback") {
|
|
71
|
+
const promptBlock = autoBriefResult.promptBlock.trim();
|
|
72
|
+
if (!promptBlock)
|
|
73
|
+
return null;
|
|
74
|
+
return `${AUTO_BRIEF_HEADER}\n${promptBlock}`;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
48
78
|
function isAuthoritativePosture(posture) {
|
|
49
79
|
return AUTHORITATIVE_POSTURES.includes(posture);
|
|
50
80
|
}
|
|
@@ -116,6 +146,9 @@ Root .kb/config.json exists but some configured KB targets are missing. Guidance
|
|
|
116
146
|
function buildContextualGuidance(context) {
|
|
117
147
|
const posture = context.posture ?? "root_active";
|
|
118
148
|
const riskClass = context.riskClass;
|
|
149
|
+
const readyAutoBriefingAvailable = context.autoBriefResult?.showManualCue === false;
|
|
150
|
+
const suppressSourceLinkedBrief = context.autoBriefResult?.state === "ready" ||
|
|
151
|
+
context.autoBriefResult?.state === "tldr_fallback";
|
|
119
152
|
const showDegraded = context.showDegradedAdvisory === true &&
|
|
120
153
|
context.maintenanceDegraded === true &&
|
|
121
154
|
context.degradedMode === "warn-once";
|
|
@@ -171,16 +204,25 @@ Do not run \`kibi\` CLI commands directly; use public MCP tools (kb_autopilot_ge
|
|
|
171
204
|
if (riskClass &&
|
|
172
205
|
riskClass !== "safe_docs_only" &&
|
|
173
206
|
riskClass !== "safe_test_only") {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
207
|
+
const autoBriefBlock = riskClass === "behavior_candidate" ||
|
|
208
|
+
riskClass === "traceability_candidate"
|
|
209
|
+
? buildAutoBriefingGuidance(context.autoBriefResult, context.completionReminder === true)
|
|
210
|
+
: null;
|
|
211
|
+
if (autoBriefBlock) {
|
|
212
|
+
selectedBlock = autoBriefBlock;
|
|
179
213
|
}
|
|
180
214
|
else {
|
|
181
|
-
|
|
182
|
-
if (
|
|
183
|
-
|
|
215
|
+
// For behavior/traceability with comment suggestions, use suggestion guidance
|
|
216
|
+
if ((riskClass === "behavior_candidate" ||
|
|
217
|
+
riskClass === "traceability_candidate") &&
|
|
218
|
+
context.recentCommentSuggestion) {
|
|
219
|
+
selectedBlock = buildCommentSuggestionGuidance(context.recentCommentSuggestion);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
const block = GUIDANCE_BY_RISK[riskClass];
|
|
223
|
+
if (block)
|
|
224
|
+
selectedBlock = block;
|
|
225
|
+
}
|
|
184
226
|
}
|
|
185
227
|
}
|
|
186
228
|
// Priority 6: Legacy path-kind fallback (when no risk class)
|
|
@@ -229,7 +271,8 @@ If you're adding long explanatory comments, consider routing that knowledge to:
|
|
|
229
271
|
(riskClass === "behavior_candidate" ||
|
|
230
272
|
riskClass === "traceability_candidate") &&
|
|
231
273
|
isAuthoritativePosture(posture) &&
|
|
232
|
-
!context.maintenanceDegraded
|
|
274
|
+
!context.maintenanceDegraded &&
|
|
275
|
+
!readyAutoBriefingAvailable) {
|
|
233
276
|
selectedBlock = insertBulletAfterHeader(selectedBlock, "- Authoritative risky edit: run `/brief-kibi` before acting.");
|
|
234
277
|
}
|
|
235
278
|
// Source-linked micro-brief: insert after header line for code risk classes
|
|
@@ -239,7 +282,8 @@ If you're adding long explanatory comments, consider routing that knowledge to:
|
|
|
239
282
|
if (selectedBlock &&
|
|
240
283
|
(riskClass === "behavior_candidate" ||
|
|
241
284
|
riskClass === "traceability_candidate") &&
|
|
242
|
-
context.workspaceRoot
|
|
285
|
+
context.workspaceRoot &&
|
|
286
|
+
!suppressSourceLinkedBrief) {
|
|
243
287
|
try {
|
|
244
288
|
const lastEdit = context.recentEdits[context.recentEdits.length - 1];
|
|
245
289
|
if (lastEdit?.path) {
|
|
@@ -286,15 +330,27 @@ The Kibi workspace is in a maintenance-degraded state. Guidance remains advisory
|
|
|
286
330
|
};
|
|
287
331
|
context.cache.recordSatisfied(key, "guidance");
|
|
288
332
|
}
|
|
289
|
-
// Apply budget enforcement before appending the completion reminder so the
|
|
290
|
-
// reminder bullet is never silently trimmed when bullet count exceeds MAX_BULLETS.
|
|
291
|
-
const budgeted = selectedBlock ? enforceBudget(selectedBlock) : null;
|
|
292
|
-
// Append completion reminder for risky classes when enabled
|
|
293
333
|
const REMINDER_RISK_CLASSES = [
|
|
294
334
|
"behavior_candidate",
|
|
295
335
|
"traceability_candidate",
|
|
296
336
|
"req_policy_candidate",
|
|
297
337
|
];
|
|
338
|
+
const reminderWillBeAppended = !!selectedBlock &&
|
|
339
|
+
context.completionReminder === true &&
|
|
340
|
+
!context.maintenanceDegraded &&
|
|
341
|
+
riskClass != null &&
|
|
342
|
+
REMINDER_RISK_CLASSES.includes(riskClass) &&
|
|
343
|
+
posture !== "root_uninitialized" &&
|
|
344
|
+
posture !== "root_partial";
|
|
345
|
+
const effectiveMaxBullets = reminderWillBeAppended
|
|
346
|
+
? MAX_BULLETS - 1
|
|
347
|
+
: MAX_BULLETS;
|
|
348
|
+
// Apply budget enforcement before appending the completion reminder so the
|
|
349
|
+
// reminder bullet is never silently trimmed when bullet count exceeds MAX_BULLETS.
|
|
350
|
+
const budgeted = selectedBlock
|
|
351
|
+
? enforceBudget(selectedBlock, effectiveMaxBullets)
|
|
352
|
+
: null;
|
|
353
|
+
// Append completion reminder for risky classes when enabled
|
|
298
354
|
let finalBlock = budgeted;
|
|
299
355
|
if (finalBlock &&
|
|
300
356
|
context.completionReminder === true &&
|
|
@@ -1,21 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
message: string;
|
|
5
|
-
duration?: number;
|
|
6
|
-
};
|
|
7
|
-
export type StartupNotifierClient = {
|
|
8
|
-
tui?: {
|
|
9
|
-
showToast?: (payload: {
|
|
10
|
-
body: {
|
|
11
|
-
title?: string;
|
|
12
|
-
message: string;
|
|
13
|
-
variant?: "info" | "success" | "warning" | "error";
|
|
14
|
-
duration?: number;
|
|
15
|
-
};
|
|
16
|
-
}) => void | Promise<void>;
|
|
17
|
-
toast?: (payload: ToastPayload) => void | Promise<void>;
|
|
18
|
-
};
|
|
1
|
+
import { type ToastCapableClient } from "./toast.js";
|
|
2
|
+
export type { ToastPayload } from "./toast.js";
|
|
3
|
+
export type StartupNotifierClient = ToastCapableClient & {
|
|
19
4
|
app: {
|
|
20
5
|
log: (payload: Record<string, unknown>) => Promise<void>;
|
|
21
6
|
};
|
package/dist/startup-notifier.js
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
return typeof client.tui?.showToast === "function";
|
|
3
|
-
}
|
|
4
|
-
function hasLegacyToast(client) {
|
|
5
|
-
return typeof client.tui?.toast === "function";
|
|
6
|
-
}
|
|
1
|
+
import { hasLegacyToast, hasShowToast, sendToast, } from "./toast.js";
|
|
7
2
|
// implements REQ-opencode-kibi-plugin-v1
|
|
8
3
|
export function notifyStartup(client, cfg) {
|
|
9
4
|
const message = "kibi-opencode started";
|
|
@@ -15,7 +10,7 @@ export function notifyStartup(client, cfg) {
|
|
|
15
10
|
};
|
|
16
11
|
if (!cfg.suppressToast) {
|
|
17
12
|
if (hasShowToast(client)) {
|
|
18
|
-
void Promise.resolve(client
|
|
13
|
+
void Promise.resolve(sendToast(client, toastPayload))
|
|
19
14
|
.then((result) => void Promise.resolve(client.app.log({
|
|
20
15
|
body: {
|
|
21
16
|
service: "kibi-opencode",
|
|
@@ -43,7 +38,7 @@ export function notifyStartup(client, cfg) {
|
|
|
43
38
|
});
|
|
44
39
|
}
|
|
45
40
|
else if (hasLegacyToast(client)) {
|
|
46
|
-
void Promise.resolve(client
|
|
41
|
+
void Promise.resolve(sendToast(client, toastPayload)).catch((err) => {
|
|
47
42
|
console.error("[kibi-opencode] startup toast failed:", err);
|
|
48
43
|
});
|
|
49
44
|
}
|
package/dist/toast.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type ToastPayload = {
|
|
2
|
+
variant?: "info" | "success" | "warning" | "error";
|
|
3
|
+
title?: string;
|
|
4
|
+
message: string;
|
|
5
|
+
duration?: number;
|
|
6
|
+
};
|
|
7
|
+
type ShowToastPayload = {
|
|
8
|
+
body: ToastPayload;
|
|
9
|
+
};
|
|
10
|
+
type ShowToast = (payload: ShowToastPayload) => void | Promise<void>;
|
|
11
|
+
type LegacyToast = (payload: ToastPayload) => void | Promise<void>;
|
|
12
|
+
type ToastUi = {
|
|
13
|
+
showToast?: ShowToast;
|
|
14
|
+
toast?: LegacyToast;
|
|
15
|
+
};
|
|
16
|
+
export type ToastCapableClient = {
|
|
17
|
+
tui?: ToastUi;
|
|
18
|
+
};
|
|
19
|
+
type ClientWithShowToast = ToastCapableClient & {
|
|
20
|
+
tui: ToastUi & {
|
|
21
|
+
showToast: ShowToast;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
type ClientWithLegacyToast = ToastCapableClient & {
|
|
25
|
+
tui: ToastUi & {
|
|
26
|
+
toast: LegacyToast;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
export declare function hasShowToast(client: ToastCapableClient): client is ClientWithShowToast;
|
|
30
|
+
export declare function hasLegacyToast(client: ToastCapableClient): client is ClientWithLegacyToast;
|
|
31
|
+
export declare function sendToast(client: ToastCapableClient, payload: ToastPayload): Promise<void>;
|
|
32
|
+
export {};
|
package/dist/toast.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// implements REQ-opencode-kibi-plugin-v1
|
|
2
|
+
export function hasShowToast(client) {
|
|
3
|
+
return typeof client.tui?.showToast === "function";
|
|
4
|
+
}
|
|
5
|
+
// implements REQ-opencode-kibi-plugin-v1
|
|
6
|
+
export function hasLegacyToast(client) {
|
|
7
|
+
return typeof client.tui?.toast === "function";
|
|
8
|
+
}
|
|
9
|
+
// implements REQ-opencode-kibi-plugin-v1
|
|
10
|
+
export function sendToast(client, payload) {
|
|
11
|
+
if (hasShowToast(client)) {
|
|
12
|
+
return Promise.resolve(client.tui.showToast({ body: payload }));
|
|
13
|
+
}
|
|
14
|
+
if (hasLegacyToast(client)) {
|
|
15
|
+
return Promise.resolve(client.tui.toast(payload));
|
|
16
|
+
}
|
|
17
|
+
return Promise.resolve();
|
|
18
|
+
}
|