kibi-opencode 0.8.0 → 0.10.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 +37 -12
- package/dist/brief-intent.d.ts +41 -0
- package/dist/brief-intent.js +127 -0
- package/dist/briefing-runtime.d.ts +24 -0
- package/dist/briefing-runtime.js +277 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +9 -0
- package/dist/e2e-coverage-signals.d.ts +6 -0
- package/dist/e2e-coverage-signals.js +186 -0
- package/dist/file-entity-links.d.ts +15 -0
- package/dist/file-entity-links.js +254 -0
- package/dist/file-operation-reminders.d.ts +24 -0
- package/dist/file-operation-reminders.js +55 -0
- package/dist/file-operation-state.d.ts +29 -0
- package/dist/file-operation-state.js +113 -0
- package/dist/idle-brief-audit.d.ts +36 -0
- package/dist/idle-brief-audit.js +186 -0
- package/dist/idle-brief-paths.d.ts +6 -0
- package/dist/idle-brief-paths.js +120 -0
- package/dist/idle-brief-reader.d.ts +25 -0
- package/dist/idle-brief-reader.js +142 -0
- package/dist/idle-brief-runtime.d.ts +48 -0
- package/dist/idle-brief-runtime.js +443 -0
- package/dist/idle-brief-store.d.ts +96 -0
- package/dist/idle-brief-store.js +209 -0
- package/dist/index.d.ts +15 -1
- package/dist/index.js +645 -22
- package/dist/init-kibi-alias.d.ts +14 -0
- package/dist/init-kibi-alias.js +38 -0
- package/dist/init-kibi-capability.d.ts +32 -0
- package/dist/init-kibi-capability.js +202 -0
- package/dist/logger.js +9 -3
- package/dist/plugin-startup.d.ts +1 -0
- package/dist/plugin-startup.js +11 -2
- package/dist/prompt.d.ts +18 -3
- package/dist/prompt.js +176 -50
- package/dist/reconcile-engine.d.ts +15 -0
- package/dist/reconcile-engine.js +112 -0
- package/dist/scheduler.d.ts +1 -0
- package/dist/scheduler.js +37 -1
- package/dist/session-edit-state.d.ts +25 -0
- package/dist/session-edit-state.js +177 -0
- package/dist/session-fingerprint.d.ts +11 -0
- package/dist/session-fingerprint.js +21 -0
- package/dist/source-linked-guidance.d.ts +1 -2
- package/dist/source-linked-guidance.js +5 -168
- package/dist/startup-notifier.d.ts +3 -18
- package/dist/startup-notifier.js +42 -36
- package/dist/toast.d.ts +31 -0
- package/dist/toast.js +40 -0
- package/dist/tui-brief-delivery.d.ts +47 -0
- package/dist/tui-brief-delivery.js +138 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -113,13 +113,12 @@ The plugin injects guidance into OpenCode sessions to improve agent grounding. U
|
|
|
113
113
|
|
|
114
114
|
OpenCode exposes Kibi MCP prompts as slash commands. The \`/init-kibi\` command triggers the \`kb_autopilot_generate\` workflow to assist in retroactive bootstrap using only public MCP tools.
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
When the plugin detects an authoritative risky edit (`behavior_candidate` or `traceability_candidate` risk class), it automatically renders a Kibi briefing before the prompt. The plugin uses two complementary paths: the `file.edited` event hook as a fast-path hint, and prompt-cycle reconciliation as an authoritative fallback for programmatic edits that bypass the event bus. Briefings are rendered directly into the prompt to ensure immediate visibility.
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
-
|
|
121
|
-
-
|
|
122
|
-
- Live automatic briefing fetch from `experimental.chat.system.transform` is deferred; the hook remains text-only.
|
|
118
|
+
- **Immediate delivery**: Briefings are rendered-first into the prompt guidance block headed `🧠 **Kibi briefing available**` and TUI toasts titled `Kibi Knowledge Update`.
|
|
119
|
+
- **Narrative structure**: Delivery favors user-facing prose with `What changed` and `Why it matters`, plus conditional `Project knowledge impact` / `Interpretation note` sections when evidence or caveats exist.
|
|
120
|
+
- **TL;DR fallback**: If a full briefing is unavailable, fallback output still preserves `What changed` / `Why it matters` framing while keeping the manual command cue available.
|
|
121
|
+
- **Manual command**: Use `/brief-kibi` at any time to trigger an on-demand briefing if auto-delivery is skipped or fails.
|
|
123
122
|
|
|
124
123
|
### Discovery-first MCP guidance
|
|
125
124
|
|
|
@@ -141,7 +140,7 @@ Internal maintenance automatically syncs the knowledge base after relevant file
|
|
|
141
140
|
### Non-Blocking UX
|
|
142
141
|
|
|
143
142
|
- Sync runs in background, never blocks OpenCode
|
|
144
|
-
-
|
|
143
|
+
- **Non-blocking toast delivery**: Toast transport is best-effort. The plugin detects available OpenCode TUI capabilities (`client.tui.toast` or `client.tui.showToast`) and uses the official SDK contract. Toast failures resolve to structured `SendToastResult` objects (`delivered`, `unavailable`, `failed`) rather than throwing or falling back to raw HTTP requests.
|
|
145
144
|
|
|
146
145
|
## Configuration
|
|
147
146
|
|
|
@@ -165,6 +164,8 @@ Config files (project overrides global):
|
|
|
165
164
|
| `ux.toastFailures` | boolean | `true` | Show failure toasts for sync/check issues |
|
|
166
165
|
| `ux.toastSuccesses` | boolean | `false` | Show success toasts for sync/check completion |
|
|
167
166
|
| `ux.toastCooldownMs` | number | `10000` | Cooldown between repeated UX toasts |
|
|
167
|
+
| `ux.briefs.autoSubmit` | boolean | `true` | **Deprecated/No-op**: Auto-submission is no longer needed with render-first briefing |
|
|
168
|
+
PP|| `guidance.dynamic` | boolean | `true` | Enable dynamic contextual guidance |
|
|
168
169
|
| `guidance.dynamic` | boolean | `true` | Enable dynamic contextual guidance |
|
|
169
170
|
| `guidance.warnOnKbEdits` | boolean | `true` | Enable loud warnings for .kb/** edits |
|
|
170
171
|
| `guidance.factFirstDomainRouting` | boolean | `true` | Enable FACT-first domain routing suggestions |
|
|
@@ -194,21 +195,31 @@ The plugin follows a **silent-except-operational-errors** policy for terminal ou
|
|
|
194
195
|
|
|
195
196
|
| Classification | Examples | Surface | Terminal | Structured |
|
|
196
197
|
|---------------|----------|---------|----------|------------|
|
|
197
|
-
| **Advisory (background)** | scheduler check failures, degraded-mode latches | `
|
|
198
|
-
| **Operational (plugin)** | bootstrap-needed, sync failure, hook/init failure | `error()` | Yes,
|
|
198
|
+
| **Advisory (background)** | routine `info()`, `warn()`, scheduler check failures, degraded-mode latches, `errorStructuredOnly()` | `client.app.log()` | No | Yes, via `client.app.log()` |
|
|
199
|
+
| **Operational (plugin)** | bootstrap-needed, sync failure, hook/init failure | `error()` | Yes, exactly one prefixed `console.error` (`[kibi-opencode]`) | Yes, via `client.app.log()` |
|
|
199
200
|
| **Authoritative external** | git hooks, CLI checks | Outside plugin surface | N/A | N/A |
|
|
200
201
|
|
|
201
202
|
### Failure Routing Contract
|
|
202
203
|
|
|
203
204
|
The logger exposes two error-level surfaces with distinct routing semantics:
|
|
204
205
|
|
|
205
|
-
- **`error(msg, metadata?)`** — Operational plugin failures.
|
|
206
|
-
- **`errorStructuredOnly(msg, metadata?)`** — Advisory background maintenance failures. Routes through `client.app.log()` only when a client is bound
|
|
206
|
+
- **`error(msg, metadata?)`** — Operational plugin failures. Emits exactly one prefixed `console.error` (`[kibi-opencode]`) for terminal visibility, plus `client.app.log()` when a client is bound. Structured log rejection does not emit secondary console noise. Use for bootstrap-needed, hook/init failures, and sync failures that require developer attention.
|
|
207
|
+
- **`errorStructuredOnly(msg, metadata?)`** — Advisory background maintenance failures. Routes through `client.app.log()` only when a client is bound and remains terminal-silent even when the structured transport rejects. Use for scheduler check failures and degraded-mode latches.
|
|
207
208
|
|
|
208
|
-
**Contract rule:** Once `client` is bound (after `setClient()`), advisory
|
|
209
|
+
**Contract rule:** Once `client` is bound (after `setClient()`), advisory paths (`info()`, `warn()`, `errorStructuredOnly()`) MUST stay on `client.app.log()` and remain terminal-silent. Operational failures use `error()` for a single prefixed terminal emission without duplicating console output when structured logging rejects.
|
|
209
210
|
|
|
210
211
|
Routine diagnostics route through [`client.app.log()`](https://opencode.ai/docs/plugins/) and never appear in the terminal. Only operational error-class events break terminal silence. This keeps the developer's workspace clean while preserving full visibility in structured logs for debugging.
|
|
211
212
|
|
|
213
|
+
### Toast Transport Contract
|
|
214
|
+
|
|
215
|
+
The plugin uses the official OpenCode toast APIs with automatic capability detection:
|
|
216
|
+
|
|
217
|
+
1. **Legacy transport**: `client.tui.toast(payload)` — used when available in plugin context
|
|
218
|
+
2. **SDK transport**: `client.tui.showToast({ body: payload })` — used as fallback
|
|
219
|
+
3. **No capability**: Returns `{ status: "unavailable", reason: "missing-capability" }`
|
|
220
|
+
|
|
221
|
+
All toast delivery is best-effort and non-blocking. The `sendToast` helper returns a discriminated `SendToastResult` union and never throws. There is no raw HTTP fallback.
|
|
222
|
+
|
|
212
223
|
The `experimental.chat.system.transform` hook handles prompt injection (see [Hook Policy](#hook-policy)). The `chat.params` hook is compatibility-only and never carries prompt text.
|
|
213
224
|
|
|
214
225
|
### Hook Modes
|
|
@@ -274,6 +285,20 @@ A proposed enhancement would inject Kibi context hints into file-read results (e
|
|
|
274
285
|
|
|
275
286
|
Current workaround: static system prompt guidance directs agents to query Kibi explicitly.
|
|
276
287
|
|
|
288
|
+
### File-Context Guidance
|
|
289
|
+
|
|
290
|
+
The plugin provides proactive guidance when agents perform file operations:
|
|
291
|
+
|
|
292
|
+
- **File-create/edit guidance**: When an agent creates or edits a source file, the plugin may inject reminders to check Kibi for that path if e2e evidence exists.
|
|
293
|
+
|
|
294
|
+
- **File-delete safety guidance**: When an agent attempts to delete a file, the plugin injects a safety check reminding the agent to verify if the file implements any Kibi requirements before removal.
|
|
295
|
+
|
|
296
|
+
- **E2e reminder evidence**: File-operation reminders use exact Kibi graph evidence first (`covered_by` links to `[e2e]`-tagged entities or `/e2e/`-sourced entities) and narrow path heuristics second. Package-level e2e tests do not trigger "authoritative evidence" flags at the file level.
|
|
297
|
+
|
|
298
|
+
- **Session suppression**: To minimize prompt noise, this guidance is suppressed after the first occurrence per path per session.
|
|
299
|
+
|
|
300
|
+
- **Current-host scope**: This feature uses host-side event monitoring to detect intent; it does not intercept or modify actual file content returned by the Read tool.
|
|
301
|
+
|
|
277
302
|
## License
|
|
278
303
|
|
|
279
304
|
AGPL-3.0-or-later
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
sourceFiles: string[];
|
|
10
|
+
focusFilePath?: string;
|
|
11
|
+
seedIds?: string[];
|
|
12
|
+
}
|
|
13
|
+
export interface BriefIntentResult {
|
|
14
|
+
eligible: boolean;
|
|
15
|
+
reason: string;
|
|
16
|
+
fingerprint: string;
|
|
17
|
+
sourceFiles: string[];
|
|
18
|
+
seedIds: string[];
|
|
19
|
+
}
|
|
20
|
+
export interface BriefIntentInputs {
|
|
21
|
+
riskClass: RiskClass;
|
|
22
|
+
posture: RepoPosture;
|
|
23
|
+
maintenanceDegraded: boolean;
|
|
24
|
+
worktreeRoot: string;
|
|
25
|
+
branch: string;
|
|
26
|
+
sourceFiles: string[];
|
|
27
|
+
focusFilePath?: string;
|
|
28
|
+
seedIds?: string[];
|
|
29
|
+
}
|
|
30
|
+
export declare function deriveBriefIntent(params: BriefIntentParams): BriefIntentResult;
|
|
31
|
+
export declare function computeBriefIntent(inputs: BriefIntentInputs): BriefIntentResult;
|
|
32
|
+
export interface BriefingContextParams {
|
|
33
|
+
sourceFiles: string[];
|
|
34
|
+
seedIds?: string[];
|
|
35
|
+
changedEntityIds?: string[];
|
|
36
|
+
}
|
|
37
|
+
export interface BriefingContextResult {
|
|
38
|
+
sourceFiles: string[];
|
|
39
|
+
seedIds: string[];
|
|
40
|
+
}
|
|
41
|
+
export declare function buildBriefingContext(params: BriefingContextParams): BriefingContextResult;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// implements REQ-opencode-kibi-briefing-v2, REQ-opencode-smart-enforcement-v1
|
|
2
|
+
import { getSourceLinkedRequirementIds } from "./source-linked-guidance.js";
|
|
3
|
+
const ELIGIBLE_RISK_CLASSES = new Set([
|
|
4
|
+
"behavior_candidate",
|
|
5
|
+
"traceability_candidate",
|
|
6
|
+
]);
|
|
7
|
+
const STRICT_ELIGIBLE_POSTURES = new Set([
|
|
8
|
+
"root_active",
|
|
9
|
+
"hybrid_root_plus_vendored",
|
|
10
|
+
]);
|
|
11
|
+
function sortAndDedup(files) {
|
|
12
|
+
return [...new Set(files)].sort();
|
|
13
|
+
}
|
|
14
|
+
function deriveSeedIds(params) {
|
|
15
|
+
if (params.seedIds !== undefined && params.seedIds.length > 0) {
|
|
16
|
+
return buildBriefingContext({
|
|
17
|
+
sourceFiles: params.sourceFiles,
|
|
18
|
+
seedIds: params.seedIds,
|
|
19
|
+
}).seedIds.slice(0, 3);
|
|
20
|
+
}
|
|
21
|
+
const focusFile = params.focusFilePath ?? params.sourceFiles[0];
|
|
22
|
+
if (!focusFile) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
return buildBriefingContext({
|
|
26
|
+
sourceFiles: params.sourceFiles,
|
|
27
|
+
seedIds: getSourceLinkedRequirementIds(params.workspaceRoot, focusFile),
|
|
28
|
+
}).seedIds.slice(0, 3);
|
|
29
|
+
}
|
|
30
|
+
// implements REQ-opencode-kibi-briefing-v2, REQ-opencode-smart-enforcement-v1
|
|
31
|
+
export function deriveBriefIntent(params) {
|
|
32
|
+
const sortedSourceFiles = sortAndDedup(params.sourceFiles);
|
|
33
|
+
const fingerprint = `brief:${params.workspaceRoot}\0${params.branch}\0${params.riskClass}\0${sortedSourceFiles.join("\0")}`;
|
|
34
|
+
const seedIds = deriveSeedIds(params);
|
|
35
|
+
if (sortedSourceFiles.length === 0) {
|
|
36
|
+
return {
|
|
37
|
+
eligible: false,
|
|
38
|
+
reason: "Ineligible: no source files in session",
|
|
39
|
+
fingerprint,
|
|
40
|
+
sourceFiles: sortedSourceFiles,
|
|
41
|
+
seedIds: [],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (!ELIGIBLE_RISK_CLASSES.has(params.riskClass)) {
|
|
45
|
+
return {
|
|
46
|
+
eligible: false,
|
|
47
|
+
reason: `Ineligible: riskClass ${params.riskClass} is not auto-brief eligible`,
|
|
48
|
+
fingerprint,
|
|
49
|
+
sourceFiles: sortedSourceFiles,
|
|
50
|
+
seedIds,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (!STRICT_ELIGIBLE_POSTURES.has(params.posture)) {
|
|
54
|
+
return {
|
|
55
|
+
eligible: false,
|
|
56
|
+
reason: `Ineligible: posture ${params.posture} is not authoritative`,
|
|
57
|
+
fingerprint,
|
|
58
|
+
sourceFiles: sortedSourceFiles,
|
|
59
|
+
seedIds,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (params.maintenanceDegraded) {
|
|
63
|
+
return {
|
|
64
|
+
eligible: false,
|
|
65
|
+
reason: "Ineligible: maintenance is degraded",
|
|
66
|
+
fingerprint,
|
|
67
|
+
sourceFiles: sortedSourceFiles,
|
|
68
|
+
seedIds,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
eligible: true,
|
|
73
|
+
reason: "Eligible for auto-briefing",
|
|
74
|
+
fingerprint,
|
|
75
|
+
sourceFiles: sortedSourceFiles,
|
|
76
|
+
seedIds,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export function computeBriefIntent(
|
|
80
|
+
// implements REQ-opencode-kibi-briefing-v2
|
|
81
|
+
inputs) {
|
|
82
|
+
return deriveBriefIntent({
|
|
83
|
+
riskClass: inputs.riskClass,
|
|
84
|
+
posture: inputs.posture,
|
|
85
|
+
maintenanceDegraded: inputs.maintenanceDegraded,
|
|
86
|
+
workspaceRoot: inputs.worktreeRoot,
|
|
87
|
+
branch: inputs.branch,
|
|
88
|
+
sourceFiles: inputs.sourceFiles,
|
|
89
|
+
...(inputs.focusFilePath !== undefined
|
|
90
|
+
? { focusFilePath: inputs.focusFilePath }
|
|
91
|
+
: {}),
|
|
92
|
+
...(inputs.seedIds !== undefined ? { seedIds: inputs.seedIds } : {}),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
export function buildBriefingContext(
|
|
96
|
+
// implements REQ-opencode-kibi-briefing-v6
|
|
97
|
+
params) {
|
|
98
|
+
const sourceFiles = [...new Set(params.sourceFiles)].sort();
|
|
99
|
+
const seen = new Set();
|
|
100
|
+
const seeds = [];
|
|
101
|
+
// Take first 3 changed entity IDs in original order, dedupe, sort
|
|
102
|
+
if (params.changedEntityIds) {
|
|
103
|
+
for (const id of params.changedEntityIds.slice(0, 3)) {
|
|
104
|
+
if (!seen.has(id)) {
|
|
105
|
+
seeds.push(id);
|
|
106
|
+
seen.add(id);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Fill remaining slots from seedIds in original order
|
|
111
|
+
if (params.seedIds) {
|
|
112
|
+
for (const id of params.seedIds) {
|
|
113
|
+
if (seeds.length >= 5)
|
|
114
|
+
break;
|
|
115
|
+
if (!seen.has(id)) {
|
|
116
|
+
seeds.push(id);
|
|
117
|
+
seen.add(id);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Sort final seedIds alphabetically
|
|
122
|
+
seeds.sort((a, b) => a.localeCompare(b));
|
|
123
|
+
return {
|
|
124
|
+
sourceFiles,
|
|
125
|
+
seedIds: seeds,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -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,277 @@
|
|
|
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 DEFAULT_WHY_IT_MATTERS = "This update changes how current project knowledge should be interpreted.";
|
|
8
|
+
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.";
|
|
9
|
+
const PROMPT_FORMAT = {
|
|
10
|
+
type: "json_schema",
|
|
11
|
+
schema: {
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
briefingState: { type: "string" },
|
|
15
|
+
tldr: { type: "string" },
|
|
16
|
+
promptBlock: { type: "string" },
|
|
17
|
+
citations: { type: "array", items: { type: "object" } },
|
|
18
|
+
activationState: { type: "string" },
|
|
19
|
+
confidence: { type: "string" },
|
|
20
|
+
freshness: { type: "string" },
|
|
21
|
+
},
|
|
22
|
+
required: ["briefingState"],
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
const workerSessionIds = new Map();
|
|
26
|
+
const workerSessionPromises = new Map();
|
|
27
|
+
const resultCache = new Map();
|
|
28
|
+
const inFlightResults = new Map();
|
|
29
|
+
function noBriefingResult() {
|
|
30
|
+
return {
|
|
31
|
+
state: "no_briefing",
|
|
32
|
+
promptBlock: "",
|
|
33
|
+
tldr: "",
|
|
34
|
+
citations: [],
|
|
35
|
+
showManualCue: true,
|
|
36
|
+
toastMessage: UNAVAILABLE_TOAST,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function workspaceSessionKey(workspaceCtx) {
|
|
40
|
+
return `${workspaceCtx.workspaceRoot}\0${workspaceCtx.branch}`;
|
|
41
|
+
}
|
|
42
|
+
function asRecord(value) {
|
|
43
|
+
return typeof value === "object" && value !== null
|
|
44
|
+
? value
|
|
45
|
+
: null;
|
|
46
|
+
}
|
|
47
|
+
function asString(value) {
|
|
48
|
+
return typeof value === "string" ? value : "";
|
|
49
|
+
}
|
|
50
|
+
function sanitizePromptBlock(value) {
|
|
51
|
+
return asString(value).trim();
|
|
52
|
+
}
|
|
53
|
+
function sanitizeCitations(value) {
|
|
54
|
+
if (!Array.isArray(value)) {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
const citations = [];
|
|
58
|
+
for (const item of value) {
|
|
59
|
+
const record = asRecord(item);
|
|
60
|
+
if (!record) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const id = asString(record.id).trim();
|
|
64
|
+
if (!id) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const citation = { id };
|
|
68
|
+
const type = asString(record.type).trim();
|
|
69
|
+
const title = asString(record.title).trim();
|
|
70
|
+
const source = asString(record.source).trim();
|
|
71
|
+
const textRef = asString(record.textRef).trim();
|
|
72
|
+
if (type) {
|
|
73
|
+
citation.type = type;
|
|
74
|
+
}
|
|
75
|
+
if (title) {
|
|
76
|
+
citation.title = title;
|
|
77
|
+
}
|
|
78
|
+
if (source) {
|
|
79
|
+
citation.source = source;
|
|
80
|
+
}
|
|
81
|
+
if (textRef) {
|
|
82
|
+
citation.textRef = textRef;
|
|
83
|
+
}
|
|
84
|
+
citations.push(citation);
|
|
85
|
+
}
|
|
86
|
+
return citations;
|
|
87
|
+
}
|
|
88
|
+
function hasBriefingState(value) {
|
|
89
|
+
const record = asRecord(value);
|
|
90
|
+
return record !== null && "briefingState" in record;
|
|
91
|
+
}
|
|
92
|
+
function extractParts(response) {
|
|
93
|
+
const root = asRecord(response);
|
|
94
|
+
if (!root) {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
const data = asRecord(root.data);
|
|
98
|
+
const parts = data?.parts ?? root.parts;
|
|
99
|
+
return Array.isArray(parts) ? parts : [];
|
|
100
|
+
}
|
|
101
|
+
function parsePromptPayload(response) {
|
|
102
|
+
const parts = extractParts(response);
|
|
103
|
+
for (let index = parts.length - 1; index >= 0; index -= 1) {
|
|
104
|
+
const part = asRecord(parts[index]);
|
|
105
|
+
if (!part || part.type !== "text") {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const text = asString(part.text);
|
|
109
|
+
if (!text) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const parsed = JSON.parse(text);
|
|
114
|
+
if (hasBriefingState(parsed)) {
|
|
115
|
+
return parsed;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// Ignore malformed text parts and continue searching from the end.
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
function normalizeResult(payload) {
|
|
125
|
+
if (!payload) {
|
|
126
|
+
return noBriefingResult();
|
|
127
|
+
}
|
|
128
|
+
const briefingState = asString(payload.briefingState).trim();
|
|
129
|
+
const tldr = asString(payload.tldr).trim();
|
|
130
|
+
const promptBlock = sanitizePromptBlock(payload.promptBlock);
|
|
131
|
+
if (briefingState === "ready" && promptBlock) {
|
|
132
|
+
return {
|
|
133
|
+
state: "ready",
|
|
134
|
+
promptBlock,
|
|
135
|
+
tldr,
|
|
136
|
+
citations: sanitizeCitations(payload.citations),
|
|
137
|
+
showManualCue: false,
|
|
138
|
+
toastMessage: READY_TOAST,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (briefingState === "ready" && tldr) {
|
|
142
|
+
return {
|
|
143
|
+
state: "tldr_fallback",
|
|
144
|
+
promptBlock: `- What changed: ${tldr}\n- Why it matters: ${DEFAULT_WHY_IT_MATTERS}`,
|
|
145
|
+
tldr,
|
|
146
|
+
citations: [],
|
|
147
|
+
showManualCue: true,
|
|
148
|
+
toastMessage: TLDR_FALLBACK_TOAST,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return noBriefingResult();
|
|
152
|
+
}
|
|
153
|
+
function getSessionApi(client) {
|
|
154
|
+
const root = asRecord(client);
|
|
155
|
+
const session = asRecord(root?.session);
|
|
156
|
+
if (!session) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
const create = session.create;
|
|
160
|
+
const prompt = session.prompt;
|
|
161
|
+
if (typeof create !== "function" || typeof prompt !== "function") {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
create: create,
|
|
166
|
+
prompt: prompt,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function extractSessionId(response) {
|
|
170
|
+
const root = asRecord(response);
|
|
171
|
+
if (!root) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
const directId = asString(root.id).trim();
|
|
175
|
+
if (directId) {
|
|
176
|
+
return directId;
|
|
177
|
+
}
|
|
178
|
+
const data = asRecord(root.data);
|
|
179
|
+
const dataId = asString(data?.id).trim();
|
|
180
|
+
return dataId || null;
|
|
181
|
+
}
|
|
182
|
+
async function getWorkerSessionId(sessionApi, workspaceCtx) {
|
|
183
|
+
const key = workspaceSessionKey(workspaceCtx);
|
|
184
|
+
const existing = workerSessionIds.get(key);
|
|
185
|
+
if (existing) {
|
|
186
|
+
return existing;
|
|
187
|
+
}
|
|
188
|
+
const pending = workerSessionPromises.get(key);
|
|
189
|
+
if (pending) {
|
|
190
|
+
return pending;
|
|
191
|
+
}
|
|
192
|
+
const promise = (async () => {
|
|
193
|
+
const response = await sessionApi.create({
|
|
194
|
+
directory: workspaceCtx.workspaceRoot,
|
|
195
|
+
title: WORKER_TITLE,
|
|
196
|
+
});
|
|
197
|
+
const sessionId = extractSessionId(response);
|
|
198
|
+
if (!sessionId) {
|
|
199
|
+
throw new Error("Failed to resolve worker session ID");
|
|
200
|
+
}
|
|
201
|
+
workerSessionIds.set(key, sessionId);
|
|
202
|
+
return sessionId;
|
|
203
|
+
})().finally(() => {
|
|
204
|
+
workerSessionPromises.delete(key);
|
|
205
|
+
});
|
|
206
|
+
workerSessionPromises.set(key, promise);
|
|
207
|
+
return promise;
|
|
208
|
+
}
|
|
209
|
+
async function loadBriefingResult(sessionApi, workspaceCtx, intentResult) {
|
|
210
|
+
const sessionID = await getWorkerSessionId(sessionApi, workspaceCtx);
|
|
211
|
+
const response = await sessionApi.prompt({
|
|
212
|
+
sessionID,
|
|
213
|
+
tools: { kb_briefing_generate: true },
|
|
214
|
+
format: PROMPT_FORMAT,
|
|
215
|
+
parts: [
|
|
216
|
+
{
|
|
217
|
+
type: "text",
|
|
218
|
+
text: PROMPT_INSTRUCTION,
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
type: "text",
|
|
222
|
+
text: JSON.stringify({
|
|
223
|
+
sourceFiles: intentResult.sourceFiles,
|
|
224
|
+
seedIds: intentResult.seedIds,
|
|
225
|
+
}),
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
});
|
|
229
|
+
return normalizeResult(parsePromptPayload(response));
|
|
230
|
+
}
|
|
231
|
+
// implements REQ-opencode-kibi-briefing-v2
|
|
232
|
+
export async function fetchBriefingResult(client, workspaceCtx, intentResult) {
|
|
233
|
+
const ttlMs = workspaceCtx.ttlMs ?? DEFAULT_TTL_MS;
|
|
234
|
+
const cached = resultCache.get(intentResult.fingerprint);
|
|
235
|
+
const now = Date.now();
|
|
236
|
+
if (cached && now - cached.timestamp <= ttlMs) {
|
|
237
|
+
return cached.result;
|
|
238
|
+
}
|
|
239
|
+
if (cached) {
|
|
240
|
+
resultCache.delete(intentResult.fingerprint);
|
|
241
|
+
}
|
|
242
|
+
const pending = inFlightResults.get(intentResult.fingerprint);
|
|
243
|
+
if (pending) {
|
|
244
|
+
return pending;
|
|
245
|
+
}
|
|
246
|
+
const sessionApi = getSessionApi(client);
|
|
247
|
+
if (!sessionApi || !intentResult.eligible) {
|
|
248
|
+
const result = noBriefingResult();
|
|
249
|
+
resultCache.set(intentResult.fingerprint, {
|
|
250
|
+
result,
|
|
251
|
+
timestamp: now,
|
|
252
|
+
});
|
|
253
|
+
return result;
|
|
254
|
+
}
|
|
255
|
+
const promise = (async () => {
|
|
256
|
+
try {
|
|
257
|
+
const result = await loadBriefingResult(sessionApi, workspaceCtx, intentResult);
|
|
258
|
+
resultCache.set(intentResult.fingerprint, {
|
|
259
|
+
result,
|
|
260
|
+
timestamp: Date.now(),
|
|
261
|
+
});
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
const result = noBriefingResult();
|
|
266
|
+
resultCache.set(intentResult.fingerprint, {
|
|
267
|
+
result,
|
|
268
|
+
timestamp: Date.now(),
|
|
269
|
+
});
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
272
|
+
})().finally(() => {
|
|
273
|
+
inFlightResults.delete(intentResult.fingerprint);
|
|
274
|
+
});
|
|
275
|
+
inFlightResults.set(intentResult.fingerprint, promise);
|
|
276
|
+
return promise;
|
|
277
|
+
}
|
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -11,6 +11,9 @@ const DEFAULTS = {
|
|
|
11
11
|
toastFailures: true,
|
|
12
12
|
toastSuccesses: false,
|
|
13
13
|
toastCooldownMs: 10000,
|
|
14
|
+
briefs: {
|
|
15
|
+
autoSubmit: true,
|
|
16
|
+
},
|
|
14
17
|
},
|
|
15
18
|
guidance: {
|
|
16
19
|
dynamic: true,
|
|
@@ -100,6 +103,12 @@ export function validateAndMerge(obj) {
|
|
|
100
103
|
out.ux.toastSuccesses = u.toastSuccesses;
|
|
101
104
|
if (typeof u.toastCooldownMs === "number")
|
|
102
105
|
out.ux.toastCooldownMs = u.toastCooldownMs;
|
|
106
|
+
if (u.briefs && typeof u.briefs === "object") {
|
|
107
|
+
const b = u.briefs;
|
|
108
|
+
out.ux.briefs = { autoSubmit: true };
|
|
109
|
+
if (typeof b.autoSubmit === "boolean")
|
|
110
|
+
out.ux.briefs.autoSubmit = b.autoSubmit;
|
|
111
|
+
}
|
|
103
112
|
}
|
|
104
113
|
if (typeof src.logLevel === "string")
|
|
105
114
|
out.logLevel = src.logLevel;
|