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
@@ -0,0 +1,209 @@
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 { renderFullBriefReasons, renderToastSummary } from "./brief-delivery-reasons.js";
11
+ // ─── Helpers ───────────────────────────────────────────────────────────────
12
+ function firstNonEmpty(...values) {
13
+ for (const value of values) {
14
+ const trimmed = value?.trim();
15
+ if (trimmed) {
16
+ return trimmed;
17
+ }
18
+ }
19
+ return undefined;
20
+ }
21
+ function isOperationalByEntityIds(item) {
22
+ if (item.entityIds.length === 0)
23
+ return false;
24
+ return item.entityIds.every((id) => {
25
+ const dashIdx = id.indexOf("-");
26
+ if (dashIdx < 0)
27
+ return false;
28
+ return /\.[a-zA-Z0-9]+$/.test(id.slice(dashIdx + 1));
29
+ });
30
+ }
31
+ function isOperationalDeliveryItem(item, allItems) {
32
+ if (item.kind === "relationship_changed") {
33
+ // relationship_changed items have no entityIds; treat as operational when
34
+ // all entity-level items in the set are operational (they're relationship side-effects)
35
+ const entityItems = allItems.filter((i) => i.kind === "entity_added" || i.kind === "entity_modified" || i.kind === "entity_removed");
36
+ return entityItems.length > 0 && entityItems.every(isOperationalByEntityIds);
37
+ }
38
+ return isOperationalByEntityIds(item);
39
+ }
40
+ function deriveWhatChanged(envelope) {
41
+ const briefing = envelope.briefing;
42
+ const deliveryReasons = briefing.deliveryReasons;
43
+ if (deliveryReasons?.items?.length) {
44
+ const domainItems = deliveryReasons.items.filter((item) => !isOperationalDeliveryItem(item, deliveryReasons.items));
45
+ if (domainItems.length > 0) {
46
+ return domainItems.map((item) => item.text);
47
+ }
48
+ }
49
+ if (envelope.schemaVersion === "2.0") {
50
+ const narrative = envelope.briefing.changeNarrative
51
+ .map((line) => line.trim())
52
+ .filter(Boolean)
53
+ .filter((line) => !line.includes(".sisyphus/"));
54
+ if (narrative.length > 0) {
55
+ return narrative.slice(0, 2);
56
+ }
57
+ const fallbackEntity = envelope.changes.entities.modified[0] ??
58
+ envelope.changes.entities.added[0];
59
+ if (fallbackEntity) {
60
+ const action = envelope.changes.entities.modified[0] ? "Modified" : "Added";
61
+ return [
62
+ `${action} ${fallbackEntity.id}: ${fallbackEntity.title ?? "Untitled"}`,
63
+ ];
64
+ }
65
+ }
66
+ const fallback = firstNonEmpty(envelope.summary, envelope.briefing.tldr);
67
+ return fallback ? [fallback] : [];
68
+ }
69
+ // ─── Public API ────────────────────────────────────────────────────────────
70
+ /**
71
+ * Build a structured view model from a persisted brief envelope.
72
+ *
73
+ * Derives all route-rendering data (title, sections, citations, counts) from
74
+ * the envelope without regenerating any content. Supports both schema 1.0 and
75
+ * 2.0 during the migration window.
76
+ *
77
+ * @param envelope - The persisted brief envelope
78
+ * @returns A deterministic view model suitable for route rendering
79
+ */
80
+ export function buildTuiBriefViewModel(// implements REQ-opencode-kibi-briefing-v6
81
+ envelope) {
82
+ const briefing = envelope.briefing;
83
+ const deliveryReasons = briefing.deliveryReasons;
84
+ let title = firstNonEmpty(envelope.summary, envelope.briefing.tldr);
85
+ if (deliveryReasons?.items?.length) {
86
+ const filteredToast = renderToastSummary(deliveryReasons);
87
+ if (filteredToast) {
88
+ title = filteredToast.summary;
89
+ }
90
+ }
91
+ else if (envelope.schemaVersion === "2.0" && envelope.briefing.changeNarrative.length > 0) {
92
+ title = envelope.briefing.changeNarrative[0]?.trim() ?? title;
93
+ }
94
+ const base = {
95
+ briefId: envelope.briefId,
96
+ schemaVersion: envelope.schemaVersion,
97
+ branch: envelope.branch,
98
+ createdAt: envelope.createdAt,
99
+ type: envelope.type,
100
+ unread: envelope.unread,
101
+ contentHash: envelope.contentHash,
102
+ title: title ?? "Kibi Brief",
103
+ whatChanged: deriveWhatChanged(envelope),
104
+ whyItMatters: deliveryReasons?.items?.length
105
+ ? renderToastSummary(deliveryReasons)?.whyItMatters || undefined
106
+ : undefined,
107
+ knowledgeImpact: {
108
+ citations: envelope.briefing.citations,
109
+ constraints: envelope.briefing.constraints ?? [],
110
+ regressionRisks: envelope.briefing.regressionRisks ?? [],
111
+ },
112
+ interpretationNote: {
113
+ validationCount: envelope.validation.count,
114
+ missingEvidence: envelope.briefing.missingEvidence ?? [],
115
+ },
116
+ };
117
+ if (envelope.schemaVersion === "2.0") {
118
+ return {
119
+ ...base,
120
+ counts: {
121
+ schemaVersion: "2.0",
122
+ entitiesAdded: envelope.counts.entitiesAdded,
123
+ entitiesModified: envelope.counts.entitiesModified,
124
+ entitiesRemoved: envelope.counts.entitiesRemoved,
125
+ relationshipsChanged: envelope.counts.relationshipsChanged,
126
+ },
127
+ };
128
+ }
129
+ return {
130
+ ...base,
131
+ counts: {
132
+ schemaVersion: "1.0",
133
+ requirementsAdded: envelope.counts.requirementsAdded,
134
+ relationshipsAdded: envelope.counts.relationshipsAdded,
135
+ entitiesDeleted: envelope.counts.entitiesDeleted,
136
+ },
137
+ };
138
+ }
139
+ /**
140
+ * Build a short summary text from a persisted brief envelope.
141
+ *
142
+ * Reuses the same section-building logic as `buildTuiBriefMessage` from
143
+ * `tui-brief-delivery.ts`, producing a deterministic plain-text summary
144
+ * suitable for TUI route rendering or server-side summary generation.
145
+ *
146
+ * @param envelope - The persisted brief envelope
147
+ * @returns A multi-line summary string
148
+ */
149
+ export function buildTuiBriefSummary(envelope) {
150
+ const briefing = envelope.briefing;
151
+ const deliveryReasons = briefing.deliveryReasons;
152
+ if (deliveryReasons?.items?.length) {
153
+ return renderFullBriefReasons(deliveryReasons);
154
+ }
155
+ const lines = [];
156
+ // What changed
157
+ lines.push("## What changed");
158
+ lines.push(...deriveWhatChanged(envelope));
159
+ lines.push("");
160
+ // Why it matters
161
+ if (deliveryReasons?.items?.length) {
162
+ lines.push("## Why it matters");
163
+ lines.push(deliveryReasons.toast.whyItMatters || "");
164
+ lines.push("");
165
+ }
166
+ // Project knowledge impact
167
+ const hasKnowledgeImpact = envelope.briefing.citations.length > 0 ||
168
+ (envelope.briefing.constraints?.length ?? 0) > 0 ||
169
+ (envelope.briefing.regressionRisks?.length ?? 0) > 0;
170
+ if (hasKnowledgeImpact) {
171
+ lines.push("## Project knowledge impact");
172
+ if (envelope.briefing.citations.length > 0) {
173
+ for (const citation of envelope.briefing.citations) {
174
+ lines.push(`- **${citation.id}**${citation.title ? `: ${citation.title}` : ""}${citation.source ? ` (${citation.source})` : ""}`);
175
+ }
176
+ }
177
+ if ((envelope.briefing.constraints?.length ?? 0) > 0) {
178
+ for (const constraint of envelope.briefing.constraints ?? []) {
179
+ lines.push(`- ${constraint.statement}`);
180
+ }
181
+ }
182
+ if ((envelope.briefing.regressionRisks?.length ?? 0) > 0) {
183
+ for (const risk of envelope.briefing.regressionRisks ?? []) {
184
+ lines.push(`- ${risk.statement}`);
185
+ }
186
+ }
187
+ lines.push("");
188
+ }
189
+ // Interpretation note
190
+ const hasMissingEvidence = (envelope.briefing.missingEvidence?.length ?? 0) > 0;
191
+ if (envelope.validation.count > 0 || hasMissingEvidence) {
192
+ lines.push("## Interpretation note");
193
+ if (envelope.validation.count > 0) {
194
+ lines.push(`Validation checks reported unresolved items: ${envelope.validation.count} issue(s).`);
195
+ }
196
+ if (hasMissingEvidence) {
197
+ lines.push("This brief includes unresolved evidence notes:");
198
+ for (const item of envelope.briefing.missingEvidence ?? []) {
199
+ lines.push(`- ${item.statement}`);
200
+ }
201
+ }
202
+ lines.push("");
203
+ }
204
+ // Trim trailing blank lines
205
+ while (lines.length > 0 && lines[lines.length - 1] === "") {
206
+ lines.pop();
207
+ }
208
+ return lines.join("\n");
209
+ }
package/dist/tui.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import type { TuiPlugin } from "@opencode-ai/plugin/tui";
2
+ declare const tui: TuiPlugin;
3
+ export { tui };
4
+ declare const _default: {
5
+ readonly id: "kibi-opencode";
6
+ readonly tui: TuiPlugin;
7
+ };
8
+ export default _default;
package/dist/tui.js ADDED
@@ -0,0 +1,413 @@
1
+ // @bun
2
+ // src/idle-brief-reader.ts
3
+ import * as fs from "fs";
4
+ import * as path2 from "path";
5
+
6
+ // src/idle-brief-paths.ts
7
+ import * as path from "path";
8
+ import { loadBriefConfig } from "kibi-cli/brief-config";
9
+ function resolveBriefsDir(workspaceRoot) {
10
+ return path.join(workspaceRoot, ".kb", "briefs");
11
+ }
12
+
13
+ // src/idle-brief-store.ts
14
+ function isRecord(value) {
15
+ return typeof value === "object" && value !== null;
16
+ }
17
+ function isStringArray(value) {
18
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
19
+ }
20
+ function isCitation(value) {
21
+ return isRecord(value) && typeof value.id === "string";
22
+ }
23
+ function isStatement(value) {
24
+ return isRecord(value) && typeof value.statement === "string" && isStringArray(value.citationIds);
25
+ }
26
+ function isValidationViolation(value) {
27
+ return isRecord(value) && typeof value.rule === "string" && typeof value.entityId === "string" && typeof value.description === "string";
28
+ }
29
+ function isValidationDiagnostic(value) {
30
+ return isRecord(value) && typeof value.category === "string" && typeof value.severity === "string" && typeof value.message === "string";
31
+ }
32
+ function isAuditCursor(value) {
33
+ return isRecord(value) && typeof value.lastTimestamp === "string" && typeof value.lastOperation === "string" && typeof value.entryCount === "number" && typeof value.fileSize === "number";
34
+ }
35
+ function isValidation(value) {
36
+ return isRecord(value) && Array.isArray(value.violations) && value.violations.every(isValidationViolation) && typeof value.count === "number" && Array.isArray(value.diagnostics) && value.diagnostics.every(isValidationDiagnostic);
37
+ }
38
+ function isBriefingBase(value) {
39
+ return isRecord(value) && typeof value.tldr === "string" && typeof value.promptBlock === "string" && Array.isArray(value.citations) && value.citations.every(isCitation) && (value.constraints === undefined || Array.isArray(value.constraints) && value.constraints.every(isStatement)) && (value.regressionRisks === undefined || Array.isArray(value.regressionRisks) && value.regressionRisks.every(isStatement)) && (value.missingEvidence === undefined || Array.isArray(value.missingEvidence) && value.missingEvidence.every(isStatement));
40
+ }
41
+ function isBriefingV2(value) {
42
+ return isBriefingBase(value) && isStringArray(value.changeNarrative) && (value.deliveryReasons === undefined || isDeliveryReasons(value.deliveryReasons));
43
+ }
44
+ function isReasonItem(value) {
45
+ return isRecord(value) && typeof value.kind === "string" && typeof value.text === "string" && isStringArray(value.entityIds) && (value.citationIds === undefined || isStringArray(value.citationIds)) && (value.severity === undefined || value.severity === "info" || value.severity === "warning" || value.severity === "error");
46
+ }
47
+ function isDeliveryReasons(value) {
48
+ return isRecord(value) && value.version === 1 && isRecord(value.toast) && typeof value.toast.title === "string" && typeof value.toast.summary === "string" && typeof value.toast.whyItMatters === "string" && Array.isArray(value.items) && value.items.every(isReasonItem);
49
+ }
50
+ function isChangeItem(value) {
51
+ return isRecord(value) && typeof value.id === "string" && typeof value.type === "string";
52
+ }
53
+ function isIdleBriefEnvelope(value) {
54
+ if (!isRecord(value))
55
+ return false;
56
+ const hasBaseFields = (value.schemaVersion === "1.0" || value.schemaVersion === "2.0") && typeof value.briefId === "string" && (value.type === "success" || value.type === "warning") && typeof value.sessionId === "string" && typeof value.branch === "string" && typeof value.createdAt === "string" && typeof value.unread === "boolean" && isAuditCursor(value.auditCursor) && typeof value.summary === "string" && isValidation(value.validation) && typeof value.contentHash === "string";
57
+ if (!hasBaseFields)
58
+ return false;
59
+ if (value.schemaVersion === "1.0") {
60
+ return isRecord(value.counts) && typeof value.counts.requirementsAdded === "number" && typeof value.counts.relationshipsAdded === "number" && typeof value.counts.entitiesDeleted === "number" && isBriefingBase(value.briefing);
61
+ }
62
+ return isRecord(value.counts) && typeof value.counts.entitiesAdded === "number" && typeof value.counts.entitiesModified === "number" && typeof value.counts.entitiesRemoved === "number" && typeof value.counts.relationshipsChanged === "number" && isRecord(value.changes) && isRecord(value.changes.entities) && Array.isArray(value.changes.entities.added) && value.changes.entities.added.every(isChangeItem) && Array.isArray(value.changes.entities.modified) && value.changes.entities.modified.every(isChangeItem) && Array.isArray(value.changes.entities.removed) && value.changes.entities.removed.every(isChangeItem) && isRecord(value.changes.relationships) && typeof value.changes.relationships.changed === "number" && isBriefingV2(value.briefing);
63
+ }
64
+
65
+ // src/idle-brief-reader.ts
66
+ var BRIEF_FILENAME_RE = /^(\d+)_brief\.json$/;
67
+ var TUI_SEEN_FILE = ".tui-seen.json";
68
+ function resolveTuiSeenPath(workspaceRoot) {
69
+ return path2.join(resolveBriefsDir(workspaceRoot), TUI_SEEN_FILE);
70
+ }
71
+ function markBriefTuiSeen(workspaceRoot, branch, contentHash) {
72
+ const briefsDir = resolveBriefsDir(workspaceRoot);
73
+ fs.mkdirSync(briefsDir, { recursive: true });
74
+ const seenPath = resolveTuiSeenPath(workspaceRoot);
75
+ let parsed = {};
76
+ try {
77
+ const raw = JSON.parse(fs.readFileSync(seenPath, "utf-8"));
78
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
79
+ parsed = raw;
80
+ }
81
+ } catch {}
82
+ const existing = Array.isArray(parsed[branch]) ? parsed[branch] : [];
83
+ const next = [contentHash, ...existing.filter((entry) => entry !== contentHash)].slice(0, 100);
84
+ parsed[branch] = next;
85
+ const tempPath = `${seenPath}.tmp`;
86
+ fs.writeFileSync(tempPath, JSON.stringify(parsed, null, 2), "utf-8");
87
+ fs.renameSync(tempPath, seenPath);
88
+ }
89
+ function extractTimestamp(filename) {
90
+ const match = filename.match(BRIEF_FILENAME_RE);
91
+ if (!match)
92
+ return null;
93
+ return Number(match[1]);
94
+ }
95
+ function scanBriefs(workspaceRoot, branch, filterUnread) {
96
+ const briefsDir = resolveBriefsDir(workspaceRoot);
97
+ if (!fs.existsSync(briefsDir)) {
98
+ return null;
99
+ }
100
+ const files = fs.readdirSync(briefsDir);
101
+ const candidates = [];
102
+ for (const file of files) {
103
+ if (file.endsWith(".tmp"))
104
+ continue;
105
+ const timestamp = extractTimestamp(file);
106
+ if (timestamp === null)
107
+ continue;
108
+ const filePath = path2.join(briefsDir, file);
109
+ let envelope;
110
+ try {
111
+ const raw = fs.readFileSync(filePath, "utf-8");
112
+ const parsed = JSON.parse(raw);
113
+ if (!isIdleBriefEnvelope(parsed)) {
114
+ continue;
115
+ }
116
+ envelope = parsed;
117
+ } catch {
118
+ continue;
119
+ }
120
+ if (envelope.branch === branch && (envelope.schemaVersion === "1.0" || envelope.schemaVersion === "2.0") && (!filterUnread || envelope.unread === true)) {
121
+ candidates.push({ timestamp, envelope, filePath });
122
+ }
123
+ }
124
+ if (candidates.length === 0) {
125
+ return null;
126
+ }
127
+ candidates.sort((a, b) => b.timestamp - a.timestamp);
128
+ const latest = candidates[0];
129
+ if (!latest) {
130
+ return null;
131
+ }
132
+ return {
133
+ envelope: latest.envelope,
134
+ filePath: latest.filePath
135
+ };
136
+ }
137
+ function selectLatestPersistedBrief(workspaceRoot, branch) {
138
+ return scanBriefs(workspaceRoot, branch, false);
139
+ }
140
+ function markBriefRead(workspaceRoot, briefPath) {
141
+ const briefsDir = resolveBriefsDir(workspaceRoot);
142
+ const resolvedBriefPath = path2.resolve(briefPath);
143
+ const resolvedBriefsDir = path2.resolve(briefsDir);
144
+ if (!resolvedBriefPath.startsWith(resolvedBriefsDir + path2.sep)) {
145
+ throw new Error(`Invalid brief path: ${briefPath} is not inside ${briefsDir}`);
146
+ }
147
+ const raw = fs.readFileSync(briefPath, "utf-8");
148
+ const brief = JSON.parse(raw);
149
+ brief.unread = false;
150
+ const tempPath = `${briefPath}.tmp`;
151
+ fs.writeFileSync(tempPath, JSON.stringify(brief, null, 2), "utf-8");
152
+ fs.renameSync(tempPath, briefPath);
153
+ }
154
+
155
+ // src/tui.tsx
156
+ import { loadBriefConfig as loadBriefConfig2 } from "kibi-cli/brief-config";
157
+
158
+ // src/brief-delivery-reasons.ts
159
+ function toastSummary(items) {
160
+ const first = items[0]?.text?.trim() ?? "";
161
+ const second = items[1]?.text?.trim() ?? "";
162
+ if (first && second)
163
+ return `${first}, ${second}`;
164
+ return first || second || undefined;
165
+ }
166
+ function toastWhy(items) {
167
+ if (items.some((i) => i.kind === "conflict_detected"))
168
+ return "There is a knowledge conflict to resolve before using the brief.";
169
+ if (items.some((i) => i.kind === "validation_issue"))
170
+ return "Validation issues need attention before the update is treated as settled.";
171
+ const hasEntities = items.some((i) => i.kind === "entity_added" || i.kind === "entity_modified" || i.kind === "entity_removed");
172
+ const hasRelationships = items.some((i) => i.kind === "relationship_changed");
173
+ if (hasEntities && hasRelationships)
174
+ return "Requirements and facts were updated.";
175
+ if (hasEntities)
176
+ return "Entities were updated.";
177
+ if (hasRelationships)
178
+ return "Relationships were updated.";
179
+ return;
180
+ }
181
+ function isOperationalByEntityIds(item) {
182
+ if (item.entityIds.length === 0)
183
+ return false;
184
+ return item.entityIds.every((id) => {
185
+ const dashIdx = id.indexOf("-");
186
+ if (dashIdx < 0)
187
+ return false;
188
+ const name = id.slice(dashIdx + 1);
189
+ return /\.[a-zA-Z0-9]+$/.test(name);
190
+ });
191
+ }
192
+ function isOperationalItem(item, allItems) {
193
+ if (item.kind === "relationship_changed") {
194
+ const entityItems = allItems.filter((i) => i.kind === "entity_added" || i.kind === "entity_modified" || i.kind === "entity_removed");
195
+ return entityItems.length > 0 && entityItems.every(isOperationalByEntityIds);
196
+ }
197
+ return isOperationalByEntityIds(item);
198
+ }
199
+ function renderToastSummary(reasons) {
200
+ const domainItems = reasons.items.filter((i) => !isOperationalItem(i, reasons.items));
201
+ if (domainItems.length === 0) {
202
+ return;
203
+ }
204
+ return {
205
+ title: "Kibi Knowledge Update",
206
+ summary: toastSummary(domainItems) ?? "",
207
+ whyItMatters: toastWhy(domainItems) ?? ""
208
+ };
209
+ }
210
+
211
+ // src/tui-brief-view-model.ts
212
+ function firstNonEmpty(...values) {
213
+ for (const value of values) {
214
+ const trimmed = value?.trim();
215
+ if (trimmed) {
216
+ return trimmed;
217
+ }
218
+ }
219
+ return;
220
+ }
221
+ function isOperationalByEntityIds2(item) {
222
+ if (item.entityIds.length === 0)
223
+ return false;
224
+ return item.entityIds.every((id) => {
225
+ const dashIdx = id.indexOf("-");
226
+ if (dashIdx < 0)
227
+ return false;
228
+ return /\.[a-zA-Z0-9]+$/.test(id.slice(dashIdx + 1));
229
+ });
230
+ }
231
+ function isOperationalDeliveryItem(item, allItems) {
232
+ if (item.kind === "relationship_changed") {
233
+ const entityItems = allItems.filter((i) => i.kind === "entity_added" || i.kind === "entity_modified" || i.kind === "entity_removed");
234
+ return entityItems.length > 0 && entityItems.every(isOperationalByEntityIds2);
235
+ }
236
+ return isOperationalByEntityIds2(item);
237
+ }
238
+ function deriveWhatChanged(envelope) {
239
+ const briefing = envelope.briefing;
240
+ const deliveryReasons = briefing.deliveryReasons;
241
+ if (deliveryReasons?.items?.length) {
242
+ const domainItems = deliveryReasons.items.filter((item) => !isOperationalDeliveryItem(item, deliveryReasons.items));
243
+ if (domainItems.length > 0) {
244
+ return domainItems.map((item) => item.text);
245
+ }
246
+ }
247
+ if (envelope.schemaVersion === "2.0") {
248
+ const narrative = envelope.briefing.changeNarrative.map((line) => line.trim()).filter(Boolean).filter((line) => !line.includes(".sisyphus/"));
249
+ if (narrative.length > 0) {
250
+ return narrative.slice(0, 2);
251
+ }
252
+ const fallbackEntity = envelope.changes.entities.modified[0] ?? envelope.changes.entities.added[0];
253
+ if (fallbackEntity) {
254
+ const action = envelope.changes.entities.modified[0] ? "Modified" : "Added";
255
+ return [
256
+ `${action} ${fallbackEntity.id}: ${fallbackEntity.title ?? "Untitled"}`
257
+ ];
258
+ }
259
+ }
260
+ const fallback = firstNonEmpty(envelope.summary, envelope.briefing.tldr);
261
+ return fallback ? [fallback] : [];
262
+ }
263
+ function buildTuiBriefViewModel(envelope) {
264
+ const briefing = envelope.briefing;
265
+ const deliveryReasons = briefing.deliveryReasons;
266
+ let title = firstNonEmpty(envelope.summary, envelope.briefing.tldr);
267
+ if (deliveryReasons?.items?.length) {
268
+ const filteredToast = renderToastSummary(deliveryReasons);
269
+ if (filteredToast) {
270
+ title = filteredToast.summary;
271
+ }
272
+ } else if (envelope.schemaVersion === "2.0" && envelope.briefing.changeNarrative.length > 0) {
273
+ title = envelope.briefing.changeNarrative[0]?.trim() ?? title;
274
+ }
275
+ const base = {
276
+ briefId: envelope.briefId,
277
+ schemaVersion: envelope.schemaVersion,
278
+ branch: envelope.branch,
279
+ createdAt: envelope.createdAt,
280
+ type: envelope.type,
281
+ unread: envelope.unread,
282
+ contentHash: envelope.contentHash,
283
+ title: title ?? "Kibi Brief",
284
+ whatChanged: deriveWhatChanged(envelope),
285
+ whyItMatters: deliveryReasons?.items?.length ? renderToastSummary(deliveryReasons)?.whyItMatters || undefined : undefined,
286
+ knowledgeImpact: {
287
+ citations: envelope.briefing.citations,
288
+ constraints: envelope.briefing.constraints ?? [],
289
+ regressionRisks: envelope.briefing.regressionRisks ?? []
290
+ },
291
+ interpretationNote: {
292
+ validationCount: envelope.validation.count,
293
+ missingEvidence: envelope.briefing.missingEvidence ?? []
294
+ }
295
+ };
296
+ if (envelope.schemaVersion === "2.0") {
297
+ return {
298
+ ...base,
299
+ counts: {
300
+ schemaVersion: "2.0",
301
+ entitiesAdded: envelope.counts.entitiesAdded,
302
+ entitiesModified: envelope.counts.entitiesModified,
303
+ entitiesRemoved: envelope.counts.entitiesRemoved,
304
+ relationshipsChanged: envelope.counts.relationshipsChanged
305
+ }
306
+ };
307
+ }
308
+ return {
309
+ ...base,
310
+ counts: {
311
+ schemaVersion: "1.0",
312
+ requirementsAdded: envelope.counts.requirementsAdded,
313
+ relationshipsAdded: envelope.counts.relationshipsAdded,
314
+ entitiesDeleted: envelope.counts.entitiesDeleted
315
+ }
316
+ };
317
+ }
318
+
319
+ // src/tui.tsx
320
+ var tui = async (api, _options, _meta) => {
321
+ let currentContentHash = null;
322
+ api.route.register([
323
+ {
324
+ name: "kibi.brief",
325
+ render: () => {
326
+ const workspace = api.state.path.worktree || "";
327
+ const branch = api.state.vcs?.branch || "main";
328
+ const brief = selectLatestPersistedBrief(workspace, branch);
329
+ if (!brief) {
330
+ currentContentHash = null;
331
+ return /* @__PURE__ */ h("box", {
332
+ flexDirection: "column",
333
+ gap: 1,
334
+ padding: 1
335
+ }, /* @__PURE__ */ h("text", {
336
+ fg: api.theme.current.error
337
+ }, "No meaningful KB update"), /* @__PURE__ */ h("text", null, "There is no latest persisted brief for this branch."));
338
+ }
339
+ const { envelope } = brief;
340
+ const isNewContent = envelope.contentHash !== currentContentHash;
341
+ currentContentHash = envelope.contentHash;
342
+ if (isNewContent && envelope.unread) {
343
+ markBriefTuiSeen(workspace, branch, envelope.contentHash);
344
+ try {
345
+ const config = loadBriefConfig2(workspace);
346
+ if (!config.channels.vscode) {
347
+ markBriefRead(workspace, brief.filePath);
348
+ }
349
+ } catch {}
350
+ }
351
+ const viewModel = buildTuiBriefViewModel(envelope);
352
+ return /* @__PURE__ */ h("scrollbox", {
353
+ flexDirection: "column",
354
+ gap: 1,
355
+ padding: 1
356
+ }, /* @__PURE__ */ h("text", {
357
+ fg: api.theme.current.accent
358
+ }, /* @__PURE__ */ h("strong", null, viewModel.title)), /* @__PURE__ */ h("box", {
359
+ flexDirection: "column"
360
+ }, /* @__PURE__ */ h("text", null, /* @__PURE__ */ h("strong", null, "What changed:")), /* @__PURE__ */ h("text", null, viewModel.whatChanged.map((line) => `- ${line}`).join(`
361
+ `))), /* @__PURE__ */ h("box", {
362
+ flexDirection: "column"
363
+ }, /* @__PURE__ */ h("text", null, /* @__PURE__ */ h("strong", null, "Why it matters:")), /* @__PURE__ */ h("text", null, viewModel.whyItMatters)), (viewModel.knowledgeImpact.citations.length > 0 || viewModel.knowledgeImpact.constraints.length > 0 || viewModel.knowledgeImpact.regressionRisks.length > 0) && /* @__PURE__ */ h("box", {
364
+ flexDirection: "column"
365
+ }, /* @__PURE__ */ h("text", null, /* @__PURE__ */ h("strong", null, "Project knowledge impact:")), viewModel.knowledgeImpact.citations.length > 0 && /* @__PURE__ */ h("text", null, viewModel.knowledgeImpact.citations.map((citation) => `- ${citation.id}${citation.title ? `: ${citation.title}` : ""}`).join(`
366
+ `)), viewModel.knowledgeImpact.constraints.length > 0 && /* @__PURE__ */ h("text", null, viewModel.knowledgeImpact.constraints.map((constraint) => `- ${constraint.statement}`).join(`
367
+ `)), viewModel.knowledgeImpact.regressionRisks.length > 0 && /* @__PURE__ */ h("text", null, viewModel.knowledgeImpact.regressionRisks.map((risk) => `- ${risk.statement}`).join(`
368
+ `))), (viewModel.interpretationNote.validationCount > 0 || viewModel.interpretationNote.missingEvidence.length > 0) && /* @__PURE__ */ h("box", {
369
+ flexDirection: "column"
370
+ }, /* @__PURE__ */ h("text", {
371
+ fg: api.theme.current.warning
372
+ }, /* @__PURE__ */ h("strong", null, "Interpretation note:")), viewModel.interpretationNote.validationCount > 0 && /* @__PURE__ */ h("text", null, "Validation checks reported unresolved items: ", viewModel.interpretationNote.validationCount, " issue(s)."), viewModel.interpretationNote.missingEvidence.length > 0 && /* @__PURE__ */ h("text", null, viewModel.interpretationNote.missingEvidence.map((item) => `- ${item.statement}`).join(`
373
+ `))));
374
+ }
375
+ }
376
+ ]);
377
+ if (api.command?.register) {
378
+ api.command.register(() => [
379
+ {
380
+ title: "Kibi: Open Latest Brief",
381
+ value: "kibi.open_latest_brief",
382
+ description: "Opens the latest persisted brief for the current workspace and branch",
383
+ onSelect: () => {
384
+ api.route.navigate("kibi.brief");
385
+ }
386
+ },
387
+ {
388
+ title: "Kibi: Open Latest Brief",
389
+ value: "kibi-brief",
390
+ description: "Opens the latest persisted brief for the current workspace and branch",
391
+ onSelect: () => {
392
+ api.route.navigate("kibi.brief");
393
+ }
394
+ },
395
+ {
396
+ title: "Kibi: Refresh Brief",
397
+ value: "kibi.refresh_brief",
398
+ description: "Re-reads the latest persisted brief and refreshes the view",
399
+ onSelect: () => {
400
+ api.route.navigate("kibi.brief");
401
+ }
402
+ }
403
+ ]);
404
+ }
405
+ };
406
+ var tui_default = {
407
+ id: "kibi-opencode",
408
+ tui
409
+ };
410
+ export {
411
+ tui,
412
+ tui_default as default
413
+ };