@mandujs/core 0.54.2 → 0.54.4
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 +5 -2
- package/scripts/postinstall-lock.ts +153 -0
- package/src/agent/__tests__/context.test.ts +237 -0
- package/src/agent/context.ts +535 -0
- package/src/agent/index.ts +6 -0
- package/src/agent/plan.ts +283 -0
- package/src/agent/repair.ts +172 -0
- package/src/agent/sync.ts +200 -0
- package/src/agent/types.ts +308 -0
- package/src/agent/verify.ts +406 -0
- package/src/bundler/__tests__/build-runner.ts +33 -13
- package/src/bundler/__tests__/cold-start.test.ts +35 -7
- package/src/bundler/__tests__/css.test.ts +20 -0
- package/src/bundler/analyzer.ts +15 -7
- package/src/bundler/build.test.ts +53 -11
- package/src/bundler/build.ts +447 -185
- package/src/bundler/css.ts +42 -12
- package/src/bundler/manifest-schema.ts +21 -14
- package/src/bundler/types.ts +31 -14
- package/src/client/island.ts +79 -29
- package/src/guard/config-guard.ts +13 -7
- package/src/guard/fs-routes-policy.ts +51 -0
- package/src/guard/index.ts +11 -6
- package/src/index.ts +3 -2
- package/src/router/client-entry.ts +71 -0
- package/src/router/fs-routes.ts +16 -8
- package/src/router/fs-scanner.ts +4 -3
- package/src/runtime/__tests__/page-render-response.test.ts +49 -0
- package/src/runtime/page-render-response.ts +1 -5
- package/src/runtime/ssr.ts +39 -30
- package/src/runtime/streaming-ssr.ts +22 -13
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import type {
|
|
4
|
+
AgentApplyReport,
|
|
5
|
+
AgentDomain,
|
|
6
|
+
AgentPlan,
|
|
7
|
+
AgentPlanRisk,
|
|
8
|
+
AgentSuggestedCommand,
|
|
9
|
+
BuildAgentApplyOptions,
|
|
10
|
+
BuildAgentPlanOptions,
|
|
11
|
+
} from "./types";
|
|
12
|
+
|
|
13
|
+
const DEFAULT_PLAN_PATH = ".mandu/agent-plan.json";
|
|
14
|
+
|
|
15
|
+
const DOMAIN_KEYWORDS: Array<[AgentDomain, RegExp]> = [
|
|
16
|
+
["hydration", /\b(hydrat|island|partial|client|counter|interactive|ssr data)\b/i],
|
|
17
|
+
["contract", /\b(contract|openapi|schema|typed api|rpc)\b/i],
|
|
18
|
+
["api", /\b(api|endpoint|route handler|server action|post|get|put|delete)\b/i],
|
|
19
|
+
["slot", /\b(slot|filling|ctx\.|handler chain)\b/i],
|
|
20
|
+
["guard", /\b(guard|architecture|import boundary|layer|rule)\b/i],
|
|
21
|
+
["testing", /\b(test|spec|coverage|ate|e2e|playwright)\b/i],
|
|
22
|
+
["deploy", /\b(deploy|vercel|netlify|fly|render|docker|worker|edge)\b/i],
|
|
23
|
+
["design", /\b(design|ui|component|style|theme|tailwind|shadcn)\b/i],
|
|
24
|
+
["docs", /\b(doc|readme|guide|manual|changelog)\b/i],
|
|
25
|
+
["db", /\b(database|db|migration|seed|sqlite|postgres)\b/i],
|
|
26
|
+
["route", /\b(page|route|dashboard|screen|layout|fs route)\b/i],
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
function unique<T>(values: T[]): T[] {
|
|
30
|
+
return [...new Set(values)];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function inferDomains(intent: string): AgentDomain[] {
|
|
34
|
+
const domains = DOMAIN_KEYWORDS
|
|
35
|
+
.filter(([, pattern]) => pattern.test(intent))
|
|
36
|
+
.map(([domain]) => domain);
|
|
37
|
+
|
|
38
|
+
if (domains.includes("api") && !domains.includes("contract")) {
|
|
39
|
+
domains.push("contract");
|
|
40
|
+
}
|
|
41
|
+
if (domains.includes("hydration") && !domains.includes("route")) {
|
|
42
|
+
domains.push("route");
|
|
43
|
+
}
|
|
44
|
+
if (domains.length === 0) {
|
|
45
|
+
domains.push("unknown");
|
|
46
|
+
}
|
|
47
|
+
return unique(domains);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function filesToRead(domains: AgentDomain[]): string[] {
|
|
51
|
+
const out: string[] = ["docs/plans/20_agent_surface_consolidation_plan.md"];
|
|
52
|
+
if (domains.includes("route")) out.push("app/", ".mandu/routes.manifest.json");
|
|
53
|
+
if (domains.includes("api")) out.push("app/api/", "spec/contracts/");
|
|
54
|
+
if (domains.includes("contract")) out.push("spec/contracts/", "docs/resource-architecture.md");
|
|
55
|
+
if (domains.includes("slot")) out.push("spec/slots/", "packages/mcp/src/resources/skills/mandu-slot/SKILL.md");
|
|
56
|
+
if (domains.includes("hydration")) out.push("packages/mcp/src/resources/skills/mandu-hydration/SKILL.md", "app/**/*.partial.tsx");
|
|
57
|
+
if (domains.includes("guard")) out.push("mandu.config.ts", "packages/mcp/src/resources/skills/mandu-guard/SKILL.md");
|
|
58
|
+
if (domains.includes("testing")) out.push("packages/mcp/src/resources/skills/mandu-testing/SKILL.md");
|
|
59
|
+
if (domains.includes("deploy")) out.push(".mandu/deploy.intent.json", "docs/deploy/README.md");
|
|
60
|
+
if (domains.includes("design")) out.push("DESIGN.md", "packages/mcp/src/resources/skills/mandu-ui/SKILL.md");
|
|
61
|
+
if (domains.includes("db")) out.push("spec/resources/", "spec/migrations/");
|
|
62
|
+
return unique(out);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function filesToCreate(intent: string, domains: AgentDomain[]): string[] {
|
|
66
|
+
const lower = intent.toLowerCase();
|
|
67
|
+
const out: string[] = [];
|
|
68
|
+
if (domains.includes("route")) {
|
|
69
|
+
if (lower.includes("dashboard")) out.push("app/dashboard/page.tsx");
|
|
70
|
+
else out.push("app/<route>/page.tsx");
|
|
71
|
+
}
|
|
72
|
+
if (domains.includes("api")) out.push("app/api/<name>/route.ts");
|
|
73
|
+
if (domains.includes("contract")) out.push("spec/contracts/<name>.contract.ts");
|
|
74
|
+
if (domains.includes("slot")) out.push("spec/slots/<name>.slot.ts");
|
|
75
|
+
if (domains.includes("hydration")) out.push("app/<route>/<component>.partial.tsx");
|
|
76
|
+
if (domains.includes("testing")) out.push("packages/<target>/__tests__/<feature>.test.ts");
|
|
77
|
+
if (domains.includes("docs")) out.push("docs/<topic>.md");
|
|
78
|
+
return unique(out);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function mcpTools(domains: AgentDomain[]): string[] {
|
|
82
|
+
const out = ["mandu.agent.context", "mandu.agent.verify"];
|
|
83
|
+
if (domains.includes("route")) out.push("mandu.route.list", "mandu.generate");
|
|
84
|
+
if (domains.includes("api")) out.push("mandu.generate", "mandu.contract.create");
|
|
85
|
+
if (domains.includes("contract")) out.push("mandu.contract.validate", "mandu.contract.openapi");
|
|
86
|
+
if (domains.includes("slot")) out.push("mandu.slot.validate", "mandu.slot.constraints");
|
|
87
|
+
if (domains.includes("hydration")) out.push("mandu.island.list", "mandu.hydration.set");
|
|
88
|
+
if (domains.includes("guard")) out.push("mandu.guard.check", "mandu.guard.explain");
|
|
89
|
+
if (domains.includes("testing")) out.push("mandu.run.tests", "mandu.ate.generate");
|
|
90
|
+
if (domains.includes("deploy")) out.push("mandu.deploy.plan", "mandu.deploy.compile");
|
|
91
|
+
if (domains.includes("design")) out.push("mandu.design.get", "mandu.design.check");
|
|
92
|
+
return unique(out);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function risks(domains: AgentDomain[]): AgentPlanRisk[] {
|
|
96
|
+
const out: AgentPlanRisk[] = [];
|
|
97
|
+
if (domains.includes("db")) out.push({ level: "high", reason: "Database changes can affect persistent data." });
|
|
98
|
+
if (domains.includes("deploy")) out.push({ level: "high", reason: "Deploy artifacts may affect production behavior." });
|
|
99
|
+
if (domains.includes("guard")) out.push({ level: "medium", reason: "Architecture rule changes can hide future violations." });
|
|
100
|
+
if (domains.includes("hydration")) out.push({ level: "medium", reason: "SSR/client boundaries can regress hydration." });
|
|
101
|
+
if (domains.includes("api") || domains.includes("contract")) {
|
|
102
|
+
out.push({ level: "medium", reason: "API and contract changes must stay synchronized." });
|
|
103
|
+
}
|
|
104
|
+
if (out.length === 0) out.push({ level: "low", reason: "No high-risk domain detected by deterministic planner." });
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function verification(domains: AgentDomain[]): AgentSuggestedCommand[] {
|
|
109
|
+
const out: AgentSuggestedCommand[] = [
|
|
110
|
+
{
|
|
111
|
+
command: "mandu agent verify --changed --json --write",
|
|
112
|
+
reason: "Canonical post-change verification gate.",
|
|
113
|
+
required: true,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
command: "bun run typecheck",
|
|
117
|
+
reason: "Type boundary check for generated or edited TypeScript.",
|
|
118
|
+
required: true,
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
if (domains.includes("hydration") || domains.includes("route")) {
|
|
122
|
+
out.push({
|
|
123
|
+
command: "bun test packages/core/src/bundler packages/core/tests/client",
|
|
124
|
+
reason: "Route and hydration changes can affect bundling and client runtime.",
|
|
125
|
+
required: true,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
if (domains.includes("api") || domains.includes("contract") || domains.includes("slot")) {
|
|
129
|
+
out.push({
|
|
130
|
+
command: "bun test packages/core/src/contract packages/core/src/slot packages/core/tests/routes",
|
|
131
|
+
reason: "API, contract, and slot changes need framework contract coverage.",
|
|
132
|
+
required: true,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
if (domains.includes("deploy")) {
|
|
136
|
+
out.push({
|
|
137
|
+
command: "mandu deploy:plan --dry-run",
|
|
138
|
+
reason: "Deploy intent should be inspected before writing provider artifacts.",
|
|
139
|
+
required: true,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return unique(out.map((item) => JSON.stringify(item))).map((item) => JSON.parse(item) as AgentSuggestedCommand);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function buildAgentPlan(options: BuildAgentPlanOptions): AgentPlan {
|
|
146
|
+
const intent = options.intent.trim();
|
|
147
|
+
const domains = inferDomains(intent);
|
|
148
|
+
return {
|
|
149
|
+
schemaVersion: 1,
|
|
150
|
+
framework: "mandu",
|
|
151
|
+
generatedAt: new Date().toISOString(),
|
|
152
|
+
intent,
|
|
153
|
+
domains,
|
|
154
|
+
filesToRead: filesToRead(domains),
|
|
155
|
+
filesToCreate: filesToCreate(intent, domains),
|
|
156
|
+
filesToModify: [],
|
|
157
|
+
mcpTools: mcpTools(domains),
|
|
158
|
+
risks: risks(domains),
|
|
159
|
+
verification: verification(domains),
|
|
160
|
+
notes: [
|
|
161
|
+
"This deterministic plan is intentionally conservative. It does not execute edits.",
|
|
162
|
+
"Prefer MCP/domain tools before direct file edits, then run agent verify.",
|
|
163
|
+
],
|
|
164
|
+
executable: false,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function agentPlanPath(rootDir: string): string {
|
|
169
|
+
return path.join(rootDir, ".mandu", "agent-plan.json");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function writeAgentPlan(rootDir: string, plan: AgentPlan): Promise<{ path: string; plan: AgentPlan }> {
|
|
173
|
+
const outPath = agentPlanPath(rootDir);
|
|
174
|
+
await fs.mkdir(path.dirname(outPath), { recursive: true });
|
|
175
|
+
await fs.writeFile(outPath, `${JSON.stringify(plan, null, 2)}\n`, "utf8");
|
|
176
|
+
return { path: outPath, plan };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function readAgentPlan(filePath: string): Promise<AgentPlan | null> {
|
|
180
|
+
try {
|
|
181
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
182
|
+
const parsed = JSON.parse(raw) as AgentPlan;
|
|
183
|
+
if (parsed?.framework !== "mandu" || !Array.isArray(parsed.domains)) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
return parsed;
|
|
187
|
+
} catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function resolveInside(rootDir: string, value: string): string {
|
|
193
|
+
const root = path.resolve(rootDir);
|
|
194
|
+
const resolved = path.resolve(root, value);
|
|
195
|
+
if (resolved !== root && !resolved.startsWith(root + path.sep)) {
|
|
196
|
+
throw new Error("agent apply input must stay inside the project root");
|
|
197
|
+
}
|
|
198
|
+
return resolved;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function buildAgentApplyReport(
|
|
202
|
+
rootDir: string = process.cwd(),
|
|
203
|
+
options: BuildAgentApplyOptions = {},
|
|
204
|
+
): Promise<AgentApplyReport> {
|
|
205
|
+
const sourceRel = options.from ?? DEFAULT_PLAN_PATH;
|
|
206
|
+
const sourcePath = resolveInside(rootDir, sourceRel);
|
|
207
|
+
const plan = await readAgentPlan(sourcePath);
|
|
208
|
+
if (!plan) {
|
|
209
|
+
return {
|
|
210
|
+
schemaVersion: 1,
|
|
211
|
+
framework: "mandu",
|
|
212
|
+
generatedAt: new Date().toISOString(),
|
|
213
|
+
ok: false,
|
|
214
|
+
dryRun: true,
|
|
215
|
+
sourcePlan: sourceRel,
|
|
216
|
+
intent: "",
|
|
217
|
+
domains: ["unknown"],
|
|
218
|
+
actions: [
|
|
219
|
+
{
|
|
220
|
+
kind: "verify",
|
|
221
|
+
description: "Create an agent plan first.",
|
|
222
|
+
command: "mandu agent plan \"<task>\" --json --write",
|
|
223
|
+
applied: false,
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
warnings: [`Could not read ${sourceRel}.`],
|
|
227
|
+
nextVerifyCommand: "mandu agent verify --changed --json --write",
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
schemaVersion: 1,
|
|
233
|
+
framework: "mandu",
|
|
234
|
+
generatedAt: new Date().toISOString(),
|
|
235
|
+
ok: true,
|
|
236
|
+
dryRun: options.dryRun !== false,
|
|
237
|
+
sourcePlan: sourceRel,
|
|
238
|
+
intent: plan.intent,
|
|
239
|
+
domains: plan.domains,
|
|
240
|
+
actions: [
|
|
241
|
+
...plan.filesToRead.map((file) => ({
|
|
242
|
+
kind: "read_file" as const,
|
|
243
|
+
description: `Read ${file} before editing.`,
|
|
244
|
+
file,
|
|
245
|
+
applied: false as const,
|
|
246
|
+
})),
|
|
247
|
+
...plan.mcpTools.map((tool) => ({
|
|
248
|
+
kind: "mcp_tool" as const,
|
|
249
|
+
description: `Consider ${tool} for this plan.`,
|
|
250
|
+
tool,
|
|
251
|
+
applied: false as const,
|
|
252
|
+
})),
|
|
253
|
+
...plan.filesToCreate.map((file) => ({
|
|
254
|
+
kind: "manual_edit" as const,
|
|
255
|
+
description: `Create ${file} only after confirming the local pattern.`,
|
|
256
|
+
file,
|
|
257
|
+
applied: false as const,
|
|
258
|
+
})),
|
|
259
|
+
...plan.verification.map((item) => ({
|
|
260
|
+
kind: "verify" as const,
|
|
261
|
+
description: item.reason,
|
|
262
|
+
command: item.command,
|
|
263
|
+
applied: false as const,
|
|
264
|
+
})),
|
|
265
|
+
],
|
|
266
|
+
warnings: [
|
|
267
|
+
"Agent apply is dry-run only until typed operation payloads are introduced.",
|
|
268
|
+
"No filesystem changes were made by this report.",
|
|
269
|
+
],
|
|
270
|
+
nextVerifyCommand: "mandu agent verify --changed --json --write",
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export async function writeAgentApplyReport(
|
|
275
|
+
rootDir: string,
|
|
276
|
+
report: AgentApplyReport,
|
|
277
|
+
): Promise<{ path: string; report: AgentApplyReport }> {
|
|
278
|
+
const outPath = path.join(rootDir, ".mandu", "agent-apply.json");
|
|
279
|
+
await fs.mkdir(path.dirname(outPath), { recursive: true });
|
|
280
|
+
await fs.writeFile(outPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
|
281
|
+
return { path: outPath, report };
|
|
282
|
+
}
|
|
283
|
+
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import type {
|
|
4
|
+
AgentDiagnostic,
|
|
5
|
+
AgentRepairAction,
|
|
6
|
+
AgentRepairReport,
|
|
7
|
+
AgentVerifyReport,
|
|
8
|
+
BuildAgentRepairOptions,
|
|
9
|
+
} from "./types";
|
|
10
|
+
|
|
11
|
+
const DEFAULT_VERIFY_INPUT = ".mandu/agent-verify.json";
|
|
12
|
+
|
|
13
|
+
function resolveInside(rootDir: string, value: string): string {
|
|
14
|
+
const root = path.resolve(rootDir);
|
|
15
|
+
const resolved = path.resolve(root, value);
|
|
16
|
+
if (resolved !== root && !resolved.startsWith(root + path.sep)) {
|
|
17
|
+
throw new Error("repair input must stay inside the project root");
|
|
18
|
+
}
|
|
19
|
+
return resolved;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function readVerifyReport(filePath: string): Promise<AgentVerifyReport | null> {
|
|
23
|
+
try {
|
|
24
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
25
|
+
const parsed = JSON.parse(raw) as AgentVerifyReport;
|
|
26
|
+
if (parsed?.framework !== "mandu" || !Array.isArray(parsed.diagnostics)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return parsed;
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function actionForDiagnostic(diagnostic: AgentDiagnostic): AgentRepairAction {
|
|
36
|
+
const fix = diagnostic.suggestedFix;
|
|
37
|
+
if (!fix) {
|
|
38
|
+
return {
|
|
39
|
+
diagnosticCode: diagnostic.code,
|
|
40
|
+
kind: "manual",
|
|
41
|
+
description: `Inspect ${diagnostic.code}: ${diagnostic.cause}`,
|
|
42
|
+
safeToApply: false,
|
|
43
|
+
applied: false,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (fix.type === "run_command" && fix.command) {
|
|
48
|
+
return {
|
|
49
|
+
diagnosticCode: diagnostic.code,
|
|
50
|
+
kind: "run_command",
|
|
51
|
+
description: fix.description,
|
|
52
|
+
command: fix.command,
|
|
53
|
+
safeToApply: false,
|
|
54
|
+
applied: false,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if ((fix.type === "create_file" || fix.type === "modify_file") && fix.path) {
|
|
59
|
+
return {
|
|
60
|
+
diagnosticCode: diagnostic.code,
|
|
61
|
+
kind: "patch",
|
|
62
|
+
description: fix.description,
|
|
63
|
+
file: fix.path,
|
|
64
|
+
// Current diagnostic shape does not carry patch content. Keep this
|
|
65
|
+
// conservative until a typed patch payload exists.
|
|
66
|
+
safeToApply: false,
|
|
67
|
+
applied: false,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
diagnosticCode: diagnostic.code,
|
|
73
|
+
kind: "manual",
|
|
74
|
+
description: fix.description,
|
|
75
|
+
safeToApply: false,
|
|
76
|
+
applied: false,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function agentRepairReportPath(rootDir: string): string {
|
|
81
|
+
return path.join(rootDir, ".mandu", "agent-repair.json");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function buildAgentRepairReport(
|
|
85
|
+
rootDir: string = process.cwd(),
|
|
86
|
+
options: BuildAgentRepairOptions = {},
|
|
87
|
+
): Promise<AgentRepairReport> {
|
|
88
|
+
const root = path.resolve(rootDir);
|
|
89
|
+
const sourceRel = options.from ?? DEFAULT_VERIFY_INPUT;
|
|
90
|
+
const sourcePath = resolveInside(root, sourceRel);
|
|
91
|
+
const warnings: string[] = [];
|
|
92
|
+
const report = await readVerifyReport(sourcePath);
|
|
93
|
+
|
|
94
|
+
if (!report) {
|
|
95
|
+
return {
|
|
96
|
+
schemaVersion: 1,
|
|
97
|
+
framework: "mandu",
|
|
98
|
+
generatedAt: new Date().toISOString(),
|
|
99
|
+
ok: false,
|
|
100
|
+
status: "input_missing",
|
|
101
|
+
sourceReport: sourceRel,
|
|
102
|
+
diagnostics: [
|
|
103
|
+
{
|
|
104
|
+
code: "MANDU_REPAIR_INPUT_MISSING",
|
|
105
|
+
severity: "error",
|
|
106
|
+
title: "Agent verify report missing",
|
|
107
|
+
cause: `Could not read ${sourceRel}.`,
|
|
108
|
+
suggestedFix: {
|
|
109
|
+
type: "run_command",
|
|
110
|
+
command: "mandu agent verify --changed --json --write",
|
|
111
|
+
description: "Generate a fresh agent verify report first.",
|
|
112
|
+
},
|
|
113
|
+
docs: "docs/plans/20_agent_surface_consolidation_plan.md",
|
|
114
|
+
repairable: true,
|
|
115
|
+
source: "agent.repair",
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
actions: [
|
|
119
|
+
{
|
|
120
|
+
diagnosticCode: "MANDU_REPAIR_INPUT_MISSING",
|
|
121
|
+
kind: "run_command",
|
|
122
|
+
description: "Generate a fresh agent verify report first.",
|
|
123
|
+
command: "mandu agent verify --changed --json --write",
|
|
124
|
+
safeToApply: false,
|
|
125
|
+
applied: false,
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
appliedActions: [],
|
|
129
|
+
warnings: [],
|
|
130
|
+
nextVerifyCommand: "mandu agent verify --changed --json --write",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const diagnostics = report.diagnostics;
|
|
135
|
+
const actions = diagnostics.map(actionForDiagnostic);
|
|
136
|
+
const appliedActions: AgentRepairAction[] = [];
|
|
137
|
+
|
|
138
|
+
if (options.apply) {
|
|
139
|
+
const safePatchActions = actions.filter((action) => action.kind === "patch" && action.safeToApply);
|
|
140
|
+
if (safePatchActions.length === 0) {
|
|
141
|
+
warnings.push("No safe file patch candidates were available to apply.");
|
|
142
|
+
}
|
|
143
|
+
// Future extension: apply typed patch payloads here. Command execution is
|
|
144
|
+
// intentionally never auto-applied by repair.
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
schemaVersion: 1,
|
|
149
|
+
framework: "mandu",
|
|
150
|
+
generatedAt: new Date().toISOString(),
|
|
151
|
+
ok: true,
|
|
152
|
+
status: diagnostics.length === 0 ? "nothing_to_repair" : "ready",
|
|
153
|
+
sourceReport: sourceRel,
|
|
154
|
+
diagnostics,
|
|
155
|
+
actions,
|
|
156
|
+
appliedActions,
|
|
157
|
+
warnings,
|
|
158
|
+
nextVerifyCommand: "mandu agent verify --changed --json --write",
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function writeAgentRepairReport(
|
|
163
|
+
rootDir: string = process.cwd(),
|
|
164
|
+
report?: AgentRepairReport,
|
|
165
|
+
): Promise<{ path: string; report: AgentRepairReport }> {
|
|
166
|
+
const value = report ?? await buildAgentRepairReport(rootDir);
|
|
167
|
+
const outPath = agentRepairReportPath(rootDir);
|
|
168
|
+
await fs.mkdir(path.dirname(outPath), { recursive: true });
|
|
169
|
+
await fs.writeFile(outPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
170
|
+
return { path: outPath, report: value };
|
|
171
|
+
}
|
|
172
|
+
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import type {
|
|
4
|
+
AgentSuggestedCommand,
|
|
5
|
+
AgentSyncFile,
|
|
6
|
+
AgentSyncReport,
|
|
7
|
+
AgentSyncTarget,
|
|
8
|
+
BuildAgentSyncOptions,
|
|
9
|
+
} from "./types";
|
|
10
|
+
|
|
11
|
+
const SYNC_ROOT = path.join(".mandu", "agent-sync");
|
|
12
|
+
const TARGETS = ["codex", "claude", "gemini"] as const;
|
|
13
|
+
type ConcreteTarget = (typeof TARGETS)[number];
|
|
14
|
+
|
|
15
|
+
function toPosix(value: string): string {
|
|
16
|
+
return value.split(path.sep).join("/");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isTarget(value: string | undefined): value is AgentSyncTarget {
|
|
20
|
+
return value === "codex" || value === "claude" || value === "gemini" || value === "all";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function concreteTargets(target: AgentSyncTarget): ConcreteTarget[] {
|
|
24
|
+
return target === "all" ? [...TARGETS] : [target];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function targetFile(target: ConcreteTarget): string {
|
|
28
|
+
if (target === "codex") return path.join(SYNC_ROOT, "codex", "AGENTS.md");
|
|
29
|
+
if (target === "claude") return path.join(SYNC_ROOT, "claude", "CLAUDE.md");
|
|
30
|
+
return path.join(SYNC_ROOT, "gemini", "GEMINI.md");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function renderInstructions(target: ConcreteTarget): string {
|
|
34
|
+
const title =
|
|
35
|
+
target === "codex"
|
|
36
|
+
? "Codex"
|
|
37
|
+
: target === "claude"
|
|
38
|
+
? "Claude Code"
|
|
39
|
+
: "Gemini CLI";
|
|
40
|
+
return [
|
|
41
|
+
`# Mandu Agent Workflow for ${title}`,
|
|
42
|
+
"",
|
|
43
|
+
"Mandu is an agent-native fullstack framework. Use the official agent surface before direct source edits.",
|
|
44
|
+
"",
|
|
45
|
+
"## Canonical Loop",
|
|
46
|
+
"",
|
|
47
|
+
"```text",
|
|
48
|
+
"context -> plan -> apply -> verify -> repair",
|
|
49
|
+
"```",
|
|
50
|
+
"",
|
|
51
|
+
"## Required First Choices",
|
|
52
|
+
"",
|
|
53
|
+
"1. Start with `mandu.agent.context` or `mandu agent context --json`.",
|
|
54
|
+
"2. Create a plan with `mandu.agent.plan` or `mandu agent plan \"<task>\" --json --write`.",
|
|
55
|
+
"3. Prefer `mandu.agent.apply` and domain MCP tools before direct file edits.",
|
|
56
|
+
"4. End code-changing work with `mandu.agent.verify` or `mandu agent verify --changed --json --write`.",
|
|
57
|
+
"5. If verification fails, run `mandu.agent.repair` or `mandu agent repair --from .mandu/agent-verify.json --json`, then verify again.",
|
|
58
|
+
"",
|
|
59
|
+
"## MCP Profile",
|
|
60
|
+
"",
|
|
61
|
+
"Use the reduced default profile:",
|
|
62
|
+
"",
|
|
63
|
+
"```bash",
|
|
64
|
+
"MANDU_MCP_PROFILE=agent-core",
|
|
65
|
+
"```",
|
|
66
|
+
"",
|
|
67
|
+
"Escalate to `agent-full` only when the plan selects route, API, slot, hydration, contract, guard, testing, or lint domains. Use `internal` only for framework maintenance.",
|
|
68
|
+
"",
|
|
69
|
+
"## Domain Skill Escalation",
|
|
70
|
+
"",
|
|
71
|
+
"- route/api: `mandu-fs-routes`",
|
|
72
|
+
"- hydration/island/partial: `mandu-hydration`",
|
|
73
|
+
"- slot/filling: `mandu-slot`",
|
|
74
|
+
"- guard/import boundary: `mandu-guard`",
|
|
75
|
+
"- test/e2e/ATE: `mandu-testing`",
|
|
76
|
+
"- deploy: `mandu-deployment`",
|
|
77
|
+
"- security/auth/session: `mandu-security`",
|
|
78
|
+
"- styling/ui/design: `mandu-styling`, `mandu-ui`, `mandu-composition`",
|
|
79
|
+
"",
|
|
80
|
+
"Domain skills are addenda. They do not replace the canonical loop.",
|
|
81
|
+
"",
|
|
82
|
+
].join("\n");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function renderClaudeSkill(): string {
|
|
86
|
+
return [
|
|
87
|
+
"---",
|
|
88
|
+
"name: mandu-agent-workflow",
|
|
89
|
+
"description: Canonical context -> plan -> apply -> verify -> repair workflow for Mandu projects.",
|
|
90
|
+
"---",
|
|
91
|
+
"",
|
|
92
|
+
"# Mandu Agent Workflow",
|
|
93
|
+
"",
|
|
94
|
+
"Use this skill first in Mandu projects. Follow `context -> plan -> apply -> verify -> repair`.",
|
|
95
|
+
"",
|
|
96
|
+
"Preferred tools: `mandu.agent.context`, `mandu.agent.plan`, `mandu.agent.apply`, `mandu.agent.verify`, `mandu.agent.repair`.",
|
|
97
|
+
"",
|
|
98
|
+
"CLI fallback:",
|
|
99
|
+
"",
|
|
100
|
+
"```bash",
|
|
101
|
+
"mandu agent context --json",
|
|
102
|
+
"mandu agent plan \"<task>\" --json --write",
|
|
103
|
+
"mandu agent apply --from .mandu/agent-plan.json --json",
|
|
104
|
+
"mandu agent verify --changed --json --write",
|
|
105
|
+
"mandu agent repair --from .mandu/agent-verify.json --json",
|
|
106
|
+
"```",
|
|
107
|
+
"",
|
|
108
|
+
].join("\n");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function syncEntries(target: ConcreteTarget): Array<{ target: ConcreteTarget; relPath: string; content: string }> {
|
|
112
|
+
const entries = [
|
|
113
|
+
{
|
|
114
|
+
target,
|
|
115
|
+
relPath: targetFile(target),
|
|
116
|
+
content: renderInstructions(target),
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
if (target === "claude") {
|
|
120
|
+
entries.push({
|
|
121
|
+
target,
|
|
122
|
+
relPath: path.join(SYNC_ROOT, "claude", "skills", "mandu-agent-workflow", "SKILL.md"),
|
|
123
|
+
content: renderClaudeSkill(),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return entries;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function writeEntry(
|
|
130
|
+
rootDir: string,
|
|
131
|
+
entry: { target: ConcreteTarget; relPath: string; content: string },
|
|
132
|
+
dryRun: boolean,
|
|
133
|
+
): Promise<AgentSyncFile> {
|
|
134
|
+
const absPath = path.join(rootDir, entry.relPath);
|
|
135
|
+
let action: AgentSyncFile["action"] = "created";
|
|
136
|
+
try {
|
|
137
|
+
const existing = await fs.readFile(absPath, "utf8");
|
|
138
|
+
action = existing === entry.content ? "unchanged" : "updated";
|
|
139
|
+
} catch {
|
|
140
|
+
action = "created";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (dryRun) {
|
|
144
|
+
action = "planned";
|
|
145
|
+
} else if (action !== "unchanged") {
|
|
146
|
+
await fs.mkdir(path.dirname(absPath), { recursive: true });
|
|
147
|
+
await fs.writeFile(absPath, entry.content, "utf8");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
target: entry.target,
|
|
152
|
+
path: toPosix(entry.relPath),
|
|
153
|
+
action,
|
|
154
|
+
bytes: Buffer.byteLength(entry.content, "utf8"),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function nextCommands(): AgentSuggestedCommand[] {
|
|
159
|
+
return [
|
|
160
|
+
{
|
|
161
|
+
command: "mandu agent context --json",
|
|
162
|
+
reason: "Confirm the generated workflow matches the current project state.",
|
|
163
|
+
required: true,
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
command: "MANDU_MCP_PROFILE=agent-core",
|
|
167
|
+
reason: "Use the reduced default MCP exposure for coding agents.",
|
|
168
|
+
required: true,
|
|
169
|
+
},
|
|
170
|
+
];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function buildAgentSyncReport(
|
|
174
|
+
rootDir: string = process.cwd(),
|
|
175
|
+
options: BuildAgentSyncOptions = {},
|
|
176
|
+
): Promise<AgentSyncReport> {
|
|
177
|
+
const target = isTarget(options.target) ? options.target : "all";
|
|
178
|
+
const dryRun = options.dryRun === true;
|
|
179
|
+
const root = path.resolve(rootDir);
|
|
180
|
+
const files: AgentSyncFile[] = [];
|
|
181
|
+
|
|
182
|
+
for (const concrete of concreteTargets(target)) {
|
|
183
|
+
for (const entry of syncEntries(concrete)) {
|
|
184
|
+
files.push(await writeEntry(root, entry, dryRun));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
schemaVersion: 1,
|
|
190
|
+
framework: "mandu",
|
|
191
|
+
generatedAt: new Date().toISOString(),
|
|
192
|
+
ok: true,
|
|
193
|
+
target,
|
|
194
|
+
profile: "agent-core",
|
|
195
|
+
workflow: ["context", "plan", "apply", "verify", "repair"],
|
|
196
|
+
files,
|
|
197
|
+
warnings: dryRun ? ["Dry-run only. No files were written."] : [],
|
|
198
|
+
nextCommands: nextCommands(),
|
|
199
|
+
};
|
|
200
|
+
}
|