kibi-opencode 0.8.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.
- package/README.md +37 -12
- package/dist/brief-intent.d.ts +41 -0
- package/dist/brief-intent.js +127 -0
- package/dist/briefing-runtime.d.ts +24 -0
- package/dist/briefing-runtime.js +277 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +9 -0
- package/dist/e2e-coverage-signals.d.ts +6 -0
- package/dist/e2e-coverage-signals.js +186 -0
- package/dist/file-entity-links.d.ts +15 -0
- package/dist/file-entity-links.js +254 -0
- package/dist/file-operation-reminders.d.ts +24 -0
- package/dist/file-operation-reminders.js +55 -0
- package/dist/file-operation-state.d.ts +29 -0
- package/dist/file-operation-state.js +113 -0
- package/dist/idle-brief-audit.d.ts +36 -0
- package/dist/idle-brief-audit.js +186 -0
- package/dist/idle-brief-paths.d.ts +6 -0
- package/dist/idle-brief-paths.js +120 -0
- package/dist/idle-brief-reader.d.ts +25 -0
- package/dist/idle-brief-reader.js +142 -0
- package/dist/idle-brief-runtime.d.ts +48 -0
- package/dist/idle-brief-runtime.js +443 -0
- package/dist/idle-brief-store.d.ts +96 -0
- package/dist/idle-brief-store.js +209 -0
- package/dist/index.d.ts +15 -1
- package/dist/index.js +645 -22
- package/dist/init-kibi-alias.d.ts +14 -0
- package/dist/init-kibi-alias.js +38 -0
- package/dist/init-kibi-capability.d.ts +32 -0
- package/dist/init-kibi-capability.js +202 -0
- package/dist/logger.js +9 -3
- package/dist/plugin-startup.d.ts +1 -0
- package/dist/plugin-startup.js +11 -2
- package/dist/prompt.d.ts +18 -3
- package/dist/prompt.js +176 -50
- package/dist/reconcile-engine.d.ts +15 -0
- package/dist/reconcile-engine.js +112 -0
- package/dist/scheduler.d.ts +1 -0
- package/dist/scheduler.js +37 -1
- package/dist/session-edit-state.d.ts +25 -0
- package/dist/session-edit-state.js +177 -0
- package/dist/session-fingerprint.d.ts +11 -0
- package/dist/session-fingerprint.js +21 -0
- package/dist/source-linked-guidance.d.ts +1 -2
- package/dist/source-linked-guidance.js +5 -168
- package/dist/startup-notifier.d.ts +3 -18
- package/dist/startup-notifier.js +42 -36
- package/dist/toast.d.ts +31 -0
- package/dist/toast.js +40 -0
- package/dist/tui-brief-delivery.d.ts +47 -0
- package/dist/tui-brief-delivery.js +138 -0
- package/package.json +4 -3
package/dist/prompt.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import { isPluginEnabled } from "./config.js";
|
|
3
|
+
import { getInitKibiCommandCapability, } from "./init-kibi-capability.js";
|
|
3
4
|
import { getSourceLinkedRequirementIds } from "./source-linked-guidance.js";
|
|
4
5
|
const SENTINEL = "<!-- kibi-opencode -->";
|
|
5
6
|
// ── Token budget enforcement ───────────────────────────────────────────
|
|
6
7
|
const MAX_BULLETS = 5;
|
|
8
|
+
const MAX_AUTO_BRIEF_BULLETS_WITH_REMINDER = 4;
|
|
7
9
|
const MAX_WORDS = 117; // Reserve 3 words for sentinel so total injected prompt stays ≤ 120
|
|
10
|
+
const AUTO_BRIEF_HEADER = "🧠 **Kibi briefing available**";
|
|
8
11
|
const AUTHORITATIVE_POSTURES = [
|
|
9
12
|
"root_active",
|
|
10
13
|
"hybrid_root_plus_vendored",
|
|
@@ -15,14 +18,21 @@ function countWords(text) {
|
|
|
15
18
|
function countBullets(lines) {
|
|
16
19
|
return lines.filter((l) => l.startsWith("-")).length;
|
|
17
20
|
}
|
|
18
|
-
|
|
21
|
+
const ENTITY_ID_RE = /\b(?:REQ|SYM|SCEN|TEST|ADR|FACT|FLAG|EVT)-[A-Za-z0-9_-]+\b/g;
|
|
22
|
+
// implements REQ-opencode-file-context-guidance-v1
|
|
23
|
+
function hasOverlappingEntityIds(textA, textB) {
|
|
24
|
+
const idsA = new Set(textA.match(ENTITY_ID_RE) ?? []);
|
|
25
|
+
const idsB = textB.match(ENTITY_ID_RE) ?? [];
|
|
26
|
+
return idsB.length > 0 && idsB.some((id) => idsA.has(id));
|
|
27
|
+
}
|
|
28
|
+
function enforceBudget(block, maxBullets = MAX_BULLETS) {
|
|
19
29
|
const lines = block.split("\n");
|
|
20
|
-
if (countBullets(lines) >
|
|
21
|
-
// Trim to budget: keep header + first
|
|
30
|
+
if (countBullets(lines) > maxBullets || countWords(block) > MAX_WORDS) {
|
|
31
|
+
// Trim to budget: keep header + first maxBullets bullet lines
|
|
22
32
|
const header = [];
|
|
23
33
|
const bullets = [];
|
|
24
34
|
for (const line of lines) {
|
|
25
|
-
if (line.startsWith("-") && bullets.length <
|
|
35
|
+
if (line.startsWith("-") && bullets.length < maxBullets) {
|
|
26
36
|
bullets.push(line);
|
|
27
37
|
}
|
|
28
38
|
else if (!line.startsWith("-")) {
|
|
@@ -45,6 +55,41 @@ function insertBulletAfterHeader(block, bullet) {
|
|
|
45
55
|
return `${block}\n${bullet}`;
|
|
46
56
|
return `${block.slice(0, headerEnd + 1)}${bullet}\n${block.slice(headerEnd + 1)}`;
|
|
47
57
|
}
|
|
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
|
+
}
|
|
48
93
|
function isAuthoritativePosture(posture) {
|
|
49
94
|
return AUTHORITATIVE_POSTURES.includes(posture);
|
|
50
95
|
}
|
|
@@ -52,6 +97,21 @@ function isAuthoritativePosture(posture) {
|
|
|
52
97
|
function deriveFileBucket(pathKind) {
|
|
53
98
|
return pathKind;
|
|
54
99
|
}
|
|
100
|
+
function getFocusEdit(context) {
|
|
101
|
+
return context.focusEdit ?? context.recentEdits[context.recentEdits.length - 1];
|
|
102
|
+
}
|
|
103
|
+
function buildInitKibiBootstrapReference(capability = getInitKibiCommandCapability()) {
|
|
104
|
+
if (capability.supported) {
|
|
105
|
+
return "Bootstrap existing repos: when the Kibi OpenCode plugin is active and native injection is supported, `/init-kibi` is the canonical short alias; `/kibi:init-kibi:mcp` remains the namespaced MCP fallback for the retroactive initialization (`kb_autopilot_generate`) workflow.";
|
|
106
|
+
}
|
|
107
|
+
return "Bootstrap existing repos: this host does not support native `/init-kibi` injection, so Kibi must fail closed and does not register a fake native alias; use `/kibi:init-kibi:mcp` for the retroactive initialization (`kb_autopilot_generate`) workflow.";
|
|
108
|
+
}
|
|
109
|
+
function buildBootstrapRequiredBody(capability = getInitKibiCommandCapability()) {
|
|
110
|
+
const commandBullet = capability.supported
|
|
111
|
+
? "- 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
|
+
: "- 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\`.`;
|
|
114
|
+
}
|
|
55
115
|
// ── Guidance blocks by risk class ──────────────────────────────────────
|
|
56
116
|
const GUIDANCE_BY_RISK = {
|
|
57
117
|
safe_docs_only: null,
|
|
@@ -86,7 +146,7 @@ The Kibi knowledge base is managed through public MCP tools. Direct manual edits
|
|
|
86
146
|
- Use kb_check to validate consistency`,
|
|
87
147
|
};
|
|
88
148
|
// ── Posture overrides ──────────────────────────────────────────────────
|
|
89
|
-
export function postureGuidance(posture) {
|
|
149
|
+
export function postureGuidance(posture, capability = getInitKibiCommandCapability()) {
|
|
90
150
|
// implements REQ-opencode-prompt-injection
|
|
91
151
|
switch (posture) {
|
|
92
152
|
case "vendored_only":
|
|
@@ -95,12 +155,7 @@ export function postureGuidance(posture) {
|
|
|
95
155
|
case "root_uninitialized":
|
|
96
156
|
return `🔧 **Bootstrap required**
|
|
97
157
|
|
|
98
|
-
|
|
99
|
-
- Start with \`kb_autopilot_generate\` to discover entities and bootstrap the KB (preferred workflow)
|
|
100
|
-
- Use \`/init-kibi\` as the sanctioned slash command for initial repo setup
|
|
101
|
-
- Ask the user/operator to run setup or repair outside this session if bootstrap is insufficient
|
|
102
|
-
|
|
103
|
-
Do not run \`kibi\` CLI commands directly; use public MCP tools (kb_autopilot_generate, kb_search, kb_query, kb_status, kb_find_gaps, kb_coverage, kb_graph, kb_upsert, kb_delete, kb_check).`;
|
|
158
|
+
${buildBootstrapRequiredBody(capability)}`;
|
|
104
159
|
case "root_partial":
|
|
105
160
|
return `⚠️ **Partial KB setup detected**
|
|
106
161
|
|
|
@@ -113,12 +168,19 @@ Root .kb/config.json exists but some configured KB targets are missing. Guidance
|
|
|
113
168
|
/**
|
|
114
169
|
* Build prompt guidance block based on posture, risk class, and cache state.
|
|
115
170
|
*/
|
|
116
|
-
function buildContextualGuidance(context) {
|
|
171
|
+
function buildContextualGuidance(context, capability = getInitKibiCommandCapability()) {
|
|
117
172
|
const posture = context.posture ?? "root_active";
|
|
118
173
|
const riskClass = context.riskClass;
|
|
174
|
+
const readyAutoBriefingAvailable = context.autoBriefResult?.showManualCue === false;
|
|
175
|
+
const suppressSourceLinkedBrief = context.autoBriefResult?.state === "ready" ||
|
|
176
|
+
context.autoBriefResult?.state === "tldr_fallback";
|
|
119
177
|
const showDegraded = context.showDegradedAdvisory === true &&
|
|
120
178
|
context.maintenanceDegraded === true &&
|
|
121
179
|
context.degradedMode === "warn-once";
|
|
180
|
+
const fileOpReminder = context.fileOperationReminder;
|
|
181
|
+
const hasFileOpReminders = fileOpReminder !== undefined &&
|
|
182
|
+
(fileOpReminder.lifecycleReminder !== null ||
|
|
183
|
+
fileOpReminder.e2eReminder !== null);
|
|
122
184
|
// ── Single-block priority selection ──
|
|
123
185
|
// Priority order (highest wins): manual_kb_edit > posture > risk_class > safe/none
|
|
124
186
|
let selectedBlock = null;
|
|
@@ -132,59 +194,71 @@ function buildContextualGuidance(context) {
|
|
|
132
194
|
}
|
|
133
195
|
// Priority 3: Posture warnings for non-active states — not cache-suppressed
|
|
134
196
|
else if (posture === "root_uninitialized" || posture === "root_partial") {
|
|
135
|
-
const postureBlock = postureGuidance(posture);
|
|
197
|
+
const postureBlock = postureGuidance(posture, capability);
|
|
136
198
|
if (postureBlock)
|
|
137
199
|
selectedBlock = postureBlock;
|
|
138
200
|
}
|
|
139
201
|
else if (!context.posture && context.workspaceHealth?.needsBootstrap) {
|
|
140
202
|
selectedBlock = `🔧 **Bootstrap required**
|
|
141
203
|
|
|
142
|
-
|
|
143
|
-
- Start with \`kb_autopilot_generate\` to discover entities and bootstrap the KB (preferred workflow)
|
|
144
|
-
- Use \`/init-kibi\` as the sanctioned slash command for initial repo setup
|
|
145
|
-
- Ask the user/operator to run setup or repair outside this session if bootstrap is insufficient
|
|
146
|
-
|
|
147
|
-
Do not run \`kibi\` CLI commands directly; use public MCP tools (kb_autopilot_generate, kb_search, kb_query, kb_status, kb_find_gaps, kb_coverage, kb_graph, kb_upsert, kb_delete, kb_check).`;
|
|
204
|
+
${buildBootstrapRequiredBody(capability)}`;
|
|
148
205
|
// Advisory guidance: check cache before selecting, since these blocks can be safely suppressed
|
|
149
206
|
}
|
|
150
207
|
else {
|
|
151
208
|
// Cache check: skip repeated advisory guidance — only after critical signals are handled above
|
|
152
209
|
// Allow degraded advisory to bypass cache so it is always visible
|
|
210
|
+
// File-operation reminders also bypass cache (per-path suppression handled by caller)
|
|
211
|
+
let cacheSuppressedSemantic = false;
|
|
153
212
|
if (!showDegraded &&
|
|
154
213
|
context.cache &&
|
|
155
214
|
context.workspaceRoot &&
|
|
156
215
|
context.branch &&
|
|
157
216
|
riskClass) {
|
|
158
|
-
const
|
|
217
|
+
const focusEdit = getFocusEdit(context);
|
|
159
218
|
const key = {
|
|
160
219
|
workspaceRoot: context.workspaceRoot,
|
|
161
220
|
branch: context.branch,
|
|
162
221
|
posture,
|
|
163
222
|
riskClass,
|
|
164
|
-
fileBucket: deriveFileBucket(
|
|
223
|
+
fileBucket: deriveFileBucket(focusEdit?.kind ?? "unknown"),
|
|
165
224
|
};
|
|
166
225
|
if (context.cache.isSatisfied(key)) {
|
|
167
|
-
|
|
226
|
+
if (hasFileOpReminders) {
|
|
227
|
+
cacheSuppressedSemantic = true;
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
return SENTINEL; // skip guidance — recently satisfied
|
|
231
|
+
}
|
|
168
232
|
}
|
|
169
233
|
}
|
|
170
234
|
// Priority 5: Risk-class-driven guidance (for non-safe classes)
|
|
171
|
-
if (
|
|
235
|
+
if (!cacheSuppressedSemantic &&
|
|
236
|
+
riskClass &&
|
|
172
237
|
riskClass !== "safe_docs_only" &&
|
|
173
238
|
riskClass !== "safe_test_only") {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
239
|
+
const autoBriefBlock = riskClass === "behavior_candidate" ||
|
|
240
|
+
riskClass === "traceability_candidate"
|
|
241
|
+
? buildAutoBriefingGuidance(context.autoBriefResult, context.completionReminder === true)
|
|
242
|
+
: null;
|
|
243
|
+
if (autoBriefBlock) {
|
|
244
|
+
selectedBlock = autoBriefBlock;
|
|
179
245
|
}
|
|
180
246
|
else {
|
|
181
|
-
|
|
182
|
-
if (
|
|
183
|
-
|
|
247
|
+
// For behavior/traceability with comment suggestions, use suggestion guidance
|
|
248
|
+
if ((riskClass === "behavior_candidate" ||
|
|
249
|
+
riskClass === "traceability_candidate") &&
|
|
250
|
+
context.recentCommentSuggestion) {
|
|
251
|
+
selectedBlock = buildCommentSuggestionGuidance(context.recentCommentSuggestion);
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
const block = GUIDANCE_BY_RISK[riskClass];
|
|
255
|
+
if (block)
|
|
256
|
+
selectedBlock = block;
|
|
257
|
+
}
|
|
184
258
|
}
|
|
185
259
|
}
|
|
186
260
|
// Priority 6: Legacy path-kind fallback (when no risk class)
|
|
187
|
-
else if (!riskClass) {
|
|
261
|
+
else if (!cacheSuppressedSemantic && !riskClass) {
|
|
188
262
|
const codeEdits = context.recentEdits.filter((e) => e.kind === "code");
|
|
189
263
|
const reqEdits = context.recentEdits.filter((e) => e.kind === "requirement");
|
|
190
264
|
const kbDocEdits = context.recentEdits.filter((e) => [
|
|
@@ -229,7 +303,8 @@ If you're adding long explanatory comments, consider routing that knowledge to:
|
|
|
229
303
|
(riskClass === "behavior_candidate" ||
|
|
230
304
|
riskClass === "traceability_candidate") &&
|
|
231
305
|
isAuthoritativePosture(posture) &&
|
|
232
|
-
!context.maintenanceDegraded
|
|
306
|
+
!context.maintenanceDegraded &&
|
|
307
|
+
!readyAutoBriefingAvailable) {
|
|
233
308
|
selectedBlock = insertBulletAfterHeader(selectedBlock, "- Authoritative risky edit: run `/brief-kibi` before acting.");
|
|
234
309
|
}
|
|
235
310
|
// Source-linked micro-brief: insert after header line for code risk classes
|
|
@@ -239,11 +314,12 @@ If you're adding long explanatory comments, consider routing that knowledge to:
|
|
|
239
314
|
if (selectedBlock &&
|
|
240
315
|
(riskClass === "behavior_candidate" ||
|
|
241
316
|
riskClass === "traceability_candidate") &&
|
|
242
|
-
context.workspaceRoot
|
|
317
|
+
context.workspaceRoot &&
|
|
318
|
+
!suppressSourceLinkedBrief) {
|
|
243
319
|
try {
|
|
244
|
-
const
|
|
245
|
-
if (
|
|
246
|
-
const editedPath =
|
|
320
|
+
const focusEdit = getFocusEdit(context);
|
|
321
|
+
if (focusEdit?.path) {
|
|
322
|
+
const editedPath = focusEdit.path;
|
|
247
323
|
const absEdited = path.isAbsolute(editedPath)
|
|
248
324
|
? editedPath
|
|
249
325
|
: path.join(context.workspaceRoot, editedPath);
|
|
@@ -257,6 +333,41 @@ If you're adding long explanatory comments, consider routing that knowledge to:
|
|
|
257
333
|
// Non-fatal: source-linked brief is best-effort
|
|
258
334
|
}
|
|
259
335
|
}
|
|
336
|
+
// ── File-operation reminder folding ─────────────────────────────────
|
|
337
|
+
// File-operation reminders bypass generic GuidanceCache suppression but
|
|
338
|
+
// are subject to prompt budget trimming. Per-path suppression is handled
|
|
339
|
+
// by the caller via file-operation-state hasShown/markShown.
|
|
340
|
+
// implements REQ-opencode-file-context-guidance-v1
|
|
341
|
+
if (hasFileOpReminders && fileOpReminder) {
|
|
342
|
+
const foBullets = [];
|
|
343
|
+
if (fileOpReminder.lifecycleReminder) {
|
|
344
|
+
// Skip lifecycleReminder if source-linked brief already shows the same IDs
|
|
345
|
+
const hasSourceLinked = selectedBlock?.includes("- Existing Kibi links:") === true;
|
|
346
|
+
const lifecycleHasEntities = fileOpReminder.lifecycleReminder.includes("Kibi entities:");
|
|
347
|
+
const overlapsSourceLinked = hasSourceLinked &&
|
|
348
|
+
lifecycleHasEntities &&
|
|
349
|
+
selectedBlock !== null &&
|
|
350
|
+
hasOverlappingEntityIds(selectedBlock, fileOpReminder.lifecycleReminder);
|
|
351
|
+
if (!overlapsSourceLinked) {
|
|
352
|
+
foBullets.push(fileOpReminder.lifecycleReminder);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (fileOpReminder.e2eReminder) {
|
|
356
|
+
foBullets.push(fileOpReminder.e2eReminder);
|
|
357
|
+
}
|
|
358
|
+
if (foBullets.length > 0) {
|
|
359
|
+
if (selectedBlock) {
|
|
360
|
+
// Fold into existing semantic block
|
|
361
|
+
for (const bullet of foBullets) {
|
|
362
|
+
selectedBlock = insertBulletAfterHeader(selectedBlock, bullet);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
// Create file-operation-only compact block
|
|
367
|
+
selectedBlock = `🧠 **File operation detected**\n${foBullets.join("\n")}`;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
260
371
|
// Inject degraded advisory block for warn-once mode
|
|
261
372
|
if (showDegraded) {
|
|
262
373
|
const advisory = `⚠️ **Maintenance degraded**
|
|
@@ -276,25 +387,37 @@ The Kibi workspace is in a maintenance-degraded state. Guidance remains advisory
|
|
|
276
387
|
context.workspaceRoot &&
|
|
277
388
|
context.branch &&
|
|
278
389
|
riskClass) {
|
|
279
|
-
const
|
|
390
|
+
const focusEdit = getFocusEdit(context);
|
|
280
391
|
const key = {
|
|
281
392
|
workspaceRoot: context.workspaceRoot,
|
|
282
393
|
branch: context.branch,
|
|
283
394
|
posture,
|
|
284
395
|
riskClass,
|
|
285
|
-
fileBucket: deriveFileBucket(
|
|
396
|
+
fileBucket: deriveFileBucket(focusEdit?.kind ?? "unknown"),
|
|
286
397
|
};
|
|
287
398
|
context.cache.recordSatisfied(key, "guidance");
|
|
288
399
|
}
|
|
289
|
-
// Apply budget enforcement before appending the completion reminder so the
|
|
290
|
-
// reminder bullet is never silently trimmed when bullet count exceeds MAX_BULLETS.
|
|
291
|
-
const budgeted = selectedBlock ? enforceBudget(selectedBlock) : null;
|
|
292
|
-
// Append completion reminder for risky classes when enabled
|
|
293
400
|
const REMINDER_RISK_CLASSES = [
|
|
294
401
|
"behavior_candidate",
|
|
295
402
|
"traceability_candidate",
|
|
296
403
|
"req_policy_candidate",
|
|
297
404
|
];
|
|
405
|
+
const reminderWillBeAppended = !!selectedBlock &&
|
|
406
|
+
context.completionReminder === true &&
|
|
407
|
+
!context.maintenanceDegraded &&
|
|
408
|
+
riskClass != null &&
|
|
409
|
+
REMINDER_RISK_CLASSES.includes(riskClass) &&
|
|
410
|
+
posture !== "root_uninitialized" &&
|
|
411
|
+
posture !== "root_partial";
|
|
412
|
+
const effectiveMaxBullets = reminderWillBeAppended
|
|
413
|
+
? MAX_BULLETS - 1
|
|
414
|
+
: MAX_BULLETS;
|
|
415
|
+
// Apply budget enforcement before appending the completion reminder so the
|
|
416
|
+
// reminder bullet is never silently trimmed when bullet count exceeds MAX_BULLETS.
|
|
417
|
+
const budgeted = selectedBlock
|
|
418
|
+
? enforceBudget(selectedBlock, effectiveMaxBullets)
|
|
419
|
+
: null;
|
|
420
|
+
// Append completion reminder for risky classes when enabled
|
|
298
421
|
let finalBlock = budgeted;
|
|
299
422
|
if (finalBlock &&
|
|
300
423
|
context.completionReminder === true &&
|
|
@@ -360,7 +483,8 @@ Before implementing or explaining code:
|
|
|
360
483
|
/**
|
|
361
484
|
* Build the static guidance block (original behavior).
|
|
362
485
|
*/
|
|
363
|
-
|
|
486
|
+
function buildBaseGuidance(capability = getInitKibiCommandCapability()) {
|
|
487
|
+
return `${SENTINEL}
|
|
364
488
|
This project uses Kibi (via MCP). Prefer storing durable knowledge in Kibi over code comments.
|
|
365
489
|
|
|
366
490
|
Before changing behavior: use kb_search for discovery, then kb_query by sourceFile, id, type, or tags for exact follow-up; do not rely on undocumented tools.
|
|
@@ -379,26 +503,28 @@ Dogfood note for this repo: OpenCode here uses local built \`kibi-mcp\` and \`ki
|
|
|
379
503
|
5. **Link during work**: When creating KB entities, include relationship rows: specified_by (req→scenario), implements (symbol→req for ownership), covered_by (symbol→test for coverage), executable_for (test code→test).
|
|
380
504
|
6. **Validate**: Run kb_check after KB mutations to catch violations early.
|
|
381
505
|
|
|
382
|
-
**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\
|
|
506
|
+
**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)}`;
|
|
507
|
+
}
|
|
383
508
|
/**
|
|
384
509
|
* Build prompt with contextual guidance based on posture, risk class, and cache state.
|
|
385
510
|
*/
|
|
386
|
-
export function buildPrompt(context) {
|
|
511
|
+
export function buildPrompt(context, capability = getInitKibiCommandCapability()) {
|
|
387
512
|
if (!context) {
|
|
388
|
-
return
|
|
513
|
+
return buildBaseGuidance(capability).trim();
|
|
389
514
|
}
|
|
390
|
-
return buildContextualGuidance(context).trim();
|
|
515
|
+
return buildContextualGuidance(context, capability).trim();
|
|
391
516
|
}
|
|
392
517
|
/**
|
|
393
518
|
* Inject prompt guidance if not already present.
|
|
394
519
|
*/
|
|
395
|
-
|
|
520
|
+
// implements REQ-opencode-kibi-briefing-v2
|
|
521
|
+
export function injectPrompt(current, config, context, capability = getInitKibiCommandCapability()) {
|
|
396
522
|
if (!config.prompt.enabled || !isPluginEnabled(config)) {
|
|
397
523
|
return current;
|
|
398
524
|
}
|
|
399
525
|
if (current.includes(SENTINEL)) {
|
|
400
526
|
return current;
|
|
401
527
|
}
|
|
402
|
-
return `${current}\n\n${buildPrompt(context)}`;
|
|
528
|
+
return `${current}\n\n${buildPrompt(context, capability)}`;
|
|
403
529
|
}
|
|
404
530
|
export { SENTINEL };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AuditEntry } from "./idle-brief-audit.js";
|
|
2
|
+
export interface EntityChangeItem {
|
|
3
|
+
id: string;
|
|
4
|
+
type: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
source?: string;
|
|
7
|
+
textRef?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ReconcileResult {
|
|
10
|
+
added: EntityChangeItem[];
|
|
11
|
+
modified: EntityChangeItem[];
|
|
12
|
+
removed: EntityChangeItem[];
|
|
13
|
+
relationshipsChanged: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function reconcileAuditEntries(entries: AuditEntry[]): ReconcileResult;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
function normalizeWhitespace(value) {
|
|
2
|
+
return value.trim().replace(/\s+/g, " ");
|
|
3
|
+
}
|
|
4
|
+
function normalizeValue(value) {
|
|
5
|
+
if (typeof value === "string") {
|
|
6
|
+
return normalizeWhitespace(value);
|
|
7
|
+
}
|
|
8
|
+
if (Array.isArray(value)) {
|
|
9
|
+
return value.map((entry) => normalizeValue(entry));
|
|
10
|
+
}
|
|
11
|
+
if (value && typeof value === "object") {
|
|
12
|
+
return Object.fromEntries(Object.entries(value)
|
|
13
|
+
.filter(([key]) => key !== "created_at" && key !== "updated_at")
|
|
14
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
15
|
+
.map(([key, entry]) => [key, normalizeValue(entry)]));
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
function fingerprintPayload(payload) {
|
|
20
|
+
return JSON.stringify(normalizeValue(payload.properties));
|
|
21
|
+
}
|
|
22
|
+
function isEntityPayload(payload) {
|
|
23
|
+
return payload?.kind === "entity";
|
|
24
|
+
}
|
|
25
|
+
function toChangeItem(payload, entityId) {
|
|
26
|
+
const title = payload.title ??
|
|
27
|
+
(typeof payload.properties.title === "string"
|
|
28
|
+
? payload.properties.title
|
|
29
|
+
: undefined);
|
|
30
|
+
const source = payload.source ??
|
|
31
|
+
(typeof payload.properties.source === "string"
|
|
32
|
+
? payload.properties.source
|
|
33
|
+
: undefined);
|
|
34
|
+
const textRef = payload.textRef ??
|
|
35
|
+
(typeof payload.properties.text_ref === "string"
|
|
36
|
+
? payload.properties.text_ref
|
|
37
|
+
: undefined);
|
|
38
|
+
return {
|
|
39
|
+
id: entityId,
|
|
40
|
+
type: payload.entityType,
|
|
41
|
+
...(title ? { title } : {}),
|
|
42
|
+
...(source ? { source } : {}),
|
|
43
|
+
...(textRef ? { textRef } : {}),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function compareChangeItems(left, right) {
|
|
47
|
+
return left.type.localeCompare(right.type) || left.id.localeCompare(right.id);
|
|
48
|
+
}
|
|
49
|
+
export function reconcileAuditEntries(
|
|
50
|
+
// implements REQ-opencode-kibi-briefing-v6
|
|
51
|
+
entries) {
|
|
52
|
+
const states = new Map();
|
|
53
|
+
let relationshipsChanged = 0;
|
|
54
|
+
for (const entry of [...entries].sort((left, right) => left.timestamp.localeCompare(right.timestamp))) {
|
|
55
|
+
if (entry.operation === "upsert_rel") {
|
|
56
|
+
relationshipsChanged += 1;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const state = states.get(entry.entityId) ?? {
|
|
60
|
+
sawCreate: false,
|
|
61
|
+
sawLegacyUpsert: false,
|
|
62
|
+
deleted: false,
|
|
63
|
+
};
|
|
64
|
+
if (entry.operation === "delete") {
|
|
65
|
+
state.deleted = true;
|
|
66
|
+
states.set(entry.entityId, state);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (!isEntityPayload(entry.payload)) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (entry.payload.changeKind === "created") {
|
|
73
|
+
state.sawCreate = true;
|
|
74
|
+
}
|
|
75
|
+
if (!entry.payload.changeKind) {
|
|
76
|
+
state.sawLegacyUpsert = true;
|
|
77
|
+
}
|
|
78
|
+
state.lastKnown = toChangeItem(entry.payload, entry.entityId);
|
|
79
|
+
state.lastFingerprint = fingerprintPayload(entry.payload);
|
|
80
|
+
state.deleted = false;
|
|
81
|
+
states.set(entry.entityId, state);
|
|
82
|
+
}
|
|
83
|
+
const added = [];
|
|
84
|
+
const modified = [];
|
|
85
|
+
const removed = [];
|
|
86
|
+
for (const state of states.values()) {
|
|
87
|
+
if (!state.lastKnown) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (state.deleted) {
|
|
91
|
+
if (state.sawCreate) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
removed.push(state.lastKnown);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (state.sawCreate || state.sawLegacyUpsert) {
|
|
98
|
+
added.push(state.lastKnown);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
modified.push(state.lastKnown);
|
|
102
|
+
}
|
|
103
|
+
added.sort(compareChangeItems);
|
|
104
|
+
modified.sort(compareChangeItems);
|
|
105
|
+
removed.sort(compareChangeItems);
|
|
106
|
+
return {
|
|
107
|
+
added,
|
|
108
|
+
modified,
|
|
109
|
+
removed,
|
|
110
|
+
relationshipsChanged,
|
|
111
|
+
};
|
|
112
|
+
}
|
package/dist/scheduler.d.ts
CHANGED
|
@@ -36,6 +36,7 @@ export interface SyncScheduler {
|
|
|
36
36
|
scheduleSync(reason: string, filePath?: string, checkRules?: string[]): void;
|
|
37
37
|
onFileEdited(filePath: string): void;
|
|
38
38
|
onToolExecuteAfter(reason?: string): void;
|
|
39
|
+
flush(): Promise<void>;
|
|
39
40
|
dispose(): void;
|
|
40
41
|
}
|
|
41
42
|
export declare function createSyncScheduler(opts: SchedulerOptions): SyncScheduler;
|
package/dist/scheduler.js
CHANGED
|
@@ -18,6 +18,7 @@ class WorktreeSyncScheduler {
|
|
|
18
18
|
pending = null;
|
|
19
19
|
trailing = null;
|
|
20
20
|
lastFileEditedAt = 0;
|
|
21
|
+
flushWaiters = [];
|
|
21
22
|
constructor(opts) {
|
|
22
23
|
this.worktree = path.resolve(opts.worktree);
|
|
23
24
|
this.config = opts.config;
|
|
@@ -32,7 +33,9 @@ class WorktreeSyncScheduler {
|
|
|
32
33
|
scheduleSync(reason, filePath, checkRules) {
|
|
33
34
|
if (!this.config.sync.enabled)
|
|
34
35
|
return;
|
|
35
|
-
|
|
36
|
+
// Treat file.created, file.edited, and file.deleted same relevance-wise
|
|
37
|
+
const isFileLifecycle = reason === "file.edited" || reason === "file.created" || reason === "file.deleted";
|
|
38
|
+
if (isFileLifecycle) {
|
|
36
39
|
if (!filePath)
|
|
37
40
|
return;
|
|
38
41
|
if (!shouldHandleFile(filePath, this.worktree))
|
|
@@ -71,11 +74,30 @@ class WorktreeSyncScheduler {
|
|
|
71
74
|
this.scheduleSync(reason);
|
|
72
75
|
}
|
|
73
76
|
}
|
|
77
|
+
async flush() {
|
|
78
|
+
if (!this.config.sync.enabled)
|
|
79
|
+
return;
|
|
80
|
+
if (this.timer) {
|
|
81
|
+
this.clearTimeoutFn(this.timer);
|
|
82
|
+
this.timer = null;
|
|
83
|
+
}
|
|
84
|
+
this.flushPending();
|
|
85
|
+
if (this.isIdle()) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
await new Promise((resolve) => {
|
|
89
|
+
this.flushWaiters.push(resolve);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
74
92
|
dispose() {
|
|
75
93
|
if (this.timer) {
|
|
76
94
|
this.clearTimeoutFn(this.timer);
|
|
77
95
|
this.timer = null;
|
|
78
96
|
}
|
|
97
|
+
const waiters = this.flushWaiters.splice(0);
|
|
98
|
+
for (const waiter of waiters) {
|
|
99
|
+
waiter();
|
|
100
|
+
}
|
|
79
101
|
}
|
|
80
102
|
isToolExecuteAfterEnabled() {
|
|
81
103
|
if (this.explicitToolAfterHint)
|
|
@@ -147,6 +169,20 @@ class WorktreeSyncScheduler {
|
|
|
147
169
|
: {}),
|
|
148
170
|
});
|
|
149
171
|
}
|
|
172
|
+
this.resolveFlushWaitersIfIdle();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
isIdle() {
|
|
176
|
+
return !this.inFlight && !this.timer && !this.pending && !this.dirty && !this.trailing;
|
|
177
|
+
}
|
|
178
|
+
resolveFlushWaitersIfIdle() {
|
|
179
|
+
if (!this.isIdle())
|
|
180
|
+
return;
|
|
181
|
+
if (this.flushWaiters.length === 0)
|
|
182
|
+
return;
|
|
183
|
+
const waiters = this.flushWaiters.splice(0);
|
|
184
|
+
for (const waiter of waiters) {
|
|
185
|
+
waiter();
|
|
150
186
|
}
|
|
151
187
|
}
|
|
152
188
|
emitCompletion(trigger, startedAt, exitCode, checkExitCode, checkRules) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type EditEventKind = string;
|
|
2
|
+
export interface SessionEditEntry {
|
|
3
|
+
/** Relative file path (relative to worktree root). */
|
|
4
|
+
filePath: string;
|
|
5
|
+
/** Hash of the file content at first sight (baseline). "<deleted>" sentinel if file was missing. */
|
|
6
|
+
baselineHash: string;
|
|
7
|
+
/** Current hash at last reconciliation. */
|
|
8
|
+
currentHash: string;
|
|
9
|
+
/** Timestamp (ms) of last reconciliation pass. */
|
|
10
|
+
lastReconciledAt: number;
|
|
11
|
+
}
|
|
12
|
+
export interface SessionEditState {
|
|
13
|
+
recordEventHint(filePath: string, kind: EditEventKind, timestamp?: number): void;
|
|
14
|
+
reconcilePath(filePath: string): void;
|
|
15
|
+
reconcileKnownPaths(): void;
|
|
16
|
+
getSessionEdits(): SessionEditEntry[];
|
|
17
|
+
getFocusEdit(): SessionEditEntry | null;
|
|
18
|
+
hasSessionEdits(): boolean;
|
|
19
|
+
forceEdit(filePath: string, kind?: EditEventKind, timestamp?: number): void;
|
|
20
|
+
}
|
|
21
|
+
export declare function createSessionEditState(opts: {
|
|
22
|
+
worktree: string;
|
|
23
|
+
/** Custom clock for testing. Defaults to Date.now. */
|
|
24
|
+
now?: () => number;
|
|
25
|
+
}): SessionEditState;
|