kibi-opencode 0.14.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +18 -12
  2. package/dist/config.d.ts +0 -3
  3. package/dist/config.js +0 -9
  4. package/dist/e2e-coverage-signals.js +1 -3
  5. package/dist/enforcement-policy.d.ts +2 -0
  6. package/dist/enforcement-policy.js +18 -3
  7. package/dist/enforcement-scope.js +1 -3
  8. package/dist/file-entity-links.js +2 -6
  9. package/dist/file-operation-reminders.d.ts +4 -4
  10. package/dist/file-operation-state.js +4 -2
  11. package/dist/init-kibi-alias.js +0 -1
  12. package/dist/init-kibi-capability.js +1 -5
  13. package/dist/kb-freshness-state.d.ts +62 -0
  14. package/dist/kb-freshness-state.js +215 -0
  15. package/dist/kibi-checkpoint-runner.d.ts +1 -1
  16. package/dist/kibi-checkpoint-runner.js +6 -3
  17. package/dist/knowledge-classifier.js +2 -1
  18. package/dist/meaningful-change-classifier.d.ts +30 -0
  19. package/dist/meaningful-change-classifier.js +103 -0
  20. package/dist/plugin-startup.js +0 -1
  21. package/dist/plugin.js +101 -391
  22. package/dist/prompt.d.ts +6 -5
  23. package/dist/prompt.js +48 -84
  24. package/dist/reconcile-engine.d.ts +15 -1
  25. package/dist/reconcile-engine.js +1 -3
  26. package/dist/scheduler.js +16 -4
  27. package/dist/session-edit-state.js +2 -1
  28. package/dist/source-linked-guidance.js +3 -1
  29. package/dist/startup-notifier.d.ts +7 -1
  30. package/dist/startup-notifier.js +51 -17
  31. package/dist/tui.js +1 -403
  32. package/dist/tui.jsx +1 -113
  33. package/dist/version-metadata.d.ts +13 -0
  34. package/dist/version-metadata.js +78 -0
  35. package/dist/version-metadata.json +1 -0
  36. package/package.json +5 -7
  37. package/dist/brief-delivery-reasons.d.ts +0 -12
  38. package/dist/brief-delivery-reasons.js +0 -132
  39. package/dist/brief-intent.d.ts +0 -41
  40. package/dist/brief-intent.js +0 -142
  41. package/dist/briefing-runtime.d.ts +0 -24
  42. package/dist/briefing-runtime.js +0 -277
  43. package/dist/graph-narrator.d.ts +0 -25
  44. package/dist/graph-narrator.js +0 -408
  45. package/dist/idle-brief-audit.d.ts +0 -36
  46. package/dist/idle-brief-audit.js +0 -186
  47. package/dist/idle-brief-paths.d.ts +0 -6
  48. package/dist/idle-brief-paths.js +0 -120
  49. package/dist/idle-brief-reader.d.ts +0 -37
  50. package/dist/idle-brief-reader.js +0 -163
  51. package/dist/idle-brief-runtime.d.ts +0 -55
  52. package/dist/idle-brief-runtime.js +0 -496
  53. package/dist/idle-brief-store.d.ts +0 -113
  54. package/dist/idle-brief-store.js +0 -262
  55. package/dist/session-fingerprint.d.ts +0 -11
  56. package/dist/session-fingerprint.js +0 -21
  57. package/dist/tui-brief-delivery.d.ts +0 -67
  58. package/dist/tui-brief-delivery.js +0 -279
  59. package/dist/tui-brief-view-model.d.ts +0 -63
  60. package/dist/tui-brief-view-model.js +0 -209
  61. package/dist/utils/brief-marker.d.ts +0 -19
  62. package/dist/utils/brief-marker.js +0 -101
