@danielblomma/cortex-mcp 2.0.7 → 2.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scaffold/mcp/src/cli/stage.ts +42 -0
- package/scaffold/mcp/src/core/workflow/default-workflows.ts +35 -0
- package/scaffold/mcp/src/core/workflow/enforcement.ts +7 -1
- package/scaffold/mcp/src/core/workflow/envelope.ts +26 -1
- package/scaffold/mcp/src/core/workflow/index.ts +1 -0
- package/scaffold/mcp/src/core/workflow/mcp-tools.ts +16 -0
- package/scaffold/mcp/src/core/workflow/run-lifecycle.ts +49 -2
- package/scaffold/mcp/src/core/workflow/schemas.ts +45 -0
- package/scaffold/mcp/src/core/workflow/synced-capability-registry.ts +66 -0
- package/scaffold/mcp/src/daemon/capability-sync-checker.ts +295 -0
- package/scaffold/mcp/src/daemon/main.ts +15 -0
- package/scaffold/mcp/src/enterprise/index.ts +5 -0
- package/scaffold/mcp/src/enterprise/tools/harness.ts +98 -0
- package/scaffold/mcp/src/server.ts +4 -74
- package/scaffold/mcp/tests/workflow-cli.test.mjs +41 -0
- package/scaffold/mcp/tests/workflow-synced-capabilities.test.mjs +226 -0
- package/scaffold/mcp/tests/workflow-validators-override.test.mjs +272 -0
|
@@ -0,0 +1,226 @@
|
|
|
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
|
+
loadSyncedCapabilities,
|
|
9
|
+
syncedCapabilitiesCachePath,
|
|
10
|
+
} from "../dist/core/workflow/synced-capability-registry.js";
|
|
11
|
+
import { evaluateToolCall } from "../dist/core/workflow/enforcement.js";
|
|
12
|
+
import { createRun } from "../dist/core/workflow/run-lifecycle.js";
|
|
13
|
+
|
|
14
|
+
function makeWorkspace() {
|
|
15
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "cortex-synced-caps-"));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const FRONTEND_BUILDER = {
|
|
19
|
+
name: "frontend-builder",
|
|
20
|
+
description: "Frontend-only profile",
|
|
21
|
+
read_globs: ["**"],
|
|
22
|
+
write_globs: ["src/components/**"],
|
|
23
|
+
tools_allowed: [],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const WORKFLOW = {
|
|
27
|
+
id: "fe",
|
|
28
|
+
description: "Frontend-only build",
|
|
29
|
+
version: 1,
|
|
30
|
+
stages: [
|
|
31
|
+
{
|
|
32
|
+
name: "build",
|
|
33
|
+
artifact: "changes.md",
|
|
34
|
+
reads: [],
|
|
35
|
+
required_fields: [],
|
|
36
|
+
validators: [],
|
|
37
|
+
capability: "frontend-builder",
|
|
38
|
+
description: "Build the frontend",
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function writeCache(dir, payload) {
|
|
44
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
45
|
+
fs.writeFileSync(
|
|
46
|
+
syncedCapabilitiesCachePath(dir),
|
|
47
|
+
JSON.stringify(payload, null, 2),
|
|
48
|
+
"utf8",
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
test("syncedCapabilitiesCachePath: defaults to ~/.cortex/capabilities.local.json", () => {
|
|
53
|
+
const expected = path.join(os.homedir(), ".cortex", "capabilities.local.json");
|
|
54
|
+
assert.equal(syncedCapabilitiesCachePath(), expected);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("loadSyncedCapabilities: returns {} when cache is missing", () => {
|
|
58
|
+
const dir = makeWorkspace();
|
|
59
|
+
assert.deepEqual(loadSyncedCapabilities(dir), {});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("loadSyncedCapabilities: returns {} when cache is unreadable JSON", () => {
|
|
63
|
+
const dir = makeWorkspace();
|
|
64
|
+
fs.writeFileSync(syncedCapabilitiesCachePath(dir), "not json", "utf8");
|
|
65
|
+
assert.deepEqual(loadSyncedCapabilities(dir), {});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("loadSyncedCapabilities: returns {} when 'capabilities' key is missing", () => {
|
|
69
|
+
const dir = makeWorkspace();
|
|
70
|
+
writeCache(dir, { last_synced_at: "x" });
|
|
71
|
+
assert.deepEqual(loadSyncedCapabilities(dir), {});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("loadSyncedCapabilities: drops entries whose definition fails schema", () => {
|
|
75
|
+
const dir = makeWorkspace();
|
|
76
|
+
writeCache(dir, {
|
|
77
|
+
capabilities: {
|
|
78
|
+
"valid-one": {
|
|
79
|
+
capability_name: "valid-one",
|
|
80
|
+
updated_at: "2026-05-07T12:00:00.000Z",
|
|
81
|
+
definition: FRONTEND_BUILDER,
|
|
82
|
+
},
|
|
83
|
+
"broken-one": {
|
|
84
|
+
capability_name: "broken-one",
|
|
85
|
+
updated_at: "2026-05-07T12:00:00.000Z",
|
|
86
|
+
definition: { name: "broken-one" /* missing description */ },
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
const loaded = loadSyncedCapabilities(dir);
|
|
91
|
+
assert.deepEqual(Object.keys(loaded).sort(), ["valid-one"]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("loadSyncedCapabilities: returns valid capability definitions", () => {
|
|
95
|
+
const dir = makeWorkspace();
|
|
96
|
+
writeCache(dir, {
|
|
97
|
+
capabilities: {
|
|
98
|
+
"frontend-builder": {
|
|
99
|
+
capability_name: "frontend-builder",
|
|
100
|
+
updated_at: "2026-05-07T12:00:00.000Z",
|
|
101
|
+
definition: FRONTEND_BUILDER,
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
const loaded = loadSyncedCapabilities(dir);
|
|
106
|
+
assert.equal(loaded["frontend-builder"].name, "frontend-builder");
|
|
107
|
+
assert.deepEqual(loaded["frontend-builder"].write_globs, ["src/components/**"]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("evaluateToolCall integration: synced capability is consulted via merged registry", () => {
|
|
111
|
+
const cwd = makeWorkspace();
|
|
112
|
+
// Sandbox the home-dir-based loader.
|
|
113
|
+
const fakeHome = makeWorkspace();
|
|
114
|
+
process.env.HOME = fakeHome;
|
|
115
|
+
try {
|
|
116
|
+
fs.mkdirSync(path.join(fakeHome, ".cortex"), { recursive: true });
|
|
117
|
+
fs.writeFileSync(
|
|
118
|
+
path.join(fakeHome, ".cortex", "capabilities.local.json"),
|
|
119
|
+
JSON.stringify({
|
|
120
|
+
capabilities: {
|
|
121
|
+
"frontend-builder": {
|
|
122
|
+
capability_name: "frontend-builder",
|
|
123
|
+
updated_at: "2026-05-07T12:00:00.000Z",
|
|
124
|
+
definition: FRONTEND_BUILDER,
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
}),
|
|
128
|
+
"utf8",
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
createRun({
|
|
132
|
+
cwd,
|
|
133
|
+
taskId: "task-1",
|
|
134
|
+
workflow: WORKFLOW,
|
|
135
|
+
taskDescription: "Build frontend",
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Allowed: src/components/**
|
|
139
|
+
const allowed = evaluateToolCall({
|
|
140
|
+
cwd,
|
|
141
|
+
taskId: "task-1",
|
|
142
|
+
call: { toolName: "Edit", toolInput: { file_path: "src/components/Foo.tsx" } },
|
|
143
|
+
workflows: { fe: WORKFLOW },
|
|
144
|
+
});
|
|
145
|
+
assert.equal(allowed.allowed, true);
|
|
146
|
+
|
|
147
|
+
// Blocked: outside write_globs
|
|
148
|
+
const blocked = evaluateToolCall({
|
|
149
|
+
cwd,
|
|
150
|
+
taskId: "task-1",
|
|
151
|
+
call: { toolName: "Edit", toolInput: { file_path: "src/server/api.ts" } },
|
|
152
|
+
workflows: { fe: WORKFLOW },
|
|
153
|
+
});
|
|
154
|
+
assert.equal(blocked.allowed, false);
|
|
155
|
+
assert.match(blocked.reason, /frontend-builder/);
|
|
156
|
+
} finally {
|
|
157
|
+
delete process.env.HOME;
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("evaluateToolCall integration: synced capability with same name as bundled overrides bundled", () => {
|
|
162
|
+
const cwd = makeWorkspace();
|
|
163
|
+
const fakeHome = makeWorkspace();
|
|
164
|
+
process.env.HOME = fakeHome;
|
|
165
|
+
try {
|
|
166
|
+
// Override the bundled "builder" capability with a much stricter version
|
|
167
|
+
// — only test files writable.
|
|
168
|
+
const stricterBuilder = {
|
|
169
|
+
name: "builder",
|
|
170
|
+
description: "Org-overridden strict builder",
|
|
171
|
+
read_globs: ["**"],
|
|
172
|
+
write_globs: ["tests/**"],
|
|
173
|
+
tools_allowed: [],
|
|
174
|
+
};
|
|
175
|
+
fs.mkdirSync(path.join(fakeHome, ".cortex"), { recursive: true });
|
|
176
|
+
fs.writeFileSync(
|
|
177
|
+
path.join(fakeHome, ".cortex", "capabilities.local.json"),
|
|
178
|
+
JSON.stringify({
|
|
179
|
+
capabilities: {
|
|
180
|
+
builder: {
|
|
181
|
+
capability_name: "builder",
|
|
182
|
+
updated_at: "2026-05-07T12:00:00.000Z",
|
|
183
|
+
definition: stricterBuilder,
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
}),
|
|
187
|
+
"utf8",
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const builderWorkflow = {
|
|
191
|
+
id: "build-only",
|
|
192
|
+
description: "Build-only workflow using bundled name",
|
|
193
|
+
version: 1,
|
|
194
|
+
stages: [
|
|
195
|
+
{
|
|
196
|
+
name: "build",
|
|
197
|
+
artifact: "changes.md",
|
|
198
|
+
reads: [],
|
|
199
|
+
required_fields: [],
|
|
200
|
+
validators: [],
|
|
201
|
+
capability: "builder",
|
|
202
|
+
description: "Build",
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
};
|
|
206
|
+
createRun({
|
|
207
|
+
cwd,
|
|
208
|
+
taskId: "task-2",
|
|
209
|
+
workflow: builderWorkflow,
|
|
210
|
+
taskDescription: "Build",
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Bundled builder allows src/** + tests/**, but the org override only
|
|
214
|
+
// allows tests/**. src/** must be blocked under the override.
|
|
215
|
+
const result = evaluateToolCall({
|
|
216
|
+
cwd,
|
|
217
|
+
taskId: "task-2",
|
|
218
|
+
call: { toolName: "Edit", toolInput: { file_path: "src/main.ts" } },
|
|
219
|
+
workflows: { "build-only": builderWorkflow },
|
|
220
|
+
});
|
|
221
|
+
assert.equal(result.allowed, false);
|
|
222
|
+
assert.match(result.reason, /Org-overridden|tests/i);
|
|
223
|
+
} finally {
|
|
224
|
+
delete process.env.HOME;
|
|
225
|
+
}
|
|
226
|
+
});
|
|
@@ -0,0 +1,272 @@
|
|
|
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
|
+
createRun,
|
|
9
|
+
advanceStage,
|
|
10
|
+
} from "../dist/core/workflow/run-lifecycle.js";
|
|
11
|
+
import { composeStageEnvelope } from "../dist/core/workflow/envelope.js";
|
|
12
|
+
import {
|
|
13
|
+
runWorkflowAdvance,
|
|
14
|
+
runWorkflowStart,
|
|
15
|
+
} from "../dist/core/workflow/mcp-tools.js";
|
|
16
|
+
|
|
17
|
+
function makeWorkspace() {
|
|
18
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "cortex-validators-"));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const WORKFLOW = {
|
|
22
|
+
id: "validated",
|
|
23
|
+
description: "One stage that requires two validators",
|
|
24
|
+
version: 1,
|
|
25
|
+
stages: [
|
|
26
|
+
{
|
|
27
|
+
name: "build",
|
|
28
|
+
artifact: "changes.md",
|
|
29
|
+
reads: [],
|
|
30
|
+
required_fields: [],
|
|
31
|
+
validators: [
|
|
32
|
+
{ id: "tests-pass", description: "Test suite must pass" },
|
|
33
|
+
{ id: "build-passes", description: "Build must succeed" },
|
|
34
|
+
],
|
|
35
|
+
capability: "builder",
|
|
36
|
+
description: "Implement the change.",
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const REGISTRY = { validated: WORKFLOW };
|
|
42
|
+
|
|
43
|
+
test("advanceStage: blocks when validators_passed misses required ids", () => {
|
|
44
|
+
const cwd = makeWorkspace();
|
|
45
|
+
createRun({
|
|
46
|
+
cwd,
|
|
47
|
+
taskId: "task-1",
|
|
48
|
+
workflow: WORKFLOW,
|
|
49
|
+
taskDescription: "Test",
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
assert.throws(
|
|
53
|
+
() =>
|
|
54
|
+
advanceStage({
|
|
55
|
+
cwd,
|
|
56
|
+
taskId: "task-1",
|
|
57
|
+
workflow: WORKFLOW,
|
|
58
|
+
stageName: "build",
|
|
59
|
+
artifactName: "changes.md",
|
|
60
|
+
frontmatter: { stage: "build", status: "complete", references: [] },
|
|
61
|
+
body: "# Changes",
|
|
62
|
+
validatorsPassed: ["tests-pass"], // missing build-passes
|
|
63
|
+
}),
|
|
64
|
+
/Missing: build-passes/,
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("advanceStage: allows when validators_passed covers required ids", () => {
|
|
69
|
+
const cwd = makeWorkspace();
|
|
70
|
+
createRun({
|
|
71
|
+
cwd,
|
|
72
|
+
taskId: "task-1",
|
|
73
|
+
workflow: WORKFLOW,
|
|
74
|
+
taskDescription: "Test",
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const next = advanceStage({
|
|
78
|
+
cwd,
|
|
79
|
+
taskId: "task-1",
|
|
80
|
+
workflow: WORKFLOW,
|
|
81
|
+
stageName: "build",
|
|
82
|
+
artifactName: "changes.md",
|
|
83
|
+
frontmatter: { stage: "build", status: "complete", references: [] },
|
|
84
|
+
body: "# Changes",
|
|
85
|
+
validatorsPassed: ["tests-pass", "build-passes"],
|
|
86
|
+
});
|
|
87
|
+
assert.equal(next.outcome, "complete");
|
|
88
|
+
assert.deepEqual(next.stages[0].validators_passed, ["tests-pass", "build-passes"]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("advanceStage: override.skipped_validators bypasses missing-validator block", () => {
|
|
92
|
+
const cwd = makeWorkspace();
|
|
93
|
+
createRun({
|
|
94
|
+
cwd,
|
|
95
|
+
taskId: "task-1",
|
|
96
|
+
workflow: WORKFLOW,
|
|
97
|
+
taskDescription: "Test",
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const next = advanceStage({
|
|
101
|
+
cwd,
|
|
102
|
+
taskId: "task-1",
|
|
103
|
+
workflow: WORKFLOW,
|
|
104
|
+
stageName: "build",
|
|
105
|
+
artifactName: "changes.md",
|
|
106
|
+
frontmatter: { stage: "build", status: "complete", references: [] },
|
|
107
|
+
body: "# Changes",
|
|
108
|
+
validatorsPassed: ["tests-pass"],
|
|
109
|
+
override: {
|
|
110
|
+
reason: "Build infra is down on CI; skipping per ops-incident-2026-05-06",
|
|
111
|
+
skipped_validators: ["build-passes"],
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
assert.equal(next.outcome, "complete");
|
|
116
|
+
assert.equal(next.stages[0].override?.reason.includes("Build infra is down"), true);
|
|
117
|
+
assert.deepEqual(next.stages[0].override?.skipped_validators, ["build-passes"]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("advanceStage: blocked status is exempt from validator coverage check", () => {
|
|
121
|
+
const cwd = makeWorkspace();
|
|
122
|
+
createRun({
|
|
123
|
+
cwd,
|
|
124
|
+
taskId: "task-1",
|
|
125
|
+
workflow: WORKFLOW,
|
|
126
|
+
taskDescription: "Test",
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const next = advanceStage({
|
|
130
|
+
cwd,
|
|
131
|
+
taskId: "task-1",
|
|
132
|
+
workflow: WORKFLOW,
|
|
133
|
+
stageName: "build",
|
|
134
|
+
artifactName: "changes.md",
|
|
135
|
+
frontmatter: { stage: "build", status: "blocked", references: [] },
|
|
136
|
+
body: "# Plan blocked\n\nMissing context.",
|
|
137
|
+
status: "blocked",
|
|
138
|
+
validatorsPassed: [], // exempt
|
|
139
|
+
});
|
|
140
|
+
assert.equal(next.outcome, "blocked");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("advanceStage: override is stamped into the artifact frontmatter", () => {
|
|
144
|
+
const cwd = makeWorkspace();
|
|
145
|
+
createRun({
|
|
146
|
+
cwd,
|
|
147
|
+
taskId: "task-1",
|
|
148
|
+
workflow: WORKFLOW,
|
|
149
|
+
taskDescription: "Test",
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
advanceStage({
|
|
153
|
+
cwd,
|
|
154
|
+
taskId: "task-1",
|
|
155
|
+
workflow: WORKFLOW,
|
|
156
|
+
stageName: "build",
|
|
157
|
+
artifactName: "changes.md",
|
|
158
|
+
frontmatter: { stage: "build", status: "complete", references: [] },
|
|
159
|
+
body: "# Changes",
|
|
160
|
+
validatorsPassed: ["tests-pass"],
|
|
161
|
+
override: {
|
|
162
|
+
reason: "Hot fix, will follow up",
|
|
163
|
+
skipped_validators: ["build-passes"],
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const text = fs.readFileSync(
|
|
168
|
+
path.join(cwd, ".agents", "task-1", "changes.md"),
|
|
169
|
+
"utf8",
|
|
170
|
+
);
|
|
171
|
+
assert.match(text, /override:/);
|
|
172
|
+
assert.match(text, /reason: Hot fix/);
|
|
173
|
+
assert.match(text, /- build-passes/);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("composeStageEnvelope: renders VALIDATORS section when stage declares them", () => {
|
|
177
|
+
const cwd = makeWorkspace();
|
|
178
|
+
createRun({
|
|
179
|
+
cwd,
|
|
180
|
+
taskId: "task-1",
|
|
181
|
+
workflow: WORKFLOW,
|
|
182
|
+
taskDescription: "Test",
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const env = composeStageEnvelope({
|
|
186
|
+
cwd,
|
|
187
|
+
taskId: "task-1",
|
|
188
|
+
workflow: WORKFLOW,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
assert.match(env.prompt, /# VALIDATORS/);
|
|
192
|
+
assert.match(env.prompt, /`tests-pass` — Test suite must pass/);
|
|
193
|
+
assert.match(env.prompt, /`build-passes` — Build must succeed/);
|
|
194
|
+
assert.match(env.prompt, /`validators_passed: \[<id1>, <id2>, \.\.\.\]`/);
|
|
195
|
+
assert.deepEqual(env.validators.map((v) => v.id), ["tests-pass", "build-passes"]);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("composeStageEnvelope: VALIDATORS section is empty when stage has none", () => {
|
|
199
|
+
const cwd = makeWorkspace();
|
|
200
|
+
const noValidators = {
|
|
201
|
+
...WORKFLOW,
|
|
202
|
+
stages: [{ ...WORKFLOW.stages[0], validators: [] }],
|
|
203
|
+
};
|
|
204
|
+
createRun({
|
|
205
|
+
cwd,
|
|
206
|
+
taskId: "task-1",
|
|
207
|
+
workflow: noValidators,
|
|
208
|
+
taskDescription: "Test",
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const env = composeStageEnvelope({
|
|
212
|
+
cwd,
|
|
213
|
+
taskId: "task-1",
|
|
214
|
+
workflow: noValidators,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
assert.match(env.prompt, /No validators required for this stage/);
|
|
218
|
+
assert.equal(env.validators.length, 0);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("runWorkflowAdvance MCP runner: forwards validators_passed + override", () => {
|
|
222
|
+
const cwd = makeWorkspace();
|
|
223
|
+
runWorkflowStart(
|
|
224
|
+
{ task_id: "task-1", task_description: "x", workflow_id: "validated" },
|
|
225
|
+
{ cwd, workflows: REGISTRY },
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const result = runWorkflowAdvance(
|
|
229
|
+
{
|
|
230
|
+
task_id: "task-1",
|
|
231
|
+
stage: "build",
|
|
232
|
+
frontmatter: {},
|
|
233
|
+
body: "# Changes",
|
|
234
|
+
validators_passed: ["tests-pass"],
|
|
235
|
+
override: {
|
|
236
|
+
reason: "CI builder offline; manual verification done",
|
|
237
|
+
skipped_validators: ["build-passes"],
|
|
238
|
+
skipped_requirements: [],
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
{ cwd, workflows: REGISTRY },
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
assert.equal(result.state.outcome, "complete");
|
|
245
|
+
assert.equal(
|
|
246
|
+
result.state.stages[0].override?.reason.includes("CI builder offline"),
|
|
247
|
+
true,
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("runWorkflowAdvance MCP runner: rejects missing validators without override", () => {
|
|
252
|
+
const cwd = makeWorkspace();
|
|
253
|
+
runWorkflowStart(
|
|
254
|
+
{ task_id: "task-1", task_description: "x", workflow_id: "validated" },
|
|
255
|
+
{ cwd, workflows: REGISTRY },
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
assert.throws(
|
|
259
|
+
() =>
|
|
260
|
+
runWorkflowAdvance(
|
|
261
|
+
{
|
|
262
|
+
task_id: "task-1",
|
|
263
|
+
stage: "build",
|
|
264
|
+
frontmatter: {},
|
|
265
|
+
body: "# Changes",
|
|
266
|
+
validators_passed: [],
|
|
267
|
+
},
|
|
268
|
+
{ cwd, workflows: REGISTRY },
|
|
269
|
+
),
|
|
270
|
+
/Missing: tests-pass, build-passes/,
|
|
271
|
+
);
|
|
272
|
+
});
|