@precode/mcp 0.3.0 → 0.3.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/README.md +2 -1
- package/dist/index.js +37 -27
- package/dist/store.js +58 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -121,7 +121,8 @@ Declare checks in `.precode/checks.json`. Each `auto` check is **run by the MCP*
|
|
|
121
121
|
- `kind: "auto"` — executed; pass = exit 0 (and `expect` substring/`/regex/` matches output if set). Optional `timeoutMs` (default 120s).
|
|
122
122
|
- `kind: "manual"` — never auto-passed; surfaced as a human gate in `TODO_FOR_YOU.md` (a manual-only task closes with its gate routed to you).
|
|
123
123
|
- `taskIndex` — scope a check to one task; omit for a global check that runs on every task.
|
|
124
|
-
- **A task never closes on a self-reported pass.** Reported `checkResults` are advisory only. A check declared with an empty `cmd` is treated as unconfigured and **HOLDs** the task — wire a real command, mark it `manual`, or `precode.defer_task`.
|
|
124
|
+
- **A task never closes on a self-reported pass.** Reported `checkResults` are advisory only. A check declared with an empty `cmd` is treated as unconfigured and **HOLDs** the task — wire a real command, mark it `manual`, or `precode.defer_task`.
|
|
125
|
+
- **`adopt_spec` infers real commands** from your project (`package.json` scripts, `Cargo.toml`, `go.mod`) so a fresh spec is verifiable out of the box. Only when nothing is detected does it write empty placeholders that HOLD until you fill them. Failure advice references the actual command that ran, and `defer_task` is refused if the task's checks actually pass (so it can't be used to dodge verifiable work). Global `manual` gates are listed once in `TODO_FOR_YOU.md`, not repeated per task.
|
|
125
126
|
|
|
126
127
|
**Security:** the runner only executes commands you put in your own repo's `checks.json` — the same trust level as the agent already running your build. Commands run with a timeout and captured output. Because `checks.json` lives in your repo, gate tampering shows up in your diff.
|
|
127
128
|
|
package/dist/index.js
CHANGED
|
@@ -161,14 +161,16 @@ server.tool("precode.verify", "Close the done-gate for the current task. The MCP
|
|
|
161
161
|
const autoSpecs = specs.filter((c) => checkKind(c) === "auto");
|
|
162
162
|
const manualSpecs = specs.filter((c) => checkKind(c) === "manual");
|
|
163
163
|
const placeholderSpecs = specs.filter((c) => checkKind(c) === "skip");
|
|
164
|
-
//
|
|
164
|
+
// Only TASK-SCOPED manual gates are routed per task. Global (project-level)
|
|
165
|
+
// manual gates are listed once by precode.finalize — not repeated per task.
|
|
166
|
+
const taskManual = manualSpecs.filter((c) => c.taskIndex === task.index);
|
|
165
167
|
const routeManual = async () => {
|
|
166
|
-
if (!
|
|
168
|
+
if (!taskManual.length)
|
|
167
169
|
return;
|
|
168
170
|
await s.appendTodo([
|
|
169
171
|
`## Task #${task.index} — manual verification required`,
|
|
170
172
|
"",
|
|
171
|
-
...
|
|
173
|
+
...taskManual.map((c) => `- [ ] ${c.name}`),
|
|
172
174
|
].join("\n"));
|
|
173
175
|
};
|
|
174
176
|
const closeTask = async (verifiedLine) => {
|
|
@@ -178,16 +180,16 @@ server.tool("precode.verify", "Close the done-gate for the current task. The MCP
|
|
|
178
180
|
`## Task #${task.index}: ${task.text}`,
|
|
179
181
|
taskLabel ? `Host task: ${taskLabel}` : "",
|
|
180
182
|
verifiedLine,
|
|
181
|
-
|
|
182
|
-
? `Manual gates routed to TODO_FOR_YOU.md: ${
|
|
183
|
+
taskManual.length
|
|
184
|
+
? `Manual gates routed to TODO_FOR_YOU.md: ${taskManual.map((c) => c.name).join(", ")}`
|
|
183
185
|
: "",
|
|
184
186
|
notes ? `Notes: ${notes}` : "",
|
|
185
187
|
]
|
|
186
188
|
.filter(Boolean)
|
|
187
189
|
.join("\n"));
|
|
188
190
|
const next = await s.nextOpenTask();
|
|
189
|
-
const manualNote =
|
|
190
|
-
? ` ${
|
|
191
|
+
const manualNote = taskManual.length
|
|
192
|
+
? ` ${taskManual.length} manual check(s) routed to TODO_FOR_YOU.md for you.`
|
|
191
193
|
: "";
|
|
192
194
|
return text((next
|
|
193
195
|
? `PASS. Task #${task.index} marked done.${manualNote} Call precode.next_task for the next step (#${next.index}).`
|
|
@@ -219,7 +221,16 @@ server.tool("precode.verify", "Close the done-gate for the current task. The MCP
|
|
|
219
221
|
const runs = await runChecks(autoSpecs, ROOT);
|
|
220
222
|
const failed = runs.filter((r) => !r.passed);
|
|
221
223
|
if (failed.length > 0) {
|
|
222
|
-
|
|
224
|
+
const cmdById = new Map(autoSpecs.map((c) => [c.id, c.cmd ?? ""]));
|
|
225
|
+
return await gateFailure(failed.map((r) => `${r.name}: FAILED — ${r.detail}`),
|
|
226
|
+
// Advice references the ACTUAL command that ran, never a guess from
|
|
227
|
+
// the check's display name (which may not match the command).
|
|
228
|
+
failed.map((r) => {
|
|
229
|
+
const cmd = cmdById.get(r.id);
|
|
230
|
+
return cmd
|
|
231
|
+
? `${r.name}: read the output above, fix the cause, then re-run \`${cmd}\`.`
|
|
232
|
+
: `${r.name}: read the output above and fix the failure.`;
|
|
233
|
+
}));
|
|
223
234
|
}
|
|
224
235
|
await recordTelemetry({
|
|
225
236
|
eventName: "mcp_verify_pass",
|
|
@@ -322,11 +333,15 @@ server.tool("precode.finalize", "Write the handoff: confirm all tasks done and r
|
|
|
322
333
|
const deferredSet = new Set(deferred.map((e) => e.index));
|
|
323
334
|
const open = tasks.filter((t) => !t.done && !deferredSet.has(t.index));
|
|
324
335
|
// Manual gates the human still owns, regenerated from checks.json for done tasks.
|
|
325
|
-
|
|
336
|
+
// Global (project-level) manual gates appear ONCE; task-scoped ones per task.
|
|
337
|
+
const allChecks = await s.checks();
|
|
338
|
+
const manualGates = allChecks
|
|
339
|
+
.filter((c) => checkKind(c) === "manual" && c.taskIndex === undefined)
|
|
340
|
+
.map((c) => c.name);
|
|
326
341
|
for (const t of tasks.filter((t) => t.done)) {
|
|
327
|
-
const
|
|
328
|
-
for (const c of manual)
|
|
342
|
+
for (const c of allChecks.filter((c) => checkKind(c) === "manual" && c.taskIndex === t.index)) {
|
|
329
343
|
manualGates.push(`Task #${t.index}: ${c.name}`);
|
|
344
|
+
}
|
|
330
345
|
}
|
|
331
346
|
await recordTelemetry({
|
|
332
347
|
eventName: "mcp_finalize",
|
|
@@ -391,6 +406,17 @@ server.tool("precode.defer_task", "Honest escape hatch for the CURRENT open task
|
|
|
391
406
|
const task = await s.nextOpenTask();
|
|
392
407
|
if (!task)
|
|
393
408
|
return text("No open task to defer. Run precode.finalize.");
|
|
409
|
+
// Anti-dodge guard: if the task's real auto checks PASS right now, it is
|
|
410
|
+
// verifiable here — refuse the defer and make the agent verify instead.
|
|
411
|
+
const autoSpecs = (await s.checksForTask(task.index)).filter((c) => checkKind(c) === "auto");
|
|
412
|
+
if (autoSpecs.length > 0) {
|
|
413
|
+
const runs = await runChecks(autoSpecs, ROOT);
|
|
414
|
+
if (runs.every((r) => r.passed)) {
|
|
415
|
+
return text(`REFUSED. Task #${task.index} is verifiable here — its checks pass (${runs
|
|
416
|
+
.map((r) => `${r.name} ✓`)
|
|
417
|
+
.join(", ")}). Call precode.verify to close it; defer_task is only for checks that genuinely cannot run in this environment.`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
394
420
|
await s.deferTask(task.index, reason);
|
|
395
421
|
await recordTelemetry({
|
|
396
422
|
eventName: "mcp_defer_task",
|
|
@@ -468,19 +494,3 @@ main().catch((err) => {
|
|
|
468
494
|
console.error("[precode-mcp] fatal:", err);
|
|
469
495
|
process.exit(1);
|
|
470
496
|
});
|
|
471
|
-
function fixHint(checkName) {
|
|
472
|
-
const lower = checkName.toLowerCase();
|
|
473
|
-
if (lower.includes("type") || lower.includes("tsc")) {
|
|
474
|
-
return "open the TypeScript diagnostics, fix the reported type errors, then rerun the same type-check command.";
|
|
475
|
-
}
|
|
476
|
-
if (lower.includes("lint") || lower.includes("eslint")) {
|
|
477
|
-
return "fix the reported lint violations without changing spec scope, then rerun lint.";
|
|
478
|
-
}
|
|
479
|
-
if (lower.includes("build")) {
|
|
480
|
-
return "inspect the build error, fix the first failing route/module/env issue, then rerun the production build.";
|
|
481
|
-
}
|
|
482
|
-
if (lower.includes("test") || lower.includes("smoke") || lower.includes("e2e")) {
|
|
483
|
-
return "reproduce the failing flow, align behavior to the acceptance criteria, then rerun the test.";
|
|
484
|
-
}
|
|
485
|
-
return "inspect the failing command output, make the smallest spec-aligned fix, then rerun this exact check.";
|
|
486
|
-
}
|
package/dist/store.js
CHANGED
|
@@ -383,18 +383,25 @@ export async function adoptSpec(searchRoot, specPathHint) {
|
|
|
383
383
|
"",
|
|
384
384
|
].join("\n"), "utf8");
|
|
385
385
|
await fs.writeFile(path.join(dir, "manifest.json"), JSON.stringify({ project: { name: "Adopted spec", appType: "app" }, adoptedFrom: found }, null, 2), "utf8");
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
await
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
checks: [
|
|
386
|
+
// checks.json: infer REAL commands from the project (package.json scripts,
|
|
387
|
+
// Cargo, Go) so a fresh spec is verifiable out of the box. Only fall back to
|
|
388
|
+
// empty placeholders (which HOLD a task until filled) when nothing is found.
|
|
389
|
+
const inferred = await inferAutoChecks(searchRoot);
|
|
390
|
+
const autoChecks = inferred.length
|
|
391
|
+
? inferred
|
|
392
|
+
: [
|
|
394
393
|
{ id: "typecheck", name: "Type-check", kind: "auto", cmd: "" },
|
|
395
394
|
{ id: "lint", name: "Lint", kind: "auto", cmd: "" },
|
|
396
395
|
{ id: "build", name: "Production build", kind: "auto", cmd: "" },
|
|
397
396
|
{ id: "test", name: "Tests", kind: "auto", cmd: "" },
|
|
397
|
+
];
|
|
398
|
+
await fs.writeFile(path.join(dir, "checks.json"), JSON.stringify({
|
|
399
|
+
$comment: "Each 'auto' check is RUN by the MCP from the project root; a task is done " +
|
|
400
|
+
"only when the real exit code is 0. Commands below were inferred from your " +
|
|
401
|
+
"project — edit/remove as needed. Empty cmd = unconfigured (the task HOLDs " +
|
|
402
|
+
"until you fill it, mark it manual, or defer). 'manual' = human gate, never auto-passed.",
|
|
403
|
+
checks: [
|
|
404
|
+
...autoChecks,
|
|
398
405
|
{
|
|
399
406
|
id: "secrets",
|
|
400
407
|
name: "Secrets & deploy env verified in real accounts",
|
|
@@ -404,3 +411,46 @@ export async function adoptSpec(searchRoot, specPathHint) {
|
|
|
404
411
|
}, null, 2), "utf8");
|
|
405
412
|
return { ok: true, dir };
|
|
406
413
|
}
|
|
414
|
+
/**
|
|
415
|
+
* Best-effort detection of real verification commands for the adopted project,
|
|
416
|
+
* so checks.json ships runnable instead of empty. Covers the common stacks.
|
|
417
|
+
*/
|
|
418
|
+
async function inferAutoChecks(root) {
|
|
419
|
+
const checks = [];
|
|
420
|
+
const has = async (rel) => fs.stat(path.join(root, rel)).then(() => true, () => false);
|
|
421
|
+
if (await has("package.json")) {
|
|
422
|
+
try {
|
|
423
|
+
const pkg = JSON.parse(await fs.readFile(path.join(root, "package.json"), "utf8"));
|
|
424
|
+
const scripts = pkg.scripts ?? {};
|
|
425
|
+
const pick = (...names) => names.find((n) => typeof scripts[n] === "string");
|
|
426
|
+
const tc = pick("typecheck", "type-check", "tsc", "types");
|
|
427
|
+
if (tc)
|
|
428
|
+
checks.push({ id: "typecheck", name: "Type-check", kind: "auto", cmd: `npm run ${tc}` });
|
|
429
|
+
const lint = pick("lint", "eslint");
|
|
430
|
+
if (lint)
|
|
431
|
+
checks.push({ id: "lint", name: "Lint", kind: "auto", cmd: `npm run ${lint}` });
|
|
432
|
+
const build = pick("build", "compile");
|
|
433
|
+
if (build)
|
|
434
|
+
checks.push({ id: "build", name: "Build", kind: "auto", cmd: `npm run ${build}` });
|
|
435
|
+
const test = pick("test", "tests");
|
|
436
|
+
if (test)
|
|
437
|
+
checks.push({ id: "test", name: "Tests", kind: "auto", cmd: `npm test` });
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
/* malformed package.json — fall through to whatever else we detect */
|
|
441
|
+
}
|
|
442
|
+
return checks;
|
|
443
|
+
}
|
|
444
|
+
if (await has("Cargo.toml")) {
|
|
445
|
+
checks.push({ id: "build", name: "Cargo build", kind: "auto", cmd: "cargo build" });
|
|
446
|
+
checks.push({ id: "test", name: "Cargo test", kind: "auto", cmd: "cargo test" });
|
|
447
|
+
return checks;
|
|
448
|
+
}
|
|
449
|
+
if (await has("go.mod")) {
|
|
450
|
+
checks.push({ id: "build", name: "Go build", kind: "auto", cmd: "go build ./..." });
|
|
451
|
+
checks.push({ id: "vet", name: "Go vet", kind: "auto", cmd: "go vet ./..." });
|
|
452
|
+
checks.push({ id: "test", name: "Go test", kind: "auto", cmd: "go test ./..." });
|
|
453
|
+
return checks;
|
|
454
|
+
}
|
|
455
|
+
return checks;
|
|
456
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@precode/mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Open, agent-agnostic MCP server that turns a spec (any SPEC.md, best with a PreCode .precode/ package) into a self-correcting, verified build. Drives a phased build → recheck → fix loop with hard definition-of-done gates and an implemented-vs-todo ledger.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://useprecode.vercel.app/mcp",
|