@danielblomma/cortex-mcp 2.0.16 → 2.0.19

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 (52) hide show
  1. package/README.md +6 -4
  2. package/bin/cortex.mjs +75 -10
  3. package/package.json +2 -1
  4. package/scaffold/.context/config.yaml +2 -3
  5. package/scaffold/.githooks/_cortex-update-runner.sh +2 -2
  6. package/scaffold/mcp/package-lock.json +59 -60
  7. package/scaffold/mcp/package.json +2 -2
  8. package/scaffold/mcp/src/cli/stage.ts +15 -18
  9. package/scaffold/mcp/src/core/validators/builtins.ts +24 -4
  10. package/scaffold/mcp/src/core/workflow/index.ts +1 -0
  11. package/scaffold/mcp/src/core/workflow/mcp-tools.ts +15 -25
  12. package/scaffold/mcp/src/core/workflow/resolution.ts +113 -0
  13. package/scaffold/mcp/src/embeddings.ts +1 -1
  14. package/scaffold/mcp/src/enterprise/index.ts +16 -1
  15. package/scaffold/mcp/src/enterprise/reviews/policy-selection.ts +65 -0
  16. package/scaffold/mcp/src/enterprise/reviews/push.ts +55 -4
  17. package/scaffold/mcp/src/enterprise/reviews/trust-state.ts +305 -0
  18. package/scaffold/mcp/src/enterprise/tools/enterprise.ts +40 -47
  19. package/scaffold/mcp/src/enterprise/tools/harness.ts +2 -0
  20. package/scaffold/mcp/src/enterprise/workflow/state.ts +21 -0
  21. package/scaffold/mcp/src/graph.ts +3 -3
  22. package/scaffold/mcp/tests/enterprise-review-trust.test.mjs +318 -0
  23. package/scaffold/mcp/tests/fixtures/org-skillz-contract.json +59 -0
  24. package/scaffold/mcp/tests/fixtures/review-trust-contract.json +111 -0
  25. package/scaffold/mcp/tests/review-policy-selection.test.mjs +85 -0
  26. package/scaffold/mcp/tests/review-trust-contract.test.mjs +167 -0
  27. package/scaffold/mcp/tests/review-validator-source.test.mjs +52 -0
  28. package/scaffold/mcp/tests/skill-sync-checker.test.mjs +277 -0
  29. package/scaffold/mcp/tests/workflow-cli.test.mjs +102 -1
  30. package/scaffold/mcp/tests/workflow-mcp-tools.test.mjs +22 -0
  31. package/scaffold/mcp/tests/workflow-synced-registry.test.mjs +56 -0
  32. package/scaffold/scripts/bootstrap.sh +9 -8
  33. package/scaffold/scripts/context.sh +1 -1
  34. package/scaffold/scripts/dashboard.mjs +3 -1
  35. package/scaffold/scripts/dashboard.sh +1 -1
  36. package/scaffold/scripts/doctor.sh +2 -1
  37. package/scaffold/scripts/embed.sh +2 -1
  38. package/scaffold/scripts/ingest.mjs +3 -1
  39. package/scaffold/scripts/ingest.sh +3 -2
  40. package/scaffold/scripts/install-git-hooks.sh +2 -1
  41. package/scaffold/scripts/load-kuzu.sh +2 -2
  42. package/scaffold/scripts/load-ryu.sh +2 -1
  43. package/scaffold/scripts/memory-compile.mjs +2 -2
  44. package/scaffold/scripts/memory-compile.sh +3 -2
  45. package/scaffold/scripts/memory-lint.mjs +2 -2
  46. package/scaffold/scripts/memory-lint.sh +3 -2
  47. package/scaffold/scripts/parsers/node_modules/.package-lock.json +0 -1
  48. package/scaffold/scripts/parsers/package-lock.json +0 -1
  49. package/scaffold/scripts/refresh.sh +2 -2
  50. package/scaffold/scripts/status.sh +6 -5
  51. package/scaffold/scripts/update-context.sh +5 -5
  52. package/scaffold/scripts/watch.sh +7 -8
@@ -1,8 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { advanceStage, createRun, getRunState } from "./run-lifecycle.js";
3
3
  import { composeStageEnvelope } from "./envelope.js";
