kibi-opencode 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +36 -12
  2. package/dist/brief-intent.d.ts +15 -4
  3. package/dist/brief-intent.js +63 -25
  4. package/dist/briefing-runtime.js +2 -1
  5. package/dist/config.d.ts +3 -0
  6. package/dist/config.js +9 -0
  7. package/dist/e2e-coverage-signals.d.ts +6 -0
  8. package/dist/e2e-coverage-signals.js +186 -0
  9. package/dist/file-entity-links.d.ts +15 -0
  10. package/dist/file-entity-links.js +254 -0
  11. package/dist/file-operation-reminders.d.ts +24 -0
  12. package/dist/file-operation-reminders.js +55 -0
  13. package/dist/file-operation-state.d.ts +29 -0
  14. package/dist/file-operation-state.js +113 -0
  15. package/dist/idle-brief-audit.d.ts +36 -0
  16. package/dist/idle-brief-audit.js +186 -0
  17. package/dist/idle-brief-paths.d.ts +6 -0
  18. package/dist/idle-brief-paths.js +120 -0
  19. package/dist/idle-brief-reader.d.ts +25 -0
  20. package/dist/idle-brief-reader.js +142 -0
  21. package/dist/idle-brief-runtime.d.ts +48 -0
  22. package/dist/idle-brief-runtime.js +443 -0
  23. package/dist/idle-brief-store.d.ts +96 -0
  24. package/dist/idle-brief-store.js +209 -0
  25. package/dist/index.d.ts +14 -1
  26. package/dist/index.js +626 -50
  27. package/dist/init-kibi-alias.d.ts +14 -0
  28. package/dist/init-kibi-alias.js +38 -0
  29. package/dist/init-kibi-capability.d.ts +32 -0
  30. package/dist/init-kibi-capability.js +202 -0
  31. package/dist/logger.js +9 -3
  32. package/dist/plugin-startup.d.ts +1 -0
  33. package/dist/plugin-startup.js +11 -2
  34. package/dist/prompt.d.ts +15 -3
  35. package/dist/prompt.js +103 -33
  36. package/dist/reconcile-engine.d.ts +15 -0
  37. package/dist/reconcile-engine.js +112 -0
  38. package/dist/scheduler.d.ts +1 -0
  39. package/dist/scheduler.js +37 -1
  40. package/dist/session-edit-state.d.ts +25 -0
  41. package/dist/session-edit-state.js +177 -0
  42. package/dist/session-fingerprint.d.ts +11 -0
  43. package/dist/session-fingerprint.js +21 -0
  44. package/dist/source-linked-guidance.d.ts +1 -2
  45. package/dist/source-linked-guidance.js +5 -168
  46. package/dist/startup-notifier.js +42 -31
  47. package/dist/toast.d.ts +21 -22
  48. package/dist/toast.js +36 -14
  49. package/dist/tui-brief-delivery.d.ts +47 -0
  50. package/dist/tui-brief-delivery.js +138 -0
  51. package/package.json +4 -3
