@martinloop/mcp 0.2.5 → 0.2.7
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/README.md +101 -138
- package/dist/discovery-metadata.d.ts +10 -5
- package/dist/discovery-metadata.js +95 -5
- package/dist/package-version.d.ts +1 -1
- package/dist/package-version.js +1 -1
- package/dist/prompts.js +93 -1
- package/dist/resources.d.ts +8 -0
- package/dist/resources.js +245 -14
- package/dist/server-validation.d.ts +1 -1
- package/dist/server-validation.js +116 -0
- package/dist/server.js +361 -3
- package/dist/tools/doctor.d.ts +2 -0
- package/dist/tools/doctor.js +6 -2
- package/dist/tools/eval.d.ts +24 -0
- package/dist/tools/eval.js +65 -0
- package/dist/tools/get-status.d.ts +8 -0
- package/dist/tools/get-status.js +18 -0
- package/dist/tools/logs.d.ts +25 -0
- package/dist/tools/logs.js +49 -0
- package/dist/tools/plan.d.ts +20 -0
- package/dist/tools/plan.js +10 -0
- package/dist/tools/pr-tools.d.ts +31 -0
- package/dist/tools/pr-tools.js +111 -0
- package/dist/tools/preflight.d.ts +10 -0
- package/dist/tools/preflight.js +11 -2
- package/dist/tools/run-controls.d.ts +36 -0
- package/dist/tools/run-controls.js +88 -0
- package/dist/tools/run-dossier.d.ts +14 -0
- package/dist/tools/run-dossier.js +61 -1
- package/dist/tools/run-loop.js +21 -2
- package/dist/tools/tool-errors.d.ts +1 -1
- package/dist/tools/tool-support.js +28 -1
- package/dist/tools/workflow-governance.d.ts +133 -0
- package/dist/tools/workflow-governance.js +581 -0
- package/dist/vendor/adapters/cli-bridge.d.ts +5 -0
- package/dist/vendor/adapters/cli-bridge.js +16 -8
- package/dist/vendor/adapters/index.d.ts +2 -1
- package/dist/vendor/adapters/index.js +2 -0
- package/dist/vendor/adapters/openai-compatible.d.ts +47 -0
- package/dist/vendor/adapters/openai-compatible.js +242 -0
- package/dist/workflow-state.d.ts +25 -0
- package/dist/workflow-state.js +102 -0
- package/package.json +3 -3
- package/server.json +2 -2
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface MartinLogsInput {
|
|
2
|
+
file?: string;
|
|
3
|
+
loopId?: string;
|
|
4
|
+
runsDir?: string;
|
|
5
|
+
latest?: boolean;
|
|
6
|
+
limit?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface MartinLogsOutput {
|
|
9
|
+
source: string;
|
|
10
|
+
sourceKind: "file" | "loop_id" | "latest" | "runs_root";
|
|
11
|
+
loopId: string;
|
|
12
|
+
logCount: number;
|
|
13
|
+
live: {
|
|
14
|
+
lifecycleState: string;
|
|
15
|
+
pauseState: "active" | "paused" | "cancellation_requested";
|
|
16
|
+
approvalState: "not_required" | "resume_requested";
|
|
17
|
+
};
|
|
18
|
+
entries: Array<{
|
|
19
|
+
timestamp?: string;
|
|
20
|
+
source: "event" | "ledger" | "control";
|
|
21
|
+
kind: string;
|
|
22
|
+
payload: Record<string, unknown>;
|
|
23
|
+
}>;
|
|
24
|
+
}
|
|
25
|
+
export declare function martinLogsTool(input: MartinLogsInput): Promise<MartinLogsOutput>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { readRunControlState } from "./run-controls.js";
|
|
2
|
+
import { loadDetailedLoopRecord, readLedgerEvents } from "./run-store.js";
|
|
3
|
+
export async function martinLogsTool(input) {
|
|
4
|
+
const detail = await loadDetailedLoopRecord(input);
|
|
5
|
+
const ledgerEvents = await readLedgerEvents(detail);
|
|
6
|
+
const controls = await readRunControlState(detail);
|
|
7
|
+
const limit = input.limit ?? 20;
|
|
8
|
+
const eventEntries = (detail.loop.events ?? []).map((event) => ({
|
|
9
|
+
timestamp: event.timestamp,
|
|
10
|
+
source: "event",
|
|
11
|
+
kind: event.type,
|
|
12
|
+
payload: event.payload ?? {}
|
|
13
|
+
}));
|
|
14
|
+
const ledgerEntries = ledgerEvents.map((event) => ({
|
|
15
|
+
timestamp: event.timestamp,
|
|
16
|
+
source: "ledger",
|
|
17
|
+
kind: event.kind,
|
|
18
|
+
payload: (event.payload ?? {})
|
|
19
|
+
}));
|
|
20
|
+
const controlEntries = controls.receipts.map((receipt) => ({
|
|
21
|
+
timestamp: receipt.requestedAt,
|
|
22
|
+
source: "control",
|
|
23
|
+
kind: `run.${receipt.action}`,
|
|
24
|
+
payload: {
|
|
25
|
+
controlId: receipt.controlId,
|
|
26
|
+
...(receipt.reason ? { reason: receipt.reason } : {}),
|
|
27
|
+
...(receipt.requestedBy ? { requestedBy: receipt.requestedBy } : {})
|
|
28
|
+
}
|
|
29
|
+
}));
|
|
30
|
+
const entries = [...eventEntries, ...ledgerEntries, ...controlEntries]
|
|
31
|
+
.sort((left, right) => {
|
|
32
|
+
const leftTime = left.timestamp ? new Date(left.timestamp).getTime() : 0;
|
|
33
|
+
const rightTime = right.timestamp ? new Date(right.timestamp).getTime() : 0;
|
|
34
|
+
return rightTime - leftTime;
|
|
35
|
+
})
|
|
36
|
+
.slice(0, limit);
|
|
37
|
+
return {
|
|
38
|
+
source: detail.source,
|
|
39
|
+
sourceKind: detail.sourceKind,
|
|
40
|
+
loopId: detail.loop.loopId,
|
|
41
|
+
logCount: entries.length,
|
|
42
|
+
live: {
|
|
43
|
+
lifecycleState: detail.loop.lifecycleState,
|
|
44
|
+
pauseState: controls.requestedState,
|
|
45
|
+
approvalState: controls.approvalState
|
|
46
|
+
},
|
|
47
|
+
entries
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type MartinPlanProposal, type MartinPolicyPack } from "./workflow-governance.js";
|
|
2
|
+
export interface MartinPlanInput {
|
|
3
|
+
objective: string;
|
|
4
|
+
workingDirectory?: string;
|
|
5
|
+
context?: string;
|
|
6
|
+
verificationPlan?: string[];
|
|
7
|
+
allowedPaths?: string[];
|
|
8
|
+
deniedPaths?: string[];
|
|
9
|
+
policyPack?: MartinPolicyPack;
|
|
10
|
+
maxUsd?: number;
|
|
11
|
+
maxIterations?: number;
|
|
12
|
+
maxTokens?: number;
|
|
13
|
+
maxMinutes?: number;
|
|
14
|
+
maxFilesChanged?: number;
|
|
15
|
+
maxCommands?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface MartinPlanOutput extends MartinPlanProposal {
|
|
18
|
+
workingDirectory: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function martinPlanTool(input: MartinPlanInput): Promise<MartinPlanOutput>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { resolveSafeRepoRoot } from "../server-validation.js";
|
|
2
|
+
import { buildPlanProposal } from "./workflow-governance.js";
|
|
3
|
+
export async function martinPlanTool(input) {
|
|
4
|
+
const workingDirectory = resolveSafeRepoRoot(input.workingDirectory);
|
|
5
|
+
const proposal = buildPlanProposal(workingDirectory, input);
|
|
6
|
+
return {
|
|
7
|
+
workingDirectory,
|
|
8
|
+
...proposal
|
|
9
|
+
};
|
|
10
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type MartinRunDossierInput } from "./run-dossier.js";
|
|
2
|
+
export interface MartinPrSummaryOutput {
|
|
3
|
+
loopId: string;
|
|
4
|
+
title: string;
|
|
5
|
+
body: string;
|
|
6
|
+
grade: string;
|
|
7
|
+
score: number;
|
|
8
|
+
}
|
|
9
|
+
export interface MartinCreatePrInput extends MartinRunDossierInput {
|
|
10
|
+
title?: string;
|
|
11
|
+
base?: string;
|
|
12
|
+
execute?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface MartinCreatePrOutput extends MartinPrSummaryOutput {
|
|
15
|
+
execute: boolean;
|
|
16
|
+
created: boolean;
|
|
17
|
+
url?: string;
|
|
18
|
+
branch?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface MartinReviewPrInput extends MartinRunDossierInput {
|
|
21
|
+
prBody?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface MartinReviewPrOutput {
|
|
24
|
+
loopId: string;
|
|
25
|
+
verdict: "approve_with_review" | "needs_changes" | "blocked";
|
|
26
|
+
findings: string[];
|
|
27
|
+
summary: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function martinPrSummaryTool(input: MartinRunDossierInput): Promise<MartinPrSummaryOutput>;
|
|
30
|
+
export declare function martinCreatePrTool(input: MartinCreatePrInput): Promise<MartinCreatePrOutput>;
|
|
31
|
+
export declare function martinReviewPrTool(input: MartinReviewPrInput): Promise<MartinReviewPrOutput>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { loadDetailedLoopRecord } from "./run-store.js";
|
|
3
|
+
import { martinRunDossierTool } from "./run-dossier.js";
|
|
4
|
+
import { martinEvalTool } from "./eval.js";
|
|
5
|
+
import { detectCliAvailability } from "./tool-support.js";
|
|
6
|
+
import { MartinToolError } from "./tool-errors.js";
|
|
7
|
+
export async function martinPrSummaryTool(input) {
|
|
8
|
+
const dossier = await martinRunDossierTool({ ...input, format: "github-pr" });
|
|
9
|
+
const evaluation = await martinEvalTool(input);
|
|
10
|
+
const title = `martin: ${trimForTitle(dossier.loop.objective)}`;
|
|
11
|
+
const body = dossier.rendered ?? "";
|
|
12
|
+
return {
|
|
13
|
+
loopId: dossier.loop.loopId,
|
|
14
|
+
title,
|
|
15
|
+
body,
|
|
16
|
+
grade: evaluation.grade,
|
|
17
|
+
score: evaluation.score
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export async function martinCreatePrTool(input) {
|
|
21
|
+
const summary = await martinPrSummaryTool(input);
|
|
22
|
+
const detail = await loadDetailedLoopRecord(input);
|
|
23
|
+
const repoRoot = detail.loop.task?.repoRoot ?? process.cwd();
|
|
24
|
+
const branch = readGitValue(repoRoot, ["branch", "--show-current"]);
|
|
25
|
+
const title = input.title?.trim() || summary.title;
|
|
26
|
+
if (!input.execute) {
|
|
27
|
+
return {
|
|
28
|
+
...summary,
|
|
29
|
+
title,
|
|
30
|
+
execute: false,
|
|
31
|
+
created: false,
|
|
32
|
+
...(branch ? { branch } : {})
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const gh = detectCliAvailability("gh");
|
|
36
|
+
if (!gh.available) {
|
|
37
|
+
throw new MartinToolError("engine_unavailable", "GitHub CLI is not available on PATH.", {
|
|
38
|
+
category: "environment",
|
|
39
|
+
suggestion: "Install gh or rerun martin_create_pr with execute=false to preview the PR body.",
|
|
40
|
+
retryable: false
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
const args = ["pr", "create", "--title", title, "--body", summary.body];
|
|
44
|
+
if (input.base) {
|
|
45
|
+
args.push("--base", input.base);
|
|
46
|
+
}
|
|
47
|
+
const result = spawnSync("gh", args, {
|
|
48
|
+
cwd: repoRoot,
|
|
49
|
+
encoding: "utf8",
|
|
50
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
51
|
+
});
|
|
52
|
+
if (result.status !== 0) {
|
|
53
|
+
throw new MartinToolError("tool_execution_failed", "GitHub PR creation failed.", {
|
|
54
|
+
category: "transient",
|
|
55
|
+
suggestion: (result.stderr || result.stdout || "Check gh auth and branch state.").trim(),
|
|
56
|
+
retryable: false
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
const url = `${result.stdout ?? ""}`.split(/\r?\n/u).map((line) => line.trim()).find(Boolean);
|
|
60
|
+
return {
|
|
61
|
+
...summary,
|
|
62
|
+
title,
|
|
63
|
+
execute: true,
|
|
64
|
+
created: true,
|
|
65
|
+
...(url ? { url } : {}),
|
|
66
|
+
...(branch ? { branch } : {})
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export async function martinReviewPrTool(input) {
|
|
70
|
+
const summary = await martinPrSummaryTool(input);
|
|
71
|
+
const evaluation = await martinEvalTool(input);
|
|
72
|
+
const findings = [];
|
|
73
|
+
if (evaluation.grade === "blocked" || evaluation.grade === "insufficient_evidence") {
|
|
74
|
+
findings.push("Run evidence is not strong enough for a safe merge decision.");
|
|
75
|
+
}
|
|
76
|
+
if (evaluation.checks.verifier !== "passed") {
|
|
77
|
+
findings.push("Verifier evidence is not green.");
|
|
78
|
+
}
|
|
79
|
+
if (evaluation.checks.securityRisk !== "passed") {
|
|
80
|
+
findings.push("Risk score requires closer human review.");
|
|
81
|
+
}
|
|
82
|
+
if (input.prBody && !/MartinLoop Run Dossier/iu.test(input.prBody)) {
|
|
83
|
+
findings.push("PR body is missing the MartinLoop dossier section.");
|
|
84
|
+
}
|
|
85
|
+
const verdict = findings.length === 0
|
|
86
|
+
? "approve_with_review"
|
|
87
|
+
: findings.some((finding) => /not strong enough|not green/iu.test(finding))
|
|
88
|
+
? "blocked"
|
|
89
|
+
: "needs_changes";
|
|
90
|
+
return {
|
|
91
|
+
loopId: summary.loopId,
|
|
92
|
+
verdict,
|
|
93
|
+
findings,
|
|
94
|
+
summary: verdict === "approve_with_review"
|
|
95
|
+
? "PR evidence looks reviewable."
|
|
96
|
+
: verdict === "needs_changes"
|
|
97
|
+
? "PR needs changes before review is complete."
|
|
98
|
+
: "PR is blocked by evidence or verifier gaps."
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function trimForTitle(value) {
|
|
102
|
+
return value.length > 60 ? `${value.slice(0, 57).trimEnd()}...` : value;
|
|
103
|
+
}
|
|
104
|
+
function readGitValue(cwd, args) {
|
|
105
|
+
const result = spawnSync("git", args, {
|
|
106
|
+
cwd,
|
|
107
|
+
encoding: "utf8",
|
|
108
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
109
|
+
});
|
|
110
|
+
return result.status === 0 ? result.stdout.trim() || undefined : undefined;
|
|
111
|
+
}
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { type MartinEngine } from "./tool-support.js";
|
|
2
|
+
import { buildPolicyPackDefinition, type MartinPlanProposal, type MartinPolicyPack, type MartinRiskAssessment, type MartinRunContract } from "./workflow-governance.js";
|
|
2
3
|
export interface MartinPreflightInput {
|
|
3
4
|
objective: string;
|
|
4
5
|
workingDirectory?: string;
|
|
5
6
|
engine?: MartinEngine;
|
|
6
7
|
model?: string;
|
|
8
|
+
context?: string;
|
|
9
|
+
policyPack?: MartinPolicyPack;
|
|
7
10
|
maxUsd?: number;
|
|
8
11
|
maxIterations?: number;
|
|
9
12
|
maxTokens?: number;
|
|
13
|
+
maxMinutes?: number;
|
|
14
|
+
maxFilesChanged?: number;
|
|
15
|
+
maxCommands?: number;
|
|
10
16
|
verificationPlan?: string[];
|
|
11
17
|
allowedPaths?: string[];
|
|
12
18
|
deniedPaths?: string[];
|
|
@@ -58,5 +64,9 @@ export interface MartinPreflightOutput {
|
|
|
58
64
|
loopRecordPathPattern: string;
|
|
59
65
|
};
|
|
60
66
|
};
|
|
67
|
+
policy: ReturnType<typeof buildPolicyPackDefinition>;
|
|
68
|
+
risk: MartinRiskAssessment;
|
|
69
|
+
runContract: MartinRunContract;
|
|
70
|
+
plan: MartinPlanProposal;
|
|
61
71
|
}
|
|
62
72
|
export declare function martinPreflightTool(input: MartinPreflightInput): Promise<MartinPreflightOutput>;
|
package/dist/tools/preflight.js
CHANGED
|
@@ -2,9 +2,11 @@ import { DEFAULT_BUDGET } from "../vendor/contracts/index.js";
|
|
|
2
2
|
import { resolveRunsRoot } from "../vendor/core/index.js";
|
|
3
3
|
import { resolveSafeRepoRoot } from "../server-validation.js";
|
|
4
4
|
import { formatUsd, getEngineAvailability, resolveExecutionMode } from "./tool-support.js";
|
|
5
|
+
import { buildPlanProposal, buildRunContract, buildPolicyPackDefinition, inspectRepoSignals } from "./workflow-governance.js";
|
|
5
6
|
export async function martinPreflightTool(input) {
|
|
6
7
|
const executionMode = resolveExecutionMode();
|
|
7
8
|
const workingDirectory = resolveSafeRepoRoot(input.workingDirectory);
|
|
9
|
+
const signals = inspectRepoSignals(workingDirectory);
|
|
8
10
|
const engine = input.engine ?? "claude";
|
|
9
11
|
const engineAvailability = getEngineAvailability(engine);
|
|
10
12
|
const warnings = [];
|
|
@@ -32,11 +34,14 @@ export async function martinPreflightTool(input) {
|
|
|
32
34
|
if (overlappingScopes.length > 0) {
|
|
33
35
|
warnings.push(`Some path patterns appear in both allowedPaths and deniedPaths: ${overlappingScopes.join(", ")}.`);
|
|
34
36
|
}
|
|
37
|
+
const plan = buildPlanProposal(workingDirectory, input);
|
|
38
|
+
const runContract = buildRunContract(workingDirectory, input);
|
|
39
|
+
const policy = buildPolicyPackDefinition(input.policyPack, signals);
|
|
35
40
|
const ok = !executionMode.liveMode || engineAvailability.available;
|
|
36
41
|
return {
|
|
37
42
|
ok,
|
|
38
43
|
summary: ok
|
|
39
|
-
? `Preflight ready for ${engine} in ${workingDirectory} with a ${formatUsd(budget.maxUsd)} budget cap.`
|
|
44
|
+
? `Preflight ready for ${engine} in ${workingDirectory} with a ${formatUsd(budget.maxUsd)} budget cap and ${runContract.risk.level} risk.`
|
|
40
45
|
: `Preflight blocked: ${engine} is not available for live execution.`,
|
|
41
46
|
warnings,
|
|
42
47
|
readiness: {
|
|
@@ -76,6 +81,10 @@ export async function martinPreflightTool(input) {
|
|
|
76
81
|
runDirectoryPattern: "<runsRoot>/<loopId>/",
|
|
77
82
|
loopRecordPathPattern: "<runsRoot>/<loopId>/loop-record.json"
|
|
78
83
|
}
|
|
79
|
-
}
|
|
84
|
+
},
|
|
85
|
+
policy,
|
|
86
|
+
risk: runContract.risk,
|
|
87
|
+
runContract,
|
|
88
|
+
plan
|
|
80
89
|
};
|
|
81
90
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type DetailedLoopSource } from "./run-store.js";
|
|
2
|
+
export type MartinControlAction = "pause" | "cancel" | "continue";
|
|
3
|
+
export interface MartinRunControlRequestInput {
|
|
4
|
+
file?: string;
|
|
5
|
+
loopId?: string;
|
|
6
|
+
runsDir?: string;
|
|
7
|
+
latest?: boolean;
|
|
8
|
+
reason?: string;
|
|
9
|
+
requestedBy?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface MartinRunControlReceipt {
|
|
12
|
+
controlId: string;
|
|
13
|
+
loopId: string;
|
|
14
|
+
action: MartinControlAction;
|
|
15
|
+
requestedAt: string;
|
|
16
|
+
reason?: string;
|
|
17
|
+
requestedBy?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface MartinRunControlState {
|
|
20
|
+
requestedState: "active" | "paused" | "cancellation_requested";
|
|
21
|
+
latestReceipt?: MartinRunControlReceipt;
|
|
22
|
+
approvalState: "not_required" | "resume_requested";
|
|
23
|
+
receipts: MartinRunControlReceipt[];
|
|
24
|
+
receiptPath?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface MartinRunControlOutput {
|
|
27
|
+
ok: boolean;
|
|
28
|
+
summary: string;
|
|
29
|
+
loopId: string;
|
|
30
|
+
requestedAction: MartinControlAction;
|
|
31
|
+
state: MartinRunControlState;
|
|
32
|
+
}
|
|
33
|
+
export declare function createRunControlReceipt(action: MartinControlAction, input: MartinRunControlRequestInput): Promise<MartinRunControlOutput>;
|
|
34
|
+
export declare function readRunControlState(detailOrInput: DetailedLoopSource | MartinRunControlRequestInput): Promise<MartinRunControlState>;
|
|
35
|
+
export declare function readControlReceipts(receiptPath: string): Promise<MartinRunControlReceipt[]>;
|
|
36
|
+
export declare function validateControlReason(value?: string): string | undefined;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { appendFile, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { invalidArgumentsError, unsupportedOperationError } from "./tool-errors.js";
|
|
4
|
+
import { loadDetailedLoopRecord } from "./run-store.js";
|
|
5
|
+
export async function createRunControlReceipt(action, input) {
|
|
6
|
+
const detail = await loadDetailedLoopRecord(input);
|
|
7
|
+
if (!detail.canonicalRunDirectory) {
|
|
8
|
+
throw unsupportedOperationError("Run control receipts require a canonical run directory.", "Use a canonical loopId-backed Martin run before writing control receipts.");
|
|
9
|
+
}
|
|
10
|
+
const receipt = {
|
|
11
|
+
controlId: `ctl_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
|
|
12
|
+
loopId: detail.loop.loopId,
|
|
13
|
+
action,
|
|
14
|
+
requestedAt: new Date().toISOString(),
|
|
15
|
+
...(input.reason ? { reason: input.reason } : {}),
|
|
16
|
+
...(input.requestedBy ? { requestedBy: input.requestedBy } : {})
|
|
17
|
+
};
|
|
18
|
+
const receiptPath = path.join(detail.canonicalRunDirectory, "controls.jsonl");
|
|
19
|
+
await appendFile(receiptPath, `${JSON.stringify(receipt)}\n`, "utf8");
|
|
20
|
+
const state = await readRunControlState(detail);
|
|
21
|
+
return {
|
|
22
|
+
ok: true,
|
|
23
|
+
summary: action === "cancel"
|
|
24
|
+
? `Cancellation request recorded for ${detail.loop.loopId}.`
|
|
25
|
+
: action === "pause"
|
|
26
|
+
? `Pause request recorded for ${detail.loop.loopId}.`
|
|
27
|
+
: `Continue request recorded for ${detail.loop.loopId}.`,
|
|
28
|
+
loopId: detail.loop.loopId,
|
|
29
|
+
requestedAction: action,
|
|
30
|
+
state
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export async function readRunControlState(detailOrInput) {
|
|
34
|
+
const detail = isDetailedLoopSource(detailOrInput)
|
|
35
|
+
? detailOrInput
|
|
36
|
+
: await loadDetailedLoopRecord(detailOrInput);
|
|
37
|
+
const receiptPath = detail.canonicalRunDirectory
|
|
38
|
+
? path.join(detail.canonicalRunDirectory, "controls.jsonl")
|
|
39
|
+
: undefined;
|
|
40
|
+
const receipts = receiptPath ? await readControlReceipts(receiptPath) : [];
|
|
41
|
+
const latestReceipt = receipts.at(-1);
|
|
42
|
+
return {
|
|
43
|
+
requestedState: latestReceipt?.action === "pause"
|
|
44
|
+
? "paused"
|
|
45
|
+
: latestReceipt?.action === "cancel"
|
|
46
|
+
? "cancellation_requested"
|
|
47
|
+
: "active",
|
|
48
|
+
...(latestReceipt ? { latestReceipt } : {}),
|
|
49
|
+
approvalState: latestReceipt?.action === "pause" ? "resume_requested" : "not_required",
|
|
50
|
+
receipts,
|
|
51
|
+
...(receiptPath ? { receiptPath } : {})
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export async function readControlReceipts(receiptPath) {
|
|
55
|
+
try {
|
|
56
|
+
const contents = await readFile(receiptPath, "utf8");
|
|
57
|
+
return contents
|
|
58
|
+
.split(/\r?\n/u)
|
|
59
|
+
.map((line) => line.trim())
|
|
60
|
+
.filter(Boolean)
|
|
61
|
+
.map((line) => JSON.parse(line))
|
|
62
|
+
.filter(isRunControlReceipt);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function isDetailedLoopSource(value) {
|
|
69
|
+
return typeof value === "object" && value !== null && "loop" in value && "runsRoot" in value;
|
|
70
|
+
}
|
|
71
|
+
function isRunControlReceipt(value) {
|
|
72
|
+
return (typeof value === "object" &&
|
|
73
|
+
value !== null &&
|
|
74
|
+
typeof value.controlId === "string" &&
|
|
75
|
+
typeof value.loopId === "string" &&
|
|
76
|
+
typeof value.action === "string" &&
|
|
77
|
+
typeof value.requestedAt === "string");
|
|
78
|
+
}
|
|
79
|
+
export function validateControlReason(value) {
|
|
80
|
+
if (value === undefined) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
const trimmed = value.trim();
|
|
84
|
+
if (trimmed.length === 0) {
|
|
85
|
+
throw invalidArgumentsError("Invalid reason.");
|
|
86
|
+
}
|
|
87
|
+
return trimmed;
|
|
88
|
+
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { buildArtifactSummary, buildBudgetSnapshot, buildCostSnapshot, buildEventSummaries, buildLoopPreview, buildVerificationSummary } from "./tool-support.js";
|
|
2
|
+
import { readRunControlState } from "./run-controls.js";
|
|
3
|
+
import { martinEvalTool } from "./eval.js";
|
|
4
|
+
import { assessRunRisk } from "./workflow-governance.js";
|
|
2
5
|
export interface MartinRunDossierInput {
|
|
3
6
|
file?: string;
|
|
4
7
|
loopId?: string;
|
|
5
8
|
runsDir?: string;
|
|
6
9
|
latest?: boolean;
|
|
10
|
+
format?: "json" | "md" | "github-pr";
|
|
7
11
|
}
|
|
8
12
|
export interface MartinRunDossierOutput {
|
|
9
13
|
source: string;
|
|
@@ -30,6 +34,16 @@ export interface MartinRunDossierOutput {
|
|
|
30
34
|
resources: string[];
|
|
31
35
|
prompts: string[];
|
|
32
36
|
};
|
|
37
|
+
review: {
|
|
38
|
+
diffSummary: string;
|
|
39
|
+
risk: ReturnType<typeof assessRunRisk>;
|
|
40
|
+
outcome: "passed" | "failed" | "needs_review";
|
|
41
|
+
nextAction: string;
|
|
42
|
+
};
|
|
43
|
+
evaluation: Awaited<ReturnType<typeof martinEvalTool>>;
|
|
44
|
+
control: Awaited<ReturnType<typeof readRunControlState>>;
|
|
45
|
+
format: "json" | "md" | "github-pr";
|
|
46
|
+
rendered?: string;
|
|
33
47
|
inspection: {
|
|
34
48
|
runsRoot: string;
|
|
35
49
|
canonicalRunDirectory?: string;
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
import { buildArtifactSummary, buildBudgetSnapshot, buildCostSnapshot, buildEventSummaries, buildLoopPreview, buildSuggestedPromptNames, buildSuggestedResourceUris, buildVerificationSummary } from "./tool-support.js";
|
|
2
2
|
import { loadDetailedLoopRecord, readAttemptArtifactFiles, readLedgerEvents } from "./run-store.js";
|
|
3
|
+
import { readRunControlState } from "./run-controls.js";
|
|
4
|
+
import { martinEvalTool } from "./eval.js";
|
|
5
|
+
import { assessRunRisk, inspectRepoSignals } from "./workflow-governance.js";
|
|
3
6
|
export async function martinRunDossierTool(input) {
|
|
4
7
|
const detail = await loadDetailedLoopRecord(input);
|
|
5
8
|
const ledgerEvents = await readLedgerEvents(detail);
|
|
6
9
|
const verification = buildVerificationSummary(detail.loop, ledgerEvents);
|
|
10
|
+
const control = await readRunControlState(detail);
|
|
11
|
+
const evaluation = await martinEvalTool(input);
|
|
12
|
+
const repoRoot = detail.loop.task?.repoRoot ?? process.cwd();
|
|
13
|
+
const risk = assessRunRisk({
|
|
14
|
+
objective: detail.loop.task?.objective ?? detail.loop.loopId,
|
|
15
|
+
allowedPaths: detail.loop.task?.allowedPaths ?? [],
|
|
16
|
+
blockedPaths: detail.loop.task?.deniedPaths ?? [],
|
|
17
|
+
verifiers: detail.loop.task?.verificationPlan ?? [],
|
|
18
|
+
signals: inspectRepoSignals(repoRoot)
|
|
19
|
+
});
|
|
7
20
|
const attempts = await Promise.all(detail.loop.attempts.map(async (attempt) => ({
|
|
8
21
|
index: attempt.index,
|
|
9
22
|
...(attempt.attemptId ? { attemptId: attempt.attemptId } : {}),
|
|
@@ -16,7 +29,23 @@ export async function martinRunDossierTool(input) {
|
|
|
16
29
|
...(attempt.summary ? { summary: attempt.summary } : {}),
|
|
17
30
|
artifactFiles: await readAttemptArtifactFiles(detail, attempt.index)
|
|
18
31
|
})));
|
|
19
|
-
|
|
32
|
+
const review = {
|
|
33
|
+
diffSummary: attempts.length > 0
|
|
34
|
+
? `Run touched ${attempts.length} attempt(s); latest summary: ${attempts.at(-1)?.summary ?? "No attempt summary recorded."}`
|
|
35
|
+
: "No attempts were recorded for this run.",
|
|
36
|
+
risk,
|
|
37
|
+
outcome: verification.status === "passed"
|
|
38
|
+
? "passed"
|
|
39
|
+
: verification.status === "failed"
|
|
40
|
+
? "failed"
|
|
41
|
+
: "needs_review",
|
|
42
|
+
nextAction: verification.status === "passed"
|
|
43
|
+
? "Review the dossier and evaluation, then decide whether to merge or promote."
|
|
44
|
+
: verification.status === "failed"
|
|
45
|
+
? "Investigate the latest verifier failure before retrying or promoting."
|
|
46
|
+
: "Collect more evidence before claiming completion."
|
|
47
|
+
};
|
|
48
|
+
const output = {
|
|
20
49
|
source: detail.source,
|
|
21
50
|
sourceKind: detail.sourceKind,
|
|
22
51
|
loop: buildLoopPreview(detail.loop),
|
|
@@ -30,6 +59,10 @@ export async function martinRunDossierTool(input) {
|
|
|
30
59
|
resources: buildSuggestedResourceUris(detail.loop.loopId),
|
|
31
60
|
prompts: buildSuggestedPromptNames()
|
|
32
61
|
},
|
|
62
|
+
review,
|
|
63
|
+
evaluation,
|
|
64
|
+
control,
|
|
65
|
+
format: input.format ?? "json",
|
|
33
66
|
inspection: {
|
|
34
67
|
runsRoot: detail.runsRoot,
|
|
35
68
|
...(detail.canonicalRunDirectory ? { canonicalRunDirectory: detail.canonicalRunDirectory } : {}),
|
|
@@ -38,4 +71,31 @@ export async function martinRunDossierTool(input) {
|
|
|
38
71
|
},
|
|
39
72
|
warnings: [...detail.warnings, ...verification.warnings]
|
|
40
73
|
};
|
|
74
|
+
if (output.format !== "json") {
|
|
75
|
+
output.rendered = renderDossier(output);
|
|
76
|
+
}
|
|
77
|
+
return output;
|
|
78
|
+
}
|
|
79
|
+
function renderDossier(output) {
|
|
80
|
+
const lines = [
|
|
81
|
+
output.format === "github-pr" ? "## MartinLoop Run Dossier" : "# MartinLoop Run Dossier",
|
|
82
|
+
"",
|
|
83
|
+
`Objective: ${output.loop.objective}`,
|
|
84
|
+
`Run: ${output.loop.loopId}`,
|
|
85
|
+
`Status: ${output.loop.status} / ${output.loop.lifecycleState}`,
|
|
86
|
+
`Attempts: ${output.attempts.length}`,
|
|
87
|
+
`Verifiers: ${output.verification.status}`,
|
|
88
|
+
`Risk: ${output.review.risk.level} (${output.review.risk.score})`,
|
|
89
|
+
`Allowed paths: ${output.review.risk.reasons.length > 0 ? output.review.risk.reasons.join("; ") : "No major risk reasons recorded."}`,
|
|
90
|
+
"",
|
|
91
|
+
"Review Summary:",
|
|
92
|
+
output.review.diffSummary,
|
|
93
|
+
"",
|
|
94
|
+
"Next Action:",
|
|
95
|
+
output.review.nextAction
|
|
96
|
+
];
|
|
97
|
+
if (output.format === "github-pr") {
|
|
98
|
+
lines.push("", `Evaluation: ${output.evaluation.grade} (${output.evaluation.score})`);
|
|
99
|
+
}
|
|
100
|
+
return lines.join("\n");
|
|
41
101
|
}
|
package/dist/tools/run-loop.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createClaudeCliAdapter, createCodexCliAdapter, createStubDirectProvider
|
|
|
2
2
|
import { createFileRunStore, evaluateCostGovernor, resolveRunsRoot, runMartin } from "../vendor/core/index.js";
|
|
3
3
|
import { DEFAULT_BUDGET } from "../vendor/contracts/index.js";
|
|
4
4
|
import { normalizeSafePathPatterns, resolveSafeRepoRoot } from "../server-validation.js";
|
|
5
|
+
import { evaluateMcpRunGate } from "../workflow-state.js";
|
|
5
6
|
import { MartinToolError } from "./tool-errors.js";
|
|
6
7
|
import { buildArtifactSummary, buildVerificationSummary, buildLoopPreview, buildRunRecordPaths, getEngineAvailability, resolveExecutionMode } from "./tool-support.js";
|
|
7
8
|
export async function runLoopTool(input) {
|
|
@@ -12,6 +13,25 @@ export async function runLoopTool(input) {
|
|
|
12
13
|
const deniedPaths = normalizeSafePathPatterns(input.deniedPaths, "deniedPaths");
|
|
13
14
|
const executionMode = resolveExecutionMode();
|
|
14
15
|
const engineAvailability = getEngineAvailability(engine);
|
|
16
|
+
const runsRoot = resolveRunsRoot(process.env);
|
|
17
|
+
const gate = await evaluateMcpRunGate({
|
|
18
|
+
runsRoot,
|
|
19
|
+
workingDirectory,
|
|
20
|
+
objective: input.objective,
|
|
21
|
+
engine,
|
|
22
|
+
verificationPlan: input.verificationPlan
|
|
23
|
+
});
|
|
24
|
+
if (!gate.allowed) {
|
|
25
|
+
throw new MartinToolError("policy_blocked", gate.summary, {
|
|
26
|
+
category: "policy_blocked",
|
|
27
|
+
suggestion: gate.nextAction,
|
|
28
|
+
retryable: false,
|
|
29
|
+
details: {
|
|
30
|
+
missingSteps: gate.missingSteps,
|
|
31
|
+
nextAction: gate.nextAction
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
15
35
|
if (executionMode.liveMode && !engineAvailability.available) {
|
|
16
36
|
throw new MartinToolError("engine_unavailable", `Engine '${engine}' is not available on PATH.`, {
|
|
17
37
|
category: "environment",
|
|
@@ -41,7 +61,7 @@ export async function runLoopTool(input) {
|
|
|
41
61
|
const result = await runMartin({
|
|
42
62
|
workspaceId: input.workspaceId ?? "ws_mcp",
|
|
43
63
|
projectId: input.projectId ?? "proj_mcp",
|
|
44
|
-
store: createFileRunStore({ runsRoot
|
|
64
|
+
store: createFileRunStore({ runsRoot }),
|
|
45
65
|
task: {
|
|
46
66
|
title: input.objective.slice(0, 100),
|
|
47
67
|
objective: input.objective,
|
|
@@ -65,7 +85,6 @@ export async function runLoopTool(input) {
|
|
|
65
85
|
},
|
|
66
86
|
attemptsUsed: result.loop.attempts.length
|
|
67
87
|
});
|
|
68
|
-
const runsRoot = resolveRunsRoot(process.env);
|
|
69
88
|
const recordPaths = buildRunRecordPaths(runsRoot, result.loop.loopId);
|
|
70
89
|
const verification = buildVerificationSummary(result.loop);
|
|
71
90
|
const artifacts = buildArtifactSummary(result.loop);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { MartinErrorCategory } from "../vendor/contracts/index.js";
|
|
2
|
-
export type ToolFailureCode = "attempt_not_found" | "engine_unavailable" | "invalid_arguments" | "invalid_json" | "invalid_path" | "invalid_selector" | "no_loop_records" | "store_unreadable" | "tool_execution_failed" | "unknown_tool" | "unsupported_operation";
|
|
2
|
+
export type ToolFailureCode = "attempt_not_found" | "engine_unavailable" | "invalid_arguments" | "invalid_json" | "invalid_path" | "invalid_selector" | "no_loop_records" | "policy_blocked" | "store_unreadable" | "tool_execution_failed" | "unknown_tool" | "unsupported_operation";
|
|
3
3
|
export type ToolFailureCategory = MartinErrorCategory;
|
|
4
4
|
export interface ToolFailure {
|
|
5
5
|
code: ToolFailureCode;
|