4
- import { DEFAULT_WORKFLOWS } from "./default-workflows.js";
5
- import { loadSyncedWorkflows } from "./synced-registry.js";
4
+ import { resolveWorkflowDefinition } from "./resolution.js";
6
5
  import {
7
6
  stageOverrideSchema,
8
7
  stageStatusSchema,
@@ -88,33 +87,18 @@ export function resolveProjectRoot(): string {
88
87
  export type WorkflowToolContext = {
89
88
  cwd: string;
90
89
  workflows?: Record<string, WorkflowDefinition>;
90
+ bundledFallbackPolicy?: "allow" | "warn" | "block";
91
91
  };
92
92
 
93
- function resolveWorkflow(
94
- workflowId: string,
95
- registry: Record<string, WorkflowDefinition> | undefined,
96
- ): WorkflowDefinition {
97
- // When the caller passes an explicit registry, it wins outright (used
98
- // by tests). Otherwise we merge bundled defaults with the org-authored
99
- // workflows the daemon has synced into ~/.cortex/workflows.local.json,
100
- // with the synced ones taking precedence on workflow_id collisions so
101
- // org overrides actually override.
102
- const workflows =
103
- registry ?? { ...DEFAULT_WORKFLOWS, ...loadSyncedWorkflows() };
104
- const workflow = workflows[workflowId];
105
- if (!workflow) {
106
- throw new Error(
107
- `Unknown workflow_id: ${workflowId}. Available: ${Object.keys(workflows).join(", ") || "<none>"}`,
108
- );
109
- }
110
- return workflow;
111
- }
112
-
113
93
  export function runWorkflowStart(
114
94
  input: WorkflowStartInputT,
115
95
  ctx: WorkflowToolContext,
116
96
  ) {
117
- const workflow = resolveWorkflow(input.workflow_id, ctx.workflows);
97
+ const resolved = resolveWorkflowDefinition(input.workflow_id, {
98
+ registry: ctx.workflows,
99
+ bundledFallbackPolicy: ctx.bundledFallbackPolicy,
100
+ });
101
+ const workflow = resolved.workflow;
118
102
  const state = createRun({
119
103
  cwd: ctx.cwd,
120
104
  taskId: input.task_id,
@@ -129,6 +113,8 @@ export function runWorkflowStart(
129
113
  return {
130
114
  state,
131
115
  envelope,
116
+ workflow_source: resolved.source,
117
+ warnings: resolved.warnings,
132
118
  };
133
119
  }
134
120
 
@@ -142,7 +128,9 @@ export function runWorkflowAdvance(
142
128
  `No run state found for task ${input.task_id}. Call cortex.workflow.start first.`,
143
129
  );
144
130
  }
145
- const workflow = resolveWorkflow(state.workflow_id, ctx.workflows);
131
+ const workflow = resolveWorkflowDefinition(state.workflow_id, {
132
+ registry: ctx.workflows,
133
+ }).workflow;
146
134
  const stage = workflow.stages.find((s) => s.name === input.stage);
147
135
  if (!stage) {
148
136
  throw new Error(`Stage ${input.stage} is not defined in workflow ${workflow.id}`);
@@ -220,7 +208,9 @@ export function runWorkflowEnvelope(
220
208
  `No run state found for task ${input.task_id}. Call cortex.workflow.start first.`,
221
209
  );
222
210
  }
223
- const workflow = resolveWorkflow(state.workflow_id, ctx.workflows);
211
+ const workflow = resolveWorkflowDefinition(state.workflow_id, {
212
+ registry: ctx.workflows,
213
+ }).workflow;
224
214
  const envelope = composeStageEnvelope({
225
215
  cwd: ctx.cwd,
226
216
  taskId: input.task_id,
@@ -0,0 +1,113 @@
1
+ import { DEFAULT_WORKFLOWS } from "./default-workflows.js";
2
+ import { loadSyncedWorkflows } from "./synced-registry.js";
3
+ import type { WorkflowDefinition } from "./schemas.js";
4
+
5
+ export type WorkflowSource = "bundled" | "synced" | "injected";
6
+
7
+ export type WorkflowResolutionWarning = {
8
+ code: "bundled-workflow-fallback";
9
+ workflow_id: string;
10
+ message: string;
11
+ bundled_available: boolean;
12
+ synced_available: boolean;
13
+ };
14
+
15
+ export type WorkflowResolution = {
16
+ workflow: WorkflowDefinition;
17
+ source: WorkflowSource;
18
+ warnings: WorkflowResolutionWarning[];
19
+ bundled_ids: string[];
20
+ synced_ids: string[];
21
+ available_ids: string[];
22
+ };
23
+
24
+ type ResolveWorkflowOptions = {
25
+ registry?: Record<string, WorkflowDefinition>;
26
+ syncedDir?: string;
27
+ emitBundledFallbackWarning?: boolean;
28
+ bundledFallbackPolicy?: "allow" | "warn" | "block";
29
+ };
30
+
31
+ function sortIds(ids: Iterable<string>): string[] {
32
+ return [...ids].sort((left, right) => left.localeCompare(right));
33
+ }
34
+
35
+ function formatIds(ids: string[]): string {
36
+ return ids.length > 0 ? ids.join(", ") : "<none>";
37
+ }
38
+
39
+ export function resolveWorkflowDefinition(
40
+ workflowId: string,
41
+ options: ResolveWorkflowOptions = {},
42
+ ): WorkflowResolution {
43
+ if (options.registry) {
44
+ const availableIds = sortIds(Object.keys(options.registry));
45
+ const workflow = options.registry[workflowId];
46
+ if (!workflow) {
47
+ throw new Error(
48
+ `Unknown workflow_id: ${workflowId}. Available injected: ${formatIds(availableIds)}`,
49
+ );
50
+ }
51
+ return {
52
+ workflow,
53
+ source: "injected",
54
+ warnings: [],
55
+ bundled_ids: [],
56
+ synced_ids: [],
57
+ available_ids: availableIds,
58
+ };
59
+ }
60
+
61
+ const bundled = DEFAULT_WORKFLOWS;
62
+ const synced = loadSyncedWorkflows(options.syncedDir);
63
+ const bundledIds = sortIds(Object.keys(bundled));
64
+ const syncedIds = sortIds(Object.keys(synced));
65
+ const merged = { ...bundled, ...synced };
66
+ const availableIds = sortIds(Object.keys(merged));
67
+ const workflow = merged[workflowId];
68
+
69
+ if (!workflow) {
70
+ throw new Error(
71
+ `Unknown workflow_id: ${workflowId}. Available bundled: ${formatIds(bundledIds)}. ` +
72
+ `Available synced: ${formatIds(syncedIds)}. Available all: ${formatIds(availableIds)}`,
73
+ );
74
+ }
75
+
76
+ const source: WorkflowSource = Object.prototype.hasOwnProperty.call(synced, workflowId)
77
+ ? "synced"
78
+ : "bundled";
79
+ const warnings: WorkflowResolutionWarning[] = [];
80
+ const fallbackPolicy = options.bundledFallbackPolicy ?? "allow";
81
+
82
+ if (source === "bundled" && fallbackPolicy === "block") {
83
+ throw new Error(
84
+ `Workflow "${workflowId}" is only available from the bundled registry, ` +
85
+ "but enforced govern mode requires a synced org workflow. " +
86
+ `Available synced: ${formatIds(syncedIds)}.`,
87
+ );
88
+ }
89
+
90
+ if (
91
+ source === "bundled" &&
92
+ (options.emitBundledFallbackWarning || fallbackPolicy === "warn")
93
+ ) {
94
+ warnings.push({
95
+ code: "bundled-workflow-fallback",
96
+ workflow_id: workflowId,
97
+ bundled_available: true,
98
+ synced_available: syncedIds.length > 0,
99
+ message:
100
+ `Workflow "${workflowId}" was resolved from the bundled registry because ` +
101
+ "no synced org workflow with that id exists in the local cache.",
102
+ });
103
+ }
104
+
105
+ return {
106
+ workflow,
107
+ source,
108
+ warnings,
109
+ bundled_ids: bundledIds,
110
+ synced_ids: syncedIds,
111
+ available_ids: availableIds,
112
+ };
113
+ }
@@ -101,7 +101,7 @@ export function loadEmbeddingIndex(): EmbeddingIndex {
101
101
  embeddingsCache = {
102
102
  model: null,
103
103
  vectors: new Map(),
104
- warning: "Embedding index missing (run: ./scripts/context.sh embed)"
104
+ warning: "Embedding index missing (run: cortex embed)"
105
105
  };
106
106
  return embeddingsCache;
107
107
  }
@@ -17,7 +17,8 @@ import { registerEnterpriseTools } from "./tools/enterprise.js";
17
17
  import { registerHarnessTools } from "./tools/harness.js";
18
18
  import { pushViolations, setViolationPushContext } from "./violations/push.js";
19
19
  import { pushReviewResults, setReviewPushContext } from "./reviews/push.js";
20
- import { setWorkflowPushContext } from "./workflow/push.js";
20
+ import { pushWorkflowSnapshot, setWorkflowPushContext } from "./workflow/push.js";
21
+ import { hasWorkflowState, loadWorkflowState } from "./workflow/state.js";
21
22
 
22
23
  const require = createRequire(import.meta.url);
23
24
  const pkg = require("../package.json") as { version: string };
@@ -32,6 +33,7 @@ let activeAuditWriter: AuditWriter | null = null;
32
33
  let activeInstanceId: string | null = null;
33
34
  let activeSessionId: string | null = null;
34
35
  let activeRepo: string | null = null;
36
+ let activeContextDir: string | null = null;
35
37
 
36
38
  async function flushComplianceQueues(
37
39
  config: EnterpriseConfig,
@@ -64,6 +66,15 @@ async function flushComplianceQueues(
64
66
  if (!result.success) {
65
67
  process.stderr.write(`[cortex-enterprise] ${reason} reviews push failed: ${result.error}\n`);
66
68
  }
69
+ if (result.attempted && activeContextDir && hasWorkflowState(activeContextDir)) {
70
+ const workflowState = loadWorkflowState(activeContextDir);
71
+ const workflowResult = await pushWorkflowSnapshot(baseUrl, apiKey, workflowState);
72
+ if (!workflowResult.success) {
73
+ process.stderr.write(
74
+ `[cortex-enterprise] ${reason} workflow snapshot refresh after review push failed: ${workflowResult.error}\n`,
75
+ );
76
+ }
77
+ }
67
78
  } catch (err) {
68
79
  process.stderr.write(`[cortex-enterprise] ${reason} reviews push error: ${err}\n`);
69
80
  }
@@ -113,6 +124,7 @@ export function shutdown(): void {
113
124
  activeInstanceId = null;
114
125
  activeSessionId = null;
115
126
  activeRepo = null;
127
+ activeContextDir = null;
116
128
  setAuditPushContext({});
117
129
  setViolationPushContext({});
118
130
  setReviewPushContext({});
@@ -248,6 +260,7 @@ export async function onSessionEvent(event: SessionEvent): Promise<void> {
248
260
  export async function register(server: McpServer): Promise<void> {
249
261
  const projectRoot = process.env.CORTEX_PROJECT_ROOT?.trim() || process.cwd();
250
262
  const contextDir = path.join(projectRoot, ".context");
263
+ activeContextDir = contextDir;
251
264
 
252
265
  const config = loadEnterpriseConfig(contextDir);
253
266
  const activation = resolveEnterpriseActivation(config);
@@ -295,6 +308,8 @@ export async function register(server: McpServer): Promise<void> {
295
308
  repo: activeRepo ?? undefined,
296
309
  instance_id: activeInstanceId ?? undefined,
297
310
  session_id: activeSessionId ?? undefined,
311
+ context_dir: contextDir,
312
+ project_root: projectRoot,
298
313
  });
299
314
  setWorkflowPushContext({
300
315
  repo: activeRepo ?? undefined,
@@ -0,0 +1,65 @@
1
+ import type { OrgPolicy } from "../../core/policy/store.js";
2
+ import {
3
+ getGenericEvaluator,
4
+ getValidator,
5
+ type EnforcedPolicy,
6
+ } from "../../core/validators/engine.js";
7
+ import type { ReviewSkippedPolicy } from "./trust-state.js";
8
+
9
+ export const DEFERRED_CODE_REVIEW_REASON =
10
+ "Current context.review invocation is the review being recorded; validate this policy from workflow state on the next run.";
11
+
12
+ export type PartitionedReviewPolicies = {
13
+ enforced: EnforcedPolicy[];
14
+ skipped: ReviewSkippedPolicy[];
15
+ };
16
+
17
+ export function partitionReviewPolicies(
18
+ policies: OrgPolicy[],
19
+ ): PartitionedReviewPolicies {
20
+ const enforced: EnforcedPolicy[] = [];
21
+ const skipped: ReviewSkippedPolicy[] = [];
22
+
23
+ for (const policy of policies) {
24
+ if (!policy.enforce) continue;
25
+
26
+ if (policy.id === "require-code-review") {
27
+ skipped.push({
28
+ policy_id: policy.id,
29
+ kind: policy.kind ?? null,
30
+ type: policy.type ?? null,
31
+ reason: DEFERRED_CODE_REVIEW_REASON,
32
+ });
33
+ continue;
34
+ }
35
+
36
+ if (policy.type) {
37
+ if (!getGenericEvaluator(policy.type)) {
38
+ skipped.push({
39
+ policy_id: policy.id,
40
+ kind: policy.kind ?? null,
41
+ type: policy.type,
42
+ reason: `No evaluator registered for type "${policy.type}"`,
43
+ });
44
+ continue;
45
+ }
46
+ } else if (!getValidator(policy.id)) {
47
+ skipped.push({
48
+ policy_id: policy.id,
49
+ kind: policy.kind ?? null,
50
+ type: null,
51
+ reason: "No executable validator registered for this policy",
52
+ });
53
+ continue;
54
+ }
55
+
56
+ enforced.push({
57
+ id: policy.id,
58
+ type: policy.type ?? null,
59
+ config: policy.config ?? null,
60
+ severity: policy.severity ?? "block",
61
+ });
62
+ }
63
+
64
+ return { enforced, skipped };
65
+ }
@@ -1,3 +1,5 @@
1
+ import { recordReviewDeliveryStatus } from "./trust-state.js";
2
+
1
3
  export type ReviewPushItem = {
2
4
  policy_id: string;
3
5
  pass: boolean;
@@ -11,12 +13,16 @@ export type ReviewPushContext = {
11
13
  repo?: string;
12
14
  instance_id?: string;
13
15
  session_id?: string;
16
+ context_dir?: string;
17
+ project_root?: string;
14
18
  };
15
19
 
16
20
  export type ReviewPushResult = {
17
21
  success: boolean;
18
22
  count: number;
19
23
  error?: string;
24
+ attempted?: boolean;
25
+ delivery_status?: "accepted" | "failed";
20
26
  };
21
27
 
22
28
  const pending: ReviewPushItem[] = [];
@@ -26,6 +32,10 @@ export function setReviewPushContext(context: ReviewPushContext): void {
26
32
  activeContext = { ...context };
27
33
  }
28
34
 
35
+ export function getReviewPushContext(): ReviewPushContext {
36
+ return { ...activeContext };
37
+ }
38
+
29
39
  export function queueReviewResult(item: ReviewPushItem): void {
30
40
  pending.push(item);
31
41
  }
@@ -39,11 +49,18 @@ export async function pushReviewResults(
39
49
  apiKey: string,
40
50
  ): Promise<ReviewPushResult> {
41
51
  if (pending.length === 0) {
42
- return { success: true, count: 0 };
52
+ return { success: true, count: 0, attempted: false };
43
53
  }
44
54
 
45
55
  const reviewsUrl = `${baseUrl.replace(/\/$/, "")}/api/v1/reviews/push`;
46
56
  const batch = pending.splice(0, 100);
57
+ const attemptedAt = new Date().toISOString();
58
+ if (activeContext.context_dir) {
59
+ recordReviewDeliveryStatus(activeContext.context_dir, "pushed", {
60
+ reviewCount: batch.length,
61
+ attemptedAt,
62
+ });
63
+ }
47
64
 
48
65
  try {
49
66
  const response = await fetch(reviewsUrl, {
@@ -64,16 +81,50 @@ export async function pushReviewResults(
64
81
 
65
82
  if (!response.ok) {
66
83
  pending.unshift(...batch);
67
- return { success: false, count: 0, error: `HTTP ${response.status}` };
84
+ if (activeContext.context_dir) {
85
+ recordReviewDeliveryStatus(activeContext.context_dir, "failed", {
86
+ reviewCount: batch.length,
87
+ attemptedAt,
88
+ error: `HTTP ${response.status}`,
89
+ });
90
+ }
91
+ return {
92
+ success: false,
93
+ count: 0,
94
+ error: `HTTP ${response.status}`,
95
+ attempted: true,
96
+ delivery_status: "failed",
97
+ };
68
98
  }
69
99
 
70
- return { success: true, count: batch.length };
100
+ if (activeContext.context_dir) {
101
+ recordReviewDeliveryStatus(activeContext.context_dir, "accepted", {
102
+ reviewCount: batch.length,
103
+ attemptedAt,
104
+ });
105
+ }
106
+ return {
107
+ success: true,
108
+ count: batch.length,
109
+ attempted: true,
110
+ delivery_status: "accepted",
111
+ };
71
112
  } catch (err) {
72
113
  pending.unshift(...batch);
114
+ const error = err instanceof Error ? err.message : "unknown error";
115
+ if (activeContext.context_dir) {
116
+ recordReviewDeliveryStatus(activeContext.context_dir, "failed", {
117
+ reviewCount: batch.length,
118
+ attemptedAt,
119
+ error,
120
+ });
121
+ }
73
122
  return {
74
123
  success: false,
75
124
  count: 0,
76
- error: err instanceof Error ? err.message : "unknown error",
125
+ error,
126
+ attempted: true,
127
+ delivery_status: "failed",
77
128
  };
78
129
  }
79
130
  }