@kal-elsam/kairo-runtime 0.11.0 → 0.13.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.
Files changed (71) hide show
  1. package/global-template/components/agent-skills/LICENSE +21 -0
  2. package/global-template/components/agent-skills/PROVENANCE.md +26 -0
  3. package/global-template/components/agent-skills/skills/context-engineering/SKILL.md +289 -0
  4. package/global-template/components/agent-skills/skills/frontend-ui-engineering/SKILL.md +328 -0
  5. package/global-template/components/agent-skills/skills/observability-and-instrumentation/SKILL.md +203 -0
  6. package/global-template/components/agent-skills/skills/performance-optimization/SKILL.md +396 -0
  7. package/global-template/components/agent-skills/skills/source-driven-development/SKILL.md +194 -0
  8. package/global-template/components/catalog.json +29 -0
  9. package/package.json +5 -2
  10. package/scripts/cockpit-smoke.mjs +2 -1
  11. package/src/cli.js +136 -8
  12. package/src/global/component-builders.js +3 -1
  13. package/src/global/components/agent-skills.js +27 -0
  14. package/src/global/ink/cockpit-control-center.js +107 -4
  15. package/src/global/ink/cockpit-scan.js +20 -2
  16. package/src/global/ink/ecosystem-updates-display.js +37 -0
  17. package/src/global/ink/launch-input.js +32 -1
  18. package/src/global/ink/obsidian-vault-display.js +37 -0
  19. package/src/global/ink/orchestrator-app.js +2 -1
  20. package/src/global/ink/orchestrator-state.js +17 -2
  21. package/src/global/ink/system-resources-display.js +109 -0
  22. package/src/global/ink/use-orchestrator-data.js +40 -4
  23. package/src/global/ink/ux/live-overview.js +11 -1
  24. package/src/global/mcp/kairo-mcp.js +230 -0
  25. package/src/global/observability/build-companion-snapshot.js +302 -0
  26. package/src/global/observability/build-observability-snapshot.js +24 -0
  27. package/src/global/observability/ecosystem-updates.js +224 -0
  28. package/src/global/observability/gentle-bundle-export.js +71 -0
  29. package/src/global/observability/gentle-bundle-import.js +122 -0
  30. package/src/global/observability/gentle-probe.js +155 -0
  31. package/src/global/observability/graphify-ops.js +133 -0
  32. package/src/global/observability/graphify-parse-cache.js +90 -0
  33. package/src/global/observability/graphify-probe.js +185 -0
  34. package/src/global/observability/hermes-activity.js +163 -0
  35. package/src/global/observability/hermes-probe.js +171 -0
  36. package/src/global/observability/index.js +124 -0
  37. package/src/global/observability/obsidian-knowledge-preview.js +214 -0
  38. package/src/global/observability/obsidian-knowledge-views.js +227 -0
  39. package/src/global/observability/obsidian-publisher.js +181 -0
  40. package/src/global/observability/obsidian-status.js +76 -0
  41. package/src/global/observability/obsidian-vault.js +259 -0
  42. package/src/global/observability/passive-snapshot-flight.js +93 -0
  43. package/src/global/observability/probe-contract.js +38 -0
  44. package/src/global/observability/probe-registry.js +30 -0
  45. package/src/global/observability/resource-advisor.js +71 -0
  46. package/src/global/observability/system-resources.js +171 -0
  47. package/src/global/runtime/alerts/alert-cli.js +31 -0
  48. package/src/global/runtime/alerts/alert-store.js +29 -6
  49. package/src/global/runtime/alerts/alert-validate.js +25 -1
  50. package/src/global/runtime/alerts/controlled-alert-actions.js +56 -0
  51. package/src/global/runtime/execution-adapters/claude.js +2 -1
  52. package/src/global/runtime/execution-adapters/codex.js +2 -1
  53. package/src/global/runtime/execution-adapters/create-execution-adapter.js +3 -14
  54. package/src/global/runtime/execution-adapters/cursor.js +2 -1
  55. package/src/global/runtime/execution-adapters/opencode.js +2 -1
  56. package/src/global/runtime/execution-adapters/pi.js +2 -1
  57. package/src/global/runtime/review/index.js +1 -1
  58. package/src/global/runtime/review/review-cli.js +113 -3
  59. package/src/global/runtime/review/review-git.js +142 -11
  60. package/src/global/runtime/review/review-patch.js +2 -0
  61. package/src/global/runtime/review/review-receipts.js +12 -7
  62. package/src/global/runtime/review/review-runner.js +2 -2
  63. package/src/global/runtime/review/review-types.js +8 -5
  64. package/src/global/runtime/review/review-validate.js +5 -1
  65. package/src/global/runtime/run-cli.js +2 -0
  66. package/src/global/runtime/run-manager.js +39 -18
  67. package/src/global/runtime/run-permissions.js +231 -0
  68. package/src/global/runtime/run-profile.js +2 -0
  69. package/src/global/runtime/run-supervisor.js +77 -37
  70. package/src/global/runtime/run-types.js +2 -0
  71. package/src/global/updates-cli.js +41 -0
