@danielblomma/cortex-mcp 2.0.15 → 2.0.18

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 (33) hide show
  1. package/README.md +5 -3
  2. package/package.json +1 -1
  3. package/scaffold/mcp/package-lock.json +59 -60
  4. package/scaffold/mcp/package.json +2 -2
  5. package/scaffold/mcp/src/cli/stage.ts +15 -18
  6. package/scaffold/mcp/src/cli/telemetry-test.ts +2 -1
  7. package/scaffold/mcp/src/core/license.ts +3 -2
  8. package/scaffold/mcp/src/core/telemetry/collector.ts +5 -4
  9. package/scaffold/mcp/src/core/telemetry/state-dir.ts +40 -0
  10. package/scaffold/mcp/src/core/validators/builtins.ts +24 -4
  11. package/scaffold/mcp/src/core/workflow/index.ts +1 -0
  12. package/scaffold/mcp/src/core/workflow/mcp-tools.ts +15 -25
  13. package/scaffold/mcp/src/core/workflow/resolution.ts +113 -0
  14. package/scaffold/mcp/src/daemon/main.ts +4 -3
  15. package/scaffold/mcp/src/enterprise/index.ts +16 -1
  16. package/scaffold/mcp/src/enterprise/reviews/policy-selection.ts +65 -0
  17. package/scaffold/mcp/src/enterprise/reviews/push.ts +55 -4
  18. package/scaffold/mcp/src/enterprise/reviews/trust-state.ts +305 -0
  19. package/scaffold/mcp/src/enterprise/tools/enterprise.ts +40 -47
  20. package/scaffold/mcp/src/enterprise/tools/harness.ts +2 -0
  21. package/scaffold/mcp/src/enterprise/workflow/state.ts +21 -0
  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/telemetry-collector.test.mjs +31 -1
  30. package/scaffold/mcp/tests/workflow-cli.test.mjs +102 -1
  31. package/scaffold/mcp/tests/workflow-mcp-tools.test.mjs +22 -0
  32. package/scaffold/mcp/tests/workflow-synced-registry.test.mjs +56 -0
  33. package/scaffold/scripts/parsers/node_modules/.package-lock.json +0 -1
package/README.md CHANGED
@@ -346,11 +346,13 @@ This repository includes two GitHub Actions workflows:
346
346
  - Triggers on tag push `v*.*.*`
347
347
  - Verifies tag/version sync
348
348
  - Runs root tests + MCP build/tests
349
- - Publishes `@danielblomma/cortex-mcp` to npm
349
+ - Publishes `@danielblomma/cortex-mcp` to npm via npm trusted publishing (GitHub OIDC)
350
350
 
351
- Required GitHub secret:
351
+ Required npm configuration:
352
352
 
353
- - `NPM_TOKEN` (npm automation token with publish rights for `@danielblomma/cortex-mcp`)
353
+ - Configure a trusted publisher for `@danielblomma/cortex-mcp` on npmjs.com
354
+ - Use GitHub Actions publisher `DanielBlomma/cortex`
355
+ - Workflow filename must match `release-publish.yml`
354
356
 
355
357
  ## Limitations
356
358
 
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.15",
4
+ "version": "2.0.18",
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",
@@ -91,12 +91,6 @@
91
91
  "integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==",
92
92
  "license": "Apache-2.0"
93
93
  },
94
- "node_modules/@huggingface/transformers/node_modules/long": {
95
- "version": "5.3.2",
96
- "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
97
- "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
98
- "license": "Apache-2.0"
99
- },
100
94
  "node_modules/@huggingface/transformers/node_modules/onnxruntime-common": {
101
95
  "version": "1.24.3",
102
96
  "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.24.3.tgz",
@@ -140,30 +134,6 @@
140
134
  "integrity": "sha512-BOoomdHYmNRL5r4iQ4bMvsl2t0/hzVQ3OM3PHD0gxeXu1PmggqBv3puZicEUVOA3AtHHYmqZtjMj9FOfGrATTw==",
141
135
  "license": "MIT"
142
136
  },
