@danielblomma/cortex-mcp 2.2.5 → 2.4.1

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 (196) hide show
  1. package/CHANGELOG.md +201 -0
  2. package/README.md +73 -3
  3. package/bin/cortex.mjs +339 -242
  4. package/bin/daemon-control.mjs +162 -0
  5. package/package.json +7 -3
  6. package/scaffold/mcp/dist/.cortex-build-hash +1 -0
  7. package/scaffold/mcp/dist/cli/enterprise-setup.js +134 -0
  8. package/scaffold/mcp/dist/cli/govern.js +937 -0
  9. package/scaffold/mcp/dist/cli/query.js +409 -0
  10. package/scaffold/mcp/dist/cli/run.js +295 -0
  11. package/scaffold/mcp/dist/cli/stage.js +308 -0
  12. package/scaffold/mcp/dist/cli/telemetry-test.js +141 -0
  13. package/scaffold/mcp/dist/cli/ungoverned-detector.js +133 -0
  14. package/scaffold/mcp/dist/contextEntities.js +282 -0
  15. package/scaffold/mcp/dist/core/audit/query.js +72 -0
  16. package/scaffold/mcp/dist/core/audit/writer.js +28 -0
  17. package/scaffold/mcp/dist/core/config.js +235 -0
  18. package/scaffold/mcp/dist/core/enterprise-host-identity.js +193 -0
  19. package/scaffold/mcp/dist/core/enterprise-identity.js +18 -0
  20. package/scaffold/mcp/dist/core/enterprise-rotation.js +158 -0
  21. package/scaffold/mcp/dist/core/govern-paths.js +21 -0
  22. package/scaffold/mcp/dist/core/index.js +14 -0
  23. package/scaffold/mcp/dist/core/license.js +278 -0
  24. package/scaffold/mcp/dist/core/policy/enforce.js +66 -0
  25. package/scaffold/mcp/dist/core/policy/injection.js +172 -0
  26. package/scaffold/mcp/dist/core/policy/store.js +179 -0
  27. package/scaffold/mcp/dist/core/rbac/check.js +30 -0
  28. package/scaffold/mcp/dist/core/secure-endpoint.js +25 -0
  29. package/scaffold/mcp/dist/core/telemetry/collector.js +285 -0
  30. package/scaffold/mcp/dist/core/telemetry/state-dir.js +31 -0
  31. package/scaffold/mcp/dist/core/validators/builtins.js +632 -0
  32. package/scaffold/mcp/dist/core/validators/config.js +44 -0
  33. package/scaffold/mcp/dist/core/validators/engine.js +120 -0
  34. package/scaffold/mcp/dist/core/validators/evaluators/code_comments.js +236 -0
  35. package/scaffold/mcp/dist/core/validators/evaluators/regex.js +116 -0
  36. package/scaffold/mcp/dist/core/workflow/artifact-io.js +103 -0
  37. package/scaffold/mcp/dist/core/workflow/capabilities.js +88 -0
  38. package/scaffold/mcp/dist/core/workflow/default-workflows.js +102 -0
  39. package/scaffold/mcp/dist/core/workflow/enforcement.js +164 -0
  40. package/scaffold/mcp/dist/core/workflow/envelope.js +132 -0
  41. package/scaffold/mcp/dist/core/workflow/index.js +11 -0
  42. package/scaffold/mcp/dist/core/workflow/mcp-tools.js +175 -0
  43. package/scaffold/mcp/dist/core/workflow/resolution.js +69 -0
  44. package/scaffold/mcp/dist/core/workflow/run-lifecycle.js +123 -0
  45. package/scaffold/mcp/dist/core/workflow/schemas.js +141 -0
  46. package/scaffold/mcp/dist/core/workflow/synced-capability-registry.js +60 -0
  47. package/scaffold/mcp/dist/core/workflow/synced-registry.js +60 -0
  48. package/scaffold/mcp/dist/daemon/capability-sync-checker.js +242 -0
  49. package/scaffold/mcp/dist/daemon/client.js +130 -0
  50. package/scaffold/mcp/dist/daemon/egress-proxy.js +288 -0
  51. package/scaffold/mcp/dist/daemon/global-host-events.js +222 -0
  52. package/scaffold/mcp/dist/daemon/heartbeat-pusher.js +116 -0
  53. package/scaffold/mcp/dist/daemon/heartbeat-tracker.js +165 -0
  54. package/scaffold/mcp/dist/daemon/host-events-pusher.js +249 -0
  55. package/scaffold/mcp/dist/daemon/main.js +449 -0
  56. package/scaffold/mcp/dist/daemon/paths.js +36 -0
  57. package/scaffold/mcp/dist/daemon/project-service-registry.js +78 -0
  58. package/scaffold/mcp/dist/daemon/protocol.js +8 -0
  59. package/scaffold/mcp/dist/daemon/server.js +180 -0
  60. package/scaffold/mcp/dist/daemon/skill-sync-checker.js +557 -0
  61. package/scaffold/mcp/dist/daemon/sync-checker.js +165 -0
  62. package/scaffold/mcp/dist/daemon/ungoverned-scanner.js +136 -0
  63. package/scaffold/mcp/dist/daemon/workflow-sync-checker.js +249 -0
  64. package/scaffold/mcp/dist/defaults.js +6 -0
  65. package/scaffold/mcp/dist/embed.js +627 -0
  66. package/scaffold/mcp/dist/embedScheduler.js +479 -0
  67. package/scaffold/mcp/dist/embeddings.js +167 -0
  68. package/scaffold/mcp/dist/enterprise/audit/push.js +66 -0
  69. package/scaffold/mcp/dist/enterprise/index.js +327 -0
  70. package/scaffold/mcp/dist/enterprise/model/deploy.js +27 -0
  71. package/scaffold/mcp/dist/enterprise/policy/sync.js +129 -0
  72. package/scaffold/mcp/dist/enterprise/privacy/boundary.js +184 -0
  73. package/scaffold/mcp/dist/enterprise/reviews/changed-files.js +33 -0
  74. package/scaffold/mcp/dist/enterprise/reviews/pattern-context.js +202 -0
  75. package/scaffold/mcp/dist/enterprise/reviews/policy-selection.js +46 -0
  76. package/scaffold/mcp/dist/enterprise/reviews/push.js +102 -0
  77. package/scaffold/mcp/dist/enterprise/reviews/trust-state.js +186 -0
  78. package/scaffold/mcp/dist/enterprise/telemetry/sync.js +57 -0
  79. package/scaffold/mcp/dist/enterprise/tools/enterprise.js +826 -0
  80. package/scaffold/mcp/dist/enterprise/tools/harness.js +40 -0
  81. package/scaffold/mcp/dist/enterprise/tools/walk.js +73 -0
  82. package/scaffold/mcp/dist/enterprise/violations/push.js +77 -0
  83. package/scaffold/mcp/dist/enterprise/workflow/push.js +44 -0
  84. package/scaffold/mcp/dist/enterprise/workflow/state.js +329 -0
  85. package/scaffold/mcp/dist/frontmatter.js +33 -0
  86. package/scaffold/mcp/dist/graph.js +769 -0
  87. package/scaffold/mcp/dist/graphCsv.js +55 -0
  88. package/scaffold/mcp/dist/graphMetrics.js +8 -0
  89. package/scaffold/mcp/dist/hooks/permission-request.js +89 -0
  90. package/scaffold/mcp/dist/hooks/post-tool-use.js +105 -0
  91. package/scaffold/mcp/dist/hooks/pre-compact.js +29 -0
  92. package/scaffold/mcp/dist/hooks/pre-tool-use.js +78 -0
  93. package/scaffold/mcp/dist/hooks/session-end.js +43 -0
  94. package/scaffold/mcp/dist/hooks/session-start.js +41 -0
  95. package/scaffold/mcp/dist/hooks/shared.js +194 -0
  96. package/scaffold/mcp/dist/hooks/stop.js +28 -0
  97. package/scaffold/mcp/dist/hooks/user-prompt-submit.js +33 -0
  98. package/scaffold/mcp/dist/impactPresentation.js +137 -0
  99. package/scaffold/mcp/dist/impactRanking.js +191 -0
  100. package/scaffold/mcp/dist/impactResponse.js +30 -0
  101. package/scaffold/mcp/dist/impactResults.js +105 -0
  102. package/scaffold/mcp/dist/impactSeed.js +20 -0
  103. package/scaffold/mcp/dist/impactTraversal.js +64 -0
  104. package/scaffold/mcp/dist/jsonl.js +77 -0
  105. package/scaffold/mcp/dist/loadGraph.js +759 -0
  106. package/scaffold/mcp/dist/lruCache.js +38 -0
  107. package/scaffold/mcp/dist/paths.js +97 -0
  108. package/scaffold/mcp/dist/patternEvidence.js +272 -0
  109. package/scaffold/mcp/dist/plugin.js +81 -0
  110. package/scaffold/mcp/dist/presets.js +78 -0
  111. package/scaffold/mcp/dist/relatedResponse.js +18 -0
  112. package/scaffold/mcp/dist/relatedTraversal.js +78 -0
  113. package/scaffold/mcp/dist/rules.js +23 -0
  114. package/scaffold/mcp/dist/search.js +212 -0
  115. package/scaffold/mcp/dist/searchCore.js +457 -0
  116. package/scaffold/mcp/dist/searchResults.js +230 -0
  117. package/scaffold/mcp/dist/server.js +317 -0
  118. package/scaffold/mcp/dist/types.js +1 -0
  119. package/scaffold/mcp/package-lock.json +336 -212
  120. package/scaffold/mcp/package.json +15 -6
  121. package/scaffold/mcp/src/cli/enterprise-setup.ts +82 -8
  122. package/scaffold/mcp/src/cli/govern.ts +137 -52
  123. package/scaffold/mcp/src/cli/query.ts +36 -2
  124. package/scaffold/mcp/src/cli/run.ts +53 -19
  125. package/scaffold/mcp/src/cli/stage.ts +8 -3
  126. package/scaffold/mcp/src/core/config.ts +9 -2
  127. package/scaffold/mcp/src/core/enterprise-host-identity.ts +259 -0
  128. package/scaffold/mcp/src/core/enterprise-identity.ts +20 -0
  129. package/scaffold/mcp/src/core/enterprise-rotation.ts +185 -0
  130. package/scaffold/mcp/src/core/govern-paths.ts +30 -0
  131. package/scaffold/mcp/src/core/license.ts +186 -17
  132. package/scaffold/mcp/src/core/secure-endpoint.ts +23 -0
  133. package/scaffold/mcp/src/core/workflow/enforcement.ts +8 -1
  134. package/scaffold/mcp/src/core/workflow/mcp-tools.ts +3 -0
  135. package/scaffold/mcp/src/core/workflow/resolution.ts +9 -1
  136. package/scaffold/mcp/src/core/workflow/synced-capability-registry.ts +15 -0
  137. package/scaffold/mcp/src/core/workflow/synced-registry.ts +15 -0
  138. package/scaffold/mcp/src/daemon/capability-sync-checker.ts +38 -2
  139. package/scaffold/mcp/src/daemon/egress-proxy.ts +14 -8
  140. package/scaffold/mcp/src/daemon/global-host-events.ts +288 -0
  141. package/scaffold/mcp/src/daemon/heartbeat-pusher.ts +4 -0
  142. package/scaffold/mcp/src/daemon/heartbeat-tracker.ts +2 -0
  143. package/scaffold/mcp/src/daemon/host-events-pusher.ts +5 -0
  144. package/scaffold/mcp/src/daemon/main.ts +99 -25
  145. package/scaffold/mcp/src/daemon/project-service-registry.ts +107 -0
  146. package/scaffold/mcp/src/daemon/skill-sync-checker.ts +368 -31
  147. package/scaffold/mcp/src/daemon/sync-checker.ts +4 -0
  148. package/scaffold/mcp/src/daemon/ungoverned-scanner.ts +57 -25
  149. package/scaffold/mcp/src/daemon/workflow-sync-checker.ts +41 -2
  150. package/scaffold/mcp/src/embed.ts +211 -10
  151. package/scaffold/mcp/src/enterprise/audit/push.ts +8 -0
  152. package/scaffold/mcp/src/enterprise/index.ts +1 -1
  153. package/scaffold/mcp/src/enterprise/policy/sync.ts +12 -0
  154. package/scaffold/mcp/src/enterprise/reviews/changed-files.ts +34 -0
  155. package/scaffold/mcp/src/enterprise/reviews/pattern-context.ts +231 -0
  156. package/scaffold/mcp/src/enterprise/reviews/push.ts +9 -0
  157. package/scaffold/mcp/src/enterprise/reviews/trust-state.ts +3 -1
  158. package/scaffold/mcp/src/enterprise/telemetry/sync.ts +9 -0
  159. package/scaffold/mcp/src/enterprise/tools/enterprise.ts +49 -36
  160. package/scaffold/mcp/src/enterprise/violations/push.ts +9 -0
  161. package/scaffold/mcp/src/enterprise/workflow/push.ts +7 -0
  162. package/scaffold/mcp/src/paths.ts +3 -5
  163. package/scaffold/mcp/src/patternEvidence.ts +347 -0
  164. package/scaffold/mcp/src/plugin.ts +20 -0
  165. package/scaffold/mcp/src/search.ts +35 -9
  166. package/scaffold/mcp/src/searchCore.ts +254 -12
  167. package/scaffold/mcp/src/searchResults.ts +96 -9
  168. package/scaffold/mcp/src/types.ts +7 -0
  169. package/scaffold/mcp/tests/changed-files.test.mjs +41 -0
  170. package/scaffold/mcp/tests/copilot-shim.test.mjs +19 -1
  171. package/scaffold/mcp/tests/egress-proxy.test.mjs +37 -0
  172. package/scaffold/mcp/tests/embed-entities.test.mjs +109 -1
  173. package/scaffold/mcp/tests/enterprise-identity-sync.test.mjs +401 -0
  174. package/scaffold/mcp/tests/enterprise-pattern-context.test.mjs +322 -0
  175. package/scaffold/mcp/tests/enterprise-setup.test.mjs +189 -0
  176. package/scaffold/mcp/tests/global-host-events.test.mjs +81 -0
  177. package/scaffold/mcp/tests/govern-install.test.mjs +95 -2
  178. package/scaffold/mcp/tests/govern-repair.test.mjs +20 -3
  179. package/scaffold/mcp/tests/heartbeat-tracker.test.mjs +26 -0
  180. package/scaffold/mcp/tests/license.test.mjs +367 -0
  181. package/scaffold/mcp/tests/paths.test.mjs +11 -3
  182. package/scaffold/mcp/tests/pattern-evidence.test.mjs +321 -0
  183. package/scaffold/mcp/tests/project-service-registry.test.mjs +172 -0
  184. package/scaffold/mcp/tests/query-cli.test.mjs +73 -1
  185. package/scaffold/mcp/tests/review-trust-contract.test.mjs +18 -0
  186. package/scaffold/mcp/tests/search-graph-score.test.mjs +167 -0
  187. package/scaffold/mcp/tests/secure-enterprise-endpoint.test.mjs +46 -0
  188. package/scaffold/mcp/tests/skill-sync-checker.test.mjs +446 -2
  189. package/scaffold/mcp/tests/ungoverned-scanner.test.mjs +104 -22
  190. package/scaffold/mcp/tests/workflow-cli.test.mjs +14 -1
  191. package/scaffold/mcp/tests/workflow-synced-capabilities.test.mjs +37 -0
  192. package/scaffold/mcp/tests/workflow-synced-registry.test.mjs +40 -1
  193. package/scaffold/scripts/dashboard.mjs +29 -18
  194. package/scaffold/scripts/doctor.sh +26 -8
  195. package/scaffold/scripts/ingest.mjs +37 -4
  196. package/scaffold/scripts/status.sh +13 -6
