kibi-opencode 0.3.1 → 0.4.2
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 +54 -7
- package/dist/config.d.ts +16 -0
- package/dist/config.js +49 -1
- package/dist/index.d.ts +20 -7
- package/dist/index.js +111 -11
- package/dist/knowledge-classifier.d.ts +13 -0
- package/dist/knowledge-classifier.js +137 -0
- package/dist/path-kind.d.ts +7 -0
- package/dist/path-kind.js +64 -0
- package/dist/prompt.d.ts +19 -3
- package/dist/prompt.js +123 -8
- package/dist/scheduler.d.ts +15 -5
- package/dist/scheduler.js +56 -24
- package/dist/session-tracker.d.ts +49 -0
- package/dist/session-tracker.js +132 -0
- package/dist/workspace-health.d.ts +10 -0
- package/dist/workspace-health.js +39 -0
- package/package.json +21 -1
package/README.md
CHANGED
|
@@ -12,22 +12,61 @@ Or via OpenCode's plugin system in `opencode.json`:
|
|
|
12
12
|
|
|
13
13
|
```json
|
|
14
14
|
{
|
|
15
|
-
"
|
|
15
|
+
"plugin": ["kibi-opencode"]
|
|
16
16
|
}
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
## Features
|
|
20
20
|
|
|
21
|
-
###
|
|
21
|
+
### Dynamic Contextual Guidance
|
|
22
|
+
|
|
23
|
+
The plugin provides context-aware prompt guidance based on recent edits and workspace state:
|
|
24
|
+
|
|
25
|
+
- **Code edits**: Guidance for querying Kibi by sourceFile, preferring Kibi over comments, and adding `// implements REQ-xxx` traceability
|
|
26
|
+
- **Requirement edits**: Guidance for maintaining separate REQ/SCEN/TEST artifacts and avoiding embedded scenarios
|
|
27
|
+
- **KB doc edits**: Guidance for proper entity relationships and validation
|
|
28
|
+
- **Bootstrap needed**: Detection and nudges for uninitialized repos
|
|
29
|
+
|
|
30
|
+
### Targeted Validation Checks
|
|
31
|
+
|
|
32
|
+
After KB-document edits, the plugin queues targeted `kibi check` rules to run after sync:
|
|
33
|
+
|
|
34
|
+
- **Requirement/scenario/test/ADR/fact edits**: `kibi check --rules required-fields,no-dangling-refs`
|
|
35
|
+
|
|
36
|
+
Runs in background after sync completes, non-blocking. Can be disabled via `guidance.targetedChecks.enabled: false`.
|
|
22
37
|
|
|
23
|
-
|
|
38
|
+
### Loud `.kb/**` Edit Warnings
|
|
24
39
|
|
|
40
|
+
When `guidance.warnOnKbEdits` is enabled (default: `true`), manual edits to files under `.kb/**` trigger prominent warnings:
|
|
41
|
+
|
|
42
|
+
- Logs warning immediately
|
|
43
|
+
- Injects prompt guidance discouraging manual `.kb` edits
|
|
44
|
+
- Directs agents toward MCP/CLI tools (`kb_upsert`, `kb_query`, etc.)
|
|
45
|
+
|
|
46
|
+
### Session Tracking and Pattern Detection
|
|
47
|
+
|
|
48
|
+
The plugin tracks warning patterns across the session and provides periodic summaries:
|
|
49
|
+
|
|
50
|
+
- **Warning categories**: kb-edit, embedded-scenario-in-req, embedded-test-in-req, long-comment-missed-fact, missing-traceability, bootstrap-needed
|
|
51
|
+
- **Repeated pattern alerts**: Warns when the same anti-pattern occurs 3+ times
|
|
52
|
+
- **Session summaries**: Periodic logs of total warnings and top patterns (default: every 30 minutes)
|
|
53
|
+
- **Top files with warnings**: Tracks which files generate the most guidance
|
|
54
|
+
- **Requirement linting**: Detects embedded scenarios/tests in requirement files
|
|
55
|
+
|
|
56
|
+
Example session summary:
|
|
25
57
|
```
|
|
26
|
-
|
|
58
|
+
session.summary: 12 total warnings
|
|
59
|
+
kb-edit: 3
|
|
60
|
+
missing-traceability: 5
|
|
61
|
+
bootstrap-needed: 1
|
|
62
|
+
embedded-scenario-in-req: 3
|
|
63
|
+
session.patterns: Repeated anti-patterns detected:
|
|
64
|
+
missing-traceability: 5 occurrences
|
|
27
65
|
```
|
|
28
66
|
|
|
29
|
-
|
|
30
|
-
|
|
67
|
+
### Prompt Guidance Injection
|
|
68
|
+
|
|
69
|
+
The plugin injects guidance into OpenCode sessions to improve agent grounding. Uses `<!-- kibi-opencode -->` sentinel to prevent duplicate injections and respects `prompt.enabled` and overall `enabled` config flags.
|
|
31
70
|
|
|
32
71
|
### Bootstrap Command
|
|
33
72
|
|
|
@@ -64,6 +103,14 @@ Config files (project overrides global):
|
|
|
64
103
|
| `sync.debounceMs` | number | `2000` | Debounce window in milliseconds |
|
|
65
104
|
| `sync.ignore` | string[] | `[]` | Additional paths to ignore |
|
|
66
105
|
| `sync.relevant` | string[] | `[]` | Additional relevant paths |
|
|
106
|
+
| `guidance.dynamic` | boolean | `true` | Enable dynamic contextual guidance |
|
|
107
|
+
| `guidance.warnOnKbEdits` | boolean | `true` | Enable loud warnings for .kb/** edits |
|
|
108
|
+
| `guidance.factFirstDomainRouting` | boolean | `true` | Enable FACT-first domain routing suggestions |
|
|
109
|
+
| `guidance.commentDetection.enabled` | boolean | `true` | Enable comment content analysis |
|
|
110
|
+
| `guidance.commentDetection.minLines` | number | `6` | Minimum lines to trigger comment analysis |
|
|
111
|
+
| `guidance.targetedChecks.enabled` | boolean | `true` | Enable post-sync targeted validation checks |
|
|
112
|
+
| `guidance.sessionSummary.enabled` | boolean | `true` | Enable periodic session summary logs |
|
|
113
|
+
| `guidance.sessionSummary.logIntervalMs` | number | `1800000` | Session summary interval (30 min) |
|
|
67
114
|
| `logLevel` | string | `"info"` | Log level: `debug`, `info`, `warn`, `error` |
|
|
68
115
|
|
|
69
116
|
### Hook Policy
|
|
@@ -108,7 +155,7 @@ Disable specific features while keeping others:
|
|
|
108
155
|
|
|
109
156
|
## Dogfooding
|
|
110
157
|
|
|
111
|
-
This repository
|
|
158
|
+
This repository's OpenCode setup dogfoods local built artifacts. `opencode.json` starts the local `kibi-mcp` server, `.opencode/plugins/kibi.ts` re-exports `packages/opencode/dist/index.js`, and the published npm package (`kibi-opencode`) remains the distribution artifact for external consumers. See [DEV.md](DEV.md) for the repo-local workflow and rebuild rule.
|
|
112
159
|
|
|
113
160
|
## Architecture
|
|
114
161
|
|
package/dist/config.d.ts
CHANGED
|
@@ -15,6 +15,22 @@ export interface KibiConfig {
|
|
|
15
15
|
toastSuccesses: boolean;
|
|
16
16
|
toastCooldownMs: number;
|
|
17
17
|
};
|
|
18
|
+
guidance: {
|
|
19
|
+
dynamic: boolean;
|
|
20
|
+
warnOnKbEdits: boolean;
|
|
21
|
+
factFirstDomainRouting: boolean;
|
|
22
|
+
commentDetection: {
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
minLines: number;
|
|
25
|
+
};
|
|
26
|
+
targetedChecks: {
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
};
|
|
29
|
+
sessionSummary: {
|
|
30
|
+
enabled: boolean;
|
|
31
|
+
logIntervalMs: number;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
18
34
|
logLevel: string;
|
|
19
35
|
}
|
|
20
36
|
declare const DEFAULTS: KibiConfig;
|
package/dist/config.js
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import * as logger from "./logger";
|
|
4
|
+
import * as logger from "./logger.js";
|
|
5
5
|
const DEFAULTS = {
|
|
6
6
|
enabled: true,
|
|
7
7
|
prompt: { enabled: true, hookMode: "auto" },
|
|
8
8
|
sync: { enabled: true, debounceMs: 2000, ignore: [], relevant: [] },
|
|
9
9
|
ux: { toastFailures: true, toastSuccesses: false, toastCooldownMs: 10000 },
|
|
10
|
+
guidance: {
|
|
11
|
+
dynamic: true,
|
|
12
|
+
warnOnKbEdits: true,
|
|
13
|
+
factFirstDomainRouting: true,
|
|
14
|
+
commentDetection: {
|
|
15
|
+
enabled: true,
|
|
16
|
+
minLines: 6,
|
|
17
|
+
},
|
|
18
|
+
targetedChecks: {
|
|
19
|
+
enabled: true,
|
|
20
|
+
},
|
|
21
|
+
sessionSummary: {
|
|
22
|
+
enabled: true,
|
|
23
|
+
logIntervalMs: 30 * 60 * 1000, // 30 minutes
|
|
24
|
+
},
|
|
25
|
+
},
|
|
10
26
|
logLevel: "info",
|
|
11
27
|
};
|
|
12
28
|
function readJsonIfExists(filePath) {
|
|
@@ -70,6 +86,38 @@ function validateAndMerge(obj) {
|
|
|
70
86
|
}
|
|
71
87
|
if (typeof src.logLevel === "string")
|
|
72
88
|
out.logLevel = src.logLevel;
|
|
89
|
+
if (src.guidance && typeof src.guidance === "object") {
|
|
90
|
+
const g = src.guidance;
|
|
91
|
+
out.guidance = { ...DEFAULTS.guidance };
|
|
92
|
+
if (typeof g.dynamic === "boolean")
|
|
93
|
+
out.guidance.dynamic = g.dynamic;
|
|
94
|
+
if (typeof g.warnOnKbEdits === "boolean")
|
|
95
|
+
out.guidance.warnOnKbEdits = g.warnOnKbEdits;
|
|
96
|
+
if (typeof g.factFirstDomainRouting === "boolean")
|
|
97
|
+
out.guidance.factFirstDomainRouting = g.factFirstDomainRouting;
|
|
98
|
+
if (g.commentDetection && typeof g.commentDetection === "object") {
|
|
99
|
+
const cd = g.commentDetection;
|
|
100
|
+
out.guidance.commentDetection = { ...DEFAULTS.guidance.commentDetection };
|
|
101
|
+
if (typeof cd.enabled === "boolean")
|
|
102
|
+
out.guidance.commentDetection.enabled = cd.enabled;
|
|
103
|
+
if (typeof cd.minLines === "number")
|
|
104
|
+
out.guidance.commentDetection.minLines = cd.minLines;
|
|
105
|
+
}
|
|
106
|
+
if (g.targetedChecks && typeof g.targetedChecks === "object") {
|
|
107
|
+
const tc = g.targetedChecks;
|
|
108
|
+
out.guidance.targetedChecks = { ...DEFAULTS.guidance.targetedChecks };
|
|
109
|
+
if (typeof tc.enabled === "boolean")
|
|
110
|
+
out.guidance.targetedChecks.enabled = tc.enabled;
|
|
111
|
+
}
|
|
112
|
+
if (g.sessionSummary && typeof g.sessionSummary === "object") {
|
|
113
|
+
const ss = g.sessionSummary;
|
|
114
|
+
out.guidance.sessionSummary = { ...DEFAULTS.guidance.sessionSummary };
|
|
115
|
+
if (typeof ss.enabled === "boolean")
|
|
116
|
+
out.guidance.sessionSummary.enabled = ss.enabled;
|
|
117
|
+
if (typeof ss.logIntervalMs === "number")
|
|
118
|
+
out.guidance.sessionSummary.logIntervalMs = ss.logIntervalMs;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
73
121
|
return out;
|
|
74
122
|
}
|
|
75
123
|
// implements REQ-opencode-kibi-plugin-v1
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
export interface PluginInput {
|
|
2
|
+
worktree: string;
|
|
3
|
+
directory: string;
|
|
4
|
+
}
|
|
5
|
+
interface OpencodeEventPayload {
|
|
6
|
+
type: string;
|
|
7
|
+
properties?: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
interface EventHookInput {
|
|
10
|
+
event: OpencodeEventPayload;
|
|
11
|
+
}
|
|
12
|
+
interface SystemTransformOutput {
|
|
13
|
+
system: string[];
|
|
14
|
+
}
|
|
15
|
+
export interface Hooks {
|
|
16
|
+
event?: (input: EventHookInput) => void | Promise<void>;
|
|
17
|
+
"experimental.chat.system.transform"?: (input: unknown, output: SystemTransformOutput) => void | Promise<void>;
|
|
18
|
+
"chat.params"?: (input: unknown, output: unknown) => void | Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
export type Plugin = (input: PluginInput) => Hooks | Promise<Hooks>;
|
|
7
21
|
declare const kibiOpencodePlugin: Plugin;
|
|
8
22
|
export default kibiOpencodePlugin;
|
|
9
|
-
export { config, fileFilter, createSyncScheduler, injectPrompt, SENTINEL };
|
package/dist/index.js
CHANGED
|
@@ -1,18 +1,80 @@
|
|
|
1
|
-
import * as config from "./config";
|
|
2
|
-
import * as fileFilter from "./file-filter";
|
|
3
|
-
import * as logger from "./logger";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import * as config from "./config.js";
|
|
2
|
+
import * as fileFilter from "./file-filter.js";
|
|
3
|
+
import * as logger from "./logger.js";
|
|
4
|
+
import { analyzePath } from "./path-kind.js";
|
|
5
|
+
import { injectPrompt } from "./prompt.js";
|
|
6
|
+
import { createSyncScheduler } from "./scheduler.js";
|
|
7
|
+
import { getSessionTracker } from "./session-tracker.js";
|
|
8
|
+
import { checkWorkspaceHealth } from "./workspace-health.js";
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
/**
|
|
11
|
+
* Lint requirement document for anti-patterns.
|
|
12
|
+
*/
|
|
13
|
+
function lintRequirementDoc(filePath, worktree) {
|
|
14
|
+
const warnings = [];
|
|
15
|
+
try {
|
|
16
|
+
const resolvedPath = worktree && !filePath.startsWith("/")
|
|
17
|
+
? `${worktree}/${filePath}`
|
|
18
|
+
: filePath;
|
|
19
|
+
const content = fs.readFileSync(resolvedPath, "utf-8");
|
|
20
|
+
// Check for embedded scenarios (Given/When/Then patterns) - implements REQ-opencode-kibi-plugin-v1
|
|
21
|
+
if (/given\s+[\s\S]*?when\s+[\s\S]*?then/i.test(content)) {
|
|
22
|
+
warnings.push({
|
|
23
|
+
category: "embedded-scenario-in-req",
|
|
24
|
+
message: `Requirement file ${filePath} appears to contain embedded scenario (Given/When/Then). Consider extracting to a separate SCEN entity.`,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
// Check for embedded tests (assert/verify patterns)
|
|
28
|
+
if (/\b(assert|verify|expected\s+to|should\s+return)\b/i.test(content)) {
|
|
29
|
+
warnings.push({
|
|
30
|
+
category: "embedded-test-in-req",
|
|
31
|
+
message: `Requirement file ${filePath} appears to contain embedded test assertions. Consider extracting to a separate TEST entity.`,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
// Check for very long requirement that might need splitting
|
|
35
|
+
const lines = content.split("\n");
|
|
36
|
+
const contentLines = lines.filter((l) => l.trim() && !l.startsWith("---") && !l.startsWith("#"));
|
|
37
|
+
if (contentLines.length > 50) {
|
|
38
|
+
warnings.push({
|
|
39
|
+
category: "missing-traceability",
|
|
40
|
+
message: `Requirement file ${filePath} is very long (${contentLines.length} content lines). Consider splitting into multiple requirements or extracting scenarios/tests.`,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Ignore read errors
|
|
46
|
+
}
|
|
47
|
+
return warnings;
|
|
48
|
+
}
|
|
6
49
|
let scheduler = null;
|
|
7
|
-
let cfg
|
|
50
|
+
let cfg;
|
|
51
|
+
// Track recent edits for contextual guidance
|
|
52
|
+
const MAX_RECENT_EDITS = 5;
|
|
53
|
+
let recentEdits = [];
|
|
54
|
+
let hasRecentKbEdit = false;
|
|
8
55
|
// implements REQ-opencode-kibi-plugin-v1
|
|
9
56
|
const kibiOpencodePlugin = async (input) => {
|
|
10
57
|
// Load config
|
|
11
|
-
|
|
58
|
+
const loadedCfg = config.loadConfig(input.directory);
|
|
59
|
+
cfg = loadedCfg;
|
|
12
60
|
if (!cfg.enabled) {
|
|
13
61
|
logger.info("kibi-opencode: disabled via config");
|
|
14
62
|
return {};
|
|
15
63
|
}
|
|
64
|
+
// Check workspace health for bootstrap nudges
|
|
65
|
+
const workspaceHealth = checkWorkspaceHealth(input.worktree);
|
|
66
|
+
if (workspaceHealth.needsBootstrap) {
|
|
67
|
+
logger.warn("kibi-opencode: workspace needs Kibi bootstrap");
|
|
68
|
+
getSessionTracker().recordWarning("bootstrap-needed", input.worktree, "Workspace missing Kibi bootstrap");
|
|
69
|
+
}
|
|
70
|
+
// Log session summary periodically (gated on config)
|
|
71
|
+
if (cfg.guidance.sessionSummary.enabled) {
|
|
72
|
+
const tracker = getSessionTracker();
|
|
73
|
+
if (tracker.isSessionExpired(cfg.guidance.sessionSummary.logIntervalMs)) {
|
|
74
|
+
tracker.logSummary();
|
|
75
|
+
tracker.reset();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
16
78
|
logger.info("kibi-opencode: setting up hooks");
|
|
17
79
|
const hooks = {};
|
|
18
80
|
// Setup file-edit triggered sync via event hook
|
|
@@ -25,13 +87,48 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
25
87
|
hooks.event = async ({ event }) => {
|
|
26
88
|
if (event.type !== "file.edited")
|
|
27
89
|
return;
|
|
28
|
-
const filePath = event
|
|
90
|
+
const filePath = event
|
|
91
|
+
.properties.file;
|
|
29
92
|
if (!filePath)
|
|
30
93
|
return;
|
|
94
|
+
// Analyze path for tracking and classification
|
|
95
|
+
const pathAnalysis = analyzePath(filePath, input.worktree);
|
|
96
|
+
// Check for .kb edit (loud warning) — gated on guidance.warnOnKbEdits
|
|
97
|
+
if (pathAnalysis.isUnderKb && cfg.guidance.warnOnKbEdits) {
|
|
98
|
+
hasRecentKbEdit = true;
|
|
99
|
+
logger.warn(`kibi-opencode: .kb edit detected for ${filePath}`);
|
|
100
|
+
getSessionTracker().recordWarning("kb-edit", filePath, `Manual .kb edit: ${filePath}`);
|
|
101
|
+
}
|
|
102
|
+
// Lint requirement docs for anti-patterns
|
|
103
|
+
if (pathAnalysis.kind === "requirement") {
|
|
104
|
+
const lintWarnings = lintRequirementDoc(filePath, input.worktree);
|
|
105
|
+
for (const warning of lintWarnings) {
|
|
106
|
+
getSessionTracker().recordWarning(warning.category, filePath, warning.message);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Track recent edits
|
|
110
|
+
const now = Date.now();
|
|
111
|
+
recentEdits.push({
|
|
112
|
+
path: filePath,
|
|
113
|
+
kind: pathAnalysis.kind,
|
|
114
|
+
timestamp: now,
|
|
115
|
+
});
|
|
116
|
+
// Keep only recent edits
|
|
117
|
+
if (recentEdits.length > MAX_RECENT_EDITS) {
|
|
118
|
+
recentEdits = recentEdits.slice(-MAX_RECENT_EDITS);
|
|
119
|
+
}
|
|
120
|
+
// Only schedule sync for relevant files (not .kb)
|
|
31
121
|
if (!fileFilter.shouldHandleFile(filePath, input.worktree))
|
|
32
122
|
return;
|
|
123
|
+
// Determine targeted checks based on edit type (gated on guidance.targetedChecks.enabled)
|
|
124
|
+
let checkRules;
|
|
125
|
+
if (cfg.guidance.targetedChecks.enabled) {
|
|
126
|
+
if (["requirement", "scenario", "test", "adr", "fact"].includes(pathAnalysis.kind)) {
|
|
127
|
+
checkRules = ["required-fields", "no-dangling-refs"];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
33
130
|
logger.info(`kibi-opencode: scheduling sync for ${filePath}`);
|
|
34
|
-
scheduler
|
|
131
|
+
scheduler?.scheduleSync("file.edited", filePath, checkRules);
|
|
35
132
|
};
|
|
36
133
|
}
|
|
37
134
|
// Setup prompt injection hook
|
|
@@ -40,7 +137,11 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
40
137
|
if (hookMode === "system-transform" || hookMode === "auto") {
|
|
41
138
|
hooks["experimental.chat.system.transform"] = async (_input, output) => {
|
|
42
139
|
const currentSystem = output.system.join("\n");
|
|
43
|
-
const injected = injectPrompt(currentSystem, cfg
|
|
140
|
+
const injected = injectPrompt(currentSystem, cfg, {
|
|
141
|
+
recentEdits,
|
|
142
|
+
workspaceHealth,
|
|
143
|
+
hasRecentKbEdit,
|
|
144
|
+
});
|
|
44
145
|
output.system.length = 0;
|
|
45
146
|
output.system.push(injected);
|
|
46
147
|
};
|
|
@@ -61,4 +162,3 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
61
162
|
return hooks;
|
|
62
163
|
};
|
|
63
164
|
export default kibiOpencodePlugin;
|
|
64
|
-
export { config, fileFilter, createSyncScheduler, injectPrompt, SENTINEL };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heuristic classifier for routing durable knowledge prose to appropriate Kibi entity types.
|
|
3
|
+
* This is guidance-only and does not auto-create entities.
|
|
4
|
+
*/
|
|
5
|
+
export type KnowledgeSuggestion = {
|
|
6
|
+
type: "fact" | "req" | "adr" | "scenario" | "test";
|
|
7
|
+
confidence: "low" | "medium" | "high";
|
|
8
|
+
reasoning: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Analyze a comment or prose block and suggest most appropriate entity type.
|
|
12
|
+
*/
|
|
13
|
+
export declare function classifyKnowledge(text: string): KnowledgeSuggestion | null;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// implements REQ-opencode-kibi-plugin-v1
|
|
2
|
+
/**
|
|
3
|
+
* Cues for FACT entities (domain invariants, properties, limits, cardinalities)
|
|
4
|
+
*/
|
|
5
|
+
const FACT_CUES = [
|
|
6
|
+
"must be unique",
|
|
7
|
+
"at most",
|
|
8
|
+
"exactly one",
|
|
9
|
+
"default",
|
|
10
|
+
"default is",
|
|
11
|
+
"expires after",
|
|
12
|
+
"cannot exceed",
|
|
13
|
+
"maximum of",
|
|
14
|
+
"minimum of",
|
|
15
|
+
"always",
|
|
16
|
+
"never",
|
|
17
|
+
"state is",
|
|
18
|
+
"invariant",
|
|
19
|
+
"property value",
|
|
20
|
+
"cardinality",
|
|
21
|
+
"limit is",
|
|
22
|
+
"uniqueness constraint",
|
|
23
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* Cues for REQ entities (system behavior, capabilities, obligations)
|
|
26
|
+
*/
|
|
27
|
+
const REQ_CUES = [
|
|
28
|
+
"system must",
|
|
29
|
+
"user can",
|
|
30
|
+
"user should",
|
|
31
|
+
"should allow",
|
|
32
|
+
"shall",
|
|
33
|
+
"must support",
|
|
34
|
+
"capability",
|
|
35
|
+
"permission",
|
|
36
|
+
];
|
|
37
|
+
/**
|
|
38
|
+
* Cues for ADR entities (technical decisions, tradeoffs, rationale)
|
|
39
|
+
*/
|
|
40
|
+
const ADR_CUES = [
|
|
41
|
+
"decision",
|
|
42
|
+
"tradeoff",
|
|
43
|
+
"trade-off",
|
|
44
|
+
"we chose",
|
|
45
|
+
"because",
|
|
46
|
+
"rationale",
|
|
47
|
+
"constraint",
|
|
48
|
+
"architecture decision",
|
|
49
|
+
"design decision",
|
|
50
|
+
];
|
|
51
|
+
/**
|
|
52
|
+
* Cues for SCENARIO entities (behavior examples, flows)
|
|
53
|
+
*/
|
|
54
|
+
const SCENARIO_CUES = [
|
|
55
|
+
"given",
|
|
56
|
+
"then",
|
|
57
|
+
"user flow",
|
|
58
|
+
"example interaction",
|
|
59
|
+
"acceptance criteria",
|
|
60
|
+
];
|
|
61
|
+
/**
|
|
62
|
+
* Cues for TEST entities (verification language, assertions)
|
|
63
|
+
*/
|
|
64
|
+
const TEST_CUES = [
|
|
65
|
+
"verify",
|
|
66
|
+
"assert",
|
|
67
|
+
"expected",
|
|
68
|
+
"test case",
|
|
69
|
+
"asserts that",
|
|
70
|
+
"should verify",
|
|
71
|
+
];
|
|
72
|
+
/**
|
|
73
|
+
* Analyze a comment or prose block and suggest most appropriate entity type.
|
|
74
|
+
*/
|
|
75
|
+
export function classifyKnowledge(text) {
|
|
76
|
+
if (!text || text.trim().length < 50) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const lower = text.toLowerCase();
|
|
80
|
+
let bestMatch = null;
|
|
81
|
+
let maxMatches = 0;
|
|
82
|
+
function scoreMatches(cues, target) {
|
|
83
|
+
return cues.filter((cue) => lower.includes(cue)).length;
|
|
84
|
+
}
|
|
85
|
+
const factScore = scoreMatches(FACT_CUES, text);
|
|
86
|
+
const reqScore = scoreMatches(REQ_CUES, text);
|
|
87
|
+
const adrScore = scoreMatches(ADR_CUES, text);
|
|
88
|
+
const scenarioScore = scoreMatches(SCENARIO_CUES, text);
|
|
89
|
+
const testScore = scoreMatches(TEST_CUES, text);
|
|
90
|
+
// Determine the best match with some tie-breaking logic
|
|
91
|
+
// Confidence: 3+ matches = high, 1-2 matches = medium, 0 = no result
|
|
92
|
+
if (factScore > maxMatches) {
|
|
93
|
+
maxMatches = factScore;
|
|
94
|
+
bestMatch = {
|
|
95
|
+
type: "fact",
|
|
96
|
+
confidence: factScore >= 3 ? "high" : "medium",
|
|
97
|
+
reasoning: 'Contains domain invariant or property cues like "must be unique", "at most", or "default is"',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (reqScore > maxMatches) {
|
|
101
|
+
maxMatches = reqScore;
|
|
102
|
+
bestMatch = {
|
|
103
|
+
type: "req",
|
|
104
|
+
confidence: reqScore >= 3 ? "high" : "medium",
|
|
105
|
+
reasoning: 'Contains system behavior or obligation cues like "system must", "user can", or "shall"',
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
if (adrScore > maxMatches) {
|
|
109
|
+
maxMatches = adrScore;
|
|
110
|
+
bestMatch = {
|
|
111
|
+
type: "adr",
|
|
112
|
+
confidence: adrScore >= 3 ? "high" : "medium",
|
|
113
|
+
reasoning: 'Contains decision or tradeoff cues like "we chose", "because", or "constraint"',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (scenarioScore > maxMatches) {
|
|
117
|
+
maxMatches = scenarioScore;
|
|
118
|
+
bestMatch = {
|
|
119
|
+
type: "scenario",
|
|
120
|
+
confidence: scenarioScore >= 3 ? "high" : "medium",
|
|
121
|
+
reasoning: 'Contains behavior example cues like "given/when/then" or "user flow"',
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (testScore > maxMatches) {
|
|
125
|
+
maxMatches = testScore;
|
|
126
|
+
bestMatch = {
|
|
127
|
+
type: "test",
|
|
128
|
+
confidence: testScore >= 3 ? "high" : "medium",
|
|
129
|
+
reasoning: 'Contains verification cues like "verify", "assert", or "expected"',
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
// Return best match (any match with at least 1 cue is medium+ confidence)
|
|
133
|
+
if (bestMatch) {
|
|
134
|
+
return bestMatch;
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type PathKind = "code" | "requirement" | "scenario" | "test" | "adr" | "fact" | "kb" | "unknown";
|
|
2
|
+
export interface PathAnalysis {
|
|
3
|
+
kind: PathKind;
|
|
4
|
+
isUnderKb: boolean;
|
|
5
|
+
isKibiDocRelevant: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function analyzePath(filePath: string, cwd: string): PathAnalysis;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// implements REQ-opencode-kibi-plugin-v1
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const CODE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx"];
|
|
4
|
+
const KB_PREFIX = ".kb";
|
|
5
|
+
const KIBI_DOC_PATTERNS = [
|
|
6
|
+
"requirements/**",
|
|
7
|
+
"scenarios/**",
|
|
8
|
+
"tests/**",
|
|
9
|
+
"adr/**",
|
|
10
|
+
"flags/**",
|
|
11
|
+
"events/**",
|
|
12
|
+
"facts/**",
|
|
13
|
+
"symbols.yaml",
|
|
14
|
+
];
|
|
15
|
+
export function analyzePath(filePath, cwd) {
|
|
16
|
+
const rel = path.isAbsolute(filePath)
|
|
17
|
+
? path.relative(cwd, filePath).split(path.sep).join("/")
|
|
18
|
+
: filePath.split(path.sep).join("/");
|
|
19
|
+
let kind = "unknown";
|
|
20
|
+
let isUnderKb = false;
|
|
21
|
+
let isKibiDocRelevant = false;
|
|
22
|
+
// Check if under .kb/**
|
|
23
|
+
if (rel.startsWith(`${KB_PREFIX}/`)) {
|
|
24
|
+
kind = "kb";
|
|
25
|
+
isUnderKb = true;
|
|
26
|
+
}
|
|
27
|
+
// Check for Kibi doc paths
|
|
28
|
+
const normalized = rel.toLowerCase();
|
|
29
|
+
for (const pattern of KIBI_DOC_PATTERNS) {
|
|
30
|
+
const patternPrefix = pattern.replace(/\*\*/g, "");
|
|
31
|
+
const fullPathPattern = `documentation/${patternPrefix}`;
|
|
32
|
+
if (normalized.startsWith(fullPathPattern)) {
|
|
33
|
+
isKibiDocRelevant = true;
|
|
34
|
+
if (kind === "unknown") {
|
|
35
|
+
// Map to specific kind based on path
|
|
36
|
+
if (patternPrefix.includes("requirements"))
|
|
37
|
+
kind = "requirement";
|
|
38
|
+
else if (patternPrefix.includes("scenarios"))
|
|
39
|
+
kind = "scenario";
|
|
40
|
+
else if (patternPrefix.includes("tests"))
|
|
41
|
+
kind = "test";
|
|
42
|
+
else if (patternPrefix.includes("adr"))
|
|
43
|
+
kind = "adr";
|
|
44
|
+
else if (patternPrefix.includes("facts"))
|
|
45
|
+
kind = "fact";
|
|
46
|
+
else if (patternPrefix.includes("events"))
|
|
47
|
+
kind = "fact"; // events map to fact for routing
|
|
48
|
+
else if (patternPrefix.includes("flags"))
|
|
49
|
+
kind = "fact"; // flags map to fact for routing
|
|
50
|
+
else if (patternPrefix.includes("symbols"))
|
|
51
|
+
kind = "fact";
|
|
52
|
+
}
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Check for code files
|
|
57
|
+
if (kind === "unknown") {
|
|
58
|
+
const ext = path.extname(rel).toLowerCase();
|
|
59
|
+
if (CODE_EXTENSIONS.includes(ext)) {
|
|
60
|
+
kind = "code";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { kind, isUnderKb, isKibiDocRelevant };
|
|
64
|
+
}
|
package/dist/prompt.d.ts
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
|
-
import type { KibiConfig } from "./config";
|
|
1
|
+
import type { KibiConfig } from "./config.js";
|
|
2
|
+
import type { PathKind } from "./path-kind.js";
|
|
3
|
+
import type { WorkspaceHealth } from "./workspace-health.js";
|
|
2
4
|
declare const SENTINEL = "<!-- kibi-opencode -->";
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
+
export interface PromptContext {
|
|
6
|
+
recentEdits: Array<{
|
|
7
|
+
path: string;
|
|
8
|
+
kind: PathKind;
|
|
9
|
+
}>;
|
|
10
|
+
workspaceHealth?: WorkspaceHealth;
|
|
11
|
+
hasRecentKbEdit?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Build prompt with contextual guidance based on recent edits and workspace state.
|
|
15
|
+
*/
|
|
16
|
+
export declare function buildPrompt(context?: PromptContext): string;
|
|
17
|
+
/**
|
|
18
|
+
* Inject prompt guidance if not already present.
|
|
19
|
+
*/
|
|
20
|
+
export declare function injectPrompt(current: string, config: KibiConfig, context?: PromptContext): string;
|
|
5
21
|
export { SENTINEL };
|
package/dist/prompt.js
CHANGED
|
@@ -1,6 +1,112 @@
|
|
|
1
|
-
import { isPluginEnabled } from "./config";
|
|
1
|
+
import { isPluginEnabled } from "./config.js";
|
|
2
2
|
const SENTINEL = "<!-- kibi-opencode -->";
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Build prompt guidance block based on path kind.
|
|
5
|
+
*/
|
|
6
|
+
// implements REQ-opencode-kibi-plugin-v1
|
|
7
|
+
function buildContextualGuidance(context) {
|
|
8
|
+
const parts = [SENTINEL];
|
|
9
|
+
// 1. Check for recent .kb edits (loud warning)
|
|
10
|
+
if (context.hasRecentKbEdit) {
|
|
11
|
+
parts.push(`
|
|
12
|
+
⚠️ **WARNING: Do not edit .kb/** files manually.**
|
|
13
|
+
|
|
14
|
+
The Kibi knowledge base is managed through MCP and CLI tools. Direct manual edits to files under .kb/** can cause inconsistencies and should be avoided.
|
|
15
|
+
|
|
16
|
+
Instead:
|
|
17
|
+
- Use kb_upsert to create/update entities
|
|
18
|
+
- Use kb_query to inspect the KB
|
|
19
|
+
- Use kb_check to validate consistency
|
|
20
|
+
`);
|
|
21
|
+
}
|
|
22
|
+
// 2. Check for bootstrap/health issues
|
|
23
|
+
if (context.workspaceHealth?.needsBootstrap) {
|
|
24
|
+
parts.push(`
|
|
25
|
+
🔧 **Bootstrap required**
|
|
26
|
+
|
|
27
|
+
This repository does not appear to have Kibi initialized. Consider running:
|
|
28
|
+
- \`/init-kibi\` for retroactive bootstrap of existing repos
|
|
29
|
+
- \`kibi init\` for new repos
|
|
30
|
+
- \`kibi doctor\` to verify your environment
|
|
31
|
+
`);
|
|
32
|
+
}
|
|
33
|
+
// 3. Analyze recent edits and provide targeted guidance
|
|
34
|
+
const codeEdits = context.recentEdits.filter((e) => e.kind === "code");
|
|
35
|
+
const reqEdits = context.recentEdits.filter((e) => e.kind === "requirement");
|
|
36
|
+
const kbDocEdits = context.recentEdits.filter((e) => ["requirement", "scenario", "test", "adr", "fact"].includes(e.kind));
|
|
37
|
+
// Code edit guidance
|
|
38
|
+
if (codeEdits.length > 0) {
|
|
39
|
+
parts.push(`
|
|
40
|
+
📝 **Code changes detected**
|
|
41
|
+
|
|
42
|
+
Before implementing or explaining code:
|
|
43
|
+
1. **Query Kibi first** - Run kb_query by sourceFile to find related requirements, ADRs, tests, and symbols.
|
|
44
|
+
2. **Prefer Kibi over comments** - Store durable knowledge in KB entities instead of inline comments.
|
|
45
|
+
3. **Add traceability** - Add \`// implements REQ-xxx\` to every new or modified function/class so the pre-commit hook can verify coverage.
|
|
46
|
+
|
|
47
|
+
If you're adding long explanatory comments, consider routing that knowledge to:
|
|
48
|
+
- \`FACT\` for domain invariants, properties, limits, cardinalities
|
|
49
|
+
- \`ADR\` for technical decisions, tradeoffs, rationale
|
|
50
|
+
- \`REQ\` for system behavior requirements
|
|
51
|
+
- \`SCEN\` for behavior examples and flows
|
|
52
|
+
- \`TEST\` for verification intent
|
|
53
|
+
`);
|
|
54
|
+
}
|
|
55
|
+
// Requirement edit guidance
|
|
56
|
+
if (reqEdits.length > 0) {
|
|
57
|
+
parts.push(`
|
|
58
|
+
📋 **Requirement changes detected**
|
|
59
|
+
|
|
60
|
+
When editing requirements:
|
|
61
|
+
1. **Keep artifacts separate** - Do not embed scenarios or tests inside requirement files.
|
|
62
|
+
2. **Add verification** - Create or update linked \`SCEN\` and \`TEST\` entities.
|
|
63
|
+
3. **Check coverage** - For \`priority: must\` requirements, ensure both scenario and test coverage.
|
|
64
|
+
|
|
65
|
+
Preferred structure:
|
|
66
|
+
- \`REQ-xxx.md\` contains the requirement statement
|
|
67
|
+
- \`SCEN-xxx.md\` specifies behavior via Given/When/Then
|
|
68
|
+
- \`TEST-xxx.md\` verifies the requirement
|
|
69
|
+
`);
|
|
70
|
+
}
|
|
71
|
+
// KB doc edit guidance (requirement, scenario, test, ADR, fact)
|
|
72
|
+
if (kbDocEdits.length > 0 && reqEdits.length === 0) {
|
|
73
|
+
parts.push(`
|
|
74
|
+
📚 **Kibi documentation changes detected**
|
|
75
|
+
|
|
76
|
+
When editing KB documentation:
|
|
77
|
+
1. **Maintain traceability** - Link entities using relationships: specified_by (req→scenario), verified_by (req→test), etc.
|
|
78
|
+
2. **Validate** - Run \`kibi check\` after making changes to catch integrity issues.
|
|
79
|
+
3. **Follow entity patterns** - Ensure each entity has proper frontmatter with required fields.
|
|
80
|
+
`);
|
|
81
|
+
}
|
|
82
|
+
// Only include general Kibi workflow if no specific context (beyond the sentinel)
|
|
83
|
+
if (parts.length === 1) {
|
|
84
|
+
parts.push(`This project uses Kibi (via MCP). Prefer storing durable knowledge in Kibi over code comments.
|
|
85
|
+
|
|
86
|
+
Before changing behavior: query Kibi by sourceFile, id, type, or tags; do not rely on undocumented tools.
|
|
87
|
+
|
|
88
|
+
Keep changed symbols traceable: add \`// implements REQ-xxx\` to every new or modified function/class so the pre-commit hook can verify coverage.
|
|
89
|
+
|
|
90
|
+
Run kb_check after KB mutations.
|
|
91
|
+
|
|
92
|
+
Dogfood note for this repo: OpenCode here uses local built \`kibi-mcp\` and \`kibi-opencode\` artifacts. If you change package versions or local package wiring, run \`bun run build\` before relying on OpenCode in this workspace.
|
|
93
|
+
|
|
94
|
+
**Kibi-first workflow:**
|
|
95
|
+
1. **Discover**: Run kb_query with filters (sourceFile, type, tags) to find related requirements, ADRs, tests, and symbols.
|
|
96
|
+
2. **Document intent**: If you are about to explain code, STOP. Route that explanation to kb_upsert instead of inline comments.
|
|
97
|
+
3. **Link during work**: When creating KB entities, include relationship rows: specified_by (req→scenario), verified_by (req→test), implements (symbol→req), covered_by (symbol→test).
|
|
98
|
+
4. **Validate**: Run kb_check after KB mutations to catch violations early.
|
|
99
|
+
|
|
100
|
+
**Public Kibi tools only:** kb_query, kb_upsert, kb_delete, kb_check.
|
|
101
|
+
|
|
102
|
+
Bootstrap existing repos: use \`/init-kibi\` to run the retroactive initialization workflow.`);
|
|
103
|
+
}
|
|
104
|
+
return parts.join("\n\n").trim();
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Build the static guidance block (original behavior).
|
|
108
|
+
*/
|
|
109
|
+
const BASE_GUIDANCE = `${SENTINEL}
|
|
4
110
|
This project uses Kibi (via MCP). Prefer storing durable knowledge in Kibi over code comments.
|
|
5
111
|
|
|
6
112
|
Before changing behavior: query Kibi by sourceFile, id, type, or tags; do not rely on undocumented tools.
|
|
@@ -9,6 +115,8 @@ Keep changed symbols traceable: add \`// implements REQ-xxx\` to every new or mo
|
|
|
9
115
|
|
|
10
116
|
Run kb_check after KB mutations.
|
|
11
117
|
|
|
118
|
+
Dogfood note for this repo: OpenCode here uses local built \`kibi-mcp\` and \`kibi-opencode\` artifacts. If you change package versions or local package wiring, run \`bun run build\` before relying on OpenCode in this workspace.
|
|
119
|
+
|
|
12
120
|
**Kibi-first workflow:**
|
|
13
121
|
1. **Discover**: Run kb_query with filters (sourceFile, type, tags) to find related requirements, ADRs, tests, and symbols.
|
|
14
122
|
2. **Document intent**: If you are about to explain code, STOP. Route that explanation to kb_upsert instead of inline comments.
|
|
@@ -18,18 +126,25 @@ Run kb_check after KB mutations.
|
|
|
18
126
|
**Public Kibi tools only:** kb_query, kb_upsert, kb_delete, kb_check.
|
|
19
127
|
|
|
20
128
|
Bootstrap existing repos: use \`/init-kibi\` to run the retroactive initialization workflow.`;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
129
|
+
/**
|
|
130
|
+
* Build prompt with contextual guidance based on recent edits and workspace state.
|
|
131
|
+
*/
|
|
132
|
+
export function buildPrompt(context) {
|
|
133
|
+
if (!context) {
|
|
134
|
+
return BASE_GUIDANCE.trim();
|
|
135
|
+
}
|
|
136
|
+
return buildContextualGuidance(context).trim();
|
|
24
137
|
}
|
|
25
|
-
|
|
26
|
-
|
|
138
|
+
/**
|
|
139
|
+
* Inject prompt guidance if not already present.
|
|
140
|
+
*/
|
|
141
|
+
export function injectPrompt(current, config, context) {
|
|
27
142
|
if (!config.prompt.enabled || !isPluginEnabled(config)) {
|
|
28
143
|
return current;
|
|
29
144
|
}
|
|
30
145
|
if (current.includes(SENTINEL)) {
|
|
31
146
|
return current;
|
|
32
147
|
}
|
|
33
|
-
return `${current}\n\n${buildPrompt()}`;
|
|
148
|
+
return `${current}\n\n${buildPrompt(context)}`;
|
|
34
149
|
}
|
|
35
150
|
export { SENTINEL };
|
package/dist/scheduler.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { KibiConfig } from "./config";
|
|
2
|
-
type TimeoutHandle = ReturnType<typeof setTimeout>;
|
|
1
|
+
import type { KibiConfig } from "./config.js";
|
|
2
|
+
export type TimeoutHandle = ReturnType<typeof setTimeout>;
|
|
3
3
|
export interface SyncRunMetadata {
|
|
4
4
|
reason: string;
|
|
5
5
|
worktree: string;
|
|
@@ -7,25 +7,35 @@ export interface SyncRunMetadata {
|
|
|
7
7
|
debounceWindowMs: number;
|
|
8
8
|
durationMs: number;
|
|
9
9
|
exitCode: number;
|
|
10
|
+
checkExitCode?: number;
|
|
11
|
+
checkRules?: string[];
|
|
10
12
|
}
|
|
11
|
-
type SyncRunner = (worktree: string) => Promise<{
|
|
13
|
+
export type SyncRunner = (worktree: string) => Promise<{
|
|
14
|
+
exitCode: number;
|
|
15
|
+
}>;
|
|
16
|
+
export type CheckRunner = (worktree: string, rules: string[]) => Promise<{
|
|
12
17
|
exitCode: number;
|
|
13
18
|
}>;
|
|
14
19
|
export interface SchedulerOptions {
|
|
15
20
|
worktree: string;
|
|
16
21
|
config: KibiConfig;
|
|
17
22
|
runSync?: SyncRunner;
|
|
23
|
+
runCheck?: CheckRunner;
|
|
18
24
|
now?: () => number;
|
|
19
25
|
setTimeoutFn?: (fn: () => void, ms: number) => TimeoutHandle;
|
|
20
26
|
clearTimeoutFn?: (handle: TimeoutHandle) => void;
|
|
21
27
|
onRunComplete?: (meta: SyncRunMetadata) => void;
|
|
22
28
|
enableToolExecuteAfterHint?: boolean;
|
|
23
29
|
}
|
|
30
|
+
export type PendingTrigger = {
|
|
31
|
+
reason: string;
|
|
32
|
+
filePath?: string;
|
|
33
|
+
checkRules?: string[];
|
|
34
|
+
};
|
|
24
35
|
export interface SyncScheduler {
|
|
25
|
-
scheduleSync(reason: string, filePath?: string): void;
|
|
36
|
+
scheduleSync(reason: string, filePath?: string, checkRules?: string[]): void;
|
|
26
37
|
onFileEdited(filePath: string): void;
|
|
27
38
|
onToolExecuteAfter(reason?: string): void;
|
|
28
39
|
dispose(): void;
|
|
29
40
|
}
|
|
30
41
|
export declare function createSyncScheduler(opts: SchedulerOptions): SyncScheduler;
|
|
31
|
-
export {};
|
package/dist/scheduler.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { exec } from "node:child_process";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { shouldHandleFile } from "./file-filter";
|
|
4
|
-
import * as logger from "./logger";
|
|
3
|
+
import { shouldHandleFile } from "./file-filter.js";
|
|
4
|
+
import * as logger from "./logger.js";
|
|
5
5
|
class WorktreeSyncScheduler {
|
|
6
6
|
worktree;
|
|
7
7
|
now;
|
|
8
8
|
setTimeoutFn;
|
|
9
9
|
clearTimeoutFn;
|
|
10
10
|
runSync;
|
|
11
|
+
runCheck;
|
|
11
12
|
config;
|
|
12
13
|
onRunComplete;
|
|
13
14
|
explicitToolAfterHint;
|
|
@@ -24,10 +25,11 @@ class WorktreeSyncScheduler {
|
|
|
24
25
|
this.setTimeoutFn = opts.setTimeoutFn ?? setTimeout;
|
|
25
26
|
this.clearTimeoutFn = opts.clearTimeoutFn ?? clearTimeout;
|
|
26
27
|
this.runSync = opts.runSync ?? runKibiSync;
|
|
28
|
+
this.runCheck = opts.runCheck ?? runKibiCheck;
|
|
27
29
|
this.onRunComplete = opts.onRunComplete;
|
|
28
30
|
this.explicitToolAfterHint = Boolean(opts.enableToolExecuteAfterHint);
|
|
29
31
|
}
|
|
30
|
-
scheduleSync(reason, filePath) {
|
|
32
|
+
scheduleSync(reason, filePath, checkRules) {
|
|
31
33
|
if (!this.config.sync.enabled)
|
|
32
34
|
return;
|
|
33
35
|
if (reason === "file.edited") {
|
|
@@ -37,7 +39,7 @@ class WorktreeSyncScheduler {
|
|
|
37
39
|
return;
|
|
38
40
|
this.lastFileEditedAt = this.now();
|
|
39
41
|
}
|
|
40
|
-
this.pending = { reason, filePath };
|
|
42
|
+
this.pending = { reason, filePath, checkRules };
|
|
41
43
|
if (this.timer)
|
|
42
44
|
this.clearTimeoutFn(this.timer);
|
|
43
45
|
this.timer = this.setTimeoutFn(() => {
|
|
@@ -88,7 +90,7 @@ class WorktreeSyncScheduler {
|
|
|
88
90
|
}
|
|
89
91
|
this.startRun(trigger);
|
|
90
92
|
}
|
|
91
|
-
startRun(trigger) {
|
|
93
|
+
async startRun(trigger) {
|
|
92
94
|
this.inFlight = true;
|
|
93
95
|
const startedAt = this.now();
|
|
94
96
|
logger.info(`sync.started ${JSON.stringify({
|
|
@@ -97,29 +99,49 @@ class WorktreeSyncScheduler {
|
|
|
97
99
|
filePath: trigger.filePath,
|
|
98
100
|
debounceWindowMs: this.config.sync.debounceMs,
|
|
99
101
|
})}`);
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
.
|
|
102
|
+
let syncExitCode = 0;
|
|
103
|
+
let checkExitCode;
|
|
104
|
+
let checkRules;
|
|
105
|
+
try {
|
|
106
|
+
const syncResult = await this.runSync(this.worktree);
|
|
107
|
+
syncExitCode = syncResult.exitCode;
|
|
108
|
+
// Run targeted checks if sync succeeded and rules specified
|
|
109
|
+
if (syncExitCode === 0 &&
|
|
110
|
+
trigger.checkRules &&
|
|
111
|
+
trigger.checkRules.length > 0) {
|
|
112
|
+
checkRules = trigger.checkRules;
|
|
113
|
+
logger.info(`check.started ${JSON.stringify({ rules: checkRules })}`);
|
|
114
|
+
const checkResult = await this.runCheck(this.worktree, checkRules);
|
|
115
|
+
checkExitCode = checkResult.exitCode;
|
|
116
|
+
if (checkExitCode !== 0) {
|
|
117
|
+
logger.warn(`check.failed ${JSON.stringify({ rules: checkRules, exitCode: checkExitCode })}`);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
logger.info(`check.succeeded ${JSON.stringify({ rules: checkRules })}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
105
125
|
const message = err instanceof Error ? err.message : String(err);
|
|
106
126
|
logger.error(`sync.failed ${message}`);
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
|
|
127
|
+
syncExitCode = 1;
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
this.emitCompletion(trigger, startedAt, syncExitCode, checkExitCode, checkRules);
|
|
110
131
|
this.inFlight = false;
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
132
|
+
if (this.dirty) {
|
|
133
|
+
const trailing = this.trailing ?? { reason: "sync.trailing" };
|
|
134
|
+
this.dirty = false;
|
|
135
|
+
this.trailing = null;
|
|
136
|
+
void this.startRun({
|
|
137
|
+
reason: `${trailing.reason}.trailing`,
|
|
138
|
+
filePath: trailing.filePath,
|
|
139
|
+
checkRules: trailing.checkRules,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
121
143
|
}
|
|
122
|
-
emitCompletion(trigger, startedAt, exitCode) {
|
|
144
|
+
emitCompletion(trigger, startedAt, exitCode, checkExitCode, checkRules) {
|
|
123
145
|
const durationMs = Math.max(0, this.now() - startedAt);
|
|
124
146
|
const meta = {
|
|
125
147
|
reason: trigger.reason,
|
|
@@ -128,6 +150,8 @@ class WorktreeSyncScheduler {
|
|
|
128
150
|
debounceWindowMs: this.config.sync.debounceMs,
|
|
129
151
|
durationMs,
|
|
130
152
|
exitCode,
|
|
153
|
+
checkExitCode,
|
|
154
|
+
checkRules,
|
|
131
155
|
};
|
|
132
156
|
if (exitCode === 0) {
|
|
133
157
|
logger.info(`sync.succeeded ${JSON.stringify(meta)}`);
|
|
@@ -145,6 +169,14 @@ async function runKibiSync(worktree) {
|
|
|
145
169
|
});
|
|
146
170
|
});
|
|
147
171
|
}
|
|
172
|
+
async function runKibiCheck(worktree, rules) {
|
|
173
|
+
return new Promise((resolve) => {
|
|
174
|
+
const rulesArg = rules.join(",");
|
|
175
|
+
exec(`kibi check --rules ${rulesArg}`, { cwd: worktree }, (error) => {
|
|
176
|
+
resolve({ exitCode: error ? (error.code ?? 1) : 0 });
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
}
|
|
148
180
|
// implements REQ-opencode-kibi-plugin-v1
|
|
149
181
|
export function createSyncScheduler(opts) {
|
|
150
182
|
return new WorktreeSyncScheduler(opts);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export type WarningCategory = "kb-edit" | "embedded-scenario-in-req" | "embedded-test-in-req" | "long-comment-missed-fact" | "long-comment-missed-adr" | "missing-traceability" | "bootstrap-needed";
|
|
2
|
+
export interface WarningEvent {
|
|
3
|
+
category: WarningCategory;
|
|
4
|
+
path: string;
|
|
5
|
+
timestamp: number;
|
|
6
|
+
message: string;
|
|
7
|
+
}
|
|
8
|
+
export interface SessionSummary {
|
|
9
|
+
totalWarnings: number;
|
|
10
|
+
warningsByCategory: Record<WarningCategory, number>;
|
|
11
|
+
repeatedPatterns: Array<{
|
|
12
|
+
category: WarningCategory;
|
|
13
|
+
count: number;
|
|
14
|
+
}>;
|
|
15
|
+
topFilesWithWarnings: Array<{
|
|
16
|
+
path: string;
|
|
17
|
+
count: number;
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
declare class SessionTracker {
|
|
21
|
+
private warnings;
|
|
22
|
+
private sessionStart;
|
|
23
|
+
constructor();
|
|
24
|
+
/**
|
|
25
|
+
* Record a warning event.
|
|
26
|
+
*/
|
|
27
|
+
recordWarning(category: WarningCategory, path: string, message: string): void;
|
|
28
|
+
/**
|
|
29
|
+
* Generate a session summary of warnings and patterns.
|
|
30
|
+
*/
|
|
31
|
+
generateSummary(): SessionSummary;
|
|
32
|
+
/**
|
|
33
|
+
* Log a summary at session end or on demand.
|
|
34
|
+
*/
|
|
35
|
+
logSummary(): void;
|
|
36
|
+
/**
|
|
37
|
+
* Reset the session.
|
|
38
|
+
*/
|
|
39
|
+
reset(): void;
|
|
40
|
+
/**
|
|
41
|
+
* Check if session has expired.
|
|
42
|
+
*/
|
|
43
|
+
isSessionExpired(intervalMs?: number): boolean;
|
|
44
|
+
private logWarning;
|
|
45
|
+
private checkRepeatedPattern;
|
|
46
|
+
}
|
|
47
|
+
export declare function getSessionTracker(): SessionTracker;
|
|
48
|
+
export declare function resetSessionTracker(): void;
|
|
49
|
+
export { SessionTracker };
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// implements REQ-opencode-kibi-plugin-v1
|
|
2
|
+
import * as logger from "./logger.js";
|
|
3
|
+
const WARNING_THRESHOLD_REPEAT = 3;
|
|
4
|
+
const SESSION_DURATION_MS = 30 * 60 * 1000; // 30 minutes
|
|
5
|
+
class SessionTracker {
|
|
6
|
+
warnings = [];
|
|
7
|
+
sessionStart;
|
|
8
|
+
constructor() {
|
|
9
|
+
this.sessionStart = Date.now();
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Record a warning event.
|
|
13
|
+
*/
|
|
14
|
+
recordWarning(category, path, message) {
|
|
15
|
+
const event = {
|
|
16
|
+
category,
|
|
17
|
+
path,
|
|
18
|
+
timestamp: Date.now(),
|
|
19
|
+
message,
|
|
20
|
+
};
|
|
21
|
+
this.warnings.push(event);
|
|
22
|
+
// Log with category prefix for richer filtering
|
|
23
|
+
this.logWarning(category, message);
|
|
24
|
+
// Check for repeated patterns
|
|
25
|
+
this.checkRepeatedPattern(category);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Generate a session summary of warnings and patterns.
|
|
29
|
+
*/
|
|
30
|
+
generateSummary() {
|
|
31
|
+
const byCategory = {
|
|
32
|
+
"kb-edit": 0,
|
|
33
|
+
"embedded-scenario-in-req": 0,
|
|
34
|
+
"embedded-test-in-req": 0,
|
|
35
|
+
"long-comment-missed-fact": 0,
|
|
36
|
+
"long-comment-missed-adr": 0,
|
|
37
|
+
"missing-traceability": 0,
|
|
38
|
+
"bootstrap-needed": 0,
|
|
39
|
+
};
|
|
40
|
+
const byFile = {};
|
|
41
|
+
for (const warning of this.warnings) {
|
|
42
|
+
byCategory[warning.category]++;
|
|
43
|
+
byFile[warning.path] = (byFile[warning.path] || 0) + 1;
|
|
44
|
+
}
|
|
45
|
+
const repeatedPatterns = Object.entries(byCategory)
|
|
46
|
+
.filter(([, count]) => count >= WARNING_THRESHOLD_REPEAT)
|
|
47
|
+
.map(([category, count]) => ({
|
|
48
|
+
category: category,
|
|
49
|
+
count,
|
|
50
|
+
}))
|
|
51
|
+
.sort((a, b) => b.count - a.count);
|
|
52
|
+
const topFiles = Object.entries(byFile)
|
|
53
|
+
.sort((a, b) => b[1] - a[1])
|
|
54
|
+
.slice(0, 5)
|
|
55
|
+
.map(([path, count]) => ({ path, count }));
|
|
56
|
+
return {
|
|
57
|
+
totalWarnings: this.warnings.length,
|
|
58
|
+
warningsByCategory: byCategory,
|
|
59
|
+
repeatedPatterns,
|
|
60
|
+
topFilesWithWarnings: topFiles,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Log a summary at session end or on demand.
|
|
65
|
+
*/
|
|
66
|
+
logSummary() {
|
|
67
|
+
const summary = this.generateSummary();
|
|
68
|
+
if (summary.totalWarnings === 0) {
|
|
69
|
+
logger.info("session.summary: No warnings recorded");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
logger.info(`session.summary: ${summary.totalWarnings} total warnings`);
|
|
73
|
+
for (const [category, count] of Object.entries(summary.warningsByCategory)) {
|
|
74
|
+
if (count > 0) {
|
|
75
|
+
logger.info(` ${category}: ${count}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (summary.repeatedPatterns.length > 0) {
|
|
79
|
+
logger.warn("session.patterns: Repeated anti-patterns detected:");
|
|
80
|
+
for (const pattern of summary.repeatedPatterns) {
|
|
81
|
+
logger.warn(` ${pattern.category}: ${pattern.count} occurrences`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Reset the session.
|
|
87
|
+
*/
|
|
88
|
+
reset() {
|
|
89
|
+
this.warnings = [];
|
|
90
|
+
this.sessionStart = Date.now();
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Check if session has expired.
|
|
94
|
+
*/
|
|
95
|
+
isSessionExpired(intervalMs = SESSION_DURATION_MS) {
|
|
96
|
+
return Date.now() - this.sessionStart > intervalMs;
|
|
97
|
+
}
|
|
98
|
+
logWarning(category, message) {
|
|
99
|
+
const prefix = `[${category}]`;
|
|
100
|
+
switch (category) {
|
|
101
|
+
case "kb-edit":
|
|
102
|
+
logger.warn(`${prefix} ${message}`);
|
|
103
|
+
break;
|
|
104
|
+
case "bootstrap-needed":
|
|
105
|
+
logger.warn(`${prefix} ${message}`);
|
|
106
|
+
break;
|
|
107
|
+
case "missing-traceability":
|
|
108
|
+
logger.info(`${prefix} ${message}`);
|
|
109
|
+
break;
|
|
110
|
+
default:
|
|
111
|
+
logger.info(`${prefix} ${message}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
checkRepeatedPattern(category) {
|
|
115
|
+
const count = this.warnings.filter((w) => w.category === category).length;
|
|
116
|
+
if (count === WARNING_THRESHOLD_REPEAT) {
|
|
117
|
+
logger.warn(`pattern.repeat: ${category} has occurred ${count} times. Consider addressing this pattern systematically.`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Singleton instance
|
|
122
|
+
let globalTracker = null;
|
|
123
|
+
export function getSessionTracker() {
|
|
124
|
+
if (!globalTracker) {
|
|
125
|
+
globalTracker = new SessionTracker();
|
|
126
|
+
}
|
|
127
|
+
return globalTracker;
|
|
128
|
+
}
|
|
129
|
+
export function resetSessionTracker() {
|
|
130
|
+
globalTracker = new SessionTracker();
|
|
131
|
+
}
|
|
132
|
+
export { SessionTracker };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface WorkspaceHealth {
|
|
2
|
+
needsBootstrap: boolean;
|
|
3
|
+
missingConfig: boolean;
|
|
4
|
+
missingDocDirs: string[];
|
|
5
|
+
hasKbEvidence: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Analyze workspace health for Kibi bootstrap and initialization.
|
|
9
|
+
*/
|
|
10
|
+
export declare function checkWorkspaceHealth(cwd: string): WorkspaceHealth;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
// implements REQ-opencode-kibi-plugin-v1
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const KB_CONFIG_FILE = ".kb/config.json";
|
|
5
|
+
const KIBI_DOC_DIRS = [
|
|
6
|
+
"documentation/requirements",
|
|
7
|
+
"documentation/scenarios",
|
|
8
|
+
"documentation/tests",
|
|
9
|
+
"documentation/adr",
|
|
10
|
+
"documentation/flags",
|
|
11
|
+
"documentation/events",
|
|
12
|
+
"documentation/facts",
|
|
13
|
+
"documentation/symbols.yaml",
|
|
14
|
+
];
|
|
15
|
+
/**
|
|
16
|
+
* Analyze workspace health for Kibi bootstrap and initialization.
|
|
17
|
+
*/
|
|
18
|
+
export function checkWorkspaceHealth(cwd) {
|
|
19
|
+
const configPath = path.join(cwd, KB_CONFIG_FILE);
|
|
20
|
+
const missingConfig = !fs.existsSync(configPath);
|
|
21
|
+
const missingDocDirs = [];
|
|
22
|
+
for (const docDir of KIBI_DOC_DIRS) {
|
|
23
|
+
const fullPath = path.join(cwd, docDir);
|
|
24
|
+
if (!fs.existsSync(fullPath)) {
|
|
25
|
+
missingDocDirs.push(docDir);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Check for any evidence of Kibi usage
|
|
29
|
+
const kbDir = path.join(cwd, ".kb");
|
|
30
|
+
const hasKbEvidence = fs.existsSync(kbDir) && fs.readdirSync(kbDir).length > 0;
|
|
31
|
+
// If missing config or more than 2 doc dirs are missing, suggest bootstrap
|
|
32
|
+
const needsBootstrap = missingConfig || missingDocDirs.length > 2;
|
|
33
|
+
return {
|
|
34
|
+
needsBootstrap,
|
|
35
|
+
missingConfig,
|
|
36
|
+
missingDocDirs,
|
|
37
|
+
hasKbEvidence,
|
|
38
|
+
};
|
|
39
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kibi-opencode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Kibi OpenCode plugin - thin adapter to integrate Kibi with OpenCode sessions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -10,6 +10,26 @@
|
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
11
|
"import": "./dist/index.js",
|
|
12
12
|
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./config": {
|
|
15
|
+
"types": "./dist/config.d.ts",
|
|
16
|
+
"import": "./dist/config.js",
|
|
17
|
+
"default": "./dist/config.js"
|
|
18
|
+
},
|
|
19
|
+
"./prompt": {
|
|
20
|
+
"types": "./dist/prompt.d.ts",
|
|
21
|
+
"import": "./dist/prompt.js",
|
|
22
|
+
"default": "./dist/prompt.js"
|
|
23
|
+
},
|
|
24
|
+
"./scheduler": {
|
|
25
|
+
"types": "./dist/scheduler.d.ts",
|
|
26
|
+
"import": "./dist/scheduler.js",
|
|
27
|
+
"default": "./dist/scheduler.js"
|
|
28
|
+
},
|
|
29
|
+
"./file-filter": {
|
|
30
|
+
"types": "./dist/file-filter.d.ts",
|
|
31
|
+
"import": "./dist/file-filter.js",
|
|
32
|
+
"default": "./dist/file-filter.js"
|
|
13
33
|
}
|
|
14
34
|
},
|
|
15
35
|
"files": [
|