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/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.\n\nDo not invoke Kibi CLI commands directly from the agent.\n\n${buildInitKibiBootstrapReference(capability)}`;
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
- version?: string;
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;
@@ -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
- const displayMessage = cfg.version
6
- ? `${message} (v${cfg.version})`
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.log({
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
- }).catch(() => {
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.log({
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
- }).catch(() => {
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.log({
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
- }).catch(() => {
79
+ })
80
+ .catch(() => {
55
81
  // Advisory log failure stays silent
56
82
  });
57
83
  }
58
84
  });
59
85
  }
60
- void Promise.resolve(client.app.log({
61
- body: {
62
- service: "kibi-opencode",
63
- level: "info",
64
- message,
65
- ...(cfg.version ? { version: cfg.version } : {}),
66
- ...(cfg.directory ? { directory: cfg.directory } : {}),
67
- },
68
- })).catch((err) => {
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.13.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.0"
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
+ }