@kal-elsam/kairo-runtime 0.4.2 → 0.5.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 (103) hide show
  1. package/README.md +124 -17
  2. package/global-template/components/catalog.json +63 -6
  3. package/global-template/components/sdd-core/personas/teaching.md +42 -0
  4. package/global-template/components/sdd-core/skills/sdd-apply/SKILL.md +34 -0
  5. package/global-template/components/sdd-core/skills/sdd-apply/references/contract.md +4 -0
  6. package/global-template/components/sdd-core/skills/sdd-archive/SKILL.md +34 -0
  7. package/global-template/components/sdd-core/skills/sdd-archive/references/contract.md +4 -0
  8. package/global-template/components/sdd-core/skills/sdd-design/SKILL.md +34 -0
  9. package/global-template/components/sdd-core/skills/sdd-design/references/contract.md +4 -0
  10. package/global-template/components/sdd-core/skills/sdd-explore/SKILL.md +34 -0
  11. package/global-template/components/sdd-core/skills/sdd-explore/references/contract.md +4 -0
  12. package/global-template/components/sdd-core/skills/sdd-init/SKILL.md +34 -0
  13. package/global-template/components/sdd-core/skills/sdd-init/references/contract.md +4 -0
  14. package/global-template/components/sdd-core/skills/sdd-propose/SKILL.md +35 -0
  15. package/global-template/components/sdd-core/skills/sdd-propose/references/contract.md +4 -0
  16. package/global-template/components/sdd-core/skills/sdd-spec/SKILL.md +34 -0
  17. package/global-template/components/sdd-core/skills/sdd-spec/references/contract.md +4 -0
  18. package/global-template/components/sdd-core/skills/sdd-tasks/SKILL.md +34 -0
  19. package/global-template/components/sdd-core/skills/sdd-tasks/references/contract.md +4 -0
  20. package/global-template/components/sdd-core/skills/sdd-verify/SKILL.md +34 -0
  21. package/global-template/components/sdd-core/skills/sdd-verify/references/contract.md +4 -0
  22. package/package.json +1 -1
  23. package/scripts/cockpit-smoke.mjs +29 -8
  24. package/scripts/smoke-test.sh +9 -0
  25. package/src/cli.js +54 -5
  26. package/src/global/action-planner.js +17 -4
  27. package/src/global/component-authoring.js +1 -2
  28. package/src/global/component-ecosystem-checks.js +74 -15
  29. package/src/global/component-health.js +56 -0
  30. package/src/global/component-integration-cli.js +302 -0
  31. package/src/global/component-manifest.js +214 -0
  32. package/src/global/component-registry.js +19 -2
  33. package/src/global/component-resolve.js +51 -0
  34. package/src/global/components/sdd-core.js +6 -0
  35. package/src/global/control-plane-proposals.js +229 -0
  36. package/src/global/control-plane-snapshot.js +212 -0
  37. package/src/global/dashboard-guidance.js +27 -3
  38. package/src/global/diff.js +2 -5
  39. package/src/global/global-cli.js +16 -1
  40. package/src/global/global-doctor.js +75 -2
  41. package/src/global/global-installer.js +43 -6
  42. package/src/global/governance-actions.js +212 -0
  43. package/src/global/governance-repair.js +13 -0
  44. package/src/global/ink/cockpit-changes.js +124 -0
  45. package/src/global/ink/cockpit-control-center.js +106 -0
  46. package/src/global/ink/cockpit-controller.js +17 -0
  47. package/src/global/ink/cockpit-enter.js +27 -0
  48. package/src/global/ink/cockpit-focus.js +7 -1
  49. package/src/global/ink/cockpit-models.js +98 -46
  50. package/src/global/ink/cockpit-proposals.js +82 -0
  51. package/src/global/ink/cockpit-recovery.js +120 -0
  52. package/src/global/ink/cockpit-runs.js +47 -0
  53. package/src/global/ink/cockpit-scan.js +60 -0
  54. package/src/global/ink/cockpit-views.js +144 -74
  55. package/src/global/ink/orchestrator-app.js +156 -42
  56. package/src/global/ink/orchestrator-state.js +22 -1
  57. package/src/global/ink/use-orchestrator-data.js +239 -17
  58. package/src/global/integrations/engram-apply.js +105 -0
  59. package/src/global/integrations/engram-evidence.js +218 -0
  60. package/src/global/integrations/engram-exec.js +107 -0
  61. package/src/global/integrations/engram-plan.js +60 -0
  62. package/src/global/integrations/engram-provider.js +22 -0
  63. package/src/global/integrations/engram-receipts.js +116 -0
  64. package/src/global/integrations/engram-rollback.js +131 -0
  65. package/src/global/integrations/index.js +12 -0
  66. package/src/global/integrations/provider-contract.js +83 -0
  67. package/src/global/integrations/provider-registry.js +47 -0
  68. package/src/global/integrations/sdd-apply.js +190 -0
  69. package/src/global/integrations/sdd-destinations.js +95 -0
  70. package/src/global/integrations/sdd-evidence.js +89 -0
  71. package/src/global/integrations/sdd-fs-guard.js +183 -0
  72. package/src/global/integrations/sdd-lifecycle.js +84 -0
  73. package/src/global/integrations/sdd-persona.js +71 -0
  74. package/src/global/integrations/sdd-plan.js +87 -0
  75. package/src/global/integrations/sdd-provider.js +21 -0
  76. package/src/global/integrations/sdd-receipts.js +44 -0
  77. package/src/global/integrations/sdd-rollback.js +269 -0
  78. package/src/global/integrations/sdd-skill-files.js +61 -0
  79. package/src/global/integrations/sdd-state.js +175 -0
  80. package/src/global/integrations/sdd-verify.js +122 -0
  81. package/src/global/intelligence/backends/opencode-catalog.js +212 -0
  82. package/src/global/intelligence/backends/opencode-cli.js +247 -0
  83. package/src/global/intelligence/backends/opencode-evidence.js +129 -0
  84. package/src/global/intelligence/backends/opencode-http.js +152 -0
  85. package/src/global/intelligence/backends/opencode-invoke.js +83 -0
  86. package/src/global/intelligence/backends/opencode-providers.js +67 -0
  87. package/src/global/intelligence/backends/opencode-runtime.js +242 -0
  88. package/src/global/intelligence/context-compiler.js +95 -50
  89. package/src/global/intelligence/index.js +38 -1
  90. package/src/global/intelligence/orchestrate.js +4 -2
  91. package/src/global/intelligence/registry.js +62 -6
  92. package/src/global/intelligence/router.js +188 -70
  93. package/src/global/intelligence/transport-registry.js +147 -0
  94. package/src/global/intelligence/types.js +21 -2
  95. package/src/global/intelligence-cli.js +48 -17
  96. package/src/global/intelligence-session.js +37 -0
  97. package/src/global/json-output.js +1 -0
  98. package/src/global/load-component-catalog.js +10 -28
  99. package/src/global/load-workspace-component-catalog.js +22 -63
  100. package/src/global/state-migration.js +21 -1
  101. package/src/global/state.js +13 -8
  102. package/src/global/status.js +4 -17
  103. package/src/global/sync.js +2 -5
