@danielblomma/cortex-mcp 2.0.4 → 2.0.6

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.
@@ -0,0 +1,283 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+
7
+ import {
8
+ parseStageArtifact,
9
+ renderStageArtifact,
10
+ readRunState,
11
+ readStageArtifact,
12
+ } from "../dist/core/workflow/artifact-io.js";
13
+ import {
14
+ createRun,
15
+ advanceStage,
16
+ getRunState,
17
+ } from "../dist/core/workflow/run-lifecycle.js";
18
+ import {
19
+ workflowDefinitionSchema,
20
+ stageArtifactFrontmatterSchema,
21
+ runStateSchema,
22
+ } from "../dist/core/workflow/schemas.js";
23
+ import { SECURE_BUILD_WORKFLOW } from "../dist/core/workflow/default-workflows.js";
24
+
25
+ function makeWorkspace() {
26
+ return fs.mkdtempSync(path.join(os.tmpdir(), "cortex-workflow-"));
27
+ }
28
+
29
+ const TINY_WORKFLOW = {
30
+ id: "tiny",
31
+ description: "Two-stage workflow used for tests",
32
+ version: 1,
33
+ stages: [
34
+ {
35
+ name: "plan",
36
+ artifact: "plan.md",
37
+ reads: [],
38
+ required_fields: [],
39
+ description: "Produce a plan",
40
+ },
41
+ {
42
+ name: "review",
43
+ artifact: "review.md",
44
+ reads: ["plan"],
45
+ required_fields: ["approved"],
46
+ description: "Review the plan",
47
+ },
48
+ ],
49
+ };
50
+
51
+ test("schemas: SECURE_BUILD_WORKFLOW validates against workflowDefinitionSchema", () => {
52
+ const parsed = workflowDefinitionSchema.parse(SECURE_BUILD_WORKFLOW);
53
+ assert.equal(parsed.id, "secure-build");
54
+ assert.ok(parsed.stages.length > 0);
55
+ });
56
+
57
+ test("schemas: stage names must be slug-cased", () => {
58
+ assert.throws(() =>
59
+ workflowDefinitionSchema.parse({
60
+ ...TINY_WORKFLOW,
61
+ stages: [
62
+ { ...TINY_WORKFLOW.stages[0], name: "Bad Name" },
63
+ TINY_WORKFLOW.stages[1],
64
+ ],
65
+ }),
66
+ );
67
+ });
68
+
69
+ test("schemas: stage artifact frontmatter requires status + stage", () => {
70
+ assert.throws(() =>
71
+ stageArtifactFrontmatterSchema.parse({
72
+ stage: "plan",
73
+ // status missing
74
+ written_at: new Date().toISOString(),
75
+ }),
76
+ );
77
+ });
78
+
79
+ test("artifact-io: render + parse round-trips frontmatter", () => {
80
+ const fm = stageArtifactFrontmatterSchema.parse({
81
+ stage: "plan",
82
+ status: "complete",
83
+ references: [],
84
+ written_at: "2026-05-06T19:00:00.000Z",
85
+ });
86
+ const text = renderStageArtifact(fm, "# Plan\n\nDo the thing.");
87
+ const parsed = parseStageArtifact(text);
88
+ assert.equal(parsed.frontmatter.stage, "plan");
89
+ assert.equal(parsed.frontmatter.status, "complete");
90
+ assert.equal(parsed.body, "# Plan\n\nDo the thing.");
91
+ });
92
+
93
+ test("artifact-io: parseStageArtifact rejects missing frontmatter", () => {
94
+ assert.throws(() => parseStageArtifact("# No frontmatter here\n"));
95
+ });
96
+
97
+ test("artifact-io: parseStageArtifact rejects unterminated frontmatter", () => {
98
+ assert.throws(() =>
99
+ parseStageArtifact("---\nstage: plan\nstatus: complete\n# no close marker\n"),
100
+ );
101
+ });
102
+
103
+ test("artifact-io: parseStageArtifact preserves passthrough fields", () => {
104
+ const text = `---
105
+ stage: review
106
+ status: complete
107
+ references:
108
+ - plan.md
109
+ written_at: "2026-05-06T19:00:00.000Z"
110
+ approved: true
111
+ blocking_comments: 0
112
+ ---
113
+
114
+ # Review
115
+
116
+ Looks good.
117
+ `;
118
+ const parsed = parseStageArtifact(text);
119
+ assert.equal(parsed.frontmatter.stage, "review");
120
+ assert.deepEqual(parsed.frontmatter.references, ["plan.md"]);
121
+ assert.equal(parsed.frontmatter.approved, true);
122
+ assert.equal(parsed.frontmatter.blocking_comments, 0);
123
+ });
124
+
125
+ test("createRun: writes state.json with all stages pending and current_stage = first", () => {
126
+ const cwd = makeWorkspace();
127
+ const state = createRun({
128
+ cwd,
129
+ taskId: "2026-05-06-fixture",
130
+ workflow: TINY_WORKFLOW,
131
+ taskDescription: "Test run",
132
+ });
133
+
134
+ assert.equal(state.task_id, "2026-05-06-fixture");
135
+ assert.equal(state.current_stage, "plan");
136
+ assert.equal(state.outcome, "in_progress");
137
+ assert.deepEqual(
138
+ state.stages.map((s) => s.status),
139
+ ["pending", "pending"],
140
+ );
141
+
142
+ const persisted = readRunState(cwd, "2026-05-06-fixture");
143
+ assert.deepEqual(persisted, state);
144
+ });
145
+
146
+ test("advanceStage: writes artifact, updates state, advances current_stage", () => {
147
+ const cwd = makeWorkspace();
148
+ const taskId = "2026-05-06-advance";
149
+ createRun({ cwd, taskId, workflow: TINY_WORKFLOW, taskDescription: "Test" });
150
+
151
+ const after = advanceStage({
152
+ cwd,
153
+ taskId,
154
+ workflow: TINY_WORKFLOW,
155
+ stageName: "plan",
156
+ artifactName: "plan.md",
157
+ frontmatter: { stage: "plan", status: "complete", references: [] },
158
+ body: "# Plan\n\n- step 1\n- step 2",
159
+ });
160
+
161
+ assert.equal(after.current_stage, "review");
162
+ assert.equal(after.outcome, "in_progress");
163
+ assert.equal(after.stages[0].status, "complete");
164
+ assert.equal(after.stages[0].artifact, "plan.md");
165
+ assert.equal(after.stages[1].status, "pending");
166
+
167
+ // Artifact lives on disk under .agents/<taskId>/
168
+ const artifactPath = path.join(cwd, ".agents", taskId, "plan.md");
169
+ assert.ok(fs.existsSync(artifactPath));
170
+ const parsed = readStageArtifact(cwd, taskId, "plan.md");
171
+ assert.equal(parsed.frontmatter.stage, "plan");
172
+ });
173
+
174
+ test("advanceStage: marks run complete after final stage", () => {
175
+ const cwd = makeWorkspace();
176
+ const taskId = "2026-05-06-final";
177
+ createRun({ cwd, taskId, workflow: TINY_WORKFLOW, taskDescription: "Test" });
178
+
179
+ advanceStage({
180
+ cwd,
181
+ taskId,
182
+ workflow: TINY_WORKFLOW,
183
+ stageName: "plan",
184
+ artifactName: "plan.md",
185
+ frontmatter: { stage: "plan", status: "complete", references: [] },
186
+ body: "# Plan",
187
+ });
188
+
189
+ const after = advanceStage({
190
+ cwd,
191
+ taskId,
192
+ workflow: TINY_WORKFLOW,
193
+ stageName: "review",
194
+ artifactName: "review.md",
195
+ frontmatter: {
196
+ stage: "review",
197
+ status: "complete",
198
+ references: ["plan.md"],
199
+ approved: true,
200
+ },
201
+ body: "# Review\n\napproved",
202
+ outcome: { approved: true },
203
+ });
204
+
205
+ assert.equal(after.current_stage, null);
206
+ assert.equal(after.outcome, "complete");
207
+ assert.ok(after.completed_at);
208
+ assert.deepEqual(after.stages[1].outcome, { approved: true });
209
+ });
210
+
211
+ test("advanceStage: blocked status surfaces as run outcome", () => {
212
+ const cwd = makeWorkspace();
213
+ const taskId = "2026-05-06-blocked";
214
+ createRun({ cwd, taskId, workflow: TINY_WORKFLOW, taskDescription: "Test" });
215
+
216
+ const after = advanceStage({
217
+ cwd,
218
+ taskId,
219
+ workflow: TINY_WORKFLOW,
220
+ stageName: "plan",
221
+ artifactName: "plan.md",
222
+ frontmatter: { stage: "plan", status: "blocked", references: [] },
223
+ body: "# Plan blocked",
224
+ status: "blocked",
225
+ });
226
+
227
+ assert.equal(after.outcome, "blocked");
228
+ assert.equal(after.current_stage, null);
229
+ });
230
+
231
+ test("advanceStage: refuses to advance the wrong stage", () => {
232
+ const cwd = makeWorkspace();
233
+ const taskId = "2026-05-06-wrong";
234
+ createRun({ cwd, taskId, workflow: TINY_WORKFLOW, taskDescription: "Test" });
235
+
236
+ assert.throws(() =>
237
+ advanceStage({
238
+ cwd,
239
+ taskId,
240
+ workflow: TINY_WORKFLOW,
241
+ stageName: "review",
242
+ artifactName: "review.md",
243
+ frontmatter: { stage: "review", status: "complete", references: [] },
244
+ body: "# Out of order",
245
+ }),
246
+ );
247
+ });
248
+
249
+ test("getRunState: returns null for missing tasks", () => {
250
+ const cwd = makeWorkspace();
251
+ assert.equal(getRunState(cwd, "no-such-task"), null);
252
+ });
253
+
254
+ test("readRunState: validates persisted state against schema", () => {
255
+ const cwd = makeWorkspace();
256
+ const taskId = "2026-05-06-corrupt";
257
+ createRun({ cwd, taskId, workflow: TINY_WORKFLOW, taskDescription: "Test" });
258
+
259
+ // Corrupt the file: drop required field.
260
+ const statePath = path.join(cwd, ".agents", taskId, "state.json");
261
+ const raw = JSON.parse(fs.readFileSync(statePath, "utf8"));
262
+ delete raw.workflow_id;
263
+ fs.writeFileSync(statePath, JSON.stringify(raw, null, 2));
264
+
265
+ assert.throws(() => readRunState(cwd, taskId));
266
+ });
267
+
268
+ test("runStateSchema: rejects unknown outcome", () => {
269
+ assert.throws(() =>
270
+ runStateSchema.parse({
271
+ schema_version: 1,
272
+ task_id: "x",
273
+ workflow_id: "tiny",
274
+ workflow_version: 1,
275
+ task_description: "y",
276
+ current_stage: null,
277
+ outcome: "totally-bogus",
278
+ started_at: new Date().toISOString(),
279
+ completed_at: null,
280
+ stages: [{ name: "plan", status: "complete" }],
281
+ }),
282
+ );
283
+ });
@@ -2,7 +2,7 @@
2
2
  set -euo pipefail
