kibi-opencode 0.7.2 → 0.9.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 +15 -4
- package/dist/brief-intent.d.ts +30 -0
- package/dist/brief-intent.js +89 -0
- package/dist/briefing-runtime.d.ts +24 -0
- package/dist/briefing-runtime.js +276 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +53 -4
- package/dist/knowledge-classifier.js +1 -1
- package/dist/plugin-startup.js +11 -1
- package/dist/prompt.d.ts +3 -0
- package/dist/prompt.js +112 -32
- package/dist/scheduler.js +7 -0
- package/dist/startup-notifier.d.ts +3 -18
- package/dist/startup-notifier.js +3 -8
- package/dist/toast.d.ts +32 -0
- package/dist/toast.js +18 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,6 +25,7 @@ The plugin now uses a posture-aware, low-token smart-enforcement model before em
|
|
|
25
25
|
- **Repo posture detection**: distinguishes `root_active`, `root_partial`, `root_uninitialized`, `vendored_only`, and `hybrid_root_plus_vendored`
|
|
26
26
|
- **Risk classification**: separates `safe_docs_only`, `safe_test_only`, `kb_doc_structural`, `req_policy_candidate`, `behavior_candidate`, `traceability_candidate`, and `manual_kb_edit`
|
|
27
27
|
- **Source-linked micro-briefs**: risky code edits (`behavior_candidate`, `traceability_candidate`) prepend a concise list of existing Kibi links (e.g., `- Existing Kibi links: REQ-001, REQ-002`) when 1-3 concrete source-linked KB hits are found in `documentation/symbols.yaml`. Skip on cache hit.
|
|
28
|
+
- **Start-task risky cue**: authoritative risky edits also add a compact `/brief-kibi` cue so agents can start with an explicit Kibi briefing before acting, while staying inside the same single prompt block and token budget.
|
|
28
29
|
- **Effective mode gating**: `strict` is only possible for `root_active` and `hybrid_root_plus_vendored` when `requireRootKbForStrict` is enabled; `maintenanceDegraded` overrides everything back to `advisory`
|
|
29
30
|
- **Low-token prompt policy**: docs-only and test-only edits avoid unnecessary discovery prompts; vendored-only repos suppress operational bootstrap nudges; at most one contextual block is injected per prompt (≤120 words, ≤5 bullets)
|
|
30
31
|
- **Completion reminder**: when `completionReminder` is enabled, risky code edits append a single prompt-visible `kb_check` reminder exactly once per cached context
|
|
@@ -45,10 +46,10 @@ The plugin provides context-aware prompt guidance based on recent edits and work
|
|
|
45
46
|
|
|
46
47
|
After KB-document edits, the plugin queues targeted validation rules to run via background sync operations:
|
|
47
48
|
|
|
48
|
-
- **Must-priority requirement edits**: elevated validation including coverage checks (`must-priority-coverage`)
|
|
49
|
+
- **Must-priority requirement edits**: elevated validation including coverage checks (`must-priority-coverage`) and `strict-req-fact-pairing`
|
|
49
50
|
- **Traceability candidate code edits**: schedules `symbol-traceability` via reason `smart-enforcement.traceability`
|
|
50
51
|
- **Fact KB doc edits**: includes `strict-fact-shape` validation alongside standard structural checks
|
|
51
|
-
- **
|
|
52
|
+
- **Requirement KB doc edits**: includes `strict-req-fact-pairing` validation alongside standard structural checks
|
|
52
53
|
|
|
53
54
|
The plugin inspects requirement frontmatter to detect `priority: must` and schedules elevated validation for critical requirements. Runs in background after sync completes, non-blocking. Can be disabled via `guidance.targetedChecks.enabled: false`.
|
|
54
55
|
|
|
@@ -87,9 +88,9 @@ When editing code files, the plugin analyzes long comments and docstrings for du
|
|
|
87
88
|
|
|
88
89
|
- **Supported languages**: JavaScript/TypeScript (`//`, `/* */`, `/** */`) and Python (`#` blocks, true docstrings)
|
|
89
90
|
- **Smart filtering**: Only analyzes comments above `guidance.commentDetection.minLines` threshold
|
|
90
|
-
- **Classification**: Automatically categorizes as FACT (invariants/limits), ADR (decisions/tradeoffs), REQ (behavior), SCEN (flows), or TEST (verification)
|
|
91
|
+
- **Classification**: Automatically categorizes as FACT (strict domain facts: invariants/limits/cardinalities), ADR (decisions/tradeoffs), REQ (behavior), SCEN (flows), or TEST (verification)
|
|
91
92
|
- **Specific routing guidance**: Injects targeted prompts based on classification:
|
|
92
|
-
- FACT: "This looks like a domain invariant; route to a FACT via Kibi"
|
|
93
|
+
- FACT: "This looks like a strict domain fact (invariant/property/limit); route to a FACT via Kibi. Bug/workaround notes use `fact_kind: observation` or `meta`."
|
|
93
94
|
- ADR: "This looks like decision rationale; route to an ADR"
|
|
94
95
|
- REQ: "This looks like behavior intent; route to a REQ"
|
|
95
96
|
- **Deduplication**: Tracks seen comments by fingerprint to avoid repeated guidance
|
|
@@ -111,6 +112,16 @@ The plugin injects guidance into OpenCode sessions to improve agent grounding. U
|
|
|
111
112
|
### Bootstrap Command
|
|
112
113
|
|
|
113
114
|
OpenCode exposes Kibi MCP prompts as slash commands. The \`/init-kibi\` command triggers the \`kb_autopilot_generate\` workflow to assist in retroactive bootstrap using only public MCP tools.
|
|
115
|
+
|
|
116
|
+
### Start-Task Briefing
|
|
117
|
+
|
|
118
|
+
When the plugin detects an authoritative risky edit (`behavior_candidate` or `traceability_candidate` risk class), it automatically fetches a Kibi briefing from a background worker session via the `file.edited` event path. Auto-briefing is no longer deferred and provides immediate project context before you act.
|
|
119
|
+
|
|
120
|
+
- **Automatic delivery**: Briefings appear in a toast notification and inside the guidance block headed `🧠 **Kibi briefing available**`.
|
|
121
|
+
- **Contextual richness**: The briefing includes a summary and key source-linked bullets generated by the `kb_briefing_generate` MCP tool.
|
|
122
|
+
- **TL;DR fallback**: If a full briefing is unavailable, a summary is provided with a cue to use the manual command.
|
|
123
|
+
- **Manual command**: Use `/brief-kibi` at any time to trigger an on-demand briefing if auto-delivery is skipped or fails.
|
|
124
|
+
|
|
114
125
|
### Discovery-first MCP guidance
|
|
115
126
|
|
|
116
127
|
Agent-visible guidance is intentionally limited to the curated public MCP surface:
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { RepoPosture } from "./repo-posture.js";
|
|
2
|
+
import type { RiskClass } from "./risk-classifier.js";
|
|
3
|
+
export interface BriefIntentParams {
|
|
4
|
+
riskClass: RiskClass;
|
|
5
|
+
posture: RepoPosture;
|
|
6
|
+
maintenanceDegraded: boolean;
|
|
7
|
+
workspaceRoot: string;
|
|
8
|
+
branch: string;
|
|
9
|
+
editedFilePath: string | undefined;
|
|
10
|
+
seedIds?: string[];
|
|
11
|
+
}
|
|
12
|
+
export interface BriefIntentResult {
|
|
13
|
+
eligible: boolean;
|
|
14
|
+
reason: string;
|
|
15
|
+
fingerprint: string;
|
|
16
|
+
sourceFiles: string[];
|
|
17
|
+
seedIds: string[];
|
|
18
|
+
}
|
|
19
|
+
export interface BriefIntentInputs {
|
|
20
|
+
riskClass: RiskClass;
|
|
21
|
+
posture: RepoPosture;
|
|
22
|
+
maintenanceDegraded: boolean;
|
|
23
|
+
worktreeRoot: string;
|
|
24
|
+
branch: string;
|
|
25
|
+
editedFile: string | undefined;
|
|
26
|
+
seedIds?: string[];
|
|
27
|
+
}
|
|
28
|
+
export declare function deriveBriefIntent(params: BriefIntentParams): BriefIntentResult;
|
|
29
|
+
export declare function computeBriefIntent(// implements REQ-opencode-kibi-briefing-v2
|
|
30
|
+
inputs: BriefIntentInputs): BriefIntentResult;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// implements REQ-opencode-kibi-briefing-v2, REQ-opencode-smart-enforcement-v1
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { getSourceLinkedRequirementIds } from "./source-linked-guidance.js";
|
|
4
|
+
const ELIGIBLE_RISK_CLASSES = new Set([
|
|
5
|
+
"behavior_candidate",
|
|
6
|
+
"traceability_candidate",
|
|
7
|
+
]);
|
|
8
|
+
const STRICT_ELIGIBLE_POSTURES = new Set([
|
|
9
|
+
"root_active",
|
|
10
|
+
"hybrid_root_plus_vendored",
|
|
11
|
+
]);
|
|
12
|
+
function hasEditedFilePath(editedFilePath) {
|
|
13
|
+
return typeof editedFilePath === "string" && editedFilePath.length > 0;
|
|
14
|
+
}
|
|
15
|
+
function deriveSeedIds(params) {
|
|
16
|
+
if (!hasEditedFilePath(params.editedFilePath)) {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
if (params.seedIds !== undefined) {
|
|
20
|
+
return params.seedIds.slice(0, 3);
|
|
21
|
+
}
|
|
22
|
+
const absoluteEditedPath = path.isAbsolute(params.editedFilePath)
|
|
23
|
+
? params.editedFilePath
|
|
24
|
+
: path.join(params.workspaceRoot, params.editedFilePath);
|
|
25
|
+
return getSourceLinkedRequirementIds(params.workspaceRoot, absoluteEditedPath).slice(0, 3);
|
|
26
|
+
}
|
|
27
|
+
// implements REQ-opencode-kibi-briefing-v2, REQ-opencode-smart-enforcement-v1
|
|
28
|
+
export function deriveBriefIntent(params) {
|
|
29
|
+
const fingerprint = `brief:${params.workspaceRoot}\0${params.branch}\0${params.editedFilePath ?? ""}\0${params.riskClass}`;
|
|
30
|
+
const sourceFiles = hasEditedFilePath(params.editedFilePath)
|
|
31
|
+
? [params.editedFilePath]
|
|
32
|
+
: [];
|
|
33
|
+
const seedIds = deriveSeedIds(params);
|
|
34
|
+
if (!hasEditedFilePath(params.editedFilePath)) {
|
|
35
|
+
return {
|
|
36
|
+
eligible: false,
|
|
37
|
+
reason: "Ineligible: edited file path is missing",
|
|
38
|
+
fingerprint,
|
|
39
|
+
sourceFiles,
|
|
40
|
+
seedIds,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (!ELIGIBLE_RISK_CLASSES.has(params.riskClass)) {
|
|
44
|
+
return {
|
|
45
|
+
eligible: false,
|
|
46
|
+
reason: `Ineligible: riskClass ${params.riskClass} is not auto-brief eligible`,
|
|
47
|
+
fingerprint,
|
|
48
|
+
sourceFiles,
|
|
49
|
+
seedIds,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (!STRICT_ELIGIBLE_POSTURES.has(params.posture)) {
|
|
53
|
+
return {
|
|
54
|
+
eligible: false,
|
|
55
|
+
reason: `Ineligible: posture ${params.posture} is not authoritative`,
|
|
56
|
+
fingerprint,
|
|
57
|
+
sourceFiles,
|
|
58
|
+
seedIds,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (params.maintenanceDegraded) {
|
|
62
|
+
return {
|
|
63
|
+
eligible: false,
|
|
64
|
+
reason: "Ineligible: maintenance is degraded",
|
|
65
|
+
fingerprint,
|
|
66
|
+
sourceFiles,
|
|
67
|
+
seedIds,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
eligible: true,
|
|
72
|
+
reason: "Eligible for auto-briefing",
|
|
73
|
+
fingerprint,
|
|
74
|
+
sourceFiles,
|
|
75
|
+
seedIds,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export function computeBriefIntent(// implements REQ-opencode-kibi-briefing-v2
|
|
79
|
+
inputs) {
|
|
80
|
+
return deriveBriefIntent({
|
|
81
|
+
riskClass: inputs.riskClass,
|
|
82
|
+
posture: inputs.posture,
|
|
83
|
+
maintenanceDegraded: inputs.maintenanceDegraded,
|
|
84
|
+
workspaceRoot: inputs.worktreeRoot,
|
|
85
|
+
branch: inputs.branch,
|
|
86
|
+
editedFilePath: inputs.editedFile,
|
|
87
|
+
...(inputs.seedIds !== undefined ? { seedIds: inputs.seedIds } : {}),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { BriefIntentResult } from "./brief-intent.js";
|
|
2
|
+
export type BriefingWorkspaceCtx = {
|
|
3
|
+
workspaceRoot: string;
|
|
4
|
+
branch: string;
|
|
5
|
+
directory?: string;
|
|
6
|
+
workspace?: string;
|
|
7
|
+
ttlMs?: number;
|
|
8
|
+
};
|
|
9
|
+
export type BriefingCitation = {
|
|
10
|
+
id: string;
|
|
11
|
+
type?: string;
|
|
12
|
+
title?: string;
|
|
13
|
+
source?: string;
|
|
14
|
+
textRef?: string;
|
|
15
|
+
};
|
|
16
|
+
export type BriefingRuntimeResult = {
|
|
17
|
+
state: "ready" | "tldr_fallback" | "no_briefing";
|
|
18
|
+
promptBlock: string;
|
|
19
|
+
tldr: string;
|
|
20
|
+
citations: BriefingCitation[];
|
|
21
|
+
showManualCue: boolean;
|
|
22
|
+
toastMessage: string;
|
|
23
|
+
};
|
|
24
|
+
export declare function fetchBriefingResult(client: unknown, workspaceCtx: BriefingWorkspaceCtx, intentResult: BriefIntentResult): Promise<BriefingRuntimeResult>;
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
// implements REQ-opencode-kibi-briefing-v2
|
|
2
|
+
const DEFAULT_TTL_MS = 300_000;
|
|
3
|
+
const WORKER_TITLE = "Kibi Auto Brief Worker";
|
|
4
|
+
const READY_TOAST = "Kibi brief ready — summary added to guidance.";
|
|
5
|
+
const TLDR_FALLBACK_TOAST = "Kibi brief summary added — use /brief-kibi for full details.";
|
|
6
|
+
const UNAVAILABLE_TOAST = "Kibi brief unavailable — keeping /brief-kibi manual path.";
|
|
7
|
+
const PROMPT_INSTRUCTION = "Call only kb_briefing_generate once with the provided sourceFiles and seedIds. If briefingState is ready, copy only cited fields. If briefingState is no_briefing, return empty promptBlock/citations and keep manual cue availability. Never invent claims.";
|
|
8
|
+
const PROMPT_FORMAT = {
|
|
9
|
+
type: "json_schema",
|
|
10
|
+
schema: {
|
|
11
|
+
type: "object",
|
|
12
|
+
properties: {
|
|
13
|
+
briefingState: { type: "string" },
|
|
14
|
+
tldr: { type: "string" },
|
|
15
|
+
promptBlock: { type: "string" },
|
|
16
|
+
citations: { type: "array", items: { type: "object" } },
|
|
17
|
+
activationState: { type: "string" },
|
|
18
|
+
confidence: { type: "string" },
|
|
19
|
+
freshness: { type: "string" },
|
|
20
|
+
},
|
|
21
|
+
required: ["briefingState"],
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
const workerSessionIds = new Map();
|
|
25
|
+
const workerSessionPromises = new Map();
|
|
26
|
+
const resultCache = new Map();
|
|
27
|
+
const inFlightResults = new Map();
|
|
28
|
+
function noBriefingResult() {
|
|
29
|
+
return {
|
|
30
|
+
state: "no_briefing",
|
|
31
|
+
promptBlock: "",
|
|
32
|
+
tldr: "",
|
|
33
|
+
citations: [],
|
|
34
|
+
showManualCue: true,
|
|
35
|
+
toastMessage: UNAVAILABLE_TOAST,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function workspaceSessionKey(workspaceCtx) {
|
|
39
|
+
return `${workspaceCtx.workspaceRoot}\0${workspaceCtx.branch}`;
|
|
40
|
+
}
|
|
41
|
+
function asRecord(value) {
|
|
42
|
+
return typeof value === "object" && value !== null
|
|
43
|
+
? value
|
|
44
|
+
: null;
|
|
45
|
+
}
|
|
46
|
+
function asString(value) {
|
|
47
|
+
return typeof value === "string" ? value : "";
|
|
48
|
+
}
|
|
49
|
+
function sanitizePromptBlock(value) {
|
|
50
|
+
return asString(value).trim();
|
|
51
|
+
}
|
|
52
|
+
function sanitizeCitations(value) {
|
|
53
|
+
if (!Array.isArray(value)) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
const citations = [];
|
|
57
|
+
for (const item of value) {
|
|
58
|
+
const record = asRecord(item);
|
|
59
|
+
if (!record) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const id = asString(record.id).trim();
|
|
63
|
+
if (!id) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const citation = { id };
|
|
67
|
+
const type = asString(record.type).trim();
|
|
68
|
+
const title = asString(record.title).trim();
|
|
69
|
+
const source = asString(record.source).trim();
|
|
70
|
+
const textRef = asString(record.textRef).trim();
|
|
71
|
+
if (type) {
|
|
72
|
+
citation.type = type;
|
|
73
|
+
}
|
|
74
|
+
if (title) {
|
|
75
|
+
citation.title = title;
|
|
76
|
+
}
|
|
77
|
+
if (source) {
|
|
78
|
+
citation.source = source;
|
|
79
|
+
}
|
|
80
|
+
if (textRef) {
|
|
81
|
+
citation.textRef = textRef;
|
|
82
|
+
}
|
|
83
|
+
citations.push(citation);
|
|
84
|
+
}
|
|
85
|
+
return citations;
|
|
86
|
+
}
|
|
87
|
+
function hasBriefingState(value) {
|
|
88
|
+
const record = asRecord(value);
|
|
89
|
+
return record !== null && "briefingState" in record;
|
|
90
|
+
}
|
|
91
|
+
function extractParts(response) {
|
|
92
|
+
const root = asRecord(response);
|
|
93
|
+
if (!root) {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
const data = asRecord(root.data);
|
|
97
|
+
const parts = data?.parts ?? root.parts;
|
|
98
|
+
return Array.isArray(parts) ? parts : [];
|
|
99
|
+
}
|
|
100
|
+
function parsePromptPayload(response) {
|
|
101
|
+
const parts = extractParts(response);
|
|
102
|
+
for (let index = parts.length - 1; index >= 0; index -= 1) {
|
|
103
|
+
const part = asRecord(parts[index]);
|
|
104
|
+
if (!part || part.type !== "text") {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const text = asString(part.text);
|
|
108
|
+
if (!text) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const parsed = JSON.parse(text);
|
|
113
|
+
if (hasBriefingState(parsed)) {
|
|
114
|
+
return parsed;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Ignore malformed text parts and continue searching from the end.
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
function normalizeResult(payload) {
|
|
124
|
+
if (!payload) {
|
|
125
|
+
return noBriefingResult();
|
|
126
|
+
}
|
|
127
|
+
const briefingState = asString(payload.briefingState).trim();
|
|
128
|
+
const tldr = asString(payload.tldr).trim();
|
|
129
|
+
const promptBlock = sanitizePromptBlock(payload.promptBlock);
|
|
130
|
+
if (briefingState === "ready" && promptBlock) {
|
|
131
|
+
return {
|
|
132
|
+
state: "ready",
|
|
133
|
+
promptBlock,
|
|
134
|
+
tldr,
|
|
135
|
+
citations: sanitizeCitations(payload.citations),
|
|
136
|
+
showManualCue: false,
|
|
137
|
+
toastMessage: READY_TOAST,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (briefingState === "ready" && tldr) {
|
|
141
|
+
return {
|
|
142
|
+
state: "tldr_fallback",
|
|
143
|
+
promptBlock: `- ${tldr}\n- Full details: run /brief-kibi.`,
|
|
144
|
+
tldr,
|
|
145
|
+
citations: [],
|
|
146
|
+
showManualCue: true,
|
|
147
|
+
toastMessage: TLDR_FALLBACK_TOAST,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return noBriefingResult();
|
|
151
|
+
}
|
|
152
|
+
function getSessionApi(client) {
|
|
153
|
+
const root = asRecord(client);
|
|
154
|
+
const session = asRecord(root?.session);
|
|
155
|
+
if (!session) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
const create = session.create;
|
|
159
|
+
const prompt = session.prompt;
|
|
160
|
+
if (typeof create !== "function" || typeof prompt !== "function") {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
create: create,
|
|
165
|
+
prompt: prompt,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function extractSessionId(response) {
|
|
169
|
+
const root = asRecord(response);
|
|
170
|
+
if (!root) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
const directId = asString(root.id).trim();
|
|
174
|
+
if (directId) {
|
|
175
|
+
return directId;
|
|
176
|
+
}
|
|
177
|
+
const data = asRecord(root.data);
|
|
178
|
+
const dataId = asString(data?.id).trim();
|
|
179
|
+
return dataId || null;
|
|
180
|
+
}
|
|
181
|
+
async function getWorkerSessionId(sessionApi, workspaceCtx) {
|
|
182
|
+
const key = workspaceSessionKey(workspaceCtx);
|
|
183
|
+
const existing = workerSessionIds.get(key);
|
|
184
|
+
if (existing) {
|
|
185
|
+
return existing;
|
|
186
|
+
}
|
|
187
|
+
const pending = workerSessionPromises.get(key);
|
|
188
|
+
if (pending) {
|
|
189
|
+
return pending;
|
|
190
|
+
}
|
|
191
|
+
const promise = (async () => {
|
|
192
|
+
const response = await sessionApi.create({
|
|
193
|
+
directory: workspaceCtx.workspaceRoot,
|
|
194
|
+
title: WORKER_TITLE,
|
|
195
|
+
});
|
|
196
|
+
const sessionId = extractSessionId(response);
|
|
197
|
+
if (!sessionId) {
|
|
198
|
+
throw new Error("Failed to resolve worker session ID");
|
|
199
|
+
}
|
|
200
|
+
workerSessionIds.set(key, sessionId);
|
|
201
|
+
return sessionId;
|
|
202
|
+
})().finally(() => {
|
|
203
|
+
workerSessionPromises.delete(key);
|
|
204
|
+
});
|
|
205
|
+
workerSessionPromises.set(key, promise);
|
|
206
|
+
return promise;
|
|
207
|
+
}
|
|
208
|
+
async function loadBriefingResult(sessionApi, workspaceCtx, intentResult) {
|
|
209
|
+
const sessionID = await getWorkerSessionId(sessionApi, workspaceCtx);
|
|
210
|
+
const response = await sessionApi.prompt({
|
|
211
|
+
sessionID,
|
|
212
|
+
tools: { kb_briefing_generate: true },
|
|
213
|
+
format: PROMPT_FORMAT,
|
|
214
|
+
parts: [
|
|
215
|
+
{
|
|
216
|
+
type: "text",
|
|
217
|
+
text: PROMPT_INSTRUCTION,
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
type: "text",
|
|
221
|
+
text: JSON.stringify({
|
|
222
|
+
sourceFiles: intentResult.sourceFiles,
|
|
223
|
+
seedIds: intentResult.seedIds,
|
|
224
|
+
}),
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
});
|
|
228
|
+
return normalizeResult(parsePromptPayload(response));
|
|
229
|
+
}
|
|
230
|
+
// implements REQ-opencode-kibi-briefing-v2
|
|
231
|
+
export async function fetchBriefingResult(client, workspaceCtx, intentResult) {
|
|
232
|
+
const ttlMs = workspaceCtx.ttlMs ?? DEFAULT_TTL_MS;
|
|
233
|
+
const cached = resultCache.get(intentResult.fingerprint);
|
|
234
|
+
const now = Date.now();
|
|
235
|
+
if (cached && now - cached.timestamp <= ttlMs) {
|
|
236
|
+
return cached.result;
|
|
237
|
+
}
|
|
238
|
+
if (cached) {
|
|
239
|
+
resultCache.delete(intentResult.fingerprint);
|
|
240
|
+
}
|
|
241
|
+
const pending = inFlightResults.get(intentResult.fingerprint);
|
|
242
|
+
if (pending) {
|
|
243
|
+
return pending;
|
|
244
|
+
}
|
|
245
|
+
const sessionApi = getSessionApi(client);
|
|
246
|
+
if (!sessionApi || !intentResult.eligible) {
|
|
247
|
+
const result = noBriefingResult();
|
|
248
|
+
resultCache.set(intentResult.fingerprint, {
|
|
249
|
+
result,
|
|
250
|
+
timestamp: now,
|
|
251
|
+
});
|
|
252
|
+
return result;
|
|
253
|
+
}
|
|
254
|
+
const promise = (async () => {
|
|
255
|
+
try {
|
|
256
|
+
const result = await loadBriefingResult(sessionApi, workspaceCtx, intentResult);
|
|
257
|
+
resultCache.set(intentResult.fingerprint, {
|
|
258
|
+
result,
|
|
259
|
+
timestamp: Date.now(),
|
|
260
|
+
});
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
const result = noBriefingResult();
|
|
265
|
+
resultCache.set(intentResult.fingerprint, {
|
|
266
|
+
result,
|
|
267
|
+
timestamp: Date.now(),
|
|
268
|
+
});
|
|
269
|
+
return result;
|
|
270
|
+
}
|
|
271
|
+
})().finally(() => {
|
|
272
|
+
inFlightResults.delete(intentResult.fingerprint);
|
|
273
|
+
});
|
|
274
|
+
inFlightResults.set(intentResult.fingerprint, promise);
|
|
275
|
+
return promise;
|
|
276
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
+
import { computeBriefIntent } from "./brief-intent.js";
|
|
3
|
+
import { fetchBriefingResult, } from "./briefing-runtime.js";
|
|
2
4
|
import { analyzeCodeFile, } from "./comment-analysis.js";
|
|
3
5
|
import * as fileFilter from "./file-filter.js";
|
|
4
6
|
import * as logger from "./logger.js";
|
|
@@ -9,6 +11,7 @@ import { classifyRisk } from "./risk-classifier.js";
|
|
|
9
11
|
import { getSessionTracker } from "./session-tracker.js";
|
|
10
12
|
import { notifyStartup } from "./startup-notifier.js";
|
|
11
13
|
import { runPluginStartup } from "./plugin-startup.js";
|
|
14
|
+
import { sendToast } from "./toast.js";
|
|
12
15
|
import * as fs from "node:fs";
|
|
13
16
|
function deriveFileBucket(kind) {
|
|
14
17
|
return kind;
|
|
@@ -65,7 +68,11 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
65
68
|
let hasRecentKbEdit = false;
|
|
66
69
|
let recentCommentSuggestion = null;
|
|
67
70
|
const seenFingerprints = new Set(); // For deduplication
|
|
71
|
+
const autoBriefResults = new Map();
|
|
72
|
+
const toastedFingerprints = new Set();
|
|
68
73
|
let lastRiskClass = null;
|
|
74
|
+
let lastEditedFilePath = null;
|
|
75
|
+
let lastBriefFingerprint = null;
|
|
69
76
|
let degradedWarnedOnce = false;
|
|
70
77
|
hooks.event = async ({ event }) => {
|
|
71
78
|
if (event.type !== "file.edited")
|
|
@@ -105,7 +112,10 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
105
112
|
const effectiveRiskClass = riskClass === "safe_docs_only" && precomputedSuggestion
|
|
106
113
|
? "traceability_candidate"
|
|
107
114
|
: riskClass;
|
|
115
|
+
const isAutoBriefRisk = effectiveRiskClass === "behavior_candidate" ||
|
|
116
|
+
effectiveRiskClass === "traceability_candidate";
|
|
108
117
|
lastRiskClass = effectiveRiskClass;
|
|
118
|
+
lastEditedFilePath = filePath;
|
|
109
119
|
logger.info("smart-enforcement.risk", {
|
|
110
120
|
event: "smart_enforcement_risk",
|
|
111
121
|
file: filePath,
|
|
@@ -143,6 +153,7 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
143
153
|
"required-fields",
|
|
144
154
|
"no-dangling-refs",
|
|
145
155
|
...(pathAnalysis.kind === "fact" ? ["strict-fact-shape"] : []),
|
|
156
|
+
...(pathAnalysis.kind === "requirement" ? ["strict-req-fact-pairing"] : []),
|
|
146
157
|
]
|
|
147
158
|
: null;
|
|
148
159
|
const checkRules = traceabilityRules ?? kbStructuralRules;
|
|
@@ -215,7 +226,9 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
215
226
|
posture: posture.state,
|
|
216
227
|
posture_state: posture.state,
|
|
217
228
|
});
|
|
218
|
-
|
|
229
|
+
if (!isAutoBriefRisk) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
219
232
|
}
|
|
220
233
|
logger.info("smart-enforcement.cache", {
|
|
221
234
|
event: "smart_enforcement_cache",
|
|
@@ -260,11 +273,12 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
260
273
|
"required-fields",
|
|
261
274
|
"no-dangling-refs",
|
|
262
275
|
"must-priority-coverage",
|
|
276
|
+
"strict-req-fact-pairing",
|
|
263
277
|
];
|
|
264
278
|
logger.info(`kibi-opencode: must-priority requirement detected, scheduling elevated checks for ${filePath}`);
|
|
265
279
|
}
|
|
266
280
|
else {
|
|
267
|
-
checkRules = ["required-fields", "no-dangling-refs"];
|
|
281
|
+
checkRules = ["required-fields", "no-dangling-refs", "strict-req-fact-pairing"];
|
|
268
282
|
}
|
|
269
283
|
}
|
|
270
284
|
logger.info("smart-enforcement.targeted-checks", {
|
|
@@ -310,8 +324,7 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
310
324
|
}
|
|
311
325
|
return;
|
|
312
326
|
}
|
|
313
|
-
if (
|
|
314
|
-
effectiveRiskClass === "traceability_candidate") {
|
|
327
|
+
if (isAutoBriefRisk) {
|
|
315
328
|
if (pathAnalysis.kind === "code" &&
|
|
316
329
|
cfg.guidance.commentDetection.enabled) {
|
|
317
330
|
const suggestion = precomputedSuggestion;
|
|
@@ -336,6 +349,38 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
336
349
|
else {
|
|
337
350
|
recentCommentSuggestion = null;
|
|
338
351
|
}
|
|
352
|
+
const intentResult = computeBriefIntent({
|
|
353
|
+
riskClass: effectiveRiskClass,
|
|
354
|
+
posture: posture.state,
|
|
355
|
+
maintenanceDegraded: getMaintenanceDegraded(),
|
|
356
|
+
editedFile: filePath,
|
|
357
|
+
worktreeRoot: input.worktree,
|
|
358
|
+
branch: currentBranch,
|
|
359
|
+
});
|
|
360
|
+
lastBriefFingerprint = intentResult.fingerprint;
|
|
361
|
+
if (intentResult.eligible &&
|
|
362
|
+
input.client &&
|
|
363
|
+
!getMaintenanceDegraded() &&
|
|
364
|
+
(posture.state === "root_active" ||
|
|
365
|
+
posture.state === "hybrid_root_plus_vendored")) {
|
|
366
|
+
const client = input.client;
|
|
367
|
+
const fingerprint = intentResult.fingerprint;
|
|
368
|
+
const workspaceCtx = {
|
|
369
|
+
workspaceRoot: input.worktree,
|
|
370
|
+
branch: currentBranch,
|
|
371
|
+
directory: input.directory,
|
|
372
|
+
...(input.workspace !== undefined ? { workspace: input.workspace } : {}),
|
|
373
|
+
};
|
|
374
|
+
void fetchBriefingResult(client, workspaceCtx, intentResult).then((result) => {
|
|
375
|
+
autoBriefResults.set(fingerprint, result);
|
|
376
|
+
if (!toastedFingerprints.has(fingerprint)) {
|
|
377
|
+
toastedFingerprints.add(fingerprint);
|
|
378
|
+
void sendToast(client, { message: result.toastMessage }).catch(() => {
|
|
379
|
+
// toast delivery failure is non-fatal
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
}
|
|
339
384
|
}
|
|
340
385
|
return;
|
|
341
386
|
};
|
|
@@ -351,6 +396,9 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
351
396
|
const showDegradedAdvisory = maintenanceDegraded &&
|
|
352
397
|
cfg.guidance.smartEnforcement.degradedMode === "warn-once" &&
|
|
353
398
|
!degradedWarnedOnce;
|
|
399
|
+
const autoBriefResult = lastBriefFingerprint != null
|
|
400
|
+
? autoBriefResults.get(lastBriefFingerprint)
|
|
401
|
+
: undefined;
|
|
354
402
|
// Build only the guidance block and append it; existing entries are preserved
|
|
355
403
|
const guidance = buildPrompt({
|
|
356
404
|
recentEdits,
|
|
@@ -365,6 +413,7 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
365
413
|
maintenanceDegraded,
|
|
366
414
|
degradedMode: cfg.guidance.smartEnforcement.degradedMode,
|
|
367
415
|
showDegradedAdvisory,
|
|
416
|
+
...(autoBriefResult !== undefined ? { autoBriefResult } : {}),
|
|
368
417
|
...(lastRiskClass != null ? { riskClass: lastRiskClass } : {}),
|
|
369
418
|
});
|
|
370
419
|
logger.info("smart-enforcement.guidance", {
|
|
@@ -94,7 +94,7 @@ export function classifyKnowledge(text) {
|
|
|
94
94
|
bestMatch = {
|
|
95
95
|
type: "fact",
|
|
96
96
|
confidence: factScore >= 3 ? "high" : "medium",
|
|
97
|
-
reasoning: 'Contains domain
|
|
97
|
+
reasoning: 'Contains strict domain fact cues (invariants, properties, limits, cardinalities) for the strict fact lane',
|
|
98
98
|
};
|
|
99
99
|
}
|
|
100
100
|
if (reqScore > maxMatches) {
|
package/dist/plugin-startup.js
CHANGED
|
@@ -9,6 +9,15 @@ import { createSyncScheduler } from "./scheduler.js";
|
|
|
9
9
|
import { getSessionTracker } from "./session-tracker.js";
|
|
10
10
|
import { computeEffectiveMode, } from "./smart-enforcement.js";
|
|
11
11
|
import { checkWorkspaceHealth } from "./workspace-health.js";
|
|
12
|
+
function isSmartEnforcementSyncReason(reason) {
|
|
13
|
+
if (!reason) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
const normalizedReason = reason.endsWith(".trailing")
|
|
17
|
+
? reason.slice(0, -".trailing".length)
|
|
18
|
+
: reason;
|
|
19
|
+
return normalizedReason.startsWith("smart-enforcement.");
|
|
20
|
+
}
|
|
12
21
|
const workspaceCacheState = new Map();
|
|
13
22
|
function resolveCurrentBranch(cwd) {
|
|
14
23
|
try {
|
|
@@ -148,7 +157,8 @@ export async function runPluginStartup(input) {
|
|
|
148
157
|
worktree: input.worktree,
|
|
149
158
|
config: cfg,
|
|
150
159
|
onRunComplete: (meta) => {
|
|
151
|
-
if (meta.exitCode !== 0
|
|
160
|
+
if (meta.exitCode !== 0 &&
|
|
161
|
+
!isSmartEnforcementSyncReason(meta.reason))
|
|
152
162
|
latchRuntimeDegraded("scheduler_sync_failed");
|
|
153
163
|
if (meta.checkExitCode !== undefined &&
|
|
154
164
|
meta.checkExitCode !== 0) {
|
package/dist/prompt.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CommentAnalysisResult } from "./comment-analysis.js";
|
|
2
|
+
import type { BriefingRuntimeResult } from "./briefing-runtime.js";
|
|
2
3
|
import type { KibiConfig } from "./config.js";
|
|
3
4
|
import type { GuidanceCache } from "./guidance-cache.js";
|
|
4
5
|
import type { PathKind } from "./path-kind.js";
|
|
@@ -32,6 +33,8 @@ export interface PromptContext {
|
|
|
32
33
|
degradedMode?: "warn-once" | "structured-only";
|
|
33
34
|
/** Whether to show the degraded advisory block this invocation */
|
|
34
35
|
showDegradedAdvisory?: boolean;
|
|
36
|
+
/** Stored auto-brief runtime result for the current fingerprint */
|
|
37
|
+
autoBriefResult?: BriefingRuntimeResult;
|
|
35
38
|
}
|
|
36
39
|
export declare function postureGuidance(posture: RepoPosture): string | null;
|
|
37
40
|
/**
|
package/dist/prompt.js
CHANGED
|
@@ -4,21 +4,27 @@ import { getSourceLinkedRequirementIds } from "./source-linked-guidance.js";
|
|
|
4
4
|
const SENTINEL = "<!-- kibi-opencode -->";
|
|
5
5
|
// ── Token budget enforcement ───────────────────────────────────────────
|
|
6
6
|
const MAX_BULLETS = 5;
|
|
7
|
+
const MAX_AUTO_BRIEF_BULLETS_WITH_REMINDER = 4;
|
|
7
8
|
const MAX_WORDS = 117; // Reserve 3 words for sentinel so total injected prompt stays ≤ 120
|
|
9
|
+
const AUTO_BRIEF_HEADER = "🧠 **Kibi briefing available**";
|
|
10
|
+
const AUTHORITATIVE_POSTURES = [
|
|
11
|
+
"root_active",
|
|
12
|
+
"hybrid_root_plus_vendored",
|
|
13
|
+
];
|
|
8
14
|
function countWords(text) {
|
|
9
15
|
return text.split(/\s+/).filter(Boolean).length;
|
|
10
16
|
}
|
|
11
17
|
function countBullets(lines) {
|
|
12
18
|
return lines.filter((l) => l.startsWith("-")).length;
|
|
13
19
|
}
|
|
14
|
-
function enforceBudget(block) {
|
|
20
|
+
function enforceBudget(block, maxBullets = MAX_BULLETS) {
|
|
15
21
|
const lines = block.split("\n");
|
|
16
|
-
if (countBullets(lines) >
|
|
17
|
-
// Trim to budget: keep header + first
|
|
22
|
+
if (countBullets(lines) > maxBullets || countWords(block) > MAX_WORDS) {
|
|
23
|
+
// Trim to budget: keep header + first maxBullets bullet lines
|
|
18
24
|
const header = [];
|
|
19
25
|
const bullets = [];
|
|
20
26
|
for (const line of lines) {
|
|
21
|
-
if (line.startsWith("-") && bullets.length <
|
|
27
|
+
if (line.startsWith("-") && bullets.length < maxBullets) {
|
|
22
28
|
bullets.push(line);
|
|
23
29
|
}
|
|
24
30
|
else if (!line.startsWith("-")) {
|
|
@@ -35,6 +41,43 @@ function enforceBudget(block) {
|
|
|
35
41
|
}
|
|
36
42
|
return block;
|
|
37
43
|
}
|
|
44
|
+
function insertBulletAfterHeader(block, bullet) {
|
|
45
|
+
const headerEnd = block.indexOf("\n");
|
|
46
|
+
if (headerEnd === -1)
|
|
47
|
+
return `${block}\n${bullet}`;
|
|
48
|
+
return `${block.slice(0, headerEnd + 1)}${bullet}\n${block.slice(headerEnd + 1)}`;
|
|
49
|
+
}
|
|
50
|
+
// implements REQ-opencode-kibi-briefing-v2
|
|
51
|
+
function buildAutoBriefingGuidance(autoBriefResult, completionReminder) {
|
|
52
|
+
if (!autoBriefResult)
|
|
53
|
+
return null;
|
|
54
|
+
if (autoBriefResult.state === "ready") {
|
|
55
|
+
const promptBlock = autoBriefResult.promptBlock.trim();
|
|
56
|
+
if (!promptBlock)
|
|
57
|
+
return null;
|
|
58
|
+
const maxBullets = completionReminder
|
|
59
|
+
? MAX_AUTO_BRIEF_BULLETS_WITH_REMINDER
|
|
60
|
+
: MAX_BULLETS;
|
|
61
|
+
const briefingLines = promptBlock
|
|
62
|
+
.split("\n")
|
|
63
|
+
.map((line) => line.trim())
|
|
64
|
+
.filter((line) => line.startsWith("-"))
|
|
65
|
+
.slice(0, maxBullets);
|
|
66
|
+
if (briefingLines.length === 0)
|
|
67
|
+
return null;
|
|
68
|
+
return `${AUTO_BRIEF_HEADER}\n${briefingLines.join("\n")}`;
|
|
69
|
+
}
|
|
70
|
+
if (autoBriefResult.state === "tldr_fallback") {
|
|
71
|
+
const promptBlock = autoBriefResult.promptBlock.trim();
|
|
72
|
+
if (!promptBlock)
|
|
73
|
+
return null;
|
|
74
|
+
return `${AUTO_BRIEF_HEADER}\n${promptBlock}`;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
function isAuthoritativePosture(posture) {
|
|
79
|
+
return AUTHORITATIVE_POSTURES.includes(posture);
|
|
80
|
+
}
|
|
38
81
|
// ── File bucket derivation ─────────────────────────────────────────────
|
|
39
82
|
function deriveFileBucket(pathKind) {
|
|
40
83
|
return pathKind;
|
|
@@ -56,12 +99,15 @@ Requirement edits need policy alignment. Run kb_check with required-fields and n
|
|
|
56
99
|
- Add verification: create or update linked SCEN and TEST entities`,
|
|
57
100
|
behavior_candidate: `📝 **Code changes detected**
|
|
58
101
|
|
|
59
|
-
Production code: use \`implements\` (symbol→req) for requirement ownership. Test code: use \`executable_for\` (symbol→test).
|
|
102
|
+
Production code: use \`implements\` (symbol→req) for requirement ownership. Test code: use \`executable_for\` (symbol→test).
|
|
103
|
+
- \`covered_by\` is coverage evidence only
|
|
104
|
+
- Prefer scenario-first: req→scenario→test when scenarios exist`,
|
|
60
105
|
traceability_candidate: `📝 **Code changes detected**
|
|
61
106
|
|
|
62
|
-
Production code: use \`implements\` (symbol→req) for requirement ownership. Test code: use \`executable_for\` (symbol→test).
|
|
63
|
-
-
|
|
64
|
-
-
|
|
107
|
+
Production code: use \`implements\` (symbol→req) for requirement ownership. Test code: use \`executable_for\` (symbol→test).
|
|
108
|
+
- \`covered_by\` is coverage evidence only
|
|
109
|
+
- Prefer scenario-first: req→scenario→test when scenarios exist
|
|
110
|
+
- Route durable knowledge comments to KB entities, not inline comments`,
|
|
65
111
|
manual_kb_edit: `⚠️ **WARNING: Direct .kb/ edits bypass validation**
|
|
66
112
|
|
|
67
113
|
The Kibi knowledge base is managed through public MCP tools. Direct manual edits to .kb/** can cause inconsistencies.
|
|
@@ -100,6 +146,9 @@ Root .kb/config.json exists but some configured KB targets are missing. Guidance
|
|
|
100
146
|
function buildContextualGuidance(context) {
|
|
101
147
|
const posture = context.posture ?? "root_active";
|
|
102
148
|
const riskClass = context.riskClass;
|
|
149
|
+
const readyAutoBriefingAvailable = context.autoBriefResult?.showManualCue === false;
|
|
150
|
+
const suppressSourceLinkedBrief = context.autoBriefResult?.state === "ready" ||
|
|
151
|
+
context.autoBriefResult?.state === "tldr_fallback";
|
|
103
152
|
const showDegraded = context.showDegradedAdvisory === true &&
|
|
104
153
|
context.maintenanceDegraded === true &&
|
|
105
154
|
context.degradedMode === "warn-once";
|
|
@@ -155,16 +204,25 @@ Do not run \`kibi\` CLI commands directly; use public MCP tools (kb_autopilot_ge
|
|
|
155
204
|
if (riskClass &&
|
|
156
205
|
riskClass !== "safe_docs_only" &&
|
|
157
206
|
riskClass !== "safe_test_only") {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
207
|
+
const autoBriefBlock = riskClass === "behavior_candidate" ||
|
|
208
|
+
riskClass === "traceability_candidate"
|
|
209
|
+
? buildAutoBriefingGuidance(context.autoBriefResult, context.completionReminder === true)
|
|
210
|
+
: null;
|
|
211
|
+
if (autoBriefBlock) {
|
|
212
|
+
selectedBlock = autoBriefBlock;
|
|
163
213
|
}
|
|
164
214
|
else {
|
|
165
|
-
|
|
166
|
-
if (
|
|
167
|
-
|
|
215
|
+
// For behavior/traceability with comment suggestions, use suggestion guidance
|
|
216
|
+
if ((riskClass === "behavior_candidate" ||
|
|
217
|
+
riskClass === "traceability_candidate") &&
|
|
218
|
+
context.recentCommentSuggestion) {
|
|
219
|
+
selectedBlock = buildCommentSuggestionGuidance(context.recentCommentSuggestion);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
const block = GUIDANCE_BY_RISK[riskClass];
|
|
223
|
+
if (block)
|
|
224
|
+
selectedBlock = block;
|
|
225
|
+
}
|
|
168
226
|
}
|
|
169
227
|
}
|
|
170
228
|
// Priority 6: Legacy path-kind fallback (when no risk class)
|
|
@@ -196,7 +254,7 @@ Before implementing or explaining code:
|
|
|
196
254
|
4. **Add traceability** - Production code: \`implements\` (symbol→req) for ownership. Test code: \`executable_for\`. \`covered_by\` is coverage evidence only for production symbols.
|
|
197
255
|
|
|
198
256
|
If you're adding long explanatory comments, consider routing that knowledge to:
|
|
199
|
-
- \`FACT\` for domain invariants, properties, limits, cardinalities
|
|
257
|
+
- \`FACT\` for strict domain facts (invariants, properties, limits, cardinalities); bug/workaround notes use \`fact_kind: observation\` or \`meta\`
|
|
200
258
|
- \`ADR\` for technical decisions, tradeoffs, rationale
|
|
201
259
|
- \`REQ\` for system behavior requirements`;
|
|
202
260
|
}
|
|
@@ -209,6 +267,14 @@ If you're adding long explanatory comments, consider routing that knowledge to:
|
|
|
209
267
|
}
|
|
210
268
|
}
|
|
211
269
|
} // closing brace for Priority 2-4 else block starting at 187
|
|
270
|
+
if (selectedBlock &&
|
|
271
|
+
(riskClass === "behavior_candidate" ||
|
|
272
|
+
riskClass === "traceability_candidate") &&
|
|
273
|
+
isAuthoritativePosture(posture) &&
|
|
274
|
+
!context.maintenanceDegraded &&
|
|
275
|
+
!readyAutoBriefingAvailable) {
|
|
276
|
+
selectedBlock = insertBulletAfterHeader(selectedBlock, "- Authoritative risky edit: run `/brief-kibi` before acting.");
|
|
277
|
+
}
|
|
212
278
|
// Source-linked micro-brief: insert after header line for code risk classes
|
|
213
279
|
// Inserting after the header (not prepending before it) preserves the header
|
|
214
280
|
// under enforceBudget's trimming logic, which only collects non-bullet lines
|
|
@@ -216,7 +282,8 @@ If you're adding long explanatory comments, consider routing that knowledge to:
|
|
|
216
282
|
if (selectedBlock &&
|
|
217
283
|
(riskClass === "behavior_candidate" ||
|
|
218
284
|
riskClass === "traceability_candidate") &&
|
|
219
|
-
context.workspaceRoot
|
|
285
|
+
context.workspaceRoot &&
|
|
286
|
+
!suppressSourceLinkedBrief) {
|
|
220
287
|
try {
|
|
221
288
|
const lastEdit = context.recentEdits[context.recentEdits.length - 1];
|
|
222
289
|
if (lastEdit?.path) {
|
|
@@ -226,10 +293,7 @@ If you're adding long explanatory comments, consider routing that knowledge to:
|
|
|
226
293
|
: path.join(context.workspaceRoot, editedPath);
|
|
227
294
|
const linkedIds = getSourceLinkedRequirementIds(context.workspaceRoot, absEdited);
|
|
228
295
|
if (linkedIds.length >= 1 && linkedIds.length <= 3) {
|
|
229
|
-
|
|
230
|
-
if (headerEnd !== -1) {
|
|
231
|
-
selectedBlock = `${selectedBlock.slice(0, headerEnd + 1)}- Existing Kibi links: ${linkedIds.join(", ")}\n${selectedBlock.slice(headerEnd + 1)}`;
|
|
232
|
-
}
|
|
296
|
+
selectedBlock = insertBulletAfterHeader(selectedBlock, `- Existing Kibi links: ${linkedIds.join(", ")}`);
|
|
233
297
|
}
|
|
234
298
|
}
|
|
235
299
|
}
|
|
@@ -266,24 +330,40 @@ The Kibi workspace is in a maintenance-degraded state. Guidance remains advisory
|
|
|
266
330
|
};
|
|
267
331
|
context.cache.recordSatisfied(key, "guidance");
|
|
268
332
|
}
|
|
269
|
-
// Append completion reminder for risky classes when enabled
|
|
270
333
|
const REMINDER_RISK_CLASSES = [
|
|
271
334
|
"behavior_candidate",
|
|
272
335
|
"traceability_candidate",
|
|
273
336
|
"req_policy_candidate",
|
|
274
337
|
];
|
|
275
|
-
|
|
338
|
+
const reminderWillBeAppended = !!selectedBlock &&
|
|
339
|
+
context.completionReminder === true &&
|
|
340
|
+
!context.maintenanceDegraded &&
|
|
341
|
+
riskClass != null &&
|
|
342
|
+
REMINDER_RISK_CLASSES.includes(riskClass) &&
|
|
343
|
+
posture !== "root_uninitialized" &&
|
|
344
|
+
posture !== "root_partial";
|
|
345
|
+
const effectiveMaxBullets = reminderWillBeAppended
|
|
346
|
+
? MAX_BULLETS - 1
|
|
347
|
+
: MAX_BULLETS;
|
|
348
|
+
// Apply budget enforcement before appending the completion reminder so the
|
|
349
|
+
// reminder bullet is never silently trimmed when bullet count exceeds MAX_BULLETS.
|
|
350
|
+
const budgeted = selectedBlock
|
|
351
|
+
? enforceBudget(selectedBlock, effectiveMaxBullets)
|
|
352
|
+
: null;
|
|
353
|
+
// Append completion reminder for risky classes when enabled
|
|
354
|
+
let finalBlock = budgeted;
|
|
355
|
+
if (finalBlock &&
|
|
276
356
|
context.completionReminder === true &&
|
|
277
357
|
!context.maintenanceDegraded &&
|
|
278
358
|
riskClass &&
|
|
279
359
|
REMINDER_RISK_CLASSES.includes(riskClass) &&
|
|
280
360
|
posture !== "root_uninitialized" &&
|
|
281
361
|
posture !== "root_partial") {
|
|
282
|
-
|
|
362
|
+
finalBlock = `${finalBlock}\n- Run \`kb_check\` before completing this task.`;
|
|
283
363
|
}
|
|
284
364
|
// Return: sentinel + one targeted block (or just sentinel if no block)
|
|
285
|
-
return
|
|
286
|
-
? `${SENTINEL}\n\n${
|
|
365
|
+
return finalBlock
|
|
366
|
+
? `${SENTINEL}\n\n${finalBlock}`
|
|
287
367
|
: SENTINEL;
|
|
288
368
|
}
|
|
289
369
|
// ── Comment suggestion guidance (legacy compat) ────────────────────────
|
|
@@ -292,14 +372,14 @@ function buildCommentSuggestionGuidance(suggestion) {
|
|
|
292
372
|
case "fact":
|
|
293
373
|
return `🎯 **Durable knowledge detected: FACT**
|
|
294
374
|
|
|
295
|
-
Your recent code edit contains a comment that looks like a **domain
|
|
375
|
+
Your recent code edit contains a comment that looks like a **strict domain fact** (invariants, properties, limits, defaults, or cardinality constraints).
|
|
296
376
|
|
|
297
|
-
**Action**:
|
|
298
|
-
- Create \`documentation/facts/FACT-xxx.md\` with the invariant
|
|
377
|
+
**Action**: Route to a FACT entity in the strict fact lane:
|
|
378
|
+
- Create \`documentation/facts/FACT-xxx.md\` with the invariant (use \`constrains\` + \`requires_property\` for contradiction-safe reasoning)
|
|
379
|
+
- Bug/workaround notes: use \`fact_kind: observation\` or \`meta\` instead — these are non-blocking and excluded from contradiction inference
|
|
299
380
|
- Link it to relevant requirements
|
|
300
|
-
- Reference the FACT in code with a comment
|
|
301
381
|
|
|
302
|
-
This keeps domain truths centralized and
|
|
382
|
+
This keeps domain truths centralized, searchable, and contradiction-safe.`;
|
|
303
383
|
case "adr":
|
|
304
384
|
return `🎯 **Durable knowledge detected: ADR**
|
|
305
385
|
|
package/dist/scheduler.js
CHANGED
|
@@ -151,6 +151,10 @@ class WorktreeSyncScheduler {
|
|
|
151
151
|
}
|
|
152
152
|
emitCompletion(trigger, startedAt, exitCode, checkExitCode, checkRules) {
|
|
153
153
|
const durationMs = Math.max(0, this.now() - startedAt);
|
|
154
|
+
const normalizedReason = trigger.reason.endsWith(".trailing")
|
|
155
|
+
? trigger.reason.slice(0, -".trailing".length)
|
|
156
|
+
: trigger.reason;
|
|
157
|
+
const isSmartEnforcementSync = normalizedReason.startsWith("smart-enforcement.");
|
|
154
158
|
const meta = {
|
|
155
159
|
reason: trigger.reason,
|
|
156
160
|
worktree: this.worktree,
|
|
@@ -164,6 +168,9 @@ class WorktreeSyncScheduler {
|
|
|
164
168
|
if (exitCode === 0) {
|
|
165
169
|
logger.info(`sync.succeeded ${JSON.stringify(meta)}`);
|
|
166
170
|
}
|
|
171
|
+
else if (isSmartEnforcementSync) {
|
|
172
|
+
logger.errorStructuredOnly(`sync.failed ${JSON.stringify(meta)}`);
|
|
173
|
+
}
|
|
167
174
|
else {
|
|
168
175
|
logger.error(`sync.failed ${JSON.stringify(meta)}`);
|
|
169
176
|
}
|
|
@@ -1,21 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
message: string;
|
|
5
|
-
duration?: number;
|
|
6
|
-
};
|
|
7
|
-
export type StartupNotifierClient = {
|
|
8
|
-
tui?: {
|
|
9
|
-
showToast?: (payload: {
|
|
10
|
-
body: {
|
|
11
|
-
title?: string;
|
|
12
|
-
message: string;
|
|
13
|
-
variant?: "info" | "success" | "warning" | "error";
|
|
14
|
-
duration?: number;
|
|
15
|
-
};
|
|
16
|
-
}) => void | Promise<void>;
|
|
17
|
-
toast?: (payload: ToastPayload) => void | Promise<void>;
|
|
18
|
-
};
|
|
1
|
+
import { type ToastCapableClient } from "./toast.js";
|
|
2
|
+
export type { ToastPayload } from "./toast.js";
|
|
3
|
+
export type StartupNotifierClient = ToastCapableClient & {
|
|
19
4
|
app: {
|
|
20
5
|
log: (payload: Record<string, unknown>) => Promise<void>;
|
|
21
6
|
};
|
package/dist/startup-notifier.js
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
return typeof client.tui?.showToast === "function";
|
|
3
|
-
}
|
|
4
|
-
function hasLegacyToast(client) {
|
|
5
|
-
return typeof client.tui?.toast === "function";
|
|
6
|
-
}
|
|
1
|
+
import { hasLegacyToast, hasShowToast, sendToast, } from "./toast.js";
|
|
7
2
|
// implements REQ-opencode-kibi-plugin-v1
|
|
8
3
|
export function notifyStartup(client, cfg) {
|
|
9
4
|
const message = "kibi-opencode started";
|
|
@@ -15,7 +10,7 @@ export function notifyStartup(client, cfg) {
|
|
|
15
10
|
};
|
|
16
11
|
if (!cfg.suppressToast) {
|
|
17
12
|
if (hasShowToast(client)) {
|
|
18
|
-
void Promise.resolve(client
|
|
13
|
+
void Promise.resolve(sendToast(client, toastPayload))
|
|
19
14
|
.then((result) => void Promise.resolve(client.app.log({
|
|
20
15
|
body: {
|
|
21
16
|
service: "kibi-opencode",
|
|
@@ -43,7 +38,7 @@ export function notifyStartup(client, cfg) {
|
|
|
43
38
|
});
|
|
44
39
|
}
|
|
45
40
|
else if (hasLegacyToast(client)) {
|
|
46
|
-
void Promise.resolve(client
|
|
41
|
+
void Promise.resolve(sendToast(client, toastPayload)).catch((err) => {
|
|
47
42
|
console.error("[kibi-opencode] startup toast failed:", err);
|
|
48
43
|
});
|
|
49
44
|
}
|
package/dist/toast.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type ToastPayload = {
|
|
2
|
+
variant?: "info" | "success" | "warning" | "error";
|
|
3
|
+
title?: string;
|
|
4
|
+
message: string;
|
|
5
|
+
duration?: number;
|
|
6
|
+
};
|
|
7
|
+
type ShowToastPayload = {
|
|
8
|
+
body: ToastPayload;
|
|
9
|
+
};
|
|
10
|
+
type ShowToast = (payload: ShowToastPayload) => void | Promise<void>;
|
|
11
|
+
type LegacyToast = (payload: ToastPayload) => void | Promise<void>;
|
|
12
|
+
type ToastUi = {
|
|
13
|
+
showToast?: ShowToast;
|
|
14
|
+
toast?: LegacyToast;
|
|
15
|
+
};
|
|
16
|
+
export type ToastCapableClient = {
|
|
17
|
+
tui?: ToastUi;
|
|
18
|
+
};
|
|
19
|
+
type ClientWithShowToast = ToastCapableClient & {
|
|
20
|
+
tui: ToastUi & {
|
|
21
|
+
showToast: ShowToast;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
type ClientWithLegacyToast = ToastCapableClient & {
|
|
25
|
+
tui: ToastUi & {
|
|
26
|
+
toast: LegacyToast;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
export declare function hasShowToast(client: ToastCapableClient): client is ClientWithShowToast;
|
|
30
|
+
export declare function hasLegacyToast(client: ToastCapableClient): client is ClientWithLegacyToast;
|
|
31
|
+
export declare function sendToast(client: ToastCapableClient, payload: ToastPayload): Promise<void>;
|
|
32
|
+
export {};
|
package/dist/toast.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// implements REQ-opencode-kibi-plugin-v1
|
|
2
|
+
export function hasShowToast(client) {
|
|
3
|
+
return typeof client.tui?.showToast === "function";
|
|
4
|
+
}
|
|
5
|
+
// implements REQ-opencode-kibi-plugin-v1
|
|
6
|
+
export function hasLegacyToast(client) {
|
|
7
|
+
return typeof client.tui?.toast === "function";
|
|
8
|
+
}
|
|
9
|
+
// implements REQ-opencode-kibi-plugin-v1
|
|
10
|
+
export function sendToast(client, payload) {
|
|
11
|
+
if (hasShowToast(client)) {
|
|
12
|
+
return Promise.resolve(client.tui.showToast({ body: payload }));
|
|
13
|
+
}
|
|
14
|
+
if (hasLegacyToast(client)) {
|
|
15
|
+
return Promise.resolve(client.tui.toast(payload));
|
|
16
|
+
}
|
|
17
|
+
return Promise.resolve();
|
|
18
|
+
}
|