@@ -9,12 +9,14 @@ import {
9
9
  } from "./intelligence/index.js";
10
10
  import { BRAND } from "./brand/index.js";
11
11
  import { formatCliCommand } from "./brand/cli.js";
12
+ import { resolveSessionOverride } from "./intelligence-session.js";
12
13
 
13
14
  export async function runIntelligenceCli(options, packageManifest) {
14
15
  const action = options.intelligenceAction ?? "status";
15
16
  const homeDir = resolveHomeDir();
16
17
  const workspaceRoot = options.cwd;
17
18
  const { profile, sources } = await resolveProfile({ homeDir, workspaceRoot });
19
+ const sessionOverride = resolveSessionOverride(options);
18
20
 
19
21
  switch (action) {
20
22
  case "status":
@@ -24,13 +26,15 @@ export async function runIntelligenceCli(options, packageManifest) {
24
26
  profile,
25
27
  sources,
26
28
  json: options.json,
27
- env: process.env
29
+ env: process.env,
30
+ sessionOverride
28
31
  });
29
32
  case "models":
30
33
  return printIntelligenceModels({
31
34
  profile,
32
35
  json: options.json,
33
- env: process.env
36
+ env: process.env,
37
+ sessionOverride
34
38
  });
35
39
  case "context":
36
40
  return printIntelligenceContext({
@@ -49,7 +53,8 @@ export async function runIntelligenceCli(options, packageManifest) {
49
53
  task: options.intelligenceTask,
50
54
  cloudConsent: options.cloudConsent,
51
55
  json: options.json,
52
- env: process.env
56
+ env: process.env,
57
+ sessionOverride
53
58
  });
54
59
  case "ask":
55
60
  return runIntelligenceAsk({
@@ -63,7 +68,8 @@ export async function runIntelligenceCli(options, packageManifest) {
63
68
  confirmed: options.yes || options.confirm,
64
69
  json: options.json,
65
70
  env: process.env,
66
- packageManifest
71
+ packageManifest,
72
+ sessionOverride
67
73
  });
68
74
  default:
69
75
  throw new Error(
@@ -72,7 +78,15 @@ export async function runIntelligenceCli(options, packageManifest) {
72
78
  }
73
79
  }
74
80
 
75
- async function printIntelligenceStatus({ homeDir, workspaceRoot, profile, sources, json, env }) {
81
+ async function printIntelligenceStatus({
82
+ homeDir,
83
+ workspaceRoot,
84
+ profile,
85
+ sources,
86
+ json,
87
+ env,
88
+ sessionOverride
89
+ }) {
76
90
  const backends = await inspectIntelligenceBackends({
77
91
  env,
78
92
  customProviders: profile.customProviders
@@ -81,7 +95,8 @@ async function printIntelligenceStatus({ homeDir, workspaceRoot, profile, source
81
95
  const routing = resolveRoutingDecision({
82
96
  backends,
83
97
  profile,
84
- cloudConsent: false
98
+ cloudConsent: false,
99
+ sessionOverride
85
100
  });
86
101
 
87
102
  const payload = {
@@ -89,6 +104,7 @@ async function printIntelligenceStatus({ homeDir, workspaceRoot, profile, source
89
104
  homeDir,
90
105
  workspaceRoot,
91
106
  profile: buildProfileJson({ profile, sources }),
107
+ sessionOverride,
92
108
  backends,
93
109
  summary,
94
110
  routing
@@ -119,13 +135,17 @@ async function printIntelligenceStatus({ homeDir, workspaceRoot, profile, source
119
135
  return payload;
120
136
  }
121
137
 
122
- async function printIntelligenceModels({ profile, json, env }) {
138
+ async function printIntelligenceModels({ profile, json, env, sessionOverride }) {
123
139
  const backends = await inspectIntelligenceBackends({
124
140
  env,
125
141
  customProviders: profile.customProviders
126
142
  });
143
+ const filterId = sessionOverride?.preferredBackend ?? null;
144
+ const filtered = filterId
145
+ ? backends.filter((backend) => backend.id === filterId)
146
+ : backends;
127
147
 
128
- const models = backends.flatMap((backend) =>
148
+ const models = filtered.flatMap((backend) =>
129
149
  (backend.models ?? []).map((model) => ({
130
150
  ...model,
131
151
  backendState: backend.state,
@@ -133,11 +153,16 @@ async function printIntelligenceModels({ profile, json, env }) {
133
153
  }))
134
154
  );
135
155
 
136
- const payload = { readOnly: true, models, backends: backends.map((entry) => ({
137
- id: entry.id,
138
- state: entry.state,
139
- available: entry.available
140
- })) };
156
+ const payload = {
157
+ readOnly: true,
158
+ sessionOverride,
159
+ models,
160
+ backends: filtered.map((entry) => ({
161
+ id: entry.id,
162
+ state: entry.state,
163
+ available: entry.available
164
+ }))
165
+ };
141
166
 
142
167
  if (json) {
143
168
  console.log(JSON.stringify(payload, null, 2));
@@ -219,7 +244,8 @@ async function printIntelligenceRoute({
219
244
  task,
220
245
  cloudConsent,
221
246
  json,
222
- env
247
+ env,
248
+ sessionOverride
223
249
  }) {
224
250
  const backends = await inspectIntelligenceBackends({
225
251
  env,
@@ -231,11 +257,13 @@ async function printIntelligenceRoute({
231
257
  profile,
232
258
  contextPack: pack,
233
259
  task,
234
- cloudConsent: Boolean(cloudConsent)
260
+ cloudConsent: Boolean(cloudConsent),
261
+ sessionOverride
235
262
  });
236
263
 
237
264
  const payload = {
238
265
  readOnly: true,
266
+ sessionOverride,
239
267
  routing,
240
268
  estimatedTokens: pack.estimatedTokens,
241
269
  evidenceUsed: pack.evidence.filter((entry) => entry.kind === "file").map((entry) => entry.path)
@@ -266,7 +294,8 @@ async function runIntelligenceAsk({
266
294
  cloudConsent,
267
295
  confirmed,
268
296
  json,
269
- env
297
+ env,
298
+ sessionOverride
270
299
  }) {
271
300
  if (!prompt && !task) {
272
301
  throw new Error(`Missing prompt. Use: ${formatCliCommand("intelligence ask --prompt \"...\"")}`);
@@ -281,7 +310,8 @@ async function runIntelligenceAsk({
281
310
  includePrivate,
282
311
  cloudConsent,
283
312
  confirmed,
284
- env
313
+ env,
314
+ sessionOverride
285
315
  });
286
316
 
287
317
  if (json) {
@@ -289,6 +319,7 @@ async function runIntelligenceAsk({
289
319
  ok: outcome.ok,
290
320
  mode: outcome.mode,
291
321
  diagnosticsOnly: outcome.diagnosticsOnly,
322
+ sessionOverride,
292
323
  routing: outcome.routing,
293
324
  explanation: outcome.explanation,
294
325
  telemetry: outcome.telemetry,
@@ -0,0 +1,37 @@
1
+ import { BACKEND_IDS } from "./intelligence/index.js";
2
+
3
+ const EPHEMERAL_BACKENDS = new Set([
4
+ BACKEND_IDS.OLLAMA,
5
+ BACKEND_IDS.OPENROUTER,
6
+ BACKEND_IDS.OPENCODE_GO,
7
+ BACKEND_IDS.OPENCODE_ZEN,
8
+ BACKEND_IDS.OPENCODE
9
+ ]);
10
+
11
+ const BACKEND_HINT =
12
+ "Use opencode-go, opencode-zen, opencode, ollama, or openrouter.";
13
+
14
+ /**
15
+ * Resolve ephemeral CLI --backend/--model overrides.
16
+ * Does not read or write profiles; credentials stay in env.
17
+ */
18
+ export function resolveSessionOverride(options = {}) {
19
+ if (options.intelligenceBackend != null && String(options.intelligenceBackend).trim() === "") {
20
+ throw new Error(`Missing --backend value. ${BACKEND_HINT}`);
21
+ }
22
+
23
+ const preferredBackend = options.intelligenceBackend == null
24
+ ? null
25
+ : String(options.intelligenceBackend).trim();
26
+ const preferredModel = options.model == null || String(options.model).trim() === ""
27
+ ? null
28
+ : String(options.model).trim();
29
+
30
+ if (!preferredBackend && !preferredModel) return null;
31
+
32
+ if (preferredBackend && !EPHEMERAL_BACKENDS.has(preferredBackend)) {
33
+ throw new Error(`Unknown --backend "${preferredBackend}". ${BACKEND_HINT}`);
34
+ }
35
+
36
+ return { preferredBackend, preferredModel };
37
+ }
@@ -8,6 +8,7 @@ export function buildControlPlaneJson(report, { cliVersion, extras = {} } = {})
8
8
  overall: report.overall,
9
9
  agents: report.agents,
10
10
  components: report.components,
11
+ componentHealth: report.componentHealth ?? report.components,
11
12
  checks: report.checks,
12
13
  backups: report.backups,
13
14
  nextAction: report.nextAction,
@@ -1,41 +1,23 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { createManagedSectionBuilder } from "./component-builders.js";
3
+ import { normalizeCatalogDocument } from "./component-manifest.js";
3
4
 
4
5
  const defaultCatalogUrl = new URL("../../global-template/components/catalog.json", import.meta.url);
5
6
 
6
- function validateCatalogEntry(entry) {
7
- const required = ["id", "label", "version", "defaultEnabled", "assetFiles"];
8
-
9
- for (const field of required) {
10
- if (entry[field] === undefined) {
11
- throw new Error(`Component catalog entry is missing "${field}".`);
12
- }
13
- }
14
-
15
- if (!Array.isArray(entry.assetFiles) || entry.assetFiles.length === 0) {
16
- throw new Error(`Component "${entry.id}" must declare at least one asset file.`);
17
- }
18
- }
19
-
20
7
  export function readComponentCatalogDocument(catalogUrl = defaultCatalogUrl) {
21
8
  return JSON.parse(readFileSync(catalogUrl, "utf8"));
22
9
  }
23
10
 
24
11
  export function loadComponentCatalog(catalogUrl = defaultCatalogUrl) {
25
12
  const catalog = readComponentCatalogDocument(catalogUrl);
26
-
27
- return catalog.components.map((entry) => {
28
- validateCatalogEntry(entry);
29
-
30
- return {
31
- id: entry.id,
32
- label: entry.label,
33
- version: entry.version,
34
- source: "bundled",
35
- defaultEnabled: entry.defaultEnabled,
36
- assetFiles: [...entry.assetFiles],
37
- adapterHints: { ...(entry.adapterHints ?? {}) },
38
- buildManagedSection: createManagedSectionBuilder(entry.id, entry)
39
- };
13
+ const { components } = normalizeCatalogDocument(catalog, {
14
+ source: "bundled",
15
+ requireDefaultEnabled: true
40
16
  });
17
+
18
+ return components.map((entry) => ({
19
+ ...entry,
20
+ source: "bundled",
21
+ buildManagedSection: createManagedSectionBuilder(entry.id, entry)
22
+ }));
41
23
  }
@@ -1,6 +1,7 @@
1
1
  import { existsSync, lstatSync, readFileSync, realpathSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
3
  import { createWorkspaceManagedSectionBuilder } from "./component-builders.js";
4
+ import { normalizeCatalogDocument } from "./component-manifest.js";
4
5
  import { isPathInside } from "./component-paths.js";
5
6
 
6
7
  export function workspaceCatalogPath(workspaceRoot) {
@@ -24,77 +25,35 @@ export function loadWorkspaceComponentCatalog(workspaceRoot, { bundledIds = [] }
24
25
  const resolvedRoot = resolve(workspaceRoot);
25
26
  const catalog = readWorkspaceCatalogDocument(resolvedRoot);
26
27
  const componentsRoot = join(resolvedRoot, ".harness", "components");
27
- const entries = catalog.components;
28
-
29
- if (!Array.isArray(entries)) {
30
- throw new Error("Workspace component catalog must declare a components array.");
31
- }
32
-
33
- const seen = new Set();
34
- const components = [];
35
-
36
- for (const entry of entries) {
37
- validateWorkspaceEntry(entry, { componentsRoot, workspaceRoot: resolvedRoot, bundledIds, seen });
38
- seen.add(entry.id);
28
+ const { components: entries } = normalizeCatalogDocument(catalog, {
29
+ source: "workspace",
30
+ bundledIds,
31
+ requireDefaultEnabled: false
32
+ });
33
+
34
+ return entries.map((entry) => {
35
+ const componentDir = join(componentsRoot, entry.id);
36
+
37
+ for (const assetFile of entry.assetFiles) {
38
+ validateAssetPresence(assetFile, {
39
+ componentDir,
40
+ componentId: entry.id,
41
+ workspaceRoot: resolvedRoot
42
+ });
43
+ }
39
44
 
40
- components.push({
41
- id: entry.id,
42
- label: entry.label,
43
- version: entry.version,
45
+ return {
46
+ ...entry,
44
47
  source: "workspace",
45
48
  defaultEnabled: false,
46
- assetFiles: [...entry.assetFiles],
47
- adapterHints: { ...(entry.adapterHints ?? {}) },
48
49
  instructions: entry.instructions ?? null,
49
50
  workspaceRoot: resolvedRoot,
50
51
  buildManagedSection: createWorkspaceManagedSectionBuilder(entry)
51
- });
52
- }
53
-
54
- return components;
55
- }
56
-
57
- function validateWorkspaceEntry(entry, { componentsRoot, workspaceRoot, bundledIds, seen }) {
58
- const required = ["id", "label", "version", "assetFiles"];
59
-
60
- for (const field of required) {
61
- if (entry[field] === undefined) {
62
- throw new Error(`Workspace component catalog entry is missing "${field}".`);
63
- }
64
- }
65
-
66
- if (!Array.isArray(entry.assetFiles) || entry.assetFiles.length === 0) {
67
- throw new Error(`Workspace component "${entry.id}" must declare at least one asset file.`);
68
- }
69
-
70
- if (bundledIds.includes(entry.id)) {
71
- throw new Error(`Workspace component "${entry.id}" conflicts with a bundled component.`);
72
- }
73
-
74
- if (seen.has(entry.id)) {
75
- throw new Error(`Duplicate workspace component id "${entry.id}".`);
76
- }
77
-
78
- const componentDir = join(componentsRoot, entry.id);
79
-
80
- for (const assetFile of entry.assetFiles) {
81
- validateAssetFile(assetFile, { componentDir, componentId: entry.id, workspaceRoot });
82
- }
52
+ };
53
+ });
83
54
  }
84
55
 
85
- function validateAssetFile(assetFile, { componentDir, componentId, workspaceRoot }) {
86
- if (typeof assetFile !== "string" || assetFile.length === 0) {
87
- throw new Error(`Workspace component "${componentId}" has an invalid asset path.`);
88
- }
89
-
90
- if (assetFile.startsWith("/") || assetFile.includes("\\") || assetFile.includes("..")) {
91
- throw new Error(`Workspace component "${componentId}" asset "${assetFile}" must be a relative path without "..".`);
92
- }
93
-
94
- if (assetFile.endsWith("/")) {
95
- throw new Error(`Workspace component "${componentId}" asset "${assetFile}" must reference a file.`);
96
- }
97
-
56
+ function validateAssetPresence(assetFile, { componentDir, componentId, workspaceRoot }) {
98
57
  const assetPath = join(componentDir, ...assetFile.split("/"));
99
58
  const normalized = resolve(assetPath);
100
59
 
@@ -1,5 +1,9 @@
1
1
  import { resolveAdapter } from "./registry.js";
2
2
  import { resolveComponent } from "./component-registry.js";
3
+ import { normalizeSddState } from "./integrations/sdd-state.js";
4
+
5
+ export const STATE_VERSION = 4;
6
+ export const UNSUPPORTED_STATE_VERSION = "UNSUPPORTED_STATE_VERSION";
3
7
 
4
8
  const LEGACY_ORCHESTRATOR_COMPONENT = {
5
9
  id: "orchestrator",
@@ -7,17 +11,33 @@ const LEGACY_ORCHESTRATOR_COMPONENT = {
7
11
  managedTargets: []
8
12
  };
9
13
 
14
+ /** Fail closed on future state versions — never silently downgrade. */
15
+ export function assertCompatibleStateVersion(state) {
16
+ const version = state?.stateVersion;
17
+ if (typeof version === "number" && version > STATE_VERSION) {
18
+ const error = new Error(
19
+ `Unsupported stateVersion ${version}; this runtime supports up to ${STATE_VERSION}.`
20
+ );
21
+ error.code = UNSUPPORTED_STATE_VERSION;
22
+ throw error;
23
+ }
24
+ }
25
+
26
+ /** Normalize any compatible v3/v4 (or legacy) state into explicit stateVersion 4. Idempotent for v4. */
10
27
  export function normalizeGlobalState(state) {
11
28
  if (!state) return null;
29
+ assertCompatibleStateVersion(state);
12
30
 
13
31
  const adapters = state.adapters ?? migrateLegacyAgents(state.agents ?? []);
14
32
  const components = state.components ?? migrateLegacyComponents(state, adapters);
15
33
 
16
34
  return {
17
35
  ...state,
36
+ stateVersion: STATE_VERSION,
18
37
  adapters,
19
38
  agents: toLegacyAgents(adapters),
20
- components
39
+ components,
40
+ sdd: normalizeSddState(state.sdd)
21
41
  };
22
42
  }
23
43
 
@@ -1,9 +1,10 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import { dirname } from "node:path";
4
- import { normalizeGlobalState } from "./state-migration.js";
4
+ import { normalizeGlobalState, STATE_VERSION, UNSUPPORTED_STATE_VERSION } from "./state-migration.js";
5
+ import { normalizeSddState } from "./integrations/sdd-state.js";
5
6
 
6
- export const STATE_VERSION = 3;
7
+ export { STATE_VERSION, UNSUPPORTED_STATE_VERSION };
7
8
 
8
9
  export async function readGlobalState(statePath) {
9
10
  if (!existsSync(statePath)) return null;
@@ -11,14 +12,16 @@ export async function readGlobalState(statePath) {
11
12
  try {
12
13
  const raw = JSON.parse(await readFile(statePath, "utf8"));
13
14
  return normalizeGlobalState(raw);
14
- } catch {
15
+ } catch (error) {
16
+ if (error?.code === UNSUPPORTED_STATE_VERSION) throw error;
15
17
  return null;
16
18
  }
17
19
  }
18
20
 
19
21
  export async function writeGlobalState(statePath, state) {
22
+ const normalized = normalizeGlobalState(state) ?? state;
20
23
  await mkdir(dirname(statePath), { recursive: true });
21
- await writeFile(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
24
+ await writeFile(statePath, `${JSON.stringify(normalized, null, 2)}\n`, "utf8");
22
25
  }
23
26
 
24
27
  export function createGlobalState({
@@ -28,13 +31,14 @@ export function createGlobalState({
28
31
  components = [],
29
32
  coreFiles,
30
33
  backups,
31
- installedAt
34
+ installedAt,
35
+ sdd
32
36
  }) {
33
37
  const now = new Date().toISOString();
34
38
  const normalizedAdapters = adapters.map((entry) => ({ ...entry }));
35
39
  const normalizedComponents = components.map((entry) => ({ ...entry }));
36
40
 
37
- return {
41
+ return normalizeGlobalState({
38
42
  stateVersion: STATE_VERSION,
39
43
  packageName,
40
44
  cliVersion,
@@ -45,6 +49,7 @@ export function createGlobalState({
45
49
  agents: normalizedAdapters.map(({ id, configFile, present }) => ({ id, configFile, present })),
46
50
  components: normalizedComponents,
47
51
  coreFiles,
48
- backups
49
- };
52
+ backups,
53
+ sdd: normalizeSddState(sdd)
54
+ });
50
55
  }
@@ -1,4 +1,5 @@
1
1
  import { listBackupSnapshots } from "./backups.js";
2
+ import { buildComponentHealthEntries } from "./component-health.js";
2
3
  import { runGlobalDoctorChecks } from "./global-doctor.js";
3
4
  import { buildEffectivePolicy } from "./policy.js";
4
5
  import { harnessHomePaths } from "./paths.js";
@@ -20,16 +21,8 @@ export async function buildStatusReport(homeDir, { packageRoot, workspaceRoot =
20
21
  managed: installedAgentIds.has(id)
21
22
  }));
22
23
 
23
- const components = (state?.components ?? []).map((entry) => {
24
- const related = doctor.checks.filter((check) => check.componentId === entry.id);
25
- const status = summarizeCheckStatuses(related);
26
- return {
27
- id: entry.id,
28
- version: entry.version,
29
- source: entry.source ?? "bundled",
30
- status
31
- };
32
- });
24
+ const components = buildComponentHealthEntries(state?.components ?? [], doctor.checks)
25
+ .map(({ checks, ...entry }) => entry);
33
26
 
34
27
  const counts = {
35
28
  ok: doctor.checks.filter((check) => check.status === "ok").length,
@@ -48,6 +41,7 @@ export async function buildStatusReport(homeDir, { packageRoot, workspaceRoot =
48
41
  state,
49
42
  agents,
50
43
  components,
44
+ componentHealth: doctor.componentHealth,
51
45
  checks: doctor.checks,
52
46
  backups: backups.length,
53
47
  counts,
@@ -58,13 +52,6 @@ export async function buildStatusReport(homeDir, { packageRoot, workspaceRoot =
58
52
  };
59
53
  }
60
54
 
61
- function summarizeCheckStatuses(checks) {
62
- if (checks.length === 0) return "ok";
63
- if (checks.some((check) => check.status === "missing")) return "missing";
64
- if (checks.some((check) => check.status === "stale")) return "stale";
65
- return "ok";
66
- }
67
-
68
55
  function resolveOverallStatus({ state, doctor }) {
69
56
  if (!state) return "missing";
70
57
  if (doctor.hasDrift) return "drift";
@@ -6,6 +6,7 @@ import {
6
6
  shouldPromptApplyConfirmation
7
7
  } from "./apply-confirmation.js";
8
8
  import { buildDiffReport } from "./diff.js";
9
+ import { needsManagedRepair } from "./governance-repair.js";
9
10
  import { harnessHomePaths } from "./paths.js";
10
11
  import { printManagedPreflight, shouldShowPreflight, summarizeDiffPreflight } from "./preflight.js";
11
12
  import { loadConsentAudit } from "./policy.js";
@@ -42,11 +43,7 @@ export async function runHarnessSync({
42
43
  }
43
44
 
44
45
  const preReport = await buildStatusReport(homeDir, { packageRoot, workspaceRoot });
45
- const needsRepair = preReport.overall === "drift"
46
- || preReport.counts.missing > 0
47
- || preReport.counts.stale > 0;
48
-
49
- if (!needsRepair) {
46
+ if (!needsManagedRepair(preReport)) {
50
47
  return {
51
48
  action: "noop",
52
49
  wrote: false,