@@ -0,0 +1,308 @@
1
+ import { spawn } from "node:child_process";
2
+ import { readFileSync } from "node:fs";
3
+ import { composeStageEnvelope, createRun, advanceStage, getRunState, } from "../core/workflow/index.js";
4
+ import { resolveWorkflowDefinition } from "../core/workflow/resolution.js";
5
+ import { isEnforcedMode, isEnterpriseProject } from "../hooks/shared.js";
6
+ /**
7
+ * `cortex stage` CLI surface. Each subcommand is a thin shell wrapper
8
+ * around the workflow primitives, mirroring the cortex.workflow.* MCP
9
+ * tools so shell scripts and CI can drive the harness without an MCP
10
+ * client.
11
+ *
12
+ * Subcommands:
13
+ * start --task-id <id> --description "..." [--workflow <id>]
14
+ * status --task-id <id>
15
+ * envelope --task-id <id> [--stage <name>]
16
+ * advance --task-id <id> --stage <name> --body-file <path>
17
+ * [--frontmatter-file <path>] [--status <complete|blocked|failed>]
18
+ * run --task-id <id> -- <command> [args...]
19
+ * Sets CORTEX_ACTIVE_TASK_ID and execs the command. Use this to
20
+ * spawn an agent that runs under the harness's pre-tool-use gate.
21
+ *
22
+ * All commands resolve cwd from CORTEX_PROJECT_ROOT (preferred) or
23
+ * process.cwd() so they work both inside an MCP server context and as
24
+ * standalone shell calls.
25
+ */
26
+ const ENTERPRISE_REQUIRED_MESSAGE = "cortex stage is part of the Cortex Harness — an enterprise-only feature. " +
27
+ "This project does not have an enterprise license configured (no enterprise.api_key " +
28
+ "in .context/enterprise.yml). Run 'cortex enterprise install --api-key-stdin' to enable it, or " +
29
+ "contact your org admin.";
30
+ export async function runStageCommand(args) {
31
+ const sub = args[0] ?? "help";
32
+ const rest = args.slice(1);
33
+ // help is allowed in community mode so users can discover the feature exists.
34
+ if (sub !== "help" && sub !== "--help" && sub !== "-h") {
35
+ if (!isEnterpriseProject(projectRoot())) {
36
+ throw new Error(ENTERPRISE_REQUIRED_MESSAGE);
37
+ }
38
+ }
39
+ switch (sub) {
40
+ case "start":
41
+ return runStart(rest);
42
+ case "status":
43
+ return runStatus(rest);
44
+ case "envelope":
45
+ return runEnvelope(rest);
46
+ case "advance":
47
+ return runAdvance(rest);
48
+ case "run":
49
+ return runRun(rest);
50
+ case "help":
51
+ case "--help":
52
+ case "-h":
53
+ printHelp();
54
+ return;
55
+ default:
56
+ printHelp();
57
+ throw new Error(`Unknown stage subcommand: ${sub}`);
58
+ }
59
+ }
60
+ function printHelp() {
61
+ const lines = [
62
+ "Usage:",
63
+ " cortex stage start --task-id <id> --description \"...\" [--workflow <id>]",
64
+ " cortex stage status --task-id <id>",
65
+ " cortex stage envelope --task-id <id> [--stage <name>]",
66
+ " cortex stage advance --task-id <id> --stage <name> --body-file <path>",
67
+ " [--frontmatter-file <path>] [--status <s>] [--outcome-file <path>]",
68
+ " [--validators-passed <id1,id2,...>]",
69
+ " [--override-reason \"...\"] [--override-skipped-validators <id1,id2>]",
70
+ " cortex stage run --task-id <id> -- <command> [args...]",
71
+ "",
72
+ "Status values: complete (default) | blocked | failed",
73
+ "All commands operate on .agents/<task-id>/ in the current project root.",
74
+ ];
75
+ process.stdout.write(lines.join("\n") + "\n");
76
+ }
77
+ function projectRoot() {
78
+ return process.env.CORTEX_PROJECT_ROOT?.trim() || process.cwd();
79
+ }
80
+ function parseFlags(args) {
81
+ const flags = {};
82
+ const rest = [];
83
+ let i = 0;
84
+ while (i < args.length) {
85
+ const arg = args[i];
86
+ if (arg === "--") {
87
+ rest.push(...args.slice(i + 1));
88
+ break;
89
+ }
90
+ if (arg.startsWith("--")) {
91
+ const next = args[i + 1];
92
+ if (next === undefined || next.startsWith("--")) {
93
+ flags[arg.slice(2)] = true;
94
+ i += 1;
95
+ }
96
+ else {
97
+ flags[arg.slice(2)] = next;
98
+ i += 2;
99
+ }
100
+ continue;
101
+ }
102
+ rest.push(arg);
103
+ i += 1;
104
+ }
105
+ return { flags, rest };
106
+ }
107
+ function requireFlag(flags, name) {
108
+ const value = flags[name];
109
+ if (typeof value !== "string" || value.length === 0) {
110
+ throw new Error(`Missing required flag: --${name}`);
111
+ }
112
+ return value;
113
+ }
114
+ function emitJson(data) {
115
+ process.stdout.write(JSON.stringify(data, null, 2) + "\n");
116
+ }
117
+ async function runStart(args) {
118
+ const { flags } = parseFlags(args);
119
+ const taskId = requireFlag(flags, "task-id");
120
+ const description = requireFlag(flags, "description");
121
+ const workflowId = typeof flags.workflow === "string" ? flags.workflow : "secure-build";
122
+ const resolved = resolveWorkflowDefinition(workflowId, {
123
+ cwd: projectRoot(),
124
+ emitBundledFallbackWarning: true,
125
+ bundledFallbackPolicy: isEnforcedMode(projectRoot()) ? "block" : "warn",
126
+ });
127
+ const workflow = resolved.workflow;
128
+ const state = createRun({
129
+ cwd: projectRoot(),
130
+ taskId,
131
+ workflow,
132
+ taskDescription: description,
133
+ });
134
+ const envelope = composeStageEnvelope({
135
+ cwd: projectRoot(),
136
+ taskId,
137
+ workflow,
138
+ });
139
+ emitJson({
140
+ state,
141
+ envelope,
142
+ workflow_source: resolved.source,
143
+ warnings: resolved.warnings,
144
+ });
145
+ }
146
+ async function runStatus(args) {
147
+ const { flags } = parseFlags(args);
148
+ const taskId = requireFlag(flags, "task-id");
149
+ const state = getRunState(projectRoot(), taskId);
150
+ emitJson({ state });
151
+ }
152
+ async function runEnvelope(args) {
153
+ const { flags } = parseFlags(args);
154
+ const taskId = requireFlag(flags, "task-id");
155
+ const stageName = typeof flags.stage === "string" ? flags.stage : undefined;
156
+ const state = getRunState(projectRoot(), taskId);
157
+ if (!state) {
158
+ throw new Error(`No run state for task ${taskId}. Start one with 'cortex stage start'.`);
159
+ }
160
+ const workflow = resolveWorkflowDefinition(state.workflow_id, {
161
+ cwd: projectRoot(),
162
+ }).workflow;
163
+ const envelope = composeStageEnvelope({
164
+ cwd: projectRoot(),
165
+ taskId,
166
+ workflow,
167
+ stageName,
168
+ });
169
+ emitJson({ envelope });
170
+ }
171
+ async function runAdvance(args) {
172
+ const { flags } = parseFlags(args);
173
+ const taskId = requireFlag(flags, "task-id");
174
+ const stageName = requireFlag(flags, "stage");
175
+ const bodyPath = requireFlag(flags, "body-file");
176
+ const frontmatterPath = typeof flags["frontmatter-file"] === "string"
177
+ ? flags["frontmatter-file"]
178
+ : null;
179
+ const outcomePath = typeof flags["outcome-file"] === "string" ? flags["outcome-file"] : null;
180
+ const statusFlag = typeof flags.status === "string" ? flags.status : undefined;
181
+ const validatorsRaw = typeof flags["validators-passed"] === "string"
182
+ ? flags["validators-passed"]
183
+ : null;
184
+ const overrideReason = typeof flags["override-reason"] === "string"
185
+ ? flags["override-reason"]
186
+ : null;
187
+ const overrideSkippedRaw = typeof flags["override-skipped-validators"] === "string"
188
+ ? flags["override-skipped-validators"]
189
+ : null;
190
+ const body = readFileSync(bodyPath, "utf8");
191
+ const frontmatter = frontmatterPath
192
+ ? parseJsonObject(frontmatterPath)
193
+ : {};
194
+ const outcome = outcomePath ? parseJsonObject(outcomePath) : undefined;
195
+ const validatorsPassed = validatorsRaw
196
+ ? validatorsRaw.split(",").map((s) => s.trim()).filter(Boolean)
197
+ : undefined;
198
+ const override = overrideReason
199
+ ? {
200
+ reason: overrideReason,
201
+ skipped_validators: overrideSkippedRaw
202
+ ? overrideSkippedRaw.split(",").map((s) => s.trim()).filter(Boolean)
203
+ : [],
204
+ skipped_requirements: [],
205
+ }
206
+ : undefined;
207
+ const state = getRunState(projectRoot(), taskId);
208
+ if (!state) {
209
+ throw new Error(`No run state for task ${taskId}. Start one with 'cortex stage start'.`);
210
+ }
211
+ const workflow = resolveWorkflowDefinition(state.workflow_id, {
212
+ cwd: projectRoot(),
213
+ }).workflow;
214
+ const stage = workflow.stages.find((s) => s.name === stageName);
215
+ if (!stage) {
216
+ throw new Error(`Stage ${stageName} is not defined in workflow ${workflow.id}`);
217
+ }
218
+ const finalStatus = statusFlag ?? "complete";
219
+ const next = advanceStage({
220
+ cwd: projectRoot(),
221
+ taskId,
222
+ workflow,
223
+ stageName,
224
+ artifactName: stage.artifact,
225
+ frontmatter: {
226
+ ...frontmatter,
227
+ stage: stageName,
228
+ status: finalStatus,
229
+ references: (Array.isArray(frontmatter.references)
230
+ ? frontmatter.references
231
+ .filter((v) => typeof v === "string")
232
+ : null) ?? deriveReferencesFromReads(stage.reads, workflow),
233
+ },
234
+ body,
235
+ status: finalStatus,
236
+ outcome,
237
+ validatorsPassed,
238
+ override,
239
+ });
240
+ let nextEnvelope = null;
241
+ if (next.outcome === "in_progress" && next.current_stage) {
242
+ nextEnvelope = composeStageEnvelope({
243
+ cwd: projectRoot(),
244
+ taskId,
245
+ workflow,
246
+ });
247
+ }
248
+ emitJson({ state: next, next_envelope: nextEnvelope });
249
+ }
250
+ function parseJsonObject(filePath) {
251
+ const raw = readFileSync(filePath, "utf8");
252
+ let value;
253
+ try {
254
+ value = JSON.parse(raw);
255
+ }
256
+ catch (err) {
257
+ throw new Error(`Failed to parse JSON in ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
258
+ }
259
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
260
+ throw new Error(`Expected JSON object in ${filePath}, got ${typeof value}`);
261
+ }
262
+ return value;
263
+ }
264
+ function deriveReferencesFromReads(reads, workflow) {
265
+ const refs = [];
266
+ for (const readName of reads) {
267
+ const stage = workflow.stages.find((s) => s.name === readName);
268
+ if (stage)
269
+ refs.push(stage.artifact);
270
+ }
271
+ return refs;
272
+ }
273
+ async function runRun(args) {
274
+ const { flags, rest } = parseFlags(args);
275
+ const taskId = requireFlag(flags, "task-id");
276
+ if (rest.length === 0) {
277
+ throw new Error("cortex stage run requires a command after --, e.g. 'cortex stage run --task-id task-1 -- claude'");
278
+ }
279
+ const state = getRunState(projectRoot(), taskId);
280
+ if (!state) {
281
+ throw new Error(`No run state for task ${taskId}. Start one with 'cortex stage start'.`);
282
+ }
283
+ if (state.outcome !== "in_progress" || !state.current_stage) {
284
+ throw new Error(`Run ${taskId} is not in progress (outcome=${state.outcome}). Cannot spawn agent.`);
285
+ }
286
+ const [command, ...commandArgs] = rest;
287
+ const child = spawn(command, commandArgs, {
288
+ stdio: "inherit",
289
+ env: {
290
+ ...process.env,
291
+ CORTEX_ACTIVE_TASK_ID: taskId,
292
+ },
293
+ });
294
+ await new Promise((resolve, reject) => {
295
+ child.on("error", reject);
296
+ child.on("exit", (code, signal) => {
297
+ if (signal) {
298
+ reject(new Error(`spawned process terminated by signal ${signal}`));
299
+ return;
300
+ }
301
+ if (code !== 0) {
302
+ reject(new Error(`spawned process exited with code ${code}`));
303
+ return;
304
+ }
305
+ resolve();
306
+ });
307
+ });
308
+ }
@@ -0,0 +1,141 @@
1
+ import { basename, join } from "node:path";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { createHash } from "node:crypto";
4
+ import { hostname, platform, arch } from "node:os";
5
+ import { loadEnterpriseConfig, resolveEnterpriseActivation, } from "../core/config.js";
6
+ import { telemetryStatePath } from "../core/telemetry/state-dir.js";
7
+ import { pushMetrics } from "../enterprise/telemetry/sync.js";
8
+ /**
9
+ * Smoke-test the telemetry pipeline end-to-end.
10
+ *
11
+ * $ cortex telemetry test
12
+ *
13
+ * Reads .context/enterprise.yml, builds a synthetic-but-valid metrics
14
+ * payload, sends it to the configured endpoint, and reports the result
15
+ * with actionable diagnostics on failure.
16
+ *
17
+ * This exists because the silent-failure path (push fails, dashboard
18
+ * stays empty, no error surfaces anywhere) was the original v2.0.0
19
+ * motivator. The smoke-test gives users a single command to verify
20
+ * their entire pipeline.
21
+ */
22
+ function readMachineId(contextDir) {
23
+ const path = telemetryStatePath(contextDir, "machine_id");
24
+ if (existsSync(path)) {
25
+ try {
26
+ const id = readFileSync(path, "utf8").trim();
27
+ if (id)
28
+ return id;
29
+ }
30
+ catch {
31
+ // fall through to compute one
32
+ }
33
+ }
34
+ return createHash("sha256")
35
+ .update(`${hostname()}|${platform()}|${arch()}|test`)
36
+ .digest("hex")
37
+ .slice(0, 16);
38
+ }
39
+ function buildSyntheticMetrics(instanceId, version) {
40
+ const now = new Date();
41
+ const periodStart = new Date(now.getTime() - 60_000).toISOString();
42
+ const periodEnd = now.toISOString();
43
+ return {
44
+ period_start: periodStart,
45
+ period_end: periodEnd,
46
+ total_tool_calls: 1,
47
+ successful_tool_calls: 1,
48
+ failed_tool_calls: 0,
49
+ total_duration_ms: 100,
50
+ session_starts: 1,
51
+ session_ends: 1,
52
+ session_duration_ms_total: 100,
53
+ searches: 1,
54
+ related_lookups: 0,
55
+ caller_lookups: 0,
56
+ trace_lookups: 0,
57
+ impact_analyses: 0,
58
+ rule_lookups: 0,
59
+ reloads: 0,
60
+ total_results_returned: 1,
61
+ estimated_tokens_saved: 100,
62
+ estimated_tokens_total: 500,
63
+ client_version: version,
64
+ instance_id: instanceId,
65
+ tool_metrics: {
66
+ "telemetry.test": {
67
+ calls: 1,
68
+ failures: 0,
69
+ total_duration_ms: 100,
70
+ total_results_returned: 1,
71
+ estimated_tokens_saved: 100,
72
+ },
73
+ },
74
+ };
75
+ }
76
+ export async function runTelemetryTest() {
77
+ const projectRoot = process.env.CORTEX_PROJECT_ROOT?.trim() || process.cwd();
78
+ const contextDir = join(projectRoot, ".context");
79
+ if (!existsSync(contextDir)) {
80
+ console.error(`No .context directory at ${projectRoot}`);
81
+ console.error(`Run 'cortex init --bootstrap' first.`);
82
+ return 1;
83
+ }
84
+ const config = loadEnterpriseConfig(contextDir);
85
+ const activation = resolveEnterpriseActivation(config);
86
+ console.log(`Project: ${projectRoot}`);
87
+ console.log(`Enterprise active: ${activation.active} (${activation.reason})`);
88
+ if (!activation.active) {
89
+ console.error("");
90
+ console.error("Cannot run telemetry test in community mode.");
91
+ console.error("Configure .context/enterprise.yml with valid api_key + endpoint.");
92
+ return 1;
93
+ }
94
+ if (!config.telemetry.enabled) {
95
+ console.error("");
96
+ console.error("telemetry.enabled is false in enterprise.yml");
97
+ return 1;
98
+ }
99
+ console.log(`Endpoint: ${config.telemetry.endpoint}`);
100
+ console.log(`API key prefix: ${config.telemetry.api_key.slice(0, 8)}...`);
101
+ const instanceId = readMachineId(contextDir);
102
+ const version = process.env.CORTEX_VERSION ?? "telemetry-test";
103
+ const metrics = buildSyntheticMetrics(instanceId, version);
104
+ console.log("");
105
+ console.log("Sending synthetic metrics...");
106
+ const start = Date.now();
107
+ const result = await pushMetrics(metrics, config.telemetry.endpoint, config.telemetry.api_key, { repo: basename(projectRoot) });
108
+ const elapsed = Date.now() - start;
109
+ if (result.success) {
110
+ console.log("");
111
+ console.log(`✓ Push succeeded in ${elapsed}ms (HTTP ${result.status})`);
112
+ console.log("");
113
+ console.log("Next: open the dashboard. The synthetic event should appear within 60s.");
114
+ console.log("If it does not, the issue is on the server (ingest or read path),");
115
+ console.log("not the client.");
116
+ return 0;
117
+ }
118
+ console.error("");
119
+ console.error(`✗ Push failed after ${elapsed}ms: ${result.error ?? "unknown"}`);
120
+ console.error("");
121
+ if (result.status === 401) {
122
+ console.error(" → API key was rejected. Check that the key in enterprise.yml");
123
+ console.error(" matches a row in cortex-web's api_keys table and is not");
124
+ console.error(" revoked or expired.");
125
+ }
126
+ else if (result.status === 403) {
127
+ console.error(" → API key lacks 'telemetry' scope. Add the scope in cortex-web.");
128
+ }
129
+ else if (result.status === 400) {
130
+ console.error(" → Payload schema rejected. Likely a client/server version skew.");
131
+ console.error(" Check telemetryPushSchema vs TelemetryMetrics.");
132
+ }
133
+ else if (result.status && result.status >= 500) {
134
+ console.error(" → Server error. Check cortex-web logs and DB connectivity.");
135
+ }
136
+ else {
137
+ console.error(" → Network error. Verify endpoint is reachable from this machine.");
138
+ console.error(` curl -I ${config.telemetry.endpoint}`);
139
+ }
140
+ return 1;
141
+ }
@@ -0,0 +1,133 @@
1
+ import { execSync } from "node:child_process";
2
+ import { basename } from "node:path";
3
+ import { hostname, userInfo } from "node:os";
4
+ export const DEFAULT_AI_BINARIES = [
5
+ "claude",
6
+ "codex",
7
+ "copilot",
8
+ "gemini-cli",
9
+ "gemini",
10
+ "aider",
11
+ "cursor",
12
+ ];
13
+ const PS_LINE_RE = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s*(.*)$/;
14
+ export function listProcesses() {
15
+ try {
16
+ const out = execSync("ps -axo pid,ppid,user,comm,args", {
17
+ encoding: "utf8",
18
+ timeout: 5000,
19
+ maxBuffer: 8 * 1024 * 1024,
20
+ });
21
+ const lines = out.trim().split("\n").slice(1);
22
+ return lines
23
+ .map(parseProcessLine)
24
+ .filter((p) => p !== null);
25
+ }
26
+ catch {
27
+ return [];
28
+ }
29
+ }
30
+ export function parseProcessLine(line) {
31
+ const m = line.match(PS_LINE_RE);
32
+ if (!m)
33
+ return null;
34
+ const pid = parseInt(m[1], 10);
35
+ const ppid = parseInt(m[2], 10);
36
+ if (!Number.isFinite(pid) || !Number.isFinite(ppid))
37
+ return null;
38
+ return {
39
+ pid,
40
+ ppid,
41
+ user: m[3],
42
+ comm: m[4],
43
+ args: m[5] ?? "",
44
+ };
45
+ }
46
+ export function isCortexAncestor(args) {
47
+ if (!args)
48
+ return false;
49
+ // Accept invocations whose command line shows cortex orchestration:
50
+ // - `cortex run ...` → wrapper spawned the AI CLI (Tier 2 governed)
51
+ // - `cortex enterprise` → install-time orchestration
52
+ // - `cortex daemon` → daemon-supervised
53
+ // - any path ending in `cortex.mjs` or named `cortex` followed by space/EOL
54
+ if (/\bcortex\s+run\b/.test(args))
55
+ return true;
56
+ if (/\bcortex\s+enterprise\b/.test(args))
57
+ return true;
58
+ if (/\bcortex\s+daemon\b/.test(args))
59
+ return true;
60
+ if (/\bcortex\s+hook\b/.test(args))
61
+ return true;
62
+ if (/\bcortex(\s|$)/.test(args))
63
+ return true;
64
+ if (/cortex\.mjs/.test(args))
65
+ return true;
66
+ return false;
67
+ }
68
+ function findCli(comm, knownBinaries) {
69
+ const base = basename(comm);
70
+ return knownBinaries.includes(base) ? base : null;
71
+ }
72
+ export function detectUngoverned(options = {}) {
73
+ const known = options.knownBinaries ?? DEFAULT_AI_BINARIES;
74
+ const procs = options.processes ?? listProcesses();
75
+ const byPid = new Map();
76
+ for (const p of procs)
77
+ byPid.set(p.pid, p);
78
+ const hostId = options.hostId ?? hostname();
79
+ const findings = [];
80
+ for (const proc of procs) {
81
+ const cli = findCli(proc.comm, known);
82
+ if (!cli)
83
+ continue;
84
+ if (isCortexAncestor(proc.args))
85
+ continue;
86
+ const chain = [proc.pid];
87
+ let current = byPid.get(proc.ppid);
88
+ let governed = false;
89
+ let depth = 0;
90
+ while (current && current.pid > 1 && depth < 32) {
91
+ chain.push(current.pid);
92
+ if (isCortexAncestor(current.args)) {
93
+ governed = true;
94
+ break;
95
+ }
96
+ const next = byPid.get(current.ppid);
97
+ if (!next || next.pid === current.pid)
98
+ break;
99
+ current = next;
100
+ depth += 1;
101
+ }
102
+ if (!governed) {
103
+ findings.push({
104
+ pid: proc.pid,
105
+ ppid: proc.ppid,
106
+ user: proc.user,
107
+ cli,
108
+ binary: proc.comm,
109
+ args: proc.args,
110
+ parent_chain: chain,
111
+ detected_at: new Date().toISOString(),
112
+ host_id: hostId,
113
+ });
114
+ }
115
+ }
116
+ return findings;
117
+ }
118
+ export function enforceFinding(finding, options) {
119
+ if (options.mode !== "enforced")
120
+ return "logged";
121
+ const me = options.currentUser ?? userInfo().username;
122
+ if (finding.user !== me) {
123
+ return "skipped_cross_user";
124
+ }
125
+ const send = options.sendSignal ?? ((pid, sig) => process.kill(pid, sig));
126
+ try {
127
+ send(finding.pid, "SIGTERM");
128
+ }
129
+ catch {
130
+ // process exited between detection and signal — best-effort
131
+ }
132
+ return "sigterm";
133
+ }