kibi-opencode 0.10.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.
- package/README.md +7 -6
- package/dist/brief-delivery-reasons.d.ts +12 -0
- package/dist/brief-delivery-reasons.js +132 -0
- package/dist/brief-intent.js +17 -2
- package/dist/idle-brief-reader.d.ts +12 -0
- package/dist/idle-brief-reader.js +31 -10
- package/dist/idle-brief-runtime.js +44 -9
- package/dist/idle-brief-store.d.ts +17 -0
- package/dist/idle-brief-store.js +54 -1
- package/dist/index.d.ts +2 -52
- package/dist/index.js +1 -1068
- package/dist/logger.d.ts +1 -0
- package/dist/logger.js +8 -1
- package/dist/plugin.d.ts +52 -0
- package/dist/plugin.js +1068 -0
- package/dist/prompt.js +3 -3
- package/dist/scheduler.d.ts +12 -2
- package/dist/scheduler.js +49 -6
- package/dist/toast.d.ts +2 -0
- package/dist/tui-brief-delivery.d.ts +20 -0
- package/dist/tui-brief-delivery.js +154 -13
- package/dist/tui-brief-view-model.d.ts +63 -0
- package/dist/tui-brief-view-model.js +209 -0
- package/dist/tui.d.ts +8 -0
- package/dist/tui.js +413 -0
- package/dist/tui.jsx +120 -0
- package/package.json +12 -4
package/dist/prompt.js
CHANGED
|
@@ -280,7 +280,7 @@ ${buildBootstrapRequiredBody(capability)}`;
|
|
|
280
280
|
selectedBlock = `π **Code changes detected**
|
|
281
281
|
|
|
282
282
|
Before implementing or explaining code:
|
|
283
|
-
1. **Discover
|
|
283
|
+
1. **Discover**: Run kb_search for REQ, ADR, TEST, FACT. Decompose broad queries (e.g., "Apple Sign-In").
|
|
284
284
|
2. **Follow up exactly** - Run kb_query by sourceFile, id, type, or tags once you know what you need.
|
|
285
285
|
3. **Prefer Kibi over comments** - Store durable knowledge in KB entities instead of inline comments.
|
|
286
286
|
4. **Add traceability** - Production code: \`implements\` (symbolβreq) for ownership. Test code: \`executable_for\`. \`covered_by\` is coverage evidence only for production symbols.
|
|
@@ -473,7 +473,7 @@ This ensures behavior is documented and traceable.`;
|
|
|
473
473
|
return `π **Code changes detected**
|
|
474
474
|
|
|
475
475
|
Before implementing or explaining code:
|
|
476
|
-
1. **Discover
|
|
476
|
+
1. **Discover**: Run kb_search for REQ, ADR, TEST, FACT. Decompose broad queries (e.g., "Apple Sign-In").
|
|
477
477
|
2. **Follow up exactly** - Run kb_query by sourceFile, id, type, or tags once you know what you need.
|
|
478
478
|
3. **Prefer Kibi over comments** - Store durable knowledge in KB entities instead of inline comments.
|
|
479
479
|
4. **Add traceability** - Production code: \`implements\` (symbolβreq) for ownership. Test code: \`executable_for\`. \`covered_by\` is coverage evidence only for production symbols.`;
|
|
@@ -496,7 +496,7 @@ Run kb_check after KB mutations.
|
|
|
496
496
|
Dogfood note for this repo: OpenCode here uses local built \`kibi-mcp\` and \`kibi-opencode\` artifacts. If you change package versions or local package wiring, run \`bun run build\` before relying on OpenCode in this workspace.
|
|
497
497
|
|
|
498
498
|
**Kibi-first workflow:**
|
|
499
|
-
1. **Discover**: Run kb_search
|
|
499
|
+
1. **Discover**: Run kb_search for REQ, ADR, TEST, FACT. Decompose broad queries (e.g., "Apple Sign-In").
|
|
500
500
|
2. **Confirm**: Run kb_query with sourceFile, id, type, or tags once you know the exact follow-up target.
|
|
501
501
|
3. **Inspect freshness**: Run kb_status when branch or stale-state confidence matters.
|
|
502
502
|
4. **Document intent**: If you are about to explain code, STOP. Route that explanation to kb_upsert instead of inline comments.
|
package/dist/scheduler.d.ts
CHANGED
|
@@ -9,10 +9,20 @@ export interface SyncRunMetadata {
|
|
|
9
9
|
exitCode: number;
|
|
10
10
|
checkExitCode?: number;
|
|
11
11
|
checkRules?: string[];
|
|
12
|
+
/** Operational sync.failed observability only. */
|
|
13
|
+
syncCommand?: string;
|
|
14
|
+
syncStdout?: string;
|
|
15
|
+
syncStderr?: string;
|
|
16
|
+
syncErrorMessage?: string;
|
|
12
17
|
}
|
|
13
|
-
export type
|
|
18
|
+
export type SyncRunnerResult = {
|
|
14
19
|
exitCode: number;
|
|
15
|
-
|
|
20
|
+
syncCommand?: string;
|
|
21
|
+
syncStdout?: string;
|
|
22
|
+
syncStderr?: string;
|
|
23
|
+
syncErrorMessage?: string;
|
|
24
|
+
};
|
|
25
|
+
export type SyncRunner = (worktree: string) => Promise<SyncRunnerResult>;
|
|
16
26
|
export type CheckRunner = (worktree: string, rules: string[]) => Promise<{
|
|
17
27
|
exitCode: number;
|
|
18
28
|
}>;
|
package/dist/scheduler.js
CHANGED
|
@@ -128,9 +128,17 @@ class WorktreeSyncScheduler {
|
|
|
128
128
|
let syncExitCode = 0;
|
|
129
129
|
let checkExitCode;
|
|
130
130
|
let checkRules;
|
|
131
|
+
let syncCommand;
|
|
132
|
+
let syncStdout;
|
|
133
|
+
let syncStderr;
|
|
134
|
+
let syncErrorMessage;
|
|
131
135
|
try {
|
|
132
136
|
const syncResult = await this.runSync(this.worktree);
|
|
133
137
|
syncExitCode = syncResult.exitCode;
|
|
138
|
+
syncCommand = syncResult.syncCommand;
|
|
139
|
+
syncStdout = syncResult.syncStdout;
|
|
140
|
+
syncStderr = syncResult.syncStderr;
|
|
141
|
+
syncErrorMessage = syncResult.syncErrorMessage;
|
|
134
142
|
// Run targeted checks if sync succeeded and rules specified
|
|
135
143
|
if (syncExitCode === 0 &&
|
|
136
144
|
trigger.checkRules &&
|
|
@@ -149,11 +157,11 @@ class WorktreeSyncScheduler {
|
|
|
149
157
|
}
|
|
150
158
|
catch (err) {
|
|
151
159
|
const message = err instanceof Error ? err.message : String(err);
|
|
152
|
-
logger.error(`sync.failed ${message}`);
|
|
153
160
|
syncExitCode = 1;
|
|
161
|
+
syncErrorMessage = message;
|
|
154
162
|
}
|
|
155
163
|
finally {
|
|
156
|
-
this.emitCompletion(trigger, startedAt, syncExitCode, checkExitCode, checkRules);
|
|
164
|
+
this.emitCompletion(trigger, startedAt, syncExitCode, checkExitCode, checkRules, truncateSyncOutput(syncStdout), truncateSyncOutput(syncStderr), syncErrorMessage, syncCommand);
|
|
157
165
|
this.inFlight = false;
|
|
158
166
|
if (this.dirty) {
|
|
159
167
|
const trailing = this.trailing ?? { reason: "sync.trailing" };
|
|
@@ -185,7 +193,7 @@ class WorktreeSyncScheduler {
|
|
|
185
193
|
waiter();
|
|
186
194
|
}
|
|
187
195
|
}
|
|
188
|
-
emitCompletion(trigger, startedAt, exitCode, checkExitCode, checkRules) {
|
|
196
|
+
emitCompletion(trigger, startedAt, exitCode, checkExitCode, checkRules, syncStdout, syncStderr, syncErrorMessage, syncCommand) {
|
|
189
197
|
const durationMs = Math.max(0, this.now() - startedAt);
|
|
190
198
|
const normalizedReason = trigger.reason.endsWith(".trailing")
|
|
191
199
|
? trigger.reason.slice(0, -".trailing".length)
|
|
@@ -200,6 +208,10 @@ class WorktreeSyncScheduler {
|
|
|
200
208
|
...(trigger.filePath !== undefined ? { filePath: trigger.filePath } : {}),
|
|
201
209
|
...(checkExitCode !== undefined ? { checkExitCode } : {}),
|
|
202
210
|
...(checkRules !== undefined ? { checkRules } : {}),
|
|
211
|
+
...(syncStdout !== undefined ? { syncStdout } : {}),
|
|
212
|
+
...(syncStderr !== undefined ? { syncStderr } : {}),
|
|
213
|
+
...(syncErrorMessage !== undefined ? { syncErrorMessage } : {}),
|
|
214
|
+
...(syncCommand !== undefined ? { syncCommand } : {}),
|
|
203
215
|
};
|
|
204
216
|
if (exitCode === 0) {
|
|
205
217
|
logger.info(`sync.succeeded ${JSON.stringify(meta)}`);
|
|
@@ -213,11 +225,42 @@ class WorktreeSyncScheduler {
|
|
|
213
225
|
this.onRunComplete?.(meta);
|
|
214
226
|
}
|
|
215
227
|
}
|
|
228
|
+
const TRUNCATE_LIMIT = 4000;
|
|
229
|
+
const TRUNCATE_SUFFIX = "\n...[truncated]";
|
|
230
|
+
function truncateSyncOutput(value) {
|
|
231
|
+
if (!value)
|
|
232
|
+
return undefined;
|
|
233
|
+
if (value.length > TRUNCATE_LIMIT) {
|
|
234
|
+
return value.slice(0, TRUNCATE_LIMIT) + TRUNCATE_SUFFIX;
|
|
235
|
+
}
|
|
236
|
+
return value;
|
|
237
|
+
}
|
|
216
238
|
async function runKibiSync(worktree) {
|
|
217
239
|
return new Promise((resolve) => {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
240
|
+
try {
|
|
241
|
+
exec("kibi sync", { cwd: worktree }, (error, stdout, stderr) => {
|
|
242
|
+
if (error) {
|
|
243
|
+
const truncatedOut = truncateSyncOutput(stdout || undefined);
|
|
244
|
+
const truncatedErr = truncateSyncOutput(stderr || undefined);
|
|
245
|
+
const signal = error.signal ? ` (signal: ${error.signal})` : "";
|
|
246
|
+
const errorMessage = error.message ? `${error.message}${signal}` : signal || undefined;
|
|
247
|
+
resolve({
|
|
248
|
+
exitCode: error.code ?? 1,
|
|
249
|
+
syncCommand: "kibi sync",
|
|
250
|
+
...(truncatedOut !== undefined ? { syncStdout: truncatedOut } : {}),
|
|
251
|
+
...(truncatedErr !== undefined ? { syncStderr: truncatedErr } : {}),
|
|
252
|
+
...(errorMessage ? { syncErrorMessage: errorMessage } : {}),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
resolve({ exitCode: 0, syncCommand: "kibi sync" });
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
262
|
+
resolve({ exitCode: 1, syncCommand: "kibi sync", syncErrorMessage: message });
|
|
263
|
+
}
|
|
221
264
|
});
|
|
222
265
|
}
|
|
223
266
|
async function runKibiCheck(worktree, rules) {
|
package/dist/toast.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export type ToastCapableClient = {
|
|
|
24
24
|
showToast?: (payload: {
|
|
25
25
|
body: ToastPayload;
|
|
26
26
|
}) => void | Promise<void>;
|
|
27
|
+
/** SDK command bridge - invoke TUI command */
|
|
28
|
+
executeCommand?: (command: string, args?: object) => void | Promise<void>;
|
|
27
29
|
clearPrompt?: () => void | Promise<void>;
|
|
28
30
|
submitPrompt?: () => void | Promise<void>;
|
|
29
31
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ToastCapableClient as SendToastCapableClient } from "./toast.js";
|
|
1
2
|
import type { IdleBriefEnvelope } from "./idle-brief-store.js";
|
|
2
3
|
export type ToastPayload = {
|
|
3
4
|
variant?: "info" | "success" | "warning" | "error";
|
|
@@ -45,3 +46,22 @@ export type DeliverResult = {
|
|
|
45
46
|
* @param localConfig - Local OpenCode config
|
|
46
47
|
*/
|
|
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>;
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* the Free Software Foundation, either version 3 of the License, or
|
|
8
8
|
* (at your option) any later version.
|
|
9
9
|
*/
|
|
10
|
+
import { sendToast } from "./toast.js";
|
|
11
|
+
import { renderToastSummary } from "./brief-delivery-reasons.js";
|
|
10
12
|
import * as logger from "./logger.js";
|
|
11
13
|
function firstNonEmpty(...values) {
|
|
12
14
|
for (const value of values) {
|
|
@@ -15,16 +17,20 @@ function firstNonEmpty(...values) {
|
|
|
15
17
|
return trimmed;
|
|
16
18
|
}
|
|
17
19
|
}
|
|
18
|
-
return
|
|
19
|
-
}
|
|
20
|
-
function defaultWhyItMatters() {
|
|
21
|
-
return "This update changes how the project knowledge should be interpreted and applied.";
|
|
20
|
+
return undefined;
|
|
22
21
|
}
|
|
23
22
|
function buildTuiBriefMessage(envelope) {
|
|
24
23
|
const lines = [];
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
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
|
+
: [];
|
|
28
34
|
lines.push("## What changed");
|
|
29
35
|
if (whatChanged.length > 0) {
|
|
30
36
|
lines.push(...whatChanged.slice(0, 2));
|
|
@@ -36,16 +42,23 @@ function buildTuiBriefMessage(envelope) {
|
|
|
36
42
|
lines.push(`${action} ${fallbackEntity.id}: ${fallbackEntity.title ?? "Untitled"}`);
|
|
37
43
|
}
|
|
38
44
|
else {
|
|
39
|
-
|
|
45
|
+
const fallback = firstNonEmpty(envelope.summary, envelope.briefing.tldr);
|
|
46
|
+
if (fallback)
|
|
47
|
+
lines.push(fallback);
|
|
40
48
|
}
|
|
41
49
|
}
|
|
42
50
|
else {
|
|
43
|
-
|
|
51
|
+
const fallback = firstNonEmpty(envelope.summary, envelope.briefing.tldr);
|
|
52
|
+
if (fallback)
|
|
53
|
+
lines.push(fallback);
|
|
44
54
|
}
|
|
45
55
|
lines.push("");
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
+
}
|
|
49
62
|
const hasKnowledgeImpact = envelope.briefing.citations.length > 0 ||
|
|
50
63
|
(envelope.briefing.constraints?.length ?? 0) > 0 ||
|
|
51
64
|
(envelope.briefing.regressionRisks?.length ?? 0) > 0;
|
|
@@ -85,7 +98,65 @@ function buildTuiBriefMessage(envelope) {
|
|
|
85
98
|
while (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
86
99
|
lines.pop();
|
|
87
100
|
}
|
|
88
|
-
|
|
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;
|
|
89
160
|
}
|
|
90
161
|
/**
|
|
91
162
|
* Delivers a Kibi briefing to the TUI via toast notification.
|
|
@@ -111,8 +182,14 @@ export async function deliverBriefTui(client, envelope, sharedPolicy, _localConf
|
|
|
111
182
|
const tui = client.tui;
|
|
112
183
|
// Toast is the primary delivery mechanism
|
|
113
184
|
if (sharedPolicy.briefs.tui.toast && typeof tui?.showToast === "function") {
|
|
185
|
+
if (isNoOpBriefEnvelope(envelope)) {
|
|
186
|
+
return { delivered: false };
|
|
187
|
+
}
|
|
114
188
|
try {
|
|
115
189
|
const message = buildTuiBriefMessage(envelope);
|
|
190
|
+
if (message === undefined) {
|
|
191
|
+
return { delivered: false };
|
|
192
|
+
}
|
|
116
193
|
await tui.showToast({
|
|
117
194
|
body: {
|
|
118
195
|
variant: envelope.type === "warning" ? "warning" : "info",
|
|
@@ -136,3 +213,67 @@ export async function deliverBriefTui(client, envelope, sharedPolicy, _localConf
|
|
|
136
213
|
return { delivered: false };
|
|
137
214
|
}
|
|
138
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
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { IdleBriefCitation, IdleBriefEnvelope, IdleBriefStatement } from "./idle-brief-store.js";
|
|
2
|
+
export interface TuiBriefViewModel {
|
|
3
|
+
briefId: string;
|
|
4
|
+
schemaVersion: "1.0" | "2.0";
|
|
5
|
+
branch: string;
|
|
6
|
+
createdAt: string;
|
|
7
|
+
type: "success" | "warning";
|
|
8
|
+
unread: boolean;
|
|
9
|
+
contentHash: string;
|
|
10
|
+
/** Short human-readable title derived from the envelope */
|
|
11
|
+
title: string;
|
|
12
|
+
/** "What changed" section content */
|
|
13
|
+
whatChanged: string[];
|
|
14
|
+
/** "Why it matters" section content */
|
|
15
|
+
whyItMatters: string | undefined;
|
|
16
|
+
/** Project knowledge impact section (citations, constraints, risks) */
|
|
17
|
+
knowledgeImpact: {
|
|
18
|
+
citations: IdleBriefCitation[];
|
|
19
|
+
constraints: IdleBriefStatement[];
|
|
20
|
+
regressionRisks: IdleBriefStatement[];
|
|
21
|
+
};
|
|
22
|
+
/** Interpretation note section (validation + missing evidence) */
|
|
23
|
+
interpretationNote: {
|
|
24
|
+
validationCount: number;
|
|
25
|
+
missingEvidence: IdleBriefStatement[];
|
|
26
|
+
};
|
|
27
|
+
/** Summary counts (schema-aware) */
|
|
28
|
+
counts: {
|
|
29
|
+
schemaVersion: "1.0";
|
|
30
|
+
requirementsAdded: number;
|
|
31
|
+
relationshipsAdded: number;
|
|
32
|
+
entitiesDeleted: number;
|
|
33
|
+
} | {
|
|
34
|
+
schemaVersion: "2.0";
|
|
35
|
+
entitiesAdded: number;
|
|
36
|
+
entitiesModified: number;
|
|
37
|
+
entitiesRemoved: number;
|
|
38
|
+
relationshipsChanged: number;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Build a structured view model from a persisted brief envelope.
|
|
43
|
+
*
|
|
44
|
+
* Derives all route-rendering data (title, sections, citations, counts) from
|
|
45
|
+
* the envelope without regenerating any content. Supports both schema 1.0 and
|
|
46
|
+
* 2.0 during the migration window.
|
|
47
|
+
*
|
|
48
|
+
* @param envelope - The persisted brief envelope
|
|
49
|
+
* @returns A deterministic view model suitable for route rendering
|
|
50
|
+
*/
|
|
51
|
+
export declare function buildTuiBriefViewModel(// implements REQ-opencode-kibi-briefing-v6
|
|
52
|
+
envelope: IdleBriefEnvelope): TuiBriefViewModel;
|
|
53
|
+
/**
|
|
54
|
+
* Build a short summary text from a persisted brief envelope.
|
|
55
|
+
*
|
|
56
|
+
* Reuses the same section-building logic as `buildTuiBriefMessage` from
|
|
57
|
+
* `tui-brief-delivery.ts`, producing a deterministic plain-text summary
|
|
58
|
+
* suitable for TUI route rendering or server-side summary generation.
|
|
59
|
+
*
|
|
60
|
+
* @param envelope - The persisted brief envelope
|
|
61
|
+
* @returns A multi-line summary string
|
|
62
|
+
*/
|
|
63
|
+
export declare function buildTuiBriefSummary(envelope: IdleBriefEnvelope): string;
|
|
@@ -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
|
+
}
|