@@ -0,0 +1,171 @@
1
+ import { isAbsolute } from "node:path";
2
+ import {
3
+ parseVersionFromOutput,
4
+ probeCommand as defaultProbeCommand
5
+ } from "../cli-probe.js";
6
+ import { normalizeProbeResult } from "./probe-contract.js";
7
+
8
+ /** Surfaces recognized from `hermes --help` only — never executed by this probe. */
9
+ export const HERMES_DIAGNOSTIC_SURFACES = Object.freeze(["version", "doctor", "status"]);
10
+ export const HERMES_MANDATORY_SURFACES = Object.freeze(["version", "doctor"]);
11
+
12
+ const WHICH_TIMEOUT_MS = 3000;
13
+ const VERSION_TIMEOUT_MS = 8000;
14
+ const HELP_TIMEOUT_MS = 8000;
15
+ const MAX_PROBE_BYTES = 65_536;
16
+
17
+ function result(partial) {
18
+ return normalizeProbeResult({
19
+ id: "hermes", version: null, contractCompatible: null,
20
+ diagnostics: [], evidence: [], error: null, ...partial
21
+ }, "hermes");
22
+ }
23
+
24
+ function boundText(raw, maxBytes = MAX_PROBE_BYTES) {
25
+ const text = String(raw ?? "");
26
+ if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
27
+ return Buffer.from(text, "utf8").subarray(0, maxBytes).toString("utf8");
28
+ }
29
+
30
+ function whichAbsolutePath(command, env, run = defaultProbeCommand) {
31
+ const which = run("which", [command], { env, timeoutMs: WHICH_TIMEOUT_MS });
32
+ if (!which?.ok) return "";
33
+ const path = String(which.stdout ?? "").trim().split(/\r?\n/)[0] ?? "";
34
+ return isAbsolute(path) ? path : "";
35
+ }
36
+
37
+ /** Resolve absolute Hermes binary; never return a bare command name. */
38
+ export function resolveHermesBinaryPath(command = "hermes", env = process.env, {
39
+ whichCommand,
40
+ probeCommand = defaultProbeCommand
41
+ } = {}) {
42
+ const resolver = typeof whichCommand === "function"
43
+ ? whichCommand
44
+ : (cmd, e) => whichAbsolutePath(cmd, e, probeCommand);
45
+ const resolved = resolver(command, env) || null;
46
+ return resolved && isAbsolute(resolved) ? resolved : null;
47
+ }
48
+
49
+ /** Presence means the CLI *declares* the surface — not that Kairo may run it. */
50
+ export function detectHermesDiagnosticSurfaces(helpText) {
51
+ const text = boundText(helpText);
52
+ const found = Object.create(null);
53
+ for (const name of HERMES_DIAGNOSTIC_SURFACES) {
54
+ found[name] = new RegExp(`(?:^|[\\s,{|/])${name}(?=[\\s,}|/-]|$)`, "m").test(text);
55
+ }
56
+ return Object.freeze(found);
57
+ }
58
+
59
+ function softError(label, kind, evidence, extra = {}) {
60
+ return result({
61
+ state: "error",
62
+ diagnostics: [`hermes ${label} ${kind === "timeout" ? "timed out" : "failed"}`],
63
+ error: kind === "timeout" ? "timeout" : kind === "exit" ? `exit ${extra.code}` : "spawn_error",
64
+ evidence,
65
+ ...extra.fields
66
+ });
67
+ }
68
+
69
+ function spawnFailure(label, probeResult, evidence, extra = {}) {
70
+ if (probeResult?.timedOut) return softError(label, "timeout", evidence, extra);
71
+ if (probeResult?.error) return softError(label, "spawn", evidence, extra);
72
+ if (probeResult?.status !== 0) {
73
+ return softError(label, "exit", evidence, { ...extra, code: probeResult?.status ?? "unknown" });
74
+ }
75
+ return null;
76
+ }
77
+
78
+ /** which: preserve timeout/spawn_error; exit/empty → missing (not failure). */
79
+ function whichFailure(probeResult, evidence) {
80
+ if (probeResult?.timedOut) return softError("which", "timeout", evidence);
81
+ if (probeResult?.error) return softError("which", "spawn", evidence);
82
+ return null;
83
+ }
84
+
85
+ function runProbe(probeCommand, cmd, args, opts) {
86
+ try { return { ok: true, value: probeCommand(cmd, args, opts) }; }
87
+ catch { return { ok: false }; }
88
+ }
89
+
90
+ export async function probeHermes({
91
+ env = process.env,
92
+ cwd = process.cwd(),
93
+ whichCommand,
94
+ probeCommand = defaultProbeCommand
95
+ } = {}) {
96
+ const noBinary = [{ kind: "binary", path: null }];
97
+ let path;
98
+
99
+ if (typeof whichCommand === "function") {
100
+ try { path = resolveHermesBinaryPath("hermes", env, { whichCommand, probeCommand }); }
101
+ catch { return softError("which", "spawn", noBinary); }
102
+ } else {
103
+ const inv = runProbe(probeCommand, "which", ["hermes"], { env, timeoutMs: WHICH_TIMEOUT_MS });
104
+ if (!inv.ok) return softError("which", "spawn", noBinary);
105
+ const fail = whichFailure(inv.value, noBinary);
106
+ if (fail) return fail;
107
+ const candidate = String(inv.value?.stdout ?? "").trim().split(/\r?\n/)[0] ?? "";
108
+ path = inv.value?.ok && isAbsolute(candidate) ? candidate : null;
109
+ }
110
+
111
+ if (!path) {
112
+ return result({
113
+ state: "missing",
114
+ diagnostics: [
115
+ "hermes absolute binary not resolved. Install Hermes Agent separately, then re-run the probe."
116
+ ],
117
+ evidence: noBinary
118
+ });
119
+ }
120
+
121
+ const evidence = [{ kind: "binary", path }];
122
+ const versionInv = runProbe(probeCommand, path, ["--version"], {
123
+ cwd, env, timeoutMs: VERSION_TIMEOUT_MS
124
+ });
125
+ if (!versionInv.ok) return softError("--version", "spawn", evidence);
126
+ const versionFail = spawnFailure("--version", versionInv.value, evidence);
127
+ if (versionFail) return versionFail;
128
+ const version = parseVersionFromOutput(boundText(versionInv.value.stdout));
129
+ evidence.push({ kind: "version", version, ok: true });
130
+ if (!version) {
131
+ return result({
132
+ state: "incompatible", contractCompatible: false, evidence,
133
+ diagnostics: ["hermes --version did not yield a parseable version"]
134
+ });
135
+ }
136
+
137
+ const helpInv = runProbe(probeCommand, path, ["--help"], {
138
+ cwd, env, timeoutMs: HELP_TIMEOUT_MS
139
+ });
140
+ if (!helpInv.ok) return softError("--help", "spawn", evidence, { fields: { version } });
141
+ const helpFail = spawnFailure("--help", helpInv.value, evidence, { fields: { version } });
142
+ if (helpFail) return helpFail;
143
+
144
+ const surfaces = detectHermesDiagnosticSurfaces(helpInv.value.stdout);
145
+ // Capability availability only — surfaces were not executed.
146
+ evidence.push({ kind: "diagnostic_surfaces", surfaces: { ...surfaces }, executed: false });
147
+
148
+ const missingMandatory = HERMES_MANDATORY_SURFACES.filter((name) => !surfaces[name]);
149
+ if (missingMandatory.length) {
150
+ return result({
151
+ state: "incompatible", version, contractCompatible: false, evidence,
152
+ diagnostics: missingMandatory.map((name) => `missing mandatory surface in --help: ${name}`)
153
+ });
154
+ }
155
+
156
+ return result({
157
+ state: "available", version, contractCompatible: true, evidence,
158
+ diagnostics: surfaces.status ? [] : ["optional surface absent in --help: status"]
159
+ });
160
+ }
161
+
162
+ export function createHermesProbe(deps = {}) {
163
+ return {
164
+ id: "hermes",
165
+ declaredEvents: Object.freeze([]),
166
+ declaredActions: Object.freeze([]),
167
+ async probe(context = {}) {
168
+ return probeHermes({ ...deps, ...context, env: context.env ?? deps.env ?? process.env });
169
+ }
170
+ };
171
+ }
@@ -0,0 +1,124 @@
1
+ import { createGentleProbe } from "./gentle-probe.js";
2
+ import { createGraphifyProbe } from "./graphify-probe.js";
3
+ import { createHermesProbe } from "./hermes-probe.js";
4
+ import { getObservabilityProbe, registerObservabilityProbe } from "./probe-registry.js";
5
+
6
+ export {
7
+ OBSERVABILITY_PROBE_STATES,
8
+ assertObservabilityProbeContract,
9
+ normalizeProbeResult
10
+ } from "./probe-contract.js";
11
+ export {
12
+ registerObservabilityProbe,
13
+ getObservabilityProbe,
14
+ listObservabilityProbes,
15
+ resetObservabilityProbesForTests
16
+ } from "./probe-registry.js";
17
+ export { buildObservabilitySnapshot } from "./build-observability-snapshot.js";
18
+ export {
19
+ PASSIVE_SNAPSHOT_TTL_MS,
20
+ PASSIVE_SNAPSHOT_MAX_ENTRIES,
21
+ buildPassiveSnapshotKey,
22
+ resetPassiveSnapshotFlightForTests,
23
+ passiveSnapshotInFlightSizeForTests,
24
+ runPassiveObservabilitySnapshot
25
+ } from "./passive-snapshot-flight.js";
26
+ export {
27
+ SUPPORTED_PROTOCOL, SUPPORTED_SCHEMA, SUPPORTED_CONTRACT,
28
+ SUPPORTED_MANDATORY_FEATURES, evaluateGentleCapabilities, probeGentle, createGentleProbe,
29
+ resolveGentleBinaryPath
30
+ } from "./gentle-probe.js";
31
+ export { exportGentleReviewBundle, resolveNegotiatedGentleBinary } from "./gentle-bundle-export.js";
32
+ export { importGentleReviewBundle } from "./gentle-bundle-import.js";
33
+ export {
34
+ inspectGraphArtifact, assertGraphInsideWorkspace,
35
+ resolveGraphifyBinaryPath, resolveGitHeadSha, scrubGitOverrideEnv,
36
+ probeGraphify, createGraphifyProbe
37
+ } from "./graphify-probe.js";
38
+ export {
39
+ GRAPHIFY_PARSE_TTL_MS,
40
+ GRAPHIFY_PARSE_MAX_ENTRIES,
41
+ buildGraphifyParseIdentity,
42
+ resetGraphifyParseCacheForTests,
43
+ inspectGraphArtifactCached
44
+ } from "./graphify-parse-cache.js";
45
+ export { runGraphifyOp, runGraphifyCli } from "./graphify-ops.js";
46
+ export {
47
+ HERMES_DIAGNOSTIC_SURFACES, HERMES_MANDATORY_SURFACES,
48
+ detectHermesDiagnosticSurfaces, resolveHermesBinaryPath,
49
+ probeHermes, createHermesProbe
50
+ } from "./hermes-probe.js";
51
+ export {
52
+ DEFAULT_HERMES_API_URL,
53
+ HERMES_ACTIVITY_LIMIT_DEFAULT, HERMES_ACTIVITY_LIMIT_MAX,
54
+ HERMES_ACTIVITY_TIMEOUT_MS, HERMES_ACTIVE_WINDOW_MS,
55
+ assertHermesLoopbackUrl, capabilitiesAdvertiseSessionsList,
56
+ normalizeHermesSession, loadHermesActivity
57
+ } from "./hermes-activity.js";
58
+ export {
59
+ SYSTEM_RESOURCES_TIMEOUT_MS, PROCESS_ALLOWLIST,
60
+ parseProcessTable, loadSystemResources
61
+ } from "./system-resources.js";
62
+ export { recommendSystemResources } from "./resource-advisor.js";
63
+ export {
64
+ ECOSYSTEM_UPDATES_CACHE_MS,
65
+ ECOSYSTEM_UPDATES_TIMEOUT_MS,
66
+ AGENT_SKILLS_PINNED_REV,
67
+ parseGentleUpdateOutput,
68
+ parseHermesUpdateCheck,
69
+ loadEcosystemUpdates
70
+ } from "./ecosystem-updates.js";
71
+ export {
72
+ KAIRO_VAULT_SUBDIR,
73
+ EXCLUDED_DIR_NAMES,
74
+ normalizeVaultPath,
75
+ isExcludedDirName,
76
+ isSecretBasename,
77
+ isAllowedKairoNoteName,
78
+ assertInsideKairoRoot,
79
+ resolveKairoNotePath,
80
+ inspectObsidianVault
81
+ } from "./obsidian-vault.js";
82
+ export {
83
+ formatKnowledgeFrontmatter,
84
+ renderDecisionMarkdown,
85
+ renderArchitectureMarkdown,
86
+ buildObsidianKnowledgePreview,
87
+ loadObsidianKnowledgePreview
88
+ } from "./obsidian-knowledge-preview.js";
89
+ export {
90
+ KAIRO_MANAGED_FRONTMATTER,
91
+ BACKUP_DIR_NAME,
92
+ hasConsent,
93
+ classifyNoteWrite,
94
+ planObsidianPublish,
95
+ publishObsidianProposals
96
+ } from "./obsidian-publisher.js";
97
+ export {
98
+ KAIRO_VIEW_KINDS,
99
+ parseKnowledgeFrontmatter,
100
+ extractWikilinks,
101
+ buildObsidianKnowledgeViews,
102
+ buildKnowledgeIndexProposals,
103
+ loadObsidianKnowledgeViews
104
+ } from "./obsidian-knowledge-views.js";
105
+ export {
106
+ emptyObsidianVaultStatus,
107
+ summarizeObsidianVaultStatus,
108
+ loadObsidianVaultStatus
109
+ } from "./obsidian-status.js";
110
+ export {
111
+ SOFT_LINK_WINDOW_MS,
112
+ parseCompanionTimestamp,
113
+ resolveRunTimestamp,
114
+ resolveReviewTimestamp,
115
+ softLinkReviewToRun,
116
+ summarizeCompanionProbes,
117
+ buildCompanionSnapshot
118
+ } from "./build-companion-snapshot.js";
119
+
120
+ export function ensureObservabilityProbesRegistered() {
121
+ if (!getObservabilityProbe("gentle")) registerObservabilityProbe(createGentleProbe());
122
+ if (!getObservabilityProbe("graphify")) registerObservabilityProbe(createGraphifyProbe());
123
+ if (!getObservabilityProbe("hermes")) registerObservabilityProbe(createHermesProbe());
124
+ }
@@ -0,0 +1,214 @@
1
+ import { resolveKairoNotePath } from "./obsidian-vault.js";
2
+
3
+ const MAX_PROPOSALS = 40;
4
+ const TITLE_MAX = 80;
5
+
6
+ function envelope(partial = {}) {
7
+ return {
8
+ state: "error",
9
+ proposals: [],
10
+ diagnostics: [],
11
+ error: null,
12
+ generatedAt: null,
13
+ ...partial
14
+ };
15
+ }
16
+
17
+ function scrubTitle(raw, fallback = "untitled") {
18
+ const text = String(raw ?? "")
19
+ .replace(/[\r\n\t]+/g, " ")
20
+ .replace(/[\[\]#|\\/]+/g, " ")
21
+ .trim()
22
+ .slice(0, TITLE_MAX);
23
+ return text || fallback;
24
+ }
25
+
26
+ function slugify(title) {
27
+ return scrubTitle(title, "note")
28
+ .toLowerCase()
29
+ .replace(/[^a-z0-9]+/g, "-")
30
+ .replace(/^-+|-+$/g, "")
31
+ .slice(0, 48) || "note";
32
+ }
33
+
34
+ /** Stable YAML-ish frontmatter — no nested objects; values are scalars only. */
35
+ export function formatKnowledgeFrontmatter(fields = {}) {
36
+ const lines = ["---"];
37
+ for (const key of Object.keys(fields).sort()) {
38
+ const value = fields[key];
39
+ if (value == null || value === "") continue;
40
+ const safe = String(value).replace(/[\r\n]+/g, " ").replace(/"/g, "'");
41
+ lines.push(`${key}: "${safe}"`);
42
+ }
43
+ lines.push("---", "");
44
+ return lines.join("\n");
45
+ }
46
+
47
+ export function renderDecisionMarkdown(entry, { generatedAt } = {}) {
48
+ const title = scrubTitle(entry?.title ?? entry?.id, "decision");
49
+ const id = scrubTitle(entry?.id ?? slugify(title), slugify(title));
50
+ const body = String(entry?.body ?? entry?.content ?? "").trim();
51
+ const fm = formatKnowledgeFrontmatter({
52
+ kairo_kind: "decision",
53
+ kairo_id: id,
54
+ source: "engram-export",
55
+ generated_at: generatedAt ?? null,
56
+ title
57
+ });
58
+ const wiki = `[[decisions/${slugify(title)}]]`;
59
+ return {
60
+ relativePath: `decisions/${slugify(title)}.md`,
61
+ title,
62
+ provenance: { system: "engram", kind: "decision", id },
63
+ markdown: `${fm}# ${title}\n\n${body || "_No body provided._"}\n\n---\nSource: Engram export · ${wiki}\n`
64
+ };
65
+ }
66
+
67
+ export function renderArchitectureMarkdown(entry, { generatedAt } = {}) {
68
+ const title = scrubTitle(entry?.title ?? entry?.name ?? entry?.id, "architecture");
69
+ const id = scrubTitle(entry?.id ?? slugify(title), slugify(title));
70
+ const detail = String(entry?.detail ?? entry?.summary ?? "").trim();
71
+ const links = Array.isArray(entry?.related)
72
+ ? entry.related.map((r) => `- [[architecture/${slugify(r)}]]`).join("\n")
73
+ : "";
74
+ const fm = formatKnowledgeFrontmatter({
75
+ kairo_kind: "architecture",
76
+ kairo_id: id,
77
+ source: "graphify-export",
78
+ generated_at: generatedAt ?? null,
79
+ title
80
+ });
81
+ return {
82
+ relativePath: `architecture/${slugify(title)}.md`,
83
+ title,
84
+ provenance: { system: "graphify", kind: "architecture", id },
85
+ markdown: `${fm}# ${title}\n\n${detail || "_No summary provided._"}\n${links ? `\n## Related\n${links}\n` : ""}`
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Pure composer — accepts already-exported records only.
91
+ * Never opens Engram/Graphify internal DBs or vault files.
92
+ */
93
+ export function buildObsidianKnowledgePreview({
94
+ decisions = [],
95
+ architecture = [],
96
+ generatedAt = new Date().toISOString(),
97
+ maxProposals = MAX_PROPOSALS,
98
+ kairoRoot = "/virtual/Kairo"
99
+ } = {}) {
100
+ const diagnostics = [];
101
+ const proposals = [];
102
+
103
+ const push = (draft) => {
104
+ if (proposals.length >= maxProposals) return;
105
+ const gate = resolveKairoNotePath(kairoRoot, draft.relativePath);
106
+ if (!gate.ok) {
107
+ diagnostics.push(`rejected ${draft.relativePath}: ${gate.reason}`);
108
+ return;
109
+ }
110
+ proposals.push({
111
+ relativePath: draft.relativePath,
112
+ title: draft.title,
113
+ markdown: draft.markdown,
114
+ provenance: draft.provenance,
115
+ absolutePath: gate.path
116
+ });
117
+ };
118
+
119
+ for (const entry of decisions ?? []) {
120
+ if (entry == null || typeof entry !== "object") {
121
+ diagnostics.push("skipped malformed decision");
122
+ continue;
123
+ }
124
+ push(renderDecisionMarkdown(entry, { generatedAt }));
125
+ }
126
+ for (const entry of architecture ?? []) {
127
+ if (entry == null || typeof entry !== "object") {
128
+ diagnostics.push("skipped malformed architecture");
129
+ continue;
130
+ }
131
+ push(renderArchitectureMarkdown(entry, { generatedAt }));
132
+ }
133
+
134
+ if (proposals.length === 0 && diagnostics.length === 0) {
135
+ return envelope({
136
+ state: "empty",
137
+ proposals: [],
138
+ diagnostics: ["no export records provided"],
139
+ generatedAt,
140
+ error: null
141
+ });
142
+ }
143
+
144
+ return envelope({
145
+ state: proposals.length > 0 ? "available" : "partial",
146
+ proposals,
147
+ diagnostics,
148
+ generatedAt,
149
+ error: null
150
+ });
151
+ }
152
+
153
+ /**
154
+ * Load preview via injectable export adapters — never vault writes.
155
+ * Adapters must return plain arrays of records (already exported).
156
+ */
157
+ export async function loadObsidianKnowledgePreview({
158
+ loadEngramExport = null,
159
+ loadGraphifyExport = null,
160
+ kairoRoot = "/virtual/Kairo",
161
+ generatedAt = new Date().toISOString(),
162
+ maxProposals = MAX_PROPOSALS
163
+ } = {}) {
164
+ const diagnostics = [];
165
+ let decisions = [];
166
+ let architecture = [];
167
+
168
+ if (typeof loadEngramExport === "function") {
169
+ try {
170
+ const raw = await loadEngramExport();
171
+ decisions = Array.isArray(raw) ? raw : Array.isArray(raw?.decisions) ? raw.decisions : [];
172
+ if (!Array.isArray(raw) && raw != null && !Array.isArray(raw?.decisions)) {
173
+ diagnostics.push("engram export shape unrecognized");
174
+ }
175
+ } catch (err) {
176
+ diagnostics.push(`engram export failed: ${String(err?.message ?? err)}`);
177
+ }
178
+ } else {
179
+ diagnostics.push("engram export adapter not provided");
180
+ }
181
+
182
+ if (typeof loadGraphifyExport === "function") {
183
+ try {
184
+ const raw = await loadGraphifyExport();
185
+ architecture = Array.isArray(raw)
186
+ ? raw
187
+ : Array.isArray(raw?.architecture)
188
+ ? raw.architecture
189
+ : Array.isArray(raw?.communities)
190
+ ? raw.communities
191
+ : [];
192
+ if (
193
+ !Array.isArray(raw)
194
+ && raw != null
195
+ && !Array.isArray(raw?.architecture)
196
+ && !Array.isArray(raw?.communities)
197
+ ) {
198
+ diagnostics.push("graphify export shape unrecognized");
199
+ }
200
+ } catch (err) {
201
+ diagnostics.push(`graphify export failed: ${String(err?.message ?? err)}`);
202
+ }
203
+ } else {
204
+ diagnostics.push("graphify export adapter not provided");
205
+ }
206
+
207
+ const preview = buildObsidianKnowledgePreview({
208
+ decisions, architecture, generatedAt, maxProposals, kairoRoot
209
+ });
210
+ return {
211
+ ...preview,
212
+ diagnostics: [...diagnostics, ...(preview.diagnostics ?? [])]
213
+ };
214
+ }