@@ -1,262 +0,0 @@
1
- import * as crypto from "node:crypto";
2
- function isRecord(value) {
3
- return typeof value === "object" && value !== null;
4
- }
5
- function isStringArray(value) {
6
- return (Array.isArray(value) && value.every((entry) => typeof entry === "string"));
7
- }
8
- function isCitation(value) {
9
- return isRecord(value) && typeof value.id === "string";
10
- }
11
- function isStatement(value) {
12
- return (isRecord(value) &&
13
- typeof value.statement === "string" &&
14
- isStringArray(value.citationIds));
15
- }
16
- function isValidationViolation(value) {
17
- return (isRecord(value) &&
18
- typeof value.rule === "string" &&
19
- typeof value.entityId === "string" &&
20
- typeof value.description === "string");
21
- }
22
- function isValidationDiagnostic(value) {
23
- return (isRecord(value) &&
24
- typeof value.category === "string" &&
25
- typeof value.severity === "string" &&
26
- typeof value.message === "string");
27
- }
28
- function isAuditCursor(value) {
29
- return (isRecord(value) &&
30
- typeof value.lastTimestamp === "string" &&
31
- typeof value.lastOperation === "string" &&
32
- typeof value.entryCount === "number" &&
33
- typeof value.fileSize === "number");
34
- }
35
- function isValidation(value) {
36
- return (isRecord(value) &&
37
- Array.isArray(value.violations) &&
38
- value.violations.every(isValidationViolation) &&
39
- typeof value.count === "number" &&
40
- Array.isArray(value.diagnostics) &&
41
- value.diagnostics.every(isValidationDiagnostic));
42
- }
43
- function isBriefingBase(value) {
44
- return (isRecord(value) &&
45
- typeof value.tldr === "string" &&
46
- typeof value.promptBlock === "string" &&
47
- Array.isArray(value.citations) &&
48
- value.citations.every(isCitation) &&
49
- (value.constraints === undefined ||
50
- (Array.isArray(value.constraints) &&
51
- value.constraints.every(isStatement))) &&
52
- (value.regressionRisks === undefined ||
53
- (Array.isArray(value.regressionRisks) &&
54
- value.regressionRisks.every(isStatement))) &&
55
- (value.missingEvidence === undefined ||
56
- (Array.isArray(value.missingEvidence) &&
57
- value.missingEvidence.every(isStatement))));
58
- }
59
- function isBriefingV2(value) {
60
- return (isBriefingBase(value) &&
61
- isStringArray(value.changeNarrative) &&
62
- (value.deliveryReasons === undefined ||
63
- isDeliveryReasons(value.deliveryReasons)));
64
- }
65
- function isReasonItem(value) {
66
- return (isRecord(value) &&
67
- typeof value.kind === "string" &&
68
- typeof value.text === "string" &&
69
- isStringArray(value.entityIds) &&
70
- (value.citationIds === undefined || isStringArray(value.citationIds)) &&
71
- (value.severity === undefined ||
72
- value.severity === "info" ||
73
- value.severity === "warning" ||
74
- value.severity === "error"));
75
- }
76
- function isDeliveryReasons(value) {
77
- return (isRecord(value) &&
78
- value.version === 1 &&
79
- isRecord(value.toast) &&
80
- typeof value.toast.title === "string" &&
81
- typeof value.toast.summary === "string" &&
82
- typeof value.toast.whyItMatters === "string" &&
83
- Array.isArray(value.items) &&
84
- value.items.every(isReasonItem));
85
- }
86
- function isChangeItem(value) {
87
- return (isRecord(value) &&
88
- typeof value.id === "string" &&
89
- typeof value.type === "string");
90
- }
91
- function hasRenderableText(value) {
92
- return value.trim().replace(/\s+/g, " ").length > 0;
93
- }
94
- export function isIdleBriefEnvelope(
95
- // implements REQ-opencode-kibi-briefing-v4
96
- value) {
97
- if (!isRecord(value))
98
- return false;
99
- const hasBaseFields = (value.schemaVersion === "1.0" || value.schemaVersion === "2.0") &&
100
- typeof value.briefId === "string" &&
101
- (value.type === "success" || value.type === "warning") &&
102
- typeof value.sessionId === "string" &&
103
- typeof value.branch === "string" &&
104
- typeof value.createdAt === "string" &&
105
- typeof value.unread === "boolean" &&
106
- isAuditCursor(value.auditCursor) &&
107
- typeof value.summary === "string" &&
108
- isValidation(value.validation) &&
109
- typeof value.contentHash === "string";
110
- if (!hasBaseFields)
111
- return false;
112
- if (value.schemaVersion === "1.0") {
113
- return (isRecord(value.counts) &&
114
- typeof value.counts.requirementsAdded === "number" &&
115
- typeof value.counts.relationshipsAdded === "number" &&
116
- typeof value.counts.entitiesDeleted === "number" &&
117
- isBriefingBase(value.briefing));
118
- }
119
- return (isRecord(value.counts) &&
120
- typeof value.counts.entitiesAdded === "number" &&
121
- typeof value.counts.entitiesModified === "number" &&
122
- typeof value.counts.entitiesRemoved === "number" &&
123
- typeof value.counts.relationshipsChanged === "number" &&
124
- isRecord(value.changes) &&
125
- isRecord(value.changes.entities) &&
126
- Array.isArray(value.changes.entities.added) &&
127
- value.changes.entities.added.every(isChangeItem) &&
128
- Array.isArray(value.changes.entities.modified) &&
129
- value.changes.entities.modified.every(isChangeItem) &&
130
- Array.isArray(value.changes.entities.removed) &&
131
- value.changes.entities.removed.every(isChangeItem) &&
132
- isRecord(value.changes.relationships) &&
133
- typeof value.changes.relationships.changed === "number" &&
134
- isBriefingV2(value.briefing));
135
- }
136
- export function createBriefId() {
137
- // implements REQ-opencode-kibi-briefing-v4
138
- return `brief-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
139
- }
140
- export function computeContentHash(payload) {
141
- // implements REQ-opencode-kibi-briefing-v4
142
- const env = payload;
143
- // Normalize string: trim and collapse internal whitespace
144
- const norm = (s) => s.trim().replace(/\s+/g, " ");
145
- const normalizeCitations = (citations) => citations.map((c) => ({
146
- id: c.id,
147
- ...(c.type ? { type: norm(c.type) } : {}),
148
- ...(c.title ? { title: norm(c.title) } : {}),
149
- ...(c.source ? { source: norm(c.source) } : {}),
150
- ...(c.textRef ? { textRef: norm(c.textRef) } : {}),
151
- }));
152
- const normalizeStatements = (statements = []) => statements.map((statement) => ({
153
- statement: norm(statement.statement),
154
- citationIds: statement.citationIds,
155
- }));
156
- const normalizeReasonItems = (items = []) => items
157
- .map((item) => ({
158
- kind: item.kind,
159
- text: norm(item.text),
160
- entityIds: [...item.entityIds].sort(),
161
- ...(item.citationIds ? { citationIds: [...item.citationIds].sort() } : {}),
162
- ...(item.severity ? { severity: item.severity } : {}),
163
- }))
164
- .filter((item) => hasRenderableText(item.text));
165
- const normalizeDeliveryReasons = (deliveryReasons) => {
166
- if (!deliveryReasons)
167
- return undefined;
168
- const items = normalizeReasonItems(deliveryReasons.items);
169
- if (items.length === 0)
170
- return undefined;
171
- return {
172
- version: deliveryReasons.version,
173
- toast: {
174
- title: norm(deliveryReasons.toast.title),
175
- summary: norm(deliveryReasons.toast.summary),
176
- whyItMatters: norm(deliveryReasons.toast.whyItMatters),
177
- },
178
- items,
179
- };
180
- };
181
- const normalizeChangeItems = (items) => items.map((item) => ({
182
- id: item.id,
183
- type: norm(item.type),
184
- ...(item.title ? { title: norm(item.title) } : {}),
185
- ...(item.source ? { source: norm(item.source) } : {}),
186
- ...(item.textRef ? { textRef: norm(item.textRef) } : {}),
187
- }));
188
- // Build canonical visible-content projection (ignoring volatile fields)
189
- const projection = env.schemaVersion === "2.0"
190
- ? {
191
- schemaVersion: "2.0",
192
- type: env.type,
193
- summary: norm(env.summary),
194
- counts: env.counts,
195
- changes: {
196
- entities: {
197
- added: normalizeChangeItems(env.changes.entities.added),
198
- modified: normalizeChangeItems(env.changes.entities.modified),
199
- removed: normalizeChangeItems(env.changes.entities.removed),
200
- },
201
- relationships: {
202
- changed: env.changes.relationships.changed,
203
- },
204
- },
205
- briefing: {
206
- tldr: norm(env.briefing.tldr),
207
- normalizedPromptBlock: norm(env.briefing.promptBlock),
208
- citations: normalizeCitations(env.briefing.citations ?? []),
209
- changeNarrative: env.briefing.changeNarrative.map((line) => norm(line)),
210
- deliveryReasons: normalizeDeliveryReasons(env.briefing.deliveryReasons),
211
- constraints: normalizeStatements(env.briefing.constraints),
212
- regressionRisks: normalizeStatements(env.briefing.regressionRisks),
213
- missingEvidence: normalizeStatements(env.briefing.missingEvidence),
214
- },
215
- validation: {
216
- count: env.validation.count,
217
- violations: env.validation.violations.map((v) => ({
218
- rule: v.rule,
219
- entityId: v.entityId,
220
- description: norm(v.description),
221
- })),
222
- },
223
- }
224
- : {
225
- type: env.type,
226
- summary: norm(env.summary),
227
- counts: env.counts,
228
- briefing: {
229
- tldr: norm(env.briefing.tldr),
230
- normalizedPromptBlock: norm(env.briefing.promptBlock),
231
- citations: (env.briefing.citations ?? []).map((c) => ({
232
- id: c.id,
233
- title: c.title ?? "",
234
- })),
235
- constraints: (env.briefing.constraints ?? []).map((c) => ({
236
- statement: norm(c.statement),
237
- citationIds: c.citationIds,
238
- })),
239
- regressionRisks: (env.briefing.regressionRisks ?? []).map((r) => ({
240
- statement: norm(r.statement),
241
- citationIds: r.citationIds,
242
- })),
243
- missingEvidence: (env.briefing.missingEvidence ?? []).map((m) => ({
244
- statement: norm(m.statement),
245
- citationIds: m.citationIds,
246
- })),
247
- deliveryReasons: normalizeDeliveryReasons(env.briefing.deliveryReasons),
248
- },
249
- validation: {
250
- count: env.validation.count,
251
- violations: env.validation.violations.map((v) => ({
252
- rule: v.rule,
253
- entityId: v.entityId,
254
- description: norm(v.description),
255
- })),
256
- },
257
- };
258
- return crypto
259
- .createHash("sha256")
260
- .update(JSON.stringify(projection))
261
- .digest("hex");
262
- }
@@ -1,11 +0,0 @@
1
- export interface SessionFingerprintInput {
2
- sessionId?: string | undefined;
3
- branch: string;
4
- worktree: string;
5
- }
6
- export interface SessionBaselineState<Cursor> {
7
- fingerprint: string | null;
8
- cursor: Cursor | null;
9
- }
10
- export declare function buildSessionFingerprint(input: SessionFingerprintInput): string;
11
- export declare function syncSessionBaselineState<Cursor>(state: SessionBaselineState<Cursor>, input: SessionFingerprintInput, captureBaseline: () => Cursor | null): SessionBaselineState<Cursor>;
@@ -1,21 +0,0 @@
1
- export function buildSessionFingerprint(
2
- // implements REQ-opencode-kibi-briefing-v6
3
- input) {
4
- return [
5
- input.sessionId?.trim() || "unknown",
6
- input.branch,
7
- input.worktree,
8
- ].join("\0");
9
- }
10
- export function syncSessionBaselineState(
11
- // implements REQ-opencode-kibi-briefing-v6
12
- state, input, captureBaseline) {
13
- const fingerprint = buildSessionFingerprint(input);
14
- if (state.fingerprint === fingerprint) {
15
- return state;
16
- }
17
- return {
18
- fingerprint,
19
- cursor: captureBaseline(),
20
- };
21
- }
@@ -1,67 +0,0 @@
1
- import type { ToastCapableClient as SendToastCapableClient } from "./toast.js";
2
- import type { IdleBriefEnvelope } from "./idle-brief-store.js";
3
- export type ToastPayload = {
4
- variant?: "info" | "success" | "warning" | "error";
5
- title?: string;
6
- message: string;
7
- duration?: number;
8
- };
9
- export type ToastCapableClient = {
10
- tui?: {
11
- showToast?: (payload: {
12
- body: ToastPayload;
13
- }) => void | Promise<void>;
14
- };
15
- };
16
- export type SharedBriefPolicy = {
17
- briefs: {
18
- enabled: boolean;
19
- channels: {
20
- tui: boolean;
21
- vscode: boolean;
22
- };
23
- tui: {
24
- toast: boolean;
25
- };
26
- };
27
- };
28
- export type LocalBriefConfig = {
29
- autoSubmit: boolean;
30
- };
31
- export type DeliverResult = {
32
- delivered: boolean;
33
- };
34
- /**
35
- * Delivers a Kibi briefing to the TUI via toast notification.
36
- *
37
- * Uses the REAL OpenCode plugin API:
38
- * - client.tui.showToast(payload) — primary (and only) delivery mechanism
39
- *
40
- * The toast contains a rich summary from the envelope and is displayed
41
- * for 8 seconds so users can read the content.
42
- *
43
- * @param client - OpenCode client with optional TUI capabilities
44
- * @param envelope - Idle brief envelope containing briefing content
45
- * @param sharedPolicy - Shared brief policy from `.kb/config.json`
46
- * @param localConfig - Local OpenCode config
47
- */
48
- export declare function deliverBriefTui(client: ToastCapableClient, envelope: IdleBriefEnvelope, sharedPolicy: SharedBriefPolicy, _localConfig: LocalBriefConfig): Promise<DeliverResult>;
49
- /**
50
- * Client type for announcement-only TUI delivery.
51
- * Extends toast capability with the SDK command bridge.
52
- */
53
- export type AnnouncementClient = SendToastCapableClient;
54
- export type AnnouncementResult = {
55
- toastDelivered: boolean;
56
- commandPublished: boolean;
57
- };
58
- /**
59
- * Announcement-only TUI delivery coordinator.
60
- *
61
- * Sends the summary toast and invokes the official SDK bridge
62
- * (`executeCommand`) but does NOT mutate read/seen state.
63
- * The caller is responsible for any state transitions after the
64
- * TUI route confirms render success.
65
- */
66
- export declare function announceBriefTui(// implements REQ-opencode-kibi-briefing-v6
67
- client: AnnouncementClient, envelope: IdleBriefEnvelope, sharedPolicy: SharedBriefPolicy): Promise<AnnouncementResult>;
@@ -1,279 +0,0 @@
1
- /*
2
- * Kibi — repo-local, per-branch, queryable long-term memory for software projects
3
- * Copyright (C) 2026 Piotr Franczyk
4
- *
5
- * This program is free software: you can redistribute it and/or modify
6
- * it under the terms of the GNU Affero General Public License as published by
7
- * the Free Software Foundation, either version 3 of the License, or
8
- * (at your option) any later version.
9
- */
10
- import { sendToast } from "./toast.js";
11
- import { renderToastSummary } from "./brief-delivery-reasons.js";
12
- import * as logger from "./logger.js";
13
- function firstNonEmpty(...values) {
14
- for (const value of values) {
15
- const trimmed = value?.trim();
16
- if (trimmed) {
17
- return trimmed;
18
- }
19
- }
20
- return undefined;
21
- }
22
- function buildTuiBriefMessage(envelope) {
23
- const lines = [];
24
- const briefing = envelope.briefing;
25
- const deliveryReasons = briefing.deliveryReasons;
26
- const renderedToast = deliveryReasons?.items?.length
27
- ? renderToastSummary(deliveryReasons)
28
- : undefined;
29
- const whatChanged = renderedToast
30
- ? [renderedToast.summary]
31
- : envelope.schemaVersion === "2.0"
32
- ? envelope.briefing.changeNarrative.map((line) => line.trim()).filter(Boolean).filter((line) => !line.includes(".sisyphus/"))
33
- : [];
34
- lines.push("## What changed");
35
- if (whatChanged.length > 0) {
36
- lines.push(...whatChanged.slice(0, 2));
37
- }
38
- else if (envelope.schemaVersion === "2.0") {
39
- const fallbackEntity = envelope.changes.entities.modified[0] ?? envelope.changes.entities.added[0];
40
- if (fallbackEntity) {
41
- const action = envelope.changes.entities.modified[0] ? "Modified" : "Added";
42
- lines.push(`${action} ${fallbackEntity.id}: ${fallbackEntity.title ?? "Untitled"}`);
43
- }
44
- else {
45
- const fallback = firstNonEmpty(envelope.summary, envelope.briefing.tldr);
46
- if (fallback)
47
- lines.push(fallback);
48
- }
49
- }
50
- else {
51
- const fallback = firstNonEmpty(envelope.summary, envelope.briefing.tldr);
52
- if (fallback)
53
- lines.push(fallback);
54
- }
55
- lines.push("");
56
- const whyItMatters = firstNonEmpty(deliveryReasons?.items?.length ? renderedToast?.whyItMatters : undefined);
57
- if (whyItMatters) {
58
- lines.push("## Why it matters");
59
- lines.push(whyItMatters);
60
- lines.push("");
61
- }
62
- const hasKnowledgeImpact = envelope.briefing.citations.length > 0 ||
63
- (envelope.briefing.constraints?.length ?? 0) > 0 ||
64
- (envelope.briefing.regressionRisks?.length ?? 0) > 0;
65
- if (hasKnowledgeImpact) {
66
- lines.push("## Project knowledge impact");
67
- if (envelope.briefing.citations.length > 0) {
68
- for (const citation of envelope.briefing.citations) {
69
- lines.push(`- **${citation.id}**${citation.title ? `: ${citation.title}` : ""}${citation.source ? ` (${citation.source})` : ""}`);
70
- }
71
- }
72
- if ((envelope.briefing.constraints?.length ?? 0) > 0) {
73
- for (const constraint of envelope.briefing.constraints ?? []) {
74
- lines.push(`- ${constraint.statement}`);
75
- }
76
- }
77
- if ((envelope.briefing.regressionRisks?.length ?? 0) > 0) {
78
- for (const risk of envelope.briefing.regressionRisks ?? []) {
79
- lines.push(`- ${risk.statement}`);
80
- }
81
- }
82
- lines.push("");
83
- }
84
- const hasMissingEvidence = (envelope.briefing.missingEvidence?.length ?? 0) > 0;
85
- if (envelope.validation.count > 0 || hasMissingEvidence) {
86
- lines.push("## Interpretation note");
87
- if (envelope.validation.count > 0) {
88
- lines.push(`Validation checks reported unresolved items: ${envelope.validation.count} issue(s).`);
89
- }
90
- if (hasMissingEvidence) {
91
- lines.push("This brief includes unresolved evidence notes:");
92
- for (const item of envelope.briefing.missingEvidence ?? []) {
93
- lines.push(`- ${item.statement}`);
94
- }
95
- }
96
- lines.push("");
97
- }
98
- while (lines.length > 0 && lines[lines.length - 1] === "") {
99
- lines.pop();
100
- }
101
- const result = lines.join("\n");
102
- if (result === "## What changed") {
103
- return undefined;
104
- }
105
- return result;
106
- }
107
- function buildTuiBriefToastPayload(envelope) {
108
- const message = buildTuiBriefMessage(envelope);
109
- if (message === undefined) {
110
- return undefined;
111
- }
112
- return {
113
- variant: envelope.type === "warning" ? "warning" : "info",
114
- title: "Kibi Knowledge Update",
115
- message,
116
- duration: 8000,
117
- };
118
- }
119
- function hasSignificantBriefingImpact(envelope) {
120
- const briefing = envelope.briefing;
121
- return !(briefing.citations.length === 0 &&
122
- (!briefing.constraints || briefing.constraints.length === 0) &&
123
- (!briefing.regressionRisks || briefing.regressionRisks.length === 0) &&
124
- (!briefing.missingEvidence || briefing.missingEvidence.length === 0));
125
- }
126
- function isNoOpBriefEnvelope(envelope) {
127
- const counts = envelope.counts;
128
- const zeroCounts = "relationshipsChanged" in counts
129
- ? counts.entitiesAdded === 0 &&
130
- counts.entitiesModified === 0 &&
131
- counts.entitiesRemoved === 0 &&
132
- counts.relationshipsChanged === 0
133
- : counts.requirementsAdded === 0 &&
134
- counts.relationshipsAdded === 0 &&
135
- counts.entitiesDeleted === 0;
136
- const briefing = envelope.briefing;
137
- const hasDeliveryReasons = (briefing.deliveryReasons?.items.length ?? 0) > 0;
138
- if (hasDeliveryReasons) {
139
- const toast = briefing.deliveryReasons ? renderToastSummary(briefing.deliveryReasons) : undefined;
140
- if (toast === undefined)
141
- return true; // all operational → no-op
142
- return false;
143
- }
144
- // Suppress legacy (no deliveryReasons) envelopes only when all three conditions hold:
145
- // zero change counts, no validation issues, and no significant briefing impact.
146
- // Matching summary/tldr alone is not sufficient — a domain-specific brief may legitimately
147
- // have the same value in both fields.
148
- return (zeroCounts &&
149
- envelope.validation.count === 0 &&
150
- !hasSignificantBriefingImpact(envelope));
151
- }
152
- function getEnvelopeChangeTotal(envelope) {
153
- const counts = envelope.counts;
154
- return "relationshipsChanged" in counts
155
- ? counts.entitiesAdded +
156
- counts.entitiesModified +
157
- counts.entitiesRemoved +
158
- counts.relationshipsChanged
159
- : counts.requirementsAdded + counts.relationshipsAdded + counts.entitiesDeleted;
160
- }
161
- /**
162
- * Delivers a Kibi briefing to the TUI via toast notification.
163
- *
164
- * Uses the REAL OpenCode plugin API:
165
- * - client.tui.showToast(payload) — primary (and only) delivery mechanism
166
- *
167
- * The toast contains a rich summary from the envelope and is displayed
168
- * for 8 seconds so users can read the content.
169
- *
170
- * @param client - OpenCode client with optional TUI capabilities
171
- * @param envelope - Idle brief envelope containing briefing content
172
- * @param sharedPolicy - Shared brief policy from `.kb/config.json`
173
- * @param localConfig - Local OpenCode config
174
- */
175
- // implements REQ-opencode-kibi-briefing-v4
176
- export async function deliverBriefTui(client, envelope, sharedPolicy, _localConfig) {
177
- // Early exit if TUI delivery is disabled
178
- if (!sharedPolicy.briefs.channels.tui) {
179
- logger.info("TUI brief delivery disabled by shared policy");
180
- return { delivered: false };
181
- }
182
- const tui = client.tui;
183
- // Toast is the primary delivery mechanism
184
- if (sharedPolicy.briefs.tui.toast && typeof tui?.showToast === "function") {
185
- if (isNoOpBriefEnvelope(envelope)) {
186
- return { delivered: false };
187
- }
188
- try {
189
- const message = buildTuiBriefMessage(envelope);
190
- if (message === undefined) {
191
- return { delivered: false };
192
- }
193
- await tui.showToast({
194
- body: {
195
- variant: envelope.type === "warning" ? "warning" : "info",
196
- title: "Kibi Knowledge Update",
197
- message,
198
- duration: 8000,
199
- },
200
- });
201
- return { delivered: true };
202
- }
203
- catch (err) {
204
- logger.error("Failed to deliver brief toast", {
205
- event: "idle_brief_toast_failed",
206
- error: err instanceof Error ? err.message : String(err),
207
- });
208
- return { delivered: false };
209
- }
210
- }
211
- else {
212
- logger.info("TUI showToast API unavailable, brief not delivered");
213
- return { delivered: false };
214
- }
215
- }
216
- /**
217
- * Announcement-only TUI delivery coordinator.
218
- *
219
- * Sends the summary toast and invokes the official SDK bridge
220
- * (`executeCommand`) but does NOT mutate read/seen state.
221
- * The caller is responsible for any state transitions after the
222
- * TUI route confirms render success.
223
- */
224
- export async function announceBriefTui(// implements REQ-opencode-kibi-briefing-v6
225
- client, envelope, sharedPolicy) {
226
- if (!sharedPolicy.briefs.channels.tui) {
227
- logger.info("TUI brief delivery disabled by shared policy");
228
- return { toastDelivered: false, commandPublished: false };
229
- }
230
- const briefing = envelope.briefing;
231
- if (!envelope.unread &&
232
- isNoOpBriefEnvelope(envelope) &&
233
- !(briefing.deliveryReasons?.items.length ?? 0)) {
234
- return { toastDelivered: false, commandPublished: false };
235
- }
236
- const totalChanges = getEnvelopeChangeTotal(envelope);
237
- const hasDeliveryReasons = (briefing.deliveryReasons?.items.length ?? 0) > 0;
238
- if (!envelope.unread &&
239
- totalChanges === 0 &&
240
- envelope.validation.count === 0 &&
241
- !hasDeliveryReasons &&
242
- isNoOpBriefEnvelope(envelope)) {
243
- return { toastDelivered: false, commandPublished: false };
244
- }
245
- let toastDelivered = false;
246
- let commandPublished = false;
247
- if (sharedPolicy.briefs.tui.toast) {
248
- const payload = buildTuiBriefToastPayload(envelope);
249
- if (payload !== undefined) {
250
- const toastResult = await sendToast(client, payload);
251
- if (toastResult.status === "delivered") {
252
- toastDelivered = true;
253
- }
254
- else if (toastResult.status === "failed") {
255
- logger.error("Failed to deliver brief toast", {
256
- event: "idle_brief_toast_failed",
257
- error: toastResult.error ?? toastResult.reason,
258
- });
259
- }
260
- else {
261
- logger.info("TUI showToast API unavailable, brief not delivered");
262
- }
263
- }
264
- }
265
- // Step 2: Invoke the SDK command bridge to open the brief in the TUI
266
- if (typeof client.tui?.executeCommand === "function") {
267
- try {
268
- await client.tui.executeCommand("kibi.open_latest_brief", {});
269
- commandPublished = true;
270
- }
271
- catch (err) {
272
- logger.error("Failed to publish open_latest_brief command", {
273
- event: "idle_brief_command_failed",
274
- error: err instanceof Error ? err.message : String(err),
275
- });
276
- }
277
- }
278
- return { toastDelivered, commandPublished };
279
- }