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/README.md
CHANGED
|
@@ -115,10 +115,10 @@ OpenCode exposes Kibi MCP prompts as slash commands. The \`/init-kibi\` command
|
|
|
115
115
|
|
|
116
116
|
When the plugin detects an authoritative risky edit (`behavior_candidate` or `traceability_candidate` risk class), it automatically renders a Kibi briefing before the prompt. The plugin uses two complementary paths: the `file.edited` event hook as a fast-path hint, and prompt-cycle reconciliation as an authoritative fallback for programmatic edits that bypass the event bus. Briefings are rendered directly into the prompt to ensure immediate visibility.
|
|
117
117
|
|
|
118
|
-
- **Immediate delivery**: Briefings are rendered-first into the prompt guidance block headed `🧠 **Kibi briefing available**` and TUI toasts titled `Kibi Knowledge Update`.
|
|
118
|
+
- **Immediate delivery**: Briefings are rendered-first into the prompt guidance block headed `🧠 **Kibi briefing available**` and TUI toasts titled `Kibi Knowledge Update`. Unread briefings automatically open the interactive TUI via the `kibi.brief` route.
|
|
119
119
|
- **Narrative structure**: Delivery favors user-facing prose with `What changed` and `Why it matters`, plus conditional `Project knowledge impact` / `Interpretation note` sections when evidence or caveats exist.
|
|
120
120
|
- **TL;DR fallback**: If a full briefing is unavailable, fallback output still preserves `What changed` / `Why it matters` framing while keeping the manual command cue available.
|
|
121
|
-
- **Manual command**: Use `/brief-kibi` at any time to trigger an on-demand briefing if auto-delivery is skipped or fails.
|
|
121
|
+
- **Manual command**: Use `/brief-kibi` or the `kibi.open_latest_brief` command at any time to trigger an on-demand briefing if auto-delivery is skipped or fails.
|
|
122
122
|
|
|
123
123
|
### Discovery-first MCP guidance
|
|
124
124
|
|
|
@@ -136,6 +136,7 @@ Internal maintenance automatically syncs the knowledge base after relevant file
|
|
|
136
136
|
- Single-flight scheduler (no overlapping syncs)
|
|
137
137
|
- Debounce window (default: 2000ms)
|
|
138
138
|
- Dirty flag triggers one trailing rerun after active sync completes
|
|
139
|
+
- **Idle suppression**: Background sync attempts triggered by session idle are suppressed after an operational sync failure is latched (`scheduler_sync_failed`). Manual edits and tool executions continue to schedule syncs to allow for recovery.
|
|
139
140
|
|
|
140
141
|
### Non-Blocking UX
|
|
141
142
|
|
|
@@ -203,9 +204,11 @@ The plugin follows a **silent-except-operational-errors** policy for terminal ou
|
|
|
203
204
|
|
|
204
205
|
The logger exposes two error-level surfaces with distinct routing semantics:
|
|
205
206
|
|
|
206
|
-
- **`error(msg, metadata?)`** — Operational plugin failures. Emits exactly one prefixed `console.error` (`[kibi-opencode]`) for terminal visibility, plus `client.app.log()` when a client is bound. Structured log rejection does not emit secondary console noise. Use for bootstrap-needed, hook/init failures, and sync failures that require developer attention.
|
|
207
|
+
- **`error(msg, metadata?)`** — Operational plugin failures. Emits exactly one prefixed `console.error` (`[kibi-opencode]`) for terminal visibility, plus `client.app.log()` when a client is bound. Structured log rejection does not emit secondary console noise. Use for bootstrap-needed, hook/init failures, and sync failures that require developer attention. Operational sync failure payloads include diagnostic metadata: `syncCommand`, `syncStdout`, `syncStderr`, and `syncErrorMessage`.
|
|
207
208
|
- **`errorStructuredOnly(msg, metadata?)`** — Advisory background maintenance failures. Routes through `client.app.log()` only when a client is bound and remains terminal-silent even when the structured transport rejects. Use for scheduler check failures and degraded-mode latches.
|
|
208
209
|
|
|
210
|
+
|
|
211
|
+
|
|
209
212
|
**Contract rule:** Once `client` is bound (after `setClient()`), advisory paths (`info()`, `warn()`, `errorStructuredOnly()`) MUST stay on `client.app.log()` and remain terminal-silent. Operational failures use `error()` for a single prefixed terminal emission without duplicating console output when structured logging rejects.
|
|
210
213
|
|
|
211
214
|
Routine diagnostics route through [`client.app.log()`](https://opencode.ai/docs/plugins/) and never appear in the terminal. Only operational error-class events break terminal silence. This keeps the developer's workspace clean while preserving full visibility in structured logs for debugging.
|
|
@@ -217,9 +220,7 @@ The plugin uses the official OpenCode toast APIs with automatic capability detec
|
|
|
217
220
|
1. **Legacy transport**: `client.tui.toast(payload)` — used when available in plugin context
|
|
218
221
|
2. **SDK transport**: `client.tui.showToast({ body: payload })` — used as fallback
|
|
219
222
|
3. **No capability**: Returns `{ status: "unavailable", reason: "missing-capability" }`
|
|
220
|
-
|
|
221
|
-
All toast delivery is best-effort and non-blocking. The `sendToast` helper returns a discriminated `SendToastResult` union and never throws. There is no raw HTTP fallback.
|
|
222
|
-
|
|
223
|
+
The `sendToast` helper returns a discriminated `SendToastResult` union and never throws. There is no raw HTTP fallback. For rich briefing delivery, the plugin uses the `kibi.brief` route and the `kibi.open_latest_brief` command for manual retrieval.
|
|
223
224
|
The `experimental.chat.system.transform` hook handles prompt injection (see [Hook Policy](#hook-policy)). The `chat.params` hook is compatibility-only and never carries prompt text.
|
|
224
225
|
|
|
225
226
|
### Hook Modes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DeliveryReasons } from "./idle-brief-store.js";
|
|
2
|
+
export type BuildInput = {
|
|
3
|
+
entitiesAdded: string[];
|
|
4
|
+
entitiesModified: string[];
|
|
5
|
+
entitiesRemoved: string[];
|
|
6
|
+
relationshipsChanged: number;
|
|
7
|
+
validationCount: number;
|
|
8
|
+
conflictReasons?: string[];
|
|
9
|
+
};
|
|
10
|
+
export declare function buildDeliveryReasons(input: BuildInput): DeliveryReasons | undefined;
|
|
11
|
+
export declare function renderToastSummary(reasons: DeliveryReasons): DeliveryReasons["toast"] | undefined;
|
|
12
|
+
export declare function renderFullBriefReasons(reasons: DeliveryReasons): string;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Inlined from kibi-cli/operational-artifacts to avoid heavy module resolution
|
|
2
|
+
function isOperationalArtifactPath(pathLike) {
|
|
3
|
+
const normalized = pathLike.replaceAll("\\", "/");
|
|
4
|
+
return /(^|\/)\.sisyphus\//.test(normalized);
|
|
5
|
+
}
|
|
6
|
+
const ORDER = {
|
|
7
|
+
conflict_detected: 0,
|
|
8
|
+
validation_issue: 1,
|
|
9
|
+
entity_modified: 2,
|
|
10
|
+
entity_added: 3,
|
|
11
|
+
entity_removed: 4,
|
|
12
|
+
relationship_changed: 5,
|
|
13
|
+
};
|
|
14
|
+
const TYPE_NAMES = {
|
|
15
|
+
REQ: "requirement",
|
|
16
|
+
FACT: "fact",
|
|
17
|
+
TEST: "test",
|
|
18
|
+
SCEN: "scenario",
|
|
19
|
+
SYM: "code traceability",
|
|
20
|
+
ADR: "decision",
|
|
21
|
+
FLAG: "runtime flag",
|
|
22
|
+
EVENT: "event",
|
|
23
|
+
};
|
|
24
|
+
function prefixName(id) {
|
|
25
|
+
const prefix = id.split("-")[0]?.toUpperCase() ?? id;
|
|
26
|
+
return TYPE_NAMES[prefix] ?? prefix;
|
|
27
|
+
}
|
|
28
|
+
function mk(kind, text, entityIds) {
|
|
29
|
+
return { kind, text, entityIds };
|
|
30
|
+
}
|
|
31
|
+
function entityItems(kind, ids) {
|
|
32
|
+
if (!ids.length)
|
|
33
|
+
return [];
|
|
34
|
+
const verb = kind === "entity_added" ? "Added" : kind === "entity_modified" ? "Updated" : "Removed";
|
|
35
|
+
const grouped = new Map();
|
|
36
|
+
for (const id of [...ids].sort()) {
|
|
37
|
+
const prefix = id.split("-")[0]?.toUpperCase() ?? id;
|
|
38
|
+
const group = grouped.get(prefix);
|
|
39
|
+
if (group) {
|
|
40
|
+
group.push(id);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
grouped.set(prefix, [id]);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return [...grouped.entries()].map(([, groupedIds]) => {
|
|
47
|
+
const noun = prefixName(groupedIds[0] ?? "");
|
|
48
|
+
const text = groupedIds.length === 1
|
|
49
|
+
? `${verb} ${noun} ${groupedIds[0]}`
|
|
50
|
+
: `${verb} ${groupedIds.length} ${noun}s (${groupedIds.join(", ")})`;
|
|
51
|
+
return mk(kind, text, groupedIds);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function toastSummary(items) {
|
|
55
|
+
const first = items[0]?.text?.trim() ?? "";
|
|
56
|
+
const second = items[1]?.text?.trim() ?? "";
|
|
57
|
+
if (first && second)
|
|
58
|
+
return `${first}, ${second}`;
|
|
59
|
+
return first || second || undefined;
|
|
60
|
+
}
|
|
61
|
+
function toastWhy(items) {
|
|
62
|
+
if (items.some((i) => i.kind === "conflict_detected"))
|
|
63
|
+
return "There is a knowledge conflict to resolve before using the brief.";
|
|
64
|
+
if (items.some((i) => i.kind === "validation_issue"))
|
|
65
|
+
return "Validation issues need attention before the update is treated as settled.";
|
|
66
|
+
const hasEntities = items.some((i) => i.kind === "entity_added" || i.kind === "entity_modified" || i.kind === "entity_removed");
|
|
67
|
+
const hasRelationships = items.some((i) => i.kind === "relationship_changed");
|
|
68
|
+
if (hasEntities && hasRelationships)
|
|
69
|
+
return "Requirements and facts were updated.";
|
|
70
|
+
if (hasEntities)
|
|
71
|
+
return "Entities were updated.";
|
|
72
|
+
if (hasRelationships)
|
|
73
|
+
return "Relationships were updated.";
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
export function buildDeliveryReasons(input) {
|
|
77
|
+
const items = [];
|
|
78
|
+
if (input.conflictReasons?.length)
|
|
79
|
+
items.push(mk("conflict_detected", input.conflictReasons[0]?.trim() || "Conflict detected", []));
|
|
80
|
+
if (input.validationCount > 0)
|
|
81
|
+
items.push(mk("validation_issue", `${input.validationCount} validation issue${input.validationCount === 1 ? "" : "s"} detected`, []));
|
|
82
|
+
items.push(...entityItems("entity_modified", input.entitiesModified));
|
|
83
|
+
items.push(...entityItems("entity_added", input.entitiesAdded));
|
|
84
|
+
items.push(...entityItems("entity_removed", input.entitiesRemoved));
|
|
85
|
+
if (input.relationshipsChanged > 0)
|
|
86
|
+
items.push(mk("relationship_changed", `Updated ${input.relationshipsChanged} relationships`, []));
|
|
87
|
+
if (!items.length)
|
|
88
|
+
return undefined;
|
|
89
|
+
items.sort((a, b) => ORDER[a.kind] - ORDER[b.kind]);
|
|
90
|
+
return { version: 1, items, toast: { title: "Kibi Knowledge Update", summary: toastSummary(items) ?? "", whyItMatters: toastWhy(items) ?? "" } };
|
|
91
|
+
}
|
|
92
|
+
function isOperationalByEntityIds(item) {
|
|
93
|
+
if (item.entityIds.length === 0)
|
|
94
|
+
return false;
|
|
95
|
+
return item.entityIds.every((id) => {
|
|
96
|
+
const dashIdx = id.indexOf("-");
|
|
97
|
+
if (dashIdx < 0)
|
|
98
|
+
return false;
|
|
99
|
+
const name = id.slice(dashIdx + 1);
|
|
100
|
+
// Entity names with file extensions are likely from operational artifact files
|
|
101
|
+
return /\.[a-zA-Z0-9]+$/.test(name);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function isOperationalItem(item, allItems) {
|
|
105
|
+
if (item.kind === "relationship_changed") {
|
|
106
|
+
// relationship_changed items have no entityIds; treat as operational when
|
|
107
|
+
// all entity-level items in the set are operational (they're relationship side-effects)
|
|
108
|
+
const entityItems = allItems.filter((i) => i.kind === "entity_added" || i.kind === "entity_modified" || i.kind === "entity_removed");
|
|
109
|
+
return entityItems.length > 0 && entityItems.every(isOperationalByEntityIds);
|
|
110
|
+
}
|
|
111
|
+
return isOperationalByEntityIds(item);
|
|
112
|
+
}
|
|
113
|
+
export function renderToastSummary(reasons) {
|
|
114
|
+
const domainItems = reasons.items.filter((i) => !isOperationalItem(i, reasons.items));
|
|
115
|
+
if (domainItems.length === 0) {
|
|
116
|
+
return undefined; // suppress: specific-or-silent policy
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
title: "Kibi Knowledge Update",
|
|
120
|
+
summary: toastSummary(domainItems) ?? "",
|
|
121
|
+
whyItMatters: toastWhy(domainItems) ?? "",
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
export function renderFullBriefReasons(reasons) {
|
|
125
|
+
const domainItems = reasons.items.filter((i) => !isOperationalItem(i, reasons.items));
|
|
126
|
+
const lines = ["## What changed", ...domainItems.map((r) => `- ${r.text}`)];
|
|
127
|
+
const whyItMatters = toastWhy(domainItems);
|
|
128
|
+
if (whyItMatters) {
|
|
129
|
+
lines.push("", "## Why it matters", whyItMatters);
|
|
130
|
+
}
|
|
131
|
+
return lines.join("\n");
|
|
132
|
+
}
|
package/dist/brief-intent.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
// implements REQ-opencode-kibi-briefing-v2, REQ-opencode-smart-enforcement-v1
|
|
2
|
+
// Inlined from kibi-cli/operational-artifacts to avoid heavy module resolution
|
|
3
|
+
function isOperationalArtifactPath(pathLike) {
|
|
4
|
+
const normalized = pathLike.replaceAll("\\", "/");
|
|
5
|
+
return /(^|\/)\.sisyphus\//.test(normalized);
|
|
6
|
+
}
|
|
2
7
|
import { getSourceLinkedRequirementIds } from "./source-linked-guidance.js";
|
|
3
8
|
const ELIGIBLE_RISK_CLASSES = new Set([
|
|
4
9
|
"behavior_candidate",
|
|
@@ -30,7 +35,8 @@ function deriveSeedIds(params) {
|
|
|
30
35
|
// implements REQ-opencode-kibi-briefing-v2, REQ-opencode-smart-enforcement-v1
|
|
31
36
|
export function deriveBriefIntent(params) {
|
|
32
37
|
const sortedSourceFiles = sortAndDedup(params.sourceFiles);
|
|
33
|
-
const
|
|
38
|
+
const nonOperationalSourceFiles = sortedSourceFiles.filter((f) => !isOperationalArtifactPath(f));
|
|
39
|
+
const fingerprint = `brief:${params.workspaceRoot}\0${params.branch}\0${params.riskClass}\0${nonOperationalSourceFiles.join("\0")}`;
|
|
34
40
|
const seedIds = deriveSeedIds(params);
|
|
35
41
|
if (sortedSourceFiles.length === 0) {
|
|
36
42
|
return {
|
|
@@ -41,6 +47,15 @@ export function deriveBriefIntent(params) {
|
|
|
41
47
|
seedIds: [],
|
|
42
48
|
};
|
|
43
49
|
}
|
|
50
|
+
if (nonOperationalSourceFiles.length === 0) {
|
|
51
|
+
return {
|
|
52
|
+
eligible: false,
|
|
53
|
+
reason: "All source changes are operational task-tracking artifacts",
|
|
54
|
+
fingerprint,
|
|
55
|
+
sourceFiles: sortedSourceFiles,
|
|
56
|
+
seedIds: [],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
44
59
|
if (!ELIGIBLE_RISK_CLASSES.has(params.riskClass)) {
|
|
45
60
|
return {
|
|
46
61
|
eligible: false,
|
|
@@ -72,7 +87,7 @@ export function deriveBriefIntent(params) {
|
|
|
72
87
|
eligible: true,
|
|
73
88
|
reason: "Eligible for auto-briefing",
|
|
74
89
|
fingerprint,
|
|
75
|
-
sourceFiles:
|
|
90
|
+
sourceFiles: nonOperationalSourceFiles,
|
|
76
91
|
seedIds,
|
|
77
92
|
};
|
|
78
93
|
}
|
|
@@ -13,6 +13,18 @@ export declare function selectLatestUnreadBrief(workspaceRoot: string, branch: s
|
|
|
13
13
|
envelope: IdleBriefEnvelope;
|
|
14
14
|
filePath: string;
|
|
15
15
|
} | null;
|
|
16
|
+
/**
|
|
17
|
+
* Select the latest persisted brief for the given branch, regardless of read status.
|
|
18
|
+
*
|
|
19
|
+
* Same scanning logic as `selectLatestUnreadBrief` but returns the latest brief
|
|
20
|
+
* even if all briefs have been read. Useful for route rendering where the user
|
|
21
|
+
* wants to review the most recent brief.
|
|
22
|
+
*/
|
|
23
|
+
export declare function selectLatestPersistedBrief(// implements REQ-opencode-kibi-briefing-v6
|
|
24
|
+
workspaceRoot: string, branch: string): {
|
|
25
|
+
envelope: IdleBriefEnvelope;
|
|
26
|
+
filePath: string;
|
|
27
|
+
} | null;
|
|
16
28
|
/**
|
|
17
29
|
* Atomically mark a brief as read by setting `unread` to false.
|
|
18
30
|
*
|
|
@@ -56,16 +56,13 @@ function extractTimestamp(filename) {
|
|
|
56
56
|
return Number(match[1]);
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
|
-
*
|
|
59
|
+
* Internal shared scanner for brief files.
|
|
60
60
|
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* or null if no unread briefs exist.
|
|
61
|
+
* @param workspaceRoot - The root of the workspace
|
|
62
|
+
* @param branch - Branch name to filter by
|
|
63
|
+
* @param filterUnread - If true, only return briefs with unread === true
|
|
65
64
|
*/
|
|
66
|
-
|
|
67
|
-
// implements REQ-opencode-kibi-briefing-v4
|
|
68
|
-
workspaceRoot, branch) {
|
|
65
|
+
function scanBriefs(workspaceRoot, branch, filterUnread) {
|
|
69
66
|
const briefsDir = resolveBriefsDir(workspaceRoot);
|
|
70
67
|
if (!fs.existsSync(briefsDir)) {
|
|
71
68
|
return null;
|
|
@@ -93,10 +90,10 @@ workspaceRoot, branch) {
|
|
|
93
90
|
// Skip invalid JSON
|
|
94
91
|
continue;
|
|
95
92
|
}
|
|
96
|
-
// Filter by branch, schemaVersion, and unread status
|
|
93
|
+
// Filter by branch, schemaVersion, and optionally unread status
|
|
97
94
|
if (envelope.branch === branch &&
|
|
98
95
|
(envelope.schemaVersion === "1.0" || envelope.schemaVersion === "2.0") &&
|
|
99
|
-
envelope.unread === true) {
|
|
96
|
+
(!filterUnread || envelope.unread === true)) {
|
|
100
97
|
candidates.push({ timestamp, envelope, filePath });
|
|
101
98
|
}
|
|
102
99
|
}
|
|
@@ -114,6 +111,30 @@ workspaceRoot, branch) {
|
|
|
114
111
|
filePath: latest.filePath,
|
|
115
112
|
};
|
|
116
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Select the latest unread brief for the given branch.
|
|
116
|
+
*
|
|
117
|
+
* Scans `.kb/briefs/` for `{timestamp}_brief.json` files, ignoring `.tmp` files
|
|
118
|
+
* and invalid JSON. Filters by `branch`, supported schema version, and
|
|
119
|
+
* `unread === true`. Returns the brief with the highest filename timestamp,
|
|
120
|
+
* or null if no unread briefs exist.
|
|
121
|
+
*/
|
|
122
|
+
export function selectLatestUnreadBrief(
|
|
123
|
+
// implements REQ-opencode-kibi-briefing-v4
|
|
124
|
+
workspaceRoot, branch) {
|
|
125
|
+
return scanBriefs(workspaceRoot, branch, true);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Select the latest persisted brief for the given branch, regardless of read status.
|
|
129
|
+
*
|
|
130
|
+
* Same scanning logic as `selectLatestUnreadBrief` but returns the latest brief
|
|
131
|
+
* even if all briefs have been read. Useful for route rendering where the user
|
|
132
|
+
* wants to review the most recent brief.
|
|
133
|
+
*/
|
|
134
|
+
export function selectLatestPersistedBrief(// implements REQ-opencode-kibi-briefing-v6
|
|
135
|
+
workspaceRoot, branch) {
|
|
136
|
+
return scanBriefs(workspaceRoot, branch, false);
|
|
137
|
+
}
|
|
117
138
|
/**
|
|
118
139
|
* Atomically mark a brief as read by setting `unread` to false.
|
|
119
140
|
*
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// implements REQ-opencode-kibi-briefing-v4
|
|
2
2
|
import { buildBriefingContext } from "./brief-intent.js";
|
|
3
|
+
import { buildDeliveryReasons } from "./brief-delivery-reasons.js";
|
|
3
4
|
import { atomicWriteBrief, pruneOldBriefs, resolveBriefFilePath, } from "./idle-brief-paths.js";
|
|
4
5
|
import { computeContentHash, createBriefId, } from "./idle-brief-store.js";
|
|
5
6
|
import { reconcileAuditEntries } from "./reconcile-engine.js";
|
|
@@ -273,6 +274,13 @@ function computeSummary(counts, violationsCount) {
|
|
|
273
274
|
const changeText = parts.length > 0 ? parts.join(", ") : "no changes";
|
|
274
275
|
return `${changeText} | ${validationText}`;
|
|
275
276
|
}
|
|
277
|
+
function hasSignificantBriefingImpact(briefingResult) {
|
|
278
|
+
return !(briefingResult.briefingState === "no_briefing" &&
|
|
279
|
+
briefingResult.citations.length === 0 &&
|
|
280
|
+
(!briefingResult.constraints || briefingResult.constraints.length === 0) &&
|
|
281
|
+
(!briefingResult.regressionRisks || briefingResult.regressionRisks.length === 0) &&
|
|
282
|
+
(!briefingResult.missingEvidence || briefingResult.missingEvidence.length === 0));
|
|
283
|
+
}
|
|
276
284
|
function humanizeEntityType(type) {
|
|
277
285
|
switch (type) {
|
|
278
286
|
case "req":
|
|
@@ -310,7 +318,7 @@ function buildChangeNarrative(auditDelta) {
|
|
|
310
318
|
}
|
|
311
319
|
return lines;
|
|
312
320
|
}
|
|
313
|
-
function buildEnvelopeParts(briefId, type, sessionId, branch, createdAt, auditDelta, summary, counts, checkResult, briefingResult) {
|
|
321
|
+
function buildEnvelopeParts(briefId, type, sessionId, branch, createdAt, auditDelta, summary, counts, checkResult, briefingResult, deliveryReasons) {
|
|
314
322
|
const reconciled = reconcileAuditEntries(auditDelta.entries);
|
|
315
323
|
return {
|
|
316
324
|
schemaVersion: "2.0",
|
|
@@ -343,6 +351,7 @@ function buildEnvelopeParts(briefId, type, sessionId, branch, createdAt, auditDe
|
|
|
343
351
|
promptBlock: briefingResult.promptBlock,
|
|
344
352
|
citations: briefingResult.citations,
|
|
345
353
|
changeNarrative: buildChangeNarrative(auditDelta),
|
|
354
|
+
...(deliveryReasons ? { deliveryReasons } : {}),
|
|
346
355
|
...(briefingResult.constraints && briefingResult.constraints.length > 0
|
|
347
356
|
? { constraints: briefingResult.constraints }
|
|
348
357
|
: {}),
|
|
@@ -362,13 +371,6 @@ export async function generateIdleBrief(client, workspaceCtx, auditDelta, sessio
|
|
|
362
371
|
if (!client) {
|
|
363
372
|
return { success: true, briefPath: null, envelope: null };
|
|
364
373
|
}
|
|
365
|
-
if (!auditDelta.hasChanges) {
|
|
366
|
-
return {
|
|
367
|
-
success: true,
|
|
368
|
-
briefPath: null,
|
|
369
|
-
envelope: null,
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
374
|
const reconciled = reconcileAuditEntries(auditDelta.entries);
|
|
373
375
|
const derivedSourceFiles = [
|
|
374
376
|
...reconciled.added
|
|
@@ -414,13 +416,46 @@ export async function generateIdleBrief(client, workspaceCtx, auditDelta, sessio
|
|
|
414
416
|
}
|
|
415
417
|
const counts = computeCounts(auditDelta);
|
|
416
418
|
const violationsCount = checkResult.violations.length;
|
|
419
|
+
if (counts.entitiesAdded === 0 &&
|
|
420
|
+
counts.entitiesModified === 0 &&
|
|
421
|
+
counts.entitiesRemoved === 0 &&
|
|
422
|
+
counts.relationshipsChanged === 0 &&
|
|
423
|
+
checkResult.count === 0 &&
|
|
424
|
+
briefingResult.briefingState === "no_briefing" &&
|
|
425
|
+
briefingResult.citations.length === 0 &&
|
|
426
|
+
!briefingResult.constraints?.length &&
|
|
427
|
+
!briefingResult.regressionRisks?.length &&
|
|
428
|
+
!briefingResult.missingEvidence?.length) {
|
|
429
|
+
return { success: true, briefPath: null, envelope: null };
|
|
430
|
+
}
|
|
417
431
|
const isSuccess = violationsCount === 0;
|
|
418
432
|
const type = isSuccess ? "success" : "warning";
|
|
419
433
|
const summary = computeSummary(counts, violationsCount);
|
|
434
|
+
const deliveryReasons = buildDeliveryReasons({
|
|
435
|
+
entitiesAdded: reconciled.added
|
|
436
|
+
.filter((item) => item.id !== "workspace-sync")
|
|
437
|
+
.map((item) => item.id),
|
|
438
|
+
entitiesModified: reconciled.modified
|
|
439
|
+
.filter((item) => item.id !== "workspace-sync")
|
|
440
|
+
.map((item) => item.id),
|
|
441
|
+
entitiesRemoved: reconciled.removed
|
|
442
|
+
.filter((item) => item.id !== "workspace-sync")
|
|
443
|
+
.map((item) => item.id),
|
|
444
|
+
relationshipsChanged: counts.relationshipsChanged,
|
|
445
|
+
validationCount: checkResult.count,
|
|
446
|
+
});
|
|
447
|
+
if (counts.entitiesAdded === 0 &&
|
|
448
|
+
counts.entitiesModified === 0 &&
|
|
449
|
+
counts.entitiesRemoved === 0 &&
|
|
450
|
+
counts.relationshipsChanged === 0 &&
|
|
451
|
+
violationsCount === 0 &&
|
|
452
|
+
!hasSignificantBriefingImpact(briefingResult)) {
|
|
453
|
+
return { success: true, briefPath: null, envelope: null };
|
|
454
|
+
}
|
|
420
455
|
const briefId = createBriefId();
|
|
421
456
|
const timestamp = Date.now();
|
|
422
457
|
const createdAt = new Date().toISOString();
|
|
423
|
-
const envelopeWithoutHash = buildEnvelopeParts(briefId, type, sessionId, workspaceCtx.branch, createdAt, auditDelta, summary, counts, checkResult, briefingResult);
|
|
458
|
+
const envelopeWithoutHash = buildEnvelopeParts(briefId, type, sessionId, workspaceCtx.branch, createdAt, auditDelta, summary, counts, checkResult, briefingResult, deliveryReasons);
|
|
424
459
|
const contentHash = computeContentHash(envelopeWithoutHash);
|
|
425
460
|
const envelope = {
|
|
426
461
|
...envelopeWithoutHash,
|
|
@@ -16,6 +16,22 @@ export interface IdleBriefStatement {
|
|
|
16
16
|
statement: string;
|
|
17
17
|
citationIds: string[];
|
|
18
18
|
}
|
|
19
|
+
export type ReasonItem = {
|
|
20
|
+
kind: "entity_added" | "entity_modified" | "entity_removed" | "relationship_changed" | "validation_issue" | "conflict_detected";
|
|
21
|
+
text: string;
|
|
22
|
+
entityIds: string[];
|
|
23
|
+
citationIds?: string[];
|
|
24
|
+
severity?: "info" | "warning" | "error";
|
|
25
|
+
};
|
|
26
|
+
export type DeliveryReasons = {
|
|
27
|
+
version: 1;
|
|
28
|
+
toast: {
|
|
29
|
+
title: string;
|
|
30
|
+
summary: string;
|
|
31
|
+
whyItMatters: string;
|
|
32
|
+
};
|
|
33
|
+
items: ReasonItem[];
|
|
34
|
+
};
|
|
19
35
|
export interface IdleBriefValidationViolation {
|
|
20
36
|
rule: string;
|
|
21
37
|
entityId: string;
|
|
@@ -85,6 +101,7 @@ export interface IdleBriefEnvelopeV2 extends IdleBriefBaseEnvelope {
|
|
|
85
101
|
promptBlock: string;
|
|
86
102
|
citations: IdleBriefCitation[];
|
|
87
103
|
changeNarrative: string[];
|
|
104
|
+
deliveryReasons?: DeliveryReasons;
|
|
88
105
|
constraints?: IdleBriefStatement[];
|
|
89
106
|
regressionRisks?: IdleBriefStatement[];
|
|
90
107
|
missingEvidence?: IdleBriefStatement[];
|
package/dist/idle-brief-store.js
CHANGED
|
@@ -58,13 +58,39 @@ function isBriefingBase(value) {
|
|
|
58
58
|
}
|
|
59
59
|
function isBriefingV2(value) {
|
|
60
60
|
return (isBriefingBase(value) &&
|
|
61
|
-
isStringArray(value.changeNarrative)
|
|
61
|
+
isStringArray(value.changeNarrative) &&
|
|
62
|
+
(value.deliveryReasons === undefined ||
|
|
63
|
+
isDeliveryReasons(value.deliveryReasons)));
|
|
64
|
+
}
|
|
65
|
+
function isReasonItem(value) {
|
|
66
|
+
return (isRecord(value) &&
|
|
67
|
+
typeof value.kind === "string" &&
|
|
68
|
+
typeof value.text === "string" &&
|
|
69
|
+
isStringArray(value.entityIds) &&
|
|
70
|
+
(value.citationIds === undefined || isStringArray(value.citationIds)) &&
|
|
71
|
+
(value.severity === undefined ||
|
|
72
|
+
value.severity === "info" ||
|
|
73
|
+
value.severity === "warning" ||
|
|
74
|
+
value.severity === "error"));
|
|
75
|
+
}
|
|
76
|
+
function isDeliveryReasons(value) {
|
|
77
|
+
return (isRecord(value) &&
|
|
78
|
+
value.version === 1 &&
|
|
79
|
+
isRecord(value.toast) &&
|
|
80
|
+
typeof value.toast.title === "string" &&
|
|
81
|
+
typeof value.toast.summary === "string" &&
|
|
82
|
+
typeof value.toast.whyItMatters === "string" &&
|
|
83
|
+
Array.isArray(value.items) &&
|
|
84
|
+
value.items.every(isReasonItem));
|
|
62
85
|
}
|
|
63
86
|
function isChangeItem(value) {
|
|
64
87
|
return (isRecord(value) &&
|
|
65
88
|
typeof value.id === "string" &&
|
|
66
89
|
typeof value.type === "string");
|
|
67
90
|
}
|
|
91
|
+
function hasRenderableText(value) {
|
|
92
|
+
return value.trim().replace(/\s+/g, " ").length > 0;
|
|
93
|
+
}
|
|
68
94
|
export function isIdleBriefEnvelope(
|
|
69
95
|
// implements REQ-opencode-kibi-briefing-v4
|
|
70
96
|
value) {
|
|
@@ -127,6 +153,31 @@ export function computeContentHash(payload) {
|
|
|
127
153
|
statement: norm(statement.statement),
|
|
128
154
|
citationIds: statement.citationIds,
|
|
129
155
|
}));
|
|
156
|
+
const normalizeReasonItems = (items = []) => items
|
|
157
|
+
.map((item) => ({
|
|
158
|
+
kind: item.kind,
|
|
159
|
+
text: norm(item.text),
|
|
160
|
+
entityIds: [...item.entityIds].sort(),
|
|
161
|
+
...(item.citationIds ? { citationIds: [...item.citationIds].sort() } : {}),
|
|
162
|
+
...(item.severity ? { severity: item.severity } : {}),
|
|
163
|
+
}))
|
|
164
|
+
.filter((item) => hasRenderableText(item.text));
|
|
165
|
+
const normalizeDeliveryReasons = (deliveryReasons) => {
|
|
166
|
+
if (!deliveryReasons)
|
|
167
|
+
return undefined;
|
|
168
|
+
const items = normalizeReasonItems(deliveryReasons.items);
|
|
169
|
+
if (items.length === 0)
|
|
170
|
+
return undefined;
|
|
171
|
+
return {
|
|
172
|
+
version: deliveryReasons.version,
|
|
173
|
+
toast: {
|
|
174
|
+
title: norm(deliveryReasons.toast.title),
|
|
175
|
+
summary: norm(deliveryReasons.toast.summary),
|
|
176
|
+
whyItMatters: norm(deliveryReasons.toast.whyItMatters),
|
|
177
|
+
},
|
|
178
|
+
items,
|
|
179
|
+
};
|
|
180
|
+
};
|
|
130
181
|
const normalizeChangeItems = (items) => items.map((item) => ({
|
|
131
182
|
id: item.id,
|
|
132
183
|
type: norm(item.type),
|
|
@@ -156,6 +207,7 @@ export function computeContentHash(payload) {
|
|
|
156
207
|
normalizedPromptBlock: norm(env.briefing.promptBlock),
|
|
157
208
|
citations: normalizeCitations(env.briefing.citations ?? []),
|
|
158
209
|
changeNarrative: env.briefing.changeNarrative.map((line) => norm(line)),
|
|
210
|
+
deliveryReasons: normalizeDeliveryReasons(env.briefing.deliveryReasons),
|
|
159
211
|
constraints: normalizeStatements(env.briefing.constraints),
|
|
160
212
|
regressionRisks: normalizeStatements(env.briefing.regressionRisks),
|
|
161
213
|
missingEvidence: normalizeStatements(env.briefing.missingEvidence),
|
|
@@ -192,6 +244,7 @@ export function computeContentHash(payload) {
|
|
|
192
244
|
statement: norm(m.statement),
|
|
193
245
|
citationIds: m.citationIds,
|
|
194
246
|
})),
|
|
247
|
+
deliveryReasons: normalizeDeliveryReasons(env.briefing.deliveryReasons),
|
|
195
248
|
},
|
|
196
249
|
validation: {
|
|
197
250
|
count: env.validation.count,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,52 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
worktree: string;
|
|
4
|
-
directory: string;
|
|
5
|
-
sessionId?: string;
|
|
6
|
-
serverUrl?: unknown;
|
|
7
|
-
workspace?: string;
|
|
8
|
-
project?: unknown;
|
|
9
|
-
$?: unknown;
|
|
10
|
-
client?: {
|
|
11
|
-
tui?: {
|
|
12
|
-
toast?: (payload: {
|
|
13
|
-
variant?: "info" | "success" | "warning" | "error";
|
|
14
|
-
title?: string;
|
|
15
|
-
message: string;
|
|
16
|
-
duration?: number;
|
|
17
|
-
}) => void | Promise<void>;
|
|
18
|
-
showToast?: (payload: {
|
|
19
|
-
body: {
|
|
20
|
-
variant?: "info" | "success" | "warning" | "error";
|
|
21
|
-
title?: string;
|
|
22
|
-
message: string;
|
|
23
|
-
duration?: number;
|
|
24
|
-
};
|
|
25
|
-
}) => void | Promise<void>;
|
|
26
|
-
clearPrompt?: () => void | Promise<void>;
|
|
27
|
-
submitPrompt?: () => void | Promise<void>;
|
|
28
|
-
};
|
|
29
|
-
app: {
|
|
30
|
-
log: (payload: Record<string, unknown>) => Promise<void>;
|
|
31
|
-
};
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
interface OpencodeEventPayload {
|
|
35
|
-
type: string;
|
|
36
|
-
properties?: Record<string, unknown>;
|
|
37
|
-
}
|
|
38
|
-
interface EventHookInput {
|
|
39
|
-
event: OpencodeEventPayload;
|
|
40
|
-
}
|
|
41
|
-
interface SystemTransformOutput {
|
|
42
|
-
system: string[];
|
|
43
|
-
}
|
|
44
|
-
export interface Hooks {
|
|
45
|
-
event?: (input: EventHookInput) => void | Promise<void>;
|
|
46
|
-
config?: (input: OpenCodeConfigHookInput) => void | Promise<void>;
|
|
47
|
-
"experimental.chat.system.transform"?: (input: unknown, output: SystemTransformOutput) => void | Promise<void>;
|
|
48
|
-
"chat.params"?: (input: unknown, output: unknown) => void | Promise<void>;
|
|
49
|
-
}
|
|
50
|
-
export type Plugin = (input: PluginInput) => Hooks | Promise<Hooks>;
|
|
51
|
-
declare const kibiOpencodePlugin: Plugin;
|
|
52
|
-
export default kibiOpencodePlugin;
|
|
1
|
+
export { default } from "./plugin.js";
|
|
2
|
+
export type { Hooks, Plugin, PluginInput } from "./plugin.js";
|