143
- "node_modules/@huggingface/transformers/node_modules/protobufjs": {
144
- "version": "7.5.5",
145
- "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.5.tgz",
146
- "integrity": "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==",
147
- "hasInstallScript": true,
148
- "license": "BSD-3-Clause",
149
- "dependencies": {
150
- "@protobufjs/aspromise": "^1.1.2",
151
- "@protobufjs/base64": "^1.1.2",
152
- "@protobufjs/codegen": "^2.0.4",
153
- "@protobufjs/eventemitter": "^1.1.0",
154
- "@protobufjs/fetch": "^1.1.0",
155
- "@protobufjs/float": "^1.0.2",
156
- "@protobufjs/inquire": "^1.1.0",
157
- "@protobufjs/path": "^1.1.2",
158
- "@protobufjs/pool": "^1.1.0",
159
- "@protobufjs/utf8": "^1.1.0",
160
- "@types/node": ">=13.7.0",
161
- "long": "^5.0.0"
162
- },
163
- "engines": {
164
- "node": ">=12.0.0"
165
- }
166
- },
167
137
  "node_modules/@huggingface/transformers/node_modules/sharp": {
168
138
  "version": "0.34.5",
169
139
  "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
@@ -766,9 +736,9 @@
766
736
  "license": "BSD-3-Clause"
767
737
  },
768
738
  "node_modules/@protobufjs/codegen": {
769
- "version": "2.0.4",
770
- "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
771
- "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
739
+ "version": "2.0.5",
740
+ "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz",
741
+ "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==",
772
742
  "license": "BSD-3-Clause"
773
743
  },
774
744
  "node_modules/@protobufjs/eventemitter": {
@@ -778,13 +748,12 @@
778
748
  "license": "BSD-3-Clause"
779
749
  },
780
750
  "node_modules/@protobufjs/fetch": {
781
- "version": "1.1.0",
782
- "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
783
- "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
751
+ "version": "1.1.1",
752
+ "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz",
753
+ "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==",
784
754
  "license": "BSD-3-Clause",
785
755
  "dependencies": {
786
- "@protobufjs/aspromise": "^1.1.1",
787
- "@protobufjs/inquire": "^1.1.0"
756
+ "@protobufjs/aspromise": "^1.1.1"
788
757
  }
789
758
  },
790
759
  "node_modules/@protobufjs/float": {
@@ -794,9 +763,9 @@
794
763
  "license": "BSD-3-Clause"
795
764
  },
796
765
  "node_modules/@protobufjs/inquire": {
797
- "version": "1.1.0",
798
- "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
799
- "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
766
+ "version": "1.1.2",
767
+ "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz",
768
+ "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==",
800
769
  "license": "BSD-3-Clause"
801
770
  },
802
771
  "node_modules/@protobufjs/path": {
@@ -812,9 +781,9 @@
812
781
  "license": "BSD-3-Clause"
813
782
  },
814
783
  "node_modules/@protobufjs/utf8": {
815
- "version": "1.1.0",
816
- "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
817
- "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
784
+ "version": "1.1.1",
785
+ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz",
786
+ "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==",
818
787
  "license": "BSD-3-Clause"
819
788
  },
820
789
  "node_modules/@tsconfig/node10": {
@@ -1044,9 +1013,9 @@
1044
1013
  "license": "MIT"
1045
1014
  },
1046
1015
  "node_modules/brace-expansion": {
1047
- "version": "5.0.5",
1048
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
1049
- "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
1016
+ "version": "5.0.6",
1017
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
1018
+ "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
1050
1019
  "license": "MIT",
1051
1020
  "dependencies": {
1052
1021
  "balanced-match": "^4.0.2"
@@ -1525,12 +1494,12 @@
1525
1494
  }
1526
1495
  },
