kibi-opencode 0.6.1 → 0.7.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 +4 -0
- package/dist/comment-analysis.js +31 -38
- package/dist/config.d.ts +2 -0
- package/dist/config.js +12 -3
- package/dist/index.d.ts +8 -0
- package/dist/index.js +16 -165
- package/dist/plugin-startup.d.ts +28 -0
- package/dist/plugin-startup.js +177 -0
- package/dist/prompt.d.ts +1 -0
- package/dist/prompt.js +9 -11
- package/dist/repo-posture.js +2 -0
- package/dist/requirement-doc.js +6 -2
- package/dist/scheduler.js +14 -6
- package/dist/smart-enforcement.js +2 -0
- package/dist/source-linked-guidance.js +21 -6
- package/dist/startup-notifier.d.ts +28 -0
- package/dist/startup-notifier.js +62 -0
- package/package.json +2 -4
package/README.md
CHANGED
|
@@ -152,6 +152,10 @@ Config files (project overrides global):
|
|
|
152
152
|
| `sync.debounceMs` | number | `2000` | Debounce window in milliseconds |
|
|
153
153
|
| `sync.ignore` | string[] | `[]` | Additional paths to ignore |
|
|
154
154
|
| `sync.relevant` | string[] | `[]` | Additional relevant paths |
|
|
155
|
+
| `ux.toastStartup` | boolean | `true` | Show the startup confirmation toast independently from sync-status toasts |
|
|
156
|
+
| `ux.toastFailures` | boolean | `true` | Show failure toasts for sync/check issues |
|
|
157
|
+
| `ux.toastSuccesses` | boolean | `false` | Show success toasts for sync/check completion |
|
|
158
|
+
| `ux.toastCooldownMs` | number | `10000` | Cooldown between repeated UX toasts |
|
|
155
159
|
| `guidance.dynamic` | boolean | `true` | Enable dynamic contextual guidance |
|
|
156
160
|
| `guidance.warnOnKbEdits` | boolean | `true` | Enable loud warnings for .kb/** edits |
|
|
157
161
|
| `guidance.factFirstDomainRouting` | boolean | `true` | Enable FACT-first domain routing suggestions |
|
package/dist/comment-analysis.js
CHANGED
|
@@ -41,11 +41,15 @@ function extractJsTsComments(content) {
|
|
|
41
41
|
let i = 0;
|
|
42
42
|
while (i < lines.length) {
|
|
43
43
|
const line = lines[i];
|
|
44
|
+
if (line === undefined)
|
|
45
|
+
break;
|
|
44
46
|
if (line.trim().startsWith("/*")) {
|
|
45
47
|
const blockLines = [];
|
|
46
48
|
let j = i;
|
|
47
49
|
while (j < lines.length) {
|
|
48
50
|
const blockLine = lines[j];
|
|
51
|
+
if (blockLine === undefined)
|
|
52
|
+
break;
|
|
49
53
|
blockLines.push(blockLine);
|
|
50
54
|
if (blockLine.includes("*/"))
|
|
51
55
|
break;
|
|
@@ -68,8 +72,12 @@ function extractJsTsComments(content) {
|
|
|
68
72
|
if (line.trim().startsWith("//")) {
|
|
69
73
|
const commentLines = [];
|
|
70
74
|
let j = i;
|
|
71
|
-
while (j < lines.length
|
|
72
|
-
|
|
75
|
+
while (j < lines.length) {
|
|
76
|
+
const commentLine = lines[j];
|
|
77
|
+
if (commentLine === undefined || !commentLine.trim().startsWith("//")) {
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
commentLines.push(commentLine.trim().replace(/^\/\/\s?/, ""));
|
|
73
81
|
j++;
|
|
74
82
|
}
|
|
75
83
|
if (commentLines.length > 0) {
|
|
@@ -85,12 +93,6 @@ function extractJsTsComments(content) {
|
|
|
85
93
|
}
|
|
86
94
|
return comments;
|
|
87
95
|
}
|
|
88
|
-
/**
|
|
89
|
-
* Check if a line is an assignment (x = y), which would disqualify a triple-quoted string from being a docstring.
|
|
90
|
-
*/
|
|
91
|
-
function isAssignment(line) {
|
|
92
|
-
return /^\s*\w+\s*=\s*["']/.test(line);
|
|
93
|
-
}
|
|
94
96
|
/**
|
|
95
97
|
* Check if a line starts a class or function definition.
|
|
96
98
|
*/
|
|
@@ -109,7 +111,7 @@ function extractPythonComments(content) {
|
|
|
109
111
|
let classOrDefIndent = 0;
|
|
110
112
|
let foundClassDocstring = false;
|
|
111
113
|
function getIndent(line) {
|
|
112
|
-
return line.match(/^(\s*)/)?.[1]
|
|
114
|
+
return line.match(/^(\s*)/)?.[1]?.length ?? 0;
|
|
113
115
|
}
|
|
114
116
|
function isSignificantLine(line) {
|
|
115
117
|
const trimmed = line.trim();
|
|
@@ -118,15 +120,20 @@ function extractPythonComments(content) {
|
|
|
118
120
|
function extractDocstring(startIdx, quote, indent) {
|
|
119
121
|
const docstringLines = [];
|
|
120
122
|
let j = startIdx;
|
|
121
|
-
const startLine = lines[j]
|
|
123
|
+
const startLine = lines[j];
|
|
124
|
+
if (startLine === undefined)
|
|
125
|
+
return null;
|
|
126
|
+
const trimmedStartLine = startLine.trim();
|
|
122
127
|
// Extract content from opening line
|
|
123
|
-
const openingMatch =
|
|
128
|
+
const openingMatch = trimmedStartLine.match(new RegExp(`^\\s*${quote}(.*)$`));
|
|
124
129
|
if (openingMatch?.[1]) {
|
|
125
130
|
docstringLines.push(openingMatch[1].trim());
|
|
126
131
|
}
|
|
127
132
|
j++;
|
|
128
133
|
while (j < lines.length) {
|
|
129
134
|
const docLine = lines[j];
|
|
135
|
+
if (docLine === undefined)
|
|
136
|
+
break;
|
|
130
137
|
if (docLine.includes(quote)) {
|
|
131
138
|
// Closing line
|
|
132
139
|
const closingMatch = docLine.match(new RegExp(`^(.*?)${quote}`));
|
|
@@ -150,6 +157,8 @@ function extractPythonComments(content) {
|
|
|
150
157
|
let i = 0;
|
|
151
158
|
while (i < lines.length) {
|
|
152
159
|
const line = lines[i];
|
|
160
|
+
if (line === undefined)
|
|
161
|
+
break;
|
|
153
162
|
const trimmed = line.trim();
|
|
154
163
|
const indent = getIndent(line);
|
|
155
164
|
// Process # line comments FIRST (before skipping)
|
|
@@ -160,12 +169,17 @@ function extractPythonComments(content) {
|
|
|
160
169
|
// Collect contiguous # comments at same indent level
|
|
161
170
|
while (j < lines.length) {
|
|
162
171
|
const commentLine = lines[j];
|
|
172
|
+
if (commentLine === undefined)
|
|
173
|
+
break;
|
|
163
174
|
const lineHashMatch = commentLine.match(/^(\s*)#(.*)$/);
|
|
164
175
|
if (!lineHashMatch)
|
|
165
176
|
break;
|
|
166
177
|
if (getIndent(commentLine) !== currentIndent)
|
|
167
178
|
break;
|
|
168
|
-
|
|
179
|
+
const commentText = lineHashMatch[2];
|
|
180
|
+
if (commentText === undefined)
|
|
181
|
+
break;
|
|
182
|
+
commentLines.push(commentText.trim());
|
|
169
183
|
j++;
|
|
170
184
|
}
|
|
171
185
|
if (commentLines.length > 0) {
|
|
@@ -199,24 +213,6 @@ function extractPythonComments(content) {
|
|
|
199
213
|
// Check for triple-quoted strings
|
|
200
214
|
const isTripleQuote = trimmed.startsWith('"""') || trimmed.startsWith("'''");
|
|
201
215
|
if (isTripleQuote) {
|
|
202
|
-
// Skip if it's an assignment (x = """...""")
|
|
203
|
-
if (isAssignment(line)) {
|
|
204
|
-
// Track that we've seen a significant non-docstring statement
|
|
205
|
-
if (!foundModuleDocstring && !insideClassOrDef) {
|
|
206
|
-
foundModuleDocstring = true;
|
|
207
|
-
}
|
|
208
|
-
if (insideClassOrDef && !foundClassDocstring) {
|
|
209
|
-
foundClassDocstring = true;
|
|
210
|
-
}
|
|
211
|
-
// Skip to end of string
|
|
212
|
-
const quote = trimmed.startsWith('"""') ? '"""' : "'''";
|
|
213
|
-
i++;
|
|
214
|
-
while (i < lines.length && !lines[i].includes(quote)) {
|
|
215
|
-
i++;
|
|
216
|
-
}
|
|
217
|
-
i++;
|
|
218
|
-
continue;
|
|
219
|
-
}
|
|
220
216
|
const quote = trimmed.startsWith('"""') ? '"""' : "'''";
|
|
221
217
|
// Check if this is a valid docstring position
|
|
222
218
|
let isDocstring = false;
|
|
@@ -241,16 +237,13 @@ function extractPythonComments(content) {
|
|
|
241
237
|
continue;
|
|
242
238
|
}
|
|
243
239
|
}
|
|
244
|
-
// Not a docstring - mark as significant and skip it
|
|
245
|
-
if (!foundModuleDocstring && !insideClassOrDef) {
|
|
246
|
-
foundModuleDocstring = true;
|
|
247
|
-
}
|
|
248
|
-
if (insideClassOrDef && !foundClassDocstring) {
|
|
249
|
-
foundClassDocstring = true;
|
|
250
|
-
}
|
|
251
240
|
// Find the closing quote
|
|
252
241
|
i++;
|
|
253
|
-
while (i < lines.length
|
|
242
|
+
while (i < lines.length) {
|
|
243
|
+
const nextLine = lines[i];
|
|
244
|
+
if (nextLine === undefined || nextLine.includes(quote)) {
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
254
247
|
i++;
|
|
255
248
|
}
|
|
256
249
|
i++;
|
package/dist/config.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface KibiConfig {
|
|
|
11
11
|
relevant: string[];
|
|
12
12
|
};
|
|
13
13
|
ux: {
|
|
14
|
+
toastStartup: boolean;
|
|
14
15
|
toastFailures: boolean;
|
|
15
16
|
toastSuccesses: boolean;
|
|
16
17
|
toastCooldownMs: number;
|
|
@@ -43,6 +44,7 @@ export interface KibiConfig {
|
|
|
43
44
|
logLevel: string;
|
|
44
45
|
}
|
|
45
46
|
declare const DEFAULTS: KibiConfig;
|
|
47
|
+
export declare function validateAndMerge(obj: unknown): KibiConfig;
|
|
46
48
|
export declare function loadConfig(projectDir?: string): KibiConfig;
|
|
47
49
|
export declare function isPluginEnabled(cfg?: KibiConfig): boolean;
|
|
48
50
|
export { DEFAULTS };
|
package/dist/config.js
CHANGED
|
@@ -6,7 +6,12 @@ const DEFAULTS = {
|
|
|
6
6
|
enabled: true,
|
|
7
7
|
prompt: { enabled: true, hookMode: "auto" },
|
|
8
8
|
sync: { enabled: true, debounceMs: 2000, ignore: [], relevant: [] },
|
|
9
|
-
ux: {
|
|
9
|
+
ux: {
|
|
10
|
+
toastStartup: true,
|
|
11
|
+
toastFailures: true,
|
|
12
|
+
toastSuccesses: false,
|
|
13
|
+
toastCooldownMs: 10000,
|
|
14
|
+
},
|
|
10
15
|
guidance: {
|
|
11
16
|
dynamic: true,
|
|
12
17
|
warnOnKbEdits: true,
|
|
@@ -49,7 +54,8 @@ function readJsonIfExists(filePath) {
|
|
|
49
54
|
return null;
|
|
50
55
|
}
|
|
51
56
|
}
|
|
52
|
-
|
|
57
|
+
// implements REQ-opencode-kibi-plugin-v1
|
|
58
|
+
export function validateAndMerge(obj) {
|
|
53
59
|
if (!obj || typeof obj !== "object") {
|
|
54
60
|
logger.warn("Config is not an object, using defaults");
|
|
55
61
|
return DEFAULTS;
|
|
@@ -86,6 +92,8 @@ function validateAndMerge(obj) {
|
|
|
86
92
|
if (src.ux && typeof src.ux === "object") {
|
|
87
93
|
const u = src.ux;
|
|
88
94
|
out.ux = { ...DEFAULTS.ux };
|
|
95
|
+
if (typeof u.toastStartup === "boolean")
|
|
96
|
+
out.ux.toastStartup = u.toastStartup;
|
|
89
97
|
if (typeof u.toastFailures === "boolean")
|
|
90
98
|
out.ux.toastFailures = u.toastFailures;
|
|
91
99
|
if (typeof u.toastSuccesses === "boolean")
|
|
@@ -139,7 +147,8 @@ function validateAndMerge(obj) {
|
|
|
139
147
|
out.guidance.smartEnforcement.preflightTtlMs = se.preflightTtlMs;
|
|
140
148
|
if (typeof se.idleResetMs === "number")
|
|
141
149
|
out.guidance.smartEnforcement.idleResetMs = se.idleResetMs;
|
|
142
|
-
if (se.degradedMode === "warn-once" ||
|
|
150
|
+
if (se.degradedMode === "warn-once" ||
|
|
151
|
+
se.degradedMode === "structured-only")
|
|
143
152
|
out.guidance.smartEnforcement.degradedMode = se.degradedMode;
|
|
144
153
|
if (typeof se.requireRootKbForStrict === "boolean")
|
|
145
154
|
out.guidance.smartEnforcement.requireRootKbForStrict =
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,14 @@ export interface PluginInput {
|
|
|
5
5
|
serverUrl?: unknown;
|
|
6
6
|
$?: unknown;
|
|
7
7
|
client?: {
|
|
8
|
+
tui?: {
|
|
9
|
+
toast?: (payload: {
|
|
10
|
+
variant?: "info" | "success" | "warning" | "error";
|
|
11
|
+
title?: string;
|
|
12
|
+
message: string;
|
|
13
|
+
duration?: number;
|
|
14
|
+
}) => void | Promise<void>;
|
|
15
|
+
};
|
|
8
16
|
app: {
|
|
9
17
|
log: (payload: Record<string, unknown>) => Promise<void>;
|
|
10
18
|
};
|
package/dist/index.js
CHANGED
|
@@ -1,44 +1,18 @@
|
|
|
1
|
-
import { execSync } from "node:child_process";
|
|
2
1
|
import * as path from "node:path";
|
|
3
2
|
import { analyzeCodeFile, } from "./comment-analysis.js";
|
|
4
|
-
import * as config from "./config.js";
|
|
5
3
|
import * as fileFilter from "./file-filter.js";
|
|
6
|
-
import { getGuidanceCache } from "./guidance-cache.js";
|
|
7
4
|
import * as logger from "./logger.js";
|
|
8
5
|
import { analyzePath } from "./path-kind.js";
|
|
9
6
|
import { SENTINEL, buildPrompt } from "./prompt.js";
|
|
10
|
-
import { detectPosture } from "./repo-posture.js";
|
|
11
7
|
import { isMustPriorityRequirement } from "./requirement-doc.js";
|
|
12
8
|
import { classifyRisk } from "./risk-classifier.js";
|
|
13
|
-
import { createSyncScheduler as importedCreateSyncScheduler, } from "./scheduler.js";
|
|
14
9
|
import { getSessionTracker } from "./session-tracker.js";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
10
|
+
import { notifyStartup } from "./startup-notifier.js";
|
|
11
|
+
import { runPluginStartup } from "./plugin-startup.js";
|
|
17
12
|
import * as fs from "node:fs";
|
|
18
13
|
function deriveFileBucket(kind) {
|
|
19
14
|
return kind;
|
|
20
15
|
}
|
|
21
|
-
function resolveCurrentBranch(cwd) {
|
|
22
|
-
try {
|
|
23
|
-
return execSync("git rev-parse --abbrev-ref HEAD", {
|
|
24
|
-
cwd,
|
|
25
|
-
encoding: "utf8",
|
|
26
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
27
|
-
}).trim();
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
return "unknown";
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
function readConfigFingerprint(cwd) {
|
|
34
|
-
try {
|
|
35
|
-
return fs.readFileSync(path.join(cwd, ".kb", "config.json"), "utf-8");
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
return "missing";
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
const workspaceCacheState = new Map();
|
|
42
16
|
/**
|
|
43
17
|
* Lint requirement documents for embedded scenarios/tests and oversized content.
|
|
44
18
|
*/
|
|
@@ -78,119 +52,11 @@ function lintRequirementDoc(filePath, worktree) {
|
|
|
78
52
|
}
|
|
79
53
|
// implements REQ-opencode-kibi-plugin-v1
|
|
80
54
|
const kibiOpencodePlugin = async (input) => {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (!cfg.enabled) {
|
|
84
|
-
logger.info("kibi-opencode: disabled via config");
|
|
55
|
+
const startup = await runPluginStartup(input);
|
|
56
|
+
if (!startup) {
|
|
85
57
|
return {};
|
|
86
58
|
}
|
|
87
|
-
|
|
88
|
-
// Reset the logger client first to avoid leaking a previous invocation's
|
|
89
|
-
// client into this instance, then set the new one if provided.
|
|
90
|
-
logger.resetClient();
|
|
91
|
-
if (input.client) {
|
|
92
|
-
logger.setClient(input.client);
|
|
93
|
-
}
|
|
94
|
-
const workspaceHealth = checkWorkspaceHealth(input.worktree);
|
|
95
|
-
if (workspaceHealth.needsBootstrap) {
|
|
96
|
-
logger.error("kibi-opencode: workspace needs Kibi bootstrap");
|
|
97
|
-
getSessionTracker().recordWarning("bootstrap-needed", input.worktree, "Workspace missing Kibi bootstrap");
|
|
98
|
-
}
|
|
99
|
-
// Log session summary periodically (gated on config)
|
|
100
|
-
if (cfg.guidance.sessionSummary.enabled) {
|
|
101
|
-
const tracker = getSessionTracker();
|
|
102
|
-
if (tracker.isSessionExpired(cfg.guidance.sessionSummary.logIntervalMs)) {
|
|
103
|
-
tracker.logSummary();
|
|
104
|
-
tracker.reset();
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
const posture = detectPosture(input.worktree);
|
|
108
|
-
const currentBranch = resolveCurrentBranch(input.worktree);
|
|
109
|
-
const configFingerprint = readConfigFingerprint(input.worktree);
|
|
110
|
-
const cache = getGuidanceCache(cfg.guidance.smartEnforcement.preflightTtlMs, cfg.guidance.smartEnforcement.idleResetMs);
|
|
111
|
-
const previousCacheState = workspaceCacheState.get(input.worktree);
|
|
112
|
-
if (previousCacheState) {
|
|
113
|
-
if (previousCacheState.branch !== currentBranch) {
|
|
114
|
-
cache.invalidateForBranch(previousCacheState.branch);
|
|
115
|
-
}
|
|
116
|
-
if (previousCacheState.posture !== posture.state ||
|
|
117
|
-
previousCacheState.configFingerprint !== configFingerprint) {
|
|
118
|
-
cache.invalidateForWorkspace(input.worktree);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
workspaceCacheState.set(input.worktree, {
|
|
122
|
-
branch: currentBranch,
|
|
123
|
-
posture: posture.state,
|
|
124
|
-
configFingerprint,
|
|
125
|
-
});
|
|
126
|
-
// Session-local runtime degraded overlay (latched, never cleared)
|
|
127
|
-
const runtimeOverlay = {
|
|
128
|
-
degraded: false,
|
|
129
|
-
causes: [],
|
|
130
|
-
};
|
|
131
|
-
let degradedWarnedOnce = false;
|
|
132
|
-
function latchRuntimeDegraded(cause) {
|
|
133
|
-
if (!runtimeOverlay.degraded) {
|
|
134
|
-
runtimeOverlay.degraded = true;
|
|
135
|
-
runtimeOverlay.primaryCause = cause;
|
|
136
|
-
runtimeOverlay.causes.push(cause);
|
|
137
|
-
logger.info("smart-enforcement.degraded", {
|
|
138
|
-
event: "smart_enforcement_degraded",
|
|
139
|
-
overlay_cause: cause,
|
|
140
|
-
runtime_degraded: true,
|
|
141
|
-
static_degraded: posture.maintenanceDegraded,
|
|
142
|
-
merged_degraded: getMaintenanceDegraded(),
|
|
143
|
-
maintenance_state: getMaintenanceDegraded()
|
|
144
|
-
? "maintenance_degraded"
|
|
145
|
-
: "maintenance_available",
|
|
146
|
-
effective_mode: getEffectiveMode(),
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
else if (!runtimeOverlay.causes.includes(cause)) {
|
|
150
|
-
runtimeOverlay.causes.push(cause);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
function getMaintenanceDegraded() {
|
|
154
|
-
return posture.maintenanceDegraded || runtimeOverlay.degraded;
|
|
155
|
-
}
|
|
156
|
-
function getEffectiveMode() {
|
|
157
|
-
return computeEffectiveMode({
|
|
158
|
-
mode: cfg.guidance.smartEnforcement.mode,
|
|
159
|
-
requireRootKbForStrict: cfg.guidance.smartEnforcement.requireRootKbForStrict,
|
|
160
|
-
posture: posture.state,
|
|
161
|
-
maintenanceDegraded: getMaintenanceDegraded(),
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
// Compute effective smart-enforcement mode from config + posture + runtime overlay
|
|
165
|
-
// Latch startup-level runtime degraded causes
|
|
166
|
-
if (posture.state === "vendored_only" ||
|
|
167
|
-
posture.state === "root_uninitialized" ||
|
|
168
|
-
posture.state === "root_partial") {
|
|
169
|
-
latchRuntimeDegraded("non_authoritative_posture");
|
|
170
|
-
}
|
|
171
|
-
if (!cfg.sync.enabled) {
|
|
172
|
-
latchRuntimeDegraded("sync_disabled");
|
|
173
|
-
}
|
|
174
|
-
const maintenanceDegraded = getMaintenanceDegraded();
|
|
175
|
-
logger.info("smart-enforcement.posture", {
|
|
176
|
-
event: "smart_enforcement_posture",
|
|
177
|
-
posture: posture.state,
|
|
178
|
-
posture_state: posture.state,
|
|
179
|
-
maintenance_state: maintenanceDegraded
|
|
180
|
-
? "maintenance_degraded"
|
|
181
|
-
: "maintenance_available",
|
|
182
|
-
needs_bootstrap: workspaceHealth.needsBootstrap,
|
|
183
|
-
posture_reason: posture.reason,
|
|
184
|
-
reason_code: posture.reason,
|
|
185
|
-
smart_enforcement_mode: cfg.guidance.smartEnforcement.mode,
|
|
186
|
-
effective_mode: getEffectiveMode(),
|
|
187
|
-
static_degraded: posture.maintenanceDegraded,
|
|
188
|
-
runtime_degraded: runtimeOverlay.degraded,
|
|
189
|
-
merged_degraded: maintenanceDegraded,
|
|
190
|
-
overlay_cause: runtimeOverlay.primaryCause ?? null,
|
|
191
|
-
branch: currentBranch,
|
|
192
|
-
});
|
|
193
|
-
logger.info("kibi-opencode: setting up hooks");
|
|
59
|
+
const { cfg, workspaceHealth, posture, currentBranch, cache, runtimeOverlay, scheduler, maintenanceDegraded, getMaintenanceDegraded, getEffectiveMode, latchRuntimeDegraded, } = startup;
|
|
194
60
|
const hooks = {};
|
|
195
61
|
// Plugin instance state (not module globals)
|
|
196
62
|
const MAX_RECENT_EDITS = 5;
|
|
@@ -199,30 +65,7 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
199
65
|
let recentCommentSuggestion = null;
|
|
200
66
|
const seenFingerprints = new Set(); // For deduplication
|
|
201
67
|
let lastRiskClass = null;
|
|
202
|
-
|
|
203
|
-
// Create scheduler only if sync is enabled
|
|
204
|
-
let scheduler = null;
|
|
205
|
-
if (cfg.sync.enabled) {
|
|
206
|
-
try {
|
|
207
|
-
const schedulerOpts = {
|
|
208
|
-
worktree: input.worktree,
|
|
209
|
-
config: cfg,
|
|
210
|
-
onRunComplete: (meta) => {
|
|
211
|
-
if (meta.exitCode !== 0) {
|
|
212
|
-
latchRuntimeDegraded("scheduler_sync_failed");
|
|
213
|
-
}
|
|
214
|
-
if (meta.checkExitCode !== undefined && meta.checkExitCode !== 0) {
|
|
215
|
-
latchRuntimeDegraded("scheduler_check_failed");
|
|
216
|
-
}
|
|
217
|
-
},
|
|
218
|
-
};
|
|
219
|
-
scheduler = createSyncScheduler(schedulerOpts);
|
|
220
|
-
}
|
|
221
|
-
catch {
|
|
222
|
-
latchRuntimeDegraded("scheduler_unavailable");
|
|
223
|
-
scheduler = null;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
68
|
+
let degradedWarnedOnce = false;
|
|
226
69
|
hooks.event = async ({ event }) => {
|
|
227
70
|
if (event.type !== "file.edited")
|
|
228
71
|
return;
|
|
@@ -492,7 +335,6 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
492
335
|
else {
|
|
493
336
|
recentCommentSuggestion = null;
|
|
494
337
|
}
|
|
495
|
-
return;
|
|
496
338
|
}
|
|
497
339
|
return;
|
|
498
340
|
};
|
|
@@ -515,7 +357,6 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
515
357
|
hasRecentKbEdit,
|
|
516
358
|
recentCommentSuggestion,
|
|
517
359
|
posture: posture.state,
|
|
518
|
-
riskClass: lastRiskClass ?? undefined,
|
|
519
360
|
cache,
|
|
520
361
|
workspaceRoot: input.worktree,
|
|
521
362
|
branch: currentBranch,
|
|
@@ -523,6 +364,7 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
523
364
|
maintenanceDegraded,
|
|
524
365
|
degradedMode: cfg.guidance.smartEnforcement.degradedMode,
|
|
525
366
|
showDegradedAdvisory,
|
|
367
|
+
...(lastRiskClass != null ? { riskClass: lastRiskClass } : {}),
|
|
526
368
|
});
|
|
527
369
|
logger.info("smart-enforcement.guidance", {
|
|
528
370
|
event: "smart_enforcement_guidance",
|
|
@@ -582,6 +424,15 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
582
424
|
}
|
|
583
425
|
}
|
|
584
426
|
logger.info("kibi-opencode: setup complete");
|
|
427
|
+
if (input.client && !maintenanceDegraded) {
|
|
428
|
+
const client = input.client;
|
|
429
|
+
setTimeout(() => {
|
|
430
|
+
notifyStartup(client, {
|
|
431
|
+
suppressToast: cfg.ux.toastStartup === false,
|
|
432
|
+
directory: input.directory,
|
|
433
|
+
});
|
|
434
|
+
}, 2000);
|
|
435
|
+
}
|
|
585
436
|
return hooks;
|
|
586
437
|
};
|
|
587
438
|
export default kibiOpencodePlugin;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as config from "./config.js";
|
|
2
|
+
import { getGuidanceCache } from "./guidance-cache.js";
|
|
3
|
+
import { detectPosture } from "./repo-posture.js";
|
|
4
|
+
import { createSyncScheduler } from "./scheduler.js";
|
|
5
|
+
import { type EffectiveMode } from "./smart-enforcement.js";
|
|
6
|
+
import { checkWorkspaceHealth } from "./workspace-health.js";
|
|
7
|
+
import type { PluginInput } from "./index.js";
|
|
8
|
+
type PostureSnapshot = ReturnType<typeof detectPosture>;
|
|
9
|
+
export interface RuntimeDegradedOverlay {
|
|
10
|
+
degraded: boolean;
|
|
11
|
+
primaryCause?: "sync_disabled" | "scheduler_unavailable" | "scheduler_sync_failed" | "scheduler_check_failed" | "non_authoritative_posture";
|
|
12
|
+
causes: string[];
|
|
13
|
+
}
|
|
14
|
+
export interface PluginStartupContext {
|
|
15
|
+
cfg: ReturnType<typeof config.loadConfig>;
|
|
16
|
+
workspaceHealth: ReturnType<typeof checkWorkspaceHealth>;
|
|
17
|
+
posture: PostureSnapshot;
|
|
18
|
+
currentBranch: string;
|
|
19
|
+
cache: ReturnType<typeof getGuidanceCache>;
|
|
20
|
+
runtimeOverlay: RuntimeDegradedOverlay;
|
|
21
|
+
scheduler: ReturnType<typeof createSyncScheduler> | null;
|
|
22
|
+
maintenanceDegraded: boolean;
|
|
23
|
+
getMaintenanceDegraded: () => boolean;
|
|
24
|
+
getEffectiveMode: () => EffectiveMode;
|
|
25
|
+
latchRuntimeDegraded: (cause: NonNullable<RuntimeDegradedOverlay["primaryCause"]>) => void;
|
|
26
|
+
}
|
|
27
|
+
export declare function runPluginStartup(input: PluginInput): Promise<PluginStartupContext | null>;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import * as config from "./config.js";
|
|
5
|
+
import { getGuidanceCache } from "./guidance-cache.js";
|
|
6
|
+
import * as logger from "./logger.js";
|
|
7
|
+
import { detectPosture } from "./repo-posture.js";
|
|
8
|
+
import { createSyncScheduler } from "./scheduler.js";
|
|
9
|
+
import { getSessionTracker } from "./session-tracker.js";
|
|
10
|
+
import { computeEffectiveMode, } from "./smart-enforcement.js";
|
|
11
|
+
import { checkWorkspaceHealth } from "./workspace-health.js";
|
|
12
|
+
const workspaceCacheState = new Map();
|
|
13
|
+
function resolveCurrentBranch(cwd) {
|
|
14
|
+
try {
|
|
15
|
+
return execSync("git rev-parse --abbrev-ref HEAD", {
|
|
16
|
+
cwd,
|
|
17
|
+
encoding: "utf8",
|
|
18
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
19
|
+
}).trim();
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return "unknown";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function readConfigFingerprint(cwd) {
|
|
26
|
+
try {
|
|
27
|
+
return fs.readFileSync(path.join(cwd, ".kb", "config.json"), "utf-8");
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return "missing";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// implements REQ-opencode-smart-enforcement-v1, REQ-opencode-kibi-plugin-v1
|
|
34
|
+
export async function runPluginStartup(input) {
|
|
35
|
+
const cfg = config.loadConfig(input.directory);
|
|
36
|
+
if (!cfg.enabled) {
|
|
37
|
+
logger.info("kibi-opencode: disabled via config");
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
logger.resetClient();
|
|
41
|
+
if (input.client) {
|
|
42
|
+
logger.setClient(input.client);
|
|
43
|
+
}
|
|
44
|
+
const workspaceHealth = checkWorkspaceHealth(input.worktree);
|
|
45
|
+
if (workspaceHealth.needsBootstrap) {
|
|
46
|
+
logger.error("kibi-opencode: workspace needs Kibi bootstrap");
|
|
47
|
+
getSessionTracker().recordWarning("bootstrap-needed", input.worktree, "Workspace missing Kibi bootstrap");
|
|
48
|
+
}
|
|
49
|
+
if (cfg.guidance.sessionSummary.enabled) {
|
|
50
|
+
const tracker = getSessionTracker();
|
|
51
|
+
if (tracker.isSessionExpired(cfg.guidance.sessionSummary.logIntervalMs)) {
|
|
52
|
+
tracker.logSummary();
|
|
53
|
+
tracker.reset();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const posture = detectPosture(input.worktree);
|
|
57
|
+
const currentBranch = resolveCurrentBranch(input.worktree);
|
|
58
|
+
const configFingerprint = readConfigFingerprint(input.worktree);
|
|
59
|
+
const cache = getGuidanceCache(cfg.guidance.smartEnforcement.preflightTtlMs, cfg.guidance.smartEnforcement.idleResetMs);
|
|
60
|
+
const previousCacheState = workspaceCacheState.get(input.worktree);
|
|
61
|
+
if (previousCacheState) {
|
|
62
|
+
if (previousCacheState.branch !== currentBranch) {
|
|
63
|
+
cache.invalidateForBranch(previousCacheState.branch);
|
|
64
|
+
}
|
|
65
|
+
if (previousCacheState.posture !== posture.state ||
|
|
66
|
+
previousCacheState.configFingerprint !== configFingerprint) {
|
|
67
|
+
cache.invalidateForWorkspace(input.worktree);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
workspaceCacheState.set(input.worktree, {
|
|
71
|
+
branch: currentBranch,
|
|
72
|
+
posture: posture.state,
|
|
73
|
+
configFingerprint,
|
|
74
|
+
});
|
|
75
|
+
const runtimeOverlay = {
|
|
76
|
+
degraded: false,
|
|
77
|
+
causes: [],
|
|
78
|
+
};
|
|
79
|
+
function latchRuntimeDegraded(cause) {
|
|
80
|
+
if (!runtimeOverlay.degraded) {
|
|
81
|
+
runtimeOverlay.degraded = true;
|
|
82
|
+
runtimeOverlay.primaryCause = cause;
|
|
83
|
+
runtimeOverlay.causes.push(cause);
|
|
84
|
+
logger.info("smart-enforcement.degraded", {
|
|
85
|
+
event: "smart_enforcement_degraded",
|
|
86
|
+
overlay_cause: cause,
|
|
87
|
+
runtime_degraded: true,
|
|
88
|
+
static_degraded: posture.maintenanceDegraded,
|
|
89
|
+
merged_degraded: getMaintenanceDegraded(),
|
|
90
|
+
maintenance_state: getMaintenanceDegraded()
|
|
91
|
+
? "maintenance_degraded"
|
|
92
|
+
: "maintenance_available",
|
|
93
|
+
effective_mode: getEffectiveMode(),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
else if (!runtimeOverlay.causes.includes(cause)) {
|
|
97
|
+
runtimeOverlay.causes.push(cause);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function getMaintenanceDegraded() {
|
|
101
|
+
return posture.maintenanceDegraded || runtimeOverlay.degraded;
|
|
102
|
+
}
|
|
103
|
+
function getEffectiveMode() {
|
|
104
|
+
return computeEffectiveMode({
|
|
105
|
+
mode: cfg.guidance.smartEnforcement.mode,
|
|
106
|
+
requireRootKbForStrict: cfg.guidance.smartEnforcement.requireRootKbForStrict,
|
|
107
|
+
posture: posture.state,
|
|
108
|
+
maintenanceDegraded: getMaintenanceDegraded(),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (posture.state === "vendored_only" ||
|
|
112
|
+
posture.state === "root_uninitialized" ||
|
|
113
|
+
posture.state === "root_partial") {
|
|
114
|
+
latchRuntimeDegraded("non_authoritative_posture");
|
|
115
|
+
}
|
|
116
|
+
if (!cfg.sync.enabled) {
|
|
117
|
+
latchRuntimeDegraded("sync_disabled");
|
|
118
|
+
}
|
|
119
|
+
const maintenanceDegraded = getMaintenanceDegraded();
|
|
120
|
+
logger.info("smart-enforcement.posture", {
|
|
121
|
+
event: "smart_enforcement_posture",
|
|
122
|
+
posture: posture.state,
|
|
123
|
+
posture_state: posture.state,
|
|
124
|
+
maintenance_state: maintenanceDegraded
|
|
125
|
+
? "maintenance_degraded"
|
|
126
|
+
: "maintenance_available",
|
|
127
|
+
needs_bootstrap: workspaceHealth.needsBootstrap,
|
|
128
|
+
posture_reason: posture.reason,
|
|
129
|
+
reason_code: posture.reason,
|
|
130
|
+
smart_enforcement_mode: cfg.guidance.smartEnforcement.mode,
|
|
131
|
+
effective_mode: getEffectiveMode(),
|
|
132
|
+
static_degraded: posture.maintenanceDegraded,
|
|
133
|
+
runtime_degraded: runtimeOverlay.degraded,
|
|
134
|
+
merged_degraded: maintenanceDegraded,
|
|
135
|
+
overlay_cause: runtimeOverlay.primaryCause ?? null,
|
|
136
|
+
branch: currentBranch,
|
|
137
|
+
});
|
|
138
|
+
logger.info("kibi-opencode: setting up hooks");
|
|
139
|
+
const schedulerFactory = globalThis.__kibi_test_scheduler_factory ?? createSyncScheduler;
|
|
140
|
+
const scheduler = cfg.sync
|
|
141
|
+
.enabled
|
|
142
|
+
? (() => {
|
|
143
|
+
try {
|
|
144
|
+
const schedulerOpts = {
|
|
145
|
+
worktree: input.worktree,
|
|
146
|
+
config: cfg,
|
|
147
|
+
onRunComplete: (meta) => {
|
|
148
|
+
if (meta.exitCode !== 0)
|
|
149
|
+
latchRuntimeDegraded("scheduler_sync_failed");
|
|
150
|
+
if (meta.checkExitCode !== undefined &&
|
|
151
|
+
meta.checkExitCode !== 0) {
|
|
152
|
+
latchRuntimeDegraded("scheduler_check_failed");
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
return schedulerFactory(schedulerOpts);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
latchRuntimeDegraded("scheduler_unavailable");
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
})()
|
|
163
|
+
: null;
|
|
164
|
+
return {
|
|
165
|
+
cfg,
|
|
166
|
+
workspaceHealth,
|
|
167
|
+
posture,
|
|
168
|
+
currentBranch,
|
|
169
|
+
cache,
|
|
170
|
+
runtimeOverlay,
|
|
171
|
+
scheduler,
|
|
172
|
+
maintenanceDegraded,
|
|
173
|
+
getMaintenanceDegraded,
|
|
174
|
+
getEffectiveMode,
|
|
175
|
+
latchRuntimeDegraded,
|
|
176
|
+
};
|
|
177
|
+
}
|
package/dist/prompt.d.ts
CHANGED
|
@@ -33,6 +33,7 @@ export interface PromptContext {
|
|
|
33
33
|
/** Whether to show the degraded advisory block this invocation */
|
|
34
34
|
showDegradedAdvisory?: boolean;
|
|
35
35
|
}
|
|
36
|
+
export declare function postureGuidance(posture: RepoPosture): string | null;
|
|
36
37
|
/**
|
|
37
38
|
* Build prompt with contextual guidance based on posture, risk class, and cache state.
|
|
38
39
|
*/
|
package/dist/prompt.js
CHANGED
|
@@ -56,10 +56,10 @@ Requirement edits need policy alignment. Run kb_check with required-fields and n
|
|
|
56
56
|
- Add verification: create or update linked SCEN and TEST entities`,
|
|
57
57
|
behavior_candidate: `📝 **Code changes detected**
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
Production code: use \`implements\` (symbol→req) for requirement ownership. Test code: use \`executable_for\` (symbol→test). \`covered_by\` is coverage evidence only. Prefer scenario-first: req→scenario→test when scenarios exist.`,
|
|
60
60
|
traceability_candidate: `📝 **Code changes detected**
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
Production code: use \`implements\` (symbol→req) for requirement ownership. Test code: use \`executable_for\` (symbol→test). \`covered_by\` is coverage evidence only. Prefer scenario-first: req→scenario→test when scenarios exist.
|
|
63
63
|
- Durable knowledge comment detected — route to KB instead of inline comments
|
|
64
64
|
- Use kb_upsert for FACT, ADR, or REQ entities as appropriate`,
|
|
65
65
|
manual_kb_edit: `⚠️ **WARNING: Direct .kb/ edits bypass validation**
|
|
@@ -70,7 +70,8 @@ The Kibi knowledge base is managed through public MCP tools. Direct manual edits
|
|
|
70
70
|
- Use kb_check to validate consistency`,
|
|
71
71
|
};
|
|
72
72
|
// ── Posture overrides ──────────────────────────────────────────────────
|
|
73
|
-
function postureGuidance(posture) {
|
|
73
|
+
export function postureGuidance(posture) {
|
|
74
|
+
// implements REQ-opencode-prompt-injection
|
|
74
75
|
switch (posture) {
|
|
75
76
|
case "vendored_only":
|
|
76
77
|
// Minimal guidance only, no bootstrap nags
|
|
@@ -191,7 +192,7 @@ Before implementing or explaining code:
|
|
|
191
192
|
1. **Discover first** - Run kb_search to find related requirements, ADRs, tests, facts, and symbols.
|
|
192
193
|
2. **Follow up exactly** - Run kb_query by sourceFile, id, type, or tags once you know what you need.
|
|
193
194
|
3. **Prefer Kibi over comments** - Store durable knowledge in KB entities instead of inline comments.
|
|
194
|
-
4. **Add traceability** -
|
|
195
|
+
4. **Add traceability** - Production code: \`implements\` (symbol→req) for ownership. Test code: \`executable_for\`. \`covered_by\` is coverage evidence only for production symbols.
|
|
195
196
|
|
|
196
197
|
If you're adding long explanatory comments, consider routing that knowledge to:
|
|
197
198
|
- \`FACT\` for domain invariants, properties, limits, cardinalities
|
|
@@ -228,9 +229,6 @@ If you're adding long explanatory comments, consider routing that knowledge to:
|
|
|
228
229
|
if (headerEnd !== -1) {
|
|
229
230
|
selectedBlock = `${selectedBlock.slice(0, headerEnd + 1)}- Existing Kibi links: ${linkedIds.join(", ")}\n${selectedBlock.slice(headerEnd + 1)}`;
|
|
230
231
|
}
|
|
231
|
-
else {
|
|
232
|
-
selectedBlock = `${selectedBlock}\n- Existing Kibi links: ${linkedIds.join(", ")}`;
|
|
233
|
-
}
|
|
234
232
|
}
|
|
235
233
|
}
|
|
236
234
|
}
|
|
@@ -320,7 +318,7 @@ Your recent code edit contains a comment that looks like **behavior intent** (sy
|
|
|
320
318
|
**Action**: Instead of inline comments, route this to a REQ entity:
|
|
321
319
|
- Create \`documentation/requirements/REQ-xxx.md\` with the behavior description
|
|
322
320
|
- Add SCEN and TEST entities for specification and verification
|
|
323
|
-
- Link code
|
|
321
|
+
- Link code: production uses \`implements\` (symbol→req) for ownership; test code uses \`executable_for\`; \`covered_by\` is coverage evidence only
|
|
324
322
|
|
|
325
323
|
This ensures behavior is documented and traceable.`;
|
|
326
324
|
default:
|
|
@@ -330,7 +328,7 @@ Before implementing or explaining code:
|
|
|
330
328
|
1. **Discover first** - Run kb_search to find related requirements, ADRs, tests, facts, and symbols.
|
|
331
329
|
2. **Follow up exactly** - Run kb_query by sourceFile, id, type, or tags once you know what you need.
|
|
332
330
|
3. **Prefer Kibi over comments** - Store durable knowledge in KB entities instead of inline comments.
|
|
333
|
-
4. **Add traceability** -
|
|
331
|
+
4. **Add traceability** - Production code: \`implements\` (symbol→req) for ownership. Test code: \`executable_for\`. \`covered_by\` is coverage evidence only for production symbols.`;
|
|
334
332
|
}
|
|
335
333
|
}
|
|
336
334
|
// ── Base guidance (no context) ─────────────────────────────────────────
|
|
@@ -342,7 +340,7 @@ This project uses Kibi (via MCP). Prefer storing durable knowledge in Kibi over
|
|
|
342
340
|
|
|
343
341
|
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.
|
|
344
342
|
|
|
345
|
-
Keep changed symbols traceable:
|
|
343
|
+
Keep changed symbols traceable: production code uses \`implements\` (symbol→req) for ownership; test code uses \`executable_for\`; \`covered_by\` is coverage evidence only. Inline \`// implements REQ-xxx\` comments remain backward-compatible.
|
|
346
344
|
|
|
347
345
|
Run kb_check after KB mutations.
|
|
348
346
|
|
|
@@ -353,7 +351,7 @@ Dogfood note for this repo: OpenCode here uses local built \`kibi-mcp\` and \`ki
|
|
|
353
351
|
2. **Confirm**: Run kb_query with sourceFile, id, type, or tags once you know the exact follow-up target.
|
|
354
352
|
3. **Inspect freshness**: Run kb_status when branch or stale-state confidence matters.
|
|
355
353
|
4. **Document intent**: If you are about to explain code, STOP. Route that explanation to kb_upsert instead of inline comments.
|
|
356
|
-
5. **Link during work**: When creating KB entities, include relationship rows: specified_by (req→scenario),
|
|
354
|
+
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).
|
|
357
355
|
6. **Validate**: Run kb_check after KB mutations to catch violations early.
|
|
358
356
|
|
|
359
357
|
**Public Kibi tools only:** kb_search, kb_query, kb_status, kb_find_gaps, kb_coverage, kb_graph, kb_upsert, kb_delete, kb_check.
|
package/dist/repo-posture.js
CHANGED
|
@@ -73,6 +73,8 @@ function rootTargetsAllResolve(cwd) {
|
|
|
73
73
|
];
|
|
74
74
|
for (const key of defaultKeys) {
|
|
75
75
|
const raw = paths?.[key] ?? DEFAULT_SYNC_PATHS[key];
|
|
76
|
+
if (!raw)
|
|
77
|
+
return false;
|
|
76
78
|
// Normalize: strip trailing slashes and glob patterns to get the root dir/file path
|
|
77
79
|
const normalized = raw.replace(/\/+$/, "");
|
|
78
80
|
const isFile = normalized.endsWith(".yaml") || normalized.endsWith(".yml");
|
package/dist/requirement-doc.js
CHANGED
|
@@ -42,6 +42,8 @@ function parseFrontmatter(content) {
|
|
|
42
42
|
if (!match)
|
|
43
43
|
return null;
|
|
44
44
|
const frontmatterText = match[1];
|
|
45
|
+
if (frontmatterText === undefined)
|
|
46
|
+
return null;
|
|
45
47
|
const result = {};
|
|
46
48
|
// Simple YAML-like parsing for top-level scalar values only
|
|
47
49
|
// Handles inline comments by ignoring everything after # (unless quoted)
|
|
@@ -56,8 +58,10 @@ function parseFrontmatter(content) {
|
|
|
56
58
|
let value = line.slice(colonIndex + 1).trim();
|
|
57
59
|
// Strip inline comments (simple heuristic: unquoted #)
|
|
58
60
|
const commentMatch = value.match(/^(.*?)\s+#\s/);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
const commentValue = commentMatch?.[1];
|
|
62
|
+
if (commentValue !== undefined &&
|
|
63
|
+
!isInsideQuotes(value, commentValue.length)) {
|
|
64
|
+
value = commentValue.trim();
|
|
61
65
|
}
|
|
62
66
|
if (key && value) {
|
|
63
67
|
if ((value.startsWith('"') && value.endsWith('"')) ||
|
package/dist/scheduler.js
CHANGED
|
@@ -39,7 +39,11 @@ class WorktreeSyncScheduler {
|
|
|
39
39
|
return;
|
|
40
40
|
this.lastFileEditedAt = this.now();
|
|
41
41
|
}
|
|
42
|
-
this.pending = {
|
|
42
|
+
this.pending = {
|
|
43
|
+
reason,
|
|
44
|
+
...(filePath !== undefined ? { filePath } : {}),
|
|
45
|
+
...(checkRules !== undefined ? { checkRules } : {}),
|
|
46
|
+
};
|
|
43
47
|
if (this.timer)
|
|
44
48
|
this.clearTimeoutFn(this.timer);
|
|
45
49
|
this.timer = this.setTimeoutFn(() => {
|
|
@@ -135,8 +139,12 @@ class WorktreeSyncScheduler {
|
|
|
135
139
|
this.trailing = null;
|
|
136
140
|
void this.startRun({
|
|
137
141
|
reason: `${trailing.reason}.trailing`,
|
|
138
|
-
|
|
139
|
-
|
|
142
|
+
...(trailing.filePath !== undefined
|
|
143
|
+
? { filePath: trailing.filePath }
|
|
144
|
+
: {}),
|
|
145
|
+
...(trailing.checkRules !== undefined
|
|
146
|
+
? { checkRules: trailing.checkRules }
|
|
147
|
+
: {}),
|
|
140
148
|
});
|
|
141
149
|
}
|
|
142
150
|
}
|
|
@@ -146,12 +154,12 @@ class WorktreeSyncScheduler {
|
|
|
146
154
|
const meta = {
|
|
147
155
|
reason: trigger.reason,
|
|
148
156
|
worktree: this.worktree,
|
|
149
|
-
filePath: trigger.filePath,
|
|
150
157
|
debounceWindowMs: this.config.sync.debounceMs,
|
|
151
158
|
durationMs,
|
|
152
159
|
exitCode,
|
|
153
|
-
|
|
154
|
-
|
|
160
|
+
...(trigger.filePath !== undefined ? { filePath: trigger.filePath } : {}),
|
|
161
|
+
...(checkExitCode !== undefined ? { checkExitCode } : {}),
|
|
162
|
+
...(checkRules !== undefined ? { checkRules } : {}),
|
|
155
163
|
};
|
|
156
164
|
if (exitCode === 0) {
|
|
157
165
|
logger.info(`sync.succeeded ${JSON.stringify(meta)}`);
|
|
@@ -10,6 +10,7 @@ const STRICT_ELIGIBLE_POSTURES = new Set([
|
|
|
10
10
|
* Only root_active and hybrid_root_plus_vendored are considered authoritative.
|
|
11
11
|
*/
|
|
12
12
|
export function isStrictEligible(inputs) {
|
|
13
|
+
// implements REQ-opencode-smart-enforcement-v1
|
|
13
14
|
if (inputs.maintenanceDegraded)
|
|
14
15
|
return false;
|
|
15
16
|
if (inputs.requireRootKbForStrict) {
|
|
@@ -31,6 +32,7 @@ export function isStrictEligible(inputs) {
|
|
|
31
32
|
* - maintenance-degraded → advisory regardless of config
|
|
32
33
|
*/
|
|
33
34
|
export function computeEffectiveMode(inputs) {
|
|
35
|
+
// implements REQ-opencode-smart-enforcement-v1
|
|
34
36
|
// Maintenance-degraded always forces advisory
|
|
35
37
|
if (inputs.maintenanceDegraded) {
|
|
36
38
|
return "advisory";
|
|
@@ -87,7 +87,7 @@ function parseSymbolsYaml(content) {
|
|
|
87
87
|
let section = "none";
|
|
88
88
|
let pendingRel = null;
|
|
89
89
|
function flushRel() {
|
|
90
|
-
if (pendingRel?.type && pendingRel.target && current) {
|
|
90
|
+
if (pendingRel?.type && pendingRel.target && current?.relationships) {
|
|
91
91
|
current.relationships.push({
|
|
92
92
|
type: pendingRel.type,
|
|
93
93
|
target: pendingRel.target,
|
|
@@ -110,7 +110,10 @@ function parseSymbolsYaml(content) {
|
|
|
110
110
|
const entryMatch = raw.match(/^\s+-\s+id:\s*(.+)$/);
|
|
111
111
|
if (entryMatch) {
|
|
112
112
|
flushEntry();
|
|
113
|
-
|
|
113
|
+
const entryId = entryMatch[1];
|
|
114
|
+
if (entryId === undefined)
|
|
115
|
+
continue;
|
|
116
|
+
current = { id: entryId.trim(), links: [], relationships: [] };
|
|
114
117
|
section = "none";
|
|
115
118
|
continue;
|
|
116
119
|
}
|
|
@@ -119,7 +122,10 @@ function parseSymbolsYaml(content) {
|
|
|
119
122
|
// sourceFile
|
|
120
123
|
const srcMatch = raw.match(/^\s+sourceFile:\s*(.+)$/);
|
|
121
124
|
if (srcMatch) {
|
|
122
|
-
|
|
125
|
+
const sourceFile = srcMatch[1];
|
|
126
|
+
if (sourceFile === undefined)
|
|
127
|
+
continue;
|
|
128
|
+
current.sourceFile = sourceFile.trim();
|
|
123
129
|
section = "none";
|
|
124
130
|
continue;
|
|
125
131
|
}
|
|
@@ -139,7 +145,10 @@ function parseSymbolsYaml(content) {
|
|
|
139
145
|
if (section === "links") {
|
|
140
146
|
const linkMatch = raw.match(/^\s+-\s+(REQ-[A-Za-z0-9_-]+)\s*$/);
|
|
141
147
|
if (linkMatch) {
|
|
142
|
-
|
|
148
|
+
const linkId = linkMatch[1];
|
|
149
|
+
if (linkId !== undefined && current.links) {
|
|
150
|
+
current.links.push(linkId);
|
|
151
|
+
}
|
|
143
152
|
continue;
|
|
144
153
|
}
|
|
145
154
|
}
|
|
@@ -148,13 +157,19 @@ function parseSymbolsYaml(content) {
|
|
|
148
157
|
const relTypeMatch = raw.match(/^\s+-\s+type:\s*(.+)$/);
|
|
149
158
|
if (relTypeMatch) {
|
|
150
159
|
flushRel();
|
|
151
|
-
|
|
160
|
+
const relationType = relTypeMatch[1];
|
|
161
|
+
if (relationType === undefined)
|
|
162
|
+
continue;
|
|
163
|
+
pendingRel = { type: relationType.trim() };
|
|
152
164
|
continue;
|
|
153
165
|
}
|
|
154
166
|
// Relationship target: " target: REQ-..."
|
|
155
167
|
const relTargetMatch = raw.match(/^\s+target:\s*(.+)$/);
|
|
156
168
|
if (relTargetMatch && pendingRel) {
|
|
157
|
-
|
|
169
|
+
const target = relTargetMatch[1];
|
|
170
|
+
if (target === undefined)
|
|
171
|
+
continue;
|
|
172
|
+
pendingRel.target = target.trim();
|
|
158
173
|
continue;
|
|
159
174
|
}
|
|
160
175
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type ToastPayload = {
|
|
2
|
+
variant?: "info" | "success" | "warning" | "error";
|
|
3
|
+
title?: string;
|
|
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
|
+
};
|
|
19
|
+
app: {
|
|
20
|
+
log: (payload: Record<string, unknown>) => Promise<void>;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
export type StartupNotifierConfig = {
|
|
24
|
+
version?: string;
|
|
25
|
+
suppressToast?: boolean;
|
|
26
|
+
directory?: string;
|
|
27
|
+
};
|
|
28
|
+
export declare function notifyStartup(client: StartupNotifierClient, cfg: StartupNotifierConfig): void;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
function hasShowToast(client) {
|
|
2
|
+
return typeof client.tui?.showToast === "function";
|
|
3
|
+
}
|
|
4
|
+
function hasLegacyToast(client) {
|
|
5
|
+
return typeof client.tui?.toast === "function";
|
|
6
|
+
}
|
|
7
|
+
// implements REQ-opencode-kibi-plugin-v1
|
|
8
|
+
export function notifyStartup(client, cfg) {
|
|
9
|
+
const message = "kibi-opencode started";
|
|
10
|
+
const toastPayload = {
|
|
11
|
+
variant: "success",
|
|
12
|
+
title: "Kibi OpenCode",
|
|
13
|
+
message,
|
|
14
|
+
duration: 4000,
|
|
15
|
+
};
|
|
16
|
+
if (!cfg.suppressToast) {
|
|
17
|
+
if (hasShowToast(client)) {
|
|
18
|
+
void Promise.resolve(client.tui.showToast({ body: toastPayload }))
|
|
19
|
+
.then((result) => void Promise.resolve(client.app.log({
|
|
20
|
+
body: {
|
|
21
|
+
service: "kibi-opencode",
|
|
22
|
+
level: "info",
|
|
23
|
+
message: "startup toast result",
|
|
24
|
+
result: String(result),
|
|
25
|
+
...(cfg.directory ? { directory: cfg.directory } : {}),
|
|
26
|
+
},
|
|
27
|
+
})).catch((logErr) => {
|
|
28
|
+
console.error("[kibi-opencode] startup toast result log failed:", logErr);
|
|
29
|
+
}))
|
|
30
|
+
.catch((err) => {
|
|
31
|
+
console.error("[kibi-opencode] startup toast failed:", err);
|
|
32
|
+
void Promise.resolve(client.app.log({
|
|
33
|
+
body: {
|
|
34
|
+
service: "kibi-opencode",
|
|
35
|
+
level: "warn",
|
|
36
|
+
message: "startup toast failed",
|
|
37
|
+
error: String(err),
|
|
38
|
+
...(cfg.directory ? { directory: cfg.directory } : {}),
|
|
39
|
+
},
|
|
40
|
+
})).catch((logErr) => {
|
|
41
|
+
console.error("[kibi-opencode] startup toast log failed:", logErr);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
else if (hasLegacyToast(client)) {
|
|
46
|
+
void Promise.resolve(client.tui.toast(toastPayload)).catch((err) => {
|
|
47
|
+
console.error("[kibi-opencode] startup toast failed:", err);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
void Promise.resolve(client.app.log({
|
|
52
|
+
body: {
|
|
53
|
+
service: "kibi-opencode",
|
|
54
|
+
level: "info",
|
|
55
|
+
message,
|
|
56
|
+
...(cfg.version ? { version: cfg.version } : {}),
|
|
57
|
+
...(cfg.directory ? { directory: cfg.directory } : {}),
|
|
58
|
+
},
|
|
59
|
+
})).catch((err) => {
|
|
60
|
+
console.error("[kibi-opencode] startup log failed:", err);
|
|
61
|
+
});
|
|
62
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kibi-opencode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Kibi OpenCode plugin - thin adapter to integrate Kibi with OpenCode sessions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -32,9 +32,7 @@
|
|
|
32
32
|
"default": "./dist/file-filter.js"
|
|
33
33
|
}
|
|
34
34
|
},
|
|
35
|
-
"files": [
|
|
36
|
-
"dist"
|
|
37
|
-
],
|
|
35
|
+
"files": ["dist"],
|
|
38
36
|
"engines": {
|
|
39
37
|
"node": ">=18"
|
|
40
38
|
},
|