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
package/dist/prompt.d.ts CHANGED
@@ -1,14 +1,13 @@
1
1
  import type { CommentAnalysisResult } from "./comment-analysis.js";
2
- import type { BriefingRuntimeResult } from "./briefing-runtime.js";
3
2
  import type { KibiConfig } from "./config.js";
4
- import { type InitKibiCommandCapability } from "./init-kibi-capability.js";
5
3
  import type { GuidanceCache } from "./guidance-cache.js";
4
+ import { type InitKibiCommandCapability } from "./init-kibi-capability.js";
5
+ import type { KbFreshnessEvaluation } from "./kb-freshness-state.js";
6
6
  import type { PathKind } from "./path-kind.js";
7
7
  import type { RepoPosture } from "./repo-posture.js";
8
8
  import type { RiskClass } from "./risk-classifier.js";
9
9
  import type { WorkspaceHealth } from "./workspace-health.js";
10
10
  declare const SENTINEL = "<!-- kibi-opencode -->";
11
- export declare function buildAutoBriefingGuidance(autoBriefResult: BriefingRuntimeResult | undefined, completionReminder: boolean): string | null;
12
11
  export interface PromptContext {
13
12
  recentEdits: Array<{
14
13
  path: string;
@@ -39,8 +38,6 @@ export interface PromptContext {
39
38
  degradedMode?: "warn-once" | "structured-only";
40
39
  /** Whether to show the degraded advisory block this invocation */
41
40
  showDegradedAdvisory?: boolean;
42
- /** Stored auto-brief runtime result for the current fingerprint */
43
- autoBriefResult?: BriefingRuntimeResult;
44
41
  /** File-operation reminder from lifecycle and e2e coverage signals */
45
42
  fileOperationReminder?: {
46
43
  path: string;
@@ -53,6 +50,10 @@ export interface PromptContext {
53
50
  remainingCount: number;
54
51
  reason?: string;
55
52
  };
53
+ /** KB freshness evaluation from evidence store */
54
+ kbFreshness?: KbFreshnessEvaluation;
55
+ /** Paths changed for freshness evaluation (capped at 5) */
56
+ freshnessChangedPaths?: string[];
56
57
  }
57
58
  export declare function postureGuidance(posture: RepoPosture, capability?: InitKibiCommandCapability): string | null;
58
59
  /**
package/dist/prompt.js CHANGED
@@ -5,13 +5,7 @@ import { getSourceLinkedRequirementIds } from "./source-linked-guidance.js";
5
5
  const SENTINEL = "<!-- kibi-opencode -->";
6
6
  // ── Token budget enforcement ───────────────────────────────────────────
7
7
  const MAX_BULLETS = 5;
8
- const MAX_AUTO_BRIEF_BULLETS_WITH_REMINDER = 4;
9
8
  const MAX_WORDS = 117; // Reserve 3 words for sentinel so total injected prompt stays ≤ 120
10
- const AUTO_BRIEF_HEADER = "🧠 **Kibi briefing available**";
11
- const AUTHORITATIVE_POSTURES = [
12
- "root_active",
13
- "hybrid_root_plus_vendored",
14
- ];
15
9
  function countWords(text) {
16
10
  return text.split(/\s+/).filter(Boolean).length;
17
11
  }
@@ -55,50 +49,12 @@ function insertBulletAfterHeader(block, bullet) {
55
49
  return `${block}\n${bullet}`;
56
50
  return `${block.slice(0, headerEnd + 1)}${bullet}\n${block.slice(headerEnd + 1)}`;
57
51
  }
58
- // implements REQ-opencode-kibi-briefing-v2
59
- export function buildAutoBriefingGuidance(autoBriefResult, completionReminder) {
60
- if (!autoBriefResult)
61
- return null;
62
- // Defensive: idle-brief results are persisted to .kb/briefs/, never injected into prompts.
63
- // This function only handles auto-briefs from the file.edited risk-classification flow.
64
- if (typeof autoBriefResult === "object" &&
65
- autoBriefResult !== null &&
66
- ("briefId" in autoBriefResult || "schemaVersion" in autoBriefResult)) {
67
- return null;
68
- }
69
- if (autoBriefResult.state === "ready") {
70
- const promptBlock = autoBriefResult.promptBlock.trim();
71
- if (!promptBlock)
72
- return null;
73
- const maxBullets = completionReminder
74
- ? MAX_AUTO_BRIEF_BULLETS_WITH_REMINDER
75
- : MAX_BULLETS;
76
- const briefingLines = promptBlock
77
- .split("\n")
78
- .map((line) => line.trim())
79
- .filter((line) => line.startsWith("-"))
80
- .slice(0, maxBullets);
81
- if (briefingLines.length === 0)
82
- return null;
83
- return `${AUTO_BRIEF_HEADER}\n${briefingLines.join("\n")}`;
84
- }
85
- if (autoBriefResult.state === "tldr_fallback") {
86
- const promptBlock = autoBriefResult.promptBlock.trim();
87
- if (!promptBlock)
88
- return null;
89
- return `${AUTO_BRIEF_HEADER}\n${promptBlock}`;
90
- }
91
- return null;
92
- }
93
- function isAuthoritativePosture(posture) {
94
- return AUTHORITATIVE_POSTURES.includes(posture);
95
- }
96
52
  // ── File bucket derivation ─────────────────────────────────────────────
97
53
  function deriveFileBucket(pathKind) {
98
54
  return pathKind;
99
55
  }
100
56
  function getFocusEdit(context) {
101
- return context.focusEdit ?? context.recentEdits[context.recentEdits.length - 1];
57
+ return (context.focusEdit ?? context.recentEdits[context.recentEdits.length - 1]);
102
58
  }
103
59
  function buildInitKibiBootstrapReference(capability = getInitKibiCommandCapability()) {
104
60
  if (capability.supported) {
@@ -110,7 +66,7 @@ function buildBootstrapRequiredBody(capability = getInitKibiCommandCapability())
110
66
  const commandBullet = capability.supported
111
67
  ? "- When the Kibi OpenCode plugin is active and native injection is supported, use `/init-kibi` as the canonical short alias; `/kibi:init-kibi:mcp` remains the namespaced MCP fallback."
112
68
  : "- This host does not support native `/init-kibi` injection. Kibi must fail closed and does not register a fake native alias; use `/kibi:init-kibi:mcp` instead.";
113
- return `This repository does not appear to have Kibi initialized. Agents should:\n${commandBullet}\n- The workflow uses \`kb_autopilot_generate\` for read-only synthesis; always preview and get approval before writes.\n- Ask the user/operator to run setup or repair outside this session if bootstrap is insufficient.\n\nUse public MCP tools only: \`kb_autopilot_generate\`, \`kb_search\`, \`kb_query\`, \`kb_status\`, \`kb_find_gaps\`, \`kb_coverage\`, \`kb_graph\`, \`kb_upsert\`, \`kb_delete\`, \`kb_check\`.`;
69
+ return `This repository does not appear to have Kibi initialized. Agents should:\n${commandBullet}\n- The workflow uses \`kb_autopilot_generate\` for read-only synthesis; always preview and get approval before writes.\n- Ask the user/operator to run setup or repair outside this session if bootstrap is insufficient.\n- For comprehensive Kibi usage guidance, use \`kb_skills_load\` with skill id \`kibi-usage\`.\n\nUse public MCP tools only: \`kb_autopilot_generate\`, \`kb_search\`, \`kb_query\`, \`kb_status\`, \`kb_find_gaps\`, \`kb_coverage\`, \`kb_graph\`, \`kb_upsert\`, \`kb_delete\`, \`kb_check\`, \`kb_skills_load\`.`;
114
70
  }
115
71
  function buildHardGateBlock(block) {
116
72
  const pathLines = block.shownPaths.map((path) => `- \`${path}\``);
@@ -198,9 +154,6 @@ function buildContextualGuidance(context, capability = getInitKibiCommandCapabil
198
154
  }
199
155
  const posture = context.posture ?? "root_active";
200
156
  const riskClass = context.riskClass;
201
- const readyAutoBriefingAvailable = context.autoBriefResult?.showManualCue === false;
202
- const suppressSourceLinkedBrief = context.autoBriefResult?.state === "ready" ||
203
- context.autoBriefResult?.state === "tldr_fallback";
204
157
  const showDegraded = context.showDegradedAdvisory === true &&
205
158
  context.maintenanceDegraded === true &&
206
159
  context.degradedMode === "warn-once";
@@ -240,6 +193,7 @@ ${buildBootstrapRequiredBody(capability)}`;
240
193
  context.cache &&
241
194
  context.workspaceRoot &&
242
195
  context.branch &&
196
+ !context.kbFreshness?.requiresEvidence &&
243
197
  riskClass) {
244
198
  const focusEdit = getFocusEdit(context);
245
199
  const key = {
@@ -263,25 +217,16 @@ ${buildBootstrapRequiredBody(capability)}`;
263
217
  riskClass &&
264
218
  riskClass !== "safe_docs_only" &&
265
219
  riskClass !== "safe_test_only") {
266
- const autoBriefBlock = riskClass === "behavior_candidate" ||
267
- riskClass === "traceability_candidate"
268
- ? buildAutoBriefingGuidance(context.autoBriefResult, context.completionReminder === true)
269
- : null;
270
- if (autoBriefBlock) {
271
- selectedBlock = autoBriefBlock;
220
+ // For behavior/traceability with comment suggestions, use suggestion guidance
221
+ if ((riskClass === "behavior_candidate" ||
222
+ riskClass === "traceability_candidate") &&
223
+ context.recentCommentSuggestion) {
224
+ selectedBlock = buildCommentSuggestionGuidance(context.recentCommentSuggestion);
272
225
  }
273
226
  else {
274
- // For behavior/traceability with comment suggestions, use suggestion guidance
275
- if ((riskClass === "behavior_candidate" ||
276
- riskClass === "traceability_candidate") &&
277
- context.recentCommentSuggestion) {
278
- selectedBlock = buildCommentSuggestionGuidance(context.recentCommentSuggestion);
279
- }
280
- else {
281
- const block = GUIDANCE_BY_RISK[riskClass];
282
- if (block)
283
- selectedBlock = block;
284
- }
227
+ const block = GUIDANCE_BY_RISK[riskClass];
228
+ if (block)
229
+ selectedBlock = block;
285
230
  }
286
231
  }
287
232
  // Priority 6: Legacy path-kind fallback (when no risk class)
@@ -326,23 +271,14 @@ If you're adding long explanatory comments, consider routing that knowledge to:
326
271
  }
327
272
  }
328
273
  } // closing brace for Priority 2-4 else block starting at 187
329
- if (selectedBlock &&
330
- (riskClass === "behavior_candidate" ||
331
- riskClass === "traceability_candidate") &&
332
- isAuthoritativePosture(posture) &&
333
- !context.maintenanceDegraded &&
334
- !readyAutoBriefingAvailable) {
335
- selectedBlock = insertBulletAfterHeader(selectedBlock, "- Authoritative risky edit: run `/brief-kibi` before acting.");
336
- }
337
- // Source-linked micro-brief: insert after header line for code risk classes
274
+ // Source-linked context: insert after header line for code risk classes
338
275
  // Inserting after the header (not prepending before it) preserves the header
339
276
  // under enforceBudget's trimming logic, which only collects non-bullet lines
340
277
  // before the first bullet.
341
278
  if (selectedBlock &&
342
279
  (riskClass === "behavior_candidate" ||
343
280
  riskClass === "traceability_candidate") &&
344
- context.workspaceRoot &&
345
- !suppressSourceLinkedBrief) {
281
+ context.workspaceRoot) {
346
282
  try {
347
283
  const focusEdit = getFocusEdit(context);
348
284
  if (focusEdit?.path) {
@@ -357,7 +293,7 @@ If you're adding long explanatory comments, consider routing that knowledge to:
357
293
  }
358
294
  }
359
295
  catch {
360
- // Non-fatal: source-linked brief is best-effort
296
+ // Non-fatal: source-linked context is best-effort
361
297
  }
362
298
  }
363
299
  // ── File-operation reminder folding ─────────────────────────────────
@@ -368,7 +304,7 @@ If you're adding long explanatory comments, consider routing that knowledge to:
368
304
  if (hasFileOpReminders && fileOpReminder) {
369
305
  const foBullets = [];
370
306
  if (fileOpReminder.lifecycleReminder) {
371
- // Skip lifecycleReminder if source-linked brief already shows the same IDs
307
+ // Skip lifecycleReminder if source-linked context already shows the same IDs
372
308
  const hasSourceLinked = selectedBlock?.includes("- Existing Kibi links:") === true;
373
309
  const lifecycleHasEntities = fileOpReminder.lifecycleReminder.includes("Kibi entities:");
374
310
  const overlapsSourceLinked = hasSourceLinked &&
@@ -455,10 +391,33 @@ The Kibi workspace is in a maintenance-degraded state. Guidance remains advisory
455
391
  posture !== "root_partial") {
456
392
  finalBlock = `${finalBlock}\n- Kibi impact evidence is required before completion/commit: run \`kb_check\` before completing this task.`;
457
393
  }
394
+ // Prepend KB freshness block when evidence is required
395
+ if (context.kbFreshness?.requiresEvidence) {
396
+ const changedListing = context.freshnessChangedPaths?.length
397
+ ? `Changed: ${context.freshnessChangedPaths
398
+ .slice(0, 5)
399
+ .map((p) => `\`${p}\``)
400
+ .join(", ")}`
401
+ : null;
402
+ const freshnessLines = [
403
+ "🧠 **Kibi freshness required**",
404
+ changedListing,
405
+ `State: ${context.kbFreshness.state}. ${context.kbFreshness.reason}`,
406
+ ...(context.kbFreshness.missingEvidence.length > 0
407
+ ? [`Missing: ${context.kbFreshness.missingEvidence.join(", ")}`]
408
+ : []),
409
+ "Resolution:",
410
+ "- **KB updated**: run `kb_search` for discovery, then `kb_upsert`/`kb_delete` for mutations, then `kb_check`.",
411
+ "- **No KB impact**: include a no-impact rationale in your final report after source-linked discovery via `kb_search`/`kb_query(sourceFile=...)` and `kb_check`.",
412
+ ]
413
+ .filter(Boolean)
414
+ .join("\n");
415
+ finalBlock = finalBlock
416
+ ? `${freshnessLines}\n\n${finalBlock}`
417
+ : freshnessLines;
418
+ }
458
419
  // Return: sentinel + one targeted block (or just sentinel if no block)
459
- return finalBlock
460
- ? `${SENTINEL}\n\n${finalBlock}`
461
- : SENTINEL;
420
+ return finalBlock ? `${SENTINEL}\n\n${finalBlock}` : SENTINEL;
462
421
  }
463
422
  // ── Comment suggestion guidance (legacy compat) ────────────────────────
464
423
  function buildCommentSuggestionGuidance(suggestion) {
@@ -531,7 +490,13 @@ Dogfood note for this repo: OpenCode here uses local built \`kibi-mcp\` and \`ki
531
490
  6. **Validate**: Run kb_check after KB mutations to catch violations early.
532
491
  7. **Before completion/commit**: Kibi impact evidence is required before completion/commit. If extraction output changes, refresh documentation/symbols.yaml and do not revert that update as scope creep.
533
492
 
534
- **Public Kibi tools only:** kb_autopilot_generate, kb_search, kb_query, kb_status, kb_find_gaps, kb_coverage, kb_graph, kb_upsert, kb_delete, kb_check.\n\nDo not invoke Kibi CLI commands directly from the agent.\n\n${buildInitKibiBootstrapReference(capability)}`;
493
+ **Public Kibi tools only:** kb_autopilot_generate, kb_search, kb_query, kb_status, kb_find_gaps, kb_coverage, kb_graph, kb_upsert, kb_delete, kb_check, kb_skills_load.
494
+
495
+ For comprehensive Kibi usage guidance (relationships, fact lanes, workflows), use \`kb_skills_load\` with skill id \`kibi-usage\`.
496
+
497
+ Do not invoke Kibi CLI commands directly from the agent.
498
+
499
+ ${buildInitKibiBootstrapReference(capability)}`;
535
500
  }
536
501
  /**
537
502
  * Build prompt with contextual guidance based on posture, risk class, and cache state.
@@ -545,7 +510,6 @@ export function buildPrompt(context, capability = getInitKibiCommandCapability()
545
510
  /**
546
511
  * Inject prompt guidance if not already present.
547
512
  */
548
- // implements REQ-opencode-kibi-briefing-v2
549
513
  export function injectPrompt(current, config, context, capability = getInitKibiCommandCapability()) {
550
514
  if (!config.prompt.enabled || !isPluginEnabled(config)) {
551
515
  return current;
@@ -1,4 +1,18 @@
1
- import type { AuditEntry } from "./idle-brief-audit.js";
1
+ export interface AuditEntityPayload {
2
+ kind: "entity";
3
+ entityType: string;
4
+ changeKind?: "created" | "updated";
5
+ title?: string;
6
+ source?: string;
7
+ textRef?: string;
8
+ properties: Record<string, unknown>;
9
+ }
10
+ export interface AuditEntry {
11
+ timestamp: string;
12
+ operation: "upsert" | "delete" | "upsert_rel";
13
+ entityId: string;
14
+ payload?: AuditEntityPayload | Record<string, unknown>;
15
+ }
2
16
  export interface EntityChangeItem {
3
17
  id: string;
4
18
  type: string;
@@ -46,9 +46,7 @@ function toChangeItem(payload, entityId) {
46
46
  function compareChangeItems(left, right) {
47
47
  return left.type.localeCompare(right.type) || left.id.localeCompare(right.id);
48
48
  }
49
- export function reconcileAuditEntries(
50
- // implements REQ-opencode-kibi-briefing-v6
51
- entries) {
49
+ export function reconcileAuditEntries(entries) {
52
50
  const states = new Map();
53
51
  let relationshipsChanged = 0;
54
52
  for (const entry of [...entries].sort((left, right) => left.timestamp.localeCompare(right.timestamp))) {
package/dist/scheduler.js CHANGED
@@ -34,7 +34,9 @@ class WorktreeSyncScheduler {
34
34
  if (!this.config.sync.enabled)
35
35
  return;
36
36
  // Treat file.created, file.edited, and file.deleted same relevance-wise
37
- const isFileLifecycle = reason === "file.edited" || reason === "file.created" || reason === "file.deleted";
37
+ const isFileLifecycle = reason === "file.edited" ||
38
+ reason === "file.created" ||
39
+ reason === "file.deleted";
38
40
  if (isFileLifecycle) {
39
41
  if (!filePath)
40
42
  return;
@@ -181,7 +183,11 @@ class WorktreeSyncScheduler {
181
183
  }
182
184
  }
183
185
  isIdle() {
184
- return !this.inFlight && !this.timer && !this.pending && !this.dirty && !this.trailing;
186
+ return (!this.inFlight &&
187
+ !this.timer &&
188
+ !this.pending &&
189
+ !this.dirty &&
190
+ !this.trailing);
185
191
  }
186
192
  resolveFlushWaitersIfIdle() {
187
193
  if (!this.isIdle())
@@ -245,7 +251,9 @@ async function runKibiSync(worktree) {
245
251
  const truncatedOut = truncateSyncOutput(stdout || undefined);
246
252
  const truncatedErr = truncateSyncOutput(stderr || undefined);
247
253
  const signal = error.signal ? ` (signal: ${error.signal})` : "";
248
- const errorMessage = error.message ? `${error.message}${signal}` : signal || undefined;
254
+ const errorMessage = error.message
255
+ ? `${error.message}${signal}`
256
+ : signal || undefined;
249
257
  resolve({
250
258
  exitCode: error.code ?? 1,
251
259
  syncCommand: "kibi sync",
@@ -261,7 +269,11 @@ async function runKibiSync(worktree) {
261
269
  }
262
270
  catch (err) {
263
271
  const message = err instanceof Error ? err.message : String(err);
264
- resolve({ exitCode: 1, syncCommand: "kibi sync", syncErrorMessage: message });
272
+ resolve({
273
+ exitCode: 1,
274
+ syncCommand: "kibi sync",
275
+ syncErrorMessage: message,
276
+ });
265
277
  }
266
278
  });
267
279
  }
@@ -125,7 +125,8 @@ export function createSessionEditState(opts) {
125
125
  if (edits.length === 0)
126
126
  return null;
127
127
  // edits are sorted ascending by lastReconciledAt, so last = most recent
128
- return edits[edits.length - 1];
128
+ const focusEdit = edits.at(-1);
129
+ return focusEdit ?? null;
129
130
  }
130
131
  function hasSessionEdits() {
131
132
  for (const [, entry] of tracked) {
@@ -12,5 +12,7 @@ import { getFileLinkedTargetsByType } from "./file-entity-links.js";
12
12
  export function getSourceLinkedRequirementIds(worktree, editedAbsolutePath) {
13
13
  // Delegate to the shared file-entity-links resolver with implements-only filter.
14
14
  // implements relationships always target REQ- IDs, so no additional filtering needed.
15
- return getFileLinkedTargetsByType(worktree, editedAbsolutePath, ["implements"]).slice(0, 3);
15
+ return getFileLinkedTargetsByType(worktree, editedAbsolutePath, [
16
+ "implements",
17
+ ]).slice(0, 3);
16
18
  }
@@ -6,8 +6,14 @@ export type StartupNotifierClient = ToastCapableClient & {
6
6
  };
7
7
  };
8
8
  export type StartupNotifierConfig = {
9
- version?: string;
9
+ versions?: {
10
+ opencode?: string;
11
+ mcp?: string;
12
+ cli?: string;
13
+ core?: string;
14
+ };
10
15
  suppressToast?: boolean;
11
16
  directory?: string;
17
+ versionMetadataSource?: string;
12
18
  };
13
19
  export declare function notifyStartup(client: StartupNotifierClient, cfg: StartupNotifierConfig): void;
@@ -1,10 +1,30 @@
1
1
  import { sendToast, } from "./toast.js";
2
+ const VERSION_ORDER = ["opencode", "mcp", "cli", "core"];
2
3
  // implements REQ-opencode-kibi-plugin-v1
3
4
  export function notifyStartup(client, cfg) {
4
5
  const message = "kibi-opencode started";
5
- const displayMessage = cfg.version
6
- ? `${message} (v${cfg.version})`
6
+ // Build toast message from known versions in order
7
+ const versionParts = [];
8
+ if (cfg.versions) {
9
+ for (const key of VERSION_ORDER) {
10
+ const val = cfg.versions[key];
11
+ if (val !== undefined) {
12
+ versionParts.push(`${key} v${val}`);
13
+ }
14
+ }
15
+ }
16
+ const displayMessage = versionParts.length > 0
17
+ ? `${message} (${versionParts.join(", ")})`
7
18
  : message;
19
+ // Compute unknown versions (keys from VERSION_ORDER not present in cfg.versions)
20
+ const unknownVersions = [];
21
+ if (cfg.versions) {
22
+ for (const key of VERSION_ORDER) {
23
+ if (cfg.versions[key] === undefined) {
24
+ unknownVersions.push(key);
25
+ }
26
+ }
27
+ }
8
28
  const toastPayload = {
9
29
  variant: "success",
10
30
  title: "Kibi OpenCode",
@@ -18,31 +38,36 @@ export function notifyStartup(client, cfg) {
18
38
  ...(cfg.directory ? { directory: cfg.directory } : {}),
19
39
  };
20
40
  if (result.status === "delivered") {
21
- void client.app.log({
41
+ void client.app
42
+ .log({
22
43
  body: {
23
44
  ...base,
24
45
  level: "info",
25
46
  message: "startup toast delivered",
26
47
  transport: result.transport,
27
48
  },
28
- }).catch(() => {
49
+ })
50
+ .catch(() => {
29
51
  // Advisory log failure stays silent
30
52
  });
31
53
  }
32
54
  else if (result.status === "unavailable") {
33
- void client.app.log({
55
+ void client.app
56
+ .log({
34
57
  body: {
35
58
  ...base,
36
59
  level: "info",
37
60
  message: "startup toast unavailable",
38
61
  reason: result.reason,
39
62
  },
40
- }).catch(() => {
63
+ })
64
+ .catch(() => {
41
65
  // Advisory log failure stays silent
42
66
  });
43
67
  }
44
68
  else if (result.status === "failed") {
45
- void client.app.log({
69
+ void client.app
70
+ .log({
46
71
  body: {
47
72
  ...base,
48
73
  level: "warn",
@@ -51,21 +76,30 @@ export function notifyStartup(client, cfg) {
51
76
  reason: result.reason,
52
77
  ...(result.error ? { error: result.error } : {}),
53
78
  },
54
- }).catch(() => {
79
+ })
80
+ .catch(() => {
55
81
  // Advisory log failure stays silent
56
82
  });
57
83
  }
58
84
  });
59
85
  }
60
- void Promise.resolve(client.app.log({
61
- body: {
62
- service: "kibi-opencode",
63
- level: "info",
64
- message,
65
- ...(cfg.version ? { version: cfg.version } : {}),
66
- ...(cfg.directory ? { directory: cfg.directory } : {}),
67
- },
68
- })).catch((err) => {
86
+ // Structured startup log
87
+ const logBody = {
88
+ service: "kibi-opencode",
89
+ level: "info",
90
+ message,
91
+ };
92
+ if (cfg.versions && Object.keys(cfg.versions).length > 0) {
93
+ logBody.versions = cfg.versions;
94
+ logBody.unknownVersions = unknownVersions;
95
+ }
96
+ if (cfg.versionMetadataSource) {
97
+ logBody.versionMetadataSource = cfg.versionMetadataSource;
98
+ }
99
+ if (cfg.directory) {
100
+ logBody.directory = cfg.directory;
101
+ }
102
+ void Promise.resolve(client.app.log({ body: logBody })).catch(() => {
69
103
  // Advisory log failure stays silent
70
104
  });
71
105
  }