3
3
 
4
4
  REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
- MCP_DIR="$REPO_ROOT/mcp"
5
+ MCP_DIR="$REPO_ROOT/.context/mcp"
6
6
  TOTAL_STEPS=6
7
7
  STEP_INDEX=0
8
8
 
@@ -3,7 +3,7 @@ set -euo pipefail
3
3
 
4
4
  REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
5
  CONTEXT_DIR="$REPO_ROOT/.context"
6
- MCP_DIR="$REPO_ROOT/mcp"
6
+ MCP_DIR="$CONTEXT_DIR/mcp"
7
7
 
8
8
  PASS=0
9
9
  FAIL=0
@@ -165,15 +165,15 @@ echo ""
165
165
  echo " MCP Server"
166
166
 
167
167
  if [[ -f "$MCP_DIR/dist/server.js" ]]; then
168
- pass "mcp/dist/server.js exists"
168
+ pass ".context/mcp/dist/server.js exists"
169
169
  else
170
- fail "mcp/dist/server.js missing — run: cd mcp && npm run build"
170
+ fail ".context/mcp/dist/server.js missing — run: cd .context/mcp && npm run build"
171
171
  fi
172
172
 
173
173
  if [[ -d "$MCP_DIR/node_modules" ]]; then
174
- pass "mcp/node_modules present"
174
+ pass ".context/mcp/node_modules present"
175
175
  else