1527
1496
  "node_modules/express-rate-limit": {
1528
- "version": "8.3.1",
1529
- "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz",
1530
- "integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==",
1497
+ "version": "8.5.1",
1498
+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.1.tgz",
1499
+ "integrity": "sha512-5O6KYmyJEpuPJV5hNTXKbAHWRqrzyu+OI3vUnSd2kXFubIVpG7ezpgxQy76Zo5GQZtrQBg86hF+CM/NX+cioiQ==",
1531
1500
  "license": "MIT",
1532
1501
  "dependencies": {
1533
- "ip-address": "10.1.0"
1502
+ "ip-address": "^10.2.0"
1534
1503
  },
1535
1504
  "engines": {
1536
1505
  "node": ">= 16"
@@ -1574,9 +1543,9 @@
1574
1543
  "license": "MIT"
1575
1544
  },
1576
1545
  "node_modules/fast-uri": {
1577
- "version": "3.1.0",
1578
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
1579
- "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
1546
+ "version": "3.1.2",
1547
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
1548
+ "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
1580
1549
  "funding": [
1581
1550
  {
1582
1551
  "type": "github",
@@ -1791,9 +1760,9 @@
1791
1760
  }
1792
1761
  },
1793
1762
  "node_modules/hono": {
1794
- "version": "4.12.14",
1795
- "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz",
1796
- "integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==",
1763
+ "version": "4.12.18",
1764
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.18.tgz",
1765
+ "integrity": "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==",
1797
1766
  "license": "MIT",
1798
1767
  "engines": {
1799
1768
  "node": ">=16.9.0"
@@ -1848,9 +1817,9 @@
1848
1817
  "license": "ISC"
1849
1818
  },
1850
1819
  "node_modules/ip-address": {
1851
- "version": "10.1.0",
1852
- "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
1853
- "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
1820
+ "version": "10.2.0",
1821
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
1822
+ "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
1854
1823
  "license": "MIT",
1855
1824
  "engines": {
1856
1825
  "node": ">= 12"
@@ -1937,6 +1906,12 @@
1937
1906
  "graceful-fs": "^4.1.6"
1938
1907
  }
1939
1908
  },
1909
+ "node_modules/long": {
1910
+ "version": "5.3.2",
1911
+ "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
1912
+ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
1913
+ "license": "Apache-2.0"
1914
+ },
1940
1915
  "node_modules/make-error": {
1941
1916
  "version": "1.3.6",
1942
1917
  "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
@@ -2152,6 +2127,30 @@
2152
2127
  "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==",
2153
2128
  "license": "MIT"
2154
2129
  },
2130
+ "node_modules/protobufjs": {
2131
+ "version": "7.5.9",
2132
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.9.tgz",
2133
+ "integrity": "sha512-Od4muIm3HW1AouyHF5lONOf1FWo3hY1NbFDoy191X9GzhpgW1clCoaFjfVs2rKJNFYpTNJbje4cbAIDBZJ63ZA==",
2134
+ "hasInstallScript": true,
2135
+ "license": "BSD-3-Clause",
2136
+ "dependencies": {
2137
+ "@protobufjs/aspromise": "^1.1.2",
2138
+ "@protobufjs/base64": "^1.1.2",
2139
+ "@protobufjs/codegen": "^2.0.5",
2140
+ "@protobufjs/eventemitter": "^1.1.0",
2141
+ "@protobufjs/fetch": "^1.1.1",
2142
+ "@protobufjs/float": "^1.0.2",
2143
+ "@protobufjs/inquire": "^1.1.2",
2144
+ "@protobufjs/path": "^1.1.2",
2145
+ "@protobufjs/pool": "^1.1.0",
2146
+ "@protobufjs/utf8": "^1.1.1",
2147
+ "@types/node": ">=13.7.0",
2148
+ "long": "^5.0.0"
2149
+ },
2150
+ "engines": {
2151
+ "node": ">=12.0.0"
2152
+ }
2153
+ },
2155
2154
  "node_modules/proxy-addr": {
2156
2155
  "version": "2.0.7",
2157
2156
  "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -23,8 +23,8 @@
23
23
  },
24
24
  "overrides": {
25
25
  "cmake-js": "^8.0.0",
26
- "express-rate-limit": "^8.3.1",
27
- "hono": "^4.12.14",
26
+ "express-rate-limit": "^8.5.1",
27
+ "hono": "^4.12.18",
28
28
  "@hono/node-server": "^1.19.13",
29
29
  "tar": "^7.5.11"
30
30
  },
@@ -8,8 +8,8 @@ import {
8
8
  type StageStatus,
9
9
  type WorkflowDefinition,
10
10
  } from "../core/workflow/index.js";
11
- import { DEFAULT_WORKFLOWS } from "../core/workflow/default-workflows.js";
12
- import { isEnterpriseProject } from "../hooks/shared.js";
11
+ import { resolveWorkflowDefinition } from "../core/workflow/resolution.js";
12
+ import { isEnforcedMode, isEnterpriseProject } from "../hooks/shared.js";
13
13
 
14
14
  /**
15
15
  * `cortex stage` CLI surface. Each subcommand is a thin shell wrapper
@@ -93,18 +93,6 @@ function projectRoot(): string {
93
93
  return process.env.CORTEX_PROJECT_ROOT?.trim() || process.cwd();
94
94
  }
95
95
 
96
- function resolveWorkflow(workflowId: string): WorkflowDefinition {
97
- const wf = DEFAULT_WORKFLOWS[workflowId];
98
- if (!wf) {
99
- throw new Error(
100
- `Unknown workflow_id: ${workflowId}. Available: ${
101
- Object.keys(DEFAULT_WORKFLOWS).join(", ") || "<none>"
102
- }`,
103
- );
104
- }
105
- return wf;
106
- }
107
-
108
96
  type Flags = Record<string, string | boolean>;
109
97
 
110
98
  function parseFlags(args: string[]): { flags: Flags; rest: string[] } {
@@ -153,7 +141,11 @@ async function runStart(args: string[]): Promise<void> {
153
141
  const workflowId =
154
142
  typeof flags.workflow === "string" ? flags.workflow : "secure-build";
155
143
 
156
- const workflow = resolveWorkflow(workflowId);
144
+ const resolved = resolveWorkflowDefinition(workflowId, {
145
+ emitBundledFallbackWarning: true,
146
+ bundledFallbackPolicy: isEnforcedMode(projectRoot()) ? "block" : "warn",
147
+ });
148
+ const workflow = resolved.workflow;
157
149
  const state = createRun({
158
150
  cwd: projectRoot(),
159
151
  taskId,
@@ -165,7 +157,12 @@ async function runStart(args: string[]): Promise<void> {
165
157
  taskId,
166
158
  workflow,
167
159
  });
168
- emitJson({ state, envelope });
160
+ emitJson({
161
+ state,
162
+ envelope,
163
+ workflow_source: resolved.source,
164
+ warnings: resolved.warnings,
165
+ });
169
166
  }
170
167
 
171
168
  async function runStatus(args: string[]): Promise<void> {
@@ -186,7 +183,7 @@ async function runEnvelope(args: string[]): Promise<void> {
186
183
  `No run state for task ${taskId}. Start one with 'cortex stage start'.`,
187
184
  );
188
185
  }
189
- const workflow = resolveWorkflow(state.workflow_id);
186
+ const workflow = resolveWorkflowDefinition(state.workflow_id).workflow;
190
187
  const envelope = composeStageEnvelope({
191
188
  cwd: projectRoot(),
192
189
  taskId,
@@ -246,7 +243,7 @@ async function runAdvance(args: string[]): Promise<void> {
246
243
  `No run state for task ${taskId}. Start one with 'cortex stage start'.`,
247
244
  );
248
245
  }
249
- const workflow = resolveWorkflow(state.workflow_id);
246
+ const workflow = resolveWorkflowDefinition(state.workflow_id).workflow;
250
247
  const stage = workflow.stages.find((s) => s.name === stageName);
251
248
  if (!stage) {
252
249
  throw new Error(
@@ -6,6 +6,7 @@ import {
6
6
  loadEnterpriseConfig,
7
7
  resolveEnterpriseActivation,
8
8
  } from "../core/config.js";
9
+ import { telemetryStatePath } from "../core/telemetry/state-dir.js";
9
10
  import { pushMetrics } from "../enterprise/telemetry/sync.js";
10
11
  import type { TelemetryMetrics } from "../core/telemetry/collector.js";
11
12
 
@@ -25,7 +26,7 @@ import type { TelemetryMetrics } from "../core/telemetry/collector.js";
25
26
  */
26
27
 
27
28
  function readMachineId(contextDir: string): string {
28
- const path = join(contextDir, "telemetry", "machine_id");
29
+ const path = telemetryStatePath(contextDir, "machine_id");
29
30
  if (existsSync(path)) {
30
31
  try {
31
32
  const id = readFileSync(path, "utf8").trim();
@@ -1,5 +1,6 @@
1
1
  import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { resolveTelemetryStateDir, telemetryStatePath } from "./telemetry/state-dir.js";
3
4
 
4
5
  export type LicenseVerification =
5
6
  | {
@@ -30,7 +31,7 @@ const GRACE_PERIOD_MS = 7 * 24 * 60 * 60 * 1000; // 7d grace if endpoint unreach
30
31
  const REQUEST_TIMEOUT_MS = 5000;
31
32
 
32
33
  function cachePath(contextDir: string): string {
33
- return join(contextDir, "telemetry", CACHE_FILE);
34
+ return telemetryStatePath(contextDir, CACHE_FILE);
34
35
  }
35
36
 
36
37
  function readCache(contextDir: string): CacheEntry | null {
@@ -47,7 +48,7 @@ function readCache(contextDir: string): CacheEntry | null {
47
48
  function writeCache(contextDir: string, entry: CacheEntry): void {
48
49
  const path = cachePath(contextDir);
49
50
  try {
50
- mkdirSync(join(contextDir, "telemetry"), { recursive: true });
51
+ mkdirSync(resolveTelemetryStateDir(contextDir), { recursive: true });
51
52
  writeFileSync(path, JSON.stringify(entry, null, 2), "utf8");
52
53
  } catch {
53
54
  // Cache failures are non-fatal — license check just won't be cached.
@@ -2,6 +2,7 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
2
2
  import { createHash } from "node:crypto";
3
3
  import { hostname, platform, arch } from "node:os";
4
4
  import { join } from "node:path";
5
+ import { resolveTelemetryStateDir, telemetryStatePath } from "./state-dir.js";
5
6
 
6
7
  export type TelemetryMetrics = {
7
8
  period_start: string;
@@ -37,7 +38,8 @@ export type TelemetryMetrics = {
37
38
  const AVG_TOKENS_PER_RESULT = 400;
38
39
 
39
40
  function generateInstanceId(contextDir: string): string {
40
- const idPath = join(contextDir, "telemetry", "machine_id");
41
+ const telemetryDir = resolveTelemetryStateDir(contextDir);
42
+ const idPath = join(telemetryDir, "machine_id");
41
43
  if (existsSync(idPath)) {
42
44
  try {
43
45
  const existing = readFileSync(idPath, "utf8").trim();
@@ -51,7 +53,7 @@ function generateInstanceId(contextDir: string): string {
51
53
  const fingerprint = `${hostname()}|${platform()}|${arch()}`;
52
54
  const id = createHash("sha256").update(fingerprint).digest("hex").slice(0, 16);
53
55
  try {
54
- mkdirSync(join(contextDir, "telemetry"), { recursive: true });
56
+ mkdirSync(telemetryDir, { recursive: true });
55
57
  writeFileSync(idPath, id, "utf8");
56
58
  } catch (err) {
57
59
  process.stderr.write(`[cortex-enterprise] Could not persist instance id: ${err instanceof Error ? err.message : String(err)}\n`);
@@ -142,8 +144,7 @@ export class TelemetryCollector {
142
144
  constructor(contextDir: string, clientVersion = "unknown") {
143
145
  this.clientVersion = clientVersion;
144
146
  this.instanceId = generateInstanceId(contextDir);
145
- const telemetryDir = join(contextDir, "telemetry");
146
- this.metricsPath = join(telemetryDir, "metrics.json");
147
+ this.metricsPath = telemetryStatePath(contextDir, "metrics.json");
147
148
 
148
149
  // Load existing metrics or start fresh
149
150
  try {
@@ -0,0 +1,40 @@
1
+ import { accessSync, constants, mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ const warnedFallbacks = new Set<string>();
5
+
6
+ function canUseDirectory(dir: string): boolean {
7
+ try {
8
+ mkdirSync(dir, { recursive: true });
9
+ accessSync(dir, constants.R_OK | constants.W_OK | constants.X_OK);
10
+ return true;
11
+ } catch {
12
+ return false;
13
+ }
14
+ }
15
+
16
+ export function resolveTelemetryStateDir(contextDir: string): string {
17
+ const primary = join(contextDir, "telemetry");
18
+ if (canUseDirectory(primary)) return primary;
19
+
20
+ const fallback = join(contextDir, "cache", "telemetry");
21
+ if (canUseDirectory(fallback)) {
22
+ const warningKey = `${primary}->${fallback}`;
23
+ if (!warnedFallbacks.has(warningKey)) {
24
+ warnedFallbacks.add(warningKey);
25
+ process.stderr.write(
26
+ `[cortex-enterprise] telemetry dir not writable at ${primary}; using ${fallback}\n`,
27
+ );
28
+ }
29
+ return fallback;
30
+ }
31
+
32
+ return primary;
33
+ }
34
+
35
+ export function telemetryStatePath(
36
+ contextDir: string,
37
+ ...parts: string[]
38
+ ): string {
39
+ return join(resolveTelemetryStateDir(contextDir), ...parts);
40
+ }
@@ -100,6 +100,21 @@ type ReviewStatusPayload = {
100
100
  reviewedFiles: ReviewedFileSnapshot[] | null;
101
101
  };
102
102
 
103
+ function formatReviewEvidenceSource(
104
+ source: ReviewStatusPayload["source"] | "none",
105
+ ): string {
106
+ switch (source) {
107
+ case "workflow-state":
108
+ return "workflow state";
109
+ case "workflow-artifact":
110
+ return "workflow artifact";
111
+ case "legacy-review-status":
112
+ return "legacy file";
113
+ default:
114
+ return "none";
115
+ }
116
+ }
117
+
103
118
  type ReviewedFileSnapshot = {
104
119
  path: string;
105
120
  exists: boolean;
@@ -474,7 +489,9 @@ registerValidator({
474
489
  pass: false,
475
490
  severity: "warning",
476
491
  message: `${sourceLabel} at ${reviewStatus.timestamp} is stale for current changes`,
477
- detail: `Source: ${reviewStatus.source}. ${freshness.detail ?? "Current changes no longer match the reviewed code."}`,
492
+ detail:
493
+ `Source: ${formatReviewEvidenceSource(reviewStatus.source)}. ` +
494
+ `${freshness.detail ?? "Current changes no longer match the reviewed code."}`,
478
495
  };
479
496
  }
480
497
 
@@ -485,7 +502,7 @@ registerValidator({
485
502
  message: reviewStatus.reviewed
486
503
  ? `Enterprise review completed by ${reviewStatus.reviewer} at ${reviewStatus.timestamp}`
487
504
  : `Enterprise review recorded at ${reviewStatus.timestamp} but did not pass`,
488
- detail: `Source: ${reviewStatus.source}`,
505
+ detail: `Source: ${formatReviewEvidenceSource(reviewStatus.source)}`,
489
506
  };
490
507
  }
491
508
 
@@ -496,7 +513,9 @@ registerValidator({
496
513
  message: reviewStatus.reviewed
497
514
  ? `Code review completed by ${reviewStatus.reviewer} at ${reviewStatus.timestamp}`
498
515
  : "Code review recorded but not approved",
499
- detail: reviewStatus.reviewed ? undefined : `Source: ${reviewStatus.source}`,
516
+ detail: reviewStatus.reviewed
517
+ ? undefined
518
+ : `Source: ${formatReviewEvidenceSource(reviewStatus.source)}`,
500
519
  };
501
520
  }
502
521
 
@@ -504,7 +523,8 @@ registerValidator({
504
523
  pass: false,
505
524
  severity: "warning",
506
525
  message: "No code review recorded for current changes",
507
- detail: "Run /review or context.review, or ensure your CI writes .context/review-status.json.",
526
+ detail:
527
+ "Source: none. Run /review or context.review, or ensure your CI writes .context/review-status.json.",
508
528
  };
509
529
  },
510
530
  });
@@ -8,3 +8,4 @@ export * from "./capabilities.js";
8
8
  export * from "./enforcement.js";
9
9
  export * from "./synced-registry.js";
10
10
  export * from "./synced-capability-registry.js";
11
+ export * from "./resolution.js";
@@ -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,