@@ -0,0 +1,443 @@
1
+ // implements REQ-opencode-kibi-briefing-v4
2
+ import { buildBriefingContext } from "./brief-intent.js";
3
+ import { atomicWriteBrief, pruneOldBriefs, resolveBriefFilePath, } from "./idle-brief-paths.js";
4
+ import { computeContentHash, createBriefId, } from "./idle-brief-store.js";
5
+ import { reconcileAuditEntries } from "./reconcile-engine.js";
6
+ function asRecord(value) {
7
+ return typeof value === "object" && value !== null
8
+ ? value
9
+ : null;
10
+ }
11
+ function asString(value) {
12
+ return typeof value === "string" ? value : "";
13
+ }
14
+ function asNumber(value) {
15
+ return typeof value === "number" ? value : 0;
16
+ }
17
+ function getSessionApi(client) {
18
+ const root = asRecord(client);
19
+ const session = asRecord(root?.session);
20
+ if (!session) {
21
+ return null;
22
+ }
23
+ const create = session.create;
24
+ const prompt = session.prompt;
25
+ if (typeof create !== "function" || typeof prompt !== "function") {
26
+ return null;
27
+ }
28
+ return {
29
+ create: create,
30
+ prompt: prompt,
31
+ };
32
+ }
33
+ function extractSessionId(response) {
34
+ const root = asRecord(response);
35
+ if (!root) {
36
+ return null;
37
+ }
38
+ const directId = asString(root.id).trim();
39
+ if (directId) {
40
+ return directId;
41
+ }
42
+ const data = asRecord(root.data);
43
+ return asString(data?.id).trim() || null;
44
+ }
45
+ function extractPromptResponseJson(response) {
46
+ const root = asRecord(response);
47
+ if (!root)
48
+ return null;
49
+ const data = asRecord(root.data);
50
+ const parts = Array.isArray(data?.parts)
51
+ ? data.parts
52
+ : Array.isArray(root.parts)
53
+ ? root.parts
54
+ : null;
55
+ if (!parts)
56
+ return null;
57
+ for (const part of parts) {
58
+ const partRecord = asRecord(part);
59
+ if (partRecord?.type === "text") {
60
+ const text = asString(partRecord.text);
61
+ if (text) {
62
+ try {
63
+ const parsed = JSON.parse(text);
64
+ return asRecord(parsed) ?? null;
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
70
+ }
71
+ }
72
+ return null;
73
+ }
74
+ const CHECK_PROMPT_FORMAT = {
75
+ type: "json_schema",
76
+ schema: {
77
+ type: "object",
78
+ properties: {
79
+ violations: { type: "array" },
80
+ count: { type: "number" },
81
+ diagnostics: { type: "array" },
82
+ },
83
+ required: ["violations", "count", "diagnostics"],
84
+ },
85
+ };
86
+ const BRIEFING_PROMPT_FORMAT = {
87
+ type: "json_schema",
88
+ schema: {
89
+ type: "object",
90
+ properties: {
91
+ briefingState: { type: "string" },
92
+ tldr: { type: "string" },
93
+ promptBlock: { type: "string" },
94
+ citations: { type: "array" },
95
+ constraints: { type: "array" },
96
+ regressionRisks: { type: "array" },
97
+ missingEvidence: { type: "array" },
98
+ },
99
+ required: ["briefingState"],
100
+ },
101
+ };
102
+ function parseCheckResult(response) {
103
+ const record = asRecord(response);
104
+ if (!record || !("violations" in record)) {
105
+ return { violations: [], count: 0, diagnostics: [] };
106
+ }
107
+ const violations = Array.isArray(record.violations)
108
+ ? record.violations.map((v) => asRecord(v) ?? {})
109
+ : [];
110
+ const diagnostics = Array.isArray(record.diagnostics)
111
+ ? record.diagnostics.map((d) => asRecord(d) ?? {})
112
+ : [];
113
+ return {
114
+ violations: violations.map((v) => ({
115
+ rule: asString(v.rule),
116
+ entityId: asString(v.entityId),
117
+ description: asString(v.description),
118
+ suggestion: asString(v.suggestion),
119
+ source: asString(v.source),
120
+ })),
121
+ count: asNumber(record.count),
122
+ diagnostics: diagnostics.map((d) => ({
123
+ category: asString(d.category),
124
+ severity: asString(d.severity),
125
+ message: asString(d.message),
126
+ file: asString(d.file),
127
+ suggestion: asString(d.suggestion),
128
+ })),
129
+ };
130
+ }
131
+ async function loadCheckResult(client, workspaceCtx) {
132
+ const sessionApi = getSessionApi(client);
133
+ if (!sessionApi)
134
+ return { violations: [], count: 0, diagnostics: [] };
135
+ try {
136
+ const worker = await sessionApi.create({
137
+ directory: workspaceCtx.workspaceRoot,
138
+ title: "Kibi Idle Brief Worker",
139
+ });
140
+ const sessionID = extractSessionId(worker);
141
+ if (!sessionID)
142
+ throw new Error("Failed to resolve worker session ID");
143
+ const result = await sessionApi.prompt({
144
+ sessionID,
145
+ parts: [
146
+ { type: "text", text: JSON.stringify({ tool: "kb_check", args: {} }) },
147
+ ],
148
+ tools: { kb_check: true },
149
+ format: CHECK_PROMPT_FORMAT,
150
+ });
151
+ return parseCheckResult(extractPromptResponseJson(result));
152
+ }
153
+ catch {
154
+ return { violations: [], count: 0, diagnostics: [] };
155
+ }
156
+ }
157
+ function parseBriefStatements(value) {
158
+ if (!Array.isArray(value))
159
+ return [];
160
+ return value
161
+ .map((item) => {
162
+ const rec = asRecord(item);
163
+ if (!rec)
164
+ return null;
165
+ return {
166
+ statement: asString(rec.statement),
167
+ citationIds: Array.isArray(rec.citationIds)
168
+ ? rec.citationIds.map((id) => String(id))
169
+ : [],
170
+ };
171
+ })
172
+ .filter((s) => s !== null);
173
+ }
174
+ async function loadBriefingResultForIdle(client, workspaceCtx, sourceFiles, seedIds) {
175
+ const sessionApi = getSessionApi(client);
176
+ if (!sessionApi) {
177
+ return {
178
+ briefingState: "no_briefing",
179
+ tldr: "",
180
+ promptBlock: "",
181
+ citations: [],
182
+ };
183
+ }
184
+ if (sourceFiles.length === 0) {
185
+ return {
186
+ briefingState: "no_briefing",
187
+ tldr: "",
188
+ promptBlock: "",
189
+ citations: [],
190
+ };
191
+ }
192
+ try {
193
+ const worker = await sessionApi.create({
194
+ directory: workspaceCtx.workspaceRoot,
195
+ title: "Kibi Idle Brief Worker",
196
+ });
197
+ const sessionID = extractSessionId(worker);
198
+ if (!sessionID)
199
+ throw new Error("Failed to resolve worker session ID");
200
+ const result = await sessionApi.prompt({
201
+ sessionID,
202
+ parts: [
203
+ {
204
+ type: "text",
205
+ text: JSON.stringify({
206
+ tool: "kb_briefing_generate",
207
+ args: { sourceFiles, seedIds },
208
+ }),
209
+ },
210
+ ],
211
+ tools: { kb_briefing_generate: true },
212
+ format: BRIEFING_PROMPT_FORMAT,
213
+ });
214
+ const record = extractPromptResponseJson(result);
215
+ if (record && "briefingState" in record) {
216
+ const citations = Array.isArray(record.citations)
217
+ ? record.citations.map((c) => asRecord(c) ?? {})
218
+ : [];
219
+ return {
220
+ briefingState: asString(record.briefingState),
221
+ tldr: asString(record.tldr),
222
+ promptBlock: asString(record.promptBlock),
223
+ citations: citations.map((c) => ({
224
+ id: asString(c.id),
225
+ type: asString(c.type),
226
+ title: asString(c.title),
227
+ source: asString(c.source),
228
+ textRef: asString(c.textRef),
229
+ })),
230
+ constraints: parseBriefStatements(record.constraints),
231
+ regressionRisks: parseBriefStatements(record.regressionRisks),
232
+ missingEvidence: parseBriefStatements(record.missingEvidence),
233
+ };
234
+ }
235
+ }
236
+ catch {
237
+ // briefing command not available or failed
238
+ }
239
+ return {
240
+ briefingState: "no_briefing",
241
+ tldr: "",
242
+ promptBlock: "",
243
+ citations: [],
244
+ };
245
+ }
246
+ function computeCounts(auditDelta) {
247
+ const reconciled = reconcileAuditEntries(auditDelta.entries);
248
+ const added = reconciled.added.filter((item) => item.id !== "workspace-sync");
249
+ const modified = reconciled.modified.filter((item) => item.id !== "workspace-sync");
250
+ const removed = reconciled.removed.filter((item) => item.id !== "workspace-sync");
251
+ return {
252
+ entitiesAdded: added.length,
253
+ entitiesModified: modified.length,
254
+ entitiesRemoved: removed.length,
255
+ relationshipsChanged: reconciled.relationshipsChanged,
256
+ };
257
+ }
258
+ function computeSummary(counts, violationsCount) {
259
+ const parts = [];
260
+ const entitiesChanged = counts.entitiesAdded + counts.entitiesModified;
261
+ if (entitiesChanged > 0) {
262
+ parts.push(`${entitiesChanged} entit${entitiesChanged > 1 ? "ies" : "y"} changed`);
263
+ }
264
+ if (counts.relationshipsChanged > 0) {
265
+ parts.push(`${counts.relationshipsChanged} relationship${counts.relationshipsChanged > 1 ? "s" : ""} changed`);
266
+ }
267
+ if (counts.entitiesRemoved > 0) {
268
+ parts.push(`${counts.entitiesRemoved} entit${counts.entitiesRemoved > 1 ? "ies" : "y"} deleted`);
269
+ }
270
+ const validationText = violationsCount === 0
271
+ ? "clean"
272
+ : `${violationsCount} issue${violationsCount > 1 ? "s" : ""}`;
273
+ const changeText = parts.length > 0 ? parts.join(", ") : "no changes";
274
+ return `${changeText} | ${validationText}`;
275
+ }
276
+ function humanizeEntityType(type) {
277
+ switch (type) {
278
+ case "req":
279
+ return "requirement";
280
+ case "scenario":
281
+ return "scenario";
282
+ case "test":
283
+ return "test";
284
+ case "fact":
285
+ return "fact";
286
+ case "adr":
287
+ return "ADR";
288
+ case "flag":
289
+ return "flag";
290
+ case "event":
291
+ return "event";
292
+ case "symbol":
293
+ return "symbol";
294
+ default:
295
+ return type;
296
+ }
297
+ }
298
+ function buildChangeNarrative(auditDelta) {
299
+ const reconciled = reconcileAuditEntries(auditDelta.entries);
300
+ const added = reconciled.added.filter((item) => item.id !== "workspace-sync");
301
+ const modified = reconciled.modified.filter((item) => item.id !== "workspace-sync");
302
+ const removed = reconciled.removed.filter((item) => item.id !== "workspace-sync");
303
+ const lines = [
304
+ ...added.map((item) => `Added ${humanizeEntityType(item.type)} ${item.id}${item.title ? `: ${item.title}` : ""}`),
305
+ ...modified.map((item) => `Modified ${humanizeEntityType(item.type)} ${item.id}${item.title ? `: ${item.title}` : ""}`),
306
+ ...removed.map((item) => `Removed ${humanizeEntityType(item.type)} ${item.id}${item.title ? `: ${item.title}` : ""}`),
307
+ ];
308
+ if (reconciled.relationshipsChanged > 0) {
309
+ lines.push(`Changed ${reconciled.relationshipsChanged} relationship${reconciled.relationshipsChanged > 1 ? "s" : ""}`);
310
+ }
311
+ return lines;
312
+ }
313
+ function buildEnvelopeParts(briefId, type, sessionId, branch, createdAt, auditDelta, summary, counts, checkResult, briefingResult) {
314
+ const reconciled = reconcileAuditEntries(auditDelta.entries);
315
+ return {
316
+ schemaVersion: "2.0",
317
+ briefId,
318
+ type,
319
+ sessionId,
320
+ branch,
321
+ createdAt,
322
+ unread: true,
323
+ auditCursor: auditDelta.newCursor,
324
+ summary,
325
+ counts,
326
+ changes: {
327
+ entities: {
328
+ added: reconciled.added,
329
+ modified: reconciled.modified,
330
+ removed: reconciled.removed,
331
+ },
332
+ relationships: {
333
+ changed: reconciled.relationshipsChanged,
334
+ },
335
+ },
336
+ validation: {
337
+ violations: checkResult.violations,
338
+ count: checkResult.count,
339
+ diagnostics: checkResult.diagnostics,
340
+ },
341
+ briefing: {
342
+ tldr: briefingResult.tldr || summary,
343
+ promptBlock: briefingResult.promptBlock,
344
+ citations: briefingResult.citations,
345
+ changeNarrative: buildChangeNarrative(auditDelta),
346
+ ...(briefingResult.constraints && briefingResult.constraints.length > 0
347
+ ? { constraints: briefingResult.constraints }
348
+ : {}),
349
+ ...(briefingResult.regressionRisks &&
350
+ briefingResult.regressionRisks.length > 0
351
+ ? { regressionRisks: briefingResult.regressionRisks }
352
+ : {}),
353
+ ...(briefingResult.missingEvidence &&
354
+ briefingResult.missingEvidence.length > 0
355
+ ? { missingEvidence: briefingResult.missingEvidence }
356
+ : {}),
357
+ },
358
+ };
359
+ }
360
+ // implements REQ-opencode-kibi-briefing-v4
361
+ export async function generateIdleBrief(client, workspaceCtx, auditDelta, sessionId, options) {
362
+ if (!client) {
363
+ return { success: true, briefPath: null, envelope: null };
364
+ }
365
+ if (!auditDelta.hasChanges) {
366
+ return {
367
+ success: true,
368
+ briefPath: null,
369
+ envelope: null,
370
+ };
371
+ }
372
+ const reconciled = reconcileAuditEntries(auditDelta.entries);
373
+ const derivedSourceFiles = [
374
+ ...reconciled.added
375
+ .map((item) => item.source)
376
+ .filter((source) => !!source),
377
+ ...reconciled.modified
378
+ .map((item) => item.source)
379
+ .filter((source) => !!source),
380
+ ...reconciled.removed
381
+ .map((item) => item.source)
382
+ .filter((source) => !!source),
383
+ ];
384
+ const sourceFiles = options?.sourceFiles !== undefined
385
+ ? options.sourceFiles
386
+ : derivedSourceFiles.length > 0
387
+ ? derivedSourceFiles
388
+ : [auditDelta.entries[0]?.entityId ?? "unknown"];
389
+ const briefingContext = buildBriefingContext({
390
+ sourceFiles,
391
+ ...(options?.changedEntityIds
392
+ ? { changedEntityIds: options.changedEntityIds }
393
+ : {}),
394
+ });
395
+ const { seedIds } = briefingContext;
396
+ let checkResult;
397
+ let briefingResult;
398
+ try {
399
+ checkResult = await loadCheckResult(client, workspaceCtx);
400
+ }
401
+ catch {
402
+ checkResult = { violations: [], count: 0, diagnostics: [] };
403
+ }
404
+ try {
405
+ briefingResult = await loadBriefingResultForIdle(client, workspaceCtx, sourceFiles, seedIds);
406
+ }
407
+ catch {
408
+ briefingResult = {
409
+ briefingState: "no_briefing",
410
+ tldr: "",
411
+ promptBlock: "",
412
+ citations: [],
413
+ };
414
+ }
415
+ const counts = computeCounts(auditDelta);
416
+ const violationsCount = checkResult.violations.length;
417
+ const isSuccess = violationsCount === 0;
418
+ const type = isSuccess ? "success" : "warning";
419
+ const summary = computeSummary(counts, violationsCount);
420
+ const briefId = createBriefId();
421
+ const timestamp = Date.now();
422
+ const createdAt = new Date().toISOString();
423
+ const envelopeWithoutHash = buildEnvelopeParts(briefId, type, sessionId, workspaceCtx.branch, createdAt, auditDelta, summary, counts, checkResult, briefingResult);
424
+ const contentHash = computeContentHash(envelopeWithoutHash);
425
+ const envelope = {
426
+ ...envelopeWithoutHash,
427
+ contentHash,
428
+ };
429
+ let briefPath = null;
430
+ try {
431
+ atomicWriteBrief(workspaceCtx.workspaceRoot, timestamp, JSON.stringify(envelope, null, 2));
432
+ briefPath = resolveBriefFilePath(workspaceCtx.workspaceRoot, timestamp);
433
+ pruneOldBriefs(workspaceCtx.workspaceRoot, workspaceCtx.branch);
434
+ }
435
+ catch {
436
+ // still return envelope
437
+ }
438
+ return {
439
+ success: true,
440
+ briefPath,
441
+ envelope,
442
+ };
443
+ }
@@ -0,0 +1,96 @@
1
+ import type { EntityChangeItem } from "./reconcile-engine.js";
2
+ export interface IdleBriefAuditCursor {
3
+ lastTimestamp: string;
4
+ lastOperation: string;
5
+ entryCount: number;
6
+ fileSize: number;
7
+ }
8
+ export interface IdleBriefCitation {
9
+ id: string;
10
+ type?: string;
11
+ title?: string;
12
+ source?: string;
13
+ textRef?: string;
14
+ }
15
+ export interface IdleBriefStatement {
16
+ statement: string;
17
+ citationIds: string[];
18
+ }
19
+ export interface IdleBriefValidationViolation {
20
+ rule: string;
21
+ entityId: string;
22
+ description: string;
23
+ suggestion?: string;
24
+ source?: string;
25
+ }
26
+ export interface IdleBriefValidationDiagnostic {
27
+ category: string;
28
+ severity: string;
29
+ message: string;
30
+ file?: string;
31
+ suggestion?: string;
32
+ }
33
+ export interface IdleBriefBaseEnvelope {
34
+ briefId: string;
35
+ type: "success" | "warning";
36
+ sessionId: string;
37
+ branch: string;
38
+ createdAt: string;
39
+ unread: boolean;
40
+ auditCursor: IdleBriefAuditCursor;
41
+ summary: string;
42
+ validation: {
43
+ violations: IdleBriefValidationViolation[];
44
+ count: number;
45
+ diagnostics: IdleBriefValidationDiagnostic[];
46
+ };
47
+ contentHash: string;
48
+ }
49
+ export interface IdleBriefEnvelopeV1 extends IdleBriefBaseEnvelope {
50
+ schemaVersion: "1.0";
51
+ counts: {
52
+ requirementsAdded: number;
53
+ relationshipsAdded: number;
54
+ entitiesDeleted: number;
55
+ };
56
+ briefing: {
57
+ tldr: string;
58
+ promptBlock: string;
59
+ citations: IdleBriefCitation[];
60
+ constraints?: IdleBriefStatement[];
61
+ regressionRisks?: IdleBriefStatement[];
62
+ missingEvidence?: IdleBriefStatement[];
63
+ };
64
+ }
65
+ export interface IdleBriefEnvelopeV2 extends IdleBriefBaseEnvelope {
66
+ schemaVersion: "2.0";
67
+ counts: {
68
+ entitiesAdded: number;
69
+ entitiesModified: number;
70
+ entitiesRemoved: number;
71
+ relationshipsChanged: number;
72
+ };
73
+ changes: {
74
+ entities: {
75
+ added: EntityChangeItem[];
76
+ modified: EntityChangeItem[];
77
+ removed: EntityChangeItem[];
78
+ };
79
+ relationships: {
80
+ changed: number;
81
+ };
82
+ };
83
+ briefing: {
84
+ tldr: string;
85
+ promptBlock: string;
86
+ citations: IdleBriefCitation[];
87
+ changeNarrative: string[];
88
+ constraints?: IdleBriefStatement[];
89
+ regressionRisks?: IdleBriefStatement[];
90
+ missingEvidence?: IdleBriefStatement[];
91
+ };
92
+ }
93
+ export type IdleBriefEnvelope = IdleBriefEnvelopeV1 | IdleBriefEnvelopeV2;
94
+ export declare function isIdleBriefEnvelope(value: unknown): value is IdleBriefEnvelope;
95
+ export declare function createBriefId(): string;
96
+ export declare function computeContentHash(payload: object): string;