176
- fail "mcp/node_modules missing — run: cd mcp && npm install"
176
+ fail ".context/mcp/node_modules missing — run: cd .context/mcp && npm install"
177
177
  fi
178
178
 
179
179
  # Quick MCP import check
@@ -181,7 +181,7 @@ if [[ -f "$MCP_DIR/dist/server.js" ]] && [[ -d "$MCP_DIR/node_modules" ]]; then
181
181
  MCP_CHECK=$(cd "$REPO_ROOT" && timeout 10 node -e '
182
182
  const start = Date.now();
183
183
  try {
184
- require("./mcp/dist/graph.js");
184
+ require("./.context/mcp/dist/graph.js");
185
185
  console.log("ok " + (Date.now() - start));
186
186
  } catch(e) {
187
187
  console.log("fail " + e.message);
@@ -2,7 +2,7 @@
2
2
  set -euo pipefail
3
3
 
4
4
  REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
- MCP_DIR="$REPO_ROOT/mcp"
5
+ MCP_DIR="$REPO_ROOT/.context/mcp"
6
6
 
7
7
  if ! command -v npm >/dev/null 2>&1; then
8
8
  echo "[embed] npm is required but not found on PATH"
@@ -11,5 +11,5 @@ fi
11
11
 
12
12
  mkdir -p "$MCP_DIR/.npm-cache"
13
13
 
14
- echo "[embed] generating embeddings via mcp/embed"
14
+ echo "[embed] generating embeddings via .context/mcp/embed"
15
15
  NPM_CONFIG_CACHE="$MCP_DIR/.npm-cache" npm --prefix "$MCP_DIR" run embed --silent -- "$@"
@@ -2,7 +2,7 @@
2
2
  set -euo pipefail
3
3
 
4
4
  REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
- MCP_DIR="$REPO_ROOT/mcp"
5
+ MCP_DIR="$REPO_ROOT/.context/mcp"
6
6
 
7
7
  if [[ ! -f "$MCP_DIR/package.json" ]]; then
8
8
  echo "[graph-load] missing $MCP_DIR/package.json"
@@ -10,8 +10,8 @@ if [[ ! -f "$MCP_DIR/package.json" ]]; then
10
10
  fi
11
11
 
12
12
  if [[ ! -d "$MCP_DIR/node_modules" ]]; then
13
- echo "[graph-load] node_modules missing in mcp/"
14
- echo "[graph-load] run: cd mcp && NPM_CONFIG_CACHE=$MCP_DIR/.npm-cache npm install"
13
+ echo "[graph-load] node_modules missing in .context/mcp/"
14
+ echo "[graph-load] run: cd .context/mcp && NPM_CONFIG_CACHE=$MCP_DIR/.npm-cache npm install"
15
15
  exit 1
16
16
  fi
17
17
 
@@ -2,7 +2,7 @@
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { parseFrontmatter, parseStringList } from "../mcp/dist/frontmatter.js";
5
+ import { parseFrontmatter, parseStringList } from "../.context/mcp/dist/frontmatter.js";
6
6
 
7
7
  const __filename = fileURLToPath(import.meta.url);
8
8
  const __dirname = path.dirname(__filename);
@@ -2,7 +2,7 @@
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { parseFrontmatter, parseStringList } from "../mcp/dist/frontmatter.js";
5
+ import { parseFrontmatter, parseStringList } from "../.context/mcp/dist/frontmatter.js";
6
6
 
7
7
  const __filename = fileURLToPath(import.meta.url);
8
8
  const __dirname = path.dirname(__filename);
@@ -146,11 +146,9 @@ status_digest() {
146
146
  fi
147
147
 
148
148
  # Fallback for non-git directories.
149
+ # .context/ excludes the relocated .context/mcp/ tree as well.
149
150
  find "$REPO_ROOT" -type f \
150
151
  ! -path "$REPO_ROOT/.context/*" \
151
- ! -path "$REPO_ROOT/mcp/node_modules/*" \
152
- ! -path "$REPO_ROOT/mcp/dist/*" \
153
- ! -path "$REPO_ROOT/mcp/.npm-cache/*" \
154
152
  ! -path "$REPO_ROOT/scripts/parsers/node_modules/*" \
155
153
  ! -path "$REPO_ROOT/scripts/parsers/.npm-cache/*" \
156
154
  -print \
@@ -201,16 +199,13 @@ wait_for_change_event() {
201
199
  inotifywait)
202
200
  inotifywait -q -r \
203
201
  -e modify,create,delete,move \
204
- --exclude '(^|/)\\.git(/|$)|(^|/)\\.context(/|$)|(^|/)mcp/(node_modules|dist|\\.npm-cache)(/|$)|(^|/)scripts/parsers/(node_modules|\\.npm-cache)(/|$)' \
202
+ --exclude '(^|/)\\.git(/|$)|(^|/)\\.context(/|$)|(^|/)scripts/parsers/(node_modules|\\.npm-cache)(/|$)' \
205
203
  "$REPO_ROOT" >/dev/null 2>&1 || true
206
204
  ;;
207
205
  fswatch)
208
206
  fswatch -1 -r \
209
207
  --exclude '(^|/)\\.git(/|$)' \
210
208
  --exclude '(^|/)\\.context(/|$)' \
211
- --exclude '(^|/)mcp/node_modules(/|$)' \
212
- --exclude '(^|/)mcp/dist(/|$)' \
213
- --exclude '(^|/)mcp/\\.npm-cache(/|$)' \
214
209
  --exclude '(^|/)scripts/parsers/node_modules(/|$)' \
215
210
  --exclude '(^|/)scripts/parsers/\\.npm-cache(/|$)' \
216
211
  "$REPO_ROOT" >/dev/null 2>&1 || true