@mandujs/mcp 0.24.0 → 0.27.0
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 +4 -4
- package/src/tools/ate-boundary-probe.ts +109 -0
- package/src/tools/ate-context.ts +173 -96
- package/src/tools/ate-coverage.ts +71 -0
- package/src/tools/ate-mutate.ts +103 -0
- package/src/tools/ate-mutation-report.ts +64 -0
- package/src/tools/ate-oracle-pending.ts +49 -0
- package/src/tools/ate-oracle-replay.ts +44 -0
- package/src/tools/ate-oracle-verdict.ts +70 -0
- package/src/tools/ate-recall.ts +85 -0
- package/src/tools/ate-remember.ts +79 -0
- package/src/tools/ate-save.ts +160 -139
- package/src/tools/ate.ts +34 -7
- package/src/tools/index.ts +82 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mandu_ate_oracle_pending` — Phase C.4.
|
|
3
|
+
*
|
|
4
|
+
* List pending semantic oracle entries for agent judgment.
|
|
5
|
+
* Read-only.
|
|
6
|
+
*/
|
|
7
|
+
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
import { findOraclePending } from "@mandujs/ate";
|
|
9
|
+
|
|
10
|
+
export const ateOraclePendingToolDefinitions: Tool[] = [
|
|
11
|
+
{
|
|
12
|
+
name: "mandu_ate_oracle_pending",
|
|
13
|
+
annotations: {
|
|
14
|
+
readOnlyHint: true,
|
|
15
|
+
},
|
|
16
|
+
description:
|
|
17
|
+
"Phase C.4 — list pending semantic oracle entries. Returns the most recent " +
|
|
18
|
+
"`status=pending` entries from `.mandu/ate-oracle-queue.jsonl`. Each entry " +
|
|
19
|
+
"carries an assertionId, the spec path, the claim text, and an artifactPath " +
|
|
20
|
+
"pointing to screenshot / DOM captures. The agent reviews these and issues a " +
|
|
21
|
+
"verdict via `mandu_ate_oracle_verdict`. CI never blocks on these — " +
|
|
22
|
+
"expectSemantic is deterministic-non-blocking by default.",
|
|
23
|
+
inputSchema: {
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
repoRoot: { type: "string", description: "Absolute path to the Mandu project root." },
|
|
27
|
+
limit: { type: "number", description: "Maximum entries to return. Default 20." },
|
|
28
|
+
specPath: { type: "string", description: "Filter to a specific spec file." },
|
|
29
|
+
},
|
|
30
|
+
required: ["repoRoot"],
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
export function ateOraclePendingTools(_projectRoot: string) {
|
|
36
|
+
return {
|
|
37
|
+
mandu_ate_oracle_pending: async (args: Record<string, unknown>) => {
|
|
38
|
+
const repoRoot = args.repoRoot as string | undefined;
|
|
39
|
+
if (!repoRoot || typeof repoRoot !== "string") {
|
|
40
|
+
return { ok: false, error: "repoRoot is required" };
|
|
41
|
+
}
|
|
42
|
+
const entries = findOraclePending(repoRoot, {
|
|
43
|
+
...(typeof args.limit === "number" ? { limit: args.limit } : {}),
|
|
44
|
+
...(typeof args.specPath === "string" ? { specPath: args.specPath } : {}),
|
|
45
|
+
});
|
|
46
|
+
return { ok: true, count: entries.length, entries };
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mandu_ate_oracle_replay` — Phase C.4.
|
|
3
|
+
*
|
|
4
|
+
* Read-only. Return every oracle entry (pending + judged) for a spec
|
|
5
|
+
* path — lets agents review the history of semantic claims for a file
|
|
6
|
+
* before re-issuing similar assertions.
|
|
7
|
+
*/
|
|
8
|
+
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
+
import { findOracleEntriesForSpec } from "@mandujs/ate";
|
|
10
|
+
|
|
11
|
+
export const ateOracleReplayToolDefinitions: Tool[] = [
|
|
12
|
+
{
|
|
13
|
+
name: "mandu_ate_oracle_replay",
|
|
14
|
+
annotations: {
|
|
15
|
+
readOnlyHint: true,
|
|
16
|
+
},
|
|
17
|
+
description:
|
|
18
|
+
"Phase C.4 — replay every oracle verdict (pending + passed + failed) for a " +
|
|
19
|
+
"given spec. Returns the full audit trail sorted newest → oldest. Useful for " +
|
|
20
|
+
"agents reviewing past `failed` verdicts before re-issuing similar semantic " +
|
|
21
|
+
"claims, or for human auditors walking the queue history.",
|
|
22
|
+
inputSchema: {
|
|
23
|
+
type: "object",
|
|
24
|
+
properties: {
|
|
25
|
+
repoRoot: { type: "string", description: "Absolute path to the Mandu project root." },
|
|
26
|
+
specPath: { type: "string", description: "Spec file path to replay." },
|
|
27
|
+
},
|
|
28
|
+
required: ["repoRoot", "specPath"],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
export function ateOracleReplayTools(_projectRoot: string) {
|
|
34
|
+
return {
|
|
35
|
+
mandu_ate_oracle_replay: async (args: Record<string, unknown>) => {
|
|
36
|
+
const repoRoot = args.repoRoot as string | undefined;
|
|
37
|
+
const specPath = args.specPath as string | undefined;
|
|
38
|
+
if (!repoRoot) return { ok: false, error: "repoRoot is required" };
|
|
39
|
+
if (!specPath) return { ok: false, error: "specPath is required" };
|
|
40
|
+
const entries = findOracleEntriesForSpec(repoRoot, specPath);
|
|
41
|
+
return { ok: true, count: entries.length, entries };
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mandu_ate_oracle_verdict` — Phase C.4.
|
|
3
|
+
*
|
|
4
|
+
* Apply an agent / human verdict to a pending oracle entry. Rewrites
|
|
5
|
+
* matching pending rows with `status = passed|failed` + verdict metadata.
|
|
6
|
+
*/
|
|
7
|
+
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
import { setOracleVerdict } from "@mandujs/ate";
|
|
9
|
+
|
|
10
|
+
export const ateOracleVerdictToolDefinitions: Tool[] = [
|
|
11
|
+
{
|
|
12
|
+
name: "mandu_ate_oracle_verdict",
|
|
13
|
+
annotations: {
|
|
14
|
+
readOnlyHint: false,
|
|
15
|
+
},
|
|
16
|
+
description:
|
|
17
|
+
"Phase C.4 — record an oracle verdict for a pending semantic assertion. " +
|
|
18
|
+
"`verdict: 'pass' | 'fail'`. `judgedBy`: 'agent' (default) or 'human'. " +
|
|
19
|
+
"`reason` is the short free-form justification the agent (or human) provides. " +
|
|
20
|
+
"Every pending queue entry with the matching assertionId transitions to the " +
|
|
21
|
+
"given verdict — subsequent `promoteVerdicts: true` expectSemantic calls will " +
|
|
22
|
+
"see past `failed` verdicts and throw deterministically.",
|
|
23
|
+
inputSchema: {
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
repoRoot: { type: "string", description: "Absolute path to the Mandu project root." },
|
|
27
|
+
assertionId: { type: "string", description: "Stable assertion id returned by expectSemantic." },
|
|
28
|
+
verdict: {
|
|
29
|
+
type: "string",
|
|
30
|
+
enum: ["pass", "fail"],
|
|
31
|
+
description: "Whether the agent judges the claim satisfied.",
|
|
32
|
+
},
|
|
33
|
+
reason: { type: "string", description: "Free-form justification." },
|
|
34
|
+
judgedBy: {
|
|
35
|
+
type: "string",
|
|
36
|
+
enum: ["agent", "human"],
|
|
37
|
+
description: "Source of the verdict. Defaults to 'agent'.",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
required: ["repoRoot", "assertionId", "verdict", "reason"],
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
export function ateOracleVerdictTools(_projectRoot: string) {
|
|
46
|
+
return {
|
|
47
|
+
mandu_ate_oracle_verdict: async (args: Record<string, unknown>) => {
|
|
48
|
+
const repoRoot = args.repoRoot as string | undefined;
|
|
49
|
+
const assertionId = args.assertionId as string | undefined;
|
|
50
|
+
const verdict = args.verdict as string | undefined;
|
|
51
|
+
const reason = args.reason as string | undefined;
|
|
52
|
+
const judgedBy = args.judgedBy as string | undefined;
|
|
53
|
+
if (!repoRoot) return { ok: false, error: "repoRoot is required" };
|
|
54
|
+
if (!assertionId) return { ok: false, error: "assertionId is required" };
|
|
55
|
+
if (verdict !== "pass" && verdict !== "fail") {
|
|
56
|
+
return { ok: false, error: "verdict must be 'pass' or 'fail'" };
|
|
57
|
+
}
|
|
58
|
+
if (!reason || typeof reason !== "string") {
|
|
59
|
+
return { ok: false, error: "reason is required" };
|
|
60
|
+
}
|
|
61
|
+
const res = setOracleVerdict(repoRoot, {
|
|
62
|
+
assertionId,
|
|
63
|
+
verdict,
|
|
64
|
+
reason,
|
|
65
|
+
...(judgedBy === "agent" || judgedBy === "human" ? { judgedBy } : {}),
|
|
66
|
+
});
|
|
67
|
+
return { ok: true, updated: res.updated, entries: res.entries };
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mandu_ate_recall` — Phase B.2 memory read tool.
|
|
3
|
+
*
|
|
4
|
+
* See docs/ate/phase-b-spec.md §B.2. Agents call this BEFORE generating
|
|
5
|
+
* a spec so they can reference prior intent / rejected healing history.
|
|
6
|
+
*
|
|
7
|
+
* Snake_case (§11 decision #4). Read-only.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
11
|
+
import { recallMemory, type MemoryEventKind } from "@mandujs/ate";
|
|
12
|
+
|
|
13
|
+
export const ateRecallToolDefinitions: Tool[] = [
|
|
14
|
+
{
|
|
15
|
+
name: "mandu_ate_recall",
|
|
16
|
+
annotations: {
|
|
17
|
+
readOnlyHint: true,
|
|
18
|
+
},
|
|
19
|
+
description:
|
|
20
|
+
"Phase B.2 memory recall. Queries the project-local " +
|
|
21
|
+
".mandu/ate-memory.jsonl append-only log with substring + token-" +
|
|
22
|
+
"overlap scoring (no embeddings). Useful BEFORE generation to see " +
|
|
23
|
+
"previously rejected specs, accepted heals, or intent history for " +
|
|
24
|
+
"the same route. Returns { events, totalMatching }. Default limit 10, " +
|
|
25
|
+
"default sinceDays 90. Filter by kind: intent_history | rejected_spec " +
|
|
26
|
+
"| accepted_healing | rejected_healing | prompt_version_drift | " +
|
|
27
|
+
"boundary_gap_filled | coverage_snapshot.",
|
|
28
|
+
inputSchema: {
|
|
29
|
+
type: "object",
|
|
30
|
+
properties: {
|
|
31
|
+
repoRoot: {
|
|
32
|
+
type: "string",
|
|
33
|
+
description: "Absolute path to the Mandu project root.",
|
|
34
|
+
},
|
|
35
|
+
intent: {
|
|
36
|
+
type: "string",
|
|
37
|
+
description: "Natural-language intent to search (substring + token overlap).",
|
|
38
|
+
},
|
|
39
|
+
route: {
|
|
40
|
+
type: "string",
|
|
41
|
+
description: "Route id or pattern ('api-signup' or '/api/signup').",
|
|
42
|
+
},
|
|
43
|
+
kind: {
|
|
44
|
+
type: "string",
|
|
45
|
+
description: "Filter by event kind.",
|
|
46
|
+
},
|
|
47
|
+
limit: {
|
|
48
|
+
type: "number",
|
|
49
|
+
description: "Max events to return. Default 10.",
|
|
50
|
+
},
|
|
51
|
+
sinceDays: {
|
|
52
|
+
type: "number",
|
|
53
|
+
description: "Drop events older than N days. Default 90.",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
required: ["repoRoot"],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
export function ateRecallTools(_projectRoot: string) {
|
|
62
|
+
return {
|
|
63
|
+
mandu_ate_recall: async (args: Record<string, unknown>) => {
|
|
64
|
+
const repoRoot = args.repoRoot as string | undefined;
|
|
65
|
+
if (!repoRoot || typeof repoRoot !== "string") {
|
|
66
|
+
return { ok: false, error: "repoRoot is required" };
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const result = recallMemory(repoRoot, {
|
|
70
|
+
intent: typeof args.intent === "string" ? args.intent : undefined,
|
|
71
|
+
route: typeof args.route === "string" ? args.route : undefined,
|
|
72
|
+
kind:
|
|
73
|
+
typeof args.kind === "string"
|
|
74
|
+
? (args.kind as MemoryEventKind)
|
|
75
|
+
: undefined,
|
|
76
|
+
limit: typeof args.limit === "number" ? args.limit : undefined,
|
|
77
|
+
sinceDays: typeof args.sinceDays === "number" ? args.sinceDays : undefined,
|
|
78
|
+
});
|
|
79
|
+
return { ok: true, ...result };
|
|
80
|
+
} catch (err) {
|
|
81
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mandu_ate_remember` — Phase B.2 memory write tool.
|
|
3
|
+
*
|
|
4
|
+
* Snake_case (§11 decision #4). Idempotent append.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
import {
|
|
9
|
+
appendMemoryEvent,
|
|
10
|
+
parseMemoryEvent,
|
|
11
|
+
type MemoryEvent,
|
|
12
|
+
} from "@mandujs/ate";
|
|
13
|
+
|
|
14
|
+
export const ateRememberToolDefinitions: Tool[] = [
|
|
15
|
+
{
|
|
16
|
+
name: "mandu_ate_remember",
|
|
17
|
+
description:
|
|
18
|
+
"Phase B.2 memory write. Appends one event to the project-local " +
|
|
19
|
+
".mandu/ate-memory.jsonl. File auto-rotates to .bak when it crosses " +
|
|
20
|
+
"10 MB. Supported event kinds (discriminated union by `kind`): " +
|
|
21
|
+
"intent_history | rejected_spec | accepted_healing | rejected_healing " +
|
|
22
|
+
"| prompt_version_drift | boundary_gap_filled | coverage_snapshot. " +
|
|
23
|
+
"Timestamp defaults to now (ISO-8601 UTC) if omitted.",
|
|
24
|
+
inputSchema: {
|
|
25
|
+
type: "object",
|
|
26
|
+
properties: {
|
|
27
|
+
repoRoot: {
|
|
28
|
+
type: "string",
|
|
29
|
+
description: "Absolute path to the Mandu project root.",
|
|
30
|
+
},
|
|
31
|
+
event: {
|
|
32
|
+
type: "object",
|
|
33
|
+
description:
|
|
34
|
+
"MemoryEvent object. Must carry a `kind` discriminator plus the " +
|
|
35
|
+
"event-kind-specific required fields (see @mandujs/ate memory/schema.ts).",
|
|
36
|
+
additionalProperties: true,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
required: ["repoRoot", "event"],
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
export function ateRememberTools(_projectRoot: string) {
|
|
45
|
+
return {
|
|
46
|
+
mandu_ate_remember: async (args: Record<string, unknown>) => {
|
|
47
|
+
const repoRoot = args.repoRoot as string | undefined;
|
|
48
|
+
const eventRaw = args.event;
|
|
49
|
+
|
|
50
|
+
if (!repoRoot || typeof repoRoot !== "string") {
|
|
51
|
+
return { ok: false, error: "repoRoot is required" };
|
|
52
|
+
}
|
|
53
|
+
if (!eventRaw || typeof eventRaw !== "object") {
|
|
54
|
+
return { ok: false, error: "event is required" };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Default the timestamp if the caller omitted it (agents do).
|
|
58
|
+
const draft = { ...(eventRaw as Record<string, unknown>) };
|
|
59
|
+
if (typeof draft.timestamp !== "string") {
|
|
60
|
+
draft.timestamp = new Date().toISOString();
|
|
61
|
+
}
|
|
62
|
+
const parsed = parseMemoryEvent(draft);
|
|
63
|
+
if (!parsed) {
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
error:
|
|
67
|
+
"Event failed validation. Check that `kind` and the kind-specific required fields are present.",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const result = appendMemoryEvent(repoRoot, parsed as MemoryEvent);
|
|
73
|
+
return { ok: true, written: result.written, rotation: result.rotation ?? null };
|
|
74
|
+
} catch (err) {
|
|
75
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
package/src/tools/ate-save.ts
CHANGED
|
@@ -1,139 +1,160 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `mandu_ate_save` — Phase A.3 spec persistence with lint-before-write.
|
|
3
|
-
*
|
|
4
|
-
* See `docs/ate/roadmap-v2-agent-native.md` §4.7 and the §7 extension
|
|
5
|
-
* ("mandu_ate_save lint-before-write").
|
|
6
|
-
*
|
|
7
|
-
* Semantics:
|
|
8
|
-
* 1. Run `lintSpecContent` (from @mandujs/ate) which:
|
|
9
|
-
* - parses with ts-morph (syntax errors block),
|
|
10
|
-
* - walks import declarations (banned typos, unknown @mandujs/* barrels),
|
|
11
|
-
* - detects anti-patterns (bare localhost, hand-rolled CSRF, DB mocks).
|
|
12
|
-
* 2. If any *blocking* diagnostic fires, return { saved: false, ... } WITHOUT
|
|
13
|
-
* writing. Otherwise write and return { saved: true, path }.
|
|
14
|
-
*
|
|
15
|
-
* Snake_case tool name (§11 decision #4).
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
19
|
-
import { writeFileSync, mkdirSync, existsSync, statSync } from "node:fs";
|
|
20
|
-
import { dirname, isAbsolute } from "node:path";
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
type: "string",
|
|
49
|
-
description:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
},
|
|
56
|
-
|
|
57
|
-
type: "string",
|
|
58
|
-
description:
|
|
59
|
-
"Optional
|
|
60
|
-
},
|
|
61
|
-
|
|
62
|
-
type: "
|
|
63
|
-
description:
|
|
64
|
-
"Optional
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
},
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
path,
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* `mandu_ate_save` — Phase A.3 spec persistence with lint-before-write.
|
|
3
|
+
*
|
|
4
|
+
* See `docs/ate/roadmap-v2-agent-native.md` §4.7 and the §7 extension
|
|
5
|
+
* ("mandu_ate_save lint-before-write").
|
|
6
|
+
*
|
|
7
|
+
* Semantics:
|
|
8
|
+
* 1. Run `lintSpecContent` (from @mandujs/ate) which:
|
|
9
|
+
* - parses with ts-morph (syntax errors block),
|
|
10
|
+
* - walks import declarations (banned typos, unknown @mandujs/* barrels),
|
|
11
|
+
* - detects anti-patterns (bare localhost, hand-rolled CSRF, DB mocks).
|
|
12
|
+
* 2. If any *blocking* diagnostic fires, return { saved: false, ... } WITHOUT
|
|
13
|
+
* writing. Otherwise write and return { saved: true, path }.
|
|
14
|
+
*
|
|
15
|
+
* Snake_case tool name (§11 decision #4).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
19
|
+
import { writeFileSync, mkdirSync, existsSync, statSync } from "node:fs";
|
|
20
|
+
import { dirname, isAbsolute } from "node:path";
|
|
21
|
+
import {
|
|
22
|
+
lintSpecContent,
|
|
23
|
+
appendMemoryEvent,
|
|
24
|
+
nowTimestamp,
|
|
25
|
+
type LintDiagnostic,
|
|
26
|
+
} from "@mandujs/ate";
|
|
27
|
+
|
|
28
|
+
// Re-export the diagnostic shape so callers can type-check against it without
|
|
29
|
+
// pulling @mandujs/ate directly.
|
|
30
|
+
export type { LintDiagnostic, LintSeverity } from "@mandujs/ate";
|
|
31
|
+
|
|
32
|
+
export const ateSaveToolDefinitions: Tool[] = [
|
|
33
|
+
{
|
|
34
|
+
name: "mandu_ate_save",
|
|
35
|
+
description:
|
|
36
|
+
"Phase A.3 persist-with-lint. Writes an agent-generated test file to " +
|
|
37
|
+
"disk, but first runs a small lint pass that blocks common LLM mistakes: " +
|
|
38
|
+
"ts-morph syntax errors, unresolved / banned import paths, hand-rolled " +
|
|
39
|
+
"CSRF cookies, DB mocks when createTestDb is available, and bare " +
|
|
40
|
+
"`localhost:<port>` URLs (prefer 127.0.0.1 per roadmap §9.2). Returns " +
|
|
41
|
+
"{ saved: true, path, lintDiagnostics: [warnings...] } on success or " +
|
|
42
|
+
"{ saved: false, blockingErrors: [...], lintDiagnostics: [...] } when " +
|
|
43
|
+
"a blocker fires (in which case no file is written).",
|
|
44
|
+
inputSchema: {
|
|
45
|
+
type: "object",
|
|
46
|
+
properties: {
|
|
47
|
+
path: {
|
|
48
|
+
type: "string",
|
|
49
|
+
description:
|
|
50
|
+
"Absolute path where the spec will be written. Parent directories are created if needed.",
|
|
51
|
+
},
|
|
52
|
+
content: {
|
|
53
|
+
type: "string",
|
|
54
|
+
description: "The full TypeScript test source to write.",
|
|
55
|
+
},
|
|
56
|
+
intent: {
|
|
57
|
+
type: "string",
|
|
58
|
+
description:
|
|
59
|
+
"Optional short description of what the test is verifying (logged to ATE memory).",
|
|
60
|
+
},
|
|
61
|
+
kind: {
|
|
62
|
+
type: "string",
|
|
63
|
+
description:
|
|
64
|
+
"Optional prompt kind this spec was generated for (filling_unit, filling_integration, e2e_playwright).",
|
|
65
|
+
},
|
|
66
|
+
sourcePrompt: {
|
|
67
|
+
type: "object",
|
|
68
|
+
description:
|
|
69
|
+
"Optional { kind, version } back-reference to the prompt that produced this spec (used by future memory queries).",
|
|
70
|
+
additionalProperties: true,
|
|
71
|
+
},
|
|
72
|
+
allowWarnings: {
|
|
73
|
+
type: "boolean",
|
|
74
|
+
description:
|
|
75
|
+
"When true, non-blocking warnings still allow the write (default true). When false, even warnings block.",
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
required: ["path", "content"],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
export function ateSaveTools(projectRoot: string) {
|
|
84
|
+
return {
|
|
85
|
+
mandu_ate_save: async (args: Record<string, unknown>) => {
|
|
86
|
+
const path = args.path as string | undefined;
|
|
87
|
+
const content = args.content as string | undefined;
|
|
88
|
+
const intent = typeof args.intent === "string" ? args.intent : undefined;
|
|
89
|
+
const kind = typeof args.kind === "string" ? args.kind : undefined;
|
|
90
|
+
const allowWarnings = args.allowWarnings !== false;
|
|
91
|
+
|
|
92
|
+
if (!path || typeof path !== "string") {
|
|
93
|
+
return { saved: false, error: "'path' is required" };
|
|
94
|
+
}
|
|
95
|
+
if (!isAbsolute(path)) {
|
|
96
|
+
return {
|
|
97
|
+
saved: false,
|
|
98
|
+
error: "'path' must be absolute — relative paths are rejected to prevent cwd drift.",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (typeof content !== "string") {
|
|
102
|
+
return { saved: false, error: "'content' is required and must be a string" };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const diagnostics = await lintSpecContent(path, content);
|
|
106
|
+
|
|
107
|
+
const blocking = diagnostics.filter((d) => d.blocking);
|
|
108
|
+
const warnings = diagnostics.filter((d) => !d.blocking);
|
|
109
|
+
|
|
110
|
+
if (blocking.length > 0 || (!allowWarnings && warnings.length > 0)) {
|
|
111
|
+
return {
|
|
112
|
+
saved: false,
|
|
113
|
+
path,
|
|
114
|
+
blockingErrors: blocking,
|
|
115
|
+
lintDiagnostics: diagnostics,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const parent = dirname(path);
|
|
120
|
+
if (!existsSync(parent)) {
|
|
121
|
+
mkdirSync(parent, { recursive: true });
|
|
122
|
+
}
|
|
123
|
+
if (!statSync(parent).isDirectory()) {
|
|
124
|
+
return { saved: false, path, error: `Parent path is not a directory: ${parent}` };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
writeFileSync(path, content, "utf8");
|
|
128
|
+
|
|
129
|
+
// Phase B.2 — auto-record intent_history event. Non-fatal on failure.
|
|
130
|
+
try {
|
|
131
|
+
appendMemoryEvent(projectRoot, {
|
|
132
|
+
kind: "intent_history",
|
|
133
|
+
timestamp: nowTimestamp(),
|
|
134
|
+
intent: intent ?? "(no intent supplied)",
|
|
135
|
+
agent: typeof args.agent === "string" ? (args.agent as string) : "unknown",
|
|
136
|
+
resulting: { saved: [path] },
|
|
137
|
+
...(kind ? { routeId: kind } : {}),
|
|
138
|
+
});
|
|
139
|
+
} catch {
|
|
140
|
+
// swallow
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
saved: true,
|
|
145
|
+
path,
|
|
146
|
+
bytes: Buffer.byteLength(content, "utf8"),
|
|
147
|
+
lintDiagnostics: diagnostics,
|
|
148
|
+
};
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Re-export for tests that need direct access (package.json test patterns
|
|
154
|
+
// already reach here).
|
|
155
|
+
export async function lintContent(
|
|
156
|
+
path: string,
|
|
157
|
+
content: string,
|
|
158
|
+
): Promise<LintDiagnostic[]> {
|
|
159
|
+
return lintSpecContent(path, content);
|
|
160
|
+
}
|