kibi-opencode 0.9.0 → 0.11.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.
Files changed (61) hide show
  1. package/README.md +38 -13
  2. package/dist/brief-delivery-reasons.d.ts +12 -0
  3. package/dist/brief-delivery-reasons.js +132 -0
  4. package/dist/brief-intent.d.ts +15 -4
  5. package/dist/brief-intent.js +78 -25
  6. package/dist/briefing-runtime.js +2 -1
  7. package/dist/config.d.ts +3 -0
  8. package/dist/config.js +9 -0
  9. package/dist/e2e-coverage-signals.d.ts +6 -0
  10. package/dist/e2e-coverage-signals.js +186 -0
  11. package/dist/file-entity-links.d.ts +15 -0
  12. package/dist/file-entity-links.js +254 -0
  13. package/dist/file-operation-reminders.d.ts +24 -0
  14. package/dist/file-operation-reminders.js +55 -0
  15. package/dist/file-operation-state.d.ts +29 -0
  16. package/dist/file-operation-state.js +113 -0
  17. package/dist/idle-brief-audit.d.ts +36 -0
  18. package/dist/idle-brief-audit.js +186 -0
  19. package/dist/idle-brief-paths.d.ts +6 -0
  20. package/dist/idle-brief-paths.js +120 -0
  21. package/dist/idle-brief-reader.d.ts +37 -0
  22. package/dist/idle-brief-reader.js +163 -0
  23. package/dist/idle-brief-runtime.d.ts +48 -0
  24. package/dist/idle-brief-runtime.js +478 -0
  25. package/dist/idle-brief-store.d.ts +113 -0
  26. package/dist/idle-brief-store.js +262 -0
  27. package/dist/index.d.ts +2 -39
  28. package/dist/index.js +1 -492
  29. package/dist/init-kibi-alias.d.ts +14 -0
  30. package/dist/init-kibi-alias.js +38 -0
  31. package/dist/init-kibi-capability.d.ts +32 -0
  32. package/dist/init-kibi-capability.js +202 -0
  33. package/dist/logger.d.ts +1 -0
  34. package/dist/logger.js +17 -4
  35. package/dist/plugin-startup.d.ts +1 -0
  36. package/dist/plugin-startup.js +11 -2
  37. package/dist/plugin.d.ts +52 -0
  38. package/dist/plugin.js +1068 -0
  39. package/dist/prompt.d.ts +15 -3
  40. package/dist/prompt.js +106 -36
  41. package/dist/reconcile-engine.d.ts +15 -0
  42. package/dist/reconcile-engine.js +112 -0
  43. package/dist/scheduler.d.ts +13 -2
  44. package/dist/scheduler.js +86 -7
  45. package/dist/session-edit-state.d.ts +25 -0
  46. package/dist/session-edit-state.js +177 -0
  47. package/dist/session-fingerprint.d.ts +11 -0
  48. package/dist/session-fingerprint.js +21 -0
  49. package/dist/source-linked-guidance.d.ts +1 -2
  50. package/dist/source-linked-guidance.js +5 -168
  51. package/dist/startup-notifier.js +42 -31
  52. package/dist/toast.d.ts +23 -22
  53. package/dist/toast.js +36 -14
  54. package/dist/tui-brief-delivery.d.ts +67 -0
  55. package/dist/tui-brief-delivery.js +279 -0
  56. package/dist/tui-brief-view-model.d.ts +63 -0
  57. package/dist/tui-brief-view-model.js +209 -0
  58. package/dist/tui.d.ts +8 -0
  59. package/dist/tui.js +413 -0
  60. package/dist/tui.jsx +120 -0
  61. package/package.json +13 -4
