@danielblomma/cortex-mcp 2.0.7 → 2.0.9

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@danielblomma/cortex-mcp",
3
3
  "mcpName": "io.github.DanielBlomma/cortex",
4
- "version": "2.0.7",
4
+ "version": "2.0.9",
5
5
  "description": "Local, repo-scoped context platform for coding assistants. Semantic search, graph relationships, and architectural rule context.",
6
6
  "type": "module",
7
7
  "author": "Daniel Blomma",
@@ -9,6 +9,7 @@ import {
9
9
  type WorkflowDefinition,
10
10
  } from "../core/workflow/index.js";
11
11
  import { DEFAULT_WORKFLOWS } from "../core/workflow/default-workflows.js";
12
+ import { isEnterpriseProject } from "../hooks/shared.js";
12
13
 
13
14
  /**
14
15
  * `cortex stage` CLI surface. Each subcommand is a thin shell wrapper
@@ -31,10 +32,23 @@ import { DEFAULT_WORKFLOWS } from "../core/workflow/default-workflows.js";
31
32
  * standalone shell calls.
32
33
  */
33
34
 
35
+ const ENTERPRISE_REQUIRED_MESSAGE =
36
+ "cortex stage is part of the Cortex Harness — an enterprise-only feature. " +
37
+ "This project does not have an enterprise license configured (no enterprise.api_key " +
38
+ "in .context/enterprise.yml). Run 'cortex enterprise <api-key>' to enable it, or " +
39
+ "contact your org admin.";
40
+
34
41
  export async function runStageCommand(args: string[]): Promise<void> {
35
42
  const sub = args[0] ?? "help";
36
43
  const rest = args.slice(1);
37
44
 
45
+ // help is allowed in community mode so users can discover the feature exists.
46
+ if (sub !== "help" && sub !== "--help" && sub !== "-h") {
47
+ if (!isEnterpriseProject(projectRoot())) {
48
+ throw new Error(ENTERPRISE_REQUIRED_MESSAGE);
49
+ }
50
+ }
51
+
38
52
  switch (sub) {
39
53
  case "start":
40
54
  return runStart(rest);
@@ -65,6 +79,8 @@ function printHelp(): void {
65
79
  " cortex stage envelope --task-id <id> [--stage <name>]",
66
80
  " cortex stage advance --task-id <id> --stage <name> --body-file <path>",
67
81
  " [--frontmatter-file <path>] [--status <s>] [--outcome-file <path>]",
82
+ " [--validators-passed <id1,id2,...>]",
83
+ " [--override-reason \"...\"] [--override-skipped-validators <id1,id2>]",
68
84
  " cortex stage run --task-id <id> -- <command> [args...]",
69
85
  "",
70
86
  "Status values: complete (default) | blocked | failed",
@@ -193,12 +209,36 @@ async function runAdvance(args: string[]): Promise<void> {
193
209
  typeof flags["outcome-file"] === "string" ? flags["outcome-file"] : null;
194
210
  const statusFlag =
195
211
  typeof flags.status === "string" ? (flags.status as StageStatus) : undefined;
212
+ const validatorsRaw =
213
+ typeof flags["validators-passed"] === "string"
214
+ ? flags["validators-passed"]
215
+ : null;
216
+ const overrideReason =
217
+ typeof flags["override-reason"] === "string"
218
+ ? flags["override-reason"]
219
+ : null;
220
+ const overrideSkippedRaw =
221
+ typeof flags["override-skipped-validators"] === "string"
222
+ ? flags["override-skipped-validators"]
223
+ : null;
196
224
 
197
225
  const body = readFileSync(bodyPath, "utf8");
198
226
  const frontmatter: Record<string, unknown> = frontmatterPath
199
227
  ? parseJsonObject(frontmatterPath)
200
228
  : {};
201
229
  const outcome = outcomePath ? parseJsonObject(outcomePath) : undefined;
230
+ const validatorsPassed = validatorsRaw
231
+ ? validatorsRaw.split(",").map((s) => s.trim()).filter(Boolean)
232
+ : undefined;
233
+ const override = overrideReason
234
+ ? {
235
+ reason: overrideReason,
236
+ skipped_validators: overrideSkippedRaw
237
+ ? overrideSkippedRaw.split(",").map((s) => s.trim()).filter(Boolean)
238
+ : [],
239
+ skipped_requirements: [],
240
+ }
241
+ : undefined;
202
242
 
203
243
  const state = getRunState(projectRoot(), taskId);
204
244
  if (!state) {
@@ -234,6 +274,8 @@ async function runAdvance(args: string[]): Promise<void> {
234
274
  body,
235
275
  status: finalStatus,
236
276
  outcome,
277
+ validatorsPassed,
278
+ override,
237
279
  });
238
280
 
239
281
  let nextEnvelope = null;
@@ -17,6 +17,7 @@ export const SECURE_BUILD_WORKFLOW: WorkflowDefinition = {
17
17
  artifact: "plan.md",
18
18
  reads: [],
19
19
  required_fields: ["files_targeted", "constraints"],
20
+ validators: [],
20
21
  capability: "planner",
21
22
  description:
22
23
  "Produce a step-by-step implementation plan grounded in the repo's rules and memory.",
@@ -26,6 +27,7 @@ export const SECURE_BUILD_WORKFLOW: WorkflowDefinition = {
26
27
  artifact: "plan-review.md",
27
28
  reads: ["plan"],
28
29
  required_fields: ["approved", "blocking_comments"],
30
+ validators: [],
29
31
  capability: "reviewer",
30
32
  description:
31
33
  "Review the plan for architectural fit and rule compliance before any code is written.",
@@ -35,6 +37,18 @@ export const SECURE_BUILD_WORKFLOW: WorkflowDefinition = {
35
37
  artifact: "changes.md",
36
38
  reads: ["plan", "plan-review"],
37
39
  required_fields: ["files_changed"],
40
+ validators: [
41
+ {
42
+ id: "build-passes",
43
+ description:
44
+ "The project's build / typecheck command must succeed on the produced diff.",
45
+ },
46
+ {
47
+ id: "tests-pass",
48
+ description:
49
+ "The project's existing test suite must pass on the produced diff.",
50
+ },
51
+ ],
38
52
  capability: "builder",
39
53
  description:
40
54
  "Implement the approved plan. Produces the diff manifest used by the downstream reviewers.",
@@ -44,6 +58,7 @@ export const SECURE_BUILD_WORKFLOW: WorkflowDefinition = {
44
58
  artifact: "build-review.md",
45
59
  reads: ["plan", "changes"],
46
60
  required_fields: ["approved", "blocking_comments"],
61
+ validators: [],
47
62
  capability: "reviewer",
48
63
  description:
49
64
  "Review the implementation against the plan and the project's rules.",
@@ -53,6 +68,13 @@ export const SECURE_BUILD_WORKFLOW: WorkflowDefinition = {
53
68
  artifact: "mutation-report.md",
54
69
  reads: ["changes"],
55
70
  required_fields: ["score", "survived"],
71
+ validators: [
72
+ {
73
+ id: "mutation-score",
74
+ description:
75
+ "Mutation testing tool of the agent's choice (e.g. stryker, mutmut) must run and report a score against the changed files.",
76
+ },
77
+ ],
56
78
  capability: "tester",
57
79
  description:
58
80
  "Run mutation tests on the changed files. Report score + surviving mutants.",
@@ -62,6 +84,18 @@ export const SECURE_BUILD_WORKFLOW: WorkflowDefinition = {
62
84
  artifact: "security-report.md",
63
85
  reads: ["changes"],
64
86
  required_fields: ["findings", "severity_summary"],
87
+ validators: [
88
+ {
89
+ id: "secret-scan",
90
+ description:
91
+ "Scan the diff for newly committed secrets (API keys, tokens, credentials).",
92
+ },
93
+ {
94
+ id: "dependency-audit",
95
+ description:
96
+ "Audit any added or upgraded dependencies for known vulnerabilities.",
97
+ },
98
+ ],
65
99
  capability: "security-reviewer",
66
100
  description:
67
101
  "Security review focused on the diff: injection, authn/authz, secrets, dependency risk.",
@@ -71,6 +105,7 @@ export const SECURE_BUILD_WORKFLOW: WorkflowDefinition = {
71
105
  artifact: "approval.md",
72
106
  reads: ["plan", "changes", "build-review", "mutation", "security"],
73
107
  required_fields: ["approved", "approver"],
108
+ validators: [],
74
109
  capability: "human",
75
110
  description:
76
111
  "Human sign-off. Pulls every prior artifact for the approver to read; the approver writes the artifact.",
@@ -4,6 +4,7 @@ import { readRunState } from "./artifact-io.js";
4
4
  import { DEFAULT_CAPABILITIES, type CapabilityDefinition } from "./capabilities.js";
5
5
  import { workflowDefinitionSchema, type WorkflowDefinition } from "./schemas.js";
6
6
  import { DEFAULT_WORKFLOWS } from "./default-workflows.js";
7
+ import { loadSyncedCapabilities } from "./synced-capability-registry.js";
7
8
 
8
9
  /**
9
10
  * Pre-tool-use enforcement for the harness. Pure function: takes the tool
@@ -83,7 +84,12 @@ export function evaluateToolCall(options: EvaluateOptions): EnforcementResult {
83
84
  return { allowed: true, reason: "stage has no capability declared" };
84
85
  }
85
86
 
86
- const capabilities = options.capabilities ?? DEFAULT_CAPABILITIES;
87
+ // When the caller passes an explicit registry, use it as-is (tests).
88
+ // Otherwise merge bundled defaults with the daemon-synced org-authored
89
+ // capabilities, with synced ones taking precedence on name collisions
90
+ // so org overrides actually override.
91
+ const capabilities =
92
+ options.capabilities ?? { ...DEFAULT_CAPABILITIES, ...loadSyncedCapabilities() };
87
93
  const capability = capabilities[stage.capability];
88
94
  if (!capability) {
89
95
  return {
@@ -35,6 +35,8 @@ export type ComposedEnvelope = {
35
35
  requiredFields: string[];
36
36
  /** Capability key the stage runs under (informational). */
37
37
  capability: string | null;
38
+ /** Validators the stage requires the agent to have run. */
39
+ validators: { id: string; description: string }[];
38
40
  };
39
41
 
40
42
  export type ComposeStageEnvelopeOptions = {
@@ -107,6 +109,7 @@ export function composeStageEnvelope(
107
109
 
108
110
  const requiredFields = stage.required_fields;
109
111
  const capability = stage.capability ?? null;
112
+ const validators = stage.validators;
110
113
 
111
114
  const prompt = renderPrompt({
112
115
  taskDescription: state.task_description,
@@ -118,6 +121,7 @@ export function composeStageEnvelope(
118
121
  requiredFields,
119
122
  capability,
120
123
  handoffs,
124
+ validators,
121
125
  });
122
126
 
123
127
  return {
@@ -125,6 +129,7 @@ export function composeStageEnvelope(
125
129
  expectedArtifact: stage.artifact,
126
130
  requiredFields,
127
131
  capability,
132
+ validators,
128
133
  };
129
134
  }
130
135
 
@@ -150,6 +155,7 @@ type RenderPromptOptions = {
150
155
  requiredFields: string[];
151
156
  capability: string | null;
152
157
  handoffs: string[];
158
+ validators: { id: string; description: string }[];
153
159
  };
154
160
 
155
161
  function renderPrompt(o: RenderPromptOptions): string {
@@ -200,13 +206,32 @@ function renderPrompt(o: RenderPromptOptions): string {
200
206
  ? `_No additional required fields beyond the harness defaults._`
201
207
  : o.requiredFields.map((f) => `- \`${f}\``).join("\n");
202
208
 
209
+ const validatorLines =
210
+ o.validators.length === 0
211
+ ? `_No validators required for this stage._`
212
+ : o.validators
213
+ .map((v) => `- \`${v.id}\` — ${v.description}`)
214
+ .join("\n");
215
+
216
+ sections.push(
217
+ [
218
+ `# VALIDATORS`,
219
+ ``,
220
+ `Cortex defines what must be validated; you (the agent) pick the concrete tooling and run it in this environment, then report back.`,
221
+ ``,
222
+ validatorLines,
223
+ ``,
224
+ `When you call \`cortex.workflow.advance\`, set \`validators_passed: [<id1>, <id2>, ...]\` listing every validator you successfully ran. The harness blocks the advance if a required validator is missing — unless you explicitly pass \`override\` with a reason and the list of skipped validator ids.`,
225
+ ].join("\n"),
226
+ );
227
+
203
228
  sections.push(
204
229
  [
205
230
  `# OUTPUT`,
206
231
  ``,
207
232
  `Produce a single markdown file named \`${o.expectedArtifact}\` with YAML frontmatter on top.`,
208
233
  ``,
209
- `Required frontmatter fields (in addition to \`stage\`, \`status\`, \`references\`, \`written_at\` which the harness manages):`,
234
+ `Required frontmatter fields (in addition to \`stage\`, \`status\`, \`references\`, \`validators_passed\`, \`written_at\` which the harness manages):`,
210
235
  ``,
211
236
  requiredLines,
212
237
  ``,
@@ -7,3 +7,4 @@ export * from "./mcp-tools.js";
7
7
  export * from "./capabilities.js";
8
8
  export * from "./enforcement.js";
9
9
  export * from "./synced-registry.js";
10
+ export * from "./synced-capability-registry.js";
@@ -4,6 +4,7 @@ import { composeStageEnvelope } from "./envelope.js";
4
4
  import { DEFAULT_WORKFLOWS } from "./default-workflows.js";
5
5
  import { loadSyncedWorkflows } from "./synced-registry.js";
6
6
  import {
7
+ stageOverrideSchema,
7
8
  stageStatusSchema,
8
9
  type StageStatus,
9
10
  type WorkflowDefinition,
@@ -45,6 +46,19 @@ export const WorkflowAdvanceInput = z.object({
45
46
  status: stageStatusSchema.optional(),
46
47
  /** Optional structured outcome surfaced into state.json for fast lookup by later stages. */
47
48
  outcome: z.record(z.string(), z.unknown()).optional(),
49
+ /**
50
+ * Validators the agent reports having run for this stage. Compared
51
+ * against the stage definition's required validators on advance —
52
+ * missing entries block the call unless an override is supplied.
53
+ */
54
+ validators_passed: z.array(z.string().min(1)).default([]),
55
+ /**
56
+ * Process override. Required when validators_passed does not cover
57
+ * every validator the stage declares. Logged as a high-evidence
58
+ * audit event and stamped into the artifact's frontmatter so the
59
+ * deviation is visible in the evidence trail.
60
+ */
61
+ override: stageOverrideSchema.optional(),
48
62
  });
49
63
  export type WorkflowAdvanceInputT = z.infer<typeof WorkflowAdvanceInput>;
50
64
 
@@ -155,6 +169,8 @@ export function runWorkflowAdvance(
155
169
  body: input.body,
156
170
  outcome: input.outcome,
157
171
  status: finalStatus,
172
+ validatorsPassed: input.validators_passed,
173
+ override: input.override,
158
174
  });
159
175
 
160
176
  // If the run is still going, also return the next envelope so the caller
@@ -6,8 +6,10 @@ import {
6
6
  import {
7
7
  runStateSchema,
8
8
  stageArtifactFrontmatterSchema,
9
+ stageOverrideSchema,
9
10
  workflowDefinitionSchema,
10
11
  type RunState,
12
+ type StageOverride,
11
13
  type StageRecord,
12
14
  type StageStatus,
13
15
  type WorkflowDefinition,
@@ -36,6 +38,7 @@ export function createRun(options: CreateRunOptions): RunState {
36
38
  const stages: StageRecord[] = workflow.stages.map((stage) => ({
37
39
  name: stage.name,
38
40
  status: "pending" as StageStatus,
41
+ validators_passed: [],
39
42
  }));
40
43
 
41
44
  const state: RunState = {
@@ -80,6 +83,10 @@ export type AdvanceStageOptions = {
80
83
  outcome?: Record<string, unknown>;
81
84
  /** Final status to record for this stage. Defaults to "complete". */
82
85
  status?: StageStatus;
86
+ /** Validators the agent reports having run. Compared against stage.validators. */
87
+ validatorsPassed?: string[];
88
+ /** Process override; required when validators_passed doesn't cover stage.validators. */
89
+ override?: StageOverride;
83
90
  now?: () => Date;
84
91
  };
85
92
 
@@ -111,12 +118,52 @@ export function advanceStage(options: AdvanceStageOptions): RunState {
111
118
  );
112
119
  }
113
120
 
121
+ const stageIndex = workflow.stages.findIndex((s) => s.name === options.stageName);
122
+ const stageDef = workflow.stages[stageIndex];
123
+
124
+ // Validator coverage check: every validator the stage declares must
125
+ // appear in validators_passed unless the override explicitly skips it.
126
+ // Process is enforced here even though the validators themselves run
127
+ // in the agent's environment.
128
+ const validatorsPassed = options.validatorsPassed ?? [];
129
+ const override = options.override
130
+ ? stageOverrideSchema.parse(options.override)
131
+ : undefined;
132
+ const requiredValidators = stageDef.validators.map((v) => v.id);
133
+ const declaredSkipped = new Set(override?.skipped_validators ?? []);
134
+ const missingValidators = requiredValidators.filter(
135
+ (id) => !validatorsPassed.includes(id) && !declaredSkipped.has(id),
136
+ );
137
+ // blocked / failed stages are exempt from validator coverage — the
138
+ // stage is explicitly halting before completion, so the validators
139
+ // logically cannot have run.
140
+ const finalStatus = options.status ?? "complete";
141
+ const exemptStatus = finalStatus === "blocked" || finalStatus === "failed";
142
+ if (missingValidators.length > 0 && !exemptStatus) {
143
+ throw new Error(
144
+ `Stage ${options.stageName} requires validators ${requiredValidators.join(", ")} ` +
145
+ `but artifact reported only ${validatorsPassed.join(", ") || "<none>"}. ` +
146
+ `Missing: ${missingValidators.join(", ")}. ` +
147
+ `Pass override.skipped_validators with a reason to advance anyway.`,
148
+ );
149
+ }
150
+
114
151
  const now = (options.now ?? (() => new Date()))();
115
152
  const completedAt = now.toISOString();
116
153
 
117
154
  const frontmatter = stageArtifactFrontmatterSchema.parse({
118
155
  ...options.frontmatter,
119
156
  stage: options.stageName,
157
+ validators_passed: validatorsPassed,
158
+ ...(override
159
+ ? {
160
+ override: {
161
+ reason: override.reason,
162
+ skipped_validators: override.skipped_validators,
163
+ skipped_requirements: override.skipped_requirements,
164
+ },
165
+ }
166
+ : {}),
120
167
  written_at: options.frontmatter.written_at ?? completedAt,
121
168
  });
122
169
  writeStageArtifact(
@@ -127,9 +174,7 @@ export function advanceStage(options: AdvanceStageOptions): RunState {
127
174
  options.body,
128
175
  );
129
176
 
130
- const stageIndex = workflow.stages.findIndex((s) => s.name === options.stageName);
131
177
  const nextStage = workflow.stages[stageIndex + 1] ?? null;
132
- const finalStatus = options.status ?? "complete";
133
178
 
134
179
  const updatedStages: StageRecord[] = state.stages.map((record) => {
135
180
  if (record.name !== options.stageName) return record;
@@ -140,6 +185,8 @@ export function advanceStage(options: AdvanceStageOptions): RunState {
140
185
  started_at: record.started_at ?? state.started_at,
141
186
  completed_at: completedAt,
142
187
  outcome: options.outcome,
188
+ validators_passed: validatorsPassed,
189
+ override,
143
190
  };
144
191
  });
145
192
 
@@ -26,6 +26,23 @@ const slugSchema = z
26
26
  "Must be lowercase alphanumeric with hyphens (no leading/trailing hyphen)",
27
27
  );
28
28
 
29
+ /**
30
+ * Validator requirement declared by a workflow stage. Cortex enforces
31
+ * that the agent reports having run each declared validator (via the
32
+ * artifact's `validators_passed` frontmatter), but never executes the
33
+ * validator itself — the agent picks the concrete tooling.
34
+ *
35
+ * id is a stable identifier (e.g. "mutation-score") that the agent
36
+ * echoes back; description is human-readable context for the agent
37
+ * rendered into the stage envelope.
38
+ */
39
+ export const validatorRequirementSchema = z.object({
40
+ id: slugSchema,
41
+ description: z.string().min(1).max(500),
42
+ });
43
+
44
+ export type ValidatorRequirement = z.infer<typeof validatorRequirementSchema>;
45
+
29
46
  /**
30
47
  * Static definition of a single stage in a workflow. Authored at the
31
48
  * organization level (in cortex-web later) and synced down to projects.
@@ -37,6 +54,15 @@ export const stageDefinitionSchema = z.object({
37
54
  reads: z.array(slugSchema).default([]),
38
55
  /** Required frontmatter fields the produced artifact must populate. */
39
56
  required_fields: z.array(z.string().min(1)).default([]),
57
+ /**
58
+ * Validators the stage requires the agent to have run. The agent
59
+ * picks the actual tooling (e.g. stryker for mutation testing) and
60
+ * reports the result by listing each validator's id under
61
+ * `validators_passed` in the artifact frontmatter. Cortex enforces
62
+ * the list-coverage contract on advance unless the call carries an
63
+ * explicit override.
64
+ */
65
+ validators: z.array(validatorRequirementSchema).default([]),
40
66
  /** Capability key the stage runs under. References a separate capability registry. */
41
67
  capability: z.string().min(1).optional(),
42
68
  /** Human-readable summary surfaced in dashboards and audit. */
@@ -70,6 +96,21 @@ export const stageStatusSchema = z.enum([
70
96
 
71
97
  export type StageStatus = z.infer<typeof stageStatusSchema>;
72
98
 
99
+ /**
100
+ * Process-override record. When a stage advances despite missing or
101
+ * failed validators, the caller must pass an override with a free-text
102
+ * reason. The override is recorded on the StageRecord, stamped into
103
+ * the artifact's frontmatter, and emitted as a high-evidence audit
104
+ * event so reviewers can see the deviation in the evidence trail.
105
+ */
106
+ export const stageOverrideSchema = z.object({
107
+ reason: z.string().min(1).max(2000),
108
+ skipped_validators: z.array(z.string().min(1)).default([]),
109
+ skipped_requirements: z.array(z.string().min(1)).default([]),
110
+ });
111
+
112
+ export type StageOverride = z.infer<typeof stageOverrideSchema>;
113
+
73
114
  /**
74
115
  * Per-stage record inside state.json. Holds outcome metadata that the next
75
116
  * stage's envelope composer needs without re-parsing every artifact.
@@ -82,6 +123,10 @@ export const stageRecordSchema = z.object({
82
123
  completed_at: z.string().datetime().optional(),
83
124
  /** Frontmatter outcome surfaced for fast lookup (e.g. approved=true on review). */
84
125
  outcome: z.record(z.string(), z.unknown()).optional(),
126
+ /** Validators the agent reported having run for this stage. */
127
+ validators_passed: z.array(z.string().min(1)).default([]),
128
+ /** Override record if the stage advanced despite missing/failed requirements. */
129
+ override: stageOverrideSchema.optional(),
85
130
  });
86
131
 
87
132
  export type StageRecord = z.infer<typeof stageRecordSchema>;
@@ -0,0 +1,66 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import {
5
+ capabilityDefinitionSchema,
6
+ type CapabilityDefinition,
7
+ } from "./capabilities.js";
8
+
9
+ /**
10
+ * Read side of the org-capability sync cache. The daemon's
11
+ * capability-sync-checker writes ~/.cortex/capabilities.local.json;
12
+ * this module reads it. Kept in core/workflow/ rather than daemon/ so
13
+ * enforcement.ts can consult the cache without depending on daemon code.
14
+ *
15
+ * Each entry is validated against capabilityDefinitionSchema before being
16
+ * surfaced — if the cache file is corrupt or contains stale shapes from
17
+ * an older daemon, those entries are silently dropped rather than
18
+ * crashing the read.
19
+ */
20
+
21
+ export const SYNCED_CAPABILITIES_FILENAME = "capabilities.local.json";
22
+
23
+ type LocalCapabilityRecord = {
24
+ capability_name: string;
25
+ updated_at: string;
26
+ definition: unknown;
27
+ };
28
+
29
+ type LocalCapabilitiesState = {
30
+ capabilities?: Record<string, LocalCapabilityRecord>;
31
+ };
32
+
33
+ export function syncedCapabilitiesCachePath(dir?: string): string {
34
+ return join(dir ?? join(homedir(), ".cortex"), SYNCED_CAPABILITIES_FILENAME);
35
+ }
36
+
37
+ /**
38
+ * Returns the synced org-authored capabilities keyed by capability name.
39
+ * Empty object when the cache is missing, unreadable, malformed, or
40
+ * contains no valid entries. The optional `dir` argument is for tests;
41
+ * production callers leave it unset.
42
+ */
43
+ export function loadSyncedCapabilities(
44
+ dir?: string,
45
+ ): Record<string, CapabilityDefinition> {
46
+ const path = syncedCapabilitiesCachePath(dir);
47
+ if (!existsSync(path)) return {};
48
+
49
+ let parsed: LocalCapabilitiesState;
50
+ try {
51
+ parsed = JSON.parse(readFileSync(path, "utf8")) as LocalCapabilitiesState;
52
+ } catch {
53
+ return {};
54
+ }
55
+ const records = parsed.capabilities;
56
+ if (!records || typeof records !== "object") return {};
57
+
58
+ const out: Record<string, CapabilityDefinition> = {};
59
+ for (const [name, record] of Object.entries(records)) {
60
+ if (!record || typeof record !== "object") continue;
61
+ const result = capabilityDefinitionSchema.safeParse(record.definition);
62
+ if (!result.success) continue;
63
+ out[name] = result.data;
64
+ }
65
+ return out;
66
+ }