kibi-opencode 0.19.4 → 2.0.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 +8 -6
- package/dist/auto-update.d.ts +2 -0
- package/dist/auto-update.js +5 -4
- package/dist/e2e-coverage-signals.js +3 -3
- package/dist/enforcement-policy.d.ts +10 -0
- package/dist/enforcement-policy.js +23 -23
- package/dist/file-entity-links.d.ts +4 -0
- package/dist/file-entity-links.js +6 -4
- package/dist/file-filter.d.ts +9 -9
- package/dist/file-filter.js +16 -68
- package/dist/kibi-bootstrap-alias.d.ts +2 -0
- package/dist/kibi-bootstrap-alias.js +14 -0
- package/dist/kibi-bootstrap-capability.d.ts +34 -0
- package/dist/{init-kibi-capability.js → kibi-bootstrap-capability.js} +26 -16
- package/dist/meaningful-change-classifier.d.ts +1 -0
- package/dist/meaningful-change-classifier.js +40 -10
- package/dist/path-kind.js +21 -39
- package/dist/plugin-startup.js +3 -3
- package/dist/plugin.d.ts +4 -1
- package/dist/plugin.js +81 -374
- package/dist/prompt.d.ts +4 -4
- package/dist/prompt.js +24 -60
- package/dist/repo-posture.js +31 -84
- package/dist/risk-classifier.d.ts +3 -3
- package/dist/risk-classifier.js +9 -6
- package/dist/scheduler.js +2 -2
- package/dist/session-edit-state.d.ts +4 -0
- package/dist/session-edit-state.js +5 -3
- package/dist/smart-enforcement-events.d.ts +100 -0
- package/dist/smart-enforcement-events.js +414 -0
- package/dist/version-metadata.json +1 -1
- package/dist/work-context-resolver.d.ts +10 -0
- package/dist/work-context-resolver.js +15 -17
- package/dist/workspace-health.js +8 -56
- package/package.json +6 -4
- package/dist/init-kibi-alias.d.ts +0 -14
- package/dist/init-kibi-alias.js +0 -43
- package/dist/init-kibi-capability.d.ts +0 -32
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ The plugin now uses a posture-aware, low-token smart-enforcement model before em
|
|
|
52
52
|
|
|
53
53
|
- **Repo posture detection**: distinguishes `root_active`, `root_partial`, `root_uninitialized`, `vendored_only`, and `hybrid_root_plus_vendored`
|
|
54
54
|
- **Risk classification**: separates `safe_docs_only`, `safe_test_only`, `kb_doc_structural`, `req_policy_candidate`, `behavior_candidate`, `traceability_candidate`, and `manual_kb_edit`
|
|
55
|
-
- **Source-linked Kibi links**: risky code edits (`behavior_candidate`, `traceability_candidate`) prepend a concise list of existing Kibi links (e.g., `- Existing Kibi links: REQ-001, REQ-002`) when 1-3 concrete source-linked KB hits are found in
|
|
55
|
+
- **Source-linked Kibi links**: risky code edits (`behavior_candidate`, `traceability_candidate`) prepend a concise list of existing Kibi links (e.g., `- Existing Kibi links: REQ-001, REQ-002`) when 1-3 concrete source-linked KB hits are found in `.kb/symbols.yaml`. Skip on cache hit.
|
|
56
56
|
- **Start-task risky cue**: authoritative risky edits also add a compact Kibi context cue so agents can start with explicit source-linked guidance before acting, while staying inside the same single prompt block and token budget.
|
|
57
57
|
- **Effective mode gating**: `advisory` (default) never blocks; `strict` escalates checks and reminders for `root_active` and `hybrid_root_plus_vendored` postures when `requireRootKbForStrict` is enabled; `hard` fails closed for authoritative roots and linked git worktrees, injecting capability-based recovery steps until the Kibi checkpoint passes. `maintenanceDegraded` overrides everything back to `advisory`.
|
|
58
58
|
- **Low-token prompt policy**: docs-only and test-only edits avoid unnecessary discovery prompts; vendored-only repos suppress operational bootstrap nudges; at most one contextual block is injected per prompt (≤120 words, ≤5 bullets)
|
|
@@ -155,9 +155,11 @@ The plugin injects guidance into OpenCode sessions to improve agent grounding. U
|
|
|
155
155
|
|
|
156
156
|
### Bootstrap Command
|
|
157
157
|
|
|
158
|
-
OpenCode exposes
|
|
158
|
+
OpenCode exposes the canonical bootstrap skill as `/kibi-bootstrap`. The
|
|
159
|
+
command routes an explicit request to the read-only planner and its typed
|
|
160
|
+
approval/apply workflow.
|
|
159
161
|
|
|
160
|
-
OpenCode may show Kibi MCP tools with the configured server prefix. For example, canonical MCP names such as `
|
|
162
|
+
OpenCode may show Kibi MCP tools with the configured server prefix. For example, canonical MCP names such as `kb_plan_bootstrap`, `kb_upsert`, and `kb_check` can appear to agents as `kibi_kb_plan_bootstrap`, `kibi_kb_upsert`, and `kibi_kb_check`. Use the visible `kibi_kb_*` identifier when OpenCode requires an exact tool name; it maps to the same MCP operation.
|
|
161
163
|
|
|
162
164
|
### Discovery-first capability guidance
|
|
163
165
|
|
|
@@ -301,7 +303,7 @@ Disable specific features while keeping others:
|
|
|
301
303
|
|
|
302
304
|
## Troubleshooting
|
|
303
305
|
|
|
304
|
-
If you see a false "workspace needs Kibi bootstrap" warning even though
|
|
306
|
+
If you see a false "workspace needs Kibi bootstrap" warning even though `.kb/manifest.json` and the canonical knowledge lanes exist, this usually means a stale plugin cache. Leftover `.kb/config.json` custom paths are ignored. See [the main troubleshooting docs](../../docs/troubleshooting.md#opencode-shows-workspace-needs-kibi-bootstrap-before-the-tui) for recovery steps.
|
|
305
307
|
|
|
306
308
|
## Architecture
|
|
307
309
|
|
|
@@ -318,7 +320,7 @@ A proposed enhancement would inject Kibi context hints into file-read results (e
|
|
|
318
320
|
|
|
319
321
|
1. OpenCode's current plugin surface does not expose file-content interception hooks
|
|
320
322
|
2. The `experimental.chat.system.transform` hook only supports system prompt injection
|
|
321
|
-
3. Symbol metadata from
|
|
323
|
+
3. Symbol metadata from `.kb/symbols.yaml` can inform this feature once host support exists
|
|
322
324
|
|
|
323
325
|
Current workaround: static system prompt guidance directs agents to query Kibi explicitly.
|
|
324
326
|
|
|
@@ -335,7 +337,7 @@ The plugin provides proactive guidance when agents perform file operations:
|
|
|
335
337
|
The OpenCode plugin respects the repository ignore policy used by Kibi's discovery pipeline. In practice this means:
|
|
336
338
|
|
|
337
339
|
- The plugin's background sync and file-event handling skip paths matched by repository `.gitignore` files, nested `.gitignore` files, and `.git/info/exclude`.
|
|
338
|
-
- The plugin also treats a curated set of tool/runtime directories as never-relevant for KB sync (for example: `.sisyphus`, `.opencode`, `.
|
|
340
|
+
- The plugin also treats a curated set of tool/runtime directories as never-relevant for KB sync (for example: `.sisyphus`, `.opencode`, `.git`, `node_modules`, `vendor`, `third_party`). Derived `.kb/branches`, `.kb/recovery`, `.kb/verification`, and `.kb/briefs` trees are ignored; authored knowledge lanes under `.kb/` are not.
|
|
339
341
|
|
|
340
342
|
When a file event occurs for an ignored path, the plugin skips processing and will not surface candidate guidance for that file. This avoids noisy sync triggers and prevents build/editor artifacts from triggering KB sync work.
|
|
341
343
|
|
package/dist/auto-update.d.ts
CHANGED
|
@@ -23,9 +23,11 @@ export interface AutoUpdateRunnerDeps {
|
|
|
23
23
|
log: (message: string, metadata?: Record<string, unknown>) => void;
|
|
24
24
|
}
|
|
25
25
|
export declare function findKibiOpencodePluginEntry(directory: string): KibiOpencodePluginEntry | null;
|
|
26
|
+
export declare function findPackageJsonUp(startUrl: string): string | null;
|
|
26
27
|
export declare function getCachedPluginVersion(): string | null;
|
|
27
28
|
export declare function getLatestPluginVersion(channel?: string): Promise<string | null>;
|
|
28
29
|
export declare function invalidateKibiOpencodePackage(): boolean;
|
|
29
30
|
export declare function runBunInstallForOpenCodePlugin(): Promise<boolean>;
|
|
30
31
|
export declare function createAutoUpdateRunner(deps: AutoUpdateRunnerDeps): (input: AutoUpdateInput) => Promise<AutoUpdateResult>;
|
|
32
|
+
export declare function notifyAutoUpdate(message: string): Promise<void>;
|
|
31
33
|
export declare const runKibiOpencodeAutoUpdate: (input: AutoUpdateInput) => Promise<AutoUpdateResult>;
|
package/dist/auto-update.js
CHANGED
|
@@ -188,7 +188,7 @@ function readVersionFromPackageJson(packageJsonPath) {
|
|
|
188
188
|
const parsed = parseJsonObject(fs.readFileSync(packageJsonPath, "utf8"));
|
|
189
189
|
return typeof parsed?.version === "string" ? parsed.version : null;
|
|
190
190
|
}
|
|
191
|
-
function findPackageJsonUp(startUrl) {
|
|
191
|
+
export function findPackageJsonUp(startUrl) {
|
|
192
192
|
let current = path.dirname(fileURLToPath(startUrl));
|
|
193
193
|
while (true) {
|
|
194
194
|
const packageJsonPath = path.join(current, "package.json");
|
|
@@ -384,13 +384,14 @@ export function createAutoUpdateRunner(deps) {
|
|
|
384
384
|
return { status: "updated", currentVersion, latestVersion };
|
|
385
385
|
};
|
|
386
386
|
}
|
|
387
|
+
export async function notifyAutoUpdate(message) {
|
|
388
|
+
logger.info("auto-update.notification", { message });
|
|
389
|
+
}
|
|
387
390
|
export const runKibiOpencodeAutoUpdate = createAutoUpdateRunner({
|
|
388
391
|
getCurrentVersion: getCachedPluginVersion,
|
|
389
392
|
getLatestVersion: getLatestPluginVersion,
|
|
390
393
|
invalidatePackage: invalidateKibiOpencodePackage,
|
|
391
394
|
runInstall: runBunInstallForOpenCodePlugin,
|
|
392
|
-
notify:
|
|
393
|
-
logger.info("auto-update.notification", { message });
|
|
394
|
-
},
|
|
395
|
+
notify: notifyAutoUpdate,
|
|
395
396
|
log: (message, metadata) => logger.info(message, metadata),
|
|
396
397
|
});
|
|
@@ -4,13 +4,13 @@ import * as path from "node:path";
|
|
|
4
4
|
import { getFileLinkedTargetsByType } from "./file-entity-links.js";
|
|
5
5
|
// ── TEST doc reader ──────────────────────────────────────────────
|
|
6
6
|
//
|
|
7
|
-
// Reads a TEST-*.md file from
|
|
7
|
+
// Reads a TEST-*.md file from .kb/tests/ and extracts
|
|
8
8
|
// frontmatter tags, source, and body.
|
|
9
9
|
function readTestDoc(worktree, testId) {
|
|
10
10
|
// Try common locations for TEST docs
|
|
11
11
|
const candidates = [
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
`.kb/tests/${testId}.md`,
|
|
13
|
+
`.kb/tests/${testId.toLowerCase()}.md`,
|
|
14
14
|
];
|
|
15
15
|
for (const rel of candidates) {
|
|
16
16
|
const fullPath = path.join(worktree, rel);
|
|
@@ -47,6 +47,11 @@ export interface EnforcementPolicyInput {
|
|
|
47
47
|
/** Posture fallback when resolvedContext is unavailable. */
|
|
48
48
|
posture?: RepoPosture | undefined;
|
|
49
49
|
}
|
|
50
|
+
export interface NormalizedPolicyEvent extends EnforcementLifecycleEvent {
|
|
51
|
+
pathKind: PathKind;
|
|
52
|
+
linkedEntityResult: PolicyLinkedEntityResult;
|
|
53
|
+
e2eSignal: E2eCoverageSignal;
|
|
54
|
+
}
|
|
50
55
|
interface EnforcementPolicyResultBase {
|
|
51
56
|
affectedPaths: string[];
|
|
52
57
|
dirtyFileCount: number;
|
|
@@ -70,4 +75,9 @@ export type EnforcementPolicyResult = (EnforcementPolicyResultBase & {
|
|
|
70
75
|
text: null;
|
|
71
76
|
});
|
|
72
77
|
export declare function computeEnforcementPolicy(input: EnforcementPolicyInput): EnforcementPolicyResult;
|
|
78
|
+
export declare function advisoryResultFromEvents(relevantEvents: readonly NormalizedPolicyEvent[], extras: {
|
|
79
|
+
affectedPaths: string[];
|
|
80
|
+
e2eReminder: string | null;
|
|
81
|
+
reminderKindsToMark: ReminderKind[];
|
|
82
|
+
}): EnforcementPolicyResult;
|
|
73
83
|
export {};
|
|
@@ -185,30 +185,20 @@ function hardBlockText(events) {
|
|
|
185
185
|
const representativePath = events[0]?.normalizedPath ?? "<changed-file>";
|
|
186
186
|
const evidenceNotes = [linkedIdsText(events), e2eEvidenceText(events)].filter((note) => note !== null);
|
|
187
187
|
const deletionCleanup = deletedWithoutLinks
|
|
188
|
-
? "Deleted
|
|
189
|
-
: "Use
|
|
188
|
+
? "Deleted paths without linked IDs still require a source-linked traceability review before cleanup."
|
|
189
|
+
: "Use the canonical traceability workflow when relationships or source-linked facts need updates.";
|
|
190
190
|
return {
|
|
191
191
|
text: [
|
|
192
192
|
"🛑 **Hard Kibi checkpoint required**",
|
|
193
193
|
"Changed relevant files require Kibi verification before continuing:",
|
|
194
194
|
...pathLines,
|
|
195
195
|
...evidenceNotes,
|
|
196
|
-
"
|
|
197
|
-
"-
|
|
198
|
-
"-
|
|
199
|
-
|
|
200
|
-
"- If neither interface is available, Kibi operation is blocked; tell the operator to enable MCP or the trusted project-local CLI.",
|
|
201
|
-
"- Do not infer MCP availability from config file existence. Do not read or edit `.kb/` files directly.",
|
|
202
|
-
"- Query before mutate. Run `kb_upsert` sequentially. Run `kb_check` before completion.",
|
|
203
|
-
"- Run `kb_search` to discover impacted requirements, tests, ADRs, and facts.",
|
|
204
|
-
`- Run \`kb_query\` with \`sourceFile\` (example sourceFile: \"${representativePath}\") for each listed path.`,
|
|
205
|
-
"- Run `kb_status` if branch or snapshot freshness matters.",
|
|
196
|
+
"- Inspect typed Kibi status `nextActions` and route to the canonical kibi-usage, kibi-freshness, or kibi-traceability skill.",
|
|
197
|
+
"- Use the host-visible approved Kibi peer surface; if it is unavailable, report the blocked capability.",
|
|
198
|
+
"- Never read or edit `.kb/` files directly.",
|
|
199
|
+
`- Preserve source-linked evidence for \`${representativePath}\` through the typed traceability workflow.`,
|
|
206
200
|
`- ${deletionCleanup}`,
|
|
207
|
-
"-
|
|
208
|
-
"KB freshness resolution:",
|
|
209
|
-
"- **KB updated**: run `kb_search` for discovery, then `kb_upsert`/`kb_delete` for mutations, then `kb_check`.",
|
|
210
|
-
"- **No KB impact**: provide a no-impact rationale in your final report after source-linked discovery via `kb_search` or `kb_query(sourceFile=...)` and `kb_check`.",
|
|
211
|
-
"- **Deferred/failed**: do not claim task completion.",
|
|
201
|
+
"- Follow typed check/status next actions before completing the task; unresolved or blocked state is not completion.",
|
|
212
202
|
].join("\n"),
|
|
213
203
|
shownPaths,
|
|
214
204
|
remainingCount,
|
|
@@ -276,23 +266,33 @@ export function computeEnforcementPolicy(input) {
|
|
|
276
266
|
text: null,
|
|
277
267
|
};
|
|
278
268
|
}
|
|
269
|
+
return advisoryResultFromEvents(relevantEvents, {
|
|
270
|
+
affectedPaths,
|
|
271
|
+
e2eReminder,
|
|
272
|
+
reminderKindsToMark,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
// Defensive empty-list checkpoint. `computeEnforcementPolicy` returns earlier
|
|
276
|
+
// when `relevantEvents` is empty; tests call this helper directly so the
|
|
277
|
+
// branch stays executable and covered.
|
|
278
|
+
export function advisoryResultFromEvents(relevantEvents, extras) {
|
|
279
279
|
const firstRelevantEvent = relevantEvents[0];
|
|
280
280
|
if (!firstRelevantEvent) {
|
|
281
281
|
return {
|
|
282
282
|
kind: "checkpoint_passed",
|
|
283
|
-
affectedPaths,
|
|
283
|
+
affectedPaths: extras.affectedPaths,
|
|
284
284
|
dirtyFileCount: 0,
|
|
285
|
-
e2eReminder,
|
|
286
|
-
reminderKindsToMark,
|
|
285
|
+
e2eReminder: extras.e2eReminder,
|
|
286
|
+
reminderKindsToMark: extras.reminderKindsToMark,
|
|
287
287
|
text: null,
|
|
288
288
|
};
|
|
289
289
|
}
|
|
290
290
|
return {
|
|
291
291
|
kind: "advisory_guidance",
|
|
292
|
-
affectedPaths,
|
|
292
|
+
affectedPaths: extras.affectedPaths,
|
|
293
293
|
dirtyFileCount: relevantEvents.length,
|
|
294
|
-
e2eReminder,
|
|
295
|
-
reminderKindsToMark,
|
|
294
|
+
e2eReminder: extras.e2eReminder,
|
|
295
|
+
reminderKindsToMark: extras.reminderKindsToMark,
|
|
296
296
|
text: advisoryText(firstRelevantEvent),
|
|
297
297
|
};
|
|
298
298
|
}
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { loadKbSyncPaths } from "./file-filter.js";
|
|
5
|
+
// ── Shared types ────────────────────────────────────────────────────
|
|
6
|
+
export function emptyLinkedTargets() {
|
|
7
|
+
return { ids: [], source: "none" };
|
|
8
|
+
}
|
|
5
9
|
// ── Lightweight YAML parser (symbols.yaml subset) ───────────────────
|
|
6
10
|
//
|
|
7
11
|
// Handles:
|
|
@@ -139,14 +143,12 @@ function resolveDocPathIdentity(relPath, syncPaths) {
|
|
|
139
143
|
if (!pattern)
|
|
140
144
|
continue;
|
|
141
145
|
// Strip glob from pattern to get the root dir prefix
|
|
142
|
-
// e.g. "
|
|
146
|
+
// e.g. ".kb/requirements/**/*.md" → ".kb/requirements"
|
|
143
147
|
const rootDir = pattern.replace(/\/\*\*\/.*$/, "").replace(/\/+$/, "");
|
|
144
148
|
if (normalizedRel.startsWith(`${rootDir}/`)) {
|
|
145
149
|
return entityId;
|
|
146
150
|
}
|
|
147
151
|
}
|
|
148
|
-
// If no specific root matched but path starts with documentation/,
|
|
149
|
-
// still accept (covers default configuration)
|
|
150
152
|
return null;
|
|
151
153
|
}
|
|
152
154
|
// ── Symbols file resolution ─────────────────────────────────────────
|
|
@@ -220,7 +222,7 @@ export function getFileLinkedEntityIds(worktree, filePath) {
|
|
|
220
222
|
return { ids: orderedIds.slice(0, 3), source: "symbols" };
|
|
221
223
|
}
|
|
222
224
|
catch {
|
|
223
|
-
return
|
|
225
|
+
return emptyLinkedTargets();
|
|
224
226
|
}
|
|
225
227
|
}
|
|
226
228
|
// implements REQ-opencode-file-context-guidance-v1
|
package/dist/file-filter.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
export declare function loadKbSyncPaths(
|
|
2
|
-
requirements:
|
|
3
|
-
scenarios:
|
|
4
|
-
tests:
|
|
5
|
-
adr:
|
|
6
|
-
flags:
|
|
7
|
-
events:
|
|
8
|
-
facts:
|
|
9
|
-
symbols:
|
|
1
|
+
export declare function loadKbSyncPaths(_cwd?: string): {
|
|
2
|
+
readonly requirements: ".kb/requirements/**/*.md";
|
|
3
|
+
readonly scenarios: ".kb/scenarios/**/*.md";
|
|
4
|
+
readonly tests: ".kb/tests/**/*.md";
|
|
5
|
+
readonly adr: ".kb/adr/**/*.md";
|
|
6
|
+
readonly flags: ".kb/flags/**/*.md";
|
|
7
|
+
readonly events: ".kb/events/**/*.md";
|
|
8
|
+
readonly facts: ".kb/facts/**/*.md";
|
|
9
|
+
readonly symbols: ".kb/symbols.yaml";
|
|
10
10
|
};
|
|
11
11
|
export interface KbExistenceTarget {
|
|
12
12
|
key: string;
|
package/dist/file-filter.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
1
|
// implements REQ-opencode-kibi-plugin-v1
|
|
3
2
|
import { createRequire } from "node:module";
|
|
4
3
|
import * as path from "node:path";
|
|
5
|
-
import { createRepoIgnorePolicy } from "kibi-cli/ignore-policy";
|
|
6
4
|
const _require = createRequire(import.meta.url);
|
|
7
5
|
// Lightweight fallback matcher if picomatch isn't installed.
|
|
8
6
|
let picomatch;
|
|
@@ -23,40 +21,20 @@ catch {
|
|
|
23
21
|
},
|
|
24
22
|
};
|
|
25
23
|
}
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
const
|
|
29
|
-
requirements: "
|
|
30
|
-
scenarios: "
|
|
31
|
-
tests: "
|
|
32
|
-
adr: "
|
|
33
|
-
flags: "
|
|
34
|
-
events: "
|
|
35
|
-
facts: "
|
|
36
|
-
symbols: "
|
|
24
|
+
// Canonical Kibi knowledge paths — must stay in sync with
|
|
25
|
+
// packages/cli/src/utils/kb-paths.ts CANONICAL_ENTITY_PATHS.
|
|
26
|
+
const CANONICAL_KB_PATHS = {
|
|
27
|
+
requirements: ".kb/requirements/**/*.md",
|
|
28
|
+
scenarios: ".kb/scenarios/**/*.md",
|
|
29
|
+
tests: ".kb/tests/**/*.md",
|
|
30
|
+
adr: ".kb/adr/**/*.md",
|
|
31
|
+
flags: ".kb/flags/**/*.md",
|
|
32
|
+
events: ".kb/events/**/*.md",
|
|
33
|
+
facts: ".kb/facts/**/*.md",
|
|
34
|
+
symbols: ".kb/symbols.yaml",
|
|
37
35
|
};
|
|
38
|
-
function
|
|
39
|
-
|
|
40
|
-
let userConfig = {};
|
|
41
|
-
if (existsSync(configPath)) {
|
|
42
|
-
try {
|
|
43
|
-
userConfig = JSON.parse(readFileSync(configPath, "utf8")) || {};
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
userConfig = {};
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return {
|
|
50
|
-
paths: {
|
|
51
|
-
...DEFAULT_SYNC_PATHS,
|
|
52
|
-
...(userConfig.paths ?? {}),
|
|
53
|
-
},
|
|
54
|
-
defaultBranch: userConfig.defaultBranch,
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
export function loadKbSyncPaths(cwd = process.cwd()) {
|
|
58
|
-
const cfg = loadSyncConfigLocal(cwd);
|
|
59
|
-
return cfg.paths ?? DEFAULT_SYNC_PATHS;
|
|
36
|
+
export function loadKbSyncPaths(_cwd = process.cwd()) {
|
|
37
|
+
return CANONICAL_KB_PATHS;
|
|
60
38
|
}
|
|
61
39
|
// implements REQ-opencode-kibi-plugin-v1
|
|
62
40
|
export function getKbExistenceTargets(cwd = process.cwd()) {
|
|
@@ -117,32 +95,12 @@ function normalizePattern(p) {
|
|
|
117
95
|
// otherwise treat directory as markdown collection
|
|
118
96
|
return `${p.replace(/\/+$/, "")}/**/*.md`;
|
|
119
97
|
}
|
|
120
|
-
const DEFAULT_IGNORES = [
|
|
121
|
-
".kb/**",
|
|
122
|
-
".git/**",
|
|
123
|
-
"node_modules/**",
|
|
124
|
-
"dist/**",
|
|
125
|
-
"coverage/**",
|
|
126
|
-
".opencode/**",
|
|
127
|
-
"**/*~",
|
|
128
|
-
"**/~*",
|
|
129
|
-
"**/.#*",
|
|
130
|
-
"**/*.swp",
|
|
131
|
-
"**/*.swo",
|
|
132
|
-
"**/.DS_Store",
|
|
133
|
-
];
|
|
134
98
|
// implements REQ-opencode-kibi-plugin-v1
|
|
135
99
|
export function shouldHandleFile(filePath, cwd = process.cwd()) {
|
|
136
100
|
const rel = path.isAbsolute(filePath)
|
|
137
101
|
? path.relative(cwd, filePath).split(path.sep).join("/")
|
|
138
102
|
: filePath.split(path.sep).join("/");
|
|
139
|
-
// Check shared ignore policy (root .gitignore, nested .gitignore, .git/info/exclude,
|
|
140
|
-
// and hard denylist such as .sisyphus) before other pattern checks.
|
|
141
|
-
const policy = createRepoIgnorePolicy(cwd);
|
|
142
|
-
if (policy.isIgnored(rel))
|
|
143
|
-
return false;
|
|
144
103
|
const paths = loadKbSyncPaths(cwd);
|
|
145
|
-
// Build include patterns from kibi paths
|
|
146
104
|
const includeCandidates = [
|
|
147
105
|
paths.requirements,
|
|
148
106
|
paths.scenarios,
|
|
@@ -156,21 +114,11 @@ export function shouldHandleFile(filePath, cwd = process.cwd()) {
|
|
|
156
114
|
const includePatterns = includeCandidates
|
|
157
115
|
.map(normalizePattern)
|
|
158
116
|
.filter((p) => Boolean(p));
|
|
159
|
-
// default ignores then allow extension by .kb/config.json -> sync.ignore (not implemented here)
|
|
160
|
-
const ignorePatterns = DEFAULT_IGNORES;
|
|
161
|
-
// Compile matchers
|
|
162
|
-
const isIgnored = ignorePatterns.some((ig) => picomatch.isMatch(rel, ig));
|
|
163
|
-
if (isIgnored)
|
|
164
|
-
return false;
|
|
165
|
-
// If any include pattern matches, accept
|
|
166
117
|
const included = includePatterns.some((pat) => picomatch.isMatch(rel, pat));
|
|
167
|
-
if (included
|
|
118
|
+
if (included ||
|
|
119
|
+
(paths.symbols !== undefined &&
|
|
120
|
+
(paths.symbols === rel || picomatch.isMatch(rel, paths.symbols)))) {
|
|
168
121
|
return true;
|
|
169
|
-
// If symbols path is configured as exact file and matches exactly, accept
|
|
170
|
-
if (paths.symbols) {
|
|
171
|
-
const sym = paths.symbols;
|
|
172
|
-
if (sym === rel || picomatch.isMatch(rel, sym))
|
|
173
|
-
return true;
|
|
174
122
|
}
|
|
175
123
|
return false;
|
|
176
124
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Builds the host-local route to the canonical kibi-bootstrap skill. */
|
|
2
|
+
// implements REQ-KIBI-BOOTSTRAP-PLAN
|
|
3
|
+
export function buildKibiBootstrapAlias() {
|
|
4
|
+
const lines = [
|
|
5
|
+
"# /kibi-bootstrap",
|
|
6
|
+
"",
|
|
7
|
+
"Bootstrap Kibi knowledge for an existing repository.",
|
|
8
|
+
"",
|
|
9
|
+
"## Route",
|
|
10
|
+
"If Kibi infrastructure is missing, run `kibi init` first.",
|
|
11
|
+
"Then invoke the canonical `kibi-bootstrap` skill. OpenCode may expose this route as a native command or as `/kibi:kibi-bootstrap:mcp`; use whichever host syntax is available.",
|
|
12
|
+
];
|
|
13
|
+
return lines.join("\n");
|
|
14
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export declare const KIBI_BOOTSTRAP_COMMAND_NAME = "kibi-bootstrap";
|
|
2
|
+
export declare const KIBI_BOOTSTRAP_COMMAND_TEMPLATE: string;
|
|
3
|
+
export declare const KIBI_BOOTSTRAP_COMMAND_DESCRIPTION = "Bootstrap Kibi knowledge for an existing repository.";
|
|
4
|
+
export interface OpenCodeCommandDefinition {
|
|
5
|
+
template: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
agent?: string;
|
|
8
|
+
model?: string;
|
|
9
|
+
subtask?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface OpenCodeConfigHookInput {
|
|
12
|
+
command?: Record<string, OpenCodeCommandDefinition>;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
export type KibiBootstrapCommandCapability = {
|
|
16
|
+
supported: true;
|
|
17
|
+
pluginVersion: string;
|
|
18
|
+
} | {
|
|
19
|
+
supported: false;
|
|
20
|
+
reason: string;
|
|
21
|
+
pluginVersion?: string;
|
|
22
|
+
};
|
|
23
|
+
interface KibiBootstrapCapabilityDetectionInput {
|
|
24
|
+
pluginVersion?: string;
|
|
25
|
+
pluginHooksDts?: string;
|
|
26
|
+
sdkTypesDts?: string;
|
|
27
|
+
}
|
|
28
|
+
export declare function findSdkPackageJsonForPluginRoot(pluginRoot: string): string | undefined;
|
|
29
|
+
export declare function capabilityInputsOrEmpty<T extends object>(load: () => T): T | Record<string, never>;
|
|
30
|
+
export declare function detectKibiBootstrapCommandCapability(input: KibiBootstrapCapabilityDetectionInput): KibiBootstrapCommandCapability;
|
|
31
|
+
export declare function getKibiBootstrapCommandCapability(): KibiBootstrapCommandCapability;
|
|
32
|
+
export declare function _resetKibiBootstrapCapabilityCacheForTests(): void;
|
|
33
|
+
export declare function registerKibiBootstrapCommand(configInput: unknown, capability?: KibiBootstrapCommandCapability): KibiBootstrapCommandCapability;
|
|
34
|
+
export {};
|
|
@@ -2,10 +2,10 @@ import fs from "node:fs";
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import {
|
|
6
|
-
export const
|
|
7
|
-
export const
|
|
8
|
-
export const
|
|
5
|
+
import { buildKibiBootstrapAlias } from "./kibi-bootstrap-alias.js";
|
|
6
|
+
export const KIBI_BOOTSTRAP_COMMAND_NAME = "kibi-bootstrap";
|
|
7
|
+
export const KIBI_BOOTSTRAP_COMMAND_TEMPLATE = buildKibiBootstrapAlias();
|
|
8
|
+
export const KIBI_BOOTSTRAP_COMMAND_DESCRIPTION = "Bootstrap Kibi knowledge for an existing repository."; // implements REQ-001
|
|
9
9
|
const require = createRequire(import.meta.url);
|
|
10
10
|
let cachedCapability = null;
|
|
11
11
|
const initialProcessCwd = process.cwd();
|
|
@@ -111,6 +111,14 @@ function hasConfigHook(pluginHooksDts) {
|
|
|
111
111
|
function hasConfigCommandField(sdkTypesDts) {
|
|
112
112
|
return /\bcommand\??:\s*\{[\s\S]*?\[key:\s*string\]:\s*\{[\s\S]*?\btemplate:\s*string\s*;/.test(sdkTypesDts);
|
|
113
113
|
}
|
|
114
|
+
export function capabilityInputsOrEmpty(load) {
|
|
115
|
+
try {
|
|
116
|
+
return load();
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return {};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
114
122
|
function resolveHostCapabilityInputs() {
|
|
115
123
|
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
116
124
|
const dogfoodHost = resolveDogfoodHostCapabilityInputs([
|
|
@@ -125,7 +133,7 @@ function resolveHostCapabilityInputs() {
|
|
|
125
133
|
if (dogfoodHost) {
|
|
126
134
|
return dogfoodHost;
|
|
127
135
|
}
|
|
128
|
-
|
|
136
|
+
return capabilityInputsOrEmpty(() => {
|
|
129
137
|
const pluginPackageJsonPath = require.resolve("@opencode-ai/plugin/package.json");
|
|
130
138
|
const pluginRoot = path.dirname(pluginPackageJsonPath);
|
|
131
139
|
const sdkPackageJsonPath = findSdkPackageJsonForPluginRoot(pluginRoot) ??
|
|
@@ -139,10 +147,7 @@ function resolveHostCapabilityInputs() {
|
|
|
139
147
|
...(pluginHooksDts ? { pluginHooksDts } : {}),
|
|
140
148
|
...(sdkTypesDts ? { sdkTypesDts } : {}),
|
|
141
149
|
};
|
|
142
|
-
}
|
|
143
|
-
catch {
|
|
144
|
-
return {};
|
|
145
|
-
}
|
|
150
|
+
});
|
|
146
151
|
}
|
|
147
152
|
function isRecord(value) {
|
|
148
153
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -150,7 +155,7 @@ function isRecord(value) {
|
|
|
150
155
|
function isCommandMap(value) {
|
|
151
156
|
return isRecord(value);
|
|
152
157
|
}
|
|
153
|
-
export function
|
|
158
|
+
export function detectKibiBootstrapCommandCapability(input) {
|
|
154
159
|
const { pluginVersion, pluginHooksDts, sdkTypesDts } = input;
|
|
155
160
|
if (typeof pluginHooksDts !== "string" || pluginHooksDts.length === 0) {
|
|
156
161
|
return buildUnsupportedReason(pluginVersion, "host Hooks definition is unavailable for config hook inspection.");
|
|
@@ -169,14 +174,19 @@ export function detectInitKibiCommandCapability(input) {
|
|
|
169
174
|
pluginVersion: pluginVersion ?? "unknown",
|
|
170
175
|
};
|
|
171
176
|
}
|
|
172
|
-
export function
|
|
177
|
+
export function getKibiBootstrapCommandCapability() {
|
|
173
178
|
if (cachedCapability?.supported) {
|
|
174
179
|
return cachedCapability;
|
|
175
180
|
}
|
|
176
|
-
cachedCapability =
|
|
181
|
+
cachedCapability = detectKibiBootstrapCommandCapability(resolveHostCapabilityInputs());
|
|
177
182
|
return cachedCapability;
|
|
178
183
|
}
|
|
179
|
-
|
|
184
|
+
// implements REQ-KIBI-BOOTSTRAP-PLAN
|
|
185
|
+
export function _resetKibiBootstrapCapabilityCacheForTests() {
|
|
186
|
+
cachedCapability = null;
|
|
187
|
+
}
|
|
188
|
+
// implements REQ-KIBI-BOOTSTRAP-PLAN
|
|
189
|
+
export function registerKibiBootstrapCommand(configInput, capability = getKibiBootstrapCommandCapability()) {
|
|
180
190
|
if (!capability.supported) {
|
|
181
191
|
return capability;
|
|
182
192
|
}
|
|
@@ -189,9 +199,9 @@ export function registerInitKibiCommand(configInput, capability = getInitKibiCom
|
|
|
189
199
|
}
|
|
190
200
|
configInput.command = {
|
|
191
201
|
...(existingCommands ?? {}),
|
|
192
|
-
[
|
|
193
|
-
template:
|
|
194
|
-
description:
|
|
202
|
+
[KIBI_BOOTSTRAP_COMMAND_NAME]: {
|
|
203
|
+
template: KIBI_BOOTSTRAP_COMMAND_TEMPLATE,
|
|
204
|
+
description: KIBI_BOOTSTRAP_COMMAND_DESCRIPTION,
|
|
195
205
|
},
|
|
196
206
|
};
|
|
197
207
|
return capability;
|
|
@@ -10,6 +10,7 @@ import type { RiskClass } from "./risk-classifier.js";
|
|
|
10
10
|
* - "ignored": The change is in a build artifact or third-party path — no KB
|
|
11
11
|
* impact.
|
|
12
12
|
*/
|
|
13
|
+
export declare function isSafeDocsUnknownPath(riskClass: RiskClass, pathKind: PathKind): boolean;
|
|
13
14
|
export type MeaningfulChangeClass = "requires-kb-evidence" | "advisory" | "ignored";
|
|
14
15
|
/**
|
|
15
16
|
* Classify whether a file change requires KB freshness evidence.
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classification for whether a file change requires KB freshness evidence.
|
|
3
|
+
*
|
|
4
|
+
* - "requires-kb-evidence": The change is meaningful for KB freshness — the KB
|
|
5
|
+
* snapshot should be considered stale and needs a re-sync.
|
|
6
|
+
* - "advisory": The change is low-risk (lockfiles, docs-only) — advisory only.
|
|
7
|
+
* - "ignored": The change is in a build artifact or third-party path — no KB
|
|
8
|
+
* impact.
|
|
9
|
+
*/
|
|
10
|
+
export function isSafeDocsUnknownPath(riskClass, pathKind) {
|
|
11
|
+
return riskClass === "safe_docs_only" && pathKind === "unknown";
|
|
12
|
+
}
|
|
1
13
|
// Directories whose contents are always ignored for KB freshness.
|
|
2
14
|
const IGNORED_DIRECTORIES = [
|
|
3
15
|
".git",
|
|
@@ -8,6 +20,26 @@ const IGNORED_DIRECTORIES = [
|
|
|
8
20
|
"dist",
|
|
9
21
|
"coverage",
|
|
10
22
|
];
|
|
23
|
+
/** Derived `.kb/` runtime trees — must stay in sync with kb-paths.ts DERIVED_KB_PREFIXES. */
|
|
24
|
+
const DERIVED_KB_PREFIXES = [
|
|
25
|
+
".kb/branches",
|
|
26
|
+
".kb/recovery",
|
|
27
|
+
".kb/verification",
|
|
28
|
+
".kb/briefs",
|
|
29
|
+
".kb/migrations",
|
|
30
|
+
];
|
|
31
|
+
function isDerivedKbPath(normalizedPath) {
|
|
32
|
+
for (const prefix of DERIVED_KB_PREFIXES) {
|
|
33
|
+
if (normalizedPath === prefix ||
|
|
34
|
+
normalizedPath.startsWith(`${prefix}/`) ||
|
|
35
|
+
normalizedPath.includes(`/${prefix}/`) ||
|
|
36
|
+
normalizedPath.endsWith(`/${prefix}`)) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return (normalizedPath === ".kb/config.json" ||
|
|
41
|
+
normalizedPath.endsWith("/.kb/config.json"));
|
|
42
|
+
}
|
|
11
43
|
// Filenames that are advisory-only (lockfiles).
|
|
12
44
|
const ADVISORY_FILENAMES = new Set([
|
|
13
45
|
"bun.lock",
|
|
@@ -35,7 +67,7 @@ const UPGRADE_RISK_CLASSES = new Set([
|
|
|
35
67
|
"traceability_candidate",
|
|
36
68
|
"req_policy_candidate",
|
|
37
69
|
]);
|
|
38
|
-
function isIgnoredPath(normalizedPath
|
|
70
|
+
function isIgnoredPath(normalizedPath) {
|
|
39
71
|
// Check extension-based ignored patterns.
|
|
40
72
|
if (normalizedPath.endsWith(".tsbuildinfo") ||
|
|
41
73
|
normalizedPath.endsWith(".map")) {
|
|
@@ -48,12 +80,11 @@ function isIgnoredPath(normalizedPath, pathKind) {
|
|
|
48
80
|
return true;
|
|
49
81
|
}
|
|
50
82
|
}
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
83
|
+
// Derived `.kb/` runtime (stores, recovery, leftover config) is ignored.
|
|
84
|
+
// Canonical entity lanes, symbols, relationships, and the manifest are
|
|
85
|
+
// authored knowledge and still require evidence.
|
|
86
|
+
if (isDerivedKbPath(normalizedPath)) {
|
|
87
|
+
return true;
|
|
57
88
|
}
|
|
58
89
|
return false;
|
|
59
90
|
}
|
|
@@ -77,7 +108,7 @@ function getFilename(normalizedPath) {
|
|
|
77
108
|
export function classifyMeaningfulChange(params) {
|
|
78
109
|
const { normalizedPath, pathKind, lifecycle: _lifecycle, riskClass } = params;
|
|
79
110
|
// 1. IGNORED paths (for freshness contract)
|
|
80
|
-
if (isIgnoredPath(normalizedPath
|
|
111
|
+
if (isIgnoredPath(normalizedPath)) {
|
|
81
112
|
return "ignored";
|
|
82
113
|
}
|
|
83
114
|
// 2. ADVISORY paths (low-risk lockfiles)
|
|
@@ -94,9 +125,8 @@ export function classifyMeaningfulChange(params) {
|
|
|
94
125
|
if (UPGRADE_RISK_CLASSES.has(riskClass)) {
|
|
95
126
|
return "requires-kb-evidence";
|
|
96
127
|
}
|
|
97
|
-
if (riskClass
|
|
128
|
+
if (isSafeDocsUnknownPath(riskClass, pathKind))
|
|
98
129
|
return "advisory";
|
|
99
|
-
}
|
|
100
130
|
}
|
|
101
131
|
// 5. DEFAULT — safety default for unknown paths
|
|
102
132
|
return "requires-kb-evidence";
|