package/README.md CHANGED
@@ -113,14 +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
- ### Start-Task Briefing
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
- 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
-
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.
118
+ - **Immediate delivery**: Briefings are rendered-first into the prompt guidance block headed `🧠 **Kibi briefing available**` and TUI toasts titled `Kibi Knowledge Update`. Unread briefings automatically open the interactive TUI via the `kibi.brief` route.
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` or the `kibi.open_latest_brief` command at any time to trigger an on-demand briefing if auto-delivery is skipped or fails.
124
122
 
125
123
  ### Discovery-first MCP guidance
126
124
 
@@ -138,11 +136,12 @@ Internal maintenance automatically syncs the knowledge base after relevant file
138
136
  - Single-flight scheduler (no overlapping syncs)
139
137
  - Debounce window (default: 2000ms)
140
138
  - Dirty flag triggers one trailing rerun after active sync completes
139
+ - **Idle suppression**: Background sync attempts triggered by session idle are suppressed after an operational sync failure is latched (`scheduler_sync_failed`). Manual edits and tool executions continue to schedule syncs to allow for recovery.
141
140
 
142
141
  ### Non-Blocking UX
143
142
 
144
143
  - Sync runs in background, never blocks OpenCode
145
- - Failures reported via console logs only, never as blocking UI elements
144
+ - **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.
146
145
 
147
146
  ## Configuration
148
147
 
@@ -166,6 +165,8 @@ Config files (project overrides global):
166
165
  | `ux.toastFailures` | boolean | `true` | Show failure toasts for sync/check issues |
167
166
  | `ux.toastSuccesses` | boolean | `false` | Show success toasts for sync/check completion |
168
167
  | `ux.toastCooldownMs` | number | `10000` | Cooldown between repeated UX toasts |
168
+ | `ux.briefs.autoSubmit` | boolean | `true` | **Deprecated/No-op**: Auto-submission is no longer needed with render-first briefing |
169
+ PP|| `guidance.dynamic` | boolean | `true` | Enable dynamic contextual guidance |
169
170
  | `guidance.dynamic` | boolean | `true` | Enable dynamic contextual guidance |
170
171
  | `guidance.warnOnKbEdits` | boolean | `true` | Enable loud warnings for .kb/** edits |
171
172
  | `guidance.factFirstDomainRouting` | boolean | `true` | Enable FACT-first domain routing suggestions |
@@ -195,21 +196,31 @@ The plugin follows a **silent-except-operational-errors** policy for terminal ou
195
196
 
196
197
  | Classification | Examples | Surface | Terminal | Structured |
197
198
  |---------------|----------|---------|----------|------------|
198
- | **Advisory (background)** | scheduler check failures, degraded-mode latches | `errorStructuredOnly()` | No | Yes, via `client.app.log()` |
199
- | **Operational (plugin)** | bootstrap-needed, sync failure, hook/init failure | `error()` | Yes, via `console.error` | Yes, via `client.app.log()` |
199
+ | **Advisory (background)** | routine `info()`, `warn()`, scheduler check failures, degraded-mode latches, `errorStructuredOnly()` | `client.app.log()` | No | Yes, via `client.app.log()` |
200
+ | **Operational (plugin)** | bootstrap-needed, sync failure, hook/init failure | `error()` | Yes, exactly one prefixed `console.error` (`[kibi-opencode]`) | Yes, via `client.app.log()` |
200
201
  | **Authoritative external** | git hooks, CLI checks | Outside plugin surface | N/A | N/A |
201
202
 
202
203
  ### Failure Routing Contract
203
204
 
204
205
  The logger exposes two error-level surfaces with distinct routing semantics:
205
206
 
206
- - **`error(msg, metadata?)`** — Operational plugin failures. Always emits to `console.error` for terminal visibility, plus `client.app.log()` when a client is bound. 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; completely silent when no client is bound (no `console.error` fallback). Use for scheduler check failures and degraded-mode latches.
207
+ - **`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. Operational sync failure payloads include diagnostic metadata: `syncCommand`, `syncStdout`, `syncStderr`, and `syncErrorMessage`.
208
+ - **`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.
209
+
210
+
208
211
 
209
- **Contract rule:** Once `client` is bound (after `setClient()`), advisory logging MUST use `errorStructuredOnly()`. When no client is bound, `errorStructuredOnly()` is completely silent it does not fall back to `console.error`.
212
+ **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.
210
213
 
211
214
  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.
212
215
 
216
+ ### Toast Transport Contract
217
+
218
+ The plugin uses the official OpenCode toast APIs with automatic capability detection:
219
+
220
+ 1. **Legacy transport**: `client.tui.toast(payload)` — used when available in plugin context
221
+ 2. **SDK transport**: `client.tui.showToast({ body: payload })` — used as fallback
222
+ 3. **No capability**: Returns `{ status: "unavailable", reason: "missing-capability" }`
223
+ The `sendToast` helper returns a discriminated `SendToastResult` union and never throws. There is no raw HTTP fallback. For rich briefing delivery, the plugin uses the `kibi.brief` route and the `kibi.open_latest_brief` command for manual retrieval.
213
224
  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.
214
225
 
215
226
  ### Hook Modes
@@ -275,6 +286,20 @@ A proposed enhancement would inject Kibi context hints into file-read results (e
275
286
 
276
287
  Current workaround: static system prompt guidance directs agents to query Kibi explicitly.
277
288
 
289
+ ### File-Context Guidance
290
+
291
+ The plugin provides proactive guidance when agents perform file operations:
292
+
293
+ - **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.
294
+
295
+ - **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.
296
+
297
+ - **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.
298
+
299
+ - **Session suppression**: To minimize prompt noise, this guidance is suppressed after the first occurrence per path per session.
300
+
301
+ - **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.
302
+
278
303
  ## License
279
304
 
280
305
  AGPL-3.0-or-later
@@ -0,0 +1,12 @@
1
+ import type { DeliveryReasons } from "./idle-brief-store.js";
2
+ export type BuildInput = {
3
+ entitiesAdded: string[];
4
+ entitiesModified: string[];
5
+ entitiesRemoved: string[];
6
+ relationshipsChanged: number;
7
+ validationCount: number;
8
+ conflictReasons?: string[];
9
+ };
10
+ export declare function buildDeliveryReasons(input: BuildInput): DeliveryReasons | undefined;
11
+ export declare function renderToastSummary(reasons: DeliveryReasons): DeliveryReasons["toast"] | undefined;
12
+ export declare function renderFullBriefReasons(reasons: DeliveryReasons): string;
@@ -0,0 +1,132 @@
1
+ // Inlined from kibi-cli/operational-artifacts to avoid heavy module resolution
2
+ function isOperationalArtifactPath(pathLike) {
3
+ const normalized = pathLike.replaceAll("\\", "/");
4
+ return /(^|\/)\.sisyphus\//.test(normalized);
5
+ }
6
+ const ORDER = {
7
+ conflict_detected: 0,
8
+ validation_issue: 1,
9
+ entity_modified: 2,
10
+ entity_added: 3,
11
+ entity_removed: 4,
12
+ relationship_changed: 5,
13
+ };
14
+ const TYPE_NAMES = {
15
+ REQ: "requirement",
16
+ FACT: "fact",
17
+ TEST: "test",
18
+ SCEN: "scenario",
19
+ SYM: "code traceability",
20
+ ADR: "decision",
21
+ FLAG: "runtime flag",
22
+ EVENT: "event",
23
+ };
24
+ function prefixName(id) {
25
+ const prefix = id.split("-")[0]?.toUpperCase() ?? id;
26
+ return TYPE_NAMES[prefix] ?? prefix;
27
+ }
28
+ function mk(kind, text, entityIds) {
29
+ return { kind, text, entityIds };
30
+ }
31
+ function entityItems(kind, ids) {
32
+ if (!ids.length)
33
+ return [];
34
+ const verb = kind === "entity_added" ? "Added" : kind === "entity_modified" ? "Updated" : "Removed";
35
+ const grouped = new Map();
36
+ for (const id of [...ids].sort()) {
37
+ const prefix = id.split("-")[0]?.toUpperCase() ?? id;
38
+ const group = grouped.get(prefix);
39
+ if (group) {
40
+ group.push(id);
41
+ }
42
+ else {
43
+ grouped.set(prefix, [id]);
44
+ }
45
+ }
46
+ return [...grouped.entries()].map(([, groupedIds]) => {
47
+ const noun = prefixName(groupedIds[0] ?? "");
48
+ const text = groupedIds.length === 1
49
+ ? `${verb} ${noun} ${groupedIds[0]}`
50
+ : `${verb} ${groupedIds.length} ${noun}s (${groupedIds.join(", ")})`;
51
+ return mk(kind, text, groupedIds);
52
+ });
53
+ }
54
+ function toastSummary(items) {
55
+ const first = items[0]?.text?.trim() ?? "";
56
+ const second = items[1]?.text?.trim() ?? "";
57
+ if (first && second)
58
+ return `${first}, ${second}`;
59
+ return first || second || undefined;
60
+ }
61
+ function toastWhy(items) {
62
+ if (items.some((i) => i.kind === "conflict_detected"))
63
+ return "There is a knowledge conflict to resolve before using the brief.";
64
+ if (items.some((i) => i.kind === "validation_issue"))
65
+ return "Validation issues need attention before the update is treated as settled.";
66
+ const hasEntities = items.some((i) => i.kind === "entity_added" || i.kind === "entity_modified" || i.kind === "entity_removed");
67
+ const hasRelationships = items.some((i) => i.kind === "relationship_changed");
68
+ if (hasEntities && hasRelationships)
69
+ return "Requirements and facts were updated.";
70
+ if (hasEntities)
71
+ return "Entities were updated.";
72
+ if (hasRelationships)
73
+ return "Relationships were updated.";
74
+ return undefined;
75
+ }
76
+ export function buildDeliveryReasons(input) {
77
+ const items = [];
78
+ if (input.conflictReasons?.length)
79
+ items.push(mk("conflict_detected", input.conflictReasons[0]?.trim() || "Conflict detected", []));
80
+ if (input.validationCount > 0)
81
+ items.push(mk("validation_issue", `${input.validationCount} validation issue${input.validationCount === 1 ? "" : "s"} detected`, []));
82
+ items.push(...entityItems("entity_modified", input.entitiesModified));
83
+ items.push(...entityItems("entity_added", input.entitiesAdded));
84
+ items.push(...entityItems("entity_removed", input.entitiesRemoved));
85
+ if (input.relationshipsChanged > 0)
86
+ items.push(mk("relationship_changed", `Updated ${input.relationshipsChanged} relationships`, []));
87
+ if (!items.length)
88
+ return undefined;
89
+ items.sort((a, b) => ORDER[a.kind] - ORDER[b.kind]);
90
+ return { version: 1, items, toast: { title: "Kibi Knowledge Update", summary: toastSummary(items) ?? "", whyItMatters: toastWhy(items) ?? "" } };
91
+ }
92
+ function isOperationalByEntityIds(item) {
93
+ if (item.entityIds.length === 0)
94
+ return false;
95
+ return item.entityIds.every((id) => {
96
+ const dashIdx = id.indexOf("-");
97
+ if (dashIdx < 0)
98
+ return false;
99
+ const name = id.slice(dashIdx + 1);
100
+ // Entity names with file extensions are likely from operational artifact files
101
+ return /\.[a-zA-Z0-9]+$/.test(name);
102
+ });
103
+ }
104
+ function isOperationalItem(item, allItems) {
105
+ if (item.kind === "relationship_changed") {
106
+ // relationship_changed items have no entityIds; treat as operational when
107
+ // all entity-level items in the set are operational (they're relationship side-effects)
108
+ const entityItems = allItems.filter((i) => i.kind === "entity_added" || i.kind === "entity_modified" || i.kind === "entity_removed");
109
+ return entityItems.length > 0 && entityItems.every(isOperationalByEntityIds);
110
+ }
111
+ return isOperationalByEntityIds(item);
112
+ }
113
+ export function renderToastSummary(reasons) {
114
+ const domainItems = reasons.items.filter((i) => !isOperationalItem(i, reasons.items));
115
+ if (domainItems.length === 0) {
116
+ return undefined; // suppress: specific-or-silent policy
117
+ }
118
+ return {
119
+ title: "Kibi Knowledge Update",
120
+ summary: toastSummary(domainItems) ?? "",
121
+ whyItMatters: toastWhy(domainItems) ?? "",
122
+ };
123
+ }
124
+ export function renderFullBriefReasons(reasons) {
125
+ const domainItems = reasons.items.filter((i) => !isOperationalItem(i, reasons.items));
126
+ const lines = ["## What changed", ...domainItems.map((r) => `- ${r.text}`)];
127
+ const whyItMatters = toastWhy(domainItems);
128
+ if (whyItMatters) {
129
+ lines.push("", "## Why it matters", whyItMatters);
130
+ }
131
+ return lines.join("\n");
132
+ }
@@ -6,7 +6,8 @@ export interface BriefIntentParams {
6
6
  maintenanceDegraded: boolean;
7
7
  workspaceRoot: string;
8
8
  branch: string;
9
- editedFilePath: string | undefined;
9
+ sourceFiles: string[];
10
+ focusFilePath?: string;
10
11
  seedIds?: string[];
11
12
  }
12
13
  export interface BriefIntentResult {
@@ -22,9 +23,19 @@ export interface BriefIntentInputs {
22
23
  maintenanceDegraded: boolean;
23
24
  worktreeRoot: string;
24
25
  branch: string;
25
- editedFile: string | undefined;
26
+ sourceFiles: string[];
27
+ focusFilePath?: string;
26
28
  seedIds?: string[];
27
29
  }
28
30
  export declare function deriveBriefIntent(params: BriefIntentParams): BriefIntentResult;
29
- export declare function computeBriefIntent(// implements REQ-opencode-kibi-briefing-v2
30
- inputs: BriefIntentInputs): 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;
@@ -1,5 +1,9 @@
1
1
  // implements REQ-opencode-kibi-briefing-v2, REQ-opencode-smart-enforcement-v1
2
- import * as path from "node:path";
2
+ // Inlined from kibi-cli/operational-artifacts to avoid heavy module resolution
3
+ function isOperationalArtifactPath(pathLike) {
4
+ const normalized = pathLike.replaceAll("\\", "/");
5
+ return /(^|\/)\.sisyphus\//.test(normalized);
6
+ }
3
7
  import { getSourceLinkedRequirementIds } from "./source-linked-guidance.js";
4
8
  const ELIGIBLE_RISK_CLASSES = new Set([
5
9
  "behavior_candidate",
@@ -9,35 +13,47 @@ const STRICT_ELIGIBLE_POSTURES = new Set([
9
13
  "root_active",
10
14
  "hybrid_root_plus_vendored",
11
15
  ]);
12
- function hasEditedFilePath(editedFilePath) {
13
- return typeof editedFilePath === "string" && editedFilePath.length > 0;
16
+ function sortAndDedup(files) {
17
+ return [...new Set(files)].sort();
14
18
  }
15
19
  function deriveSeedIds(params) {
16
- if (!hasEditedFilePath(params.editedFilePath)) {
17
- return [];
20
+ if (params.seedIds !== undefined && params.seedIds.length > 0) {
21
+ return buildBriefingContext({
22
+ sourceFiles: params.sourceFiles,
23
+ seedIds: params.seedIds,
24
+ }).seedIds.slice(0, 3);
18
25
  }
19
- if (params.seedIds !== undefined) {
20
- return params.seedIds.slice(0, 3);
26
+ const focusFile = params.focusFilePath ?? params.sourceFiles[0];
27
+ if (!focusFile) {
28
+ return [];
21
29
  }
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);
30
+ return buildBriefingContext({
31
+ sourceFiles: params.sourceFiles,
32
+ seedIds: getSourceLinkedRequirementIds(params.workspaceRoot, focusFile),
33
+ }).seedIds.slice(0, 3);
26
34
  }
27
35
  // implements REQ-opencode-kibi-briefing-v2, REQ-opencode-smart-enforcement-v1
28
36
  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
- : [];
37
+ const sortedSourceFiles = sortAndDedup(params.sourceFiles);
38
+ const nonOperationalSourceFiles = sortedSourceFiles.filter((f) => !isOperationalArtifactPath(f));
39
+ const fingerprint = `brief:${params.workspaceRoot}\0${params.branch}\0${params.riskClass}\0${nonOperationalSourceFiles.join("\0")}`;
33
40
  const seedIds = deriveSeedIds(params);
34
- if (!hasEditedFilePath(params.editedFilePath)) {
41
+ if (sortedSourceFiles.length === 0) {
35
42
  return {
36
43
  eligible: false,
37
- reason: "Ineligible: edited file path is missing",
44
+ reason: "Ineligible: no source files in session",
38
45
  fingerprint,
39
- sourceFiles,
40
- seedIds,
46
+ sourceFiles: sortedSourceFiles,
47
+ seedIds: [],
48
+ };
49
+ }
50
+ if (nonOperationalSourceFiles.length === 0) {
51
+ return {
52
+ eligible: false,
53
+ reason: "All source changes are operational task-tracking artifacts",
54
+ fingerprint,
55
+ sourceFiles: sortedSourceFiles,
56
+ seedIds: [],
41
57
  };
42
58
  }
43
59
  if (!ELIGIBLE_RISK_CLASSES.has(params.riskClass)) {
@@ -45,7 +61,7 @@ export function deriveBriefIntent(params) {
45
61
  eligible: false,
46
62
  reason: `Ineligible: riskClass ${params.riskClass} is not auto-brief eligible`,
47
63
  fingerprint,
48
- sourceFiles,
64
+ sourceFiles: sortedSourceFiles,
49
65
  seedIds,
50
66
  };
51
67
  }
@@ -54,7 +70,7 @@ export function deriveBriefIntent(params) {
54
70
  eligible: false,
55
71
  reason: `Ineligible: posture ${params.posture} is not authoritative`,
56
72
  fingerprint,
57
- sourceFiles,
73
+ sourceFiles: sortedSourceFiles,
58
74
  seedIds,
59
75
  };
60
76
  }
@@ -63,7 +79,7 @@ export function deriveBriefIntent(params) {
63
79
  eligible: false,
64
80
  reason: "Ineligible: maintenance is degraded",
65
81
  fingerprint,
66
- sourceFiles,
82
+ sourceFiles: sortedSourceFiles,
67
83
  seedIds,
68
84
  };
69
85
  }
@@ -71,11 +87,12 @@ export function deriveBriefIntent(params) {
71
87
  eligible: true,
72
88
  reason: "Eligible for auto-briefing",
73
89
  fingerprint,
74
- sourceFiles,
90
+ sourceFiles: nonOperationalSourceFiles,
75
91
  seedIds,
76
92
  };
77
93
  }
78
- export function computeBriefIntent(// implements REQ-opencode-kibi-briefing-v2
94
+ export function computeBriefIntent(
95
+ // implements REQ-opencode-kibi-briefing-v2
79
96
  inputs) {
80
97
  return deriveBriefIntent({
81
98
  riskClass: inputs.riskClass,
@@ -83,7 +100,43 @@ inputs) {
83
100
  maintenanceDegraded: inputs.maintenanceDegraded,
84
101
  workspaceRoot: inputs.worktreeRoot,
85
102
  branch: inputs.branch,
86
- editedFilePath: inputs.editedFile,
103
+ sourceFiles: inputs.sourceFiles,
104
+ ...(inputs.focusFilePath !== undefined
105
+ ? { focusFilePath: inputs.focusFilePath }
106
+ : {}),
87
107
  ...(inputs.seedIds !== undefined ? { seedIds: inputs.seedIds } : {}),
88
108
  });
89
109
  }
110
+ export function buildBriefingContext(
111
+ // implements REQ-opencode-kibi-briefing-v6
112
+ params) {
113
+ const sourceFiles = [...new Set(params.sourceFiles)].sort();
114
+ const seen = new Set();
115
+ const seeds = [];
116
+ // Take first 3 changed entity IDs in original order, dedupe, sort
117
+ if (params.changedEntityIds) {
118
+ for (const id of params.changedEntityIds.slice(0, 3)) {
119
+ if (!seen.has(id)) {
120
+ seeds.push(id);
121
+ seen.add(id);
122
+ }
123
+ }
124
+ }
125
+ // Fill remaining slots from seedIds in original order
126
+ if (params.seedIds) {
127
+ for (const id of params.seedIds) {
128
+ if (seeds.length >= 5)
129
+ break;
130
+ if (!seen.has(id)) {
131
+ seeds.push(id);
132
+ seen.add(id);
133
+ }
134
+ }
135
+ }
136
+ // Sort final seedIds alphabetically
137
+ seeds.sort((a, b) => a.localeCompare(b));
138
+ return {
139
+ sourceFiles,
140
+ seedIds: seeds,
141
+ };
142
+ }
@@ -4,6 +4,7 @@ const WORKER_TITLE = "Kibi Auto Brief Worker";
4
4
  const READY_TOAST = "Kibi brief ready — summary added to guidance.";
5
5
  const TLDR_FALLBACK_TOAST = "Kibi brief summary added — use /brief-kibi for full details.";
6
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.";
7
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.";
8
9
  const PROMPT_FORMAT = {
9
10
  type: "json_schema",
@@ -140,7 +141,7 @@ function normalizeResult(payload) {
140
141
  if (briefingState === "ready" && tldr) {
141
142
  return {
142
143
  state: "tldr_fallback",
143
- promptBlock: `- ${tldr}\n- Full details: run /brief-kibi.`,
144
+ promptBlock: `- What changed: ${tldr}\n- Why it matters: ${DEFAULT_WHY_IT_MATTERS}`,
144
145
  tldr,
145
146
  citations: [],
146
147
  showManualCue: true,
package/dist/config.d.ts CHANGED
@@ -15,6 +15,9 @@ export interface KibiConfig {
15
15
  toastFailures: boolean;
16
16
  toastSuccesses: boolean;
17
17
  toastCooldownMs: number;
18
+ briefs?: {
19
+ autoSubmit: boolean;
20
+ };
18
21
  };
19
22
  guidance: {
20
23
  dynamic: boolean;
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;
@@ -0,0 +1,6 @@
1
+ export interface E2eCoverageSignal {
2
+ level: "exact" | "heuristic" | "none";
3
+ evidence: string[];
4
+ reminderText: string | null;
5
+ }
6
+ export declare function getE2eCoverageSignal(worktree: string, filePath: string): E2eCoverageSignal;