kibi-opencode 0.13.0 → 0.15.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 +18 -4
- package/dist/config.d.ts +1 -1
- package/dist/config.js +2 -2
- package/dist/enforcement-policy.d.ts +73 -0
- package/dist/enforcement-policy.js +279 -0
- package/dist/enforcement-scope.d.ts +15 -0
- package/dist/enforcement-scope.js +36 -0
- package/dist/file-operation-reminders.d.ts +13 -0
- package/dist/file-operation-reminders.js +24 -37
- package/dist/guidance-cache.d.ts +3 -0
- package/dist/guidance-cache.js +1 -1
- package/dist/kb-freshness-state.d.ts +62 -0
- package/dist/kb-freshness-state.js +211 -0
- package/dist/kibi-checkpoint-runner.d.ts +83 -0
- package/dist/kibi-checkpoint-runner.js +254 -0
- package/dist/meaningful-change-classifier.d.ts +30 -0
- package/dist/meaningful-change-classifier.js +104 -0
- package/dist/plugin.d.ts +1 -0
- package/dist/plugin.js +480 -177
- package/dist/prompt.d.ts +11 -0
- package/dist/prompt.js +54 -2
- package/dist/smart-enforcement.d.ts +6 -2
- package/dist/smart-enforcement.js +7 -1
- package/dist/startup-notifier.d.ts +7 -1
- package/dist/startup-notifier.js +51 -17
- package/dist/version-metadata.d.ts +13 -0
- package/dist/version-metadata.js +78 -0
- package/dist/version-metadata.json +1 -0
- package/dist/work-context-resolver.d.ts +21 -0
- package/dist/work-context-resolver.js +197 -0
- package/package.json +4 -4
package/dist/prompt.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { PathKind } from "./path-kind.js";
|
|
|
7
7
|
import type { RepoPosture } from "./repo-posture.js";
|
|
8
8
|
import type { RiskClass } from "./risk-classifier.js";
|
|
9
9
|
import type { WorkspaceHealth } from "./workspace-health.js";
|
|
10
|
+
import type { KbFreshnessEvaluation } from "./kb-freshness-state.js";
|
|
10
11
|
declare const SENTINEL = "<!-- kibi-opencode -->";
|
|
11
12
|
export declare function buildAutoBriefingGuidance(autoBriefResult: BriefingRuntimeResult | undefined, completionReminder: boolean): string | null;
|
|
12
13
|
export interface PromptContext {
|
|
@@ -47,6 +48,16 @@ export interface PromptContext {
|
|
|
47
48
|
lifecycleReminder: string | null;
|
|
48
49
|
e2eReminder: string | null;
|
|
49
50
|
};
|
|
51
|
+
/** Hard-mode checkpoint block that must take prompt priority over advisory guidance. */
|
|
52
|
+
hardGateBlock?: {
|
|
53
|
+
shownPaths: string[];
|
|
54
|
+
remainingCount: number;
|
|
55
|
+
reason?: string;
|
|
56
|
+
};
|
|
57
|
+
/** KB freshness evaluation from evidence store */
|
|
58
|
+
kbFreshness?: KbFreshnessEvaluation;
|
|
59
|
+
/** Paths changed for freshness evaluation (capped at 5) */
|
|
60
|
+
freshnessChangedPaths?: string[];
|
|
50
61
|
}
|
|
51
62
|
export declare function postureGuidance(posture: RepoPosture, capability?: InitKibiCommandCapability): string | null;
|
|
52
63
|
/**
|
package/dist/prompt.js
CHANGED
|
@@ -110,7 +110,29 @@ function buildBootstrapRequiredBody(capability = getInitKibiCommandCapability())
|
|
|
110
110
|
const commandBullet = capability.supported
|
|
111
111
|
? "- When the Kibi OpenCode plugin is active and native injection is supported, use `/init-kibi` as the canonical short alias; `/kibi:init-kibi:mcp` remains the namespaced MCP fallback."
|
|
112
112
|
: "- This host does not support native `/init-kibi` injection. Kibi must fail closed and does not register a fake native alias; use `/kibi:init-kibi:mcp` instead.";
|
|
113
|
-
return `This repository does not appear to have Kibi initialized. Agents should:\n${commandBullet}\n- The workflow uses \`kb_autopilot_generate\` for read-only synthesis; always preview and get approval before writes.\n- Ask the user/operator to run setup or repair outside this session if bootstrap is insufficient.\n\nUse public MCP tools only: \`kb_autopilot_generate\`, \`kb_search\`, \`kb_query\`, \`kb_status\`, \`kb_find_gaps\`, \`kb_coverage\`, \`kb_graph\`, \`kb_upsert\`, \`kb_delete\`, \`kb_check\`.`;
|
|
113
|
+
return `This repository does not appear to have Kibi initialized. Agents should:\n${commandBullet}\n- The workflow uses \`kb_autopilot_generate\` for read-only synthesis; always preview and get approval before writes.\n- Ask the user/operator to run setup or repair outside this session if bootstrap is insufficient.\n- For comprehensive Kibi usage guidance, use \`kb_skills_load\` with skill id \`kibi-usage\`.\n\nUse public MCP tools only: \`kb_autopilot_generate\`, \`kb_search\`, \`kb_query\`, \`kb_status\`, \`kb_find_gaps\`, \`kb_coverage\`, \`kb_graph\`, \`kb_upsert\`, \`kb_delete\`, \`kb_check\`, \`kb_skills_load\`.`;
|
|
114
|
+
}
|
|
115
|
+
function buildHardGateBlock(block) {
|
|
116
|
+
const pathLines = block.shownPaths.map((path) => `- \`${path}\``);
|
|
117
|
+
if (block.remainingCount > 0) {
|
|
118
|
+
pathLines.push(`- +${block.remainingCount} more dirty files`);
|
|
119
|
+
}
|
|
120
|
+
const reasonLine = block.reason ? `Reason: ${block.reason}.` : null;
|
|
121
|
+
return [
|
|
122
|
+
"🛑 Kibi hard gate blocked",
|
|
123
|
+
"STOP implementation until this authoritative Kibi checkpoint is satisfied.",
|
|
124
|
+
reasonLine,
|
|
125
|
+
"Affected files:",
|
|
126
|
+
...pathLines,
|
|
127
|
+
"MCP-only recovery steps:",
|
|
128
|
+
"- Run `kb_search` for impacted requirements, tests, ADRs, and facts.",
|
|
129
|
+
"- Run `kb_query` with `sourceFile` for each affected file.",
|
|
130
|
+
"- Run `kb_status` if branch or snapshot freshness matters.",
|
|
131
|
+
"- Run `kb_upsert` for required traceability, relationship, or fact updates.",
|
|
132
|
+
"- Run `kb_check` before continuing.",
|
|
133
|
+
]
|
|
134
|
+
.filter((line) => line !== null)
|
|
135
|
+
.join("\n");
|
|
114
136
|
}
|
|
115
137
|
// ── Guidance blocks by risk class ──────────────────────────────────────
|
|
116
138
|
const GUIDANCE_BY_RISK = {
|
|
@@ -171,6 +193,9 @@ Root .kb/config.json exists but some configured KB targets are missing. Guidance
|
|
|
171
193
|
* Build prompt guidance block based on posture, risk class, and cache state.
|
|
172
194
|
*/
|
|
173
195
|
function buildContextualGuidance(context, capability = getInitKibiCommandCapability()) {
|
|
196
|
+
if (context.hardGateBlock) {
|
|
197
|
+
return `${SENTINEL}\n\n${buildHardGateBlock(context.hardGateBlock)}`;
|
|
198
|
+
}
|
|
174
199
|
const posture = context.posture ?? "root_active";
|
|
175
200
|
const riskClass = context.riskClass;
|
|
176
201
|
const readyAutoBriefingAvailable = context.autoBriefResult?.showManualCue === false;
|
|
@@ -215,6 +240,7 @@ ${buildBootstrapRequiredBody(capability)}`;
|
|
|
215
240
|
context.cache &&
|
|
216
241
|
context.workspaceRoot &&
|
|
217
242
|
context.branch &&
|
|
243
|
+
!context.kbFreshness?.requiresEvidence &&
|
|
218
244
|
riskClass) {
|
|
219
245
|
const focusEdit = getFocusEdit(context);
|
|
220
246
|
const key = {
|
|
@@ -430,6 +456,26 @@ The Kibi workspace is in a maintenance-degraded state. Guidance remains advisory
|
|
|
430
456
|
posture !== "root_partial") {
|
|
431
457
|
finalBlock = `${finalBlock}\n- Kibi impact evidence is required before completion/commit: run \`kb_check\` before completing this task.`;
|
|
432
458
|
}
|
|
459
|
+
// Prepend KB freshness block when evidence is required
|
|
460
|
+
if (context.kbFreshness?.requiresEvidence) {
|
|
461
|
+
const changedListing = context.freshnessChangedPaths?.length
|
|
462
|
+
? `Changed: ${context.freshnessChangedPaths.slice(0, 5).map(p => `\`${p}\``).join(", ")}`
|
|
463
|
+
: null;
|
|
464
|
+
const freshnessLines = [
|
|
465
|
+
"🧠 **Kibi freshness required**",
|
|
466
|
+
changedListing,
|
|
467
|
+
`State: ${context.kbFreshness.state}. ${context.kbFreshness.reason}`,
|
|
468
|
+
...(context.kbFreshness.missingEvidence.length > 0
|
|
469
|
+
? [`Missing: ${context.kbFreshness.missingEvidence.join(", ")}`]
|
|
470
|
+
: []),
|
|
471
|
+
"Resolution:",
|
|
472
|
+
"- **KB updated**: run `kb_search` for discovery, then `kb_upsert`/`kb_delete` for mutations, then `kb_check`.",
|
|
473
|
+
"- **No KB impact**: include a no-impact rationale in your final report after source-linked discovery via `kb_search`/`kb_query(sourceFile=...)` and `kb_check`.",
|
|
474
|
+
].filter(Boolean).join("\n");
|
|
475
|
+
finalBlock = finalBlock
|
|
476
|
+
? `${freshnessLines}\n\n${finalBlock}`
|
|
477
|
+
: freshnessLines;
|
|
478
|
+
}
|
|
433
479
|
// Return: sentinel + one targeted block (or just sentinel if no block)
|
|
434
480
|
return finalBlock
|
|
435
481
|
? `${SENTINEL}\n\n${finalBlock}`
|
|
@@ -506,7 +552,13 @@ Dogfood note for this repo: OpenCode here uses local built \`kibi-mcp\` and \`ki
|
|
|
506
552
|
6. **Validate**: Run kb_check after KB mutations to catch violations early.
|
|
507
553
|
7. **Before completion/commit**: Kibi impact evidence is required before completion/commit. If extraction output changes, refresh documentation/symbols.yaml and do not revert that update as scope creep.
|
|
508
554
|
|
|
509
|
-
**Public Kibi tools only:** kb_autopilot_generate, kb_search, kb_query, kb_status, kb_find_gaps, kb_coverage, kb_graph, kb_upsert, kb_delete, kb_check
|
|
555
|
+
**Public Kibi tools only:** kb_autopilot_generate, kb_search, kb_query, kb_status, kb_find_gaps, kb_coverage, kb_graph, kb_upsert, kb_delete, kb_check, kb_skills_load.
|
|
556
|
+
|
|
557
|
+
For comprehensive Kibi usage guidance (relationships, fact lanes, workflows), use \`kb_skills_load\` with skill id \`kibi-usage\`.
|
|
558
|
+
|
|
559
|
+
Do not invoke Kibi CLI commands directly from the agent.
|
|
560
|
+
|
|
561
|
+
${buildInitKibiBootstrapReference(capability)}`;
|
|
510
562
|
}
|
|
511
563
|
/**
|
|
512
564
|
* Build prompt with contextual guidance based on posture, risk class, and cache state.
|
|
@@ -5,14 +5,16 @@ import type { RepoPosture } from "./repo-posture.js";
|
|
|
5
5
|
* - "strict": plugin may escalate targeted checks, completion reminders, and
|
|
6
6
|
* structured logging. Hooks/checks remain the hard enforcement boundary
|
|
7
7
|
* regardless of mode.
|
|
8
|
+
* - "hard": authoritative root-KB postures may hard-block through durable
|
|
9
|
+
* hook/check boundaries, even when maintenance guidance is degraded.
|
|
8
10
|
*/
|
|
9
|
-
export type EffectiveMode = "advisory" | "strict";
|
|
11
|
+
export type EffectiveMode = "advisory" | "strict" | "hard";
|
|
10
12
|
/**
|
|
11
13
|
* Inputs required to determine the effective smart-enforcement mode.
|
|
12
14
|
*/
|
|
13
15
|
export interface ModeInputs {
|
|
14
16
|
/** Configured smart-enforcement mode. */
|
|
15
|
-
mode: "advisory" | "strict";
|
|
17
|
+
mode: "advisory" | "strict" | "hard";
|
|
16
18
|
/** When true, strict mode only activates for authoritative root KB postures. */
|
|
17
19
|
requireRootKbForStrict: boolean;
|
|
18
20
|
/** Current repository posture from detectPosture(). */
|
|
@@ -37,5 +39,7 @@ export declare function isStrictEligible(inputs: ModeInputs): boolean;
|
|
|
37
39
|
* - strict config + requireRootKbForStrict=false → strict may apply to all
|
|
38
40
|
* postures (but hooks/checks remain hard gate regardless)
|
|
39
41
|
* - maintenance-degraded → advisory regardless of config
|
|
42
|
+
* - hard config → hard only for authoritative root-KB postures, even when
|
|
43
|
+
* maintenance is degraded; non-authoritative postures stay advisory
|
|
40
44
|
*/
|
|
41
45
|
export declare function computeEffectiveMode(inputs: ModeInputs): EffectiveMode;
|
|
@@ -30,9 +30,15 @@ export function isStrictEligible(inputs) {
|
|
|
30
30
|
* - strict config + requireRootKbForStrict=false → strict may apply to all
|
|
31
31
|
* postures (but hooks/checks remain hard gate regardless)
|
|
32
32
|
* - maintenance-degraded → advisory regardless of config
|
|
33
|
+
* - hard config → hard only for authoritative root-KB postures, even when
|
|
34
|
+
* maintenance is degraded; non-authoritative postures stay advisory
|
|
33
35
|
*/
|
|
36
|
+
// implements REQ-opencode-worktree-hard-enforcement-v1
|
|
34
37
|
export function computeEffectiveMode(inputs) {
|
|
35
|
-
// implements REQ-opencode-smart-enforcement-v1
|
|
38
|
+
// implements REQ-opencode-smart-enforcement-v1, REQ-opencode-worktree-hard-enforcement-v1
|
|
39
|
+
if (inputs.mode === "hard") {
|
|
40
|
+
return STRICT_ELIGIBLE_POSTURES.has(inputs.posture) ? "hard" : "advisory";
|
|
41
|
+
}
|
|
36
42
|
// Maintenance-degraded always forces advisory
|
|
37
43
|
if (inputs.maintenanceDegraded) {
|
|
38
44
|
return "advisory";
|
|
@@ -6,8 +6,14 @@ export type StartupNotifierClient = ToastCapableClient & {
|
|
|
6
6
|
};
|
|
7
7
|
};
|
|
8
8
|
export type StartupNotifierConfig = {
|
|
9
|
-
|
|
9
|
+
versions?: {
|
|
10
|
+
opencode?: string;
|
|
11
|
+
mcp?: string;
|
|
12
|
+
cli?: string;
|
|
13
|
+
core?: string;
|
|
14
|
+
};
|
|
10
15
|
suppressToast?: boolean;
|
|
11
16
|
directory?: string;
|
|
17
|
+
versionMetadataSource?: string;
|
|
12
18
|
};
|
|
13
19
|
export declare function notifyStartup(client: StartupNotifierClient, cfg: StartupNotifierConfig): void;
|
package/dist/startup-notifier.js
CHANGED
|
@@ -1,10 +1,30 @@
|
|
|
1
1
|
import { sendToast, } from "./toast.js";
|
|
2
|
+
const VERSION_ORDER = ["opencode", "mcp", "cli", "core"];
|
|
2
3
|
// implements REQ-opencode-kibi-plugin-v1
|
|
3
4
|
export function notifyStartup(client, cfg) {
|
|
4
5
|
const message = "kibi-opencode started";
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
// Build toast message from known versions in order
|
|
7
|
+
const versionParts = [];
|
|
8
|
+
if (cfg.versions) {
|
|
9
|
+
for (const key of VERSION_ORDER) {
|
|
10
|
+
const val = cfg.versions[key];
|
|
11
|
+
if (val !== undefined) {
|
|
12
|
+
versionParts.push(`${key} v${val}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const displayMessage = versionParts.length > 0
|
|
17
|
+
? `${message} (${versionParts.join(", ")})`
|
|
7
18
|
: message;
|
|
19
|
+
// Compute unknown versions (keys from VERSION_ORDER not present in cfg.versions)
|
|
20
|
+
const unknownVersions = [];
|
|
21
|
+
if (cfg.versions) {
|
|
22
|
+
for (const key of VERSION_ORDER) {
|
|
23
|
+
if (cfg.versions[key] === undefined) {
|
|
24
|
+
unknownVersions.push(key);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
8
28
|
const toastPayload = {
|
|
9
29
|
variant: "success",
|
|
10
30
|
title: "Kibi OpenCode",
|
|
@@ -18,31 +38,36 @@ export function notifyStartup(client, cfg) {
|
|
|
18
38
|
...(cfg.directory ? { directory: cfg.directory } : {}),
|
|
19
39
|
};
|
|
20
40
|
if (result.status === "delivered") {
|
|
21
|
-
void client.app
|
|
41
|
+
void client.app
|
|
42
|
+
.log({
|
|
22
43
|
body: {
|
|
23
44
|
...base,
|
|
24
45
|
level: "info",
|
|
25
46
|
message: "startup toast delivered",
|
|
26
47
|
transport: result.transport,
|
|
27
48
|
},
|
|
28
|
-
})
|
|
49
|
+
})
|
|
50
|
+
.catch(() => {
|
|
29
51
|
// Advisory log failure stays silent
|
|
30
52
|
});
|
|
31
53
|
}
|
|
32
54
|
else if (result.status === "unavailable") {
|
|
33
|
-
void client.app
|
|
55
|
+
void client.app
|
|
56
|
+
.log({
|
|
34
57
|
body: {
|
|
35
58
|
...base,
|
|
36
59
|
level: "info",
|
|
37
60
|
message: "startup toast unavailable",
|
|
38
61
|
reason: result.reason,
|
|
39
62
|
},
|
|
40
|
-
})
|
|
63
|
+
})
|
|
64
|
+
.catch(() => {
|
|
41
65
|
// Advisory log failure stays silent
|
|
42
66
|
});
|
|
43
67
|
}
|
|
44
68
|
else if (result.status === "failed") {
|
|
45
|
-
void client.app
|
|
69
|
+
void client.app
|
|
70
|
+
.log({
|
|
46
71
|
body: {
|
|
47
72
|
...base,
|
|
48
73
|
level: "warn",
|
|
@@ -51,21 +76,30 @@ export function notifyStartup(client, cfg) {
|
|
|
51
76
|
reason: result.reason,
|
|
52
77
|
...(result.error ? { error: result.error } : {}),
|
|
53
78
|
},
|
|
54
|
-
})
|
|
79
|
+
})
|
|
80
|
+
.catch(() => {
|
|
55
81
|
// Advisory log failure stays silent
|
|
56
82
|
});
|
|
57
83
|
}
|
|
58
84
|
});
|
|
59
85
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
86
|
+
// Structured startup log
|
|
87
|
+
const logBody = {
|
|
88
|
+
service: "kibi-opencode",
|
|
89
|
+
level: "info",
|
|
90
|
+
message,
|
|
91
|
+
};
|
|
92
|
+
if (cfg.versions && Object.keys(cfg.versions).length > 0) {
|
|
93
|
+
logBody.versions = cfg.versions;
|
|
94
|
+
logBody.unknownVersions = unknownVersions;
|
|
95
|
+
}
|
|
96
|
+
if (cfg.versionMetadataSource) {
|
|
97
|
+
logBody.versionMetadataSource = cfg.versionMetadataSource;
|
|
98
|
+
}
|
|
99
|
+
if (cfg.directory) {
|
|
100
|
+
logBody.directory = cfg.directory;
|
|
101
|
+
}
|
|
102
|
+
void Promise.resolve(client.app.log({ body: logBody })).catch(() => {
|
|
69
103
|
// Advisory log failure stays silent
|
|
70
104
|
});
|
|
71
105
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface KibiPackageVersions {
|
|
2
|
+
opencode: string;
|
|
3
|
+
mcp: string;
|
|
4
|
+
cli: string;
|
|
5
|
+
core: string;
|
|
6
|
+
source: "generated-dist" | "workspace-packages" | "unknown";
|
|
7
|
+
missing: string[];
|
|
8
|
+
}
|
|
9
|
+
export interface ReadKibiPackageVersionsOptions {
|
|
10
|
+
baseUrl?: string | URL;
|
|
11
|
+
readFileSync?: (path: string | URL, encoding: "utf-8") => string;
|
|
12
|
+
}
|
|
13
|
+
export declare function readKibiPackageVersions(options?: ReadKibiPackageVersionsOptions): KibiPackageVersions;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { readFileSync as fsReadFileSync } from "node:fs";
|
|
2
|
+
// implements REQ-opencode-toast-package-versions-v1
|
|
3
|
+
export function readKibiPackageVersions(options) {
|
|
4
|
+
const baseUrl = options?.baseUrl ?? import.meta.url;
|
|
5
|
+
const readFs = options?.readFileSync ?? fsReadFileSync;
|
|
6
|
+
// Tier 1: generated dist JSON (build-time artifact)
|
|
7
|
+
try {
|
|
8
|
+
const distUrl = new URL("./version-metadata.json", baseUrl);
|
|
9
|
+
const raw = readFs(distUrl, "utf-8");
|
|
10
|
+
const parsed = JSON.parse(raw);
|
|
11
|
+
if (typeof parsed.opencode === "string" &&
|
|
12
|
+
typeof parsed.mcp === "string" &&
|
|
13
|
+
typeof parsed.cli === "string" &&
|
|
14
|
+
typeof parsed.core === "string") {
|
|
15
|
+
return {
|
|
16
|
+
opencode: parsed.opencode,
|
|
17
|
+
mcp: parsed.mcp,
|
|
18
|
+
cli: parsed.cli,
|
|
19
|
+
core: parsed.core,
|
|
20
|
+
source: "generated-dist",
|
|
21
|
+
missing: [],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// fall through
|
|
27
|
+
}
|
|
28
|
+
// Tier 2: workspace package.json files
|
|
29
|
+
try {
|
|
30
|
+
const entries = [
|
|
31
|
+
["opencode", "../package.json"],
|
|
32
|
+
["mcp", "../../mcp/package.json"],
|
|
33
|
+
["cli", "../../cli/package.json"],
|
|
34
|
+
["core", "../../core/package.json"],
|
|
35
|
+
];
|
|
36
|
+
const versions = {};
|
|
37
|
+
const missing = [];
|
|
38
|
+
for (const [name, relPath] of entries) {
|
|
39
|
+
try {
|
|
40
|
+
const pkgUrl = new URL(relPath, baseUrl);
|
|
41
|
+
const raw = readFs(pkgUrl, "utf-8");
|
|
42
|
+
const pkg = JSON.parse(raw);
|
|
43
|
+
versions[name] =
|
|
44
|
+
typeof pkg.version === "string" ? pkg.version : "unknown";
|
|
45
|
+
if (typeof pkg.version !== "string") {
|
|
46
|
+
missing.push(name);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
versions[name] = "unknown";
|
|
51
|
+
missing.push(name);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Only claim workspace-packages if at least one resolved
|
|
55
|
+
if (Object.values(versions).some((v) => v !== "unknown")) {
|
|
56
|
+
return {
|
|
57
|
+
opencode: versions.opencode ?? "unknown",
|
|
58
|
+
mcp: versions.mcp ?? "unknown",
|
|
59
|
+
cli: versions.cli ?? "unknown",
|
|
60
|
+
core: versions.core ?? "unknown",
|
|
61
|
+
source: "workspace-packages",
|
|
62
|
+
missing: missing.length > 0 ? missing : [],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// fall through
|
|
68
|
+
}
|
|
69
|
+
// Tier 3: all unknown — never throws
|
|
70
|
+
return {
|
|
71
|
+
opencode: "unknown",
|
|
72
|
+
mcp: "unknown",
|
|
73
|
+
cli: "unknown",
|
|
74
|
+
core: "unknown",
|
|
75
|
+
source: "unknown",
|
|
76
|
+
missing: ["opencode", "mcp", "cli", "core"],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"opencode":"0.15.0","mcp":"0.14.2","cli":"0.11.1","core":"0.5.3"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type RepoPosture } from "./repo-posture.js";
|
|
2
|
+
export interface ResolveWorkContextInput {
|
|
3
|
+
inputDirectory: string;
|
|
4
|
+
inputWorktree: string;
|
|
5
|
+
filePath?: string;
|
|
6
|
+
sessionId?: string;
|
|
7
|
+
agentIdentity?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface WorkContext {
|
|
10
|
+
worktreeRoot: string;
|
|
11
|
+
kibiAuthorityRoot: string;
|
|
12
|
+
branch: string;
|
|
13
|
+
repoRelativePath: string;
|
|
14
|
+
posture: RepoPosture;
|
|
15
|
+
isAuthoritative: boolean;
|
|
16
|
+
isLinkedWorktree: boolean;
|
|
17
|
+
sessionId: string | undefined;
|
|
18
|
+
agentIdentity: string;
|
|
19
|
+
}
|
|
20
|
+
declare const resolveWorkContext: (input: ResolveWorkContextInput) => WorkContext;
|
|
21
|
+
export { resolveWorkContext };
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep, } from "node:path";
|
|
3
|
+
import { detectPosture } from "./repo-posture.js";
|
|
4
|
+
function safeStat(path) {
|
|
5
|
+
try {
|
|
6
|
+
return statSync(path);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function readFirstLine(path) {
|
|
13
|
+
try {
|
|
14
|
+
const [firstLine] = readFileSync(path, "utf8").split(/\r?\n/, 1);
|
|
15
|
+
const trimmed = firstLine?.trim();
|
|
16
|
+
return trimmed && trimmed.length > 0 ? trimmed : null;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function readLinkedGitDir(worktreeRoot, gitFilePath) {
|
|
23
|
+
const firstLine = readFirstLine(gitFilePath);
|
|
24
|
+
if (!firstLine?.startsWith("gitdir:")) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const rawGitDir = firstLine.slice("gitdir:".length).trim();
|
|
28
|
+
if (rawGitDir.length === 0) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return isAbsolute(rawGitDir)
|
|
32
|
+
? resolve(rawGitDir)
|
|
33
|
+
: resolve(worktreeRoot, rawGitDir);
|
|
34
|
+
}
|
|
35
|
+
function readCommonGitDir(gitDir) {
|
|
36
|
+
const rawCommonDir = readFirstLine(join(gitDir, "commondir"));
|
|
37
|
+
if (!rawCommonDir) {
|
|
38
|
+
return gitDir;
|
|
39
|
+
}
|
|
40
|
+
return isAbsolute(rawCommonDir)
|
|
41
|
+
? resolve(rawCommonDir)
|
|
42
|
+
: resolve(gitDir, rawCommonDir);
|
|
43
|
+
}
|
|
44
|
+
function directorySearchStart(candidatePath) {
|
|
45
|
+
const resolved = resolve(candidatePath);
|
|
46
|
+
const stats = safeStat(resolved);
|
|
47
|
+
return stats?.isDirectory() ? resolved : dirname(resolved);
|
|
48
|
+
}
|
|
49
|
+
function findGitMetadata(candidatePath) {
|
|
50
|
+
let current = directorySearchStart(candidatePath);
|
|
51
|
+
while (true) {
|
|
52
|
+
const dotGitPath = join(current, ".git");
|
|
53
|
+
const dotGitStats = safeStat(dotGitPath);
|
|
54
|
+
if (dotGitStats?.isFile()) {
|
|
55
|
+
const gitDir = readLinkedGitDir(current, dotGitPath);
|
|
56
|
+
if (gitDir) {
|
|
57
|
+
return {
|
|
58
|
+
worktreeRoot: current,
|
|
59
|
+
gitDir,
|
|
60
|
+
commonGitDir: readCommonGitDir(gitDir),
|
|
61
|
+
isLinkedWorktree: true,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (dotGitStats?.isDirectory()) {
|
|
66
|
+
return {
|
|
67
|
+
worktreeRoot: current,
|
|
68
|
+
gitDir: dotGitPath,
|
|
69
|
+
commonGitDir: dotGitPath,
|
|
70
|
+
isLinkedWorktree: false,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const parent = dirname(current);
|
|
74
|
+
if (parent === current) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
current = parent;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function hasRootKbConfig(root) {
|
|
81
|
+
return existsSync(join(root, ".kb", "config.json"));
|
|
82
|
+
}
|
|
83
|
+
function uniqueResolved(paths) {
|
|
84
|
+
const seen = new Set();
|
|
85
|
+
const result = [];
|
|
86
|
+
for (const path of paths) {
|
|
87
|
+
if (!path) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const resolved = resolve(path);
|
|
91
|
+
if (seen.has(resolved)) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
seen.add(resolved);
|
|
95
|
+
result.push(resolved);
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
function authorityRootFromCommonGitDir(commonGitDir) {
|
|
100
|
+
return basename(commonGitDir) === ".git" ? dirname(commonGitDir) : null;
|
|
101
|
+
}
|
|
102
|
+
function authorityRootFromLinkedGitDir(gitDir) {
|
|
103
|
+
const normalizedGitDir = resolve(gitDir);
|
|
104
|
+
const marker = `${sep}.git${sep}worktrees${sep}`;
|
|
105
|
+
const markerIndex = normalizedGitDir.indexOf(marker);
|
|
106
|
+
if (markerIndex < 0) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
return normalizedGitDir.slice(0, markerIndex);
|
|
110
|
+
}
|
|
111
|
+
function ancestorKbRoots(start) {
|
|
112
|
+
const roots = [];
|
|
113
|
+
let current = resolve(start);
|
|
114
|
+
while (true) {
|
|
115
|
+
if (hasRootKbConfig(current)) {
|
|
116
|
+
roots.push(current);
|
|
117
|
+
}
|
|
118
|
+
const parent = dirname(current);
|
|
119
|
+
if (parent === current) {
|
|
120
|
+
return roots;
|
|
121
|
+
}
|
|
122
|
+
current = parent;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function resolveAuthorityRoot(git, inputDirectory) {
|
|
126
|
+
if (!git.isLinkedWorktree) {
|
|
127
|
+
return git.worktreeRoot;
|
|
128
|
+
}
|
|
129
|
+
const candidates = uniqueResolved([
|
|
130
|
+
authorityRootFromCommonGitDir(git.commonGitDir),
|
|
131
|
+
authorityRootFromLinkedGitDir(git.gitDir),
|
|
132
|
+
...ancestorKbRoots(git.worktreeRoot),
|
|
133
|
+
inputDirectory,
|
|
134
|
+
git.worktreeRoot,
|
|
135
|
+
]);
|
|
136
|
+
return (candidates.find((candidate) => hasRootKbConfig(candidate)) ??
|
|
137
|
+
candidates[0] ??
|
|
138
|
+
git.worktreeRoot);
|
|
139
|
+
}
|
|
140
|
+
function resolveBranch(git) {
|
|
141
|
+
if (!git) {
|
|
142
|
+
return "unknown";
|
|
143
|
+
}
|
|
144
|
+
const head = readFirstLine(join(git.gitDir, "HEAD"));
|
|
145
|
+
if (!head) {
|
|
146
|
+
return "unknown";
|
|
147
|
+
}
|
|
148
|
+
if (!head.startsWith("ref:")) {
|
|
149
|
+
return "HEAD";
|
|
150
|
+
}
|
|
151
|
+
const ref = head.slice("ref:".length).trim();
|
|
152
|
+
if (!ref.startsWith("refs/heads/")) {
|
|
153
|
+
return "HEAD";
|
|
154
|
+
}
|
|
155
|
+
const branch = ref.slice("refs/heads/".length);
|
|
156
|
+
return branch === "master" ? "main" : branch;
|
|
157
|
+
}
|
|
158
|
+
function normalizeRepoRelativePath(fromRoot, targetPath) {
|
|
159
|
+
const rawRelativePath = relative(fromRoot, targetPath);
|
|
160
|
+
if (rawRelativePath.length === 0) {
|
|
161
|
+
return ".";
|
|
162
|
+
}
|
|
163
|
+
return rawRelativePath.split(sep).join("/");
|
|
164
|
+
}
|
|
165
|
+
function authoritativeForPosture(posture) {
|
|
166
|
+
return posture === "root_active" || posture === "hybrid_root_plus_vendored";
|
|
167
|
+
}
|
|
168
|
+
// implements REQ-opencode-worktree-hard-enforcement-v1
|
|
169
|
+
const resolveWorkContext = function resolveWorkContext(input) {
|
|
170
|
+
const declaredWorktreeRoot = resolve(input.inputWorktree || input.inputDirectory);
|
|
171
|
+
const declaredDirectory = resolve(input.inputDirectory || input.inputWorktree);
|
|
172
|
+
const absoluteFilePath = input.filePath
|
|
173
|
+
? isAbsolute(input.filePath)
|
|
174
|
+
? resolve(input.filePath)
|
|
175
|
+
: resolve(declaredWorktreeRoot, input.filePath)
|
|
176
|
+
: undefined;
|
|
177
|
+
const git = (absoluteFilePath ? findGitMetadata(absoluteFilePath) : null) ??
|
|
178
|
+
findGitMetadata(declaredWorktreeRoot);
|
|
179
|
+
const worktreeRoot = git?.worktreeRoot ?? declaredWorktreeRoot;
|
|
180
|
+
const kibiAuthorityRoot = git
|
|
181
|
+
? resolveAuthorityRoot(git, declaredDirectory)
|
|
182
|
+
: declaredWorktreeRoot;
|
|
183
|
+
const posture = detectPosture(kibiAuthorityRoot).state;
|
|
184
|
+
const repoRelativePath = normalizeRepoRelativePath(worktreeRoot, absoluteFilePath ?? worktreeRoot);
|
|
185
|
+
return {
|
|
186
|
+
worktreeRoot,
|
|
187
|
+
kibiAuthorityRoot,
|
|
188
|
+
branch: resolveBranch(git),
|
|
189
|
+
repoRelativePath,
|
|
190
|
+
posture,
|
|
191
|
+
isAuthoritative: authoritativeForPosture(posture),
|
|
192
|
+
isLinkedWorktree: git?.isLinkedWorktree ?? false,
|
|
193
|
+
sessionId: input.sessionId,
|
|
194
|
+
agentIdentity: input.agentIdentity ?? "unknown",
|
|
195
|
+
};
|
|
196
|
+
};
|
|
197
|
+
export { resolveWorkContext };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kibi-opencode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.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",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"access": "public"
|
|
53
53
|
},
|
|
54
54
|
"scripts": {
|
|
55
|
-
"build": "tsc -p tsconfig.json && bun run scripts/build-tui.ts && tsc -p tsconfig.tui-types.json",
|
|
55
|
+
"build": "tsc -p tsconfig.json && bun run scripts/generate-version-metadata.ts && bun run scripts/build-tui.ts && tsc -p tsconfig.tui-types.json",
|
|
56
56
|
"build:tui": "bun run scripts/build-tui.ts && tsc -p tsconfig.tui-types.json",
|
|
57
57
|
"dev": "tsc -p tsconfig.json --watch",
|
|
58
58
|
"clean": "rm -rf dist",
|
|
@@ -62,10 +62,10 @@
|
|
|
62
62
|
"@opencode-ai/plugin": "^1.4.7",
|
|
63
63
|
"@opentui/core": "^0.1.99",
|
|
64
64
|
"@opentui/solid": "^0.1.99",
|
|
65
|
-
"kibi-cli": "^0.11.
|
|
65
|
+
"kibi-cli": "^0.11.1"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@types/node": "latest",
|
|
69
69
|
"typescript": "^5.0.0"
|
|
70
70
|
}
|
|
71
|
-
}
|
|
71
|
+
}
|