kibi-opencode 0.5.4 → 0.6.1

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.
@@ -0,0 +1,196 @@
1
+ // implements REQ-opencode-smart-enforcement-v1, REQ-opencode-kibi-plugin-v1
2
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ // Default sync paths — must stay in sync with file-filter.ts DEFAULT_SYNC_PATHS
5
+ const DEFAULT_SYNC_PATHS = {
6
+ requirements: "documentation/requirements/**/*.md",
7
+ scenarios: "documentation/scenarios/**/*.md",
8
+ tests: "documentation/tests/**/*.md",
9
+ adr: "documentation/adr/**/*.md",
10
+ flags: "documentation/flags/**/*.md",
11
+ events: "documentation/events/**/*.md",
12
+ facts: "documentation/facts/**/*.md",
13
+ symbols: "documentation/symbols.yaml",
14
+ };
15
+ // ── helpers ────────────────────────────────────────────────────────────
16
+ function findVendoredTrees(cwd) {
17
+ const results = [];
18
+ const vendoredMarkers = [
19
+ ["kibi", "opencode.json"],
20
+ ["kibi", "package.json"],
21
+ ["kibi", "packages", "mcp"],
22
+ ["kibi", "documentation"],
23
+ ];
24
+ for (const marker of vendoredMarkers) {
25
+ const markerPath = join(cwd, ...marker);
26
+ if (existsSync(markerPath)) {
27
+ results.push(marker.join("/"));
28
+ }
29
+ }
30
+ const nodeModules = join(cwd, "node_modules");
31
+ if (existsSync(nodeModules)) {
32
+ for (const entry of readdirSync(nodeModules)) {
33
+ if (entry === "kibi" || entry.startsWith("kibi-")) {
34
+ results.push(`node_modules/${entry}`);
35
+ }
36
+ }
37
+ }
38
+ return [...new Set(results)];
39
+ }
40
+ /** Check if .kb/config.json exists at root. */
41
+ function rootKbConfigExists(cwd) {
42
+ return existsSync(join(cwd, ".kb", "config.json"));
43
+ }
44
+ /** Read and parse .kb/config.json. Returns null on failure. */
45
+ function readRootConfig(cwd) {
46
+ try {
47
+ return (JSON.parse(readFileSync(join(cwd, ".kb", "config.json"), "utf8")) || null);
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ }
53
+ /** Check if all configured KB targets resolve (directories / files exist). */
54
+ function rootTargetsAllResolve(cwd) {
55
+ const configPath = join(cwd, ".kb", "config.json");
56
+ let config = {};
57
+ try {
58
+ config = JSON.parse(readFileSync(configPath, "utf8")) || {};
59
+ }
60
+ catch {
61
+ return false;
62
+ }
63
+ const paths = config.paths;
64
+ const defaultKeys = [
65
+ "requirements",
66
+ "scenarios",
67
+ "tests",
68
+ "adr",
69
+ "flags",
70
+ "events",
71
+ "facts",
72
+ "symbols",
73
+ ];
74
+ for (const key of defaultKeys) {
75
+ const raw = paths?.[key] ?? DEFAULT_SYNC_PATHS[key];
76
+ // Normalize: strip trailing slashes and glob patterns to get the root dir/file path
77
+ const normalized = raw.replace(/\/+$/, "");
78
+ const isFile = normalized.endsWith(".yaml") || normalized.endsWith(".yml");
79
+ if (isFile) {
80
+ if (!existsSync(resolve(cwd, normalized)))
81
+ return false;
82
+ }
83
+ else {
84
+ // Strip first glob segment
85
+ const segments = normalized.split("/");
86
+ const rootSegments = [];
87
+ for (const seg of segments) {
88
+ if (seg.includes("*") || seg.includes("?") || seg.includes("["))
89
+ break;
90
+ rootSegments.push(seg);
91
+ }
92
+ const dirPath = rootSegments.join("/") || ".";
93
+ if (!existsSync(resolve(cwd, dirPath)))
94
+ return false;
95
+ }
96
+ }
97
+ return true;
98
+ }
99
+ /** Detect root-level Kibi intent (plugin config present but no .kb). */
100
+ function hasRootKibiIntent(cwd) {
101
+ if (existsSync(join(cwd, ".opencode", "kibi.json"))) {
102
+ return true;
103
+ }
104
+ try {
105
+ const oc = JSON.parse(readFileSync(join(cwd, "opencode.json"), "utf8"));
106
+ if (oc &&
107
+ Array.isArray(oc.plugin) &&
108
+ oc.plugin.some((p) => typeof p === "string" && p.includes("kibi"))) {
109
+ return true;
110
+ }
111
+ }
112
+ catch { }
113
+ for (const guidanceFile of [
114
+ "AGENTS.md",
115
+ join(".github", "copilot-instructions.md"),
116
+ ]) {
117
+ try {
118
+ const content = readFileSync(join(cwd, guidanceFile), "utf8");
119
+ if (content.includes("kb_query") || content.includes("kb_search")) {
120
+ return true;
121
+ }
122
+ }
123
+ catch { }
124
+ }
125
+ return false;
126
+ }
127
+ // ── main classifier ────────────────────────────────────────────────────
128
+ export function detectPosture(cwd) {
129
+ const hasRootConfig = rootKbConfigExists(cwd);
130
+ const vendoredTrees = findVendoredTrees(cwd);
131
+ // 1. hybrid_root_plus_vendored — root config AND vendored subtrees
132
+ if (hasRootConfig && vendoredTrees.length > 0) {
133
+ return {
134
+ state: "hybrid_root_plus_vendored",
135
+ needsBootstrap: false,
136
+ reason: `Root .kb/config.json exists alongside vendored tree(s): ${vendoredTrees.join(", ")}`,
137
+ maintenanceDegraded: false,
138
+ };
139
+ }
140
+ // 2-3. root config exists (check maintenance mode and target resolution)
141
+ if (hasRootConfig) {
142
+ const config = readRootConfig(cwd);
143
+ const maint = config?.maintenance;
144
+ const maintenanceDisabled = maint !== undefined && maint.enabled === false;
145
+ // maintenance_degraded overlay: maintenance explicitly disabled → root_active
146
+ if (maintenanceDisabled) {
147
+ return {
148
+ state: "root_active",
149
+ needsBootstrap: false,
150
+ reason: "Root .kb/config.json exists; maintenance mode explicitly disabled",
151
+ maintenanceDegraded: true,
152
+ };
153
+ }
154
+ const allResolve = rootTargetsAllResolve(cwd);
155
+ if (allResolve) {
156
+ return {
157
+ state: "root_active",
158
+ needsBootstrap: false,
159
+ reason: "Root .kb/config.json exists and all configured KB targets resolve",
160
+ maintenanceDegraded: false,
161
+ };
162
+ }
163
+ // 3. root_partial — root config exists but targets are missing
164
+ return {
165
+ state: "root_partial",
166
+ needsBootstrap: true,
167
+ reason: "Root .kb/config.json exists but some configured KB targets are missing",
168
+ maintenanceDegraded: false,
169
+ };
170
+ }
171
+ // 4. vendored_only — no root config, but vendored markers found
172
+ if (vendoredTrees.length > 0) {
173
+ return {
174
+ state: "vendored_only",
175
+ needsBootstrap: false,
176
+ reason: `No root .kb/config.json, but vendored Kibi tree(s) detected: ${vendoredTrees.join(", ")}`,
177
+ maintenanceDegraded: false,
178
+ };
179
+ }
180
+ // 5. root_uninitialized — no root .kb, no vendored trees, but root declares intent
181
+ if (hasRootKibiIntent(cwd)) {
182
+ return {
183
+ state: "root_uninitialized",
184
+ needsBootstrap: true,
185
+ reason: "No root .kb and no vendored trees, but Kibi plugin intent detected at root",
186
+ maintenanceDegraded: false,
187
+ };
188
+ }
189
+ // Fallback: treat as uninitialized (no kibi presence at all)
190
+ return {
191
+ state: "root_uninitialized",
192
+ needsBootstrap: true,
193
+ reason: "No Kibi presence detected in workspace",
194
+ maintenanceDegraded: false,
195
+ };
196
+ }
@@ -0,0 +1,39 @@
1
+ import type { PathKind } from "./path-kind.js";
2
+ /**
3
+ * Risk classification for file edits.
4
+ * Ordered from safest to riskiest for consumer convenience.
5
+ */
6
+ export type RiskClass = "safe_docs_only" | "safe_test_only" | "kb_doc_structural" | "req_policy_candidate" | "behavior_candidate" | "traceability_candidate" | "manual_kb_edit";
7
+ /**
8
+ * Input parameters for risk classification.
9
+ * All fields are cheaply computable — no AST parsing or async required.
10
+ * pathKind already encodes path-based classification from analyzePath().
11
+ */
12
+ export interface ClassifyRiskParams {
13
+ pathKind: PathKind;
14
+ isUnderKb: boolean;
15
+ hasMustPriority: boolean;
16
+ hasDurableComment: boolean;
17
+ fileContent: string;
18
+ }
19
+ /**
20
+ * Result of risk classification with the determined class and human-readable reasons.
21
+ */
22
+ export interface RiskClassification {
23
+ riskClass: RiskClass;
24
+ reasons: string[];
25
+ }
26
+ /**
27
+ * Classify a file edit into a deterministic risk class.
28
+ *
29
+ * Classification order (first match wins):
30
+ * 1. manual_kb_edit — file is under .kb/
31
+ * 2. safe_test_only — pathKind is "test"
32
+ * 3. req_policy_candidate — pathKind is "requirement"
33
+ * 4. kb_doc_structural — pathKind is "scenario", "adr", or "fact"
34
+ * 5. safe_docs_only — pathKind is "unknown" (markdown/config/etc.)
35
+ * 6. traceability_candidate — code with exports AND (hasDurableComment OR missing traceability)
36
+ * 7. behavior_candidate — code with exports, already has traceability, no durable comment
37
+ * 8. safe_docs_only — code without exports, or fallback
38
+ */
39
+ export declare function classifyRisk(params: ClassifyRiskParams): RiskClassification;
@@ -0,0 +1,111 @@
1
+ // implements REQ-opencode-smart-enforcement-v1, REQ-opencode-kibi-plugin-v1
2
+ /**
3
+ * Regex to detect exportable code constructs (functions, classes, variables).
4
+ */
5
+ const BEHAVIOR_PATTERN = /(?:export\s+(?:function|class|const|let|var)|\bclass\s+\w+|\bfunction\s+\w+|\bdef\s+\w+)/;
6
+ /**
7
+ * Regex to detect traceability annotations.
8
+ */
9
+ const TRACEABILITY_PATTERN = /implements\s+REQ-[A-Za-z0-9_-]+/i;
10
+ /**
11
+ * Classify a file edit into a deterministic risk class.
12
+ *
13
+ * Classification order (first match wins):
14
+ * 1. manual_kb_edit — file is under .kb/
15
+ * 2. safe_test_only — pathKind is "test"
16
+ * 3. req_policy_candidate — pathKind is "requirement"
17
+ * 4. kb_doc_structural — pathKind is "scenario", "adr", or "fact"
18
+ * 5. safe_docs_only — pathKind is "unknown" (markdown/config/etc.)
19
+ * 6. traceability_candidate — code with exports AND (hasDurableComment OR missing traceability)
20
+ * 7. behavior_candidate — code with exports, already has traceability, no durable comment
21
+ * 8. safe_docs_only — code without exports, or fallback
22
+ */
23
+ export function classifyRisk(params) {
24
+ const { pathKind, isUnderKb, hasMustPriority, hasDurableComment, fileContent, } = params;
25
+ // 1. manual_kb_edit — direct KB manipulation is always risky
26
+ if (isUnderKb) {
27
+ return {
28
+ riskClass: "manual_kb_edit",
29
+ reasons: ["File is under .kb/ — manual edits bypass KB validation"],
30
+ };
31
+ }
32
+ // 2. safe_test_only — test files are low-risk
33
+ if (pathKind === "test") {
34
+ return {
35
+ riskClass: "safe_test_only",
36
+ reasons: ["File is a test file — edits are low risk"],
37
+ };
38
+ }
39
+ // 3. req_policy_candidate — requirement docs need policy checks
40
+ if (pathKind === "requirement") {
41
+ const reasons = [
42
+ "File is a requirement document — policy checks recommended",
43
+ ];
44
+ if (hasMustPriority) {
45
+ reasons.push("Requirement has priority:must — elevated validation required");
46
+ }
47
+ return {
48
+ riskClass: "req_policy_candidate",
49
+ reasons,
50
+ };
51
+ }
52
+ // 4. kb_doc_structural — structural KB documentation
53
+ if (pathKind === "scenario" ||
54
+ pathKind === "adr" ||
55
+ pathKind === "fact" ||
56
+ pathKind === "flag" ||
57
+ pathKind === "event" ||
58
+ pathKind === "symbol") {
59
+ return {
60
+ riskClass: "kb_doc_structural",
61
+ reasons: [
62
+ "File is structural KB documentation — relationship and field validation recommended",
63
+ ],
64
+ };
65
+ }
66
+ // 5. safe_docs_only — non-code, non-kb-doc files (markdown, config, etc.)
67
+ if (pathKind === "unknown") {
68
+ return {
69
+ riskClass: "safe_docs_only",
70
+ reasons: ["File is not a code or KB document — low risk"],
71
+ };
72
+ }
73
+ // pathKind === "code" from here on
74
+ if (pathKind === "code") {
75
+ const hasBehavior = BEHAVIOR_PATTERN.test(fileContent);
76
+ const hasTraceability = TRACEABILITY_PATTERN.test(fileContent);
77
+ if (hasBehavior) {
78
+ // 6. traceability_candidate — needs traceability attention
79
+ if (hasDurableComment || !hasTraceability) {
80
+ const reasons = [];
81
+ if (!hasTraceability) {
82
+ reasons.push("Code file contains exports without // implements REQ-xxx annotation");
83
+ }
84
+ if (hasDurableComment) {
85
+ reasons.push("Durable knowledge comment detected — traceability review recommended");
86
+ }
87
+ return {
88
+ riskClass: "traceability_candidate",
89
+ reasons,
90
+ };
91
+ }
92
+ // 7. behavior_candidate — has exports and traceability, no durable comment
93
+ return {
94
+ riskClass: "behavior_candidate",
95
+ reasons: [
96
+ "Code file contains exportable constructs — discovery guidance applies",
97
+ ],
98
+ };
99
+ }
100
+ // Code file without export patterns — treat as safe
101
+ return {
102
+ riskClass: "safe_docs_only",
103
+ reasons: ["Code file has no detected export patterns — low risk"],
104
+ };
105
+ }
106
+ // Fallback — should not be reached since all PathKind values are handled
107
+ return {
108
+ riskClass: "safe_docs_only",
109
+ reasons: ["File type is unhandled — defaulting to safe classification"],
110
+ };
111
+ }
@@ -0,0 +1,41 @@
1
+ import type { RepoPosture } from "./repo-posture.js";
2
+ /**
3
+ * The effective enforcement mode after resolving config + posture + maintenance state.
4
+ * - "advisory": plugin emits guidance, logs, reminders — never blocks
5
+ * - "strict": plugin may escalate targeted checks, completion reminders, and
6
+ * structured logging. Hooks/checks remain the hard enforcement boundary
7
+ * regardless of mode.
8
+ */
9
+ export type EffectiveMode = "advisory" | "strict";
10
+ /**
11
+ * Inputs required to determine the effective smart-enforcement mode.
12
+ */
13
+ export interface ModeInputs {
14
+ /** Configured smart-enforcement mode. */
15
+ mode: "advisory" | "strict";
16
+ /** When true, strict mode only activates for authoritative root KB postures. */
17
+ requireRootKbForStrict: boolean;
18
+ /** Current repository posture from detectPosture(). */
19
+ posture: RepoPosture;
20
+ /** Whether the maintenance subsystem is in a degraded state. */
21
+ maintenanceDegraded: boolean;
22
+ }
23
+ /**
24
+ * Determine whether the current posture qualifies for strict enforcement
25
+ * when `requireRootKbForStrict` is true.
26
+ *
27
+ * Only root_active and hybrid_root_plus_vendored are considered authoritative.
28
+ */
29
+ export declare function isStrictEligible(inputs: ModeInputs): boolean;
30
+ /**
31
+ * Compute the effective smart-enforcement mode.
32
+ *
33
+ * Decision matrix:
34
+ * - advisory config → always advisory
35
+ * - strict config + requireRootKbForStrict=true → strict only for root_active
36
+ * and hybrid_root_plus_vendored postures
37
+ * - strict config + requireRootKbForStrict=false → strict may apply to all
38
+ * postures (but hooks/checks remain hard gate regardless)
39
+ * - maintenance-degraded → advisory regardless of config
40
+ */
41
+ export declare function computeEffectiveMode(inputs: ModeInputs): EffectiveMode;
@@ -0,0 +1,48 @@
1
+ /** Postures considered authoritative for strict enforcement. */
2
+ const STRICT_ELIGIBLE_POSTURES = new Set([
3
+ "root_active",
4
+ "hybrid_root_plus_vendored",
5
+ ]);
6
+ /**
7
+ * Determine whether the current posture qualifies for strict enforcement
8
+ * when `requireRootKbForStrict` is true.
9
+ *
10
+ * Only root_active and hybrid_root_plus_vendored are considered authoritative.
11
+ */
12
+ export function isStrictEligible(inputs) {
13
+ if (inputs.maintenanceDegraded)
14
+ return false;
15
+ if (inputs.requireRootKbForStrict) {
16
+ return STRICT_ELIGIBLE_POSTURES.has(inputs.posture);
17
+ }
18
+ // When requireRootKbForStrict is false, strict may apply to all postures
19
+ // (but still subject to maintenance-degraded override in computeEffectiveMode).
20
+ return true;
21
+ }
22
+ /**
23
+ * Compute the effective smart-enforcement mode.
24
+ *
25
+ * Decision matrix:
26
+ * - advisory config → always advisory
27
+ * - strict config + requireRootKbForStrict=true → strict only for root_active
28
+ * and hybrid_root_plus_vendored postures
29
+ * - strict config + requireRootKbForStrict=false → strict may apply to all
30
+ * postures (but hooks/checks remain hard gate regardless)
31
+ * - maintenance-degraded → advisory regardless of config
32
+ */
33
+ export function computeEffectiveMode(inputs) {
34
+ // Maintenance-degraded always forces advisory
35
+ if (inputs.maintenanceDegraded) {
36
+ return "advisory";
37
+ }
38
+ // Advisory config always produces advisory behavior
39
+ if (inputs.mode === "advisory") {
40
+ return "advisory";
41
+ }
42
+ // Strict config: check posture eligibility
43
+ if (isStrictEligible(inputs)) {
44
+ return "strict";
45
+ }
46
+ // Strict config but not eligible → fall back to advisory
47
+ return "advisory";
48
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Resolve the configured symbols manifest path using loadKbSyncPaths(worktree),
3
+ * read the YAML synchronously, and return up to 3 deduped REQ IDs linked to
4
+ * the edited file path. Preference is given to relationships[type=implements].target
5
+ * (in file order) then static links as a fallback, preserving file order.
6
+ *
7
+ * Supports both YAML formats: top-level array and { symbols: [...] } object.
8
+ * This function is purely synchronous and makes no runtime KB queries.
9
+ */
10
+ export declare function getSourceLinkedRequirementIds(worktree: string, editedAbsolutePath: string): string[];
@@ -0,0 +1,164 @@
1
+ // implements REQ-opencode-smart-enforcement-v1
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import * as path from "node:path";
4
+ import { loadKbSyncPaths } from "./file-filter.js";
5
+ /**
6
+ * Resolve the configured symbols manifest path using loadKbSyncPaths(worktree),
7
+ * read the YAML synchronously, and return up to 3 deduped REQ IDs linked to
8
+ * the edited file path. Preference is given to relationships[type=implements].target
9
+ * (in file order) then static links as a fallback, preserving file order.
10
+ *
11
+ * Supports both YAML formats: top-level array and { symbols: [...] } object.
12
+ * This function is purely synchronous and makes no runtime KB queries.
13
+ */
14
+ // implements REQ-opencode-smart-enforcement-v1
15
+ export function getSourceLinkedRequirementIds(worktree, editedAbsolutePath) {
16
+ try {
17
+ const paths = loadKbSyncPaths(worktree);
18
+ const symbolsPathRaw = paths.symbols;
19
+ if (!symbolsPathRaw)
20
+ return [];
21
+ const symbolsPath = path.isAbsolute(symbolsPathRaw)
22
+ ? symbolsPathRaw
23
+ : path.join(worktree, symbolsPathRaw);
24
+ if (!existsSync(symbolsPath))
25
+ return [];
26
+ const content = readFileSync(symbolsPath, "utf8");
27
+ const symbols = parseSymbolsYaml(content);
28
+ const relEdited = path
29
+ .relative(worktree, editedAbsolutePath)
30
+ .split(path.sep)
31
+ .join("/");
32
+ const matchedRows = symbols.filter((s) => s.sourceFile === relEdited);
33
+ if (matchedRows.length === 0)
34
+ return [];
35
+ const seen = new Set();
36
+ const orderedIds = [];
37
+ // First pass: collect implements relationships in file order
38
+ for (const row of matchedRows) {
39
+ for (const r of row.relationships ?? []) {
40
+ if (r.type === "implements") {
41
+ const id = r.target;
42
+ if (!seen.has(id)) {
43
+ seen.add(id);
44
+ orderedIds.push(id);
45
+ if (orderedIds.length >= 3)
46
+ return orderedIds.slice(0, 3);
47
+ }
48
+ }
49
+ }
50
+ }
51
+ // Second pass: fall back to static links, preserving file order
52
+ for (const row of matchedRows) {
53
+ for (const l of row.links ?? []) {
54
+ if (!seen.has(l)) {
55
+ seen.add(l);
56
+ orderedIds.push(l);
57
+ if (orderedIds.length >= 3)
58
+ return orderedIds.slice(0, 3);
59
+ }
60
+ }
61
+ }
62
+ return orderedIds.slice(0, 3);
63
+ }
64
+ catch {
65
+ return [];
66
+ }
67
+ }
68
+ // ── Lightweight YAML parser (symbols.yaml subset) ────────────────────
69
+ //
70
+ // Handles:
71
+ // symbols:
72
+ // - id: SYM-xxx
73
+ // sourceFile: path/to/file
74
+ // links:
75
+ // - REQ-xxx
76
+ // relationships:
77
+ // - type: implements
78
+ // target: REQ-xxx
79
+ //
80
+ // And bare array format (no wrapping `symbols:` key):
81
+ // - id: SYM-xxx
82
+ // ...
83
+ function parseSymbolsYaml(content) {
84
+ const entries = [];
85
+ const lines = content.split("\n");
86
+ let current = null;
87
+ let section = "none";
88
+ let pendingRel = null;
89
+ function flushRel() {
90
+ if (pendingRel?.type && pendingRel.target && current) {
91
+ current.relationships.push({
92
+ type: pendingRel.type,
93
+ target: pendingRel.target,
94
+ });
95
+ }
96
+ pendingRel = null;
97
+ }
98
+ function flushEntry() {
99
+ flushRel();
100
+ if (current?.id && current?.sourceFile) {
101
+ entries.push(current);
102
+ }
103
+ current = null;
104
+ section = "none";
105
+ }
106
+ for (const raw of lines) {
107
+ if (raw.trim().startsWith("#"))
108
+ continue;
109
+ // New entry: " - id: ..."
110
+ const entryMatch = raw.match(/^\s+-\s+id:\s*(.+)$/);
111
+ if (entryMatch) {
112
+ flushEntry();
113
+ current = { id: entryMatch[1].trim(), links: [], relationships: [] };
114
+ section = "none";
115
+ continue;
116
+ }
117
+ if (!current)
118
+ continue;
119
+ // sourceFile
120
+ const srcMatch = raw.match(/^\s+sourceFile:\s*(.+)$/);
121
+ if (srcMatch) {
122
+ current.sourceFile = srcMatch[1].trim();
123
+ section = "none";
124
+ continue;
125
+ }
126
+ // links section header
127
+ if (/^\s+links:\s*$/.test(raw)) {
128
+ flushRel();
129
+ section = "links";
130
+ continue;
131
+ }
132
+ // relationships section header
133
+ if (/^\s+relationships:\s*$/.test(raw)) {
134
+ flushRel();
135
+ section = "relationships";
136
+ continue;
137
+ }
138
+ // Link item: " - REQ-xxx"
139
+ if (section === "links") {
140
+ const linkMatch = raw.match(/^\s+-\s+(REQ-[A-Za-z0-9_-]+)\s*$/);
141
+ if (linkMatch) {
142
+ current.links.push(linkMatch[1]);
143
+ continue;
144
+ }
145
+ }
146
+ // Relationship type: " - type: implements"
147
+ if (section === "relationships") {
148
+ const relTypeMatch = raw.match(/^\s+-\s+type:\s*(.+)$/);
149
+ if (relTypeMatch) {
150
+ flushRel();
151
+ pendingRel = { type: relTypeMatch[1].trim() };
152
+ continue;
153
+ }
154
+ // Relationship target: " target: REQ-..."
155
+ const relTargetMatch = raw.match(/^\s+target:\s*(.+)$/);
156
+ if (relTargetMatch && pendingRel) {
157
+ pendingRel.target = relTargetMatch[1].trim();
158
+ continue;
159
+ }
160
+ }
161
+ }
162
+ flushEntry();
163
+ return entries;
164
+ }
@@ -6,5 +6,7 @@ export interface WorkspaceHealth {
6
6
  }
7
7
  /**
8
8
  * Analyze workspace health for Kibi bootstrap and initialization.
9
+ * Uses detectPosture() for root-scoped classification and delegates
10
+ * bootstrap-needs to the posture result.
9
11
  */
10
12
  export declare function checkWorkspaceHealth(cwd: string): WorkspaceHealth;