@danielblomma/cortex-mcp 2.4.0 → 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.
- package/CHANGELOG.md +201 -0
- package/README.md +19 -3
- package/bin/cortex.mjs +308 -75
- package/bin/daemon-control.mjs +162 -0
- package/package.json +7 -3
- package/scaffold/mcp/dist/.cortex-build-hash +1 -0
- package/scaffold/mcp/dist/cli/enterprise-setup.js +134 -0
- package/scaffold/mcp/dist/cli/govern.js +937 -0
- package/scaffold/mcp/dist/cli/query.js +409 -0
- package/scaffold/mcp/dist/cli/run.js +295 -0
- package/scaffold/mcp/dist/cli/stage.js +308 -0
- package/scaffold/mcp/dist/cli/telemetry-test.js +141 -0
- package/scaffold/mcp/dist/cli/ungoverned-detector.js +133 -0
- package/scaffold/mcp/dist/contextEntities.js +282 -0
- package/scaffold/mcp/dist/core/audit/query.js +72 -0
- package/scaffold/mcp/dist/core/audit/writer.js +28 -0
- package/scaffold/mcp/dist/core/config.js +235 -0
- package/scaffold/mcp/dist/core/enterprise-host-identity.js +193 -0
- package/scaffold/mcp/dist/core/enterprise-identity.js +18 -0
- package/scaffold/mcp/dist/core/enterprise-rotation.js +158 -0
- package/scaffold/mcp/dist/core/govern-paths.js +21 -0
- package/scaffold/mcp/dist/core/index.js +14 -0
- package/scaffold/mcp/dist/core/license.js +278 -0
- package/scaffold/mcp/dist/core/policy/enforce.js +66 -0
- package/scaffold/mcp/dist/core/policy/injection.js +172 -0
- package/scaffold/mcp/dist/core/policy/store.js +179 -0
- package/scaffold/mcp/dist/core/rbac/check.js +30 -0
- package/scaffold/mcp/dist/core/secure-endpoint.js +25 -0
- package/scaffold/mcp/dist/core/telemetry/collector.js +285 -0
- package/scaffold/mcp/dist/core/telemetry/state-dir.js +31 -0
- package/scaffold/mcp/dist/core/validators/builtins.js +632 -0
- package/scaffold/mcp/dist/core/validators/config.js +44 -0
- package/scaffold/mcp/dist/core/validators/engine.js +120 -0
- package/scaffold/mcp/dist/core/validators/evaluators/code_comments.js +236 -0
- package/scaffold/mcp/dist/core/validators/evaluators/regex.js +116 -0
- package/scaffold/mcp/dist/core/workflow/artifact-io.js +103 -0
- package/scaffold/mcp/dist/core/workflow/capabilities.js +88 -0
- package/scaffold/mcp/dist/core/workflow/default-workflows.js +102 -0
- package/scaffold/mcp/dist/core/workflow/enforcement.js +164 -0
- package/scaffold/mcp/dist/core/workflow/envelope.js +132 -0
- package/scaffold/mcp/dist/core/workflow/index.js +11 -0
- package/scaffold/mcp/dist/core/workflow/mcp-tools.js +175 -0
- package/scaffold/mcp/dist/core/workflow/resolution.js +69 -0
- package/scaffold/mcp/dist/core/workflow/run-lifecycle.js +123 -0
- package/scaffold/mcp/dist/core/workflow/schemas.js +141 -0
- package/scaffold/mcp/dist/core/workflow/synced-capability-registry.js +60 -0
- package/scaffold/mcp/dist/core/workflow/synced-registry.js +60 -0
- package/scaffold/mcp/dist/daemon/capability-sync-checker.js +242 -0
- package/scaffold/mcp/dist/daemon/client.js +130 -0
- package/scaffold/mcp/dist/daemon/egress-proxy.js +288 -0
- package/scaffold/mcp/dist/daemon/global-host-events.js +222 -0
- package/scaffold/mcp/dist/daemon/heartbeat-pusher.js +116 -0
- package/scaffold/mcp/dist/daemon/heartbeat-tracker.js +165 -0
- package/scaffold/mcp/dist/daemon/host-events-pusher.js +249 -0
- package/scaffold/mcp/dist/daemon/main.js +449 -0
- package/scaffold/mcp/dist/daemon/paths.js +36 -0
- package/scaffold/mcp/dist/daemon/project-service-registry.js +78 -0
- package/scaffold/mcp/dist/daemon/protocol.js +8 -0
- package/scaffold/mcp/dist/daemon/server.js +180 -0
- package/scaffold/mcp/dist/daemon/skill-sync-checker.js +557 -0
- package/scaffold/mcp/dist/daemon/sync-checker.js +165 -0
- package/scaffold/mcp/dist/daemon/ungoverned-scanner.js +136 -0
- package/scaffold/mcp/dist/daemon/workflow-sync-checker.js +249 -0
- package/scaffold/mcp/dist/defaults.js +6 -0
- package/scaffold/mcp/dist/embed.js +627 -0
- package/scaffold/mcp/dist/embedScheduler.js +479 -0
- package/scaffold/mcp/dist/embeddings.js +167 -0
- package/scaffold/mcp/dist/enterprise/audit/push.js +66 -0
- package/scaffold/mcp/dist/enterprise/index.js +327 -0
- package/scaffold/mcp/dist/enterprise/model/deploy.js +27 -0
- package/scaffold/mcp/dist/enterprise/policy/sync.js +129 -0
- package/scaffold/mcp/dist/enterprise/privacy/boundary.js +184 -0
- package/scaffold/mcp/dist/enterprise/reviews/changed-files.js +33 -0
- package/scaffold/mcp/dist/enterprise/reviews/pattern-context.js +202 -0
- package/scaffold/mcp/dist/enterprise/reviews/policy-selection.js +46 -0
- package/scaffold/mcp/dist/enterprise/reviews/push.js +102 -0
- package/scaffold/mcp/dist/enterprise/reviews/trust-state.js +186 -0
- package/scaffold/mcp/dist/enterprise/telemetry/sync.js +57 -0
- package/scaffold/mcp/dist/enterprise/tools/enterprise.js +826 -0
- package/scaffold/mcp/dist/enterprise/tools/harness.js +40 -0
- package/scaffold/mcp/dist/enterprise/tools/walk.js +73 -0
- package/scaffold/mcp/dist/enterprise/violations/push.js +77 -0
- package/scaffold/mcp/dist/enterprise/workflow/push.js +44 -0
- package/scaffold/mcp/dist/enterprise/workflow/state.js +329 -0
- package/scaffold/mcp/dist/frontmatter.js +33 -0
- package/scaffold/mcp/dist/graph.js +769 -0
- package/scaffold/mcp/dist/graphCsv.js +55 -0
- package/scaffold/mcp/dist/graphMetrics.js +8 -0
- package/scaffold/mcp/dist/hooks/permission-request.js +89 -0
- package/scaffold/mcp/dist/hooks/post-tool-use.js +105 -0
- package/scaffold/mcp/dist/hooks/pre-compact.js +29 -0
- package/scaffold/mcp/dist/hooks/pre-tool-use.js +78 -0
- package/scaffold/mcp/dist/hooks/session-end.js +43 -0
- package/scaffold/mcp/dist/hooks/session-start.js +41 -0
- package/scaffold/mcp/dist/hooks/shared.js +194 -0
- package/scaffold/mcp/dist/hooks/stop.js +28 -0
- package/scaffold/mcp/dist/hooks/user-prompt-submit.js +33 -0
- package/scaffold/mcp/dist/impactPresentation.js +137 -0
- package/scaffold/mcp/dist/impactRanking.js +191 -0
- package/scaffold/mcp/dist/impactResponse.js +30 -0
- package/scaffold/mcp/dist/impactResults.js +105 -0
- package/scaffold/mcp/dist/impactSeed.js +20 -0
- package/scaffold/mcp/dist/impactTraversal.js +64 -0
- package/scaffold/mcp/dist/jsonl.js +77 -0
- package/scaffold/mcp/dist/loadGraph.js +759 -0
- package/scaffold/mcp/dist/lruCache.js +38 -0
- package/scaffold/mcp/dist/paths.js +97 -0
- package/scaffold/mcp/dist/patternEvidence.js +272 -0
- package/scaffold/mcp/dist/plugin.js +81 -0
- package/scaffold/mcp/dist/presets.js +78 -0
- package/scaffold/mcp/dist/relatedResponse.js +18 -0
- package/scaffold/mcp/dist/relatedTraversal.js +78 -0
- package/scaffold/mcp/dist/rules.js +23 -0
- package/scaffold/mcp/dist/search.js +212 -0
- package/scaffold/mcp/dist/searchCore.js +457 -0
- package/scaffold/mcp/dist/searchResults.js +230 -0
- package/scaffold/mcp/dist/server.js +317 -0
- package/scaffold/mcp/dist/types.js +1 -0
- package/scaffold/mcp/package-lock.json +321 -200
- package/scaffold/mcp/package.json +15 -6
- package/scaffold/mcp/src/cli/enterprise-setup.ts +82 -8
- package/scaffold/mcp/src/cli/govern.ts +137 -52
- package/scaffold/mcp/src/cli/run.ts +53 -19
- package/scaffold/mcp/src/cli/stage.ts +8 -3
- package/scaffold/mcp/src/core/config.ts +9 -2
- package/scaffold/mcp/src/core/enterprise-host-identity.ts +259 -0
- package/scaffold/mcp/src/core/enterprise-identity.ts +20 -0
- package/scaffold/mcp/src/core/enterprise-rotation.ts +185 -0
- package/scaffold/mcp/src/core/govern-paths.ts +30 -0
- package/scaffold/mcp/src/core/license.ts +186 -17
- package/scaffold/mcp/src/core/secure-endpoint.ts +23 -0
- package/scaffold/mcp/src/core/workflow/enforcement.ts +8 -1
- package/scaffold/mcp/src/core/workflow/mcp-tools.ts +3 -0
- package/scaffold/mcp/src/core/workflow/resolution.ts +9 -1
- package/scaffold/mcp/src/core/workflow/synced-capability-registry.ts +15 -0
- package/scaffold/mcp/src/core/workflow/synced-registry.ts +15 -0
- package/scaffold/mcp/src/daemon/capability-sync-checker.ts +38 -2
- package/scaffold/mcp/src/daemon/egress-proxy.ts +14 -8
- package/scaffold/mcp/src/daemon/global-host-events.ts +288 -0
- package/scaffold/mcp/src/daemon/heartbeat-pusher.ts +4 -0
- package/scaffold/mcp/src/daemon/heartbeat-tracker.ts +2 -0
- package/scaffold/mcp/src/daemon/host-events-pusher.ts +5 -0
- package/scaffold/mcp/src/daemon/main.ts +99 -25
- package/scaffold/mcp/src/daemon/project-service-registry.ts +107 -0
- package/scaffold/mcp/src/daemon/skill-sync-checker.ts +368 -31
- package/scaffold/mcp/src/daemon/sync-checker.ts +4 -0
- package/scaffold/mcp/src/daemon/ungoverned-scanner.ts +57 -25
- package/scaffold/mcp/src/daemon/workflow-sync-checker.ts +41 -2
- package/scaffold/mcp/src/enterprise/audit/push.ts +8 -0
- package/scaffold/mcp/src/enterprise/policy/sync.ts +12 -0
- package/scaffold/mcp/src/enterprise/reviews/push.ts +9 -0
- package/scaffold/mcp/src/enterprise/reviews/trust-state.ts +3 -1
- package/scaffold/mcp/src/enterprise/telemetry/sync.ts +9 -0
- package/scaffold/mcp/src/enterprise/violations/push.ts +9 -0
- package/scaffold/mcp/src/enterprise/workflow/push.ts +7 -0
- package/scaffold/mcp/src/plugin.ts +20 -0
- package/scaffold/mcp/tests/copilot-shim.test.mjs +19 -1
- package/scaffold/mcp/tests/egress-proxy.test.mjs +37 -0
- package/scaffold/mcp/tests/enterprise-identity-sync.test.mjs +401 -0
- package/scaffold/mcp/tests/enterprise-setup.test.mjs +189 -0
- package/scaffold/mcp/tests/global-host-events.test.mjs +81 -0
- package/scaffold/mcp/tests/govern-install.test.mjs +95 -2
- package/scaffold/mcp/tests/govern-repair.test.mjs +20 -3
- package/scaffold/mcp/tests/heartbeat-tracker.test.mjs +26 -0
- package/scaffold/mcp/tests/license.test.mjs +367 -0
- package/scaffold/mcp/tests/project-service-registry.test.mjs +172 -0
- package/scaffold/mcp/tests/review-trust-contract.test.mjs +18 -0
- package/scaffold/mcp/tests/secure-enterprise-endpoint.test.mjs +46 -0
- package/scaffold/mcp/tests/skill-sync-checker.test.mjs +446 -2
- package/scaffold/mcp/tests/ungoverned-scanner.test.mjs +104 -22
- package/scaffold/mcp/tests/workflow-cli.test.mjs +14 -1
- package/scaffold/mcp/tests/workflow-synced-capabilities.test.mjs +37 -0
- package/scaffold/mcp/tests/workflow-synced-registry.test.mjs +40 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { isEnforcedMode } from "../../hooks/shared.js";
|
|
2
|
+
import { WorkflowAdvanceInput, WorkflowEnvelopeInput, WorkflowStartInput, WorkflowStatusInput, resolveProjectRoot, runWorkflowAdvance, runWorkflowEnvelope, runWorkflowStart, runWorkflowStatus, } from "../../core/workflow/mcp-tools.js";
|
|
3
|
+
export function registerHarnessTools(server) {
|
|
4
|
+
server.registerTool("cortex.workflow.start", {
|
|
5
|
+
description: "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.",
|
|
6
|
+
inputSchema: WorkflowStartInput,
|
|
7
|
+
}, async (input) => buildResult(runWorkflowStart(WorkflowStartInput.parse(input ?? {}), {
|
|
8
|
+
cwd: resolveProjectRoot(),
|
|
9
|
+
bundledFallbackPolicy: isEnforcedMode(resolveProjectRoot()) ? "block" : "allow",
|
|
10
|
+
})));
|
|
11
|
+
server.registerTool("cortex.workflow.advance", {
|
|
12
|
+
description: "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.",
|
|
13
|
+
inputSchema: WorkflowAdvanceInput,
|
|
14
|
+
}, async (input) => buildResult(runWorkflowAdvance(WorkflowAdvanceInput.parse(input ?? {}), {
|
|
15
|
+
cwd: resolveProjectRoot(),
|
|
16
|
+
})));
|
|
17
|
+
server.registerTool("cortex.workflow.status", {
|
|
18
|
+
description: "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.",
|
|
19
|
+
inputSchema: WorkflowStatusInput,
|
|
20
|
+
}, async (input) => buildResult(runWorkflowStatus(WorkflowStatusInput.parse(input ?? {}), {
|
|
21
|
+
cwd: resolveProjectRoot(),
|
|
22
|
+
})));
|
|
23
|
+
server.registerTool("cortex.workflow.envelope", {
|
|
24
|
+
description: "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.",
|
|
25
|
+
inputSchema: WorkflowEnvelopeInput,
|
|
26
|
+
}, async (input) => buildResult(runWorkflowEnvelope(WorkflowEnvelopeInput.parse(input ?? {}), {
|
|
27
|
+
cwd: resolveProjectRoot(),
|
|
28
|
+
})));
|
|
29
|
+
}
|
|
30
|
+
function buildResult(data) {
|
|
31
|
+
return {
|
|
32
|
+
content: [
|
|
33
|
+
{
|
|
34
|
+
type: "text",
|
|
35
|
+
text: JSON.stringify(data, null, 2),
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
structuredContent: data,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { join, relative, sep } from "node:path";
|
|
3
|
+
// Directories skipped while walking the project for a scope=all review.
|
|
4
|
+
// Common build/vendor conventions across JS/Python/Go/Rust/C#/Java
|
|
5
|
+
// ecosystems; keep permissive enough that individual validators still do
|
|
6
|
+
// their own per-language filtering.
|
|
7
|
+
const EXCLUDED_DIRS = new Set([
|
|
8
|
+
"node_modules",
|
|
9
|
+
".git",
|
|
10
|
+
"dist",
|
|
11
|
+
"build",
|
|
12
|
+
".next",
|
|
13
|
+
"target",
|
|
14
|
+
"bin",
|
|
15
|
+
"obj",
|
|
16
|
+
".venv",
|
|
17
|
+
"venv",
|
|
18
|
+
"__pycache__",
|
|
19
|
+
".nuxt",
|
|
20
|
+
"vendor",
|
|
21
|
+
".terraform",
|
|
22
|
+
"coverage",
|
|
23
|
+
".cache",
|
|
24
|
+
".turbo",
|
|
25
|
+
]);
|
|
26
|
+
const TEXT_EXT_RE = /\.(?:ts|tsx|js|jsx|mjs|cjs|json|ya?ml|toml|ini|env|config|xml|properties|tf|tfvars|sh|ps1|py|cs|vb|java|go|rs|rb|php|sql|md|txt|html?|css|scss|less)$/i;
|
|
27
|
+
// Extensionless text files the secrets validator cares about: dotenv
|
|
28
|
+
// variants and .NET appsettings.*
|
|
29
|
+
const PATH_EXTRA_RE = /(?:^|\/)\.env(?:\.|$)|(?:^|\/)appsettings(?:\.|$)/i;
|
|
30
|
+
const DEFAULT_MAX_FILES = 10_000;
|
|
31
|
+
// Recursively list project-relative text file paths under `root`. Used as
|
|
32
|
+
// the file set for scope=all reviews and as a fallback when scope=changed
|
|
33
|
+
// cannot resolve a git diff (non-git working copy, git missing, etc.).
|
|
34
|
+
export function walkProjectFiles(root, options = {}) {
|
|
35
|
+
const maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
|
|
36
|
+
const results = [];
|
|
37
|
+
function walk(dir) {
|
|
38
|
+
if (results.length >= maxFiles)
|
|
39
|
+
return;
|
|
40
|
+
let entries;
|
|
41
|
+
try {
|
|
42
|
+
entries = readdirSync(dir);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
for (const name of entries) {
|
|
48
|
+
if (results.length >= maxFiles)
|
|
49
|
+
return;
|
|
50
|
+
if (EXCLUDED_DIRS.has(name))
|
|
51
|
+
continue;
|
|
52
|
+
const abs = join(dir, name);
|
|
53
|
+
let st;
|
|
54
|
+
try {
|
|
55
|
+
st = statSync(abs);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (st.isDirectory()) {
|
|
61
|
+
walk(abs);
|
|
62
|
+
}
|
|
63
|
+
else if (st.isFile()) {
|
|
64
|
+
const rel = relative(root, abs).split(sep).join("/");
|
|
65
|
+
if (TEXT_EXT_RE.test(rel) || PATH_EXTRA_RE.test(rel)) {
|
|
66
|
+
results.push(rel);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
walk(root);
|
|
72
|
+
return results;
|
|
73
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Push policy violations to the Cortex Cloud API.
|
|
3
|
+
*
|
|
4
|
+
* Uses the same endpoint/api_key as policy sync since
|
|
5
|
+
* violations require the "policy" scope.
|
|
6
|
+
*/
|
|
7
|
+
import { isAllowedEnterpriseEndpoint } from "../../core/secure-endpoint.js";
|
|
8
|
+
const pending = [];
|
|
9
|
+
let activeContext = {};
|
|
10
|
+
export function setViolationPushContext(context) {
|
|
11
|
+
activeContext = { ...context };
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Queue a violation for the next push.
|
|
15
|
+
*/
|
|
16
|
+
export function queueViolation(item) {
|
|
17
|
+
pending.push(item);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Push all queued violations to cortex-web.
|
|
21
|
+
* Fire-and-forget — errors are logged but do not throw.
|
|
22
|
+
*/
|
|
23
|
+
export async function pushViolations(baseUrl, apiKey) {
|
|
24
|
+
if (pending.length === 0) {
|
|
25
|
+
return { success: true, count: 0 };
|
|
26
|
+
}
|
|
27
|
+
if (!baseUrl || !apiKey) {
|
|
28
|
+
return { success: false, count: 0, error: "endpoint or api_key not configured" };
|
|
29
|
+
}
|
|
30
|
+
if (!isAllowedEnterpriseEndpoint(baseUrl)) {
|
|
31
|
+
return {
|
|
32
|
+
success: false,
|
|
33
|
+
count: 0,
|
|
34
|
+
error: "insecure or invalid enterprise endpoint",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const violationsUrl = `${baseUrl.replace(/\/$/, "")}/api/v1/violations/push`;
|
|
38
|
+
const batch = pending.splice(0, 100); // max 100 per push
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetch(violationsUrl, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: {
|
|
43
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
44
|
+
"Content-Type": "application/json",
|
|
45
|
+
"Accept": "application/json",
|
|
46
|
+
},
|
|
47
|
+
body: JSON.stringify({
|
|
48
|
+
repo: activeContext.repo,
|
|
49
|
+
instance_id: activeContext.instance_id,
|
|
50
|
+
session_id: activeContext.session_id,
|
|
51
|
+
violations: batch,
|
|
52
|
+
}),
|
|
53
|
+
signal: AbortSignal.timeout(10_000),
|
|
54
|
+
});
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
// Put them back so they can be retried
|
|
57
|
+
pending.unshift(...batch);
|
|
58
|
+
return { success: false, count: 0, error: `HTTP ${response.status}` };
|
|
59
|
+
}
|
|
60
|
+
return { success: true, count: batch.length };
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
// Put them back for retry
|
|
64
|
+
pending.unshift(...batch);
|
|
65
|
+
return {
|
|
66
|
+
success: false,
|
|
67
|
+
count: 0,
|
|
68
|
+
error: err instanceof Error ? err.message : "unknown error",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Return the current queue length (for diagnostics).
|
|
74
|
+
*/
|
|
75
|
+
export function pendingCount() {
|
|
76
|
+
return pending.length;
|
|
77
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { isAllowedEnterpriseEndpoint } from "../../core/secure-endpoint.js";
|
|
2
|
+
let activeContext = {};
|
|
3
|
+
export function setWorkflowPushContext(context) {
|
|
4
|
+
activeContext = { ...context };
|
|
5
|
+
}
|
|
6
|
+
export async function pushWorkflowSnapshot(baseUrl, apiKey, workflow) {
|
|
7
|
+
if (!baseUrl || !apiKey) {
|
|
8
|
+
return { success: false, error: "endpoint or api_key not configured" };
|
|
9
|
+
}
|
|
10
|
+
if (!isAllowedEnterpriseEndpoint(baseUrl)) {
|
|
11
|
+
return {
|
|
12
|
+
success: false,
|
|
13
|
+
error: "insecure or invalid enterprise endpoint",
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
const workflowUrl = `${baseUrl.replace(/\/$/, "")}/api/v1/workflow/push`;
|
|
17
|
+
try {
|
|
18
|
+
const response = await fetch(workflowUrl, {
|
|
19
|
+
method: "POST",
|
|
20
|
+
headers: {
|
|
21
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
22
|
+
"Content-Type": "application/json",
|
|
23
|
+
"Accept": "application/json",
|
|
24
|
+
},
|
|
25
|
+
body: JSON.stringify({
|
|
26
|
+
repo: activeContext.repo,
|
|
27
|
+
instance_id: activeContext.instance_id,
|
|
28
|
+
session_id: activeContext.session_id,
|
|
29
|
+
workflow,
|
|
30
|
+
}),
|
|
31
|
+
signal: AbortSignal.timeout(10_000),
|
|
32
|
+
});
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
return { success: false, status: response.status, error: `HTTP ${response.status}` };
|
|
35
|
+
}
|
|
36
|
+
return { success: true, status: response.status };
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
return {
|
|
40
|
+
success: false,
|
|
41
|
+
error: err instanceof Error ? err.message : "unknown error",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
function nowIso() {
|
|
4
|
+
return new Date().toISOString();
|
|
5
|
+
}
|
|
6
|
+
function workflowDir(contextDir) {
|
|
7
|
+
return join(contextDir, "workflow");
|
|
8
|
+
}
|
|
9
|
+
function workflowStatePath(contextDir) {
|
|
10
|
+
return join(workflowDir(contextDir), "state.json");
|
|
11
|
+
}
|
|
12
|
+
export function hasWorkflowState(contextDir) {
|
|
13
|
+
return existsSync(workflowStatePath(contextDir));
|
|
14
|
+
}
|
|
15
|
+
function workflowReviewsDir(contextDir) {
|
|
16
|
+
return join(workflowDir(contextDir), "reviews");
|
|
17
|
+
}
|
|
18
|
+
function initialState() {
|
|
19
|
+
const now = nowIso();
|
|
20
|
+
return {
|
|
21
|
+
version: 1,
|
|
22
|
+
created_at: now,
|
|
23
|
+
updated_at: now,
|
|
24
|
+
phase: "planning",
|
|
25
|
+
blocked_reasons: [
|
|
26
|
+
{
|
|
27
|
+
code: "plan_missing",
|
|
28
|
+
message: "A plan must be created before implementation can begin.",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
code: "code_review_required",
|
|
32
|
+
message: "A passing code review is required before approval.",
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
plan: {
|
|
36
|
+
title: null,
|
|
37
|
+
summary: null,
|
|
38
|
+
tasks: [],
|
|
39
|
+
status: "missing",
|
|
40
|
+
updated_at: null,
|
|
41
|
+
reviewed_at: null,
|
|
42
|
+
review_notes: null,
|
|
43
|
+
},
|
|
44
|
+
last_review: {
|
|
45
|
+
status: "not_run",
|
|
46
|
+
scope: null,
|
|
47
|
+
reviewed_at: null,
|
|
48
|
+
artifact_path: null,
|
|
49
|
+
summary: null,
|
|
50
|
+
failed_policies: [],
|
|
51
|
+
warning_policies: [],
|
|
52
|
+
reviewed_files: null,
|
|
53
|
+
},
|
|
54
|
+
approval: {
|
|
55
|
+
status: "blocked",
|
|
56
|
+
approved_at: null,
|
|
57
|
+
notes: null,
|
|
58
|
+
},
|
|
59
|
+
next_note_id: 1,
|
|
60
|
+
next_todo_id: 1,
|
|
61
|
+
notes: [],
|
|
62
|
+
todos: [],
|
|
63
|
+
history: [],
|
|
64
|
+
review_trust: null,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function ensureWorkflowDirs(contextDir) {
|
|
68
|
+
mkdirSync(workflowReviewsDir(contextDir), { recursive: true });
|
|
69
|
+
}
|
|
70
|
+
function clampHistory(history) {
|
|
71
|
+
return history.slice(-100);
|
|
72
|
+
}
|
|
73
|
+
function blockersFor(state) {
|
|
74
|
+
const blockers = [];
|
|
75
|
+
if (!state.plan.title || state.plan.status === "missing") {
|
|
76
|
+
blockers.push({
|
|
77
|
+
code: "plan_missing",
|
|
78
|
+
message: "A plan must be created before implementation can begin.",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (state.plan.status !== "approved") {
|
|
82
|
+
blockers.push({
|
|
83
|
+
code: "plan_not_approved",
|
|
84
|
+
message: "The plan must be reviewed and approved before approval.",
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (!state.history.some((entry) => entry.event === "implementation_started")) {
|
|
88
|
+
blockers.push({
|
|
89
|
+
code: "implementation_not_started",
|
|
90
|
+
message: "Implementation has not started yet.",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
if (state.last_review.status === "not_run") {
|
|
94
|
+
blockers.push({
|
|
95
|
+
code: "code_review_required",
|
|
96
|
+
message: "A passing code review is required before approval.",
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (state.last_review.status === "failed") {
|
|
100
|
+
blockers.push({
|
|
101
|
+
code: "review_failed",
|
|
102
|
+
message: "The latest code review failed and must be resolved before approval.",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return blockers;
|
|
106
|
+
}
|
|
107
|
+
function withRecalculatedApproval(state, preserveApproved = false) {
|
|
108
|
+
const blockers = blockersFor(state);
|
|
109
|
+
state.blocked_reasons = blockers;
|
|
110
|
+
if (blockers.length > 0) {
|
|
111
|
+
state.approval = {
|
|
112
|
+
status: "blocked",
|
|
113
|
+
approved_at: null,
|
|
114
|
+
notes: null,
|
|
115
|
+
};
|
|
116
|
+
return state;
|
|
117
|
+
}
|
|
118
|
+
if (preserveApproved && state.approval.status === "approved") {
|
|
119
|
+
state.approval.status = "approved";
|
|
120
|
+
return state;
|
|
121
|
+
}
|
|
122
|
+
state.approval = {
|
|
123
|
+
status: "ready",
|
|
124
|
+
approved_at: null,
|
|
125
|
+
notes: null,
|
|
126
|
+
};
|
|
127
|
+
return state;
|
|
128
|
+
}
|
|
129
|
+
function writeState(contextDir, state, touchUpdatedAt = true) {
|
|
130
|
+
ensureWorkflowDirs(contextDir);
|
|
131
|
+
if (touchUpdatedAt) {
|
|
132
|
+
state.updated_at = nowIso();
|
|
133
|
+
}
|
|
134
|
+
state.history = clampHistory(state.history);
|
|
135
|
+
writeFileSync(workflowStatePath(contextDir), `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
136
|
+
return state;
|
|
137
|
+
}
|
|
138
|
+
export function loadWorkflowState(contextDir) {
|
|
139
|
+
ensureWorkflowDirs(contextDir);
|
|
140
|
+
const statePath = workflowStatePath(contextDir);
|
|
141
|
+
if (!existsSync(statePath)) {
|
|
142
|
+
return writeState(contextDir, initialState());
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const parsed = JSON.parse(readFileSync(statePath, "utf8"));
|
|
146
|
+
return withRecalculatedApproval(parsed, true);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return writeState(contextDir, initialState());
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function addHistory(state, event, details) {
|
|
153
|
+
state.history.push({
|
|
154
|
+
at: nowIso(),
|
|
155
|
+
event,
|
|
156
|
+
details,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
export function setWorkflowPlan(contextDir, input) {
|
|
160
|
+
const state = loadWorkflowState(contextDir);
|
|
161
|
+
state.phase = "plan_review";
|
|
162
|
+
state.plan = {
|
|
163
|
+
title: input.title,
|
|
164
|
+
summary: input.summary,
|
|
165
|
+
tasks: input.tasks?.filter(Boolean) ?? [],
|
|
166
|
+
status: "pending_review",
|
|
167
|
+
updated_at: nowIso(),
|
|
168
|
+
reviewed_at: null,
|
|
169
|
+
review_notes: null,
|
|
170
|
+
};
|
|
171
|
+
state.last_review = {
|
|
172
|
+
status: "not_run",
|
|
173
|
+
scope: null,
|
|
174
|
+
reviewed_at: null,
|
|
175
|
+
artifact_path: null,
|
|
176
|
+
summary: null,
|
|
177
|
+
failed_policies: [],
|
|
178
|
+
warning_policies: [],
|
|
179
|
+
reviewed_files: null,
|
|
180
|
+
};
|
|
181
|
+
addHistory(state, "plan_set", {
|
|
182
|
+
title: input.title,
|
|
183
|
+
task_count: state.plan.tasks.length,
|
|
184
|
+
});
|
|
185
|
+
return writeState(contextDir, withRecalculatedApproval(state));
|
|
186
|
+
}
|
|
187
|
+
export function reviewWorkflowPlan(contextDir, input) {
|
|
188
|
+
const state = loadWorkflowState(contextDir);
|
|
189
|
+
if (!state.plan.title) {
|
|
190
|
+
return { ok: false, state, error: "No plan exists to review" };
|
|
191
|
+
}
|
|
192
|
+
state.plan.status = input.approved ? "approved" : "changes_requested";
|
|
193
|
+
state.plan.reviewed_at = nowIso();
|
|
194
|
+
state.plan.review_notes = input.notes?.trim() || null;
|
|
195
|
+
state.phase = input.approved ? "implementation_pending" : "planning";
|
|
196
|
+
addHistory(state, "plan_reviewed", {
|
|
197
|
+
approved: input.approved,
|
|
198
|
+
});
|
|
199
|
+
return { ok: true, state: writeState(contextDir, withRecalculatedApproval(state)) };
|
|
200
|
+
}
|
|
201
|
+
export function startWorkflowImplementation(contextDir) {
|
|
202
|
+
const state = loadWorkflowState(contextDir);
|
|
203
|
+
if (state.plan.status !== "approved") {
|
|
204
|
+
return {
|
|
205
|
+
ok: false,
|
|
206
|
+
state,
|
|
207
|
+
error: "Plan must be approved before implementation can start",
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
state.phase = "implementation";
|
|
211
|
+
addHistory(state, "implementation_started");
|
|
212
|
+
return { ok: true, state: writeState(contextDir, withRecalculatedApproval(state)) };
|
|
213
|
+
}
|
|
214
|
+
export function recordWorkflowReview(contextDir, input) {
|
|
215
|
+
const state = loadWorkflowState(contextDir);
|
|
216
|
+
const recordedAt = nowIso();
|
|
217
|
+
const blockingFailures = input.output.results.filter((result) => !result.pass && result.severity === "error");
|
|
218
|
+
const warningFailures = input.output.results.filter((result) => !result.pass && result.severity === "warning");
|
|
219
|
+
const hasBlockingFailures = blockingFailures.length > 0;
|
|
220
|
+
const fileName = `review-${recordedAt.replace(/[:.]/g, "-")}.json`;
|
|
221
|
+
const relativeArtifactPath = `.context/workflow/reviews/${fileName}`;
|
|
222
|
+
const artifact = {
|
|
223
|
+
recorded_at: recordedAt,
|
|
224
|
+
scope: input.scope,
|
|
225
|
+
summary: input.output.summary,
|
|
226
|
+
results: input.output.results,
|
|
227
|
+
reviewed_files: input.reviewed_files ?? null,
|
|
228
|
+
};
|
|
229
|
+
ensureWorkflowDirs(contextDir);
|
|
230
|
+
writeFileSync(join(workflowReviewsDir(contextDir), fileName), `${JSON.stringify(artifact, null, 2)}\n`, "utf8");
|
|
231
|
+
state.last_review = {
|
|
232
|
+
status: hasBlockingFailures ? "failed" : "passed",
|
|
233
|
+
scope: input.scope,
|
|
234
|
+
reviewed_at: recordedAt,
|
|
235
|
+
artifact_path: relativeArtifactPath,
|
|
236
|
+
summary: input.output.summary,
|
|
237
|
+
failed_policies: blockingFailures.map((result) => result.policy_id),
|
|
238
|
+
warning_policies: warningFailures.map((result) => result.policy_id),
|
|
239
|
+
reviewed_files: input.reviewed_files ?? null,
|
|
240
|
+
};
|
|
241
|
+
state.phase = hasBlockingFailures ? "iterating" : "reviewed";
|
|
242
|
+
addHistory(state, "review_recorded", {
|
|
243
|
+
scope: input.scope,
|
|
244
|
+
total: input.output.summary.total,
|
|
245
|
+
failed: blockingFailures.length,
|
|
246
|
+
warnings: warningFailures.length,
|
|
247
|
+
});
|
|
248
|
+
return writeState(contextDir, withRecalculatedApproval(state));
|
|
249
|
+
}
|
|
250
|
+
export function recordWorkflowUpdate(contextDir, input) {
|
|
251
|
+
const state = loadWorkflowState(contextDir);
|
|
252
|
+
if (input.phase) {
|
|
253
|
+
state.phase = input.phase;
|
|
254
|
+
}
|
|
255
|
+
addHistory(state, "workflow_updated", {
|
|
256
|
+
summary: input.summary,
|
|
257
|
+
phase: input.phase ?? state.phase,
|
|
258
|
+
});
|
|
259
|
+
return writeState(contextDir, withRecalculatedApproval(state, input.phase === undefined));
|
|
260
|
+
}
|
|
261
|
+
export function setWorkflowReviewTrust(contextDir, reviewTrust) {
|
|
262
|
+
if (!hasWorkflowState(contextDir)) {
|
|
263
|
+
const state = initialState();
|
|
264
|
+
state.review_trust = reviewTrust;
|
|
265
|
+
return writeState(contextDir, withRecalculatedApproval(state, true));
|
|
266
|
+
}
|
|
267
|
+
const state = loadWorkflowState(contextDir);
|
|
268
|
+
state.review_trust = reviewTrust;
|
|
269
|
+
return writeState(contextDir, withRecalculatedApproval(state, true));
|
|
270
|
+
}
|
|
271
|
+
export function addWorkflowNote(contextDir, input) {
|
|
272
|
+
const state = loadWorkflowState(contextDir);
|
|
273
|
+
const note = {
|
|
274
|
+
id: state.next_note_id++,
|
|
275
|
+
title: input.title,
|
|
276
|
+
details: input.details,
|
|
277
|
+
created_at: nowIso(),
|
|
278
|
+
};
|
|
279
|
+
state.notes.push(note);
|
|
280
|
+
addHistory(state, "note_added", { note_id: note.id, title: note.title });
|
|
281
|
+
return writeState(contextDir, withRecalculatedApproval(state, true));
|
|
282
|
+
}
|
|
283
|
+
export function addWorkflowTodo(contextDir, input) {
|
|
284
|
+
const state = loadWorkflowState(contextDir);
|
|
285
|
+
const now = nowIso();
|
|
286
|
+
const todo = {
|
|
287
|
+
id: state.next_todo_id++,
|
|
288
|
+
title: input.title,
|
|
289
|
+
details: input.details?.trim() || "",
|
|
290
|
+
status: "open",
|
|
291
|
+
created_at: now,
|
|
292
|
+
updated_at: now,
|
|
293
|
+
};
|
|
294
|
+
state.todos.push(todo);
|
|
295
|
+
addHistory(state, "todo_added", { todo_id: todo.id, title: todo.title });
|
|
296
|
+
return writeState(contextDir, withRecalculatedApproval(state, true));
|
|
297
|
+
}
|
|
298
|
+
export function completeWorkflowTodo(contextDir, todoId) {
|
|
299
|
+
const state = loadWorkflowState(contextDir);
|
|
300
|
+
const todo = state.todos.find((item) => item.id === todoId);
|
|
301
|
+
if (!todo) {
|
|
302
|
+
return { ok: false, state, error: `Todo ${todoId} was not found` };
|
|
303
|
+
}
|
|
304
|
+
todo.status = "done";
|
|
305
|
+
todo.updated_at = nowIso();
|
|
306
|
+
addHistory(state, "todo_completed", { todo_id: todo.id });
|
|
307
|
+
return { ok: true, state: writeState(contextDir, withRecalculatedApproval(state, true)) };
|
|
308
|
+
}
|
|
309
|
+
export function approveWorkflow(contextDir, notes) {
|
|
310
|
+
const state = loadWorkflowState(contextDir);
|
|
311
|
+
const blockers = blockersFor(state);
|
|
312
|
+
if (blockers.length > 0) {
|
|
313
|
+
const refreshed = writeState(contextDir, withRecalculatedApproval(state));
|
|
314
|
+
return {
|
|
315
|
+
ok: false,
|
|
316
|
+
state: refreshed,
|
|
317
|
+
error: blockers.map((blocker) => blocker.message).join(" "),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
state.phase = "approved";
|
|
321
|
+
state.blocked_reasons = [];
|
|
322
|
+
state.approval = {
|
|
323
|
+
status: "approved",
|
|
324
|
+
approved_at: nowIso(),
|
|
325
|
+
notes: notes?.trim() || null,
|
|
326
|
+
};
|
|
327
|
+
addHistory(state, "workflow_approved");
|
|
328
|
+
return { ok: true, state: writeState(contextDir, state) };
|
|
329
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export function parseFrontmatter(markdown) {
|
|
2
|
+
const lines = markdown.split(/\r?\n/);
|
|
3
|
+
if (lines[0]?.trim() !== "---") {
|
|
4
|
+
return { fields: new Map(), body: markdown };
|
|
5
|
+
}
|
|
6
|
+
const fields = new Map();
|
|
7
|
+
let index = 1;
|
|
8
|
+
for (; index < lines.length; index += 1) {
|
|
9
|
+
const line = lines[index];
|
|
10
|
+
if (line.trim() === "---") {
|
|
11
|
+
index += 1;
|
|
12
|
+
break;
|
|
13
|
+
}
|
|
14
|
+
const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
15
|
+
if (!match) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
fields.set(match[1].toLowerCase(), match[2].trim());
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
fields,
|
|
22
|
+
body: lines.slice(index).join("\n").trim()
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function parseStringList(value) {
|
|
26
|
+
if (!value || !value.trim()) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
return value
|
|
30
|
+
.split(",")
|
|
31
|
+
.map((item) => item.trim())
|
|
32
|
+
.filter(Boolean);
|
|
33
|
+
}
|