@danielblomma/cortex-mcp 2.0.7 → 2.0.8
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/envelope.ts +26 -1
- 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/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-validators-override.test.mjs +272 -0
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.
|
|
4
|
+
"version": "2.0.8",
|
|
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",
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
type WorkflowDefinition,
|
|
10
10
|
} from "../core/workflow/index.js";
|
|
11
11
|
import { DEFAULT_WORKFLOWS } from "../core/workflow/default-workflows.js";
|
|
12
|
+
import { isEnterpriseProject } from "../hooks/shared.js";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* `cortex stage` CLI surface. Each subcommand is a thin shell wrapper
|
|
@@ -31,10 +32,23 @@ import { DEFAULT_WORKFLOWS } from "../core/workflow/default-workflows.js";
|
|
|
31
32
|
* standalone shell calls.
|
|
32
33
|
*/
|
|
33
34
|
|
|
35
|
+
const ENTERPRISE_REQUIRED_MESSAGE =
|
|
36
|
+
"cortex stage is part of the Cortex Harness — an enterprise-only feature. " +
|
|
37
|
+
"This project does not have an enterprise license configured (no enterprise.api_key " +
|
|
38
|
+
"in .context/enterprise.yml). Run 'cortex enterprise <api-key>' to enable it, or " +
|
|
39
|
+
"contact your org admin.";
|
|
40
|
+
|
|
34
41
|
export async function runStageCommand(args: string[]): Promise<void> {
|
|
35
42
|
const sub = args[0] ?? "help";
|
|
36
43
|
const rest = args.slice(1);
|
|
37
44
|
|
|
45
|
+
// help is allowed in community mode so users can discover the feature exists.
|
|
46
|
+
if (sub !== "help" && sub !== "--help" && sub !== "-h") {
|
|
47
|
+
if (!isEnterpriseProject(projectRoot())) {
|
|
48
|
+
throw new Error(ENTERPRISE_REQUIRED_MESSAGE);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
38
52
|
switch (sub) {
|
|
39
53
|
case "start":
|
|
40
54
|
return runStart(rest);
|
|
@@ -65,6 +79,8 @@ function printHelp(): void {
|
|
|
65
79
|
" cortex stage envelope --task-id <id> [--stage <name>]",
|
|
66
80
|
" cortex stage advance --task-id <id> --stage <name> --body-file <path>",
|
|
67
81
|
" [--frontmatter-file <path>] [--status <s>] [--outcome-file <path>]",
|
|
82
|
+
" [--validators-passed <id1,id2,...>]",
|
|
83
|
+
" [--override-reason \"...\"] [--override-skipped-validators <id1,id2>]",
|
|
68
84
|
" cortex stage run --task-id <id> -- <command> [args...]",
|
|
69
85
|
"",
|
|
70
86
|
"Status values: complete (default) | blocked | failed",
|
|
@@ -193,12 +209,36 @@ async function runAdvance(args: string[]): Promise<void> {
|
|
|
193
209
|
typeof flags["outcome-file"] === "string" ? flags["outcome-file"] : null;
|
|
194
210
|
const statusFlag =
|
|
195
211
|
typeof flags.status === "string" ? (flags.status as StageStatus) : undefined;
|
|
212
|
+
const validatorsRaw =
|
|
213
|
+
typeof flags["validators-passed"] === "string"
|
|
214
|
+
? flags["validators-passed"]
|
|
215
|
+
: null;
|
|
216
|
+
const overrideReason =
|
|
217
|
+
typeof flags["override-reason"] === "string"
|
|
218
|
+
? flags["override-reason"]
|
|
219
|
+
: null;
|
|
220
|
+
const overrideSkippedRaw =
|
|
221
|
+
typeof flags["override-skipped-validators"] === "string"
|
|
222
|
+
? flags["override-skipped-validators"]
|
|
223
|
+
: null;
|
|
196
224
|
|
|
197
225
|
const body = readFileSync(bodyPath, "utf8");
|
|
198
226
|
const frontmatter: Record<string, unknown> = frontmatterPath
|
|
199
227
|
? parseJsonObject(frontmatterPath)
|
|
200
228
|
: {};
|
|
201
229
|
const outcome = outcomePath ? parseJsonObject(outcomePath) : undefined;
|
|
230
|
+
const validatorsPassed = validatorsRaw
|
|
231
|
+
? validatorsRaw.split(",").map((s) => s.trim()).filter(Boolean)
|
|
232
|
+
: undefined;
|
|
233
|
+
const override = overrideReason
|
|
234
|
+
? {
|
|
235
|
+
reason: overrideReason,
|
|
236
|
+
skipped_validators: overrideSkippedRaw
|
|
237
|
+
? overrideSkippedRaw.split(",").map((s) => s.trim()).filter(Boolean)
|
|
238
|
+
: [],
|
|
239
|
+
skipped_requirements: [],
|
|
240
|
+
}
|
|
241
|
+
: undefined;
|
|
202
242
|
|
|
203
243
|
const state = getRunState(projectRoot(), taskId);
|
|
204
244
|
if (!state) {
|
|
@@ -234,6 +274,8 @@ async function runAdvance(args: string[]): Promise<void> {
|
|
|
234
274
|
body,
|
|
235
275
|
status: finalStatus,
|
|
236
276
|
outcome,
|
|
277
|
+
validatorsPassed,
|
|
278
|
+
override,
|
|
237
279
|
});
|
|
238
280
|
|
|
239
281
|
let nextEnvelope = null;
|
|
@@ -17,6 +17,7 @@ export const SECURE_BUILD_WORKFLOW: WorkflowDefinition = {
|
|
|
17
17
|
artifact: "plan.md",
|
|
18
18
|
reads: [],
|
|
19
19
|
required_fields: ["files_targeted", "constraints"],
|
|
20
|
+
validators: [],
|
|
20
21
|
capability: "planner",
|
|
21
22
|
description:
|
|
22
23
|
"Produce a step-by-step implementation plan grounded in the repo's rules and memory.",
|
|
@@ -26,6 +27,7 @@ export const SECURE_BUILD_WORKFLOW: WorkflowDefinition = {
|
|
|
26
27
|
artifact: "plan-review.md",
|
|
27
28
|
reads: ["plan"],
|
|
28
29
|
required_fields: ["approved", "blocking_comments"],
|
|
30
|
+
validators: [],
|
|
29
31
|
capability: "reviewer",
|
|
30
32
|
description:
|
|
31
33
|
"Review the plan for architectural fit and rule compliance before any code is written.",
|
|
@@ -35,6 +37,18 @@ export const SECURE_BUILD_WORKFLOW: WorkflowDefinition = {
|
|
|
35
37
|
artifact: "changes.md",
|
|
36
38
|
reads: ["plan", "plan-review"],
|
|
37
39
|
required_fields: ["files_changed"],
|
|
40
|
+
validators: [
|
|
41
|
+
{
|
|
42
|
+
id: "build-passes",
|
|
43
|
+
description:
|
|
44
|
+
"The project's build / typecheck command must succeed on the produced diff.",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: "tests-pass",
|
|
48
|
+
description:
|
|
49
|
+
"The project's existing test suite must pass on the produced diff.",
|
|
50
|
+
},
|
|
51
|
+
],
|
|
38
52
|
capability: "builder",
|
|
39
53
|
description:
|
|
40
54
|
"Implement the approved plan. Produces the diff manifest used by the downstream reviewers.",
|
|
@@ -44,6 +58,7 @@ export const SECURE_BUILD_WORKFLOW: WorkflowDefinition = {
|
|
|
44
58
|
artifact: "build-review.md",
|
|
45
59
|
reads: ["plan", "changes"],
|
|
46
60
|
required_fields: ["approved", "blocking_comments"],
|
|
61
|
+
validators: [],
|
|
47
62
|
capability: "reviewer",
|
|
48
63
|
description:
|
|
49
64
|
"Review the implementation against the plan and the project's rules.",
|
|
@@ -53,6 +68,13 @@ export const SECURE_BUILD_WORKFLOW: WorkflowDefinition = {
|
|
|
53
68
|
artifact: "mutation-report.md",
|
|
54
69
|
reads: ["changes"],
|
|
55
70
|
required_fields: ["score", "survived"],
|
|
71
|
+
validators: [
|
|
72
|
+
{
|
|
73
|
+
id: "mutation-score",
|
|
74
|
+
description:
|
|
75
|
+
"Mutation testing tool of the agent's choice (e.g. stryker, mutmut) must run and report a score against the changed files.",
|
|
76
|
+
},
|
|
77
|
+
],
|
|
56
78
|
capability: "tester",
|
|
57
79
|
description:
|
|
58
80
|
"Run mutation tests on the changed files. Report score + surviving mutants.",
|
|
@@ -62,6 +84,18 @@ export const SECURE_BUILD_WORKFLOW: WorkflowDefinition = {
|
|
|
62
84
|
artifact: "security-report.md",
|
|
63
85
|
reads: ["changes"],
|
|
64
86
|
required_fields: ["findings", "severity_summary"],
|
|
87
|
+
validators: [
|
|
88
|
+
{
|
|
89
|
+
id: "secret-scan",
|
|
90
|
+
description:
|
|
91
|
+
"Scan the diff for newly committed secrets (API keys, tokens, credentials).",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: "dependency-audit",
|
|
95
|
+
description:
|
|
96
|
+
"Audit any added or upgraded dependencies for known vulnerabilities.",
|
|
97
|
+
},
|
|
98
|
+
],
|
|
65
99
|
capability: "security-reviewer",
|
|
66
100
|
description:
|
|
67
101
|
"Security review focused on the diff: injection, authn/authz, secrets, dependency risk.",
|
|
@@ -71,6 +105,7 @@ export const SECURE_BUILD_WORKFLOW: WorkflowDefinition = {
|
|
|
71
105
|
artifact: "approval.md",
|
|
72
106
|
reads: ["plan", "changes", "build-review", "mutation", "security"],
|
|
73
107
|
required_fields: ["approved", "approver"],
|
|
108
|
+
validators: [],
|
|
74
109
|
capability: "human",
|
|
75
110
|
description:
|
|
76
111
|
"Human sign-off. Pulls every prior artifact for the approver to read; the approver writes the artifact.",
|
|
@@ -35,6 +35,8 @@ export type ComposedEnvelope = {
|
|
|
35
35
|
requiredFields: string[];
|
|
36
36
|
/** Capability key the stage runs under (informational). */
|
|
37
37
|
capability: string | null;
|
|
38
|
+
/** Validators the stage requires the agent to have run. */
|
|
39
|
+
validators: { id: string; description: string }[];
|
|
38
40
|
};
|
|
39
41
|
|
|
40
42
|
export type ComposeStageEnvelopeOptions = {
|
|
@@ -107,6 +109,7 @@ export function composeStageEnvelope(
|
|
|
107
109
|
|
|
108
110
|
const requiredFields = stage.required_fields;
|
|
109
111
|
const capability = stage.capability ?? null;
|
|
112
|
+
const validators = stage.validators;
|
|
110
113
|
|
|
111
114
|
const prompt = renderPrompt({
|
|
112
115
|
taskDescription: state.task_description,
|
|
@@ -118,6 +121,7 @@ export function composeStageEnvelope(
|
|
|
118
121
|
requiredFields,
|
|
119
122
|
capability,
|
|
120
123
|
handoffs,
|
|
124
|
+
validators,
|
|
121
125
|
});
|
|
122
126
|
|
|
123
127
|
return {
|
|
@@ -125,6 +129,7 @@ export function composeStageEnvelope(
|
|
|
125
129
|
expectedArtifact: stage.artifact,
|
|
126
130
|
requiredFields,
|
|
127
131
|
capability,
|
|
132
|
+
validators,
|
|
128
133
|
};
|
|
129
134
|
}
|
|
130
135
|
|
|
@@ -150,6 +155,7 @@ type RenderPromptOptions = {
|
|
|
150
155
|
requiredFields: string[];
|
|
151
156
|
capability: string | null;
|
|
152
157
|
handoffs: string[];
|
|
158
|
+
validators: { id: string; description: string }[];
|
|
153
159
|
};
|
|
154
160
|
|
|
155
161
|
function renderPrompt(o: RenderPromptOptions): string {
|
|
@@ -200,13 +206,32 @@ function renderPrompt(o: RenderPromptOptions): string {
|
|
|
200
206
|
? `_No additional required fields beyond the harness defaults._`
|
|
201
207
|
: o.requiredFields.map((f) => `- \`${f}\``).join("\n");
|
|
202
208
|
|
|
209
|
+
const validatorLines =
|
|
210
|
+
o.validators.length === 0
|
|
211
|
+
? `_No validators required for this stage._`
|
|
212
|
+
: o.validators
|
|
213
|
+
.map((v) => `- \`${v.id}\` — ${v.description}`)
|
|
214
|
+
.join("\n");
|
|
215
|
+
|
|
216
|
+
sections.push(
|
|
217
|
+
[
|
|
218
|
+
`# VALIDATORS`,
|
|
219
|
+
``,
|
|
220
|
+
`Cortex defines what must be validated; you (the agent) pick the concrete tooling and run it in this environment, then report back.`,
|
|
221
|
+
``,
|
|
222
|
+
validatorLines,
|
|
223
|
+
``,
|
|
224
|
+
`When you call \`cortex.workflow.advance\`, set \`validators_passed: [<id1>, <id2>, ...]\` listing every validator you successfully ran. The harness blocks the advance if a required validator is missing — unless you explicitly pass \`override\` with a reason and the list of skipped validator ids.`,
|
|
225
|
+
].join("\n"),
|
|
226
|
+
);
|
|
227
|
+
|
|
203
228
|
sections.push(
|
|
204
229
|
[
|
|
205
230
|
`# OUTPUT`,
|
|
206
231
|
``,
|
|
207
232
|
`Produce a single markdown file named \`${o.expectedArtifact}\` with YAML frontmatter on top.`,
|
|
208
233
|
``,
|
|
209
|
-
`Required frontmatter fields (in addition to \`stage\`, \`status\`, \`references\`, \`written_at\` which the harness manages):`,
|
|
234
|
+
`Required frontmatter fields (in addition to \`stage\`, \`status\`, \`references\`, \`validators_passed\`, \`written_at\` which the harness manages):`,
|
|
210
235
|
``,
|
|
211
236
|
requiredLines,
|
|
212
237
|
``,
|
|
@@ -4,6 +4,7 @@ import { composeStageEnvelope } from "./envelope.js";
|
|
|
4
4
|
import { DEFAULT_WORKFLOWS } from "./default-workflows.js";
|
|
5
5
|
import { loadSyncedWorkflows } from "./synced-registry.js";
|
|
6
6
|
import {
|
|
7
|
+
stageOverrideSchema,
|
|
7
8
|
stageStatusSchema,
|
|
8
9
|
type StageStatus,
|
|
9
10
|
type WorkflowDefinition,
|
|
@@ -45,6 +46,19 @@ export const WorkflowAdvanceInput = z.object({
|
|
|
45
46
|
status: stageStatusSchema.optional(),
|
|
46
47
|
/** Optional structured outcome surfaced into state.json for fast lookup by later stages. */
|
|
47
48
|
outcome: z.record(z.string(), z.unknown()).optional(),
|
|
49
|
+
/**
|
|
50
|
+
* Validators the agent reports having run for this stage. Compared
|
|
51
|
+
* against the stage definition's required validators on advance —
|
|
52
|
+
* missing entries block the call unless an override is supplied.
|
|
53
|
+
*/
|
|
54
|
+
validators_passed: z.array(z.string().min(1)).default([]),
|
|
55
|
+
/**
|
|
56
|
+
* Process override. Required when validators_passed does not cover
|
|
57
|
+
* every validator the stage declares. Logged as a high-evidence
|
|
58
|
+
* audit event and stamped into the artifact's frontmatter so the
|
|
59
|
+
* deviation is visible in the evidence trail.
|
|
60
|
+
*/
|
|
61
|
+
override: stageOverrideSchema.optional(),
|
|
48
62
|
});
|
|
49
63
|
export type WorkflowAdvanceInputT = z.infer<typeof WorkflowAdvanceInput>;
|
|
50
64
|
|
|
@@ -155,6 +169,8 @@ export function runWorkflowAdvance(
|
|
|
155
169
|
body: input.body,
|
|
156
170
|
outcome: input.outcome,
|
|
157
171
|
status: finalStatus,
|
|
172
|
+
validatorsPassed: input.validators_passed,
|
|
173
|
+
override: input.override,
|
|
158
174
|
});
|
|
159
175
|
|
|
160
176
|
// If the run is still going, also return the next envelope so the caller
|
|
@@ -6,8 +6,10 @@ import {
|
|
|
6
6
|
import {
|
|
7
7
|
runStateSchema,
|
|
8
8
|
stageArtifactFrontmatterSchema,
|
|
9
|
+
stageOverrideSchema,
|
|
9
10
|
workflowDefinitionSchema,
|
|
10
11
|
type RunState,
|
|
12
|
+
type StageOverride,
|
|
11
13
|
type StageRecord,
|
|
12
14
|
type StageStatus,
|
|
13
15
|
type WorkflowDefinition,
|
|
@@ -36,6 +38,7 @@ export function createRun(options: CreateRunOptions): RunState {
|
|
|
36
38
|
const stages: StageRecord[] = workflow.stages.map((stage) => ({
|
|
37
39
|
name: stage.name,
|
|
38
40
|
status: "pending" as StageStatus,
|
|
41
|
+
validators_passed: [],
|
|
39
42
|
}));
|
|
40
43
|
|
|
41
44
|
const state: RunState = {
|
|
@@ -80,6 +83,10 @@ export type AdvanceStageOptions = {
|
|
|
80
83
|
outcome?: Record<string, unknown>;
|
|
81
84
|
/** Final status to record for this stage. Defaults to "complete". */
|
|
82
85
|
status?: StageStatus;
|
|
86
|
+
/** Validators the agent reports having run. Compared against stage.validators. */
|
|
87
|
+
validatorsPassed?: string[];
|
|
88
|
+
/** Process override; required when validators_passed doesn't cover stage.validators. */
|
|
89
|
+
override?: StageOverride;
|
|
83
90
|
now?: () => Date;
|
|
84
91
|
};
|
|
85
92
|
|
|
@@ -111,12 +118,52 @@ export function advanceStage(options: AdvanceStageOptions): RunState {
|
|
|
111
118
|
);
|
|
112
119
|
}
|
|
113
120
|
|
|
121
|
+
const stageIndex = workflow.stages.findIndex((s) => s.name === options.stageName);
|
|
122
|
+
const stageDef = workflow.stages[stageIndex];
|
|
123
|
+
|
|
124
|
+
// Validator coverage check: every validator the stage declares must
|
|
125
|
+
// appear in validators_passed unless the override explicitly skips it.
|
|
126
|
+
// Process is enforced here even though the validators themselves run
|
|
127
|
+
// in the agent's environment.
|
|
128
|
+
const validatorsPassed = options.validatorsPassed ?? [];
|
|
129
|
+
const override = options.override
|
|
130
|
+
? stageOverrideSchema.parse(options.override)
|
|
131
|
+
: undefined;
|
|
132
|
+
const requiredValidators = stageDef.validators.map((v) => v.id);
|
|
133
|
+
const declaredSkipped = new Set(override?.skipped_validators ?? []);
|
|
134
|
+
const missingValidators = requiredValidators.filter(
|
|
135
|
+
(id) => !validatorsPassed.includes(id) && !declaredSkipped.has(id),
|
|
136
|
+
);
|
|
137
|
+
// blocked / failed stages are exempt from validator coverage — the
|
|
138
|
+
// stage is explicitly halting before completion, so the validators
|
|
139
|
+
// logically cannot have run.
|
|
140
|
+
const finalStatus = options.status ?? "complete";
|
|
141
|
+
const exemptStatus = finalStatus === "blocked" || finalStatus === "failed";
|
|
142
|
+
if (missingValidators.length > 0 && !exemptStatus) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`Stage ${options.stageName} requires validators ${requiredValidators.join(", ")} ` +
|
|
145
|
+
`but artifact reported only ${validatorsPassed.join(", ") || "<none>"}. ` +
|
|
146
|
+
`Missing: ${missingValidators.join(", ")}. ` +
|
|
147
|
+
`Pass override.skipped_validators with a reason to advance anyway.`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
114
151
|
const now = (options.now ?? (() => new Date()))();
|
|
115
152
|
const completedAt = now.toISOString();
|
|
116
153
|
|
|
117
154
|
const frontmatter = stageArtifactFrontmatterSchema.parse({
|
|
118
155
|
...options.frontmatter,
|
|
119
156
|
stage: options.stageName,
|
|
157
|
+
validators_passed: validatorsPassed,
|
|
158
|
+
...(override
|
|
159
|
+
? {
|
|
160
|
+
override: {
|
|
161
|
+
reason: override.reason,
|
|
162
|
+
skipped_validators: override.skipped_validators,
|
|
163
|
+
skipped_requirements: override.skipped_requirements,
|
|
164
|
+
},
|
|
165
|
+
}
|
|
166
|
+
: {}),
|
|
120
167
|
written_at: options.frontmatter.written_at ?? completedAt,
|
|
121
168
|
});
|
|
122
169
|
writeStageArtifact(
|
|
@@ -127,9 +174,7 @@ export function advanceStage(options: AdvanceStageOptions): RunState {
|
|
|
127
174
|
options.body,
|
|
128
175
|
);
|
|
129
176
|
|
|
130
|
-
const stageIndex = workflow.stages.findIndex((s) => s.name === options.stageName);
|
|
131
177
|
const nextStage = workflow.stages[stageIndex + 1] ?? null;
|
|
132
|
-
const finalStatus = options.status ?? "complete";
|
|
133
178
|
|
|
134
179
|
const updatedStages: StageRecord[] = state.stages.map((record) => {
|
|
135
180
|
if (record.name !== options.stageName) return record;
|
|
@@ -140,6 +185,8 @@ export function advanceStage(options: AdvanceStageOptions): RunState {
|
|
|
140
185
|
started_at: record.started_at ?? state.started_at,
|
|
141
186
|
completed_at: completedAt,
|
|
142
187
|
outcome: options.outcome,
|
|
188
|
+
validators_passed: validatorsPassed,
|
|
189
|
+
override,
|
|
143
190
|
};
|
|
144
191
|
});
|
|
145
192
|
|
|
@@ -26,6 +26,23 @@ const slugSchema = z
|
|
|
26
26
|
"Must be lowercase alphanumeric with hyphens (no leading/trailing hyphen)",
|
|
27
27
|
);
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Validator requirement declared by a workflow stage. Cortex enforces
|
|
31
|
+
* that the agent reports having run each declared validator (via the
|
|
32
|
+
* artifact's `validators_passed` frontmatter), but never executes the
|
|
33
|
+
* validator itself — the agent picks the concrete tooling.
|
|
34
|
+
*
|
|
35
|
+
* id is a stable identifier (e.g. "mutation-score") that the agent
|
|
36
|
+
* echoes back; description is human-readable context for the agent
|
|
37
|
+
* rendered into the stage envelope.
|
|
38
|
+
*/
|
|
39
|
+
export const validatorRequirementSchema = z.object({
|
|
40
|
+
id: slugSchema,
|
|
41
|
+
description: z.string().min(1).max(500),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export type ValidatorRequirement = z.infer<typeof validatorRequirementSchema>;
|
|
45
|
+
|
|
29
46
|
/**
|
|
30
47
|
* Static definition of a single stage in a workflow. Authored at the
|
|
31
48
|
* organization level (in cortex-web later) and synced down to projects.
|
|
@@ -37,6 +54,15 @@ export const stageDefinitionSchema = z.object({
|
|
|
37
54
|
reads: z.array(slugSchema).default([]),
|
|
38
55
|
/** Required frontmatter fields the produced artifact must populate. */
|
|
39
56
|
required_fields: z.array(z.string().min(1)).default([]),
|
|
57
|
+
/**
|
|
58
|
+
* Validators the stage requires the agent to have run. The agent
|
|
59
|
+
* picks the actual tooling (e.g. stryker for mutation testing) and
|
|
60
|
+
* reports the result by listing each validator's id under
|
|
61
|
+
* `validators_passed` in the artifact frontmatter. Cortex enforces
|
|
62
|
+
* the list-coverage contract on advance unless the call carries an
|
|
63
|
+
* explicit override.
|
|
64
|
+
*/
|
|
65
|
+
validators: z.array(validatorRequirementSchema).default([]),
|
|
40
66
|
/** Capability key the stage runs under. References a separate capability registry. */
|
|
41
67
|
capability: z.string().min(1).optional(),
|
|
42
68
|
/** Human-readable summary surfaced in dashboards and audit. */
|
|
@@ -70,6 +96,21 @@ export const stageStatusSchema = z.enum([
|
|
|
70
96
|
|
|
71
97
|
export type StageStatus = z.infer<typeof stageStatusSchema>;
|
|
72
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Process-override record. When a stage advances despite missing or
|
|
101
|
+
* failed validators, the caller must pass an override with a free-text
|
|
102
|
+
* reason. The override is recorded on the StageRecord, stamped into
|
|
103
|
+
* the artifact's frontmatter, and emitted as a high-evidence audit
|
|
104
|
+
* event so reviewers can see the deviation in the evidence trail.
|
|
105
|
+
*/
|
|
106
|
+
export const stageOverrideSchema = z.object({
|
|
107
|
+
reason: z.string().min(1).max(2000),
|
|
108
|
+
skipped_validators: z.array(z.string().min(1)).default([]),
|
|
109
|
+
skipped_requirements: z.array(z.string().min(1)).default([]),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
export type StageOverride = z.infer<typeof stageOverrideSchema>;
|
|
113
|
+
|
|
73
114
|
/**
|
|
74
115
|
* Per-stage record inside state.json. Holds outcome metadata that the next
|
|
75
116
|
* stage's envelope composer needs without re-parsing every artifact.
|
|
@@ -82,6 +123,10 @@ export const stageRecordSchema = z.object({
|
|
|
82
123
|
completed_at: z.string().datetime().optional(),
|
|
83
124
|
/** Frontmatter outcome surfaced for fast lookup (e.g. approved=true on review). */
|
|
84
125
|
outcome: z.record(z.string(), z.unknown()).optional(),
|
|
126
|
+
/** Validators the agent reported having run for this stage. */
|
|
127
|
+
validators_passed: z.array(z.string().min(1)).default([]),
|
|
128
|
+
/** Override record if the stage advanced despite missing/failed requirements. */
|
|
129
|
+
override: stageOverrideSchema.optional(),
|
|
85
130
|
});
|
|
86
131
|
|
|
87
132
|
export type StageRecord = z.infer<typeof stageRecordSchema>;
|
|
@@ -14,6 +14,7 @@ import { pushAuditEvents, queueAuditEvent, setAuditPushContext } from "./audit/p
|
|
|
14
14
|
import { PolicyStore } from "../core/policy/store.js";
|
|
15
15
|
import { syncFromCloud, syncFromLocal } from "./policy/sync.js";
|
|
16
16
|
import { registerEnterpriseTools } from "./tools/enterprise.js";
|
|
17
|
+
import { registerHarnessTools } from "./tools/harness.js";
|
|
17
18
|
import { pushViolations, setViolationPushContext } from "./violations/push.js";
|
|
18
19
|
import { pushReviewResults, setReviewPushContext } from "./reviews/push.js";
|
|
19
20
|
import { setWorkflowPushContext } from "./workflow/push.js";
|
|
@@ -319,6 +320,10 @@ export async function register(server: McpServer): Promise<void> {
|
|
|
319
320
|
}
|
|
320
321
|
|
|
321
322
|
registerEnterpriseTools(server, collector, auditWriter, config, contextDir, policyStore, version);
|
|
323
|
+
// Cortex Harness MCP tools (cortex.workflow.*) — only registered for
|
|
324
|
+
// enterprise projects, since they depend on org-authored workflows
|
|
325
|
+
// synced from cortex-web (also enterprise-only).
|
|
326
|
+
registerHarnessTools(server);
|
|
322
327
|
|
|
323
328
|
// v2.0.0: globalThis.__cortexContextToolHook bridge removed.
|
|
324
329
|
// Enterprise is now in-process with cortex-mcp; tool events flow via
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import {
|
|
3
|
+
WorkflowAdvanceInput,
|
|
4
|
+
WorkflowEnvelopeInput,
|
|
5
|
+
WorkflowStartInput,
|
|
6
|
+
WorkflowStatusInput,
|
|
7
|
+
resolveProjectRoot,
|
|
8
|
+
runWorkflowAdvance,
|
|
9
|
+
runWorkflowEnvelope,
|
|
10
|
+
runWorkflowStart,
|
|
11
|
+
runWorkflowStatus,
|
|
12
|
+
} from "../../core/workflow/mcp-tools.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Registers the cortex.workflow.* tools that drive the Cortex Harness.
|
|
16
|
+
* These are an enterprise-only feature: they're only registered when
|
|
17
|
+
* the enterprise plugin successfully loads (license + config valid).
|
|
18
|
+
*
|
|
19
|
+
* Community-mode MCP servers do not see these tools at all — the
|
|
20
|
+
* harness depends on org-authored workflows from cortex-web, which
|
|
21
|
+
* itself requires an enterprise plan.
|
|
22
|
+
*
|
|
23
|
+
* Pure runner functions live in core/workflow/mcp-tools.ts so they can
|
|
24
|
+
* be unit-tested without spinning up an MCP server. This module only
|
|
25
|
+
* wires them onto the server with the right tool names + input schemas.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
type ToolPayload = Record<string, unknown>;
|
|
29
|
+
|
|
30
|
+
export function registerHarnessTools(server: McpServer): void {
|
|
31
|
+
server.registerTool(
|
|
32
|
+
"cortex.workflow.start",
|
|
33
|
+
{
|
|
34
|
+
description:
|
|
35
|
+
"Start a Cortex Harness workflow run for a task. Creates .agents/<task_id>/state.json and returns the first stage's envelope (the prompt the agent should answer). Enterprise-only.",
|
|
36
|
+
inputSchema: WorkflowStartInput,
|
|
37
|
+
},
|
|
38
|
+
async (input) => buildResult(
|
|
39
|
+
runWorkflowStart(WorkflowStartInput.parse(input ?? {}), {
|
|
40
|
+
cwd: resolveProjectRoot(),
|
|
41
|
+
}) as ToolPayload,
|
|
42
|
+
),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
server.registerTool(
|
|
46
|
+
"cortex.workflow.advance",
|
|
47
|
+
{
|
|
48
|
+
description:
|
|
49
|
+
"Complete the current stage of a workflow run by writing its artifact and advancing the run pointer. Returns the new run state plus the next stage's envelope (or null when the run is finished, blocked, or failed). Enterprise-only.",
|
|
50
|
+
inputSchema: WorkflowAdvanceInput,
|
|
51
|
+
},
|
|
52
|
+
async (input) => buildResult(
|
|
53
|
+
runWorkflowAdvance(WorkflowAdvanceInput.parse(input ?? {}), {
|
|
54
|
+
cwd: resolveProjectRoot(),
|
|
55
|
+
}) as ToolPayload,
|
|
56
|
+
),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
server.registerTool(
|
|
60
|
+
"cortex.workflow.status",
|
|
61
|
+
{
|
|
62
|
+
description:
|
|
63
|
+
"Read the current run state for a task (current stage, completed stages, outcome). Returns null state when no run exists for the given task_id. Enterprise-only.",
|
|
64
|
+
inputSchema: WorkflowStatusInput,
|
|
65
|
+
},
|
|
66
|
+
async (input) => buildResult(
|
|
67
|
+
runWorkflowStatus(WorkflowStatusInput.parse(input ?? {}), {
|
|
68
|
+
cwd: resolveProjectRoot(),
|
|
69
|
+
}) as ToolPayload,
|
|
70
|
+
),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
server.registerTool(
|
|
74
|
+
"cortex.workflow.envelope",
|
|
75
|
+
{
|
|
76
|
+
description:
|
|
77
|
+
"Compose the prompt envelope for a workflow stage without advancing the run. Defaults to the run's current_stage; pass `stage` to dry-run a different stage. Enterprise-only.",
|
|
78
|
+
inputSchema: WorkflowEnvelopeInput,
|
|
79
|
+
},
|
|
80
|
+
async (input) => buildResult(
|
|
81
|
+
runWorkflowEnvelope(WorkflowEnvelopeInput.parse(input ?? {}), {
|
|
82
|
+
cwd: resolveProjectRoot(),
|
|
83
|
+
}) as ToolPayload,
|
|
84
|
+
),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function buildResult(data: ToolPayload) {
|
|
89
|
+
return {
|
|
90
|
+
content: [
|
|
91
|
+
{
|
|
92
|
+
type: "text" as const,
|
|
93
|
+
text: JSON.stringify(data, null, 2),
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
structuredContent: data,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -11,17 +11,6 @@ import {
|
|
|
11
11
|
getSessionEventHook,
|
|
12
12
|
loadPlugins,
|
|
13
13
|
} from "./plugin.js";
|
|
14
|
-
import {
|
|
15
|
-
WorkflowStartInput,
|
|
16
|
-
WorkflowAdvanceInput,
|
|
17
|
-
WorkflowStatusInput,
|
|
18
|
-
WorkflowEnvelopeInput,
|
|
19
|
-
resolveProjectRoot,
|
|
20
|
-
runWorkflowAdvance,
|
|
21
|
-
runWorkflowEnvelope,
|
|
22
|
-
runWorkflowStart,
|
|
23
|
-
runWorkflowStatus,
|
|
24
|
-
} from "./core/workflow/mcp-tools.js";
|
|
25
14
|
|
|
26
15
|
type ToolPayload = Record<string, unknown>;
|
|
27
16
|
|
|
@@ -334,69 +323,10 @@ function registerTools(server: McpServer): void {
|
|
|
334
323
|
})
|
|
335
324
|
);
|
|
336
325
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
"Start a Cortex Harness workflow run for a task. Creates .agents/<task_id>/state.json and returns the first stage's envelope (the prompt the agent should answer).",
|
|
342
|
-
inputSchema: WorkflowStartInput,
|
|
343
|
-
},
|
|
344
|
-
async (input) => executeInstrumentedTool(
|
|
345
|
-
"cortex.workflow.start",
|
|
346
|
-
input,
|
|
347
|
-
async () => runWorkflowStart(WorkflowStartInput.parse(input ?? {}), {
|
|
348
|
-
cwd: resolveProjectRoot(),
|
|
349
|
-
}) as ToolPayload,
|
|
350
|
-
),
|
|
351
|
-
);
|
|
352
|
-
|
|
353
|
-
server.registerTool(
|
|
354
|
-
"cortex.workflow.advance",
|
|
355
|
-
{
|
|
356
|
-
description:
|
|
357
|
-
"Complete the current stage of a workflow run by writing its artifact and advancing the run pointer. Returns the new run state plus the next stage's envelope (or null when the run is finished, blocked, or failed).",
|
|
358
|
-
inputSchema: WorkflowAdvanceInput,
|
|
359
|
-
},
|
|
360
|
-
async (input) => executeInstrumentedTool(
|
|
361
|
-
"cortex.workflow.advance",
|
|
362
|
-
input,
|
|
363
|
-
async () => runWorkflowAdvance(WorkflowAdvanceInput.parse(input ?? {}), {
|
|
364
|
-
cwd: resolveProjectRoot(),
|
|
365
|
-
}) as ToolPayload,
|
|
366
|
-
),
|
|
367
|
-
);
|
|
368
|
-
|
|
369
|
-
server.registerTool(
|
|
370
|
-
"cortex.workflow.status",
|
|
371
|
-
{
|
|
372
|
-
description:
|
|
373
|
-
"Read the current run state for a task (current stage, completed stages, outcome). Returns null state when no run exists for the given task_id.",
|
|
374
|
-
inputSchema: WorkflowStatusInput,
|
|
375
|
-
},
|
|
376
|
-
async (input) => executeInstrumentedTool(
|
|
377
|
-
"cortex.workflow.status",
|
|
378
|
-
input,
|
|
379
|
-
async () => runWorkflowStatus(WorkflowStatusInput.parse(input ?? {}), {
|
|
380
|
-
cwd: resolveProjectRoot(),
|
|
381
|
-
}) as ToolPayload,
|
|
382
|
-
),
|
|
383
|
-
);
|
|
384
|
-
|
|
385
|
-
server.registerTool(
|
|
386
|
-
"cortex.workflow.envelope",
|
|
387
|
-
{
|
|
388
|
-
description:
|
|
389
|
-
"Compose the prompt envelope for a workflow stage without advancing the run. Defaults to the run's current_stage; pass `stage` to dry-run a different stage.",
|
|
390
|
-
inputSchema: WorkflowEnvelopeInput,
|
|
391
|
-
},
|
|
392
|
-
async (input) => executeInstrumentedTool(
|
|
393
|
-
"cortex.workflow.envelope",
|
|
394
|
-
input,
|
|
395
|
-
async () => runWorkflowEnvelope(WorkflowEnvelopeInput.parse(input ?? {}), {
|
|
396
|
-
cwd: resolveProjectRoot(),
|
|
397
|
-
}) as ToolPayload,
|
|
398
|
-
),
|
|
399
|
-
);
|
|
326
|
+
// Note: cortex.workflow.* tools (the Cortex Harness) are enterprise-only
|
|
327
|
+
// and registered by enterprise/index.ts::register() once the license has
|
|
328
|
+
// verified. They intentionally do not appear here so community-mode MCP
|
|
329
|
+
// servers do not surface them at all.
|
|
400
330
|
}
|
|
401
331
|
|
|
402
332
|
let shutdownCalled = false;
|
|
@@ -9,6 +9,15 @@ import { runStageCommand } from "../dist/cli/stage.js";
|
|
|
9
9
|
function makeWorkspace() {
|
|
10
10
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-stage-cli-"));
|
|
11
11
|
process.env.CORTEX_PROJECT_ROOT = dir;
|
|
12
|
+
// cortex stage is enterprise-only; satisfy the gate by writing a
|
|
13
|
+
// minimal enterprise.yml. isEnterpriseProject only requires a
|
|
14
|
+
// non-empty enterprise.api_key field.
|
|
15
|
+
fs.mkdirSync(path.join(dir, ".context"), { recursive: true });
|
|
16
|
+
fs.writeFileSync(
|
|
17
|
+
path.join(dir, ".context", "enterprise.yml"),
|
|
18
|
+
"enterprise:\n api_key: test-key-for-cli-tests\n",
|
|
19
|
+
"utf8",
|
|
20
|
+
);
|
|
12
21
|
return dir;
|
|
13
22
|
}
|
|
14
23
|
|
|
@@ -289,5 +298,37 @@ test("stage help: prints help text and returns without throwing", async () => {
|
|
|
289
298
|
});
|
|
290
299
|
|
|
291
300
|
test("stage <unknown>: throws with help text", async () => {
|
|
301
|
+
makeWorkspace();
|
|
292
302
|
await assert.rejects(runStageCommand(["frobnicate"]), /Unknown stage subcommand/);
|
|
293
303
|
});
|
|
304
|
+
|
|
305
|
+
test("stage start: blocked in community mode (no enterprise.yml)", async () => {
|
|
306
|
+
// Bypass the helper that auto-writes enterprise.yml.
|
|
307
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-stage-cli-community-"));
|
|
308
|
+
process.env.CORTEX_PROJECT_ROOT = dir;
|
|
309
|
+
try {
|
|
310
|
+
await assert.rejects(
|
|
311
|
+
runStageCommand([
|
|
312
|
+
"start",
|
|
313
|
+
"--task-id",
|
|
314
|
+
"task-1",
|
|
315
|
+
"--description",
|
|
316
|
+
"x",
|
|
317
|
+
]),
|
|
318
|
+
/Cortex Harness — an enterprise-only feature/,
|
|
319
|
+
);
|
|
320
|
+
} finally {
|
|
321
|
+
delete process.env.CORTEX_PROJECT_ROOT;
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("stage help: still prints in community mode (so users discover the feature)", async () => {
|
|
326
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-stage-cli-community-help-"));
|
|
327
|
+
process.env.CORTEX_PROJECT_ROOT = dir;
|
|
328
|
+
try {
|
|
329
|
+
const { captured } = await captureStdout(() => runStageCommand(["help"]));
|
|
330
|
+
assert.match(captured, /Usage:/);
|
|
331
|
+
} finally {
|
|
332
|
+
delete process.env.CORTEX_PROJECT_ROOT;
|
|
333
|
+
}
|
|
334
|
+
});
|
|
@@ -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
|
+
});
|